From b00bb40293cc2603e5730b225d32a47857831cf6 Mon Sep 17 00:00:00 2001 From: franzmueller Date: Tue, 10 Dec 2024 12:07:09 +0100 Subject: [PATCH] SNRGY-3064 add stacked bar charts with custom sorting --- src/app/core/services/util.service.ts | 19 +- .../charts/export/charts-export.component.ts | 48 +++- .../axis-config/axis-config.component.css | 65 +++++ .../axis-config/axis-config.component.html | 132 +++++++++ .../axis-config/axis-config.component.ts | 113 ++++++++ .../charts-export-edit-dialog.component.css | 9 + .../charts-export-edit-dialog.component.html | 185 ++----------- .../charts-export-edit-dialog.component.ts | 255 +++++++++++------- .../shared/charts-export-properties.model.ts | 1 + .../export/shared/charts-export.service.ts | 139 +++++----- .../data-source-selector.component.html | 10 +- .../data-source-selector.component.ts | 113 ++++---- src/app/widgets/widget.module.ts | 10 +- 13 files changed, 721 insertions(+), 378 deletions(-) create mode 100644 src/app/widgets/charts/export/dialog/axis-config/axis-config.component.css create mode 100644 src/app/widgets/charts/export/dialog/axis-config/axis-config.component.html create mode 100644 src/app/widgets/charts/export/dialog/axis-config/axis-config.component.ts diff --git a/src/app/core/services/util.service.ts b/src/app/core/services/util.service.ts index 91510d64..5de53fc1 100644 --- a/src/app/core/services/util.service.ts +++ b/src/app/core/services/util.service.ts @@ -88,7 +88,7 @@ export class UtilService { } - dateIsToday(dateTime: string | number): Boolean { + dateIsToday(dateTime: string | number): boolean { const today = new Date(); today.setHours(0,0,0,0); @@ -100,3 +100,20 @@ export class UtilService { return date.getTime() === today.getTime(); } } + +export function hashCode(s: string): number { + let hash = 0; + let i = 0; + let chr = 0; + if (s.length === 0) { + return hash; + } + for (i = 0; i < s.length; i++) { + chr = s.charCodeAt(i); + // eslint-disable-next-line no-bitwise + hash = ((hash << 5) - hash) + chr; + // eslint-disable-next-line no-bitwise + hash |= 0; // Convert to 32bit integer + } + return hash; +} diff --git a/src/app/widgets/charts/export/charts-export.component.ts b/src/app/widgets/charts/export/charts-export.component.ts index 26151af2..87f5b069 100644 --- a/src/app/widgets/charts/export/charts-export.component.ts +++ b/src/app/widgets/charts/export/charts-export.component.ts @@ -217,7 +217,9 @@ export class ChartsExportComponent implements OnInit, OnDestroy, AfterViewInit { widget.properties.vAxes = this.modifiedVaxes; } - this.chartsExportService.getChartData(widget, this.from?.toISOString(), this.to?.toISOString(), this.groupTime || undefined, this.hAxisFormat || undefined, lastOverride).subscribe((resp: ChartsModel | ErrorModel) => { + widget.properties.stacked = this.stacked; + + this.chartsExportService.getChartData(widget, this.from?.toISOString(), this.to?.toISOString(), this.groupTime || undefined, this.hAxisFormat || undefined, lastOverride, this.chooseColors).subscribe((resp: ChartsModel | ErrorModel) => { if (this.errorHandlerService.checkIfErrorExists(resp)) { this.errorHasOccured = true; this.errorMessage = 'No data'; @@ -361,11 +363,20 @@ export class ChartsExportComponent implements OnInit, OnDestroy, AfterViewInit { return; } const axis = axes[$event.column - 1]; // time column - if (axis.deviceGroupMergingStrategy === ChartsExportDeviceGroupMergingStrategy.Sum && (axis.deviceGroupId !== undefined || axis.locationId !== undefined)) { + if (axis.subAxes !== undefined && axis.subAxes.length > 0) { + const cpy = JSON.parse(JSON.stringify(axis.subAxes)) as ChartsExportVAxesModel[]; + cpy.forEach(sub => sub.displayOnSecondVAxis = axis.displayOnSecondVAxis); + this.modifiedVaxes = cpy; + this.stacked = true; + this.ready = false; + this.refresh(); + } else if (axis.deviceGroupMergingStrategy === ChartsExportDeviceGroupMergingStrategy.Sum && (axis.deviceGroupId !== undefined || axis.locationId !== undefined)) { // we can split this! + this.chooseColors = true; const cpy = JSON.parse(JSON.stringify(axis)) as ChartsExportVAxesModel; cpy.deviceGroupMergingStrategy = ChartsExportDeviceGroupMergingStrategy.Separate; this.modifiedVaxes = [cpy]; + this.stacked = true; this.ready = false; this.refresh(); } @@ -509,6 +520,39 @@ export class ChartsExportComponent implements OnInit, OnDestroy, AfterViewInit { } } + private get stacked(): boolean { + const str = localStorage.getItem(this.widget.id + '_stacked'); + if (str === null) { + return this.widget.properties.stacked || false; + } + return JSON.parse(str); + } + + private set stacked(stacked: boolean | null) { + if (stacked === null) { + localStorage.removeItem(this.widget.id + '_stacked'); + } else { + localStorage.setItem(this.widget.id + '_stacked', '' + stacked); + } + } + + private get chooseColors(): boolean { + const str = localStorage.getItem(this.widget.id + '_chooseColors'); + if (str === null) { + return false; + } + return JSON.parse(str); + } + + private set chooseColors(chooseColors: boolean | null) { + if (chooseColors === null) { + localStorage.removeItem(this.widget.id + '_chooseColors'); + } else { + localStorage.setItem(this.widget.id + '_chooseColors', '' + chooseColors); + } + } + + getCustomIcons(header: boolean): { icons: string[]; disabled: boolean[]; tooltips: string[] } { const res = { icons: [] as string[], disabled: [] as boolean[], tooltips: [] as string[] }; diff --git a/src/app/widgets/charts/export/dialog/axis-config/axis-config.component.css b/src/app/widgets/charts/export/dialog/axis-config/axis-config.component.css new file mode 100644 index 00000000..da9d3221 --- /dev/null +++ b/src/app/widgets/charts/export/dialog/axis-config/axis-config.component.css @@ -0,0 +1,65 @@ +/* + * Copyright 2024 InfAI (CC SES) + * + * 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. + */ + +.column { + display: flex; + flex-direction: column; +} + +.row { + display: flex; + flex-direction: row; +} + +.w-100 { + width: 100%; +} + +.w-50 { + width: 50%; +} + +.pb { + padding-bottom: 0.5em; +} + +.mat-mdc-card-title { + font-size: 16px; +} + +.mat-mdc-card-header { + display: flex; + justify-content: space-between; +} + +.cpy-delete { + margin-top: -0.75rem; +} + +.mat-mdc-form-field { + padding-right: 1rem; +} + +.rules-btn-spacer { + padding-left: 2rem; + margin-top: -0.25rem; +} + +.drop-zone { + height: 4em; + border-top: dotted; + text-align: center; +} diff --git a/src/app/widgets/charts/export/dialog/axis-config/axis-config.component.html b/src/app/widgets/charts/export/dialog/axis-config/axis-config.component.html new file mode 100644 index 00000000..c5d96dcb --- /dev/null +++ b/src/app/widgets/charts/export/dialog/axis-config/axis-config.component.html @@ -0,0 +1,132 @@ +
  • + + + +
    + + + + {{element.exportName}} - {{element.valueName}} ({{element.valueType}}) + +
    + + +
    +
    + +
    +
    + + Value Alias + + + +
    +
    + + Color + + + +
    +
    + + Math + + + +
    +
    +
    + +
    + + Filter + + None + + {{option}} + + + +
    +
    + + Filter Value + + + +
    +
    +
    +
    + + Tags + + + + +
    +
    + + Merging Strategy + + Separate + Merge + Sum + + + +
    +
    +
    +
    + Use second Y-Axis + +
    +
    + +
    +
    +
    + + +
    +
    +
    +
    +
    +
    +
  • \ No newline at end of file diff --git a/src/app/widgets/charts/export/dialog/axis-config/axis-config.component.ts b/src/app/widgets/charts/export/dialog/axis-config/axis-config.component.ts new file mode 100644 index 00000000..0727fbd9 --- /dev/null +++ b/src/app/widgets/charts/export/dialog/axis-config/axis-config.component.ts @@ -0,0 +1,113 @@ +/* + * Copyright 2024 InfAI (CC SES) + * + * 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 { Component, EventEmitter, Input, Output } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { ChartsExportVAxesModel, ChartsExportDeviceGroupMergingStrategy, ChartsExportConversion } from '../../shared/charts-export-properties.model'; +import { ListRulesComponent } from '../list-rules/list-rules.component'; +import { NestedTreeControl } from '@angular/cdk/tree'; +import { CdkDragDrop } from '@angular/cdk/drag-drop'; +import { ChartsExportEditDialogComponent } from '../charts-export-edit-dialog.component'; +import { hashCode } from 'src/app/core/services/util.service'; + +@Component({ + selector: 'senergy-axis-config', + templateUrl: './axis-config.component.html', + styleUrls: ['./axis-config.component.css'] +}) +export class AxisConfigComponent { + @Input() groupTypeIsDifference = false; + @Input() userHasUpdatePropertiesAuthorization = false; + @Input() exportTags: Map> = new Map(); + @Input() groupType?: string; + @Input() element: ChartsExportVAxesModel = {} as ChartsExportVAxesModel; + @Input() treeControl: NestedTreeControl = new NestedTreeControl((node) => node.subAxes); + @Input() enableDragDrop = false; + @Input() subElement = false; + @Input() connectedNodes: (not?: ChartsExportVAxesModel) => string[] = (_) => []; + @Input() dragging = false; + + @Output() copyClicked = new EventEmitter(); + @Output() deleteClicked = new EventEmitter(); + @Output() dragStart = new EventEmitter(); + @Output() dragEnd = new EventEmitter(); + @Output() dropped = new EventEmitter<{ $event: CdkDragDrop; target: ChartsExportVAxesModel }>(); + + chartsExportDeviceGroupMergingStrategy = ChartsExportDeviceGroupMergingStrategy; + super = ChartsExportEditDialogComponent; + + constructor( + private dialog: MatDialog, + ) { } + + listRules(element: ChartsExportVAxesModel) { + const dialog = this.dialog.open(ListRulesComponent, { + data: element.conversions || [] + }); + + dialog.afterClosed().subscribe({ + next: (rules: ChartsExportConversion[]) => { + if (rules != null) { + element.conversions = rules; + } + }, + error: (_) => { + + } + }); + } + + compareFilterTypes(a: string, b: string): boolean { + return a === b; + } + + + filerTypeSelected(element: ChartsExportVAxesModel) { + if (element.filterType === undefined) { + element.filterValue = undefined; + } + } + + getTags(element: ChartsExportVAxesModel): Map { + return this.exportTags.get(element.instanceId || '') || new Map(); + } + + getTagOptionDisabledFunction(tab: ChartsExportVAxesModel): (option: { value: string; parent: string }) => boolean { + return (option: { value: string; parent: string }) => { + const selection = tab.tagSelection; + if (selection === null || selection === undefined || Object.keys(selection).length === 0) { + return false; + } + const existing = selection.find((s) => s.startsWith(option.parent) && this.getTagValue(option) !== s); + return existing !== undefined; + }; + } + + getTagValue(a: { value: string; parent: string }): string { + return a.parent + '!' + a.value; + } + + get expanded(): boolean { + if (this.element === undefined) { + return false; + } + return this.treeControl?.isExpanded(this.element) || false; + } + + getId(): string { + return '' + hashCode(JSON.stringify(this.element)); + } +} diff --git a/src/app/widgets/charts/export/dialog/charts-export-edit-dialog.component.css b/src/app/widgets/charts/export/dialog/charts-export-edit-dialog.component.css index d31c54e8..0a5023f1 100644 --- a/src/app/widgets/charts/export/dialog/charts-export-edit-dialog.component.css +++ b/src/app/widgets/charts/export/dialog/charts-export-edit-dialog.component.css @@ -59,4 +59,13 @@ mat-form-field { width: 100px; } +senergy-axis-config { + width: 100%; + padding-bottom: 0.5em; +} +.root-drop-zone { + height: 2em; + border-top: dotted; + text-align: center; +} diff --git a/src/app/widgets/charts/export/dialog/charts-export-edit-dialog.component.html b/src/app/widgets/charts/export/dialog/charts-export-edit-dialog.component.html index 022ed8f2..3213587a 100644 --- a/src/app/widgets/charts/export/dialog/charts-export-edit-dialog.component.html +++ b/src/app/widgets/charts/export/dialog/charts-export-edit-dialog.component.html @@ -59,7 +59,7 @@

    Edit Chart Export

    - Configure Chart -
    - - X-Axis - - - X-Axis-Label @@ -113,158 +107,31 @@

    Configure Chart

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Source - {{element.exportName}} - Value - {{element.valueName}} - Type {{element.valueType}} Alias - - Value Alias - - - - Color - - Color - - - - Math - - Math - - - - Rules - - - Filter - - Filter - - None - - {{option}} - - - - Filter Value - - Filter Value - - - - Tags - - Tags - - - - - Merging Strategy - - Merging Strategy - - Separate - Merge - Sum - - - - Use second Y-Axis - - - - - -
    +
    + + + + + + + + + + + +
    +

    Move to top level

    +
    +
    diff --git a/src/app/widgets/charts/export/dialog/charts-export-edit-dialog.component.ts b/src/app/widgets/charts/export/dialog/charts-export-edit-dialog.component.ts index dd30f434..8befd7e4 100644 --- a/src/app/widgets/charts/export/dialog/charts-export-edit-dialog.component.ts +++ b/src/app/widgets/charts/export/dialog/charts-export-edit-dialog.component.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import {Component, Inject, OnInit} from '@angular/core'; -import {WidgetModel} from '../../../../modules/dashboard/shared/dashboard-widget.model'; -import {DashboardService} from '../../../../modules/dashboard/shared/dashboard.service'; -import {DashboardResponseMessageModel} from '../../../../modules/dashboard/shared/dashboard-response-message.model'; +import { Component, Inject, OnInit, ViewChild } from '@angular/core'; +import { WidgetModel } from '../../../../modules/dashboard/shared/dashboard-widget.model'; +import { DashboardService } from '../../../../modules/dashboard/shared/dashboard.service'; +import { DashboardResponseMessageModel } from '../../../../modules/dashboard/shared/dashboard-response-message.model'; import { AbstractControl, FormArray, @@ -26,21 +26,22 @@ import { UntypedFormGroup, ValidatorFn, Validators } from '@angular/forms'; -import {ExportService} from '../../../../modules/exports/shared/export.service'; -import {ChartsExportConversion, ChartsExportDeviceGroupMergingStrategy, ChartsExportMeasurementModel, ChartsExportVAxesModel} from '../shared/charts-export-properties.model'; -import {ChartsExportRangeTimeTypeEnum} from '../shared/charts-export-range-time-type.enum'; -import {MatTableDataSource} from '@angular/material/table'; -import {MAT_DIALOG_DATA, MatDialogRef, MatDialog} from '@angular/material/dialog'; -import {forkJoin, Observable, of} from 'rxjs'; -import {map} from 'rxjs/operators'; -import {DeviceTypeDeviceClassModel, DeviceTypeFunctionModel} from '../../../../modules/metadata/device-types-overview/shared/device-type.model'; -import {environment} from '../../../../../environments/environment'; +import { ExportService } from '../../../../modules/exports/shared/export.service'; +import { ChartsExportDeviceGroupMergingStrategy, ChartsExportMeasurementModel, ChartsExportVAxesModel } from '../shared/charts-export-properties.model'; +import { ChartsExportRangeTimeTypeEnum } from '../shared/charts-export-range-time-type.enum'; +import { MatTableDataSource } from '@angular/material/table'; +import { MAT_DIALOG_DATA, MatDialogRef, MatDialog } from '@angular/material/dialog'; +import { forkJoin, Observable, of } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { DeviceTypeDeviceClassModel, DeviceTypeFunctionModel } from '../../../../modules/metadata/device-types-overview/shared/device-type.model'; +import { environment } from '../../../../../environments/environment'; import { DeviceGroupCriteriaModel, DeviceGroupModel } from 'src/app/modules/devices/device-groups/shared/device-groups.model'; import { AspectsPermSearchModel } from 'src/app/modules/metadata/aspects/shared/aspects-perm-search.model'; import { ConceptsCharacteristicsModel } from 'src/app/modules/metadata/concepts/shared/concepts-characteristics.model'; -import { ListRulesComponent } from './list-rules/list-rules.component'; -import { DataSourceConfig } from '../../shared/data-source-selector/data-source-selector.component'; +import { DataSourceConfig, DataSourceSelectorComponent } from '../../shared/data-source-selector/data-source-selector.component'; import { DeviceInstanceModel } from 'src/app/modules/devices/device-instances/shared/device-instances.model'; +import { NestedTreeControl } from '@angular/cdk/tree'; +import { hashCode } from 'src/app/core/services/util.service'; @Component({ templateUrl: './charts-export-edit-dialog.component.html', @@ -93,6 +94,8 @@ export class ChartsExportEditDialogComponent implements OnInit { deviceClasses: DeviceTypeDeviceClassModel[] = []; concepts: Map = new Map(); + @ViewChild('datasourceselector', {static: false}) dataSourceSelector?: DataSourceSelectorComponent; + constructor( private dialogRef: MatDialogRef, private dialog: MatDialog, @@ -130,7 +133,7 @@ export class ChartsExportEditDialogComponent implements OnInit { let timeRangeValue = ''; let timeRangeLevel = ''; const timeRange = widget.properties.time?.ahead || widget.properties.time?.last; - if(timeRange != null) { + if (timeRange != null) { const timeRangeSplit = timeRange.match(/[a-zA-Z]+|[0-9]+/g); timeRangeValue = timeRangeSplit?.[0] || ''; timeRangeLevel = timeRangeSplit?.[1] || ''; @@ -139,7 +142,7 @@ export class ChartsExportEditDialogComponent implements OnInit { let groupValue = ''; let groupLevel = ''; const group = widget.properties.group?.time; - if(group != null) { + if (group != null) { const groupSplit = group.match(/[a-zA-Z]+|[0-9]+/g); groupValue = groupSplit?.[0] || ''; groupLevel = groupSplit?.[1] || ''; @@ -171,23 +174,23 @@ export class ChartsExportEditDialogComponent implements OnInit { const timeRangeType = updatedDataSourceConfig.timeRange?.type; const timeRangeLevel = updatedDataSourceConfig.timeRange?.level || ''; - if(timeRangeType === ChartsExportRangeTimeTypeEnum.Absolute) { + if (timeRangeType === ChartsExportRangeTimeTypeEnum.Absolute) { const start = updatedDataSourceConfig.timeRange?.start; - if(start != null && start !== '') { + if (start != null && start !== '') { this.formGroupController.get('properties.time.start')?.patchValue(start + timeRangeLevel); } const end = updatedDataSourceConfig.timeRange?.end; - if(end != null && end !== '') { + if (end != null && end !== '') { this.formGroupController.get('properties.time.end')?.patchValue(end + timeRangeLevel); } - } else if(timeRangeType === ChartsExportRangeTimeTypeEnum.Relative) { + } else if (timeRangeType === ChartsExportRangeTimeTypeEnum.Relative) { const last = updatedDataSourceConfig.timeRange?.time; - if(last != null) { + if (last != null) { this.formGroupController.get('properties.time.last')?.patchValue(last + timeRangeLevel); } - } else if(timeRangeType === ChartsExportRangeTimeTypeEnum.RelativeAhead) { + } else if (timeRangeType === ChartsExportRangeTimeTypeEnum.RelativeAhead) { const ahead = updatedDataSourceConfig.timeRange?.time;; - if(ahead != null) { + if (ahead != null) { this.formGroupController.get('properties.time.ahead')?.patchValue(ahead + timeRangeLevel); } } @@ -308,10 +311,6 @@ export class ChartsExportEditDialogComponent implements OnInit { return a && b && a.id === b.id && a.name === b.name; } - compareFilterTypes(a: string, b: string): boolean { - return a === b; - } - close(): void { this.dialogRef.close(); } @@ -331,40 +330,21 @@ export class ChartsExportEditDialogComponent implements OnInit { save(): void { const obs = []; - if(this.userHasUpdateNameAuthorization) { + if (this.userHasUpdateNameAuthorization) { obs.push(this.updateName()); } - if(this.userHasUpdatePropertiesAuthorization) { + if (this.userHasUpdatePropertiesAuthorization) { obs.push(this.updateProperties()); } forkJoin(obs).subscribe(responses => { const errorOccured = responses.find((response) => response.message !== 'OK'); - if(!errorOccured) { + if (!errorOccured) { this.dialogRef.close(this.formGroupController.value); } }); } - - filerTypeSelected(element: ChartsExportVAxesModel) { - if (element.filterType === undefined) { - element.filterValue = undefined; - } - } - - duplicate(element: ChartsExportVAxesModel, index: number) { - const newElement = JSON.parse(JSON.stringify(element)) as ChartsExportVAxesModel; - newElement.isDuplicate = true; - this.dataSource.data.splice(index + 1, 0, newElement); - this.reloadTable(); - } - - deleteDuplicate(_: ChartsExportVAxesModel, index: number) { - this.dataSource.data.splice(index, 1); - this.reloadTable(); - } - private reloadTable() { this.dataSource._updateChangeSubscription(); } @@ -404,25 +384,6 @@ export class ChartsExportEditDialogComponent implements OnInit { return this.formGroupController.get(['properties', 'timeRangeType']) as FormControl; } - listRules(element: ChartsExportVAxesModel) { - const dialog = this.dialog.open(ListRulesComponent, { - data: element.conversions || [] - }); - - dialog.afterClosed().subscribe({ - next: (rules: ChartsExportConversion[]) => { - if(rules != null) { - element.conversions = rules; - } - }, - error: (_) => { - - } - }); - } - - - addConversion(element: any) { if (element.conversions === undefined) { element.conversions = []; @@ -435,17 +396,13 @@ export class ChartsExportEditDialogComponent implements OnInit { if (this.chartType.value !== 'Timeline' && this.chartType.value !== 'PieChart') { to = JSON.parse(to); } - element.conversions.push({from, to, color: element.__color, alias: element.__alias}); + element.conversions.push({ from, to, color: element.__color, alias: element.__alias }); element.__from = undefined; element.__to = undefined; element.__color = undefined; element.__alias = undefined; } - getTags(element: ChartsExportVAxesModel): Map { - return this.exportTags.get(element.instanceId || '') || this.emptyMap; - } - private preloadExportTags(exportId: string): Observable { if (this.exportTags.get(exportId) !== undefined) { return of(this.exportTags.get(exportId)); @@ -457,7 +414,7 @@ export class ChartsExportEditDialogComponent implements OnInit { res.forEach((v, k) => m.set( k, - v.map((t) => ({value: t, parent: k})), + v.map((t) => ({ value: t, parent: k })), ), ); this.exportTags?.set(exportId, m); @@ -466,33 +423,18 @@ export class ChartsExportEditDialogComponent implements OnInit { ); } - getTagOptionDisabledFunction(tab: ChartsExportVAxesModel): (option: { value: string; parent: string }) => boolean { - return (option: { value: string; parent: string }) => { - const selection = tab.tagSelection; - if (selection === null || selection === undefined || Object.keys(selection).length === 0) { - return false; - } - const existing = selection.find((s) => s.startsWith(option.parent) && this.getTagValue(option) !== s); - return existing !== undefined; - }; - } - - getTagValue(a: { value: string; parent: string }): string { - return a.parent + '!' + a.value; - } - validateInterval: ValidatorFn = (control: AbstractControl) => { const type = this.formGroupController.get('properties.group.type')?.value; if (type === undefined || type === null || type.length === 0) { return null; } if (control.value === undefined || control.value === null || control.value.length === 0) { - return {validateInterval: {value: control.value}}; + return { validateInterval: { value: control.value } }; } const re = new RegExp('\\d+(ns|u|µ|ms|s|months|y|m|h|d|w)'); const matches = re.exec(control.value); if (matches == null || matches.length === 0 || matches[0].length !== control.value.length) { - return {validateInterval: {value: control.value}}; + return { validateInterval: { value: control.value } }; } return null; }; @@ -500,4 +442,133 @@ export class ChartsExportEditDialogComponent implements OnInit { describeCriteria(): (criteria: DeviceGroupCriteriaModel) => string { return criteria => (this.functions.find(f => f.id === criteria.function_id)?.display_name || criteria.function_id) + ' ' + (criteria.device_class_id !== '' ? this.deviceClasses.find(dc => dc.id === criteria.device_class_id)?.name || '' : '') + ' ' + (criteria.aspect_id !== '' ? this.aspects.find(a => a.id === criteria.aspect_id)?.name || '' : ''); } + + treeControl = new NestedTreeControl((node) => node.subAxes); + + dragging = false; + + hasChild(_: number, node: ChartsExportVAxesModel): boolean { + return node.subAxes !== undefined && node.subAxes.length > 0; + } + + deleteDuplicate(node: ChartsExportVAxesModel) { + const stringified = JSON.stringify(node); + const index = this.dataSource.data.findIndex(x => JSON.stringify(x) === stringified); + this.dataSource.data.splice(index, 1); + const t = this.dataSource.data; // required for change detection + this.dataSource.data = t; + } + + duplicate(node: ChartsExportVAxesModel) { + const stringified = JSON.stringify(node); + const newElement = JSON.parse(stringified) as ChartsExportVAxesModel; + newElement.isDuplicate = true; + const index = this.dataSource.data.findIndex(x => JSON.stringify(x) === stringified); + this.dataSource.data.splice(index + 1, 0, newElement); + this.redraw(); + } + + dropped($event: any, target?: ChartsExportVAxesModel) { + const node = $event.item.data as ChartsExportVAxesModel; + if (node === target) { + console.warn('Can\'t move node into itself'); + return; + } + const expanded = this.treeControl.isExpanded(node); + + const clone = JSON.parse(JSON.stringify(node)); + if (target !== undefined) { + if (target.subAxes === undefined || target.subAxes === null) { + target.subAxes = [clone]; + } else { + target.subAxes.push(clone); + } + } else { + this.dataSource.data.push(clone); + } + this.deleteNode(node); + if (expanded) { + this.treeControl.expand(clone); + } + this.redraw(); + } + + deleteNode(node: ChartsExportVAxesModel) { + this.dataSource.data.forEach((sub, i) => { + if (sub === node) { + const del = (dele: boolean) => { + if (dele) { + this.dataSource.data.splice(i, 1); + this.redraw(); + } + }; + del(true); + + } else { + this.findAndDeleteChild(sub, node); + } + }); + this.redraw(); + } + + startDrag() { + this.dragging = true; + } + + stopDrag() { + this.dragging = false; + } + + private findAndDeleteChild(data: ChartsExportVAxesModel, searchElement: ChartsExportVAxesModel) { + if (data.subAxes === null || data.subAxes === undefined) { + return; + } + const i = data.subAxes.indexOf(searchElement); + if (i === -1) { + data.subAxes.forEach((sub) => this.findAndDeleteChild(sub, searchElement)); + } else { + data.subAxes?.splice(i, 1); + this.redraw(); + } + } + + private redraw() { + const data = this.dataSource.data; + if (this.dataSourceSelector !== undefined) { + this.dataSourceSelector?.patchFields(this.dataSource.data); + } + const expanded = this.treeControl.expansionModel.selected; + this.dataSource.data = []; + this.dataSource.data = data; + data.filter(f => expanded.some(e => e === f)).forEach(n => this.treeControl.expand(n)); + } + + // eslint-disable-next-line @typescript-eslint/naming-convention + dontDropPredicate = (_: any, __: any) => false; + + getConnectedNodesFn() { + const that = this; + return (not: ChartsExportVAxesModel | undefined) => ChartsExportEditDialogComponent.connectedNodes(not, that); + } + + static connectedNodes(not: ChartsExportVAxesModel | undefined, that: ChartsExportEditDialogComponent): string[] { + const res: string[] = []; + if (not !== undefined) { + res.push('rootDropZone'); + } + res.push(...ChartsExportEditDialogComponent.connectedChildNodes(that.dataSource.data, not)); + return res; + } + static connectedChildNodes(nodes: ChartsExportVAxesModel[], not?: ChartsExportVAxesModel): string[] { + const res: string[] = []; + nodes.forEach(n => { + if (n !== not) { + res.push('' + hashCode(JSON.stringify(n))); + } + if (n.subAxes !== undefined) { + res.push(...ChartsExportEditDialogComponent.connectedChildNodes(n.subAxes, not)); + } + }); + return res; + } } diff --git a/src/app/widgets/charts/export/shared/charts-export-properties.model.ts b/src/app/widgets/charts/export/shared/charts-export-properties.model.ts index 4cd510f9..d782403e 100644 --- a/src/app/widgets/charts/export/shared/charts-export-properties.model.ts +++ b/src/app/widgets/charts/export/shared/charts-export-properties.model.ts @@ -96,6 +96,7 @@ export interface ChartsExportVAxesModel { deviceGroupId?: string; deviceGroupMergingStrategy?: ChartsExportDeviceGroupMergingStrategy; locationId?: string; + subAxes?: ChartsExportVAxesModel[]; } export interface ApexChartOptions { diff --git a/src/app/widgets/charts/export/shared/charts-export.service.ts b/src/app/widgets/charts/export/shared/charts-export.service.ts index 44ed9c6f..746814ee 100644 --- a/src/app/widgets/charts/export/shared/charts-export.service.ts +++ b/src/app/widgets/charts/export/shared/charts-export.service.ts @@ -46,6 +46,7 @@ import { catchError, concatMap, map } from 'rxjs/operators'; import { environment } from '../../../../../environments/environment'; import { DeviceInstancesTotalModel, DeviceInstancesWithDeviceTypeTotalModel, DeviceInstanceWithDeviceTypeModel } from 'src/app/modules/devices/device-instances/shared/device-instances.model'; import { DeviceInstancesService } from 'src/app/modules/devices/device-instances/shared/device-instances.service'; +import { hashCode } from 'src/app/core/services/util.service'; const customColor = '#4484ce'; // /* cc */ @@ -213,73 +214,73 @@ export class ChartsExportService { const metadataElem = { exportId: values[elementIndex].exportId, deviceId: values[elementIndex].deviceId, serviceId: values[elementIndex].serviceId, columnName: columnNames !== undefined && columnNames.length === 1 ? columnNames[0] : undefined }; if (threeD !== null) { switch ((properties.vAxes || [])[timescaleResultMapper[values[elementIndex].requestIndex]].deviceGroupMergingStrategy) { - case ChartsExportDeviceGroupMergingStrategy.Merge: - // merge into one table row - if (res[values[elementIndex].requestIndex].length === 0) { - res[values[elementIndex].requestIndex].push([]); - } - threeD.forEach(rows => { - if (rows.length === 0) { - return; + case ChartsExportDeviceGroupMergingStrategy.Merge: + // merge into one table row + if (res[values[elementIndex].requestIndex].length === 0) { + res[values[elementIndex].requestIndex].push([]); } - if (rows[0].length > 2) { - rows.forEach(row => { - row.slice(1).forEach(r => { - res[values[elementIndex].requestIndex][0].push([row[0], r]); + threeD.forEach(rows => { + if (rows.length === 0) { + return; + } + if (rows[0].length > 2) { + rows.forEach(row => { + row.slice(1).forEach(r => { + res[values[elementIndex].requestIndex][0].push([row[0], r]); + }); }); - }); - } else { - res[values[elementIndex].requestIndex][0].push(...rows); - } - }); - break; - case ChartsExportDeviceGroupMergingStrategy.Sum: - // sum by timestamp - // can only be selected together with a group to ensure identical timestamps - if (res[values[elementIndex].requestIndex].length === 0) { - res[values[elementIndex].requestIndex].push([]); - } - threeD.forEach(rows => { - rows.forEach(row => { - const elem = res[values[elementIndex].requestIndex][0].find((r: any) => r.length > 0 && r[0] === row[0]); - if (elem === undefined) { - res[values[elementIndex].requestIndex][0].push(row); } else { - let v = (elem[1] as number); - row.slice(1).forEach(n => v += n); - elem[1] = v; + res[values[elementIndex].requestIndex][0].push(...rows); } }); - }); - break; - case ChartsExportDeviceGroupMergingStrategy.Separate: - default: - //preserve individual rows - threeD.forEach(rows => { - if (rows.length === 0) { - return; + break; + case ChartsExportDeviceGroupMergingStrategy.Sum: + // sum by timestamp + // can only be selected together with a group to ensure identical timestamps + if (res[values[elementIndex].requestIndex].length === 0) { + res[values[elementIndex].requestIndex].push([]); } - if (rows[0].length > 2) { - const off = res[values[elementIndex].requestIndex].length; + threeD.forEach(rows => { rows.forEach(row => { - row.slice(1).forEach((r, i) => { - while (res[values[elementIndex].requestIndex].length <= off + i) { - res[values[elementIndex].requestIndex].push([]); - metadata[values[elementIndex].requestIndex].push({}); - } - res[values[elementIndex].requestIndex][off + i].push([row[0], r]); - metadata[values[elementIndex].requestIndex][off + i] = JSON.parse(JSON.stringify(metadataElem)); - if (columnNames !== undefined && columnNames.length > i) { - metadata[values[elementIndex].requestIndex][off + i].columnName = columnNames[i]; - } - }); + const elem = res[values[elementIndex].requestIndex][0].find((r: any) => r.length > 0 && r[0] === row[0]); + if (elem === undefined) { + res[values[elementIndex].requestIndex][0].push(row); + } else { + let v = (elem[1] as number); + row.slice(1).forEach(n => v += n); + elem[1] = v; + } }); - } else { - res[values[elementIndex].requestIndex].push(rows); - metadata[values[elementIndex].requestIndex].push(metadataElem); - } - }); - break; + }); + break; + case ChartsExportDeviceGroupMergingStrategy.Separate: + default: + //preserve individual rows + threeD.forEach(rows => { + if (rows.length === 0) { + return; + } + if (rows[0].length > 2) { + const off = res[values[elementIndex].requestIndex].length; + rows.forEach(row => { + row.slice(1).forEach((r, i) => { + while (res[values[elementIndex].requestIndex].length <= off + i) { + res[values[elementIndex].requestIndex].push([]); + metadata[values[elementIndex].requestIndex].push({}); + } + res[values[elementIndex].requestIndex][off + i].push([row[0], r]); + metadata[values[elementIndex].requestIndex][off + i] = JSON.parse(JSON.stringify(metadataElem)); + if (columnNames !== undefined && columnNames.length > i) { + metadata[values[elementIndex].requestIndex][off + i].columnName = columnNames[i]; + } + }); + }); + } else { + res[values[elementIndex].requestIndex].push(rows); + metadata[values[elementIndex].requestIndex].push(metadataElem); + } + }); + break; } } }); @@ -297,12 +298,12 @@ export class ChartsExportService { let mapper: number[] = []; res.forEach(r => { switch (r.source) { - case 'influx': - mapper = influxResultMapper; - break; - case 'timescale': - mapper = timescaleResultMapper; - break; + case 'influx': + mapper = influxResultMapper; + break; + case 'timescale': + mapper = timescaleResultMapper; + break; } r.res.forEach((req, index) => { table[mapper[index]] = req; @@ -326,10 +327,10 @@ export class ChartsExportService { } - getChartData(widget: WidgetModel, from?: string, to?: string, groupInterval?: string, hAxisFormat?: string, lastOverride?: string): Observable { + getChartData(widget: WidgetModel, from?: string, to?: string, groupInterval?: string, hAxisFormat?: string, lastOverride?: string, chooseColors = false): Observable { return new Observable((observer) => { this.getData(widget.properties, from, to, groupInterval, lastOverride).pipe(concatMap(r => { - let obs: Observable = of({result: [], total: 0}); + let obs: Observable = of({ result: [], total: 0 }); const deviceIds: string[] = []; r.metadata.forEach(m => { m.forEach(subM => { @@ -358,6 +359,10 @@ export class ChartsExportService { observer.next(resp as ErrorModel); } else { const tableData = this.setData(resp, widget.properties, r.metadata); + if (chooseColors) { + tableData.colors = tableData.table.data[0].slice(1).map(title => + '#' + Math.abs(hashCode(JSON.stringify(title))).toString(16).slice(0, 6)); // semi-random color that is always the same for the same title + } observer.next(this.setProcessInstancesStatusValues(widget, tableData.table, tableData.colors, hAxisFormat)); } observer.complete(); diff --git a/src/app/widgets/charts/shared/data-source-selector/data-source-selector.component.html b/src/app/widgets/charts/shared/data-source-selector/data-source-selector.component.html index 2ea34722..01708cf6 100644 --- a/src/app/widgets/charts/shared/data-source-selector/data-source-selector.component.html +++ b/src/app/widgets/charts/shared/data-source-selector/data-source-selector.component.html @@ -25,7 +25,7 @@

    Choose a Data Source

    - +

    Choose the Exported Fields

    Fields @@ -52,7 +52,7 @@

    Choose a Time Range

    + *ngIf="form.get('timeRange.type')?.value === timeRangeEnum.RelativeAhead || form.get('timeRange.type')?.value === timeRangeEnum.Relative"> Time @@ -60,7 +60,7 @@

    Choose a Time Range

    + *ngIf="form.get('timeRange.type')?.value === timeRangeEnum.RelativeAhead || form.get('timeRange.type')?.value === timeRangeEnum.Relative"> Interval @@ -72,14 +72,14 @@

    Choose a Time Range

    -
    +
    Start
    -
    +
    End diff --git a/src/app/widgets/charts/shared/data-source-selector/data-source-selector.component.ts b/src/app/widgets/charts/shared/data-source-selector/data-source-selector.component.ts index 6f2e5385..8643f897 100644 --- a/src/app/widgets/charts/shared/data-source-selector/data-source-selector.component.ts +++ b/src/app/widgets/charts/shared/data-source-selector/data-source-selector.component.ts @@ -1,5 +1,5 @@ -import {ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output} from '@angular/core'; -import { AbstractControl, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms'; +import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { AbstractControl, FormControl, FormGroup, UntypedFormGroup, ValidatorFn, Validators } from '@angular/forms'; import { catchError, concatMap, defaultIfEmpty, forkJoin, map, mergeMap, Observable, of, throwError } from 'rxjs'; import { DeviceGroupCriteriaModel, DeviceGroupModel } from 'src/app/modules/devices/device-groups/shared/device-groups.model'; import { DeviceInstanceModel } from 'src/app/modules/devices/device-instances/shared/device-instances.model'; @@ -43,7 +43,7 @@ export interface DataSourceConfig { }) export class DataSourceSelectorComponent implements OnInit { - form: any; + form: UntypedFormGroup = new UntypedFormGroup({});; deviceTypes: Map = new Map(); deviceGroups: DeviceGroupModel[] = []; aspects: DeviceTypeAspectModel[] = []; @@ -117,7 +117,7 @@ export class DataSourceSelectorComponent implements OnInit { private deviceGroupsService: DeviceGroupsService, private locationsService: LocationsService, private functionsService: FunctionsService, - ) {} + ) { } ngOnInit(): void { this.setupDataSources().pipe( @@ -125,7 +125,10 @@ export class DataSourceSelectorComponent implements OnInit { map((fieldOptions) => { this.fieldOptionsTMP = fieldOptions; this.initForm(); - this.form.get('fieldOptions').patchValue(fieldOptions); + const f = this.form.get('fieldOptions'); + if (f !== null) { + f.patchValue(fieldOptions); + } this.setupOutput(); this.setupGroupTypes(); this.form.controls['dataSourceClass'].valueChanges.subscribe((val: string) => { @@ -149,7 +152,7 @@ export class DataSourceSelectorComponent implements OnInit { initForm() { this.form = new FormGroup({ dataSourceClass: new FormControl(this.getDataSourceClassFromExports(this.dataSourceConfig?.exports) || '', Validators.required), - exports: new FormControl(this.dataSourceConfig?.exports || [], Validators.required), + exports: new FormControl(this.dataSourceConfig?.exports || [], Validators.required), timeRange: new FormGroup({ type: new FormControl(this.dataSourceConfig?.timeRange?.type || '', Validators.required), start: new FormControl(this.dataSourceConfig?.timeRange?.start || ''), @@ -166,7 +169,7 @@ export class DataSourceSelectorComponent implements OnInit { fieldOptions: new FormControl([]) }); setTimeout(() => { - this.currentDataSourceOptions = this.getDataSourceOptions(this.getDataSourceClassFromExports(this.dataSourceConfig?.exports)||''); + this.currentDataSourceOptions = this.getDataSourceOptions(this.getDataSourceClassFromExports(this.dataSourceConfig?.exports) || ''); }, 0); } @@ -208,15 +211,15 @@ export class DataSourceSelectorComponent implements OnInit { if (f === undefined || f.concept_id == null || f.concept_id === '') { return; } - let conceptSubs: Observable; + let conceptSubs: Observable; const conceptTmp = this.concepts.get(f.concept_id); - if(conceptTmp !== undefined) { + if (conceptTmp !== undefined) { conceptSubs = of(conceptTmp); } else { conceptSubs = this.conceptsService.getConceptWithCharacteristics(f.concept_id).pipe( map(c => { - if (c!==null) { + if (c !== null) { this.concepts.set(c?.id, c); } return c; @@ -251,11 +254,11 @@ export class DataSourceSelectorComponent implements OnInit { map((fieldOptions) => { const filteredFieldOptions: ChartsExportVAxesModel[] = []; fieldOptions.forEach(fieldOption => { - if(fieldOption != null) { + if (fieldOption != null) { filteredFieldOptions.push(fieldOption); } }); - if(filteredFieldOptions.length !== 0) { + if (filteredFieldOptions.length !== 0) { return { name: deviceGroup.name, fieldOptions: filteredFieldOptions @@ -316,7 +319,7 @@ export class DataSourceSelectorComponent implements OnInit { }); return forkJoin(observables).pipe(map(options => { - const res: { name: string; fieldOptions: ChartsExportVAxesModel[] } = {name: location.name, fieldOptions: []}; + const res: { name: string; fieldOptions: ChartsExportVAxesModel[] } = { name: location.name, fieldOptions: [] }; options.forEach(o => { if (o === null) { return; @@ -356,7 +359,7 @@ export class DataSourceSelectorComponent implements OnInit { service.outputs.forEach(output => { this.deviceTypeService.getValuePathsAndTypes(output.content_variable).forEach(path => { const newVAxis = { - exportName: selectedElement.name, + exportName: selectedElement.display_name || selectedElement.name, valueName: service.name + ': ' + path.path, valuePath: path.path, serviceId: service.id, @@ -372,7 +375,7 @@ export class DataSourceSelectorComponent implements OnInit { }); }); return { - name: selectedElement.name, + name: selectedElement.display_name || selectedElement.name, fieldOptions: deviceFieldOptions };; }) @@ -447,7 +450,7 @@ export class DataSourceSelectorComponent implements OnInit { } getDevices() { - return this.deviceInstancesService.getDeviceInstances({limit: 9999, offset: 0}).pipe( + return this.deviceInstancesService.getDeviceInstances({ limit: 9999, offset: 0 }).pipe( map(devices => { this.dataSourceOptions.set('Devices', devices.result); }), @@ -499,26 +502,26 @@ export class DataSourceSelectorComponent implements OnInit { } getLocations(): Observable { - return this.locationsService.getLocations({limit: 9999, offset: 0}).pipe(map(locations => this.dataSourceOptions.set('Locations', locations.result))); + return this.locationsService.getLocations({ limit: 9999, offset: 0 }).pipe(map(locations => this.dataSourceOptions.set('Locations', locations.result))); } setupDataSources(): Observable { const obs: Observable[] = []; - if(this.showExportsAsSource) { + if (this.showExportsAsSource) { obs.push(this.getExports()); } - if(this.showDevicesAsSource) { + if (this.showDevicesAsSource) { obs.push(this.getDevices()); } - if(this.showDeviceGroupsAsSource) { + if (this.showDeviceGroupsAsSource) { obs.push(this.getDeviceGroups()); } - if(this.showLocationsAsSource) { + if (this.showLocationsAsSource) { obs.push(this.getLocations()); } obs.push(this.functionsService.getFunctions('', 9999, 0, 'name', 'asc').pipe(map(functions => this.functions = functions.result))); - if(obs.length === 0) { + if (obs.length === 0) { obs.push(of(true)); } return forkJoin(obs).pipe(defaultIfEmpty(true)); // in case no datsa sources are selected @@ -529,10 +532,16 @@ export class DataSourceSelectorComponent implements OnInit { this.loadFieldOptions(selectedDataSource).pipe( map((fieldOptions) => { this.fieldOptionsTMP = fieldOptions; - this.form.get('fieldOptions').value = fieldOptions; + let f = this.form.get('fieldOptions'); + if (f !== null) { + f.setValue(fieldOptions); + } const filteredFields = this.filterSelectedFields(selectedDataSource); - this.form.get('fields').setValue(filteredFields); + f = this.form.get('fields'); + if (f !== null) { + f.setValue(filteredFields); + } return null; }) @@ -557,14 +566,14 @@ export class DataSourceSelectorComponent implements OnInit { selectedDataSources.forEach(dataSource => { const exportDataSourceExists = (field.instanceId != null && - field.instanceId === dataSource.id && - field.exportName === dataSource.name); + field.instanceId === dataSource.id && + field.exportName === dataSource.name); const deviceDataSourceExists = (field.deviceId != null && - field.deviceId === dataSource.id); + field.deviceId === dataSource.id); const deviceGroupDataSourceExists = (field.deviceGroupId != null && - field.deviceGroupId === dataSource.id); + field.deviceGroupId === dataSource.id); dataSourceIsSelected = exportDataSourceExists || deviceDataSourceExists || deviceGroupDataSourceExists; - if(dataSourceIsSelected) { + if (dataSourceIsSelected) { filteredFields.push(field); return; } @@ -594,7 +603,7 @@ export class DataSourceSelectorComponent implements OnInit { map((results: any) => { const options: Map = new Map(); results.forEach((result: any) => { - if(result != null) { + if (result != null) { options.set(result.name, result.fieldOptions); } }); @@ -676,7 +685,7 @@ export class DataSourceSelectorComponent implements OnInit { name: 'Difference-Median' } ]; - const influxExp = (this.form.get('exports').value as (ChartsExportMeasurementModel | DeviceInstanceModel | DeviceGroupModel)[]) + const influxExp = (this.form.get('exports')?.value as (ChartsExportMeasurementModel | DeviceInstanceModel | DeviceGroupModel)[]) ?.find(exp => (exp as DeviceInstanceModel).device_type_id === undefined && (((exp as ChartsExportMeasurementModel).exportDatabaseId === undefined && (exp as DeviceGroupModel).criteria === undefined) || (exp as ChartsExportMeasurementModel).exportDatabaseId === environment.exportDatabaseIdInternalInfluxDb)); if (influxExp === undefined) { @@ -693,17 +702,17 @@ export class DataSourceSelectorComponent implements OnInit { } validateInterval: ValidatorFn = (control: AbstractControl) => { - const type = this.form.control.get('group.type')?.value; + const type = this.form.get('group.type')?.value; if (type === undefined || type === null || type.length === 0) { return null; } if (control.value === undefined || control.value === null || control.value.length === 0) { - return {validateInterval: {value: control.value}}; + return { validateInterval: { value: control.value } }; } const re = new RegExp('\\d+(ns|u|µ|ms|s|months|y|m|h|d|w)'); const matches = re.exec(control.value); if (matches == null || matches.length === 0 || matches[0].length !== control.value.length) { - return {validateInterval: {value: control.value}}; + return { validateInterval: { value: control.value } }; } return null; }; @@ -714,19 +723,19 @@ export class DataSourceSelectorComponent implements OnInit { compareFields(a: any, b: any): boolean { const exportsMatch = (a.instanceId != null && b.instanceId != null && - a.instanceId === b.instanceId && - a.exportName === b.exportName && - a.valueName === b.valueName); + a.instanceId === b.instanceId && + a.exportName === b.exportName && + a.valueName === b.valueName); const deviceMatch = (a.deviceId != null && b.deviceId != null && - a.deviceId === b.deviceId && - a.serviceId === b.serviceId && - a.valuePath === b.valuePath); + a.deviceId === b.deviceId && + a.serviceId === b.serviceId && + a.valuePath === b.valuePath); const deviceGroupMatch = (a.deviceGroupId != null && b.deviceGroupId != null && - a.deviceGroupId === b.deviceGroupId && - a.criteria.function_id === b.criteria.function_id); + a.deviceGroupId === b.deviceGroupId && + a.criteria.function_id === b.criteria.function_id); const locationsMatch = (a.locationId != null && b.locationId != null && - a.locationId === b.locationId && - a.criteria.function_id === b.criteria.function_id); + a.locationId === b.locationId && + a.criteria.function_id === b.criteria.function_id); return exportsMatch || deviceMatch || deviceGroupMatch || locationsMatch; } @@ -738,7 +747,7 @@ export class DataSourceSelectorComponent implements OnInit { let classes: string[] = []; Array.from(this.dataSourceOptions.keys()).forEach(cl => { const exports: any = this.dataSourceOptions.get(cl as string); - if (exports.length>0) { + if (exports.length > 0) { classes.push(cl as string); } }); @@ -756,7 +765,7 @@ export class DataSourceSelectorComponent implements OnInit { } updateCurrentDataSourceOptions(dataSourceClass: string) { - if (dataSourceClass!== this.currentDataSourceClass) { + if (dataSourceClass !== this.currentDataSourceClass) { this.form.controls['exports'].reset([]); } this.currentDataSourceClass = dataSourceClass; @@ -764,7 +773,7 @@ export class DataSourceSelectorComponent implements OnInit { this.cdref.detectChanges(); } - getLabelFromCurrentDataSource(){ + getLabelFromCurrentDataSource() { const val = this.form.controls['dataSourceClass'].value; const dataSourceSingular: Map = new Map([ ['Device Groups', 'Device Group'], @@ -775,8 +784,8 @@ export class DataSourceSelectorComponent implements OnInit { return dataSourceSingular.get(val) as string; } - getDataSourceClassFromExports(exports: (ChartsExportMeasurementModel | DeviceInstanceModel | DeviceGroupModel)[] | undefined){ - if (!exports){ + getDataSourceClassFromExports(exports: (ChartsExportMeasurementModel | DeviceInstanceModel | DeviceGroupModel)[] | undefined) { + if (!exports) { return null; } const searchItem = exports[0]; @@ -788,7 +797,7 @@ export class DataSourceSelectorComponent implements OnInit { return null; } - getExportIDs(exportValues: (ChartsExportMeasurementModel | DeviceInstanceModel | DeviceGroupModel) []){ + getExportIDs(exportValues: (ChartsExportMeasurementModel | DeviceInstanceModel | DeviceGroupModel)[]) { const ids: string[] = []; exportValues.forEach(item => { if ('id' in item) { @@ -797,4 +806,8 @@ export class DataSourceSelectorComponent implements OnInit { }); return ids; } + + patchFields(fields: ChartsExportVAxesModel[]) { + this.form.patchValue({ fields }); + } } diff --git a/src/app/widgets/widget.module.ts b/src/app/widgets/widget.module.ts index dde58716..7a0eaafd 100644 --- a/src/app/widgets/widget.module.ts +++ b/src/app/widgets/widget.module.ts @@ -125,6 +125,9 @@ import { AnomalyReconstructionComponent } from './anomaly/reconstruction/reconst import { LineComponent } from './anomaly/sub-widgets/line/line.component'; import { BadVentilationComponent } from './bad-ventilation/bad-ventilation.component'; import { EditVentilationWidgetComponent } from './bad-ventilation/dialog/edit/edit.component'; +import { MatTreeModule } from '@angular/material/tree'; +import { AxisConfigComponent } from './charts/export/dialog/axis-config/axis-config.component'; +import { DragDropModule } from '@angular/cdk/drag-drop'; registerLocaleData(localeDe, 'de'); @@ -162,7 +165,9 @@ registerLocaleData(localeDe, 'de'); MatSortModule, MatSliderModule, MatDatepickerModule, - NgApexchartsModule + NgApexchartsModule, + MatTreeModule, + DragDropModule, ], declarations: [ RangeSliderComponent, @@ -241,7 +246,8 @@ registerLocaleData(localeDe, 'de'); AnomalyReconstructionComponent, LineComponent, BadVentilationComponent, - EditVentilationWidgetComponent + EditVentilationWidgetComponent, + AxisConfigComponent, ], exports: [ SwitchComponent,