From ebce8698e346eab5cf1fe5c4369850573f7cc664 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 7 May 2024 19:58:48 +0300 Subject: [PATCH] UI: Refactoring. --- ui-ngx/src/app/core/api/widget-api.models.ts | 3 +- .../basic/basic-widget-config.module.ts | 10 +- .../scada-test-basic-config.component.html | 26 - .../svg/iot-svg-basic-config.component.html | 48 ++ .../iot-svg-basic-config.component.ts} | 55 +- .../widget/lib/scada/scada.models.ts | 797 ------------------ .../iot-svg-object-settings.component.html} | 15 +- .../iot-svg-object-settings.component.ts} | 34 +- .../iot-svg-object-settings.models.ts} | 8 +- .../common/widget-settings-common.module.ts | 8 +- .../iot-svg-widget.component.html} | 9 +- .../iot-svg-widget.component.scss} | 34 +- .../iot-svg-widget.component.ts} | 59 +- .../iot-svg-widget.models.ts} | 23 +- .../widget/lib/svg/iot-svg.models.ts | 634 ++++++++++++++ .../widget/widget-components.module.ts | 6 +- .../components/widget/widget.component.ts | 35 +- .../assets/widget/{scada => svg}/drawing.svg | 382 ++------- 18 files changed, 970 insertions(+), 1216 deletions(-) delete mode 100644 ui-ngx/src/app/modules/home/components/widget/config/basic/scada/scada-test-basic-config.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/config/basic/svg/iot-svg-basic-config.component.html rename ui-ngx/src/app/modules/home/components/widget/config/basic/{scada/scada-test-basic-config.component.ts => svg/iot-svg-basic-config.component.ts} (50%) delete mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/scada/scada.models.ts rename ui-ngx/src/app/modules/home/components/widget/lib/settings/common/{scada/scada-object-settings.component.html => svg/iot-svg-object-settings.component.html} (87%) rename ui-ngx/src/app/modules/home/components/widget/lib/settings/common/{scada/scada-object-settings.component.ts => svg/iot-svg-object-settings.component.ts} (88%) rename ui-ngx/src/app/modules/home/components/widget/lib/settings/common/{scada/scada-object-settings.models.ts => svg/iot-svg-object-settings.models.ts} (82%) rename ui-ngx/src/app/modules/home/components/widget/lib/{scada/scada-test-widget.component.html => svg/iot-svg-widget.component.html} (57%) rename ui-ngx/src/app/modules/home/components/widget/lib/{scada/scada-test-widget.component.scss => svg/iot-svg-widget.component.scss} (54%) rename ui-ngx/src/app/modules/home/components/widget/lib/{scada/scada-test-widget.component.ts => svg/iot-svg-widget.component.ts} (51%) rename ui-ngx/src/app/modules/home/components/widget/lib/{scada/scada-test-widget.models.ts => svg/iot-svg-widget.models.ts} (50%) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts rename ui-ngx/src/assets/widget/{scada => svg}/drawing.svg (54%) diff --git a/ui-ngx/src/app/core/api/widget-api.models.ts b/ui-ngx/src/app/core/api/widget-api.models.ts index 1fcd18bd93..0300c89d7f 100644 --- a/ui-ngx/src/app/core/api/widget-api.models.ts +++ b/ui-ngx/src/app/core/api/widget-api.models.ts @@ -23,7 +23,7 @@ import { DatasourceType, KeyInfo, LegendConfig, - LegendData, TargetDevice, + LegendData, TargetDevice, WidgetAction, WidgetActionDescriptor, widgetType } from '@shared/models/widget.models'; @@ -95,6 +95,7 @@ export interface WidgetActionsApi { getActionDescriptors: (actionSourceId: string) => Array; handleWidgetAction: ($event: Event, descriptor: WidgetActionDescriptor, entityId?: EntityId, entityName?: string, additionalParams?: any, entityLabel?: string) => void; + onWidgetAction: ($event: Event, action: WidgetAction) => void; elementClick: ($event: Event) => void; cardClick: ($event: Event) => void; click: ($event: Event) => void; diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts index 79af9bee18..6f83f9ac25 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts @@ -129,9 +129,7 @@ import { import { RadarChartBasicConfigComponent } from '@home/components/widget/config/basic/chart/radar-chart-basic-config.component'; -import { - ScadaTestBasicConfigComponent -} from '@home/components/widget/config/basic/scada/scada-test-basic-config.component'; +import { IotSvgBasicConfigComponent } from '@home/components/widget/config/basic/svg/iot-svg-basic-config.component'; @NgModule({ declarations: [ @@ -175,7 +173,7 @@ import { BarChartBasicConfigComponent, PolarAreaChartBasicConfigComponent, RadarChartBasicConfigComponent, - ScadaTestBasicConfigComponent + IotSvgBasicConfigComponent ], imports: [ CommonModule, @@ -221,7 +219,7 @@ import { BarChartBasicConfigComponent, PolarAreaChartBasicConfigComponent, RadarChartBasicConfigComponent, - ScadaTestBasicConfigComponent + IotSvgBasicConfigComponent ] }) export class BasicWidgetConfigModule { @@ -261,5 +259,5 @@ export const basicWidgetConfigComponentsMap: {[key: string]: Type - - - - - diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/svg/iot-svg-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/svg/iot-svg-basic-config.component.html new file mode 100644 index 0000000000..f0dfeacf66 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/svg/iot-svg-basic-config.component.html @@ -0,0 +1,48 @@ + + + + + +
+
widget-config.card-appearance
+
+
{{ 'widgets.background.background' | translate }}
+ + +
+
+
widget-config.show-card-buttons
+ + {{ 'fullscreen.fullscreen' | translate }} + +
+
+
{{ 'widget-config.card-border-radius' | translate }}
+ + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/scada/scada-test-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/svg/iot-svg-basic-config.component.ts similarity index 50% rename from ui-ngx/src/app/modules/home/components/widget/config/basic/scada/scada-test-basic-config.component.ts rename to ui-ngx/src/app/modules/home/components/widget/config/basic/svg/iot-svg-basic-config.component.ts index 3d6a4a6a45..cccdf03aba 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/scada/scada-test-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/svg/iot-svg-basic-config.component.ts @@ -20,22 +20,26 @@ 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, } from '@shared/models/widget.models'; +import { TargetDevice, WidgetConfig, } from '@shared/models/widget.models'; import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; -import { ScadaTestWidgetSettings } from '@home/components/widget/lib/scada/scada-test-widget.models'; +import { + iotSvgWidgetDefaultSettings, + IotSvgWidgetSettings +} from '@home/components/widget/lib/svg/iot-svg-widget.models'; +import { isUndefined } from '@core/utils'; @Component({ - selector: 'tb-scada-test-basic-config', - templateUrl: './scada-test-basic-config.component.html', + selector: 'tb-iot-svg-basic-config', + templateUrl: './iot-svg-basic-config.component.html', styleUrls: ['../basic-config.scss'] }) -export class ScadaTestBasicConfigComponent extends BasicWidgetConfigComponent { +export class IotSvgBasicConfigComponent extends BasicWidgetConfigComponent { get targetDevice(): TargetDevice { - return this.scadaTestWidgetConfigForm.get('targetDevice').value; + return this.iotSvgWidgetConfigForm.get('targetDevice').value; } - scadaTestWidgetConfigForm: UntypedFormGroup; + iotSvgWidgetConfigForm: UntypedFormGroup; constructor(protected store: Store, protected widgetConfigComponent: WidgetConfigComponent, @@ -44,21 +48,48 @@ export class ScadaTestBasicConfigComponent extends BasicWidgetConfigComponent { } protected configForm(): UntypedFormGroup { - return this.scadaTestWidgetConfigForm; + return this.iotSvgWidgetConfigForm; } protected onConfigSet(configData: WidgetConfigComponentData) { - const settings: ScadaTestWidgetSettings = {...(configData.config.settings as ScadaTestWidgetSettings || { scadaObject: {}})}; - this.scadaTestWidgetConfigForm = this.fb.group({ + const settings: IotSvgWidgetSettings = {...iotSvgWidgetDefaultSettings, ...(configData.config.settings || {})}; + this.iotSvgWidgetConfigForm = this.fb.group({ targetDevice: [configData.config.targetDevice, []], - scadaObject: [settings.scadaObject, []] + iotSvg: [settings.iotSvg, []], + iotSvgObject: [settings.iotSvgObject, []], + + background: [settings.background, []], + + cardButtons: [this.getCardButtons(configData.config), []], + borderRadius: [configData.config.borderRadius, []], + + actions: [configData.config.actions || {}, []] }); } protected prepareOutputConfig(config: any): WidgetConfigComponentData { this.widgetConfig.config.targetDevice = config.targetDevice; this.widgetConfig.config.settings = this.widgetConfig.config.settings || {}; - this.widgetConfig.config.settings.scadaObject = config.scadaObject; + this.widgetConfig.config.settings.iotSvg = config.iotSvg; + this.widgetConfig.config.settings.iotSvgObject = config.iotSvgObject; + this.widgetConfig.config.settings.background = config.background; + + this.setCardButtons(config.cardButtons, this.widgetConfig.config); + this.widgetConfig.config.borderRadius = config.borderRadius; + + this.widgetConfig.config.actions = config.actions; return this.widgetConfig; } + + 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'); + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada.models.ts deleted file mode 100644 index d0aeac27a7..0000000000 --- a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada.models.ts +++ /dev/null @@ -1,797 +0,0 @@ -/// -/// 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 { ValueType } from '@shared/models/constants'; -import { Box, Element, Runner, Svg, SVG, Timeline, Text } from '@svgdotjs/svg.js'; -import { - DataToValueType, - GetValueAction, - GetValueSettings, SetValueAction, - SetValueSettings, ValueToDataType -} from '@shared/models/action-widget-settings.models'; -import { - formatValue, - insertVariable, - isDefinedAndNotNull, - isNumber, - isNumeric, - isUndefinedOrNull, - mergeDeep, - parseFunction -} from '@core/utils'; -import { BehaviorSubject, forkJoin, Observable } from 'rxjs'; -import { map, 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'; -import { ColorProcessor, constantColor, Font } from '@shared/models/widget-settings.models'; -import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; - -export type ScadaObjectValueType = 'input' | 'constant' | 'property' | 'function' | 'valueFormat'; - -export interface ScadaObjectValueBase { - type: ScadaObjectValueType; -} - -export interface ScadaObjectValueConstant extends ScadaObjectValueBase { - constantValue?: any; -} - -export interface ScadaObjectValueProperty extends ScadaObjectValueBase { - propertyId?: string; - computedPropertyValue?: any; -} - -export interface ScadaObjectValueFunction extends ScadaObjectValueBase { - valueConvertFunction?: string; - valueConverter?: (val: any) => any; -} - -export interface ScadaObjectValueFormat extends ScadaObjectValueBase { - units?: ScadaObjectValue; - decimals?: ScadaObjectValue; - computedUnits?: string; - computedDecimals?: number; -} - -export type ScadaObjectValue = ScadaObjectValueProperty & ScadaObjectValueConstant & ScadaObjectValueFunction & ScadaObjectValueFormat; - -export interface ScadaObjectAttribute { - name: string; - value: ScadaObjectValue; -} - -export interface ScadaObjectText { - content?: ScadaObjectValue; - font?: ScadaObjectValue; - color?: ScadaObjectValue; -} - -export interface ScadaObjectElementState { - tag: string; - inputValue?: string; - show?: ScadaObjectValue; - text?: ScadaObjectText; - attributes?: ScadaObjectAttribute[]; - animate?: number; - addClass?: string; - removeClass?: string; - animationTimeline?: Timeline; -} - -export interface ScadaObjectState { - initial?: boolean; - triggerValues?: string[]; - conditionFunction?: string; - condition?: (values: {[key: string]: any}) => boolean; - state: ScadaObjectElementState[]; -} - -export interface ScadaObjectUpdateState { - updateValue: string; -} - -export enum ScadaObjectBehaviorType { - setValue = 'setValue', - getValue = 'getValue' -} - -export interface ScadaObjectBehaviorBase { - id: string; - name: string; - type: ScadaObjectBehaviorType; -} - -export interface ScadaObjectBehaviorGet extends ScadaObjectBehaviorBase { - valueType: ValueType; - defaultValue: any; - onUpdate: ScadaObjectUpdateState[]; -} - -export interface ScadaObjectBehaviorSet extends ScadaObjectBehaviorBase { - valueToDataType: ValueToDataType; - constantValue: any; - valueToDataFunction: string; -} - -export type ScadaObjectBehavior = ScadaObjectBehaviorGet | ScadaObjectBehaviorSet; - -export type ScadaObjectPropertyType = 'string' | 'number' | 'color' | 'color-settings' | 'font' | 'units' | 'switch'; - -export interface ScadaObjectPropertyBase { - id: string; - name: string; - type: ScadaObjectPropertyType; - default: any; - required?: boolean; - subLabel?: string; - divider?: boolean; - fieldSuffix?: string; - disableOnProperty?: string; - rowClass?: string; - fieldClass?: string; -} - -export interface ScadaObjectNumberProperty extends ScadaObjectPropertyBase { - min?: number; - max?: number; - step?: number; -} - -export type ScadaObjectProperty = ScadaObjectPropertyBase & ScadaObjectNumberProperty; - -export interface ScadaObjectCallBehavior { - behaviorId: string; - conditionFunction?: string; - condition?: (value: any) => boolean; -} - -export interface ScadaObjectStateValue { - initialValue: any; - callBehavior?: ScadaObjectCallBehavior[]; -} - -export type ScadaObjectElementActionTrigger = 'click'; - -export type ScadaObjectActionType = 'updateValue'; -export type ScadaObjectActionUpdateValueType = 'toggle' | 'increment' | 'constant'; - -export interface ScadaObjectElementAction { - trigger: ScadaObjectElementActionTrigger; - enabledTriggerValues?: string[]; - enabledConditionFunction?: string; - enabledCondition?: (values: {[key: string]: any}) => boolean; - actionType: ScadaObjectActionType; - updateValueId?: string; - updateValueType?: ScadaObjectActionUpdateValueType; - updateValueConstant?: any; - updateValueInc?: number; -} - -export interface ScadaObjectMetadata { - title: string; - stateValues: {[id: string]: ScadaObjectStateValue}; - actions: {[tag: string]: ScadaObjectElementAction}; - states: {[id: string]: ScadaObjectState}; - behavior: ScadaObjectBehavior[]; - properties: ScadaObjectProperty[]; -} - -export const emptyMetadata: ScadaObjectMetadata = { - title: '', - stateValues: {}, - actions: {}, - states: {}, - behavior: [], - properties: [] -}; - - -export const parseScadaObjectMetadataFromContent = (svgContent: string): ScadaObjectMetadata => { - try { - const svgDoc = new DOMParser().parseFromString(svgContent, 'image/svg+xml'); - return parseScadaObjectMetadataFromDom(svgDoc); - } catch (_e) { - return emptyMetadata; - } -}; - -const parseScadaObjectMetadataFromDom = (svgDoc: Document): ScadaObjectMetadata => { - try { - const elements = svgDoc.getElementsByTagName('tb:metadata'); - if (elements.length) { - return JSON.parse(elements[0].innerHTML); - } else { - return emptyMetadata; - } - } catch (_e) { - console.error(_e); - return emptyMetadata; - } -}; - -const defaultGetValueSettings = (get: ScadaObjectBehaviorGet): GetValueSettings => ({ - action: GetValueAction.DO_NOTHING, - defaultValue: get.defaultValue, - 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;' - } - }); - -const defaultSetValueSettings = (set: ScadaObjectBehaviorSet): SetValueSettings => ({ - action: SetValueAction.EXECUTE_RPC, - executeRpc: { - method: 'setState', - requestTimeout: 5000, - requestPersistent: false, - persistentPollingInterval: 1000 - }, - setAttribute: { - key: 'state', - scope: AttributeScope.SERVER_SCOPE - }, - putTimeSeries: { - key: 'state' - }, - valueToData: { - type: set.valueToDataType, - constantValue: set.constantValue, - valueToDataFunction: set.valueToDataFunction ? set.valueToDataFunction : - '/* Convert input boolean value to RPC parameters or attribute/time-series value */\nreturn value;' - } -}); - -export const defaultScadaObjectSettings = (metadata: ScadaObjectMetadata): ScadaObjectSettings => { - const settings: ScadaObjectSettings = {}; - for (const behavior of metadata.behavior) { - if (behavior.type === ScadaObjectBehaviorType.getValue) { - settings[behavior.id] = defaultGetValueSettings(behavior as ScadaObjectBehaviorGet); - } else if (behavior.type === ScadaObjectBehaviorType.setValue) { - settings[behavior.id] = defaultSetValueSettings(behavior as ScadaObjectBehaviorSet); - } - } - for (const property of metadata.properties) { - settings[property.id] = property.default; - } - return settings; -}; - -export type ScadaObjectSettings = {[id: string]: any}; - -export class ScadaObject { - - private metadata: ScadaObjectMetadata; - private settings: ScadaObjectSettings; - - private rootElement: HTMLElement; - private svgShape: Svg; - private box: Box; - private targetWidth: number; - private targetHeight: number; - - private loadingSubject = new BehaviorSubject(false); - private valueGetters: ValueGetter[] = []; - private valueActions: ValueAction[] = []; - private valueSetters: {[behaviorId: string]: ValueSetter} = {}; - - private stateValueSubjects: {[id: string]: BehaviorSubject} = {}; - private stateValues: {[id: string]: any} = {}; - - loading$ = this.loadingSubject.asObservable().pipe(share()); - - constructor(private ctx: WidgetContext, - private svgPath: string, - private inputSettings: ScadaObjectSettings) {} - - public init(): Observable { - return this.ctx.http.get(this.svgPath, {responseType: 'text'}).pipe( - map((inputSvgContent) => { - const doc: XMLDocument = new DOMParser().parseFromString(inputSvgContent, 'image/svg+xml'); - this.metadata = parseScadaObjectMetadataFromDom(doc); - const defaults = defaultScadaObjectSettings(this.metadata); - this.settings = mergeDeep({}, defaults, this.inputSettings || {}); - this.prepareMetadata(); - this.prepareSvgShape(doc); - this.initStates(); - }) - ); - } - - public addTo(element: HTMLElement) { - this.rootElement = element; - if (this.svgShape) { - this.svgShape.addTo(element); - } - } - - public destroy() { - for (const stateValueId of Object.keys(this.stateValueSubjects)) { - this.stateValueSubjects[stateValueId].complete(); - this.stateValueSubjects[stateValueId].unsubscribe(); - } - this.valueActions.forEach(v => v.destroy()); - this.loadingSubject.complete(); - this.loadingSubject.unsubscribe(); - } - - public setSize(targetWidth: number, targetHeight: number) { - this.targetWidth = targetWidth; - this.targetHeight = targetHeight; - if (this.svgShape) { - this.resize(); - } - } - - private prepareMetadata() { - for (const stateValueId of Object.keys(this.metadata.stateValues)) { - const stateValue = this.metadata.stateValues[stateValueId]; - if (stateValue.callBehavior) { - stateValue.callBehavior.forEach(callBehavior => { - let condition = () => true; - if (callBehavior.conditionFunction) { - try { - condition = parseFunction(this.insertVariables(callBehavior.conditionFunction), ['value']); - } catch (e) { - condition = () => false; - } - } - callBehavior.condition = condition; - }); - } - } - for (const stateId of Object.keys(this.metadata.states)) { - const state = this.metadata.states[stateId]; - let condition = () => true; - if (state.conditionFunction) { - try { - condition = parseFunction(this.insertVariables(state.conditionFunction), ['values']); - } catch (e) { - condition = () => false; - } - } - state.condition = condition; - for (const elementState of state.state) { - this.prepareValue(elementState.show); - this.prepareValue(elementState.text?.content); - this.prepareValue(elementState.text?.font); - this.prepareValue(elementState.text?.color); - if (elementState.attributes) { - for (const attribute of elementState.attributes) { - this.prepareValue(attribute.value); - } - } - } - } - } - - private prepareValue(value: ScadaObjectValue) { - if (value) { - if (value.type === 'function' && value.valueConvertFunction) { - try { - value.valueConverter = parseFunction(this.insertVariables(value.valueConvertFunction), ['value']); - } catch (e) { - value.valueConverter = (v) => v; - } - } else if (value.type === 'property') { - value.computedPropertyValue = this.getPropertyValue(value.propertyId); - } else if (value.type === 'valueFormat') { - if (value.units) { - this.prepareValue(value.units); - } - if (value.decimals) { - this.prepareValue(value.decimals); - } - } - } - } - - private insertVariables(content: string): string { - for (const property of this.metadata.properties) { - const value = this.getPropertyValue(property.id); - content = insertVariable(content, property.id, value); - } - return content; - } - - private prepareSvgShape(doc: XMLDocument) { - const elements = doc.getElementsByTagName('tb:metadata'); - for (let i=0;i(stateValue.initialValue); - this.stateValues[stateValueId] = stateValue.initialValue; - this.stateValueSubjects[stateValueId].subscribe(val => { - this.stateValues[stateValueId] = val; - const states = Object.values(this.metadata.states); - const triggerStates = states.filter(s => s.triggerValues && s.triggerValues.includes(stateValueId)); - for (const state of triggerStates) { - if (state.condition(this.stateValues)) { - this.updateState(state); - } - } - }); - } - for (const tag of Object.keys(this.metadata.actions)) { - const action = this.metadata.actions[tag]; - const elements = this.svgShape.find(`[tb\\:tag="${tag}"]`); - switch (action.trigger) { - case 'click': - elements.forEach(e => { - e.attr('cursor', 'pointer'); - e.on('click', () => { - this.triggerAction(action); - }); - }); - break; - } - } - for (const behavior of this.metadata.behavior) { - if (behavior.type === ScadaObjectBehaviorType.getValue) { - const getBehavior = behavior as ScadaObjectBehaviorGet; - let getValueSettings: GetValueSettings = this.settings[getBehavior.id]; - getValueSettings = {...getValueSettings, actionLabel: getBehavior.name}; - const valueGetter = - ValueGetter.fromSettings(this.ctx, getValueSettings, getBehavior.valueType, { - next: (val) => {this.onValue(getBehavior.id, val);}, - error: (e) => {} - }); - this.valueGetters.push(valueGetter); - this.valueActions.push(valueGetter); - } else if (behavior.type === ScadaObjectBehaviorType.setValue) { - const setBehavior = behavior as ScadaObjectBehaviorSet; - let setValueSettings: SetValueSettings = this.settings[setBehavior.id]; - setValueSettings = {...setValueSettings, actionLabel: setBehavior.name}; - const valueSetter = ValueSetter.fromSettings(this.ctx, setValueSettings); - this.valueSetters[setBehavior.id] = valueSetter; - this.valueActions.push(valueSetter); - } - } - const initialState = Object.values(this.metadata.states).find(s => s.initial); - if (initialState) { - this.updateState(initialState); - } - if (this.valueGetters.length) { - const getValueObservables: Array> = []; - this.valueGetters.forEach(valueGetter => { - getValueObservables.push(valueGetter.getValue()); - }); - this.loadingSubject.next(true); - forkJoin(getValueObservables).subscribe( - { - next: () => { - this.loadingSubject.next(false); - }, - error: () => { - this.loadingSubject.next(false); - } - } - ); - } - } - - private triggerAction(action: ScadaObjectElementAction) { - switch (action.actionType) { - case 'updateValue': - const targetValue = action.updateValueId; - const valueSubject = this.stateValueSubjects[targetValue]; - if (valueSubject) { - const currentVal = valueSubject.value; - let newValue: any; - switch (action.updateValueType) { - case 'toggle': - newValue = !currentVal; - break; - case 'increment': - if (isNumber(currentVal)) { - newValue = currentVal + action.updateValueInc; - } - break; - case 'constant': - newValue = action.updateValueConstant; - break; - } - if (isDefinedAndNotNull(newValue)) { - valueSubject.next(newValue); - const stateValue = this.metadata.stateValues[targetValue]; - if (stateValue.callBehavior) { - const observables: Observable[] = []; - for (const behavior of stateValue.callBehavior) { - if (behavior.condition(newValue)) { - const valueSetter = this.valueSetters[behavior.behaviorId]; - observables.push(valueSetter.setValue(newValue)); - } - } - if (observables.length) { - this.loadingSubject.next(true); - forkJoin(observables).subscribe( - { - next: () => { - this.loadingSubject.next(false); - }, - error: (err) => { - this.loadingSubject.next(false); - valueSubject.next(currentVal); - } - } - ); - } - } - } - } - break; - } - } - - private resize() { - let scale: number; - if (this.targetWidth < this.targetHeight) { - scale = this.targetWidth / this.box.width; - } else { - scale = this.targetHeight / this.box.height; - } - this.svgShape.node.style.transform = `scale(${scale})`; - } - - private onValue(id: string, value: any) { - const getBehavior = this.metadata.behavior.find(b => b.id === id) as ScadaObjectBehaviorGet; - value = this.normalizeValue(value, getBehavior.valueType); - for (const onUpdate of getBehavior.onUpdate) { - const targetStateValueId = onUpdate.updateValue; - this.stateValueSubjects[targetStateValueId].next(value); - } - } - - private updateState(state: ScadaObjectState) { - if (state) { - for (const elementState of state.state) { - const tag = elementState.tag; - let value; - if (elementState.inputValue) { - value = this.stateValues[elementState.inputValue]; - } - if (elementState.animationTimeline) { - elementState.animationTimeline.finish(); - } - const elements = this.svgShape.find(`[tb\\:tag="${tag}"]`); - if (elements.length) { - if (elementState.show) { - const show: boolean = this.computeValue(elementState.show, value); - elements.forEach(e => { - if (show) { - e.show(); - } else { - e.hide(); - } - }); - } - if (elementState.addClass) { - elements.forEach(e => { - e.addClass(elementState.addClass); - }); - } - if (elementState.removeClass) { - elements.forEach(e => { - e.removeClass(elementState.removeClass); - }); - } - if (elementState.attributes) { - const attrs = this.computeAttributes(elementState.attributes, value); - elements.forEach(e => { - this.setElementAttributes(elementState, e, attrs, elementState.animate); - }); - } - if (elementState.text) { - if (elementState.text.content) { - const text: string = this.computeValue(elementState.text.content, value); - elements.forEach(e => { - this.setElementText(e, text); - }); - } - if (elementState.text.font || elementState.text.color) { - let font: Font = this.computeValue(elementState.text.font, value); - if (typeof font !== 'object') { - font = undefined; - } - let color: string = this.computeValue(elementState.text.color, value); - if (typeof color !== 'string') { - color = undefined; - } - elements.forEach(e => { - this.setElementFont(e, font, color); - }); - } - } - } - } - } - } - - private normalizeValue(value: any, type: ValueType): any { - if (isUndefinedOrNull(value)) { - switch (type) { - case ValueType.STRING: - return ''; - case ValueType.INTEGER: - case ValueType.DOUBLE: - return 0; - case ValueType.BOOLEAN: - return false; - case ValueType.JSON: - return {}; - } - } else { - return value; - } - } - - private computeAttributes(attributes: ScadaObjectAttribute[], value: any): {[attr: string]: any} { - const res: {[attr: string]: any} = {}; - for (const attribute of attributes) { - const attr = attribute.name; - res[attr] = this.computeValue(attribute.value, value); - } - return res; - } - - private setElementAttributes(elementState: ScadaObjectElementState, element: Element, attrs: {[attr: string]: any}, animate?: number) { - if (isDefinedAndNotNull(animate)) { - this.animation(elementState, element, animate).attr(attrs); - } else { - element.attr(attrs); - } - } - - private setElementText(element: Element, text: string) { - let textElement: Text; - if (element.type === 'text') { - const children = element.children(); - if (children.length && children[0].type === 'tspan') { - textElement = children[0] as Text; - } else { - textElement = element as Text; - } - } else if (element.type === 'tspan') { - textElement = element as Text; - } - if (textElement) { - textElement.text(text); - } - } - - private setElementFont(element: Element, font: Font, color: string) { - if (element.type === 'text') { - const textElement = element as Text; - if (font) { - textElement.font({ - family: font.family, - size: (isDefinedAndNotNull(font.size) && isDefinedAndNotNull(font.sizeUnit)) ? - font.size + font.sizeUnit : null, - weight: font.weight, - style: font.style - }); - } - if (color) { - textElement.fill(color); - } - } - } - - private animation(elementState: ScadaObjectElementState, element: Element, duration: number): Runner { - if (!elementState.animationTimeline) { - elementState.animationTimeline = new Timeline(); - } - element.timeline(elementState.animationTimeline); - return element.animate(duration, 0, 'now'); - } - - private computeValue(objectValue: ScadaObjectValue, value: any): any { - if (objectValue) { - switch (objectValue.type) { - case 'input': - return value; - case 'constant': - return objectValue.constantValue; - case 'property': - const property = this.getProperty(objectValue.propertyId); - if (property.type === 'color-settings') { - const colorProcessor: ColorProcessor = objectValue.computedPropertyValue; - colorProcessor.update(value); - return colorProcessor.color; - } else { - return objectValue.computedPropertyValue; - } - case 'function': - try { - return objectValue.valueConverter(value); - } catch (_e) { - return value; - } - case 'valueFormat': - let units = ''; - let decimals = 0; - if (objectValue.units) { - units = this.computeValue(objectValue.units, value); - } - if (objectValue.decimals) { - decimals = this.computeValue(objectValue.decimals, value); - } - return formatValue(value, decimals, units, false); - } - } else { - return ''; - } - } - - private getProperty(id: string): ScadaObjectProperty { - return this.metadata.properties.find(p => p.id === id); - } - - private getPropertyValue(id: string): any { - const property = this.getProperty(id); - if (property) { - const value = this.settings[id]; - if (isDefinedAndNotNull(value)) { - if (property.type === 'color-settings') { - return ColorProcessor.fromSettings(value); - } - return value; - } else { - switch (property.type) { - case 'string': - return ''; - case 'number': - return 0; - case 'color': - return '#000'; - case 'color-settings': - return constantColor('#000'); - } - } - } else { - return ''; - } - } -} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.component.html similarity index 87% rename from ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.component.html rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.component.html index 972a5c532c..6d4345825e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.component.html @@ -21,7 +21,7 @@
widgets.slider.behavior
{{ behaviour.name }}
- - + formControlName="{{ behaviour.id }}"> + + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.component.ts similarity index 88% rename from ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.component.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.component.ts index 3e0bd7409d..32cec1be22 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.component.ts @@ -31,10 +31,10 @@ import { AppState } from '@core/core.state'; import { defaultScadaObjectSettings, parseScadaObjectMetadataFromContent, - ScadaObjectBehaviorType, - ScadaObjectMetadata, - ScadaObjectSettings -} from '@home/components/widget/lib/scada/scada.models'; + IotSvgBehaviorType, + IotSvgMetadata, + IotSvgObjectSettings +} 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'; @@ -43,35 +43,36 @@ import { isDefinedAndNotNull } from '@core/utils'; import { ScadaPropertyRow, toPropertyRows -} from '@home/components/widget/lib/settings/common/scada/scada-object-settings.models'; +} from '@home/components/widget/lib/settings/common/svg/iot-svg-object-settings.models'; import { merge, Observable, Subscription } from 'rxjs'; +import { WidgetActionCallbacks } from '@home/components/widget/action/manage-widget-actions.component.models'; @Component({ - selector: 'tb-scada-object-settings', - templateUrl: './scada-object-settings.component.html', + selector: 'tb-iot-svg-object-settings', + templateUrl: './iot-svg-object-settings.component.html', styleUrls: ['./../../widget-settings.scss'], providers: [ { provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => ScadaObjectSettingsComponent), + useExisting: forwardRef(() => IotSvgObjectSettingsComponent), multi: true }, { provide: NG_VALIDATORS, - useExisting: forwardRef(() => ScadaObjectSettingsComponent), + useExisting: forwardRef(() => IotSvgObjectSettingsComponent), multi: true } ] }) -export class ScadaObjectSettingsComponent implements OnInit, OnChanges, ControlValueAccessor, Validator { +export class IotSvgObjectSettingsComponent implements OnInit, OnChanges, ControlValueAccessor, Validator { - ScadaObjectBehaviorType = ScadaObjectBehaviorType; + IotSvgBehaviorType = IotSvgBehaviorType; @Input() disabled: boolean; @Input() - svgPath = '/assets/widget/scada/drawing.svg'; + svgPath = 'drawing.svg'; @Input() aliasController: IAliasController; @@ -79,10 +80,13 @@ export class ScadaObjectSettingsComponent implements OnInit, OnChanges, ControlV @Input() targetDevice: TargetDevice; + @Input() + callbacks: WidgetActionCallbacks; + @Input() widgetType: widgetType; - private modelValue: ScadaObjectSettings; + private modelValue: IotSvgObjectSettings; private propagateChange = null; @@ -91,7 +95,7 @@ export class ScadaObjectSettingsComponent implements OnInit, OnChanges, ControlV public scadaObjectSettingsFormGroup: UntypedFormGroup; - metadata: ScadaObjectMetadata; + metadata: IotSvgMetadata; propertyRows: ScadaPropertyRow[]; constructor(protected store: Store, @@ -136,7 +140,7 @@ export class ScadaObjectSettingsComponent implements OnInit, OnChanges, ControlV } } - writeValue(value: ScadaObjectSettings): void { + writeValue(value: IotSvgObjectSettings): void { this.modelValue = value || {}; this.setupValue(); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.models.ts similarity index 82% rename from ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.models.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.models.ts index 07009a98cf..12231d2b1a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.models.ts @@ -14,16 +14,16 @@ /// limitations under the License. /// -import { ScadaObjectProperty } from '@home/components/widget/lib/scada/scada.models'; +import { IotSvgProperty } from '@home/components/widget/lib/svg/iot-svg.models'; export interface ScadaPropertyRow { label: string; - properties: ScadaObjectProperty[]; - switch?: ScadaObjectProperty; + properties: IotSvgProperty[]; + switch?: IotSvgProperty; rowClass?: string; } -export const toPropertyRows = (properties: ScadaObjectProperty[]): ScadaPropertyRow[] => { +export const toPropertyRows = (properties: IotSvgProperty[]): ScadaPropertyRow[] => { const result: ScadaPropertyRow[] = []; for (const property of properties) { let propertyRow = result.find(r => r.label === property.name); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts index 152cf25b35..5f2229cba5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts @@ -150,8 +150,8 @@ import { } from '@home/components/widget/lib/settings/common/indicator/status-widget-state-settings.component'; import { ChartBarSettingsComponent } from '@home/components/widget/lib/settings/common/chart/chart-bar-settings.component'; import { - ScadaObjectSettingsComponent -} from '@home/components/widget/lib/settings/common/scada/scada-object-settings.component'; + IotSvgObjectSettingsComponent +} from '@home/components/widget/lib/settings/common/svg/iot-svg-object-settings.component'; @NgModule({ declarations: [ @@ -207,7 +207,7 @@ import { TimeSeriesChartStateRowComponent, TimeSeriesChartGridSettingsComponent, StatusWidgetStateSettingsComponent, - ScadaObjectSettingsComponent, + IotSvgObjectSettingsComponent, DataKeyInputComponent, EntityAliasInputComponent ], @@ -269,7 +269,7 @@ import { TimeSeriesChartStateRowComponent, TimeSeriesChartGridSettingsComponent, StatusWidgetStateSettingsComponent, - ScadaObjectSettingsComponent, + IotSvgObjectSettingsComponent, DataKeyInputComponent, EntityAliasInputComponent ], diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-test-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.component.html similarity index 57% rename from ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-test-widget.component.html rename to ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.component.html index f811e7f889..b2f4ce1815 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-test-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.component.html @@ -15,5 +15,12 @@ limitations under the License. --> -
+
+
+ +
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-test-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.component.scss similarity index 54% rename from ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-test-widget.component.scss rename to ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.component.scss index 5012fe3a7c..b109bac863 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-test-widget.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.component.scss @@ -13,10 +13,38 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -.tb-scada-shape { + +.tb-iot-svg-panel { width: 100%; height: 100%; + position: relative; display: flex; - align-items: center; - justify-content: center; + flex-direction: column; + gap: 16px; + padding: 20px 24px 24px 24px; + > div:not(.tb-iot-svg-overlay) { + z-index: 1; + } + .tb-iot-svg-overlay { + position: absolute; + top: 12px; + left: 12px; + bottom: 12px; + right: 12px; + } + div.tb-widget-title { + padding: 0; + } + .tb-iot-svg-content { + flex: 1; + min-width: 0; + min-height: 0; + .tb-iot-svg-shape { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + } + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-test-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.component.ts similarity index 51% rename from ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-test-widget.component.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.component.ts index 25451dcf15..c7b5e87723 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-test-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.component.ts @@ -28,28 +28,35 @@ import { 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 { SVG, Svg } from '@svgdotjs/svg.js'; import { HttpClient } from '@angular/common/http'; -import { ScadaObject, ScadaObjectSettings } from '@home/components/widget/lib/scada/scada.models'; +import { IotSvgObject } from '@home/components/widget/lib/svg/iot-svg.models'; import { ResizeObserver } from '@juggle/resize-observer'; -import { ScadaTestWidgetSettings } from '@home/components/widget/lib/scada/scada-test-widget.models'; +import { + iotSvgWidgetDefaultSettings, + IotSvgWidgetSettings +} from '@home/components/widget/lib/svg/iot-svg-widget.models'; +import { Observable } from 'rxjs'; +import { backgroundStyle, ComponentStyle, overlayStyle } from '@shared/models/widget-settings.models'; @Component({ - selector: 'tb-scada-test-widget', - templateUrl: './scada-test-widget.component.html', - styleUrls: ['../action/action-widget.scss', './scada-test-widget.component.scss'], + selector: 'tb-iot-svg-widget', + templateUrl: './iot-svg-widget.component.html', + styleUrls: ['../action/action-widget.scss', './iot-svg-widget.component.scss'], encapsulation: ViewEncapsulation.None }) -export class ScadaTestWidgetComponent extends +export class IotSvgWidgetComponent extends BasicActionWidgetComponent implements OnInit, AfterViewInit, OnDestroy { - @ViewChild('scadaShape', {static: false}) - scadaShape: ElementRef; + @ViewChild('iotSvgShape', {static: false}) + iotSvgShape: ElementRef; + + private settings: IotSvgWidgetSettings; - private settings: ScadaTestWidgetSettings; + backgroundStyle$: Observable; + overlayStyle: ComponentStyle = {}; + iotSvgObject: IotSvgObject; private autoScale = true; - private scadaObject: ScadaObject; private shapeResize$: ResizeObserver; @@ -63,18 +70,25 @@ export class ScadaTestWidgetComponent extends ngOnInit(): void { super.ngOnInit(); - this.settings = {...this.ctx.settings}; - this.scadaObject = new ScadaObject(this.ctx, '/assets/widget/scada/drawing.svg', this.settings.scadaObject); - this.scadaObject.init().subscribe(); + this.settings = {...iotSvgWidgetDefaultSettings, ...this.ctx.settings}; + + this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer); + this.overlayStyle = overlayStyle(this.settings.background.overlay); + + this.iotSvgObject = new IotSvgObject(this.ctx, this.settings.iotSvg, this.settings.iotSvgObject); + this.iotSvgObject.onError((error) => { + this.ctx.showErrorToast(error, 'bottom', 'center', this.ctx.toastTargetId, true); + }); + this.iotSvgObject.init().subscribe(); } ngAfterViewInit(): void { - this.scadaObject.addTo(this.scadaShape.nativeElement); + this.iotSvgObject.addTo(this.iotSvgShape.nativeElement); if (this.autoScale) { this.shapeResize$ = new ResizeObserver(() => { this.onResize(); }); - this.shapeResize$.observe(this.scadaShape.nativeElement); + this.shapeResize$.observe(this.iotSvgShape.nativeElement); this.onResize(); } super.ngAfterViewInit(); @@ -84,19 +98,22 @@ export class ScadaTestWidgetComponent extends if (this.shapeResize$) { this.shapeResize$.disconnect(); } - if (this.scadaObject) { - this.scadaObject.destroy(); + 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.scadaShape.nativeElement.getBoundingClientRect().width; - const shapeHeight = this.scadaShape.nativeElement.getBoundingClientRect().height; - this.scadaObject.setSize(shapeWidth, shapeHeight); + const shapeWidth = this.iotSvgShape.nativeElement.getBoundingClientRect().width; + const shapeHeight = this.iotSvgShape.nativeElement.getBoundingClientRect().height; + this.iotSvgObject.setSize(shapeWidth, shapeHeight); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-test-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.models.ts similarity index 50% rename from ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-test-widget.models.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.models.ts index a74d81e3d5..e4992ae62f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-test-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.models.ts @@ -14,8 +14,25 @@ /// limitations under the License. /// -import { ScadaObjectSettings } from '@home/components/widget/lib/scada/scada.models'; +import { IotSvgObjectSettings } from '@home/components/widget/lib/svg/iot-svg.models'; +import { BackgroundSettings, BackgroundType } from '@shared/models/widget-settings.models'; -export interface ScadaTestWidgetSettings { - scadaObject: ScadaObjectSettings; +export interface IotSvgWidgetSettings { + iotSvg: string; + iotSvgObject: IotSvgObjectSettings; + background: BackgroundSettings; } + +export const iotSvgWidgetDefaultSettings: IotSvgWidgetSettings = { + iotSvg: '/assets/widget/svg/drawing.svg', + iotSvgObject: {}, + background: { + type: BackgroundType.color, + color: '#fff', + overlay: { + enabled: false, + color: 'rgba(255,255,255,0.72)', + blur: 3 + } + } +}; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts new file mode 100644 index 0000000000..d0bd66dcbf --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts @@ -0,0 +1,634 @@ +/// +/// 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 { ValueType } from '@shared/models/constants'; +import { Box, Element, Runner, Svg, SVG, Text } from '@svgdotjs/svg.js'; +import { + DataToValueType, + GetValueAction, + GetValueSettings, + SetValueAction, + SetValueSettings, + ValueToDataType +} from '@shared/models/action-widget-settings.models'; +import { formatValue, isDefinedAndNotNull, isUndefinedOrNull, mergeDeep, parseFunction } from '@core/utils'; +import { BehaviorSubject, forkJoin, Observable, Observer } from 'rxjs'; +import { map, 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'; +import { ColorProcessor, constantColor, Font } from '@shared/models/widget-settings.models'; +import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; +import { UtilsService } from '@core/services/utils.service'; +import { WidgetAction, WidgetActionType } from '@shared/models/widget.models'; + +export interface IotSvgApi { + formatValue: (value: any, dec?: number, units?: string, showZeroDecimals?: boolean) => string | undefined; + text: (element: Element | Element[], text: string) => void; + font: (element: Element | Element[], font: Font, color: string) => void; + animate: (element: Element, duration: number) => Runner; + disable: (element: Element | Element[]) => void; + enable: (element: Element | Element[]) => void; + callAction: (event: Event, behaviorId: string, value?: any, observer?: Partial>) => void; + setValue: (valueId: string, value: any) => void; +} + +export interface IotSvgContext { + api: IotSvgApi; + tags: {[id: string]: Element[]}; + values: {[id: string]: any}; + properties: {[id: string]: any}; +} + +export type IotSvgStateRenderFunction = (svg: Svg, ctx: IotSvgContext) => void; + +export type IotSvgTagStateRenderFunction = (element: Element, ctx: IotSvgContext) => void; + +export type IotSvgActionTrigger = 'click'; + +export type IotSvgActionFunction = (event: Event, ctx: IotSvgContext) => void; +export interface IotSvgAction { + actionFunction?: string; + action?: IotSvgActionFunction; +} + +export interface IotSvgTag { + tag: string; + stateRenderFunction?: string; + stateRender?: IotSvgTagStateRenderFunction; + actions?: {[trigger: string]: IotSvgAction}; +} + +export enum IotSvgBehaviorType { + value = 'value', + action = 'action', + widgetAction = 'widgetAction' +} + +export interface IotSvgBehaviorBase { + id: string; + name: string; + type: IotSvgBehaviorType; +} + +export interface IotSvgBehaviorValue extends IotSvgBehaviorBase { + valueType: ValueType; + defaultValue: any; + valueId: string; +} + +export interface IotSvgBehaviorAction extends IotSvgBehaviorBase { + valueToDataType: ValueToDataType; + constantValue: any; + valueToDataFunction: string; +} + +export type IotSvgBehavior = IotSvgBehaviorValue | IotSvgBehaviorAction; + +export type IotSvgPropertyType = 'string' | 'number' | 'color' | 'color-settings' | 'font' | 'units' | 'switch'; + +export interface IotSvgPropertyBase { + id: string; + name: string; + type: IotSvgPropertyType; + default: any; + required?: boolean; + subLabel?: string; + divider?: boolean; + fieldSuffix?: string; + disableOnProperty?: string; + rowClass?: string; + fieldClass?: string; +} + +export interface IotSvgNumberProperty extends IotSvgPropertyBase { + min?: number; + max?: number; + step?: number; +} + +export type IotSvgProperty = IotSvgPropertyBase & IotSvgNumberProperty; + +export type ScadaObjectElementActionTrigger = 'click'; + +export type ScadaObjectActionType = 'updateValue'; +export type ScadaObjectActionUpdateValueType = 'toggle' | 'increment' | 'constant'; + +export interface ScadaObjectElementAction { + trigger: ScadaObjectElementActionTrigger; + enabledTriggerValues?: string[]; + enabledConditionFunction?: string; + enabledCondition?: (values: {[key: string]: any}) => boolean; + actionType: ScadaObjectActionType; + updateValueId?: string; + updateValueType?: ScadaObjectActionUpdateValueType; + updateValueConstant?: any; + updateValueInc?: number; +} + +export interface IotSvgMetadata { + title: string; + stateRenderFunction?: string; + stateRender?: IotSvgStateRenderFunction; + tags: IotSvgTag[]; + behavior: IotSvgBehavior[]; + properties: IotSvgProperty[]; +} + +export const emptyMetadata: IotSvgMetadata = { + title: '', + tags: [], + behavior: [], + properties: [] +}; + + +export const parseScadaObjectMetadataFromContent = (svgContent: string): IotSvgMetadata => { + try { + const svgDoc = new DOMParser().parseFromString(svgContent, 'image/svg+xml'); + return parseScadaObjectMetadataFromDom(svgDoc); + } catch (_e) { + return emptyMetadata; + } +}; + +const parseScadaObjectMetadataFromDom = (svgDoc: Document): IotSvgMetadata => { + try { + const elements = svgDoc.getElementsByTagName('tb:metadata'); + if (elements.length) { + return JSON.parse(elements[0].textContent); + } else { + return emptyMetadata; + } + } catch (_e) { + console.error(_e); + return emptyMetadata; + } +}; + +const defaultGetValueSettings = (get: IotSvgBehaviorValue): GetValueSettings => ({ + action: GetValueAction.DO_NOTHING, + defaultValue: get.defaultValue, + 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;' + } + }); + +const defaultSetValueSettings = (set: IotSvgBehaviorAction): SetValueSettings => ({ + action: SetValueAction.EXECUTE_RPC, + executeRpc: { + method: 'setState', + requestTimeout: 5000, + requestPersistent: false, + persistentPollingInterval: 1000 + }, + setAttribute: { + key: 'state', + scope: AttributeScope.SERVER_SCOPE + }, + putTimeSeries: { + key: 'state' + }, + valueToData: { + type: set.valueToDataType, + constantValue: set.constantValue, + valueToDataFunction: set.valueToDataFunction ? set.valueToDataFunction : + '/* Convert input boolean value to RPC parameters or attribute/time-series value */\nreturn value;' + } +}); + +const defaultWidgetActionSettings = (widgetAction: IotSvgBehavior): WidgetAction => ({ + type: WidgetActionType.updateDashboardState, + targetDashboardStateId: null, + openRightLayout: false, + setEntityId: true, + stateEntityParamName: null +}); + +export const defaultScadaObjectSettings = (metadata: IotSvgMetadata): IotSvgObjectSettings => { + const settings: IotSvgObjectSettings = {}; + for (const behavior of metadata.behavior) { + if (behavior.type === IotSvgBehaviorType.value) { + settings[behavior.id] = defaultGetValueSettings(behavior as IotSvgBehaviorValue); + } else if (behavior.type === IotSvgBehaviorType.action) { + settings[behavior.id] = defaultSetValueSettings(behavior as IotSvgBehaviorAction); + } else if (behavior.type === IotSvgBehaviorType.widgetAction) { + settings[behavior.id] = defaultWidgetActionSettings(behavior); + } + } + for (const property of metadata.properties) { + settings[property.id] = property.default; + } + return settings; +}; + +export type IotSvgObjectSettings = {[id: string]: any}; + +const parseError = (ctx: WidgetContext, err: any): string => + ctx.$injector.get(UtilsService).parseException(err).message || 'Unknown Error'; + +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[] = []; + private valueActions: ValueAction[] = []; + private valueSetters: {[behaviorId: string]: ValueSetter} = {}; + + private stateValueSubjects: {[id: string]: BehaviorSubject} = {}; + + loading$ = this.loadingSubject.asObservable().pipe(share()); + + private _onError: (error: string) => void = () => {}; + + constructor(private ctx: WidgetContext, + private svgPath: string, + private inputSettings: IotSvgObjectSettings) {} + + public init(): Observable { + return this.ctx.http.get(this.svgPath, {responseType: 'text'}).pipe( + map((inputSvgContent) => { + const doc: XMLDocument = new DOMParser().parseFromString(inputSvgContent, 'image/svg+xml'); + this.metadata = parseScadaObjectMetadataFromDom(doc); + const defaults = defaultScadaObjectSettings(this.metadata); + this.settings = mergeDeep({}, defaults, this.inputSettings || {}); + this.prepareMetadata(); + this.prepareSvgShape(doc); + this.initialize(); + }) + ); + } + + public onError(onError: (error: string) => void) { + this._onError = onError; + } + + public addTo(element: HTMLElement) { + this.rootElement = element; + if (this.svgShape) { + this.svgShape.addTo(element); + } + } + + public destroy() { + for (const stateValueId of Object.keys(this.stateValueSubjects)) { + this.stateValueSubjects[stateValueId].complete(); + this.stateValueSubjects[stateValueId].unsubscribe(); + } + this.valueActions.forEach(v => v.destroy()); + this.loadingSubject.complete(); + this.loadingSubject.unsubscribe(); + } + + public setSize(targetWidth: number, targetHeight: number) { + this.targetWidth = targetWidth; + this.targetHeight = targetHeight; + if (this.svgShape) { + this.resize(); + } + } + + private prepareMetadata() { + this.metadata.stateRender = parseFunction(this.metadata.stateRenderFunction, ['svg', 'ctx']) || (() => {}); + for (const tag of this.metadata.tags) { + tag.stateRender = parseFunction(tag.stateRenderFunction, ['element', 'ctx']) || (() => {}); + if (tag.actions) { + for (const trigger of Object.keys(tag.actions)) { + const action = tag.actions[trigger]; + action.action = parseFunction(action.actionFunction, ['event', 'ctx']) || (() => {}); + } + } + } + } + + private prepareSvgShape(doc: XMLDocument) { + const elements = doc.getElementsByTagName('tb:metadata'); + for (let i=0;i { + element.attr('cursor', 'pointer'); + element.on(trigger, (event) => { + action.action(event, this.context); + }); + }); + } + } + } + for (const behavior of this.metadata.behavior) { + if (behavior.type === IotSvgBehaviorType.value) { + const getBehavior = behavior as IotSvgBehaviorValue; + let getValueSettings: GetValueSettings = this.settings[getBehavior.id]; + getValueSettings = {...getValueSettings, actionLabel: getBehavior.name}; + const stateValueSubject = new BehaviorSubject(getValueSettings.defaultValue); + this.stateValueSubjects[getBehavior.valueId] = stateValueSubject; + this.context.values[getBehavior.valueId] = getValueSettings.defaultValue; + stateValueSubject.subscribe((value) => { + this.onStateValueChanged(getBehavior.valueId, value); + }); + const valueGetter = + ValueGetter.fromSettings(this.ctx, getValueSettings, getBehavior.valueType, { + next: (val) => {this.onValue(getBehavior.id, val);}, + error: (err) => { + const message = parseError(this.ctx, err); + this._onError(message); + } + }); + this.valueGetters.push(valueGetter); + this.valueActions.push(valueGetter); + } else if (behavior.type === IotSvgBehaviorType.action) { + const setBehavior = behavior as IotSvgBehaviorAction; + let setValueSettings: SetValueSettings = this.settings[setBehavior.id]; + setValueSettings = {...setValueSettings, actionLabel: setBehavior.name}; + const valueSetter = ValueSetter.fromSettings(this.ctx, setValueSettings); + this.valueSetters[setBehavior.id] = valueSetter; + this.valueActions.push(valueSetter); + } else if (behavior.type === IotSvgBehaviorType.widgetAction) { + // TODO: + } + } + this.renderState(); + if (this.valueGetters.length) { + const getValueObservables: Array> = []; + this.valueGetters.forEach(valueGetter => { + getValueObservables.push(valueGetter.getValue()); + }); + this.loadingSubject.next(true); + forkJoin(getValueObservables).subscribe( + { + next: () => { + this.loadingSubject.next(false); + }, + error: () => { + this.loadingSubject.next(false); + } + } + ); + } + } + + private callAction(event: Event, behaviorId: string, value?: any, observer?: Partial>) { + const behavior = this.metadata.behavior.find(b => b.id === behaviorId); + if (behavior) { + if (behavior.type === IotSvgBehaviorType.action) { + const valueSetter = this.valueSetters[behaviorId]; + if (valueSetter) { + this.loadingSubject.next(true); + valueSetter.setValue(value).subscribe( + { + next: () => { + if (observer?.next) { + observer.next(); + } + this.loadingSubject.next(false); + }, + error: (err) => { + this.loadingSubject.next(false); + if (observer?.error) { + observer.error(err); + } + const message = parseError(this.ctx, err); + this._onError(message); + } + } + ); + } + } else if (behavior.type === IotSvgBehaviorType.widgetAction) { + const widgetAction: WidgetAction = this.settings[behavior.id]; + this.ctx.actionsApi.onWidgetAction(event, widgetAction); + } + } + } + + private resize() { + let scale: number; + if (this.targetWidth < this.targetHeight) { + scale = this.targetWidth / this.box.width; + } else { + scale = this.targetHeight / this.box.height; + } + this.svgShape.node.style.transform = `scale(${scale})`; + } + + private onValue(id: string, value: any) { + const valueBehavior = this.metadata.behavior.find(b => b.id === id) as IotSvgBehaviorValue; + value = this.normalizeValue(value, valueBehavior.valueType); + const valueId = valueBehavior.valueId; + this.setValue(valueId, value); + } + + private setValue(valueId: string, value: any) { + const stateValueSubject = this.stateValueSubjects[valueId]; + if (stateValueSubject && stateValueSubject.value !== value) { + stateValueSubject.next(value); + } + } + + private onStateValueChanged(id: string, value: any) { + if (this.context.values[id] !== value) { + this.context.values[id] = value; + this.renderState(); + } + } + + 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}"]`); + elements.forEach(element => { + tag.stateRender(element, this.context); + }); + } + } + + private normalizeValue(value: any, type: ValueType): any { + if (isUndefinedOrNull(value)) { + switch (type) { + case ValueType.STRING: + return ''; + case ValueType.INTEGER: + case ValueType.DOUBLE: + return 0; + case ValueType.BOOLEAN: + return false; + case ValueType.JSON: + return {}; + } + } else { + return value; + } + } + + private setElementText(e: Element | Element[], text: string) { + this.elements(e).forEach(element => { + let textElement: Text; + if (element.type === 'text') { + const children = element.children(); + if (children.length && children[0].type === 'tspan') { + textElement = children[0] as Text; + } else { + textElement = element as Text; + } + } else if (element.type === 'tspan') { + textElement = element as Text; + } + if (textElement) { + textElement.text(text); + } + }); + } + + private setElementFont(e: Element | Element[], font: Font, color: string) { + this.elements(e).forEach(element => { + if (element.type === 'text') { + const textElement = element as Text; + if (font) { + textElement.font({ + family: font.family, + size: (isDefinedAndNotNull(font.size) && isDefinedAndNotNull(font.sizeUnit)) ? + font.size + font.sizeUnit : null, + weight: font.weight, + style: font.style + }); + } + if (color) { + textElement.fill(color); + } + } + }); + } + + private animate(element: Element, duration: number): Runner { + element.timeline().finish(); + return element.animate(duration, 0, 'now'); + } + + private disableElement(e: Element | Element[]) { + this.elements(e).forEach(element => { + element.attr({'pointer-events': 'none'}); + }); + } + + private enableElement(e: Element | Element[]) { + this.elements(e).forEach(element => { + element.attr({'pointer-events': null}); + }); + } + + private elements(element: Element | Element[]): Element[] { + return Array.isArray(element) ? element : [element]; + } + + private getProperty(id: string): IotSvgProperty { + return this.metadata.properties.find(p => p.id === id); + } + + private getPropertyValue(id: string): any { + const property = this.getProperty(id); + if (property) { + const value = this.settings[id]; + if (isDefinedAndNotNull(value)) { + if (property.type === 'color-settings') { + return ColorProcessor.fromSettings(value); + } + return value; + } else { + switch (property.type) { + case 'string': + return ''; + case 'number': + return 0; + case 'color': + return '#000'; + case 'color-settings': + return constantColor('#000'); + } + } + } else { + return ''; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts index f062494763..ae9034d1cd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts @@ -92,7 +92,7 @@ import { PieChartWidgetComponent } from '@home/components/widget/lib/chart/pie-c import { BarChartWidgetComponent } from '@home/components/widget/lib/chart/bar-chart-widget.component'; import { PolarAreaWidgetComponent } from '@home/components/widget/lib/chart/polar-area-widget.component'; import { RadarChartWidgetComponent } from '@home/components/widget/lib/chart/radar-chart-widget.component'; -import { ScadaTestWidgetComponent } from '@home/components/widget/lib/scada/scada-test-widget.component'; +import { IotSvgWidgetComponent } from '@home/components/widget/lib/svg/iot-svg-widget.component'; @NgModule({ declarations: @@ -152,7 +152,7 @@ import { ScadaTestWidgetComponent } from '@home/components/widget/lib/scada/scad BarChartWidgetComponent, PolarAreaWidgetComponent, RadarChartWidgetComponent, - ScadaTestWidgetComponent + IotSvgWidgetComponent ], imports: [ CommonModule, @@ -215,7 +215,7 @@ import { ScadaTestWidgetComponent } from '@home/components/widget/lib/scada/scad BarChartWidgetComponent, PolarAreaWidgetComponent, RadarChartWidgetComponent, - ScadaTestWidgetComponent + IotSvgWidgetComponent ], providers: [ {provide: WIDGET_COMPONENTS_MODULE_TOKEN, useValue: WidgetComponentsModule } diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts index a79fa89242..b8f9be3f56 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts @@ -39,7 +39,7 @@ import { } from '@angular/core'; import { DashboardWidget } from '@home/models/dashboard-component.models'; import { - Widget, + Widget, WidgetAction, WidgetActionDescriptor, widgetActionSources, WidgetActionType, @@ -57,7 +57,7 @@ import { WidgetService } from '@core/http/widget.service'; import { UtilsService } from '@core/services/utils.service'; import { forkJoin, Observable, of, ReplaySubject, Subscription, throwError } from 'rxjs'; import { - deepClone, + deepClone, guid, insertVariable, isDefined, isNotEmptyStr, @@ -254,6 +254,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI actionDescriptorsBySourceId, getActionDescriptors: this.getActionDescriptors.bind(this), handleWidgetAction: this.handleWidgetAction.bind(this), + onWidgetAction: this.onWidgetAction.bind(this), elementClick: this.elementClick.bind(this), cardClick: this.cardClick.bind(this), click: this.click.bind(this), @@ -1039,7 +1040,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI return result; } - private handleWidgetAction($event: Event, descriptor: WidgetActionDescriptor, + private handleWidgetAction($event: Event, descriptor: WidgetAction, entityId?: EntityId, entityName?: string, additionalParams?: any, entityLabel?: string): void { const type = descriptor.type; const targetEntityParamName = descriptor.stateEntityParamName; @@ -1112,7 +1113,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI const customHtml = descriptor.customHtml; const customCss = descriptor.customCss; const customResources = descriptor.customResources; - const actionNamespace = `custom-action-pretty-${descriptor.name.toLowerCase()}`; + const actionNamespace = `custom-action-pretty-${guid()}`; let htmlTemplate = ''; if (isDefined(customHtml) && customHtml.length > 0) { htmlTemplate = customHtml; @@ -1420,13 +1421,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI const idsList = descriptors.map(descriptor => `#${descriptor.name}`).join(','); const targetElement = $(elementClicked).closest(idsList, this.widgetContext.$container[0]); if (targetElement.length && targetElement[0].id) { - $event.stopPropagation(); const descriptor = descriptors.find(descriptorInfo => descriptorInfo.name === targetElement[0].id); - const entityInfo = this.getActiveEntityInfo(); - const entityId = entityInfo ? entityInfo.entityId : null; - const entityName = entityInfo ? entityInfo.entityName : null; - const entityLabel = entityInfo && entityInfo.entityLabel ? entityInfo.entityLabel : null; - this.handleWidgetAction($event, descriptor, entityId, entityName, null, entityLabel); + this.onWidgetAction($event, descriptor); } } } @@ -1442,18 +1438,23 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI private onClick($event: Event, sourceId: string) { const descriptors = this.getActionDescriptors(sourceId); if (descriptors.length) { + this.onWidgetAction($event, descriptors[0]); + } + } + + private onWidgetAction($event: Event, action: WidgetAction) { + if ($event) { $event.stopPropagation(); - const descriptor = descriptors[0]; - const entityInfo = this.getActiveEntityInfo(); - const entityId = entityInfo ? entityInfo.entityId : null; - const entityName = entityInfo ? entityInfo.entityName : null; - const entityLabel = entityInfo && entityInfo.entityLabel ? entityInfo.entityLabel : null; - this.handleWidgetAction($event, descriptor, entityId, entityName, null, entityLabel); } + const entityInfo = this.getActiveEntityInfo(); + const entityId = entityInfo ? entityInfo.entityId : null; + const entityName = entityInfo ? entityInfo.entityName : null; + const entityLabel = entityInfo && entityInfo.entityLabel ? entityInfo.entityLabel : null; + this.handleWidgetAction($event, action, entityId, entityName, null, entityLabel); } private loadCustomActionResources(actionNamespace: string, customCss: string, customResources: Array, - actionDescriptor: WidgetActionDescriptor): Observable { + actionDescriptor: WidgetAction): Observable { const resourceTasks: Observable[] = []; const modulesTasks: Observable[] = []; diff --git a/ui-ngx/src/assets/widget/scada/drawing.svg b/ui-ngx/src/assets/widget/svg/drawing.svg similarity index 54% rename from ui-ngx/src/assets/widget/scada/drawing.svg rename to ui-ngx/src/assets/widget/svg/drawing.svg index 61e8d3f1ca..91bfb8b47d 100644 --- a/ui-ngx/src/assets/widget/scada/drawing.svg +++ b/ui-ngx/src/assets/widget/svg/drawing.svg @@ -1,341 +1,115 @@ - { - "title": "My first SCADA Object", - "stateValues": { - "level": { - "initialValue": 0 - }, - "disabled": { - "initialValue": false + { + "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});" }, - "on": { - "initialValue": false, - "callBehavior": [ - { - "behaviorId": "onUpdateState", - "conditionFunction": "return value === true;" - }, - { - "behaviorId": "offUpdateState", - "conditionFunction": "return value === false;" - } - ] - } - }, - "actions": { - "onButton": { - "trigger": "click", - "actionType": "updateValue", - "updateValueId": "on", - "updateValueType": "constant", - "updateValueConstant": true + { + "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}" }, - "offButton": { - "trigger": "click", - "actionType": "updateValue", - "updateValueId": "on", - "updateValueType": "constant", - "updateValueConstant": false - } - }, - "states": { - "initialState": { - "initial": true, - "state": [ - { - "tag": "levelTitle", - "show": { - "type": "property", - "propertyId": "showLevelTitle" - }, - "text": { - "content": { - "type": "property", - "propertyId": "levelTitle" - }, - "font": { - "type": "property", - "propertyId": "levelTitleFont" - }, - "color": { - "type": "property", - "propertyId": "levelTitleColor" - } - } - }, - { - "tag": "level", - "inputValue": "level", - "attributes": [ - { - "name": "fill", - "value": { - "type": "property", - "propertyId": "levelBackground" - } - } - ] - }, - { - "tag": "levelValueBackground", - "show": { - "type": "property", - "propertyId": "showValue" - } - }, - { - "tag": "levelValue", - "show": { - "type": "property", - "propertyId": "showValue" - }, - "text": { - "font": { - "type": "property", - "propertyId": "valueFont" - }, - "color": { - "type": "property", - "propertyId": "valueColor" - } - } - }, - { - "tag": "minLevel", - "show": { - "type": "property", - "propertyId": "showMinMaxLevel" - }, - "text": { - "content": { - "type": "property", - "propertyId": "minLevel" - }, - "font": { - "type": "property", - "propertyId": "minMaxLevelFont" - }, - "color": { - "type": "property", - "propertyId": "minMaxLevelColor" - } - } - }, - { - "tag": "maxLevel", - "show": { - "type": "property", - "propertyId": "showMinMaxLevel" - }, - "text": { - "content": { - "type": "property", - "propertyId": "maxLevel" - }, - "font": { - "type": "property", - "propertyId": "minMaxLevelFont" - }, - "color": { - "type": "property", - "propertyId": "minMaxLevelColor" - } - } - } - ] + { + "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');" + } + } }, - "updateLevelState": { - "triggerValues": ["level"], - "state": [ - { - "tag": "level", - "inputValue": "level", - "animate": 200, - "attributes": [ - { - "name": "height", - "value": { - "type": "function", - "valueConvertFunction": "var level = (value - ${minLevel}) / (${maxLevel} - ${minLevel});\nlevel = Math.max(0, Math.min(1, level));\nreturn level*80;" - } - } - ] - }, - { - "tag": "levelValue", - "inputValue": "level", - "text": { - "content": { - "type": "valueFormat", - "units": { - "type": "property", - "propertyId": "valueUnits" - }, - "decimals": { - "type": "property", - "propertyId": "valueDecimals" - } - } - } - } - ] + { + "tag": "fan", + "stateRenderFunction": "var on = ctx.values.on;\nif (on) {\n element.addClass('fan-rotate');\n} else {\n element.removeClass('fan-rotate');\n}\n" }, - "updateLevelBackgroundState": { - "triggerValues": ["disabled", "level"], - "conditionFunction": "return values.disabled === false;", - "state": [ - { - "tag": "level", - "inputValue": "level", - "attributes": [ - { - "name": "fill", - "value": { - "type": "property", - "propertyId": "levelBackground" - } - } - ] - } - ] + { + "tag": "onButton", + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'onUpdateState');" + } + } }, - "disabledState": { - "triggerValues": ["disabled"], - "conditionFunction": "return values.disabled === true;", - "state": [ - { - "tag": "level", - "attributes": [ - { - "name": "fill", - "value": { - "type": "property", - "propertyId": "disabledLevelBackground" - } - } - ] - } - ] + { + "tag": "offButton", + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'offUpdateState');" + } + } }, - "onState": { - "triggerValues": ["on"], - "conditionFunction": "return values.on === true;", - "state": [ - { - "tag": "fan", - "addClass": "fan-rotate" - }, - { - "tag": "onButtonBackground", - "attributes": [ - { - "name": "fill", - "value": { - "type": "constant", - "constantValue": "#777" - } - } - ] - }, - { - "tag": "offButtonBackground", - "attributes": [ - { - "name": "fill", - "value": { - "type": "constant", - "constantValue": "#ed121f" - } - } - ] - } - ] + { + "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});" + } + } }, - "offState": { - "triggerValues": ["on"], - "conditionFunction": "return values.on === false;", - "state": [ - { - "tag": "fan", - "removeClass": "fan-rotate" - }, - { - "tag": "onButtonBackground", - "attributes": [ - { - "name": "fill", - "value": { - "type": "constant", - "constantValue": "#12ed19" - } - } - ] - }, - { - "tag": "offButtonBackground", - "attributes": [ - { - "name": "fill", - "value": { - "type": "constant", - "constantValue": "#777" - } - } - ] - } - ] + { + "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});" + } + } } - }, + ], "behavior": [ { "id": "levelState", "name": "Level", - "type": "getValue", + "type": "value", "valueType": "DOUBLE", "defaultValue": 0, - "onUpdate": [ - { - "updateValue": "level" - } - ] + "valueId": "level" }, { "id": "disabledState", "name": "Disabled state", - "type": "getValue", + "type": "value", "valueType": "BOOLEAN", "defaultValue": false, - "onUpdate": [ - { - "updateValue": "disabled" - } - ] + "valueId": "disabled" }, { "id": "onState", "name": "On/Off state", - "type": "getValue", + "type": "value", "valueType": "BOOLEAN", "defaultValue": false, - "onUpdate": [ - { - "updateValue": "on" - } - ] + "valueId": "on" }, { "id": "onUpdateState", "name": "Power On", - "type": "setValue", + "type": "action", + "valueType": "BOOLEAN", "valueToDataType": "CONSTANT", "constantValue": true }, { "id": "offUpdateState", "name": "Power Off", - "type": "setValue", + "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": [ @@ -489,7 +263,7 @@ "default": "#ccc" } ] - }</tb:metadata> + }