diff --git a/src/app/widgets/single-value/dialog/add-threshold/add-threshold.component.css b/src/app/widgets/single-value/dialog/add-threshold/add-threshold.component.css new file mode 100644 index 00000000..e69de29b diff --git a/src/app/widgets/single-value/dialog/add-threshold/add-threshold.component.html b/src/app/widgets/single-value/dialog/add-threshold/add-threshold.component.html new file mode 100644 index 00000000..32dc2809 --- /dev/null +++ b/src/app/widgets/single-value/dialog/add-threshold/add-threshold.component.html @@ -0,0 +1,27 @@ + +
+ + Threshold + + + + + Direction + + <= + >= + + + + + Color + + +
+
+ + + + + + \ No newline at end of file diff --git a/src/app/widgets/single-value/dialog/add-threshold/add-threshold.component.spec.ts b/src/app/widgets/single-value/dialog/add-threshold/add-threshold.component.spec.ts new file mode 100644 index 00000000..ecd4f13d --- /dev/null +++ b/src/app/widgets/single-value/dialog/add-threshold/add-threshold.component.spec.ts @@ -0,0 +1,28 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; + +import { AddThresholdComponent } from './add-threshold.component'; + +describe('AddThresholdComponent', () => { + let component: AddThresholdComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ AddThresholdComponent ], + providers: [ + {provide: MatDialogRef, useValue: {}}, + {provide: MAT_DIALOG_DATA, useValue: []}, + ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(AddThresholdComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/widgets/single-value/dialog/add-threshold/add-threshold.component.ts b/src/app/widgets/single-value/dialog/add-threshold/add-threshold.component.ts new file mode 100644 index 00000000..44678945 --- /dev/null +++ b/src/app/widgets/single-value/dialog/add-threshold/add-threshold.component.ts @@ -0,0 +1,38 @@ +import { Component, Inject } from '@angular/core'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { ValueHighlightConfig } from '../../shared/single-value.model'; + +@Component({ + selector: 'single-value-add-threshold', + templateUrl: './add-threshold.component.html', + styleUrls: ['./add-threshold.component.css'] +}) +export class AddThresholdComponent { + form = new FormGroup({ + threshold: new FormControl(null, {validators: Validators.required}), + color: new FormControl('', {nonNullable: true, validators: Validators.required}), + direction: new FormControl('', {nonNullable: true, validators: Validators.required}), + }); + submitButtonText = 'Add'; + + constructor( + private dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public config?: ValueHighlightConfig, + ) { + if(config != null) { + this.form.controls.threshold.patchValue(config.threshold); + this.form.controls.color.patchValue(config.color); + this.form.controls.direction.patchValue(config.direction); + this.submitButtonText = 'Update'; + } + } + + cancel() { + this.dialogRef.close(); + } + + add() { + this.dialogRef.close(this.form.value); + } +} diff --git a/src/app/widgets/single-value/dialog/single-value-edit-dialog.component.css b/src/app/widgets/single-value/dialog/single-value-edit-dialog.component.css index 3e2d4f79..92576d61 100644 --- a/src/app/widgets/single-value/dialog/single-value-edit-dialog.component.css +++ b/src/app/widgets/single-value/dialog/single-value-edit-dialog.component.css @@ -14,10 +14,22 @@ * limitations under the License. */ -table { - width: 100%; -} mat-radio-group mat-radio-button:not(:first-child) { padding-left: 8px; } + +mat-form-field { + width: 100%; +} + +.one-line-form-container { + display: flex; + flex-direction: row; + align-items: stretch; +} + +.one-line-form-container > div { + flex: 1; + margin-right: 10px +} \ No newline at end of file diff --git a/src/app/widgets/single-value/dialog/single-value-edit-dialog.component.html b/src/app/widgets/single-value/dialog/single-value-edit-dialog.component.html index 979a142e..ed16c908 100644 --- a/src/app/widgets/single-value/dialog/single-value-edit-dialog.component.html +++ b/src/app/widgets/single-value/dialog/single-value-edit-dialog.component.html @@ -17,7 +17,7 @@

Edit Single Value

-
+
Name @@ -29,13 +29,13 @@

Edit Single Value

Device Device Group - + - - +
+
-
- + No devices found. - - + + +
-
+ No exports found. - - + + +
-
- + No device groups found. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + +
+
+ Show timestamp? +
+
+ +
+
+ Highlight timestamp by age? + +
+
+ +
+
+ + Maximum age for warning + + +
+
+ + + + Seconds + Minutes + Hours + Days + Months + + +
+
+ +
+
+ + Maximum age for problem + + +
+
+ + + + Seconds + Minutes + Hours + Days + Months + + +
+
+ +
+
+ Highlight value by threshold? +
+
-
+
Choose Device Edit Single Value -
+ +
Choose Service -
+
Choose Export Edit Single Value -
+
Choose Device Group - -
+ + +
Choose Criteria -
+ + + + + +
+
-
+ +
-
- + +
+ Unit - + Unit -
+ +
Aggregation -
- - - String - Number - Date - Currency - Percent - - - - - Format - - -
- - - none - - {{t}} - - - - - - Group time - - -
+ +
+
+ + + String + Number + Date + Currency + Percent + + +
+
+ + Format + + +
+
+
+
+ + + none + + {{t}} + + + +
+
+ + Group time + + +
+
+
Math -
+ +
Max. font size (px) -
- Show timestamp -
- Highlight timestamp by age -
- - Maximum age for warning - - - - - - - Seconds - Minutes - Hours - Days - Months - - -
- - Maximum age for problem - - - - - - - Seconds - Minutes - Hours - Days - Months - - -
+ +
+
diff --git a/src/app/widgets/single-value/dialog/single-value-edit-dialog.component.ts b/src/app/widgets/single-value/dialog/single-value-edit-dialog.component.ts index 8a7ebfd9..bd2b6c15 100644 --- a/src/app/widgets/single-value/dialog/single-value-edit-dialog.component.ts +++ b/src/app/widgets/single-value/dialog/single-value-edit-dialog.component.ts @@ -15,7 +15,7 @@ */ import { Component, Inject, OnInit } from '@angular/core'; -import { FormControl, UntypedFormBuilder } from '@angular/forms'; +import { Form, FormControl, FormGroup, UntypedFormBuilder } from '@angular/forms'; import { WidgetModel } from '../../../modules/dashboard/shared/dashboard-widget.model'; import { ChartsExportMeasurementModel } from '../../charts/export/shared/charts-export-properties.model'; import { DeploymentsService } from '../../../modules/processes/deployments/shared/deployments.service'; @@ -32,7 +32,7 @@ import { DeviceTypeCharacteristicsModel, DeviceTypeFunctionModel, DeviceTypeServ import { DeviceTypeService } from '../../../modules/metadata/device-types-overview/shared/device-type.service'; import { DeviceInstancesService } from '../../../modules/devices/device-instances/shared/device-instances.service'; import { ChartsExportRequestPayloadGroupModel } from '../../charts/export/shared/charts-export-request-payload.model'; -import { forkJoin, map, mergeMap, Observable, of } from 'rxjs'; +import { concatMap, forkJoin, map, mergeMap, Observable, of } from 'rxjs'; import { DeviceGroupsService } from 'src/app/modules/devices/device-groups/shared/device-groups.service'; import { DeviceGroupsPermSearchModel } from 'src/app/modules/devices/device-groups/shared/device-groups-perm-search.model'; import { DeviceGroupCriteriaModel } from 'src/app/modules/devices/device-groups/shared/device-groups.model'; @@ -40,20 +40,22 @@ import { AspectsPermSearchModel } from 'src/app/modules/metadata/aspects/shared/ import { DeviceClassesPermSearchModel } from 'src/app/modules/metadata/device-classes/shared/device-classes-perm-search.model'; import { ConceptsCharacteristicsModel } from 'src/app/modules/metadata/concepts/shared/concepts-characteristics.model'; import { ConceptsService } from 'src/app/modules/metadata/concepts/shared/concepts.service'; -import { SingleValueAggregations } from '../shared/single-value.model'; +import { SingleValueAggregations, ValueHighlightConfig } from '../shared/single-value.model'; @Component({ templateUrl: './single-value-edit-dialog.component.html', styleUrls: ['./single-value-edit-dialog.component.css'], }) export class SingleValueEditDialogComponent implements OnInit { + formIsReady = false; + dataSourceFieldsReady = true; exports: ChartsExportMeasurementModel[] = []; dashboardId: string; widgetId: string; widget: WidgetModel = {} as WidgetModel; vAxisValues: ExportValueModel[] = []; disableSave = false; - waitingForDataSourceChange = true; + dataSourceSelectionReady = false; groupTypes = [ 'mean', 'sum', @@ -83,35 +85,7 @@ export class SingleValueEditDialogComponent implements OnInit { deviceClasses: DeviceClassesPermSearchModel[] = []; concept?: ConceptsCharacteristicsModel | null; - form = this.fb.group({ - vAxis: {}, - vAxisLabel: '', - name: '', - type: '', - format: '', - threshold: 128, - math: '', - measurement: '', - group: this.fb.group({ - time: '', - type: '', - }), - sourceType: '', - device: {}, - service: {}, - deviceGroupId: '', - deviceGroupCriteria: {}, - deviceGroupAggregation: 'latest', - targetCharacteristic: '', - timestampConfig: this.fb.group({ - showTimestamp: new FormControl(false), - highlightTimestamp: new FormControl(false), - warningTimeLevel: new FormControl('d'), - warningAge: new FormControl(1), - problemTimeLevel: new FormControl('d'), - problemAge: new FormControl(7), - }) - }); + form: FormGroup = new FormGroup({}); userHasUpdateNameAuthorization = false; userHasUpdatePropertiesAuthorization = false; @@ -140,16 +114,59 @@ export class SingleValueEditDialogComponent implements OnInit { } ngOnInit() { - this.getWidgetData().subscribe({ - next: (_: any) => { - this.loadDataSourceOptions(this.widget.properties.sourceType || 'export'); + this.initForm(); + this.getWidgetData().pipe( + concatMap((_: any) => this.loadDataSourceOptions(this.widget.properties.sourceType || 'export')), + concatMap(() => this.setAllOptions()), + map(() => { this.listenForFormChanges(); + }) + ).subscribe({ + next: (_) => {}, + error: (err) => { + console.log(err); } }); } + initForm() { + this.form = this.fb.group({ + vAxis: {}, + vAxisLabel: '', + name: '', + type: '', + format: '', + threshold: 128, + math: '', + measurement: '', + group: this.fb.group({ + time: '', + type: '', + }), + sourceType: '', + device: {}, + service: {}, + deviceGroupId: '', + deviceGroupCriteria: {}, + deviceGroupAggregation: 'latest', + targetCharacteristic: '', + timestampConfig: this.fb.group({ + showTimestamp: new FormControl(false), + highlightTimestamp: new FormControl(false), + warningTimeLevel: new FormControl('d'), + warningAge: new FormControl(1), + problemTimeLevel: new FormControl('d'), + problemAge: new FormControl(7), + }), + valueHighlightConfig: this.fb.group({ + highlight: new FormControl(false), + thresholds: new FormControl([]) + }) + }); + } + loadDataSourceOptions(sourceType: string) { - this.waitingForDataSourceChange = true; + this.dataSourceSelectionReady = false; let obs: Observable = of(); if(sourceType === 'export') { obs = this.initDeployments(); @@ -159,25 +176,95 @@ export class SingleValueEditDialogComponent implements OnInit { obs = this.initDeviceGroups(); } - obs.subscribe({ - next: (_) => { - this.waitingForDataSourceChange = false; - }, - error: (err) => { - this.waitingForDataSourceChange = false; - console.log(err); + return obs.pipe( + map(() => this.dataSourceSelectionReady = true) + ); + } + + listenForDeviceSelectionChange() { + this.form.get('device')?.valueChanges.pipe( + concatMap((device: DeviceInstancesPermSearchModel) => { + this.dataSourceFieldsReady = false; + this.paths = []; + this.services = []; + this.form.get('service')?.patchValue(''); + this.form.get('vAxis')?.patchValue(''); + + if (device === undefined || device == null) { + return of(null); + } + return this.loadDeviceServices(device); + }), + map((_) => { + this.dataSourceFieldsReady = true; + }) + ).subscribe(); + } + + loadDeviceServices(device: DeviceInstancesPermSearchModel) { + return this.deviceTypeService.getDeviceType(device.device_type_id).pipe( + map((dt) => { + this.services = dt?.services.filter(service => service.outputs.length === 1) || []; + }) + ); + } + + listenForServiceSelectionChange() { + this.form.get('service')?.valueChanges.subscribe((service: DeviceTypeServiceModel) => { + this.paths = []; + this.form.get('vAxis')?.patchValue(''); + if (service === undefined || service == null) { + return; } + this.loadDeviceServiceValues(service); }); } - listenForFormChanges() { - this.form.get('sourceType')?.valueChanges.subscribe(sourceType => { - this.loadDataSourceOptions(sourceType); + loadDeviceServiceValues(service: DeviceTypeServiceModel) { + this.paths = this.deviceTypeService.getValuePaths(service.outputs[0].content_variable).map(x => ({ Name: x })); + } + + listenForDeviceGroupCriteriaSelectionChange() { + this.form.get('deviceGroupCriteria')?.valueChanges.subscribe(criteria => { + this.dataSourceFieldsReady = false; + const conceptId = this.functions.find(f => f.id === criteria.function_id)?.concept_id; + if (conceptId !== undefined) { + this.loadAndSetConcept(conceptId).subscribe(() => { + this.dataSourceFieldsReady = true; + }); + } }); + } + loadAndSetConcept(conceptID: string) { + return this.conceptsService.getConceptWithCharacteristics(conceptID).pipe( + map((c) => { + this.concept = c; + }) + ); + } + + listenForExportSelection() { this.form.get('measurement')?.valueChanges.subscribe(exp => { this.vAxisValues = exp?.values; }); + } + + listenForDeviceGroupSelectionChange() { + this.form.get('deviceGroupId')?.valueChanges.subscribe(_ => { + this.form.get('vAxisLabel')?.patchValue(''); + }); + } + + listenForDataSourceTypeChange() { + this.form.get('sourceType')?.valueChanges.subscribe(sourceType => { + this.loadDataSourceOptions(sourceType).subscribe(); + }); + } + + listenForFormChanges() { + this.listenForDataSourceTypeChange(); + this.form.get('type')?.valueChanges.subscribe(v => { if (v === 'String') { this.form.patchValue({ format: '' }); @@ -187,37 +274,12 @@ export class SingleValueEditDialogComponent implements OnInit { } }); - this.form.get('device')?.valueChanges.subscribe((device: DeviceInstancesPermSearchModel) => { - this.paths = []; - this.services = []; - if (device === undefined || device == null) { - return; - } - this.deviceTypeService.getDeviceType(device.device_type_id).subscribe(dt => { - this.services = dt?.services.filter(service => service.outputs.length === 1) || []; - }); - }); + this.listenForExportSelection(); + this.listenForDeviceSelectionChange(); + this.listenForServiceSelectionChange(); + this.listenForDeviceGroupCriteriaSelectionChange(); + this.listenForDeviceGroupSelectionChange(); - this.form.get('service')?.valueChanges.subscribe((service: DeviceTypeServiceModel) => { - this.paths = []; - if (service === undefined || service == null) { - return; - } - this.paths = this.deviceTypeService.getValuePaths(service.outputs[0].content_variable).map(x => ({ Name: x })); - }); - this.form.get('deviceGroupCriteria')?.valueChanges.subscribe(criteria => { - const update = function(that: SingleValueEditDialogComponent) { - if (that.functions.length === 0) { //delay until functions populated - setTimeout(() => update(that), 100); - return; - } - const conceptId = that.functions.find(f => f.id === criteria.function_id)?.concept_id; - if (conceptId !== undefined) { - that.conceptsService.getConceptWithCharacteristics(conceptId).subscribe(c => that.concept = c); - } - }; - update(this); - }); this.form.get('vAxisLabel')?.valueChanges.subscribe(unit => { this.form.patchValue({targetCharacteristic: this.concept?.characteristics.find(c => this.getDisplay(c) === unit)?.id}); }); @@ -227,7 +289,6 @@ export class SingleValueEditDialogComponent implements OnInit { return this.dashboardService.getWidget(this.dashboardId, this.widgetId).pipe( map((widget: WidgetModel) => { this.widget = widget; - console.log(widget) this.form.patchValue({ vAxis: widget.properties.vAxis, vAxisLabel: widget.properties.vAxisLabel, @@ -262,10 +323,57 @@ export class SingleValueEditDialogComponent implements OnInit { type: widget.properties.group?.type, }); + this.form.get('valueHighlightConfig')?.patchValue(widget.properties.valueHighlightConfig); + return true; + }), + map((_) => { + this.formIsReady = true; })); } + setAllOptions() { + // when selections were made, the matching options must be loaded so that the select works + this.setExportValueOptions(this.widget.properties.measurement); + + const selectedService = this.widget.properties.service; + if(selectedService != null) { + this.loadDeviceServiceValues(selectedService); + } + + const obs: Observable[] = [ + this.setDeviceServiceOptions(this.widget.properties.device), + this.setDeviceGroupUnitOptions(this.widget.properties.deviceGroupCriteria), + ]; + return forkJoin(obs); + } + + setExportValueOptions(dataExport?: ChartsExportMeasurementModel) { + // set export field options + this.vAxisValues = dataExport?.values || []; + } + + setDeviceServiceOptions(device?: DeviceInstancesPermSearchModel) { + // load and set device service options + if(device == null) { + return of(null); + } + + return this.loadDeviceServices(device); + } + + setDeviceGroupUnitOptions(criteria?: DeviceGroupCriteriaModel) { + // load and set device group unit options + if(criteria == null) { + return of(null); + } + const conceptId = this.functions.find(f => f.id === criteria.function_id)?.concept_id; + if(conceptId == null) { + return of(null); + } + return this.loadAndSetConcept(conceptId); + } + initDeployments() { this.exports = []; return this.exportService.getExports(true, '', 9999, 0, 'name', 'asc', undefined, undefined).pipe( @@ -333,7 +441,6 @@ export class SingleValueEditDialogComponent implements OnInit { } updateProperties(): Observable { - const measurement = this.form.get('measurement')?.value as ChartsExportMeasurementModel || undefined; this.widget.properties.measurement = { id: this.form.get('measurement')?.value?.id, name: this.form.get('measurement')?.value?.name, @@ -347,7 +454,7 @@ export class SingleValueEditDialogComponent implements OnInit { this.widget.properties.threshold = this.form.get('threshold')?.value || undefined; this.widget.properties.math = this.form.get('math')?.value || undefined; this.widget.properties.group = this.form.get('group')?.value as ChartsExportRequestPayloadGroupModel || undefined; - this.widget.properties.device = this.form.get('device')?.value as DeviceInstancesModel || undefined; + this.widget.properties.device = this.form.get('device')?.value as DeviceInstancesPermSearchModel || undefined; this.widget.properties.service = this.form.get('service')?.value as DeviceTypeServiceModel || undefined; this.widget.properties.sourceType = this.form.get('sourceType')?.value || undefined; this.widget.properties.deviceGroupId = this.form.get('deviceGroupId')?.value || undefined; @@ -355,7 +462,7 @@ export class SingleValueEditDialogComponent implements OnInit { this.widget.properties.targetCharacteristic = this.form.get('targetCharacteristic')?.value || undefined; this.widget.properties.deviceGroupAggregation = this.form.get('deviceGroupAggregation')?.value || undefined; this.widget.properties.timestampConfig = this.form.get('timestampConfig')?.value || undefined; - + this.widget.properties.valueHighlightConfig = this.form.get('valueHighlightConfig')?.value || undefined; return this.dashboardService.updateWidgetProperty(this.dashboardId, this.widget.id, [], this.widget.properties); } @@ -425,4 +532,11 @@ export class SingleValueEditDialogComponent implements OnInit { getDisplay(c: DeviceTypeCharacteristicsModel): string { return c.display_unit || c.name; } + + valueHighlightConfigUpdated(thresholdConfigs: ValueHighlightConfig[]) { + this.form.get('valueHighlightConfig')?.patchValue({ + highlight: true, + thresholds: thresholdConfigs + }); + } } diff --git a/src/app/widgets/single-value/dialog/threshold/threshold.component.css b/src/app/widgets/single-value/dialog/threshold/threshold.component.css new file mode 100644 index 00000000..e69de29b diff --git a/src/app/widgets/single-value/dialog/threshold/threshold.component.html b/src/app/widgets/single-value/dialog/threshold/threshold.component.html new file mode 100644 index 00000000..24f780bf --- /dev/null +++ b/src/app/widgets/single-value/dialog/threshold/threshold.component.html @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Threshold + {{threshold.threshold}} + Direction + {{threshold.direction}} + Color + {{threshold.color}} + Delete + + Edit + +
+ + \ No newline at end of file diff --git a/src/app/widgets/single-value/dialog/threshold/threshold.component.spec.ts b/src/app/widgets/single-value/dialog/threshold/threshold.component.spec.ts new file mode 100644 index 00000000..0b96649c --- /dev/null +++ b/src/app/widgets/single-value/dialog/threshold/threshold.component.spec.ts @@ -0,0 +1,31 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialogModule } from '@angular/material/dialog'; +import { MatSnackBarModule } from '@angular/material/snack-bar'; + +import { ThresholdComponent } from './threshold.component'; + +describe('ThresholdComponent', () => { + let component: ThresholdComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ThresholdComponent ], + imports: [ + HttpClientTestingModule, + MatSnackBarModule, + MatDialogModule + ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ThresholdComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/widgets/single-value/dialog/threshold/threshold.component.ts b/src/app/widgets/single-value/dialog/threshold/threshold.component.ts new file mode 100644 index 00000000..b275bece --- /dev/null +++ b/src/app/widgets/single-value/dialog/threshold/threshold.component.ts @@ -0,0 +1,65 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { MatTableDataSource } from '@angular/material/table'; +import { ValueHighlightConfig } from '../../shared/single-value.model'; +import { AddThresholdComponent } from '../add-threshold/add-threshold.component'; + +@Component({ + selector: 'single-value-threshold', + templateUrl: './threshold.component.html', + styleUrls: ['./threshold.component.css'] +}) +export class ThresholdComponent implements OnInit { + displayedColumns = ['threshold', 'direction', 'color', 'edit', 'delete']; + dataSource = new MatTableDataSource(); + @Output() threshholdConfigUpdated = new EventEmitter(); + @Input() oldConfigs: ValueHighlightConfig[] = []; + + constructor( + public dialog: MatDialog + ) {} + + ngOnInit(): void { + this.dataSource = new MatTableDataSource(this.oldConfigs); + } + + edit(index: number) { + const oldConfig = this.dataSource.data[index]; + this.dialog.open(AddThresholdComponent, {data: oldConfig}).afterClosed().subscribe({ + next: (rule: ValueHighlightConfig) => { + if(rule != null) { + this.dataSource.data.splice(index, 1); + this.dataSource.data.push(rule); + this.dataSource.data = this.dataSource.data; + this.emitCurrentConfig(); + } + }, + error: (_) => { + } + }); + } + + delete(index: number) { + this.dataSource.data.splice(index, 1); + this.dataSource.data = this.dataSource.data; + this.emitCurrentConfig(); + } + + add() { + this.dialog.open(AddThresholdComponent).afterClosed().subscribe({ + next: (rule: ValueHighlightConfig) => { + if(rule != null) { + this.dataSource.data.push(rule); + this.dataSource.data = this.dataSource.data; + this.emitCurrentConfig(); + } + }, + error: (_) => { + } + }); + } + + emitCurrentConfig() { + this.threshholdConfigUpdated.emit(this.dataSource.data); + } +} diff --git a/src/app/widgets/single-value/shared/single-value.model.ts b/src/app/widgets/single-value/shared/single-value.model.ts index b7bf5289..828aa138 100644 --- a/src/app/widgets/single-value/shared/single-value.model.ts +++ b/src/app/widgets/single-value/shared/single-value.model.ts @@ -15,7 +15,7 @@ */ import { DeviceGroupCriteriaModel } from 'src/app/modules/devices/device-groups/shared/device-groups.model'; -import {DeviceInstancesModel} from '../../../modules/devices/device-instances/shared/device-instances.model'; +import {DeviceInstancesModel, DeviceInstancesPermSearchModel} from '../../../modules/devices/device-instances/shared/device-instances.model'; import {DeviceTypeServiceModel} from '../../../modules/metadata/device-types-overview/shared/device-type.model'; export interface SingleValueModel { @@ -33,18 +33,28 @@ interface TimestampConfig { problemTimeLevel: string; } +export interface ValueHighlightConfig { + threshold: number; + color: string; + direction: string; +} + export interface SingleValuePropertiesModel { type?: string; format?: string; math?: string; sourceType?: string; - device?: DeviceInstancesModel; + device?: DeviceInstancesPermSearchModel; service?: DeviceTypeServiceModel; deviceGroupId?: string; deviceGroupCriteria?: DeviceGroupCriteriaModel; targetCharacteristic?: string; deviceGroupAggregation?: SingleValueAggregations; timestampConfig?: TimestampConfig; + valueHighlightConfig?: { + highlight: boolean; + thresholds: ValueHighlightConfig[]; + }; } export enum SingleValueAggregations { diff --git a/src/app/widgets/single-value/single-value.component.html b/src/app/widgets/single-value/single-value.component.html index d3486445..e8b79ff2 100644 --- a/src/app/widgets/single-value/single-value.component.html +++ b/src/app/widgets/single-value/single-value.component.html @@ -29,13 +29,13 @@ [ngClass]="{'side-value': i !== svListIndex}" [class]="timestampAgeClass" fxLayoutAlign="center center" (wheel)="wheel($event)"> - + {{sv.date | date:'medium'}}
- +
{{this.sv?.date | date:"short" }}
diff --git a/src/app/widgets/single-value/single-value.component.ts b/src/app/widgets/single-value/single-value.component.ts index 9a666858..3cbb36da 100644 --- a/src/app/widgets/single-value/single-value.component.ts +++ b/src/app/widgets/single-value/single-value.component.ts @@ -116,6 +116,7 @@ export class SingleValueComponent implements OnInit, OnDestroy { marginLeft = '0'; private _svListIndex = 0; animationState = false; + highlightColor = 'black'; @Input() dashboardId = ''; @Input() widget: WidgetModel = {} as WidgetModel; @@ -193,6 +194,7 @@ export class SingleValueComponent implements OnInit, OnDestroy { map((_) => { this.showTimestamp = this.widget.properties.timestampConfig?.showTimestamp == true; this.setTimestampColor(); + this.setHighlightColor(); }) ).subscribe({ next: (_) => { @@ -252,6 +254,20 @@ export class SingleValueComponent implements OnInit, OnDestroy { this.configured = this.widget.properties.measurement !== undefined; } + private setHighlightColor() { + const config = this.widget.properties.valueHighlightConfig; + const currentValue = this.sv?.value; + if(config && currentValue && config.highlight) { + config.thresholds.forEach(thresholdConfig => { + const threshold = thresholdConfig.threshold; + const direction = thresholdConfig.direction; + const thresholdReached = (direction === '<=' && currentValue <= threshold) || (direction === '>=' && currentValue >= threshold); + if(thresholdReached) { + this.highlightColor = thresholdConfig.color; + } + }); + } + } private setTimestampColor() { if(!this.widget.properties.timestampConfig?.highlightTimestamp) { diff --git a/src/app/widgets/single-value/value/value.component.html b/src/app/widgets/single-value/value/value.component.html index d905e96a..9b30090d 100644 --- a/src/app/widgets/single-value/value/value.component.html +++ b/src/app/widgets/single-value/value/value.component.html @@ -16,21 +16,21 @@ + [maxFontSize]="maxFontSize()" [ngStyle]="{'color': color}"> + [maxFontSize]="maxFontSize()" [ngStyle]="{'color': color}"> + [maxFontSize]="maxFontSize()" [ngStyle]="{'color': color}"> + [maxFontSize]="maxFontSize()" [ngStyle]="{'color': color}"> + [maxFontSize]="maxFontSize()" [ngStyle]="{'color': color}"> diff --git a/src/app/widgets/single-value/value/value.component.ts b/src/app/widgets/single-value/value/value.component.ts index 4785f374..29c4dbcc 100644 --- a/src/app/widgets/single-value/value/value.component.ts +++ b/src/app/widgets/single-value/value/value.component.ts @@ -27,6 +27,7 @@ import {SingleValueModel} from '../shared/single-value.model'; export class ValueComponent { @Input() widget: WidgetModel | undefined; @Input() sv: SingleValueModel | undefined; + @Input() color?: string; maxFontSize() { return this.widget?.properties?.threshold ? this.widget.properties.threshold : 128; diff --git a/src/app/widgets/widget.module.ts b/src/app/widgets/widget.module.ts index 0d21fb05..532b77cd 100644 --- a/src/app/widgets/widget.module.ts +++ b/src/app/widgets/widget.module.ts @@ -108,6 +108,8 @@ import { OpenWindowComponent } from './charts/open-window/open-window.component' import { TimelineComponent } from './charts/shared/chart-types/timeline/timeline.component'; import { DataSourceSelectorComponent } from './charts/shared/data-source-selector/data-source-selector.component'; import { OpenWindowEditComponent } from './charts/open-window/dialog/edit/edit.component'; +import { ThresholdComponent } from './single-value/dialog/threshold/threshold.component'; +import { AddThresholdComponent } from './single-value/dialog/add-threshold/add-threshold.component'; registerLocaleData(localeDe, 'de'); @@ -207,7 +209,9 @@ registerLocaleData(localeDe, 'de'); OpenWindowComponent, TimelineComponent, DataSourceSelectorComponent, - OpenWindowEditComponent + OpenWindowEditComponent, + ThresholdComponent, + AddThresholdComponent ], exports: [ SwitchComponent,