UI: SCADA symbol - implement create widget from SCADA symbol, add widget size parameters, make target device optional for SCADA symbol widgets.

This commit is contained in:
Igor Kulikov 2024-06-10 17:49:15 +03:00
parent 4fd84315a5
commit cfc4d3d65f
21 changed files with 255 additions and 37 deletions

View File

@ -6,6 +6,8 @@
"level", "level",
"fan" "fan"
], ],
"widgetSizeX": 3,
"widgetSizeY": 3,
"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}", "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": [ "tags": [
{ {

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -6,12 +6,12 @@
"description": "", "description": "",
"descriptor": { "descriptor": {
"type": "rpc", "type": "rpc",
"sizeX": 3.5, "sizeX": 3,
"sizeY": 3.5, "sizeY": 3,
"resources": [], "resources": [],
"templateHtml": "<tb-scada-symbol-widget\n [ctx]='ctx'\n [widgetTitlePanel]=\"widgetTitlePanel\">\n</tb-scada-symbol-widget>", "templateHtml": "<tb-scada-symbol-widget\n [ctx]='ctx'\n [widgetTitlePanel]=\"widgetTitlePanel\">\n</tb-scada-symbol-widget>",
"templateCss": "", "templateCss": "",
"controllerScript": "self.onInit = function() {\n self.ctx.$scope.actionWidget.onInit();\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '280px',\n previewHeight: '280px',\n embedTitlePanel: true,\n displayRpcMessageToast: false\n };\n};\n\nself.onDestroy = function() {\n}\n", "controllerScript": "self.onInit = function() {\n self.ctx.$scope.actionWidget.onInit();\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '300px',\n previewHeight: '320px',\n embedTitlePanel: true,\n targetDeviceOptional: true,\n displayRpcMessageToast: false\n };\n};\n\nself.onDestroy = function() {\n}\n",
"settingsSchema": "", "settingsSchema": "",
"dataKeySettingsSchema": "{}\n", "dataKeySettingsSchema": "{}\n",
"settingsDirective": "tb-scada-symbol-widget-settings", "settingsDirective": "tb-scada-symbol-widget-settings",

View File

@ -382,6 +382,12 @@ public class InstallScripts {
} }
settings.put("scadaSymbolUrl", symbolUrl); settings.put("scadaSymbolUrl", symbolUrl);
((ObjectNode)descriptor).put("defaultConfig", JacksonUtil.toString(defaultConfig)); ((ObjectNode)descriptor).put("defaultConfig", JacksonUtil.toString(defaultConfig));
((ObjectNode)descriptor).put("sizeX", metadata.getWidgetSizeX());
((ObjectNode)descriptor).put("sizeY", metadata.getWidgetSizeY());
String controllerScript = descriptor.get("controllerScript").asText();
controllerScript = controllerScript.replaceAll("previewWidth: '\\d*px'", "previewWidth: '" + (metadata.getWidgetSizeX() * 100) + "px'");
controllerScript = controllerScript.replaceAll("previewHeight: '\\d*px'", "previewHeight: '" + (metadata.getWidgetSizeY() * 100 + 20) + "px'");
((ObjectNode)descriptor).put("controllerScript", controllerScript);
var savedWidget = widgetTypeService.saveWidgetType(scadaSymbolWidget); var savedWidget = widgetTypeService.saveWidgetType(scadaSymbolWidget);
return savedWidget.getFqn(); return savedWidget.getFqn();
} }

View File

@ -283,6 +283,8 @@ public class ImageUtils {
private String title; private String title;
private String description; private String description;
private String[] searchTags; private String[] searchTags;
private int widgetSizeX;
private int widgetSizeY;
public ScadaSymbolMetadataInfo(String fileName, JsonNode metaData) { public ScadaSymbolMetadataInfo(String fileName, JsonNode metaData) {
if (metaData != null && metaData.has("title")) { if (metaData != null && metaData.has("title")) {
@ -304,6 +306,16 @@ public class ImageUtils {
} else { } else {
searchTags = new String[0]; searchTags = new String[0];
} }
if (metaData != null && metaData.has("widgetSizeX")) {
widgetSizeX = metaData.get("widgetSizeX").asInt();
} else {
widgetSizeX = 3;
}
if (metaData != null && metaData.has("widgetSizeY")) {
widgetSizeY = metaData.get("widgetSizeY").asInt();
} else {
widgetSizeY = 3;
}
} }
} }

View File

@ -23,7 +23,9 @@
</mat-spinner> </mat-spinner>
</div> </div>
<div id="gridster-parent" <div id="gridster-parent"
fxFlex class="tb-dashboard-content layout-wrap" [class]="{'autofill-height': isAutofillHeight()}" fxFlex class="tb-dashboard-content" [class.autofill-height]="isAutofillHeight()"
[class.center-vertical]="centerVertical"
[class.center-horizontal]="centerHorizontal"
(contextmenu)="openDashboardContextMenu($event)"> (contextmenu)="openDashboardContextMenu($event)">
<div #dashboardMenuTrigger="matMenuTrigger" style="visibility: hidden; position: fixed" <div #dashboardMenuTrigger="matMenuTrigger" style="visibility: hidden; position: fixed"
[style.left]="dashboardMenuPosition.x" [style.left]="dashboardMenuPosition.x"
@ -61,7 +63,7 @@
</div> </div>
</ng-template> </ng-template>
</mat-menu> </mat-menu>
<div [class]="dashboardClass" id="gridster-background" style="height: 100%;"> <div [class]="dashboardClass" id="gridster-background">
<gridster #gridster id="gridster-child" [options]="gridsterOpts"> <gridster #gridster id="gridster-child" [options]="gridsterOpts">
<gridster-item #gridsterItem [item]="widget" [class]="{'tb-noselect': isEdit}" *ngFor="let widget of dashboardWidgets"> <gridster-item #gridsterItem [item]="widget" [class]="{'tb-noselect': isEdit}" *ngFor="let widget of dashboardWidgets">
<tb-widget-container <tb-widget-container

View File

@ -37,6 +37,10 @@
outline: none; outline: none;
overflow-y: auto; overflow-y: auto;
#gridster-background {
height: 100%;
}
gridster-item { gridster-item {
transition: none; transition: none;
overflow: visible; overflow: visible;
@ -49,6 +53,30 @@
overflow-y: hidden; overflow-y: hidden;
} }
} }
&.center-vertical, &.center-horizontal {
display: flex;
align-items: center;
justify-content: center;
#gridster-child {
width: 100%;
height: 100%;
}
}
&.center-vertical {
#gridster-background {
height: auto;
max-height: 100%;
width: 100%;
}
}
&.center-horizontal {
#gridster-background {
width: auto;
max-width: 100%;
height: 100%;
}
}
} }
#gridster-child { #gridster-child {

