Merge pull request #10611 from thingsboard/feature/status-widget

Status widget
This commit is contained in:
Igor Kulikov 2024-04-22 15:35:10 +03:00 committed by GitHub
commit e79751d3d2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 1362 additions and 11 deletions

View File

@ -2,15 +2,15 @@
"widgetsBundle": {
"alias": "status_indicators",
"title": "Status indicators",
"image": "data:image/png;base64,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",
"image": "tb-image:c3RhdHVzX2luZGljYXRvcnNfc3lzdGVtX2J1bmRsZV9pbWFnZS5wbmc=:IlN0YXR1cyBpbmRpY2F0b3JzIiBzeXN0ZW0gYnVuZGxlIGltYWdl;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAAAjVBMVEUAAADu7u7u7u7g4OD///9c35Dw8PCt78fz/ffW9+PHx8dw457r+/Hj4+Pl+u6QkJDZ+OWsrKzM9tzf+er4/vuE56xYWFi6urp0dHTV1dXS9uCZ67qCgoKF56tm4Zeenp7C89VmZmZKSkq48c7A89Wj7cFLS0s9PT08PDyF56zi+euP6bN65aWZ67lZWVkXV4nvAAAABHRSTlMA799f7FlksgAABdJJREFUeNrt3I12mjAYBuBt/YAkJCJgCVQEFLX+7v4vbwHGQEnbeQwOXF7XVjjuvDxiCN04fHv5bvaaHy/fRB7Q8gN6znfheESLCT3HFI4HtGiIhlTREA0ZJSQi5ZJ6iKSlV8i7u4qAAYTiUfwEUzVE3qIeQkKWOku+4zvixEuydZcsVA2Rt6iGxCR1VhCIx9YJiANhylVD5C2qIUu+5StYsZ9sSVxRQXagGiJvUQvhbgSMc+CMRSYnplgKVUPkLb0ffmOmGiJv0fOIhmjIVTREQ54FMquiGiJv6REy8aocFEPkLdPeILM373dQj5CZVweph8wmvojXZCoWD0ghpGl5u2qhCiEIT5Ak04lSCJ3SWTfIPyiDzKazzJKE+kghZPZG90Y3J+9AVUHoJDNkOc6mCiET+iptSdBUFWSCLGnFAt4UQnxkyIM1REM0RENuhXjShvn4ICCbEHM0QgjCksD4IDSRNeTjgzzNYNcQDflHEJNzgDRIwYwD3h+EcAAeugBOyPuBLAMHyNYMiBuznQnAUrMHSBiEwHaRaFlGZYtr3grJv4CA64AbgpOGHLYMII7cVD2kqOCixQldCCIAJ0rTGyHobHezOHQgTkggEJDIiVk/ELeGkKLFYTdCQJ5rSMxDt9wjwGLH7W+PxBUEmHh2G4Tu7W5O/gXEXDkrRt7jFUC5151eIOYqfWdkm+5AJCVuqGKMHBsIY+KLmNW3MsRUCmlaXAIQ1cOcMz2PaIiGaMgzQ6gta7DGBwFEu0Ej/J0drHk3Gzo+SCaf2ccHeZrBriEaoiGfQuizQCZY2jBHU4WQA32VQ6gyCOCZJ/1vhSlVCEFT6YlQktOJMgiaTii6Dp1gtVc+THw0k2SCVV5UQyeSzEApBJA/lYTqC88uoyEaoiHtlu/Qc34IxyNaXn6Yveb7i3A8okVHR+fT6DsM/GX0HQZuylPN7BqiIWU05P+CmBGDCJowAvK4d0DUtsghP93A5GAWq8QXA+KUz0hafC/Xln9CCMw7IEpb5JClG5kBC+LQWa4ix4mLimWcxgHZ8jhO3SAgcfwe7cjWCW6E9N9yoRbvVcBTFjrEIakTFBVxGBGxaO6cpSsSQP24BdJfixyyIksSkNAtK1Z8VVYUa7gDK0JckSV5hy27A6K4RQ5hnAABnsYRi5hYMKPq6jvOouInEyHuFqKIALkR0n+L2VlVXwsnDQ9dFYffr1v0PNJEQzREQzREQzSkjI9F6PghuVEGK4Egf51v5kU21hqjh0JeK4h1N8TPj7ZxmWRv4ZFB8CYx5EmO2WggyGoUUsucjgFC58bX2eOhQySMxLbtpEuhg4ZYSVuw2KwP9fYi7FmLC8+cDhaC7ZZig5HsINB6iZ0NFJK3LprD8FEO56TZKUgtBPlY5E4I2v/ZGRaCz0Iz+89OoUohdjX87oJQW/o2y4OsP2isEIKNKndBascJX5+jFNfLW5aH0YX7XEvQsCC+UWVzMbL3RjunuQdNst9DJRsWBFUfefzFSUoyx9cfRjwsCFgXQxfvO4buQRcVH6/z0MYIeFaO6ve6YUgptPk7GAYHaZInl9ttn2z7krJBUGe4kPbuOG3WtF6NrUV7pwwe4tvNvEg/ngmTTBUEW0WoaghNPj+Raii5Ggj9ve9VQ46SsxQ5xVYBaTa+H8iJwsehxzFAMkPk3NodyMsty8q9Ns0qG4YNgWxxyqEOtezmQDXH7RdZMHBIK/6+cy9FaDIaCNoY3czp6CDUlp+e+CODtB2JnbQW/HFBvKt/g8B5LdvcD0G5JZI9BLKu58XOTGjdD5kbZbxHQFCx1QvaHf42vRfSbONjBvvmnHcnmfOGwsggH2a8ELS2MvoEkPIXFNsbPaSeUPyxQ7J6+hg75GxUWYwdkhtVjmOHHIwq2dghYFVDZPRHLYDDcXH0nmAe6WFmt1qfVvTabC9Kyuf2xYuGDAFvLYKhE7ougtovGv9FNc9zmZOGaIiG3Bl9h4EB3mHgF3NeY+W3xB1xAAAAAElFTkSuQmCC",
"description": "Contains widgets displaying battery level and signal strength.",
"order": 9000,
"externalId": null,
"name": "Status indicators"
},
"widgetTypeFqns": [
"battery_level",
"signal_strength",
"progress_bar"
"progress_bar",
"status_widget"
]
}

