diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/liquid-level-card-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/liquid-level-card-basic-config.component.html index 9b6274586b..cfc0807718 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/liquid-level-card-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/liquid-level-card-basic-config.component.html @@ -45,7 +45,7 @@ -
+
{{ 'widgets.liquid-level-card.icon' | translate }} @@ -89,9 +89,8 @@
widgets.liquid-level-card.shape-attribute-name
@@ -124,9 +123,8 @@
widgets.liquid-level-card.datasource-units
-
@@ -144,14 +142,12 @@ @@ -170,7 +166,7 @@ + min="0.1" step="1" placeholder="{{ 'widget-config.set' | translate }}">
@@ -253,8 +247,8 @@ {{ 'widgets.liquid-level-card.level' | translate }}
- diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/liquid-level-card-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/liquid-level-card-basic-config.component.ts index de11ad1067..4144d7db0b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/liquid-level-card-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/liquid-level-card-basic-config.component.ts @@ -86,13 +86,11 @@ export class LiquidLevelCardBasicConfigComponent extends BasicWidgetConfigCompon public get volumeInput(): boolean { const datasourceUnits = this.levelCardWidgetConfigForm.get('datasourceUnits').value; const layout: LevelCardLayout = this.levelCardWidgetConfigForm.get('layout').value; - const units = this.levelCardWidgetConfigForm.get('units').value; if (layout === LevelCardLayout.absolute) { return true; } - - return !(datasourceUnits === CapacityUnits.percent && units === CapacityUnits.percent); + return datasourceUnits !== CapacityUnits.percent; } public get displayTimewindowConfig(): boolean { @@ -194,7 +192,7 @@ export class LiquidLevelCardBasicConfigComponent extends BasicWidgetConfigCompon iconColor: [configData.config.iconColor, []], volumeSource: [settings.volumeSource, []], - volumeConstant: [settings.volumeConstant, [Validators.required]], + volumeConstant: [settings.volumeConstant, [Validators.required, Validators.min(0.1)]], volumeAttributeName: [settings.volumeAttributeName, [Validators.required]], volumeUnits: [settings.volumeUnits, [Validators.required]], volumeFont: [settings.volumeFont, []], @@ -295,12 +293,8 @@ export class LiquidLevelCardBasicConfigComponent extends BasicWidgetConfigCompon protected validatorTriggers(): string[] { return [ - 'showTooltip', 'showTooltipLevel', - 'tankSelectionType', 'datasourceUnits', - 'showTitleIcon', 'volumeSource', - 'showTooltipDate', 'units', - 'layout', 'showTitle', - 'widgetUnitsSource' + 'showTooltip', 'showTooltipLevel', 'tankSelectionType', 'datasourceUnits', 'showTitleIcon', 'volumeSource', + 'showTooltipDate', 'layout', 'showTitle', 'widgetUnitsSource' ]; } @@ -399,10 +393,6 @@ export class LiquidLevelCardBasicConfigComponent extends BasicWidgetConfigCompon return createShapeLayout(svg, layout, this.sanitizer); } - public isRequired(formControlName: string): boolean { - return this.levelCardWidgetConfigForm.get(formControlName)?.hasValidator(Validators.required); - } - public fetchOptions(searchText: string): Observable> { if (this.keySearchText !== searchText || !this.lastFetchedKeys) { this.keySearchText = searchText; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.html index 98ceff33e1..4c14686b9c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.html @@ -19,4 +19,11 @@
+
+
+
+ {{ error }} +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.scss index a33cf99587..046d1a7d3e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.scss @@ -43,4 +43,30 @@ display: flex; justify-content: center; } + .tb-liquid-level-error { + position: absolute; + display: flex; + inset: 0; + width: 100%; + height: 100%; + align-items: center; + justify-content: center; + + &-container { + border-radius: 4px; + margin: 16px; + padding: 8px 12px; + background: #848484; + display: flex; + flex-direction: column; + gap: 8px; + + & > * { + font-weight: 500; + letter-spacing: .25px; + text-align: center; + color: #fff; + } + } + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.ts index edd471bcb8..9bb7053c3e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.ts @@ -25,7 +25,7 @@ import { ViewEncapsulation } from '@angular/core'; import { WidgetContext } from '@home/models/widget-component.models'; -import { isDefined, isDefinedAndNotNull, isNumber, isString } from '@core/utils'; +import { isDefined, isDefinedAndNotNull, isEmptyStr, isNotEmptyStr, isNumeric, isUndefinedOrNull } from '@core/utils'; import { CapacityUnits, ConversionType, @@ -42,7 +42,7 @@ import { SvgLimits, svgMapping } from '@home/components/widget/lib/indicator/liquid-level-widget.models'; -import { Observable, of } from 'rxjs'; +import { forkJoin, Observable, of } from 'rxjs'; import { map, switchMap } from 'rxjs/operators'; import { EntityId } from '@shared/models/id/entity-id'; import { @@ -56,6 +56,7 @@ import { } from '@shared/models/widget-settings.models'; import { ResourcesService } from '@core/services/resources.service'; import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { TranslateService } from '@ngx-translate/core'; import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance; @Component({ @@ -80,6 +81,8 @@ export class LiquidLevelWidgetComponent implements OnInit { hasCardClickAction = false; + errorsMsg: string[] = []; + private svgParams: SvgInfo; private svg: JQuery; @@ -101,7 +104,11 @@ export class LiquidLevelWidgetComponent implements OnInit { private tooltipContent: string; private widgetUnits: string; - constructor(private cd: ChangeDetectorRef) { + private capacityUnits = Object.values(CapacityUnits); + + constructor(private cd: ChangeDetectorRef, + private resourcesService: ResourcesService, + private translate: TranslateService) { } ngOnInit(): void { @@ -117,14 +124,14 @@ export class LiquidLevelWidgetComponent implements OnInit { this.getData().subscribe(data => { if (data) { const { svg, volume, units } = data; - if (svg && isString(svg) && this.liquidLevelContent.nativeElement) { + if (svg && isNotEmptyStr(svg) && this.liquidLevelContent.nativeElement) { const jQueryContainerElement = $(this.liquidLevelContent.nativeElement); jQueryContainerElement.html(svg); this.svg = jQueryContainerElement.find('svg'); this.createSVG(); this.createValueElement(); - if (this.settings.showTooltip) { + if (this.settings.showTooltip && (this.settings.showTooltipLevel || this.settings.showTooltipDate)) { this.createTooltip(); } } @@ -163,17 +170,13 @@ export class LiquidLevelWidgetComponent implements OnInit { this.shape = shape; this.svgParams = svgMapping.get(shape); if (this.svgParams) { - return this.loadSVG(this.svgParams.svg).pipe( - switchMap( svg => - this.getSecondaryResources(entityId).pipe( - map(({ volume, units }) => - ({ svg, volume, units }) - ) - ) - ) + return forkJoin([ + this.resourcesService.loadJsonResource(this.svgParams.svg), + this.getTankersParams(entityId) + ]).pipe( + map(params => ({svg: params[0], ...params[1]})) ); } - return of(null); }) ); @@ -207,11 +210,11 @@ export class LiquidLevelWidgetComponent implements OnInit { private updateData(ignoreAnimation?: boolean) { const data = this.ctx.data[0]?.data[0]; if (data && isDefinedAndNotNull(data[1])) { - const percentage = isNumber(data[1]) ? this.convertInputData(data[1]) : 0; + const percentage = isNumeric(data[1]) ? this.convertInputData(data[1]) : 0; this.updateSvg(percentage, ignoreAnimation); this.updateValueElement(data[1], percentage); - if (this.settings.showTooltip) { + if (this.settings.showTooltip && (this.settings.showTooltipLevel || this.settings.showTooltipDate)) { this.updateTooltip(data); } } @@ -268,13 +271,13 @@ export class LiquidLevelWidgetComponent implements OnInit { const percentageOverlay = jQueryContainerElement.find('.percentage-overlay'); const absoluteOverlay = jQueryContainerElement.find('.absolute-overlay'); - if (this.settings.layout === LevelCardLayout.absolute) { + if (this.settings.layout === LevelCardLayout.absolute && !this.errorsMsg.length) { this.overlayContainer = absoluteOverlay; percentageOverlay.css('visibility', 'hidden'); if (!this.settings.showBackgroundOverlay) { absoluteOverlay.css('visibility', 'hidden'); } - } else if (this.settings.layout === LevelCardLayout.percentage) { + } else if (this.settings.layout === LevelCardLayout.percentage && !this.errorsMsg.length) { this.overlayContainer = percentageOverlay; absoluteOverlay.css('visibility', 'hidden'); if (!this.settings.showBackgroundOverlay) { @@ -290,51 +293,82 @@ export class LiquidLevelWidgetComponent implements OnInit { } } - private loadSVG(url: string): Observable { - const resourcesService = this.ctx.$injector.get(ResourcesService); - return resourcesService.loadJsonResource(url); - } - private getShape(entityId: EntityId): Observable { if (this.settings.tankSelectionType === LiquidWidgetDataSourceType.attribute && entityId.id !== NULL_UUID) { - return this.ctx.attributeService.getEntityAttributes(entityId, null, - [this.settings.shapeAttributeName]).pipe( - map(attributes => - extractValue(attributes, this.settings.shapeAttributeName, this.settings.selectedShape) - ) - ); + return this.ctx.attributeService.getEntityAttributes(entityId, null, [this.settings.shapeAttributeName]) + .pipe(map(attributes => { + const shape = extractValue(attributes, this.settings.shapeAttributeName); + if (!shape || !svgMapping.has(shape)) { + this.createdErrorMgs(this.settings.shapeAttributeName, isUndefinedOrNull(shape) || isEmptyStr(shape)); + return this.settings.selectedShape; + } + return shape; + } + )); } - return of(this.settings.selectedShape); } - private getSecondaryResources(entityId: EntityId): Observable<{ volume: number; units: string }> { - const attributeNames = this.prepareAttributeNames(); + private getTankersParams(entityId: EntityId): Observable<{ volume: number; units: string }> { + const isVolumeStatic = this.settings.layout !== LevelCardLayout.absolute + && this.settings.datasourceUnits === CapacityUnits.percent + || this.settings.volumeSource === LiquidWidgetDataSourceType.static; + const isUnitStatic = this.settings.layout !== LevelCardLayout.absolute || + this.settings.widgetUnitsSource === LiquidWidgetDataSourceType.static; + + const attributeKeys: string[] = []; - if (!attributeNames.length || entityId.id === NULL_UUID) { + if (!isVolumeStatic) { + attributeKeys.push(this.settings.volumeAttributeName); + } + + if (!isUnitStatic) { + attributeKeys.push(this.settings.widgetUnitsAttributeName); + } + + if (!attributeKeys.length || entityId.id === NULL_UUID) { return of({ volume: this.settings.volumeConstant, units: this.settings.units }); } - return this.ctx.attributeService.getEntityAttributes(entityId, null, attributeNames).pipe( - map(attributes => ({ - volume: extractValue(attributes, this.settings.volumeAttributeName, this.settings.volumeConstant), - units: extractValue(attributes, this.settings.widgetUnitsAttributeName, this.settings.units) - })) + return this.ctx.attributeService.getEntityAttributes(entityId, null, attributeKeys).pipe( + map(attributes => { + let volume = isVolumeStatic ? this.settings.volumeConstant : extractValue(attributes, this.settings.volumeAttributeName); + let units = isUnitStatic ? this.settings.units : extractValue(attributes, this.settings.widgetUnitsAttributeName); + + if (!isVolumeStatic && (!volume || !isNumeric(volume) || volume < 0.1)) { + this.createdErrorMgs(this.settings.volumeAttributeName, isUndefinedOrNull(volume) || isEmptyStr(volume)); + volume = this.settings.volumeConstant; + } + + if (!isUnitStatic) { + if (isNotEmptyStr(units)) { + const normalizeUnits = units.normalize().trim(); + units = this.capacityUnits.find(unit => unit.normalize() === normalizeUnits); + } + if (isUndefinedOrNull(units) || !isNotEmptyStr(units)) { + this.createdErrorMgs(this.settings.widgetUnitsAttributeName, isUndefinedOrNull(units) || isEmptyStr(units)); + units = this.settings.units; + } + } + + return { + volume, + units + }; + }) ); } - private prepareAttributeNames(): string[] { - const names = []; - if (this.settings.volumeSource !== LiquidWidgetDataSourceType.static) { - names.push(this.settings.volumeAttributeName); - } - if (this.settings.widgetUnitsSource !== LiquidWidgetDataSourceType.static) { - names.push(this.settings.widgetUnitsAttributeName); + private createdErrorMgs(attributeName: string, isEmpty = false) { + if (isEmpty) { + this.errorsMsg.push(this.translate.instant('widgets.liquid-level-card.attribute-key-not-set', {attributeName})); + } else { + this.errorsMsg.push(this.translate.instant('widgets.liquid-level-card.attribute-key-invalid', {attributeName})); } - return names; + this.cd.markForCheck(); } private updateSvg(percentage: number, ignoreAnimation?: boolean) { @@ -373,12 +407,14 @@ export class LiquidLevelWidgetComponent implements OnInit { const surfacePositionAttr = this.shape !== Shapes.vCylinder ? 'y' : 'cy'; const animationSpeed = 500; + const levelColor = this.errorsMsg.length ? 'transparent' : this.liquidColor.color; + if (ignoreAnimation) { fill.css({y : newY}); } else { fill.animate({y : newY}, animationSpeed); } - fill.attr('fill', this.liquidColor.color); + fill.attr('fill', levelColor); surfaces.each((index, element) => { const $element = $(element); @@ -388,7 +424,7 @@ export class LiquidLevelWidgetComponent implements OnInit { $element.animate({[surfacePositionAttr]: newY}, animationSpeed); } if ($element.hasClass('tb-liquid')) { - $element.attr('fill', this.liquidColor.color); + $element.attr('fill', levelColor); } }); } @@ -399,12 +435,14 @@ export class LiquidLevelWidgetComponent implements OnInit { const shapeFill = jQueryContainerElement.find('.tb-shape-fill'); this.tankColor.update(value); + const shapeColor = this.errorsMsg.length ? '#CACACA' : this.tankColor.color; + shapeStrokes.each((index, element) => { - $(element).attr('stroke', this.tankColor.color); + $(element).attr('stroke', shapeColor); }); shapeFill.each((index, element) => { - $(element).attr('fill', this.tankColor.color); + $(element).attr('fill', shapeColor); }); } @@ -414,7 +452,7 @@ export class LiquidLevelWidgetComponent implements OnInit { const jQueryContainerElement = $(this.liquidLevelContent.nativeElement); let value = 'N/A'; - if (isNumber(data)) { + if (isNumeric(data)) { value = convertLiters(this.convertOutputData(percentage), this.widgetUnits as CapacityUnits, ConversionType.from) .toFixed(this.settings.decimals || 0); } @@ -452,7 +490,7 @@ export class LiquidLevelWidgetComponent implements OnInit { const contentValue = value || [0, '']; let tooltipValue: string | number = 'N/A'; - if (isNumber(contentValue[1])) { + if (isNumeric(contentValue[1])) { tooltipValue = this.convertTooltipData(contentValue[1]); } @@ -470,7 +508,7 @@ export class LiquidLevelWidgetComponent implements OnInit { if (this.settings.showTooltipLevel) { const levelValue = typeof tooltipValue == 'number' - ? `${tooltipValue.toFixed(this.settings.tooltipLevelDecimals)}${this.settings.tooltipUnits}` + ? `${tooltipValue.toFixed(this.settings.tooltipLevelDecimals)} ${this.settings.tooltipUnits}` : 'N/A'; content += this.createTooltipContent( this.ctx.translate.instant('widgets.liquid-level-card.level'), @@ -493,7 +531,7 @@ export class LiquidLevelWidgetComponent implements OnInit { } private createTooltipContent(labelText: string, contentValue: string, textStyle: string): string { - return `
+ return `
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.models.ts index b722852fd6..3eacd000b4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.models.ts @@ -25,7 +25,7 @@ import { lastUpdateAgoDateFormat } from '@shared/models/widget-settings.models'; import { DataKey, WidgetConfig } from '@shared/models/widget.models'; -import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { AttributeData, DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { forkJoin, Observable, of } from 'rxjs'; import { singleEntityFilterFromDeviceId } from '@shared/models/query/query.models'; import { EntityType } from '@shared/models/entity-type.models'; @@ -375,9 +375,8 @@ export const convertLiters = (value: number, units: CapacityUnits, conversionTyp return conversionType === ConversionType.to ? value / factor : value * factor; }; -export const extractValue = (attributes: any[], attributeName: string, defaultValue: any): any => { - const index = attributes.findIndex(attr => attr.key === attributeName); - return index !== -1 ? attributes[index].value : defaultValue; +export const extractValue = (attributes: Array, attributeName: string): T | undefined => { + return attributes.find(attr => attr.key === attributeName)?.value; }; export const valueContainerStyleDefaults = cssTextFromInlineStyle({ @@ -413,13 +412,13 @@ export const createAbsoluteLayout = (values?: {inputValue: number | string; volu
- +
`; }; export const createPercentLayout = (value: number | string = 50, valueTextStyle: string = valueTextStyleDefaults): string => `
- +
`; export const optionsFilter = (searchText: string): ((key: DataKey) => boolean) => @@ -534,15 +533,11 @@ export const updatedFormSettingsValidators = (formGroup: FormGroup) => { } if (layout === LevelCardLayout.simple) { - if (formGroup.get('decimals')) { - formGroup.get('decimals').disable({emitEvent: false}); - } + formGroup.get('decimals')?.disable({emitEvent: false}); formGroup.get('valueFont').disable({emitEvent: false}); formGroup.get('valueColor').disable({emitEvent: false}); } else { - if (formGroup.get('decimals')) { - formGroup.get('decimals').enable({emitEvent: false}); - } + formGroup.get('decimals')?.enable({emitEvent: false}); formGroup.get('valueFont').enable({emitEvent: false}); formGroup.get('valueColor').enable({emitEvent: false}); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/liquid-level-card-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/liquid-level-card-widget-settings.component.html index cdf96f7f89..79b5a9a037 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/liquid-level-card-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/liquid-level-card-widget-settings.component.html @@ -44,9 +44,8 @@
widgets.liquid-level-card.shape-attribute-name
@@ -82,9 +81,8 @@
widgets.liquid-level-card.units
widgets.liquid-level-card.datasource-units
-
@@ -99,16 +97,14 @@ @@ -127,7 +123,7 @@ + min="0.1" step="1" placeholder="{{ 'widget-config.set' | translate }}">
@@ -219,8 +213,7 @@
+ required class="flex" formControlName="tooltipUnits">