View File

@ -59,6 +59,8 @@ import { ResizeObserver } from '@juggle/resize-observer';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { WidgetComponentAction, WidgetComponentActionType } from '@home/components/widget/widget-container.component'; import { WidgetComponentAction, WidgetComponentActionType } from '@home/components/widget/widget-container.component';
import { TbPopoverComponent } from '@shared/components/popover.component'; import { TbPopoverComponent } from '@shared/components/popover.component';
import { displayGrids } from 'angular-gridster2/lib/gridsterConfig.interface';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({ @Component({
selector: 'tb-dashboard', selector: 'tb-dashboard',
@ -88,12 +90,30 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
@Input() @Input()
columns: number; columns: number;
@Input()
@coerceBoolean()
setGridSize = false;
@Input() @Input()
margin: number; margin: number;
@Input() @Input()
outerMargin: boolean; outerMargin: boolean;
@Input()
displayGrid: displayGrids = 'onDrag&Resize';
@Input()
gridType: GridType;
@Input()
@coerceBoolean()
centerVertical = false;
@Input()
@coerceBoolean()
centerHorizontal = false;
@Input() @Input()
isEdit: boolean; isEdit: boolean;
@ -208,7 +228,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
this.dashboardTimewindow = this.timeService.defaultTimewindow(); this.dashboardTimewindow = this.timeService.defaultTimewindow();
} }
this.gridsterOpts = { this.gridsterOpts = {
gridType: GridType.ScrollVertical, gridType: this.gridType || GridType.ScrollVertical,
keepFixedHeightInMobile: true, keepFixedHeightInMobile: true,
disableWarnings: false, disableWarnings: false,
disableAutoPositionOnConflict: false, disableAutoPositionOnConflict: false,
@ -216,6 +236,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
swap: false, swap: false,
maxRows: 3000, maxRows: 3000,
minCols: this.columns ? this.columns : 24, minCols: this.columns ? this.columns : 24,
setGridSize: this.setGridSize,
maxCols: 3000, maxCols: 3000,
maxItemCols: 1000, maxItemCols: 1000,
maxItemRows: 1000, maxItemRows: 1000,
@ -226,6 +247,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
minItemRows: 1, minItemRows: 1,
defaultItemCols: 8, defaultItemCols: 8,
defaultItemRows: 6, defaultItemRows: 6,
displayGrid: this.displayGrid,
resizable: {enabled: this.isEdit}, resizable: {enabled: this.isEdit},
draggable: {enabled: this.isEdit}, draggable: {enabled: this.isEdit},
itemChangeCallback: item => this.dashboardWidgets.sortWidgets(), itemChangeCallback: item => this.dashboardWidgets.sortWidgets(),
@ -530,7 +552,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
if (autofillHeight) { if (autofillHeight) {
this.gridsterOpts.gridType = this.isMobileSize ? GridType.Fixed : GridType.Fit; this.gridsterOpts.gridType = this.isMobileSize ? GridType.Fixed : GridType.Fit;
} else { } else {
this.gridsterOpts.gridType = this.isMobileSize ? GridType.Fixed : GridType.ScrollVertical; this.gridsterOpts.gridType = this.isMobileSize ? GridType.Fixed : this.gridType || GridType.ScrollVertical;
} }
const mobileBreakPoint = this.isMobileSize ? 20000 : 0; const mobileBreakPoint = this.isMobileSize ? 20000 : 0;
this.gridsterOpts.mobileBreakpoint = mobileBreakPoint; this.gridsterOpts.mobileBreakpoint = mobileBreakPoint;

View File

@ -24,13 +24,13 @@
</tb-toggle-select> </tb-toggle-select>
</div> </div>
<tb-entity-autocomplete *ngIf="targetDeviceFormGroup.get('type').value === targetDeviceType.device" <tb-entity-autocomplete *ngIf="targetDeviceFormGroup.get('type').value === targetDeviceType.device"
[required]="!widgetEditMode" [required]="(!widgetEditMode && !targetDeviceOptional)"
[entityType]="entityType.DEVICE" [entityType]="entityType.DEVICE"
formControlName="deviceId"> formControlName="deviceId">
</tb-entity-autocomplete> </tb-entity-autocomplete>
<tb-entity-alias-select <tb-entity-alias-select
*ngIf="targetDeviceFormGroup.get('type').value === targetDeviceType.entity" *ngIf="targetDeviceFormGroup.get('type').value === targetDeviceType.entity"
[tbRequired]="!widgetEditMode" [tbRequired]="(!widgetEditMode && !targetDeviceOptional)"
[aliasController]="aliasController" [aliasController]="aliasController"
[callbacks]="entityAliasSelectCallbacks" [callbacks]="entityAliasSelectCallbacks"
formControlName="entityAliasId"> formControlName="entityAliasId">

View File

@ -60,6 +60,10 @@ export class TargetDeviceComponent implements ControlValueAccessor, OnInit, Vali
return this.widgetConfigComponent.widgetConfigCallbacks; return this.widgetConfigComponent.widgetConfigCallbacks;
} }
public get targetDeviceOptional(): boolean {
return this.widgetConfigComponent.modelValue?.typeParameters?.targetDeviceOptional;
}
targetDeviceType = TargetDeviceType; targetDeviceType = TargetDeviceType;
entityType = EntityType; entityType = EntityType;
@ -103,9 +107,9 @@ export class TargetDeviceComponent implements ControlValueAccessor, OnInit, Vali
ngOnInit() { ngOnInit() {
this.targetDeviceFormGroup = this.fb.group({ this.targetDeviceFormGroup = this.fb.group({
type: [null, !this.widgetEditMode ? [Validators.required] : []], type: [null, (!this.widgetEditMode && !this.targetDeviceOptional) ? [Validators.required] : []],
deviceId: [null, !this.widgetEditMode ? [Validators.required] : []], deviceId: [null, (!this.widgetEditMode && !this.targetDeviceOptional) ? [Validators.required] : []],
entityAliasId: [null, !this.widgetEditMode ? [Validators.required] : []] entityAliasId: [null, (!this.widgetEditMode && !this.targetDeviceOptional) ? [Validators.required] : []]
}); });
this.targetDeviceFormGroup.get('type').valueChanges.subscribe(() => { this.targetDeviceFormGroup.get('type').valueChanges.subscribe(() => {
this.updateValidators(); this.updateValidators();

View File

@ -177,6 +177,8 @@ export interface ScadaSymbolMetadata {
title: string; title: string;
description?: string; description?: string;
searchTags?: string[]; searchTags?: string[];
widgetSizeX: number;
widgetSizeY: number;
stateRenderFunction?: string; stateRenderFunction?: string;
stateRender?: ScadaSymbolStateRenderFunction; stateRender?: ScadaSymbolStateRenderFunction;
tags: ScadaSymbolTag[]; tags: ScadaSymbolTag[];
@ -186,6 +188,8 @@ export interface ScadaSymbolMetadata {
export const emptyMetadata = (): ScadaSymbolMetadata => ({ export const emptyMetadata = (): ScadaSymbolMetadata => ({
title: '', title: '',
widgetSizeX: 3,
widgetSizeY: 3,
tags: [], tags: [],
behavior: [], behavior: [],
properties: [] properties: []
@ -443,6 +447,7 @@ export class ScadaSymbolObject {
this.stateValueSubjects[stateValueId].unsubscribe(); this.stateValueSubjects[stateValueId].unsubscribe();
} }
this.valueActions.forEach(v => v.destroy()); this.valueActions.forEach(v => v.destroy());
if (this.context) {
for (const tag of this.metadata.tags) { for (const tag of this.metadata.tags) {
const elements = this.context.tags[tag.tag]; const elements = this.context.tags[tag.tag];
elements.forEach(element => { elements.forEach(element => {
@ -450,6 +455,7 @@ export class ScadaSymbolObject {
element.timeline(null); element.timeline(null);
}); });
} }
}
if (this.svgShape) { if (this.svgShape) {
this.svgShape.remove(); this.svgShape.remove();
} }

View File

@ -589,6 +589,9 @@ export class WidgetComponentService {
if (isUndefined(result.typeParameters.displayRpcMessageToast)) { if (isUndefined(result.typeParameters.displayRpcMessageToast)) {
result.typeParameters.displayRpcMessageToast = true; result.typeParameters.displayRpcMessageToast = true;
} }
if (isUndefined(result.typeParameters.targetDeviceOptional)) {
result.typeParameters.targetDeviceOptional = false;
}
if (isFunction(widgetTypeInstance.actionSources)) { if (isFunction(widgetTypeInstance.actionSources)) {
result.actionSources = widgetTypeInstance.actionSources(); result.actionSources = widgetTypeInstance.actionSources();
} else { } else {

View File

@ -962,7 +962,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
if (this.modelValue) { if (this.modelValue) {
const config = this.modelValue.config; const config = this.modelValue.config;
if (this.widgetType === widgetType.rpc && this.modelValue.isDataEnabled) { if (this.widgetType === widgetType.rpc && this.modelValue.isDataEnabled) {
if (!this.widgetEditMode && !targetDeviceValid(config.targetDevice)) { if ((!this.widgetEditMode && !this.modelValue?.typeParameters.targetDeviceOptional) && !targetDeviceValid(config.targetDevice)) {
return { return {
targetDevice: { targetDevice: {
valid: false valid: false

View File

@ -52,6 +52,23 @@
</tb-string-items-list> </tb-string-items-list>
</div> </div>
</div> </div>
<div class="tb-form-row">
<div class="fixed-title-width" translate>scada.widget-size</div>
<div fxLayout="row" fxFlex fxLayoutAlign="end center" fxLayoutGap="8px">
<div class="tb-small-label">{{ 'scada.cols' | translate }}</div>
<mat-form-field appearance="outline" class="number flex" subscriptSizing="dynamic">
<input matInput formControlName="widgetSizeX" required
[min]="1" [max]="24" [step]="1"
type="number" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<div class="tb-small-label">{{ 'scada.rows' | translate }}</div>
<mat-form-field appearance="outline" class="number flex" subscriptSizing="dynamic">
<input matInput formControlName="widgetSizeY" required
[min]="1" [max]="24" [step]="1"
type="number" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
</div>
<div class="tb-form-panel stroked"> <div class="tb-form-panel stroked">
<mat-expansion-panel class="tb-settings" expanded> <mat-expansion-panel class="tb-settings" expanded>
<mat-expansion-panel-header fxLayout="row wrap"> <mat-expansion-panel-header fxLayout="row wrap">

View File

@ -130,6 +130,8 @@ export class ScadaSymbolMetadataComponent extends PageComponent implements OnIni
title: [null, [Validators.required]], title: [null, [Validators.required]],
description: [null], description: [null],
searchTags: [null], searchTags: [null],
widgetSizeX: [null, [Validators.min(1), Validators.max(24), Validators.required]],
widgetSizeY: [null, [Validators.min(1), Validators.max(24), Validators.required]],
stateRenderFunction: [null], stateRenderFunction: [null],
tags: [null], tags: [null],
behavior: [null], behavior: [null],
@ -176,6 +178,8 @@ export class ScadaSymbolMetadataComponent extends PageComponent implements OnIni
title: value?.title, title: value?.title,
description: value?.description, description: value?.description,
searchTags: value?.searchTags, searchTags: value?.searchTags,
widgetSizeX: value?.widgetSizeX || 3,
widgetSizeY: value?.widgetSizeY || 3,
stateRenderFunction: value?.stateRenderFunction, stateRenderFunction: value?.stateRenderFunction,
tags: value?.tags, tags: value?.tags,
behavior: value?.behavior, behavior: value?.behavior,

View File

@ -29,6 +29,13 @@
[showCloseDetails]="false" [showCloseDetails]="false"
headerHeightPx="64" headerHeightPx="64"
headerTitle="{{symbolData?.imageResource?.title}}"> headerTitle="{{symbolData?.imageResource?.title}}">
<div class="details-buttons" fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<button mat-stroked-button
[disabled]="scadaSymbolFormGroup.invalid"
(click)="createWidget()">
{{ 'scada.create-widget' | translate }}
</button>
</div>
<div *ngIf="previewMode" class="tb-scada-symbol-editor-preview-content tb-absolute-fill"> <div *ngIf="previewMode" class="tb-scada-symbol-editor-preview-content tb-absolute-fill">
<div class="tb-scada-symbol-editor-preview-header"> <div class="tb-scada-symbol-editor-preview-header">
<button mat-button <button mat-button
@ -110,7 +117,7 @@
</div> </div>
</tb-details-panel> </tb-details-panel>
</mat-drawer> </mat-drawer>
<mat-drawer-content class="tb-scada-symbol-editor-content"> <mat-drawer-content class="tb-scada-symbol-editor-content" [class.preview]="previewMode">
<tb-scada-symbol-editor *ngIf="!previewMode" #symbolEditor <tb-scada-symbol-editor *ngIf="!previewMode" #symbolEditor
[readonly]="readonly" [readonly]="readonly"
[data]="symbolEditorData" [data]="symbolEditorData"
@ -121,8 +128,13 @@
class="tb-absolute-fill" class="tb-absolute-fill"
[aliasController]="aliasController" [aliasController]="aliasController"
[widgets]="previewWidgets" [widgets]="previewWidgets"
[autofillHeight]="true" [autofillHeight]="false"
[columns]="24" displayGrid="always"
[gridType]="previewWidget.sizeX >= previewWidget.sizeY ? GridType.ScrollVertical : GridType.ScrollHorizontal"
[columns]="previewWidget.sizeX"
setGridSize
[centerVertical]="previewWidget.sizeX >= previewWidget.sizeY"
[centerHorizontal]="previewWidget.sizeX < previewWidget.sizeY"
[margin]="0" [margin]="0"
[isEdit]="false" [isEdit]="false"
[isMobileDisabled]="true" [isMobileDisabled]="true"

View File

@ -63,5 +63,12 @@
min-height: 0; min-height: 0;
max-width: 50%; max-width: 50%;
background: #fff; background: #fff;
&.preview {
#gridster-parent {
#gridster-background {
background-color: #eee;
}
}
}
} }
} }

View File

@ -45,7 +45,7 @@ import {
ScadaSymbolEditorData ScadaSymbolEditorData
} from '@home/pages/scada-symbol/scada-symbol-editor.component'; } from '@home/pages/scada-symbol/scada-symbol-editor.component';
import { ImageService } from '@core/http/image.service'; import { ImageService } from '@core/http/image.service';
import { imageResourceType, IMAGES_URL_PREFIX } from '@shared/models/resource.models'; import { imageResourceType, IMAGES_URL_PREFIX, TB_IMAGE_PREFIX } from '@shared/models/resource.models';
import { HasDirtyFlag } from '@core/guards/confirm-on-exit.guard'; import { HasDirtyFlag } from '@core/guards/confirm-on-exit.guard';
import { IAliasController, IStateController, StateParams } from '@core/api/widget-api.models'; import { IAliasController, IStateController, StateParams } from '@core/api/widget-api.models';
import { EntityAliases } from '@shared/models/alias.models'; import { EntityAliases } from '@shared/models/alias.models';
@ -54,7 +54,7 @@ import { AliasController } from '@core/api/alias-controller';
import { EntityService } from '@core/http/entity.service'; import { EntityService } from '@core/http/entity.service';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { Widget, widgetType } from '@shared/models/widget.models'; import { Widget, WidgetConfig, widgetType, WidgetTypeDetails } from '@shared/models/widget.models';
import { import {
scadaSymbolWidgetDefaultSettings, scadaSymbolWidgetDefaultSettings,
ScadaSymbolWidgetSettings ScadaSymbolWidgetSettings
@ -71,6 +71,14 @@ import {
UploadImageDialogData, UploadImageDialogResult UploadImageDialogData, UploadImageDialogResult
} from '@shared/components/image/upload-image-dialog.component'; } from '@shared/components/image/upload-image-dialog.component';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { BackgroundType } from '@shared/models/widget-settings.models';
import { GridType } from 'angular-gridster2';
import {
SaveWidgetTypeAsDialogComponent, SaveWidgetTypeAsDialogData,
SaveWidgetTypeAsDialogResult
} from '@home/pages/widget/save-widget-type-as-dialog.component';
import { WidgetService } from '@core/http/widget.service';
import { de } from 'date-fns/locale';
@Component({ @Component({
selector: 'tb-scada-symbol', selector: 'tb-scada-symbol',
@ -83,6 +91,8 @@ export class ScadaSymbolComponent extends PageComponent
widgetType = widgetType; widgetType = widgetType;
GridType = GridType;
@HostBinding('style.width') width = '100%'; @HostBinding('style.width') width = '100%';
@HostBinding('style.height') height = '100%'; @HostBinding('style.height') height = '100%';
@ -115,6 +125,8 @@ export class ScadaSymbolComponent extends PageComponent
fetchCellClickColumns: () => [] fetchCellClickColumns: () => []
}; };
previewWidget: Widget;
previewWidgets: Array<Widget> = []; previewWidgets: Array<Widget> = [];
tags: string[]; tags: string[];
@ -125,8 +137,6 @@ export class ScadaSymbolComponent extends PageComponent
private previewScadaSymbolObjectSettings: ScadaSymbolObjectSettings; private previewScadaSymbolObjectSettings: ScadaSymbolObjectSettings;
private previewWidget: Widget;
private forcePristine = false; private forcePristine = false;
private authUser = getCurrentAuthUser(this.store); private authUser = getCurrentAuthUser(this.store);
@ -149,6 +159,7 @@ export class ScadaSymbolComponent extends PageComponent
private utils: UtilsService, private utils: UtilsService,
private translate: TranslateService, private translate: TranslateService,
private imageService: ImageService, private imageService: ImageService,
private widgetService: WidgetService,
private dialog: MatDialog) { private dialog: MatDialog) {
super(store); super(store);
} }
@ -247,14 +258,23 @@ export class ScadaSymbolComponent extends PageComponent
scadaSymbolUrl: null, scadaSymbolUrl: null,
scadaSymbolContent: this.symbolData.scadaSymbolContent, scadaSymbolContent: this.symbolData.scadaSymbolContent,
scadaSymbolObjectSettings: this.previewScadaSymbolObjectSettings, scadaSymbolObjectSettings: this.previewScadaSymbolObjectSettings,
padding: '0' padding: '0',
background: {
type: BackgroundType.color,
color: 'rgba(0,0,0,0)',
overlay: {
enabled: false,
color: 'rgba(255,255,255,0.72)',
blur: 3
}
}
} }
}; };
this.previewWidget = { this.previewWidget = {
typeFullFqn: 'system.scada_symbol', typeFullFqn: 'system.scada_symbol',
type: widgetType.rpc, type: widgetType.rpc,
sizeX: 24, sizeX: this.previewMetadata.widgetSizeX || 3,
sizeY: 24, sizeY: this.previewMetadata.widgetSizeY || 3,
row: 0, row: 0,
col: 0, col: 0,
config: { config: {
@ -262,7 +282,8 @@ export class ScadaSymbolComponent extends PageComponent
showTitle: false, showTitle: false,
dropShadow: false, dropShadow: false,
padding: '0', padding: '0',
margin: '0' margin: '0',
backgroundColor: 'rgba(0,0,0,0)'
} }
}; };
this.previewWidgets = [this.previewWidget]; this.previewWidgets = [this.previewWidget];
@ -366,6 +387,56 @@ export class ScadaSymbolComponent extends PageComponent
linkElement.dispatchEvent(clickEvent); linkElement.dispatchEvent(clickEvent);
} }
createWidget() {
const metadata: ScadaSymbolMetadata = this.scadaSymbolFormGroup.get('metadata').value;
this.dialog.open<SaveWidgetTypeAsDialogComponent, SaveWidgetTypeAsDialogData,
SaveWidgetTypeAsDialogResult>(SaveWidgetTypeAsDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
title: metadata.title,
dialogTitle: 'scada.create-widget-from-symbol',
saveAsActionTitle: 'action.create'
}
}).afterClosed().subscribe(
(saveWidgetAsData) => {
if (saveWidgetAsData) {
this.widgetService.getWidgetType('system.scada_symbol').subscribe(
(widgetTemplate) => {
const symbolUrl = TB_IMAGE_PREFIX + this.symbolData.imageResource.link;
const widget: WidgetTypeDetails = {
image: symbolUrl,
description: metadata.description,
tags: metadata.searchTags,
...widgetTemplate
};
widget.fqn = undefined;
widget.id = undefined;
widget.name = saveWidgetAsData.widgetName;
const descriptor = widget.descriptor;
descriptor.sizeX = metadata.widgetSizeX;
descriptor.sizeY = metadata.widgetSizeY;
descriptor.controllerScript = descriptor.controllerScript
.replace(/previewWidth: '\d*px'/gm, `previewWidth: '${metadata.widgetSizeX * 100}px'`);
descriptor.controllerScript = descriptor.controllerScript
.replace(/previewHeight: '\d*px'/gm, `previewHeight: '${metadata.widgetSizeY * 100 + 20}px'`);
const config: WidgetConfig = JSON.parse(descriptor.defaultConfig);
config.title = saveWidgetAsData.widgetName;
config.settings = config.settings || {};
config.settings.scadaSymbolUrl = symbolUrl;
descriptor.defaultConfig = JSON.stringify(config);
this.widgetService.saveWidgetType(widget).subscribe((saved) => {
if (saveWidgetAsData.widgetBundleId) {
this.widgetService.addWidgetFqnToWidgetBundle(saveWidgetAsData.widgetBundleId, saved.fqn).subscribe();
}
});
}
);
}
}
);
}
private updatePreviewWidgetSettings() { private updatePreviewWidgetSettings() {
this.previewWidget = deepClone(this.previewWidget); this.previewWidget = deepClone(this.previewWidget);
this.previewWidget.config.settings.scadaSymbolObjectSettings = this.previewScadaSymbolObjectSettings; this.previewWidget.config.settings.scadaSymbolObjectSettings = this.previewScadaSymbolObjectSettings;

View File

@ -15,9 +15,9 @@
limitations under the License. limitations under the License.
--> -->
<form [formGroup]="saveWidgetTypeAsFormGroup" (ngSubmit)="saveAs()" style="width: 360px"> <form [formGroup]="saveWidgetTypeAsFormGroup" (ngSubmit)="saveAs()" style="min-width: 360px">
<mat-toolbar color="primary"> <mat-toolbar color="primary">
<h2 translate>widget.save-widget-as</h2> <h2 translate>{{ dialogTitle }}</h2>
<span fxFlex></span> <span fxFlex></span>
<button mat-icon-button <button mat-icon-button
(click)="cancel()" (click)="cancel()"
@ -29,7 +29,7 @@
</mat-progress-bar> </mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content> <div mat-dialog-content>
<span translate>widget.save-widget-as-text</span> <span translate>{{ dialogTitle }}</span>
<mat-form-field class="mat-block"> <mat-form-field class="mat-block">
<mat-label translate>widget.title</mat-label> <mat-label translate>widget.title</mat-label>
<input matInput formControlName="title" required> <input matInput formControlName="title" required>
@ -53,7 +53,7 @@
type="submit" type="submit"
[disabled]="(isLoading$ | async) || saveWidgetTypeAsFormGroup.invalid [disabled]="(isLoading$ | async) || saveWidgetTypeAsFormGroup.invalid
|| !saveWidgetTypeAsFormGroup.dirty"> || !saveWidgetTypeAsFormGroup.dirty">
{{ 'action.saveAs' | translate }} {{ saveAsActionTitle | translate }}
</button> </button>
</div> </div>
</form> </form>

View File

@ -14,8 +14,8 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, OnInit } from '@angular/core'; import { Component, Inject, OnInit } from '@angular/core';
import { MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, Validators } from '@angular/forms';
@ -29,6 +29,12 @@ export interface SaveWidgetTypeAsDialogResult {
widgetBundleId?: string; widgetBundleId?: string;
} }
export interface SaveWidgetTypeAsDialogData {
dialogTitle?: string;
title?: string;
saveAsActionTitle?: string;
}
@Component({ @Component({
selector: 'tb-save-widget-type-as-dialog', selector: 'tb-save-widget-type-as-dialog',
templateUrl: './save-widget-type-as-dialog.component.html', templateUrl: './save-widget-type-as-dialog.component.html',
@ -39,9 +45,12 @@ export class SaveWidgetTypeAsDialogComponent extends
saveWidgetTypeAsFormGroup: FormGroup; saveWidgetTypeAsFormGroup: FormGroup;
bundlesScope: string; bundlesScope: string;
dialogTitle = 'widget.save-widget-as';
saveAsActionTitle = 'action.saveAs';
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
protected router: Router, protected router: Router,
@Inject(MAT_DIALOG_DATA) private data: SaveWidgetTypeAsDialogData,
public dialogRef: MatDialogRef<SaveWidgetTypeAsDialogComponent, SaveWidgetTypeAsDialogResult>, public dialogRef: MatDialogRef<SaveWidgetTypeAsDialogComponent, SaveWidgetTypeAsDialogResult>,
public fb: FormBuilder) { public fb: FormBuilder) {
super(store, router, dialogRef); super(store, router, dialogRef);
@ -52,11 +61,18 @@ export class SaveWidgetTypeAsDialogComponent extends
} else { } else {
this.bundlesScope = 'system'; this.bundlesScope = 'system';
} }
if (this.data?.dialogTitle) {
this.dialogTitle = this.data.dialogTitle;
}
if (this.data?.saveAsActionTitle) {
this.saveAsActionTitle = this.data.saveAsActionTitle;
}
} }
ngOnInit(): void { ngOnInit(): void {
this.saveWidgetTypeAsFormGroup = this.fb.group({ this.saveWidgetTypeAsFormGroup = this.fb.group({
title: [null, [Validators.required]], title: [this.data?.title, [Validators.required]],
widgetsBundle: [null] widgetsBundle: [null]
}); });
} }

View File

@ -187,6 +187,7 @@ export interface WidgetTypeParameters {
defaultLatestDataKeysFunction?: (configComponent: any, configData: any) => DataKey[]; defaultLatestDataKeysFunction?: (configComponent: any, configData: any) => DataKey[];
dataKeySettingsFunction?: DataKeySettingsFunction; dataKeySettingsFunction?: DataKeySettingsFunction;
displayRpcMessageToast?: boolean; displayRpcMessageToast?: boolean;
targetDeviceOptional?: boolean;
} }
export interface WidgetControllerDescriptor { export interface WidgetControllerDescriptor {

View File

@ -3455,6 +3455,9 @@
"title": "Title", "title": "Title",
"description": "Description", "description": "Description",
"search-tags": "Search tags", "search-tags": "Search tags",
"widget-size": "Widget size",
"cols": "cols",
"rows": "rows",
"state-render-function": "State render function", "state-render-function": "State render function",
"preview": "Preview", "preview": "Preview",
"preview-widget-action-text": "Widget action '{{type}}' successfully invoked!", "preview-widget-action-text": "Widget action '{{type}}' successfully invoked!",
@ -3464,6 +3467,8 @@
"browse-symbol-from-gallery": "Browse SCADA symbol from gallery", "browse-symbol-from-gallery": "Browse SCADA symbol from gallery",
"zoom-in": "Zoom In", "zoom-in": "Zoom In",
"zoom-out": "Zoom Out", "zoom-out": "Zoom Out",
"create-widget": "Create widget",
"create-widget-from-symbol": "Create widget from SCADA symbol",
"tag": { "tag": {
"tag": "Tag", "tag": "Tag",
"on-click-action": "On click action", "on-click-action": "On click action",