95 changed files with 4587 additions and 479 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,214 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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]="singleSwitchWidgetConfigForm"> |
||||
|
<tb-target-device formControlName="targetDevice"></tb-target-device> |
||||
|
<div class="tb-form-panel"> |
||||
|
<div class="tb-form-panel-title" translate>widgets.single-switch.behavior</div> |
||||
|
<div class="tb-form-row"> |
||||
|
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.rpc-state.initial-state-hint' | translate}}" translate>widgets.rpc-state.initial-state</div> |
||||
|
<tb-rpc-initial-state-settings fxFlex |
||||
|
[stateValueType]="valueType.BOOLEAN" |
||||
|
[aliasController]="aliasController" |
||||
|
[targetDevice]="targetDevice" |
||||
|
formControlName="initialState"></tb-rpc-initial-state-settings> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.rpc-state.turn-on-hint' | translate}}" translate>widgets.rpc-state.turn-on</div> |
||||
|
<tb-rpc-update-state-settings fxFlex |
||||
|
panelTitle="widgets.rpc-state.turn-on" |
||||
|
[stateValueType]="valueType.BOOLEAN" |
||||
|
[aliasController]="aliasController" |
||||
|
[targetDevice]="targetDevice" |
||||
|
formControlName="onUpdateState"></tb-rpc-update-state-settings> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.rpc-state.turn-off-hint' | translate}}" translate>widgets.rpc-state.turn-off</div> |
||||
|
<tb-rpc-update-state-settings fxFlex |
||||
|
panelTitle="widgets.rpc-state.turn-off" |
||||
|
[stateValueType]="valueType.BOOLEAN" |
||||
|
[aliasController]="aliasController" |
||||
|
[targetDevice]="targetDevice" |
||||
|
formControlName="offUpdateState"></tb-rpc-update-state-settings> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-panel"> |
||||
|
<div class="tb-form-panel-title" translate>widget-config.appearance</div> |
||||
|
<tb-image-cards-select rowHeight="2:1" |
||||
|
[cols]="{columns: 3, |
||||
|
breakpoints: { |
||||
|
'lt-sm': 1, |
||||
|
'lt-md': 2 |
||||
|
}}" |
||||
|
label="{{ 'widgets.single-switch.layout' | translate }}" formControlName="layout"> |
||||
|
<tb-image-cards-select-option *ngFor="let layout of singleSwitchLayouts" |
||||
|
[value]="layout" |
||||
|
[image]="singleSwitchLayoutImageMap.get(layout)"> |
||||
|
{{ singleSwitchLayoutTranslationMap.get(layout) | translate }} |
||||
|
</tb-image-cards-select-option> |
||||
|
</tb-image-cards-select> |
||||
|
<div class="tb-form-row"> |
||||
|
<mat-slide-toggle class="mat-slide" formControlName="autoScale"> |
||||
|
{{ 'widgets.single-switch.auto-scale' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
</div> |
||||
|
<div class="tb-form-row"> |
||||
|
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showLabel"> |
||||
|
{{ 'widgets.single-switch.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="label" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
</mat-form-field> |
||||
|
<tb-font-settings formControlName="labelFont" |
||||
|
[previewText]="singleSwitchWidgetConfigForm.get('label').value"> |
||||
|
</tb-font-settings> |
||||
|
<tb-color-input asBoxInput |
||||
|
colorClearButton |
||||
|
formControlName="labelColor"> |
||||
|
</tb-color-input> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row"> |
||||
|
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showIcon"> |
||||
|
{{ 'widgets.single-switch.icon' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
<div fxFlex fxLayout="row" fxLayoutAlign="start 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]="singleSwitchWidgetConfigForm.get('iconColor').value?.color" |
||||
|
formControlName="icon"> |
||||
|
</tb-material-icon-select> |
||||
|
<tb-color-input asBoxInput |
||||
|
colorClearButton |
||||
|
formControlName="iconColor"> |
||||
|
</tb-color-input> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between column-xs"> |
||||
|
<div>{{ 'widgets.single-switch.switch-color' | translate }}</div> |
||||
|
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutAlign.lt-sm="space-between center" fxLayoutGap="12px"> |
||||
|
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
||||
|
<div translate>widgets.single-switch.on</div> |
||||
|
<tb-color-input asBoxInput |
||||
|
formControlName="switchColorOn"> |
||||
|
</tb-color-input> |
||||
|
</div> |
||||
|
<mat-divider vertical fxHide.lt-sm></mat-divider> |
||||
|
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
||||
|
<div translate>widgets.single-switch.off</div> |
||||
|
<tb-color-input asBoxInput |
||||
|
formControlName="switchColorOff"> |
||||
|
</tb-color-input> |
||||
|
</div> |
||||
|
<mat-divider vertical fxHide.lt-sm></mat-divider> |
||||
|
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
||||
|
<div translate>widgets.single-switch.disabled</div> |
||||
|
<tb-color-input asBoxInput |
||||
|
formControlName="switchColorDisabled"> |
||||
|
</tb-color-input> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between column-xs"> |
||||
|
<div>{{ 'widgets.single-switch.tumbler-color' | translate }}</div> |
||||
|
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutAlign.lt-sm="space-between center" fxLayoutGap="12px"> |
||||
|
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
||||
|
<div translate>widgets.single-switch.on</div> |
||||
|
<tb-color-input asBoxInput |
||||
|
formControlName="tumblerColorOn"> |
||||
|
</tb-color-input> |
||||
|
</div> |
||||
|
<mat-divider vertical fxHide.lt-sm></mat-divider> |
||||
|
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
||||
|
<div translate>widgets.single-switch.off</div> |
||||
|
<tb-color-input asBoxInput |
||||
|
formControlName="tumblerColorOff"> |
||||
|
</tb-color-input> |
||||
|
</div> |
||||
|
<mat-divider vertical fxHide.lt-sm></mat-divider> |
||||
|
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
||||
|
<div translate>widgets.single-switch.disabled</div> |
||||
|
<tb-color-input asBoxInput |
||||
|
formControlName="tumblerColorDisabled"> |
||||
|
</tb-color-input> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row"> |
||||
|
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showOnLabel"> |
||||
|
{{ 'widgets.single-switch.on-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="onLabel" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
</mat-form-field> |
||||
|
<tb-font-settings formControlName="onLabelFont" |
||||
|
[previewText]="singleSwitchWidgetConfigForm.get('onLabel').value"> |
||||
|
</tb-font-settings> |
||||
|
<tb-color-input asBoxInput |
||||
|
colorClearButton |
||||
|
formControlName="onLabelColor"> |
||||
|
</tb-color-input> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row"> |
||||
|
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showOffLabel"> |
||||
|
{{ 'widgets.single-switch.off-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="offLabel" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
</mat-form-field> |
||||
|
<tb-font-settings formControlName="offLabelFont" |
||||
|
[previewText]="singleSwitchWidgetConfigForm.get('offLabel').value"> |
||||
|
</tb-font-settings> |
||||
|
<tb-color-input asBoxInput |
||||
|
colorClearButton |
||||
|
formControlName="offLabelColor"> |
||||
|
</tb-color-input> |
||||
|
</div> |
||||
|
</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>{{ 'widgets.background.background' | translate }}</div> |
||||
|
<tb-background-settings formControlName="background"> |
||||
|
</tb-background-settings> |
||||
|
</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,231 @@ |
|||||
|
///
|
||||
|
/// 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 } from '@angular/core'; |
||||
|
import { 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 { TargetDevice, WidgetConfig, } from '@shared/models/widget.models'; |
||||
|
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; |
||||
|
import { isUndefined } from '@core/utils'; |
||||
|
import { |
||||
|
singleSwitchDefaultSettings, |
||||
|
singleSwitchLayoutImages, |
||||
|
singleSwitchLayouts, |
||||
|
singleSwitchLayoutTranslations, |
||||
|
SingleSwitchWidgetSettings |
||||
|
} from '@home/components/widget/lib/rpc/single-switch-widget.models'; |
||||
|
import { ValueType } from '@shared/models/constants'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-single-switch-basic-config', |
||||
|
templateUrl: './single-switch-basic-config.component.html', |
||||
|
styleUrls: ['../basic-config.scss'] |
||||
|
}) |
||||
|
export class SingSwitchBasicConfigComponent extends BasicWidgetConfigComponent { |
||||
|
|
||||
|
get targetDevice(): TargetDevice { |
||||
|
return this.singleSwitchWidgetConfigForm.get('targetDevice').value; |
||||
|
} |
||||
|
|
||||
|
singleSwitchLayouts = singleSwitchLayouts; |
||||
|
|
||||
|
singleSwitchLayoutTranslationMap = singleSwitchLayoutTranslations; |
||||
|
singleSwitchLayoutImageMap = singleSwitchLayoutImages; |
||||
|
|
||||
|
valueType = ValueType; |
||||
|
|
||||
|
singleSwitchWidgetConfigForm: UntypedFormGroup; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
protected widgetConfigComponent: WidgetConfigComponent, |
||||
|
private fb: UntypedFormBuilder) { |
||||
|
super(store, widgetConfigComponent); |
||||
|
} |
||||
|
|
||||
|
protected configForm(): UntypedFormGroup { |
||||
|
return this.singleSwitchWidgetConfigForm; |
||||
|
} |
||||
|
|
||||
|
protected onConfigSet(configData: WidgetConfigComponentData) { |
||||
|
const settings: SingleSwitchWidgetSettings = {...singleSwitchDefaultSettings, ...(configData.config.settings || {})}; |
||||
|
this.singleSwitchWidgetConfigForm = this.fb.group({ |
||||
|
targetDevice: [configData.config.targetDevice, []], |
||||
|
|
||||
|
initialState: [settings.initialState, []], |
||||
|
onUpdateState: [settings.onUpdateState, []], |
||||
|
offUpdateState: [settings.offUpdateState, []], |
||||
|
|
||||
|
layout: [settings.layout, []], |
||||
|
autoScale: [settings.autoScale, []], |
||||
|
|
||||
|
showLabel: [settings.showLabel, []], |
||||
|
label: [settings.label, []], |
||||
|
labelFont: [settings.labelFont, []], |
||||
|
labelColor: [settings.labelColor, []], |
||||
|
|
||||
|
showIcon: [settings.showIcon, []], |
||||
|
iconSize: [settings.iconSize, [Validators.min(0)]], |
||||
|
iconSizeUnit: [settings.iconSizeUnit, []], |
||||
|
icon: [settings.icon, []], |
||||
|
iconColor: [settings.iconColor, []], |
||||
|
|
||||
|
switchColorOn: [settings.switchColorOn, []], |
||||
|
switchColorOff: [settings.switchColorOff, []], |
||||
|
switchColorDisabled: [settings.switchColorDisabled, []], |
||||
|
|
||||
|
tumblerColorOn: [settings.tumblerColorOn, []], |
||||
|
tumblerColorOff: [settings.tumblerColorOff, []], |
||||
|
tumblerColorDisabled: [settings.tumblerColorDisabled, []], |
||||
|
|
||||
|
showOnLabel: [settings.showOnLabel, []], |
||||
|
onLabel: [settings.onLabel, []], |
||||
|
onLabelFont: [settings.onLabelFont, []], |
||||
|
onLabelColor: [settings.onLabelColor, []], |
||||
|
|
||||
|
showOffLabel: [settings.showOffLabel, []], |
||||
|
offLabel: [settings.offLabel, []], |
||||
|
offLabelFont: [settings.offLabelFont, []], |
||||
|
offLabelColor: [settings.offLabelColor, []], |
||||
|
|
||||
|
background: [settings.background, []], |
||||
|
|
||||
|
cardButtons: [this.getCardButtons(configData.config), []], |
||||
|
borderRadius: [configData.config.borderRadius, []], |
||||
|
|
||||
|
actions: [configData.config.actions || {}, []] |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
protected prepareOutputConfig(config: any): WidgetConfigComponentData { |
||||
|
this.widgetConfig.config.targetDevice = config.targetDevice; |
||||
|
|
||||
|
this.widgetConfig.config.settings = this.widgetConfig.config.settings || {}; |
||||
|
|
||||
|
this.widgetConfig.config.settings.initialState = config.initialState; |
||||
|
this.widgetConfig.config.settings.onUpdateState = config.onUpdateState; |
||||
|
this.widgetConfig.config.settings.offUpdateState = config.offUpdateState; |
||||
|
|
||||
|
this.widgetConfig.config.settings.layout = config.layout; |
||||
|
this.widgetConfig.config.settings.autoScale = config.autoScale; |
||||
|
|
||||
|
this.widgetConfig.config.settings.showLabel = config.showLabel; |
||||
|
this.widgetConfig.config.settings.label = config.label; |
||||
|
this.widgetConfig.config.settings.labelFont = config.labelFont; |
||||
|
this.widgetConfig.config.settings.labelColor = config.labelColor; |
||||
|
|
||||
|
this.widgetConfig.config.settings.showIcon = config.showIcon; |
||||
|
this.widgetConfig.config.settings.iconSize = config.iconSize; |
||||
|
this.widgetConfig.config.settings.iconSizeUnit = config.iconSizeUnit; |
||||
|
this.widgetConfig.config.settings.icon = config.icon; |
||||
|
this.widgetConfig.config.settings.iconColor = config.iconColor; |
||||
|
|
||||
|
this.widgetConfig.config.settings.switchColorOn = config.switchColorOn; |
||||
|
this.widgetConfig.config.settings.switchColorOff = config.switchColorOff; |
||||
|
this.widgetConfig.config.settings.switchColorDisabled = config.switchColorDisabled; |
||||
|
|
||||
|
this.widgetConfig.config.settings.tumblerColorOn = config.tumblerColorOn; |
||||
|
this.widgetConfig.config.settings.tumblerColorOff = config.tumblerColorOff; |
||||
|
this.widgetConfig.config.settings.tumblerColorDisabled = config.tumblerColorDisabled; |
||||
|
|
||||
|
this.widgetConfig.config.settings.showOnLabel = config.showOnLabel; |
||||
|
this.widgetConfig.config.settings.onLabel = config.onLabel; |
||||
|
this.widgetConfig.config.settings.onLabelFont = config.onLabelFont; |
||||
|
this.widgetConfig.config.settings.onLabelColor = config.onLabelColor; |
||||
|
|
||||
|
this.widgetConfig.config.settings.showOffLabel = config.showOffLabel; |
||||
|
this.widgetConfig.config.settings.offLabel = config.offLabel; |
||||
|
this.widgetConfig.config.settings.offLabelFont = config.offLabelFont; |
||||
|
this.widgetConfig.config.settings.offLabelColor = config.offLabelColor; |
||||
|
|
||||
|
this.widgetConfig.config.settings.background = config.background; |
||||
|
|
||||
|
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 ['showLabel', 'showIcon', 'showOnLabel', 'showOffLabel']; |
||||
|
} |
||||
|
|
||||
|
protected updateValidators(emitEvent: boolean, trigger?: string) { |
||||
|
const showLabel: boolean = this.singleSwitchWidgetConfigForm.get('showLabel').value; |
||||
|
const showIcon: boolean = this.singleSwitchWidgetConfigForm.get('showIcon').value; |
||||
|
const showOnLabel: boolean = this.singleSwitchWidgetConfigForm.get('showOnLabel').value; |
||||
|
const showOffLabel: boolean = this.singleSwitchWidgetConfigForm.get('showOffLabel').value; |
||||
|
|
||||
|
if (showLabel) { |
||||
|
this.singleSwitchWidgetConfigForm.get('label').enable(); |
||||
|
this.singleSwitchWidgetConfigForm.get('labelFont').enable(); |
||||
|
this.singleSwitchWidgetConfigForm.get('labelColor').enable(); |
||||
|
} else { |
||||
|
this.singleSwitchWidgetConfigForm.get('label').disable(); |
||||
|
this.singleSwitchWidgetConfigForm.get('labelFont').disable(); |
||||
|
this.singleSwitchWidgetConfigForm.get('labelColor').disable(); |
||||
|
} |
||||
|
|
||||
|
if (showIcon) { |
||||
|
this.singleSwitchWidgetConfigForm.get('iconSize').enable(); |
||||
|
this.singleSwitchWidgetConfigForm.get('iconSizeUnit').enable(); |
||||
|
this.singleSwitchWidgetConfigForm.get('icon').enable(); |
||||
|
this.singleSwitchWidgetConfigForm.get('iconColor').enable(); |
||||
|
} else { |
||||
|
this.singleSwitchWidgetConfigForm.get('iconSize').disable(); |
||||
|
this.singleSwitchWidgetConfigForm.get('iconSizeUnit').disable(); |
||||
|
this.singleSwitchWidgetConfigForm.get('icon').disable(); |
||||
|
this.singleSwitchWidgetConfigForm.get('iconColor').disable(); |
||||
|
} |
||||
|
|
||||
|
if (showOnLabel) { |
||||
|
this.singleSwitchWidgetConfigForm.get('onLabel').enable(); |
||||
|
this.singleSwitchWidgetConfigForm.get('onLabelFont').enable(); |
||||
|
this.singleSwitchWidgetConfigForm.get('onLabelColor').enable(); |
||||
|
} else { |
||||
|
this.singleSwitchWidgetConfigForm.get('onLabel').disable(); |
||||
|
this.singleSwitchWidgetConfigForm.get('onLabelFont').disable(); |
||||
|
this.singleSwitchWidgetConfigForm.get('onLabelColor').disable(); |
||||
|
} |
||||
|
|
||||
|
if (showOffLabel) { |
||||
|
this.singleSwitchWidgetConfigForm.get('offLabel').enable(); |
||||
|
this.singleSwitchWidgetConfigForm.get('offLabelFont').enable(); |
||||
|
this.singleSwitchWidgetConfigForm.get('offLabelColor').enable(); |
||||
|
} else { |
||||
|
this.singleSwitchWidgetConfigForm.get('offLabel').disable(); |
||||
|
this.singleSwitchWidgetConfigForm.get('offLabelFont').disable(); |
||||
|
this.singleSwitchWidgetConfigForm.get('offLabelColor').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'); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,39 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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]="targetDeviceFormGroup" class="tb-form-panel"> |
||||
|
<div fxLayout="row" fxLayoutAlign="space-between center"> |
||||
|
<div class="tb-form-panel-title" translate>widget-config.target-device</div> |
||||
|
<tb-toggle-select formControlName="type"> |
||||
|
<tb-toggle-option [value]="targetDeviceType.device">{{ 'device.device' | translate }}</tb-toggle-option> |
||||
|
<tb-toggle-option [value]="targetDeviceType.entity">{{ 'entity.entity-alias' | translate }}</tb-toggle-option> |
||||
|
</tb-toggle-select> |
||||
|
</div> |
||||
|
<tb-entity-autocomplete *ngIf="targetDeviceFormGroup.get('type').value === targetDeviceType.device" |
||||
|
[required]="!widgetEditMode" |
||||
|
[entityType]="entityType.DEVICE" |
||||
|
formControlName="deviceId"> |
||||
|
</tb-entity-autocomplete> |
||||
|
<tb-entity-alias-select |
||||
|
*ngIf="targetDeviceFormGroup.get('type').value === targetDeviceType.entity" |
||||
|
[tbRequired]="!widgetEditMode" |
||||
|
[aliasController]="aliasController" |
||||
|
[allowedEntityTypes]="[entityType.DEVICE]" |
||||
|
[callbacks]="entityAliasSelectCallbacks" |
||||
|
formControlName="entityAliasId"> |
||||
|
</tb-entity-alias-select> |
||||
|
</div> |
||||
@ -0,0 +1,166 @@ |
|||||
|
///
|
||||
|
/// 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 { |
||||
|
ControlValueAccessor, |
||||
|
NG_VALIDATORS, |
||||
|
NG_VALUE_ACCESSOR, |
||||
|
UntypedFormBuilder, |
||||
|
UntypedFormControl, |
||||
|
UntypedFormGroup, |
||||
|
Validator, |
||||
|
Validators |
||||
|
} from '@angular/forms'; |
||||
|
import { Component, forwardRef, Input, OnInit } from '@angular/core'; |
||||
|
import { UtilsService } from '@core/services/utils.service'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; |
||||
|
import { TargetDevice, TargetDeviceType } from '@shared/models/widget.models'; |
||||
|
import { EntityType } from '@shared/models/entity-type.models'; |
||||
|
import { IAliasController } from '@core/api/widget-api.models'; |
||||
|
import { EntityAliasSelectCallbacks } from '@home/components/alias/entity-alias-select.component.models'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-target-device', |
||||
|
templateUrl: './target-device.component.html', |
||||
|
styleUrls: [], |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => TargetDeviceComponent), |
||||
|
multi: true |
||||
|
}, |
||||
|
{ |
||||
|
provide: NG_VALIDATORS, |
||||
|
useExisting: forwardRef(() => TargetDeviceComponent), |
||||
|
multi: true, |
||||
|
} |
||||
|
] |
||||
|
}) |
||||
|
export class TargetDeviceComponent implements ControlValueAccessor, OnInit, Validator { |
||||
|
|
||||
|
public get aliasController(): IAliasController { |
||||
|
return this.widgetConfigComponent.aliasController; |
||||
|
} |
||||
|
|
||||
|
public get entityAliasSelectCallbacks(): EntityAliasSelectCallbacks { |
||||
|
return this.widgetConfigComponent.widgetConfigCallbacks; |
||||
|
} |
||||
|
|
||||
|
targetDeviceType = TargetDeviceType; |
||||
|
|
||||
|
entityType = EntityType; |
||||
|
|
||||
|
@Input() |
||||
|
disabled: boolean; |
||||
|
|
||||
|
widgetEditMode = this.utils.widgetEditMode; |
||||
|
|
||||
|
targetDeviceFormGroup: UntypedFormGroup; |
||||
|
|
||||
|
private propagateChange = (_val: any) => {}; |
||||
|
|
||||
|
constructor(private fb: UntypedFormBuilder, |
||||
|
private utils: UtilsService, |
||||
|
public translate: TranslateService, |
||||
|
private widgetConfigComponent: WidgetConfigComponent) { |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
if (!this.targetDeviceFormGroup.valid) { |
||||
|
setTimeout(() => { |
||||
|
this.targetDeviceUpdated(this.targetDeviceFormGroup.value); |
||||
|
}, 0); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: any): void { |
||||
|
} |
||||
|
|
||||
|
setDisabledState(isDisabled: boolean): void { |
||||
|
this.disabled = isDisabled; |
||||
|
if (this.disabled) { |
||||
|
this.targetDeviceFormGroup.disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.targetDeviceFormGroup.enable({emitEvent: false}); |
||||
|
this.updateValidators(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
this.targetDeviceFormGroup = this.fb.group({ |
||||
|
type: [null, !this.widgetEditMode ? [Validators.required] : []], |
||||
|
deviceId: [null, !this.widgetEditMode ? [Validators.required] : []], |
||||
|
entityAliasId: [null, !this.widgetEditMode ? [Validators.required] : []] |
||||
|
}); |
||||
|
this.targetDeviceFormGroup.get('type').valueChanges.subscribe(() => { |
||||
|
this.updateValidators(); |
||||
|
}); |
||||
|
this.targetDeviceFormGroup.valueChanges.subscribe( |
||||
|
() => { |
||||
|
this.targetDeviceUpdated(this.targetDeviceFormGroup.value); |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
writeValue(targetDevice?: TargetDevice): void { |
||||
|
this.targetDeviceFormGroup.patchValue({ |
||||
|
type: targetDevice?.type || TargetDeviceType.device, |
||||
|
deviceId: targetDevice?.deviceId, |
||||
|
entityAliasId: targetDevice?.entityAliasId |
||||
|
}, {emitEvent: false}); |
||||
|
this.updateValidators(); |
||||
|
if (targetDevice?.type === TargetDeviceType.entity && targetDevice?.entityAliasId) { |
||||
|
const entityAliases = this.aliasController.getEntityAliases(); |
||||
|
if (!entityAliases[targetDevice.entityAliasId]) { |
||||
|
this.targetDeviceFormGroup.get('entityAliasId').patchValue(null, {emitEvent: false}); |
||||
|
setTimeout(() => { |
||||
|
this.targetDeviceUpdated(this.targetDeviceFormGroup.value); |
||||
|
}, 0); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
validate(c: UntypedFormControl) { |
||||
|
return (this.targetDeviceFormGroup.valid) ? null : { |
||||
|
targetDevice: { |
||||
|
valid: false, |
||||
|
}, |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
private targetDeviceUpdated(targetDevice: TargetDevice) { |
||||
|
this.propagateChange(targetDevice); |
||||
|
} |
||||
|
|
||||
|
private updateValidators() { |
||||
|
const type: TargetDeviceType = this.targetDeviceFormGroup.get('type').value; |
||||
|
if (!this.widgetEditMode && type) { |
||||
|
if (type === TargetDeviceType.device) { |
||||
|
this.targetDeviceFormGroup.get('deviceId').enable({emitEvent: false}); |
||||
|
this.targetDeviceFormGroup.get('entityAliasId').disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.targetDeviceFormGroup.get('deviceId').disable({emitEvent: false}); |
||||
|
this.targetDeviceFormGroup.get('entityAliasId').enable({emitEvent: false}); |
||||
|
} |
||||
|
} else { |
||||
|
this.targetDeviceFormGroup.get('deviceId').disable({emitEvent: false}); |
||||
|
this.targetDeviceFormGroup.get('entityAliasId').disable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,561 @@ |
|||||
|
///
|
||||
|
/// 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 { AttributeData, LatestTelemetry } from '@shared/models/telemetry/telemetry.models'; |
||||
|
import { WidgetContext } from '@home/models/widget-component.models'; |
||||
|
import { BehaviorSubject, Observable, of } from 'rxjs'; |
||||
|
import { catchError, delay, map, share } from 'rxjs/operators'; |
||||
|
import { UtilsService } from '@core/services/utils.service'; |
||||
|
import { AfterViewInit, ChangeDetectorRef, Directive, Input, OnInit, TemplateRef } from '@angular/core'; |
||||
|
import { backgroundStyle, ComponentStyle, overlayStyle } from '@shared/models/widget-settings.models'; |
||||
|
import { ImagePipe } from '@shared/pipe/image.pipe'; |
||||
|
import { DomSanitizer } from '@angular/platform-browser'; |
||||
|
import { |
||||
|
RpcGetAttributeSettings, |
||||
|
RpcSetAttributeSettings, |
||||
|
RpcSettings, |
||||
|
RpcStateToParamsSettings, |
||||
|
RpcStateToParamsType, |
||||
|
RpcStateWidgetSettings, |
||||
|
RpcTelemetrySettings, |
||||
|
RpcUpdateStateAction |
||||
|
} from '@shared/models/rpc-widget-settings.models'; |
||||
|
import { |
||||
|
RpcDataToStateSettings, |
||||
|
RpcDataToStateType, |
||||
|
RpcInitialStateAction, |
||||
|
RpcInitialStateSettings, |
||||
|
RpcStateBehaviourSettings, |
||||
|
RpcUpdateStateSettings |
||||
|
} from '@app/shared/models/rpc-widget-settings.models'; |
||||
|
import { ValueType } from '@shared/models/constants'; |
||||
|
|
||||
|
@Directive() |
||||
|
// eslint-disable-next-line @angular-eslint/directive-class-suffix
|
||||
|
export abstract class BasicRpcStateWidgetComponent<V, S extends RpcStateWidgetSettings<V>> implements OnInit, AfterViewInit { |
||||
|
|
||||
|
@Input() |
||||
|
ctx: WidgetContext; |
||||
|
|
||||
|
@Input() |
||||
|
widgetTitlePanel: TemplateRef<any>; |
||||
|
|
||||
|
settings: S; |
||||
|
|
||||
|
behaviorApi: RpcStateBehaviorApi<V>; |
||||
|
loading$: Observable<boolean>; |
||||
|
|
||||
|
backgroundStyle$: Observable<ComponentStyle>; |
||||
|
overlayStyle: ComponentStyle = {}; |
||||
|
|
||||
|
value: V; |
||||
|
|
||||
|
error = ''; |
||||
|
|
||||
|
protected constructor(protected imagePipe: ImagePipe, |
||||
|
protected sanitizer: DomSanitizer, |
||||
|
protected cd: ChangeDetectorRef) { |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.ctx.$scope.rpcWidget = this; |
||||
|
this.settings = {...this.defaultSettings(), ...this.ctx.settings}; |
||||
|
this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer); |
||||
|
this.overlayStyle = overlayStyle(this.settings.background.overlay); |
||||
|
|
||||
|
const behaviourSettings: RpcStateBehaviourSettings<V> = { |
||||
|
initialState: this.settings.initialState, |
||||
|
updateStateByValue: val => this.getUpdateStateSettingsForValue(val) |
||||
|
}; |
||||
|
|
||||
|
const callbacks: RpcStateCallbacks<V> = { |
||||
|
onStateValue: val => this.setValue(val), |
||||
|
onError: err => this.onError(err), |
||||
|
validateStateValue: val => this.validateValue(val) |
||||
|
}; |
||||
|
|
||||
|
this.behaviorApi = new RpcStateBehaviorApi<V>(this.defaultValue(), this.ctx, |
||||
|
behaviourSettings, callbacks, this.stateValueType()); |
||||
|
this.loading$ = this.behaviorApi.loading$.pipe(share()); |
||||
|
} |
||||
|
|
||||
|
ngAfterViewInit(): void { |
||||
|
this.behaviorApi.initState(); |
||||
|
} |
||||
|
|
||||
|
public onInit() { |
||||
|
const borderRadius = this.ctx.$widgetElement.css('borderRadius'); |
||||
|
this.overlayStyle = {...this.overlayStyle, ...{borderRadius}}; |
||||
|
this.cd.detectChanges(); |
||||
|
} |
||||
|
|
||||
|
public clearError() { |
||||
|
this.error = ''; |
||||
|
} |
||||
|
|
||||
|
public updateValue() { |
||||
|
this.behaviorApi.updateState(this.value); |
||||
|
} |
||||
|
|
||||
|
private onError(error: string) { |
||||
|
this.error = error; |
||||
|
this.ctx.detectChanges(); |
||||
|
} |
||||
|
|
||||
|
protected abstract defaultSettings(): S; |
||||
|
|
||||
|
protected abstract getUpdateStateSettingsForValue(value: V): RpcUpdateStateSettings; |
||||
|
|
||||
|
protected stateValueType(): ValueType { |
||||
|
return ValueType.BOOLEAN; |
||||
|
} |
||||
|
|
||||
|
protected defaultValue(): V { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
protected setValue(value: V) { |
||||
|
this.value = value; |
||||
|
} |
||||
|
|
||||
|
protected validateValue(value: any): V { |
||||
|
return value; |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
export abstract class RpcHasLoading { |
||||
|
|
||||
|
public get loading$() { |
||||
|
return this.loadingSubject.asObservable(); |
||||
|
} |
||||
|
|
||||
|
protected loadingSubject = new BehaviorSubject(false); |
||||
|
} |
||||
|
|
||||
|
export interface RpcStateCallbacks<V> { |
||||
|
onStateValue: (value: V) => void; |
||||
|
validateStateValue: (value: any) => V; |
||||
|
onError: (error: string) => void; |
||||
|
} |
||||
|
|
||||
|
export class RpcStateBehaviorApi<V> extends RpcHasLoading { |
||||
|
|
||||
|
private readonly initialStateGetter: RpcInitialStateGetter<V>; |
||||
|
private readonly stateUpdatersMap: Map<RpcUpdateStateSettings, RpcStateUpdater<V>>; |
||||
|
|
||||
|
constructor(private state: V, |
||||
|
private ctx: WidgetContext, |
||||
|
private settings: RpcStateBehaviourSettings<V>, |
||||
|
private callbacks: RpcStateCallbacks<V>, |
||||
|
private stateValueType: ValueType) { |
||||
|
super(); |
||||
|
this.initialStateGetter = RpcInitialStateGetter.fromSettings(ctx, settings.initialState, stateValueType, callbacks); |
||||
|
this.stateUpdatersMap = new Map<RpcUpdateStateSettings, RpcStateUpdater<V>>(); |
||||
|
} |
||||
|
|
||||
|
initState() { |
||||
|
if (this.ctx.defaultSubscription.rpcEnabled) { |
||||
|
this.loadingSubject.next(true); |
||||
|
this.initialStateGetter.initState().subscribe( |
||||
|
{ |
||||
|
next: (value) => { |
||||
|
this.state = value; |
||||
|
this.loadingSubject.next(false); |
||||
|
this.callbacks.onStateValue(value); |
||||
|
this.ctx.detectChanges(); |
||||
|
}, |
||||
|
error: (err: any) => { |
||||
|
this.loadingSubject.next(false); |
||||
|
const message = parseError(this.ctx, err); |
||||
|
this.callbacks.onError(message); |
||||
|
} |
||||
|
} |
||||
|
); |
||||
|
} else { |
||||
|
this.callbacks.onError('Target device is not set!'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
updateState(value: V) { |
||||
|
this.callbacks.onError(null); |
||||
|
let updater: RpcStateUpdater<V>; |
||||
|
const updateStateSettings = this.settings.updateStateByValue(value); |
||||
|
if (updateStateSettings) { |
||||
|
updater = this.stateUpdatersMap.get(updateStateSettings); |
||||
|
if (!updater) { |
||||
|
updater = RpcStateUpdater.fromSettings(this.ctx, updateStateSettings); |
||||
|
this.stateUpdatersMap.set(updateStateSettings, updater); |
||||
|
} |
||||
|
} |
||||
|
if (updater) { |
||||
|
this.loadingSubject.next(true); |
||||
|
updater.updateState(value).subscribe( |
||||
|
{ |
||||
|
next: () => { |
||||
|
this.state = value; |
||||
|
this.loadingSubject.next(false); |
||||
|
this.ctx.detectChanges(); |
||||
|
}, |
||||
|
error: (err: any) => { |
||||
|
this.loadingSubject.next(false); |
||||
|
const message = parseError(this.ctx, err); |
||||
|
this.callbacks.onStateValue(this.state); |
||||
|
this.callbacks.onError(message); |
||||
|
this.ctx.detectChanges(); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
type RpcDataToStateFunction<V> = (data: any) => V; |
||||
|
|
||||
|
export class RpcDataToStateConverter<V> { |
||||
|
|
||||
|
private readonly dataToStateFunction: RpcDataToStateFunction<V>; |
||||
|
private readonly compareToValue: any; |
||||
|
|
||||
|
constructor(private settings: RpcDataToStateSettings, |
||||
|
private stateValueType: ValueType, |
||||
|
private callbacks: RpcStateCallbacks<V>) { |
||||
|
this.compareToValue = settings.compareToValue; |
||||
|
switch (settings.type) { |
||||
|
case RpcDataToStateType.FUNCTION: |
||||
|
try { |
||||
|
this.dataToStateFunction = new Function('data', settings.dataToStateFunction) as RpcDataToStateFunction<V>; |
||||
|
} catch (e) { |
||||
|
this.dataToStateFunction = (data) => data; |
||||
|
} |
||||
|
break; |
||||
|
case RpcDataToStateType.NONE: |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
dataToStateValue(data: any): V { |
||||
|
let result: V; |
||||
|
switch (this.settings.type) { |
||||
|
case RpcDataToStateType.FUNCTION: |
||||
|
result = data; |
||||
|
try { |
||||
|
result = this.dataToStateFunction(!!data ? JSON.parse(data) : data); |
||||
|
} catch (e) {} |
||||
|
break; |
||||
|
case RpcDataToStateType.NONE: |
||||
|
result = data; |
||||
|
break; |
||||
|
} |
||||
|
if (this.stateValueType === ValueType.BOOLEAN) { |
||||
|
result = (result === this.compareToValue) as any; |
||||
|
} |
||||
|
result = this.callbacks.validateStateValue(result); |
||||
|
return result; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export abstract class RpcInitialStateGetter<V> { |
||||
|
|
||||
|
static fromSettings<V>(ctx: WidgetContext, |
||||
|
settings: RpcInitialStateSettings<V>, |
||||
|
stateValueType: ValueType, |
||||
|
callbacks: RpcStateCallbacks<V>): RpcInitialStateGetter<V> { |
||||
|
switch (settings.action) { |
||||
|
case RpcInitialStateAction.DO_NOTHING: |
||||
|
return new RpcDefaultStateGetter<V>(ctx, settings, stateValueType, callbacks); |
||||
|
case RpcInitialStateAction.EXECUTE_RPC: |
||||
|
return new ExecuteRpcStateGetter<V>(ctx, settings, stateValueType, callbacks); |
||||
|
case RpcInitialStateAction.GET_ATTRIBUTE: |
||||
|
return new RpcAttributeStateGetter<V>(ctx, settings, stateValueType, callbacks); |
||||
|
case RpcInitialStateAction.GET_TIME_SERIES: |
||||
|
return new RpcTimeSeriesStateGetter<V>(ctx, settings, stateValueType, callbacks); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private readonly isSimulated: boolean; |
||||
|
private readonly dataConverter: RpcDataToStateConverter<V>; |
||||
|
|
||||
|
protected constructor(protected ctx: WidgetContext, |
||||
|
protected settings: RpcInitialStateSettings<V>, |
||||
|
protected stateValueType: ValueType, |
||||
|
protected callbacks: RpcStateCallbacks<V>) { |
||||
|
this.isSimulated = this.ctx.$injector.get(UtilsService).widgetEditMode; |
||||
|
if (this.settings.action !== RpcInitialStateAction.DO_NOTHING) { |
||||
|
this.dataConverter = new RpcDataToStateConverter<V>(settings.dataToState, stateValueType, this.callbacks); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
initState(): Observable<V> { |
||||
|
const stateObservable: Observable<V> = this.isSimulated ? of(null).pipe(delay(500)) : this.doGetState(); |
||||
|
return stateObservable.pipe( |
||||
|
map((data) => { |
||||
|
if (this.dataConverter) { |
||||
|
return this.dataConverter.dataToStateValue(data); |
||||
|
} else { |
||||
|
return data; |
||||
|
} |
||||
|
}) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
protected abstract doGetState(): Observable<any>; |
||||
|
} |
||||
|
|
||||
|
type RpcStateToParamsFunction<V> = (state: V) => any; |
||||
|
|
||||
|
export class RpcStateToParamsConverter<V> { |
||||
|
|
||||
|
private readonly constantValue: any; |
||||
|
private readonly stateToParamsFunction: RpcStateToParamsFunction<V>; |
||||
|
|
||||
|
constructor(protected settings: RpcStateToParamsSettings) { |
||||
|
switch (settings.type) { |
||||
|
case RpcStateToParamsType.CONSTANT: |
||||
|
this.constantValue = this.settings.constantValue; |
||||
|
break; |
||||
|
case RpcStateToParamsType.FUNCTION: |
||||
|
try { |
||||
|
this.stateToParamsFunction = new Function('value', settings.stateToParamsFunction) as RpcStateToParamsFunction<V>; |
||||
|
} catch (e) { |
||||
|
this.stateToParamsFunction = (data) => data; |
||||
|
} |
||||
|
break; |
||||
|
case RpcStateToParamsType.NONE: |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
stateToParams(state: V): any { |
||||
|
switch (this.settings.type) { |
||||
|
case RpcStateToParamsType.CONSTANT: |
||||
|
return this.constantValue; |
||||
|
case RpcStateToParamsType.FUNCTION: |
||||
|
let result = state; |
||||
|
try { |
||||
|
result = this.stateToParamsFunction(state); |
||||
|
} catch (e) {} |
||||
|
return result; |
||||
|
case RpcStateToParamsType.NONE: |
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export abstract class RpcStateUpdater<V> { |
||||
|
|
||||
|
static fromSettings<V>(ctx: WidgetContext, |
||||
|
settings: RpcUpdateStateSettings): RpcStateUpdater<V> { |
||||
|
switch (settings.action) { |
||||
|
case RpcUpdateStateAction.EXECUTE_RPC: |
||||
|
return new ExecuteRpcStateUpdater<V>(ctx, settings); |
||||
|
case RpcUpdateStateAction.SET_ATTRIBUTE: |
||||
|
return new RpcAttributeStateUpdater<V>(ctx, settings); |
||||
|
case RpcUpdateStateAction.ADD_TIME_SERIES: |
||||
|
return new RpcTimeSeriesStateUpdater<V>(ctx, settings); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private readonly isSimulated: boolean; |
||||
|
private readonly paramsConverter: RpcStateToParamsConverter<V>; |
||||
|
|
||||
|
protected constructor(protected ctx: WidgetContext, |
||||
|
protected settings: RpcUpdateStateSettings) { |
||||
|
this.isSimulated = this.ctx.$injector.get(UtilsService).widgetEditMode; |
||||
|
this.paramsConverter = new RpcStateToParamsConverter<V>(settings.stateToParams); |
||||
|
} |
||||
|
|
||||
|
updateState(state: V): Observable<any> { |
||||
|
if (this.isSimulated) { |
||||
|
return of(null).pipe(delay(500)); |
||||
|
} else { |
||||
|
return this.doUpdateState(this.paramsConverter.stateToParams(state)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected abstract doUpdateState(params: any): Observable<any>; |
||||
|
} |
||||
|
|
||||
|
export class RpcDefaultStateGetter<V> extends RpcInitialStateGetter<V> { |
||||
|
|
||||
|
private readonly defaultValue: V; |
||||
|
|
||||
|
constructor(protected ctx: WidgetContext, |
||||
|
protected settings: RpcInitialStateSettings<V>, |
||||
|
protected stateValueType: ValueType, |
||||
|
protected callbacks: RpcStateCallbacks<V>) { |
||||
|
super(ctx, settings, stateValueType, callbacks); |
||||
|
this.defaultValue = settings.defaultValue; |
||||
|
} |
||||
|
|
||||
|
protected doGetState(): Observable<V> { |
||||
|
return of(this.defaultValue); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export class ExecuteRpcStateGetter<V> extends RpcInitialStateGetter<V> { |
||||
|
|
||||
|
private readonly executeRpcSettings: RpcSettings; |
||||
|
|
||||
|
constructor(protected ctx: WidgetContext, |
||||
|
protected settings: RpcInitialStateSettings<V>, |
||||
|
protected stateValueType: ValueType, |
||||
|
protected callbacks: RpcStateCallbacks<V>) { |
||||
|
super(ctx, settings, stateValueType, callbacks); |
||||
|
this.executeRpcSettings = settings.executeRpc; |
||||
|
} |
||||
|
|
||||
|
protected doGetState(): Observable<V> { |
||||
|
return this.ctx.controlApi.sendTwoWayCommand(this.executeRpcSettings.method, null, |
||||
|
this.executeRpcSettings.requestTimeout, |
||||
|
this.executeRpcSettings.requestPersistent, |
||||
|
this.executeRpcSettings.persistentPollingInterval).pipe( |
||||
|
catchError(() => { |
||||
|
throw new Error(this.ctx.defaultSubscription.rpcErrorText); |
||||
|
}) |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export class RpcAttributeStateGetter<V> extends RpcInitialStateGetter<V> { |
||||
|
|
||||
|
private readonly getAttributeSettings: RpcGetAttributeSettings; |
||||
|
|
||||
|
constructor(protected ctx: WidgetContext, |
||||
|
protected settings: RpcInitialStateSettings<V>, |
||||
|
protected stateValueType: ValueType, |
||||
|
protected callbacks: RpcStateCallbacks<V>) { |
||||
|
super(ctx, settings, stateValueType, callbacks); |
||||
|
this.getAttributeSettings = settings.getAttribute; |
||||
|
} |
||||
|
|
||||
|
protected doGetState(): Observable<V> { |
||||
|
if (this.ctx.defaultSubscription.targetEntityId) { |
||||
|
return this.ctx.attributeService.getEntityAttributes(this.ctx.defaultSubscription.targetEntityId, |
||||
|
this.getAttributeSettings.scope, [this.getAttributeSettings.key], {ignoreLoading: true, ignoreErrors: true}) |
||||
|
.pipe( |
||||
|
map((data) => data.find(attr => attr.key === this.getAttributeSettings.key)?.value) |
||||
|
); |
||||
|
} else { |
||||
|
return of(null); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
export class RpcTimeSeriesStateGetter<V> extends RpcInitialStateGetter<V> { |
||||
|
|
||||
|
private readonly getTimeSeriesSettings: RpcTelemetrySettings; |
||||
|
|
||||
|
constructor(protected ctx: WidgetContext, |
||||
|
protected settings: RpcInitialStateSettings<V>, |
||||
|
protected stateValueType: ValueType, |
||||
|
protected callbacks: RpcStateCallbacks<V>) { |
||||
|
super(ctx, settings, stateValueType, callbacks); |
||||
|
this.getTimeSeriesSettings = settings.getTimeSeries; |
||||
|
} |
||||
|
|
||||
|
protected doGetState(): Observable<V> { |
||||
|
if (this.ctx.defaultSubscription.targetEntityId) { |
||||
|
return this.ctx.attributeService.getEntityTimeseriesLatest(this.ctx.defaultSubscription.targetEntityId, |
||||
|
[this.getTimeSeriesSettings.key], true, {ignoreLoading: true, ignoreErrors: true}) |
||||
|
.pipe( |
||||
|
map((data) => { |
||||
|
let value: any = null; |
||||
|
if (data[this.getTimeSeriesSettings.key]) { |
||||
|
const dataSet = data[this.getTimeSeriesSettings.key]; |
||||
|
if (dataSet.length) { |
||||
|
value = dataSet[0].value; |
||||
|
} |
||||
|
} |
||||
|
return value; |
||||
|
}) |
||||
|
); |
||||
|
} else { |
||||
|
return of(null); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
export class ExecuteRpcStateUpdater<V> extends RpcStateUpdater<V> { |
||||
|
|
||||
|
private readonly executeRpcSettings: RpcSettings; |
||||
|
|
||||
|
constructor(protected ctx: WidgetContext, |
||||
|
protected settings: RpcUpdateStateSettings) { |
||||
|
super(ctx, settings); |
||||
|
this.executeRpcSettings = settings.executeRpc; |
||||
|
} |
||||
|
|
||||
|
protected doUpdateState(params: any): Observable<any> { |
||||
|
return this.ctx.controlApi.sendOneWayCommand(this.executeRpcSettings.method, params, |
||||
|
this.executeRpcSettings.requestTimeout, |
||||
|
this.executeRpcSettings.requestPersistent, |
||||
|
this.executeRpcSettings.persistentPollingInterval).pipe( |
||||
|
catchError(() => { |
||||
|
throw new Error(this.ctx.defaultSubscription.rpcErrorText); |
||||
|
}) |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export class RpcAttributeStateUpdater<V> extends RpcStateUpdater<V> { |
||||
|
|
||||
|
private readonly setAttributeSettings: RpcSetAttributeSettings; |
||||
|
|
||||
|
constructor(protected ctx: WidgetContext, |
||||
|
protected settings: RpcUpdateStateSettings) { |
||||
|
super(ctx, settings); |
||||
|
this.setAttributeSettings = settings.setAttribute; |
||||
|
} |
||||
|
|
||||
|
protected doUpdateState(params: any): Observable<any> { |
||||
|
if (this.ctx.defaultSubscription.targetEntityId) { |
||||
|
const attributes: Array<AttributeData> = [{key: this.setAttributeSettings.key, value: params}]; |
||||
|
return this.ctx.attributeService.saveEntityAttributes(this.ctx.defaultSubscription.targetEntityId, |
||||
|
this.setAttributeSettings.scope, attributes, {ignoreLoading: true, ignoreErrors: true}); |
||||
|
} else { |
||||
|
return of(null); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
export class RpcTimeSeriesStateUpdater<V> extends RpcStateUpdater<V> { |
||||
|
|
||||
|
private readonly putTimeSeriesSettings: RpcTelemetrySettings; |
||||
|
|
||||
|
constructor(protected ctx: WidgetContext, |
||||
|
protected settings: RpcUpdateStateSettings) { |
||||
|
super(ctx, settings); |
||||
|
this.putTimeSeriesSettings = settings.putTimeSeries; |
||||
|
} |
||||
|
|
||||
|
protected doUpdateState(params: any): Observable<any> { |
||||
|
if (this.ctx.defaultSubscription.targetEntityId) { |
||||
|
const timeSeries: Array<AttributeData> = [{key: this.putTimeSeriesSettings.key, value: params}]; |
||||
|
return this.ctx.attributeService.saveEntityTimeseries(this.ctx.defaultSubscription.targetEntityId, |
||||
|
LatestTelemetry.LATEST_TELEMETRY, timeSeries, {ignoreLoading: true, ignoreErrors: true}); |
||||
|
} else { |
||||
|
return of(null); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
const parseError = (ctx: WidgetContext, err: any): string => |
||||
|
ctx.$injector.get(UtilsService).parseException(err).message || 'Unknown Error'; |
||||
@ -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. |
||||
|
|
||||
|
--> |
||||
|
<div #singleSwitchPanel class="tb-single-switch-panel" [style.pointer-events]="ctx.isEdit ? 'none' : 'all'" [style]="backgroundStyle$ | async"> |
||||
|
<div class="tb-single-switch-overlay" [style]="overlayStyle"></div> |
||||
|
<div class="tb-single-switch-title-panel"> |
||||
|
<ng-container *ngTemplateOutlet="widgetTitlePanel"></ng-container> |
||||
|
</div> |
||||
|
<div #singleSwitchContent class="tb-single-switch-content" [class]="this.layout" [class.no-label]="!showIcon && !showLabel" [class.auto-scale]="autoScale"> |
||||
|
<div *ngIf="showIcon || showLabel" #singleSwitchLabelRow class="tb-single-switch-label-row"> |
||||
|
<tb-icon *ngIf="showIcon" [style]="iconStyle">{{ icon }}</tb-icon> |
||||
|
<div *ngIf="showLabel" class="tb-single-switch-label" [style]="labelStyle">{{ label$ | async }}</div> |
||||
|
</div> |
||||
|
<div #singleSwitchToggleRow class="tb-single-switch-slide-toggle-row"> |
||||
|
<div *ngIf="showOffLabel" [style]="offLabelStyle">{{ offLabel }}</div> |
||||
|
<mat-slide-toggle class="tb-single-switch-toggle" [disabled]="loading$ | async" [(ngModel)]="value" (change)="updateValue()"> |
||||
|
</mat-slide-toggle> |
||||
|
<div *ngIf="showOnLabel" [style]="onLabelStyle">{{ onLabel }}</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<mat-progress-bar class="tb-single-switch-progress" style="height: 2px;" color="accent" mode="indeterminate" *ngIf="loading$ | async"></mat-progress-bar> |
||||
|
<div *ngIf="error" class="tb-single-switch-error-container"> |
||||
|
<div class="tb-single-switch-error-panel"> |
||||
|
<div class="tb-single-switch-error-text">{{ error }}</div> |
||||
|
<button class="tb-single-switch-error-clear tb-mat-20" mat-icon-button (click)="clearError()"><mat-icon>close</mat-icon></button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
@ -0,0 +1,204 @@ |
|||||
|
/** |
||||
|
* 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. |
||||
|
*/ |
||||
|
$tumblerColorOn: var(--tb-single-switch-tumbler-color-on, #fff); |
||||
|
$tumblerColorOff: var(--tb-single-switch-tumbler-color-off, #fff); |
||||
|
$tumblerColorDisabled: var(--tb-single-switch-tumbler-color-disabled, #fff); |
||||
|
|
||||
|
$switchColorOn: var(--tb-single-switch-color-on, #5469FF); |
||||
|
$switchColorOff: var(--tb-single-switch-color-off, rgba(84, 105, 255, 0.30)); |
||||
|
$switchColorDisabled: var(--tb-single-switch-color-disabled, #D5D7E5); |
||||
|
|
||||
|
.tb-single-switch-panel { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
position: relative; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
padding: 18px 24px; |
||||
|
> div:not(.tb-single-switch-overlay), > tb-icon { |
||||
|
z-index: 1; |
||||
|
} |
||||
|
.tb-single-switch-overlay { |
||||
|
position: absolute; |
||||
|
top: 12px; |
||||
|
left: 12px; |
||||
|
bottom: 12px; |
||||
|
right: 12px; |
||||
|
} |
||||
|
.tb-single-switch-progress { |
||||
|
position: absolute; |
||||
|
bottom: 0; |
||||
|
left: 0; |
||||
|
right: 0; |
||||
|
} |
||||
|
.tb-single-switch-error-container { |
||||
|
position: absolute; |
||||
|
bottom: 0; |
||||
|
left: 0; |
||||
|
right: 0; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
.tb-single-switch-error-panel { |
||||
|
display: flex; |
||||
|
padding: 4px 4px 4px 12px; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
gap: 4px; |
||||
|
border-radius: 4px; |
||||
|
border: 1px solid rgba(209, 39, 48, 0.04); |
||||
|
background: rgba(209, 39, 48, 0.04); |
||||
|
.tb-single-switch-error-text { |
||||
|
font-size: 12px; |
||||
|
font-style: normal; |
||||
|
font-weight: 400; |
||||
|
line-height: 16px; |
||||
|
color: #D12730; |
||||
|
} |
||||
|
.tb-single-switch-error-clear { |
||||
|
color: rgba(209, 39, 48, 1); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
> div.tb-single-switch-title-panel { |
||||
|
position: absolute; |
||||
|
top: 12px; |
||||
|
left: 12px; |
||||
|
right: 12px; |
||||
|
z-index: 2; |
||||
|
} |
||||
|
.tb-single-switch-content { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
position: relative; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 8px; |
||||
|
&.right { |
||||
|
flex-direction: row; |
||||
|
justify-content: space-between; |
||||
|
} |
||||
|
&.no-label.right { |
||||
|
justify-content: flex-end; |
||||
|
} |
||||
|
&.left { |
||||
|
flex-direction: row-reverse; |
||||
|
justify-content: flex-end; |
||||
|
} |
||||
|
&.centered { |
||||
|
flex-direction: row; |
||||
|
justify-content: center; |
||||
|
} |
||||
|
.tb-single-switch-label-row { |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
align-items: center; |
||||
|
gap: 8px; |
||||
|
.tb-single-switch-label { |
||||
|
white-space: nowrap; |
||||
|
} |
||||
|
} |
||||
|
&:not(.auto-scale) { |
||||
|
.tb-single-switch-label-row { |
||||
|
overflow: hidden; |
||||
|
.tb-single-switch-label { |
||||
|
overflow: hidden; |
||||
|
text-overflow: ellipsis; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
.tb-single-switch-slide-toggle-row { |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
align-items: center; |
||||
|
gap: 8px; |
||||
|
} |
||||
|
.mat-mdc-slide-toggle { |
||||
|
&.tb-single-switch-toggle { |
||||
|
.mdc-switch { |
||||
|
width: 42px; |
||||
|
height: 22px; |
||||
|
.mdc-switch__track { |
||||
|
height: 22px; |
||||
|
border-radius: 11px; |
||||
|
&::after, &::before { |
||||
|
border: none; |
||||
|
} |
||||
|
} |
||||
|
.mdc-switch__handle-track { |
||||
|
width: calc(100% - 22px); |
||||
|
left: 2px; |
||||
|
} |
||||
|
.mdc-switch__handle { |
||||
|
border-radius: 9px; |
||||
|
width: 18px; |
||||
|
height: 18px; |
||||
|
} |
||||
|
.mdc-switch__ripple { |
||||
|
width: 36px; |
||||
|
height: 36px; |
||||
|
} |
||||
|
.mdc-switch__icons { |
||||
|
display: none; |
||||
|
} |
||||
|
&:disabled { |
||||
|
.mdc-switch__track { |
||||
|
opacity: 1; |
||||
|
&::after, &::before { |
||||
|
background: $switchColorDisabled; |
||||
|
} |
||||
|
} |
||||
|
.mdc-switch__handle::after { |
||||
|
opacity: 1; |
||||
|
background: $tumblerColorDisabled; |
||||
|
} |
||||
|
} |
||||
|
&:enabled, &:enabled:hover { |
||||
|
.mdc-switch__track { |
||||
|
&::after { |
||||
|
background: $switchColorOn; |
||||
|
} |
||||
|
&::before { |
||||
|
background: $switchColorOff; |
||||
|
} |
||||
|
} |
||||
|
&.mdc-switch--selected { |
||||
|
.mdc-switch__handle::after { |
||||
|
background: $tumblerColorOn; |
||||
|
} |
||||
|
.mdc-switch__ripple::after { |
||||
|
background-color: $switchColorOn; |
||||
|
} |
||||
|
} |
||||
|
&.mdc-switch--unselected { |
||||
|
.mdc-switch__handle::after { |
||||
|
background: $tumblerColorOff; |
||||
|
} |
||||
|
.mdc-switch__ripple::after { |
||||
|
background-color: $switchColorOff; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
.mdc-form-field > label { |
||||
|
display: none; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,197 @@ |
|||||
|
///
|
||||
|
/// 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 { |
||||
|
AfterViewInit, |
||||
|
ChangeDetectorRef, |
||||
|
Component, |
||||
|
ElementRef, |
||||
|
OnDestroy, |
||||
|
OnInit, |
||||
|
Renderer2, |
||||
|
ViewChild, |
||||
|
ViewEncapsulation |
||||
|
} from '@angular/core'; |
||||
|
import { BasicRpcStateWidgetComponent } from '@home/components/widget/lib/rpc/rpc-widget.models'; |
||||
|
import { |
||||
|
singleSwitchDefaultSettings, |
||||
|
SingleSwitchLayout, |
||||
|
SingleSwitchWidgetSettings |
||||
|
} from '@home/components/widget/lib/rpc/single-switch-widget.models'; |
||||
|
import { ComponentStyle, iconStyle, textStyle } from '@shared/models/widget-settings.models'; |
||||
|
import { Observable } from 'rxjs'; |
||||
|
import { ResizeObserver } from '@juggle/resize-observer'; |
||||
|
import { ImagePipe } from '@shared/pipe/image.pipe'; |
||||
|
import { DomSanitizer } from '@angular/platform-browser'; |
||||
|
import cssjs from '@core/css/css'; |
||||
|
import { hashCode } from '@core/utils'; |
||||
|
import { RpcUpdateStateSettings } from '@shared/models/rpc-widget-settings.models'; |
||||
|
import { ValueType } from '@shared/models/constants'; |
||||
|
|
||||
|
const horizontalLayoutPadding = 48; |
||||
|
const verticalLayoutPadding = 36; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-single-switch-widget', |
||||
|
templateUrl: './single-switch-widget.component.html', |
||||
|
styleUrls: ['./single-switch-widget.component.scss'], |
||||
|
encapsulation: ViewEncapsulation.None |
||||
|
}) |
||||
|
export class SingleSwitchWidgetComponent extends |
||||
|
BasicRpcStateWidgetComponent<boolean, SingleSwitchWidgetSettings> implements OnInit, AfterViewInit, OnDestroy { |
||||
|
|
||||
|
@ViewChild('singleSwitchPanel', {static: false}) |
||||
|
singleSwitchPanel: ElementRef<HTMLElement>; |
||||
|
|
||||
|
@ViewChild('singleSwitchContent', {static: false}) |
||||
|
singleSwitchContent: ElementRef<HTMLElement>; |
||||
|
|
||||
|
@ViewChild('singleSwitchLabelRow', {static: false}) |
||||
|
singleSwitchLabelRow: ElementRef<HTMLElement>; |
||||
|
|
||||
|
@ViewChild('singleSwitchToggleRow', {static: false}) |
||||
|
singleSwitchToggleRow: ElementRef<HTMLElement>; |
||||
|
|
||||
|
layout: SingleSwitchLayout; |
||||
|
|
||||
|
showIcon = false; |
||||
|
icon = ''; |
||||
|
iconStyle: ComponentStyle = {}; |
||||
|
|
||||
|
showLabel = true; |
||||
|
label$: Observable<string>; |
||||
|
labelStyle: ComponentStyle = {}; |
||||
|
|
||||
|
showOnLabel = false; |
||||
|
onLabel = ''; |
||||
|
onLabelStyle: ComponentStyle = {}; |
||||
|
|
||||
|
showOffLabel = false; |
||||
|
offLabel = ''; |
||||
|
offLabelStyle: ComponentStyle = {}; |
||||
|
|
||||
|
autoScale = false; |
||||
|
|
||||
|
private panelResize$: ResizeObserver; |
||||
|
|
||||
|
constructor(protected imagePipe: ImagePipe, |
||||
|
protected sanitizer: DomSanitizer, |
||||
|
private renderer: Renderer2, |
||||
|
protected cd: ChangeDetectorRef, |
||||
|
private elementRef: ElementRef) { |
||||
|
super(imagePipe, sanitizer, cd); |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
super.ngOnInit(); |
||||
|
this.layout = this.settings.layout; |
||||
|
|
||||
|
this.autoScale = this.settings.autoScale; |
||||
|
|
||||
|
this.showLabel = this.settings.showLabel; |
||||
|
this.label$ = this.ctx.registerLabelPattern(this.settings.label, this.label$); |
||||
|
this.labelStyle = textStyle(this.settings.labelFont); |
||||
|
this.labelStyle.color = this.settings.labelColor; |
||||
|
|
||||
|
this.showIcon = this.settings.showIcon; |
||||
|
this.icon = this.settings.icon; |
||||
|
this.iconStyle = iconStyle(this.settings.iconSize, this.settings.iconSizeUnit ); |
||||
|
this.iconStyle.color = this.settings.iconColor; |
||||
|
|
||||
|
this.showOnLabel = this.settings.showOnLabel; |
||||
|
this.onLabel = this.settings.onLabel; |
||||
|
this.onLabelStyle = textStyle(this.settings.onLabelFont); |
||||
|
this.onLabelStyle.color = this.settings.onLabelColor; |
||||
|
|
||||
|
this.showOffLabel = this.settings.showOffLabel; |
||||
|
this.offLabel = this.settings.offLabel; |
||||
|
this.offLabelStyle = textStyle(this.settings.offLabelFont); |
||||
|
this.offLabelStyle.color = this.settings.offLabelColor; |
||||
|
const switchVariablesCss = `.tb-single-switch-panel {\n`+ |
||||
|
`--tb-single-switch-tumbler-color-on: ${this.settings.tumblerColorOn};\n`+ |
||||
|
`--tb-single-switch-tumbler-color-off: ${this.settings.tumblerColorOff};\n`+ |
||||
|
`--tb-single-switch-tumbler-color-disabled: ${this.settings.tumblerColorDisabled};\n`+ |
||||
|
`--tb-single-switch-color-on: ${this.settings.switchColorOn};\n`+ |
||||
|
`--tb-single-switch-color-off: ${this.settings.switchColorOff};\n`+ |
||||
|
`--tb-single-switch-color-disabled: ${this.settings.switchColorDisabled};\n`+ |
||||
|
`}`; |
||||
|
const cssParser = new cssjs(); |
||||
|
cssParser.testMode = false; |
||||
|
const namespace = 'single-switch-' + hashCode(switchVariablesCss); |
||||
|
cssParser.cssPreviewNamespace = namespace; |
||||
|
cssParser.createStyleElement(namespace, switchVariablesCss); |
||||
|
this.renderer.addClass(this.elementRef.nativeElement, namespace); |
||||
|
} |
||||
|
|
||||
|
ngAfterViewInit(): void { |
||||
|
if (this.autoScale) { |
||||
|
this.renderer.setStyle(this.singleSwitchContent.nativeElement, 'overflow', 'visible'); |
||||
|
this.renderer.setStyle(this.singleSwitchContent.nativeElement, 'position', 'absolute'); |
||||
|
this.panelResize$ = new ResizeObserver(() => { |
||||
|
this.onResize(); |
||||
|
}); |
||||
|
this.panelResize$.observe(this.singleSwitchPanel.nativeElement); |
||||
|
if (this.showLabel) { |
||||
|
this.panelResize$.observe(this.singleSwitchLabelRow.nativeElement); |
||||
|
} |
||||
|
this.onResize(); |
||||
|
} |
||||
|
super.ngAfterViewInit(); |
||||
|
} |
||||
|
|
||||
|
ngOnDestroy() { |
||||
|
if (this.panelResize$) { |
||||
|
this.panelResize$.disconnect(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected stateValueType(): ValueType { |
||||
|
return ValueType.BOOLEAN; |
||||
|
} |
||||
|
|
||||
|
protected defaultValue(): boolean { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
protected defaultSettings(): SingleSwitchWidgetSettings { |
||||
|
return {...singleSwitchDefaultSettings}; |
||||
|
} |
||||
|
|
||||
|
protected getUpdateStateSettingsForValue(value: boolean): RpcUpdateStateSettings { |
||||
|
return value ? this.settings.onUpdateState : this.settings.offUpdateState; |
||||
|
} |
||||
|
|
||||
|
protected validateValue(value: any): boolean { |
||||
|
return !!value; |
||||
|
} |
||||
|
|
||||
|
private onResize() { |
||||
|
const panelWidth = this.singleSwitchPanel.nativeElement.getBoundingClientRect().width - horizontalLayoutPadding; |
||||
|
const panelHeight = this.singleSwitchPanel.nativeElement.getBoundingClientRect().height - verticalLayoutPadding; |
||||
|
this.renderer.setStyle(this.singleSwitchContent.nativeElement, 'transform', `scale(1)`); |
||||
|
let contentWidth = this.singleSwitchToggleRow.nativeElement.getBoundingClientRect().width; |
||||
|
let contentHeight = this.singleSwitchToggleRow.nativeElement.getBoundingClientRect().height; |
||||
|
if (this.showIcon || this.showLabel) { |
||||
|
contentWidth += (8 + this.singleSwitchLabelRow.nativeElement.getBoundingClientRect().width); |
||||
|
contentHeight = Math.max(contentHeight, this.singleSwitchLabelRow.nativeElement.getBoundingClientRect().height); |
||||
|
} |
||||
|
const scale = Math.min(panelWidth / contentWidth, panelHeight / contentHeight); |
||||
|
const width = panelWidth / scale; |
||||
|
this.renderer.setStyle(this.singleSwitchContent.nativeElement, 'width', width + 'px'); |
||||
|
this.renderer.setStyle(this.singleSwitchContent.nativeElement, 'transform', `scale(${scale})`); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,202 @@ |
|||||
|
///
|
||||
|
/// 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 { BackgroundType, cssUnit, Font } from '@shared/models/widget-settings.models'; |
||||
|
import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; |
||||
|
import { |
||||
|
RpcDataToStateType, |
||||
|
RpcInitialStateAction, |
||||
|
RpcStateToParamsType, |
||||
|
RpcStateWidgetSettings, |
||||
|
RpcUpdateStateAction, |
||||
|
RpcUpdateStateSettings |
||||
|
} from '@shared/models/rpc-widget-settings.models'; |
||||
|
|
||||
|
export enum SingleSwitchLayout { |
||||
|
right = 'right', |
||||
|
left = 'left', |
||||
|
centered = 'centered' |
||||
|
} |
||||
|
|
||||
|
export const singleSwitchLayouts = Object.keys(SingleSwitchLayout) as SingleSwitchLayout[]; |
||||
|
|
||||
|
export const singleSwitchLayoutTranslations = new Map<SingleSwitchLayout, string>( |
||||
|
[ |
||||
|
[SingleSwitchLayout.right, 'widgets.single-switch.layout-right'], |
||||
|
[SingleSwitchLayout.left, 'widgets.single-switch.layout-left'], |
||||
|
[SingleSwitchLayout.centered, 'widgets.single-switch.layout-centered'] |
||||
|
] |
||||
|
); |
||||
|
|
||||
|
export const singleSwitchLayoutImages = new Map<SingleSwitchLayout, string>( |
||||
|
[ |
||||
|
[SingleSwitchLayout.right, 'assets/widget/single-switch/right-layout.svg'], |
||||
|
[SingleSwitchLayout.left, 'assets/widget/single-switch/left-layout.svg'], |
||||
|
[SingleSwitchLayout.centered, 'assets/widget/single-switch/centered-layout.svg'] |
||||
|
] |
||||
|
); |
||||
|
|
||||
|
export interface SingleSwitchWidgetSettings extends RpcStateWidgetSettings<boolean> { |
||||
|
onUpdateState: RpcUpdateStateSettings; |
||||
|
offUpdateState: RpcUpdateStateSettings; |
||||
|
layout: SingleSwitchLayout; |
||||
|
autoScale: boolean; |
||||
|
showLabel: boolean; |
||||
|
label: string; |
||||
|
labelFont: Font; |
||||
|
labelColor: string; |
||||
|
showIcon: boolean; |
||||
|
icon: string; |
||||
|
iconSize: number; |
||||
|
iconSizeUnit: cssUnit; |
||||
|
iconColor: string; |
||||
|
switchColorOn: string; |
||||
|
switchColorOff: string; |
||||
|
switchColorDisabled: string; |
||||
|
tumblerColorOn: string; |
||||
|
tumblerColorOff: string; |
||||
|
tumblerColorDisabled: string; |
||||
|
showOnLabel: boolean; |
||||
|
onLabel: string; |
||||
|
onLabelFont: Font; |
||||
|
onLabelColor: string; |
||||
|
showOffLabel: boolean; |
||||
|
offLabel: string; |
||||
|
offLabelFont: Font; |
||||
|
offLabelColor: string; |
||||
|
} |
||||
|
|
||||
|
export const singleSwitchDefaultSettings: SingleSwitchWidgetSettings = { |
||||
|
initialState: { |
||||
|
action: RpcInitialStateAction.EXECUTE_RPC, |
||||
|
defaultValue: false, |
||||
|
executeRpc: { |
||||
|
method: 'getState', |
||||
|
requestTimeout: 5000, |
||||
|
requestPersistent: false, |
||||
|
persistentPollingInterval: 1000 |
||||
|
}, |
||||
|
getAttribute: { |
||||
|
key: 'state', |
||||
|
scope: null |
||||
|
}, |
||||
|
getTimeSeries: { |
||||
|
key: 'state' |
||||
|
}, |
||||
|
dataToState: { |
||||
|
type: RpcDataToStateType.NONE, |
||||
|
compareToValue: true, |
||||
|
dataToStateFunction: '/* Should return boolean value */\nreturn data;' |
||||
|
} |
||||
|
}, |
||||
|
onUpdateState: { |
||||
|
action: RpcUpdateStateAction.EXECUTE_RPC, |
||||
|
executeRpc: { |
||||
|
method: 'setState', |
||||
|
requestTimeout: 5000, |
||||
|
requestPersistent: false, |
||||
|
persistentPollingInterval: 1000 |
||||
|
}, |
||||
|
setAttribute: { |
||||
|
key: 'state', |
||||
|
scope: AttributeScope.SHARED_SCOPE |
||||
|
}, |
||||
|
putTimeSeries: { |
||||
|
key: 'state' |
||||
|
}, |
||||
|
stateToParams: { |
||||
|
type: RpcStateToParamsType.CONSTANT, |
||||
|
constantValue: true, |
||||
|
stateToParamsFunction: '/* Convert input boolean value to RPC parameters or attribute/time-series value */\nreturn value;' |
||||
|
} |
||||
|
}, |
||||
|
offUpdateState: { |
||||
|
action: RpcUpdateStateAction.EXECUTE_RPC, |
||||
|
executeRpc: { |
||||
|
method: 'setState', |
||||
|
requestTimeout: 5000, |
||||
|
requestPersistent: false, |
||||
|
persistentPollingInterval: 1000 |
||||
|
}, |
||||
|
setAttribute: { |
||||
|
key: 'state', |
||||
|
scope: AttributeScope.SHARED_SCOPE |
||||
|
}, |
||||
|
putTimeSeries: { |
||||
|
key: 'state' |
||||
|
}, |
||||
|
stateToParams: { |
||||
|
type: RpcStateToParamsType.CONSTANT, |
||||
|
constantValue: false, |
||||
|
stateToParamsFunction: '/* Convert input boolean value to RPC parameters or attribute/time-series value */ \n return value;' |
||||
|
} |
||||
|
}, |
||||
|
layout: SingleSwitchLayout.right, |
||||
|
autoScale: true, |
||||
|
showLabel: true, |
||||
|
label: 'Switch', |
||||
|
labelFont: { |
||||
|
family: 'Roboto', |
||||
|
size: 16, |
||||
|
sizeUnit: 'px', |
||||
|
style: 'normal', |
||||
|
weight: '500', |
||||
|
lineHeight: '24px' |
||||
|
}, |
||||
|
labelColor: 'rgba(0, 0, 0, 0.76)', |
||||
|
showIcon: false, |
||||
|
icon: 'mdi:lightbulb-outline', |
||||
|
iconSize: 24, |
||||
|
iconSizeUnit: 'px', |
||||
|
iconColor: 'rgba(0, 0, 0, 0.76)', |
||||
|
switchColorOn: '#5469FF', |
||||
|
switchColorOff: 'rgba(84, 105, 255, 0.30)', |
||||
|
switchColorDisabled: '#D5D7E5', |
||||
|
tumblerColorOn: '#fff', |
||||
|
tumblerColorOff: '#fff', |
||||
|
tumblerColorDisabled: '#fff', |
||||
|
showOnLabel: false, |
||||
|
onLabel: 'On', |
||||
|
onLabelFont: { |
||||
|
family: 'Roboto', |
||||
|
size: 16, |
||||
|
sizeUnit: 'px', |
||||
|
style: 'normal', |
||||
|
weight: '400', |
||||
|
lineHeight: '24px' |
||||
|
}, |
||||
|
onLabelColor: 'rgba(0, 0, 0, 0.38)', |
||||
|
showOffLabel: false, |
||||
|
offLabel: 'Off', |
||||
|
offLabelFont: { |
||||
|
family: 'Roboto', |
||||
|
size: 16, |
||||
|
sizeUnit: 'px', |
||||
|
style: 'normal', |
||||
|
weight: '400', |
||||
|
lineHeight: '24px' |
||||
|
}, |
||||
|
offLabelColor: 'rgba(0, 0, 0, 0.38)', |
||||
|
background: { |
||||
|
type: BackgroundType.color, |
||||
|
color: '#fff', |
||||
|
overlay: { |
||||
|
enabled: false, |
||||
|
color: 'rgba(255,255,255,0.72)', |
||||
|
blur: 3 |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
@ -0,0 +1,211 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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-rpc-state-settings-panel" [formGroup]="initialStateSettingsFormGroup"> |
||||
|
<div class="tb-rpc-state-settings-title" translate>widgets.rpc-state.initial-state</div> |
||||
|
<div class="tb-rpc-state-settings-panel-content"> |
||||
|
<div class="tb-form-row"> |
||||
|
<div class="fixed-title-width" >{{ 'widgets.rpc-state.action' | translate }}</div> |
||||
|
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-select formControlName="action"> |
||||
|
<mat-option *ngFor="let action of rpcInitialStateActions" [value]="action"> |
||||
|
{{ rpcInitialStateTranslationsMap.get(action) | translate }} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
<ng-container [ngSwitch]="initialStateSettingsFormGroup.get('action').value"> |
||||
|
<ng-template [ngSwitchCase]="rpcInitialStateAction.DO_NOTHING"> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<div tb-hint-tooltip-icon="{{'widgets.rpc-state.init-value-hint' | translate}}" translate>widgets.rpc-state.value</div> |
||||
|
<tb-value-input [valueType]="stateValueType" |
||||
|
trueLabel="widgets.rpc-state.on" |
||||
|
falseLabel="widgets.rpc-state.off" |
||||
|
formControlName="defaultValue"></tb-value-input> |
||||
|
</div> |
||||
|
</ng-template> |
||||
|
<ng-template [ngSwitchCase]="rpcInitialStateAction.EXECUTE_RPC"> |
||||
|
<div class="tb-form-row" formGroupName="executeRpc"> |
||||
|
<div class="fixed-title-width" >{{ 'widgets.rpc-state.method' | translate }}*</div> |
||||
|
<mat-form-field class="flex tb-suffix-absolute" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput required formControlName="method" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="'widgets.rpc-state.method-name-required' | translate" |
||||
|
*ngIf="initialStateSettingsFormGroup.get('executeRpc').get('method').hasError('required') |
||||
|
&& initialStateSettingsFormGroup.get('executeRpc').get('method').touched" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</ng-template> |
||||
|
<ng-template [ngSwitchCase]="rpcInitialStateAction.GET_ATTRIBUTE"> |
||||
|
<ng-container formGroupName="getAttribute"> |
||||
|
<div class="tb-form-row"> |
||||
|
<div class="fixed-title-width">{{ 'widgets.rpc-state.attribute-scope' | translate }}</div> |
||||
|
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-select formControlName="scope" placeholder="{{ 'attribute.scope-any' | translate }}"> |
||||
|
<mat-option [value]="null"> |
||||
|
{{ 'attribute.scope-any' | translate }} |
||||
|
</mat-option> |
||||
|
<mat-option *ngFor="let scope of attributeScopes" [value]="scope"> |
||||
|
{{ telemetryTypeTranslationsMap.get(scope) | translate }} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
<div class="tb-form-row"> |
||||
|
<div class="fixed-title-width" >{{ 'widgets.rpc-state.attribute-key' | translate }}*</div> |
||||
|
<tb-device-key-autocomplete |
||||
|
fxFlex |
||||
|
formControlName="key" |
||||
|
required |
||||
|
requiredText="widgets.rpc-state.attribute-key-required" |
||||
|
inlineField |
||||
|
[aliasController]="aliasController" |
||||
|
[targetDevice]="targetDevice" |
||||
|
[keyType]="dataKeyType.attribute" |
||||
|
[attributeScope]="initialStateSettingsFormGroup.get('getAttribute').get('scope').value"> |
||||
|
</tb-device-key-autocomplete> |
||||
|
</div> |
||||
|
</ng-container> |
||||
|
</ng-template> |
||||
|
<ng-template [ngSwitchCase]="rpcInitialStateAction.GET_TIME_SERIES"> |
||||
|
<ng-container formGroupName="getTimeSeries"> |
||||
|
<div class="tb-form-row"> |
||||
|
<div class="fixed-title-width" >{{ 'widgets.rpc-state.time-series-key' | translate }}*</div> |
||||
|
<tb-device-key-autocomplete |
||||
|
fxFlex |
||||
|
formControlName="key" |
||||
|
required |
||||
|
requiredText="widgets.rpc-state.time-series-key-required" |
||||
|
inlineField |
||||
|
[aliasController]="aliasController" |
||||
|
[targetDevice]="targetDevice" |
||||
|
[keyType]="dataKeyType.timeseries"> |
||||
|
</tb-device-key-autocomplete> |
||||
|
</div> |
||||
|
</ng-container> |
||||
|
</ng-template> |
||||
|
</ng-container> |
||||
|
<div *ngIf="initialStateSettingsFormGroup.get('action').value !== rpcInitialStateAction.DO_NOTHING" |
||||
|
class="tb-form-panel stroked" formGroupName="dataToState"> |
||||
|
<div class="tb-form-row no-padding no-border column-xs"> |
||||
|
<div class="fixed-title-width" translate>widgets.rpc-state.action-result-converter</div> |
||||
|
<tb-toggle-select fxFlex formControlName="type"> |
||||
|
<tb-toggle-option [value]="dataToStateType.NONE">{{ 'widgets.rpc-state.converter-none' | translate }}</tb-toggle-option> |
||||
|
<tb-toggle-option [value]="dataToStateType.FUNCTION">{{ 'widgets.rpc-state.converter-function' | translate }}</tb-toggle-option> |
||||
|
</tb-toggle-select> |
||||
|
</div> |
||||
|
<tb-js-func *ngIf="initialStateSettingsFormGroup.get('dataToState').get('type').value === dataToStateType.FUNCTION" |
||||
|
formControlName="dataToStateFunction" |
||||
|
required |
||||
|
[globalVariables]="functionScopeVariables" |
||||
|
[functionArgs]="['data']" |
||||
|
functionTitle="{{ 'widgets.rpc-state.parse-value-function' | translate }}" |
||||
|
helpId="widget/lib/rpc/parse_value_fn"> |
||||
|
</tb-js-func> |
||||
|
<div *ngIf="stateValueType === valueType.BOOLEAN" class="tb-form-row align-start no-gap column-xs"> |
||||
|
<div class="fixed-title-width fixed-title-height" translate>widgets.rpc-state.on-when-result-is</div> |
||||
|
<tb-value-input |
||||
|
fxFlex |
||||
|
layout="column" |
||||
|
formControlName="compareToValue"> |
||||
|
</tb-value-input> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div *ngIf="initialStateSettingsFormGroup.get('action').value === rpcInitialStateAction.EXECUTE_RPC" |
||||
|
class="tb-form-panel no-border tb-slide-toggle" formGroupName="executeRpc"> |
||||
|
<mat-expansion-panel class="tb-settings no-padding"> |
||||
|
<mat-expansion-panel-header fxLayout="row wrap"> |
||||
|
<mat-panel-title> |
||||
|
</mat-panel-title> |
||||
|
<mat-panel-description fxLayoutAlign="end center" translate> |
||||
|
widget-config.advanced-settings |
||||
|
</mat-panel-description> |
||||
|
</mat-expansion-panel-header> |
||||
|
<ng-template matExpansionPanelContent> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<div>{{ 'widgets.rpc-state.request-timeout-ms' | translate }}</div> |
||||
|
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic"> |
||||
|
<input matInput formControlName="requestTimeout" type="number" required min="5000" step="1" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="(initialStateSettingsFormGroup.get('executeRpc').get('requestTimeout').hasError('required') |
||||
|
? 'widgets.rpc-state.request-timeout-required' : 'widgets.rpc-state.min-request-timeout-error') | translate" |
||||
|
*ngIf="initialStateSettingsFormGroup.get('executeRpc').get('requestTimeout').touched && |
||||
|
initialStateSettingsFormGroup.get('executeRpc').get('requestTimeout').invalid" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
<div class="tb-form-panel stroked tb-slide-toggle"> |
||||
|
<mat-expansion-panel class="tb-settings" |
||||
|
[expanded]="initialStateSettingsFormGroup.get('executeRpc').get('requestPersistent').value" |
||||
|
[disabled]="!initialStateSettingsFormGroup.get('executeRpc').get('requestPersistent').value"> |
||||
|
<mat-expansion-panel-header fxLayout="row wrap"> |
||||
|
<mat-panel-title> |
||||
|
<mat-slide-toggle class="mat-slide" formControlName="requestPersistent" (click)="$event.stopPropagation()" |
||||
|
fxLayoutAlign="center"> |
||||
|
{{ 'widgets.rpc-state.request-persistent' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
</mat-panel-title> |
||||
|
</mat-expansion-panel-header> |
||||
|
<ng-template matExpansionPanelContent> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<div tb-hint-tooltip-icon="{{'widgets.rpc-state.persistent-polling-interval-hint' | translate}}">{{ 'widgets.rpc-state.persistent-polling-interval' | translate }}</div> |
||||
|
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic"> |
||||
|
<input matInput formControlName="persistentPollingInterval" type="number" required min="1000" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="(initialStateSettingsFormGroup.get('executeRpc').get('persistentPollingInterval').hasError('required') |
||||
|
? 'widgets.rpc-state.persistent-polling-interval-required' : 'widgets.rpc-state.min-persistent-polling-interval-error') | translate" |
||||
|
*ngIf="initialStateSettingsFormGroup.get('executeRpc').get('persistentPollingInterval').touched && |
||||
|
initialStateSettingsFormGroup.get('executeRpc').get('persistentPollingInterval').invalid" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</ng-template> |
||||
|
</mat-expansion-panel> |
||||
|
</div> |
||||
|
</ng-template> |
||||
|
</mat-expansion-panel> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-rpc-state-settings-panel-buttons"> |
||||
|
<button mat-button |
||||
|
color="primary" |
||||
|
type="button" |
||||
|
(click)="cancel()"> |
||||
|
{{ 'action.cancel' | translate }} |
||||
|
</button> |
||||
|
<button mat-raised-button |
||||
|
color="primary" |
||||
|
type="button" |
||||
|
(click)="applyInitialStateSettings()" |
||||
|
[disabled]="initialStateSettingsFormGroup.invalid || !initialStateSettingsFormGroup.dirty"> |
||||
|
{{ 'action.apply' | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
@ -0,0 +1,176 @@ |
|||||
|
///
|
||||
|
/// 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, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; |
||||
|
import { PageComponent } from '@shared/components/page.component'; |
||||
|
import { TbPopoverComponent } from '@shared/components/popover.component'; |
||||
|
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { merge } from 'rxjs'; |
||||
|
import { |
||||
|
RpcDataToStateType, |
||||
|
RpcInitialStateAction, |
||||
|
rpcInitialStateActions, |
||||
|
RpcInitialStateSettings, |
||||
|
rpcInitialStateTranslations |
||||
|
} from '@shared/models/rpc-widget-settings.models'; |
||||
|
import { ValueType } from '@shared/models/constants'; |
||||
|
import { TargetDevice } from '@shared/models/widget.models'; |
||||
|
import { AttributeScope, DataKeyType, telemetryTypeTranslationsShort } from '@shared/models/telemetry/telemetry.models'; |
||||
|
import { IAliasController } from '@core/api/widget-api.models'; |
||||
|
import { WidgetService } from '@core/http/widget.service'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-rpc-initial-state-settings-panel', |
||||
|
templateUrl: './rpc-initial-state-settings-panel.component.html', |
||||
|
providers: [], |
||||
|
styleUrls: ['./rpc-state-settings-panel.component.scss'], |
||||
|
encapsulation: ViewEncapsulation.None |
||||
|
}) |
||||
|
export class RpcInitialStateSettingsPanelComponent extends PageComponent implements OnInit { |
||||
|
|
||||
|
@Input() |
||||
|
initialState: RpcInitialStateSettings<any>; |
||||
|
|
||||
|
@Input() |
||||
|
stateValueType: ValueType; |
||||
|
|
||||
|
@Input() |
||||
|
aliasController: IAliasController; |
||||
|
|
||||
|
@Input() |
||||
|
targetDevice: TargetDevice; |
||||
|
|
||||
|
@Input() |
||||
|
popover: TbPopoverComponent<RpcInitialStateSettingsPanelComponent>; |
||||
|
|
||||
|
@Output() |
||||
|
initialStateSettingsApplied = new EventEmitter<RpcInitialStateSettings<any>>(); |
||||
|
|
||||
|
rpcInitialStateAction = RpcInitialStateAction; |
||||
|
|
||||
|
rpcInitialStateActions = rpcInitialStateActions; |
||||
|
|
||||
|
rpcInitialStateTranslationsMap = rpcInitialStateTranslations; |
||||
|
|
||||
|
telemetryTypeTranslationsMap = telemetryTypeTranslationsShort; |
||||
|
|
||||
|
attributeScopes = Object.keys(AttributeScope) as AttributeScope[]; |
||||
|
|
||||
|
dataKeyType = DataKeyType; |
||||
|
|
||||
|
dataToStateType = RpcDataToStateType; |
||||
|
|
||||
|
functionScopeVariables = this.widgetService.getWidgetScopeVariables(); |
||||
|
|
||||
|
valueType = ValueType; |
||||
|
|
||||
|
initialStateSettingsFormGroup: UntypedFormGroup; |
||||
|
|
||||
|
constructor(private fb: UntypedFormBuilder, |
||||
|
private widgetService: WidgetService, |
||||
|
protected store: Store<AppState>) { |
||||
|
super(store); |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.initialStateSettingsFormGroup = this.fb.group( |
||||
|
{ |
||||
|
action: [this.initialState?.action, []], |
||||
|
defaultValue: [this.initialState?.defaultValue, [Validators.required]], |
||||
|
executeRpc: this.fb.group({ |
||||
|
method: [this.initialState?.executeRpc?.method, [Validators.required]], |
||||
|
requestTimeout: [this.initialState?.executeRpc?.requestTimeout, [Validators.required, Validators.min(5000)]], |
||||
|
requestPersistent: [this.initialState?.executeRpc?.requestPersistent, []], |
||||
|
persistentPollingInterval: [this.initialState?.executeRpc?.persistentPollingInterval, [Validators.required, Validators.min(1000)]] |
||||
|
}), |
||||
|
getAttribute: this.fb.group({ |
||||
|
scope: [this.initialState?.getAttribute?.scope, []], |
||||
|
key: [this.initialState?.getAttribute?.key, [Validators.required]], |
||||
|
}), |
||||
|
getTimeSeries: this.fb.group({ |
||||
|
key: [this.initialState?.getTimeSeries?.key, [Validators.required]], |
||||
|
}), |
||||
|
dataToState: this.fb.group({ |
||||
|
type: [this.initialState?.dataToState?.type, [Validators.required]], |
||||
|
dataToStateFunction: [this.initialState?.dataToState?.dataToStateFunction, [Validators.required]], |
||||
|
}), |
||||
|
} |
||||
|
); |
||||
|
if (this.stateValueType === ValueType.BOOLEAN) { |
||||
|
(this.initialStateSettingsFormGroup.get('dataToState') as UntypedFormGroup).addControl( |
||||
|
'compareToValue', this.fb.control(this.initialState?.dataToState?.compareToValue, [Validators.required]) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
merge(this.initialStateSettingsFormGroup.get('action').valueChanges, |
||||
|
this.initialStateSettingsFormGroup.get('dataToState').get('type').valueChanges, |
||||
|
this.initialStateSettingsFormGroup.get('executeRpc').get('requestPersistent').valueChanges).subscribe(() => { |
||||
|
this.updateValidators(); |
||||
|
}); |
||||
|
this.updateValidators(); |
||||
|
} |
||||
|
|
||||
|
cancel() { |
||||
|
this.popover?.hide(); |
||||
|
} |
||||
|
|
||||
|
applyInitialStateSettings() { |
||||
|
const initialStateSettings: RpcInitialStateSettings<any> = this.initialStateSettingsFormGroup.getRawValue(); |
||||
|
this.initialStateSettingsApplied.emit(initialStateSettings); |
||||
|
} |
||||
|
|
||||
|
private updateValidators() { |
||||
|
const action: RpcInitialStateAction = this.initialStateSettingsFormGroup.get('action').value; |
||||
|
const dataToStateType: RpcDataToStateType = this.initialStateSettingsFormGroup.get('dataToState').get('type').value; |
||||
|
|
||||
|
this.initialStateSettingsFormGroup.get('defaultValue').disable({emitEvent: false}); |
||||
|
this.initialStateSettingsFormGroup.get('executeRpc').disable({emitEvent: false}); |
||||
|
this.initialStateSettingsFormGroup.get('getAttribute').disable({emitEvent: false}); |
||||
|
this.initialStateSettingsFormGroup.get('getTimeSeries').disable({emitEvent: false}); |
||||
|
switch (action) { |
||||
|
case RpcInitialStateAction.DO_NOTHING: |
||||
|
this.initialStateSettingsFormGroup.get('defaultValue').enable({emitEvent: false}); |
||||
|
break; |
||||
|
case RpcInitialStateAction.EXECUTE_RPC: |
||||
|
this.initialStateSettingsFormGroup.get('executeRpc').enable({emitEvent: false}); |
||||
|
const requestPersistent: boolean = this.initialStateSettingsFormGroup.get('executeRpc').get('requestPersistent').value; |
||||
|
if (requestPersistent) { |
||||
|
this.initialStateSettingsFormGroup.get('executeRpc').get('persistentPollingInterval').enable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.initialStateSettingsFormGroup.get('executeRpc').get('persistentPollingInterval').disable({emitEvent: false}); |
||||
|
} |
||||
|
break; |
||||
|
case RpcInitialStateAction.GET_ATTRIBUTE: |
||||
|
this.initialStateSettingsFormGroup.get('getAttribute').enable({emitEvent: false}); |
||||
|
break; |
||||
|
case RpcInitialStateAction.GET_TIME_SERIES: |
||||
|
this.initialStateSettingsFormGroup.get('getTimeSeries').enable({emitEvent: false}); |
||||
|
break; |
||||
|
} |
||||
|
if (action === RpcInitialStateAction.DO_NOTHING) { |
||||
|
this.initialStateSettingsFormGroup.get('dataToState').disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.initialStateSettingsFormGroup.get('dataToState').enable({emitEvent: false}); |
||||
|
if (dataToStateType === RpcDataToStateType.FUNCTION) { |
||||
|
this.initialStateSettingsFormGroup.get('dataToState').get('dataToStateFunction').enable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.initialStateSettingsFormGroup.get('dataToState').get('dataToStateFunction').disable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,156 @@ |
|||||
|
///
|
||||
|
/// 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, |
||||
|
HostBinding, |
||||
|
Input, |
||||
|
OnInit, |
||||
|
Renderer2, |
||||
|
ViewContainerRef, |
||||
|
ViewEncapsulation |
||||
|
} from '@angular/core'; |
||||
|
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; |
||||
|
import { MatButton } from '@angular/material/button'; |
||||
|
import { TbPopoverService } from '@shared/components/popover.service'; |
||||
|
import { RpcInitialStateAction, RpcInitialStateSettings } from '@shared/models/rpc-widget-settings.models'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
import { ValueType } from '@shared/models/constants'; |
||||
|
import { |
||||
|
RpcInitialStateSettingsPanelComponent |
||||
|
} from '@home/components/widget/lib/settings/common/rpc/rpc-initial-state-settings-panel.component'; |
||||
|
import { IAliasController } from '@core/api/widget-api.models'; |
||||
|
import { TargetDevice } from '@shared/models/widget.models'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-rpc-initial-state-settings', |
||||
|
templateUrl: './rpc-state-settings-button.component.html', |
||||
|
styleUrls: ['./rpc-state-settings-button.scss'], |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => RpcInitialStateSettingsComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
], |
||||
|
encapsulation: ViewEncapsulation.None |
||||
|
}) |
||||
|
export class RpcInitialStateSettingsComponent implements OnInit, ControlValueAccessor { |
||||
|
|
||||
|
@HostBinding('style.overflow') |
||||
|
overflow = 'hidden'; |
||||
|
|
||||
|
@Input() |
||||
|
stateValueType: ValueType; |
||||
|
|
||||
|
@Input() |
||||
|
aliasController: IAliasController; |
||||
|
|
||||
|
@Input() |
||||
|
targetDevice: TargetDevice; |
||||
|
|
||||
|
@Input() |
||||
|
disabled = false; |
||||
|
|
||||
|
modelValue: RpcInitialStateSettings<any>; |
||||
|
|
||||
|
displayValue: string; |
||||
|
|
||||
|
private propagateChange = null; |
||||
|
|
||||
|
constructor(private translate: TranslateService, |
||||
|
private popoverService: TbPopoverService, |
||||
|
private renderer: Renderer2, |
||||
|
private viewContainerRef: ViewContainerRef, |
||||
|
private cd: ChangeDetectorRef) {} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(_fn: any): void { |
||||
|
} |
||||
|
|
||||
|
setDisabledState(isDisabled: boolean): void { |
||||
|
if (this.disabled !== isDisabled) { |
||||
|
this.disabled = isDisabled; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
writeValue(value: RpcInitialStateSettings<any>): void { |
||||
|
this.modelValue = value; |
||||
|
this.updateDisplayValue(); |
||||
|
} |
||||
|
|
||||
|
openRpcStateSettingsPopup($event: Event, matButton: MatButton) { |
||||
|
if ($event) { |
||||
|
$event.stopPropagation(); |
||||
|
} |
||||
|
const trigger = matButton._elementRef.nativeElement; |
||||
|
if (this.popoverService.hasPopover(trigger)) { |
||||
|
this.popoverService.hidePopover(trigger); |
||||
|
} else { |
||||
|
const ctx: any = { |
||||
|
initialState: this.modelValue, |
||||
|
stateValueType: this.stateValueType, |
||||
|
aliasController: this.aliasController, |
||||
|
targetDevice: this.targetDevice |
||||
|
}; |
||||
|
const initialStateSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, |
||||
|
this.viewContainerRef, RpcInitialStateSettingsPanelComponent, |
||||
|
['leftTopOnly', 'leftOnly', 'leftBottomOnly'], true, null, |
||||
|
ctx, |
||||
|
{}, |
||||
|
{}, {}, true); |
||||
|
initialStateSettingsPanelPopover.tbComponentRef.instance.popover = initialStateSettingsPanelPopover; |
||||
|
initialStateSettingsPanelPopover.tbComponentRef.instance.initialStateSettingsApplied.subscribe((initialState) => { |
||||
|
initialStateSettingsPanelPopover.hide(); |
||||
|
this.modelValue = initialState; |
||||
|
this.updateDisplayValue(); |
||||
|
this.propagateChange(this.modelValue); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private updateDisplayValue() { |
||||
|
switch (this.modelValue.action) { |
||||
|
case RpcInitialStateAction.DO_NOTHING: |
||||
|
if (this.stateValueType === ValueType.BOOLEAN) { |
||||
|
this.displayValue = this.translate.instant(!!this.modelValue.defaultValue ? 'widgets.rpc-state.on' : 'widgets.rpc-state.off'); |
||||
|
} else { |
||||
|
this.displayValue = this.modelValue.defaultValue + ''; |
||||
|
} |
||||
|
break; |
||||
|
case RpcInitialStateAction.EXECUTE_RPC: |
||||
|
const methodName = this.modelValue.executeRpc.method; |
||||
|
this.displayValue = this.translate.instant('widgets.rpc-state.execute-rpc-text', {methodName}); |
||||
|
break; |
||||
|
case RpcInitialStateAction.GET_ATTRIBUTE: |
||||
|
this.displayValue = this.translate.instant('widgets.rpc-state.get-attribute-text', {key: this.modelValue.getAttribute.key}); |
||||
|
break; |
||||
|
case RpcInitialStateAction.GET_TIME_SERIES: |
||||
|
this.displayValue = this.translate.instant('widgets.rpc-state.get-time-series-text', {key: this.modelValue.getTimeSeries.key}); |
||||
|
break; |
||||
|
} |
||||
|
this.cd.markForCheck(); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,26 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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. |
||||
|
|
||||
|
--> |
||||
|
<button #matButton [disabled]="disabled" |
||||
|
class="tb-rpc-state-settings" |
||||
|
mat-stroked-button color="primary" |
||||
|
(click)="openRpcStateSettingsPopup($event, matButton)"> |
||||
|
<div> |
||||
|
<span>{{ displayValue }}</span> |
||||
|
<mat-icon class="tb-mat-20">edit</mat-icon> |
||||
|
</div> |
||||
|
</button> |
||||
@ -0,0 +1,34 @@ |
|||||
|
/** |
||||
|
* 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. |
||||
|
*/ |
||||
|
.mdc-button.tb-rpc-state-settings { |
||||
|
width: 100%; |
||||
|
.mdc-button__label { |
||||
|
width: 100%; |
||||
|
& > div { |
||||
|
width: 100%; |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
gap: 8px; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
& > span { |
||||
|
white-space: nowrap; |
||||
|
overflow: hidden; |
||||
|
text-overflow: ellipsis; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,47 @@ |
|||||
|
/** |
||||
|
* 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'; |
||||
|
|
||||
|
.tb-rpc-state-settings-panel { |
||||
|
width: 530px; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
gap: 16px; |
||||
|
@media #{$mat-lt-md} { |
||||
|
width: 90vw; |
||||
|
} |
||||
|
.tb-rpc-state-settings-panel-content { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
gap: 16px; |
||||
|
overflow: auto; |
||||
|
} |
||||
|
.tb-rpc-state-settings-title { |
||||
|
font-size: 16px; |
||||
|
font-weight: 500; |
||||
|
line-height: 24px; |
||||
|
letter-spacing: 0.25px; |
||||
|
color: rgba(0, 0, 0, 0.87); |
||||
|
} |
||||
|
.tb-rpc-state-settings-panel-buttons { |
||||
|
height: 40px; |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
gap: 16px; |
||||
|
justify-content: flex-end; |
||||
|
align-items: flex-end; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,201 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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-rpc-state-settings-panel" [formGroup]="updateStateSettingsFormGroup"> |
||||
|
<div class="tb-rpc-state-settings-title">{{ panelTitle | translate }}</div> |
||||
|
<div class="tb-rpc-state-settings-panel-content"> |
||||
|
<div class="tb-form-row"> |
||||
|
<div class="fixed-title-width" >{{ 'widgets.rpc-state.action' | translate }}</div> |
||||
|
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-select formControlName="action"> |
||||
|
<mat-option *ngFor="let action of rpcUpdateStateActions" [value]="action"> |
||||
|
{{ rpcUpdateStateTranslationsMap.get(action) | translate }} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
<ng-container [ngSwitch]="updateStateSettingsFormGroup.get('action').value"> |
||||
|
<ng-template [ngSwitchCase]="rpcUpdateStateAction.EXECUTE_RPC"> |
||||
|
<div class="tb-form-row" formGroupName="executeRpc"> |
||||
|
<div class="fixed-title-width" >{{ 'widgets.rpc-state.method' | translate }}*</div> |
||||
|
<mat-form-field class="flex tb-suffix-absolute" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput required formControlName="method" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="'widgets.rpc-state.method-name-required' | translate" |
||||
|
*ngIf="updateStateSettingsFormGroup.get('executeRpc').get('method').hasError('required') |
||||
|
&& updateStateSettingsFormGroup.get('executeRpc').get('method').touched" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</ng-template> |
||||
|
<ng-template [ngSwitchCase]="rpcUpdateStateAction.SET_ATTRIBUTE"> |
||||
|
<ng-container formGroupName="setAttribute"> |
||||
|
<div class="tb-form-row"> |
||||
|
<div class="fixed-title-width">{{ 'widgets.rpc-state.attribute-scope' | translate }}</div> |
||||
|
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-select formControlName="scope"> |
||||
|
<mat-option *ngFor="let scope of attributeScopes" [value]="scope"> |
||||
|
{{ telemetryTypeTranslationsMap.get(scope) | translate }} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
<div class="tb-form-row"> |
||||
|
<div class="fixed-title-width" >{{ 'widgets.rpc-state.attribute-key' | translate }}*</div> |
||||
|
<tb-device-key-autocomplete |
||||
|
fxFlex |
||||
|
formControlName="key" |
||||
|
required |
||||
|
requiredText="widgets.rpc-state.attribute-key-required" |
||||
|
inlineField |
||||
|
[aliasController]="aliasController" |
||||
|
[targetDevice]="targetDevice" |
||||
|
[keyType]="dataKeyType.attribute" |
||||
|
[attributeScope]="updateStateSettingsFormGroup.get('setAttribute').get('scope').value"> |
||||
|
</tb-device-key-autocomplete> |
||||
|
</div> |
||||
|
</ng-container> |
||||
|
</ng-template> |
||||
|
<ng-template [ngSwitchCase]="rpcUpdateStateAction.ADD_TIME_SERIES"> |
||||
|
<ng-container formGroupName="putTimeSeries"> |
||||
|
<div class="tb-form-row"> |
||||
|
<div class="fixed-title-width" >{{ 'widgets.rpc-state.time-series-key' | translate }}*</div> |
||||
|
<tb-device-key-autocomplete |
||||
|
fxFlex |
||||
|
formControlName="key" |
||||
|
required |
||||
|
requiredText="widgets.rpc-state.time-series-key-required" |
||||
|
inlineField |
||||
|
[aliasController]="aliasController" |
||||
|
[targetDevice]="targetDevice" |
||||
|
[keyType]="dataKeyType.timeseries"> |
||||
|
</tb-device-key-autocomplete> |
||||
|
</div> |
||||
|
</ng-container> |
||||
|
</ng-template> |
||||
|
</ng-container> |
||||
|
<div class="tb-form-panel stroked" formGroupName="stateToParams"> |
||||
|
<div class="tb-form-row no-padding no-border column-xs"> |
||||
|
<div class="fixed-title-width">{{ (updateStateSettingsFormGroup.get('action').value === rpcUpdateStateAction.EXECUTE_RPC ? |
||||
|
'widgets.rpc-state.parameters' : 'widgets.rpc-state.value') | translate }}</div> |
||||
|
<tb-toggle-select fxFlex formControlName="type"> |
||||
|
<tb-toggle-option [value]="stateToParamsType.CONSTANT">{{ 'widgets.rpc-state.converter-constant' | translate }}</tb-toggle-option> |
||||
|
<tb-toggle-option [value]="stateToParamsType.FUNCTION">{{ 'widgets.rpc-state.converter-function' | translate }}</tb-toggle-option> |
||||
|
<tb-toggle-option *ngIf="updateStateSettingsFormGroup.get('action').value === rpcUpdateStateAction.EXECUTE_RPC" |
||||
|
[value]="stateToParamsType.NONE">{{ 'widgets.rpc-state.converter-none' | translate }}</tb-toggle-option> |
||||
|
</tb-toggle-select> |
||||
|
</div> |
||||
|
<tb-value-input *ngIf="updateStateSettingsFormGroup.get('stateToParams').get('type').value === stateToParamsType.CONSTANT" |
||||
|
fxFlex |
||||
|
[layout]="{ |
||||
|
layout: 'row', |
||||
|
breakpoints: {xs: 'column'} |
||||
|
}" |
||||
|
formControlName="constantValue"> |
||||
|
</tb-value-input> |
||||
|
<tb-js-func *ngIf="updateStateSettingsFormGroup.get('stateToParams').get('type').value === stateToParamsType.FUNCTION" |
||||
|
formControlName="stateToParamsFunction" |
||||
|
required |
||||
|
[globalVariables]="functionScopeVariables" |
||||
|
[functionArgs]="['value']" |
||||
|
functionTitle="{{ 'widgets.rpc-state.convert-value-function' | translate }}" |
||||
|
helpId="widget/lib/rpc/convert_value_fn"> |
||||
|
</tb-js-func> |
||||
|
</div> |
||||
|
<div *ngIf="updateStateSettingsFormGroup.get('action').value === rpcUpdateStateAction.EXECUTE_RPC" |
||||
|
class="tb-form-panel no-border tb-slide-toggle" formGroupName="executeRpc"> |
||||
|
<mat-expansion-panel class="tb-settings no-padding"> |
||||
|
<mat-expansion-panel-header fxLayout="row wrap"> |
||||
|
<mat-panel-title> |
||||
|
</mat-panel-title> |
||||
|
<mat-panel-description fxLayoutAlign="end center" translate> |
||||
|
widget-config.advanced-settings |
||||
|
</mat-panel-description> |
||||
|
</mat-expansion-panel-header> |
||||
|
<ng-template matExpansionPanelContent> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<div>{{ 'widgets.rpc-state.request-timeout-ms' | translate }}</div> |
||||
|
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic"> |
||||
|
<input matInput formControlName="requestTimeout" type="number" required min="5000" step="1" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="(updateStateSettingsFormGroup.get('executeRpc').get('requestTimeout').hasError('required') |
||||
|
? 'widgets.rpc-state.request-timeout-required' : 'widgets.rpc-state.min-request-timeout-error') | translate" |
||||
|
*ngIf="updateStateSettingsFormGroup.get('executeRpc').get('requestTimeout').touched && |
||||
|
updateStateSettingsFormGroup.get('executeRpc').get('requestTimeout').invalid" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
<div class="tb-form-panel stroked tb-slide-toggle"> |
||||
|
<mat-expansion-panel class="tb-settings" |
||||
|
[expanded]="updateStateSettingsFormGroup.get('executeRpc').get('requestPersistent').value" |
||||
|
[disabled]="!updateStateSettingsFormGroup.get('executeRpc').get('requestPersistent').value"> |
||||
|
<mat-expansion-panel-header fxLayout="row wrap"> |
||||
|
<mat-panel-title> |
||||
|
<mat-slide-toggle class="mat-slide" formControlName="requestPersistent" (click)="$event.stopPropagation()" |
||||
|
fxLayoutAlign="center"> |
||||
|
{{ 'widgets.rpc-state.request-persistent' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
</mat-panel-title> |
||||
|
</mat-expansion-panel-header> |
||||
|
<ng-template matExpansionPanelContent> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<div tb-hint-tooltip-icon="{{'widgets.rpc-state.persistent-polling-interval-hint' | translate}}">{{ 'widgets.rpc-state.persistent-polling-interval' | translate }}</div> |
||||
|
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic"> |
||||
|
<input matInput formControlName="persistentPollingInterval" type="number" required min="1000" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="(updateStateSettingsFormGroup.get('executeRpc').get('persistentPollingInterval').hasError('required') |
||||
|
? 'widgets.rpc-state.persistent-polling-interval-required' : 'widgets.rpc-state.min-persistent-polling-interval-error') | translate" |
||||
|
*ngIf="updateStateSettingsFormGroup.get('executeRpc').get('persistentPollingInterval').touched && |
||||
|
updateStateSettingsFormGroup.get('executeRpc').get('persistentPollingInterval').invalid" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</ng-template> |
||||
|
</mat-expansion-panel> |
||||
|
</div> |
||||
|
</ng-template> |
||||
|
</mat-expansion-panel> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-rpc-state-settings-panel-buttons"> |
||||
|
<button mat-button |
||||
|
color="primary" |
||||
|
type="button" |
||||
|
(click)="cancel()"> |
||||
|
{{ 'action.cancel' | translate }} |
||||
|
</button> |
||||
|
<button mat-raised-button |
||||
|
color="primary" |
||||
|
type="button" |
||||
|
(click)="applyUpdateStateSettings()" |
||||
|
[disabled]="updateStateSettingsFormGroup.invalid || !updateStateSettingsFormGroup.dirty"> |
||||
|
{{ 'action.apply' | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
@ -0,0 +1,180 @@ |
|||||
|
///
|
||||
|
/// 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, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; |
||||
|
import { PageComponent } from '@shared/components/page.component'; |
||||
|
import { TbPopoverComponent } from '@shared/components/popover.component'; |
||||
|
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { merge } from 'rxjs'; |
||||
|
import { |
||||
|
RpcStateToParamsType, |
||||
|
RpcUpdateStateAction, |
||||
|
rpcUpdateStateActions, |
||||
|
RpcUpdateStateSettings, |
||||
|
rpcUpdateStateTranslations |
||||
|
} from '@shared/models/rpc-widget-settings.models'; |
||||
|
import { ValueType } from '@shared/models/constants'; |
||||
|
import { TargetDevice } from '@shared/models/widget.models'; |
||||
|
import { AttributeScope, DataKeyType, telemetryTypeTranslationsShort } from '@shared/models/telemetry/telemetry.models'; |
||||
|
import { IAliasController } from '@core/api/widget-api.models'; |
||||
|
import { WidgetService } from '@core/http/widget.service'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-rpc-update-state-settings-panel', |
||||
|
templateUrl: './rpc-update-state-settings-panel.component.html', |
||||
|
providers: [], |
||||
|
styleUrls: ['./rpc-state-settings-panel.component.scss'], |
||||
|
encapsulation: ViewEncapsulation.None |
||||
|
}) |
||||
|
export class RpcUpdateStateSettingsPanelComponent extends PageComponent implements OnInit { |
||||
|
|
||||
|
@Input() |
||||
|
panelTitle: string; |
||||
|
|
||||
|
@Input() |
||||
|
updateState: RpcUpdateStateSettings; |
||||
|
|
||||
|
@Input() |
||||
|
stateValueType: ValueType; |
||||
|
|
||||
|
@Input() |
||||
|
aliasController: IAliasController; |
||||
|
|
||||
|
@Input() |
||||
|
targetDevice: TargetDevice; |
||||
|
|
||||
|
@Input() |
||||
|
popover: TbPopoverComponent<RpcUpdateStateSettingsPanelComponent>; |
||||
|
|
||||
|
@Output() |
||||
|
updateStateSettingsApplied = new EventEmitter<RpcUpdateStateSettings>(); |
||||
|
|
||||
|
rpcUpdateStateAction = RpcUpdateStateAction; |
||||
|
|
||||
|
rpcUpdateStateActions = rpcUpdateStateActions; |
||||
|
|
||||
|
rpcUpdateStateTranslationsMap = rpcUpdateStateTranslations; |
||||
|
|
||||
|
telemetryTypeTranslationsMap = telemetryTypeTranslationsShort; |
||||
|
|
||||
|
attributeScopes = [AttributeScope.SERVER_SCOPE, AttributeScope.SHARED_SCOPE]; |
||||
|
|
||||
|
dataKeyType = DataKeyType; |
||||
|
|
||||
|
stateToParamsType = RpcStateToParamsType; |
||||
|
|
||||
|
functionScopeVariables = this.widgetService.getWidgetScopeVariables(); |
||||
|
|
||||
|
valueType = ValueType; |
||||
|
|
||||
|
updateStateSettingsFormGroup: UntypedFormGroup; |
||||
|
|
||||
|
constructor(private fb: UntypedFormBuilder, |
||||
|
private widgetService: WidgetService, |
||||
|
protected store: Store<AppState>) { |
||||
|
super(store); |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.updateStateSettingsFormGroup = this.fb.group( |
||||
|
{ |
||||
|
action: [this.updateState?.action, []], |
||||
|
executeRpc: this.fb.group({ |
||||
|
method: [this.updateState?.executeRpc?.method, [Validators.required]], |
||||
|
requestTimeout: [this.updateState?.executeRpc?.requestTimeout, [Validators.required, Validators.min(5000)]], |
||||
|
requestPersistent: [this.updateState?.executeRpc?.requestPersistent, []], |
||||
|
persistentPollingInterval: [this.updateState?.executeRpc?.persistentPollingInterval, [Validators.required, Validators.min(1000)]] |
||||
|
}), |
||||
|
setAttribute: this.fb.group({ |
||||
|
scope: [this.updateState?.setAttribute?.scope, []], |
||||
|
key: [this.updateState?.setAttribute?.key, [Validators.required]], |
||||
|
}), |
||||
|
putTimeSeries: this.fb.group({ |
||||
|
key: [this.updateState?.putTimeSeries?.key, [Validators.required]], |
||||
|
}), |
||||
|
stateToParams: this.fb.group({ |
||||
|
type: [this.updateState?.stateToParams?.type, [Validators.required]], |
||||
|
constantValue: [this.updateState?.stateToParams?.constantValue, [Validators.required]], |
||||
|
stateToParamsFunction: [this.updateState?.stateToParams?.stateToParamsFunction, [Validators.required]], |
||||
|
}), |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
merge(this.updateStateSettingsFormGroup.get('action').valueChanges, |
||||
|
this.updateStateSettingsFormGroup.get('stateToParams').get('type').valueChanges, |
||||
|
this.updateStateSettingsFormGroup.get('executeRpc').get('requestPersistent').valueChanges).subscribe(() => { |
||||
|
this.updateValidators(); |
||||
|
}); |
||||
|
this.updateValidators(); |
||||
|
} |
||||
|
|
||||
|
cancel() { |
||||
|
this.popover?.hide(); |
||||
|
} |
||||
|
|
||||
|
applyUpdateStateSettings() { |
||||
|
const updateStateSettings: RpcUpdateStateSettings = this.updateStateSettingsFormGroup.getRawValue(); |
||||
|
this.updateStateSettingsApplied.emit(updateStateSettings); |
||||
|
} |
||||
|
|
||||
|
private updateValidators() { |
||||
|
const action: RpcUpdateStateAction = this.updateStateSettingsFormGroup.get('action').value; |
||||
|
let stateToParamsType: RpcStateToParamsType = this.updateStateSettingsFormGroup.get('stateToParams').get('type').value; |
||||
|
|
||||
|
this.updateStateSettingsFormGroup.get('executeRpc').disable({emitEvent: false}); |
||||
|
this.updateStateSettingsFormGroup.get('setAttribute').disable({emitEvent: false}); |
||||
|
this.updateStateSettingsFormGroup.get('putTimeSeries').disable({emitEvent: false}); |
||||
|
switch (action) { |
||||
|
case RpcUpdateStateAction.EXECUTE_RPC: |
||||
|
this.updateStateSettingsFormGroup.get('executeRpc').enable({emitEvent: false}); |
||||
|
const requestPersistent: boolean = this.updateStateSettingsFormGroup.get('executeRpc').get('requestPersistent').value; |
||||
|
if (requestPersistent) { |
||||
|
this.updateStateSettingsFormGroup.get('executeRpc').get('persistentPollingInterval').enable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.updateStateSettingsFormGroup.get('executeRpc').get('persistentPollingInterval').disable({emitEvent: false}); |
||||
|
} |
||||
|
break; |
||||
|
case RpcUpdateStateAction.SET_ATTRIBUTE: |
||||
|
case RpcUpdateStateAction.ADD_TIME_SERIES: |
||||
|
if (stateToParamsType === RpcStateToParamsType.NONE) { |
||||
|
stateToParamsType = RpcStateToParamsType.CONSTANT; |
||||
|
this.updateStateSettingsFormGroup.get('stateToParams').get('type').patchValue(stateToParamsType, {emitEvent: false}); |
||||
|
} |
||||
|
if (action === RpcUpdateStateAction.SET_ATTRIBUTE) { |
||||
|
this.updateStateSettingsFormGroup.get('setAttribute').enable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.updateStateSettingsFormGroup.get('putTimeSeries').enable({emitEvent: false}); |
||||
|
} |
||||
|
break; |
||||
|
} |
||||
|
switch (stateToParamsType) { |
||||
|
case RpcStateToParamsType.CONSTANT: |
||||
|
this.updateStateSettingsFormGroup.get('stateToParams').get('constantValue').enable({emitEvent: false}); |
||||
|
this.updateStateSettingsFormGroup.get('stateToParams').get('stateToParamsFunction').disable({emitEvent: false}); |
||||
|
break; |
||||
|
case RpcStateToParamsType.FUNCTION: |
||||
|
this.updateStateSettingsFormGroup.get('stateToParams').get('constantValue').disable({emitEvent: false}); |
||||
|
this.updateStateSettingsFormGroup.get('stateToParams').get('stateToParamsFunction').enable({emitEvent: false}); |
||||
|
break; |
||||
|
case RpcStateToParamsType.NONE: |
||||
|
this.updateStateSettingsFormGroup.get('stateToParams').get('constantValue').disable({emitEvent: false}); |
||||
|
this.updateStateSettingsFormGroup.get('stateToParams').get('stateToParamsFunction').disable({emitEvent: false}); |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,174 @@ |
|||||
|
///
|
||||
|
/// 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, |
||||
|
HostBinding, |
||||
|
Input, |
||||
|
OnInit, |
||||
|
Renderer2, |
||||
|
ViewContainerRef, |
||||
|
ViewEncapsulation |
||||
|
} from '@angular/core'; |
||||
|
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; |
||||
|
import { MatButton } from '@angular/material/button'; |
||||
|
import { TbPopoverService } from '@shared/components/popover.service'; |
||||
|
import { |
||||
|
RpcStateToParamsType, |
||||
|
RpcUpdateStateAction, |
||||
|
RpcUpdateStateSettings |
||||
|
} from '@shared/models/rpc-widget-settings.models'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
import { ValueType } from '@shared/models/constants'; |
||||
|
import { IAliasController } from '@core/api/widget-api.models'; |
||||
|
import { TargetDevice } from '@shared/models/widget.models'; |
||||
|
import { isDefinedAndNotNull } from '@core/utils'; |
||||
|
import { |
||||
|
RpcUpdateStateSettingsPanelComponent |
||||
|
} from '@home/components/widget/lib/settings/common/rpc/rpc-update-state-settings-panel.component'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-rpc-update-state-settings', |
||||
|
templateUrl: './rpc-state-settings-button.component.html', |
||||
|
styleUrls: ['./rpc-state-settings-button.scss'], |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => RpcUpdateStateSettingsComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
], |
||||
|
encapsulation: ViewEncapsulation.None |
||||
|
}) |
||||
|
export class RpcUpdateStateSettingsComponent implements OnInit, ControlValueAccessor { |
||||
|
|
||||
|
@HostBinding('style.overflow') |
||||
|
overflow = 'hidden'; |
||||
|
|
||||
|
@Input() |
||||
|
panelTitle: string; |
||||
|
|
||||
|
@Input() |
||||
|
stateValueType: ValueType; |
||||
|
|
||||
|
@Input() |
||||
|
aliasController: IAliasController; |
||||
|
|
||||
|
@Input() |
||||
|
targetDevice: TargetDevice; |
||||
|
|
||||
|
@Input() |
||||
|
disabled = false; |
||||
|
|
||||
|
modelValue: RpcUpdateStateSettings; |
||||
|
|
||||
|
displayValue: string; |
||||
|
|
||||
|
private propagateChange = null; |
||||
|
|
||||
|
constructor(private translate: TranslateService, |
||||
|
private popoverService: TbPopoverService, |
||||
|
private renderer: Renderer2, |
||||
|
private viewContainerRef: ViewContainerRef, |
||||
|
private cd: ChangeDetectorRef) {} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(_fn: any): void { |
||||
|
} |
||||
|
|
||||
|
setDisabledState(isDisabled: boolean): void { |
||||
|
if (this.disabled !== isDisabled) { |
||||
|
this.disabled = isDisabled; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
writeValue(value: RpcUpdateStateSettings): void { |
||||
|
this.modelValue = value; |
||||
|
this.updateDisplayValue(); |
||||
|
} |
||||
|
|
||||
|
openRpcStateSettingsPopup($event: Event, matButton: MatButton) { |
||||
|
if ($event) { |
||||
|
$event.stopPropagation(); |
||||
|
} |
||||
|
const trigger = matButton._elementRef.nativeElement; |
||||
|
if (this.popoverService.hasPopover(trigger)) { |
||||
|
this.popoverService.hidePopover(trigger); |
||||
|
} else { |
||||
|
const ctx: any = { |
||||
|
updateState: this.modelValue, |
||||
|
panelTitle: this.panelTitle, |
||||
|
stateValueType: this.stateValueType, |
||||
|
aliasController: this.aliasController, |
||||
|
targetDevice: this.targetDevice |
||||
|
}; |
||||
|
const updateStateSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, |
||||
|
this.viewContainerRef, RpcUpdateStateSettingsPanelComponent, |
||||
|
['leftTopOnly', 'leftOnly', 'leftBottomOnly'], true, null, |
||||
|
ctx, |
||||
|
{}, |
||||
|
{}, {}, true); |
||||
|
updateStateSettingsPanelPopover.tbComponentRef.instance.popover = updateStateSettingsPanelPopover; |
||||
|
updateStateSettingsPanelPopover.tbComponentRef.instance.updateStateSettingsApplied.subscribe((updateState) => { |
||||
|
updateStateSettingsPanelPopover.hide(); |
||||
|
this.modelValue = updateState; |
||||
|
this.updateDisplayValue(); |
||||
|
this.propagateChange(this.modelValue); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private updateDisplayValue() { |
||||
|
let value: any; |
||||
|
switch (this.modelValue.stateToParams.type) { |
||||
|
case RpcStateToParamsType.CONSTANT: |
||||
|
value = this.modelValue.stateToParams.constantValue; |
||||
|
break; |
||||
|
case RpcStateToParamsType.FUNCTION: |
||||
|
value = 'f(value)'; |
||||
|
break; |
||||
|
case RpcStateToParamsType.NONE: |
||||
|
break; |
||||
|
} |
||||
|
switch (this.modelValue.action) { |
||||
|
case RpcUpdateStateAction.EXECUTE_RPC: |
||||
|
let methodName = this.modelValue.executeRpc.method; |
||||
|
if (isDefinedAndNotNull(value)) { |
||||
|
methodName = `${methodName}(${value})`; |
||||
|
} |
||||
|
this.displayValue = this.translate.instant('widgets.rpc-state.execute-rpc-text', {methodName}); |
||||
|
break; |
||||
|
case RpcUpdateStateAction.SET_ATTRIBUTE: |
||||
|
this.displayValue = this.translate.instant('widgets.rpc-state.set-attribute-to-value-text', |
||||
|
{key: this.modelValue.setAttribute.key, value}); |
||||
|
break; |
||||
|
case RpcUpdateStateAction.ADD_TIME_SERIES: |
||||
|
this.displayValue = this.translate.instant('widgets.rpc-state.add-time-series-value-text', |
||||
|
{key: this.modelValue.setAttribute.key, value}); |
||||
|
break; |
||||
|
} |
||||
|
this.cd.markForCheck(); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,198 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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]="singleSwitchWidgetSettingsForm"> |
||||
|
<div class="tb-form-panel"> |
||||
|
<div class="tb-form-panel-title" translate>widgets.single-switch.behavior</div> |
||||
|
<div class="tb-form-row"> |
||||
|
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.rpc-state.initial-state-hint' | translate}}" translate>widgets.rpc-state.initial-state</div> |
||||
|
<tb-rpc-initial-state-settings fxFlex |
||||
|
[stateValueType]="valueType.BOOLEAN" |
||||
|
[aliasController]="aliasController" |
||||
|
[targetDevice]="targetDevice" |
||||
|
formControlName="initialState"></tb-rpc-initial-state-settings> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.rpc-state.turn-on-hint' | translate}}" translate>widgets.rpc-state.turn-on</div> |
||||
|
<tb-rpc-update-state-settings fxFlex |
||||
|
panelTitle="widgets.rpc-state.turn-on" |
||||
|
[stateValueType]="valueType.BOOLEAN" |
||||
|
[aliasController]="aliasController" |
||||
|
[targetDevice]="targetDevice" |
||||
|
formControlName="onUpdateState"></tb-rpc-update-state-settings> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.rpc-state.turn-off-hint' | translate}}" translate>widgets.rpc-state.turn-off</div> |
||||
|
<tb-rpc-update-state-settings fxFlex |
||||
|
panelTitle="widgets.rpc-state.turn-off" |
||||
|
[stateValueType]="valueType.BOOLEAN" |
||||
|
[aliasController]="aliasController" |
||||
|
[targetDevice]="targetDevice" |
||||
|
formControlName="offUpdateState"></tb-rpc-update-state-settings> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-panel"> |
||||
|
<div class="tb-form-panel-title" translate>widget-config.card-style</div> |
||||
|
<tb-image-cards-select rowHeight="2:1" |
||||
|
[cols]="{columns: 3, |
||||
|
breakpoints: { |
||||
|
'lt-sm': 1, |
||||
|
'lt-md': 2 |
||||
|
}}" |
||||
|
label="{{ 'widgets.single-switch.layout' | translate }}" formControlName="layout"> |
||||
|
<tb-image-cards-select-option *ngFor="let layout of singleSwitchLayouts" |
||||
|
[value]="layout" |
||||
|
[image]="singleSwitchLayoutImageMap.get(layout)"> |
||||
|
{{ singleSwitchLayoutTranslationMap.get(layout) | translate }} |
||||
|
</tb-image-cards-select-option> |
||||
|
</tb-image-cards-select> |
||||
|
<div class="tb-form-row"> |
||||
|
<mat-slide-toggle class="mat-slide" formControlName="autoScale"> |
||||
|
{{ 'widgets.single-switch.auto-scale' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
</div> |
||||
|
<div class="tb-form-row"> |
||||
|
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showLabel"> |
||||
|
{{ 'widgets.single-switch.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="label" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
</mat-form-field> |
||||
|
<tb-font-settings formControlName="labelFont" |
||||
|
[previewText]="singleSwitchWidgetSettingsForm.get('label').value"> |
||||
|
</tb-font-settings> |
||||
|
<tb-color-input asBoxInput |
||||
|
colorClearButton |
||||
|
formControlName="labelColor"> |
||||
|
</tb-color-input> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row"> |
||||
|
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showIcon"> |
||||
|
{{ 'widgets.single-switch.icon' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
<div fxFlex fxLayout="row" fxLayoutAlign="start 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]="singleSwitchWidgetSettingsForm.get('iconColor').value?.color" |
||||
|
formControlName="icon"> |
||||
|
</tb-material-icon-select> |
||||
|
<tb-color-input asBoxInput |
||||
|
colorClearButton |
||||
|
formControlName="iconColor"> |
||||
|
</tb-color-input> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<div>{{ 'widgets.background.background' | translate }}</div> |
||||
|
<tb-background-settings formControlName="background"> |
||||
|
</tb-background-settings> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-panel"> |
||||
|
<div class="tb-form-panel-title" translate>widgets.single-switch.switch</div> |
||||
|
<div class="tb-form-row space-between column-xs"> |
||||
|
<div>{{ 'widgets.single-switch.switch-color' | translate }}</div> |
||||
|
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutAlign.lt-sm="space-between center" fxLayoutGap="12px"> |
||||
|
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
||||
|
<div translate>widgets.single-switch.on</div> |
||||
|
<tb-color-input asBoxInput |
||||
|
formControlName="switchColorOn"> |
||||
|
</tb-color-input> |
||||
|
</div> |
||||
|
<mat-divider vertical fxHide.lt-sm></mat-divider> |
||||
|
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
||||
|
<div translate>widgets.single-switch.off</div> |
||||
|
<tb-color-input asBoxInput |
||||
|
formControlName="switchColorOff"> |
||||
|
</tb-color-input> |
||||
|
</div> |
||||
|
<mat-divider vertical fxHide.lt-sm></mat-divider> |
||||
|
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
||||
|
<div translate>widgets.single-switch.disabled</div> |
||||
|
<tb-color-input asBoxInput |
||||
|
formControlName="switchColorDisabled"> |
||||
|
</tb-color-input> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between column-xs"> |
||||
|
<div>{{ 'widgets.single-switch.tumbler-color' | translate }}</div> |
||||
|
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutAlign.lt-sm="space-between center" fxLayoutGap="12px"> |
||||
|
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
||||
|
<div translate>widgets.single-switch.on</div> |
||||
|
<tb-color-input asBoxInput |
||||
|
formControlName="tumblerColorOn"> |
||||
|
</tb-color-input> |
||||
|
</div> |
||||
|
<mat-divider vertical fxHide.lt-sm></mat-divider> |
||||
|
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
||||
|
<div translate>widgets.single-switch.off</div> |
||||
|
<tb-color-input asBoxInput |
||||
|
formControlName="tumblerColorOff"> |
||||
|
</tb-color-input> |
||||
|
</div> |
||||
|
<mat-divider vertical fxHide.lt-sm></mat-divider> |
||||
|
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
||||
|
<div translate>widgets.single-switch.disabled</div> |
||||
|
<tb-color-input asBoxInput |
||||
|
formControlName="tumblerColorDisabled"> |
||||
|
</tb-color-input> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row"> |
||||
|
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showOnLabel"> |
||||
|
{{ 'widgets.single-switch.on-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="onLabel" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
</mat-form-field> |
||||
|
<tb-font-settings formControlName="onLabelFont" |
||||
|
[previewText]="singleSwitchWidgetSettingsForm.get('onLabel').value"> |
||||
|
</tb-font-settings> |
||||
|
<tb-color-input asBoxInput |
||||
|
colorClearButton |
||||
|
formControlName="onLabelColor"> |
||||
|
</tb-color-input> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row"> |
||||
|
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showOffLabel"> |
||||
|
{{ 'widgets.single-switch.off-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="offLabel" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
</mat-form-field> |
||||
|
<tb-font-settings formControlName="offLabelFont" |
||||
|
[previewText]="singleSwitchWidgetSettingsForm.get('offLabel').value"> |
||||
|
</tb-font-settings> |
||||
|
<tb-color-input asBoxInput |
||||
|
colorClearButton |
||||
|
formControlName="offLabelColor"> |
||||
|
</tb-color-input> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</ng-container> |
||||
@ -0,0 +1,157 @@ |
|||||
|
///
|
||||
|
/// 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 } from '@angular/core'; |
||||
|
import { TargetDevice, WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; |
||||
|
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { |
||||
|
singleSwitchDefaultSettings, singleSwitchLayoutImages, |
||||
|
singleSwitchLayouts, |
||||
|
singleSwitchLayoutTranslations |
||||
|
} from '@home/components/widget/lib/rpc/single-switch-widget.models'; |
||||
|
import { ValueType } from '@shared/models/constants'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-single-switch-widget-settings', |
||||
|
templateUrl: './single-switch-widget-settings.component.html', |
||||
|
styleUrls: ['./../widget-settings.scss'] |
||||
|
}) |
||||
|
export class SingleSwitchWidgetSettingsComponent extends WidgetSettingsComponent { |
||||
|
|
||||
|
get targetDevice(): TargetDevice { |
||||
|
return this.widget?.config?.targetDevice; |
||||
|
} |
||||
|
|
||||
|
singleSwitchLayouts = singleSwitchLayouts; |
||||
|
|
||||
|
singleSwitchLayoutTranslationMap = singleSwitchLayoutTranslations; |
||||
|
singleSwitchLayoutImageMap = singleSwitchLayoutImages; |
||||
|
|
||||
|
valueType = ValueType; |
||||
|
|
||||
|
singleSwitchWidgetSettingsForm: UntypedFormGroup; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
private fb: UntypedFormBuilder) { |
||||
|
super(store); |
||||
|
} |
||||
|
|
||||
|
protected settingsForm(): UntypedFormGroup { |
||||
|
return this.singleSwitchWidgetSettingsForm; |
||||
|
} |
||||
|
|
||||
|
protected defaultSettings(): WidgetSettings { |
||||
|
return {...singleSwitchDefaultSettings}; |
||||
|
} |
||||
|
|
||||
|
protected onSettingsSet(settings: WidgetSettings) { |
||||
|
this.singleSwitchWidgetSettingsForm = this.fb.group({ |
||||
|
initialState: [settings.initialState, []], |
||||
|
onUpdateState: [settings.onUpdateState, []], |
||||
|
offUpdateState: [settings.offUpdateState, []], |
||||
|
layout: [settings.layout, []], |
||||
|
autoScale: [settings.autoScale, []], |
||||
|
|
||||
|
showLabel: [settings.showLabel, []], |
||||
|
label: [settings.label, []], |
||||
|
labelFont: [settings.labelFont, []], |
||||
|
labelColor: [settings.labelColor, []], |
||||
|
|
||||
|
showIcon: [settings.showIcon, []], |
||||
|
iconSize: [settings.iconSize, [Validators.min(0)]], |
||||
|
iconSizeUnit: [settings.iconSizeUnit, []], |
||||
|
icon: [settings.icon, []], |
||||
|
iconColor: [settings.iconColor, []], |
||||
|
|
||||
|
switchColorOn: [settings.switchColorOn, []], |
||||
|
switchColorOff: [settings.switchColorOff, []], |
||||
|
switchColorDisabled: [settings.switchColorDisabled, []], |
||||
|
|
||||
|
tumblerColorOn: [settings.tumblerColorOn, []], |
||||
|
tumblerColorOff: [settings.tumblerColorOff, []], |
||||
|
tumblerColorDisabled: [settings.tumblerColorDisabled, []], |
||||
|
|
||||
|
showOnLabel: [settings.showOnLabel, []], |
||||
|
onLabel: [settings.onLabel, []], |
||||
|
onLabelFont: [settings.onLabelFont, []], |
||||
|
onLabelColor: [settings.onLabelColor, []], |
||||
|
|
||||
|
showOffLabel: [settings.showOffLabel, []], |
||||
|
offLabel: [settings.offLabel, []], |
||||
|
offLabelFont: [settings.offLabelFont, []], |
||||
|
offLabelColor: [settings.offLabelColor, []], |
||||
|
|
||||
|
background: [settings.background, []] |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
protected validatorTriggers(): string[] { |
||||
|
return ['showLabel', 'showIcon', 'showOnLabel', 'showOffLabel']; |
||||
|
} |
||||
|
|
||||
|
protected updateValidators(emitEvent: boolean): void { |
||||
|
const showLabel: boolean = this.singleSwitchWidgetSettingsForm.get('showLabel').value; |
||||
|
const showIcon: boolean = this.singleSwitchWidgetSettingsForm.get('showIcon').value; |
||||
|
const showOnLabel: boolean = this.singleSwitchWidgetSettingsForm.get('showOnLabel').value; |
||||
|
const showOffLabel: boolean = this.singleSwitchWidgetSettingsForm.get('showOffLabel').value; |
||||
|
|
||||
|
if (showLabel) { |
||||
|
this.singleSwitchWidgetSettingsForm.get('label').enable(); |
||||
|
this.singleSwitchWidgetSettingsForm.get('labelFont').enable(); |
||||
|
this.singleSwitchWidgetSettingsForm.get('labelColor').enable(); |
||||
|
} else { |
||||
|
this.singleSwitchWidgetSettingsForm.get('label').disable(); |
||||
|
this.singleSwitchWidgetSettingsForm.get('labelFont').disable(); |
||||
|
this.singleSwitchWidgetSettingsForm.get('labelColor').disable(); |
||||
|
} |
||||
|
|
||||
|
if (showIcon) { |
||||
|
this.singleSwitchWidgetSettingsForm.get('iconSize').enable(); |
||||
|
this.singleSwitchWidgetSettingsForm.get('iconSizeUnit').enable(); |
||||
|
this.singleSwitchWidgetSettingsForm.get('icon').enable(); |
||||
|
this.singleSwitchWidgetSettingsForm.get('iconColor').enable(); |
||||
|
} else { |
||||
|
this.singleSwitchWidgetSettingsForm.get('iconSize').disable(); |
||||
|
this.singleSwitchWidgetSettingsForm.get('iconSizeUnit').disable(); |
||||
|
this.singleSwitchWidgetSettingsForm.get('icon').disable(); |
||||
|
this.singleSwitchWidgetSettingsForm.get('iconColor').disable(); |
||||
|
} |
||||
|
|
||||
|
if (showOnLabel) { |
||||
|
this.singleSwitchWidgetSettingsForm.get('onLabel').enable(); |
||||
|
this.singleSwitchWidgetSettingsForm.get('onLabelFont').enable(); |
||||
|
this.singleSwitchWidgetSettingsForm.get('onLabelColor').enable(); |
||||
|
} else { |
||||
|
this.singleSwitchWidgetSettingsForm.get('onLabel').disable(); |
||||
|
this.singleSwitchWidgetSettingsForm.get('onLabelFont').disable(); |
||||
|
this.singleSwitchWidgetSettingsForm.get('onLabelColor').disable(); |
||||
|
} |
||||
|
|
||||
|
if (showOffLabel) { |
||||
|
this.singleSwitchWidgetSettingsForm.get('offLabel').enable(); |
||||
|
this.singleSwitchWidgetSettingsForm.get('offLabelFont').enable(); |
||||
|
this.singleSwitchWidgetSettingsForm.get('offLabelColor').enable(); |
||||
|
} else { |
||||
|
this.singleSwitchWidgetSettingsForm.get('offLabel').disable(); |
||||
|
this.singleSwitchWidgetSettingsForm.get('offLabelFont').disable(); |
||||
|
this.singleSwitchWidgetSettingsForm.get('offLabelColor').disable(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected readonly ValueType = ValueType; |
||||
|
} |
||||
@ -0,0 +1,121 @@ |
|||||
|
///
|
||||
|
/// 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 { AttributeScope } from '@shared/models/telemetry/telemetry.models'; |
||||
|
import { BackgroundSettings } from '@shared/models/widget-settings.models'; |
||||
|
|
||||
|
export enum RpcInitialStateAction { |
||||
|
DO_NOTHING = 'DO_NOTHING', |
||||
|
EXECUTE_RPC = 'EXECUTE_RPC', |
||||
|
GET_ATTRIBUTE = 'GET_ATTRIBUTE', |
||||
|
GET_TIME_SERIES = 'GET_TIME_SERIES' |
||||
|
} |
||||
|
|
||||
|
export const rpcInitialStateActions = Object.keys(RpcInitialStateAction) as RpcInitialStateAction[]; |
||||
|
|
||||
|
export const rpcInitialStateTranslations = new Map<RpcInitialStateAction, string>( |
||||
|
[ |
||||
|
[RpcInitialStateAction.DO_NOTHING, 'widgets.rpc-state.do-nothing'], |
||||
|
[RpcInitialStateAction.EXECUTE_RPC, 'widgets.rpc-state.execute-rpc'], |
||||
|
[RpcInitialStateAction.GET_ATTRIBUTE, 'widgets.rpc-state.get-attribute'], |
||||
|
[RpcInitialStateAction.GET_TIME_SERIES, 'widgets.rpc-state.get-time-series'] |
||||
|
] |
||||
|
); |
||||
|
|
||||
|
export interface RpcSettings { |
||||
|
method: string; |
||||
|
requestTimeout: number; |
||||
|
requestPersistent: boolean; |
||||
|
persistentPollingInterval: number; |
||||
|
} |
||||
|
|
||||
|
export interface RpcTelemetrySettings { |
||||
|
key: string; |
||||
|
} |
||||
|
|
||||
|
export interface RpcGetAttributeSettings extends RpcTelemetrySettings { |
||||
|
scope: AttributeScope | null; |
||||
|
} |
||||
|
|
||||
|
export interface RpcSetAttributeSettings extends RpcTelemetrySettings { |
||||
|
scope: AttributeScope.SERVER_SCOPE | AttributeScope.SHARED_SCOPE; |
||||
|
} |
||||
|
|
||||
|
export enum RpcDataToStateType { |
||||
|
NONE = 'NONE', |
||||
|
FUNCTION = 'FUNCTION' |
||||
|
} |
||||
|
|
||||
|
export interface RpcDataToStateSettings { |
||||
|
type: RpcDataToStateType; |
||||
|
dataToStateFunction: string; |
||||
|
compareToValue?: any; |
||||
|
} |
||||
|
|
||||
|
export interface RpcInitialStateSettings<V> { |
||||
|
action: RpcInitialStateAction; |
||||
|
defaultValue: V; |
||||
|
executeRpc: RpcSettings; |
||||
|
getAttribute: RpcGetAttributeSettings; |
||||
|
getTimeSeries: RpcTelemetrySettings; |
||||
|
dataToState: RpcDataToStateSettings; |
||||
|
} |
||||
|
|
||||
|
export enum RpcUpdateStateAction { |
||||
|
EXECUTE_RPC = 'EXECUTE_RPC', |
||||
|
SET_ATTRIBUTE = 'SET_ATTRIBUTE', |
||||
|
ADD_TIME_SERIES = 'ADD_TIME_SERIES' |
||||
|
} |
||||
|
|
||||
|
export const rpcUpdateStateActions = Object.keys(RpcUpdateStateAction) as RpcUpdateStateAction[]; |
||||
|
|
||||
|
export const rpcUpdateStateTranslations = new Map<RpcUpdateStateAction, string>( |
||||
|
[ |
||||
|
[RpcUpdateStateAction.EXECUTE_RPC, 'widgets.rpc-state.execute-rpc'], |
||||
|
[RpcUpdateStateAction.SET_ATTRIBUTE, 'widgets.rpc-state.set-attribute'], |
||||
|
[RpcUpdateStateAction.ADD_TIME_SERIES, 'widgets.rpc-state.add-time-series'] |
||||
|
] |
||||
|
); |
||||
|
|
||||
|
export enum RpcStateToParamsType { |
||||
|
CONSTANT = 'CONSTANT', |
||||
|
FUNCTION = 'FUNCTION', |
||||
|
NONE = 'NONE' |
||||
|
} |
||||
|
|
||||
|
export interface RpcStateToParamsSettings { |
||||
|
type: RpcStateToParamsType; |
||||
|
constantValue: any; |
||||
|
stateToParamsFunction: string; |
||||
|
} |
||||
|
|
||||
|
export interface RpcUpdateStateSettings { |
||||
|
action: RpcUpdateStateAction; |
||||
|
executeRpc: RpcSettings; |
||||
|
setAttribute: RpcSetAttributeSettings; |
||||
|
putTimeSeries: RpcTelemetrySettings; |
||||
|
stateToParams: RpcStateToParamsSettings; |
||||
|
} |
||||
|
|
||||
|
export interface RpcStateBehaviourSettings<V> { |
||||
|
initialState: RpcInitialStateSettings<V>; |
||||
|
updateStateByValue: (value: V) => RpcUpdateStateSettings; |
||||
|
} |
||||
|
|
||||
|
export interface RpcStateWidgetSettings<V> { |
||||
|
initialState: RpcInitialStateSettings<V>; |
||||
|
background: BackgroundSettings; |
||||
|
} |
||||
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
Loading…
Reference in new issue