UNPKG

@c8y/ngx-components

Version:

Angular modules for Cumulocity IoT applications

1 lines 138 kB
{"version":3,"file":"c8y-ngx-components-widgets-implementations-scada.mjs","sources":["../../widgets/implementations/scada/scada-widget-config/scada.model.ts","../../widgets/implementations/scada/scada-widget.service.ts","../../widgets/implementations/scada/scada-widget-config/lit-helpers.ts","../../widgets/implementations/scada/scada-widget-config/scada-widget-config.service.ts","../../widgets/implementations/scada/scada-widget-config/scada-ai-chat-tool-details.component.ts","../../widgets/implementations/scada/scada-widget-config/scada-ai-chat-tool-details.component.html","../../widgets/implementations/scada/scada-widget-config/scada-widget-svg-selector.component.ts","../../widgets/implementations/scada/scada-widget-config/scada-widget-svg-selector.component.html","../../widgets/implementations/scada/scada-widget-config/scada-widget-placeholder-modal.component.ts","../../widgets/implementations/scada/scada-widget-view/scada-widget-view.component.ts","../../widgets/implementations/scada/scada-widget-view/scada-widget-view.component.html","../../widgets/implementations/scada/scada-widget-config/scada-widget-advanced-editor.component.ts","../../widgets/implementations/scada/scada-widget-config/scada-widget-advanced-editor.component.html","../../widgets/implementations/scada/c8y-ngx-components-widgets-implementations-scada.ts"],"sourcesContent":["import { IIdentified } from '@c8y/client';\nimport type { HtmlWidgetConfig } from '@c8y/ngx-components/widgets/implementations/html-widget';\nimport { AssetPropertyMappings, ContextWidgetConfig } from '@c8y/ngx-components/context-dashboard';\nimport { C8yPropertyType } from '@c8y/ngx-components';\n\nexport const SCADA_WIDGET_FEATURE_FLAG_NAME = 'ui.scada-widget.v2';\n\nexport interface ScadaWidgetConfig extends ContextWidgetConfig {\n config: {\n htmlWidgetConfig: HtmlWidgetConfig;\n mappings: AssetPropertyMappings;\n svgDisplaySettings?: SvgDisplaySettings;\n };\n}\n\nexport type DisplayOption = 'contain' | 'full-width';\nexport type HorizontalAlignment = 'left' | 'center' | 'right';\nexport type VerticalAlignment = 'top' | 'center' | 'bottom';\n\nexport interface SvgDisplaySettings {\n displayOption: DisplayOption;\n horizontalAlignment: HorizontalAlignment;\n verticalAlignment: VerticalAlignment;\n}\n\nexport interface SvgComputedStyling {\n svgWidth: string;\n svgHeight: string;\n svgMaxWidth: string;\n svgMaxHeight: string;\n /** null means the preserveAspectRatio attribute should be removed */\n preserveAspectRatio: string | null;\n containerDisplay: string;\n containerJustifyContent: string;\n containerAlignItems: string;\n containerOverflow: string;\n containerHeight: string;\n containerBoxSizing: string;\n}\n\nexport const defaultDisplayOption: DisplayOption = 'full-width';\nexport const defaultAlignment = 'center';\n\n// For backward compatibility with old SCADA widget configurations:\n\nexport interface ScadaWidgetConfigLegacy {\n binaryId: string;\n sanitization: 'strict' | 'lax' | 'none';\n device: Pick<IIdentified, 'id' | 'name'>;\n deviceIds: { [placeholderName: string]: string };\n mapping: { [placeholderName: string]: LegacyMapping };\n}\n\nexport type LegacyMapping = LegacyMappingBase | LegacyMappingComputed | LegacyMappingFieldbus;\n\nexport interface LegacyMappingBase {\n id: string;\n keyPath: string[];\n label: string;\n type: C8yPropertyType;\n}\n\nexport interface LegacyMappingComputed extends LegacyMappingBase {\n computed: true;\n configSchema: object;\n config: {\n _id: string;\n [key: string]: any;\n };\n}\n\nexport interface LegacyMappingFieldbus {\n fieldbus: true;\n id: string;\n keyPath: [string, string];\n label: string;\n}\n\nexport interface ActiveAlarmsStatus {\n critical: number;\n major: number;\n minor: number;\n warning: number;\n}\n\nexport const INITIAL_SVG_CODE = `<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<svg width=\"600px\" height=\"1350px\" viewBox=\"0 0 600 1350\" xmlns=\"http://www.w3.org/2000/svg\">\n <style>\n /* Typography */\n .header {\n font-family: var(--c8y-font-family-base);\n font-size: 28px;\n font-weight: bold;\n fill: var(--text-color, var(--c8y-component-color-default, var(--c8y-root-component-color-default)));\n }\n .branded {\n fill: var(--brand-primary,var(--c8y-brand-primary));\n }\n .step-number {\n font-family: var(--c8y-font-family-base);\n font-size: 18px;\n font-weight: bold;\n fill: var(--c8y-component-background-default, var(--c8y-root-component-background-default));\n }\n .step-title {\n font-family: var(--c8y-font-family-base);\n font-size: 16px;\n font-weight: bold;\n fill: var(--text-color, var(--c8y-component-color-default, var(--c8y-root-component-color-default)));\n }\n .description {\n font-family: var(--c8y-font-family-base);\n font-size: 14px;\n }\n .placeholder {\n font-family: var(--c8y-font-family-base);\n font-size: 14px;\n fill: var(--brand-primary,var(--c8y-brand-primary));\n font-weight: bold;\n }\n .code {\n font-family: var(--c8y-font-family-mono-spaced);\n font-size: 12px;\n fill: var(--c8y-code-color);\n }\n .label {\n font-family: var(--c8y-font-family-base);\n font-size: 13px;\n fill: var(--text-color, var(--c8y-component-color-default, var(--c8y-root-component-color-default)));\n }\n \n /* Components */\n .section-bg {\n fill: var(--c8y-pre-background);\n stroke: var(--c8y-pre-border-color);\n stroke-width: 1px;\n rx: 4px;\n }\n .step-circle {\n fill: var(--brand-primary,var(--c8y-brand-primary));\n }\n .code-bg {\n fill: #0000000d;\n stroke: transparent;\n stroke-width: 1px;\n rx: 13px;\n }\n .button{\n cursor: pointer;\n }\n .button-bg {\n fill: var(--brand-primary,var(--c8y-brand-primary));\n rx: var(--c8y-btn-border-radius-base);\n stroke: var(--brand-primary,var(--c8y-brand-primary));\n stroke-width: 1px;\n }\n .button-text {\n font-family: inherit;\n font-size: 14px;\n fill: var(--c8y-component-background-default, var(--c8y-root-component-background-default));\n text-anchor: middle;\n }\n .button:hover .button-bg {\n fill: var(--c8y-component-background-default, var(--c8y-root-component-background-default));\n }\n .button:hover .button-text {\n fill: var(--brand-primary,var(--c8y-brand-primary));\n }\n .divider {\n stroke: var(--c8y-component-border-color, var(--c8y-root-component-border-color));\n stroke-width: 2px;\n }\n \n /* Alarm status indicators */\n .alarm-none {\n fill: var(--c8y-palette-status-success);\n }\n .alarm-warning {\n fill: var(--c8y-palette-status-info);\n }\n .alarm-minor {\n fill: var(--c8y-palette-status-warning-high);\n }\n .alarm-major {\n fill: var(--c8y-palette-status-warning);\n }\n .alarm-critical {\n fill: var(--c8y-palette-status-danger);\n }\n \n /* Conditional text colors */\n .temp-ok {\n fill: var(--c8y-palette-status-success);\n }\n .temp-warning {\n fill:var(--c8y-palette-status-danger);\n }\n </style>\n \n <!-- Background -->\n <rect width=\"600\" height=\"1400\" fill=\"var(--c8y-component-background-default, var(--c8y-root-component-background-default))\" />\n \n <!-- Header -->\n <text x=\"40\" y=\"50\" class=\"header\">SCADA widget example</text>\n \n <text x=\"40\" y=\"90\" class=\"description\">Follow these steps to customize your visualization</text>\n \n <!-- Divider -->\n <line x1=\"40\" y1=\"110\" x2=\"560\" y2=\"110\" class=\"divider\" />\n \n <!-- STEP 1: SVG configuration -->\n <circle cx=\"55\" cy=\"150\" r=\"15\" class=\"step-circle\" />\n <text x=\"55\" y=\"157\" class=\"step-number\" text-anchor=\"middle\">1</text>\n <text x=\"85\" y=\"155\" class=\"step-title\">SVG configuration</text>\n \n <text x=\"85\" y=\"180\" class=\"description\">Use \"SVG configuration\" section to upload a file, paste code, or generate with AI.</text>\n \n <!-- STEP 2: Add Placeholders -->\n <circle cx=\"55\" cy=\"230\" r=\"15\" class=\"step-circle\" />\n <text x=\"55\" y=\"237\" class=\"step-number\" text-anchor=\"middle\">2</text>\n <text x=\"85\" y=\"235\" class=\"step-title\">Add placeholders</text>\n \n <text x=\"85\" y=\"260\" class=\"description\">Click on a text or tspan element in the preview to convert it into a placeholder.</text>\n \n <rect x=\"85\" y=\"280\" width=\"475\" height=\"80\" class=\"section-bg\" />\n <text x=\"100\" y=\"305\" class=\"label\">Example - click text below to make it dynamic:</text>\n <text x=\"100\" y=\"330\" class=\"placeholder\"><tspan>Device name</tspan> (<tspan>Device ID</tspan>)</text>\n \n <!-- STEP 3: Configure Data Binding -->\n <circle cx=\"55\" cy=\"395\" r=\"15\" class=\"step-circle\" />\n <text x=\"55\" y=\"402\" class=\"step-number\" text-anchor=\"middle\">3</text>\n <text x=\"85\" y=\"400\" class=\"step-title\">Configure data binding</text>\n \n <text x=\"85\" y=\"425\" class=\"description\">Assign asset properties to placeholders in the \"Placeholder mappings\" section</text>\n <text x=\"85\" y=\"445\" class=\"description\">to see their values in the preview.</text>\n \n <!-- Example with actual placeholders -->\n <rect x=\"40\" y=\"470\" width=\"520\" height=\"120\" class=\"section-bg\" />\n <text x=\"60\" y=\"495\" class=\"label\">Examples:</text>\n \n <text x=\"60\" y=\"525\" class=\"label\">Device name:</text>\n <text x=\"180\" y=\"525\" class=\"placeholder\">\\${this.c8yScadaValues?.deviceName || '-'}</text>\n\n <text x=\"60\" y=\"550\" class=\"label\">Temperature:</text>\n <text x=\"180\" y=\"550\" class=\"placeholder\">\\${this.c8yScadaValues?.temperature || '-'} °C</text>\n\n <text x=\"60\" y=\"575\" class=\"label\">Status:</text>\n <text x=\"180\" y=\"575\" class=\"placeholder\">\\${this.c8yScadaValues?.status || '-'}</text>\n\n <!-- STEP 4: Advanced Customization -->\n <circle cx=\"55\" cy=\"630\" r=\"15\" class=\"step-circle\" />\n <text x=\"55\" y=\"637\" class=\"step-number\" text-anchor=\"middle\">4</text>\n <text x=\"85\" y=\"635\" class=\"step-title\">Advanced: Edit component code</text>\n\n <text x=\"85\" y=\"660\" class=\"description\">Open \"Advanced editor\" section to customize the web component directly.</text>\n \n <!-- Advanced Examples -->\n <rect x=\"40\" y=\"690\" width=\"520\" height=\"630\" class=\"section-bg\" />\n <text x=\"60\" y=\"715\" class=\"label\">Advanced examples (see implementation in the editor):</text>\n \n <!-- Example 1a: Interactive Button - Device Details -->\n <text x=\"60\" y=\"745\" class=\"description\">• Button navigating to device details:</text>\n <g class=\"button\" @click=\\${() => this.c8yScadaFunctions?.goToDeviceDetails(this.c8yScadaValues?.deviceId)}>\n <rect x=\"70\" y=\"755\" width=\"200\" height=\"35\" class=\"button-bg\" />\n <text x=\"170\" y=\"777\" class=\"button-text\">Go to device details</text>\n </g>\n\n <!-- Example 1b: Interactive Button - Group Details -->\n <text x=\"60\" y=\"820\" class=\"description\">• Button navigating to group details:</text>\n <g class=\"button\" @click=\\${() => this.c8yScadaFunctions?.goToGroupDetails(this.c8yScadaValues?.groupId)}>\n <rect x=\"70\" y=\"830\" width=\"200\" height=\"35\" class=\"button-bg\" />\n <text x=\"170\" y=\"852\" class=\"button-text\">Go to group details</text>\n </g>\n\n <!-- Example 2: Conditional Styling -->\n <text x=\"60\" y=\"895\" class=\"description\">• Conditional styling based on temperature range:</text>\n <text x=\"70\" y=\"920\" \n class=\"\\${(this.c8yScadaValues?.temperature >= 20 && this.c8yScadaValues?.temperature <= 30) ? 'temp-ok' : 'temp-warning'}\"\n font-size=\"14px\"\n font-weight=\"bold\">\n Green if temperature is between 20 and 30 °C, otherwise red.\n </text>\n \n <!-- Example 3: Dynamic Width Progress Bar -->\n <text x=\"60\" y=\"975\" class=\"description\">• Dynamic calculations (progress bar):</text>\n <rect x=\"70\" y=\"985\" width=\"400\" height=\"25\" class=\"code-bg\" />\n <rect x=\"70\" y=\"985\" \n width=\"\\${Math.min((this.c8yScadaValues?.progress || 0) * 4, 400)}\" \n height=\"25\" \n fill=\"var(--brand-primary,var(--c8y-brand-primary))\" \n rx=\"13\" />\n <text x=\"480\" y=\"1000\" class=\"label\">\\${this.c8yScadaValues?.progress || 0}%</text>\n \n <!-- Example 4: Alarm Status Circle -->\n <text x=\"60\" y=\"1040\" class=\"description\">• Alarm status indicator using getActiveAlarmsStatusClass():</text>\n <circle cx=\"80\" cy=\"1065\" r=\"12\" \n class=\"\\${this.c8yScadaFunctions?.getActiveAlarmsStatusClass ? 'alarm-' + this.c8yScadaFunctions.getActiveAlarmsStatusClass(this.c8yScadaValues?.alarmsStatus) : 'alarm-none'}\" />\n <text x=\"100\" y=\"1070\" class=\"label\">\n Alarm status:\n \\${this.c8yScadaValues?.alarmsStatus?.critical > 0 ? 'CRITICAL' : this.c8yScadaValues?.alarmsStatus?.major > 0 ? 'MAJOR' : this.c8yScadaValues?.alarmsStatus?.minor > 0 ? 'MINOR' : this.c8yScadaValues?.alarmsStatus?.warning > 0 ? 'WARNING' : 'OK'}\n </text>\n \n <!-- Legend for alarm colors -->\n <text x=\"70\" y=\"1110\" class=\"label\">Alarm color legend:</text>\n <circle cx=\"80\" cy=\"1130\" r=\"8\" class=\"alarm-none\" />\n <text x=\"95\" y=\"1135\" class=\"description\">OK</text>\n \n <circle cx=\"150\" cy=\"1130\" r=\"8\" class=\"alarm-warning\" />\n <text x=\"165\" y=\"1135\" class=\"description\">Warning</text>\n \n <circle cx=\"240\" cy=\"1130\" r=\"8\" class=\"alarm-minor\" />\n <text x=\"255\" y=\"1135\" class=\"description\">Minor</text>\n \n <circle cx=\"320\" cy=\"1130\" r=\"8\" class=\"alarm-major\" />\n <text x=\"335\" y=\"1135\" class=\"description\">Major</text>\n \n <circle cx=\"400\" cy=\"1130\" r=\"8\" class=\"alarm-critical\" />\n <text x=\"415\" y=\"1135\" class=\"description\">Critical</text>\n \n <!-- Additional examples info -->\n <rect x=\"70\" y=\"1170\" width=\"460\" height=\"130\" fill=\"var(--c8y-palette-status-info-light)\" stroke=\"var(--c8y-palette-status-info)\" stroke-width=\"1\" rx=\"4\" />\n <text x=\"85\" y=\"1195\" class=\"label\" font-weight=\"bold\">💡 Tips for advanced usage:</text>\n <text x=\"85\" y=\"1220\" class=\"description\">• Use this.c8yScadaValues to access placeholder values</text>\n <text x=\"85\" y=\"1240\" class=\"description\">• Use this.c8yScadaFunctions for navigation and utilities</text>\n <text x=\"85\" y=\"1260\" class=\"description\">• Combine template literals \\\\\\${ } with conditional logic</text>\n <text x=\"85\" y=\"1280\" class=\"description\">• Apply CSS classes dynamically for styling</text>\n</svg>\n`;\n","import { Injectable } from '@angular/core';\nimport { IIdentified } from '@c8y/client';\nimport {\n ActiveAlarmsStatus,\n DisplayOption,\n HorizontalAlignment,\n SvgComputedStyling,\n SvgDisplaySettings,\n VerticalAlignment\n} from './scada-widget-config/scada.model';\n\n@Injectable()\nexport class ScadaWidgetService {\n getScadaFunctions() {\n return {\n goToGroupDetails: (groupId: IIdentified['id']) => {\n window.location.hash = `/group/${groupId}`;\n },\n goToDeviceDetails: (deviceId: IIdentified['id']) => {\n window.location.hash = `/device/${deviceId}`;\n },\n getActiveAlarmsStatusClass: (\n activeAlarmsStatus: ActiveAlarmsStatus\n ): keyof ActiveAlarmsStatus | 'none' => {\n const severities: Array<keyof ActiveAlarmsStatus> = [\n 'critical',\n 'major',\n 'minor',\n 'warning'\n ];\n for (const severity of severities) {\n if (activeAlarmsStatus?.[severity] > 0) {\n return severity;\n }\n }\n return 'none';\n }\n };\n }\n\n getSvgComputedStyling(settings: SvgDisplaySettings | null | undefined): SvgComputedStyling {\n const displayOption: DisplayOption = settings?.displayOption ?? 'full-width';\n const hAlign: HorizontalAlignment = settings?.horizontalAlignment ?? 'center';\n const vAlign: VerticalAlignment = settings?.verticalAlignment ?? 'center';\n\n if (!settings || displayOption === 'full-width') {\n return {\n svgWidth: '100%',\n svgHeight: '',\n svgMaxWidth: '',\n svgMaxHeight: '',\n preserveAspectRatio: null,\n containerDisplay: '',\n containerJustifyContent: '',\n containerAlignItems: '',\n containerOverflow: '',\n containerHeight: '100%',\n containerBoxSizing: 'border-box'\n };\n }\n\n const hFlex: Record<HorizontalAlignment, string> = {\n left: 'flex-start',\n center: 'center',\n right: 'flex-end'\n };\n const vFlex: Record<VerticalAlignment, string> = {\n top: 'flex-start',\n center: 'center',\n bottom: 'flex-end'\n };\n const hPar: Record<HorizontalAlignment, string> = { left: 'Min', center: 'Mid', right: 'Max' };\n const vPar: Record<VerticalAlignment, string> = { top: 'Min', center: 'Mid', bottom: 'Max' };\n const parBase = `x${hPar[hAlign]}Y${vPar[vAlign]}`;\n\n // contain\n return {\n svgWidth: '',\n svgHeight: '',\n svgMaxWidth: '100%',\n svgMaxHeight: '100%',\n preserveAspectRatio: `${parBase} meet`,\n containerDisplay: 'flex',\n containerJustifyContent: hFlex[hAlign],\n containerAlignItems: vFlex[vAlign],\n containerOverflow: '',\n containerHeight: '100%',\n containerBoxSizing: 'border-box'\n };\n }\n}\n","/**\n * Escapes Lit syntax to safe data attributes for XML parsing.\n *\n * Converts:\n * - @click=${...} => data-lit-event-click=\"${...}\"\n * - ?attr=${...} => data-lit-attribute-attr=\"${...}\"\n * - .prop=${...} => data-lit-property-prop=\"${...}\"\n * - `\"` inside expressions => `&quot;` (to produce valid XML/HTML attribute values)\n *\n * @param svg The SVG string containing Lit syntax\n * @returns The SVG string with escaped Lit syntax\n */\nexport function escapeLitSyntax(svg: string): string {\n return (\n svg\n // @click=${...} => data-lit-event-click=\"${...}\"\n .replace(/@([a-zA-Z0-9_-]+)=\\$\\{([^}]*)\\}/g, (_, name, expr) => {\n const escaped = expr.replace(/\"/g, '&quot;');\n return `data-lit-event-${name}=\"\\${${escaped}}\"`;\n })\n // ?attr=${...} => data-lit-attribute-attr=\"${...}\"\n .replace(/\\?([a-zA-Z0-9_-]+)=\\$\\{([^}]*)\\}/g, (_, name, expr) => {\n const escaped = expr.replace(/\"/g, '&quot;');\n return `data-lit-attribute-${name}=\"\\${${escaped}}\"`;\n })\n // .prop=${...} => data-lit-property-prop=\"${...}\"\n .replace(/\\.([a-zA-Z0-9_-]+)=\\$\\{([^}]*)\\}/g, (_, name, expr) => {\n const escaped = expr.replace(/\"/g, '&quot;');\n return `data-lit-property-${name}=\"\\${${escaped}}\"`;\n })\n );\n}\n\n/**\n * Unescapes data attributes back to Lit syntax.\n *\n * Converts:\n * - data-lit-event-click=\"${...}\" => @click=${...}\n * - data-lit-attribute-attr=\"${...}\" => ?attr=${...}\n * - data-lit-property-prop=\"${...}\" => .prop=${...}\n * - `&quot;` inside expression values => `\"` (decoded automatically via `DOMParser`)\n *\n * @param svg The SVG string with escaped data attributes\n * @returns The SVG string with restored Lit syntax\n */\nexport function unescapeLitSyntax(svg: string): string {\n return (\n svg\n // data-lit-event-click=\"${...}\" => @click=${...}\n .replace(/data-lit-event-([a-zA-Z0-9_-]+)=\"([^\"]*)\"/g, (_, name, value) => {\n const valueDocument = new DOMParser().parseFromString(value, 'text/html');\n const { textContent } = valueDocument.documentElement;\n return `@${name}=${textContent}`;\n })\n // data-lit-attribute-attr=\"${...}\" => ?attr=${...}\n .replace(/data-lit-attribute-([a-zA-Z0-9_-]+)=\"([^\"]*)\"/g, (_, name, value) => {\n const valueDocument = new DOMParser().parseFromString(value, 'text/html');\n const { textContent } = valueDocument.documentElement;\n return `?${name}=${textContent}`;\n })\n // data-lit-property-prop=\"${...}\" => .prop=${...}\n .replace(/data-lit-property-([a-zA-Z0-9_-]+)=\"([^\"]*)\"/g, (_, name, value) => {\n const valueDocument = new DOMParser().parseFromString(value, 'text/html');\n const { textContent } = valueDocument.documentElement;\n return `.${name}=${textContent}`;\n })\n );\n}\n\n/**\n * Removes Lit expressions from SVG and replaces them with placeholders.\n *\n * Converts ${...} to __LIT_EXPR_n__ to allow safe XML parsing.\n * The extracted expressions are stored in the provided array for later restoration.\n *\n * @param svg The SVG string containing Lit expressions\n * @param litExpressions Array to store extracted expressions\n * @returns The SVG string with expressions replaced by placeholders\n */\nexport function removeLitExpressions(svg: string, litExpressions: string[]): string {\n // ${...} => __LIT_EXPR_n__\n let exprIndex = 0;\n return svg.replace(/\\$\\{([^}]*)\\}/g, (_, expr) => {\n litExpressions.push(expr);\n return `__LIT_EXPR_${exprIndex++}__`;\n });\n}\n\n/**\n * Restores Lit expressions in SVG from placeholders.\n *\n * Converts __LIT_EXPR_n__ back to ${...} using expressions from the provided array.\n *\n * @param svg The SVG string with expression placeholders\n * @param litExpressions Array containing the extracted expressions\n * @returns The SVG string with restored Lit expressions\n */\nexport function restoreLitExpressions(svg: string, litExpressions: string[]): string {\n // __LIT_EXPR_n__ => ${...}\n return svg.replace(/__LIT_EXPR_(\\d+)__/g, (_, index) => {\n const expr = litExpressions[Number(index)];\n return `\\${${expr}}`;\n });\n}\n\n/**\n * Generates Lit web component code for rendering SCADA SVG elements.\n *\n * Creates a custom element class that extends LitElement with support for:\n * - Dynamic context binding (c8yContext)\n * - SCADA value placeholders (c8yScadaValues)\n * - SCADA helper functions (c8yScadaFunctions)\n * - Interactive placeholder highlighting and selection\n *\n * The generated component includes event listeners for hover and click interactions\n * on elements with the 'potential-placeholder' class, enabling users to identify\n * and select placeholders within the SCADA visualization in the widget's configuration.\n *\n * @param svgCode The SVG markup to embed in the component's render method\n * @returns A string containing the complete Lit web component code\n */\nexport function getWebComponentCode(svgCode: string): string {\n return `\nimport { LitElement, html, css } from 'lit';\n\nexport default class ScadaElement extends LitElement {\n static styles = css\\`\n :host > div {\n padding: var(--c8y-root-component-padding-default);\n }\n\n .enable-potential-placeholders .potential-placeholder {\n cursor: pointer;\n transition: fill 0.2s, stroke 0.2s;\n pointer-events: auto;\n }\n .enable-potential-placeholders .potential-placeholder.hovered {\n fill: var(--brand-primary, var(--c8y-brand-primary));\n stroke: var(--brand-primary, var(--c8y-brand-primary));\n }\n \\`;\n\n static properties = {\n // Context managed object assigned to this widget, might be null.\n c8yContext: { type: Object },\n // Object with values assigned to placeholders in the widget, might be null.\n c8yScadaValues: { type: Object },\n // Object with helper functions, might be null.\n c8yScadaFunctions: { type: Object },\n // The instant translation function.\n c8yTranslate: { type: Function },\n // Pre-computed SVG display and alignment styling.\n c8ySvgComputedStyling: { type: Object },\n // Whether to highlight potential placeholders and emit click events.\n enablePotentialPlaceholders: { type: Boolean },\n };\n\n constructor() {\n super();\n }\n\n get _wrapperStyle() {\n const s = this.c8ySvgComputedStyling;\n if (!s || !s.containerDisplay) {\n return '';\n }\n let style = 'display: ' + s.containerDisplay + '; height: ' + s.containerHeight + '; justify-content: ' + s.containerJustifyContent + '; align-items: ' + s.containerAlignItems;\n if (s.containerOverflow) {\n style += '; overflow: ' + s.containerOverflow;\n }\n if (s.containerBoxSizing) {\n style += '; box-sizing: ' + s.containerBoxSizing;\n }\n return style;\n }\n\n _applySvgStyling() {\n const svg = this.renderRoot.querySelector('svg');\n if (!svg) return;\n const s = this.c8ySvgComputedStyling;\n if (!s) {\n svg.style.width = '100%';\n svg.style.height = '';\n svg.style.maxWidth = '';\n svg.style.maxHeight = '';\n svg.removeAttribute('preserveAspectRatio');\n return;\n }\n svg.style.width = s.svgWidth;\n svg.style.height = s.svgHeight;\n svg.style.maxWidth = s.svgMaxWidth;\n svg.style.maxHeight = s.svgMaxHeight;\n if (s.preserveAspectRatio === null) {\n svg.removeAttribute('preserveAspectRatio');\n } else {\n svg.setAttribute('preserveAspectRatio', s.preserveAspectRatio);\n }\n }\n\n firstUpdated() {\n // Add hover and click listeners to potential placeholders\n const svg = this.renderRoot.querySelector('svg');\n if (svg) {\n svg.querySelectorAll('.potential-placeholder').forEach(node => {\n const dispatchPlaceholderClickEvent = () => {\n const event = new CustomEvent('potential-placeholder-click', {\n detail: { node },\n bubbles: true,\n composed: true\n });\n this.dispatchEvent(event);\n };\n\n node.setAttribute('tabindex', '0');\n node.setAttribute('role', 'button');\n node.addEventListener('mouseenter', () => {\n node.classList.add('hovered');\n });\n node.addEventListener('mouseleave', () => {\n node.classList.remove('hovered');\n });\n node.addEventListener('click', () => {\n dispatchPlaceholderClickEvent();\n });\n node.addEventListener('keydown', event => {\n if (event.key === 'Enter' || event.key === ' ' || event.key === 'Spacebar') {\n event.preventDefault();\n dispatchPlaceholderClickEvent();\n }\n });\n });\n }\n this._applySvgStyling();\n }\n\n updated(changedProperties) {\n if (changedProperties.has('c8ySvgComputedStyling')) {\n this._applySvgStyling();\n }\n }\n\n render() {\n return html\\`\n <div class=\"\\${this.enablePotentialPlaceholders ? 'enable-potential-placeholders' : ''}\"\n style=\"\\${this._wrapperStyle}\">\n ${svgCode}\n </div>\n \\`;\n }\n}`;\n}\n","import { DestroyRef, inject, Injectable } from '@angular/core';\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop';\nimport {\n AssetPropertyMappings,\n ContextWidgetConfig,\n WidgetConfigService\n} from '@c8y/ngx-components/context-dashboard';\nimport { debounceTime, firstValueFrom, shareReplay, startWith, Subject, switchMap } from 'rxjs';\nimport {\n defaultAlignment,\n INITIAL_SVG_CODE,\n LegacyMapping,\n LegacyMappingBase,\n LegacyMappingComputed,\n LegacyMappingFieldbus,\n ScadaWidgetConfig\n} from './scada.model';\nimport { IIdentified, InventoryBinaryService, InventoryService } from '@c8y/client';\nimport { AssetPropertyType, ComputedPropertiesService } from '@c8y/ngx-components/asset-properties';\nimport {\n escapeLitSyntax,\n getWebComponentCode,\n removeLitExpressions,\n restoreLitExpressions,\n unescapeLitSyntax\n} from './lit-helpers';\nimport { FieldbusService } from '@c8y/ngx-components/computed-asset-properties';\nimport { C8yPropertyType } from '@c8y/ngx-components';\nimport {\n GlobalContextState,\n GLOBAL_CONTEXT_DISPLAY_MODE,\n WidgetConfigMigrationService\n} from '@c8y/ngx-components/global-context';\nimport { merge } from 'lodash-es';\nimport {\n SVG_EXTRACT_TAG_NAME,\n MAPPING_EXTRACT_TAG_NAME\n} from '@c8y/ngx-components/ai/agents/scada';\n\n@Injectable()\nexport class ScadaWidgetConfigService {\n private readonly widgetConfigService = inject(WidgetConfigService);\n private readonly widgetConfigMigration = inject(WidgetConfigMigrationService);\n private readonly inventory = inject(InventoryService);\n private readonly inventoryBinary = inject(InventoryBinaryService);\n private readonly computedPropertiesService = inject(ComputedPropertiesService);\n private readonly fieldbusService = inject(FieldbusService);\n private readonly destroyRef = inject(DestroyRef);\n\n configChanged$ = new Subject<ScadaWidgetConfig>();\n config$ = this.widgetConfigService.currentConfig$.pipe(\n debounceTime(100),\n switchMap(config => this.initializeConfig(config)),\n switchMap(initConfig => this.configChanged$.pipe(startWith(initConfig))),\n shareReplay({ bufferSize: 1, refCount: true }),\n takeUntilDestroyed(this.destroyRef)\n );\n\n constructor() {\n this.destroyRef.onDestroy(() => this.configChanged$.complete());\n }\n\n async initializeConfig(\n config: ContextWidgetConfig | null\n ): Promise<ScadaWidgetConfig & GlobalContextState> {\n let migratedConfig = await this.migrateConfigIfNeeded(config);\n if (!migratedConfig.config?.htmlWidgetConfig) {\n migratedConfig = await this.updateConfigWithSvgCode(migratedConfig, INITIAL_SVG_CODE);\n }\n return migratedConfig;\n }\n\n save(config: ScadaWidgetConfig) {\n // - only persist SCADA-specific state (avoid writing stale global time context values)\n // - force=true applies the update synchronously so that subsequent dialog saves see the latest state\n this.widgetConfigService.updateConfig({ config: config.config }, true);\n this.configChanged$.next(config);\n }\n\n async updateConfigWithSvgFile(\n config: ScadaWidgetConfig,\n svgFile: File\n ): Promise<ScadaWidgetConfig> {\n if (!svgFile || !svgFile.text) {\n throw new Error('Invalid SVG file provided');\n }\n const originalSvgCode = await svgFile.text();\n return this.updateConfigWithSvgCode(config, originalSvgCode);\n }\n\n async updateConfigWithSvgCode(\n config: ScadaWidgetConfig,\n originalSvgCode: string\n ): Promise<ScadaWidgetConfig> {\n // Remove hardcoded dimensions to make the SVG responsive\n const svgCodeWithoutDimensions = await this.removeHardcodedSvgDimensions(originalSvgCode);\n const svgCodeWithUuids =\n await this.findAndAddMarkupForPotentialTextPlaceholders(svgCodeWithoutDimensions);\n const svgCode = this.replacePlaceholdersWithTemplateSyntax(svgCodeWithUuids);\n\n config.config = config.config || ({} as ScadaWidgetConfig['config']);\n config.config.htmlWidgetConfig = {\n config: {\n devMode: true,\n code: getWebComponentCode(svgCode),\n css: '',\n legacy: false,\n options: {\n cssEncapsulation: false,\n advancedSecurity: false\n }\n },\n device: null,\n settings: {}\n };\n this.updateMappings(config);\n\n return config;\n }\n\n extractTagContent(text: string, tagName: string): string | undefined {\n const openTag = `<${tagName}>`;\n const closeTag = `</${tagName}>`;\n const openIdx = text.indexOf(openTag);\n const closeIdx = text.indexOf(closeTag);\n if (openIdx !== -1 && closeIdx !== -1 && closeIdx > openIdx) {\n return text.substring(openIdx + openTag.length, closeIdx);\n }\n return undefined;\n }\n\n async applySvgCode(code: string): Promise<void> {\n const config = await firstValueFrom(this.config$);\n const updatedConfig = await this.updateConfigWithSvgCode(config, code);\n this.save(updatedConfig);\n }\n\n async applyMappings(mappings: string): Promise<void> {\n try {\n const parsedMappings = JSON.parse(mappings);\n const config = await firstValueFrom(this.config$);\n config.config.mappings = parsedMappings;\n this.save(config);\n } catch (ex) {\n // eslint-disable-next-line no-console\n console.error('Failed to apply placeholder mapping:', ex);\n }\n }\n\n async applyScadaOutput(content: string): Promise<void> {\n const svgContent = this.extractTagContent(content, SVG_EXTRACT_TAG_NAME);\n const mappingContent = this.extractTagContent(content, MAPPING_EXTRACT_TAG_NAME);\n\n if (svgContent) {\n await this.applySvgCode(svgContent.trim());\n }\n\n if (mappingContent) {\n await this.applyMappings(mappingContent.trim());\n }\n }\n\n /**\n * Safely parses an SVG string containing Lit expressions, allows DOM editing, and returns the edited SVG string.\n * Handles escaping/unescaping Lit syntax and expressions.\n *\n * @param svg The SVG string to process.\n * @param editFn A function that receives the parsed SVG document for editing.\n * @returns The edited SVG string, or throws an exception if parsing or editing failed.\n */\n async editSafelySvgWithLitExpressions(\n svg: string,\n editFn: (doc: Document) => Promise<void>\n ): Promise<string> {\n const litExpressions: string[] = [];\n const parseToDoc = (svg: string): Document => {\n const parser = new DOMParser();\n const doc = parser.parseFromString(\n removeLitExpressions(escapeLitSyntax(svg), litExpressions),\n 'image/svg+xml'\n );\n\n if (doc.querySelector('parsererror')) {\n throw new Error('Could not parse SVG: it may contain unsupported Lit syntax.');\n }\n\n return doc;\n };\n const serializeToString = (doc: Document): string => {\n const serializer = new XMLSerializer();\n const serializedSvg = serializer.serializeToString(doc.documentElement);\n return restoreLitExpressions(unescapeLitSyntax(serializedSvg), litExpressions);\n };\n\n const doc = parseToDoc(svg);\n await editFn(doc);\n return serializeToString(doc);\n }\n\n updateMappings(config: ScadaWidgetConfig) {\n let keys;\n if (config.config.htmlWidgetConfig.config.legacy) {\n keys = this.extractLegacyScadaKeys(config.config.htmlWidgetConfig.config.code);\n } else {\n keys = this.extractScadaKeys(config.config.htmlWidgetConfig.config.code);\n }\n const currentMappings = config.config.mappings || {};\n const newMappings: ScadaWidgetConfig['config']['mappings'] = {};\n for (const key of keys) {\n if (Object.prototype.hasOwnProperty.call(currentMappings, key)) {\n newMappings[key] = currentMappings[key];\n } else {\n newMappings[key] = { assetProperty: null };\n }\n }\n config.config.mappings = newMappings;\n }\n\n /**\n * Removes hardcoded width and height attributes from the root SVG element.\n *\n * @param svg The SVG string to process.\n * @returns The modified SVG string with width and height attributes removed from the root SVG element.\n */\n private async removeHardcodedSvgDimensions(svg: string): Promise<string> {\n return this.editSafelySvgWithLitExpressions(svg, async doc => {\n const svgElement = doc.documentElement;\n if (svgElement.tagName.toLowerCase() === 'svg') {\n svgElement.removeAttribute('width');\n svgElement.removeAttribute('height');\n }\n });\n }\n\n /**\n * Processes an SVG string to identify and mark potential text placeholders within `<text>` and `<tspan>` elements.\n *\n * This method performs the following steps:\n * 1. Escapes Lit syntax event bindings (e.g., `@click=${...}`) to avoid XML parsing issues.\n * 2. Parses the SVG string into a DOM structure.\n * 3. For each `<text>` and `<tspan>` element:\n * - Adds a unique `id` attribute if not already present.\n * - Adds the CSS class `potential-placeholder` if the element's text content does not already contain a placeholder expression (`this.c8yScadaValues?.`).\n * 4. Serializes the DOM back to an SVG string and restores the original Lit syntax.\n *\n * If parsing fails (e.g., due to unhandled Lit syntax), the original SVG string is returned unchanged.\n *\n * @param svg The SVG string to process.\n * @returns The modified SVG string with potential placeholders marked, or the original SVG if parsing fails.\n */\n private async findAndAddMarkupForPotentialTextPlaceholders(svg: string): Promise<string> {\n return this.editSafelySvgWithLitExpressions(svg, async doc => {\n // Handle both <text> and <tspan> elements\n const nodes = doc.querySelectorAll('text, tspan');\n nodes.forEach(node => {\n // exclude nodes which has child nodes\n if (node.children.length > 0) {\n return;\n }\n\n // exclude nodes which already contain expressions\n const nodeTextContent = node.textContent || '';\n if (nodeTextContent.includes('__LIT_EXPR_')) {\n return;\n }\n\n // add id attribute with random UUID if not present\n if (!node.hasAttribute('id')) {\n node.setAttribute('id', crypto.randomUUID());\n }\n\n // add class 'potential-placeholder'\n node.classList.add('potential-placeholder');\n });\n });\n }\n\n /**\n * Replaces `{{ key }}` placeholders with new template syntax `${this.c8yScadaValues?.key}`.\n *\n * @param svgCode The SVG code string with placeholders.\n * @returns The SVG code string with new template syntax.\n */\n private replacePlaceholdersWithTemplateSyntax(svgCode: string): string {\n return svgCode.replace(/\\{\\{\\s*(\\w+)\\s*\\}\\}/g, (_, key) => `\\${this.c8yScadaValues?.${key}}`);\n }\n\n /**\n * Extracts placeholder keys used in the template syntax (`${this.c8yScadaValues?.key}`)\n * from the provided SVG code string.\n *\n * @param svgCode The SVG code string containing template syntax placeholders.\n * @returns Array of unique placeholder keys.\n */\n private extractScadaKeys(svgCode: string): string[] {\n const curlyBracesRegex = /\\$\\{([^}]*)\\}/g;\n const scadaValueRegex = /this\\.c8yScadaValues\\??\\.([a-zA-Z0-9_]+)/g;\n const keys = new Set<string>();\n let curlyBracesMatch: RegExpExecArray | null;\n while ((curlyBracesMatch = curlyBracesRegex.exec(svgCode)) !== null) {\n const curlyBracesExpression = curlyBracesMatch[1];\n let scadaValueMatch: RegExpExecArray | null;\n while ((scadaValueMatch = scadaValueRegex.exec(curlyBracesExpression)) !== null) {\n keys.add(scadaValueMatch[1]);\n }\n }\n return Array.from(keys);\n }\n\n /**\n * Extracts placeholder keys used in the legacy template syntax (`{{ key }}`)\n * from the provided SVG code string.\n *\n * @param svgCode The SVG code string containing legacy template syntax placeholders.\n * @returns Array of unique placeholder keys.\n */\n private extractLegacyScadaKeys(svgCode: string): string[] {\n const legacyPlaceholderRegex = /\\{\\{\\s*(\\w+)\\s*\\}\\}/g;\n const keys = new Set<string>();\n let match: RegExpExecArray | null;\n while ((match = legacyPlaceholderRegex.exec(svgCode)) !== null) {\n keys.add(match[1]);\n }\n return Array.from(keys);\n }\n\n private async migrateConfigIfNeeded(\n config: ContextWidgetConfig | null\n ): Promise<ScadaWidgetConfig & GlobalContextState> {\n const hasNewConfig = !!config?.config?.mappings && !!config?.config?.htmlWidgetConfig;\n const hasLegacyConfig = !!config?.binaryId && !!config?.mapping && !!config?.deviceIds;\n const migrationNeeded = hasLegacyConfig && !hasNewConfig;\n\n config = this.migrateGlobalContextConfig(config);\n\n if (!migrationNeeded) {\n return config as ScadaWidgetConfig & GlobalContextState;\n }\n\n return await this.migrateFromLegacyBinary(config);\n }\n\n private migrateGlobalContextConfig(\n config: ContextWidgetConfig | null\n ): ContextWidgetConfig | null {\n // Always migrate global context config and set displayMode to 'dashboard' if no other value was explicitly used:\n const hadExplicitDisplayMode =\n typeof config?.displayMode === 'string' &&\n Object.values(GLOBAL_CONTEXT_DISPLAY_MODE).includes(\n config.displayMode as GLOBAL_CONTEXT_DISPLAY_MODE\n );\n const migratedGlobalContextConfig = this.widgetConfigMigration.migrateWidgetConfig(config);\n config = merge(config, migratedGlobalContextConfig);\n if (!hadExplicitDisplayMode) {\n config.displayMode = GLOBAL_CONTEXT_DISPLAY_MODE.DASHBOARD;\n }\n return config;\n }\n\n private async migrateFromLegacyBinary(\n config: ContextWidgetConfig\n ): Promise<ScadaWidgetConfig & GlobalContextState> {\n const binaryId = config.binaryId;\n const binary = await this.inventoryBinary.download(binaryId);\n const svgCode = await binary.text();\n\n const mappings: AssetPropertyMappings = {};\n for (const key of Object.keys(config.mapping)) {\n mappings[key] = {\n assetProperty: await this.migrateProperty(config.mapping[key], config.deviceIds[key])\n };\n }\n\n return {\n ...config,\n config: {\n htmlWidgetConfig: {\n config: {\n code: svgCode,\n css: '',\n legacy: true,\n devMode: false,\n options: {\n cssEncapsulation: false,\n // must be `false`, otherwise the legacy widgets won't work correctly\n advancedSecurity: false\n }\n },\n device: null,\n settings: {}\n },\n mappings,\n svgDisplaySettings: {\n displayOption: 'contain',\n horizontalAlignment: defaultAlignment,\n verticalAlignment: defaultAlignment\n }\n }\n };\n }\n\n private async migrateProperty(\n legacyProperty: LegacyMapping,\n assetId: IIdentified['id']\n ): Promise<AssetPropertyType> {\n const isFieldbus = (prop: LegacyMapping): prop is LegacyMappingFieldbus =>\n prop['fieldbus'] === true;\n const isComputed = (prop: LegacyMapping): prop is LegacyMappingComputed =>\n prop['computed'] === true;\n const isRegular = (prop: LegacyMapping): prop is LegacyMappingBase =>\n !isFieldbus(prop) && !isComputed(prop);\n const asset = (await this.inventory.detail(assetId)).data;\n\n if (isFieldbus(legacyProperty)) {\n return this.fieldbusService.migrateFieldbusProperty(legacyProperty, asset);\n }\n\n if (isComputed(legacyProperty)) {\n const computedPropertyName = legacyProperty.id.replace(/^c8ySchema!!/, '');\n const computedProperty = await this.computedPropertiesService.getByName(computedPropertyName);\n\n return {\n ...computedProperty.prop,\n instanceId: legacyProperty.config._id,\n active: true,\n temporary: true,\n contextAssetId: assetId,\n config: legacyProperty.config,\n asset: {\n id: asset.id,\n name: asset.name\n }\n };\n }\n\n if (isRegular(legacyProperty)) {\n const buildJsonSchema = (keyPath: string[], label: string, type: C8yPropertyType) => {\n if (keyPath.length === 1) {\n return {\n [keyPath[0]]: {\n label,\n type,\n key: keyPath[0],\n properties: {}\n }\n };\n }\n // Recursively nest properties\n const [first, ...rest] = keyPath;\n return {\n [first]: {\n properties: buildJsonSchema(rest, label, type)\n }\n };\n };\n\n const assetProperty: AssetPropertyType = {\n isEditable: true,\n c8y_JsonSchema: {\n properties: buildJsonSchema(\n legacyProperty.keyPath,\n legacyProperty.label,\n legacyProperty.type\n )\n },\n name: legacyProperty.keyPath.join('.'),\n active: true,\n label: legacyProperty.label,\n type: legacyProperty.type,\n asset: {\n id: asset.id,\n name: asset.name\n }\n };\n return assetProperty;\n }\n }\n}\n","import { ChangeDetectionStrategy, Component, computed, inject, input } from '@angular/core';\nimport { DomSanitizer } from '@angular/platform-browser';\nimport { C8yTranslatePipe, IconDirective } from '@c8y/ngx-components';\nimport { ToolCallPart } from '@c8y/ngx-components/ai';\nimport {\n MAPPING_EXTRACT_TAG_NAME,\n SVG_EXTRACT_TAG_NAME\n} from '@c8y/ngx-components/ai/agents/scada';\nimport { TooltipDirective } from 'ngx-bootstrap/tooltip';\nimport { ScadaWidgetConfigService } from './scada-widget-config.service';\n\n@Component({\n selector: 'c8y-scada-ai-chat-tool-details',\n templateUrl: './scada-ai-chat-tool-details.component.html',\n host: { style: 'display: block' },\n imports: [TooltipDirective, C8yTranslatePipe, IconDirective],\n changeDetection: ChangeDetectionStrategy.OnPush\n})\nexport class ScadaAiChatToolDetailsComponent {\n readonly tool = input.required<ToolCallPart<{ content: string }>>();\n readonly content = computed(() => this.tool().input?.content || '');\n readonly isExecuting = computed(() => this.tool().type !== 'tool-result');\n readonly svgContent = computed(() =>\n this.scadaWidgetConfigService.extractTagContent(this.content(), SVG_EXTRACT_TAG_NAME)\n );\n readonly mappingContent = computed(() =>\n this.scadaWidgetConfigService.extractTagContent(this.content(), MAPPING_EXTRACT_TAG_NAME)\n );\n readonly mappingRows = computed(() => {\n const mapping = this.mappingContent();\n if (!mapping) return null;\n try {\n const parsed: Record<\n string,\n {\n assetProperty?: {\n label?: string;\n computed?: boolean;\n asset?: { name?: string };\n config?: { dp?: Array<{ label?: string }> };\n };\n }\n > = JSON.parse(mapping);\n return Object.entries(parsed).map(([placeholder, value]) => {\n const assetProperty = value?.assetProperty;\n const asset = assetProperty?.asset?.name ?? '';\n let property = assetProperty?.label ?? '';\n if (assetProperty?.computed && assetProperty.config?.dp?.[0]?.label) {\n property = assetProperty.config.dp[0].label;\n }\n return { placeholder, asset, property };\n });\n } catch {\n return null;\n }\n });\n readonly previewSvg = computed(() =>\n this.sanitizer.bypassSecurityTrustHtml(this.simplifyPreviewSvg(this.svgContent() || ''))\n );\n\n private readonly sanitizer = inject(DomSanitizer);\n private readonly scadaWidgetConfigService = inject(ScadaWidgetConfigService);\n\n apply(): void {\n const svg = this.svgContent();\n const mapping = this.mappingContent();\n if (svg) {\n void this.scadaWidgetConfigService.applySvgCode(svg.trim());\n }\n if (mapping) {\n void this.scadaWidgetConfigService.applyMappings(mapping.trim());\n }\n }\n\n private simplifyPreviewSvg(svg: string): string {\n return svg\n .replace(/\\$\\{this\\.c8yScadaValues\\?\\.(\\w+)\\?\\.value[^}]*\\}/g, '{$1}')\n .replace(/\\$\\{this\\.c8yScadaValues\\?\\.(\\w+)\\?\\.(\\w+)[^}]*\\}/g, '{$1.$2}')\n .replace(/\\$\\{this\\.c8yScadaValues\\?\\.(\\w+)[^}]*\\}/g, '{$1}')\n .replace(/\\$\\{[^}]+\\}/g, '');\n }\n}\n","@if (svgContent()) {\n <div\n class=\"m-b-8\"\n style=\"max-height: 320px; overflow: auto\"\n [innerHTML]=\"previewSvg()\"\n ></div>\n}\n@if (mappingRows(); as rows) {\n <div\n class=\"m-b-8\"\n style=\"max-height: 160px; overflow: auto\"\n >\n <table class=\"table table-condensed m-b-0\">\n <thead>\n <tr>\n <th>{{ 'Placeholder' | translate }}</th>\n <th>{{ 'Asset' | translate }}</th>\n <th>{{ 'Property' | translate }}</th>\n </tr>\n </thead>\n <tbody>\n @for (row of rows; track row.placeholder) {\n <tr>\n <td>\n <code>{{ row.placeholder }}</code>\n </td>\n <td>{{ row.asset }}</td>\n <td>{{ row.property }}</td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n} @else if (mappingContent()) {\n <pre\n class=\"m-b-8\"\n style=\"max-height: 160px\"\n >{{ mappingContent() }}</pre\n >\n}\n@if (!isExecuting()) {\n <div class=\"d-flex j-c-end\">\n <button\n class=\"btn btn-default btn-sm\"\n [attr.aria-label]=\"'Apply' | translate\"\n [tooltip]=\"'Apply' | translate\"\n container=\"body\"\n (click)=\"apply()\"\n >\n <i c8yIcon=\"check\"></i>\n </button>\n </div>\n}\n","import {\n ChangeDetectionStrategy,\n Component,\n DestroyRef,\n inject,\n OnInit,\n signal,\n viewChild\n} from '@angular/core';\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop';\nimport {\n DroppedFile,\n DropAreaComponent,\n C8yTranslatePipe,\n AlertService,\n FormGroupComponent,\n ListGroupModule\n} from '@c8y/ngx-components';\nimport { gettext } from '@c8y/ngx-components/gettext';\nimport {\n WidgetConfigSectionComponent,\n WidgetConfigService\n} from '@c8y/ngx-components/context-dashboard';\nimport { ScadaWidgetConfigService } from './scada-widget-config.service';\nimport { firstValueFrom } from 'rxjs';\nimport { FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms';\nimport { BsDropdownModule } from 'ngx-bootstrap/dropdown';\nimport { A11yModule } from '@angular/cdk/a11y';\nimport {\n AIAssistantMessage,\n AIStreamResponse,\n AssistantMessageDisplayConfig,\n AIService,\n AgentChatConfig,\n ToolCallPart\n} from '@c8y/ngx-components/ai';\nimport {\n SCADA_OUTPUT_TAG_NAME,\n SCADA_SVG_AGENT_DEF