File diff suppressed because one or more lines are too long

View File

@ -114,6 +114,9 @@ import { ComparisonKeyRowComponent } from '@home/components/widget/config/basic/
import {
ComparisonKeysTableComponent
} from '@home/components/widget/config/basic/chart/comparison-keys-table.component';
import {
StatusWidgetBasicConfigComponent
} from '@home/components/widget/config/basic/indicator/status-widget-basic-config.component';
@NgModule({
declarations: [
@ -151,7 +154,8 @@ import {
ToggleButtonBasicConfigComponent,
TimeSeriesChartBasicConfigComponent,
ComparisonKeyRowComponent,
ComparisonKeysTableComponent
ComparisonKeysTableComponent,
StatusWidgetBasicConfigComponent
],
imports: [
CommonModule,
@ -191,7 +195,8 @@ import {
PowerButtonBasicConfigComponent,
SliderBasicConfigComponent,
ToggleButtonBasicConfigComponent,
TimeSeriesChartBasicConfigComponent
TimeSeriesChartBasicConfigComponent,
StatusWidgetBasicConfigComponent
]
})
export class BasicWidgetConfigModule {
@ -225,5 +230,6 @@ export const basicWidgetConfigComponentsMap: {[key: string]: Type<IBasicWidgetCo
'tb-power-button-basic-config': PowerButtonBasicConfigComponent,
'tb-slider-basic-config': SliderBasicConfigComponent,
'tb-toggle-button-basic-config': ToggleButtonBasicConfigComponent,
'tb-time-series-chart-basic-config': TimeSeriesChartBasicConfigComponent
'tb-time-series-chart-basic-config': TimeSeriesChartBasicConfigComponent,
'tb-status-widget-basic-config': StatusWidgetBasicConfigComponent
};

View File

@ -0,0 +1,101 @@
<!--
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]="statusWidgetConfigForm">
<tb-target-device formControlName="targetDevice"></tb-target-device>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.status-widget.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-get-value-action-settings fxFlex
panelTitle="widgets.rpc-state.initial-state"
[valueType]="valueType.BOOLEAN"
trueLabel="widgets.rpc-state.on"
falseLabel="widgets.rpc-state.off"
stateLabel="widgets.rpc-state.on"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType"
formControlName="initialState"></tb-get-value-action-settings>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.rpc-state.disabled-state-hint' | translate}}" translate>widgets.rpc-state.disabled-state</div>
<tb-get-value-action-settings fxFlex
panelTitle="widgets.rpc-state.disabled-state"
[valueType]="valueType.BOOLEAN"
stateLabel="widgets.rpc-state.disabled"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType"
formControlName="disabledState"></tb-get-value-action-settings>
</div>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.appearance</div>
<tb-image-cards-select rowHeight="1:1"
[cols]="{columns: 3,
breakpoints: {
'lt-sm': 1,
'lt-md': 2
}}"
label="{{ 'widgets.status-widget.layout' | translate }}" formControlName="layout">
<tb-image-cards-select-option *ngFor="let layout of statusWidgetLayouts"
[value]="layout"
[image]="statusWidgetLayoutImageMap.get(layout)">
{{ statusWidgetLayoutTranslationMap.get(layout) | translate }}
</tb-image-cards-select-option>
</tb-image-cards-select>
</div>
<div class="tb-form-panel">
<div fxLayout="row" fxLayoutAlign="space-between center">
<div class="tb-form-panel-title" translate>widget-config.card-style</div>
<tb-toggle-select [(ngModel)]="cardStyleMode"
[ngModelOptions]="{ standalone: true }">
<tb-toggle-option value="on">{{ 'widgets.status-widget.on' | translate }}</tb-toggle-option>
<tb-toggle-option value="off">{{ 'widgets.status-widget.off' | translate }}</tb-toggle-option>
</tb-toggle-select>
</div>
<tb-status-widget-state-settings
*ngIf="cardStyleMode === 'on'"
[layout]="statusWidgetConfigForm.get('layout').value"
formControlName="onState">
</tb-status-widget-state-settings>
<tb-status-widget-state-settings
*ngIf="cardStyleMode === 'off'"
[layout]="statusWidgetConfigForm.get('layout').value"
formControlName="offState">
</tb-status-widget-state-settings>
</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 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>

View File

@ -0,0 +1,119 @@
///
/// 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 } 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 { ValueType } from '@shared/models/constants';
import {
statusWidgetDefaultSettings,
statusWidgetLayoutImages,
statusWidgetLayouts,
statusWidgetLayoutTranslations,
StatusWidgetSettings
} from '@home/components/widget/lib/indicator/status-widget.models';
@Component({
selector: 'tb-status-widget-basic-config',
templateUrl: './status-widget-basic-config.component.html',
styleUrls: ['../basic-config.scss']
})
export class StatusWidgetBasicConfigComponent extends BasicWidgetConfigComponent {
get targetDevice(): TargetDevice {
return this.statusWidgetConfigForm.get('targetDevice').value;
}
statusWidgetLayouts = statusWidgetLayouts;
statusWidgetLayoutTranslationMap = statusWidgetLayoutTranslations;
statusWidgetLayoutImageMap = statusWidgetLayoutImages;
valueType = ValueType;
statusWidgetConfigForm: UntypedFormGroup;
cardStyleMode = 'on';
constructor(protected store: Store<AppState>,
protected widgetConfigComponent: WidgetConfigComponent,
private fb: UntypedFormBuilder) {
super(store, widgetConfigComponent);
}
protected configForm(): UntypedFormGroup {
return this.statusWidgetConfigForm;
}
protected onConfigSet(configData: WidgetConfigComponentData) {
const settings: StatusWidgetSettings = {...statusWidgetDefaultSettings, ...(configData.config.settings || {})};
this.statusWidgetConfigForm = this.fb.group({
targetDevice: [configData.config.targetDevice, []],
initialState: [settings.initialState, []],
disabledState: [settings.disabledState, []],
layout: [settings.layout, []],
onState: [settings.onState, []],
offState: [settings.offState, []],
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.disabledState = config.disabledState;
this.widgetConfig.config.settings.layout = config.layout;
this.widgetConfig.config.settings.onState = config.onState;
this.widgetConfig.config.settings.offState = config.offState;
this.setCardButtons(config.cardButtons, this.widgetConfig.config);
this.widgetConfig.config.borderRadius = config.borderRadius;
this.widgetConfig.config.actions = config.actions;
return this.widgetConfig;
}
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');
}
}

View File

@ -0,0 +1,33 @@
<!--
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 #statusWidgetPanel class="tb-status-widget-panel" [style]="backgroundStyle$ | async">
<div class="tb-status-widget-overlay" [style]="overlayStyle" [style.inset]="overlayInset"></div>
<div class="tb-status-widget-title-panel">
<ng-container *ngTemplateOutlet="widgetTitlePanel"></ng-container>
</div>
<div #statusWidgetContent class="tb-status-widget-content" [class]="this.layout">
<div class="tb-status-widget-icon-container">
<tb-icon [style]="iconStyle">{{ icon }}</tb-icon>
</div>
<div class="tb-status-widget-labels-container">
<div *ngIf="showLabel" class="tb-status-widget-label" [style]="labelStyle">{{ label$ | async }}</div>
<div *ngIf="showStatus" class="tb-status-widget-status" [style]="statusStyle">{{ status$ | async }}</div>
</div>
</div>
<mat-progress-bar class="tb-action-widget-progress" style="height: 4px;" color="accent" mode="indeterminate" *ngIf="loading$ | async"></mat-progress-bar>
</div>

View File

@ -0,0 +1,99 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.tb-status-widget-panel {
width: 100%;
height: 100%;
position: relative;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
> div:not(.tb-status-widget-overlay), > tb-icon {
z-index: 1;
}
.tb-status-widget-overlay {
position: absolute;
inset: 12px;
}
> div.tb-status-widget-title-panel {
position: absolute;
top: 12px;
left: 12px;
right: 12px;
z-index: 2;
}
.tb-status-widget-content {
width: 100%;
height: 100%;
padding: 16px;
position: relative;
display: flex;
flex-direction: column;
.tb-status-widget-icon-container {
display: flex;
width: 100%;
flex-direction: column;
}
.tb-status-widget-labels-container {
display: flex;
width: 100%;
flex-direction: column;
.tb-status-widget-label {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
.tb-status-widget-status {
text-transform: uppercase;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
&.default {
place-content: flex-start space-between;
align-items: flex-start;
}
&.center {
place-content: center flex-start;
align-items: center;
.tb-status-widget-icon-container {
flex: 1;
place-content: center;
align-items: center;
}
.tb-status-widget-labels-container {
flex-direction: column-reverse;
place-content: center flex-start;
align-items: center;
}
}
&.icon {
place-content: center;
align-items: center;
.tb-status-widget-icon-container {
flex: 1;
place-content: center;
align-items: center;
}
.tb-status-widget-labels-container {
display: none;
}
}
}
}

View File

@ -0,0 +1,242 @@
///
/// 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 { BasicActionWidgetComponent } from '@home/components/widget/lib/action/action-widget.models';
import {
statusWidgetDefaultSettings,
StatusWidgetLayout,
StatusWidgetSettings, StatusWidgetStateSettings
} from '@home/components/widget/lib/indicator/status-widget.models';
import { Observable } from 'rxjs';
import {
backgroundStyle,
ComponentStyle,
iconStyle,
overlayStyle,
textStyle
} from '@shared/models/widget-settings.models';
import { ResizeObserver } from '@juggle/resize-observer';
import { ImagePipe } from '@shared/pipe/image.pipe';
import { DomSanitizer } from '@angular/platform-browser';
import { UtilsService } from '@core/services/utils.service';
import { ValueType } from '@shared/models/constants';
const initialStatusWidgetSize = 147;
@Component({
selector: 'tb-status-widget',
templateUrl: './status-widget.component.html',
styleUrls: ['../action/action-widget.scss', './status-widget.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class StatusWidgetComponent extends
BasicActionWidgetComponent implements OnInit, AfterViewInit, OnDestroy {
@ViewChild('statusWidgetPanel', {static: false})
statusWidgetPanel: ElementRef<HTMLElement>;
@ViewChild('statusWidgetContent', {static: false})
statusWidgetContent: ElementRef<HTMLElement>;
settings: StatusWidgetSettings;
backgroundStyle$: Observable<ComponentStyle>;
overlayStyle: ComponentStyle = {};
overlayInset = '12px';
borderRadius = '';
layout: StatusWidgetLayout;
showLabel = true;
label$: Observable<string>;
labelStyle: ComponentStyle = {};
showStatus = true;
status$: Observable<string>;
statusStyle: ComponentStyle = {};
icon = '';
iconStyle: ComponentStyle = {};
private panelResize$: ResizeObserver;
private onLabel$: Observable<string>;
private onStatus$: Observable<string>;
private onBackground$: Observable<ComponentStyle>;
private onBackgroundDisabled$: Observable<ComponentStyle>;
private offLabel$: Observable<string>;
private offStatus$: Observable<string>;
private offBackground$: Observable<ComponentStyle>;
private offBackgroundDisabled$: Observable<ComponentStyle>;
private state = false;
private disabled = false;
private disabledState = false;
constructor(protected imagePipe: ImagePipe,
protected sanitizer: DomSanitizer,
private renderer: Renderer2,
private utils: UtilsService,
protected cd: ChangeDetectorRef,
private elementRef: ElementRef) {
super(cd);
}
ngOnInit(): void {
super.ngOnInit();
this.settings = {...statusWidgetDefaultSettings, ...this.ctx.settings};
this.layout = this.settings.layout;
this.onLabel$ = this.ctx.registerLabelPattern(this.settings.onState.label, this.onLabel$);
this.onStatus$ = this.ctx.registerLabelPattern(this.settings.onState.status, this.onStatus$);
this.onBackground$ = backgroundStyle(this.settings.onState.background, this.imagePipe, this.sanitizer);
this.onBackgroundDisabled$ = backgroundStyle(this.settings.onState.backgroundDisabled, this.imagePipe, this.sanitizer);
this.offLabel$ = this.ctx.registerLabelPattern(this.settings.offState.label, this.offLabel$);
this.offStatus$ = this.ctx.registerLabelPattern(this.settings.offState.status, this.offStatus$);
this.offBackground$ = backgroundStyle(this.settings.offState.background, this.imagePipe, this.sanitizer);
this.offBackgroundDisabled$ = backgroundStyle(this.settings.offState.backgroundDisabled, this.imagePipe, this.sanitizer);
const getInitialStateSettings =
{...this.settings.initialState, actionLabel: this.ctx.translate.instant('widgets.rpc-state.initial-state')};
this.createValueGetter(getInitialStateSettings, ValueType.BOOLEAN, {
next: (value) => this.onState(value)
});
const disabledStateSettings =
{...this.settings.disabledState, actionLabel: this.ctx.translate.instant('widgets.rpc-state.disabled-state')};
this.createValueGetter(disabledStateSettings, ValueType.BOOLEAN, {
next: (value) => this.onDisabled(value)
});
this.loading$.subscribe((loading) => {
this.updateDisabledState(loading || this.disabled);
});
this.updateStyle(this.state, this.disabled);
}
ngAfterViewInit(): void {
this.renderer.setStyle(this.statusWidgetContent.nativeElement, 'overflow', 'visible');
this.renderer.setStyle(this.statusWidgetContent.nativeElement, 'position', 'absolute');
this.panelResize$ = new ResizeObserver(() => {
this.onResize();
});
this.panelResize$.observe(this.statusWidgetPanel.nativeElement);
if (this.showLabel) {
this.panelResize$.observe(this.statusWidgetPanel.nativeElement);
}
this.onResize();
super.ngAfterViewInit();
}
ngOnDestroy() {
if (this.panelResize$) {
this.panelResize$.disconnect();
}
super.ngOnDestroy();
}
public onInit() {
super.onInit();
this.borderRadius = this.ctx.$widgetElement.css('borderRadius');
this.overlayStyle = {...this.overlayStyle, ...{borderRadius: this.borderRadius}};
this.cd.detectChanges();
}
private onState(value: boolean): void {
const newState = !!value;
if (this.state !== newState) {
this.state = newState;
this.updateStyle(this.state, this.disabled || this.disabledState);
}
}
private onDisabled(value: boolean): void {
const newDisabled = !!value;
if (this.disabled !== newDisabled) {
this.disabled = newDisabled;
this.updateDisabledState(this.disabled);
}
}
private updateDisabledState(disabled: boolean) {
this.disabledState = disabled;
this.updateStyle(this.state, this.disabledState);
}
private onResize() {
const panelWidth = this.statusWidgetPanel.nativeElement.getBoundingClientRect().width;
const panelHeight = this.statusWidgetPanel.nativeElement.getBoundingClientRect().height;
const targetSize = Math.min(panelWidth, panelHeight);
const scale = targetSize / initialStatusWidgetSize;
const width = initialStatusWidgetSize;
const height = initialStatusWidgetSize;
this.renderer.setStyle(this.statusWidgetContent.nativeElement, 'width', width + 'px');
this.renderer.setStyle(this.statusWidgetContent.nativeElement, 'height', height + 'px');
this.renderer.setStyle(this.statusWidgetContent.nativeElement, 'transform', `scale(${scale})`);
this.overlayInset = (Math.floor(12 * scale * 100) / 100) + 'px';
this.cd.markForCheck();
}
private updateStyle(state: boolean, disabled: boolean) {
let stateSettings: StatusWidgetStateSettings;
if (state) {
this.label$ = this.onLabel$;
this.status$ = this.onStatus$;
this.backgroundStyle$ = disabled ? this.onBackgroundDisabled$ : this.onBackground$;
stateSettings = this.settings.onState;
} else {
this.label$ = this.offLabel$;
this.status$ = this.offStatus$;
this.backgroundStyle$ = disabled ? this.offBackgroundDisabled$ : this.offBackground$;
stateSettings = this.settings.offState;
}
this.showLabel = stateSettings.showLabel && this.layout !== StatusWidgetLayout.icon;
this.showStatus = stateSettings.showStatus && this.layout !== StatusWidgetLayout.icon;
this.icon = stateSettings.icon;
const primaryColor = disabled ? stateSettings.primaryColorDisabled : stateSettings.primaryColor;
const secondaryColor = disabled ? stateSettings.secondaryColorDisabled : stateSettings.secondaryColor;
this.labelStyle = textStyle(stateSettings.labelFont);
this.labelStyle.color = primaryColor;
this.statusStyle = textStyle(stateSettings.statusFont);
this.statusStyle.color = secondaryColor;
this.iconStyle = iconStyle(stateSettings.iconSize, stateSettings.iconSizeUnit);
this.iconStyle.color = primaryColor;
this.overlayStyle = overlayStyle(disabled ? stateSettings.backgroundDisabled.overlay : stateSettings.background.overlay);
if (this.borderRadius) {
this.overlayStyle = {...this.overlayStyle, ...{borderRadius: this.borderRadius}};
}
this.cd.detectChanges();
}
}

View File

@ -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.
///
import { DataToValueType, GetValueAction, GetValueSettings } from '@shared/models/action-widget-settings.models';
import { BackgroundSettings, BackgroundType, cssUnit, Font } from '@shared/models/widget-settings.models';
export enum StatusWidgetLayout {
default = 'default',
center = 'center',
icon = 'icon'
}
export const statusWidgetLayouts = Object.keys(StatusWidgetLayout) as StatusWidgetLayout[];
export const statusWidgetLayoutTranslations = new Map<StatusWidgetLayout, string>(
[
[StatusWidgetLayout.default, 'widgets.status-widget.layout-default'],
[StatusWidgetLayout.center, 'widgets.status-widget.layout-center'],
[StatusWidgetLayout.icon, 'widgets.status-widget.layout-icon']
]
);
export const statusWidgetLayoutImages = new Map<StatusWidgetLayout, string>(
[
[StatusWidgetLayout.default, 'assets/widget/status-widget/default-layout.svg'],
[StatusWidgetLayout.center, 'assets/widget/status-widget/center-layout.svg'],
[StatusWidgetLayout.icon, 'assets/widget/status-widget/icon-layout.svg']
]
);
export interface StatusWidgetStateSettings {
showLabel: boolean;
label: string;
labelFont: Font;
showStatus: boolean;
status: string;
statusFont: Font;
icon: string;
iconSize: number;
iconSizeUnit: cssUnit;
primaryColor: string;
secondaryColor: string;
background: BackgroundSettings;
primaryColorDisabled: string;
secondaryColorDisabled: string;
backgroundDisabled: BackgroundSettings;
}
export interface StatusWidgetSettings {
initialState: GetValueSettings<boolean>;
disabledState: GetValueSettings<boolean>;
layout: StatusWidgetLayout;
onState: StatusWidgetStateSettings;
offState: StatusWidgetStateSettings;
}
export const statusWidgetDefaultSettings: StatusWidgetSettings = {
initialState: {
action: GetValueAction.EXECUTE_RPC,
defaultValue: false,
executeRpc: {
method: 'getState',
requestTimeout: 5000,
requestPersistent: false,
persistentPollingInterval: 1000
},
getAttribute: {
key: 'state',
scope: null
},
getTimeSeries: {
key: 'state'
},
dataToValue: {
type: DataToValueType.NONE,
compareToValue: true,
dataToValueFunction: '/* Should return boolean value */\nreturn data;'
}
},
disabledState: {
action: GetValueAction.DO_NOTHING,
defaultValue: false,
getAttribute: {
key: 'state',
scope: null
},
getTimeSeries: {
key: 'state'
},
dataToValue: {
type: DataToValueType.NONE,
compareToValue: true,
dataToValueFunction: '/* Should return boolean value */\nreturn data;'
}
},
layout: StatusWidgetLayout.default,
onState: {
showLabel: true,
label: 'Window left corner',
labelFont: {
family: 'Roboto',
size: 12,
sizeUnit: 'px',
style: 'normal',
weight: '500',
lineHeight: '16px'
},
showStatus: true,
status: 'Opened',
statusFont: {
family: 'Roboto',
size: 10,
sizeUnit: 'px',
style: 'normal',
weight: '500',
lineHeight: '20px'
},
icon: 'mdi:curtains',
iconSize: 32,
iconSizeUnit: 'px',
primaryColor: '#fff',
secondaryColor: 'rgba(255, 255, 255, 0.80)',
background: {
type: BackgroundType.color,
color: '#3F52DD',
overlay: {
enabled: false,
color: 'rgba(255,255,255,0.72)',
blur: 3
}
},
primaryColorDisabled: 'rgba(0, 0, 0, 0.38)',
secondaryColorDisabled: 'rgba(0, 0, 0, 0.38)',
backgroundDisabled: {
type: BackgroundType.color,
color: '#CACACA',
overlay: {
enabled: false,
color: 'rgba(255,255,255,0.72)',
blur: 3
}
}
},
offState: {
showLabel: true,
label: 'Window left corner',
labelFont: {
family: 'Roboto',
size: 12,
sizeUnit: 'px',
style: 'normal',
weight: '500',
lineHeight: '16px'
},
showStatus: true,
status: 'Closed',
statusFont: {
family: 'Roboto',
size: 10,
sizeUnit: 'px',
style: 'normal',
weight: '500',
lineHeight: '20px'
},
icon: 'mdi:curtains-closed',
iconSize: 32,
iconSizeUnit: 'px',
primaryColor: 'rgba(0, 0, 0, 0.87)',
secondaryColor: 'rgba(0, 0, 0, 0.54)',
background: {
type: BackgroundType.color,
color: '#FFF',
overlay: {
enabled: false,
color: 'rgba(255,255,255,0.72)',
blur: 3
}
},
primaryColorDisabled: 'rgba(0, 0, 0, 0.38)',
secondaryColorDisabled: 'rgba(0, 0, 0, 0.38)',
backgroundDisabled: {
type: BackgroundType.color,
color: '#CACACA',
overlay: {
enabled: false,
color: 'rgba(255,255,255,0.72)',
blur: 3
}
}
}
};

View File

@ -0,0 +1,111 @@
<!--
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]="stateSettingsFormGroup">
<div *ngIf="layout !== StatusWidgetLayout.icon" class="tb-form-row column-xs">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showLabel">
{{ 'widgets.status-widget.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]="stateSettingsFormGroup.get('label').value">
</tb-font-settings>
</div>
</div>
<div *ngIf="layout !== StatusWidgetLayout.icon" class="tb-form-row column-xs">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showStatus">
{{ 'widgets.status-widget.status' | 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="status" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-font-settings formControlName="statusFont"
[previewText]="stateSettingsFormGroup.get('status').value">
</tb-font-settings>
</div>
</div>
<div class="tb-form-row">
<div class="fixed-title-width">
{{ 'widgets.status-widget.icon' | translate }}
</div>
<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
formControlName="icon">
</tb-material-icon-select>
</div>
</div>
<div class="tb-form-row space-between" [class]="{'column-xs': layout === StatusWidgetLayout.icon, 'column-lt-md': layout !== StatusWidgetLayout.icon}">
<div>{{ 'widgets.status-widget.color-palette' | translate }}</div>
<div fxLayout="row wrap" fxLayoutAlign="start center" fxLayoutAlign.lt-sm="space-between center"
[fxLayoutAlign.lt-md]="layout !== StatusWidgetLayout.icon ? 'space-between center': 'start center'"
style="gap: 12px;">
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<div tb-hint-tooltip-icon="{{'widgets.status-widget.primary-color-hint' | translate}}" translate>widgets.status-widget.primary</div>
<tb-color-input asBoxInput
formControlName="primaryColor">
</tb-color-input>
</div>
<mat-divider *ngIf="layout !== StatusWidgetLayout.icon" vertical fxHide.lt-md></mat-divider>
<div *ngIf="layout !== StatusWidgetLayout.icon" fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<div tb-hint-tooltip-icon="{{'widgets.status-widget.secondary-color-hint' | translate}}" translate>widgets.status-widget.secondary</div>
<tb-color-input asBoxInput
formControlName="secondaryColor">
</tb-color-input>
</div>
<mat-divider vertical fxHide.lt-sm [fxHide.lt-md]="layout !== StatusWidgetLayout.icon"></mat-divider>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<div translate>widgets.status-widget.background</div>
<tb-background-settings formControlName="background">
</tb-background-settings>
</div>
</div>
</div>
<div class="tb-form-row space-between" [class]="{'column-xs': layout === StatusWidgetLayout.icon, 'column-lt-md': layout !== StatusWidgetLayout.icon}">
<div>{{ 'widgets.status-widget.disabled-color-palette' | translate }}</div>
<div fxLayout="row wrap" fxLayoutAlign="start center" fxLayoutAlign.lt-sm="space-between center"
[fxLayoutAlign.lt-md]="layout !== StatusWidgetLayout.icon ? 'space-between center': 'start center'"
style="gap: 12px;">
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<div tb-hint-tooltip-icon="{{'widgets.status-widget.primary-color-hint' | translate}}" translate>widgets.status-widget.primary</div>
<tb-color-input asBoxInput
formControlName="primaryColorDisabled">
</tb-color-input>
</div>
<mat-divider *ngIf="layout !== StatusWidgetLayout.icon" vertical fxHide.lt-md></mat-divider>
<div *ngIf="layout !== StatusWidgetLayout.icon" fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<div tb-hint-tooltip-icon="{{'widgets.status-widget.secondary-color-hint' | translate}}" translate>widgets.status-widget.secondary</div>
<tb-color-input asBoxInput
formControlName="secondaryColorDisabled">
</tb-color-input>
</div>
<mat-divider vertical fxHide.lt-sm [fxHide.lt-md]="layout !== StatusWidgetLayout.icon"></mat-divider>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<div translate>widgets.status-widget.background</div>
<tb-background-settings formControlName="backgroundDisabled">
</tb-background-settings>
</div>
</div>
</div>
</ng-container>

View File

@ -0,0 +1,158 @@
///
/// 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, forwardRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { merge } from 'rxjs';
import {
StatusWidgetLayout,
StatusWidgetStateSettings
} from '@home/components/widget/lib/indicator/status-widget.models';
@Component({
selector: 'tb-status-widget-state-settings',
templateUrl: './status-widget-state-settings.component.html',
styleUrls: ['./../../widget-settings.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => StatusWidgetStateSettingsComponent),
multi: true
}
]
})
export class StatusWidgetStateSettingsComponent implements OnInit, OnChanges, ControlValueAccessor {
StatusWidgetLayout = StatusWidgetLayout;
@Input()
disabled: boolean;
@Input()
layout: StatusWidgetLayout;
private modelValue: StatusWidgetStateSettings;
private propagateChange = null;
public stateSettingsFormGroup: UntypedFormGroup;
constructor(private fb: UntypedFormBuilder) {
}
ngOnInit(): void {
this.stateSettingsFormGroup = this.fb.group({
showLabel: [null, []],
label: [null, []],
labelFont: [null, []],
showStatus: [null, []],
status: [null, []],
statusFont: [null, []],
icon: [null, []],
iconSize: [null, []],
iconSizeUnit: [null, []],
primaryColor: [null, []],
secondaryColor: [null, []],
background: [null, []],
primaryColorDisabled: [null, []],
secondaryColorDisabled: [null, []],
backgroundDisabled: [null, []]
});
this.stateSettingsFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});
merge(this.stateSettingsFormGroup.get('showLabel').valueChanges,
this.stateSettingsFormGroup.get('showStatus').valueChanges)
.subscribe(() => {
this.updateValidators();
});
}
ngOnChanges(changes: SimpleChanges): void {
for (const propName of Object.keys(changes)) {
const change = changes[propName];
if (!change.firstChange && change.currentValue !== change.previousValue) {
if (['layout'].includes(propName)) {
this.updateValidators();
}
}
}
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(_fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (isDisabled) {
this.stateSettingsFormGroup.disable({emitEvent: false});
} else {
this.stateSettingsFormGroup.enable({emitEvent: false});
this.updateValidators();
}
}
writeValue(value: StatusWidgetStateSettings): void {
this.modelValue = value;
this.stateSettingsFormGroup.patchValue(
value, {emitEvent: false}
);
this.updateValidators();
}
private updateValidators() {
if (this.layout === StatusWidgetLayout.icon) {
this.stateSettingsFormGroup.get('showLabel').disable({emitEvent: false});
this.stateSettingsFormGroup.get('label').disable({emitEvent: false});
this.stateSettingsFormGroup.get('labelFont').disable({emitEvent: false});
this.stateSettingsFormGroup.get('showStatus').disable({emitEvent: false});
this.stateSettingsFormGroup.get('status').disable({emitEvent: false});
this.stateSettingsFormGroup.get('statusFont').disable({emitEvent: false});
this.stateSettingsFormGroup.get('secondaryColor').disable({emitEvent: false});
this.stateSettingsFormGroup.get('secondaryColorDisabled').disable({emitEvent: false});
} else {
this.stateSettingsFormGroup.get('showLabel').enable({emitEvent: false});
this.stateSettingsFormGroup.get('showStatus').enable({emitEvent: false});
this.stateSettingsFormGroup.get('secondaryColor').enable({emitEvent: false});
this.stateSettingsFormGroup.get('secondaryColorDisabled').enable({emitEvent: false});
const showLabel: boolean = this.stateSettingsFormGroup.get('showLabel').value;
const showStatus: boolean = this.stateSettingsFormGroup.get('showStatus').value;
if (showLabel) {
this.stateSettingsFormGroup.get('label').enable({emitEvent: false});
this.stateSettingsFormGroup.get('labelFont').enable({emitEvent: false});
} else {
this.stateSettingsFormGroup.get('label').disable({emitEvent: false});
this.stateSettingsFormGroup.get('labelFont').disable({emitEvent: false});
}
if (showStatus) {
this.stateSettingsFormGroup.get('status').enable({emitEvent: false});
this.stateSettingsFormGroup.get('statusFont').enable({emitEvent: false});
} else {
this.stateSettingsFormGroup.get('status').disable({emitEvent: false});
this.stateSettingsFormGroup.get('statusFont').disable({emitEvent: false});
}
}
}
private updateModel() {
this.modelValue = this.stateSettingsFormGroup.getRawValue();
this.propagateChange(this.modelValue);
}
}

View File

@ -145,6 +145,9 @@ import {
import {
TimeSeriesChartGridSettingsComponent
} from '@home/components/widget/lib/settings/common/chart/time-series-chart-grid-settings.component';
import {
StatusWidgetStateSettingsComponent
} from '@home/components/widget/lib/settings/common/indicator/status-widget-state-settings.component';
@NgModule({
declarations: [
@ -198,6 +201,7 @@ import {
TimeSeriesChartStatesPanelComponent,
TimeSeriesChartStateRowComponent,
TimeSeriesChartGridSettingsComponent,
StatusWidgetStateSettingsComponent,
DataKeyInputComponent,
EntityAliasInputComponent
],
@ -257,6 +261,7 @@ import {
TimeSeriesChartStatesPanelComponent,
TimeSeriesChartStateRowComponent,
TimeSeriesChartGridSettingsComponent,
StatusWidgetStateSettingsComponent,
DataKeyInputComponent,
EntityAliasInputComponent
],

View File

@ -0,0 +1,82 @@
<!--
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]="statusWidgetSettingsForm">
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.status-widget.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-get-value-action-settings fxFlex
panelTitle="widgets.rpc-state.initial-state"
[valueType]="valueType.BOOLEAN"
trueLabel="widgets.rpc-state.on"
falseLabel="widgets.rpc-state.off"
stateLabel="widgets.rpc-state.on"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType"
formControlName="initialState"></tb-get-value-action-settings>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.rpc-state.disabled-state-hint' | translate}}" translate>widgets.rpc-state.disabled-state</div>
<tb-get-value-action-settings fxFlex
panelTitle="widgets.rpc-state.disabled-state"
[valueType]="valueType.BOOLEAN"
stateLabel="widgets.rpc-state.disabled"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType"
formControlName="disabledState"></tb-get-value-action-settings>
</div>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.appearance</div>
<tb-image-cards-select rowHeight="1:1"
[cols]="{columns: 3,
breakpoints: {
'lt-sm': 1,
'lt-md': 2
}}"
label="{{ 'widgets.status-widget.layout' | translate }}" formControlName="layout">
<tb-image-cards-select-option *ngFor="let layout of statusWidgetLayouts"
[value]="layout"
[image]="statusWidgetLayoutImageMap.get(layout)">
{{ statusWidgetLayoutTranslationMap.get(layout) | translate }}
</tb-image-cards-select-option>
</tb-image-cards-select>
</div>
<div class="tb-form-panel">
<div fxLayout="row" fxLayoutAlign="space-between center">
<div class="tb-form-panel-title" translate>widget-config.card-style</div>
<tb-toggle-select [(ngModel)]="cardStyleMode"
[ngModelOptions]="{ standalone: true }">
<tb-toggle-option value="on">{{ 'widgets.status-widget.on' | translate }}</tb-toggle-option>
<tb-toggle-option value="off">{{ 'widgets.status-widget.off' | translate }}</tb-toggle-option>
</tb-toggle-select>
</div>
<tb-status-widget-state-settings
*ngIf="cardStyleMode === 'on'"
[layout]="statusWidgetSettingsForm.get('layout').value"
formControlName="onState">
</tb-status-widget-state-settings>
<tb-status-widget-state-settings
*ngIf="cardStyleMode === 'off'"
[layout]="statusWidgetSettingsForm.get('layout').value"
formControlName="offState">
</tb-status-widget-state-settings>
</div>
</ng-container>

View File

@ -0,0 +1,79 @@
///
/// 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, Injector } from '@angular/core';
import { TargetDevice, WidgetSettings, WidgetSettingsComponent, widgetType } from '@shared/models/widget.models';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import {
statusWidgetDefaultSettings,
statusWidgetLayoutImages,
statusWidgetLayouts,
statusWidgetLayoutTranslations
} from '@home/components/widget/lib/indicator/status-widget.models';
import { ValueType } from '@shared/models/constants';
@Component({
selector: 'tb-status-widget-settings',
templateUrl: './status-widget-settings.component.html',
styleUrls: ['./../widget-settings.scss'],
})
export class StatusWidgetSettingsComponent extends WidgetSettingsComponent {
get targetDevice(): TargetDevice {
return this.widgetConfig?.config?.targetDevice;
}
get widgetType(): widgetType {
return this.widgetConfig?.widgetType;
}
statusWidgetLayouts = statusWidgetLayouts;
statusWidgetLayoutTranslationMap = statusWidgetLayoutTranslations;
statusWidgetLayoutImageMap = statusWidgetLayoutImages;
valueType = ValueType;
statusWidgetSettingsForm: UntypedFormGroup;
cardStyleMode = 'on';
constructor(protected store: Store<AppState>,
private $injector: Injector,
private fb: UntypedFormBuilder) {
super(store);
}
protected settingsForm(): UntypedFormGroup {
return this.statusWidgetSettingsForm;
}
protected defaultSettings(): WidgetSettings {
return {...statusWidgetDefaultSettings};
}
protected onSettingsSet(settings: WidgetSettings) {
this.statusWidgetSettingsForm = this.fb.group({
initialState: [settings.initialState, []],
disabledState: [settings.disabledState, []],
layout: [settings.layout, []],
onState: [settings.onState, []],
offState: [settings.offState, []]
});
}
}

View File

@ -342,6 +342,9 @@ import {
import {
TimeSeriesChartWidgetSettingsComponent
} from '@home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component';
import {
StatusWidgetSettingsComponent
} from '@home/components/widget/lib/settings/indicator/status-widget-settings.component';
@NgModule({
declarations: [
@ -464,7 +467,8 @@ import {
TimeSeriesChartKeySettingsComponent,
TimeSeriesChartLineSettingsComponent,
TimeSeriesChartBarSettingsComponent,
TimeSeriesChartWidgetSettingsComponent
TimeSeriesChartWidgetSettingsComponent,
StatusWidgetSettingsComponent
],
imports: [
CommonModule,
@ -592,7 +596,8 @@ import {
TimeSeriesChartKeySettingsComponent,
TimeSeriesChartLineSettingsComponent,
TimeSeriesChartBarSettingsComponent,
TimeSeriesChartWidgetSettingsComponent
TimeSeriesChartWidgetSettingsComponent,
StatusWidgetSettingsComponent
]
})
export class WidgetSettingsModule {
@ -685,5 +690,6 @@ export const widgetSettingsComponentsMap: {[key: string]: Type<IWidgetSettingsCo
'tb-slider-widget-settings': SliderWidgetSettingsComponent,
'tb-toggle-button-widget-settings': ToggleButtonWidgetSettingsComponent,
'tb-time-series-chart-key-settings': TimeSeriesChartKeySettingsComponent,
'tb-time-series-chart-widget-settings': TimeSeriesChartWidgetSettingsComponent
'tb-time-series-chart-widget-settings': TimeSeriesChartWidgetSettingsComponent,
'tb-status-widget-settings': StatusWidgetSettingsComponent
};

View File

@ -86,6 +86,7 @@ import { PowerButtonWidgetComponent } from '@home/components/widget/lib/rpc/powe
import { SliderWidgetComponent } from '@home/components/widget/lib/rpc/slider-widget.component';
import { ToggleButtonWidgetComponent } from '@home/components/widget/lib/button/toggle-button-widget.component';
import { TimeSeriesChartWidgetComponent } from '@home/components/widget/lib/chart/time-series-chart-widget.component';
import { StatusWidgetComponent } from '@home/components/widget/lib/indicator/status-widget.component';
@NgModule({
declarations:
@ -138,7 +139,8 @@ import { TimeSeriesChartWidgetComponent } from '@home/components/widget/lib/char
PowerButtonWidgetComponent,
SliderWidgetComponent,
ToggleButtonWidgetComponent,
TimeSeriesChartWidgetComponent
TimeSeriesChartWidgetComponent,
StatusWidgetComponent
],
imports: [
CommonModule,
@ -195,7 +197,8 @@ import { TimeSeriesChartWidgetComponent } from '@home/components/widget/lib/char
PowerButtonWidgetComponent,
SliderWidgetComponent,
ToggleButtonWidgetComponent,
TimeSeriesChartWidgetComponent
TimeSeriesChartWidgetComponent,
StatusWidgetComponent
],
providers: [
{provide: WIDGET_COMPONENTS_MODULE_TOKEN, useValue: WidgetComponentsModule }

View File

@ -5461,6 +5461,25 @@
"signal-strength-card-style": "Signal strength card style",
"no-signal-rssi-value": "\"No signal\" rssi value"
},
"status-widget": {
"behavior": "Behavior",
"layout": "Layout",
"layout-default": "Default",
"layout-center": "Center",
"layout-icon": "Icon",
"on": "On",
"off": "Off",
"label": "Label",
"status": "Status",
"icon": "Icon",
"color-palette": "Color palette",
"disabled-color-palette": "Disabled color palette",
"primary": "Primary",
"primary-color-hint": "Color of icon and label",
"secondary": "Secondary",
"secondary-color-hint": "Color of status",
"background": "Background"
},
"chart": {
"common-settings": "Common settings",
"enable-stacking-mode": "Enable stacking mode",

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -0,0 +1,18 @@
<svg width="141" height="140" viewBox="0 0 141 140" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d_5952_143941)">
<rect x="8.66675" y="4" width="124" height="124" rx="4" fill="#3F52DD"/>
<path d="M83.0387 55.8776H58.2949V53.6282H83.0387V55.8776ZM59.4196 77.2472H63.9185C63.9185 73.8731 61.6691 71.6236 61.6691 71.6236C68.4174 67.1248 69.5421 57.0023 69.5421 57.0023H59.4196V77.2472ZM81.914 57.0023H71.7915C71.7915 57.0023 72.9162 67.1248 79.6645 71.6236C79.6645 71.6236 77.4151 73.8731 77.4151 77.2472H81.914V57.0023Z" fill="white"/>
</g>
<defs>
<filter id="filter0_d_5952_143941" x="0.666748" y="0" width="140" height="140" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="4"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.04 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_5952_143941"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_5952_143941" result="shape"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB