68 changed files with 3358 additions and 1146 deletions
@ -0,0 +1,175 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2024 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 [formGroup]="simpleGaugeWidgetConfigForm"> |
|||
<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>widgets.gauge.gauge-appearance</div> |
|||
<tb-image-cards-select rowHeight="1:1" |
|||
[cols]="{columns: 4, |
|||
breakpoints: { |
|||
'lt-md': 2 |
|||
}}" |
|||
label="{{ 'widgets.gauge.simple-gauge-type' | translate }}" |
|||
formControlName="gaugeType"> |
|||
<tb-image-cards-select-option *ngFor="let layout of digitalGaugeLayouts" |
|||
[value]="layout" |
|||
[image]="digitalGaugeLayoutImageMap.get(layout)"> |
|||
{{ digitalGaugeLayoutTranslationMap.get(layout) | translate }} |
|||
</tb-image-cards-select-option> |
|||
</tb-image-cards-select> |
|||
|
|||
<div class="tb-form-row space-between" *ngIf="simpleGaugeWidgetConfigForm.get('gaugeType').value === digitalGaugeType.donut"> |
|||
<div class="fixed-title-width">{{ 'widgets.gauge.donut-start-angle' | translate }}</div> |
|||
<mat-form-field appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput formControlName="donutStartAngle" type="number" min="0" max="360" step="1" placeholder="{{ 'widget-config.set' | translate }}"> |
|||
</mat-form-field> |
|||
</div> |
|||
|
|||
<div class="tb-form-row space-between column-xs"> |
|||
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showMinMax"> |
|||
{{ 'widgets.gauge.min-and-max-value' | translate }} |
|||
</mat-slide-toggle> |
|||
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
|||
<div class="tb-small-label" translate>widgets.gauge.min-value-short</div> |
|||
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic"> |
|||
<input matInput formControlName="minValue" type="number" step="1" placeholder="{{ 'widget-config.set' | translate }}"> |
|||
</mat-form-field> |
|||
<div class="tb-small-label" translate>widgets.gauge.max-value-short</div> |
|||
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic"> |
|||
<input matInput formControlName="maxValue" type="number" step="1" placeholder="{{ 'widget-config.set' | translate }}"> |
|||
</mat-form-field> |
|||
<tb-font-settings formControlName="minMaxFont" |
|||
disabledLineHeight |
|||
[previewText]="previewFn"> |
|||
</tb-font-settings> |
|||
<tb-color-input asBoxInput |
|||
colorClearButton |
|||
formControlName="minMaxColor"> |
|||
</tb-color-input> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="tb-form-row column-xs"> |
|||
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showValue"> |
|||
{{ 'widgets.gauge.value' | translate }} |
|||
</mat-slide-toggle> |
|||
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
|||
<tb-unit-input |
|||
class="flex" |
|||
formControlName="units"> |
|||
</tb-unit-input> |
|||
<mat-form-field appearance="outline" class="flex number" subscriptSizing="dynamic"> |
|||
<input matInput formControlName="decimals" 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="valueFont" |
|||
disabledLineHeight |
|||
[previewText]="valuePreviewFn"> |
|||
</tb-font-settings> |
|||
<tb-color-input asBoxInput |
|||
colorClearButton |
|||
formControlName="valueColor"> |
|||
</tb-color-input> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="tb-form-row"> |
|||
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showTitle"> |
|||
{{ 'widgets.gauge.label' | 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]="simpleGaugeWidgetConfigForm.get('title').value"> |
|||
</tb-font-settings> |
|||
<tb-color-input formControlName="titleColor" |
|||
asBoxInput |
|||
colorClearButton> |
|||
</tb-color-input> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="tb-form-row space-between"> |
|||
<div translate>widgets.gauge.default-color</div> |
|||
<tb-color-input asBoxInput |
|||
colorClearButton |
|||
formControlName="defaultColor"> |
|||
</tb-color-input> |
|||
</div> |
|||
|
|||
<div class="tb-form-row space-between"> |
|||
<div translate>widgets.gauge.gauge-bar-background</div> |
|||
<tb-color-input asBoxInput |
|||
colorClearButton |
|||
formControlName="gaugeColor"> |
|||
</tb-color-input> |
|||
</div> |
|||
|
|||
<div class="tb-form-row space-between"> |
|||
<div class="fixed-title-width">{{ 'widgets.gauge.bar-color' | translate }}</div> |
|||
<tb-color-settings formControlName="barColor" |
|||
rangeAdvancedMode |
|||
[minValue]="simpleGaugeWidgetConfigForm.get('minValue').value" |
|||
[maxValue]="simpleGaugeWidgetConfigForm.get('maxValue').value" |
|||
[aliasController]="aliasController" |
|||
[dataKeyCallbacks]="callbacks" |
|||
[datasource]="datasource" |
|||
settingsKey="{{'widgets.battery-level.value' | translate }}"> |
|||
</tb-color-settings> |
|||
</div> |
|||
|
|||
</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>{{ 'widget-config.background-color' | 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,276 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 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 { UntypedFormBuilder, UntypedFormGroup } 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 { |
|||
Datasource, |
|||
datasourcesHasAggregation, |
|||
datasourcesHasOnlyComparisonAggregation, |
|||
WidgetConfig, |
|||
} from '@shared/models/widget.models'; |
|||
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; |
|||
import { |
|||
getTimewindowConfig, |
|||
setTimewindowConfig |
|||
} from '@home/components/widget/config/timewindow-config-panel.component'; |
|||
import { formatValue, isUndefined } from '@core/utils'; |
|||
import { Component } from '@angular/core'; |
|||
import { |
|||
backwardCompatibilityFixedLevelColors, |
|||
defaultDigitalSimpleGaugeOptions, |
|||
digitalGaugeLayoutImages, |
|||
digitalGaugeLayouts, |
|||
digitalGaugeLayoutTranslations, |
|||
DigitalGaugeSettings, |
|||
DigitalGaugeType |
|||
} from '@home/components/widget/lib/digital-gauge.models'; |
|||
import { ColorSettings, ColorType, constantColor } from '@shared/models/widget-settings.models'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-digital-simple-gauge-basic-config', |
|||
templateUrl: './digital-simple-gauge-basic-config.component.html', |
|||
styleUrls: ['../basic-config.scss'] |
|||
}) |
|||
|
|||
export class DigitalSimpleGaugeBasicConfigComponent extends BasicWidgetConfigComponent { |
|||
|
|||
public get displayTimewindowConfig(): boolean { |
|||
const datasources = this.simpleGaugeWidgetConfigForm.get('datasources').value; |
|||
return datasourcesHasAggregation(datasources); |
|||
} |
|||
|
|||
public onlyHistoryTimewindow(): boolean { |
|||
const datasources = this.simpleGaugeWidgetConfigForm.get('datasources').value; |
|||
return datasourcesHasOnlyComparisonAggregation(datasources); |
|||
} |
|||
|
|||
public get datasource(): Datasource { |
|||
const datasources: Datasource[] = this.widgetConfig.config.datasources; |
|||
if (datasources && datasources.length) { |
|||
return datasources[0]; |
|||
} else { |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
digitalGaugeType = DigitalGaugeType; |
|||
digitalGaugeLayouts = digitalGaugeLayouts; |
|||
|
|||
digitalGaugeLayoutTranslationMap = digitalGaugeLayoutTranslations; |
|||
digitalGaugeLayoutImageMap = digitalGaugeLayoutImages; |
|||
|
|||
simpleGaugeWidgetConfigForm: UntypedFormGroup; |
|||
|
|||
valuePreviewFn = this._valuePreviewFn.bind(this, true); |
|||
previewFn = this._valuePreviewFn.bind(this, false); |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
protected widgetConfigComponent: WidgetConfigComponent, |
|||
protected fb: UntypedFormBuilder) { |
|||
super(store, widgetConfigComponent); |
|||
} |
|||
|
|||
protected configForm(): UntypedFormGroup { |
|||
return this.simpleGaugeWidgetConfigForm; |
|||
} |
|||
|
|||
protected setupDefaults(configData: WidgetConfigComponentData) { |
|||
super.setupDefaults(configData); |
|||
} |
|||
|
|||
protected onConfigSet(configData: WidgetConfigComponentData) { |
|||
const settings: DigitalGaugeSettings = {...defaultDigitalSimpleGaugeOptions, ...(configData.config.settings || {})}; |
|||
|
|||
if (!settings.barColor) { |
|||
settings.barColor = constantColor(settings.gaugeColor); |
|||
if (settings.fixedLevelColors?.length) { |
|||
settings.barColor.rangeList = { |
|||
advancedMode: settings.useFixedLevelColor, |
|||
range: null, |
|||
rangeAdvanced: backwardCompatibilityFixedLevelColors(settings.fixedLevelColors) |
|||
}; |
|||
} |
|||
if (settings.levelColors?.length) { |
|||
settings.barColor.gradient = { |
|||
advancedMode: false, |
|||
gradient: settings.levelColors as string[], |
|||
gradientAdvanced: null |
|||
}; |
|||
} |
|||
if (settings.useFixedLevelColor) { |
|||
settings.barColor.type = ColorType.range; |
|||
} else if (settings.levelColors.length) { |
|||
settings.barColor.type = ColorType.gradient; |
|||
} |
|||
} |
|||
|
|||
this.simpleGaugeWidgetConfigForm = this.fb.group({ |
|||
timewindowConfig: [getTimewindowConfig(configData.config), []], |
|||
datasources: [configData.config.datasources, []], |
|||
|
|||
gaugeType: [settings.gaugeType, []], |
|||
donutStartAngle: [settings.donutStartAngle, []], |
|||
|
|||
showMinMax: [settings.showMinMax, []], |
|||
minValue: [settings.minValue, []], |
|||
maxValue: [settings.maxValue, []], |
|||
minMaxFont: [settings.minMaxFont, []], |
|||
minMaxColor: [settings.minMaxFont?.color, []], |
|||
|
|||
showValue: [settings.showValue, []], |
|||
decimals: [configData.config.decimals, []], |
|||
units: [configData.config.units, []], |
|||
valueFont: [settings.valueFont, []], |
|||
valueColor: [settings.valueFont?.color, []], |
|||
|
|||
showTitle: [settings.showTitle, []], |
|||
title: [settings.title, []], |
|||
titleFont: [settings.titleFont, []], |
|||
titleColor: [settings.titleFont?.color, []], |
|||
|
|||
defaultColor: [settings.defaultColor, []], |
|||
gaugeColor: [settings.gaugeColor, []], |
|||
barColor: [settings.barColor, []], |
|||
|
|||
cardButtons: [this.getCardButtons(configData.config), []], |
|||
borderRadius: [configData.config.borderRadius, []], |
|||
backgroundColor: [configData.config.backgroundColor, []], |
|||
actions: [configData.config.actions || {}, []] |
|||
}); |
|||
} |
|||
|
|||
protected prepareOutputConfig(config: any): WidgetConfigComponentData { |
|||
setTimewindowConfig(this.widgetConfig.config, config.timewindowConfig); |
|||
this.widgetConfig.config.datasources = config.datasources; |
|||
this.widgetConfig.config.actions = config.actions; |
|||
|
|||
this.widgetConfig.config.settings = this.widgetConfig.config.settings || {}; |
|||
this.setCardButtons(config.cardButtons, this.widgetConfig.config); |
|||
this.widgetConfig.config.borderRadius = config.borderRadius; |
|||
this.widgetConfig.config.backgroundColor = config.backgroundColor; |
|||
|
|||
this.widgetConfig.config.settings.gaugeType = config.gaugeType; |
|||
this.widgetConfig.config.settings.donutStartAngle = config.donutStartAngle; |
|||
|
|||
this.widgetConfig.config.settings.showMinMax = config.showMinMax; |
|||
this.widgetConfig.config.settings.minValue = config.minValue; |
|||
this.widgetConfig.config.settings.maxValue = config.maxValue; |
|||
this.widgetConfig.config.settings.minMaxFont = config.minMaxFont; |
|||
this.widgetConfig.config.settings.minMaxFont.color = config.minMaxColor; |
|||
|
|||
this.widgetConfig.config.settings.showValue = config.showValue; |
|||
this.widgetConfig.config.units = config.units; |
|||
this.widgetConfig.config.decimals = config.decimals; |
|||
this.widgetConfig.config.settings.valueFont = config.valueFont; |
|||
this.widgetConfig.config.settings.valueFont.color = config.valueColor; |
|||
|
|||
this.widgetConfig.config.settings.showTitle = config.showTitle; |
|||
this.widgetConfig.config.settings.title = config.title; |
|||
this.widgetConfig.config.settings.titleFont = config.titleFont; |
|||
this.widgetConfig.config.settings.titleFont.color = config.titleColor; |
|||
|
|||
this.widgetConfig.config.settings.defaultColor = config.defaultColor; |
|||
this.widgetConfig.config.settings.gaugeColor = config.gaugeColor; |
|||
this.widgetConfig.config.settings.barColor = config.barColor; |
|||
const barColor: ColorSettings = config.barColor; |
|||
|
|||
if (barColor.type === ColorType.range) { |
|||
this.widgetConfig.config.settings.useFixedLevelColor = true; |
|||
this.widgetConfig.config.settings.fixedLevelColors = |
|||
barColor.rangeList.advancedMode ? barColor.rangeList.rangeAdvanced : barColor.rangeList.range; |
|||
} else { |
|||
this.widgetConfig.config.settings.useFixedLevelColor = false; |
|||
} |
|||
if (barColor.gradient?.gradient?.length) { |
|||
this.widgetConfig.config.settings.levelColors = barColor.gradient.gradient; |
|||
} |
|||
|
|||
return this.widgetConfig; |
|||
} |
|||
|
|||
protected validatorTriggers(): string[] { |
|||
return ['gaugeType', 'showValue', 'showTitle', 'showMinMax']; |
|||
} |
|||
|
|||
protected updateValidators(emitEvent: boolean, trigger?: string) { |
|||
const isDonut = this.simpleGaugeWidgetConfigForm.get('gaugeType').value === this.digitalGaugeType.donut; |
|||
|
|||
if (isDonut) { |
|||
this.simpleGaugeWidgetConfigForm.get('donutStartAngle').enable(); |
|||
this.simpleGaugeWidgetConfigForm.get('showMinMax').disable({emitEvent: false}); |
|||
this.simpleGaugeWidgetConfigForm.get('minValue').enable(); |
|||
this.simpleGaugeWidgetConfigForm.get('maxValue').enable(); |
|||
this.simpleGaugeWidgetConfigForm.get('minMaxFont').disable(); |
|||
this.simpleGaugeWidgetConfigForm.get('minMaxColor').disable(); |
|||
} else { |
|||
this.simpleGaugeWidgetConfigForm.get('donutStartAngle').disable(); |
|||
this.simpleGaugeWidgetConfigForm.get('showMinMax').enable({emitEvent: false}); |
|||
if (this.simpleGaugeWidgetConfigForm.get('showMinMax').value) { |
|||
this.simpleGaugeWidgetConfigForm.get('minValue').enable(); |
|||
this.simpleGaugeWidgetConfigForm.get('maxValue').enable(); |
|||
this.simpleGaugeWidgetConfigForm.get('minMaxFont').enable(); |
|||
this.simpleGaugeWidgetConfigForm.get('minMaxColor').enable(); |
|||
} else { |
|||
this.simpleGaugeWidgetConfigForm.get('minValue').disable(); |
|||
this.simpleGaugeWidgetConfigForm.get('maxValue').disable(); |
|||
this.simpleGaugeWidgetConfigForm.get('minMaxFont').disable(); |
|||
this.simpleGaugeWidgetConfigForm.get('minMaxColor').disable(); |
|||
} |
|||
} |
|||
|
|||
if (this.simpleGaugeWidgetConfigForm.get('showValue').value) { |
|||
this.simpleGaugeWidgetConfigForm.get('decimals').enable(); |
|||
this.simpleGaugeWidgetConfigForm.get('units').enable(); |
|||
this.simpleGaugeWidgetConfigForm.get('valueFont').enable(); |
|||
this.simpleGaugeWidgetConfigForm.get('valueColor').enable(); |
|||
} else { |
|||
this.simpleGaugeWidgetConfigForm.get('decimals').disable(); |
|||
this.simpleGaugeWidgetConfigForm.get('units').disable(); |
|||
this.simpleGaugeWidgetConfigForm.get('valueFont').disable(); |
|||
this.simpleGaugeWidgetConfigForm.get('valueColor').disable(); |
|||
} |
|||
|
|||
if (this.simpleGaugeWidgetConfigForm.get('showTitle').value) { |
|||
this.simpleGaugeWidgetConfigForm.get('title').enable(); |
|||
this.simpleGaugeWidgetConfigForm.get('titleFont').enable(); |
|||
this.simpleGaugeWidgetConfigForm.get('titleColor').enable(); |
|||
} else { |
|||
this.simpleGaugeWidgetConfigForm.get('title').disable(); |
|||
this.simpleGaugeWidgetConfigForm.get('titleFont').disable(); |
|||
this.simpleGaugeWidgetConfigForm.get('titleColor').disable(); |
|||
} |
|||
} |
|||
|
|||
private getCardButtons(config: WidgetConfig): string[] { |
|||
const buttons: string[] = []; |
|||
if (isUndefined(config.enableFullscreen) || config.enableFullscreen) { |
|||
buttons.push('fullscreen'); |
|||
} |
|||
return buttons; |
|||
} |
|||
|
|||
private setCardButtons(buttons: string[], config: WidgetConfig) { |
|||
config.enableFullscreen = buttons.includes('fullscreen'); |
|||
} |
|||
|
|||
private _valuePreviewFn(units: boolean): string { |
|||
return formatValue(22, 0, units ? this.simpleGaugeWidgetConfigForm.get('units').value : null, true); |
|||
} |
|||
} |
|||
@ -1,78 +0,0 @@ |
|||
/** |
|||
* Copyright © 2016-2024 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. |
|||
*/ |
|||
.tb-flot-threshold { |
|||
display: flex; |
|||
flex-direction: row; |
|||
align-items: start; |
|||
gap: 4px; |
|||
.mat-expansion-panel.tb-settings.tb-flot-threshold-settings { |
|||
box-shadow: none; |
|||
border-radius: 6px; |
|||
border: 1px solid rgba(0, 0, 0, 0.12); |
|||
.mat-expansion-panel-header { |
|||
height: 56px; |
|||
border-radius: 0; |
|||
display: flex; |
|||
flex-direction: row; |
|||
align-items: stretch; |
|||
.mat-content { |
|||
overflow: hidden; |
|||
} |
|||
.tb-threshold-header { |
|||
flex: 1; |
|||
display: flex; |
|||
flex-direction: row; |
|||
gap: 16px; |
|||
align-items: center; |
|||
padding-left: 16px; |
|||
overflow: hidden; |
|||
.mat-divider-vertical { |
|||
height: 100%; |
|||
} |
|||
} |
|||
.tb-threshold-text { |
|||
flex: 1; |
|||
font-size: 16px; |
|||
font-style: normal; |
|||
font-weight: 400; |
|||
line-height: 16px; |
|||
letter-spacing: 0.15px; |
|||
overflow: hidden; |
|||
white-space: nowrap; |
|||
text-overflow: ellipsis; |
|||
} |
|||
.mat-expansion-indicator { |
|||
margin-right: 22px; |
|||
margin-left: 22px; |
|||
margin-top: 12px; |
|||
} |
|||
} |
|||
> .mat-expansion-panel-content { |
|||
> .mat-expansion-panel-body { |
|||
padding: 16px !important; |
|||
} |
|||
} |
|||
&.mat-expanded { |
|||
.mat-expansion-panel-header { |
|||
border-bottom: 1px solid rgba(0, 0, 0, 0.12); |
|||
} |
|||
} |
|||
} |
|||
> .mdc-icon-button { |
|||
margin-top: 4px; |
|||
color: rgba(0, 0, 0, 0.54); |
|||
} |
|||
} |
|||
@ -0,0 +1,56 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2024 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. |
|||
|
|||
--> |
|||
<div class="tb-form-row no-padding no-border" style="gap: 0; width: 100%;" [formGroup]="advancedRangeLevelFormGroup"> |
|||
<div class="range-container"> |
|||
<div class="range-input range-input-fields"> |
|||
<div fxFlex fxLayout="row" fxLayoutAlign="start center"> |
|||
<div class="range-text" translate>widgets.color.from</div> |
|||
<tb-value-source-data-key fxFlex |
|||
formControlName="from" |
|||
[aliasController]="aliasController" |
|||
[dataKeyCallbacks]="dataKeyCallbacks" |
|||
[datasource]="datasource"> |
|||
</tb-value-source-data-key> |
|||
</div> |
|||
<div fxFlex fxLayout="row" fxLayoutAlign="start center"> |
|||
<div class="range-text" translate>widgets.color.to</div> |
|||
<tb-value-source-data-key fxFlex |
|||
formControlName="to" |
|||
[aliasController]="aliasController" |
|||
[dataKeyCallbacks]="dataKeyCallbacks" |
|||
[datasource]="datasource"> |
|||
</tb-value-source-data-key> |
|||
</div> |
|||
</div> |
|||
<tb-color-input asBoxInput |
|||
class="range-input" |
|||
required |
|||
colorClearButton |
|||
noBorder |
|||
formControlName="color"> |
|||
</tb-color-input> |
|||
</div> |
|||
<button *ngIf="!disabled" mat-icon-button style="min-width: 40px;" |
|||
type="button" |
|||
class="tb-box-button" |
|||
(click)="removeAdvancedRange.emit()" |
|||
matTooltip="{{ 'action.remove' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon>delete</mat-icon> |
|||
</button> |
|||
</div> |
|||
@ -0,0 +1,42 @@ |
|||
/** |
|||
* Copyright © 2016-2024 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 { |
|||
.range { |
|||
&-container { |
|||
display: flex; |
|||
flex: 1; |
|||
flex-direction: row; |
|||
align-items: center; |
|||
border: 1px solid rgba(0, 0, 0, 0.12); |
|||
border-radius: 6px; |
|||
} |
|||
&-input { |
|||
padding: 7px; |
|||
&-fields { |
|||
display: flex; |
|||
flex-direction: column; |
|||
flex: 1; |
|||
gap: 12px; |
|||
border-right: 1px solid rgba(0, 0, 0, 0.12);; |
|||
} |
|||
} |
|||
&-text { |
|||
min-width: 50px; |
|||
font-size: 14px; |
|||
color: rgba(0, 0, 0, 0.38); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,109 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 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, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core'; |
|||
import { |
|||
ControlValueAccessor, |
|||
NG_VALUE_ACCESSOR, |
|||
UntypedFormBuilder, |
|||
UntypedFormGroup, |
|||
Validators |
|||
} from '@angular/forms'; |
|||
import { PageComponent } from '@shared/components/page.component'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { IAliasController } from '@core/api/widget-api.models'; |
|||
import { AdvancedColorRange } from '@shared/models/widget-settings.models'; |
|||
import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models'; |
|||
import { Datasource } from '@shared/models/widget.models'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-advanced-range', |
|||
templateUrl: './advanced-range.component.html', |
|||
styleUrls: ['./advanced-range.component.scss'], |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => AdvancedRangeComponent), |
|||
multi: true |
|||
} |
|||
] |
|||
}) |
|||
export class AdvancedRangeComponent extends PageComponent implements OnInit, ControlValueAccessor { |
|||
|
|||
@Input() |
|||
disabled: boolean; |
|||
|
|||
@Input() |
|||
aliasController: IAliasController; |
|||
|
|||
@Input() |
|||
dataKeyCallbacks: DataKeysCallbacks; |
|||
|
|||
@Input() |
|||
datasource: Datasource; |
|||
|
|||
@Output() |
|||
removeAdvancedRange = new EventEmitter(); |
|||
|
|||
private modelValue: AdvancedColorRange; |
|||
|
|||
private propagateChange = (v: any) => { }; |
|||
|
|||
public advancedRangeLevelFormGroup: UntypedFormGroup; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
private fb: UntypedFormBuilder) { |
|||
super(store); |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
this.advancedRangeLevelFormGroup = this.fb.group({ |
|||
from: [null, []], |
|||
to: [null, []], |
|||
color: [null, [Validators.required]] |
|||
}); |
|||
this.advancedRangeLevelFormGroup.valueChanges.subscribe(() => { |
|||
this.updateModel(); |
|||
}); |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
} |
|||
|
|||
setDisabledState(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
if (isDisabled) { |
|||
this.advancedRangeLevelFormGroup.disable({emitEvent: false}); |
|||
} else { |
|||
this.advancedRangeLevelFormGroup.enable({emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
writeValue(value: AdvancedColorRange): void { |
|||
this.modelValue = value; |
|||
this.advancedRangeLevelFormGroup.patchValue(value, {emitEvent: false}); |
|||
} |
|||
|
|||
private updateModel() { |
|||
this.modelValue = this.advancedRangeLevelFormGroup.value; |
|||
this.propagateChange(this.modelValue); |
|||
} |
|||
} |
|||
@ -0,0 +1,208 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2024 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 [formGroup]="gradientFormGroup"> |
|||
|
|||
<div fxLayout="row" fxLayoutAlign="start center" *ngIf="advancedMode"> |
|||
<div fxFlex class="tb-form-panel-title" fxHide.xs translate>widgets.color.gradient-type</div> |
|||
<tb-toggle-select formControlName="advancedMode" fxFlex> |
|||
<tb-toggle-option [value]="false"> |
|||
{{ 'widgets.color.basic-mode' | translate }} |
|||
</tb-toggle-option> |
|||
<tb-toggle-option [value]="true"> |
|||
{{ 'widgets.color.advanced-mode' | translate}} |
|||
</tb-toggle-option> |
|||
</tb-toggle-select> |
|||
</div> |
|||
|
|||
<div class="gradient-preview"> |
|||
<div [style]="linearGradient" class="gradient-background" [innerHtml]="(generatePointers | safe: 'html')"> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="gradient-settings" [ngSwitch]="gradientFormGroup.get('advancedMode').value"> |
|||
<ng-template [ngSwitchCase]="false"> |
|||
<div class="gradient" formGroupName="gradient"> |
|||
<div class="gradient-container gradient-container-start"> |
|||
<div translate>widgets.color.gradient-start</div> |
|||
<div class="start-gradient-container"> |
|||
<div class="tb-form-row no-border no-padding" fxHide.xs [fxHide]="!minValue?.toString()?.length"> |
|||
<div class="gradient-text" translate>widgets.color.start-value</div> |
|||
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic"> |
|||
<input matInput type="number" min="0" disabled [(ngModel)]="minValue" [ngModelOptions]="{standalone: true}" placeholder="{{ 'widget-config.set' | translate }}"> |
|||
</mat-form-field> |
|||
</div> |
|||
<tb-color-input asBoxInput |
|||
formControlName="start"> |
|||
</tb-color-input> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="tb-colors-gradient" *ngIf="gradientListFormGroups.length"> |
|||
<div class="tb-control-list tb-drop-list" cdkDropList cdkDropListOrientation="vertical" |
|||
(cdkDropListDropped)="gradientDrop($event)"> |
|||
<div cdkDrag class="tb-form-row no-padding no-border tb-draggable" |
|||
[formGroup]="gradientFormGroup" |
|||
*ngFor="let gradientFormGroup of gradientListFormGroups; trackBy: trackByGradient; let $index = index; last as isLast;" |
|||
[ngStyle]="!isLast ? {paddingBottom: '8px'} : {}"> |
|||
<div class="gradient"> |
|||
<div class="gradient-container"> |
|||
<div translate>widgets.color.gradient-color</div> |
|||
<tb-color-input asBoxInput |
|||
formControlName="color"> |
|||
</tb-color-input> |
|||
</div> |
|||
<button type="button" |
|||
mat-icon-button |
|||
class="tb-box-button" |
|||
(click)="removeGradient($index)" |
|||
matTooltip="{{ 'action.remove' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon>delete</mat-icon> |
|||
</button> |
|||
<button class="tb-box-button" |
|||
fxHide.lt-lg |
|||
mat-icon-button |
|||
type="button" |
|||
cdkDragHandle |
|||
matTooltip="{{ 'action.drag' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon>drag_indicator</mat-icon> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<button class="tb-add-gradient" |
|||
mat-stroked-button |
|||
(click)="addGradient()"> |
|||
<mat-icon>add</mat-icon> |
|||
</button> |
|||
|
|||
<div class="gradient" formGroupName="gradient"> |
|||
<div class="gradient-container gradient-container-end"> |
|||
<div translate>widgets.color.gradient-end</div> |
|||
<div class="end-gradient-container"> |
|||
<div class="tb-form-row no-border no-padding" fxHide.xs [fxHide]="!maxValue?.toString()?.length"> |
|||
<div class="gradient-text" translate>widgets.color.end-value</div> |
|||
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic"> |
|||
<input matInput type="number" min="0" disabled [(ngModel)]="maxValue" [ngModelOptions]="{standalone: true}" placeholder="{{ 'widget-config.set' | translate }}"> |
|||
</mat-form-field> |
|||
</div> |
|||
<tb-color-input asBoxInput |
|||
formControlName="end"> |
|||
</tb-color-input> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</ng-template> |
|||
<ng-template [ngSwitchCase]="true"> |
|||
<div class="gradient" formGroupName="gradientAdvanced"> |
|||
<div class="gradient-container gradient-container-start" formGroupName="start"> |
|||
<div class="tb-gradient-text"> |
|||
<span fxHide.xs translate>widgets.color.gradient-start</span> |
|||
<span fxHide fxShow.xs translate>widgets.color.gradient-start-min</span> |
|||
</div> |
|||
<div class="tb-form-row no-border no-padding start-gradient-container start-gradient-container-advanced"> |
|||
<tb-value-source-data-key fxFlex |
|||
formControlName="source" |
|||
[aliasController]="aliasController" |
|||
[dataKeyCallbacks]="dataKeyCallbacks" |
|||
[datasource]="datasource"> |
|||
</tb-value-source-data-key> |
|||
<tb-color-input asBoxInput |
|||
formControlName="color"> |
|||
</tb-color-input> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="tb-colors-gradient" *ngIf="advancedGradientListFormGroups.length"> |
|||
<div class="tb-control-list tb-drop-list" cdkDropList cdkDropListOrientation="vertical" |
|||
(cdkDropListDropped)="gradientDrop($event, true)"> |
|||
<div cdkDrag class="tb-form-row no-padding no-border tb-draggable" |
|||
[formGroup]="gradientFormGroup" |
|||
*ngFor="let gradientFormGroup of advancedGradientListFormGroups; trackBy: trackByGradient; let $index = index; last as isLast;" |
|||
[ngStyle]="!isLast ? {paddingBottom: '8px'} : {}"> |
|||
<div class="gradient"> |
|||
<div class="gradient-container"> |
|||
<div class="tb-gradient-text"> |
|||
<span fxHide.xs translate>widgets.color.gradient-color</span> |
|||
<span fxHide fxShow.xs translate>widgets.color.gradient-color-min</span> |
|||
</div> |
|||
<div class="list-gradient-container list-gradient-container-advanced"> |
|||
<tb-value-source-data-key fxFlex |
|||
formControlName="source" |
|||
[aliasController]="aliasController" |
|||
[dataKeyCallbacks]="dataKeyCallbacks" |
|||
[datasource]="datasource"> |
|||
</tb-value-source-data-key> |
|||
<tb-color-input asBoxInput |
|||
formControlName="color"> |
|||
</tb-color-input> |
|||
</div> |
|||
</div> |
|||
<button type="button" |
|||
mat-icon-button |
|||
class="tb-box-button" |
|||
(click)="removeGradient($index, true)" |
|||
matTooltip="{{ 'action.remove' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon>delete</mat-icon> |
|||
</button> |
|||
<button class="tb-box-button" |
|||
fxHide.lt-lg |
|||
mat-icon-button |
|||
type="button" |
|||
cdkDragHandle |
|||
matTooltip="{{ 'action.drag' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon>drag_indicator</mat-icon> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<button class="tb-add-gradient" |
|||
mat-stroked-button |
|||
(click)="addGradient(true)"> |
|||
<mat-icon>add</mat-icon> |
|||
</button> |
|||
|
|||
<div class="gradient" formGroupName="gradientAdvanced"> |
|||
<div class="gradient-container gradient-container-end" formGroupName="end"> |
|||
<div class="tb-gradient-text"> |
|||
<span fxHide.xs translate>widgets.color.gradient-end</span> |
|||
<span fxHide fxShow.xs translate>widgets.color.gradient-end-min</span> |
|||
</div> |
|||
<div class="tb-form-row no-border no-padding end-gradient-container end-gradient-container-advanced"> |
|||
<tb-value-source-data-key fxFlex |
|||
formControlName="source" |
|||
[aliasController]="aliasController" |
|||
[dataKeyCallbacks]="dataKeyCallbacks" |
|||
[datasource]="datasource"> |
|||
</tb-value-source-data-key> |
|||
<tb-color-input asBoxInput |
|||
formControlName="color"> |
|||
</tb-color-input> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</ng-template> |
|||
</div> |
|||
</ng-container> |
|||
@ -0,0 +1,145 @@ |
|||
/** |
|||
* Copyright © 2016-2024 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 "../../../../../../../../scss/constants"; |
|||
|
|||
|
|||
:host { |
|||
overflow: auto; |
|||
height: 100%; |
|||
max-height: 420px; |
|||
.gradient { |
|||
display: flex; |
|||
flex: 1; |
|||
align-items: center; |
|||
flex-direction: row; |
|||
|
|||
&-container { |
|||
display: flex; |
|||
flex: 1; |
|||
flex-direction: row; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
gap: 12px; |
|||
padding: 8px; |
|||
margin-right: 12px; |
|||
border: 1px solid rgba(0, 0, 0, 0.12); |
|||
border-radius: 6px; |
|||
&-start, &-end { |
|||
margin-right: 92px; |
|||
@media #{$mat-lt-lg} { |
|||
margin-right: 52px; |
|||
} |
|||
} |
|||
|
|||
.tb-gradient-text { |
|||
width: 140px; |
|||
@media #{$mat-xs} { |
|||
width: 50px; |
|||
} |
|||
} |
|||
} |
|||
.start-gradient-container, .end-gradient-container, .list-gradient-container { |
|||
display: flex; |
|||
gap: 8px; |
|||
.gradient-text { |
|||
font-size: 14px; |
|||
color: rgba(0, 0, 0, 0.38); |
|||
} |
|||
&-advanced { |
|||
flex: 1; |
|||
@media #{$mat-xs} { |
|||
align-items: center; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
.gradient-preview { |
|||
width: 100%; |
|||
padding: 40px 12px 0; |
|||
.gradient-background { |
|||
position: relative; |
|||
height: 56px; |
|||
border-radius: 8px; |
|||
} |
|||
} |
|||
|
|||
.gradient-settings { |
|||
flex: 1; |
|||
gap: 16px; |
|||
display: flex; |
|||
flex-direction: column; |
|||
} |
|||
|
|||
.tb-add-gradient { |
|||
margin-right: 92px; |
|||
@media #{$mat-lt-lg} { |
|||
margin-right: 52px; |
|||
} |
|||
} |
|||
} |
|||
::ng-deep { |
|||
.gradient-background { |
|||
.pointer { |
|||
position: absolute; |
|||
width: 8px; |
|||
height: 60px; |
|||
border-radius: 6px; |
|||
top: -2px; |
|||
border: 2px solid white; |
|||
-webkit-box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.75); |
|||
-moz-box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.75); |
|||
box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.75); |
|||
|
|||
&.start { |
|||
left: 1px; |
|||
} |
|||
|
|||
&.end { |
|||
right: 1px; |
|||
} |
|||
|
|||
&-value { |
|||
position: absolute; |
|||
top: -40px; |
|||
left: 50%; |
|||
transform: translateX(-50%); |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
width: 33px; |
|||
height: 24px; |
|||
border-radius: 4px; |
|||
background-color: rgba(0, 0, 0, 0.06); |
|||
&-text { |
|||
font-size: 14px; |
|||
font-weight: 500; |
|||
} |
|||
&:after { |
|||
content: ""; |
|||
bottom: -8px; |
|||
position: absolute; |
|||
width: 0; |
|||
height: 0; |
|||
border-left: 8px solid transparent; |
|||
border-right: 8px solid transparent; |
|||
border-top: 8px solid rgba(0, 0, 0, 0.06); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,266 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 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, ElementRef, forwardRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; |
|||
import { |
|||
AbstractControl, |
|||
ControlValueAccessor, |
|||
FormGroup, |
|||
NG_VALUE_ACCESSOR, |
|||
UntypedFormArray, |
|||
UntypedFormBuilder, |
|||
UntypedFormGroup |
|||
} from '@angular/forms'; |
|||
import { AdvancedGradient, ColorGradientSettings, ValueSourceDataKeyType } from '@shared/models/widget-settings.models'; |
|||
import { TbPopoverComponent } from '@shared/components/popover.component'; |
|||
import { Subject } from 'rxjs'; |
|||
import { takeUntil } from 'rxjs/operators'; |
|||
import { CdkDragDrop } from '@angular/cdk/drag-drop'; |
|||
import { IAliasController } from '@core/api/widget-api.models'; |
|||
import { DomSanitizer } from '@angular/platform-browser'; |
|||
import { coerceBoolean } from '@shared/decorators/coercion'; |
|||
import { isDefinedAndNotNull } from '@core/utils'; |
|||
import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models'; |
|||
import { Datasource } from '@shared/models/widget.models'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-gradient', |
|||
templateUrl: './gradient.component.html', |
|||
styleUrls: ['color-settings-panel.component.scss', 'gradient.component.scss'], |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => GradientComponent), |
|||
multi: true |
|||
} |
|||
] |
|||
}) |
|||
export class GradientComponent implements OnInit, ControlValueAccessor, OnDestroy { |
|||
|
|||
@ViewChild('gradient') gradient: ElementRef; |
|||
|
|||
@Input() |
|||
disabled: boolean; |
|||
|
|||
@Input() |
|||
popover: TbPopoverComponent; |
|||
|
|||
@Input() |
|||
panelTitle: string; |
|||
|
|||
@Input() |
|||
aliasController: IAliasController; |
|||
|
|||
@Input() |
|||
dataKeyCallbacks: DataKeysCallbacks; |
|||
|
|||
@Input() |
|||
datasource: Datasource; |
|||
|
|||
@Input() |
|||
minValue: string; |
|||
|
|||
@Input() |
|||
maxValue: string; |
|||
|
|||
@Input() |
|||
@coerceBoolean() |
|||
advancedMode = true; |
|||
|
|||
modelValue: any; |
|||
|
|||
gradientFormGroup: UntypedFormGroup; |
|||
|
|||
private destroy$ = new Subject<void>(); |
|||
|
|||
private propagateChange = (v: any) => { }; |
|||
|
|||
constructor(private fb: UntypedFormBuilder, |
|||
private sanitizer: DomSanitizer) {} |
|||
|
|||
ngOnInit(): void { |
|||
this.gradientFormGroup = this.fb.group({ |
|||
advancedMode: [false], |
|||
gradient: this.fb.group({ |
|||
start: ['rgba(0, 255, 0, 1)'], |
|||
gradientList: this.fb.array([]), |
|||
end: ['rgba(255, 0, 0, 1)'] |
|||
}), |
|||
gradientAdvanced: this.fb.group({ |
|||
start: this.fb.group({ |
|||
source: [{type: ValueSourceDataKeyType.constant}], |
|||
color: ['rgba(0, 255, 0, 1)'] |
|||
}), |
|||
gradientList: this.fb.array([]), |
|||
end: this.fb.group({ |
|||
source: [{type: ValueSourceDataKeyType.constant}], |
|||
color: ['rgba(255, 0, 0, 1)'] |
|||
}) |
|||
}) |
|||
}); |
|||
|
|||
this.gradientFormGroup.valueChanges.pipe( |
|||
takeUntil(this.destroy$) |
|||
).subscribe(() => this.updateModel()); |
|||
} |
|||
|
|||
ngOnDestroy() { |
|||
this.destroy$.next(); |
|||
this.destroy$.complete(); |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
} |
|||
|
|||
setDisabledState(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
} |
|||
|
|||
writeValue(value: ColorGradientSettings): void { |
|||
if (isDefinedAndNotNull(value)) { |
|||
this.gradientFormGroup.get('advancedMode').patchValue(value.advancedMode, {emitEvent: false}); |
|||
if (value?.gradient?.length) { |
|||
this.gradientFormGroup.get('gradient').get('start').patchValue(value.gradient[0], {emitEvent: false}); |
|||
this.gradientFormGroup.get('gradient').get('end').patchValue(value.gradient[value.gradient.length - 1], {emitEvent: false}); |
|||
value.gradient.slice(1, -1).forEach(r => this.gradientListFormArray.push(this.colorGradientControl(r), {emitEvent: false})); |
|||
} |
|||
if (value?.gradientAdvanced?.length) { |
|||
this.gradientFormGroup.get('gradientAdvanced').get('start').patchValue(value.gradientAdvanced[0], {emitEvent: false}); |
|||
this.gradientFormGroup.get('gradientAdvanced').get('end').patchValue( |
|||
value.gradientAdvanced[value.gradientAdvanced.length - 1], {emitEvent: false} |
|||
); |
|||
value.gradientAdvanced.slice(1, -1).forEach( |
|||
r => this.advancedGradientListFormArray.push(this.advancedGradientControl(r), {emitEvent: false}) |
|||
); |
|||
} |
|||
} |
|||
} |
|||
|
|||
get generatePointers() { |
|||
if (this.gradientFormGroup.get('advancedMode').value) { |
|||
const shift = 100 / (this.advancedGradientListFormArray.value.length + 1); |
|||
return `<div class="pointer start"></div>` + |
|||
this.advancedGradientListFormArray.value.map((v, i) => this.pointer(shift * (i + 1), i+1, null, true)).join('') + |
|||
`<div class="pointer end"></div>`; |
|||
} else { |
|||
const point = (+this.maxValue - +this.minValue) / (this.gradientListFormArray.value.length + 1); |
|||
const shift = 100 / (this.gradientListFormArray.value.length + 1); |
|||
const min = isDefinedAndNotNull(this.minValue) ? this.minValue : 0; |
|||
const max = isDefinedAndNotNull(this.maxValue) ? this.maxValue : 100; |
|||
return `<div class="pointer start"><div class="pointer-value"><span class="pointer-value-text">${min}</span></div></div>` + |
|||
this.gradientListFormArray.value.map((v, i) => this.pointer(shift * (i + 1), i+1, point)).join('') + |
|||
`<div class="pointer end"><div class="pointer-value"><span class="pointer-value-text">${max}</span></div></div>`; |
|||
} |
|||
} |
|||
|
|||
pointer(shift: number, index?: number, value?: number, advanced = false) { |
|||
if (advanced) { |
|||
return `<div class="pointer" style="left: ${shift}%"></div>`; |
|||
} else { |
|||
return `<div class="pointer" style="left: ${shift}%">` + |
|||
`<div class="pointer-value"><span class="pointer-value-text">${Math.floor(+this.minValue + (value * index))}</span></div></div>`; |
|||
} |
|||
} |
|||
|
|||
get linearGradient() { |
|||
const gradient = this.gradientFormGroup.get('advancedMode').value ? |
|||
[this.gradientFormGroup.value.gradientAdvanced.start.color, |
|||
...this.gradientFormGroup.value.gradientAdvanced.gradientList.map(item => item.color), |
|||
this.gradientFormGroup.value.gradientAdvanced.end.color].join(', ') : |
|||
[this.gradientFormGroup.value.gradient.start, |
|||
...this.gradientFormGroup.value.gradient.gradientList.map(item => item.color), |
|||
this.gradientFormGroup.value.gradient.end].join(', '); |
|||
return this.sanitizer.bypassSecurityTrustStyle(`background-image: linear-gradient(90deg, ${gradient})`); |
|||
} |
|||
|
|||
private colorGradientControl(gradient: string): UntypedFormGroup { |
|||
return this.fb.group({ |
|||
color: [gradient, []] |
|||
}); |
|||
} |
|||
|
|||
get gradientListFormArray(): UntypedFormArray { |
|||
return this.gradientFormGroup.get('gradient.gradientList') as UntypedFormArray; |
|||
} |
|||
get gradientListFormGroups(): FormGroup[] { |
|||
return this.gradientListFormArray.controls as FormGroup[]; |
|||
} |
|||
|
|||
private advancedGradientControl(gradient: AdvancedGradient): UntypedFormGroup { |
|||
return this.fb.group({ |
|||
source: [gradient.source, []], |
|||
color: [gradient.color, []] |
|||
}); |
|||
} |
|||
|
|||
get advancedGradientListFormArray(): UntypedFormArray { |
|||
return this.gradientFormGroup.get('gradientAdvanced.gradientList') as UntypedFormArray; |
|||
} |
|||
get advancedGradientListFormGroups(): FormGroup[] { |
|||
return this.advancedGradientListFormArray.controls as FormGroup[]; |
|||
} |
|||
|
|||
trackByGradient(index: number, gradientControl: AbstractControl): any { |
|||
return gradientControl; |
|||
} |
|||
|
|||
removeGradient(index: number, advanced = false) { |
|||
if (advanced) { |
|||
this.advancedGradientListFormArray.removeAt(index); |
|||
} else { |
|||
this.gradientListFormArray.removeAt(index); |
|||
} |
|||
this.gradientFormGroup.markAsDirty(); |
|||
setTimeout(() => {this.popover?.updatePosition();}, 0); |
|||
} |
|||
|
|||
gradientDrop(event: CdkDragDrop<string[]>, advanced = false) { |
|||
const gradientColorsArray = advanced ? this.advancedGradientListFormArray : this.gradientListFormArray; |
|||
const gradientColor = gradientColorsArray.at(event.previousIndex); |
|||
gradientColorsArray.removeAt(event.previousIndex); |
|||
gradientColorsArray.insert(event.currentIndex, gradientColor); |
|||
} |
|||
|
|||
addGradient(advanced = false) { |
|||
if (advanced) { |
|||
this.advancedGradientListFormArray.push( |
|||
this.advancedGradientControl({source: {type: ValueSourceDataKeyType.constant}, color: 'rgba(0,0,0,0.87)'}) |
|||
); |
|||
} else { |
|||
this.gradientListFormArray.push(this.colorGradientControl('rgba(0,0,0,0.87)')); |
|||
} |
|||
this.gradientFormGroup.markAsDirty(); |
|||
setTimeout(() => {this.popover?.updatePosition();}, 0); |
|||
} |
|||
|
|||
updateModel() { |
|||
this.propagateChange( |
|||
{ |
|||
advancedMode: this.gradientFormGroup.value.advancedMode, |
|||
gradient: [this.gradientFormGroup.value.gradient.start, |
|||
...this.gradientFormGroup.value.gradient.gradientList.map(item => item.color), |
|||
this.gradientFormGroup.value.gradient.end], |
|||
gradientAdvanced: [this.gradientFormGroup.value.gradientAdvanced.start, |
|||
...this.gradientFormGroup.value.gradientAdvanced.gradientList, |
|||
this.gradientFormGroup.value.gradientAdvanced.end] |
|||
} |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,77 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2024 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. |
|||
|
|||
--> |
|||
<div [formGroup]="valueSourceFormGroup" class="tb-value-source tb-form-row no-padding no-border"> |
|||
<div class="tb-source-field"> |
|||
<mat-form-field class="tb-type-field" appearance="outline" subscriptSizing="dynamic"> |
|||
<mat-select formControlName="type"> |
|||
<mat-option *ngFor="let type of valueSourceDataKeyTypes" [value]="type"> |
|||
{{ valueSourceDataKeyTypeTranslation.get(type) | translate }} |
|||
</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
<tb-entity-alias-input |
|||
class="tb-entity-alias-field" |
|||
*ngIf="valueSourceFormGroup.get('type').value === valueSourceDataKeyType.entity" |
|||
[aliasController]="aliasController" |
|||
required |
|||
formControlName="entityAlias"> |
|||
</tb-entity-alias-input> |
|||
</div> |
|||
<div class="tb-key-value-field"> |
|||
<mat-form-field *ngIf="valueSourceFormGroup.get('type').value === valueSourceDataKeyType.constant" |
|||
appearance="outline" class="tb-constant-field number" subscriptSizing="dynamic" |
|||
[class.tb-suffix-absolute]="!valueSourceFormGroup.get('value').value"> |
|||
<input required matInput formControlName="value" type="number" placeholder="{{ 'widget-config.set' | translate }}"> |
|||
<mat-icon matSuffix |
|||
matTooltipPosition="above" |
|||
matTooltipClass="tb-error-tooltip" |
|||
[matTooltip]="'widgets.value-source.value-required' | translate" |
|||
*ngIf="valueSourceFormGroup.get('value').hasError('required') |
|||
&& valueSourceFormGroup.get('value').touched" |
|||
class="tb-error"> |
|||
warning |
|||
</mat-icon> |
|||
</mat-form-field> |
|||
<tb-data-key-input |
|||
*ngIf="valueSourceFormGroup.get('type').value === valueSourceDataKeyType.latestKey" |
|||
required |
|||
requiredText="widgets.value-source.key-required" |
|||
[datasourceType]="datasource?.type" |
|||
[entityAliasId]="datasource?.entityAliasId" |
|||
[deviceId]="datasource?.deviceId" |
|||
[aliasController]="aliasController" |
|||
[dataKeyType]="datasource?.type === datasourceType.function ? dataKeyType.function : null" |
|||
[dataKeyTypes]="[dataKeyType.attribute, dataKeyType.timeseries]" |
|||
[callbacks]="dataKeyCallbacks" |
|||
[editable]="false" |
|||
[formControl]="latestKeyFormControl"> |
|||
</tb-data-key-input> |
|||
<tb-data-key-input |
|||
*ngIf="valueSourceFormGroup.get('type').value === valueSourceDataKeyType.entity" |
|||
required |
|||
requiredText="widgets.value-source.entity-key-required" |
|||
[datasourceType]="datasourceType.entity" |
|||
[aliasController]="aliasController" |
|||
[entityAlias]="valueSourceFormGroup.get('entityAlias').value" |
|||
[dataKeyTypes]="[dataKeyType.attribute, dataKeyType.timeseries]" |
|||
[callbacks]="dataKeyCallbacks" |
|||
[editable]="false" |
|||
[formControl]="entityKeyFormControl"> |
|||
</tb-data-key-input> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,191 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 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, forwardRef, Input, OnInit } from '@angular/core'; |
|||
import { |
|||
ControlValueAccessor, |
|||
NG_VALUE_ACCESSOR, |
|||
UntypedFormBuilder, |
|||
UntypedFormControl, |
|||
UntypedFormGroup, |
|||
Validators |
|||
} from '@angular/forms'; |
|||
import { PageComponent } from '@shared/components/page.component'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { IAliasController } from '@core/api/widget-api.models'; |
|||
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; |
|||
import { DataKey, Datasource, DatasourceType } from '@app/shared/models/widget.models'; |
|||
import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models'; |
|||
import { |
|||
ValueSourceDataKeyType, |
|||
ValueSourceDataKeyTypes, |
|||
ValueSourceDataKeyTypeTranslation |
|||
} from '@shared/models/widget-settings.models'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-value-source-data-key', |
|||
templateUrl: './value-source-data-key.component.html', |
|||
styleUrls: ['value-source-data-key.component.scss'], |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => ValueSourceDataKeyComponent), |
|||
multi: true |
|||
} |
|||
] |
|||
}) |
|||
export class ValueSourceDataKeyComponent extends PageComponent implements OnInit, ControlValueAccessor { |
|||
|
|||
dataKeyType = DataKeyType; |
|||
datasourceType = DatasourceType; |
|||
|
|||
valueSourceDataKeyType = ValueSourceDataKeyType; |
|||
valueSourceDataKeyTypes = ValueSourceDataKeyTypes; |
|||
valueSourceDataKeyTypeTranslation = ValueSourceDataKeyTypeTranslation; |
|||
|
|||
@Input() |
|||
disabled: boolean; |
|||
|
|||
@Input() |
|||
aliasController: IAliasController; |
|||
|
|||
@Input() |
|||
dataKeyCallbacks: DataKeysCallbacks; |
|||
|
|||
@Input() |
|||
datasource: Datasource; |
|||
|
|||
valueSourceFormGroup: UntypedFormGroup; |
|||
|
|||
modelValue; |
|||
|
|||
latestKeyFormControl: UntypedFormControl; |
|||
entityKeyFormControl: UntypedFormControl; |
|||
|
|||
private propagateChange = (_val: any) => {}; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
private fb: UntypedFormBuilder, |
|||
private cd: ChangeDetectorRef) { |
|||
super(store); |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
this.valueSourceFormGroup = this.fb.group({ |
|||
type: [ValueSourceDataKeyType.constant, []], |
|||
value: [null, [Validators.required]], |
|||
entityAlias: [null, [Validators.required]] |
|||
}); |
|||
this.latestKeyFormControl = this.fb.control(null, [Validators.required]); |
|||
this.entityKeyFormControl = this.fb.control(null, [Validators.required]); |
|||
this.valueSourceFormGroup.valueChanges.subscribe( |
|||
() => this.updateModel() |
|||
); |
|||
this.latestKeyFormControl.valueChanges.subscribe( |
|||
() => this.updateModel() |
|||
); |
|||
this.entityKeyFormControl.valueChanges.subscribe( |
|||
() => this.updateModel() |
|||
); |
|||
this.valueSourceFormGroup.get('type').valueChanges.subscribe(() => { |
|||
this.updateValidators(); |
|||
}); |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
} |
|||
|
|||
setDisabledState(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
if (isDisabled) { |
|||
this.valueSourceFormGroup.disable({emitEvent: false}); |
|||
this.latestKeyFormControl.disable({emitEvent: false}); |
|||
this.entityKeyFormControl.disable({emitEvent: false}); |
|||
} else { |
|||
this.valueSourceFormGroup.enable({emitEvent: false}); |
|||
this.updateValidators(); |
|||
} |
|||
} |
|||
|
|||
writeValue(value): void { |
|||
this.modelValue = value; |
|||
this.valueSourceFormGroup.patchValue( |
|||
{ |
|||
type: value.type, |
|||
value: value.value, |
|||
entityAlias: value.entityAlias |
|||
}, {emitEvent: false} |
|||
); |
|||
if (value.type === ValueSourceDataKeyType.latestKey) { |
|||
this.latestKeyFormControl.patchValue({ |
|||
type: value.latestKeyType, |
|||
name: value.latestKey |
|||
}, {emitEvent: false}); |
|||
} else if (value.type === ValueSourceDataKeyType.entity) { |
|||
this.entityKeyFormControl.patchValue({ |
|||
type: value.entityKeyType, |
|||
name: value.entityKey |
|||
}, {emitEvent: false}); |
|||
} |
|||
|
|||
this.updateValidators(); |
|||
this.cd.markForCheck(); |
|||
} |
|||
|
|||
private updateModel() { |
|||
const value = this.valueSourceFormGroup.value; |
|||
this.modelValue.type = value.type; |
|||
this.modelValue.value = value.value; |
|||
this.modelValue.entityAlias = value.entityAlias; |
|||
|
|||
if (value.type === ValueSourceDataKeyType.latestKey) { |
|||
const latestKey: DataKey = this.latestKeyFormControl.value; |
|||
this.modelValue.latestKey = latestKey?.name; |
|||
this.modelValue.latestKeyType = (latestKey?.type as any); |
|||
} else if (value.type === ValueSourceDataKeyType.entity) { |
|||
const entityKey: DataKey = this.entityKeyFormControl.value; |
|||
this.modelValue.entityKey = entityKey?.name; |
|||
this.modelValue.entityKeyType = (entityKey?.type as any); |
|||
} |
|||
this.propagateChange(this.modelValue); |
|||
} |
|||
|
|||
private updateValidators(): void { |
|||
const type: ValueSourceDataKeyType = this.valueSourceFormGroup.get('type').value; |
|||
if (type === ValueSourceDataKeyType.constant) { |
|||
this.valueSourceFormGroup.get('value').enable({emitEvent: false}); |
|||
this.valueSourceFormGroup.get('entityAlias').disable({emitEvent: false}); |
|||
this.latestKeyFormControl.disable({emitEvent: false}); |
|||
this.entityKeyFormControl.disable({emitEvent: false}); |
|||
} else if (type === ValueSourceDataKeyType.latestKey) { |
|||
this.valueSourceFormGroup.get('value').disable({emitEvent: false}); |
|||
this.valueSourceFormGroup.get('entityAlias').disable({emitEvent: false}); |
|||
this.latestKeyFormControl.enable({emitEvent: false}); |
|||
this.entityKeyFormControl.disable({emitEvent: false}); |
|||
} else if (type === ValueSourceDataKeyType.entity) { |
|||
this.valueSourceFormGroup.get('value').disable({emitEvent: false}); |
|||
this.valueSourceFormGroup.get('entityAlias').enable({emitEvent: false}); |
|||
this.latestKeyFormControl.disable({emitEvent: false}); |
|||
this.entityKeyFormControl.enable({emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
} |
|||
@ -1,60 +0,0 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2024 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-expansion-panel class="fixed-color-level" fxFlex [formGroup]="fixedColorLevelFormGroup" [(expanded)]="expanded"> |
|||
<mat-expansion-panel-header> |
|||
<div fxFlex fxLayout="row" fxLayoutAlign="start center"> |
|||
<mat-panel-title> |
|||
<div fxLayout="row" fxFlex fxLayoutAlign="start center"> |
|||
<div>{{ fixedColorLevelRangeText() }}</div> |
|||
<div class="tb-color-preview" style="margin-left: 6px;"> |
|||
<div class="tb-color-result" [ngStyle]="{background: fixedColorLevelFormGroup.get('color').value}"></div> |
|||
</div> |
|||
</div> |
|||
</mat-panel-title> |
|||
<span fxFlex></span> |
|||
<button *ngIf="!disabled" mat-icon-button style="min-width: 40px;" |
|||
type="button" |
|||
(click)="removeFixedColorLevel.emit()" |
|||
matTooltip="{{ 'action.remove' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon>delete</mat-icon> |
|||
</button> |
|||
</div> |
|||
</mat-expansion-panel-header> |
|||
<ng-template matExpansionPanelContent> |
|||
<div fxLayout="column" fxLayoutGap="0.5em"> |
|||
<mat-divider></mat-divider> |
|||
<section class="tb-widget-settings" fxLayout="column"> |
|||
<fieldset class="fields-group"> |
|||
<legend class="group-title" translate>widgets.gauge.from</legend> |
|||
<tb-value-source formControlName="from" [aliasController]="aliasController"></tb-value-source> |
|||
</fieldset> |
|||
<fieldset class="fields-group"> |
|||
<legend class="group-title" translate>widgets.gauge.to</legend> |
|||
<tb-value-source formControlName="to" [aliasController]="aliasController"></tb-value-source> |
|||
</fieldset> |
|||
<tb-color-input |
|||
required |
|||
formControlName="color" |
|||
icon="format_color_fill" |
|||
label="{{ 'widgets.gauge.color' | translate }}" openOnInput colorClearButton> |
|||
</tb-color-input> |
|||
</section> |
|||
</div> |
|||
</ng-template> |
|||
</mat-expansion-panel> |
|||
@ -1,147 +0,0 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 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 { ValueSourceProperty } from '@home/components/widget/lib/settings/common/value-source.component'; |
|||
import { Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core'; |
|||
import { |
|||
AbstractControl, |
|||
ControlValueAccessor, |
|||
UntypedFormBuilder, |
|||
UntypedFormGroup, |
|||
NG_VALUE_ACCESSOR, ValidationErrors, |
|||
Validators |
|||
} from '@angular/forms'; |
|||
import { PageComponent } from '@shared/components/page.component'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { isNumber } from '@core/utils'; |
|||
import { IAliasController } from '@core/api/widget-api.models'; |
|||
|
|||
export interface FixedColorLevel { |
|||
from?: ValueSourceProperty; |
|||
to?: ValueSourceProperty; |
|||
color: string; |
|||
} |
|||
|
|||
export function fixedColorLevelValidator(control: AbstractControl): ValidationErrors | null { |
|||
const fixedColorLevel: FixedColorLevel = control.value; |
|||
if (!fixedColorLevel || !fixedColorLevel.color) { |
|||
return { |
|||
fixedColorLevel: true |
|||
}; |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'tb-fixed-color-level', |
|||
templateUrl: './fixed-color-level.component.html', |
|||
styleUrls: ['./fixed-color-level.component.scss'], |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => FixedColorLevelComponent), |
|||
multi: true |
|||
} |
|||
] |
|||
}) |
|||
export class FixedColorLevelComponent extends PageComponent implements OnInit, ControlValueAccessor { |
|||
|
|||
@Input() |
|||
disabled: boolean; |
|||
|
|||
@Input() |
|||
expanded = false; |
|||
|
|||
@Input() |
|||
aliasController: IAliasController; |
|||
|
|||
@Output() |
|||
removeFixedColorLevel = new EventEmitter(); |
|||
|
|||
private modelValue: FixedColorLevel; |
|||
|
|||
private propagateChange = null; |
|||
|
|||
public fixedColorLevelFormGroup: UntypedFormGroup; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
private translate: TranslateService, |
|||
private fb: UntypedFormBuilder) { |
|||
super(store); |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
this.fixedColorLevelFormGroup = this.fb.group({ |
|||
from: [null, []], |
|||
to: [null, []], |
|||
color: [null, [Validators.required]] |
|||
}); |
|||
this.fixedColorLevelFormGroup.valueChanges.subscribe(() => { |
|||
this.updateModel(); |
|||
}); |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
} |
|||
|
|||
setDisabledState(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
if (isDisabled) { |
|||
this.fixedColorLevelFormGroup.disable({emitEvent: false}); |
|||
} else { |
|||
this.fixedColorLevelFormGroup.enable({emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
writeValue(value: FixedColorLevel): void { |
|||
this.modelValue = value; |
|||
this.fixedColorLevelFormGroup.patchValue( |
|||
value, {emitEvent: false} |
|||
); |
|||
} |
|||
|
|||
fixedColorLevelRangeText(): string { |
|||
const value: FixedColorLevel = this.fixedColorLevelFormGroup.value; |
|||
const from = this.valueSourcePropertyText(value?.from); |
|||
const to = this.valueSourcePropertyText(value?.to); |
|||
return `${from} - ${to}`; |
|||
} |
|||
|
|||
private valueSourcePropertyText(source?: ValueSourceProperty): string { |
|||
if (source) { |
|||
if (source.valueSource === 'predefinedValue') { |
|||
return `${isNumber(source.value) ? source.value : 0}`; |
|||
} else if (source.valueSource === 'entityAttribute') { |
|||
const alias = source.entityAlias || 'Undefined'; |
|||
const key = source.attribute || 'Undefined'; |
|||
return `${alias}.${key}`; |
|||
} |
|||
} |
|||
return 'Undefined'; |
|||
} |
|||
|
|||
private updateModel() { |
|||
const value: FixedColorLevel = this.fixedColorLevelFormGroup.value; |
|||
this.modelValue = value; |
|||
this.propagateChange(this.modelValue); |
|||
} |
|||
} |
|||
|
After Width: | Height: | Size: 8.6 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 8.2 KiB |
Loading…
Reference in new issue