Browse Source

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

Status widget
pull/10622/head
Igor Kulikov 2 years ago
committed by GitHub
parent
commit
e79751d3d2
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 6
      application/src/main/data/json/system/widget_bundles/status_indicators.json
  2. 26
      application/src/main/data/json/system/widget_types/status_widget.json
  3. 12
      ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts
  4. 101
      ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/status-widget-basic-config.component.html
  5. 119
      ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/status-widget-basic-config.component.ts
  6. 33
      ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.html
  7. 99
      ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.scss
  8. 242
      ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.ts
  9. 204
      ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.models.ts
  10. 111
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/indicator/status-widget-state-settings.component.html
  11. 158
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/indicator/status-widget-state-settings.component.ts
  12. 5
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts
  13. 82
      ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/status-widget-settings.component.html
  14. 79
      ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/status-widget-settings.component.ts
  15. 12
      ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts
  16. 7
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  17. 19
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  18. 20
      ui-ngx/src/assets/widget/status-widget/center-layout.svg
  19. 20
      ui-ngx/src/assets/widget/status-widget/default-layout.svg
  20. 18
      ui-ngx/src/assets/widget/status-widget/icon-layout.svg

6
application/src/main/data/json/system/widget_bundles/status_indicators.json

@ -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,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",
"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"
]
}

26
application/src/main/data/json/system/widget_types/status_widget.json

File diff suppressed because one or more lines are too long

12
ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts

@ -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
};

101
ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/status-widget-basic-config.component.html

@ -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>

119
ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/status-widget-basic-config.component.ts

@ -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');
}
}

33
ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.html

@ -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>

99
ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.scss

@ -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;
}
}
}
}

242
ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.ts

@ -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();
}
}

204
ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.models.ts

@ -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
}
}
}
};

111
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/indicator/status-widget-state-settings.component.html

@ -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>

158
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/indicator/status-widget-state-settings.component.ts

@ -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);
}
}

5
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts

@ -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
],

82
ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/status-widget-settings.component.html

@ -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>

79
ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/status-widget-settings.component.ts

@ -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, []]
});
}
}

12
ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts

@ -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
};

7
ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts

@ -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 }

19
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -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",

20
ui-ngx/src/assets/widget/status-widget/center-layout.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB

20
ui-ngx/src/assets/widget/status-widget/default-layout.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB

18
ui-ngx/src/assets/widget/status-widget/icon-layout.svg

@ -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

Loading…
Cancel
Save