Browse Source

UI: SCADA symbol: Implement behavior settings. Improve iot svg widget initialization.

pull/11391/head
Igor Kulikov 2 years ago
parent
commit
2e4e580036
  1. 12
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.component.html
  2. 10
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.component.ts
  3. 4
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.models.ts
  4. 96
      ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.component.ts
  5. 160
      ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts
  6. 138
      ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component.html
  7. 67
      ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component.scss
  8. 148
      ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component.ts
  9. 3
      ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.html
  10. 106
      ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.ts
  11. 10
      ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component.ts
  12. 6
      ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-components.module.ts
  13. 2
      ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.html
  14. 37
      ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.models.ts
  15. 26
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  16. 589
      ui-ngx/src/assets/widget/svg/drawing.svg

12
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.component.html

@ -60,35 +60,35 @@
<div fxLayout="row" fxFlex fxLayoutAlign="end center" fxLayoutGap="8px">
<ng-container *ngFor="let property of propertyRow.properties">
<div *ngIf="property.subLabel" class="tb-small-label">{{ property.subLabel | customTranslate }}</div>
<mat-form-field *ngIf="property.type === 'string'" [class]="property.fieldClass" appearance="outline" subscriptSizing="dynamic">
<mat-form-field *ngIf="property.type === IotSvgPropertyType.text" [class]="property.fieldClass" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="{{ property.id }}" [required]="property.required" placeholder="{{ 'widget-config.set' | translate }}">
<div fxHide.lt-md matSuffix *ngIf="property.fieldSuffix">{{ property.fieldSuffix | customTranslate }}</div>
</mat-form-field>
<tb-color-input *ngIf="property.type === 'color'"
<tb-color-input *ngIf="property.type === IotSvgPropertyType.color"
[required]="property.required"
[class]="property.fieldClass"
asBoxInput
colorClearButton
formControlName="{{ property.id }}">
</tb-color-input>
<tb-color-settings *ngIf="property.type === 'color-settings'"
<tb-color-settings *ngIf="property.type === IotSvgPropertyType.color_settings"
[class]="property.fieldClass"
formControlName="{{ property.id }}"
settingsKey="{{ property.name | customTranslate }}">
</tb-color-settings>
<mat-form-field *ngIf="property.type === 'number'" [class]="property.fieldClass" appearance="outline" class="number" subscriptSizing="dynamic">
<mat-form-field *ngIf="property.type === IotSvgPropertyType.number" [class]="property.fieldClass" appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="{{ property.id }}" [required]="property.required"
[min]="property.min" [max]="property.max" [step]="property.step"
type="number" placeholder="{{ 'widget-config.set' | translate }}">
<div fxHide.lt-md matSuffix *ngIf="property.fieldSuffix">{{ property.fieldSuffix | customTranslate }}</div>
</mat-form-field>
<tb-font-settings *ngIf="property.type === 'font'"
<tb-font-settings *ngIf="property.type === IotSvgPropertyType.font"
[class]="property.fieldClass"
formControlName="{{ property.id }}"
clearButton
disabledLineHeight>
</tb-font-settings>
<tb-unit-input *ngIf="property.type === 'units'"
<tb-unit-input *ngIf="property.type === IotSvgPropertyType.units"
[class]="property.fieldClass"
[required]="property.required"
formControlName="{{ property.id }}"></tb-unit-input>

10
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.component.ts

@ -30,13 +30,13 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import {
defaultIotSvgObjectSettings,
parseIotSvgMetadataFromContent,
IotSvgBehaviorType,
IotSvgMetadata,
IotSvgObjectSettings
IotSvgObjectSettings,
IotSvgPropertyType,
parseIotSvgMetadataFromContent
} from '@home/components/widget/lib/svg/iot-svg.models';
import { HttpClient } from '@angular/common/http';
import { ValueType } from '@shared/models/constants';
import { IAliasController } from '@core/api/widget-api.models';
import { TargetDevice, widgetType } from '@shared/models/widget.models';
import { isDefinedAndNotNull, mergeDeep } from '@core/utils';
@ -69,6 +69,8 @@ export class IotSvgObjectSettingsComponent implements OnInit, OnChanges, Control
IotSvgBehaviorType = IotSvgBehaviorType;
IotSvgPropertyType = IotSvgPropertyType;
@Input()
disabled: boolean;
@ -205,7 +207,7 @@ export class IotSvgObjectSettingsComponent implements OnInit, OnChanges, Control
if (property.required) {
validators.push(Validators.required);
}
if (property.type === 'number') {
if (property.type === IotSvgPropertyType.number) {
if (isDefinedAndNotNull(property.min)) {
validators.push(Validators.min(property.min));
}

4
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.models.ts

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { IotSvgProperty } from '@home/components/widget/lib/svg/iot-svg.models';
import { IotSvgProperty, IotSvgPropertyType } from '@home/components/widget/lib/svg/iot-svg.models';
export interface IotSvgPropertyRow {
label: string;
@ -35,7 +35,7 @@ export const toPropertyRows = (properties: IotSvgProperty[]): IotSvgPropertyRow[
};
result.push(propertyRow);
}
if (property.type === 'switch') {
if (property.type === IotSvgPropertyType.switch) {
propertyRow.switch = property;
} else {
propertyRow.properties.push(property);

96
ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.component.ts

@ -19,18 +19,18 @@ import {
ChangeDetectorRef,
Component,
ElementRef,
Input,
OnDestroy,
OnInit,
Renderer2,
TemplateRef,
ViewChild,
ViewEncapsulation
} from '@angular/core';
import { BasicActionWidgetComponent } from '@home/components/widget/lib/action/action-widget.models';
import { ImagePipe } from '@shared/pipe/image.pipe';
import { DomSanitizer } from '@angular/platform-browser';
import { HttpClient } from '@angular/common/http';
import { IotSvgObject } from '@home/components/widget/lib/svg/iot-svg.models';
import { ResizeObserver } from '@juggle/resize-observer';
import { IotSvgObject, IotSvgObjectCallbacks } from '@home/components/widget/lib/svg/iot-svg.models';
import {
iotSvgWidgetDefaultSettings,
IotSvgWidgetSettings
@ -40,6 +40,7 @@ import { backgroundStyle, ComponentStyle, overlayStyle } from '@shared/models/wi
import { ImageService } from '@core/http/image.service';
import { WidgetComponent } from '@home/components/widget/widget.component';
import { isDefinedAndNotNull, mergeDeep } from '@core/utils';
import { WidgetContext } from '@home/models/widget-component.models';
@Component({
selector: 'tb-iot-svg-widget',
@ -47,110 +48,79 @@ import { isDefinedAndNotNull, mergeDeep } from '@core/utils';
styleUrls: ['../action/action-widget.scss', './iot-svg-widget.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class IotSvgWidgetComponent extends
BasicActionWidgetComponent implements OnInit, AfterViewInit, OnDestroy {
export class IotSvgWidgetComponent implements OnInit, AfterViewInit, OnDestroy, IotSvgObjectCallbacks {
@ViewChild('iotSvgShape', {static: false})
iotSvgShape: ElementRef<HTMLElement>;
@Input()
ctx: WidgetContext;
@Input()
widgetTitlePanel: TemplateRef<any>;
private settings: IotSvgWidgetSettings;
private svgContent$: Observable<string>;
backgroundStyle$: Observable<ComponentStyle>;
overlayStyle: ComponentStyle = {};
iotSvgObject: IotSvgObject;
private autoScale = true;
private shapeResize$: ResizeObserver;
constructor(public widgetComponent: WidgetComponent,
protected imagePipe: ImagePipe,
protected sanitizer: DomSanitizer,
private renderer: Renderer2,
private imageService: ImageService,
protected cd: ChangeDetectorRef,
private http: HttpClient) {
super(cd);
}
ngOnInit(): void {
super.ngOnInit();
this.ctx.$scope.actionWidget = this;
this.settings = mergeDeep({} as IotSvgWidgetSettings, iotSvgWidgetDefaultSettings, this.ctx.settings || {});
this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer);
this.overlayStyle = overlayStyle(this.settings.background.overlay);
let svgContent$: Observable<string>;
if (this.settings.iotSvgContent) {
svgContent$ = of(this.settings.iotSvgContent);
this.svgContent$ = of(this.settings.iotSvgContent);
} else if (this.settings.iotSvgUrl) {
svgContent$ = this.imageService.getImageString(this.settings.iotSvgUrl);
this.svgContent$ = this.imageService.getImageString(this.settings.iotSvgUrl);
} else {
svgContent$ = this.http.get(this.settings.iotSvg, {responseType: 'text'});
}
svgContent$.subscribe(
(content) => {
this.initObject(content);
}
);
}
private initObject(svgContent: string) {
const simulated = this.ctx.utilsService.widgetEditMode ||
this.ctx.isPreview || (isDefinedAndNotNull(this.settings.simulated) ? this.settings.simulated : false);
this.iotSvgObject = new IotSvgObject(this.ctx, svgContent, this.settings.iotSvgObject, simulated);
this.iotSvgObject.onError((error) => {
this.ctx.showErrorToast(error, 'bottom', 'center', this.ctx.toastTargetId, true);
});
this.iotSvgObject.onMessage((message) => {
this.ctx.showSuccessToast(message, 3000, 'bottom', 'center', this.ctx.toastTargetId, true);
});
this.iotSvgObject.init();
if (this.iotSvgShape) {
this.iotSvgObject.addTo(this.iotSvgShape.nativeElement);
if (this.autoScale) {
this.onResize();
}
this.svgContent$ = this.http.get(this.settings.iotSvg, {responseType: 'text'});
}
}
ngAfterViewInit(): void {
if (this.iotSvgObject) {
this.iotSvgObject.addTo(this.iotSvgShape.nativeElement);
}
if (this.autoScale) {
this.shapeResize$ = new ResizeObserver(() => {
this.onResize();
});
this.shapeResize$.observe(this.iotSvgShape.nativeElement);
this.onResize();
}
super.ngAfterViewInit();
this.svgContent$.subscribe((content) => {
this.initObject(this.iotSvgShape.nativeElement, content);
});
}
ngOnDestroy() {
if (this.shapeResize$) {
this.shapeResize$.disconnect();
}
if (this.iotSvgObject) {
this.iotSvgObject.destroy();
}
super.ngOnDestroy();
}
public onInit() {
super.onInit();
const borderRadius = this.ctx.$widgetElement.css('borderRadius');
this.overlayStyle = {...this.overlayStyle, ...{borderRadius}};
this.cd.detectChanges();
}
private onResize() {
const shapeWidth = this.iotSvgShape.nativeElement.getBoundingClientRect().width;
const shapeHeight = this.iotSvgShape.nativeElement.getBoundingClientRect().height;
if (this.iotSvgObject) {
this.iotSvgObject.setSize(shapeWidth, shapeHeight);
}
onSvgObjectError(error: string) {
this.ctx.showErrorToast(error, 'bottom', 'center', this.ctx.toastTargetId, true);
}
onSvgObjectMessage(message: string) {
this.ctx.showSuccessToast(message, 3000, 'bottom', 'center', this.ctx.toastTargetId, true);
}
private initObject(rootElement: HTMLElement,
svgContent: string) {
const simulated = this.ctx.utilsService.widgetEditMode ||
this.ctx.isPreview || (isDefinedAndNotNull(this.settings.simulated) ? this.settings.simulated : false);
this.iotSvgObject = new IotSvgObject(rootElement, this.ctx, svgContent, this.settings.iotSvgObject, this, simulated);
}
}

160
ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts

@ -15,7 +15,7 @@
///
import { ValueType } from '@shared/models/constants';
import { Box, Element, Runner, SVG, Svg, Text } from '@svgdotjs/svg.js';
import { Box, Element, Runner, SVG, Svg, Text, Timeline } from '@svgdotjs/svg.js';
import '@svgdotjs/svg.panzoom.js';
import {
DataToValueType,
@ -33,7 +33,7 @@ import {
mergeDeep,
parseFunction
} from '@core/utils';
import { BehaviorSubject, forkJoin, Observable, Observer, of } from 'rxjs';
import { BehaviorSubject, forkJoin, Observable, Observer } from 'rxjs';
import { share } from 'rxjs/operators';
import { ValueAction, ValueGetter, ValueSetter } from '@home/components/widget/lib/action/action-widget.models';
import { WidgetContext } from '@home/models/widget-component.models';
@ -41,6 +41,7 @@ import { ColorProcessor, constantColor, Font } from '@shared/models/widget-setti
import { AttributeScope } from '@shared/models/telemetry/telemetry.models';
import { UtilsService } from '@core/services/utils.service';
import { WidgetAction, WidgetActionType, widgetActionTypeTranslationMap } from '@shared/models/widget.models';
import { ResizeObserver } from '@juggle/resize-observer';
export interface IotSvgApi {
formatValue: (value: any, dec?: number, units?: string, showZeroDecimals?: boolean) => string | undefined;
@ -112,14 +113,37 @@ export interface IotSvgBehaviorValue extends IotSvgBehaviorBase {
}
export interface IotSvgBehaviorAction extends IotSvgBehaviorBase {
valueType: ValueType;
valueToDataType: ValueToDataType;
constantValue: any;
valueToDataFunction: string;
}
export type IotSvgBehavior = IotSvgBehaviorValue | IotSvgBehaviorAction;
export type IotSvgBehavior = IotSvgBehaviorValue & IotSvgBehaviorAction;
export enum IotSvgPropertyType {
text = 'text',
number = 'number',
switch = 'switch',
color = 'color',
color_settings = 'color_settings',
font = 'font',
units = 'units'
}
export const iotSvgPropertyTypes = Object.keys(IotSvgPropertyType) as IotSvgPropertyType[];
export type IotSvgPropertyType = 'string' | 'number' | 'color' | 'color-settings' | 'font' | 'units' | 'switch';
export const iotSvgPropertyTypeTranslations = new Map<IotSvgPropertyType, string>(
[
[IotSvgPropertyType.text, 'scada.property.type-text'],
[IotSvgPropertyType.number, 'scada.property.type-number'],
[IotSvgPropertyType.switch, 'scada.property.type-switch'],
[IotSvgPropertyType.color, 'scada.property.type-color'],
[IotSvgPropertyType.color_settings, 'scada.property.type-color-settings'],
[IotSvgPropertyType.font, 'scada.property.type-font'],
[IotSvgPropertyType.units, 'scada.property.type-units']
]
);
export interface IotSvgPropertyBase {
id: string;
@ -320,17 +344,19 @@ export type IotSvgObjectSettings = {
const parseError = (ctx: WidgetContext, err: any): string =>
ctx.$injector.get(UtilsService).parseException(err).message || 'Unknown Error';
export interface IotSvgObjectCallbacks {
onSvgObjectError: (error: string) => void;
onSvgObjectMessage: (message: string) => void;
}
export class IotSvgObject {
private metadata: IotSvgMetadata;
private settings: IotSvgObjectSettings;
private context: IotSvgContext;
private rootElement: HTMLElement;
private svgShape: Svg;
private box: Box;
private targetWidth: number;
private targetHeight: number;
private loadingSubject = new BehaviorSubject(false);
private valueGetters: ValueGetter<any>[] = [];
@ -339,19 +365,22 @@ export class IotSvgObject {
private stateValueSubjects: {[id: string]: BehaviorSubject<any>} = {};
loading$ = this.loadingSubject.asObservable().pipe(share());
private readonly shapeResize$: ResizeObserver;
private scale = 1;
private _onError: (error: string) => void = () => {};
private performInit = true;
private _onMessage: (message: string) => void = () => {};
loading$ = this.loadingSubject.asObservable().pipe(share());
constructor(private ctx: WidgetContext,
constructor(private rootElement: HTMLElement,
private ctx: WidgetContext,
private svgContent: string,
private inputSettings: IotSvgObjectSettings,
private callbacks: IotSvgObjectCallbacks,
private simulated: boolean) {
}
public init() {
this.shapeResize$ = new ResizeObserver(() => {
this.resize();
});
const doc: XMLDocument = new DOMParser().parseFromString(this.svgContent, 'image/svg+xml');
this.metadata = parseIotSvgMetadataFromDom(doc);
const defaults = defaultIotSvgObjectSettings(this.metadata);
@ -359,25 +388,13 @@ export class IotSvgObject {
defaults, this.inputSettings || {} as IotSvgObjectSettings);
this.prepareMetadata();
this.prepareSvgShape(doc);
this.initialize();
}
public onError(onError: (error: string) => void) {
this._onError = onError;
}
public onMessage(onMessage: (message: string) => void) {
this._onMessage = onMessage;
}
public addTo(element: HTMLElement) {
this.rootElement = element;
if (this.svgShape) {
this.svgShape.addTo(element);
}
this.shapeResize$.observe(this.rootElement);
}
public destroy() {
if (this.shapeResize$) {
this.shapeResize$.disconnect();
}
for (const stateValueId of Object.keys(this.stateValueSubjects)) {
this.stateValueSubjects[stateValueId].complete();
this.stateValueSubjects[stateValueId].unsubscribe();
@ -385,16 +402,15 @@ export class IotSvgObject {
this.valueActions.forEach(v => v.destroy());
this.loadingSubject.complete();
this.loadingSubject.unsubscribe();
if (this.svgShape) {
this.svgShape.remove();
for (const tag of this.metadata.tags) {
const elements = this.context.tags[tag.tag];
elements.forEach(element => {
element.timeline().finish();
element.timeline(null);
});
}
}
public setSize(targetWidth: number, targetHeight: number) {
this.targetWidth = targetWidth;
this.targetHeight = targetHeight;
if (this.svgShape) {
this.resize();
this.svgShape.remove();
}
}
@ -421,15 +437,10 @@ export class IotSvgObject {
this.svgShape.node.style['user-select'] = 'none';
this.box = this.svgShape.bbox();
this.svgShape.size(this.box.width, this.box.height);
if (this.rootElement) {
this.svgShape.addTo(this.rootElement);
}
if (this.targetWidth && this.targetHeight) {
this.resize();
}
this.svgShape.addTo(this.rootElement);
}
private initialize() {
private init() {
this.context = {
api: {
formatValue,
@ -447,7 +458,7 @@ export class IotSvgObject {
};
const taggedElements = this.svgShape.find(`[tb\\:tag]`);
for (const element of taggedElements) {
const tag = element.attr('tb:tag');
const tag: string = element.attr('tb:tag');
let elements = this.context.tags[tag];
if (!elements) {
elements = [];
@ -489,7 +500,7 @@ export class IotSvgObject {
next: (val) => {this.onValue(getBehavior.id, val);},
error: (err) => {
const message = parseError(this.ctx, err);
this._onError(message);
this.onError(message);
}
}, this.simulated);
this.valueGetters.push(valueGetter);
@ -526,6 +537,14 @@ export class IotSvgObject {
}
}
private onError(error: string) {
this.callbacks.onSvgObjectError(error);
}
private onMessage(message: string) {
this.callbacks.onSvgObjectMessage(message);
}
private callAction(event: Event, behaviorId: string, value?: any, observer?: Partial<Observer<void>>) {
const behavior = this.metadata.behavior.find(b => b.id === behaviorId);
if (behavior) {
@ -547,7 +566,7 @@ export class IotSvgObject {
observer.error(err);
}
const message = parseError(this.ctx, err);
this._onError(message);
this.onError(message);
}
}
);
@ -557,22 +576,38 @@ export class IotSvgObject {
if (this.simulated) {
const translatedType = this.ctx.translate.instant(widgetActionTypeTranslationMap.get(widgetAction.type));
const message = this.ctx.translate.instant('scada.preview-widget-action-text', {type: translatedType});
this._onMessage(message);
this.onMessage(message);
} else {
this.ctx.actionsApi.onWidgetAction(event, widgetAction);
}
if (observer?.next) {
observer.next();
}
}
}
}
private resize() {
let scale: number;
if (this.targetWidth < this.targetHeight) {
scale = this.targetWidth / this.box.width;
} else {
scale = this.targetHeight / this.box.height;
if (this.svgShape) {
const targetWidth = this.rootElement.getBoundingClientRect().width;
const targetHeight = this.rootElement.getBoundingClientRect().height;
if (targetWidth && targetHeight) {
let scale: number;
if (targetWidth < targetHeight) {
scale = targetWidth / this.box.width;
} else {
scale = targetHeight / this.box.height;
}
if (this.scale !== scale) {
this.scale = scale;
this.svgShape.node.style.transform = `scale(${this.scale})`;
}
if (this.performInit) {
this.performInit = false;
this.init();
}
}
}
this.svgShape.node.style.transform = `scale(${scale})`;
}
private onValue(id: string, value: any) {
@ -598,7 +633,7 @@ export class IotSvgObject {
private renderState(): void {
this.metadata.stateRender(this.svgShape, this.context);
for (const tag of this.metadata.tags) {
const elements = this.svgShape.find(`[tb\\:tag="${tag.tag}"]`);
const elements = this.context.tags[tag.tag];// this.svgShape.find(`[tb\\:tag="${tag.tag}"]`);
elements.forEach(element => {
tag.stateRender(element, this.context);
});
@ -664,6 +699,7 @@ export class IotSvgObject {
private animate(element: Element, duration: number): Runner {
element.timeline().finish();
element.timeline(new Timeline());
return element.animate(duration, 0, 'now');
}
@ -692,9 +728,9 @@ export class IotSvgObject {
if (property) {
const value = this.settings.properties[id];
if (isDefinedAndNotNull(value)) {
if (property.type === 'color-settings') {
if (property.type === IotSvgPropertyType.color_settings) {
return ColorProcessor.fromSettings(value);
} else if (property.type === 'string') {
} else if (property.type === IotSvgPropertyType.text) {
const result = this.ctx.utilsService.customTranslation(value, value);
const entityInfo = this.ctx.defaultSubscription.getFirstEntityInfo();
return createLabelFromSubscriptionEntityInfo(entityInfo, result);
@ -702,13 +738,13 @@ export class IotSvgObject {
return value;
} else {
switch (property.type) {
case 'string':
case IotSvgPropertyType.text:
return '';
case 'number':
case IotSvgPropertyType.number:
return 0;
case 'color':
case IotSvgPropertyType.color:
return '#000';
case 'color-settings':
case IotSvgPropertyType.color_settings:
return ColorProcessor.fromSettings(constantColor('#000'));
}
}

138
ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component.html

@ -0,0 +1,138 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-scada-symbol-behavior-settings-panel" [formGroup]="behaviorFormGroup">
<div class="tb-scada-symbol-behavior-settings-title">{{ panelTitle | translate }}</div>
<div class="tb-scada-symbol-behavior-settings-panel-content">
<div class="tb-form-row">
<div class="fixed-title-width" translate>scada.behavior.id</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<input required matInput formControlName="id" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" translate>scada.behavior.name</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<input required matInput formControlName="name" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" translate>scada.behavior.hint</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="hint" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" translate>scada.behavior.type</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="type">
<mat-option *ngFor="let type of iotSvgBehaviorTypes" [value]="type">
{{ iotSvgBehaviorTypeTranslations.get(type) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div *ngIf="[IotSvgBehaviorType.value, IotSvgBehaviorType.action].includes(behaviorFormGroup.get('type').value)" class="tb-form-row">
<div class="fixed-title-width" translate>scada.behavior.value-type</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="flex tb-value-type">
<mat-select formControlName="valueType">
<mat-select-trigger>
<div class="tb-value-type-row">
<mat-icon class="tb-mat-18" svgIcon="{{ valueTypesMap.get(behaviorFormGroup.get('valueType').value).icon }}"></mat-icon>
<span>{{ valueTypesMap.get(behaviorFormGroup.get('valueType').value).name | translate }}</span>
</div>
</mat-select-trigger>
<mat-option *ngFor="let valueType of valueTypes" [value]="valueType">
<mat-icon class="tb-mat-20" svgIcon="{{ valueTypesMap.get(valueType).icon }}"></mat-icon>
<span>{{ valueTypesMap.get(valueType).name | translate }}</span>
</mat-option>
</mat-select>
</mat-form-field>
</div>
<ng-container *ngIf="behaviorFormGroup.get('type').value === IotSvgBehaviorType.value">
<div class="tb-form-row ">
<div class="fixed-title-width" translate>scada.behavior.default-value</div>
<tb-value-input fxFlex [valueType]="behaviorFormGroup.get('valueType').value"
formControlName="defaultValue"></tb-value-input>
</div>
<ng-container *ngIf="behaviorFormGroup.get('valueType').value === ValueType.BOOLEAN">
<div class="tb-form-row">
<div class="fixed-title-width" translate>scada.behavior.true-label</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="trueLabel" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" translate>scada.behavior.false-label</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="falseLabel" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" translate>scada.behavior.state-label</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="stateLabel" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
</ng-container>
</ng-container>
<ng-container *ngIf="behaviorFormGroup.get('type').value === IotSvgBehaviorType.action">
<div class="tb-form-panel stroked">
<div class="tb-form-row no-padding no-border column-xs">
<div class="fixed-title-width">{{ 'scada.behavior.default-payload' | translate }}</div>
<tb-toggle-select fxFlex formControlName="valueToDataType">
<tb-toggle-option *ngIf="behaviorFormGroup.get('valueType').value !== ValueType.BOOLEAN" [value]="ValueToDataType.VALUE">{{ 'widgets.value-action.converter-value' | translate }}</tb-toggle-option>
<tb-toggle-option *ngIf="behaviorFormGroup.get('valueType').value === ValueType.BOOLEAN" [value]="ValueToDataType.CONSTANT">{{ 'widgets.value-action.converter-constant' | translate }}</tb-toggle-option>
<tb-toggle-option [value]="ValueToDataType.FUNCTION">{{ 'widgets.value-action.converter-function' | translate }}</tb-toggle-option>
<tb-toggle-option [value]="ValueToDataType.NONE">{{ 'widgets.value-action.converter-none' | translate }}</tb-toggle-option>
</tb-toggle-select>
</div>
<tb-value-input *ngIf="behaviorFormGroup.get('valueToDataType').value === ValueToDataType.CONSTANT"
fxFlex
[layout]="{
layout: 'row',
breakpoints: {xs: 'column'}
}"
formControlName="constantValue">
</tb-value-input>
<tb-js-func *ngIf="behaviorFormGroup.get('valueToDataType').value === ValueToDataType.FUNCTION"
formControlName="valueToDataFunction"
required
[globalVariables]="functionScopeVariables"
[functionArgs]="['value']"
functionTitle="{{ 'widgets.value-action.convert-value-function' | translate }}"
helpId="widget/lib/rpc/convert_value_fn">
</tb-js-func>
</div>
</ng-container>
</div>
<div class="tb-scada-symbol-behavior-settings-panel-buttons">
<button mat-button
color="primary"
type="button"
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button
color="primary"
type="button"
(click)="applyBehaviorSettings()"
[disabled]="behaviorFormGroup.invalid || !behaviorFormGroup.dirty">
{{ (isAdd ? 'action.add' : 'action.apply') | translate }}
</button>
</div>
</div>

67
ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component.scss

@ -0,0 +1,67 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import '../../../../../../scss/constants';
.tb-scada-symbol-behavior-settings-panel {
width: 540px;
display: flex;
flex-direction: column;
gap: 16px;
@media #{$mat-lt-md} {
width: 90vw;
}
.tb-scada-symbol-behavior-settings-panel-content {
display: flex;
flex-direction: column;
gap: 16px;
overflow: auto;
margin: -10px;
padding: 10px;
.mat-mdc-form-field.tb-value-type {
.mat-mdc-form-field-infix {
max-height: 40px;
}
mat-select-trigger {
.tb-value-type-row {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
gap: 10px;
.mat-icon {
color: rgba(0, 0, 0, 0.38);
vertical-align: bottom;
}
}
}
}
}
.tb-scada-symbol-behavior-settings-title {
font-size: 16px;
font-weight: 500;
line-height: 24px;
letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.87);
}
.tb-scada-symbol-behavior-settings-panel-buttons {
height: 40px;
display: flex;
flex-direction: row;
gap: 16px;
justify-content: flex-end;
align-items: flex-end;
}
}

148
ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component.ts

@ -0,0 +1,148 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core';
import { TbPopoverComponent } from '@shared/components/popover.component';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { merge } from 'rxjs';
import {
IotSvgBehavior,
IotSvgBehaviorType,
iotSvgBehaviorTypes,
iotSvgBehaviorTypeTranslations
} from '@app/modules/home/components/widget/lib/svg/iot-svg.models';
import { ValueType, valueTypesMap } from '@shared/models/constants';
import { ValueToDataType } from '@shared/models/action-widget-settings.models';
import { WidgetService } from '@core/http/widget.service';
@Component({
selector: 'tb-scada-symbol-behavior-panel',
templateUrl: './scada-symbol-behavior-panel.component.html',
styleUrls: ['./scada-symbol-behavior-panel.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class ScadaSymbolBehaviorPanelComponent implements OnInit {
IotSvgBehaviorType = IotSvgBehaviorType;
ValueType = ValueType;
ValueToDataType = ValueToDataType;
iotSvgBehaviorTypes = iotSvgBehaviorTypes;
iotSvgBehaviorTypeTranslations = iotSvgBehaviorTypeTranslations;
valueTypes = Object.keys(ValueType) as ValueType[];
valueTypesMap = valueTypesMap;
@Input()
isAdd = false;
@Input()
behavior: IotSvgBehavior;
@Input()
popover: TbPopoverComponent<ScadaSymbolBehaviorPanelComponent>;
@Output()
behaviorSettingsApplied = new EventEmitter<IotSvgBehavior>();
functionScopeVariables = this.widgetService.getWidgetScopeVariables();
panelTitle: string;
behaviorFormGroup: UntypedFormGroup;
constructor(private fb: UntypedFormBuilder,
private widgetService: WidgetService) {
}
ngOnInit(): void {
this.panelTitle = this.isAdd ? 'scada.behavior.add-behavior' : 'scada.behavior.behavior-settings';
this.behaviorFormGroup = this.fb.group(
{
id: [this.behavior.id, [Validators.required]],
name: [this.behavior.name, [Validators.required]],
hint: [this.behavior.hint, []],
type: [this.behavior.type, [Validators.required]],
valueType: [this.behavior.valueType, [Validators.required]],
defaultValue: [this.behavior.defaultValue, [Validators.required]],
trueLabel: [this.behavior.trueLabel, []],
falseLabel: [this.behavior.falseLabel, []],
stateLabel: [this.behavior.stateLabel, []],
valueToDataType: [this.behavior.valueToDataType, [Validators.required]],
constantValue: [this.behavior.constantValue, [Validators.required]],
valueToDataFunction: [this.behavior.valueToDataFunction, [Validators.required]]
}
);
merge(this.behaviorFormGroup.get('type').valueChanges,
this.behaviorFormGroup.get('valueType').valueChanges,
this.behaviorFormGroup.get('valueToDataType').valueChanges).subscribe(() => {
this.updateValidators();
});
this.updateValidators();
}
cancel() {
this.popover?.hide();
}
applyBehaviorSettings() {
const behavior = this.behaviorFormGroup.getRawValue();
this.behaviorSettingsApplied.emit(behavior);
}
private updateValidators() {
const type: IotSvgBehaviorType = this.behaviorFormGroup.get('type').value;
const valueType: ValueType = this.behaviorFormGroup.get('valueType').value;
let valueToDataType: ValueToDataType = this.behaviorFormGroup.get('valueToDataType').value;
this.behaviorFormGroup.disable({emitEvent: false});
this.behaviorFormGroup.get('id').enable({emitEvent: false});
this.behaviorFormGroup.get('name').enable({emitEvent: false});
this.behaviorFormGroup.get('type').enable({emitEvent: false});
this.behaviorFormGroup.get('hint').enable({emitEvent: false});
switch (type) {
case IotSvgBehaviorType.value:
this.behaviorFormGroup.get('valueType').enable({emitEvent: false});
this.behaviorFormGroup.get('defaultValue').enable({emitEvent: false});
if (valueType === ValueType.BOOLEAN) {
this.behaviorFormGroup.get('trueLabel').enable({emitEvent: false});
this.behaviorFormGroup.get('falseLabel').enable({emitEvent: false});
this.behaviorFormGroup.get('stateLabel').enable({emitEvent: false});
}
break;
case IotSvgBehaviorType.action:
if (valueType === ValueType.BOOLEAN && valueToDataType === ValueToDataType.VALUE) {
this.behaviorFormGroup.patchValue({valueToDataType: ValueToDataType.CONSTANT}, {emitEvent: false});
valueToDataType = ValueToDataType.CONSTANT;
} else if (valueType !== ValueType.BOOLEAN && valueToDataType === ValueToDataType.CONSTANT) {
this.behaviorFormGroup.patchValue({valueToDataType: ValueToDataType.VALUE}, {emitEvent: false});
valueToDataType = ValueToDataType.VALUE;
}
this.behaviorFormGroup.get('valueType').enable({emitEvent: false});
this.behaviorFormGroup.get('valueToDataType').enable({emitEvent: false});
if (valueToDataType === ValueToDataType.CONSTANT) {
this.behaviorFormGroup.get('constantValue').enable({emitEvent: false});
} else if (valueToDataType === ValueToDataType.FUNCTION) {
this.behaviorFormGroup.get('valueToDataFunction').enable({emitEvent: false});
}
break;
case IotSvgBehaviorType.widgetAction:
break;
}
}
}

3
ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.html

@ -32,7 +32,8 @@
<div class="tb-form-table-row-cell-buttons">
<button type="button"
mat-icon-button
(click)="editBehavior()"
#editButton
(click)="editBehavior($event, editButton)"
matTooltip="{{ 'scada.behavior.behavior-settings' | translate }}"
matTooltipPosition="above">
<mat-icon>settings</mat-icon>

106
ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.ts

@ -23,7 +23,9 @@ import {
Input,
OnInit,
Output,
Renderer2,
ViewChild,
ViewContainerRef,
ViewEncapsulation
} from '@angular/core';
import {
@ -32,17 +34,22 @@ import {
NG_VALUE_ACCESSOR,
UntypedFormBuilder,
UntypedFormGroup,
ValidationErrors
ValidationErrors,
Validators
} from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import {
IotSvgBehavior,
IotSvgBehaviorAction,
IotSvgBehaviorType,
iotSvgBehaviorTypes,
iotSvgBehaviorTypeTranslations,
IotSvgBehaviorValue
iotSvgBehaviorTypeTranslations
} from '@home/components/widget/lib/svg/iot-svg.models';
import { deepClone, isDefinedAndNotNull, isUndefinedOrNull } from '@core/utils';
import { MatButton } from '@angular/material/button';
import { TbPopoverService } from '@shared/components/popover.service';
import {
ScadaSymbolBehaviorPanelComponent
} from '@home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component';
import { ValueToDataType } from '@shared/models/action-widget-settings.models';
export const behaviorValid = (behavior: IotSvgBehavior): boolean => {
if (!behavior.id || !behavior.name || !behavior.type) {
@ -50,14 +57,20 @@ export const behaviorValid = (behavior: IotSvgBehavior): boolean => {
}
switch (behavior.type) {
case IotSvgBehaviorType.value:
const valueBehavior = behavior as IotSvgBehaviorValue;
if (!valueBehavior.valueType) {
if (!behavior.valueType || isUndefinedOrNull(behavior.defaultValue)) {
return false;
}
break;
case IotSvgBehaviorType.action:
const actionBehavior = behavior as IotSvgBehaviorAction;
if (!actionBehavior.valueToDataType) {
if (!behavior.valueToDataType) {
return false;
}
if (behavior.valueToDataType === ValueToDataType.CONSTANT
&& isUndefinedOrNull(behavior.constantValue)) {
return false;
}
if (behavior.valueToDataType === ValueToDataType.FUNCTION
&& isUndefinedOrNull(behavior.valueToDataFunction)) {
return false;
}
break;
@ -95,6 +108,9 @@ export class ScadaSymbolBehaviorRowComponent implements ControlValueAccessor, On
@ViewChild('idInput')
idInput: ElementRef<HTMLInputElement>;
@ViewChild('editButton')
editButton: MatButton;
iotSvgBehaviorTypes = iotSvgBehaviorTypes;
iotSvgBehaviorTypeTranslations = iotSvgBehaviorTypeTranslations;
@ -111,19 +127,24 @@ export class ScadaSymbolBehaviorRowComponent implements ControlValueAccessor, On
private propagateChange = (_val: any) => {};
constructor(private fb: UntypedFormBuilder,
private dialog: MatDialog,
private cd: ChangeDetectorRef) {
private cd: ChangeDetectorRef,
private popoverService: TbPopoverService,
private renderer: Renderer2,
private viewContainerRef: ViewContainerRef) {
}
ngOnInit() {
this.behaviorRowFormGroup = this.fb.group({
id: [null, []],
name: [null, []],
type: [null, []]
id: [null, [Validators.required]],
name: [null, [Validators.required]],
type: [null, [Validators.required]]
});
this.behaviorRowFormGroup.valueChanges.subscribe(
() => this.updateModel()
);
this.behaviorRowFormGroup.get('type').valueChanges.subscribe((newType: IotSvgBehaviorType) => {
this.onTypeChanged(newType);
});
}
registerOnChange(fn: any): void {
@ -154,8 +175,42 @@ export class ScadaSymbolBehaviorRowComponent implements ControlValueAccessor, On
this.cd.markForCheck();
}
editBehavior() {
editBehavior($event: Event, matButton: MatButton, add = false, editCanceled = () => {}) {
if ($event) {
$event.stopPropagation();
}
const trigger = matButton._elementRef.nativeElement;
if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger);
} else {
const ctx: any = {
isAdd: add,
behavior: deepClone(this.modelValue)
};
const scadaSymbolBehaviorPanelPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, ScadaSymbolBehaviorPanelComponent, ['leftOnly', 'leftTopOnly', 'leftBottomOnly'], true, null,
ctx,
{},
{}, {}, true);
scadaSymbolBehaviorPanelPopover.tbComponentRef.instance.popover = scadaSymbolBehaviorPanelPopover;
scadaSymbolBehaviorPanelPopover.tbComponentRef.instance.behaviorSettingsApplied.subscribe((behavior) => {
scadaSymbolBehaviorPanelPopover.hide();
this.behaviorRowFormGroup.patchValue(
{
id: behavior.id,
name: behavior.name,
type: behavior.type
}, {emitEvent: false}
);
this.modelValue = behavior;
this.propagateChange(this.modelValue);
});
scadaSymbolBehaviorPanelPopover.tbDestroy.subscribe(() => {
if (!behaviorValid(this.modelValue)) {
editCanceled();
}
});
}
}
focus() {
@ -163,6 +218,25 @@ export class ScadaSymbolBehaviorRowComponent implements ControlValueAccessor, On
this.idInput.nativeElement.focus();
}
onAdd(onCanceled: () => void) {
this.idInput.nativeElement.scrollIntoView();
this.editBehavior(null, this.editButton, true, onCanceled);
}
private onTypeChanged(newType: IotSvgBehaviorType) {
const prevType = this.modelValue.type;
this.modelValue = {...this.modelValue, ...{type: newType}};
if (!behaviorValid(this.modelValue)) {
this.editBehavior(null, this.editButton, false, () => {
this.behaviorRowFormGroup.patchValue(
{
type: prevType
}, {emitEvent: true}
);
});
}
}
private updateModel() {
const value: IotSvgBehavior = this.behaviorRowFormGroup.value;
this.modelValue = {...this.modelValue, ...value};

10
ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component.ts

@ -43,6 +43,7 @@ import {
behaviorValidator,
ScadaSymbolBehaviorRowComponent
} from '@home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component';
import { ValueToDataType } from '@shared/models/action-widget-settings.models';
@Component({
selector: 'tb-scada-symbol-metadata-behaviors',
@ -154,14 +155,19 @@ export class ScadaSymbolBehaviorsComponent implements ControlValueAccessor, OnIn
name: '',
type: IotSvgBehaviorType.value,
valueType: ValueType.BOOLEAN,
defaultValue: false
defaultValue: false,
valueToDataType: ValueToDataType.CONSTANT,
constantValue: false,
valueToDataFunction: ''
};
const behaviorsArray = this.behaviorsFormGroup.get('behaviors') as UntypedFormArray;
const behaviorControl = this.fb.control(behavior, [behaviorValidator]);
behaviorsArray.push(behaviorControl);
setTimeout(() => {
const behaviorRow = this.behaviorRows.get(this.behaviorRows.length-1);
behaviorRow.focus();
behaviorRow.onAdd(() => {
this.removeBehavior(behaviorsArray.length-1);
});
});
}

6
ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-components.module.ts

@ -30,6 +30,9 @@ import {
import {
ScadaSymbolBehaviorRowComponent
} from '@home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component';
import {
ScadaSymbolBehaviorPanelComponent
} from '@home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component';
@NgModule({
declarations:
@ -38,7 +41,8 @@ import {
ScadaSymbolMetadataTagComponent,
ScadaSymbolMetadataTagsComponent,
ScadaSymbolBehaviorsComponent,
ScadaSymbolBehaviorRowComponent
ScadaSymbolBehaviorRowComponent,
ScadaSymbolBehaviorPanelComponent
],
imports: [
CommonModule,

2
ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.html

@ -26,7 +26,7 @@
</div>
</div>
<div>
<button type="button" mat-stroked-button color="primary" (click)="addTag()">
<button *ngIf="availableTags?.length" type="button" mat-stroked-button color="primary" (click)="addTag()">
{{ 'scada.add-tag' | translate }}
</button>
</div>

37
ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.models.ts

@ -74,13 +74,14 @@ export class ScadaSymbolEditObject {
this.scale = 1;
const contentData = iotSvgContentData(svgContent);
this.svgRootNodePart = contentData.svgRootNode;
this.svgShape = SVG().addTo(this.rootElement).svg(contentData.innerSvg);
this.svgShape = SVG().svg(contentData.innerSvg);
this.svgShape.node.style.overflow = 'visible';
this.svgShape.node.style['user-select'] = 'none';
this.box = this.svgShape.bbox();
this.svgShape.size(this.box.width, this.box.height);
this.svgShape.viewbox(`0 0 ${this.box.width} ${this.box.height}`);
this.svgShape.style().attr('tb:inner', true).rule('.tb-element', {cursor: 'pointer', transition: '0.2s filter ease-in-out'});
this.svgShape.addTo(this.rootElement);
this.updateHoverFilterStyle();
this.performSetup = true;
this.shapeResize$.observe(this.rootElement);
@ -245,22 +246,24 @@ export class ScadaSymbolEditObject {
if (this.svgShape) {
const targetWidth = this.rootElement.getBoundingClientRect().width;
const targetHeight = this.rootElement.getBoundingClientRect().height;
let scale: number;
if (targetWidth < targetHeight) {
scale = targetWidth / this.box.width;
} else {
scale = targetHeight / this.box.height;
}
if (this.scale !== scale) {
this.scale = scale;
this.svgShape.node.style.transform = `scale(${this.scale})`;
this.updateHoverFilterStyle();
this.updateZoomOptions();
this.updateTooltipPositions();
}
if (this.performSetup) {
this.performSetup = false;
this.doSetup();
if (targetWidth && targetHeight) {
let scale: number;
if (targetWidth < targetHeight) {
scale = targetWidth / this.box.width;
} else {
scale = targetHeight / this.box.height;
}
if (this.scale !== scale) {
this.scale = scale;
this.svgShape.node.style.transform = `scale(${this.scale})`;
this.updateHoverFilterStyle();
this.updateZoomOptions();
this.updateTooltipPositions();
}
if (this.performSetup) {
this.performSetup = false;
this.doSetup();
}
}
}
}

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

@ -3432,7 +3432,31 @@
"type-value": "Value",
"type-widget-action": "Widget action",
"behavior-settings": "Behavior settings",
"remove-behavior": "Remove behavior"
"remove-behavior": "Remove behavior",
"hint": "Hint",
"value-type": "Value type",
"default-value": "Default value",
"true-label": "True label",
"false-label": "False label",
"state-label": "State label",
"default-payload": "Default payload"
},
"property": {
"property": "Property",
"id": "Id",
"name": "Name",
"type": "Type",
"type-text": "Text",
"type-number": "Number",
"type-switch": "Switch",
"type-color": "Color",
"type-color-settings": "Color settings",
"type-font": "Font",
"type-units": "Units",
"no-properties": "No properties configured",
"add-property": "Add property",
"property-settings": "Property settings",
"remove-property": "Remove property"
}
},
"item": {

589
ui-ngx/src/assets/widget/svg/drawing.svg

@ -1,307 +1,310 @@
<svg width="100" height="100" version="1.1" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg">
<tb:metadata><![CDATA[{
"title": "My first IoT SVG Object",
"stateRenderFunction": "var showMinMaxLevel = ctx.properties.showMinMaxLevel;\nvar minLevelElement = ctx.tags.minLevel[0];\nvar maxLevelElement = ctx.tags.maxLevel[0];\nvar minLevel = ctx.properties.minLevel; \nvar maxLevel = ctx.properties.maxLevel;\n\nif (showMinMaxLevel) {\n \n var minMaxLevelFont = ctx.properties.minMaxLevelFont;\n var minMaxLevelColor = ctx.properties.minMaxLevelColor;\n \n ctx.api.text(minLevelElement, minLevel);\n ctx.api.text(maxLevelElement, maxLevel);\n \n ctx.api.font(minLevelElement, minMaxLevelFont, minMaxLevelColor);\n ctx.api.font(maxLevelElement, minMaxLevelFont, minMaxLevelColor);\n \n} else {\n minLevelElement.hide();\n maxLevelElement.hide();\n}\n\nvar disabled = ctx.values.disabled;\nvar on = ctx.values.on;\nvar level = ctx.values.level;\n\nvar onButton = ctx.tags.onButton;\nvar offButton = ctx.tags.offButton;\nvar levelUpButton = ctx.tags.levelUpButton;\nvar levelDownButton = ctx.tags.levelDownButton;\n\nvar onButtonEnabled = !disabled && !on;\nvar offButtonEnabled = !disabled && on;\nvar levelUpEnabled = !disabled && level < maxLevel;\nvar levelDownEnabled = !disabled && level > minLevel;\n\nif (onButtonEnabled) {\n ctx.api.enable(onButton);\n onButton[0].findOne('rect').attr({fill: '#12ed19'});\n} else {\n ctx.api.disable(onButton);\n onButton[0].findOne('rect').attr({fill: '#777'});\n}\n \nif (offButtonEnabled) {\n ctx.api.enable(offButton);\n offButton[0].findOne('rect').attr({fill: '#ed121f'});\n} else {\n ctx.api.disable(offButton);\n offButton[0].findOne('rect').attr({fill: '#777'});\n} \n\n\nif (levelUpEnabled) {\n ctx.api.enable(levelUpButton);\n levelUpButton[0].findOne('rect').attr({fill: '#fff'});\n} else {\n ctx.api.disable(levelUpButton);\n levelUpButton[0].findOne('rect').attr({fill: '#777'});\n}\n \nif (levelDownEnabled) {\n ctx.api.enable(levelDownButton);\n levelDownButton[0].findOne('rect').attr({fill: '#fff'});\n} else {\n ctx.api.disable(levelDownButton);\n levelDownButton[0].findOne('rect').attr({fill: '#777'});\n}",
"tags": [
{
"tag": "level",
"stateRenderFunction": "var level = ctx.values.level; \nvar disabled = ctx.values.disabled;\nvar minLevel = ctx.properties.minLevel; \nvar maxLevel = ctx.properties.maxLevel;\n\nvar height = (level - minLevel) / (maxLevel - minLevel);\nheight = Math.max(0, Math.min(1, height))*80; \n\nctx.api.animate(element, 200).attr({height: height});\n\nvar fill;\nif (disabled) {\n fill = ctx.properties.disabledLevelBackground;\n} else {\n var colorProcessor = ctx.properties.levelBackground;\n colorProcessor.update(level);\n fill = colorProcessor.color;\n}\n\nelement.attr({fill: fill});"
},
{
"tag": "levelTitle",
"stateRenderFunction": "var showLevelTitle = ctx.properties.showLevelTitle;\n\nif (showLevelTitle) {\n var levelTitle = ctx.properties.levelTitle;\n var levelTitleFont = ctx.properties.levelTitleFont;\n var levelTitleColor = ctx.properties.levelTitleColor;\n \n ctx.api.text(element, levelTitle);\n ctx.api.font(element, levelTitleFont, levelTitleColor);\n \n} else {\n element.hide();\n}"
},
{
"tag": "levelValue",
"stateRenderFunction": "var showValue = ctx.properties.showValue;\n\nif (showValue) {\n var valueFont = ctx.properties.valueFont;\n var valueColor = ctx.properties.valueColor;\n var level = ctx.values.level; \n \n var levelText = ctx.api.formatValue(level, ctx.properties.valueDecimals, ctx.properties.valueUnits, false);\n\n ctx.api.font(element, valueFont, valueColor);\n ctx.api.text(element, levelText);\n\n} else {\n element.hide();\n ctx.tags.levelValueBackground[0].hide();\n}",
"actions": {
"click": {
"actionFunction": "ctx.api.callAction(event, 'levelValueClick');"
}
}
},
{
"tag": "fan",
"stateRenderFunction": "var on = ctx.values.on;\nif (on) {\n element.addClass('fan-rotate');\n} else {\n element.removeClass('fan-rotate');\n}\n"
},
{
"tag": "onButton",
"actions": {
"click": {
"actionFunction": "ctx.api.callAction(event, 'onUpdateState', undefined, {\n next: () => {\n ctx.api.setValue('on', true);\n }\n});"
}
}
},
{
"tag": "offButton",
"actions": {
"click": {
"actionFunction": "ctx.api.callAction(event, 'offUpdateState', undefined, {\n next: () => {\n ctx.api.setValue('on', false);\n }\n});\n"
}
}
},
{
"tag": "levelUpButton",
"actions": {
"click": {
"actionFunction": "var level = ctx.values.level; \nvar maxLevel = ctx.properties.maxLevel;\n\nvar newLevel = Math.min(maxLevel, level + 5);\nctx.api.setValue('level', newLevel);\nctx.api.callAction(event, 'levelUpdateState', newLevel, {\n error: () => {\n ctx.api.setValue('level', level);\n }\n});"
}
}
},
{
"tag": "levelDownButton",
"actions": {
"click": {
"actionFunction": "var level = ctx.values.level; \nvar minLevel = ctx.properties.minLevel;\n\nvar newLevel = Math.max(minLevel, level - 5);\nctx.api.setValue('level', newLevel);\nctx.api.callAction(event, 'levelUpdateState', newLevel, {\n error: () => {\n ctx.api.setValue('level', level);\n }\n});"
}
}
<svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="100" height="100" version="1.1" viewBox="0 0 100 100">
<tb:metadata xmlns=""><![CDATA[{
"title": "Level and Fan12",
"stateRenderFunction": "var showMinMaxLevel = ctx.properties.showMinMaxLevel;\nvar minLevelElement = ctx.tags.minLevel[0];\nvar maxLevelElement = ctx.tags.maxLevel[0];\nvar minLevel = ctx.properties.minLevel; \nvar maxLevel = ctx.properties.maxLevel;\n\nif (showMinMaxLevel) {\n \n var minMaxLevelFont = ctx.properties.minMaxLevelFont;\n var minMaxLevelColor = ctx.properties.minMaxLevelColor;\n \n ctx.api.text(minLevelElement, minLevel);\n ctx.api.text(maxLevelElement, maxLevel);\n \n ctx.api.font(minLevelElement, minMaxLevelFont, minMaxLevelColor);\n ctx.api.font(maxLevelElement, minMaxLevelFont, minMaxLevelColor);\n \n} else {\n minLevelElement.hide();\n maxLevelElement.hide();\n}\n\nvar disabled = ctx.values.disabled;\nvar on = ctx.values.on;\nvar level = ctx.values.level;\n\nvar onButton = ctx.tags.onButton;\nvar offButton = ctx.tags.offButton;\nvar levelUpButton = ctx.tags.levelUpButton;\nvar levelDownButton = ctx.tags.levelDownButton;\n\nvar onButtonEnabled = !disabled && !on;\nvar offButtonEnabled = !disabled && on;\nvar levelUpEnabled = !disabled && level < maxLevel;\nvar levelDownEnabled = !disabled && level > minLevel;\n\nif (onButtonEnabled) {\n ctx.api.enable(onButton);\n onButton[0].findOne('rect').attr({fill: '#12ed19'});\n} else {\n ctx.api.disable(onButton);\n onButton[0].findOne('rect').attr({fill: '#777'});\n}\n \nif (offButtonEnabled) {\n ctx.api.enable(offButton);\n offButton[0].findOne('rect').attr({fill: '#ed121f'});\n} else {\n ctx.api.disable(offButton);\n offButton[0].findOne('rect').attr({fill: '#777'});\n} \n\n\nif (levelUpEnabled) {\n ctx.api.enable(levelUpButton);\n levelUpButton[0].findOne('rect').attr({fill: '#fff'});\n} else {\n ctx.api.disable(levelUpButton);\n levelUpButton[0].findOne('rect').attr({fill: '#777'});\n}\n \nif (levelDownEnabled) {\n ctx.api.enable(levelDownButton);\n levelDownButton[0].findOne('rect').attr({fill: '#fff'});\n} else {\n ctx.api.disable(levelDownButton);\n levelDownButton[0].findOne('rect').attr({fill: '#777'});\n}",
"tags": [
{
"tag": "level",
"stateRenderFunction": "var level = ctx.values.level; \nvar disabled = ctx.values.disabled;\nvar minLevel = ctx.properties.minLevel; \nvar maxLevel = ctx.properties.maxLevel;\n\nvar height = (level - minLevel) / (maxLevel - minLevel);\nheight = Math.max(0, Math.min(1, height))*80; \n\nctx.api.animate(element, 200).attr({height: height});\n\nvar fill;\nif (disabled) {\n fill = ctx.properties.disabledLevelBackground;\n} else {\n var colorProcessor = ctx.properties.levelBackground;\n colorProcessor.update(level);\n fill = colorProcessor.color;\n}\n\nelement.attr({fill: fill});"
},
{
"tag": "levelTitle",
"stateRenderFunction": "var showLevelTitle = ctx.properties.showLevelTitle;\n\nif (showLevelTitle) {\n var levelTitle = ctx.properties.levelTitle;\n var levelTitleFont = ctx.properties.levelTitleFont;\n var levelTitleColor = ctx.properties.levelTitleColor;\n \n ctx.api.text(element, levelTitle);\n ctx.api.font(element, levelTitleFont, levelTitleColor);\n \n} else {\n element.hide();\n}"
},
{
"tag": "levelValue",
"stateRenderFunction": "var showValue = ctx.properties.showValue;\n\nif (showValue) {\n var valueFont = ctx.properties.valueFont;\n var valueColor = ctx.properties.valueColor;\n var level = ctx.values.level; \n \n var levelText = ctx.api.formatValue(level, ctx.properties.valueDecimals, ctx.properties.valueUnits, false);\n\n ctx.api.font(element, valueFont, valueColor);\n ctx.api.text(element, levelText);\n\n} else {\n element.hide();\n ctx.tags.levelValueBackground[0].hide();\n}",
"actions": {
"click": {
"actionFunction": "ctx.api.callAction(event, 'levelValueClick');"
}
],
"behavior": [
{
"id": "level",
"name": "Level",
"type": "value",
"valueType": "DOUBLE",
"defaultValue": 0
},
{
"id": "disabled",
"name": "{i18n:widgets.rpc-state.disabled-state}",
"hint": "{i18n:widgets.rpc-state.disabled-state-hint}",
"stateLabel": "{i18n:widgets.rpc-state.disabled}",
"type": "value",
"valueType": "BOOLEAN",
"defaultValue": false
},
{
"id": "on",
"name": "On/Off state",
"trueLabel": "{i18n:widgets.rpc-state.on}",
"falseLabel": "{i18n:widgets.rpc-state.off}",
"stateLabel": "{i18n:widgets.rpc-state.on}",
"type": "value",
"valueType": "BOOLEAN",
"defaultValue": false
},
{
"id": "onUpdateState",
"name": "{i18n:widgets.rpc-state.turn-on}",
"hint": "{i18n:widgets.rpc-state.turn-on-hint}",
"type": "action",
"valueType": "BOOLEAN",
"valueToDataType": "CONSTANT",
"constantValue": true
},
{
"id": "offUpdateState",
"name": "{i18n:widgets.rpc-state.turn-off}",
"hint": "{i18n:widgets.rpc-state.turn-off-hint}",
"type": "action",
"valueType": "BOOLEAN",
"valueToDataType": "CONSTANT",
"constantValue": false
},
{
"id": "levelUpdateState",
"name": "Update level",
"type": "action",
"valueType": "DOUBLE",
"valueToDataType": "VALUE",
"constantValue": 0
},
{
"id": "levelValueClick",
"name": "On level value click",
"type": "widgetAction"
}
},
{
"tag": "fan",
"stateRenderFunction": "var on = ctx.values.on;\nif (on) {\n var level = ctx.values.level; \n var minLevel = ctx.properties.minLevel; \n var maxLevel = ctx.properties.maxLevel;\n\n var speed = (level - minLevel) / (maxLevel - minLevel);\n speed = Math.max(0, Math.min(1, speed))*2; \n\n var t = element.timeline();\n if (!t.active()) {\n ctx.api.animate(element, 1000).ease('-').rotate(360).loop();\n t = element.timeline();\n }\n t.speed(speed);\n} else {\n element.timeline().stop();\n}\n",
"actions": null
},
{
"tag": "onButton",
"actions": {
"click": {
"actionFunction": "ctx.api.callAction(event, 'onUpdateState', undefined, {\n next: () => {\n ctx.api.setValue('on', true);\n }\n});\n\n"
}
],
"properties": [
{
"id": "showLevelTitle",
"name": "Level title",
"type": "switch",
"default": true,
"rowClass": "column-xs"
},
{
"id": "levelTitle",
"name": "Level title",
"type": "string",
"default": "{i18n:widgets.battery-level.value}",
"disableOnProperty": "showLevelTitle",
"fieldClass": "flex"
},
{
"id": "levelTitleFont",
"name": "Level title",
"type": "font",
"default": {
"size": 6,
"sizeUnit": "px",
"family": "Roboto",
"weight": "normal",
"style": "normal"
},
"disableOnProperty": "showLevelTitle"
},
{
"id": "levelTitleColor",
"name": "Level title",
"type": "color",
"default": "#000000",
"disableOnProperty": "showLevelTitle"
},
{
"id": "showValue",
"name": "Value",
"type": "switch",
"default": true,
"rowClass": "column-xs"
},
{
"id": "valueUnits",
"name": "Value",
"type": "units",
"default": "",
"disableOnProperty": "showValue",
"fieldClass": "flex"
},
{
"id": "valueDecimals",
"name": "Value",
"type": "number",
"default": 2,
"min": 0,
"max": 15,
"step": 1,
"fieldSuffix": "decimals",
"disableOnProperty": "showValue",
"fieldClass": "flex"
},
{
"id": "valueFont",
"name": "Value",
"type": "font",
"default": {
"size": 6,
"sizeUnit": "px",
"family": "Roboto",
"weight": "normal",
"style": "normal"
},
"disableOnProperty": "showValue"
},
{
"id": "valueColor",
"name": "Value",
"type": "color",
"default": "#000000",
"disableOnProperty": "showValue"
},
{
"id": "minLevel",
"name": "Level range",
"subLabel": "min",
"type": "number",
"required": true,
"default": 0,
"min": 0,
"rowClass": "column-xs",
"fieldClass": "flex-xs"
},
{
"id": "maxLevel",
"name": "Level range",
"subLabel": "max",
"type": "number",
"required": true,
"default": 100,
"min": 0,
"fieldClass": "flex-xs"
},
{
"id": "showMinMaxLevel",
"name": "Min/Max label",
"type": "switch",
"default": true
},
{
"id": "minMaxLevelFont",
"name": "Min/Max label",
"type": "font",
"default": {
"size": 6,
"sizeUnit": "px",
"family": "Roboto",
"weight": "normal",
"style": "normal"
},
"disableOnProperty": "showMinMaxLevel"
},
{
"id": "minMaxLevelColor",
"name": "Min/Max label",
"type": "color",
"default": "#666",
"disableOnProperty": "showMinMaxLevel"
},
{
"id": "levelBackground",
"name": "Level background",
"subLabel": "Enabled",
"type": "color-settings",
"default": {
"type": "constant",
"color": "#1abb48",
"colorFunction": "return value &gt; 70 ? '#d5280d' : '#1abb48';"
},
"divider": true,
"rowClass": "column-xs"
},
{
"id": "disabledLevelBackground",
"name": "Level background",
"subLabel": "Disabled",
"type": "color",
"default": "#ccc"
}
]
}]]></tb:metadata>
<style>.fan-rotate {
animation: rotate 2s linear infinite;
transform-origin: 71.894px 39.787px;
}
@keyframes rotate {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}</style>
<rect width="100" height="100" rx="0" fill="none" stroke="#ccc" stroke-width="2"/>
<rect x="8" y="15" width="20" height="81" rx="2" fill="#ececec" stroke="#000" stroke-width="1.0359"/>
<rect transform="scale(1 -1)" x="8.5" y="-95.5" width="19" height="40" rx="1.5" fill="#1abb48" tb:tag="level"/>
<text x="32" y="95" fill="#666" font-family="Roboto" font-size="6px" tb:tag="minLevel" xml:space="preserve"><tspan x="31.354307" y="96.626251">min</tspan></text>
<rect x="10.941" y="49.544" width="14.155" height="12" rx="2.8668" fill="#fff" fill-opacity=".45148" tb:tag="levelValueBackground"/>
<g font-family="Roboto" font-size="6px">
}
},
{
"tag": "offButton",
"actions": {
"click": {
"actionFunction": "ctx.api.callAction(event, 'offUpdateState', undefined, {\n next: () => {\n ctx.api.setValue('on', false);\n }\n});\n"
}
}
},
{
"tag": "levelUpButton",
"actions": {
"click": {
"actionFunction": "var level = ctx.values.level; \nvar maxLevel = ctx.properties.maxLevel;\n\nvar newLevel = Math.min(maxLevel, level + 5);\nctx.api.setValue('level', newLevel);\nctx.api.callAction(event, 'levelUpdateState', newLevel, {\n error: () => {\n ctx.api.setValue('level', level);\n }\n});"
}
}
},
{
"tag": "levelDownButton",
"actions": {
"click": {
"actionFunction": "var level = ctx.values.level; \nvar minLevel = ctx.properties.minLevel;\n\nvar newLevel = Math.max(minLevel, level - 5);\nctx.api.setValue('level', newLevel);\nctx.api.callAction(event, 'levelUpdateState', newLevel, {\n error: () => {\n ctx.api.setValue('level', level);\n }\n});"
}
}
},
{
"tag": "ramka",
"stateRenderFunction": "var on = ctx.values.on;\nif (on) {\n if (!element.timeline().active()) {\n ctx.api.animate(element, 1000).scale(1.1).loop(0, true);\n }\n} else {\n element.timeline().stop();\n}",
"actions": null
}
],
"behavior": [
{
"id": "level",
"name": "Level",
"hint": null,
"type": "value",
"valueType": "DOUBLE",
"defaultValue": 10,
"trueLabel": null,
"falseLabel": null,
"stateLabel": null,
"valueToDataType": null,
"constantValue": null,
"valueToDataFunction": null
},
{
"id": "disabled",
"name": "{i18n:widgets.rpc-state.disabled-state}",
"hint": "{i18n:widgets.rpc-state.disabled-state-hint}",
"type": "value",
"valueType": "BOOLEAN",
"defaultValue": false,
"trueLabel": "",
"falseLabel": "",
"stateLabel": "{i18n:widgets.rpc-state.disabled}",
"valueToDataType": null,
"constantValue": null,
"valueToDataFunction": null
},
{
"id": "on",
"name": "On/Off state",
"hint": null,
"type": "value",
"valueType": "BOOLEAN",
"defaultValue": true,
"trueLabel": "{i18n:widgets.rpc-state.on}",
"falseLabel": "{i18n:widgets.rpc-state.off}",
"stateLabel": "{i18n:widgets.rpc-state.on}",
"valueToDataType": null,
"constantValue": null,
"valueToDataFunction": null
},
{
"id": "onUpdateState",
"name": "{i18n:widgets.rpc-state.turn-on}",
"hint": "{i18n:widgets.rpc-state.turn-on-hint}",
"type": "action",
"valueType": "BOOLEAN",
"valueToDataType": "CONSTANT",
"constantValue": true
},
{
"id": "offUpdateState",
"name": "{i18n:widgets.rpc-state.turn-off}",
"hint": "{i18n:widgets.rpc-state.turn-off-hint}",
"type": "action",
"valueType": "BOOLEAN",
"valueToDataType": "CONSTANT",
"constantValue": false
},
{
"id": "levelUpdateState",
"name": "Update level",
"type": "action",
"valueType": "DOUBLE",
"valueToDataType": "VALUE",
"constantValue": 0
},
{
"id": "levelValueClick",
"name": "On level value click",
"type": "widgetAction"
}
],
"properties": [
{
"id": "showLevelTitle",
"name": "Level title",
"type": "switch",
"default": true,
"rowClass": "column-xs"
},
{
"id": "levelTitle",
"name": "Level title",
"type": "text",
"default": "{i18n:widgets.battery-level.value}",
"disableOnProperty": "showLevelTitle",
"fieldClass": "flex"
},
{
"id": "levelTitleFont",
"name": "Level title",
"type": "font",
"default": {
"size": 6,
"sizeUnit": "px",
"family": "Roboto",
"weight": "normal",
"style": "normal"
},
"disableOnProperty": "showLevelTitle"
},
{
"id": "levelTitleColor",
"name": "Level title",
"type": "color",
"default": "#000000",
"disableOnProperty": "showLevelTitle"
},
{
"id": "showValue",
"name": "Value",
"type": "switch",
"default": true,
"rowClass": "column-xs"
},
{
"id": "valueUnits",
"name": "Value",
"type": "units",
"default": "",
"disableOnProperty": "showValue",
"fieldClass": "flex"
},
{
"id": "valueDecimals",
"name": "Value",
"type": "number",
"default": 2,
"min": 0,
"max": 15,
"step": 1,
"fieldSuffix": "decimals",
"disableOnProperty": "showValue",
"fieldClass": "flex"
},
{
"id": "valueFont",
"name": "Value",
"type": "font",
"default": {
"size": 6,
"sizeUnit": "px",
"family": "Roboto",
"weight": "normal",
"style": "normal"
},
"disableOnProperty": "showValue"
},
{
"id": "valueColor",
"name": "Value",
"type": "color",
"default": "#000000",
"disableOnProperty": "showValue"
},
{
"id": "minLevel",
"name": "Level range",
"subLabel": "min",
"type": "number",
"required": true,
"default": 0,
"min": 0,
"rowClass": "column-xs",
"fieldClass": "flex-xs"
},
{
"id": "maxLevel",
"name": "Level range",
"subLabel": "max",
"type": "number",
"required": true,
"default": 100,
"min": 0,
"fieldClass": "flex-xs"
},
{
"id": "showMinMaxLevel",
"name": "Min/Max label",
"type": "switch",
"default": true
},
{
"id": "minMaxLevelFont",
"name": "Min/Max label",
"type": "font",
"default": {
"size": 6,
"sizeUnit": "px",
"family": "Roboto",
"weight": "normal",
"style": "normal"
},
"disableOnProperty": "showMinMaxLevel"
},
{
"id": "minMaxLevelColor",
"name": "Min/Max label",
"type": "color",
"default": "#666",
"disableOnProperty": "showMinMaxLevel"
},
{
"id": "levelBackground",
"name": "Level background",
"subLabel": "Enabled",
"type": "color_settings",
"default": {
"type": "constant",
"color": "#1abb48",
"colorFunction": "return value > 70 ? '#d5280d' : '#1abb48';"
},
"divider": true,
"rowClass": "column-xs"
},
{
"id": "disabledLevelBackground",
"name": "Level background",
"subLabel": "Disabled",
"type": "color",
"default": "#ccc"
}
]
}]]></tb:metadata>
<rect width="100" height="100" rx="0" fill="none" stroke="#ccc" stroke-width="2"/><rect x="8" y="15" width="20" height="81" rx="2" fill="#ececec" stroke="#000" stroke-width="1.0359"/><rect transform="scale(1 -1)" x="8.5" y="-95.5" width="19" height="40" rx="1.5" fill="#1abb48" tb:tag="level"/><text x="32" y="95" fill="#666" font-family="Roboto" font-size="6px" tb:tag="minLevel" xml:space="preserve"><tspan x="31.354307" y="96.626251">min</tspan></text><rect x="10.941" y="49.544" width="14.155" height="12" rx="2.8668" fill="#fff" fill-opacity=".45148" tb:tag="levelValueBackground"/><g font-family="Roboto" font-size="6px">
<text x="18" y="56" dominant-baseline="middle" fill="#000000" text-anchor="middle" tb:tag="levelValue" xml:space="preserve"><tspan>N/A</tspan></text>
<text x="17.636805" y="10.347933" dominant-baseline="middle" fill="#000000" text-anchor="middle" tb:tag="levelTitle" xml:space="preserve"><tspan>Level</tspan></text>
<text x="32" y="20" fill="#666666" tb:tag="maxLevel" xml:space="preserve"><tspan x="29.865496" y="18.716127">max</tspan></text>
</g>
<path d="m82.143 27.538h-20.499c-1.1045 0-1.9999 0.8954-1.9999 1.9999v20.499c0 1.1045 0.8954 1.9999 1.9999 1.9999h20.499c1.1045 0 1.9999-0.8954 1.9999-1.9999v-20.499c0-1.1045-0.8954-1.9999-1.9999-1.9999zm-20.499 1.4999c0.27613 0 0.49998 0.22385 0.49998 0.49998s-0.22385 0.49998-0.49998 0.49998-0.49998-0.22385-0.49998-0.49998 0.22385-0.49998 0.49998-0.49998zm0 21.499c-0.27613 0-0.49998-0.22385-0.49998-0.49998s0.22385-0.49998 0.49998-0.49998 0.49998 0.22385 0.49998 0.49998-0.22385 0.49998-0.49998 0.49998zm10.249-0.79997c-5.495 0-9.9495-4.4545-9.9495-9.9495s4.4545-9.9495 9.9495-9.9495 9.9495 4.4545 9.9495 9.9495-4.4545 9.9495-9.9495 9.9495zm10.249 0.79997c-0.27613 0-0.49998-0.22385-0.49998-0.49998s0.22385-0.49998 0.49998-0.49998 0.49998 0.22385 0.49998 0.49998-0.22385 0.49998-0.49998 0.49998zm0-20.499c-0.27613 0-0.49998-0.22385-0.49998-0.49998s0.22385-0.49998 0.49998-0.49998 0.49998 0.22385 0.49998 0.49998-0.22385 0.49998-0.49998 0.49998z" fill="#080d7d" stroke-width=".040031"/>
<path class="fan-rotate" d="m80.66 37.983c-0.04956-0.24078-0.15288-0.4126-0.34242-0.56916-1.5148-1.2518-3.6154-1.1677-6.3021 0.25239-0.17533-0.17533-0.36512-0.32353-0.5778-0.45114 1.8517-1.6612 3.5396-2.3586 5.0637-2.0924 0.44934 0.0785 0.7605-0.43637 0.48209-0.79765-0.61795-0.80201-1.3212-1.455-2.1668-2.0119-0.20536-0.13522-0.39987-0.18358-0.64461-0.16032-1.9563 0.18594-3.3822 1.7308-4.2778 4.6347-0.24799 0-0.48697 0.02938-0.72756 0.08955 0.1347-2.484 0.83504-4.1707 2.101-5.0601 0.37324-0.2622 0.22918-0.84629-0.22313-0.90497-1.0041-0.13014-1.9631-0.09459-2.9548 0.10956-0.24078 0.04956-0.4126 0.15288-0.56916 0.34242-1.2518 1.5148-1.1677 3.6154 0.25239 6.3021-0.17533 0.17533-0.32353 0.36512-0.45114 0.5778-1.6612-1.8517-2.3586-3.5396-2.0924-5.0637 0.0785-0.44934-0.43637-0.7605-0.79765-0.48213-0.80201 0.61795-1.455 1.3212-2.0119 2.1668-0.13522 0.20536-0.18358 0.39986-0.16032 0.64461 0.18594 1.9563 1.7308 3.3822 4.6347 4.2778 0 0.24799 0.02938 0.48697 0.08955 0.72756-2.484-0.1347-4.1707-0.83504-5.0601-2.101-0.2622-0.37324-0.84629-0.22918-0.90497 0.22313-0.13014 1.0041-0.09459 1.9631 0.10956 2.9548 0.04956 0.24078 0.15288 0.4126 0.34242 0.56916 1.5148 1.2518 3.6154 1.1677 6.3021-0.25239 0.17533 0.17533 0.36512 0.32353 0.5778 0.45114-1.8517 1.6612-3.5396 2.3586-5.0637 2.0924-0.44934-0.0785-0.7605 0.43637-0.48209 0.79765 0.61795 0.80201 1.3212 1.455 2.1668 2.0119 0.20536 0.13522 0.39986 0.18358 0.64461 0.16032 1.9563-0.18594 3.3822-1.7308 4.2778-4.6347 0.24799 0 0.48697-0.02938 0.72756-0.08959-0.1347 2.484-0.83504 4.1707-2.101 5.0601-0.37324 0.2622-0.22918 0.84629 0.22313 0.90497 1.0041 0.13014 1.9631 0.09459 2.9548-0.10956 0.24078-0.04956 0.4126-0.15288 0.56916-0.34242 1.2518-1.5148 1.1677-3.6154-0.25239-6.3021 0.17533-0.17533 0.32353-0.36512 0.45114-0.5778 1.6612 1.8517 2.3586 3.5396 2.0924 5.0637-0.0785 0.44934 0.43637 0.7605 0.79765 0.48209 0.80201-0.61795 1.455-1.3212 2.0119-2.1668 0.13522-0.20536 0.18358-0.39987 0.16032-0.64461-0.18594-1.9563-1.7308-3.3822-4.6347-4.2778 0-0.24799-0.02938-0.48697-0.08955-0.72756 2.484 0.1347 4.1707 0.83504 5.0601 2.101 0.2622 0.37324 0.84629 0.22918 0.90497-0.22313 0.13014-1.004 0.09459-1.963-0.10956-2.9547zm-8.7657 4.3043c-1.3806 0-2.4999-1.1193-2.4999-2.4999s1.1193-2.4999 2.4999-2.4999 2.4999 1.1193 2.4999 2.4999-1.1193 2.4999-2.4999 2.4999z" fill="#7d081e" stroke-width=".040031" tb:tag="fan"/>
<g tb:tag="onButton">
</g><path d="m82.143 27.538h-20.499c-1.1045 0-1.9999 0.8954-1.9999 1.9999v20.499c0 1.1045 0.8954 1.9999 1.9999 1.9999h20.499c1.1045 0 1.9999-0.8954 1.9999-1.9999v-20.499c0-1.1045-0.8954-1.9999-1.9999-1.9999zm-20.499 1.4999c0.27613 0 0.49998 0.22385 0.49998 0.49998s-0.22385 0.49998-0.49998 0.49998-0.49998-0.22385-0.49998-0.49998 0.22385-0.49998 0.49998-0.49998zm0 21.499c-0.27613 0-0.49998-0.22385-0.49998-0.49998s0.22385-0.49998 0.49998-0.49998 0.49998 0.22385 0.49998 0.49998-0.22385 0.49998-0.49998 0.49998zm10.249-0.79997c-5.495 0-9.9495-4.4545-9.9495-9.9495s4.4545-9.9495 9.9495-9.9495 9.9495 4.4545 9.9495 9.9495-4.4545 9.9495-9.9495 9.9495zm10.249 0.79997c-0.27613 0-0.49998-0.22385-0.49998-0.49998s0.22385-0.49998 0.49998-0.49998 0.49998 0.22385 0.49998 0.49998-0.22385 0.49998-0.49998 0.49998zm0-20.499c-0.27613 0-0.49998-0.22385-0.49998-0.49998s0.22385-0.49998 0.49998-0.49998 0.49998 0.22385 0.49998 0.49998-0.22385 0.49998-0.49998 0.49998z" fill="#080d7d" stroke-width=".040031" tb:tag="ramka"/><path d="m80.66 37.983c-0.04956-0.24078-0.15288-0.4126-0.34242-0.56916-1.5148-1.2518-3.6154-1.1677-6.3021 0.25239-0.17533-0.17533-0.36512-0.32353-0.5778-0.45114 1.8517-1.6612 3.5396-2.3586 5.0637-2.0924 0.44934 0.0785 0.7605-0.43637 0.48209-0.79765-0.61795-0.80201-1.3212-1.455-2.1668-2.0119-0.20536-0.13522-0.39987-0.18358-0.64461-0.16032-1.9563 0.18594-3.3822 1.7308-4.2778 4.6347-0.24799 0-0.48697 0.02938-0.72756 0.08955 0.1347-2.484 0.83504-4.1707 2.101-5.0601 0.37324-0.2622 0.22918-0.84629-0.22313-0.90497-1.0041-0.13014-1.9631-0.09459-2.9548 0.10956-0.24078 0.04956-0.4126 0.15288-0.56916 0.34242-1.2518 1.5148-1.1677 3.6154 0.25239 6.3021-0.17533 0.17533-0.32353 0.36512-0.45114 0.5778-1.6612-1.8517-2.3586-3.5396-2.0924-5.0637 0.0785-0.44934-0.43637-0.7605-0.79765-0.48213-0.80201 0.61795-1.455 1.3212-2.0119 2.1668-0.13522 0.20536-0.18358 0.39986-0.16032 0.64461 0.18594 1.9563 1.7308 3.3822 4.6347 4.2778 0 0.24799 0.02938 0.48697 0.08955 0.72756-2.484-0.1347-4.1707-0.83504-5.0601-2.101-0.2622-0.37324-0.84629-0.22918-0.90497 0.22313-0.13014 1.0041-0.09459 1.9631 0.10956 2.9548 0.04956 0.24078 0.15288 0.4126 0.34242 0.56916 1.5148 1.2518 3.6154 1.1677 6.3021-0.25239 0.17533 0.17533 0.36512 0.32353 0.5778 0.45114-1.8517 1.6612-3.5396 2.3586-5.0637 2.0924-0.44934-0.0785-0.7605 0.43637-0.48209 0.79765 0.61795 0.80201 1.3212 1.455 2.1668 2.0119 0.20536 0.13522 0.39986 0.18358 0.64461 0.16032 1.9563-0.18594 3.3822-1.7308 4.2778-4.6347 0.24799 0 0.48697-0.02938 0.72756-0.08959-0.1347 2.484-0.83504 4.1707-2.101 5.0601-0.37324 0.2622-0.22918 0.84629 0.22313 0.90497 1.0041 0.13014 1.9631 0.09459 2.9548-0.10956 0.24078-0.04956 0.4126-0.15288 0.56916-0.34242 1.2518-1.5148 1.1677-3.6154-0.25239-6.3021 0.17533-0.17533 0.32353-0.36512 0.45114-0.5778 1.6612 1.8517 2.3586 3.5396 2.0924 5.0637-0.0785 0.44934 0.43637 0.7605 0.79765 0.48209 0.80201-0.61795 1.455-1.3212 2.0119-2.1668 0.13522-0.20536 0.18358-0.39987 0.16032-0.64461-0.18594-1.9563-1.7308-3.3822-4.6347-4.2778 0-0.24799-0.02938-0.48697-0.08955-0.72756 2.484 0.1347 4.1707 0.83504 5.0601 2.101 0.2622 0.37324 0.84629 0.22918 0.90497-0.22313 0.13014-1.004 0.09459-1.963-0.10956-2.9547zm-8.7657 4.3043c-1.3806 0-2.4999-1.1193-2.4999-2.4999s1.1193-2.4999 2.4999-2.4999 2.4999 1.1193 2.4999 2.4999-1.1193 2.4999-2.4999 2.4999z" fill="#7d081e" stroke-width=".040031" tb:tag="fan"/><g tb:tag="onButton">
<rect x="54.702" y="60.372" width="14.263" height="7.4261" rx="1.5" fill="#12ed19" stroke="#000" tb:tag="onButtonBackground"/>
<text x="61.855518" y="64.49128" dominant-baseline="middle" fill="#000000" font-family="Roboto" font-size="4.4461px" stroke-width=".74101" text-anchor="middle" xml:space="preserve"><tspan stroke-width=".74101">On</tspan></text>
</g>
<g tb:tag="offButton">
</g><g tb:tag="offButton">
<rect x="74.367" y="60.311" width="14.263" height="7.4261" rx="1.5" fill="#ed121f" stroke="#000" tb:tag="offButtonBackground"/>
<text x="81.365944" y="64.518349" dominant-baseline="middle" fill="#000000" font-family="Roboto" font-size="4.4461px" stroke-width=".74101" text-anchor="middle" xml:space="preserve"><tspan stroke-width=".74101">Off</tspan></text>
</g>
<g stroke="#000" stroke-miterlimit="1" tb:tag="levelUpButton">
</g><g stroke="#000" stroke-miterlimit="1" tb:tag="levelUpButton">
<rect x="34.97" y="39.377" width="9.5005" height="12.584" rx=".75" fill="none"/>
<path transform="matrix(.4753 -.2938 .2938 .4753 4.8821 31.04)" d="m41.544 60.24-7.893-4.9762 8.256-4.3474-0.18153 4.6618z" fill="#12ed19"/>
</g>
<g stroke="#000" stroke-miterlimit="1" tb:tag="levelDownButton">
</g><g stroke="#000" stroke-miterlimit="1" tb:tag="levelDownButton">
<rect x="34.97" y="59.039" width="9.5005" height="12.584" rx=".75" fill="none"/>
<path transform="matrix(-.47514 .29405 -.29405 -.47514 74.568 79.771)" d="m41.544 60.24-7.893-4.9762 8.256-4.3474-0.18153 4.6618z" fill="#12ed19"/>
</g>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Loading…
Cancel
Save