Browse Source

UI: SCADA symbol editor improvements. Improve preview, add behaviors table.

pull/11391/head
Igor Kulikov 2 years ago
parent
commit
25b30b1ee7
  1. 141
      ui-ngx/src/app/modules/home/components/widget/lib/action/action-widget.models.ts
  2. 6
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.component.html
  3. 33
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.component.ts
  4. 10
      ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.component.ts
  5. 6
      ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.models.ts
  6. 81
      ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts
  7. 48
      ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.html
  8. 26
      ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.scss
  9. 172
      ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.ts
  10. 60
      ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component.html
  11. 43
      ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component.scss
  12. 177
      ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component.ts
  13. 10
      ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-components.module.ts
  14. 2
      ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.html
  15. 9
      ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.ts
  16. 4
      ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.ts
  17. 5
      ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.html
  18. 2
      ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.ts
  19. 13
      ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.ts
  20. 20
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  21. 19
      ui-ngx/src/assets/widget/svg/drawing.svg
  22. 1
      ui-ngx/src/form.scss

141
ui-ngx/src/app/modules/home/components/widget/lib/action/action-widget.models.ts

@ -24,7 +24,7 @@ import {
} from '@shared/models/telemetry/telemetry.models';
import { WidgetContext } from '@home/models/widget-component.models';
import { BehaviorSubject, forkJoin, Observable, Observer, of, Subscription, throwError } from 'rxjs';
import { catchError, delay, map, share, take, tap } from 'rxjs/operators';
import { catchError, delay, map, share, take } from 'rxjs/operators';
import { UtilsService } from '@core/services/utils.service';
import { AfterViewInit, ChangeDetectorRef, Directive, Input, OnDestroy, OnInit, TemplateRef } from '@angular/core';
import {
@ -45,6 +45,7 @@ import {
import { ValueType } from '@shared/models/constants';
import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models';
import { EntityId } from '@shared/models/id/entity-id';
import { isDefinedAndNotNull } from '@core/utils';
@Directive()
// eslint-disable-next-line @angular-eslint/directive-class-suffix
@ -117,14 +118,16 @@ export abstract class BasicActionWidgetComponent implements OnInit, OnDestroy, A
}
}
};
const valueGetter = ValueGetter.fromSettings(this.ctx, getValueSettings, valueType, observer);
const simulated = this.ctx.utilsService.widgetEditMode || this.ctx.isPreview;
const valueGetter = ValueGetter.fromSettings(this.ctx, getValueSettings, valueType, observer, simulated);
this.valueGetters.push(valueGetter);
this.valueActions.push(valueGetter);
return valueGetter;
}
protected createValueSetter<V>(setValueSettings: SetValueSettings): ValueSetter<V> {
const valueSetter = ValueSetter.fromSettings<V>(this.ctx, setValueSettings);
const simulated = this.ctx.utilsService.widgetEditMode || this.ctx.isPreview;
const valueSetter = ValueSetter.fromSettings<V>(this.ctx, setValueSettings, simulated);
this.valueActions.push(valueSetter);
return valueSetter;
}
@ -229,22 +232,22 @@ export abstract class ValueGetter<V> extends ValueAction {
static fromSettings<V>(ctx: WidgetContext,
settings: GetValueSettings<V>,
valueType: ValueType,
valueObserver: Partial<Observer<V>>): ValueGetter<V> {
valueObserver: Partial<Observer<V>>,
simulated: boolean): ValueGetter<V> {
switch (settings.action) {
case GetValueAction.DO_NOTHING:
return new DefaultValueGetter<V>(ctx, settings, valueType, valueObserver);
return new DefaultValueGetter<V>(ctx, settings, valueType, valueObserver, simulated);
case GetValueAction.EXECUTE_RPC:
return new ExecuteRpcValueGetter<V>(ctx, settings, valueType, valueObserver);
return new ExecuteRpcValueGetter<V>(ctx, settings, valueType, valueObserver, simulated);
case GetValueAction.GET_ATTRIBUTE:
return new AttributeValueGetter<V>(ctx, settings, valueType, valueObserver);
return new AttributeValueGetter<V>(ctx, settings, valueType, valueObserver, simulated);
case GetValueAction.GET_TIME_SERIES:
return new TimeSeriesValueGetter<V>(ctx, settings, valueType, valueObserver);
return new TimeSeriesValueGetter<V>(ctx, settings, valueType, valueObserver, simulated);
case GetValueAction.GET_DASHBOARD_STATE:
return new DashboardStateGetter<V>(ctx, settings, valueType, valueObserver);
return new DashboardStateGetter<V>(ctx, settings, valueType, valueObserver, simulated);
}
}
private readonly isSimulated: boolean;
private readonly dataConverter: DataToValueConverter<V>;
private getValueSubscription: Subscription;
@ -252,16 +255,16 @@ export abstract class ValueGetter<V> extends ValueAction {
protected constructor(protected ctx: WidgetContext,
protected settings: GetValueSettings<V>,
protected valueType: ValueType,
protected valueObserver: Partial<Observer<V>>) {
protected valueObserver: Partial<Observer<V>>,
protected simulated: boolean) {
super(ctx, settings);
this.isSimulated = this.ctx.$injector.get(UtilsService).widgetEditMode;
if (this.settings.action !== GetValueAction.DO_NOTHING) {
this.dataConverter = new DataToValueConverter<V>(settings.dataToValue, valueType);
}
}
getValue(): Observable<V> {
const valueObservable: Observable<V> = (this.isSimulated ? of(null).pipe(delay(500)) : this.doGetValue()).pipe(
const valueObservable: Observable<V> = this.doGetValue().pipe(
map((data) => {
if (this.dataConverter) {
return this.dataConverter.dataToValue(data);
@ -346,29 +349,29 @@ export class ValueToDataConverter<V> {
export abstract class ValueSetter<V> extends ValueAction {
static fromSettings<V>(ctx: WidgetContext,
settings: SetValueSettings): ValueSetter<V> {
settings: SetValueSettings,
simulated: boolean): ValueSetter<V> {
switch (settings.action) {
case SetValueAction.EXECUTE_RPC:
return new ExecuteRpcValueSetter<V>(ctx, settings);
return new ExecuteRpcValueSetter<V>(ctx, settings, simulated);
case SetValueAction.SET_ATTRIBUTE:
return new AttributeValueSetter<V>(ctx, settings);
return new AttributeValueSetter<V>(ctx, settings, simulated);
case SetValueAction.ADD_TIME_SERIES:
return new TimeSeriesValueSetter<V>(ctx, settings);
return new TimeSeriesValueSetter<V>(ctx, settings, simulated);
}
}
private readonly isSimulated: boolean;
private readonly valueToDataConverter: ValueToDataConverter<V>;
protected constructor(protected ctx: WidgetContext,
protected settings: SetValueSettings) {
protected settings: SetValueSettings,
protected simulated: boolean) {
super(ctx, settings);
this.isSimulated = this.ctx.$injector.get(UtilsService).widgetEditMode;
this.valueToDataConverter = new ValueToDataConverter<V>(settings.valueToData);
}
setValue(value: V): Observable<any> {
if (this.isSimulated) {
if (this.simulated) {
return of(null).pipe(delay(500));
} else {
return this.doSetValue(this.valueToDataConverter.valueToData(value)).pipe(
@ -389,8 +392,9 @@ export class DefaultValueGetter<V> extends ValueGetter<V> {
constructor(protected ctx: WidgetContext,
protected settings: GetValueSettings<V>,
protected valueType: ValueType,
protected valueObserver: Partial<Observer<V>>) {
super(ctx, settings, valueType, valueObserver);
protected valueObserver: Partial<Observer<V>>,
protected simulated: boolean) {
super(ctx, settings, valueType, valueObserver, simulated);
this.defaultValue = settings.defaultValue;
}
@ -406,20 +410,26 @@ export class ExecuteRpcValueGetter<V> extends ValueGetter<V> {
constructor(protected ctx: WidgetContext,
protected settings: GetValueSettings<V>,
protected valueType: ValueType,
protected valueObserver: Partial<Observer<V>>) {
super(ctx, settings, valueType, valueObserver);
protected valueObserver: Partial<Observer<V>>,
protected simulated: boolean) {
super(ctx, settings, valueType, valueObserver, simulated);
this.executeRpcSettings = settings.executeRpc;
}
protected doGetValue(): Observable<V> {
return this.ctx.controlApi.sendTwoWayCommand(this.executeRpcSettings.method, null,
this.executeRpcSettings.requestTimeout,
this.executeRpcSettings.requestPersistent,
this.executeRpcSettings.persistentPollingInterval).pipe(
if (this.simulated) {
const defaultValue = isDefinedAndNotNull(this.settings.defaultValue) ? this.settings.defaultValue : null;
return of(defaultValue).pipe(delay(500));
} else {
return this.ctx.controlApi.sendTwoWayCommand(this.executeRpcSettings.method, null,
this.executeRpcSettings.requestTimeout,
this.executeRpcSettings.requestPersistent,
this.executeRpcSettings.persistentPollingInterval).pipe(
catchError((err) => {
throw handleRpcError(this.ctx, err);
})
);
);
}
}
}
@ -431,24 +441,30 @@ export abstract class TelemetryValueGetter<V, S extends TelemetryValueSettings>
protected constructor(protected ctx: WidgetContext,
protected settings: GetValueSettings<V>,
protected valueType: ValueType,
protected valueObserver: Partial<Observer<V>>) {
super(ctx, settings, valueType, valueObserver);
protected valueObserver: Partial<Observer<V>>,
protected simulated: boolean) {
super(ctx, settings, valueType, valueObserver, simulated);
const entityInfo = this.ctx.defaultSubscription.getFirstEntityInfo();
this.targetEntityId = entityInfo?.entityId;
}
protected doGetValue(): Observable<V> {
if (!this.targetEntityId && !this.ctx.defaultSubscription.rpcEnabled) {
return throwError(() => new Error(this.ctx.translate.instant('widgets.value-action.error.target-entity-is-not-set')));
}
if (this.targetEntityId) {
const err = validateAttributeScope(this.ctx, this.targetEntityId, this.scope());
if (err) {
return throwError(() => err);
}
return this.subscribeForTelemetryValue();
if (this.simulated) {
const defaultValue = isDefinedAndNotNull(this.settings.defaultValue) ? this.settings.defaultValue : null;
return of(defaultValue).pipe(delay(100));
} else {
return of(null);
if (!this.targetEntityId && !this.ctx.defaultSubscription.rpcEnabled) {
return throwError(() => new Error(this.ctx.translate.instant('widgets.value-action.error.target-entity-is-not-set')));
}
if (this.targetEntityId) {
const err = validateAttributeScope(this.ctx, this.targetEntityId, this.scope());
if (err) {
return throwError(() => err);
}
return this.subscribeForTelemetryValue();
} else {
return of(null);
}
}
}
@ -492,8 +508,9 @@ export class AttributeValueGetter<V> extends TelemetryValueGetter<V, GetAttribut
constructor(protected ctx: WidgetContext,
protected settings: GetValueSettings<V>,
protected valueType: ValueType,
protected valueObserver: Partial<Observer<V>>) {
super(ctx, settings, valueType, valueObserver);
protected valueObserver: Partial<Observer<V>>,
protected simulated: boolean) {
super(ctx, settings, valueType, valueObserver, simulated);
}
protected getTelemetryValueSettings(): GetAttributeValueSettings {
@ -511,8 +528,9 @@ export class TimeSeriesValueGetter<V> extends TelemetryValueGetter<V, TelemetryV
constructor(protected ctx: WidgetContext,
protected settings: GetValueSettings<V>,
protected valueType: ValueType,
protected valueObserver: Partial<Observer<V>>) {
super(ctx, settings, valueType, valueObserver);
protected valueObserver: Partial<Observer<V>>,
protected simulated: boolean) {
super(ctx, settings, valueType, valueObserver, simulated);
}
protected getTelemetryValueSettings(): TelemetryValueSettings {
@ -524,12 +542,17 @@ export class DashboardStateGetter<V> extends ValueGetter<V> {
constructor(protected ctx: WidgetContext,
protected settings: GetValueSettings<V>,
protected valueType: ValueType,
protected valueObserver: Partial<Observer<V>>) {
super(ctx, settings, valueType, valueObserver);
protected valueObserver: Partial<Observer<V>>,
protected simulated: boolean) {
super(ctx, settings, valueType, valueObserver, simulated);
}
protected doGetValue(): Observable<string> {
return this.ctx.stateController.dashboardCtrl.dashboardCtx.stateId;
if (this.simulated) {
return of('default');
} else {
return this.ctx.stateController.dashboardCtrl.dashboardCtx.stateId;
}
}
}
@ -538,8 +561,9 @@ export class ExecuteRpcValueSetter<V> extends ValueSetter<V> {
private readonly executeRpcSettings: RpcSettings;
constructor(protected ctx: WidgetContext,
protected settings: SetValueSettings) {
super(ctx, settings);
protected settings: SetValueSettings,
protected simulated: boolean) {
super(ctx, settings, simulated);
this.executeRpcSettings = settings.executeRpc;
}
@ -560,8 +584,9 @@ export abstract class TelemetryValueSetter<V> extends ValueSetter<V> {
protected targetEntityId: EntityId;
protected constructor(protected ctx: WidgetContext,
protected settings: SetValueSettings) {
super(ctx, settings);
protected settings: SetValueSettings,
protected simulated: boolean) {
super(ctx, settings, simulated);
const entityInfo = this.ctx.defaultSubscription.getFirstEntityInfo();
this.targetEntityId = entityInfo?.entityId;
}
@ -594,8 +619,9 @@ export class AttributeValueSetter<V> extends TelemetryValueSetter<V> {
private readonly setAttributeValueSettings: SetAttributeValueSettings;
constructor(protected ctx: WidgetContext,
protected settings: SetValueSettings) {
super(ctx, settings);
protected settings: SetValueSettings,
protected simulated: boolean) {
super(ctx, settings, simulated);
this.setAttributeValueSettings = settings.setAttribute;
}
@ -616,8 +642,9 @@ export class TimeSeriesValueSetter<V> extends TelemetryValueSetter<V> {
private readonly putTimeSeriesValueSettings: TelemetryValueSettings;
constructor(protected ctx: WidgetContext,
protected settings: SetValueSettings) {
super(ctx, settings);
protected settings: SetValueSettings,
protected simulated: boolean) {
super(ctx, settings, simulated);
this.putTimeSeriesValueSettings = settings.putTimeSeries;
}

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

@ -17,8 +17,8 @@
-->
<ng-container [formGroup]="iotSvgObjectSettingsFormGroup">
<div>{{ metadata?.title | customTranslate }}</div>
<div *ngIf="metadata?.behavior?.length" class="tb-form-panel">
<div class="tb-form-panel-title" translate>iot-svg.behavior</div>
<div *ngIf="metadata?.behavior?.length" class="tb-form-panel" formGroupName="behavior">
<div class="tb-form-panel-title" translate>scada.behavior.behavior</div>
<div *ngFor="let behaviour of metadata.behavior" class="tb-form-row">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ behaviour.hint | customTranslate }}">{{ behaviour.name | customTranslate }}</div>
<tb-get-value-action-settings *ngIf="behaviour.type === IotSvgBehaviorType.value"
@ -51,7 +51,7 @@
</tb-widget-action-settings>
</div>
</div>
<div *ngIf="propertyRows?.length" class="tb-form-panel">
<div *ngIf="propertyRows?.length" class="tb-form-panel" formGroupName="properties">
<div *ngFor="let propertyRow of propertyRows" class="tb-form-row space-between" [class]="propertyRow.rowClass">
<mat-slide-toggle *ngIf="propertyRow.switch" class="mat-slide fixed-title-width" formControlName="{{ propertyRow.switch.id }}">
{{ propertyRow.label | customTranslate }}

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

@ -39,7 +39,7 @@ 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 } from '@core/utils';
import { isDefinedAndNotNull, mergeDeep } from '@core/utils';
import {
IotSvgPropertyRow,
toPropertyRows
@ -113,7 +113,10 @@ export class IotSvgObjectSettingsComponent implements OnInit, OnChanges, Control
}
ngOnInit(): void {
this.iotSvgObjectSettingsFormGroup = this.fb.group({});
this.iotSvgObjectSettingsFormGroup = this.fb.group({
behavior: this.fb.group({}),
properties: this.fb.group({})
});
this.iotSvgObjectSettingsFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});
@ -149,7 +152,7 @@ export class IotSvgObjectSettingsComponent implements OnInit, OnChanges, Control
}
writeValue(value: IotSvgObjectSettings): void {
this.modelValue = value || {};
this.modelValue = value || { behavior: {}, properties: {} };
this.setupValue();
}
@ -181,11 +184,16 @@ export class IotSvgObjectSettingsComponent implements OnInit, OnChanges, Control
(svgContent) => {
this.metadata = parseIotSvgMetadataFromContent(svgContent);
this.propertyRows = toPropertyRows(this.metadata.properties);
for (const control of Object.keys(this.iotSvgObjectSettingsFormGroup.controls)) {
this.iotSvgObjectSettingsFormGroup.removeControl(control, {emitEvent: false});
const behaviorFormGroup = this.iotSvgObjectSettingsFormGroup.get('behavior') as UntypedFormGroup;
for (const control of Object.keys(behaviorFormGroup.controls)) {
behaviorFormGroup.removeControl(control, {emitEvent: false});
}
const propertiesFormGroup = this.iotSvgObjectSettingsFormGroup.get('properties') as UntypedFormGroup;
for (const control of Object.keys(propertiesFormGroup.controls)) {
propertiesFormGroup.removeControl(control, {emitEvent: false});
}
for (const behaviour of this.metadata.behavior) {
this.iotSvgObjectSettingsFormGroup.addControl(behaviour.id, this.fb.control(null, []), {emitEvent: false});
behaviorFormGroup.addControl(behaviour.id, this.fb.control(null, []), {emitEvent: false});
}
for (const property of this.metadata.properties) {
if (property.disableOnProperty) {
@ -205,12 +213,12 @@ export class IotSvgObjectSettingsComponent implements OnInit, OnChanges, Control
validators.push(Validators.max(property.max));
}
}
this.iotSvgObjectSettingsFormGroup.addControl(property.id, this.fb.control(null, validators), {emitEvent: false});
propertiesFormGroup.addControl(property.id, this.fb.control(null, validators), {emitEvent: false});
}
if (this.validatorTriggers.length) {
const observables: Observable<any>[] = [];
for (const trigger of this.validatorTriggers) {
observables.push(this.iotSvgObjectSettingsFormGroup.get(trigger).valueChanges);
observables.push(propertiesFormGroup.get(trigger).valueChanges);
}
this.validatorSubscription = merge(...observables).subscribe(() => {
this.updateValidators();
@ -223,11 +231,12 @@ export class IotSvgObjectSettingsComponent implements OnInit, OnChanges, Control
}
private updateValidators() {
const propertiesFormGroup = this.iotSvgObjectSettingsFormGroup.get('properties') as UntypedFormGroup;
for (const trigger of this.validatorTriggers) {
const value: boolean = this.iotSvgObjectSettingsFormGroup.get(trigger).value;
const value: boolean = propertiesFormGroup.get(trigger).value;
this.metadata.properties.filter(p => p.disableOnProperty === trigger).forEach(
(p) => {
const control = this.iotSvgObjectSettingsFormGroup.get(p.id);
const control = propertiesFormGroup.get(p.id);
if (value) {
control.enable({emitEvent: false});
} else {
@ -241,7 +250,7 @@ export class IotSvgObjectSettingsComponent implements OnInit, OnChanges, Control
private setupValue() {
if (this.metadata) {
const defaults = defaultIotSvgObjectSettings(this.metadata);
this.modelValue = {...defaults, ...this.modelValue};
this.modelValue = mergeDeep<IotSvgObjectSettings>(defaults, this.modelValue);
this.iotSvgObjectSettingsFormGroup.patchValue(
this.modelValue, {emitEvent: false}
);
@ -253,6 +262,4 @@ export class IotSvgObjectSettingsComponent implements OnInit, OnChanges, Control
this.modelValue = this.iotSvgObjectSettingsFormGroup.getRawValue();
this.propagateChange(this.modelValue);
}
protected readonly ValueType = ValueType;
}

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

@ -39,6 +39,7 @@ import { Observable, of } from 'rxjs';
import { backgroundStyle, ComponentStyle, overlayStyle } from '@shared/models/widget-settings.models';
import { ImageService } from '@core/http/image.service';
import { WidgetComponent } from '@home/components/widget/widget.component';
import { isDefinedAndNotNull, mergeDeep } from '@core/utils';
@Component({
selector: 'tb-iot-svg-widget',
@ -74,7 +75,7 @@ export class IotSvgWidgetComponent extends
ngOnInit(): void {
super.ngOnInit();
this.settings = {...iotSvgWidgetDefaultSettings, ...this.ctx.settings};
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);
@ -96,10 +97,15 @@ export class IotSvgWidgetComponent extends
}
private initObject(svgContent: string) {
this.iotSvgObject = new IotSvgObject(this.ctx, svgContent, this.settings.iotSvgObject);
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);

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

@ -21,13 +21,17 @@ export interface IotSvgWidgetSettings {
iotSvg?: string;
iotSvgUrl?: string;
iotSvgContent?: string;
simulated?: boolean;
iotSvgObject: IotSvgObjectSettings;
background: BackgroundSettings;
}
export const iotSvgWidgetDefaultSettings: IotSvgWidgetSettings = {
iotSvg: '/assets/widget/svg/drawing.svg',
iotSvgObject: {},
iotSvgObject: {
behavior: {},
properties: {}
},
background: {
type: BackgroundType.color,
color: '#fff',

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

@ -33,14 +33,14 @@ import {
mergeDeep,
parseFunction
} from '@core/utils';
import { BehaviorSubject, forkJoin, Observable, Observer } from 'rxjs';
import { BehaviorSubject, forkJoin, Observable, Observer, of } 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';
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';
import { WidgetAction, WidgetActionType, widgetActionTypeTranslationMap } from '@shared/models/widget.models';
export interface IotSvgApi {
formatValue: (value: any, dec?: number, units?: string, showZeroDecimals?: boolean) => string | undefined;
@ -85,6 +85,17 @@ export enum IotSvgBehaviorType {
widgetAction = 'widgetAction'
}
export const iotSvgBehaviorTypes = Object.keys(IotSvgBehaviorType) as IotSvgBehaviorType[];
export const iotSvgBehaviorTypeTranslations = new Map<IotSvgBehaviorType, string>(
[
[IotSvgBehaviorType.value, 'scada.behavior.type-value'],
[IotSvgBehaviorType.action, 'scada.behavior.type-action'],
[IotSvgBehaviorType.widgetAction, 'scada.behavior.type-widget-action']
]
);
export interface IotSvgBehaviorBase {
id: string;
name: string;
@ -95,7 +106,6 @@ export interface IotSvgBehaviorBase {
export interface IotSvgBehaviorValue extends IotSvgBehaviorBase {
valueType: ValueType;
defaultValue: any;
valueId: string;
trueLabel?: string;
falseLabel?: string;
stateLabel?: string;
@ -283,23 +293,29 @@ const defaultWidgetActionSettings = (widgetAction: IotSvgBehavior): WidgetAction
});
export const defaultIotSvgObjectSettings = (metadata: IotSvgMetadata): IotSvgObjectSettings => {
const settings: IotSvgObjectSettings = {};
const settings: IotSvgObjectSettings = {
behavior: {},
properties: {}
};
for (const behavior of metadata.behavior) {
if (behavior.type === IotSvgBehaviorType.value) {
settings[behavior.id] = defaultGetValueSettings(behavior as IotSvgBehaviorValue);
settings.behavior[behavior.id] = defaultGetValueSettings(behavior as IotSvgBehaviorValue);
} else if (behavior.type === IotSvgBehaviorType.action) {
settings[behavior.id] = defaultSetValueSettings(behavior as IotSvgBehaviorAction);
settings.behavior[behavior.id] = defaultSetValueSettings(behavior as IotSvgBehaviorAction);
} else if (behavior.type === IotSvgBehaviorType.widgetAction) {
settings[behavior.id] = defaultWidgetActionSettings(behavior);
settings.behavior[behavior.id] = defaultWidgetActionSettings(behavior);
}
}
for (const property of metadata.properties) {
settings[property.id] = property.default;
settings.properties[property.id] = property.default;
}
return settings;
};
export type IotSvgObjectSettings = {[id: string]: any};
export type IotSvgObjectSettings = {
behavior: {[id: string]: any};
properties: {[id: string]: any};
};
const parseError = (ctx: WidgetContext, err: any): string =>
ctx.$injector.get(UtilsService).parseException(err).message || 'Unknown Error';
@ -327,16 +343,20 @@ export class IotSvgObject {
private _onError: (error: string) => void = () => {};
private _onMessage: (message: string) => void = () => {};
constructor(private ctx: WidgetContext,
private svgContent: string,
private inputSettings: IotSvgObjectSettings) {
private inputSettings: IotSvgObjectSettings,
private simulated: boolean) {
}
public init() {
const doc: XMLDocument = new DOMParser().parseFromString(this.svgContent, 'image/svg+xml');
this.metadata = parseIotSvgMetadataFromDom(doc);
const defaults = defaultIotSvgObjectSettings(this.metadata);
this.settings = mergeDeep<IotSvgObjectSettings>({}, defaults, this.inputSettings || {});
this.settings = mergeDeep<IotSvgObjectSettings>({} as IotSvgObjectSettings,
defaults, this.inputSettings || {} as IotSvgObjectSettings);
this.prepareMetadata();
this.prepareSvgShape(doc);
this.initialize();
@ -346,6 +366,10 @@ export class IotSvgObject {
this._onError = onError;
}
public onMessage(onMessage: (message: string) => void) {
this._onMessage = onMessage;
}
public addTo(element: HTMLElement) {
this.rootElement = element;
if (this.svgShape) {
@ -451,13 +475,14 @@ export class IotSvgObject {
for (const behavior of this.metadata.behavior) {
if (behavior.type === IotSvgBehaviorType.value) {
const getBehavior = behavior as IotSvgBehaviorValue;
let getValueSettings: GetValueSettings<any> = this.settings[getBehavior.id];
getValueSettings = {...getValueSettings, actionLabel: getBehavior.name};
let getValueSettings: GetValueSettings<any> = this.settings.behavior[getBehavior.id];
getValueSettings = {...getValueSettings, actionLabel:
this.ctx.utilsService.customTranslation(getBehavior.name, getBehavior.name)};
const stateValueSubject = new BehaviorSubject<any>(getValueSettings.defaultValue);
this.stateValueSubjects[getBehavior.valueId] = stateValueSubject;
this.context.values[getBehavior.valueId] = getValueSettings.defaultValue;
this.stateValueSubjects[getBehavior.id] = stateValueSubject;
this.context.values[getBehavior.id] = getValueSettings.defaultValue;
stateValueSubject.subscribe((value) => {
this.onStateValueChanged(getBehavior.valueId, value);
this.onStateValueChanged(getBehavior.id, value);
});
const valueGetter =
ValueGetter.fromSettings(this.ctx, getValueSettings, getBehavior.valueType, {
@ -466,14 +491,15 @@ export class IotSvgObject {
const message = parseError(this.ctx, err);
this._onError(message);
}
});
}, this.simulated);
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<any>(this.ctx, setValueSettings);
let setValueSettings: SetValueSettings = this.settings.behavior[setBehavior.id];
setValueSettings = {...setValueSettings, actionLabel:
this.ctx.utilsService.customTranslation(setBehavior.name, setBehavior.name)};
const valueSetter = ValueSetter.fromSettings<any>(this.ctx, setValueSettings, this.simulated);
this.valueSetters[setBehavior.id] = valueSetter;
this.valueActions.push(valueSetter);
} else if (behavior.type === IotSvgBehaviorType.widgetAction) {
@ -527,8 +553,14 @@ export class IotSvgObject {
);
}
} else if (behavior.type === IotSvgBehaviorType.widgetAction) {
const widgetAction: WidgetAction = this.settings[behavior.id];
this.ctx.actionsApi.onWidgetAction(event, widgetAction);
const widgetAction: WidgetAction = this.settings.behavior[behavior.id];
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);
} else {
this.ctx.actionsApi.onWidgetAction(event, widgetAction);
}
}
}
}
@ -546,8 +578,7 @@ export class IotSvgObject {
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);
this.setValue(valueBehavior.id, value);
}
private setValue(valueId: string, value: any) {
@ -659,7 +690,7 @@ export class IotSvgObject {
private getPropertyValue(id: string): any {
const property = this.getProperty(id);
if (property) {
const value = this.settings[id];
const value = this.settings.properties[id];
if (isDefinedAndNotNull(value)) {
if (property.type === 'color-settings') {
return ColorProcessor.fromSettings(value);

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

@ -0,0 +1,48 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div [formGroup]="behaviorRowFormGroup" class="tb-form-table-row tb-scada-symbol-metadata-behavior-row">
<mat-form-field class="tb-inline-field tb-id-field" appearance="outline" subscriptSizing="dynamic">
<input #idInput required matInput formControlName="id" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<mat-form-field class="tb-inline-field tb-name-field" appearance="outline" subscriptSizing="dynamic">
<input required matInput formControlName="name" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<mat-form-field class="tb-inline-field tb-type-field fixed-height" 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 class="tb-form-table-row-cell-buttons">
<button type="button"
mat-icon-button
(click)="editBehavior()"
matTooltip="{{ 'scada.behavior.behavior-settings' | translate }}"
matTooltipPosition="above">
<mat-icon>settings</mat-icon>
</button>
<button type="button"
mat-icon-button
(click)="behaviorRemoved.emit()"
matTooltip="{{ 'scada.behavior.remove-behavior' | translate }}"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
</div>
</div>

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

@ -0,0 +1,26 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.tb-scada-symbol-metadata-behavior-row {
.tb-id-field {
flex: 1 1 40%;
}
.tb-name-field {
flex: 1 1 40%;
}
.tb-type-field {
flex: 1 1 20%;
}
}

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

@ -0,0 +1,172 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
ChangeDetectorRef,
Component,
ElementRef,
EventEmitter,
forwardRef,
Input,
OnInit,
Output,
ViewChild,
ViewEncapsulation
} from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
NG_VALUE_ACCESSOR,
UntypedFormBuilder,
UntypedFormGroup,
ValidationErrors
} from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import {
IotSvgBehavior,
IotSvgBehaviorAction,
IotSvgBehaviorType,
iotSvgBehaviorTypes,
iotSvgBehaviorTypeTranslations,
IotSvgBehaviorValue
} from '@home/components/widget/lib/svg/iot-svg.models';
export const behaviorValid = (behavior: IotSvgBehavior): boolean => {
if (!behavior.id || !behavior.name || !behavior.type) {
return false;
}
switch (behavior.type) {
case IotSvgBehaviorType.value:
const valueBehavior = behavior as IotSvgBehaviorValue;
if (!valueBehavior.valueType) {
return false;
}
break;
case IotSvgBehaviorType.action:
const actionBehavior = behavior as IotSvgBehaviorAction;
if (!actionBehavior.valueToDataType) {
return false;
}
break;
case IotSvgBehaviorType.widgetAction:
break;
}
return true;
};
export const behaviorValidator = (control: AbstractControl): ValidationErrors | null => {
const behavior: IotSvgBehavior = control.value;
if (!behaviorValid(behavior)) {
return {
behavior: true
};
}
return null;
};
@Component({
selector: 'tb-scada-symbol-metadata-behavior-row',
templateUrl: './scada-symbol-behavior-row.component.html',
styleUrls: ['./scada-symbol-behavior-row.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ScadaSymbolBehaviorRowComponent),
multi: true
}
],
encapsulation: ViewEncapsulation.None
})
export class ScadaSymbolBehaviorRowComponent implements ControlValueAccessor, OnInit {
@ViewChild('idInput')
idInput: ElementRef<HTMLInputElement>;
iotSvgBehaviorTypes = iotSvgBehaviorTypes;
iotSvgBehaviorTypeTranslations = iotSvgBehaviorTypeTranslations;
@Input()
disabled: boolean;
@Output()
behaviorRemoved = new EventEmitter();
behaviorRowFormGroup: UntypedFormGroup;
modelValue: IotSvgBehavior;
private propagateChange = (_val: any) => {};
constructor(private fb: UntypedFormBuilder,
private dialog: MatDialog,
private cd: ChangeDetectorRef) {
}
ngOnInit() {
this.behaviorRowFormGroup = this.fb.group({
id: [null, []],
name: [null, []],
type: [null, []]
});
this.behaviorRowFormGroup.valueChanges.subscribe(
() => this.updateModel()
);
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (isDisabled) {
this.behaviorRowFormGroup.disable({emitEvent: false});
} else {
this.behaviorRowFormGroup.enable({emitEvent: false});
}
}
writeValue(value: IotSvgBehavior): void {
this.modelValue = value;
this.behaviorRowFormGroup.patchValue(
{
id: value?.id,
name: value?.name,
type: value?.type
}, {emitEvent: false}
);
this.cd.markForCheck();
}
editBehavior() {
}
focus() {
this.idInput.nativeElement.scrollIntoView();
this.idInput.nativeElement.focus();
}
private updateModel() {
const value: IotSvgBehavior = this.behaviorRowFormGroup.value;
this.modelValue = {...this.modelValue, ...value};
this.propagateChange(this.modelValue);
}
}

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

@ -0,0 +1,60 @@
<!--
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-form-panel tb-scada-symbol-behaviors">
<div class="tb-form-table">
<div class="tb-form-table-header no-padding-right">
<div class="tb-form-table-header-cell tb-id-header" translate>scada.behavior.id</div>
<div class="tb-form-table-header-cell tb-name-header" translate>scada.behavior.name</div>
<div class="tb-form-table-header-cell tb-type-header" translate>scada.behavior.type</div>
<div class="tb-form-table-header-cell tb-actions-header"></div>
</div>
<div *ngIf="behaviorsFormArray().controls.length; else noBehaviors" class="tb-form-table-body tb-drop-list"
cdkDropList cdkDropListOrientation="vertical"
[cdkDropListDisabled]="!dragEnabled"
(cdkDropListDropped)="behaviorDrop($event)">
<div cdkDrag [cdkDragDisabled]="!dragEnabled"
class="tb-draggable-form-table-row"
*ngFor="let behaviorControl of behaviorsFormArray().controls; trackBy: trackByBehavior; let $index = index;">
<tb-scada-symbol-metadata-behavior-row fxFlex
[formControl]="behaviorControl"
(behaviorRemoved)="removeBehavior($index)">
</tb-scada-symbol-metadata-behavior-row>
<div class="tb-form-table-row-cell-buttons">
<button mat-icon-button
type="button"
cdkDragHandle
[ngClass]="{'tb-hidden': !dragEnabled}"
matTooltip="{{ 'action.drag' | translate }}"
matTooltipPosition="above">
<mat-icon>drag_indicator</mat-icon>
</button>
</div>
</div>
</div>
</div>
<div>
<button type="button" mat-stroked-button color="primary" (click)="addBehavior()">
{{ 'scada.behavior.add-behavior' | translate }}
</button>
</div>
</div>
<ng-template #noBehaviors>
<span fxLayoutAlign="center center"
class="tb-prompt">{{ 'scada.behavior.no-behaviors' | translate }}</span>
</ng-template>

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

@ -0,0 +1,43 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.tb-scada-symbol-behaviors {
flex: 1;
margin: 12px;
.tb-form-table-header-cell {
&.tb-id-header {
flex: 1 1 40%;
}
&.tb-name-header {
flex: 1 1 40%;
}
&.tb-type-header {
flex: 1 1 20%;
}
&.tb-actions-header {
width: 120px;
min-width: 120px;
}
}
.tb-form-table {
overflow: hidden;
}
.tb-form-table-body {
overflow: auto;
tb-scada-symbol-metadata-behavior-row {
overflow: hidden;
}
}
}

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

@ -0,0 +1,177 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
Component,
forwardRef,
HostBinding,
Input,
OnInit,
QueryList,
ViewChildren,
ViewEncapsulation
} from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormArray,
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
Validator
} from '@angular/forms';
import { IotSvgBehavior, IotSvgBehaviorType } from '@home/components/widget/lib/svg/iot-svg.models';
import { ValueType } from '@shared/models/constants';
import { CdkDragDrop } from '@angular/cdk/drag-drop';
import {
behaviorValid,
behaviorValidator,
ScadaSymbolBehaviorRowComponent
} from '@home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component';
@Component({
selector: 'tb-scada-symbol-metadata-behaviors',
templateUrl: './scada-symbol-behaviors.component.html',
styleUrls: ['./scada-symbol-behaviors.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ScadaSymbolBehaviorsComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => ScadaSymbolBehaviorsComponent),
multi: true
}
],
encapsulation: ViewEncapsulation.None
})
export class ScadaSymbolBehaviorsComponent implements ControlValueAccessor, OnInit, Validator {
@HostBinding('style.display') styleDisplay = 'flex';
@HostBinding('style.overflow') styleOverflow = 'hidden';
@ViewChildren(ScadaSymbolBehaviorRowComponent)
behaviorRows: QueryList<ScadaSymbolBehaviorRowComponent>;
@Input()
disabled: boolean;
behaviorsFormGroup: UntypedFormGroup;
get dragEnabled(): boolean {
return this.behaviorsFormArray().controls.length > 1;
}
private propagateChange = (_val: any) => {};
constructor(private fb: UntypedFormBuilder) {
}
ngOnInit() {
this.behaviorsFormGroup = this.fb.group({
behaviors: this.fb.array([])
});
this.behaviorsFormGroup.valueChanges.subscribe(
() => {
let behaviors: IotSvgBehavior[] = this.behaviorsFormGroup.get('behaviors').value;
if (behaviors) {
behaviors = behaviors.filter(b => behaviorValid(b));
}
this.propagateChange(behaviors);
}
);
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (isDisabled) {
this.behaviorsFormGroup.disable({emitEvent: false});
} else {
this.behaviorsFormGroup.enable({emitEvent: false});
}
}
writeValue(value: IotSvgBehavior[] | undefined): void {
const behaviors= value || [];
this.behaviorsFormGroup.setControl('behaviors', this.prepareBehaviorsFormArray(behaviors), {emitEvent: false});
}
public validate(c: UntypedFormControl) {
const valid = this.behaviorsFormGroup.valid;
return valid ? null : {
behaviors: {
valid: false,
},
};
}
behaviorDrop(event: CdkDragDrop<string[]>) {
const behaviorsArray = this.behaviorsFormGroup.get('behaviors') as UntypedFormArray;
const behavior = behaviorsArray.at(event.previousIndex);
behaviorsArray.removeAt(event.previousIndex);
behaviorsArray.insert(event.currentIndex, behavior);
}
behaviorsFormArray(): UntypedFormArray {
return this.behaviorsFormGroup.get('behaviors') as UntypedFormArray;
}
trackByBehavior(index: number, behaviorControl: AbstractControl): any {
return behaviorControl;
}
removeBehavior(index: number, emitEvent = true) {
(this.behaviorsFormGroup.get('behaviors') as UntypedFormArray).removeAt(index, {emitEvent});
}
addBehavior() {
const behavior: IotSvgBehavior = {
id: '',
name: '',
type: IotSvgBehaviorType.value,
valueType: ValueType.BOOLEAN,
defaultValue: false
};
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();
});
}
private prepareBehaviorsFormArray(behaviors: IotSvgBehavior[] | undefined): UntypedFormArray {
const behaviorsControls: Array<AbstractControl> = [];
if (behaviors) {
behaviors.forEach((behavior) => {
behaviorsControls.push(this.fb.control(behavior, [behaviorValidator]));
});
}
return this.fb.array(behaviorsControls);
}
}

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

@ -24,13 +24,21 @@ import { ScadaSymbolMetadataComponent } from '@home/pages/scada-symbol/metadata-
import {
ScadaSymbolMetadataTagsComponent
} from '@home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component';
import {
ScadaSymbolBehaviorsComponent
} from '@home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component';
import {
ScadaSymbolBehaviorRowComponent
} from '@home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component';
@NgModule({
declarations:
[
ScadaSymbolMetadataComponent,
ScadaSymbolMetadataTagComponent,
ScadaSymbolMetadataTagsComponent
ScadaSymbolMetadataTagsComponent,
ScadaSymbolBehaviorsComponent,
ScadaSymbolBehaviorRowComponent
],
imports: [
CommonModule,

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

@ -22,7 +22,7 @@
<div class="tb-form-row tb-flex">
<div class="fixed-title-width" translate>scada.tag</div>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic" (click)="$event.stopPropagation()">
<mat-select required formControlName="tag" placeholder="{{ 'scada.select-tag' | translate }}">
<mat-select #tagSelect required formControlName="tag" placeholder="{{ 'scada.select-tag' | translate }}">
<mat-option *ngFor="let tag of availableTags" [value]="tag">
{{ tag }}
</mat-option>

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

@ -37,6 +37,7 @@ import {
import { IotSvgTag } from '@home/components/widget/lib/svg/iot-svg.models';
import { MatExpansionPanel } from '@angular/material/expansion';
import { JsFuncComponent } from '@shared/components/js-func.component';
import { MatSelect } from '@angular/material/select';
@Component({
selector: 'tb-scada-symbol-metadata-tag',
@ -53,6 +54,9 @@ import { JsFuncComponent } from '@shared/components/js-func.component';
})
export class ScadaSymbolMetadataTagComponent implements ControlValueAccessor, OnInit, OnChanges {
@ViewChild('tagSelect')
tagSelect: MatSelect;
@ViewChild('expansionPanel')
expansionPanel: MatExpansionPanel;
@ -163,6 +167,11 @@ export class ScadaSymbolMetadataTagComponent implements ControlValueAccessor, On
});
}
focus() {
this.tagSelect._elementRef.nativeElement.scrollIntoView();
this.tagSelect.focus();
}
private openPanelWithCallback(panel: MatExpansionPanel, callback: () => void) {
if (!panel.expanded) {
const s = panel.afterExpand.subscribe(() => {

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

@ -164,6 +164,10 @@ export class ScadaSymbolMetadataTagsComponent implements ControlValueAccessor, O
addTag() {
this.addNewTag(null);
setTimeout(() => {
const tagComponent = this.metadataTags.get(this.metadataTags.length-1);
tagComponent.focus();
});
}
editTagStateRenderFunction(tag: string): void {

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

@ -58,5 +58,10 @@
formControlName="tags">
</tb-scada-symbol-metadata-tags>
</div>
<div [fxShow]="selectedOption === 'behavior'" class="mat-content overflow-hidden">
<tb-scada-symbol-metadata-behaviors
formControlName="behavior">
</tb-scada-symbol-metadata-behaviors>
</div>
</div>
</div>

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

@ -80,7 +80,7 @@ export class ScadaSymbolMetadataComponent extends PageComponent implements OnIni
value: 'tags'
},
{
name: this.translate.instant('scada.behavior'),
name: this.translate.instant('scada.behavior.behavior'),
value: 'behavior'
},
{

13
ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.ts

@ -218,13 +218,22 @@ export class ScadaSymbolComponent extends PageComponent
enterPreviewMode() {
this.symbolData.svgContent = this.prepareSvgContent();
this.previewIotSvgObjectSettings = {};
this.previewIotSvgObjectSettings = {
behavior: {},
properties: {}
};
this.scadaPreviewFormGroup.patchValue({
iotSvgObject: this.previewIotSvgObjectSettings
}, {emitEvent: false});
this.scadaPreviewFormGroup.markAsPristine();
const settings: IotSvgWidgetSettings = {...iotSvgWidgetDefaultSettings,
...{iotSvg: null, iotSvgContent: this.symbolData.svgContent, iotSvgObject: this.previewIotSvgObjectSettings}};
...{
simulated: true,
iotSvg: null,
iotSvgContent: this.symbolData.svgContent,
iotSvgObject: this.previewIotSvgObjectSettings
}
};
this.previewWidget = {
typeFullFqn: 'system.iot_svg',
type: widgetType.rpc,

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

@ -3389,9 +3389,6 @@
"error-entities": "There was an error creating {{count}} entities."
}
},
"iot-svg": {
"behavior": "Behavior"
},
"scada": {
"symbol": "SCADA symbol",
"symbols": "SCADA symbols",
@ -3413,7 +3410,6 @@
"symbol-preview": "Symbol preview",
"general": "General",
"tags": "Tags",
"behavior": "Behavior",
"properties": "Properties",
"title": "Title",
"state-render-function": "State render function",
@ -3423,7 +3419,21 @@
"remove-tag": "Remove tag",
"add-tag": "Add tag",
"no-tags": "No tags configured",
"select-tag": "Select tag"
"select-tag": "Select tag",
"preview-widget-action-text": "Widget action '{{type}}' successfully invoked!",
"behavior": {
"behavior": "Behavior",
"id": "Id",
"name": "Name",
"type": "Type",
"no-behaviors": "No behaviors configured",
"add-behavior": "Add behavior",
"type-action": "Action",
"type-value": "Value",
"type-widget-action": "Widget action",
"behavior-settings": "Behavior settings",
"remove-behavior": "Remove behavior"
}
},
"item": {
"selected": "Selected"

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

@ -28,7 +28,7 @@
"tag": "onButton",
"actions": {
"click": {
"actionFunction": "ctx.api.callAction(event, 'onUpdateState');"
"actionFunction": "ctx.api.callAction(event, 'onUpdateState', undefined, {\n next: () => {\n ctx.api.setValue('on', true);\n }\n});"
}
}
},
@ -36,7 +36,7 @@
"tag": "offButton",
"actions": {
"click": {
"actionFunction": "ctx.api.callAction(event, 'offUpdateState');"
"actionFunction": "ctx.api.callAction(event, 'offUpdateState', undefined, {\n next: () => {\n ctx.api.setValue('on', false);\n }\n});\n"
}
}
},
@ -59,33 +59,30 @@
],
"behavior": [
{
"id": "levelState",
"id": "level",
"name": "Level",
"type": "value",
"valueType": "DOUBLE",
"defaultValue": 0,
"valueId": "level"
"defaultValue": 0
},
{
"id": "disabledState",
"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,
"valueId": "disabled"
"defaultValue": false
},
{
"id": "onState",
"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,
"valueId": "on"
"defaultValue": false
},
{
"id": "onUpdateState",

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

1
ui-ngx/src/form.scss

@ -564,6 +564,7 @@
.tb-form-table-header {
height: 48px;
min-height: 48px;
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
&-cell {
color: rgba(0, 0, 0, 0.54);

Loading…
Cancel
Save