@ -0,0 +1,334 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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. |
||||
|
|
||||
|
--> |
||||
|
<ng-container *ngIf="levelCardWidgetConfigForm" [formGroup]="levelCardWidgetConfigForm"> |
||||
|
<tb-timewindow-config-panel *ngIf="displayTimewindowConfig" |
||||
|
[onlyHistoryTimewindow]="onlyHistoryTimewindow()" |
||||
|
formControlName="timewindowConfig"> |
||||
|
</tb-timewindow-config-panel> |
||||
|
<tb-datasources |
||||
|
[configMode]="basicMode" |
||||
|
hideDataKeyLabel |
||||
|
hideDataKeyColor |
||||
|
hideDataKeyUnits |
||||
|
hideDataKeyDecimals |
||||
|
formControlName="datasources"> |
||||
|
</tb-datasources> |
||||
|
<div class="tb-form-panel"> |
||||
|
<div class="tb-form-panel-title" translate>widget-config.appearance</div> |
||||
|
<div class="tb-form-row"> |
||||
|
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showTitle"> |
||||
|
{{ 'widgets.liquid-level-card.title' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
||||
|
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput formControlName="title" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
</mat-form-field> |
||||
|
<tb-font-settings formControlName="titleFont" |
||||
|
[previewText]="levelCardWidgetConfigForm.get('title').value"> |
||||
|
</tb-font-settings> |
||||
|
<tb-color-input asBoxInput colorClearButton formControlName="titleColor"> |
||||
|
</tb-color-input> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row"> |
||||
|
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showTitleIcon"> |
||||
|
{{ 'widgets.liquid-level-card.icon' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
<div fxFlex fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="8px"> |
||||
|
<mat-form-field appearance="outline" class="flex number" subscriptSizing="dynamic"> |
||||
|
<input matInput type="number" min="0" formControlName="iconSize" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
</mat-form-field> |
||||
|
<tb-css-unit-select fxFlex formControlName="iconSizeUnit"></tb-css-unit-select> |
||||
|
<tb-material-icon-select asBoxInput |
||||
|
iconClearButton |
||||
|
[color]="levelCardWidgetConfigForm.get('iconColor').value" |
||||
|
formControlName="titleIcon"> |
||||
|
</tb-material-icon-select> |
||||
|
<tb-color-input asBoxInput colorClearButton formControlName="iconColor"> |
||||
|
</tb-color-input> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div fxFlex fxLayout="column" class="tb-form-row space-between"> |
||||
|
<div fxFlex fxLayout="row" style="width: 100%;" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width" translate>widgets.liquid-level-card.shape</div> |
||||
|
<tb-toggle-select formControlName="tankSelectionType"> |
||||
|
<tb-toggle-option [value]="shapeSelectOptions.static"> |
||||
|
{{ 'widgets.liquid-level-card.static' | translate }} |
||||
|
</tb-toggle-option> |
||||
|
<tb-toggle-option [value]="shapeSelectOptions.attribute"> |
||||
|
{{ 'widgets.liquid-level-card.attribute' | translate }} |
||||
|
</tb-toggle-option> |
||||
|
</tb-toggle-select> |
||||
|
</div> |
||||
|
<tb-image-cards-select #shapesImageCardsSelect |
||||
|
[fxShow]="levelCardWidgetConfigForm.get('tankSelectionType')?.value === shapeSelectOptions.static" |
||||
|
rowHeight="{{ '1:1' }}" |
||||
|
[cols]="5" |
||||
|
[colsLtMd]="5" |
||||
|
style="width: 100%;" |
||||
|
label="{{ 'widgets.liquid-level-card.shape-type' | translate }}" formControlName="selectedShape"> |
||||
|
<tb-image-cards-select-option *ngFor="let shape of shapes" |
||||
|
[value]="shape" |
||||
|
[image]="createShapeLayout(shapesImageMap.get(shape), this.levelCardLayouts.simple)"> |
||||
|
{{ shapesTranslationMap.get(shape) | translate }} |
||||
|
</tb-image-cards-select-option> |
||||
|
</tb-image-cards-select> |
||||
|
<div class="tb-form-row space-between" style="width: 100%;" |
||||
|
[fxShow]="levelCardWidgetConfigForm.get('tankSelectionType')?.value === shapeSelectOptions.attribute"> |
||||
|
<div class="fixed-title-width tb-required" translate>widgets.liquid-level-card.shape-attribute-name</div> |
||||
|
<tb-string-autocomplete [fetchOptionsFn]="fetchOptions.bind(this)" |
||||
|
[required]="isRequired('shapeAttributeName')" |
||||
|
[errorText]="'widgets.liquid-level-card.attribute-name-required' | translate" |
||||
|
style="flex: 1; width: auto;" |
||||
|
asBoxInput colorClearButton class="flex" |
||||
|
formControlName="shapeAttributeName"> |
||||
|
</tb-string-autocomplete> |
||||
|
</div> |
||||
|
<div fxFlex fxLayout="row" fxLayoutAlign="space-between center" style="width: 100%;" |
||||
|
[fxShow]="levelCardWidgetConfigForm.get('tankSelectionType')?.value === shapeSelectOptions.attribute"> |
||||
|
<div class="tb-hint" style="padding: 0;" translate>widgets.liquid-level-card.shape-by-attribute</div> |
||||
|
<div class="see-example" |
||||
|
[tb-help-popup]="'widget/lib/indicator/shape_attribute_fn'" |
||||
|
tb-help-popup-placement="left" |
||||
|
trigger-style="font-size: 12px" |
||||
|
[tb-help-popup-style]="{maxWidth: '820px'}" |
||||
|
trigger-text="{{ 'widgets.liquid-level-card.see-examples' | translate }}"> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<tb-image-cards-select #layoutsImageCardsSelect rowHeight="{{ '1:1' }}" |
||||
|
[cols]="3" |
||||
|
[colsLtMd]="3" |
||||
|
label="{{ 'widgets.liquid-level-card.layout' | translate }}" formControlName="layout"> |
||||
|
<tb-image-cards-select-option |
||||
|
*ngFor="let layout of [levelCardLayouts.simple, levelCardLayouts.percentage, levelCardLayouts.absolute]" |
||||
|
[value]="layout" |
||||
|
[image]="createShapeLayout(shapesImageMap.get(levelCardWidgetConfigForm.get('selectedShape').value), layout)"> |
||||
|
{{ levelCardLayoutTranslationMap.get(layout) | translate }} |
||||
|
</tb-image-cards-select-option> |
||||
|
</tb-image-cards-select> |
||||
|
</div> |
||||
|
<div class="tb-form-panel"> |
||||
|
<div class="tb-form-panel-title" translate>widgets.liquid-level-card.units</div> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<div class="fixed-title-width tb-required" translate>widgets.liquid-level-card.datasource-units</div> |
||||
|
<tb-unit-input style="max-width: 30%;" class="flex" |
||||
|
[tagFilter]="unitsType.capacity" |
||||
|
[required]="isRequired('datasourceUnits')" |
||||
|
formControlName="datasourceUnits"> |
||||
|
</tb-unit-input> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between" [fxShow]="widgetUnitsInput"> |
||||
|
<div class="fixed-title-width tb-required" translate>widgets.liquid-level-card.widget-units</div> |
||||
|
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
||||
|
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic" style="max-width: 25%"> |
||||
|
<mat-select formControlName="widgetUnitsSource" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
<mat-option *ngFor="let option of [levelOptions.static, levelOptions.attribute]" [value]="option"> |
||||
|
{{ option }} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
<tb-unit-input [fxShow]="levelCardWidgetConfigForm.get('widgetUnitsSource')?.value !== levelOptions.attribute" |
||||
|
asBoxInput colorClearButton class="flex" |
||||
|
[tagFilter]="unitsType.capacity" |
||||
|
[required]="isRequired('units')" |
||||
|
formControlName="units"> |
||||
|
</tb-unit-input> |
||||
|
<tb-string-autocomplete [fxShow]="levelCardWidgetConfigForm.get('widgetUnitsSource')?.value === levelOptions.attribute" |
||||
|
[fetchOptionsFn]="fetchOptions.bind(this)" |
||||
|
[required]="isRequired('widgetUnitsAttributeName')" |
||||
|
[errorText]="'widgets.liquid-level-card.attribute-name-required' | translate" |
||||
|
style="flex: 1; width: auto;" |
||||
|
asBoxInput colorClearButton class="flex" |
||||
|
formControlName="widgetUnitsAttributeName"> |
||||
|
</tb-string-autocomplete> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row" [fxShow]="volumeInput"> |
||||
|
<div class="fixed-title-width tb-required" translate>widgets.liquid-level-card.total-volume</div> |
||||
|
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
||||
|
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic" style="max-width: 25%"> |
||||
|
<mat-select formControlName="volumeSource" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
<mat-option *ngFor="let option of [levelOptions.static, levelOptions.attribute]" [value]="option"> |
||||
|
{{ option }} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field [fxShow]="levelCardWidgetConfigForm.get('volumeSource')?.value === levelOptions.static" |
||||
|
appearance="outline" class="flex number" subscriptSizing="dynamic"> |
||||
|
<input matInput formControlName="volumeConstant" type="number" |
||||
|
min="0" step="1" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="'widgets.liquid-level-card.total-volume-required' | translate" |
||||
|
*ngIf="levelCardWidgetConfigForm.get('volumeConstant').hasError('required')" |
||||
|
class="material-icons tb-suffix-show-always tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
</mat-form-field> |
||||
|
<tb-string-autocomplete style="max-width: 25%" |
||||
|
[fxShow]="levelCardWidgetConfigForm.get('volumeSource')?.value === levelOptions.attribute" |
||||
|
[fetchOptionsFn]="fetchOptions.bind(this)" |
||||
|
[errorText]="'widgets.liquid-level-card.attribute-name-required' | translate" |
||||
|
[required]="isRequired('volumeAttributeName')" |
||||
|
style="flex: 1; width: auto;" |
||||
|
asBoxInput colorClearButton class="flex" |
||||
|
formControlName="volumeAttributeName"> |
||||
|
</tb-string-autocomplete> |
||||
|
<tb-unit-input asBoxInput colorClearButton |
||||
|
[tagFilter]="unitsType.capacity" |
||||
|
[required]="isRequired('volumeUnits')" |
||||
|
style="max-width: 25%" class="flex" |
||||
|
formControlName="volumeUnits"> |
||||
|
</tb-unit-input> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-panel"> |
||||
|
<div class="tb-form-panel-title" translate>widgets.liquid-level-card.color-and-font</div> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<div class="fixed-title-width" translate>widgets.liquid-level-card.tank-color</div> |
||||
|
<tb-color-settings formControlName="tankColor"> |
||||
|
</tb-color-settings> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<div class="fixed-title-width" translate>widgets.liquid-level-card.liquid-color</div> |
||||
|
<tb-color-settings formControlName="liquidColor"> |
||||
|
</tb-color-settings> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between" |
||||
|
*ngIf="levelCardWidgetConfigForm.get('layout')?.value !== this.levelCardLayouts.simple"> |
||||
|
<div class="fixed-title-width" translate>widgets.liquid-level-card.value</div> |
||||
|
<div fxFlex fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="8px"> |
||||
|
<mat-form-field style="max-width: 12%;" appearance="outline" class="flex number" subscriptSizing="dynamic"> |
||||
|
<input matInput formControlName="decimals" type="number" |
||||
|
min="0" max="15" step="1" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
</mat-form-field> |
||||
|
<tb-font-settings formControlName="valueFont" |
||||
|
[previewText]="valuePreviewFn"> |
||||
|
</tb-font-settings> |
||||
|
<tb-color-settings formControlName="valueColor"> |
||||
|
</tb-color-settings> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row" [fxShow]="volumeInput"> |
||||
|
<div class="fixed-title-width" translate>widgets.liquid-level-card.total-volume</div> |
||||
|
<div fxFlex fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="8px"> |
||||
|
<tb-font-settings formControlName="volumeFont" |
||||
|
[previewText]="totalVolumeValuePreviewFn"> |
||||
|
</tb-font-settings> |
||||
|
<tb-color-settings formControlName="volumeColor"> |
||||
|
</tb-color-settings> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showBackgroundOverlay"> |
||||
|
{{ 'widgets.liquid-level-card.background-overlay' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
<tb-color-settings formControlName="backgroundOverlayColor"> |
||||
|
</tb-color-settings> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-panel tb-slide-toggle"> |
||||
|
<mat-expansion-panel class="tb-settings" |
||||
|
[expanded]="levelCardWidgetConfigForm.get('showTooltip').value" |
||||
|
[disabled]="!levelCardWidgetConfigForm.get('showTooltip').value"> |
||||
|
<mat-expansion-panel-header fxLayout="row wrap"> |
||||
|
<mat-panel-title> |
||||
|
<mat-slide-toggle class="mat-slide" formControlName="showTooltip" (click)="$event.stopPropagation()" |
||||
|
fxLayoutAlign="center"> |
||||
|
{{ 'widget-config.tooltip' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
</mat-panel-title> |
||||
|
</mat-expansion-panel-header> |
||||
|
<ng-template matExpansionPanelContent> |
||||
|
<div class="tb-form-row"> |
||||
|
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showTooltipLevel"> |
||||
|
{{ 'widgets.liquid-level-card.level' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
||||
|
<tb-unit-input [required]="isRequired('tooltipUnits')" |
||||
|
[tagFilter]="unitsType.capacity" class="flex" |
||||
|
formControlName="tooltipUnits"> |
||||
|
</tb-unit-input> |
||||
|
<mat-form-field appearance="outline" class="flex number" subscriptSizing="dynamic"> |
||||
|
<input matInput formControlName="tooltipLevelDecimals" type="number" min="0" |
||||
|
max="15" step="1" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
<div matSuffix fxHide.lt-md translate>widget-config.decimals-suffix</div> |
||||
|
</mat-form-field> |
||||
|
<tb-font-settings formControlName="tooltipLevelFont" |
||||
|
[previewText]="tooltipValuePreviewFn"> |
||||
|
</tb-font-settings> |
||||
|
<tb-color-settings formControlName="tooltipLevelColor"> |
||||
|
</tb-color-settings> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row column-xs"> |
||||
|
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showTooltipDate"> |
||||
|
{{ 'widgets.value-card.date' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
<div fxFlex.gt-xs fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
||||
|
<tb-date-format-select fxFlex formControlName="tooltipDateFormat"></tb-date-format-select> |
||||
|
<tb-font-settings formControlName="tooltipDateFont" |
||||
|
[previewText]="datePreviewFn"> |
||||
|
</tb-font-settings> |
||||
|
<tb-color-settings formControlName="tooltipDateColor"> |
||||
|
</tb-color-settings> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<div class="fixed-title-width" translate>widgets.liquid-level-card.tooltip-background</div> |
||||
|
<tb-color-settings formControlName="tooltipBackgroundColor"> |
||||
|
</tb-color-settings> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<div class="fixed-title-width" translate>widgets.liquid-level-card.background-blur</div> |
||||
|
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic"> |
||||
|
<input matInput formControlName="tooltipBackgroundBlur" type="number" min="0" step="1" |
||||
|
placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
<div matSuffix>px</div> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</ng-template> |
||||
|
</mat-expansion-panel> |
||||
|
</div> |
||||
|
<div class="tb-form-panel"> |
||||
|
<div class="tb-form-panel-title" translate>widget-config.card-appearance</div> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<div>{{ 'widgets.background.background' | translate }}</div> |
||||
|
<tb-color-input asBoxInput colorClearButton formControlName="backgroundColor"> |
||||
|
</tb-color-input> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between column-lt-md"> |
||||
|
<div translate>widget-config.show-card-buttons</div> |
||||
|
<mat-chip-listbox multiple formControlName="cardButtons"> |
||||
|
<mat-chip-option value="fullscreen">{{ 'fullscreen.fullscreen' | translate }}</mat-chip-option> |
||||
|
</mat-chip-listbox> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<div>{{ 'widget-config.card-border-radius' | translate }}</div> |
||||
|
<mat-form-field appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput formControlName="borderRadius" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<tb-widget-actions-panel |
||||
|
formControlName="actions"> |
||||
|
</tb-widget-actions-panel> |
||||
|
</ng-container> |
||||
@ -0,0 +1,656 @@ |
|||||
|
///
|
||||
|
/// 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 { AbstractControl, UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models'; |
||||
|
import { WidgetConfigComponentData } from '@home/models/widget-component.models'; |
||||
|
import { |
||||
|
DataKey, |
||||
|
Datasource, |
||||
|
datasourcesHasAggregation, |
||||
|
datasourcesHasOnlyComparisonAggregation, |
||||
|
DatasourceType, |
||||
|
WidgetConfig, |
||||
|
} from '@shared/models/widget.models'; |
||||
|
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; |
||||
|
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; |
||||
|
import { |
||||
|
getTimewindowConfig, |
||||
|
setTimewindowConfig |
||||
|
} from '@home/components/widget/config/timewindow-config-panel.component'; |
||||
|
import { formatValue, isDefined, isUndefined } from '@core/utils'; |
||||
|
import { |
||||
|
cssSizeToStrSize, |
||||
|
DateFormatProcessor, |
||||
|
DateFormatSettings, |
||||
|
resolveCssSize |
||||
|
} from '@shared/models/widget-settings.models'; |
||||
|
import { |
||||
|
CapacityUnits, |
||||
|
createAbsoluteLayout, |
||||
|
createPercentLayout, |
||||
|
levelCardDefaultSettings, |
||||
|
LevelCardLayout, |
||||
|
levelCardLayoutTranslations, |
||||
|
LevelCardWidgetSettings, |
||||
|
LevelSelectOptions, |
||||
|
optionsFilter, |
||||
|
Shapes, |
||||
|
shapesTranslations, |
||||
|
svgMapping |
||||
|
} from '@home/components/widget/lib/indicator/liquid-level-widget.models'; |
||||
|
import { UnitsType } from '@shared/models/unit.models'; |
||||
|
import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; |
||||
|
import { ImageCardsSelectComponent } from '@home/components/widget/lib/settings/common/image-cards-select.component'; |
||||
|
import { map, publishReplay, refCount, tap } from 'rxjs/operators'; |
||||
|
import { forkJoin, Observable, of } from 'rxjs'; |
||||
|
import { ResourcesService } from '@core/services/resources.service'; |
||||
|
import { UnitInputComponent } from '@shared/components/unit-input.component'; |
||||
|
import { UtilsService } from '@core/services/utils.service'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'liquid-level-card-basic-config', |
||||
|
templateUrl: './liquid-level-card-basic-config.component.html', |
||||
|
styleUrls: ['../basic-config.scss'] |
||||
|
}) |
||||
|
export class LiquidLevelCardBasicConfigComponent extends BasicWidgetConfigComponent { |
||||
|
|
||||
|
@ViewChild('layoutsImageCardsSelect', { static: false }) layoutsImageCardsSelect: ImageCardsSelectComponent; |
||||
|
|
||||
|
@ViewChild('shapesImageCardsSelect', { static: false }) shapesImageCardsSelect: ImageCardsSelectComponent; |
||||
|
|
||||
|
@ViewChild('widgetUnits', { static: false }) widgetUnits: UnitInputComponent; |
||||
|
|
||||
|
public get datasource(): Datasource { |
||||
|
if (this.widgetConfig.config.datasources && this.widgetConfig.config.datasources) { |
||||
|
return this.widgetConfig.config.datasources[0]; |
||||
|
} else { |
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
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; |
||||
|
return !(datasourceUnits === CapacityUnits.percent && layout !== LevelCardLayout.absolute |
||||
|
|| (datasourceUnits === CapacityUnits.percent && units === CapacityUnits.percent)); |
||||
|
} |
||||
|
|
||||
|
public get widgetUnitsInput(): boolean { |
||||
|
const layout: LevelCardLayout = this.levelCardWidgetConfigForm.get('layout').value; |
||||
|
|
||||
|
if (layout === LevelCardLayout.absolute) { |
||||
|
const datasourceUnits = this.levelCardWidgetConfigForm.get('datasourceUnits').value; |
||||
|
return !(datasourceUnits === CapacityUnits.percent); |
||||
|
} |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
public get displayTimewindowConfig(): boolean { |
||||
|
const datasources = this.levelCardWidgetConfigForm.get('datasources').value; |
||||
|
return datasourcesHasAggregation(datasources); |
||||
|
} |
||||
|
|
||||
|
public onlyHistoryTimewindow(): boolean { |
||||
|
const datasources = this.levelCardWidgetConfigForm.get('datasources').value; |
||||
|
return datasourcesHasOnlyComparisonAggregation(datasources); |
||||
|
} |
||||
|
|
||||
|
levelCardLayouts = LevelCardLayout; |
||||
|
|
||||
|
shapes: Shapes[] = []; |
||||
|
|
||||
|
levelOptions = LevelSelectOptions; |
||||
|
|
||||
|
shapeSelectOptions = LevelSelectOptions; |
||||
|
|
||||
|
levelCardLayoutTranslationMap = levelCardLayoutTranslations; |
||||
|
|
||||
|
shapesTranslationMap = shapesTranslations; |
||||
|
shapesImageMap: Map<Shapes, string> = new Map(); |
||||
|
|
||||
|
unitsType = UnitsType; |
||||
|
|
||||
|
levelCardWidgetConfigForm: 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<string>; |
||||
|
|
||||
|
datasources: Array<Datasource>; |
||||
|
|
||||
|
functionTypeKeys: Array<DataKey> = []; |
||||
|
|
||||
|
lastKeysId: string; |
||||
|
|
||||
|
lastFetchedKeys: Array<DataKey>; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
protected widgetConfigComponent: WidgetConfigComponent, |
||||
|
private cd: ChangeDetectorRef, |
||||
|
private $injector: Injector, |
||||
|
private fb: UntypedFormBuilder, |
||||
|
private resourcesService: ResourcesService, |
||||
|
private sanitizer: DomSanitizer, |
||||
|
private utils: UtilsService) { |
||||
|
super(store, widgetConfigComponent); |
||||
|
} |
||||
|
|
||||
|
protected configForm(): UntypedFormGroup { |
||||
|
return this.levelCardWidgetConfigForm; |
||||
|
} |
||||
|
|
||||
|
protected setupConfig(widgetConfig: WidgetConfigComponentData) { |
||||
|
this.shapes = Object.values(Shapes); |
||||
|
this.createSvgShapesMapping(); |
||||
|
|
||||
|
for (const type of this.utils.getPredefinedFunctionsList()) { |
||||
|
this.functionTypeKeys.push({ |
||||
|
name: type, |
||||
|
type: DataKeyType.function |
||||
|
}); |
||||
|
} |
||||
|
super.setupConfig(widgetConfig); |
||||
|
} |
||||
|
|
||||
|
protected setupDefaults(configData: WidgetConfigComponentData) { |
||||
|
this.setupDefaultDatasource(configData, [{ name: 'fuelLevel', label: 'Fuel Level', type: DataKeyType.timeseries }]); |
||||
|
} |
||||
|
|
||||
|
protected onConfigSet(configData: WidgetConfigComponentData) { |
||||
|
const settings: LevelCardWidgetSettings = {...configData.config, ...levelCardDefaultSettings(), ...(configData.config.settings || {})}; |
||||
|
const iconSize = resolveCssSize(configData.config.iconSize); |
||||
|
|
||||
|
this.levelCardWidgetConfigForm = this.fb.group({ |
||||
|
timewindowConfig: [getTimewindowConfig(configData.config), []], |
||||
|
datasources: [configData.config.datasources, []], |
||||
|
|
||||
|
tankSelectionType: [settings.tankSelectionType, []], |
||||
|
selectedShape: [settings.selectedShape, [Validators.required]], |
||||
|
shapeAttributeName: [settings.shapeAttributeName, [Validators.required]], |
||||
|
tankColor: [settings.tankColor, []], |
||||
|
datasourceUnits: [settings.datasourceUnits, [Validators.required]], |
||||
|
|
||||
|
layout: [settings.layout, []], |
||||
|
showTitle: [configData.config.showTitle, []], |
||||
|
title: [settings.title, []], |
||||
|
titleFont: [settings.titleFont, []], |
||||
|
titleColor: [settings.titleColor, []], |
||||
|
showTitleIcon: [configData.config.showTitleIcon, []], |
||||
|
iconSize: [iconSize[0], [Validators.min(0)]], |
||||
|
iconSizeUnit: [iconSize[1], []], |
||||
|
titleIcon: [settings.titleIcon, []], |
||||
|
iconColor: [settings.iconColor, []], |
||||
|
volumeSource: [settings.volumeSource, []], |
||||
|
volumeConstant: [settings.volumeConstant, [Validators.required]], |
||||
|
volumeAttributeName: [settings.volumeAttributeName, [Validators.required]], |
||||
|
volumeUnits: [settings.volumeUnits, [Validators.required]], |
||||
|
volumeFont: [settings.volumeFont, []], |
||||
|
volumeColor: [settings.volumeColor, []], |
||||
|
units: [settings.units, [Validators.required]], |
||||
|
widgetUnitsSource: [settings.widgetUnitsSource, [Validators.required]], |
||||
|
widgetUnitsAttributeName: [settings.widgetUnitsAttributeName, [Validators.required]], |
||||
|
decimals: [settings.decimals, []], |
||||
|
liquidColor: [settings.liquidColor, []], |
||||
|
valueFont: [settings.valueFont, []], |
||||
|
valueColor: [settings.valueColor, []], |
||||
|
showBackgroundOverlay: [settings.showBackgroundOverlay, []], |
||||
|
backgroundOverlayColor: [settings.backgroundOverlayColor, []], |
||||
|
|
||||
|
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, []], |
||||
|
|
||||
|
backgroundColor: [settings.backgroundColor, []], |
||||
|
cardButtons: [this.getCardButtons(configData.config), []], |
||||
|
borderRadius: [configData.config.borderRadius, []], |
||||
|
|
||||
|
actions: [configData.config.actions || {}, []] |
||||
|
}); |
||||
|
|
||||
|
this.levelCardWidgetConfigForm.get('selectedShape').valueChanges.subscribe((shape) => { |
||||
|
this.cd.detectChanges(); |
||||
|
this.layoutsImageCardsSelect?.imageCardsSelectOptions.notifyOnChanges(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
protected prepareOutputConfig(config: any): WidgetConfigComponentData { |
||||
|
setTimewindowConfig(this.widgetConfig.config, config.timewindowConfig); |
||||
|
this.widgetConfig.config.datasources = config.datasources; |
||||
|
|
||||
|
this.widgetConfig.config.settings = this.widgetConfig.config.settings || {}; |
||||
|
|
||||
|
this.widgetConfig.config.settings.tankSelectionType = config.tankSelectionType; |
||||
|
this.widgetConfig.config.settings.selectedShape = config.selectedShape; |
||||
|
this.widgetConfig.config.settings.shapeAttributeName = config.shapeAttributeName; |
||||
|
this.widgetConfig.config.settings.tankColor = config.tankColor; |
||||
|
this.widgetConfig.config.settings.datasourceUnits = config.datasourceUnits; |
||||
|
|
||||
|
this.widgetConfig.config.settings.layout = config.layout; |
||||
|
this.widgetConfig.config.showTitle = config.showTitle; |
||||
|
this.widgetConfig.config.title = config.title; |
||||
|
this.widgetConfig.config.titleFont = config.titleFont; |
||||
|
this.widgetConfig.config.titleColor = config.titleColor; |
||||
|
this.widgetConfig.config.showTitleIcon = config.showTitleIcon; |
||||
|
this.widgetConfig.config.iconSize = cssSizeToStrSize(config.iconSize, config.iconSizeUnit); |
||||
|
this.widgetConfig.config.titleIcon = config.titleIcon; |
||||
|
this.widgetConfig.config.iconColor = config.iconColor; |
||||
|
this.widgetConfig.config.settings.volumeSource = config.volumeSource; |
||||
|
this.widgetConfig.config.settings.volumeConstant = config.volumeConstant; |
||||
|
this.widgetConfig.config.settings.volumeAttributeName = config.volumeAttributeName; |
||||
|
this.widgetConfig.config.settings.volumeUnits = config.volumeUnits; |
||||
|
this.widgetConfig.config.settings.volumeFont = config.volumeFont; |
||||
|
this.widgetConfig.config.settings.volumeColor = config.volumeColor; |
||||
|
this.widgetConfig.config.settings.units = config.units; |
||||
|
this.widgetConfig.config.settings.widgetUnitsSource = config.widgetUnitsSource; |
||||
|
this.widgetConfig.config.settings.widgetUnitsAttributeName = config.widgetUnitsAttributeName; |
||||
|
this.widgetConfig.config.decimals = config.decimals; |
||||
|
this.widgetConfig.config.settings.liquidColor = config.liquidColor; |
||||
|
this.widgetConfig.config.settings.valueFont = config.valueFont; |
||||
|
this.widgetConfig.config.settings.valueColor = config.valueColor; |
||||
|
this.widgetConfig.config.settings.showBackgroundOverlay = config.showBackgroundOverlay; |
||||
|
this.widgetConfig.config.settings.backgroundOverlayColor = config.backgroundOverlayColor; |
||||
|
|
||||
|
this.widgetConfig.config.settings.showTooltip = config.showTooltip; |
||||
|
this.widgetConfig.config.settings.showTooltipLevel = config.showTooltipLevel; |
||||
|
this.widgetConfig.config.settings.tooltipUnits = config.tooltipUnits; |
||||
|
this.widgetConfig.config.settings.tooltipLevelDecimals = config.tooltipLevelDecimals; |
||||
|
this.widgetConfig.config.settings.tooltipLevelFont = config.tooltipLevelFont; |
||||
|
this.widgetConfig.config.settings.tooltipLevelColor = config.tooltipLevelColor; |
||||
|
this.widgetConfig.config.settings.showTooltipDate = config.showTooltipDate; |
||||
|
this.widgetConfig.config.settings.tooltipDateFormat = config.tooltipDateFormat; |
||||
|
this.widgetConfig.config.settings.tooltipDateFont = config.tooltipDateFont; |
||||
|
this.widgetConfig.config.settings.tooltipDateColor = config.tooltipDateColor; |
||||
|
this.widgetConfig.config.settings.tooltipBackgroundColor = config.tooltipBackgroundColor; |
||||
|
this.widgetConfig.config.settings.tooltipBackgroundBlur = config.tooltipBackgroundBlur; |
||||
|
|
||||
|
this.widgetConfig.config.backgroundColor = config.backgroundColor; |
||||
|
this.setCardButtons(config.cardButtons, this.widgetConfig.config); |
||||
|
this.widgetConfig.config.borderRadius = config.borderRadius; |
||||
|
|
||||
|
this.widgetConfig.config.actions = config.actions; |
||||
|
return this.widgetConfig; |
||||
|
} |
||||
|
|
||||
|
protected validatorTriggers(): string[] { |
||||
|
return [ |
||||
|
'showBackgroundOverlay', 'showTooltip', 'showTooltipLevel', |
||||
|
'tankSelectionType', 'datasourceUnits', |
||||
|
'showTitleIcon', 'volumeSource', |
||||
|
'showTooltipDate', 'units', |
||||
|
'layout', 'showTitle', |
||||
|
'widgetUnitsSource' |
||||
|
]; |
||||
|
} |
||||
|
|
||||
|
protected updateValidators(emitEvent: boolean, trigger?: string) { |
||||
|
const emitEventFields = []; |
||||
|
|
||||
|
const datasourceUnits: string = this.levelCardWidgetConfigForm.get('datasourceUnits').value; |
||||
|
const layout: LevelCardLayout = this.levelCardWidgetConfigForm.get('layout').value; |
||||
|
const showTitleIcon: AbstractControl<boolean> = this.levelCardWidgetConfigForm.get('showTitleIcon'); |
||||
|
const volumeSource: AbstractControl<LevelSelectOptions> = this.levelCardWidgetConfigForm.get('volumeSource'); |
||||
|
const units: AbstractControl<string> = this.levelCardWidgetConfigForm.get('units'); |
||||
|
const tooltipUnits: AbstractControl<string> = this.levelCardWidgetConfigForm.get('tooltipUnits'); |
||||
|
const widgetUnitsSource: AbstractControl<LevelSelectOptions> = this.levelCardWidgetConfigForm.get('widgetUnitsSource'); |
||||
|
const showTooltipLevel: AbstractControl<boolean> = this.levelCardWidgetConfigForm.get('showTooltipLevel'); |
||||
|
const showTooltipDate: AbstractControl<boolean> = this.levelCardWidgetConfigForm.get('showTooltipDate'); |
||||
|
const showTooltip: boolean = this.levelCardWidgetConfigForm.get('showTooltip').value; |
||||
|
|
||||
|
if (trigger === 'tankSelectionType') { |
||||
|
const tankSelectionType: LevelSelectOptions = this.levelCardWidgetConfigForm.get('tankSelectionType').value; |
||||
|
if (tankSelectionType === LevelSelectOptions.static) { |
||||
|
this.levelCardWidgetConfigForm.get('selectedShape').enable({emitEvent: false}); |
||||
|
this.levelCardWidgetConfigForm.get('shapeAttributeName').disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.levelCardWidgetConfigForm.get('selectedShape').disable({emitEvent: false}); |
||||
|
this.levelCardWidgetConfigForm.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 && units?.value === CapacityUnits.percent) |
||||
|
) { |
||||
|
volumeSource.disable({emitEvent: false}); |
||||
|
this.levelCardWidgetConfigForm.get('volumeConstant').disable({emitEvent: false}); |
||||
|
this.levelCardWidgetConfigForm.get('volumeAttributeName').disable({emitEvent: false}); |
||||
|
this.levelCardWidgetConfigForm.get('volumeUnits').disable({emitEvent: false}); |
||||
|
this.levelCardWidgetConfigForm.get('volumeFont').disable({emitEvent: false}); |
||||
|
this.levelCardWidgetConfigForm.get('volumeColor').disable({emitEvent: false}); |
||||
|
} else { |
||||
|
volumeSource.enable({emitEvent: false}); |
||||
|
this.levelCardWidgetConfigForm.get('volumeConstant').enable({emitEvent: false}); |
||||
|
this.levelCardWidgetConfigForm.get('volumeAttributeName').enable({emitEvent: false}); |
||||
|
this.levelCardWidgetConfigForm.get('volumeUnits').enable({emitEvent: false}); |
||||
|
this.levelCardWidgetConfigForm.get('volumeFont').enable({emitEvent: false}); |
||||
|
this.levelCardWidgetConfigForm.get('volumeColor').enable({emitEvent: false}); |
||||
|
} |
||||
|
emitEventFields.push('volumeSource', 'volumeConstant', 'volumeAttributeName', 'volumeFont', 'volumeColor', 'volumeUnits'); |
||||
|
} else if (trigger === 'volumeSource') { |
||||
|
if((datasourceUnits !== CapacityUnits.percent) || |
||||
|
(layout === LevelCardLayout.absolute && units?.value !== CapacityUnits.percent)) { |
||||
|
if (volumeSource.value === LevelSelectOptions.static) { |
||||
|
this.levelCardWidgetConfigForm.get('volumeConstant').enable({emitEvent: false}); |
||||
|
this.levelCardWidgetConfigForm.get('volumeAttributeName').disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.levelCardWidgetConfigForm.get('volumeConstant').disable({emitEvent: false}); |
||||
|
this.levelCardWidgetConfigForm.get('volumeAttributeName').enable({emitEvent: false}); |
||||
|
} |
||||
|
emitEventFields.push('volumeConstant', 'volumeAttributeName'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (trigger === 'showTitle') { |
||||
|
const showTitle: boolean = this.levelCardWidgetConfigForm.get('showTitle').value; |
||||
|
if (showTitle) { |
||||
|
this.levelCardWidgetConfigForm.get('title').enable({emitEvent: false}); |
||||
|
this.levelCardWidgetConfigForm.get('titleFont').enable({emitEvent: false}); |
||||
|
this.levelCardWidgetConfigForm.get('titleColor').enable({emitEvent: false}); |
||||
|
showTitleIcon.enable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.levelCardWidgetConfigForm.get('title').disable({emitEvent: false}); |
||||
|
this.levelCardWidgetConfigForm.get('titleFont').disable({emitEvent: false}); |
||||
|
this.levelCardWidgetConfigForm.get('titleColor').disable({emitEvent: false}); |
||||
|
showTitleIcon.disable({emitEvent: false}); |
||||
|
} |
||||
|
emitEventFields.push('title', 'titleFont', 'titleColor', 'showTitleIcon'); |
||||
|
} |
||||
|
|
||||
|
if (showTitleIcon?.value) { |
||||
|
this.levelCardWidgetConfigForm.get('titleIcon').enable({emitEvent: false}); |
||||
|
this.levelCardWidgetConfigForm.get('iconColor').enable({emitEvent: false}); |
||||
|
this.levelCardWidgetConfigForm.get('iconSize').enable({emitEvent: false}); |
||||
|
this.levelCardWidgetConfigForm.get('iconSizeUnit').enable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.levelCardWidgetConfigForm.get('titleIcon').disable({emitEvent: false}); |
||||
|
this.levelCardWidgetConfigForm.get('iconColor').disable({emitEvent: false}); |
||||
|
this.levelCardWidgetConfigForm.get('iconSize').disable({emitEvent: false}); |
||||
|
this.levelCardWidgetConfigForm.get('iconSizeUnit').disable({emitEvent: false}); |
||||
|
} |
||||
|
emitEventFields.push('titleIcon', 'iconColor', 'iconSize', 'iconSizeUnit'); |
||||
|
|
||||
|
if (trigger === 'showBackgroundOverlay') { |
||||
|
const showBackgroundOverlay: boolean = this.levelCardWidgetConfigForm.get('showBackgroundOverlay').value; |
||||
|
if (showBackgroundOverlay) { |
||||
|
this.levelCardWidgetConfigForm.get('backgroundOverlayColor').enable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.levelCardWidgetConfigForm.get('backgroundOverlayColor').disable({emitEvent: false}); |
||||
|
} |
||||
|
emitEventFields.push('backgroundOverlayColor'); |
||||
|
} |
||||
|
|
||||
|
if (trigger === 'showTooltip') { |
||||
|
if (showTooltip) { |
||||
|
showTooltipLevel.enable({emitEvent: false}); |
||||
|
showTooltipDate.enable({emitEvent: false}); |
||||
|
this.levelCardWidgetConfigForm.get('tooltipBackgroundColor').enable(); |
||||
|
this.levelCardWidgetConfigForm.get('tooltipBackgroundBlur').enable(); |
||||
|
} else { |
||||
|
showTooltipLevel.disable({emitEvent: false}); |
||||
|
showTooltipDate.disable({emitEvent: false}); |
||||
|
this.levelCardWidgetConfigForm.get('tooltipBackgroundColor').disable({emitEvent: false}); |
||||
|
this.levelCardWidgetConfigForm.get('tooltipBackgroundBlur').disable({emitEvent: false}); |
||||
|
} |
||||
|
emitEventFields.push('showTooltipLevel', 'showTooltipDate', 'tooltipBackgroundColor', 'tooltipBackgroundBlur'); |
||||
|
} |
||||
|
|
||||
|
if (trigger === 'showTooltipLevel') { |
||||
|
if (showTooltipLevel?.value && !showTooltipLevel.disabled) { |
||||
|
tooltipUnits.enable({emitEvent: false}); |
||||
|
this.levelCardWidgetConfigForm.get('tooltipLevelDecimals').enable({emitEvent: false}); |
||||
|
this.levelCardWidgetConfigForm.get('tooltipLevelFont').enable({emitEvent: false}); |
||||
|
this.levelCardWidgetConfigForm.get('tooltipLevelColor').enable({emitEvent: false}); |
||||
|
} else { |
||||
|
tooltipUnits.disable({emitEvent: false}); |
||||
|
this.levelCardWidgetConfigForm.get('tooltipLevelDecimals').disable({emitEvent: false}); |
||||
|
this.levelCardWidgetConfigForm.get('tooltipLevelFont').disable({emitEvent: false}); |
||||
|
this.levelCardWidgetConfigForm.get('tooltipLevelColor').disable({emitEvent: false}); |
||||
|
} |
||||
|
emitEventFields.push('tooltipUnits', 'tooltipLevelDecimals', 'tooltipLevelFont', 'tooltipLevelColor'); |
||||
|
} |
||||
|
|
||||
|
if (trigger === 'showTooltipDate') { |
||||
|
if (showTooltipDate?.value && !showTooltipDate.disabled) { |
||||
|
this.levelCardWidgetConfigForm.get('tooltipDateFormat').enable({emitEvent: false}); |
||||
|
this.levelCardWidgetConfigForm.get('tooltipDateFont').enable({emitEvent: false}); |
||||
|
this.levelCardWidgetConfigForm.get('tooltipDateColor').enable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.levelCardWidgetConfigForm.get('tooltipDateFormat').disable({emitEvent: false}); |
||||
|
this.levelCardWidgetConfigForm.get('tooltipDateFont').disable({emitEvent: false}); |
||||
|
this.levelCardWidgetConfigForm.get('tooltipDateColor').disable({emitEvent: false}); |
||||
|
} |
||||
|
emitEventFields.push('tooltipDateFormat', 'tooltipDateFont', 'tooltipDateColor'); |
||||
|
} |
||||
|
|
||||
|
if (trigger === 'layout' || trigger === 'datasourceUnits') { |
||||
|
if (layout === LevelCardLayout.simple) { |
||||
|
units.disable({emitEvent: false}); |
||||
|
tooltipUnits.disable({emitEvent: false}); |
||||
|
widgetUnitsSource.setValue(LevelSelectOptions.static, {emitEvent: false}); |
||||
|
widgetUnitsSource.disable({emitEvent: false}); |
||||
|
|
||||
|
this.levelCardWidgetConfigForm.get('decimals').disable({emitEvent: false}); |
||||
|
this.levelCardWidgetConfigForm.get('valueFont').disable({emitEvent: false}); |
||||
|
this.levelCardWidgetConfigForm.get('valueColor').disable({emitEvent: false}); |
||||
|
this.levelCardWidgetConfigForm.get('widgetUnitsAttributeName').disable({emitEvent: false}); |
||||
|
} else if (layout === LevelCardLayout.percentage) { |
||||
|
if (units.value !== CapacityUnits.percent) { |
||||
|
units.setValue(CapacityUnits.percent, {emitEvent: false}); |
||||
|
} |
||||
|
widgetUnitsSource.setValue(LevelSelectOptions.static, {emitEvent: false}); |
||||
|
units.disable({emitEvent: false}); |
||||
|
widgetUnitsSource.disable({emitEvent: false}); |
||||
|
this.levelCardWidgetConfigForm.get('widgetUnitsAttributeName').disable({emitEvent: false}); |
||||
|
|
||||
|
if (tooltipUnits.value !== CapacityUnits.percent) { |
||||
|
tooltipUnits.setValue(CapacityUnits.percent, {emitEvent: false}); |
||||
|
} |
||||
|
tooltipUnits.disable({emitEvent: false}); |
||||
|
} else { |
||||
|
units.enable({emitEvent: false}); |
||||
|
tooltipUnits.enable({emitEvent: false}); |
||||
|
widgetUnitsSource.enable({emitEvent: false}); |
||||
|
|
||||
|
this.levelCardWidgetConfigForm.get('decimals').enable({emitEvent: false}); |
||||
|
this.levelCardWidgetConfigForm.get('valueFont').enable({emitEvent: false}); |
||||
|
this.levelCardWidgetConfigForm.get('valueColor').enable({emitEvent: false}); |
||||
|
this.levelCardWidgetConfigForm.get('widgetUnitsAttributeName').enable({emitEvent: false}); |
||||
|
} |
||||
|
emitEventFields.push('units', 'tooltipUnits', 'decimals', 'valueFont', 'valueColor', 'widgetUnitsSource', 'widgetUnitsAttributeName'); |
||||
|
} else if (trigger === 'widgetUnitsSource') { |
||||
|
if (layout !== LevelCardLayout.percentage) { |
||||
|
if (widgetUnitsSource.value === LevelSelectOptions.static) { |
||||
|
units.enable({emitEvent: false}); |
||||
|
this.levelCardWidgetConfigForm.get('widgetUnitsAttributeName').disable({emitEvent: false}); |
||||
|
} else { |
||||
|
units.disable({emitEvent: false}); |
||||
|
this.levelCardWidgetConfigForm.get('widgetUnitsAttributeName').enable({emitEvent: false}); |
||||
|
} |
||||
|
emitEventFields.push('units', 'widgetUnitsAttributeName'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
for (const controlKey in this.levelCardWidgetConfigForm.controls) { |
||||
|
if (emitEventFields.includes(controlKey)) { |
||||
|
this.levelCardWidgetConfigForm.controls[controlKey].updateValueAndValidity({emitEvent}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private getCardButtons(config: WidgetConfig): string[] { |
||||
|
const buttons: string[] = []; |
||||
|
if (isUndefined(config.enableDataExport) || config.enableDataExport) { |
||||
|
buttons.push('dataExport'); |
||||
|
} |
||||
|
if (isUndefined(config.enableFullscreen) || config.enableFullscreen) { |
||||
|
buttons.push('fullscreen'); |
||||
|
} |
||||
|
return buttons; |
||||
|
} |
||||
|
|
||||
|
private setCardButtons(buttons: string[], config: WidgetConfig) { |
||||
|
config.enableDataExport = buttons.includes('dataExport'); |
||||
|
config.enableFullscreen = buttons.includes('fullscreen'); |
||||
|
} |
||||
|
|
||||
|
private _valuePreviewFn(): string { |
||||
|
const units: string = this.levelCardWidgetConfigForm.get('units').value; |
||||
|
const decimals: number = this.levelCardWidgetConfigForm.get('decimals').value; |
||||
|
return formatValue(22, decimals, units, true); |
||||
|
} |
||||
|
|
||||
|
private _tooltipValuePreviewFn() { |
||||
|
const units: string = this.levelCardWidgetConfigForm.get('tooltipUnits').value; |
||||
|
const decimals: number = this.levelCardWidgetConfigForm.get('tooltipLevelDecimals').value; |
||||
|
return formatValue(32, decimals, units, true); |
||||
|
} |
||||
|
|
||||
|
private _totalVolumeValuePreviewFn() { |
||||
|
const value = this.levelCardWidgetConfigForm.get('volumeConstant').value; |
||||
|
const datasourceUnits = this.levelCardWidgetConfigForm.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.levelCardWidgetConfigForm.get('tooltipDateFormat').value; |
||||
|
const processor = DateFormatProcessor.fromSettings(this.$injector, dateFormat); |
||||
|
processor.update(Date.now()); |
||||
|
return processor.formatted; |
||||
|
} |
||||
|
|
||||
|
private createSvgShapesMapping(): void { |
||||
|
const obsArray: Array<Observable<{svg: string; shape: Shapes}>> = []; |
||||
|
for (const shape of this.shapes) { |
||||
|
const svgUrl = svgMapping.get(shape).svg; |
||||
|
|
||||
|
const obs = this.resourcesService.loadJsonResource<string>(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.levelCardWidgetConfigForm.get(formControlName)?.hasValidator(Validators.required); |
||||
|
} |
||||
|
|
||||
|
public fetchOptions(searchText: string): Observable<Array<string>> { |
||||
|
if (this.keySearchText !== searchText || !this.lastFetchedKeys) { |
||||
|
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<Array<DataKey>> { |
||||
|
let fetchObservable: Observable<Array<DataKey>>; |
||||
|
const callbacks = this.widgetConfigComponent.widgetConfigCallbacks; |
||||
|
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 = callbacks.fetchEntityKeysForDevice(this.datasource?.deviceId, [DataKeyType.attribute]); |
||||
|
} else { |
||||
|
fetchObservable = of(this.lastFetchedKeys); |
||||
|
} |
||||
|
} else { |
||||
|
if (this.lastKeysId !== this.datasource?.entityAliasId || !this.lastFetchedKeys) { |
||||
|
this.lastKeysId = this.datasource.entityAliasId; |
||||
|
fetchObservable = callbacks.fetchEntityKeys(this.datasource?.entityAliasId, [DataKeyType.attribute]); |
||||
|
} else { |
||||
|
fetchObservable = of(this.lastFetchedKeys); |
||||
|
} |
||||
|
} |
||||
|
} else { |
||||
|
this.lastKeysId = null; |
||||
|
fetchObservable = of([]); |
||||
|
} |
||||
|
return fetchObservable.pipe( |
||||
|
publishReplay(1), |
||||
|
refCount() |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,428 @@ |
|||||
|
///
|
||||
|
/// 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 { |
||||
|
ColorSettings, |
||||
|
constantColor, |
||||
|
cssTextFromInlineStyle, |
||||
|
DateFormatSettings, |
||||
|
Font, |
||||
|
lastUpdateAgoDateFormat |
||||
|
} from '@shared/models/widget-settings.models'; |
||||
|
import { DataKey, WidgetConfig } from '@shared/models/widget.models'; |
||||
|
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; |
||||
|
import { Observable, of } from 'rxjs'; |
||||
|
import { singleEntityFilterFromDeviceId } from '@shared/models/query/query.models'; |
||||
|
import { EntityType } from '@shared/models/entity-type.models'; |
||||
|
import { catchError, mergeMap } from 'rxjs/operators'; |
||||
|
import { EntityService } from '@core/http/entity.service'; |
||||
|
import { IAliasController } from '@core/api/widget-api.models'; |
||||
|
|
||||
|
export interface SvgInfo { |
||||
|
svg: string; |
||||
|
limits: SvgLimits; |
||||
|
} |
||||
|
|
||||
|
export interface SvgLimits { |
||||
|
min: number; |
||||
|
max: number; |
||||
|
} |
||||
|
|
||||
|
export interface LevelCardWidgetSettings extends WidgetConfig { |
||||
|
tankSelectionType: LevelSelectOptions; |
||||
|
selectedShape: Shapes; |
||||
|
shapeAttributeName: string; |
||||
|
tankColor: ColorSettings; |
||||
|
datasourceUnits: CapacityUnits; |
||||
|
layout: LevelCardLayout; |
||||
|
volumeSource: LevelSelectOptions; |
||||
|
volumeConstant: number; |
||||
|
volumeAttributeName: string; |
||||
|
volumeUnits: CapacityUnits; |
||||
|
volumeFont: Font; |
||||
|
volumeColor: ColorSettings; |
||||
|
liquidColor: ColorSettings; |
||||
|
valueFont: Font; |
||||
|
widgetUnitsSource: LevelSelectOptions; |
||||
|
widgetUnitsAttributeName: string; |
||||
|
valueColor: ColorSettings; |
||||
|
showBackgroundOverlay: boolean; |
||||
|
backgroundOverlayColor: ColorSettings; |
||||
|
showTooltip: boolean; |
||||
|
showTooltipLevel: boolean; |
||||
|
tooltipUnits: CapacityUnits; |
||||
|
tooltipLevelDecimals: number; |
||||
|
tooltipLevelFont: Font; |
||||
|
tooltipLevelColor: ColorSettings; |
||||
|
showTooltipDate: boolean; |
||||
|
tooltipDateFormat: DateFormatSettings; |
||||
|
tooltipDateFont: Font; |
||||
|
tooltipDateColor: ColorSettings; |
||||
|
tooltipBackgroundColor: ColorSettings; |
||||
|
tooltipBackgroundBlur: number; |
||||
|
} |
||||
|
|
||||
|
export enum Shapes { |
||||
|
vOval = 'Vertical Oval', |
||||
|
vCylinder = 'Vertical Cylinder', |
||||
|
vCapsule = 'Vertical Capsule', |
||||
|
rectangle = 'Rectangle', |
||||
|
hOval = 'Horizontal Oval', |
||||
|
hEllipse = 'Horizontal Ellipse', |
||||
|
hDishEnds = 'Horizontal Dish Ends', |
||||
|
hCylinder = 'Horizontal Cylinder', |
||||
|
hCapsule = 'Horizontal Capsule', |
||||
|
hElliptical_2_1 = 'Horizontal 2:1 Elliptical' |
||||
|
} |
||||
|
|
||||
|
export enum CapacityUnits { |
||||
|
percent = '%', |
||||
|
liters = 'L', |
||||
|
cubicMillimeter = 'mm³', |
||||
|
cubicCentimeter = 'cm³', |
||||
|
cubicMeter = 'm³', |
||||
|
cubicKilometer = 'km³', |
||||
|
milliliter = 'mL', |
||||
|
hectoliter = 'hl', |
||||
|
cubicInch = 'in³', |
||||
|
cubicFoot = 'ft³', |
||||
|
cubicYard = 'yd³', |
||||
|
fluidOunce = 'fl-oz', |
||||
|
pint = 'pt', |
||||
|
quart = 'qt', |
||||
|
gallon = 'gal', |
||||
|
oilBarrels = 'bbl' |
||||
|
} |
||||
|
|
||||
|
export enum LevelCardLayout { |
||||
|
simple = 'simple', |
||||
|
percentage = 'percentage', |
||||
|
absolute = 'absolute' |
||||
|
} |
||||
|
|
||||
|
export enum LevelSelectOptions { |
||||
|
attribute = 'Attribute', |
||||
|
static = 'Static' |
||||
|
} |
||||
|
|
||||
|
export enum ConversionType { |
||||
|
to = 'to', |
||||
|
from = 'from', |
||||
|
} |
||||
|
|
||||
|
export const svgMapping = new Map<string, SvgInfo>( |
||||
|
[ |
||||
|
[ |
||||
|
Shapes.vOval, |
||||
|
{ |
||||
|
svg: 'assets/widget/liquid-level/shapes/vertical-oval.svg', |
||||
|
limits: { min: 170, max: 50 } |
||||
|
} |
||||
|
], |
||||
|
[ |
||||
|
Shapes.vCylinder, |
||||
|
{ |
||||
|
svg: 'assets/widget/liquid-level/shapes/vertical-cylinder.svg', |
||||
|
limits: { min: 161, max: 55 } |
||||
|
} |
||||
|
], |
||||
|
[ |
||||
|
Shapes.vCapsule, |
||||
|
{ |
||||
|
svg: 'assets/widget/liquid-level/shapes/vertical-capsule.svg', |
||||
|
limits: { min: 195, max: 25 } |
||||
|
} |
||||
|
], |
||||
|
[ |
||||
|
Shapes.rectangle, |
||||
|
{ |
||||
|
svg: 'assets/widget/liquid-level/shapes/rectangle.svg', |
||||
|
limits: { min: 168, max: 52 } |
||||
|
} |
||||
|
], |
||||
|
[ |
||||
|
Shapes.hOval, |
||||
|
{ |
||||
|
svg: 'assets/widget/liquid-level/shapes/horizontal-oval.svg', |
||||
|
limits: { min: 160, max: 63 } |
||||
|
} |
||||
|
], |
||||
|
[ |
||||
|
Shapes.hEllipse, |
||||
|
{ |
||||
|
svg: 'assets/widget/liquid-level/shapes/horizontal-ellipse.svg', |
||||
|
limits: { min: 160, max: 65 } |
||||
|
} |
||||
|
], |
||||
|
[ |
||||
|
Shapes.hDishEnds, |
||||
|
{ |
||||
|
svg: 'assets/widget/liquid-level/shapes/horizontal-dish-ends.svg', |
||||
|
limits: { min: 172, max: 50 } |
||||
|
} |
||||
|
], |
||||
|
[ |
||||
|
Shapes.hCylinder, |
||||
|
{ |
||||
|
svg: 'assets/widget/liquid-level/shapes/horizontal-cylinder.svg', |
||||
|
limits: { min: 165, max: 55 } |
||||
|
} |
||||
|
], |
||||
|
[ |
||||
|
Shapes.hCapsule, |
||||
|
{ |
||||
|
svg: 'assets/widget/liquid-level/shapes/horizontal-capsule.svg', |
||||
|
limits: { min: 158, max: 52 } |
||||
|
} |
||||
|
], |
||||
|
[ |
||||
|
Shapes.hElliptical_2_1, |
||||
|
{ |
||||
|
svg: 'assets/widget/liquid-level/shapes/horizontal-2_1-elliptical.svg', |
||||
|
limits: { min: 172, max: 50 } |
||||
|
} |
||||
|
] |
||||
|
] |
||||
|
); |
||||
|
|
||||
|
export const levelCardLayoutTranslations = new Map<LevelCardLayout, string>( |
||||
|
[ |
||||
|
[LevelCardLayout.simple, 'widgets.liquid-level-card.layout-simple'], |
||||
|
[LevelCardLayout.percentage, 'widgets.liquid-level-card.layout-percentage'], |
||||
|
[LevelCardLayout.absolute, 'widgets.liquid-level-card.layout-absolute'] |
||||
|
] |
||||
|
); |
||||
|
|
||||
|
export const shapesTranslations = new Map<Shapes, string>( |
||||
|
[ |
||||
|
[Shapes.vOval, 'widgets.liquid-level-card.v-oval'], |
||||
|
[Shapes.vCylinder, 'widgets.liquid-level-card.v-cylinder'], |
||||
|
[Shapes.vCapsule, 'widgets.liquid-level-card.v-capsule'], |
||||
|
[Shapes.rectangle, 'widgets.liquid-level-card.rectangle'], |
||||
|
[Shapes.hOval, 'widgets.liquid-level-card.h-oval'], |
||||
|
[Shapes.hEllipse, 'widgets.liquid-level-card.h-ellipse'], |
||||
|
[Shapes.hDishEnds, 'widgets.liquid-level-card.h-dish-ends'], |
||||
|
[Shapes.hCylinder, 'widgets.liquid-level-card.h-cylinder'], |
||||
|
[Shapes.hCapsule, 'widgets.liquid-level-card.h-capsule'], |
||||
|
[Shapes.hElliptical_2_1, 'widgets.liquid-level-card.h-elliptical_2_1'] |
||||
|
] |
||||
|
); |
||||
|
|
||||
|
export const levelCardDefaultSettings = (): LevelCardWidgetSettings => ({ |
||||
|
tankSelectionType: LevelSelectOptions.static, |
||||
|
selectedShape: Shapes.vCylinder, |
||||
|
shapeAttributeName: 'tankShape', |
||||
|
tankColor: constantColor('#242770'), |
||||
|
datasourceUnits: CapacityUnits.percent, |
||||
|
layout: LevelCardLayout.percentage, |
||||
|
showTitle: false, |
||||
|
title: 'Liquid level', |
||||
|
titleFont: { |
||||
|
family: 'Roboto', |
||||
|
size: 16, |
||||
|
sizeUnit: 'px', |
||||
|
style: 'normal', |
||||
|
weight: '500', |
||||
|
lineHeight: '1.5' |
||||
|
}, |
||||
|
titleColor: 'rgba(0, 0, 0, 0.87)', |
||||
|
showTitleIcon: false, |
||||
|
titleIcon: 'water_drop', |
||||
|
iconColor: '#5469FF', |
||||
|
volumeSource: LevelSelectOptions.static, |
||||
|
volumeConstant: 500, |
||||
|
volumeUnits: CapacityUnits.liters, |
||||
|
volumeAttributeName: 'volume', |
||||
|
volumeFont: { |
||||
|
family: 'Roboto', |
||||
|
size: 14, |
||||
|
sizeUnit: 'px', |
||||
|
style: 'normal', |
||||
|
weight: '500', |
||||
|
lineHeight: '100%' |
||||
|
}, |
||||
|
volumeColor: constantColor('rgba(0, 0, 0, 0.18)'), |
||||
|
units: CapacityUnits.percent as string, |
||||
|
widgetUnitsSource: LevelSelectOptions.static, |
||||
|
widgetUnitsAttributeName: 'units', |
||||
|
decimals: 0, |
||||
|
liquidColor: constantColor('#7A8BFF'), |
||||
|
valueFont: { |
||||
|
family: 'Roboto', |
||||
|
size: 24, |
||||
|
sizeUnit: 'px', |
||||
|
style: 'normal', |
||||
|
weight: '500', |
||||
|
lineHeight: '100%' |
||||
|
}, |
||||
|
valueColor: constantColor('rgba(0, 0, 0, 0.87)'), |
||||
|
showBackgroundOverlay: true, |
||||
|
backgroundOverlayColor: constantColor('rgba(255, 255, 255, 0.76)'), |
||||
|
showTooltip: true, |
||||
|
showTooltipLevel: true, |
||||
|
tooltipUnits: CapacityUnits.percent, |
||||
|
tooltipLevelDecimals: 0, |
||||
|
tooltipLevelFont: { |
||||
|
family: 'Roboto', |
||||
|
size: 13, |
||||
|
sizeUnit: 'px', |
||||
|
style: 'normal', |
||||
|
weight: '500', |
||||
|
lineHeight: '100%' |
||||
|
}, |
||||
|
tooltipLevelColor: constantColor('rgba(0, 0, 0, 0.76)'), |
||||
|
showTooltipDate: true, |
||||
|
tooltipDateFormat: lastUpdateAgoDateFormat(), |
||||
|
tooltipDateFont: { |
||||
|
family: 'Roboto', |
||||
|
size: 13, |
||||
|
sizeUnit: 'px', |
||||
|
style: 'normal', |
||||
|
weight: '500', |
||||
|
lineHeight: '100%' |
||||
|
}, |
||||
|
tooltipDateColor: constantColor('rgba(0, 0, 0, 0.76)'), |
||||
|
tooltipBackgroundColor: constantColor('rgba(255, 255, 255, 0.76)'), |
||||
|
tooltipBackgroundBlur: 3 |
||||
|
}); |
||||
|
|
||||
|
export const convertLiters = (value: number, units: CapacityUnits, conversionType: ConversionType): number => { |
||||
|
let factor: number; |
||||
|
|
||||
|
switch (units) { |
||||
|
case CapacityUnits.liters: |
||||
|
return value; |
||||
|
case CapacityUnits.cubicMillimeter: |
||||
|
factor = 1e6; |
||||
|
break; |
||||
|
case CapacityUnits.cubicCentimeter: |
||||
|
case CapacityUnits.milliliter: |
||||
|
factor = 1e3; |
||||
|
break; |
||||
|
case CapacityUnits.cubicMeter: |
||||
|
factor = 1e-3; |
||||
|
break; |
||||
|
case CapacityUnits.cubicKilometer: |
||||
|
factor = 1e-15; |
||||
|
break; |
||||
|
case CapacityUnits.hectoliter: |
||||
|
factor = 0.01; |
||||
|
break; |
||||
|
case CapacityUnits.cubicInch: |
||||
|
factor = 61.0237; |
||||
|
break; |
||||
|
case CapacityUnits.cubicFoot: |
||||
|
factor = 1 / 28.3168; |
||||
|
break; |
||||
|
case CapacityUnits.cubicYard: |
||||
|
factor = 1 / 764.555; |
||||
|
break; |
||||
|
case CapacityUnits.fluidOunce: |
||||
|
factor = 33.814; |
||||
|
break; |
||||
|
case CapacityUnits.pint: |
||||
|
factor = 1 / 0.473176; |
||||
|
break; |
||||
|
case CapacityUnits.quart: |
||||
|
factor = 1 / 0.946353; |
||||
|
break; |
||||
|
case CapacityUnits.gallon: |
||||
|
factor = 1 / 3.78541; |
||||
|
break; |
||||
|
case CapacityUnits.oilBarrels: |
||||
|
factor = 1 / 158.987; |
||||
|
break; |
||||
|
default: |
||||
|
return value; |
||||
|
// throw new Error(`Unknown unit: ${units}`);
|
||||
|
} |
||||
|
|
||||
|
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 valueContainerStyleDefaults = cssTextFromInlineStyle({ |
||||
|
width: '100%', |
||||
|
height: '100%', |
||||
|
display: 'flex', |
||||
|
gap: '8px', |
||||
|
'text-align': 'center', |
||||
|
'align-items': 'center', |
||||
|
'align-content': 'center', |
||||
|
'justify-content': 'center', |
||||
|
}); |
||||
|
|
||||
|
export const valueTextStyleDefaults = 'letterSpacing: normal; font-style: normal; font-weight: 500; line-height: 100%;' + |
||||
|
' font-size: 24px; font-family: Roboto; color: #000000DE'; |
||||
|
|
||||
|
export const volumeTextStyleDefaults = 'letterSpacing: normal; font-style: normal; font-weight: 500;' + |
||||
|
' line-height: 100%; font-size: 14px; font-family: Roboto; color: rgba(0, 0, 0, 0.18)'; |
||||
|
|
||||
|
export const createAbsoluteLayout = (values?: {inputValue: number | string; volume: number | string}, |
||||
|
styles?: {valueStyle: string; volumeStyle: string}, |
||||
|
units?: string): string => { |
||||
|
const inputValue = values?.inputValue ? values?.inputValue : 500; |
||||
|
const valueTextStyle = styles?.valueStyle ? styles?.valueStyle : valueTextStyleDefaults; |
||||
|
const volume = values?.volume ? values?.volume : 1000; |
||||
|
const volumeTextStyle = styles?.volumeStyle ? styles?.volumeStyle : volumeTextStyleDefaults; |
||||
|
const displayUnits = units ? units : CapacityUnits.liters; |
||||
|
|
||||
|
return `<div xmlns="http://www.w3.org/1999/xhtml" style="${valueContainerStyleDefaults}">
|
||||
|
<div style="display: flex; flex-direction: column; justify-content: center; align-content: center;"> |
||||
|
<label style="${valueTextStyle}">${inputValue}</label> |
||||
|
<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> |
||||
|
</div> |
||||
|
<label><b>${displayUnits}</b></label> |
||||
|
</div>`;
|
||||
|
}; |
||||
|
|
||||
|
export const createPercentLayout = (value: number | string = 50, valueTextStyle: string = valueTextStyleDefaults): string => |
||||
|
`<div xmlns="http://www.w3.org/1999/xhtml" style="${valueContainerStyleDefaults}">
|
||||
|
<label><span style="${valueTextStyle}">${value}</span> ${CapacityUnits.percent}</label> |
||||
|
</div>`;
|
||||
|
|
||||
|
export const optionsFilter = (searchText: string): ((key: DataKey) => boolean) => |
||||
|
key => key?.name.toUpperCase().includes(searchText?.toUpperCase()); |
||||
|
|
||||
|
export const fetchEntityKeysForDevice = (deviceId: string, dataKeyTypes: Array<DataKeyType>, |
||||
|
entityService: EntityService): Observable<Array<DataKey>> => { |
||||
|
const entityFilter = singleEntityFilterFromDeviceId(deviceId); |
||||
|
return entityService.getEntityKeysByEntityFilter( |
||||
|
entityFilter, |
||||
|
dataKeyTypes, [EntityType.DEVICE], |
||||
|
{ignoreLoading: true, ignoreErrors: true} |
||||
|
).pipe( |
||||
|
catchError(() => of([])) |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
export const fetchEntityKeys = (entityAliasId: string, dataKeyTypes: Array<DataKeyType>, |
||||
|
entityService: EntityService, aliasController: IAliasController): Observable<Array<DataKey>> => |
||||
|
aliasController.getAliasInfo(entityAliasId).pipe( |
||||
|
mergeMap((aliasInfo) => entityService.getEntityKeysByEntityFilter( |
||||
|
aliasInfo.entityFilter, |
||||
|
dataKeyTypes, [], |
||||
|
{ignoreLoading: true, ignoreErrors: true} |
||||
|
).pipe( |
||||
|
catchError(() => of([])) |
||||
|
)), |
||||
|
catchError(() => of([] as Array<DataKey>)) |
||||
|
); |
||||
@ -0,0 +1,510 @@ |
|||||
|
///
|
||||
|
/// 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 { WidgetContext } from '@home/models/widget-component.models'; |
||||
|
import { isDefined, isDefinedAndNotNull, isNumber, isString } from '@core/utils'; |
||||
|
import { |
||||
|
CapacityUnits, |
||||
|
ConversionType, |
||||
|
convertLiters, createAbsoluteLayout, createPercentLayout, |
||||
|
extractValue, |
||||
|
levelCardDefaultSettings, |
||||
|
LevelCardLayout, |
||||
|
LevelCardWidgetSettings, |
||||
|
LevelSelectOptions, |
||||
|
Shapes, |
||||
|
SvgInfo, |
||||
|
SvgLimits, |
||||
|
svgMapping |
||||
|
} from '@home/components/widget/lib/indicator/liquid-level-widget.models'; |
||||
|
import { Observable, of } from 'rxjs'; |
||||
|
import { map, switchMap } from 'rxjs/operators'; |
||||
|
import { EntityId } from '@shared/models/id/entity-id'; |
||||
|
import { |
||||
|
ColorProcessor, |
||||
|
cssTextFromInlineStyle, |
||||
|
DateFormatProcessor, |
||||
|
inlineTextStyle |
||||
|
} from '@shared/models/widget-settings.models'; |
||||
|
import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance; |
||||
|
import { ResourcesService } from '@core/services/resources.service'; |
||||
|
import { NULL_UUID } from '@shared/models/id/has-uuid'; |
||||
|
import { Component, Input, OnInit } from '@angular/core'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-liquid-level-widget', |
||||
|
template: '' |
||||
|
}) |
||||
|
export class LiquidLevelWidgetComponent implements OnInit { |
||||
|
|
||||
|
@Input() |
||||
|
ctx: WidgetContext; |
||||
|
|
||||
|
svgParams: SvgInfo; |
||||
|
|
||||
|
svg: JQuery<SVGElement>; |
||||
|
|
||||
|
tooltip: ITooltipsterInstance; |
||||
|
|
||||
|
overlayContainer: JQuery<HTMLElement>; |
||||
|
|
||||
|
shape: Shapes; |
||||
|
|
||||
|
settings: LevelCardWidgetSettings; |
||||
|
|
||||
|
tankColor: ColorProcessor; |
||||
|
|
||||
|
volumeColor: ColorProcessor; |
||||
|
|
||||
|
valueColor: ColorProcessor; |
||||
|
|
||||
|
liquidColor: ColorProcessor; |
||||
|
|
||||
|
backgroundOverlayColor: ColorProcessor; |
||||
|
|
||||
|
tooltipLevelColor: ColorProcessor; |
||||
|
|
||||
|
tooltipDateFormat: DateFormatProcessor; |
||||
|
|
||||
|
tooltipDateColor: ColorProcessor; |
||||
|
|
||||
|
tooltipBackgroundColor: ColorProcessor; |
||||
|
|
||||
|
volume: number; |
||||
|
|
||||
|
tooltipContent: string; |
||||
|
|
||||
|
widgetUnits: string; |
||||
|
|
||||
|
constructor() { |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.ctx.$scope.liquidLevelWidget = this; |
||||
|
this.settings = {...levelCardDefaultSettings(), ...this.ctx.widgetConfig, ...this.ctx.settings}; |
||||
|
this.declareStyles(); |
||||
|
} |
||||
|
|
||||
|
public onInit() { |
||||
|
this.getData().subscribe(data => { |
||||
|
if (data) { |
||||
|
const { svg, volume, units } = data; |
||||
|
if (svg && isString(svg) && this.ctx.$container) { |
||||
|
this.ctx.$container.html(svg); |
||||
|
this.svg = this.ctx.$container.find('svg'); |
||||
|
this.svg.on('click', this.cardClick.bind(this)); |
||||
|
this.createSVG(); |
||||
|
this.createValueElement(); |
||||
|
|
||||
|
if (this.settings.showTooltip) { |
||||
|
this.createTooltip(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (isDefined(volume) && isNumber(volume)) { |
||||
|
this.volume = volume; |
||||
|
} |
||||
|
|
||||
|
if (units) { |
||||
|
this.widgetUnits = units; |
||||
|
} |
||||
|
|
||||
|
this.update(); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private declareStyles():void { |
||||
|
this.tankColor = ColorProcessor.fromSettings(this.settings.tankColor); |
||||
|
this.volumeColor = ColorProcessor.fromSettings(this.settings.volumeColor); |
||||
|
this.valueColor = ColorProcessor.fromSettings(this.settings.valueColor); |
||||
|
this.liquidColor = ColorProcessor.fromSettings(this.settings.liquidColor); |
||||
|
this.backgroundOverlayColor = ColorProcessor.fromSettings(this.settings.backgroundOverlayColor); |
||||
|
this.tooltipLevelColor = ColorProcessor.fromSettings(this.settings.tooltipLevelColor); |
||||
|
this.tooltipDateColor = ColorProcessor.fromSettings(this.settings.tooltipDateColor); |
||||
|
this.tooltipBackgroundColor = ColorProcessor.fromSettings(this.settings.tooltipBackgroundColor); |
||||
|
|
||||
|
this.tooltipDateFormat = DateFormatProcessor.fromSettings(this.ctx.$injector, this.settings.tooltipDateFormat); |
||||
|
} |
||||
|
|
||||
|
private getData(): Observable<{ svg: string; volume: number; units: string }> { |
||||
|
const entityId: EntityId = { |
||||
|
entityType: this.ctx.datasources[0].entityType, |
||||
|
id: this.ctx.datasources[0].entityId |
||||
|
}; |
||||
|
|
||||
|
return this.getShape(entityId).pipe( |
||||
|
switchMap(shape => { |
||||
|
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 of(null); |
||||
|
}) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
public update() { |
||||
|
if (this.svg) { |
||||
|
this.updateData(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public destroy() { |
||||
|
|
||||
|
} |
||||
|
|
||||
|
private createSVG() { |
||||
|
if (this.svg) { |
||||
|
this.svg.css('display', 'block') |
||||
|
.css('overflow', 'hidden') |
||||
|
.css('width', '100%') |
||||
|
.css('height', '100%'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private updateData() { |
||||
|
const data = this.ctx.data[0]?.data[0]?.map(value => Number(value)); |
||||
|
if (isDefinedAndNotNull(data) && data.length && typeof Number(data[1]) === 'number') { |
||||
|
const percentage = this.convertInputData(Number(data[1])); |
||||
|
this.updateSvg(percentage); |
||||
|
this.updateValueElement(this.convertOutputData(percentage), percentage); |
||||
|
|
||||
|
if (this.settings.showTooltip) { |
||||
|
this.updateTooltip(data); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private createTooltip(): void { |
||||
|
this.tooltipContent = this.getTooltipContent(); |
||||
|
|
||||
|
import('tooltipster').then(() => { |
||||
|
if ($.tooltipster && this.tooltip) { |
||||
|
this.tooltip.destroy(); |
||||
|
this.tooltip = null; |
||||
|
} |
||||
|
this.tooltip = this.svg.tooltipster({ |
||||
|
contentAsHTML: true, |
||||
|
theme: 'tooltipster-shadow', |
||||
|
side: 'top', |
||||
|
delay: 10, |
||||
|
distance: -33, |
||||
|
triggerClose: { |
||||
|
click: true, |
||||
|
tap: true, |
||||
|
scroll: true, |
||||
|
mouseleave: true |
||||
|
}, |
||||
|
animation: 'grow', |
||||
|
updateAnimation: null, |
||||
|
trackOrigin: true, |
||||
|
functionBefore: (instance, helper) => { |
||||
|
instance.content(this.tooltipContent); |
||||
|
}, |
||||
|
functionReady: (instance, helper) => { |
||||
|
const tooltipsterBoxStyles: JQuery.PlainObject = { |
||||
|
backgroundColor: this.getTooltipBackground(), |
||||
|
backdropFilter: `blur(${this.settings.tooltipBackgroundBlur}px)`, |
||||
|
width: '100%' |
||||
|
}; |
||||
|
|
||||
|
$(helper.tooltip).css('max-width', this.svg.width() + 'px'); |
||||
|
$(helper.tooltip).find('.tooltipster-box').css(tooltipsterBoxStyles); |
||||
|
$(helper.tooltip).find('.tooltipster-arrow').empty(); |
||||
|
|
||||
|
instance.reposition(); |
||||
|
} |
||||
|
}).tooltipster('instance'); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private createValueElement(): void { |
||||
|
const containerOverlay = this.ctx.$container.find('.container-overlay'); |
||||
|
const percentageOverlay = this.ctx.$container.find('.percentage-overlay'); |
||||
|
const absoluteOverlay = this.ctx.$container.find('.absolute-overlay'); |
||||
|
|
||||
|
if (this.settings.layout === LevelCardLayout.absolute) { |
||||
|
this.overlayContainer = absoluteOverlay; |
||||
|
percentageOverlay.css('visibility', 'hidden'); |
||||
|
if (!this.settings.showBackgroundOverlay) { |
||||
|
absoluteOverlay.css('visibility', 'hidden'); |
||||
|
} |
||||
|
} else if (this.settings.layout === LevelCardLayout.percentage) { |
||||
|
this.overlayContainer = percentageOverlay; |
||||
|
absoluteOverlay.css('visibility', 'hidden'); |
||||
|
if (!this.settings.showBackgroundOverlay) { |
||||
|
percentageOverlay.css('visibility', 'hidden'); |
||||
|
} |
||||
|
} else { |
||||
|
containerOverlay.css('visibility', 'hidden'); |
||||
|
} |
||||
|
|
||||
|
if (this.overlayContainer) { |
||||
|
this.overlayContainer.attr('fill', this.backgroundOverlayColor.color); |
||||
|
this.overlayContainer.removeAttr('fill-opacity'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private loadSVG(url: string): Observable<string> { |
||||
|
const resourcesService = this.ctx.$injector.get(ResourcesService); |
||||
|
return resourcesService.loadJsonResource(url); |
||||
|
} |
||||
|
|
||||
|
private getShape(entityId: EntityId): Observable<Shapes> { |
||||
|
if (this.settings.tankSelectionType === LevelSelectOptions.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 of(this.settings.selectedShape); |
||||
|
} |
||||
|
|
||||
|
private getSecondaryResources(entityId: EntityId): Observable<{ volume: number; units: string }> { |
||||
|
const attributeNames = this.prepareAttributeNames(); |
||||
|
|
||||
|
if (!attributeNames.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) |
||||
|
})) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
private prepareAttributeNames(): string[] { |
||||
|
const names = []; |
||||
|
if (this.settings.volumeSource !== LevelSelectOptions.static) { |
||||
|
names.push(this.settings.volumeAttributeName); |
||||
|
} |
||||
|
if (this.settings.widgetUnitsSource !== LevelSelectOptions.static) { |
||||
|
names.push(this.settings.widgetUnitsAttributeName); |
||||
|
} |
||||
|
return names; |
||||
|
} |
||||
|
|
||||
|
private updateSvg(percentage: number) { |
||||
|
const yLimits: SvgLimits = { |
||||
|
min: this.svgParams.limits.min, |
||||
|
max: this.svgParams.limits.max |
||||
|
}; |
||||
|
|
||||
|
const newYPos = this.calculatePosition(percentage, yLimits); |
||||
|
this.updateShapeColor(percentage); |
||||
|
this.updateLevel(newYPos, percentage); |
||||
|
} |
||||
|
|
||||
|
private calculatePosition(percentage, limits): number { |
||||
|
return limits.min + (percentage / 100) * (limits.max - limits.min); |
||||
|
} |
||||
|
|
||||
|
private updateTooltip(value: number[]): void { |
||||
|
this.tooltipContent = this.getTooltipContent(value); |
||||
|
|
||||
|
if (this.tooltip) { |
||||
|
this.tooltip.content(this.tooltipContent); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private updateLevel(newY: number, percentage: number): void { |
||||
|
this.liquidColor.update(percentage); |
||||
|
const fill = this.ctx.$container.find('.tb-liquid-fill'); |
||||
|
const surfaces = this.ctx.$container.find('.tb-liquid-surface'); |
||||
|
const surfacePositionAttr = this.shape !== Shapes.vCylinder ? 'y' : 'cy'; |
||||
|
const animationSpeed = 500; |
||||
|
|
||||
|
if (percentage > 1) { |
||||
|
fill.css('visibility', 'visible'); |
||||
|
fill.animate({y : newY}, animationSpeed); |
||||
|
fill.attr('fill', this.liquidColor.color); |
||||
|
|
||||
|
surfaces.each((index, element) => { |
||||
|
const $element = $(element); |
||||
|
$element.css('visibility', 'visible'); |
||||
|
$element.animate({[surfacePositionAttr]: newY}, animationSpeed); |
||||
|
if ($element.hasClass('tb-liquid')) { |
||||
|
$element.attr('fill', this.liquidColor.color); |
||||
|
} |
||||
|
}); |
||||
|
} else { |
||||
|
fill.css('visibility', 'hidden').stop().clearQueue(); |
||||
|
surfaces.each((index, element) => { |
||||
|
$(element).css('visibility', 'hidden').stop().clearQueue(); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private updateShapeColor(value): void { |
||||
|
const shapeStrokes = this.ctx.$container.find('.tb-shape-stroke'); |
||||
|
const shapeFill = this.ctx.$container.find('.tb-shape-fill'); |
||||
|
this.tankColor.update(value); |
||||
|
|
||||
|
shapeStrokes.each((index, element) => { |
||||
|
$(element).attr('stroke', this.tankColor.color); |
||||
|
}); |
||||
|
|
||||
|
shapeFill.each((index, element) => { |
||||
|
$(element).attr('fill', this.tankColor.color); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private updateValueElement(data: number, percentage: number): void { |
||||
|
let content: string; |
||||
|
let container: JQuery<HTMLElement>; |
||||
|
|
||||
|
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 = `<div style="display: flex; flex-direction: column;
|
||||
|
gap: 8px; background-color: transparent">`;
|
||||
|
|
||||
|
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 += '</div>'; |
||||
|
|
||||
|
return content; |
||||
|
} |
||||
|
|
||||
|
private createTooltipContent(labelText: string, contentValue: string, textStyle: string): string { |
||||
|
return `<div style="display: flex; justify-content: space-between; gap: 8px">
|
||||
|
<label style="color: rgba(0, 0, 0, 0.38); font-size: 12px; font-weight: 400; white-space: nowrap;"> |
||||
|
${labelText} |
||||
|
</label> |
||||
|
<label style="${textStyle}"> |
||||
|
${contentValue} |
||||
|
</label> |
||||
|
</div>`;
|
||||
|
} |
||||
|
|
||||
|
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); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,267 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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. |
||||
|
|
||||
|
--> |
||||
|
<ng-container *ngIf="levelCardWidgetSettingsForm" [formGroup]="levelCardWidgetSettingsForm"> |
||||
|
<div style="display: flex; flex-direction: column; gap: 16px;"> |
||||
|
<div class="tb-form-panel"> |
||||
|
<div fxFlex fxLayout="column" class="tb-form-row space-between"> |
||||
|
<div fxFlex fxLayout="row" style="width: 100%;" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width" translate>widgets.liquid-level-card.shape</div> |
||||
|
<tb-toggle-select formControlName="tankSelectionType"> |
||||
|
<tb-toggle-option [value]="shapeSelectOptions.static"> |
||||
|
{{ 'widgets.liquid-level-card.static' | translate }} |
||||
|
</tb-toggle-option> |
||||
|
<tb-toggle-option [value]="shapeSelectOptions.attribute"> |
||||
|
{{ 'widgets.liquid-level-card.attribute' | translate }} |
||||
|
</tb-toggle-option> |
||||
|
</tb-toggle-select> |
||||
|
</div> |
||||
|
<tb-image-cards-select #shapesImageCardsSelect |
||||
|
[fxShow]="levelCardWidgetSettingsForm.get('tankSelectionType')?.value === shapeSelectOptions.static" |
||||
|
rowHeight="{{ '1:1' }}" |
||||
|
[cols]="5" |
||||
|
[colsLtMd]="5" |
||||
|
style="width: 100%;" |
||||
|
label="{{ 'widgets.liquid-level-card.shape-type' | translate }}" formControlName="selectedShape"> |
||||
|
<tb-image-cards-select-option *ngFor="let shape of shapes" |
||||
|
[value]="shape" |
||||
|
[image]="createShapeLayout(shapesImageMap.get(shape), this.levelCardLayouts.simple)"> |
||||
|
{{ shapesTranslationMap.get(shape) | translate }} |
||||
|
</tb-image-cards-select-option> |
||||
|
</tb-image-cards-select> |
||||
|
<div class="tb-form-row space-between" style="width: 100%;" |
||||
|
[fxShow]="levelCardWidgetSettingsForm.get('tankSelectionType')?.value === shapeSelectOptions.attribute"> |
||||
|
<div class="fixed-title-width" translate>widgets.liquid-level-card.shape-attribute-name</div> |
||||
|
<tb-string-autocomplete [fetchOptionsFn]="fetchOptions.bind(this)" |
||||
|
[required]="isRequired('shapeAttributeName')" |
||||
|
[errorText]="'widgets.liquid-level-card.attribute-name-required' | translate" |
||||
|
style="flex: 1; width: auto;" |
||||
|
asBoxInput colorClearButton class="flex" |
||||
|
formControlName="shapeAttributeName"> |
||||
|
</tb-string-autocomplete> |
||||
|
</div> |
||||
|
<div fxFlex fxLayout="row" fxLayoutAlign="space-between center" style="width: 100%;" |
||||
|
[fxShow]="levelCardWidgetSettingsForm.get('tankSelectionType')?.value === shapeSelectOptions.attribute"> |
||||
|
<div class="tb-hint" style="padding: 0;" translate>widgets.liquid-level-card.shape-by-attribute</div> |
||||
|
<div class="see-example" |
||||
|
[tb-help-popup]="'widget/lib/indicator/shape_attribute_fn'" |
||||
|
tb-help-popup-placement="left" |
||||
|
trigger-style="font-size: 12px" |
||||
|
[tb-help-popup-style]="{maxWidth: '820px'}" |
||||
|
trigger-text="{{ 'widgets.liquid-level-card.see-examples' | translate }}"> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-panel"> |
||||
|
<div class="tb-form-panel-title" translate>widgets.liquid-level-card.layout</div> |
||||
|
<tb-image-cards-select #layoutsImageCardsSelect |
||||
|
rowHeight="{{ '1:1' }}" |
||||
|
[cols]="3" |
||||
|
[colsLtMd]="3" |
||||
|
label="{{ 'widgets.liquid-level-card.layout' | translate }}" formControlName="layout"> |
||||
|
<tb-image-cards-select-option |
||||
|
*ngFor="let layout of [levelCardLayouts.simple, levelCardLayouts.percentage, levelCardLayouts.absolute]" |
||||
|
[value]="layout" |
||||
|
[image]="createShapeLayout(shapesImageMap.get(levelCardWidgetSettingsForm.get('selectedShape').value), layout)"> |
||||
|
{{ levelCardLayoutTranslationMap.get(layout) | translate }} |
||||
|
</tb-image-cards-select-option> |
||||
|
</tb-image-cards-select> |
||||
|
</div> |
||||
|
<div class="tb-form-panel"> |
||||
|
<div class="tb-form-panel-title" translate>widgets.liquid-level-card.units</div> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<div class="fixed-title-width tb-required" translate>widgets.liquid-level-card.datasource-units</div> |
||||
|
<tb-unit-input style="max-width: 30%;" class="flex" |
||||
|
[tagFilter]="unitsType.capacity" |
||||
|
[required]="isRequired('datasourceUnits')" |
||||
|
formControlName="datasourceUnits"> |
||||
|
</tb-unit-input> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between" [fxShow]="widgetUnitsInput"> |
||||
|
<div class="fixed-title-width tb-required" translate>widgets.liquid-level-card.widget-units</div> |
||||
|
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
||||
|
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic" style="max-width: 25%"> |
||||
|
<mat-select formControlName="widgetUnitsSource" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
<mat-option *ngFor="let option of [volumeOptions.static, volumeOptions.attribute]" [value]="option"> |
||||
|
{{ option }} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
<tb-unit-input [fxShow]="levelCardWidgetSettingsForm.get('widgetUnitsSource')?.value !== volumeOptions.attribute" |
||||
|
asBoxInput colorClearButton class="flex" |
||||
|
[tagFilter]="unitsType.capacity" |
||||
|
[required]="isRequired('units')" |
||||
|
formControlName="units"> |
||||
|
</tb-unit-input> |
||||
|
<tb-string-autocomplete [fxShow]="levelCardWidgetSettingsForm.get('widgetUnitsSource')?.value === volumeOptions.attribute" |
||||
|
[fetchOptionsFn]="fetchOptions.bind(this)" |
||||
|
[required]="isRequired('widgetUnitsAttributeName')" |
||||
|
[errorText]="'widgets.liquid-level-card.attribute-name-required' | translate" |
||||
|
style="flex: 1; width: auto;" |
||||
|
asBoxInput colorClearButton class="flex" |
||||
|
formControlName="widgetUnitsAttributeName"> |
||||
|
</tb-string-autocomplete> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row" [fxShow]="volumeInput"> |
||||
|
<div class="fixed-title-width tb-required" translate>widgets.liquid-level-card.total-volume</div> |
||||
|
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
||||
|
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic" style="max-width: 25%"> |
||||
|
<mat-select formControlName="volumeSource" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
<mat-option *ngFor="let option of [volumeOptions.static, volumeOptions.attribute]" [value]="option"> |
||||
|
{{ option }} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field [fxShow]="levelCardWidgetSettingsForm.get('volumeSource')?.value === volumeOptions.static" |
||||
|
appearance="outline" class="flex number" subscriptSizing="dynamic"> |
||||
|
<input matInput formControlName="volumeConstant" type="number" |
||||
|
min="0" step="1" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="'widgets.liquid-level-card.total-volume-required' | translate" |
||||
|
*ngIf="levelCardWidgetSettingsForm.get('volumeConstant').hasError('required')" |
||||
|
class="material-icons tb-suffix-show-always tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
</mat-form-field> |
||||
|
<tb-string-autocomplete style="max-width: 25%" |
||||
|
[fxShow]="levelCardWidgetSettingsForm.get('volumeSource')?.value === volumeOptions.attribute" |
||||
|
[fetchOptionsFn]="fetchOptions.bind(this)" |
||||
|
[required]="isRequired('volumeAttributeName')" |
||||
|
[errorText]="'widgets.liquid-level-card.attribute-name-required' | translate" |
||||
|
style="flex: 1; width: auto;" |
||||
|
asBoxInput colorClearButton class="flex" |
||||
|
formControlName="volumeAttributeName"> |
||||
|
</tb-string-autocomplete> |
||||
|
<tb-unit-input asBoxInput colorClearButton |
||||
|
[tagFilter]="unitsType.capacity" |
||||
|
[required]="isRequired('volumeUnits')" |
||||
|
style="max-width: 25%" class="flex" |
||||
|
formControlName="volumeUnits"> |
||||
|
</tb-unit-input> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-panel"> |
||||
|
<div class="tb-form-panel-title" translate>widgets.liquid-level-card.color-and-font</div> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<div class="fixed-title-width" translate>widgets.liquid-level-card.tank-color</div> |
||||
|
<tb-color-settings formControlName="tankColor"> |
||||
|
</tb-color-settings> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<div class="fixed-title-width" translate>widgets.liquid-level-card.liquid-color</div> |
||||
|
<tb-color-settings formControlName="liquidColor"> |
||||
|
</tb-color-settings> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between" |
||||
|
*ngIf="levelCardWidgetSettingsForm.get('layout')?.value !== this.levelCardLayouts.simple"> |
||||
|
<div class="fixed-title-width" translate>widgets.liquid-level-card.value</div> |
||||
|
<div fxFlex fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="8px"> |
||||
|
<tb-font-settings formControlName="valueFont" |
||||
|
[previewText]="valuePreviewFn"> |
||||
|
</tb-font-settings> |
||||
|
<tb-color-settings formControlName="valueColor"> |
||||
|
</tb-color-settings> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row" [fxShow]="volumeInput"> |
||||
|
<div class="fixed-title-width" translate>widgets.liquid-level-card.total-volume</div> |
||||
|
<div fxFlex fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="8px"> |
||||
|
<tb-font-settings formControlName="volumeFont" |
||||
|
[previewText]="totalVolumeValuePreviewFn"> |
||||
|
</tb-font-settings> |
||||
|
<tb-color-settings formControlName="volumeColor"> |
||||
|
</tb-color-settings> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showBackgroundOverlay"> |
||||
|
{{ 'widgets.liquid-level-card.background-overlay' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
<tb-color-settings formControlName="backgroundOverlayColor"> |
||||
|
</tb-color-settings> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-panel tb-slide-toggle"> |
||||
|
<mat-expansion-panel class="tb-settings" |
||||
|
[expanded]="levelCardWidgetSettingsForm.get('showTooltip').value" |
||||
|
[disabled]="!levelCardWidgetSettingsForm.get('showTooltip').value"> |
||||
|
<mat-expansion-panel-header fxLayout="row wrap"> |
||||
|
<mat-panel-title> |
||||
|
<mat-slide-toggle class="mat-slide" formControlName="showTooltip" (click)="$event.stopPropagation()" |
||||
|
fxLayoutAlign="center"> |
||||
|
{{ 'widget-config.tooltip' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
</mat-panel-title> |
||||
|
</mat-expansion-panel-header> |
||||
|
<ng-template matExpansionPanelContent> |
||||
|
<div class="tb-form-row"> |
||||
|
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showTooltipLevel"> |
||||
|
{{ 'widgets.liquid-level-card.level' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
||||
|
<tb-unit-input [tagFilter]="unitsType.capacity" |
||||
|
[required]="isRequired('tooltipUnits')" |
||||
|
class="flex" formControlName="tooltipUnits"> |
||||
|
</tb-unit-input> |
||||
|
<mat-form-field appearance="outline" class="flex number" subscriptSizing="dynamic"> |
||||
|
<input matInput formControlName="tooltipLevelDecimals" type="number" min="0" |
||||
|
max="15" step="1" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
<div matSuffix fxHide.lt-md translate>widget-config.decimals-suffix</div> |
||||
|
</mat-form-field> |
||||
|
<tb-font-settings formControlName="tooltipLevelFont" |
||||
|
[previewText]="tooltipValuePreviewFn"> |
||||
|
</tb-font-settings> |
||||
|
<tb-color-settings formControlName="tooltipLevelColor"> |
||||
|
</tb-color-settings> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row column-xs"> |
||||
|
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showTooltipDate"> |
||||
|
{{ 'widgets.value-card.date' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
<div fxFlex.gt-xs fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
||||
|
<tb-date-format-select fxFlex formControlName="tooltipDateFormat"></tb-date-format-select> |
||||
|
<tb-font-settings formControlName="tooltipDateFont" |
||||
|
[previewText]="datePreviewFn"> |
||||
|
</tb-font-settings> |
||||
|
<tb-color-settings formControlName="tooltipDateColor"> |
||||
|
</tb-color-settings> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<div class="fixed-title-width" translate>widgets.liquid-level-card.tooltip-background</div> |
||||
|
<tb-color-settings formControlName="tooltipBackgroundColor"> |
||||
|
</tb-color-settings> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<div class="fixed-title-width" translate>widgets.liquid-level-card.background-blur</div> |
||||
|
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic"> |
||||
|
<input matInput formControlName="tooltipBackgroundBlur" type="number" min="0" step="1" |
||||
|
placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
<div matSuffix>px</div> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</ng-template> |
||||
|
</mat-expansion-panel> |
||||
|
</div> |
||||
|
</div> |
||||
|
</ng-container> |
||||
@ -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<Shapes, string> = 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<string>; |
||||
|
|
||||
|
datasources: Array<Datasource>; |
||||
|
|
||||
|
functionTypeKeys: Array<DataKey> = []; |
||||
|
|
||||
|
lastKeysId: string; |
||||
|
|
||||
|
lastFetchedKeys: Array<DataKey>; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
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<LevelSelectOptions> = this.levelCardWidgetSettingsForm.get('volumeSource'); |
||||
|
const widgetUnits: AbstractControl<string> = this.levelCardWidgetSettingsForm.get('units'); |
||||
|
const tooltipUnits: AbstractControl<string> = this.levelCardWidgetSettingsForm.get('tooltipUnits'); |
||||
|
const widgetUnitsSource: AbstractControl<LevelSelectOptions> = this.levelCardWidgetSettingsForm.get('widgetUnitsSource'); |
||||
|
const showTooltipLevel: AbstractControl<boolean> = this.levelCardWidgetSettingsForm.get('showTooltipLevel'); |
||||
|
const showTooltipDate: AbstractControl<boolean> = 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<Observable<{svg: string; shape: Shapes}>> = []; |
||||
|
for (const shape of this.shapes) { |
||||
|
const svgUrl = svgMapping.get(shape).svg; |
||||
|
|
||||
|
const obs = this.resourcesService.loadJsonResource<string>(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<Array<string>> { |
||||
|
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<Array<DataKey>> { |
||||
|
let fetchObservable: Observable<Array<DataKey>>; |
||||
|
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() |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,51 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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. |
||||
|
|
||||
|
--> |
||||
|
<mat-form-field [appearance]="appearance" [ngClass]="ngClass" |
||||
|
[subscriptSizing]="subscriptSizing" style="width: 100%"> |
||||
|
<mat-label *ngIf="label">{{label}}</mat-label> |
||||
|
<input matInput #nameInput [formControl]="selectionFormControl" |
||||
|
[placeholder]="placeholderText" |
||||
|
(focusin)="onFocus()" |
||||
|
[matAutocomplete]="optionsAutocomplete"> |
||||
|
<button *ngIf="selectionFormControl.value && !disabled" |
||||
|
type="button" |
||||
|
class="tb-icon-24" |
||||
|
matSuffix mat-icon-button aria-label="Clear" |
||||
|
(click)="clear()"> |
||||
|
<mat-icon class="material-icons">close</mat-icon> |
||||
|
</button> |
||||
|
<mat-icon *ngIf="selectionFormControl.hasError('required') && !showInlineError" |
||||
|
matIconSuffix |
||||
|
[matTooltip]="errorText" |
||||
|
matTooltipPosition="above" |
||||
|
[matTooltipClass]="tooltipClass" |
||||
|
class="material-icons tb-suffix-show-always tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
<mat-error *ngIf="selectionFormControl.hasError('required') && showInlineError"> |
||||
|
{{errorText}} |
||||
|
</mat-error> |
||||
|
<mat-autocomplete |
||||
|
#optionsAutocomplete="matAutocomplete" |
||||
|
class="tb-autocomplete tb-options-input-autocomplete" |
||||
|
panelWidth="fit-content"> |
||||
|
<mat-option *ngFor="let option of filteredOptions$ | async" [value]="option"> |
||||
|
<span class="tb-option-name" fxFlex [innerHTML]="option | highlight:searchText:true:'ig'"></span> |
||||
|
</mat-option> |
||||
|
</mat-autocomplete> |
||||
|
</mat-form-field> |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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<Array<string>>; |
||||
|
|
||||
|
@ViewChild('nameInput', {static: true}) nameInput: ElementRef; |
||||
|
|
||||
|
@Input() |
||||
|
placeholderText: string = this.translate.instant('widget-config.set'); |
||||
|
|
||||
|
@Input() |
||||
|
subscriptSizing: string = 'dynamic'; |
||||
|
|
||||
|
@Input() |
||||
|
ngClass: string | string[] | Set<string> | { [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<Array<string>>; |
||||
|
|
||||
|
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); |
||||
|
} |
||||
|
} |
||||
@ -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:** |
||||
|
|
||||
|
<img style="width: 50px; height: 50px; margin-bottom: -58px; margin-left: 300px;" src="assets/widget/liquid-level/shapes/vertical-oval.svg" alt="Alt text" /> |
||||
|
|
||||
|
``` |
||||
|
Vertical Oval |
||||
|
{:copy-code} |
||||
|
``` |
||||
|
<img style="width: 50px; height: 50px; margin-bottom: -58px; margin-left: 300px;" src="assets/widget/liquid-level/shapes/vertical-cylinder.svg" alt="Alt text" /> |
||||
|
|
||||
|
``` |
||||
|
Vertical Cylinder |
||||
|
{:copy-code} |
||||
|
``` |
||||
|
<img style="width: 50px; height: 50px; margin-bottom: -58px; margin-left: 300px;" src="assets/widget/liquid-level/shapes/vertical-capsule.svg" alt="Alt text" /> |
||||
|
|
||||
|
``` |
||||
|
Vertical Capsule |
||||
|
{:copy-code} |
||||
|
``` |
||||
|
<img style="width: 50px; height: 50px; margin-bottom: -58px; margin-left: 300px;" src="assets/widget/liquid-level/shapes/rectangle.svg" alt="Alt text" /> |
||||
|
|
||||
|
``` |
||||
|
Rectangle |
||||
|
{:copy-code} |
||||
|
``` |
||||
|
<img style="width: 50px; height: 50px; margin-bottom: -58px; margin-left: 300px;" src="assets/widget/liquid-level/shapes/horizontal-oval.svg" alt="Alt text" /> |
||||
|
|
||||
|
``` |
||||
|
Horizontal Oval |
||||
|
{:copy-code} |
||||
|
``` |
||||
|
<img style="width: 50px; height: 50px; margin-bottom: -58px; margin-left: 300px;" src="assets/widget/liquid-level/shapes/horizontal-ellipse.svg" alt="Alt text" /> |
||||
|
|
||||
|
``` |
||||
|
Horizontal Ellipse |
||||
|
{:copy-code} |
||||
|
``` |
||||
|
<img style="width: 50px; height: 50px; margin-bottom: -58px; margin-left: 300px;" src="assets/widget/liquid-level/shapes/horizontal-dish-ends.svg" alt="Alt text" /> |
||||
|
|
||||
|
``` |
||||
|
Horizontal Dish Ends |
||||
|
{:copy-code} |
||||
|
``` |
||||
|
<img style="width: 50px; height: 50px; margin-bottom: -58px; margin-left: 300px;" src="assets/widget/liquid-level/shapes/horizontal-cylinder.svg" alt="Alt text" /> |
||||
|
|
||||
|
``` |
||||
|
Horizontal Cylinder |
||||
|
{:copy-code} |
||||
|
``` |
||||
|
<img style="width: 50px; height: 50px; margin-bottom: -58px; margin-left: 300px;" src="assets/widget/liquid-level/shapes/horizontal-capsule.svg" alt="Alt text" /> |
||||
|
|
||||
|
``` |
||||
|
Horizontal Capsule |
||||
|
{:copy-code} |
||||
|
``` |
||||
|
<img style="width: 50px; height: 50px; margin-bottom: -58px; margin-left: 300px;" src="assets/widget/liquid-level/shapes/horizontal-2_1-elliptical.svg" alt="Alt text" /> |
||||
|
|
||||
|
``` |
||||
|
Horizontal 2:1 Elliptical |
||||
|
{:copy-code} |
||||
|
``` |
||||
|
|
||||
|
<br> |
||||
|
<br> |
||||
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 11 KiB |