122 changed files with 5499 additions and 951 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
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,38 @@ |
|||
<!-- |
|||
|
|||
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" |
|||
[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,626 @@ |
|||
///
|
|||
/// 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, |
|||
AttributeScope, |
|||
LatestTelemetry, |
|||
telemetryTypeTranslationsShort |
|||
} from '@shared/models/telemetry/telemetry.models'; |
|||
import { WidgetContext } from '@home/models/widget-component.models'; |
|||
import { BehaviorSubject, Observable, of, throwError } 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 { |
|||
RpcActionSettings, |
|||
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'; |
|||
import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; |
|||
|
|||
@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.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 initialState(): RpcInitialStateSettings<V>; |
|||
|
|||
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>, |
|||
stateValueType: ValueType) { |
|||
super(); |
|||
this.initialStateGetter = RpcInitialStateGetter.fromSettings(ctx, settings.initialState, stateValueType, callbacks); |
|||
this.stateUpdatersMap = new Map<RpcUpdateStateSettings, RpcStateUpdater<V>>(); |
|||
} |
|||
|
|||
initState() { |
|||
if (this.ctx.defaultSubscription.targetEntityId || 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(this.ctx.translate.instant('widgets.rpc-state.error.target-entity-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 RpcAction { |
|||
|
|||
protected constructor(protected ctx: WidgetContext, |
|||
protected settings: RpcActionSettings) {} |
|||
|
|||
handleError(err: any): Error { |
|||
const reason = parseError(this.ctx, err); |
|||
let errorMessage = this.ctx.translate.instant('widgets.rpc-state.error.failed-to-perform-action', |
|||
{actionLabel: this.settings.actionLabel}); |
|||
if (reason) { |
|||
errorMessage += '<br>' + reason; |
|||
} |
|||
return new Error(errorMessage); |
|||
} |
|||
} |
|||
|
|||
export abstract class RpcInitialStateGetter<V> extends RpcAction { |
|||
|
|||
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>) { |
|||
super(ctx, settings); |
|||
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; |
|||
} |
|||
}), |
|||
catchError(err => { |
|||
throw this.handleError(err); |
|||
}) |
|||
); |
|||
} |
|||
|
|||
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> extends RpcAction { |
|||
|
|||
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) { |
|||
super(ctx, settings); |
|||
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)).pipe( |
|||
catchError(err => { |
|||
throw this.handleError(err); |
|||
}) |
|||
); |
|||
} |
|||
} |
|||
|
|||
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((err) => { |
|||
throw handleRpcError(this.ctx, err); |
|||
}) |
|||
); |
|||
} |
|||
} |
|||
|
|||
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) { |
|||
const err = validateAttributeScope(this.ctx, this.getAttributeSettings.scope); |
|||
if (err) { |
|||
return throwError(() => err); |
|||
} |
|||
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((err) => { |
|||
throw handleRpcError(this.ctx, err); |
|||
}) |
|||
); |
|||
} |
|||
} |
|||
|
|||
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 err = validateAttributeScope(this.ctx, this.setAttributeSettings.scope); |
|||
if (err) { |
|||
return throwError(() => err); |
|||
} |
|||
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'; |
|||
|
|||
const handleRpcError = (ctx: WidgetContext, err: any): Error => { |
|||
let reason: string; |
|||
if (ctx.defaultSubscription.rpcErrorText) { |
|||
reason = ctx.defaultSubscription.rpcErrorText; |
|||
} else { |
|||
reason = parseError(ctx, err); |
|||
} |
|||
return new Error(reason); |
|||
}; |
|||
|
|||
const validateAttributeScope = (ctx: WidgetContext, scope?: AttributeScope): Error | null => { |
|||
if (ctx.defaultSubscription.targetEntityId.entityType !== EntityType.DEVICE && scope && scope !== AttributeScope.SERVER_SCOPE) { |
|||
const scopeStr = ctx.translate.instant(telemetryTypeTranslationsShort.get(scope)); |
|||
const entityType = |
|||
ctx.translate.instant(entityTypeTranslations.get(ctx.defaultSubscription.targetEntityId.entityType).type); |
|||
const errorMessage = |
|||
ctx.translate.instant('widgets.rpc-state.error.invalid-attribute-scope', {scope: scopeStr, entityType}); |
|||
return new Error(errorMessage); |
|||
} else { |
|||
return null; |
|||
} |
|||
}; |
|||
@ -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" [innerHTML]="error | safe: 'html'"></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; |
|||
background-color: #fff2f3; |
|||
box-shadow: -2px 2px 4px 0px rgba(0,0,0,0.2); |
|||
.tb-single-switch-error-text { |
|||
font-size: 12px; |
|||
font-style: normal; |
|||
font-weight: 400; |
|||
line-height: 16px; |
|||
color: rgba(209, 39, 48, 1); |
|||
} |
|||
.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,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 { |
|||
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 { RpcInitialStateSettings, 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 initialState(): RpcInitialStateSettings<boolean> { |
|||
return {...this.settings.initialState, actionLabel: this.ctx.translate.instant('widgets.rpc-state.initial-state')}; |
|||
} |
|||
|
|||
protected getUpdateStateSettingsForValue(value: boolean): RpcUpdateStateSettings { |
|||
const targetSettings = value ? this.settings.onUpdateState : this.settings.offUpdateState; |
|||
return {...targetSettings, actionLabel: this.ctx.translate.instant(value ? 'widgets.rpc-state.turn-on' : 'widgets.rpc-state.turn-off')}; |
|||
} |
|||
|
|||
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; |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue