build: Add build process for newgui (#7351)
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
<div fxLayout="row" fxLayoutAlign="space-between start" [ngClass]="(_selected)?'item selected':'item'">
|
||||
<div><a href="#">{{state}}</a>: </div>
|
||||
<div>{{count}}</div>
|
||||
</div>
|
||||
@@ -0,0 +1,27 @@
|
||||
@mixin chart-item-theme($theme) {
|
||||
.item {
|
||||
cursor: pointer;
|
||||
padding: 3px 7px 3px 7px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.selected {
|
||||
background-color: #DDDDDD;
|
||||
color: #303030;
|
||||
}
|
||||
.selected a {
|
||||
color: #303030;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.selected {
|
||||
background-color: map_get($mat-grey, 900);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.selected a {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ChartItemComponent } from './chart-item.component';
|
||||
|
||||
describe('ChartItemComponent', () => {
|
||||
let component: ChartItemComponent;
|
||||
let fixture: ComponentFixture<ChartItemComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ ChartItemComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ChartItemComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-chart-item',
|
||||
templateUrl: './chart-item.component.html',
|
||||
styleUrls: ['./chart-item.component.scss']
|
||||
})
|
||||
export class ChartItemComponent {
|
||||
@Input() state: string;
|
||||
@Input() count: number;
|
||||
@Input('selected')
|
||||
set selected(s: boolean) {
|
||||
this._selected = s;
|
||||
}
|
||||
|
||||
_selected: boolean = true;
|
||||
|
||||
constructor() { }
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<app-card>
|
||||
<app-card-title>{{title | uppercase}}</app-card-title>
|
||||
<app-card-content>
|
||||
<div fxLayout="row" fxLayoutAlign="space-between stretch">
|
||||
<app-donut-chart [elementID]="chartID" fxFlex="30" [title]="title" (stateEvent)="onItemSelect($event)">
|
||||
</app-donut-chart>
|
||||
<div class=" items" fxLayout="column" fxLayoutAlign="start end" fxFlex="70">
|
||||
<app-chart-item *ngFor="let state of states" (click)="onItemSelect(state)" [state]="state.label"
|
||||
[count]="state.count" [selected]="state.selected">
|
||||
</app-chart-item>
|
||||
</div>
|
||||
</div>
|
||||
</app-card-content>
|
||||
</app-card>
|
||||
@@ -0,0 +1,26 @@
|
||||
import { async, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ChartComponent } from './chart.component';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
|
||||
class MockService {
|
||||
getEach() {
|
||||
// unimplemented
|
||||
}
|
||||
};
|
||||
|
||||
describe('ChartComponent', () => {
|
||||
let component: ChartComponent;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [HttpClientModule],
|
||||
providers: [ChartComponent]
|
||||
}).compileComponents();
|
||||
component = TestBed.inject(ChartComponent);
|
||||
}));
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,115 @@
|
||||
import { Component, OnInit, ViewChild, Input, Type } from '@angular/core';
|
||||
import Folder from '../../folder'
|
||||
import { FolderService } from 'src/app/services/folder.service';
|
||||
import { DonutChartComponent } from '../donut-chart/donut-chart.component';
|
||||
import { DeviceService } from 'src/app/services/device.service';
|
||||
import Device from 'src/app/device';
|
||||
import { StType } from '../../type';
|
||||
import { FilterService } from 'src/app/services/filter.service';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
|
||||
export interface ChartItemState {
|
||||
label: string,
|
||||
count: number,
|
||||
color: string,
|
||||
selected: boolean,
|
||||
}
|
||||
@Component({
|
||||
selector: 'app-chart',
|
||||
templateUrl: './chart.component.html',
|
||||
styleUrls: ['./chart.component.scss']
|
||||
})
|
||||
|
||||
export class ChartComponent implements OnInit {
|
||||
@ViewChild(DonutChartComponent) donutChart: DonutChartComponent;
|
||||
@Input() type: StType;
|
||||
title: string;
|
||||
chartID: string;
|
||||
states: ChartItemState[] = [];
|
||||
|
||||
private observer: Observable<any>;
|
||||
private activeChartState: ChartItemState;
|
||||
|
||||
constructor(
|
||||
private folderService: FolderService,
|
||||
private deviceService: DeviceService,
|
||||
private filterService: FilterService,
|
||||
) { }
|
||||
|
||||
onItemSelect(s: ChartItemState) {
|
||||
// Send chart item state to filter
|
||||
this.filterService.changeFilter({ type: this.type, text: s.label });
|
||||
|
||||
// Deselect all other items
|
||||
this.states.forEach(s => {
|
||||
s.selected = false;
|
||||
});
|
||||
|
||||
// Select item only
|
||||
if (s !== this.activeChartState) {
|
||||
s.selected = true;
|
||||
this.activeChartState = s;
|
||||
} else {
|
||||
this.activeChartState = null;
|
||||
this.filterService.changeFilter({ type: this.type, text: "" })
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
switch (this.type) {
|
||||
case StType.Folder:
|
||||
this.title = "Folders";
|
||||
this.chartID = 'foldersChart';
|
||||
this.observer = this.folderService.folderAdded$;
|
||||
break;
|
||||
case StType.Device:
|
||||
this.title = "Devices";
|
||||
this.chartID = 'devicesChart';
|
||||
this.observer = this.deviceService.deviceAdded$;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
let totalCount: number = 0;
|
||||
this.observer.subscribe(
|
||||
t => {
|
||||
// Count the number of folders and set chart
|
||||
totalCount++;
|
||||
this.donutChart.count = totalCount;
|
||||
|
||||
// Get StateType and convert to string
|
||||
const stateType = t.stateType;
|
||||
const state = t.state;
|
||||
let color;
|
||||
switch (this.type) {
|
||||
case StType.Folder:
|
||||
color = Folder.stateTypeToColor(t.stateType);
|
||||
break;
|
||||
case StType.Device:
|
||||
color = Device.stateTypeToColor(stateType);
|
||||
break;
|
||||
}
|
||||
|
||||
// Check if state exists
|
||||
let found: boolean = false;
|
||||
this.states.forEach(s => {
|
||||
if (s.label === state) {
|
||||
s.count = s.count + 1;
|
||||
found = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (!found) {
|
||||
this.states.push({ label: state, count: 1, color: color, selected: false });
|
||||
}
|
||||
|
||||
this.donutChart.updateData(this.states);
|
||||
},
|
||||
err => console.error('Observer got an error: ' + err),
|
||||
() => {
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<div class="chart-container">
|
||||
<canvas id={{elementID}} width="100px" height="100px"></canvas>
|
||||
<div class="center" fxLayout="column" fxLayoutAlign="center center">
|
||||
<div class="{{_countClass}}">{{_count}}</div>
|
||||
<div class="title">{{title}}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,48 @@
|
||||
.chart-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.center {
|
||||
position: absolute;
|
||||
top: 0; left: 0; bottom: 0; right: 0;
|
||||
width: 50%;
|
||||
height: 50%;
|
||||
overflow: auto;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: calc(0.5rem + 0.625vw);
|
||||
display:none;
|
||||
}
|
||||
|
||||
.count-total {
|
||||
font-size: calc(1rem + 0.625vw);
|
||||
}
|
||||
|
||||
.large-count-total {
|
||||
font-size: calc(0.5rem + 0.625vw);
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.count-total {
|
||||
font-size: calc(1.00rem + 0.625vw);
|
||||
}
|
||||
}
|
||||
@media (min-width: 800px) and (max-width: 1000px) {
|
||||
.title {
|
||||
font-size: calc(0.35rem + 0.625vw);
|
||||
}
|
||||
|
||||
.count-total {
|
||||
font-size: calc(1.35rem + 0.625vw);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width:1000px) {
|
||||
.title {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DonutChartComponent } from './donut-chart.component';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
|
||||
describe('DonutChartComponent', () => {
|
||||
let component: DonutChartComponent;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [DonutChartComponent],
|
||||
providers: [DonutChartComponent]
|
||||
}).compileComponents();
|
||||
|
||||
component = TestBed.inject(DonutChartComponent);
|
||||
}));
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { Chart } from 'chart.js'
|
||||
import { tooltip } from '../tooltip'
|
||||
import { FilterService } from 'src/app/services/filter.service';
|
||||
import { ChartItemState } from '../chart/chart.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-donut-chart',
|
||||
templateUrl: './donut-chart.component.html',
|
||||
styleUrls: ['./donut-chart.component.scss']
|
||||
})
|
||||
export class DonutChartComponent {
|
||||
@Input() elementID: string;
|
||||
@Input() title: number;
|
||||
@Output() stateEvent = new EventEmitter<ChartItemState>();;
|
||||
|
||||
_count: number;
|
||||
_countClass = "count-total";
|
||||
set count(n: number) {
|
||||
if (n >= 1000) { // use a smaller font
|
||||
this._countClass = "large-count-total"
|
||||
}
|
||||
this._count = n;
|
||||
}
|
||||
|
||||
private canvas: any;
|
||||
private ctx: any;
|
||||
private chart: Chart;
|
||||
private states: ChartItemState[];
|
||||
|
||||
constructor(private filterService: FilterService) { }
|
||||
|
||||
updateData(states: ChartItemState[]): void {
|
||||
this.states = states;
|
||||
// Using object destructuring
|
||||
for (let i = 0; i < states.length; i++) {
|
||||
let s = states[i];
|
||||
this.chart.data.labels[i] = s.label;
|
||||
this.chart.data.datasets[0].data[i] = s.count;
|
||||
this.chart.data.datasets[0].backgroundColor[i] = s.color;
|
||||
}
|
||||
this.chart.update();
|
||||
}
|
||||
|
||||
removeAllData(withAnimation: boolean): void {
|
||||
this.chart.data.labels.pop();
|
||||
this.chart.data.datasets.forEach((dataset) => {
|
||||
dataset.data = [];
|
||||
});
|
||||
this.chart.update(withAnimation);
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.canvas = document.getElementById(this.elementID);
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
this.chart = new Chart(this.ctx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
datasets: [{
|
||||
data: [],
|
||||
backgroundColor: [],
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
cutoutPercentage: 77,
|
||||
responsive: true,
|
||||
onClick: (e) => {
|
||||
var activePoints = this.chart.getElementsAtEvent(e);
|
||||
if (activePoints.length > 0) {
|
||||
const index = activePoints[0]["_index"];
|
||||
this.stateEvent.emit(this.states[index]);
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
tooltips: {
|
||||
// Disable the on-canvas tooltip
|
||||
enabled: false,
|
||||
custom: tooltip(),
|
||||
},
|
||||
animation: false
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// Adapted from https://www.chartjs.org/samples/latest/tooltips/custom-pie.html
|
||||
export let tooltip: () => (tooltip: any) => void =
|
||||
function (): (tooltip: any) => void {
|
||||
return function (tooltip: any): void {
|
||||
// Tooltip Element
|
||||
const tooltipEl = document.getElementById('chartjs-tooltip');
|
||||
|
||||
// Hide if no tooltip
|
||||
if (tooltip.opacity === 0) {
|
||||
tooltipEl.style.opacity = '0';
|
||||
return;
|
||||
}
|
||||
|
||||
// Set caret Position
|
||||
tooltipEl.classList.remove('above', 'below', 'no-transform');
|
||||
if (tooltip.yAlign) {
|
||||
tooltipEl.classList.add(tooltip.yAlign);
|
||||
} else {
|
||||
tooltipEl.classList.add('no-transform');
|
||||
}
|
||||
|
||||
function getBody(bodyItem) {
|
||||
return bodyItem.lines;
|
||||
}
|
||||
|
||||
// Set Text
|
||||
if (tooltip.body) {
|
||||
let titleLines = tooltip.title || [];
|
||||
const bodyLines = tooltip.body.map(getBody);
|
||||
|
||||
let innerHtml = '<thead>';
|
||||
|
||||
titleLines.forEach(function (title) {
|
||||
innerHtml += '<tr><th>' + title + '</th></tr>';
|
||||
});
|
||||
innerHtml += '</thead><tbody>';
|
||||
|
||||
bodyLines.forEach(function (body, i) {
|
||||
let colors = tooltip.labelColors[i];
|
||||
let style = 'background:' + colors.backgroundColor;
|
||||
style += '; border-color:' + colors.borderColor;
|
||||
style += '; border-width: 2px';
|
||||
let span = '<span class="chartjs-tooltip-key" style="' + style + '"></span>';
|
||||
innerHtml += '<tr><td>' + span + body + '</td></tr>';
|
||||
});
|
||||
innerHtml += '</tbody>';
|
||||
|
||||
let tableRoot = tooltipEl.querySelector('table');
|
||||
tableRoot.innerHTML = innerHtml;
|
||||
}
|
||||
|
||||
var position = this._chart.canvas.getBoundingClientRect();
|
||||
|
||||
// Display, position, and set styles for font
|
||||
tooltipEl.style.opacity = '1';
|
||||
tooltipEl.style.position = 'absolute';
|
||||
tooltipEl.style.left = position.left + window.pageXOffset + tooltip.caretX + 'px';
|
||||
tooltipEl.style.top = position.top + window.pageYOffset + tooltip.caretY + 'px';
|
||||
tooltipEl.style.padding = tooltip.yPadding + 'px ' + tooltip.xPadding + 'px';
|
||||
tooltipEl.style.pointerEvents = 'none';
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user