299 lines
10 KiB
TypeScript
299 lines
10 KiB
TypeScript
///
|
|
/// Copyright © 2016-2023 The Thingsboard Authors
|
|
///
|
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|
/// you may not use this file except in compliance with the License.
|
|
/// You may obtain a copy of the License at
|
|
///
|
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
|
///
|
|
/// Unless required by applicable law or agreed to in writing, software
|
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
/// See the License for the specific language governing permissions and
|
|
/// limitations under the License.
|
|
///
|
|
|
|
import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, Input, ViewChild } from '@angular/core';
|
|
import { Store } from '@ngrx/store';
|
|
import { AppState } from '@core/core.state';
|
|
import { Router } from '@angular/router';
|
|
import { FormBuilder, FormGroup } from '@angular/forms';
|
|
import { MatDialog } from '@angular/material/dialog';
|
|
import { AttributeService } from '@core/http/attribute.service';
|
|
import { DeviceService } from '@core/http/device.service';
|
|
import { TranslateService } from '@ngx-translate/core';
|
|
import { AttributeData, AttributeScope } from '@shared/models/telemetry/telemetry.models';
|
|
import { PageComponent } from '@shared/components/page.component';
|
|
import { DialogService } from '@app/core/services/dialog.service';
|
|
import { WidgetContext } from '@home/models/widget-component.models';
|
|
import { TbFlot } from '@home/components/widget/lib/flot-widget';
|
|
import { ResizeObserver } from '@juggle/resize-observer';
|
|
import { IWidgetSubscription, SubscriptionInfo, WidgetSubscriptionOptions } from '@core/api/widget-api.models';
|
|
import { UtilsService } from '@core/services/utils.service';
|
|
import { DatasourceType, LegendConfig, LegendData, LegendPosition, widgetType } from '@shared/models/widget.models';
|
|
import { EntityType } from '@shared/models/entity-type.models';
|
|
import { EntityId } from '@shared/models/id/entity-id';
|
|
import { BaseData } from '@shared/models/base-data';
|
|
import { PageLink } from '@shared/models/page/page-link';
|
|
import { Direction, SortOrder } from '@shared/models/page/sort-order';
|
|
import { MatTableDataSource } from '@angular/material/table';
|
|
import { MatSort } from '@angular/material/sort';
|
|
|
|
|
|
@Component({
|
|
selector: 'tb-gateway-statistics',
|
|
templateUrl: './gateway-statistics.component.html',
|
|
styleUrls: ['./gateway-statistics.component.scss']
|
|
})
|
|
export class GatewayStatisticsComponent extends PageComponent implements AfterViewInit {
|
|
|
|
@ViewChild(MatSort) sort: MatSort;
|
|
@ViewChild('statisticChart') statisticChart: ElementRef;
|
|
|
|
@Input()
|
|
ctx: WidgetContext;
|
|
|
|
@Input()
|
|
public general: boolean;
|
|
|
|
public isNumericData: boolean = true;
|
|
public chartInited: boolean;
|
|
private flot: TbFlot;
|
|
private flotCtx;
|
|
public statisticForm: FormGroup;
|
|
public statisticsKeys = [];
|
|
public commands = [];
|
|
public commandObj: any;
|
|
public dataSource: MatTableDataSource<any>;
|
|
public pageLink: PageLink;
|
|
private resize$: ResizeObserver;
|
|
private subscription: IWidgetSubscription;
|
|
private subscriptionInfo: SubscriptionInfo [];
|
|
public legendData: LegendData;
|
|
public displayedColumns: Array<string>;
|
|
private subscriptionOptions: WidgetSubscriptionOptions = {
|
|
callbacks: {
|
|
onDataUpdated: () => this.ctx.ngZone.run(() => {
|
|
this.onDataUpdated();
|
|
}),
|
|
onDataUpdateError: (subscription, e) => this.ctx.ngZone.run(() => {
|
|
this.onDataUpdateError(e);
|
|
})
|
|
},
|
|
useDashboardTimewindow: false,
|
|
legendConfig: {
|
|
position: LegendPosition.bottom
|
|
} as LegendConfig
|
|
};
|
|
|
|
|
|
constructor(protected router: Router,
|
|
protected store: Store<AppState>,
|
|
protected fb: FormBuilder,
|
|
protected translate: TranslateService,
|
|
protected attributeService: AttributeService,
|
|
protected deviceService: DeviceService,
|
|
protected dialogService: DialogService,
|
|
private cd: ChangeDetectorRef,
|
|
private utils: UtilsService,
|
|
public dialog: MatDialog) {
|
|
super(store);
|
|
const sortOrder: SortOrder = {property: '0', direction: Direction.DESC};
|
|
this.pageLink = new PageLink(Number.POSITIVE_INFINITY, 0, null, sortOrder);
|
|
this.displayedColumns = ['0', '1'];
|
|
this.dataSource = new MatTableDataSource<any>([]);
|
|
this.statisticForm = this.fb.group({
|
|
statisticKey: [null, []]
|
|
})
|
|
|
|
this.statisticForm.get('statisticKey').valueChanges.subscribe(value => {
|
|
this.commandObj = null;
|
|
if (this.commands.length) {
|
|
this.commandObj = this.commands.find(command => command.attributeOnGateway === value);
|
|
}
|
|
if (this.subscriptionInfo) this.createChartsSubscription(this.ctx.defaultSubscription.datasources[0].entity, value);
|
|
})
|
|
}
|
|
|
|
|
|
ngAfterViewInit() {
|
|
this.dataSource.sort = this.sort;
|
|
this.sort.sortChange.subscribe(_=>{
|
|
this.sortData();
|
|
})
|
|
this.init();
|
|
if (this.ctx.defaultSubscription.datasources.length) {
|
|
|
|
const gateway = this.ctx.defaultSubscription.datasources[0].entity;
|
|
if (!this.general) {
|
|
this.attributeService.getEntityAttributes(gateway.id, AttributeScope.SHARED_SCOPE, ["general_configuration"]).subscribe((resp: AttributeData[]) => {
|
|
if (resp && resp.length) {
|
|
this.commands = resp[0].value.statistics.commands;
|
|
if (!this.statisticForm.get('statisticKey').value && this.commands && this.commands.length) {
|
|
this.statisticForm.get('statisticKey').setValue(this.commands[0].attributeOnGateway);
|
|
this.createChartsSubscription(gateway, this.commands[0].attributeOnGateway);
|
|
}
|
|
}
|
|
})
|
|
} else {
|
|
let connectorsTs;
|
|
this.attributeService.getEntityTimeseriesLatest(gateway.id).subscribe(
|
|
data => {
|
|
connectorsTs = Object.keys(data)
|
|
.filter(el => el.includes(
|
|
'ConnectorEventsProduced'
|
|
) || el.includes(
|
|
'ConnectorEventsSent'))
|
|
this.createGeneralChartsSubscription(gateway, connectorsTs);
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
public sortData () {
|
|
this.dataSource.sortData(this.dataSource.data, this.sort);
|
|
}
|
|
|
|
public onLegendKeyHiddenChange(index: number) {
|
|
this.legendData.keys[index].dataKey.hidden = !this.legendData.keys[index].dataKey.hidden;
|
|
this.subscription.updateDataVisibility(index);
|
|
}
|
|
|
|
private createChartsSubscription(gateway: BaseData<EntityId>, attr: string) {
|
|
let subscriptionInfo = [{
|
|
type: DatasourceType.entity,
|
|
entityType: EntityType.DEVICE,
|
|
entityId: gateway.id.id,
|
|
entityName: gateway.name,
|
|
timeseries: []
|
|
}];
|
|
|
|
subscriptionInfo[0].timeseries = [{name: attr, label: attr}];
|
|
this.subscriptionInfo = subscriptionInfo;
|
|
this.changeSubscription(subscriptionInfo);
|
|
}
|
|
|
|
private createGeneralChartsSubscription(gateway: BaseData<EntityId>, attrData: [string]) {
|
|
let subscriptionInfo = [{
|
|
type: DatasourceType.entity,
|
|
entityType: EntityType.DEVICE,
|
|
entityId: gateway.id.id,
|
|
entityName: gateway.name,
|
|
timeseries: []
|
|
}];
|
|
subscriptionInfo[0].timeseries = [];
|
|
if (attrData && attrData.length) {
|
|
attrData.forEach(attr => {
|
|
subscriptionInfo[0].timeseries.push({name: attr, label: attr})
|
|
})
|
|
}
|
|
this.ctx.defaultSubscription.datasources[0].dataKeys.forEach(dataKey => {
|
|
subscriptionInfo[0].timeseries.push({name: dataKey.name, label: dataKey.label})
|
|
})
|
|
|
|
this.subscriptionInfo = subscriptionInfo;
|
|
this.changeSubscription(subscriptionInfo);
|
|
}
|
|
|
|
init = () => {
|
|
this.flotCtx = {
|
|
$scope: this.ctx.$scope,
|
|
$injector: this.ctx.$injector,
|
|
utils: this.ctx.utils,
|
|
isMobile: this.ctx.isMobile,
|
|
isEdit: this.ctx.isEdit,
|
|
subscriptionApi: this.ctx.subscriptionApi,
|
|
detectChanges: this.ctx.detectChanges,
|
|
settings: this.ctx.settings
|
|
};
|
|
}
|
|
|
|
updateChart = () => {
|
|
if (this.flot && this.ctx.defaultSubscription.data.length) {
|
|
this.flot.update();
|
|
}
|
|
}
|
|
|
|
resize = () => {
|
|
if (this.flot) {
|
|
this.flot.resize();
|
|
}
|
|
}
|
|
|
|
private reset() {
|
|
if (this.resize$) {
|
|
this.resize$.disconnect();
|
|
}
|
|
if (this.subscription) {
|
|
this.subscription.unsubscribe();
|
|
}
|
|
if (this.flot) {
|
|
this.flot.destroy();
|
|
}
|
|
}
|
|
|
|
private onDataUpdateError(e: any) {
|
|
const exceptionData = this.utils.parseException(e);
|
|
let errorText = exceptionData.name;
|
|
if (exceptionData.message) {
|
|
errorText += ': ' + exceptionData.message;
|
|
}
|
|
console.error(errorText);
|
|
}
|
|
|
|
private onDataUpdated() {
|
|
this.isDataOnlyNumbers();
|
|
if (this.isNumericData) {
|
|
if (this.chartInited) {
|
|
if (this.flot) {
|
|
this.flot.update();
|
|
}
|
|
} else {
|
|
this.initChart();
|
|
}
|
|
}
|
|
}
|
|
|
|
private initChart() {
|
|
this.chartInited = true;
|
|
this.flotCtx.$container = $(this.statisticChart.nativeElement);
|
|
this.resize$.observe(this.statisticChart.nativeElement);
|
|
this.flot = new TbFlot(this.flotCtx as WidgetContext, "line");
|
|
this.flot.update();
|
|
}
|
|
|
|
private isDataOnlyNumbers() {
|
|
if (this.general) {
|
|
this.isNumericData = true;
|
|
return;
|
|
}
|
|
this.dataSource.data = this.subscription.data.length ? this.subscription.data[0].data : [];
|
|
this.isNumericData = this.dataSource.data.every(data => !isNaN(+data[1]) );
|
|
}
|
|
|
|
|
|
changeSubscription(subscriptionInfo: SubscriptionInfo[]) {
|
|
if (this.subscription) {
|
|
this.reset();
|
|
}
|
|
if (this.ctx.datasources[0].entity) {
|
|
this.ctx.subscriptionApi.createSubscriptionFromInfo(widgetType.timeseries, subscriptionInfo, this.subscriptionOptions, false, true).subscribe(subscription => {
|
|
this.subscription = subscription;
|
|
this.isDataOnlyNumbers();
|
|
this.legendData = this.subscription.legendData;
|
|
this.flotCtx.defaultSubscription = subscription;
|
|
this.resize$ = new ResizeObserver(() => {
|
|
this.resize();
|
|
});
|
|
this.ctx.detectChanges();
|
|
if (this.isNumericData) {
|
|
this.initChart();
|
|
}
|
|
})
|
|
|
|
}
|
|
}
|
|
|
|
}
|