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