Browse Source

UI: Add liquid level error message and improvement load and validate resources

pull/9456/head
Vladyslav_Prykhodko 3 years ago
parent
commit
8300995d80
  1. 24
      ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/liquid-level-card-basic-config.component.html
  2. 18
      ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/liquid-level-card-basic-config.component.ts
  3. 7
      ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.html
  4. 26
      ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.scss
  5. 146
      ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.ts
  6. 19
      ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.models.ts
  7. 23
      ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/liquid-level-card-widget-settings.component.html
  8. 17
      ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/liquid-level-card-widget-settings.component.ts
  9. 4
      ui-ngx/src/assets/locale/locale.constant-en_US.json

24
ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/liquid-level-card-basic-config.component.html

@ -45,7 +45,7 @@
</tb-color-input> </tb-color-input>
</div> </div>
</div> </div>
<div class="tb-form-row"> <div class="tb-form-row column-xs">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showTitleIcon"> <mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showTitleIcon">
{{ 'widgets.liquid-level-card.icon' | translate }} {{ 'widgets.liquid-level-card.icon' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
@ -89,9 +89,8 @@
<div class="tb-form-row space-between" style="width: 100%;"> <div class="tb-form-row space-between" style="width: 100%;">
<div class="fixed-title-width tb-required" translate>widgets.liquid-level-card.shape-attribute-name</div> <div class="fixed-title-width tb-required" translate>widgets.liquid-level-card.shape-attribute-name</div>
<tb-string-autocomplete [fetchOptionsFn]="fetchOptions.bind(this)" <tb-string-autocomplete [fetchOptionsFn]="fetchOptions.bind(this)"
[required]="isRequired('shapeAttributeName')" required style="flex: 1"
[errorText]="'widgets.liquid-level-card.attribute-name-required' | translate" [errorText]="'widgets.liquid-level-card.attribute-name-required' | translate"
style="flex: 1"
formControlName="shapeAttributeName"> formControlName="shapeAttributeName">
</tb-string-autocomplete> </tb-string-autocomplete>
</div> </div>
@ -124,9 +123,8 @@
<div class="tb-form-row space-between"> <div class="tb-form-row space-between">
<div class="fixed-title-width tb-required" translate>widgets.liquid-level-card.datasource-units</div> <div class="fixed-title-width tb-required" translate>widgets.liquid-level-card.datasource-units</div>
<div fxFlex fxLayout="row" fxLayoutAlign="end center"> <div fxFlex fxLayout="row" fxLayoutAlign="end center">
<tb-unit-input style="max-width: 25%;" class="flex" <tb-unit-input required style="max-width: 25%;" class="flex"
[tagFilter]="unitsType.capacity" [tagFilter]="unitsType.capacity"
[required]="isRequired('datasourceUnits')"
formControlName="datasourceUnits"> formControlName="datasourceUnits">
</tb-unit-input> </tb-unit-input>
</div> </div>
@ -144,14 +142,12 @@
<tb-unit-input *ngIf="levelCardWidgetConfigForm.get('widgetUnitsSource').value === DataSourceType.static; else selectUnitsAttributes" <tb-unit-input *ngIf="levelCardWidgetConfigForm.get('widgetUnitsSource').value === DataSourceType.static; else selectUnitsAttributes"
class="flex" required class="flex" required
[tagFilter]="unitsType.capacity" [tagFilter]="unitsType.capacity"
[required]="isRequired('units')"
formControlName="units"> formControlName="units">
</tb-unit-input> </tb-unit-input>
<ng-template #selectUnitsAttributes> <ng-template #selectUnitsAttributes>
<tb-string-autocomplete [fetchOptionsFn]="fetchOptions.bind(this)" <tb-string-autocomplete [fetchOptionsFn]="fetchOptions.bind(this)"
[required]="isRequired('widgetUnitsAttributeName')" required style="flex: 1"
[errorText]="'widgets.liquid-level-card.attribute-name-required' | translate" [errorText]="'widgets.liquid-level-card.attribute-name-required' | translate"
style="flex: 1"
formControlName="widgetUnitsAttributeName"> formControlName="widgetUnitsAttributeName">
</tb-string-autocomplete> </tb-string-autocomplete>
</ng-template> </ng-template>
@ -170,7 +166,7 @@
<mat-form-field *ngIf="levelCardWidgetConfigForm.get('volumeSource').value === DataSourceType.static; else volumeAttributes" <mat-form-field *ngIf="levelCardWidgetConfigForm.get('volumeSource').value === DataSourceType.static; else volumeAttributes"
appearance="outline" class="flex number" subscriptSizing="dynamic"> appearance="outline" class="flex number" subscriptSizing="dynamic">
<input matInput formControlName="volumeConstant" type="number" <input matInput formControlName="volumeConstant" type="number"
min="0" step="1" placeholder="{{ 'widget-config.set' | translate }}"> min="0.1" step="1" placeholder="{{ 'widget-config.set' | translate }}">
<mat-icon matSuffix <mat-icon matSuffix
matTooltipPosition="above" matTooltipPosition="above"
matTooltipClass="tb-error-tooltip" matTooltipClass="tb-error-tooltip"
@ -183,14 +179,12 @@
<ng-template #volumeAttributes> <ng-template #volumeAttributes>
<tb-string-autocomplete [fetchOptionsFn]="fetchOptions.bind(this)" <tb-string-autocomplete [fetchOptionsFn]="fetchOptions.bind(this)"
[errorText]="'widgets.liquid-level-card.attribute-name-required' | translate" [errorText]="'widgets.liquid-level-card.attribute-name-required' | translate"
[required]="isRequired('volumeAttributeName')" required style="flex: 1"
style="flex: 1"
formControlName="volumeAttributeName"> formControlName="volumeAttributeName">
</tb-string-autocomplete> </tb-string-autocomplete>
</ng-template> </ng-template>
<tb-unit-input [tagFilter]="unitsType.capacity" <tb-unit-input [tagFilter]="unitsType.capacity"
[required]="isRequired('volumeUnits')" required style="max-width: 25%" class="flex"
style="max-width: 25%" class="flex"
formControlName="volumeUnits"> formControlName="volumeUnits">
</tb-unit-input> </tb-unit-input>
</div> </div>
@ -253,8 +247,8 @@
{{ 'widgets.liquid-level-card.level' | translate }} {{ 'widgets.liquid-level-card.level' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
<div fxFlex fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="8px"> <div fxFlex fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="8px">
<tb-unit-input [required]="isRequired('tooltipUnits')" <tb-unit-input required class="flex"
[tagFilter]="unitsType.capacity" class="flex" [tagFilter]="unitsType.capacity"
formControlName="tooltipUnits"> formControlName="tooltipUnits">
</tb-unit-input> </tb-unit-input>
<mat-form-field style="width: 40%;" appearance="outline" class="flex number" subscriptSizing="dynamic"> <mat-form-field style="width: 40%;" appearance="outline" class="flex number" subscriptSizing="dynamic">

18
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 { public get volumeInput(): boolean {
const datasourceUnits = this.levelCardWidgetConfigForm.get('datasourceUnits').value; const datasourceUnits = this.levelCardWidgetConfigForm.get('datasourceUnits').value;
const layout: LevelCardLayout = this.levelCardWidgetConfigForm.get('layout').value; const layout: LevelCardLayout = this.levelCardWidgetConfigForm.get('layout').value;
const units = this.levelCardWidgetConfigForm.get('units').value;
if (layout === LevelCardLayout.absolute) { if (layout === LevelCardLayout.absolute) {
return true; return true;
} }
return datasourceUnits !== CapacityUnits.percent;
return !(datasourceUnits === CapacityUnits.percent && units === CapacityUnits.percent);
} }
public get displayTimewindowConfig(): boolean { public get displayTimewindowConfig(): boolean {
@ -194,7 +192,7 @@ export class LiquidLevelCardBasicConfigComponent extends BasicWidgetConfigCompon
iconColor: [configData.config.iconColor, []], iconColor: [configData.config.iconColor, []],
volumeSource: [settings.volumeSource, []], volumeSource: [settings.volumeSource, []],
volumeConstant: [settings.volumeConstant, [Validators.required]], volumeConstant: [settings.volumeConstant, [Validators.required, Validators.min(0.1)]],
volumeAttributeName: [settings.volumeAttributeName, [Validators.required]], volumeAttributeName: [settings.volumeAttributeName, [Validators.required]],
volumeUnits: [settings.volumeUnits, [Validators.required]], volumeUnits: [settings.volumeUnits, [Validators.required]],
volumeFont: [settings.volumeFont, []], volumeFont: [settings.volumeFont, []],
@ -295,12 +293,8 @@ export class LiquidLevelCardBasicConfigComponent extends BasicWidgetConfigCompon
protected validatorTriggers(): string[] { protected validatorTriggers(): string[] {
return [ return [
'showTooltip', 'showTooltipLevel', 'showTooltip', 'showTooltipLevel', 'tankSelectionType', 'datasourceUnits', 'showTitleIcon', 'volumeSource',
'tankSelectionType', 'datasourceUnits', 'showTooltipDate', 'layout', 'showTitle', 'widgetUnitsSource'
'showTitleIcon', 'volumeSource',
'showTooltipDate', 'units',
'layout', 'showTitle',
'widgetUnitsSource'
]; ];
} }
@ -399,10 +393,6 @@ export class LiquidLevelCardBasicConfigComponent extends BasicWidgetConfigCompon
return createShapeLayout(svg, layout, this.sanitizer); return createShapeLayout(svg, layout, this.sanitizer);
} }
public isRequired(formControlName: string): boolean {
return this.levelCardWidgetConfigForm.get(formControlName)?.hasValidator(Validators.required);
}
public fetchOptions(searchText: string): Observable<Array<string>> { public fetchOptions(searchText: string): Observable<Array<string>> {
if (this.keySearchText !== searchText || !this.lastFetchedKeys) { if (this.keySearchText !== searchText || !this.lastFetchedKeys) {
this.keySearchText = searchText; this.keySearchText = searchText;

7
ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.html

@ -19,4 +19,11 @@
<div class="tb-liquid-level-overlay" [style]="overlayStyle"></div> <div class="tb-liquid-level-overlay" [style]="overlayStyle"></div>
<ng-container *ngTemplateOutlet="widgetTitlePanel"></ng-container> <ng-container *ngTemplateOutlet="widgetTitlePanel"></ng-container>
<div #liquidLevelContent class="tb-liquid-level-content"></div> <div #liquidLevelContent class="tb-liquid-level-content"></div>
<div *ngIf="errorsMsg.length" class="tb-liquid-level-error">
<div class="tb-liquid-level-error-container">
<div *ngFor="let error of errorsMsg">
{{ error }}
</div>
</div>
</div>
</div> </div>

26
ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.scss

@ -43,4 +43,30 @@
display: flex; display: flex;
justify-content: center; 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;
}
}
}
} }

146
ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.ts

@ -25,7 +25,7 @@ import {
ViewEncapsulation ViewEncapsulation
} from '@angular/core'; } from '@angular/core';
import { WidgetContext } from '@home/models/widget-component.models'; 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 { import {
CapacityUnits, CapacityUnits,
ConversionType, ConversionType,
@ -42,7 +42,7 @@ import {
SvgLimits, SvgLimits,
svgMapping svgMapping
} from '@home/components/widget/lib/indicator/liquid-level-widget.models'; } 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 { map, switchMap } from 'rxjs/operators';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { import {
@ -56,6 +56,7 @@ import {
} from '@shared/models/widget-settings.models'; } from '@shared/models/widget-settings.models';
import { ResourcesService } from '@core/services/resources.service'; import { ResourcesService } from '@core/services/resources.service';
import { NULL_UUID } from '@shared/models/id/has-uuid'; import { NULL_UUID } from '@shared/models/id/has-uuid';
import { TranslateService } from '@ngx-translate/core';
import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance; import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance;
@Component({ @Component({
@ -80,6 +81,8 @@ export class LiquidLevelWidgetComponent implements OnInit {
hasCardClickAction = false; hasCardClickAction = false;
errorsMsg: string[] = [];
private svgParams: SvgInfo; private svgParams: SvgInfo;
private svg: JQuery<SVGElement>; private svg: JQuery<SVGElement>;
@ -101,7 +104,11 @@ export class LiquidLevelWidgetComponent implements OnInit {
private tooltipContent: string; private tooltipContent: string;
private widgetUnits: 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 { ngOnInit(): void {
@ -117,14 +124,14 @@ export class LiquidLevelWidgetComponent implements OnInit {
this.getData().subscribe(data => { this.getData().subscribe(data => {
if (data) { if (data) {
const { svg, volume, units } = 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); const jQueryContainerElement = $(this.liquidLevelContent.nativeElement);
jQueryContainerElement.html(svg); jQueryContainerElement.html(svg);
this.svg = jQueryContainerElement.find('svg'); this.svg = jQueryContainerElement.find('svg');
this.createSVG(); this.createSVG();
this.createValueElement(); this.createValueElement();
if (this.settings.showTooltip) { if (this.settings.showTooltip && (this.settings.showTooltipLevel || this.settings.showTooltipDate)) {
this.createTooltip(); this.createTooltip();
} }
} }
@ -163,17 +170,13 @@ export class LiquidLevelWidgetComponent implements OnInit {
this.shape = shape; this.shape = shape;
this.svgParams = svgMapping.get(shape); this.svgParams = svgMapping.get(shape);
if (this.svgParams) { if (this.svgParams) {
return this.loadSVG(this.svgParams.svg).pipe( return forkJoin([
switchMap( svg => this.resourcesService.loadJsonResource<string>(this.svgParams.svg),
this.getSecondaryResources(entityId).pipe( this.getTankersParams(entityId)
map(({ volume, units }) => ]).pipe(
({ svg, volume, units }) map(params => ({svg: params[0], ...params[1]}))
)
)
)
); );
} }
return of(null); return of(null);
}) })
); );
@ -207,11 +210,11 @@ export class LiquidLevelWidgetComponent implements OnInit {
private updateData(ignoreAnimation?: boolean) { private updateData(ignoreAnimation?: boolean) {
const data = this.ctx.data[0]?.data[0]; const data = this.ctx.data[0]?.data[0];
if (data && isDefinedAndNotNull(data[1])) { 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.updateSvg(percentage, ignoreAnimation);
this.updateValueElement(data[1], percentage); this.updateValueElement(data[1], percentage);
if (this.settings.showTooltip) { if (this.settings.showTooltip && (this.settings.showTooltipLevel || this.settings.showTooltipDate)) {
this.updateTooltip(data); this.updateTooltip(data);
} }
} }
@ -268,13 +271,13 @@ export class LiquidLevelWidgetComponent implements OnInit {
const percentageOverlay = jQueryContainerElement.find('.percentage-overlay'); const percentageOverlay = jQueryContainerElement.find('.percentage-overlay');
const absoluteOverlay = jQueryContainerElement.find('.absolute-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; this.overlayContainer = absoluteOverlay;
percentageOverlay.css('visibility', 'hidden'); percentageOverlay.css('visibility', 'hidden');
if (!this.settings.showBackgroundOverlay) { if (!this.settings.showBackgroundOverlay) {
absoluteOverlay.css('visibility', 'hidden'); absoluteOverlay.css('visibility', 'hidden');
} }
} else if (this.settings.layout === LevelCardLayout.percentage) { } else if (this.settings.layout === LevelCardLayout.percentage && !this.errorsMsg.length) {
this.overlayContainer = percentageOverlay; this.overlayContainer = percentageOverlay;
absoluteOverlay.css('visibility', 'hidden'); absoluteOverlay.css('visibility', 'hidden');
if (!this.settings.showBackgroundOverlay) { if (!this.settings.showBackgroundOverlay) {
@ -290,51 +293,82 @@ export class LiquidLevelWidgetComponent implements OnInit {
} }
} }
private loadSVG(url: string): Observable<string> {
const resourcesService = this.ctx.$injector.get(ResourcesService);
return resourcesService.loadJsonResource(url);
}
private getShape(entityId: EntityId): Observable<Shapes> { private getShape(entityId: EntityId): Observable<Shapes> {
if (this.settings.tankSelectionType === LiquidWidgetDataSourceType.attribute && entityId.id !== NULL_UUID) { if (this.settings.tankSelectionType === LiquidWidgetDataSourceType.attribute && entityId.id !== NULL_UUID) {
return this.ctx.attributeService.getEntityAttributes(entityId, null, return this.ctx.attributeService.getEntityAttributes(entityId, null, [this.settings.shapeAttributeName])
[this.settings.shapeAttributeName]).pipe( .pipe(map(attributes => {
map(attributes => const shape = extractValue<Shapes>(attributes, this.settings.shapeAttributeName);
extractValue(attributes, this.settings.shapeAttributeName, this.settings.selectedShape) 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); return of(this.settings.selectedShape);
} }
private getSecondaryResources(entityId: EntityId): Observable<{ volume: number; units: string }> { private getTankersParams(entityId: EntityId): Observable<{ volume: number; units: string }> {
const attributeNames = this.prepareAttributeNames(); 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({ return of({
volume: this.settings.volumeConstant, volume: this.settings.volumeConstant,
units: this.settings.units units: this.settings.units
}); });
} }
return this.ctx.attributeService.getEntityAttributes(entityId, null, attributeNames).pipe( return this.ctx.attributeService.getEntityAttributes(entityId, null, attributeKeys).pipe(
map(attributes => ({ map(attributes => {
volume: extractValue(attributes, this.settings.volumeAttributeName, this.settings.volumeConstant), let volume = isVolumeStatic ? this.settings.volumeConstant : extractValue<number>(attributes, this.settings.volumeAttributeName);
units: extractValue(attributes, this.settings.widgetUnitsAttributeName, this.settings.units) let units = isUnitStatic ? this.settings.units : extractValue<string>(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[] { private createdErrorMgs(attributeName: string, isEmpty = false) {
const names = []; if (isEmpty) {
if (this.settings.volumeSource !== LiquidWidgetDataSourceType.static) { this.errorsMsg.push(this.translate.instant('widgets.liquid-level-card.attribute-key-not-set', {attributeName}));
names.push(this.settings.volumeAttributeName); } else {
} this.errorsMsg.push(this.translate.instant('widgets.liquid-level-card.attribute-key-invalid', {attributeName}));
if (this.settings.widgetUnitsSource !== LiquidWidgetDataSourceType.static) {
names.push(this.settings.widgetUnitsAttributeName);
} }
return names; this.cd.markForCheck();
} }
private updateSvg(percentage: number, ignoreAnimation?: boolean) { private updateSvg(percentage: number, ignoreAnimation?: boolean) {
@ -373,12 +407,14 @@ export class LiquidLevelWidgetComponent implements OnInit {
const surfacePositionAttr = this.shape !== Shapes.vCylinder ? 'y' : 'cy'; const surfacePositionAttr = this.shape !== Shapes.vCylinder ? 'y' : 'cy';
const animationSpeed = 500; const animationSpeed = 500;
const levelColor = this.errorsMsg.length ? 'transparent' : this.liquidColor.color;
if (ignoreAnimation) { if (ignoreAnimation) {
fill.css({y : newY}); fill.css({y : newY});
} else { } else {
fill.animate({y : newY}, animationSpeed); fill.animate({y : newY}, animationSpeed);
} }
fill.attr('fill', this.liquidColor.color); fill.attr('fill', levelColor);
surfaces.each((index, element) => { surfaces.each((index, element) => {
const $element = $(element); const $element = $(element);
@ -388,7 +424,7 @@ export class LiquidLevelWidgetComponent implements OnInit {
$element.animate({[surfacePositionAttr]: newY}, animationSpeed); $element.animate({[surfacePositionAttr]: newY}, animationSpeed);
} }
if ($element.hasClass('tb-liquid')) { 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'); const shapeFill = jQueryContainerElement.find('.tb-shape-fill');
this.tankColor.update(value); this.tankColor.update(value);
const shapeColor = this.errorsMsg.length ? '#CACACA' : this.tankColor.color;
shapeStrokes.each((index, element) => { shapeStrokes.each((index, element) => {
$(element).attr('stroke', this.tankColor.color); $(element).attr('stroke', shapeColor);
}); });
shapeFill.each((index, element) => { 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); const jQueryContainerElement = $(this.liquidLevelContent.nativeElement);
let value = 'N/A'; let value = 'N/A';
if (isNumber(data)) { if (isNumeric(data)) {
value = convertLiters(this.convertOutputData(percentage), this.widgetUnits as CapacityUnits, ConversionType.from) value = convertLiters(this.convertOutputData(percentage), this.widgetUnits as CapacityUnits, ConversionType.from)
.toFixed(this.settings.decimals || 0); .toFixed(this.settings.decimals || 0);
} }
@ -452,7 +490,7 @@ export class LiquidLevelWidgetComponent implements OnInit {
const contentValue = value || [0, '']; const contentValue = value || [0, ''];
let tooltipValue: string | number = 'N/A'; let tooltipValue: string | number = 'N/A';
if (isNumber(contentValue[1])) { if (isNumeric(contentValue[1])) {
tooltipValue = this.convertTooltipData(contentValue[1]); tooltipValue = this.convertTooltipData(contentValue[1]);
} }
@ -470,7 +508,7 @@ export class LiquidLevelWidgetComponent implements OnInit {
if (this.settings.showTooltipLevel) { if (this.settings.showTooltipLevel) {
const levelValue = typeof tooltipValue == 'number' const levelValue = typeof tooltipValue == 'number'
? `${tooltipValue.toFixed(this.settings.tooltipLevelDecimals)}${this.settings.tooltipUnits}` ? `${tooltipValue.toFixed(this.settings.tooltipLevelDecimals)}&nbsp;${this.settings.tooltipUnits}`
: 'N/A'; : 'N/A';
content += this.createTooltipContent( content += this.createTooltipContent(
this.ctx.translate.instant('widgets.liquid-level-card.level'), 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 { private createTooltipContent(labelText: string, contentValue: string, textStyle: string): string {
return `<div style="display: flex; justify-content: space-between; gap: 8px"> return `<div style="display: flex; justify-content: space-between; gap: 8px; align-items: baseline">
<label style="color: rgba(0, 0, 0, 0.38); font-size: 12px; font-weight: 400; white-space: nowrap;"> <label style="color: rgba(0, 0, 0, 0.38); font-size: 12px; font-weight: 400; white-space: nowrap;">
${labelText} ${labelText}
</label> </label>

19
ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.models.ts

@ -25,7 +25,7 @@ import {
lastUpdateAgoDateFormat lastUpdateAgoDateFormat
} from '@shared/models/widget-settings.models'; } from '@shared/models/widget-settings.models';
import { DataKey, WidgetConfig } from '@shared/models/widget.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 { forkJoin, Observable, of } from 'rxjs';
import { singleEntityFilterFromDeviceId } from '@shared/models/query/query.models'; import { singleEntityFilterFromDeviceId } from '@shared/models/query/query.models';
import { EntityType } from '@shared/models/entity-type.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; return conversionType === ConversionType.to ? value / factor : value * factor;
}; };
export const extractValue = (attributes: any[], attributeName: string, defaultValue: any): any => { export const extractValue = <T>(attributes: Array<AttributeData>, attributeName: string): T | undefined => {
const index = attributes.findIndex(attr => attr.key === attributeName); return attributes.find(attr => attr.key === attributeName)?.value;
return index !== -1 ? attributes[index].value : defaultValue;
}; };
export const valueContainerStyleDefaults = cssTextFromInlineStyle({ export const valueContainerStyleDefaults = cssTextFromInlineStyle({
@ -413,13 +412,13 @@ export const createAbsoluteLayout = (values?: {inputValue: number | string; volu
<div style="border-top: 1px solid rgba(0, 0, 0, 0.38); height: 1px; width: 100%; margin: 3px 0"></div> <div style="border-top: 1px solid rgba(0, 0, 0, 0.38); height: 1px; width: 100%; margin: 3px 0"></div>
<label style="${volumeTextStyle}">${volume}</label> <label style="${volumeTextStyle}">${volume}</label>
</div> </div>
<label><b>${displayUnits}</b></label> <label style="${valueTextStyle}">${displayUnits}</label>
</div>`; </div>`;
}; };
export const createPercentLayout = (value: number | string = 50, valueTextStyle: string = valueTextStyleDefaults): string => export const createPercentLayout = (value: number | string = 50, valueTextStyle: string = valueTextStyleDefaults): string =>
`<div xmlns="http://www.w3.org/1999/xhtml" style="${valueContainerStyleDefaults}"> `<div xmlns="http://www.w3.org/1999/xhtml" style="${valueContainerStyleDefaults}">
<label><span style="${valueTextStyle}">${value}</span> ${CapacityUnits.percent}</label> <label style="${valueTextStyle}">${value} ${CapacityUnits.percent}</label>
</div>`; </div>`;
export const optionsFilter = (searchText: string): ((key: DataKey) => boolean) => export const optionsFilter = (searchText: string): ((key: DataKey) => boolean) =>
@ -534,15 +533,11 @@ export const updatedFormSettingsValidators = (formGroup: FormGroup) => {
} }
if (layout === LevelCardLayout.simple) { 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('valueFont').disable({emitEvent: false});
formGroup.get('valueColor').disable({emitEvent: false}); formGroup.get('valueColor').disable({emitEvent: false});
} else { } 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('valueFont').enable({emitEvent: false});
formGroup.get('valueColor').enable({emitEvent: false}); formGroup.get('valueColor').enable({emitEvent: false});
} }

23
ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/liquid-level-card-widget-settings.component.html

@ -44,9 +44,8 @@
<div class="tb-form-row space-between" style="width: 100%;"> <div class="tb-form-row space-between" style="width: 100%;">
<div class="fixed-title-width" translate>widgets.liquid-level-card.shape-attribute-name</div> <div class="fixed-title-width" translate>widgets.liquid-level-card.shape-attribute-name</div>
<tb-string-autocomplete [fetchOptionsFn]="fetchOptions.bind(this)" <tb-string-autocomplete [fetchOptionsFn]="fetchOptions.bind(this)"
[required]="isRequired('shapeAttributeName')" required style="flex: 1"
[errorText]="'widgets.liquid-level-card.attribute-name-required' | translate" [errorText]="'widgets.liquid-level-card.attribute-name-required' | translate"
style="flex: 1"
formControlName="shapeAttributeName"> formControlName="shapeAttributeName">
</tb-string-autocomplete> </tb-string-autocomplete>
</div> </div>
@ -82,9 +81,8 @@
<div class="tb-form-panel-title" translate>widgets.liquid-level-card.units</div> <div class="tb-form-panel-title" translate>widgets.liquid-level-card.units</div>
<div class="tb-form-row space-between"> <div class="tb-form-row space-between">
<div class="fixed-title-width tb-required" translate>widgets.liquid-level-card.datasource-units</div> <div class="fixed-title-width tb-required" translate>widgets.liquid-level-card.datasource-units</div>
<tb-unit-input style="max-width: 30%;" class="flex" <tb-unit-input required style="max-width: 30%;" class="flex"
[tagFilter]="unitsType.capacity" [tagFilter]="unitsType.capacity"
[required]="isRequired('datasourceUnits')"
formControlName="datasourceUnits"> formControlName="datasourceUnits">
</tb-unit-input> </tb-unit-input>
</div> </div>
@ -99,16 +97,14 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<tb-unit-input *ngIf="levelCardWidgetSettingsForm.get('widgetUnitsSource').value !== DataSourceType.attribute; else selectUnitsAttributes" <tb-unit-input *ngIf="levelCardWidgetSettingsForm.get('widgetUnitsSource').value !== DataSourceType.attribute; else selectUnitsAttributes"
class="flex" required class="flex"
[tagFilter]="unitsType.capacity" [tagFilter]="unitsType.capacity"
[required]="isRequired('units')"
formControlName="units"> formControlName="units">
</tb-unit-input> </tb-unit-input>
<ng-template #selectUnitsAttributes> <ng-template #selectUnitsAttributes>
<tb-string-autocomplete [fetchOptionsFn]="fetchOptions.bind(this)" <tb-string-autocomplete [fetchOptionsFn]="fetchOptions.bind(this)"
[required]="isRequired('widgetUnitsAttributeName')" required style="flex: 1"
[errorText]="'widgets.liquid-level-card.attribute-name-required' | translate" [errorText]="'widgets.liquid-level-card.attribute-name-required' | translate"
style="flex: 1"
formControlName="widgetUnitsAttributeName"> formControlName="widgetUnitsAttributeName">
</tb-string-autocomplete> </tb-string-autocomplete>
</ng-template> </ng-template>
@ -127,7 +123,7 @@
<mat-form-field *ngIf="levelCardWidgetSettingsForm.get('volumeSource').value === DataSourceType.static; else volumeAttributes" <mat-form-field *ngIf="levelCardWidgetSettingsForm.get('volumeSource').value === DataSourceType.static; else volumeAttributes"
appearance="outline" class="flex number" subscriptSizing="dynamic"> appearance="outline" class="flex number" subscriptSizing="dynamic">
<input matInput formControlName="volumeConstant" type="number" <input matInput formControlName="volumeConstant" type="number"
min="0" step="1" placeholder="{{ 'widget-config.set' | translate }}"> min="0.1" step="1" placeholder="{{ 'widget-config.set' | translate }}">
<mat-icon matSuffix <mat-icon matSuffix
matTooltipPosition="above" matTooltipPosition="above"
matTooltipClass="tb-error-tooltip" matTooltipClass="tb-error-tooltip"
@ -139,15 +135,13 @@
</mat-form-field> </mat-form-field>
<ng-template #volumeAttributes> <ng-template #volumeAttributes>
<tb-string-autocomplete [fetchOptionsFn]="fetchOptions.bind(this)" <tb-string-autocomplete [fetchOptionsFn]="fetchOptions.bind(this)"
[required]="isRequired('volumeAttributeName')" required style="flex: 1"
[errorText]="'widgets.liquid-level-card.attribute-name-required' | translate" [errorText]="'widgets.liquid-level-card.attribute-name-required' | translate"
style="flex: 1"
formControlName="volumeAttributeName"> formControlName="volumeAttributeName">
</tb-string-autocomplete> </tb-string-autocomplete>
</ng-template> </ng-template>
<tb-unit-input [tagFilter]="unitsType.capacity" <tb-unit-input [tagFilter]="unitsType.capacity"
[required]="isRequired('volumeUnits')" required style="max-width: 25%" class="flex"
style="max-width: 25%" class="flex"
formControlName="volumeUnits"> formControlName="volumeUnits">
</tb-unit-input> </tb-unit-input>
</div> </div>
@ -219,8 +213,7 @@
</mat-slide-toggle> </mat-slide-toggle>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> <div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-unit-input [tagFilter]="unitsType.capacity" <tb-unit-input [tagFilter]="unitsType.capacity"
[required]="isRequired('tooltipUnits')" required class="flex" formControlName="tooltipUnits">
class="flex" formControlName="tooltipUnits">
</tb-unit-input> </tb-unit-input>
<mat-form-field appearance="outline" class="flex number" subscriptSizing="dynamic"> <mat-form-field appearance="outline" class="flex number" subscriptSizing="dynamic">
<input matInput formControlName="tooltipLevelDecimals" type="number" min="0" <input matInput formControlName="tooltipLevelDecimals" type="number" min="0"

17
ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/liquid-level-card-widget-settings.component.ts

@ -68,13 +68,11 @@ export class LiquidLevelCardWidgetSettingsComponent extends WidgetSettingsCompon
public get volumeInput(): boolean { public get volumeInput(): boolean {
const datasourceUnits = this.levelCardWidgetSettingsForm.get('datasourceUnits').value; const datasourceUnits = this.levelCardWidgetSettingsForm.get('datasourceUnits').value;
const layout: LevelCardLayout = this.levelCardWidgetSettingsForm.get('layout').value; const layout: LevelCardLayout = this.levelCardWidgetSettingsForm.get('layout').value;
const widgetUnits = this.levelCardWidgetSettingsForm.get('units').value;
if (layout === LevelCardLayout.absolute) { if (layout === LevelCardLayout.absolute) {
return true; return true;
} }
return datasourceUnits !== CapacityUnits.percent;
return !(datasourceUnits === CapacityUnits.percent && widgetUnits === CapacityUnits.percent);
} }
private get datasource(): Datasource { private get datasource(): Datasource {
@ -158,7 +156,7 @@ export class LiquidLevelCardWidgetSettingsComponent extends WidgetSettingsCompon
layout: [settings.layout, []], layout: [settings.layout, []],
volumeSource: [settings.volumeSource, []], volumeSource: [settings.volumeSource, []],
volumeConstant: [settings.volumeConstant, [Validators.required]], volumeConstant: [settings.volumeConstant, [Validators.required, Validators.min(0.1)]],
volumeAttributeName: [settings.volumeAttributeName, [Validators.required]], volumeAttributeName: [settings.volumeAttributeName, [Validators.required]],
volumeUnits: [settings.volumeUnits, [Validators.required]], volumeUnits: [settings.volumeUnits, [Validators.required]],
volumeFont: [settings.volumeFont, []], volumeFont: [settings.volumeFont, []],
@ -196,11 +194,8 @@ export class LiquidLevelCardWidgetSettingsComponent extends WidgetSettingsCompon
protected validatorTriggers(): string[] { protected validatorTriggers(): string[] {
return [ return [
'showBackgroundOverlay', 'showTooltip', 'showTooltipLevel', 'showBackgroundOverlay', 'showTooltip', 'showTooltipLevel', 'tankSelectionType', 'datasourceUnits',
'tankSelectionType', 'datasourceUnits', 'showTooltipDate', 'layout', 'volumeSource', 'widgetUnitsSource'
'showTooltipDate', 'units',
'layout', 'volumeSource',
'widgetUnitsSource'
]; ];
} }
@ -228,10 +223,6 @@ export class LiquidLevelCardWidgetSettingsComponent extends WidgetSettingsCompon
return createShapeLayout(svg, layout, this.sanitizer); return createShapeLayout(svg, layout, this.sanitizer);
} }
public isRequired(formControlName: string): boolean {
return this.levelCardWidgetSettingsForm.get(formControlName)?.hasValidator(Validators.required);
}
private _valuePreviewFn(): string { private _valuePreviewFn(): string {
const units: string = this.widgetConfig.config.units; const units: string = this.widgetConfig.config.units;
const decimals: number = this.widgetConfig.config.decimals; const decimals: number = this.widgetConfig.config.decimals;

4
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -6108,7 +6108,9 @@
"color-and-font": "Color and font", "color-and-font": "Color and font",
"shape-attribute-name": "Attribute name", "shape-attribute-name": "Attribute name",
"total-volume-required": "Total volume is required.", "total-volume-required": "Total volume is required.",
"attribute-name-required": "Attribute name is required." "attribute-name-required": "Attribute name is required.",
"attribute-key-not-set": "Attribute '{{attributeName}}' key not set",
"attribute-key-invalid": "Attribute '{{attributeName}}' key is invalid"
}, },
"aggregated-value-card": { "aggregated-value-card": {
"subtitle": "Subtitle", "subtitle": "Subtitle",

Loading…
Cancel
Save