;
+
+ const value = convertLiters(data, this.widgetUnits as CapacityUnits, ConversionType.from)
+ .toFixed(this.settings.decimals || 0);
+ const valueTextStyle = cssTextFromInlineStyle({...inlineTextStyle(this.settings.valueFont),
+ color: this.valueColor.color});
+ this.backgroundOverlayColor.update(percentage);
+ if (this.overlayContainer) {
+ this.overlayContainer.attr('fill', this.backgroundOverlayColor.color);
+ }
+
+ if (this.settings.layout === LevelCardLayout.absolute) {
+ this.volumeColor.update(percentage);
+
+ const volumeInLiters: number = convertLiters(this.volume, this.settings.volumeUnits as CapacityUnits, ConversionType.to);
+ const volume = convertLiters(volumeInLiters, this.widgetUnits as CapacityUnits, ConversionType.from)
+ .toFixed(this.settings.decimals || 0);
+ const volumeTextStyle = cssTextFromInlineStyle({...inlineTextStyle(this.settings.volumeFont),
+ color: this.volumeColor.color});
+
+ container = this.ctx.$container.find('.absolute-value-container');
+ content = createAbsoluteLayout({inputValue: value, volume},
+ {valueStyle: valueTextStyle, volumeStyle: volumeTextStyle}, this.widgetUnits);
+
+ } else if (this.settings.layout === LevelCardLayout.percentage) {
+ container = this.ctx.$container.find('.percentage-value-container');
+ content = createPercentLayout(value, valueTextStyle);
+ }
+
+ if (content && container) {
+ container.html(content);
+ }
+ }
+
+ private getTooltipContent(value?: number[]): string {
+ const contentValue = value || [0, 0];
+
+ if (contentValue[1]) {
+ contentValue[1] = this.convertTooltipData(contentValue[1]);
+ }
+
+ this.tooltipLevelColor.update(contentValue[1]);
+ this.tooltipDateColor.update(contentValue[0]);
+ this.tooltipDateFormat.update(contentValue[0]);
+ this.tooltipBackgroundColor.update(contentValue);
+
+ const levelTextStyle = cssTextFromInlineStyle({...inlineTextStyle(this.settings.tooltipLevelFont),
+ color: this.tooltipLevelColor.color});
+
+ const dateTextStyle = cssTextFromInlineStyle({...inlineTextStyle(this.settings.tooltipDateFont),
+ color: this.tooltipDateColor.color, overflow: 'hidden', 'text-overflow': 'ellipsis', 'white-space': 'nowrap'});
+
+ let content = ``;
+
+ if (this.settings.showTooltipLevel) {
+ const levelValue = contentValue[1]?.toFixed(this.settings.tooltipLevelDecimals) + this.settings.tooltipUnits;
+ content += this.createTooltipContent(
+ this.ctx.translate.instant('widgets.liquid-level-card.level'),
+ levelValue,
+ levelTextStyle
+ );
+ }
+
+ if (this.settings.showTooltipDate) {
+ content += this.createTooltipContent(
+ this.ctx.translate.instant('widgets.liquid-level-card.last-update'),
+ this.tooltipDateFormat.formatted,
+ dateTextStyle
+ );
+ }
+
+ content += '
';
+
+ return content;
+ }
+
+ private createTooltipContent(labelText: string, contentValue: string, textStyle: string): string {
+ return `
+
+
+
`;
+ }
+
+ private getTooltipBackground(): string {
+ return this.tooltipBackgroundColor.color;
+ }
+
+ private convertInputData(value: number): number {
+ if (this.settings.datasourceUnits !== CapacityUnits.percent) {
+ return (convertLiters(value, this.settings.datasourceUnits, ConversionType.to) /
+ convertLiters(this.volume, this.settings.volumeUnits, ConversionType.to)) * 100;
+ }
+
+ return value;
+ }
+
+ private convertOutputData(value: number): number {
+ if (this.widgetUnits !== CapacityUnits.percent) {
+ return convertLiters(this.volume * (value / 100), this.settings.volumeUnits, ConversionType.to);
+ }
+
+ return value;
+ }
+
+ private convertTooltipData(value: number): number {
+ const percentage = this.convertInputData(value);
+ if (this.settings.tooltipUnits !== CapacityUnits.percent) {
+ const liters = this.convertOutputData(percentage);
+
+ return convertLiters(liters, this.settings.tooltipUnits, ConversionType.from);
+ } else {
+ return percentage;
+ }
+ }
+
+ public cardClick($event) {
+ this.ctx.actionsApi.cardClick($event);
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/liquid-level-card-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/liquid-level-card-widget-settings.component.html
new file mode 100644
index 0000000000..0f3f943cc4
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/liquid-level-card-widget-settings.component.html
@@ -0,0 +1,267 @@
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/liquid-level-card-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/liquid-level-card-widget-settings.component.ts
new file mode 100644
index 0000000000..50a1bec81f
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/liquid-level-card-widget-settings.component.ts
@@ -0,0 +1,516 @@
+///
+/// Copyright © 2016-2023 The Thingsboard Authors
+///
+/// Licensed under the Apache License, Version 2.0 (the "License");
+/// you may not use this file except in compliance with the License.
+/// You may obtain a copy of the License at
+///
+/// http://www.apache.org/licenses/LICENSE-2.0
+///
+/// Unless required by applicable law or agreed to in writing, software
+/// distributed under the License is distributed on an "AS IS" BASIS,
+/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+/// See the License for the specific language governing permissions and
+/// limitations under the License.
+///
+
+import { ChangeDetectorRef, Component, Injector, ViewChild } from '@angular/core';
+import {
+ DataKey,
+ Datasource,
+ DatasourceType,
+ WidgetSettings,
+ WidgetSettingsComponent
+} from '@shared/models/widget.models';
+import { AbstractControl, UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
+import { Store } from '@ngrx/store';
+import { AppState } from '@core/core.state';
+import { formatValue, isDefined } from '@core/utils';
+import { WidgetConfigComponentData } from '@home/models/widget-component.models';
+import {
+ DateFormatProcessor,
+ DateFormatSettings
+} from '@shared/models/widget-settings.models';
+import {
+ levelCardDefaultSettings,
+ LevelCardLayout,
+ levelCardLayoutTranslations,
+ Shapes,
+ shapesTranslations,
+ svgMapping,
+ CapacityUnits,
+ LevelSelectOptions,
+ createPercentLayout,
+ createAbsoluteLayout,
+ optionsFilter,
+ fetchEntityKeysForDevice,
+ fetchEntityKeys
+} from '@home/components/widget/lib/indicator/liquid-level-widget.models';
+import { UnitsType } from '@shared/models/unit.models';
+import { ImageCardsSelectComponent } from '@home/components/widget/lib/settings/common/image-cards-select.component';
+import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
+import { forkJoin, Observable, of } from 'rxjs';
+import { map, publishReplay, refCount, tap } from 'rxjs/operators';
+import { ResourcesService } from '@core/services/resources.service';
+import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
+import { UtilsService } from '@core/services/utils.service';
+import { EntityService } from '@core/http/entity.service';
+
+@Component({
+ selector: 'liquid-level-card-widget-settings',
+ templateUrl: './liquid-level-card-widget-settings.component.html',
+ styleUrls: []
+})
+export class LiquidLevelCardWidgetSettingsComponent extends WidgetSettingsComponent {
+
+ @ViewChild('layoutsImageCardsSelect', { static: false }) layoutsImageCardsSelect: ImageCardsSelectComponent;
+
+ @ViewChild('shapesImageCardsSelect', { static: false }) shapesImageCardsSelect: ImageCardsSelectComponent;
+
+ public get volumeInput(): boolean {
+ const datasourceUnits = this.levelCardWidgetSettingsForm.get('datasourceUnits').value;
+ const layout: LevelCardLayout = this.levelCardWidgetSettingsForm.get('layout').value;
+ const widgetUnits = this.levelCardWidgetSettingsForm.get('units').value;
+ return !(datasourceUnits === CapacityUnits.percent && layout !== LevelCardLayout.absolute
+ || (datasourceUnits === CapacityUnits.percent && widgetUnits === CapacityUnits.percent));
+ }
+
+ public get widgetUnitsInput(): boolean {
+ const layout: LevelCardLayout = this.levelCardWidgetSettingsForm.get('layout').value;
+
+ if (layout === LevelCardLayout.absolute) {
+ const datasourceUnits = this.levelCardWidgetSettingsForm.get('datasourceUnits').value;
+ return !(datasourceUnits === CapacityUnits.percent);
+ }
+ return false;
+ }
+
+ public get datasource(): Datasource {
+ if (this.widgetConfig.config.datasources && this.widgetConfig.config.datasources) {
+ return this.widgetConfig.config.datasources[0];
+ } else {
+ return null;
+ }
+ }
+
+ shapeSelectOptions = LevelSelectOptions;
+
+ shapes: Shapes[] = [];
+
+ shapesTranslationMap = shapesTranslations;
+
+ unitsType = UnitsType;
+
+ levelCardLayouts = LevelCardLayout;
+
+ levelCardLayoutTranslationMap = levelCardLayoutTranslations;
+ shapesImageMap: Map = new Map();
+
+ volumeOptions = LevelSelectOptions;
+
+ levelCardWidgetSettingsForm: UntypedFormGroup;
+
+ valuePreviewFn = this._valuePreviewFn.bind(this);
+
+ tooltipValuePreviewFn = this._tooltipValuePreviewFn.bind(this);
+
+ totalVolumeValuePreviewFn = this._totalVolumeValuePreviewFn.bind(this);
+
+ datePreviewFn = this._datePreviewFn.bind(this);
+
+ keySearchText: string;
+
+ latestKeySearchTextResult: Array;
+
+ datasources: Array;
+
+ functionTypeKeys: Array = [];
+
+ lastKeysId: string;
+
+ lastFetchedKeys: Array;
+
+ constructor(protected store: Store,
+ private $injector: Injector,
+ private fb: UntypedFormBuilder,
+ private resourcesService: ResourcesService,
+ private sanitizer: DomSanitizer,
+ private cd: ChangeDetectorRef,
+ private utils: UtilsService,
+ private entityService: EntityService) {
+ super(store);
+ }
+
+ protected settingsForm(): UntypedFormGroup {
+ return this.levelCardWidgetSettingsForm;
+ }
+
+ protected onWidgetConfigSet(widgetConfig: WidgetConfigComponentData) {
+ this.shapes = Object.values(Shapes);
+ this.createSvgShapesMapping();
+
+ for (const type of this.utils.getPredefinedFunctionsList()) {
+ this.functionTypeKeys.push({
+ name: type,
+ type: DataKeyType.function
+ });
+ }
+ }
+
+ protected defaultSettings(): WidgetSettings {
+ return levelCardDefaultSettings();
+ }
+
+ protected onSettingsSet(settings: WidgetSettings) {
+ this.levelCardWidgetSettingsForm = this.fb.group({
+ tankSelectionType: [settings.tankSelectionType, []],
+ selectedShape: [settings.selectedShape, [Validators.required]],
+ shapeAttributeName: [settings.shapeAttributeName, [Validators.required]],
+ tankColor: [settings.tankColor, []],
+ datasourceUnits: [settings.datasourceUnits, [Validators.required]],
+
+ layout: [settings.layout, []],
+
+ volumeSource: [settings.volumeSource, []],
+ volumeConstant: [settings.volumeConstant, [Validators.required]],
+ volumeAttributeName: [settings.volumeAttributeName, [Validators.required]],
+ volumeUnits: [settings.volumeUnits, [Validators.required]],
+ volumeFont: [settings.volumeFont, []],
+ volumeColor: [settings.volumeColor, []],
+ valueFont: [settings.valueFont, []],
+ valueColor: [settings.valueColor, []],
+ units: [settings.units, [Validators.required]],
+ widgetUnitsSource: [settings.widgetUnitsSource, [Validators.required]],
+ widgetUnitsAttributeName: [settings.widgetUnitsAttributeName, [Validators.required]],
+ showBackgroundOverlay: [settings.showBackgroundOverlay, []],
+ backgroundOverlayColor: [settings.backgroundOverlayColor, []],
+
+ liquidColor: [settings.liquidColor, []],
+
+ showTooltip: [settings.showTooltip, []],
+ showTooltipLevel: [settings.showTooltipLevel, []],
+ tooltipUnits: [settings.tooltipUnits, []],
+ tooltipLevelDecimals: [settings.tooltipLevelDecimals, []],
+ tooltipLevelFont: [settings.tooltipLevelFont, []],
+ tooltipLevelColor: [settings.tooltipLevelColor, []],
+ showTooltipDate: [settings.showTooltipDate, []],
+ tooltipDateFormat: [settings.tooltipDateFormat, []],
+ tooltipDateFont: [settings.tooltipDateFont, []],
+ tooltipDateColor: [settings.tooltipDateColor, []],
+ tooltipBackgroundColor: [settings.tooltipBackgroundColor, []],
+ tooltipBackgroundBlur: [settings.tooltipBackgroundBlur, []],
+ });
+
+ this.levelCardWidgetSettingsForm.get('selectedShape').valueChanges.subscribe((shape) => {
+ this.cd.detectChanges();
+ this.layoutsImageCardsSelect?.imageCardsSelectOptions.notifyOnChanges();
+ });
+ }
+
+ protected validatorTriggers(): string[] {
+ return [
+ 'showBackgroundOverlay', 'showTooltip', 'showTooltipLevel',
+ 'tankSelectionType', 'datasourceUnits',
+ 'showTooltipDate', 'units',
+ 'layout', 'volumeSource',
+ 'widgetUnitsSource'
+ ];
+ }
+
+ protected updateValidators(emitEvent: boolean, trigger?: string) {
+ const emitEventFields = [];
+
+ const datasourceUnits: string = this.levelCardWidgetSettingsForm.get('datasourceUnits').value;
+ const layout: LevelCardLayout = this.levelCardWidgetSettingsForm.get('layout').value;
+ const volumeSource: AbstractControl = this.levelCardWidgetSettingsForm.get('volumeSource');
+ const widgetUnits: AbstractControl = this.levelCardWidgetSettingsForm.get('units');
+ const tooltipUnits: AbstractControl = this.levelCardWidgetSettingsForm.get('tooltipUnits');
+ const widgetUnitsSource: AbstractControl = this.levelCardWidgetSettingsForm.get('widgetUnitsSource');
+ const showTooltipLevel: AbstractControl = this.levelCardWidgetSettingsForm.get('showTooltipLevel');
+ const showTooltipDate: AbstractControl = this.levelCardWidgetSettingsForm.get('showTooltipDate');
+ const showTooltip: boolean = this.levelCardWidgetSettingsForm.get('showTooltip').value;
+
+ if (trigger === 'tankSelectionType') {
+ const tankSelectionType: LevelSelectOptions = this.levelCardWidgetSettingsForm.get('tankSelectionType').value;
+ if (tankSelectionType === LevelSelectOptions.static) {
+ this.levelCardWidgetSettingsForm.get('selectedShape').enable({emitEvent: false});
+ this.levelCardWidgetSettingsForm.get('shapeAttributeName').disable({emitEvent: false});
+ } else {
+ this.levelCardWidgetSettingsForm.get('selectedShape').disable({emitEvent: false});
+ this.levelCardWidgetSettingsForm.get('shapeAttributeName').enable({emitEvent: false});
+ }
+ emitEventFields.push('selectedShape', 'shapeAttributeName');
+ }
+
+ if (trigger === 'datasourceUnits' || trigger === 'layout' || trigger === 'units') {
+ if (datasourceUnits === CapacityUnits.percent && (layout !== LevelCardLayout.absolute)
+ || (datasourceUnits === CapacityUnits.percent && widgetUnits?.value === CapacityUnits.percent)
+ ) {
+ volumeSource.disable({emitEvent: false});
+ this.levelCardWidgetSettingsForm.get('volumeConstant').disable({emitEvent: false});
+ this.levelCardWidgetSettingsForm.get('volumeAttributeName').disable({emitEvent: false});
+ this.levelCardWidgetSettingsForm.get('volumeUnits').disable({emitEvent: false});
+ this.levelCardWidgetSettingsForm.get('volumeFont').disable({emitEvent: false});
+ this.levelCardWidgetSettingsForm.get('volumeColor').disable({emitEvent: false});
+ } else {
+ volumeSource.enable({emitEvent: false});
+ this.levelCardWidgetSettingsForm.get('volumeConstant').enable({emitEvent: false});
+ this.levelCardWidgetSettingsForm.get('volumeAttributeName').enable({emitEvent: false});
+ this.levelCardWidgetSettingsForm.get('volumeUnits').enable({emitEvent: false});
+ this.levelCardWidgetSettingsForm.get('volumeFont').enable({emitEvent: false});
+ this.levelCardWidgetSettingsForm.get('volumeColor').enable({emitEvent: false});
+ }
+
+ if (layout === LevelCardLayout.simple && datasourceUnits === CapacityUnits.percent) {
+ this.levelCardWidgetSettingsForm.get('valueFont').disable();
+ this.levelCardWidgetSettingsForm.get('valueColor').disable();
+ } else {
+ this.levelCardWidgetSettingsForm.get('valueFont').enable();
+ this.levelCardWidgetSettingsForm.get('valueColor').enable();
+ }
+ emitEventFields.push('volumeSource', 'volumeConstant', 'volumeAttributeName', 'volumeFont', 'volumeColor', 'volumeUnits');
+ } else if (trigger === 'volumeSource') {
+ if((datasourceUnits !== CapacityUnits.percent) ||
+ (layout === LevelCardLayout.absolute && widgetUnits?.value !== CapacityUnits.percent)) {
+ if (volumeSource.value === LevelSelectOptions.static) {
+ this.levelCardWidgetSettingsForm.get('volumeConstant').enable({emitEvent: false});
+ this.levelCardWidgetSettingsForm.get('volumeAttributeName').disable({emitEvent: false});
+ } else {
+ this.levelCardWidgetSettingsForm.get('volumeConstant').disable({emitEvent: false});
+ this.levelCardWidgetSettingsForm.get('volumeAttributeName').enable({emitEvent: false});
+ }
+ emitEventFields.push('volumeConstant', 'volumeAttributeName');
+ }
+ }
+
+ if (trigger === 'showBackgroundOverlay') {
+ const showBackgroundOverlay: boolean = this.levelCardWidgetSettingsForm.get('showBackgroundOverlay').value;
+ if (showBackgroundOverlay) {
+ this.levelCardWidgetSettingsForm.get('backgroundOverlayColor').enable({emitEvent: false});
+ } else {
+ this.levelCardWidgetSettingsForm.get('backgroundOverlayColor').disable({emitEvent: false});
+ }
+ emitEventFields.push('backgroundOverlayColor');
+ }
+
+ if (trigger === 'showTooltip') {
+ if (showTooltip) {
+ showTooltipLevel.enable({emitEvent: false});
+ showTooltipDate.enable({emitEvent: false});
+ this.levelCardWidgetSettingsForm.get('tooltipBackgroundColor').enable();
+ this.levelCardWidgetSettingsForm.get('tooltipBackgroundBlur').enable();
+ } else {
+ showTooltipLevel.disable({emitEvent: false});
+ showTooltipDate.disable({emitEvent: false});
+ this.levelCardWidgetSettingsForm.get('tooltipBackgroundColor').disable({emitEvent: false});
+ this.levelCardWidgetSettingsForm.get('tooltipBackgroundBlur').disable({emitEvent: false});
+ }
+ emitEventFields.push('showTooltipLevel', 'showTooltipDate', 'tooltipBackgroundColor', 'tooltipBackgroundBlur');
+ }
+
+ if (trigger === 'showTooltipLevel') {
+ if (showTooltipLevel?.value && !showTooltipLevel.disabled) {
+ this.levelCardWidgetSettingsForm.get('tooltipUnits').enable();
+ this.levelCardWidgetSettingsForm.get('tooltipLevelDecimals').enable({emitEvent: false});
+ this.levelCardWidgetSettingsForm.get('tooltipLevelFont').enable({emitEvent: false});
+ this.levelCardWidgetSettingsForm.get('tooltipLevelColor').enable({emitEvent: false});
+ } else {
+ this.levelCardWidgetSettingsForm.get('tooltipUnits').disable();
+ this.levelCardWidgetSettingsForm.get('tooltipLevelDecimals').disable({emitEvent: false});
+ this.levelCardWidgetSettingsForm.get('tooltipLevelFont').disable({emitEvent: false});
+ this.levelCardWidgetSettingsForm.get('tooltipLevelColor').disable({emitEvent: false});
+ }
+ emitEventFields.push('tooltipUnits', 'tooltipLevelDecimals', 'tooltipLevelFont', 'tooltipLevelColor');
+ }
+
+ if (trigger === 'showTooltipDate') {
+ if (showTooltipDate?.value && !showTooltipDate.disabled) {
+ this.levelCardWidgetSettingsForm.get('tooltipDateFormat').enable({emitEvent: false});
+ this.levelCardWidgetSettingsForm.get('tooltipDateFont').enable({emitEvent: false});
+ this.levelCardWidgetSettingsForm.get('tooltipDateColor').enable({emitEvent: false});
+ } else {
+ this.levelCardWidgetSettingsForm.get('tooltipDateFormat').disable({emitEvent: false});
+ this.levelCardWidgetSettingsForm.get('tooltipDateFont').disable({emitEvent: false});
+ this.levelCardWidgetSettingsForm.get('tooltipDateColor').disable({emitEvent: false});
+ }
+ emitEventFields.push('tooltipDateFormat', 'tooltipDateFont', 'tooltipDateColor');
+ }
+
+ if (trigger === 'layout' || trigger === 'datasourceUnits') {
+ if (layout === LevelCardLayout.simple) {
+ widgetUnits.disable({emitEvent: false});
+ tooltipUnits.disable({emitEvent: false});
+ widgetUnitsSource.setValue(LevelSelectOptions.static, {emitEvent: false});
+ widgetUnitsSource.disable({emitEvent: false});
+
+ this.levelCardWidgetSettingsForm.get('valueFont').disable({emitEvent: false});
+ this.levelCardWidgetSettingsForm.get('valueColor').disable({emitEvent: false});
+ this.levelCardWidgetSettingsForm.get('widgetUnitsAttributeName').disable({emitEvent: false});
+ } else if (layout === LevelCardLayout.percentage) {
+ if (widgetUnits.value !== CapacityUnits.percent) {
+ widgetUnits.setValue(CapacityUnits.percent, {emitEvent: false});
+ widgetUnitsSource.setValue(LevelSelectOptions.static, {emitEvent: false});
+ }
+ widgetUnits.disable({emitEvent: false});
+ widgetUnitsSource.disable({emitEvent: false});
+ this.levelCardWidgetSettingsForm.get('widgetUnitsAttributeName').disable({emitEvent: false});
+
+ if (tooltipUnits.value !== CapacityUnits.percent) {
+ tooltipUnits.setValue(CapacityUnits.percent, {emitEvent: false});
+ }
+ tooltipUnits.disable({emitEvent: false});
+ } else {
+ widgetUnits.enable({emitEvent: false});
+ tooltipUnits.enable({emitEvent: false});
+ widgetUnitsSource.enable({emitEvent: false});
+
+ this.levelCardWidgetSettingsForm.get('valueFont').enable({emitEvent: false});
+ this.levelCardWidgetSettingsForm.get('valueColor').enable({emitEvent: false});
+ this.levelCardWidgetSettingsForm.get('widgetUnitsAttributeName').enable({emitEvent: false});
+ }
+ emitEventFields.push('units', 'tooltipUnits', 'valueFont', 'valueColor', 'widgetUnitsSource', 'widgetUnitsAttributeName');
+ } else if (trigger === 'widgetUnitsSource') {
+ if (layout !== LevelCardLayout.percentage) {
+ if (widgetUnitsSource.value === LevelSelectOptions.static) {
+ widgetUnits.enable({emitEvent: false});
+ this.levelCardWidgetSettingsForm.get('widgetUnitsAttributeName').disable({emitEvent: false});
+ } else {
+ widgetUnits.disable({emitEvent: false});
+ this.levelCardWidgetSettingsForm.get('widgetUnitsAttributeName').enable({emitEvent: false});
+ }
+ emitEventFields.push('units', 'widgetUnitsAttributeName');
+ }
+ }
+
+ for (const controlKey in this.levelCardWidgetSettingsForm.controls) {
+ if (emitEventFields.includes(controlKey)) {
+ this.levelCardWidgetSettingsForm.controls[controlKey].updateValueAndValidity({emitEvent});
+ }
+ }
+ }
+
+ private createSvgShapesMapping(): void {
+ const obsArray: Array> = [];
+ for (const shape of this.shapes) {
+ const svgUrl = svgMapping.get(shape).svg;
+
+ const obs = this.resourcesService.loadJsonResource(svgUrl).pipe(
+ map((svg) => ({svg, shape}))
+ );
+
+ obsArray.push(obs);
+ }
+
+ forkJoin(obsArray).subscribe((svgData) => {
+ for (const data of svgData) {
+ this.shapesImageMap.set(data.shape, data.svg);
+ }
+
+ this.cd.detectChanges();
+ this.layoutsImageCardsSelect?.imageCardsSelectOptions.notifyOnChanges();
+ this.shapesImageCardsSelect?.imageCardsSelectOptions.notifyOnChanges();
+ });
+ }
+
+ public createShapeLayout(svg: string, layout: LevelCardLayout): SafeUrl {
+ if (svg && layout) {
+ const parser = new DOMParser();
+ const svgImage = parser.parseFromString(svg, 'image/svg+xml');
+
+ if (layout === this.levelCardLayouts.simple) {
+ svgImage.querySelector('.container-overlay').remove();
+ } else if (layout === this.levelCardLayouts.percentage) {
+ svgImage.querySelector('.absolute-overlay').remove();
+ svgImage.querySelector('.percentage-value-container').innerHTML = createPercentLayout();
+ } else {
+ svgImage.querySelector('.absolute-value-container').innerHTML = createAbsoluteLayout();
+ svgImage.querySelector('.percentage-overlay').remove();
+ }
+
+ const encodedSvg = encodeURIComponent(svgImage.documentElement.outerHTML);
+
+ return this.sanitizer.bypassSecurityTrustResourceUrl(`data:image/svg+xml,${encodedSvg}`);
+ }
+ }
+
+ public isRequired(formControlName: string): boolean {
+ return this.levelCardWidgetSettingsForm.get(formControlName)?.hasValidator(Validators.required);
+ }
+
+ private _valuePreviewFn(): string {
+ const units: string = this.widgetConfig.config.units;
+ const decimals: number = this.widgetConfig.config.decimals;
+ return formatValue(32, decimals, units, true);
+ }
+
+ private _tooltipValuePreviewFn() {
+ const units: string = this.levelCardWidgetSettingsForm.get('tooltipUnits').value;
+ const decimals: number = this.levelCardWidgetSettingsForm.get('tooltipLevelDecimals').value;
+ return formatValue(32, decimals, units, true);
+ }
+
+ private _totalVolumeValuePreviewFn() {
+ const value = this.levelCardWidgetSettingsForm.get('volumeConstant').value;
+ const datasourceUnits = this.levelCardWidgetSettingsForm.get('datasourceUnits').value;
+ const decimals: number = this.widgetConfig.config.decimals;
+ let units: string = this.widgetConfig.config.units;
+
+ if (datasourceUnits !== CapacityUnits.percent) {
+ units = datasourceUnits;
+ }
+
+ return formatValue((isDefined(value) ? value : 500), decimals, units, true);
+ }
+
+ private _datePreviewFn(): string {
+ const dateFormat: DateFormatSettings = this.levelCardWidgetSettingsForm.get('tooltipDateFormat').value;
+ const processor = DateFormatProcessor.fromSettings(this.$injector, dateFormat);
+ processor.update(Date.now());
+ return processor.formatted;
+ }
+
+ public fetchOptions(searchText: string): Observable> {
+ if (this.keySearchText !== searchText) {
+ this.keySearchText = searchText;
+ const dataKeyFilter = optionsFilter(this.keySearchText);
+ return this.getKeys().pipe(
+ tap(res => this.lastFetchedKeys !== res ? this.lastFetchedKeys = res : []),
+ map(name => name?.filter(dataKeyFilter).map(key => key.name)),
+ tap(res => this.latestKeySearchTextResult = res)
+ );
+ }
+ return of(this.latestKeySearchTextResult);
+ }
+
+ private getKeys(): Observable> {
+ let fetchObservable: Observable>;
+ if (this.datasource?.type === DatasourceType.function) {
+ fetchObservable = of(this.functionTypeKeys);
+ } else if (this.datasource?.type === DatasourceType.entity && this.datasource?.entityAliasId ||
+ this.datasource?.type === DatasourceType.device && this.datasource?.deviceId) {
+ if (this.datasource?.type === DatasourceType.device) {
+ if (this.lastKeysId !== this.datasource?.deviceId || !this.lastFetchedKeys) {
+ this.lastKeysId = this.datasource.deviceId;
+ fetchObservable = fetchEntityKeysForDevice(this.datasource?.deviceId, [DataKeyType.attribute],
+ this.entityService);
+ } else {
+ fetchObservable = of(this.lastFetchedKeys);
+ }
+ } else {
+ if (this.lastKeysId !== this.datasource?.entityAliasId || !this.lastFetchedKeys) {
+ this.lastKeysId = this.datasource.entityAliasId;
+ fetchObservable = fetchEntityKeys(this.datasource?.entityAliasId, [DataKeyType.attribute],
+ this.entityService, this.aliasController);
+ } else {
+ fetchObservable = of(this.lastFetchedKeys);
+ }
+ }
+ } else {
+ fetchObservable = of([]);
+ }
+ return fetchObservable.pipe(
+ publishReplay(1),
+ refCount()
+ );
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/image-cards-select.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/image-cards-select.component.html
index 52bd5907bd..35d7157fb4 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/image-cards-select.component.html
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/image-cards-select.component.html
@@ -33,7 +33,7 @@
{{ option.name }}
-

+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts
index 33b4323fdf..7048c15c86 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts
@@ -300,6 +300,9 @@ import {
import {
ProgressBarWidgetSettingsComponent
} from '@home/components/widget/lib/settings/cards/progress-bar-widget-settings.component';
+import {
+ LiquidLevelCardWidgetSettingsComponent
+} from '@home/components/widget/lib/settings/cards/liquid-level-card-widget-settings.component';
@NgModule({
declarations: [
@@ -410,7 +413,8 @@ import {
WindSpeedDirectionWidgetSettingsComponent,
SignalStrengthWidgetSettingsComponent,
ValueChartCardWidgetSettingsComponent,
- ProgressBarWidgetSettingsComponent
+ ProgressBarWidgetSettingsComponent,
+ LiquidLevelCardWidgetSettingsComponent
],
imports: [
CommonModule,
@@ -526,7 +530,8 @@ import {
WindSpeedDirectionWidgetSettingsComponent,
SignalStrengthWidgetSettingsComponent,
ValueChartCardWidgetSettingsComponent,
- ProgressBarWidgetSettingsComponent
+ ProgressBarWidgetSettingsComponent,
+ LiquidLevelCardWidgetSettingsComponent,
]
})
export class WidgetSettingsModule {
@@ -607,5 +612,6 @@ export const widgetSettingsComponentsMap: {[key: string]: Type
+
+ {{label}}
+
+
+
+ warning
+
+
+ {{errorText}}
+
+
+
+
+
+
+
diff --git a/ui-ngx/src/app/shared/components/string-autocomplete.component.scss b/ui-ngx/src/app/shared/components/string-autocomplete.component.scss
new file mode 100644
index 0000000000..db7a08fac8
--- /dev/null
+++ b/ui-ngx/src/app/shared/components/string-autocomplete.component.scss
@@ -0,0 +1,43 @@
+/**
+ * Copyright © 2016-2023 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+:host {
+ mat-form-field {
+ display: flex;
+ flex: 1 1 0%;
+ max-width: 100%;
+ box-sizing: border-box;
+
+ .tb-autocomplete.tb-option-input-autocomplete {
+ .mat-mdc-option {
+ border-bottom: none;
+
+ .mdc-list-item__primary-text {
+ flex: 1;
+ display: flex;
+ flex-direction: row;
+ gap: 8px;
+
+ .tb-option {
+ font-size: 14px;
+ font-weight: 400;
+ line-height: 20px;
+ letter-spacing: 0.2px;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/ui-ngx/src/app/shared/components/string-autocomplete.component.ts b/ui-ngx/src/app/shared/components/string-autocomplete.component.ts
new file mode 100644
index 0000000000..cf0e2b6253
--- /dev/null
+++ b/ui-ngx/src/app/shared/components/string-autocomplete.component.ts
@@ -0,0 +1,178 @@
+///
+/// Copyright © 2016-2023 The Thingsboard Authors
+///
+/// Licensed under the Apache License, Version 2.0 (the "License");
+/// you may not use this file except in compliance with the License.
+/// You may obtain a copy of the License at
+///
+/// http://www.apache.org/licenses/LICENSE-2.0
+///
+/// Unless required by applicable law or agreed to in writing, software
+/// distributed under the License is distributed on an "AS IS" BASIS,
+/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+/// See the License for the specific language governing permissions and
+/// limitations under the License.
+///
+
+import {
+ Component,
+ Input,
+ forwardRef,
+ OnInit,
+ ViewChild,
+ ElementRef
+} from '@angular/core';
+import {
+ ControlValueAccessor,
+ NG_VALUE_ACCESSOR,
+ FormControl,
+ Validators,
+ FormBuilder
+} from '@angular/forms';
+import { Observable, of } from 'rxjs';
+import { tap, map, switchMap, take } from 'rxjs/operators';
+import { TranslateService } from '@ngx-translate/core';
+import { coerceBoolean } from '@shared/decorators/coercion';
+
+@Component({
+ selector: 'tb-string-autocomplete',
+ templateUrl: './string-autocomplete.component.html',
+ styleUrls: ['./string-autocomplete.component.scss'],
+ providers: [
+ {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => StringAutocompleteComponent),
+ multi: true
+ }
+ ]
+})
+export class StringAutocompleteComponent implements ControlValueAccessor, OnInit {
+
+ @Input()
+ disabled: boolean;
+
+ @coerceBoolean()
+ @Input()
+ required: boolean = false;
+
+ @Input() fetchOptionsFn: (searchText?: string) => Observable>;
+
+ @ViewChild('nameInput', {static: true}) nameInput: ElementRef;
+
+ @Input()
+ placeholderText: string = this.translate.instant('widget-config.set');
+
+ @Input()
+ subscriptSizing: string = 'dynamic';
+
+ @Input()
+ ngClass: string | string[] | Set | { [klass: string]: any; } = 'tb-inline-field tb-suffix-show-on-hover';
+
+ @Input()
+ appearance: string = 'outline';
+
+ @Input()
+ label: string;
+
+ @Input()
+ tooltipClass: string = 'tb-error-tooltip';
+
+ @Input()
+ errorText: string;
+
+ @coerceBoolean()
+ @Input()
+ showInlineError: boolean = false;
+
+ selectionFormControl: FormControl;
+
+ modelValue: string | null;
+
+ filteredOptions$: Observable>;
+
+ searchText = '';
+
+ private dirty = false;
+
+ private propagateChange = (_val: any) => {};
+
+ constructor(private fb: FormBuilder,
+ private translate: TranslateService) {
+ }
+
+ ngOnInit() {
+ this.selectionFormControl = this.fb.control('', this.required ? [Validators.required] : []);
+ this.filteredOptions$ = this.selectionFormControl.valueChanges
+ .pipe(
+ tap(value => this.updateView(value)),
+ map(value => value ? value : ''),
+ switchMap(value => this.fetchOptionsFn ? this.fetchOptionsFn(value) : of([]))
+ );
+ }
+
+ writeValue(option?: string): void {
+ this.searchText = '';
+ this.modelValue = option ? option : null;
+
+ if (this.fetchOptionsFn) {
+ this.fetchOptionsFn(option)
+ .pipe(
+ map(options => {
+ if (options) {
+ const foundOption = options.find(opt => opt === option);
+ return foundOption ? foundOption : option;
+ }
+
+ return option;
+ }),
+ take(1)
+ )
+ .subscribe(result => {
+ this.selectionFormControl.patchValue(result, { emitEvent: false });
+ this.dirty = true;
+ });
+ } else {
+ this.selectionFormControl.patchValue(null, { emitEvent: false });
+ this.dirty = true;
+ }
+ }
+
+ onFocus() {
+ if (this.dirty) {
+ this.selectionFormControl.updateValueAndValidity({onlySelf: true, emitEvent: true});
+ this.dirty = false;
+ }
+ }
+
+ updateView(value: string) {
+ this.searchText = value ? value : '';
+ if (this.modelValue !== value) {
+ this.modelValue = value;
+ this.propagateChange(this.modelValue);
+ }
+ }
+
+ registerOnChange(fn: any): void {
+ this.propagateChange = fn;
+ }
+
+ registerOnTouched(fn: any): void {
+ }
+
+ setDisabledState(isDisabled: boolean): void {
+ this.disabled = isDisabled;
+ if (this.disabled) {
+ this.selectionFormControl.disable({emitEvent: false});
+ } else {
+ this.selectionFormControl.enable({emitEvent: false});
+ }
+ }
+
+ clear() {
+ this.selectionFormControl.patchValue(null, {emitEvent: true});
+ setTimeout(() => {
+ this.nameInput.nativeElement.blur();
+ this.nameInput.nativeElement.focus();
+ }, 0);
+ }
+}
diff --git a/ui-ngx/src/app/shared/components/unit-input.component.html b/ui-ngx/src/app/shared/components/unit-input.component.html
index c15b232444..b0f3625eb0 100644
--- a/ui-ngx/src/app/shared/components/unit-input.component.html
+++ b/ui-ngx/src/app/shared/components/unit-input.component.html
@@ -20,13 +20,20 @@
placeholder="{{ 'widget-config.set' | translate }}"
(focusin)="onFocus()"
[matAutocomplete]="unitsAutocomplete">
-
+
+ warning
+
>;
@@ -73,7 +87,7 @@ export class UnitInputComponent implements ControlValueAccessor, OnInit {
}
ngOnInit() {
- this.unitsFormControl = this.fb.control('', []);
+ this.unitsFormControl = this.fb.control('', this.required ? [Validators.required] : []);
this.filteredUnits = this.unitsFormControl.valueChanges
.pipe(
tap(value => {
@@ -157,11 +171,16 @@ export class UnitInputComponent implements ControlValueAccessor, OnInit {
private unitsConstant(): Observable> {
if (this.fetchUnits$ === null) {
this.fetchUnits$ = getUnits(this.resourcesService).pipe(
- map(units => units.map(u => ({
- symbol: u.symbol,
- name: this.translate.instant(u.name),
- tags: u.tags
- }))),
+ map((units) => {
+ if (this.tagFilter) {
+ units = units.filter(u => u.tags.includes(this.tagFilter));
+ }
+ return units.map(u => ({
+ symbol: u.symbol,
+ name: this.translate.instant(u.name),
+ tags: u.tags
+ }));
+ }),
shareReplay(1)
);
}
diff --git a/ui-ngx/src/app/shared/models/unit.models.ts b/ui-ngx/src/app/shared/models/unit.models.ts
index 7d9f88a068..408c0b23b9 100644
--- a/ui-ngx/src/app/shared/models/unit.models.ts
+++ b/ui-ngx/src/app/shared/models/unit.models.ts
@@ -23,6 +23,15 @@ export interface Unit {
tags: string[];
}
+export enum UnitsType {
+ capacity = 'capacity'
+}
+
+export enum Units {
+ percent = '%',
+ liters = 'L'
+}
+
export const unitBySymbol = (_units: Array, symbol: string): Unit => _units.find(u => u.symbol === symbol);
const searchUnitTags = (unit: Unit, searchText: string): boolean =>
diff --git a/ui-ngx/src/app/shared/models/widget-settings.models.ts b/ui-ngx/src/app/shared/models/widget-settings.models.ts
index bd10859e55..165a4369bc 100644
--- a/ui-ngx/src/app/shared/models/widget-settings.models.ts
+++ b/ui-ngx/src/app/shared/models/widget-settings.models.ts
@@ -401,6 +401,33 @@ export const textStyle = (font?: Font, letterSpacing = 'normal'): ComponentStyle
return style;
};
+export const inlineTextStyle = (font?: Font, letterSpacing = 'normal'): ComponentStyle => {
+ const style: ComponentStyle = {
+ letterSpacing
+ };
+ if (font?.style) {
+ style['font-style'] = font.style;
+ }
+ if (font?.weight) {
+ style['font-weight'] = font.weight;
+ }
+ if (font?.lineHeight) {
+ style['line-height'] = font.lineHeight;
+ }
+ if (font?.size) {
+ style['font-size'] = (font.size + (font.sizeUnit || 'px'));
+ }
+ if (font?.family) {
+ style['font-family'] = font.family +
+ (font.family !== 'Roboto' ? ', Roboto' : '');
+ }
+ return style;
+};
+
+export const cssTextFromInlineStyle = (styleObj: { [key: string]: string | number }): string => Object.entries(styleObj)
+ .map(([key, value]) => `${key}: ${value}`)
+ .join('; ');
+
export const isFontSet = (font: Font): boolean => (!!font && !!font.style && !!font.weight && !!font.size && !!font.family);
export const isFontPartiallySet = (font: Font): boolean => (!!font && (!!font.style || !!font.weight || !!font.size || !!font.family));
diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts
index 2211f2e0e1..042e453fe1 100644
--- a/ui-ngx/src/app/shared/shared.module.ts
+++ b/ui-ngx/src/app/shared/shared.module.ts
@@ -199,6 +199,7 @@ import { MaterialIconsComponent } from '@shared/components/material-icons.compon
import { ColorPickerPanelComponent } from '@shared/components/color-picker/color-picker-panel.component';
import { TbIconComponent } from '@shared/components/icon.component';
import { HintTooltipIconComponent } from '@shared/components/hint-tooltip-icon.component';
+import { StringAutocompleteComponent } from '@shared/components/string-autocomplete.component';
export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) {
return markedOptionsService;
@@ -374,6 +375,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
ToggleOption,
ToggleSelectComponent,
UnitInputComponent,
+ StringAutocompleteComponent,
MaterialIconsComponent,
RuleChainSelectComponent,
TbIconComponent,
@@ -609,6 +611,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
ToggleOption,
ToggleSelectComponent,
UnitInputComponent,
+ StringAutocompleteComponent,
MaterialIconsComponent,
RuleChainSelectComponent,
TbIconComponent,
diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/indicator/shape_attribute_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/indicator/shape_attribute_fn.md
new file mode 100644
index 0000000000..94ef69521e
--- /dev/null
+++ b/ui-ngx/src/assets/help/en_US/widget/lib/indicator/shape_attribute_fn.md
@@ -0,0 +1,69 @@
+#### Shape attribute name
+
+A string expression value that allows you dynamically select shape image depending on attribute name.
+
+**Attribute values that could be used to add the image:**
+
+
+
+```
+Vertical Oval
+{:copy-code}
+```
+
+
+```
+Vertical Cylinder
+{:copy-code}
+```
+
+
+```
+Vertical Capsule
+{:copy-code}
+```
+
+
+```
+Rectangle
+{:copy-code}
+```
+
+
+```
+Horizontal Oval
+{:copy-code}
+```
+
+
+```
+Horizontal Ellipse
+{:copy-code}
+```
+
+
+```
+Horizontal Dish Ends
+{:copy-code}
+```
+
+
+```
+Horizontal Cylinder
+{:copy-code}
+```
+
+
+```
+Horizontal Capsule
+{:copy-code}
+```
+
+
+```
+Horizontal 2:1 Elliptical
+{:copy-code}
+```
+
+
+
diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json
index f5c09ce75f..ae795aeada 100644
--- a/ui-ngx/src/assets/locale/locale.constant-en_US.json
+++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json
@@ -4229,20 +4229,20 @@
"carat": "Carat",
"cubic-millimeter": "Cubic Millimeter",
"cubic-centimeter": "Cubic Centimeter",
- "cubic-meter": "Cubic Meter/s",
- "cubic-kilometer": "Cubic Kilometers",
+ "cubic-meter": "Cubic Meter",
+ "cubic-kilometer": "Cubic Kilometer",
"microliter": "Microliter",
"milliliter": "Milliliter",
"liter": "Liter",
"hectoliter": "Hectolitre",
"cubic-inch": "Cubic Inch",
"cubic-foot": "Cubic Foot",
- "cubic-yard": "Cubic Yards",
+ "cubic-yard": "Cubic Yard",
"fluid-ounce": "Fluid Ounce",
"pint": "Pint",
"quart": "Quart",
"gallon": "Gallon",
- "oil-barrels": "Oil Barrels",
+ "oil-barrels": "Oil Barrel",
"cubic-meter-per-kilogram": "Cubic Meter per Kilogram",
"gill": "Gill",
"hogshead": "Hogshead",
@@ -4286,7 +4286,6 @@
"ton-force-per-square-inch": "Ton-force per Square Inch",
"kilonewton-per-square-meter": "Kilonewton per Square Meter",
"newton-per-square-millimeter": "Newton per Square Millimeter",
-
"microjoule": "Microjoule",
"millijoule": "Millijoule",
"joule": "Joule",
@@ -5029,7 +5028,9 @@
"show-card-buttons": "Show card buttons",
"card-border-radius": "Card border radius",
"card-appearance": "Card appearance",
- "color": "Color"
+ "color": "Color",
+ "tooltip": "Tooltip",
+ "units-required": "Unit is required."
},
"widget-type": {
"import": "Import widget type",
@@ -6065,6 +6066,50 @@
"value-card-style": "Value card style",
"auto-scale": "Auto scale"
},
+ "liquid-level-card": {
+ "layout-simple": "Simple",
+ "layout-percentage": "Percentage",
+ "layout-absolute": "Absolute",
+ "layout": "Layout",
+ "background-overlay": "Value background overlay",
+ "total-volume": "Total volume",
+ "tank": "Tank",
+ "shape": "Shape",
+ "datasource-units": "Source units",
+ "widget-units": "Widget units",
+ "decimals": "Decimals",
+ "liquid": "Liquid",
+ "liquid-color": "Liquid color",
+ "value": "Value",
+ "value-font": "Value font",
+ "level": "Level",
+ "last-update": "Last update",
+ "shape-by-attribute": "Set tank shape by attribute name",
+ "tooltip-background": "Background color",
+ "background-blur": "Background blur",
+ "tank-color": "Tank color",
+ "static": "Static",
+ "see-examples": "See examples",
+ "attribute": "Attribute",
+ "shape-type": "Type",
+ "v-oval": "Vertical Oval",
+ "v-cylinder": "Vertical Cylinder",
+ "v-capsule": "Vertical Capsule",
+ "rectangle": "Rectangle",
+ "h-oval": "Horizontal Oval",
+ "h-ellipse": "Horizontal Ellipse",
+ "h-dish-ends": "Horizontal Dish Ends",
+ "h-cylinder": "Horizontal Cylinder",
+ "h-capsule": "Horizontal Capsule",
+ "h-elliptical_2_1": "Horizontal 2:1 Elliptical",
+ "icon": "Card icon",
+ "title": "Card title",
+ "units": "Units",
+ "color-and-font": "Color and font",
+ "shape-attribute-name": "Attribute name",
+ "total-volume-required": "Total volume is required.",
+ "attribute-name-required": "Attribute name is required."
+ },
"aggregated-value-card": {
"subtitle": "Subtitle",
"chart": "Chart",
diff --git a/ui-ngx/src/assets/metadata/units.json b/ui-ngx/src/assets/metadata/units.json
index 5d680c6e2c..3237d6620b 100644
--- a/ui-ngx/src/assets/metadata/units.json
+++ b/ui-ngx/src/assets/metadata/units.json
@@ -249,7 +249,7 @@
},
{
"name": "unit.liter",
- "symbol": "l",
+ "symbol": "L",
"tags": ["volume","capacity","extent","liter","liters","l"]
},
{
@@ -1113,7 +1113,7 @@
"symbol": "%",
"tags": ["power source","state of charge (SoC)","battery","battery level","level","humidity","moisture","percentage",
"relative humidity","water content","soil moisture","irrigation","water in soil","soil water content","VWC",
- "Volumetric Water Content","Total Harmonic Distortion","THD","power quality","UV Transmittance","%"]
+ "Volumetric Water Content","Total Harmonic Distortion","THD","power quality","UV Transmittance","%", "capacity"]
},
{
"name": "unit.rssi",
diff --git a/ui-ngx/src/assets/widget/liquid-level/shapes/horizontal-2_1-elliptical.svg b/ui-ngx/src/assets/widget/liquid-level/shapes/horizontal-2_1-elliptical.svg
new file mode 100644
index 0000000000..ee436787ce
--- /dev/null
+++ b/ui-ngx/src/assets/widget/liquid-level/shapes/horizontal-2_1-elliptical.svg
@@ -0,0 +1,50 @@
+
diff --git a/ui-ngx/src/assets/widget/liquid-level/shapes/horizontal-capsule.svg b/ui-ngx/src/assets/widget/liquid-level/shapes/horizontal-capsule.svg
new file mode 100644
index 0000000000..3f587b065d
--- /dev/null
+++ b/ui-ngx/src/assets/widget/liquid-level/shapes/horizontal-capsule.svg
@@ -0,0 +1,49 @@
+
diff --git a/ui-ngx/src/assets/widget/liquid-level/shapes/horizontal-cylinder.svg b/ui-ngx/src/assets/widget/liquid-level/shapes/horizontal-cylinder.svg
new file mode 100644
index 0000000000..b766e3e619
--- /dev/null
+++ b/ui-ngx/src/assets/widget/liquid-level/shapes/horizontal-cylinder.svg
@@ -0,0 +1,56 @@
+
diff --git a/ui-ngx/src/assets/widget/liquid-level/shapes/horizontal-dish-ends.svg b/ui-ngx/src/assets/widget/liquid-level/shapes/horizontal-dish-ends.svg
new file mode 100644
index 0000000000..d0366e4d64
--- /dev/null
+++ b/ui-ngx/src/assets/widget/liquid-level/shapes/horizontal-dish-ends.svg
@@ -0,0 +1,50 @@
+
diff --git a/ui-ngx/src/assets/widget/liquid-level/shapes/horizontal-ellipse.svg b/ui-ngx/src/assets/widget/liquid-level/shapes/horizontal-ellipse.svg
new file mode 100644
index 0000000000..40f76dc505
--- /dev/null
+++ b/ui-ngx/src/assets/widget/liquid-level/shapes/horizontal-ellipse.svg
@@ -0,0 +1,61 @@
+
diff --git a/ui-ngx/src/assets/widget/liquid-level/shapes/horizontal-oval.svg b/ui-ngx/src/assets/widget/liquid-level/shapes/horizontal-oval.svg
new file mode 100644
index 0000000000..81430aa60a
--- /dev/null
+++ b/ui-ngx/src/assets/widget/liquid-level/shapes/horizontal-oval.svg
@@ -0,0 +1,61 @@
+
diff --git a/ui-ngx/src/assets/widget/liquid-level/shapes/rectangle.svg b/ui-ngx/src/assets/widget/liquid-level/shapes/rectangle.svg
new file mode 100644
index 0000000000..8be992be46
--- /dev/null
+++ b/ui-ngx/src/assets/widget/liquid-level/shapes/rectangle.svg
@@ -0,0 +1,49 @@
+
diff --git a/ui-ngx/src/assets/widget/liquid-level/shapes/vertical-capsule.svg b/ui-ngx/src/assets/widget/liquid-level/shapes/vertical-capsule.svg
new file mode 100644
index 0000000000..92af4943cd
--- /dev/null
+++ b/ui-ngx/src/assets/widget/liquid-level/shapes/vertical-capsule.svg
@@ -0,0 +1,49 @@
+
diff --git a/ui-ngx/src/assets/widget/liquid-level/shapes/vertical-cylinder.svg b/ui-ngx/src/assets/widget/liquid-level/shapes/vertical-cylinder.svg
new file mode 100644
index 0000000000..a62467e02a
--- /dev/null
+++ b/ui-ngx/src/assets/widget/liquid-level/shapes/vertical-cylinder.svg
@@ -0,0 +1,58 @@
+
diff --git a/ui-ngx/src/assets/widget/liquid-level/shapes/vertical-oval.svg b/ui-ngx/src/assets/widget/liquid-level/shapes/vertical-oval.svg
new file mode 100644
index 0000000000..ec8176a51f
--- /dev/null
+++ b/ui-ngx/src/assets/widget/liquid-level/shapes/vertical-oval.svg
@@ -0,0 +1,56 @@
+
diff --git a/ui-ngx/src/form.scss b/ui-ngx/src/form.scss
index 93272af3ca..3b29fe986c 100644
--- a/ui-ngx/src/form.scss
+++ b/ui-ngx/src/form.scss
@@ -368,14 +368,20 @@
.mat-mdc-text-field-wrapper {
.mat-mdc-form-field-icon-suffix {
padding: 0;
- display: none;
+ display: flex;
+ align-items: center;
+
+ > *:not(.tb-suffix-show-always) {
+ display: none;
+ }
}
}
&:hover {
.mat-mdc-text-field-wrapper {
.mat-mdc-form-field-icon-suffix {
- display: flex;
- align-items: center;
+ > * {
+ display: block;
+ }
}
}
}
diff --git a/ui-ngx/src/styles.scss b/ui-ngx/src/styles.scss
index 0ee154cf7d..ee6f6374cf 100644
--- a/ui-ngx/src/styles.scss
+++ b/ui-ngx/src/styles.scss
@@ -342,6 +342,12 @@ mat-label {
}
}
+mat-icon {
+ &.tb-error {
+ color: rgb(221, 44, 0);
+ }
+}
+
.tb-error-messages {
height: 24px; //30px
margin-top: -6px;
@@ -356,6 +362,13 @@ mat-label {
color: rgb(221, 44, 0);
}
+.mat-mdc-tooltip.tb-error-tooltip {
+ .mdc-tooltip__surface {
+ background-color: rgba(209, 39, 48, 0.12);
+ color: rgba(209, 39, 48, 1);
+ }
+}
+
.tb-autocomplete {
.mat-mdc-option {
border-bottom: 1px solid #eee;