@meshmakers/octo-meshboard 3.4.210 → 3.4.220

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -19,13 +19,13 @@ import * as i2 from '@progress/kendo-angular-buttons';
19
19
  import { ButtonsModule, ButtonModule } from '@progress/kendo-angular-buttons';
20
20
  import * as i3 from '@progress/kendo-angular-inputs';
21
21
  import { InputsModule, CheckBoxModule, NumericTextBoxModule, TextBoxModule, FormFieldModule } from '@progress/kendo-angular-inputs';
22
+ import * as i4 from '@progress/kendo-angular-dropdowns';
23
+ import { DropDownsModule, DropDownListModule } from '@progress/kendo-angular-dropdowns';
22
24
  import * as i1$2 from '@progress/kendo-angular-indicators';
23
25
  import { LoaderModule } from '@progress/kendo-angular-indicators';
24
26
  import * as i1$4 from '@progress/kendo-angular-icons';
25
27
  import { SVGIconModule } from '@progress/kendo-angular-icons';
26
28
  import { arrowUpIcon, arrowDownIcon, minusIcon, arrowRightIcon, arrowLeftIcon, linkIcon, chevronDownIcon, chevronRightIcon, circleIcon, questionCircleIcon, minusCircleIcon, warningTriangleIcon, exclamationCircleIcon, xCircleIcon, checkCircleIcon, columnsIcon, sortAscIcon, filterIcon, searchIcon, chartPieIcon, infoCircleIcon, plusIcon, trashIcon, pencilIcon, chartLineIcon, gearsIcon, clipboardMarkdownIcon, copyIcon, gridIcon, heartIcon, gridLayoutIcon, chartLineMarkersIcon, chartColumnStackedIcon, chartDoughnutIcon, tableIcon, shareIcon, fileTxtIcon, checkIcon, xIcon, downloadIcon, uploadIcon, gearIcon, saveIcon, arrowRotateCwIcon, undoIcon } from '@progress/kendo-svg-icons';
27
- import * as i4 from '@progress/kendo-angular-dropdowns';
28
- import { DropDownsModule, DropDownListModule } from '@progress/kendo-angular-dropdowns';
29
29
  import { NotificationService } from '@progress/kendo-angular-notification';
30
30
  import * as i1$6 from '@progress/kendo-angular-gauges';
31
31
  import { CollectionChangesService, KENDO_GAUGES } from '@progress/kendo-angular-gauges';
@@ -49,6 +49,8 @@ import { BreadCrumbService } from '@meshmakers/shared-services';
49
49
  * MeshBoard Widget Models
50
50
  * Defines the interfaces for MeshBoard widgets and their data sources
51
51
  */
52
+ /** The board-level default timezone mode applied when none is persisted. */
53
+ const DEFAULT_TIME_ZONE_MODE = 'local';
52
54
 
53
55
  // Re-export from octo-services (moved there for reuse by octo-ui)
54
56
 
@@ -1503,7 +1505,7 @@ class MeshBoardPersistenceService {
1503
1505
  */
1504
1506
  async createMeshBoard(config) {
1505
1507
  // Encode variables, timeFilter, and entitySelectors in description field (temporary until backend adds config field)
1506
- const encodedDescription = this.encodeVariablesInDescription(config.description ?? '', config.variables, config.timeFilter, config.entitySelectors, config.autoRefreshSeconds);
1508
+ const encodedDescription = this.encodeVariablesInDescription(config.description ?? '', config.variables, config.timeFilter, config.entitySelectors, config.autoRefreshSeconds, config.timeZoneMode);
1507
1509
  const dashboardInput = {
1508
1510
  name: config.name,
1509
1511
  description: encodedDescription,
@@ -1535,7 +1537,7 @@ class MeshBoardPersistenceService {
1535
1537
  async updateMeshBoard(rtId, config, existingWidgetRtIds = []) {
1536
1538
  const result = { createdWidgets: [] };
1537
1539
  // Encode variables, timeFilter, and entitySelectors in description field (temporary until backend adds config field)
1538
- const encodedDescription = this.encodeVariablesInDescription(config.description ?? '', config.variables, config.timeFilter, config.entitySelectors, config.autoRefreshSeconds);
1540
+ const encodedDescription = this.encodeVariablesInDescription(config.description ?? '', config.variables, config.timeFilter, config.entitySelectors, config.autoRefreshSeconds, config.timeZoneMode);
1539
1541
  const dashboardItem = {
1540
1542
  name: config.name,
1541
1543
  description: encodedDescription,
@@ -1599,7 +1601,7 @@ class MeshBoardPersistenceService {
1599
1601
  toMeshBoardConfig(meshBoard, widgets) {
1600
1602
  // Decode variables, timeFilter, entitySelectors, and autoRefresh from description field
1601
1603
  // (temporary until backend adds first-class config field)
1602
- const { description, variables, timeFilter, entitySelectors, autoRefreshSeconds } = this.decodeVariablesFromDescription(meshBoard.description);
1604
+ const { description, variables, timeFilter, entitySelectors, autoRefreshSeconds, timeZoneMode } = this.decodeVariablesFromDescription(meshBoard.description);
1603
1605
  return {
1604
1606
  id: meshBoard.rtId,
1605
1607
  name: meshBoard.name,
@@ -1610,6 +1612,7 @@ class MeshBoardPersistenceService {
1610
1612
  gap: meshBoard.gap,
1611
1613
  variables,
1612
1614
  timeFilter,
1615
+ timeZoneMode,
1613
1616
  entitySelectors,
1614
1617
  autoRefreshSeconds,
1615
1618
  widgets: widgets.map(w => this.toWidgetConfig(w))
@@ -1733,13 +1736,16 @@ class MeshBoardPersistenceService {
1733
1736
  * Note: Only static variables are persisted. TimeFilter variables are derived
1734
1737
  * from the timeFilter selection when the MeshBoard is loaded.
1735
1738
  */
1736
- encodeVariablesInDescription(description, variables, timeFilter, entitySelectors, autoRefreshSeconds) {
1739
+ encodeVariablesInDescription(description, variables, timeFilter, entitySelectors, autoRefreshSeconds, timeZoneMode) {
1737
1740
  // Filter out timeFilter and entitySelector variables (they are derived, not persisted directly)
1738
1741
  const staticVariables = variables?.filter(v => v.source !== 'timeFilter' && v.source !== 'entitySelector');
1739
- // Check if there's anything to encode
1742
+ // Check if there's anything to encode. 'local' is the default, so only the
1743
+ // non-default 'utc' mode needs persisting.
1740
1744
  const hasEntitySelectors = entitySelectors && entitySelectors.length > 0;
1741
1745
  const hasAutoRefresh = !!autoRefreshSeconds && autoRefreshSeconds > 0;
1742
- if ((!staticVariables || staticVariables.length === 0) && !timeFilter?.enabled && !hasEntitySelectors && !hasAutoRefresh) {
1746
+ const hasTimeZoneMode = timeZoneMode === 'utc';
1747
+ if ((!staticVariables || staticVariables.length === 0) &&
1748
+ !timeFilter?.enabled && !hasEntitySelectors && !hasAutoRefresh && !hasTimeZoneMode) {
1743
1749
  return description;
1744
1750
  }
1745
1751
  try {
@@ -1750,6 +1756,9 @@ class MeshBoardPersistenceService {
1750
1756
  if (timeFilter?.enabled) {
1751
1757
  data.timeFilter = timeFilter;
1752
1758
  }
1759
+ if (hasTimeZoneMode) {
1760
+ data.timeZoneMode = timeZoneMode;
1761
+ }
1753
1762
  if (hasAutoRefresh) {
1754
1763
  data.autoRefreshSeconds = autoRefreshSeconds;
1755
1764
  }
@@ -1805,6 +1814,7 @@ class MeshBoardPersistenceService {
1805
1814
  description,
1806
1815
  variables: parsed.variables ?? [],
1807
1816
  timeFilter: parsed.timeFilter,
1817
+ timeZoneMode: parsed.timeZoneMode === 'utc' || parsed.timeZoneMode === 'local' ? parsed.timeZoneMode : undefined,
1808
1818
  entitySelectors: parsed.entitySelectors,
1809
1819
  autoRefreshSeconds: typeof parsed.autoRefreshSeconds === 'number' ? parsed.autoRefreshSeconds : undefined
1810
1820
  };
@@ -2030,6 +2040,13 @@ class MeshBoardStateService {
2030
2040
  ...(ngDevMode ? [{ debugName: "isModelAvailable" }] : /* istanbul ignore next */ []));
2031
2041
  variableResolutionErrors = computed(() => this._variableResolutionErrors(), /* @ts-ignore */
2032
2042
  ...(ngDevMode ? [{ debugName: "variableResolutionErrors" }] : /* istanbul ignore next */ []));
2043
+ /**
2044
+ * The board's effective timezone mode (defaults to `'local'`). Drives both the
2045
+ * time-filter boundary computation and datetime display across all widgets.
2046
+ * Widgets read this to format timestamps consistently with the active filter.
2047
+ */
2048
+ timeZoneMode = computed(() => this._meshBoardConfig().timeZoneMode ?? DEFAULT_TIME_ZONE_MODE, /* @ts-ignore */
2049
+ ...(ngDevMode ? [{ debugName: "timeZoneMode" }] : /* istanbul ignore next */ []));
2033
2050
  // Deprecated aliases for backward compatibility
2034
2051
  /** @deprecated Use meshBoardConfig instead */
2035
2052
  dashboardConfig = this.meshBoardConfig;
@@ -2269,6 +2286,7 @@ class MeshBoardStateService {
2269
2286
  columns: settings.columns,
2270
2287
  rowHeight: settings.rowHeight,
2271
2288
  gap: settings.gap,
2289
+ timeZoneMode: settings.timeZoneMode ?? config.timeZoneMode,
2272
2290
  variables: settings.variables ?? config.variables,
2273
2291
  timeFilter: settings.timeFilter
2274
2292
  ? {
@@ -2300,6 +2318,7 @@ class MeshBoardStateService {
2300
2318
  gap: config.gap,
2301
2319
  variables: config.variables ?? [],
2302
2320
  timeFilter: config.timeFilter,
2321
+ timeZoneMode: config.timeZoneMode,
2303
2322
  entitySelectors: config.entitySelectors,
2304
2323
  autoRefreshSeconds: config.autoRefreshSeconds
2305
2324
  };
@@ -2475,8 +2494,9 @@ class MeshBoardStateService {
2475
2494
  * Stream-data persistent queries consume this to bound their result set;
2476
2495
  * runtime queries ignore it.
2477
2496
  *
2478
- * IANA-timezone-aware bucket boundaries are tracked separately (AB#4190);
2479
- * this helper currently returns UTC boundaries derived from the picker.
2497
+ * Boundaries are resolved on the board's {@link timeZoneMode} basis (local by
2498
+ * default, or UTC), the same basis widgets format their datetime display on —
2499
+ * so the selected range and the displayed timestamps stay consistent.
2480
2500
  */
2481
2501
  resolveCurrentTimeRange() {
2482
2502
  const config = this._meshBoardConfig().timeFilter;
@@ -2492,7 +2512,7 @@ class MeshBoardStateService {
2492
2512
  customFrom: selection.customFrom ? new Date(selection.customFrom) : undefined,
2493
2513
  customTo: selection.customTo ? new Date(selection.customTo) : undefined
2494
2514
  };
2495
- return TimeRangeUtils.getTimeRangeFromSelection(sharedSelection, showTime);
2515
+ return TimeRangeUtils.getTimeRangeFromSelection(sharedSelection, showTime, this.timeZoneMode());
2496
2516
  }
2497
2517
  /**
2498
2518
  * Resolves the active time filter into stream-data `{from, to}` arguments for
@@ -4017,6 +4037,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
4017
4037
 
4018
4038
  class EntityCardWidgetComponent {
4019
4039
  dataService = inject(DashboardDataService);
4040
+ stateService = inject(MeshBoardStateService);
4041
+ variableService = inject(MeshBoardVariableService);
4020
4042
  config;
4021
4043
  // Widget state signals
4022
4044
  _isLoading = signal(false, /* @ts-ignore */
@@ -4037,7 +4059,10 @@ class EntityCardWidgetComponent {
4037
4059
  if (!dataSource)
4038
4060
  return true;
4039
4061
  if (dataSource.type === 'runtimeEntity') {
4040
- return !dataSource.rtId && !dataSource.ckTypeId;
4062
+ // A selector binding or a variable-bearing rtId/ckTypeId counts as
4063
+ // configured even before a board-level selection has resolved a value —
4064
+ // the empty/loading display is handled by loadData(), not here.
4065
+ return !dataSource.entitySelectorId && !dataSource.rtId && !dataSource.ckTypeId;
4041
4066
  }
4042
4067
  if (dataSource.type === 'static') {
4043
4068
  return false; // Static data is always "configured"
@@ -4114,11 +4139,23 @@ class EntityCardWidgetComponent {
4114
4139
  return;
4115
4140
  }
4116
4141
  // Handle runtime entity data source
4117
- // Note: isNotConfigured() check ensures rtId and ckTypeId are set
4118
4142
  if (dataSource.type === 'runtimeEntity') {
4143
+ // Resolve the effective entity reference: an entity-selector binding or
4144
+ // MeshBoard variables (e.g. $mp_rtId / $mp_rtCkTypeId) may supply the
4145
+ // rtId/ckTypeId from a board-level selection.
4146
+ const { rtId, ckTypeId } = this.resolveEntityRef(dataSource);
4147
+ // No resolvable selection yet (selector unpicked or variable unresolved) —
4148
+ // show nothing rather than fetching with a placeholder string.
4149
+ if (!rtId || !ckTypeId || this.variableService.hasUnresolvedVariables(rtId) ||
4150
+ this.variableService.hasUnresolvedVariables(ckTypeId)) {
4151
+ this._data.set(null);
4152
+ this._error.set(null);
4153
+ this._isLoading.set(false);
4154
+ return;
4155
+ }
4119
4156
  this._isLoading.set(true);
4120
4157
  this._error.set(null);
4121
- this.dataService.fetchEntityWithAssociations(dataSource.rtId, dataSource.ckTypeId)
4158
+ this.dataService.fetchEntityWithAssociations(rtId, ckTypeId)
4122
4159
  .pipe(catchError(err => {
4123
4160
  console.error('Error loading entity card data:', err);
4124
4161
  this._error.set('Failed to load data');
@@ -4130,6 +4167,51 @@ class EntityCardWidgetComponent {
4130
4167
  });
4131
4168
  }
4132
4169
  }
4170
+ /**
4171
+ * Message for the neutral empty state — shown when the widget IS configured
4172
+ * but no entity is currently resolved (e.g. a bound selector hasn't been
4173
+ * picked yet, or a variable is still unresolved). This is deliberately not the
4174
+ * red "not configured" placeholder, which would wrongly imply the widget needs
4175
+ * setup.
4176
+ */
4177
+ emptyMessage() {
4178
+ const dataSource = this.config?.dataSource;
4179
+ if (dataSource?.type === 'runtimeEntity') {
4180
+ if (dataSource.entitySelectorId) {
4181
+ return 'No entity selected';
4182
+ }
4183
+ if ((dataSource.rtId?.includes('$')) || (dataSource.ckTypeId?.includes('$'))) {
4184
+ return 'Waiting for selection';
4185
+ }
4186
+ }
4187
+ return 'No data';
4188
+ }
4189
+ /**
4190
+ * Resolves the effective `{ rtId, ckTypeId }` to fetch from a runtime-entity
4191
+ * data source.
4192
+ *
4193
+ * - When `entitySelectorId` is set, the bound MeshBoard entity selector's
4194
+ * current `selectedRtId` and the picked entity's type
4195
+ * (`$<selectorId>_rtCkTypeId`, falling back to the selector's configured
4196
+ * `ckTypeId`) win — so the card follows the asset picked at board level.
4197
+ * - Otherwise the configured `rtId`/`ckTypeId` are resolved against the active
4198
+ * MeshBoard variables, allowing values like `$mp_rtId` / `$mp_rtCkTypeId`.
4199
+ */
4200
+ resolveEntityRef(dataSource) {
4201
+ const variables = this.stateService.getVariables();
4202
+ if (dataSource.entitySelectorId) {
4203
+ const selector = this.stateService.getEntitySelector(dataSource.entitySelectorId);
4204
+ const rtCkTypeIdVar = variables.find(v => v.name === `${dataSource.entitySelectorId}_rtCkTypeId`);
4205
+ return {
4206
+ rtId: selector?.selectedRtId,
4207
+ ckTypeId: rtCkTypeIdVar?.value || selector?.ckTypeId
4208
+ };
4209
+ }
4210
+ return {
4211
+ rtId: dataSource.rtId ? this.variableService.resolveVariables(dataSource.rtId, variables) : undefined,
4212
+ ckTypeId: dataSource.ckTypeId ? this.variableService.resolveVariables(dataSource.ckTypeId, variables) : undefined
4213
+ };
4214
+ }
4133
4215
  inferAttributeType(value) {
4134
4216
  if (value === null || value === undefined)
4135
4217
  return AttributeValueTypeDto.StringDto;
@@ -4163,11 +4245,11 @@ class EntityCardWidgetComponent {
4163
4245
  .trim();
4164
4246
  }
4165
4247
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: EntityCardWidgetComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4166
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: EntityCardWidgetComponent, isStandalone: true, selector: "mm-entity-card-widget", inputs: { config: "config" }, usesOnChanges: true, ngImport: i0, template: "<div class=\"entity-card\" [class.no-data]=\"!data()\" [class.loading]=\"isLoading()\" [class.error]=\"error()\">\n @if (isNotConfigured()) {\n <mm-widget-not-configured></mm-widget-not-configured>\n } @else if (isLoading()) {\n <div class=\"loading-indicator\">\n <span>Loading...</span>\n </div>\n } @else if (error()) {\n <div class=\"error-message\">\n <span>{{ error() }}</span>\n </div>\n } @else if (data()) {\n <!-- Header (like UML class name) -->\n @if (config.showHeader !== false) {\n <div class=\"entity-header\">\n <span class=\"entity-stereotype\">&laquo;{{ entityTypeName() }}&raquo;</span>\n <span class=\"entity-name\">{{ displayName() }}</span>\n </div>\n }\n\n <!-- Attributes Section (like UML attributes) -->\n @if (config.showAttributes !== false && filteredAttributes().length > 0) {\n <div class=\"entity-attributes\">\n @for (attr of filteredAttributes(); track attr.attributeName) {\n <div class=\"attribute-row\">\n <span class=\"attribute-name\">{{ formatAttributeName(attr.attributeName) }}</span>\n <span class=\"attribute-value\">\n <mm-property-value-display\n [value]=\"attr.value\"\n [type]=\"inferAttributeType(attr.value)\"\n [attributeName]=\"attr.attributeName\">\n </mm-property-value-display>\n </span>\n </div>\n }\n </div>\n }\n\n <!-- Metadata Footer -->\n <div class=\"entity-footer\">\n <span class=\"entity-id\" title=\"Runtime ID\">{{ data()!.rtId }}</span>\n </div>\n } @else {\n <mm-widget-not-configured></mm-widget-not-configured>\n }\n</div>\n", styles: [".entity-card{display:flex;flex-direction:column;height:100%;border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px;overflow:hidden;background:var(--kendo-color-surface, #fff)}.entity-card.no-data,.entity-card.loading,.entity-card.error{justify-content:center;align-items:center}.entity-card.loading{color:var(--kendo-color-primary, #0d6efd)}.entity-card.error{color:var(--kendo-color-error, #dc3545)}.loading-indicator{font-size:.875rem;font-style:italic}.error-message{font-size:.875rem;padding:12px;text-align:center}.entity-header{display:flex;flex-direction:column;align-items:center;padding:12px;background:var(--kendo-color-primary, #0d6efd);color:var(--kendo-color-on-primary, #fff);text-align:center}.entity-header .entity-stereotype{font-size:.75rem;font-style:italic;opacity:.9;margin-bottom:2px}.entity-header .entity-name{font-size:1rem;font-weight:600}.entity-attributes{flex:1;padding:8px 0;overflow-y:auto}.entity-attributes .attribute-row{display:flex;align-items:center;justify-content:space-between;padding:6px 12px;border-bottom:1px solid var(--kendo-color-border-alt, #e9ecef);font-size:.8125rem}.entity-attributes .attribute-row:last-child{border-bottom:none}.entity-attributes .attribute-row:hover{background:var(--kendo-color-surface-alt, #f8f9fa)}.entity-attributes .attribute-name{color:var(--kendo-color-subtle, #6c757d);flex-shrink:0;margin-right:12px}.entity-attributes .attribute-value{color:var(--kendo-color-on-surface, #212529);font-weight:500;text-align:right;word-break:break-word;min-width:0;flex:1}.entity-footer{padding:8px 12px;background:var(--kendo-color-surface-alt, #f8f9fa);border-top:1px solid var(--kendo-color-border, #dee2e6)}.entity-footer .entity-id{font-size:.6875rem;font-family:monospace;color:var(--kendo-color-subtle, #6c757d)}.no-data-message{font-size:.875rem}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: WidgetNotConfiguredComponent, selector: "mm-widget-not-configured" }, { kind: "component", type: PropertyValueDisplayComponent, selector: "mm-property-value-display", inputs: ["value", "type", "displayMode", "attributeName"], outputs: ["binaryDownload"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
4248
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: EntityCardWidgetComponent, isStandalone: true, selector: "mm-entity-card-widget", inputs: { config: "config" }, usesOnChanges: true, ngImport: i0, template: "<div class=\"entity-card\" [class.no-data]=\"!data()\" [class.loading]=\"isLoading()\" [class.error]=\"error()\">\n @if (isNotConfigured()) {\n <mm-widget-not-configured></mm-widget-not-configured>\n } @else if (isLoading()) {\n <div class=\"loading-indicator\">\n <span>Loading...</span>\n </div>\n } @else if (error()) {\n <div class=\"error-message\">\n <span>{{ error() }}</span>\n </div>\n } @else if (data()) {\n <!-- Header (like UML class name) -->\n @if (config.showHeader !== false) {\n <div class=\"entity-header\">\n <span class=\"entity-stereotype\">&laquo;{{ entityTypeName() }}&raquo;</span>\n <span class=\"entity-name\">{{ displayName() }}</span>\n </div>\n }\n\n <!-- Attributes Section (like UML attributes) -->\n @if (config.showAttributes !== false && filteredAttributes().length > 0) {\n <div class=\"entity-attributes\">\n @for (attr of filteredAttributes(); track attr.attributeName) {\n <div class=\"attribute-row\">\n <span class=\"attribute-name\">{{ formatAttributeName(attr.attributeName) }}</span>\n <span class=\"attribute-value\">\n <mm-property-value-display\n [value]=\"attr.value\"\n [type]=\"inferAttributeType(attr.value)\"\n [attributeName]=\"attr.attributeName\">\n </mm-property-value-display>\n </span>\n </div>\n }\n </div>\n }\n\n <!-- Metadata Footer -->\n <div class=\"entity-footer\">\n <span class=\"entity-id\" title=\"Runtime ID\">{{ data()!.rtId }}</span>\n </div>\n } @else {\n <div class=\"entity-empty\">\n <span class=\"entity-empty-text\">{{ emptyMessage() }}</span>\n </div>\n }\n</div>\n", styles: [".entity-card{display:flex;flex-direction:column;height:100%;border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px;overflow:hidden;background:var(--kendo-color-surface, #fff)}.entity-card.no-data,.entity-card.loading,.entity-card.error{justify-content:center;align-items:center}.entity-card.loading{color:var(--kendo-color-primary, #0d6efd)}.entity-card.error{color:var(--kendo-color-error, #dc3545)}.loading-indicator{font-size:.875rem;font-style:italic}.error-message{font-size:.875rem;padding:12px;text-align:center}.entity-header{display:flex;flex-direction:column;align-items:center;padding:12px;background:var(--kendo-color-primary, #0d6efd);color:var(--kendo-color-on-primary, #fff);text-align:center}.entity-header .entity-stereotype{font-size:.75rem;font-style:italic;opacity:.9;margin-bottom:2px}.entity-header .entity-name{font-size:1rem;font-weight:600}.entity-attributes{flex:1;padding:8px 0;overflow-y:auto}.entity-attributes .attribute-row{display:flex;align-items:center;justify-content:space-between;padding:6px 12px;border-bottom:1px solid var(--kendo-color-border-alt, #e9ecef);font-size:.8125rem}.entity-attributes .attribute-row:last-child{border-bottom:none}.entity-attributes .attribute-row:hover{background:var(--kendo-color-surface-alt, #f8f9fa)}.entity-attributes .attribute-name{color:var(--kendo-color-subtle, #6c757d);flex-shrink:0;margin-right:12px}.entity-attributes .attribute-value{color:var(--kendo-color-on-surface, #212529);font-weight:500;text-align:right;word-break:break-word;min-width:0;flex:1}.entity-footer{padding:8px 12px;background:var(--kendo-color-surface-alt, #f8f9fa);border-top:1px solid var(--kendo-color-border, #dee2e6)}.entity-footer .entity-id{font-size:.6875rem;font-family:monospace;color:var(--kendo-color-subtle, #6c757d)}.no-data-message{font-size:.875rem}.entity-empty{display:flex;align-items:center;justify-content:center;height:100%;padding:8px;text-align:center}.entity-empty-text{font-size:.875rem;color:var(--kendo-color-subtle, #6c757d)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: WidgetNotConfiguredComponent, selector: "mm-widget-not-configured" }, { kind: "component", type: PropertyValueDisplayComponent, selector: "mm-property-value-display", inputs: ["value", "type", "displayMode", "attributeName"], outputs: ["binaryDownload"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
4167
4249
  }
4168
4250
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: EntityCardWidgetComponent, decorators: [{
4169
4251
  type: Component,
4170
- args: [{ selector: 'mm-entity-card-widget', standalone: true, imports: [CommonModule, WidgetNotConfiguredComponent, PropertyValueDisplayComponent], changeDetection: ChangeDetectionStrategy.Eager, template: "<div class=\"entity-card\" [class.no-data]=\"!data()\" [class.loading]=\"isLoading()\" [class.error]=\"error()\">\n @if (isNotConfigured()) {\n <mm-widget-not-configured></mm-widget-not-configured>\n } @else if (isLoading()) {\n <div class=\"loading-indicator\">\n <span>Loading...</span>\n </div>\n } @else if (error()) {\n <div class=\"error-message\">\n <span>{{ error() }}</span>\n </div>\n } @else if (data()) {\n <!-- Header (like UML class name) -->\n @if (config.showHeader !== false) {\n <div class=\"entity-header\">\n <span class=\"entity-stereotype\">&laquo;{{ entityTypeName() }}&raquo;</span>\n <span class=\"entity-name\">{{ displayName() }}</span>\n </div>\n }\n\n <!-- Attributes Section (like UML attributes) -->\n @if (config.showAttributes !== false && filteredAttributes().length > 0) {\n <div class=\"entity-attributes\">\n @for (attr of filteredAttributes(); track attr.attributeName) {\n <div class=\"attribute-row\">\n <span class=\"attribute-name\">{{ formatAttributeName(attr.attributeName) }}</span>\n <span class=\"attribute-value\">\n <mm-property-value-display\n [value]=\"attr.value\"\n [type]=\"inferAttributeType(attr.value)\"\n [attributeName]=\"attr.attributeName\">\n </mm-property-value-display>\n </span>\n </div>\n }\n </div>\n }\n\n <!-- Metadata Footer -->\n <div class=\"entity-footer\">\n <span class=\"entity-id\" title=\"Runtime ID\">{{ data()!.rtId }}</span>\n </div>\n } @else {\n <mm-widget-not-configured></mm-widget-not-configured>\n }\n</div>\n", styles: [".entity-card{display:flex;flex-direction:column;height:100%;border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px;overflow:hidden;background:var(--kendo-color-surface, #fff)}.entity-card.no-data,.entity-card.loading,.entity-card.error{justify-content:center;align-items:center}.entity-card.loading{color:var(--kendo-color-primary, #0d6efd)}.entity-card.error{color:var(--kendo-color-error, #dc3545)}.loading-indicator{font-size:.875rem;font-style:italic}.error-message{font-size:.875rem;padding:12px;text-align:center}.entity-header{display:flex;flex-direction:column;align-items:center;padding:12px;background:var(--kendo-color-primary, #0d6efd);color:var(--kendo-color-on-primary, #fff);text-align:center}.entity-header .entity-stereotype{font-size:.75rem;font-style:italic;opacity:.9;margin-bottom:2px}.entity-header .entity-name{font-size:1rem;font-weight:600}.entity-attributes{flex:1;padding:8px 0;overflow-y:auto}.entity-attributes .attribute-row{display:flex;align-items:center;justify-content:space-between;padding:6px 12px;border-bottom:1px solid var(--kendo-color-border-alt, #e9ecef);font-size:.8125rem}.entity-attributes .attribute-row:last-child{border-bottom:none}.entity-attributes .attribute-row:hover{background:var(--kendo-color-surface-alt, #f8f9fa)}.entity-attributes .attribute-name{color:var(--kendo-color-subtle, #6c757d);flex-shrink:0;margin-right:12px}.entity-attributes .attribute-value{color:var(--kendo-color-on-surface, #212529);font-weight:500;text-align:right;word-break:break-word;min-width:0;flex:1}.entity-footer{padding:8px 12px;background:var(--kendo-color-surface-alt, #f8f9fa);border-top:1px solid var(--kendo-color-border, #dee2e6)}.entity-footer .entity-id{font-size:.6875rem;font-family:monospace;color:var(--kendo-color-subtle, #6c757d)}.no-data-message{font-size:.875rem}\n"] }]
4252
+ args: [{ selector: 'mm-entity-card-widget', standalone: true, imports: [CommonModule, WidgetNotConfiguredComponent, PropertyValueDisplayComponent], changeDetection: ChangeDetectionStrategy.Eager, template: "<div class=\"entity-card\" [class.no-data]=\"!data()\" [class.loading]=\"isLoading()\" [class.error]=\"error()\">\n @if (isNotConfigured()) {\n <mm-widget-not-configured></mm-widget-not-configured>\n } @else if (isLoading()) {\n <div class=\"loading-indicator\">\n <span>Loading...</span>\n </div>\n } @else if (error()) {\n <div class=\"error-message\">\n <span>{{ error() }}</span>\n </div>\n } @else if (data()) {\n <!-- Header (like UML class name) -->\n @if (config.showHeader !== false) {\n <div class=\"entity-header\">\n <span class=\"entity-stereotype\">&laquo;{{ entityTypeName() }}&raquo;</span>\n <span class=\"entity-name\">{{ displayName() }}</span>\n </div>\n }\n\n <!-- Attributes Section (like UML attributes) -->\n @if (config.showAttributes !== false && filteredAttributes().length > 0) {\n <div class=\"entity-attributes\">\n @for (attr of filteredAttributes(); track attr.attributeName) {\n <div class=\"attribute-row\">\n <span class=\"attribute-name\">{{ formatAttributeName(attr.attributeName) }}</span>\n <span class=\"attribute-value\">\n <mm-property-value-display\n [value]=\"attr.value\"\n [type]=\"inferAttributeType(attr.value)\"\n [attributeName]=\"attr.attributeName\">\n </mm-property-value-display>\n </span>\n </div>\n }\n </div>\n }\n\n <!-- Metadata Footer -->\n <div class=\"entity-footer\">\n <span class=\"entity-id\" title=\"Runtime ID\">{{ data()!.rtId }}</span>\n </div>\n } @else {\n <div class=\"entity-empty\">\n <span class=\"entity-empty-text\">{{ emptyMessage() }}</span>\n </div>\n }\n</div>\n", styles: [".entity-card{display:flex;flex-direction:column;height:100%;border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px;overflow:hidden;background:var(--kendo-color-surface, #fff)}.entity-card.no-data,.entity-card.loading,.entity-card.error{justify-content:center;align-items:center}.entity-card.loading{color:var(--kendo-color-primary, #0d6efd)}.entity-card.error{color:var(--kendo-color-error, #dc3545)}.loading-indicator{font-size:.875rem;font-style:italic}.error-message{font-size:.875rem;padding:12px;text-align:center}.entity-header{display:flex;flex-direction:column;align-items:center;padding:12px;background:var(--kendo-color-primary, #0d6efd);color:var(--kendo-color-on-primary, #fff);text-align:center}.entity-header .entity-stereotype{font-size:.75rem;font-style:italic;opacity:.9;margin-bottom:2px}.entity-header .entity-name{font-size:1rem;font-weight:600}.entity-attributes{flex:1;padding:8px 0;overflow-y:auto}.entity-attributes .attribute-row{display:flex;align-items:center;justify-content:space-between;padding:6px 12px;border-bottom:1px solid var(--kendo-color-border-alt, #e9ecef);font-size:.8125rem}.entity-attributes .attribute-row:last-child{border-bottom:none}.entity-attributes .attribute-row:hover{background:var(--kendo-color-surface-alt, #f8f9fa)}.entity-attributes .attribute-name{color:var(--kendo-color-subtle, #6c757d);flex-shrink:0;margin-right:12px}.entity-attributes .attribute-value{color:var(--kendo-color-on-surface, #212529);font-weight:500;text-align:right;word-break:break-word;min-width:0;flex:1}.entity-footer{padding:8px 12px;background:var(--kendo-color-surface-alt, #f8f9fa);border-top:1px solid var(--kendo-color-border, #dee2e6)}.entity-footer .entity-id{font-size:.6875rem;font-family:monospace;color:var(--kendo-color-subtle, #6c757d)}.no-data-message{font-size:.875rem}.entity-empty{display:flex;align-items:center;justify-content:center;height:100%;padding:8px;text-align:center}.entity-empty-text{font-size:.875rem;color:var(--kendo-color-subtle, #6c757d)}\n"] }]
4171
4253
  }], propDecorators: { config: [{
4172
4254
  type: Input
4173
4255
  }] } });
@@ -4215,27 +4297,59 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
4215
4297
  class EntityCardConfigDialogComponent {
4216
4298
  getEntitiesByCkTypeGQL = inject(GetEntitiesByCkTypeDtoGQL);
4217
4299
  ckTypeSelectorService = inject(CkTypeSelectorService);
4300
+ stateService = inject(MeshBoardStateService);
4218
4301
  windowRef = inject(WindowRef);
4219
4302
  ckTypeSelectorInput;
4220
4303
  entitySelectorInput;
4221
4304
  initialCkTypeId;
4222
4305
  initialRtId;
4306
+ initialEntitySelectorId;
4223
4307
  initialHideEmptyAttributes = false;
4308
+ bindingMode = 'fixed';
4224
4309
  selectedCkType = null;
4225
4310
  selectedEntity = null;
4226
4311
  entityDataSource;
4227
4312
  entityDialogDataSource;
4228
4313
  isLoadingInitial = false;
4229
4314
  hideEmptyAttributes = false;
4315
+ entitySelectorId = '';
4316
+ variableCkTypeId = '';
4317
+ variableRtId = '';
4318
+ get availableSelectors() {
4319
+ return this.stateService.getEntitySelectors();
4320
+ }
4230
4321
  get isValid() {
4231
- return this.selectedCkType !== null && this.selectedEntity !== null;
4322
+ switch (this.bindingMode) {
4323
+ case 'selector':
4324
+ return !!this.entitySelectorId;
4325
+ case 'variable':
4326
+ return this.variableRtId.trim().length > 0 && this.variableCkTypeId.trim().length > 0;
4327
+ default:
4328
+ return this.selectedCkType !== null && this.selectedEntity !== null;
4329
+ }
4232
4330
  }
4233
4331
  async ngOnInit() {
4234
4332
  this.hideEmptyAttributes = this.initialHideEmptyAttributes;
4333
+ // Restore the binding mode from the persisted config.
4334
+ if (this.initialEntitySelectorId) {
4335
+ this.bindingMode = 'selector';
4336
+ this.entitySelectorId = this.initialEntitySelectorId;
4337
+ return;
4338
+ }
4339
+ if (this.containsVariable(this.initialCkTypeId) || this.containsVariable(this.initialRtId)) {
4340
+ this.bindingMode = 'variable';
4341
+ this.variableCkTypeId = this.initialCkTypeId ?? '';
4342
+ this.variableRtId = this.initialRtId ?? '';
4343
+ return;
4344
+ }
4345
+ this.bindingMode = 'fixed';
4235
4346
  if (this.initialCkTypeId) {
4236
4347
  await this.loadInitialValues();
4237
4348
  }
4238
4349
  }
4350
+ containsVariable(value) {
4351
+ return !!value && value.includes('$');
4352
+ }
4239
4353
  async loadInitialValues() {
4240
4354
  if (!this.initialCkTypeId) {
4241
4355
  return;
@@ -4312,70 +4426,149 @@ class EntityCardConfigDialogComponent {
4312
4426
  this.selectedEntity = null;
4313
4427
  }
4314
4428
  onSave() {
4315
- if (this.selectedCkType && this.selectedEntity) {
4316
- this.windowRef.close({
4429
+ if (!this.isValid) {
4430
+ return;
4431
+ }
4432
+ let result;
4433
+ if (this.bindingMode === 'selector') {
4434
+ result = {
4435
+ ckTypeId: '',
4436
+ rtId: '',
4437
+ entitySelectorId: this.entitySelectorId,
4438
+ hideEmptyAttributes: this.hideEmptyAttributes
4439
+ };
4440
+ }
4441
+ else if (this.bindingMode === 'variable') {
4442
+ result = {
4443
+ ckTypeId: this.variableCkTypeId.trim(),
4444
+ rtId: this.variableRtId.trim(),
4445
+ hideEmptyAttributes: this.hideEmptyAttributes
4446
+ };
4447
+ }
4448
+ else {
4449
+ result = {
4317
4450
  ckTypeId: this.selectedCkType.rtCkTypeId,
4318
4451
  rtId: this.selectedEntity.rtId,
4319
4452
  hideEmptyAttributes: this.hideEmptyAttributes
4320
- });
4453
+ };
4321
4454
  }
4455
+ this.windowRef.close(result);
4322
4456
  }
4323
4457
  onCancel() {
4324
4458
  this.windowRef.close();
4325
4459
  }
4326
4460
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: EntityCardConfigDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4327
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: EntityCardConfigDialogComponent, isStandalone: true, selector: "mm-entity-card-config-dialog", inputs: { initialCkTypeId: "initialCkTypeId", initialRtId: "initialRtId", initialHideEmptyAttributes: "initialHideEmptyAttributes" }, viewQueries: [{ propertyName: "ckTypeSelectorInput", first: true, predicate: ["ckTypeSelector"], descendants: true }, { propertyName: "entitySelectorInput", first: true, predicate: ["entitySelector"], descendants: true }], ngImport: i0, template: `
4461
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: EntityCardConfigDialogComponent, isStandalone: true, selector: "mm-entity-card-config-dialog", inputs: { initialCkTypeId: "initialCkTypeId", initialRtId: "initialRtId", initialEntitySelectorId: "initialEntitySelectorId", initialHideEmptyAttributes: "initialHideEmptyAttributes" }, viewQueries: [{ propertyName: "ckTypeSelectorInput", first: true, predicate: ["ckTypeSelector"], descendants: true }, { propertyName: "entitySelectorInput", first: true, predicate: ["entitySelector"], descendants: true }], ngImport: i0, template: `
4328
4462
  <div class="config-container">
4329
4463
 
4330
4464
  <div class="config-form" [class.loading]="isLoadingInitial">
4331
4465
  <mm-loading-overlay [loading]="isLoadingInitial" />
4466
+
4332
4467
  <div class="form-field">
4333
- <label>Runtime Entities</label>
4334
- <mm-ck-type-selector-input
4335
- #ckTypeSelector
4336
- placeholder="Select Runtime Entities..."
4337
- [minSearchLength]="2"
4338
- dialogTitle="Select Runtime Entities"
4339
- [ngModel]="selectedCkType"
4340
- (ckTypeSelected)="onCkTypeSelected($event)"
4341
- (ckTypeCleared)="onCkTypeCleared()">
4342
- </mm-ck-type-selector-input>
4343
- <p class="field-hint">Select the type of entities to choose from.</p>
4468
+ <label>Entity source</label>
4469
+ <ul class="k-radio-list">
4470
+ <li class="k-radio-item">
4471
+ <input type="radio" kendoRadioButton name="bindingMode" value="fixed"
4472
+ [(ngModel)]="bindingMode" />
4473
+ <span class="radio-label">Fixed entity</span>
4474
+ </li>
4475
+ <li class="k-radio-item">
4476
+ <input type="radio" kendoRadioButton name="bindingMode" value="selector"
4477
+ [(ngModel)]="bindingMode" [disabled]="availableSelectors.length === 0" />
4478
+ <span class="radio-label">Entity selector{{ availableSelectors.length === 0 ? ' (none configured)' : '' }}</span>
4479
+ </li>
4480
+ <li class="k-radio-item">
4481
+ <input type="radio" kendoRadioButton name="bindingMode" value="variable"
4482
+ [(ngModel)]="bindingMode" />
4483
+ <span class="radio-label">Variable</span>
4484
+ </li>
4485
+ </ul>
4486
+ <p class="field-hint">
4487
+ Choose a fixed entity, follow a board-level entity selector, or bind the
4488
+ type and rtId to MeshBoard variables (e.g. <code>$mp_rtCkTypeId</code> / <code>$mp_rtId</code>).
4489
+ </p>
4344
4490
  </div>
4345
4491
 
4346
- <div class="form-field" [class.disabled]="!selectedCkType">
4347
- <label>Entity</label>
4348
- @if (selectedCkType && entityDataSource) {
4349
- <mm-entity-select-input
4350
- #entitySelector
4351
- [dataSource]="entityDataSource"
4352
- [dialogDataSource]="entityDialogDataSource"
4353
- placeholder="Search for an entity..."
4354
- dialogTitle="Select Entity"
4355
- [minSearchLength]="1"
4356
- [ngModel]="selectedEntity"
4357
- (entitySelected)="onEntitySelected($event)"
4358
- (entityCleared)="onEntityCleared()">
4359
- </mm-entity-select-input>
4360
- } @else {
4361
- <kendo-textbox
4362
- [disabled]="true"
4363
- placeholder="First select Runtime Entities...">
4364
- </kendo-textbox>
4365
- }
4366
- <p class="field-hint">Select the specific entity to display in this widget.</p>
4367
- </div>
4492
+ @if (bindingMode === 'fixed') {
4493
+ <div class="form-field">
4494
+ <label>Runtime Entities</label>
4495
+ <mm-ck-type-selector-input
4496
+ #ckTypeSelector
4497
+ placeholder="Select Runtime Entities..."
4498
+ [minSearchLength]="2"
4499
+ dialogTitle="Select Runtime Entities"
4500
+ [ngModel]="selectedCkType"
4501
+ (ckTypeSelected)="onCkTypeSelected($event)"
4502
+ (ckTypeCleared)="onCkTypeCleared()">
4503
+ </mm-ck-type-selector-input>
4504
+ <p class="field-hint">Select the type of entities to choose from.</p>
4505
+ </div>
4368
4506
 
4369
- @if (selectedEntity) {
4370
- <div class="selection-preview">
4371
- <h4>Selected Entity</h4>
4372
- <div class="preview-content">
4373
- <p><strong>Type:</strong> {{ selectedCkType?.rtCkTypeId }}</p>
4374
- <p><strong>RT-ID:</strong> {{ selectedEntity.rtId }}</p>
4375
- @if (selectedEntity.rtWellKnownName) {
4376
- <p><strong>Name:</strong> {{ selectedEntity.rtWellKnownName }}</p>
4377
- }
4507
+ <div class="form-field" [class.disabled]="!selectedCkType">
4508
+ <label>Entity</label>
4509
+ @if (selectedCkType && entityDataSource) {
4510
+ <mm-entity-select-input
4511
+ #entitySelector
4512
+ [dataSource]="entityDataSource"
4513
+ [dialogDataSource]="entityDialogDataSource"
4514
+ placeholder="Search for an entity..."
4515
+ dialogTitle="Select Entity"
4516
+ [minSearchLength]="1"
4517
+ [ngModel]="selectedEntity"
4518
+ (entitySelected)="onEntitySelected($event)"
4519
+ (entityCleared)="onEntityCleared()">
4520
+ </mm-entity-select-input>
4521
+ } @else {
4522
+ <kendo-textbox
4523
+ [disabled]="true"
4524
+ placeholder="First select Runtime Entities...">
4525
+ </kendo-textbox>
4526
+ }
4527
+ <p class="field-hint">Select the specific entity to display in this widget.</p>
4528
+ </div>
4529
+
4530
+ @if (selectedEntity) {
4531
+ <div class="selection-preview">
4532
+ <h4>Selected Entity</h4>
4533
+ <div class="preview-content">
4534
+ <p><strong>Type:</strong> {{ selectedCkType?.rtCkTypeId }}</p>
4535
+ <p><strong>RT-ID:</strong> {{ selectedEntity.rtId }}</p>
4536
+ @if (selectedEntity.rtWellKnownName) {
4537
+ <p><strong>Name:</strong> {{ selectedEntity.rtWellKnownName }}</p>
4538
+ }
4539
+ </div>
4378
4540
  </div>
4541
+ }
4542
+ }
4543
+
4544
+ @if (bindingMode === 'selector') {
4545
+ <div class="form-field">
4546
+ <label>Bind to entity selector</label>
4547
+ <kendo-dropdownlist
4548
+ [data]="availableSelectors"
4549
+ textField="label"
4550
+ valueField="id"
4551
+ [valuePrimitive]="true"
4552
+ [defaultItem]="{ id: '', label: '— Select —' }"
4553
+ [(ngModel)]="entitySelectorId">
4554
+ </kendo-dropdownlist>
4555
+ <p class="field-hint">
4556
+ The card follows the asset picked in this selector at board level —
4557
+ its rtId and CK type are resolved from the current selection.
4558
+ </p>
4559
+ </div>
4560
+ }
4561
+
4562
+ @if (bindingMode === 'variable') {
4563
+ <div class="form-field">
4564
+ <label>CK Type variable</label>
4565
+ <kendo-textbox [(ngModel)]="variableCkTypeId" placeholder="$mp_rtCkTypeId"></kendo-textbox>
4566
+ <p class="field-hint">CK type id, or a variable like <code>$mp_rtCkTypeId</code>.</p>
4567
+ </div>
4568
+ <div class="form-field">
4569
+ <label>rtId variable</label>
4570
+ <kendo-textbox [(ngModel)]="variableRtId" placeholder="$mp_rtId"></kendo-textbox>
4571
+ <p class="field-hint">Runtime id, or a variable like <code>$mp_rtId</code>.</p>
4379
4572
  </div>
4380
4573
  }
4381
4574
 
@@ -4402,7 +4595,7 @@ class EntityCardConfigDialogComponent {
4402
4595
  </button>
4403
4596
  </div>
4404
4597
  </div>
4405
- `, isInline: true, styles: [":host{display:block;height:100%}.config-container{display:flex;flex-direction:column;height:100%}.config-form{flex:1;overflow-y:auto;display:flex;flex-direction:column;gap:20px;padding:16px;position:relative}.config-form.loading{pointer-events:none}.form-field{display:flex;flex-direction:column;gap:6px}.form-field.disabled{opacity:.6}.form-field label{font-weight:600;font-size:.9rem;color:var(--kendo-color-on-app-surface, #212529)}.field-hint{margin:0;font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.selection-preview{padding:12px;background:var(--kendo-color-surface-alt, #f8f9fa);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px}.selection-preview h4{margin:0 0 8px;font-size:.9rem;color:var(--kendo-color-primary, #0d6efd)}.preview-content p{margin:4px 0;font-size:.85rem}.checkbox-row{display:flex;align-items:center;gap:8px;cursor:pointer;font-weight:600;font-size:.9rem;color:var(--kendo-color-on-app-surface, #212529)}.checkbox-row input{margin:0}.action-bar{display:flex;justify-content:flex-end;gap:8px;padding:8px 16px;border-top:1px solid var(--kendo-color-border, #dee2e6)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox]:not([ngNoCva])[formControlName],input[type=checkbox]:not([ngNoCva])[formControl],input[type=checkbox]:not([ngNoCva])[ngModel]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ButtonsModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: InputsModule }, { kind: "component", type: i3.TextBoxComponent, selector: "kendo-textbox", inputs: ["focusableId", "title", "type", "disabled", "readonly", "tabindex", "value", "selectOnFocus", "showSuccessIcon", "showErrorIcon", "clearButton", "successIcon", "successSvgIcon", "errorIcon", "errorSvgIcon", "clearButtonIcon", "clearButtonSvgIcon", "size", "rounded", "fillMode", "tabIndex", "placeholder", "maxlength", "inputAttributes"], outputs: ["valueChange", "inputFocus", "inputBlur", "focus", "blur"], exportAs: ["kendoTextBox"] }, { kind: "directive", type: i3.CheckBoxDirective, selector: "input[kendoCheckBox]", inputs: ["size", "rounded"] }, { kind: "component", type: CkTypeSelectorInputComponent, selector: "mm-ck-type-selector-input", inputs: ["placeholder", "minSearchLength", "maxResults", "debounceMs", "ckModelIds", "allowAbstract", "dialogTitle", "advancedSearchLabel", "derivedFromRtCkTypeId", "messages", "dialogMessages", "disabled", "required"], outputs: ["ckTypeSelected", "ckTypeCleared"] }, { kind: "component", type: EntitySelectInputComponent, selector: "mm-entity-select-input", inputs: ["dataSource", "placeholder", "minSearchLength", "maxResults", "debounceMs", "prefix", "initialDisplayValue", "dialogDataSource", "dialogTitle", "multiSelect", "advancedSearchLabel", "dialogMessages", "messages", "disabled", "required"], outputs: ["entitySelected", "entityCleared", "entitiesSelected"] }, { kind: "component", type: LoadingOverlayComponent, selector: "mm-loading-overlay", inputs: ["loading"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
4598
+ `, isInline: true, styles: [":host{display:block;height:100%}.config-container{display:flex;flex-direction:column;height:100%}.config-form{flex:1;overflow-y:auto;display:flex;flex-direction:column;gap:20px;padding:16px;position:relative}.config-form.loading{pointer-events:none}.form-field{display:flex;flex-direction:column;gap:6px}.form-field.disabled{opacity:.6}.form-field label{font-weight:600;font-size:.9rem;color:var(--kendo-color-on-app-surface, #212529)}.field-hint{margin:0;font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.selection-preview{padding:12px;background:var(--kendo-color-surface-alt, #f8f9fa);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px}.selection-preview h4{margin:0 0 8px;font-size:.9rem;color:var(--kendo-color-primary, #0d6efd)}.preview-content p{margin:4px 0;font-size:.85rem}.checkbox-row{display:flex;align-items:center;gap:8px;cursor:pointer;font-weight:600;font-size:.9rem;color:var(--kendo-color-on-app-surface, #212529)}.checkbox-row input{margin:0}.k-radio-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:6px}.radio-label{margin-left:8px;font-size:.9rem}.field-hint code{font-size:.78rem}.action-bar{display:flex;justify-content:flex-end;gap:8px;padding:8px 16px;border-top:1px solid var(--kendo-color-border, #dee2e6)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox]:not([ngNoCva])[formControlName],input[type=checkbox]:not([ngNoCva])[formControl],input[type=checkbox]:not([ngNoCva])[ngModel]" }, { kind: "directive", type: i1$1.RadioControlValueAccessor, selector: "input[type=radio]:not([ngNoCva])[formControlName],input[type=radio]:not([ngNoCva])[formControl],input[type=radio]:not([ngNoCva])[ngModel]", inputs: ["name", "formControlName", "value"] }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ButtonsModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: InputsModule }, { kind: "component", type: i3.TextBoxComponent, selector: "kendo-textbox", inputs: ["focusableId", "title", "type", "disabled", "readonly", "tabindex", "value", "selectOnFocus", "showSuccessIcon", "showErrorIcon", "clearButton", "successIcon", "successSvgIcon", "errorIcon", "errorSvgIcon", "clearButtonIcon", "clearButtonSvgIcon", "size", "rounded", "fillMode", "tabIndex", "placeholder", "maxlength", "inputAttributes"], outputs: ["valueChange", "inputFocus", "inputBlur", "focus", "blur"], exportAs: ["kendoTextBox"] }, { kind: "directive", type: i3.CheckBoxDirective, selector: "input[kendoCheckBox]", inputs: ["size", "rounded"] }, { kind: "directive", type: i3.RadioButtonDirective, selector: "input[kendoRadioButton]", inputs: ["size"] }, { kind: "ngmodule", type: DropDownsModule }, { kind: "component", type: i4.DropDownListComponent, selector: "kendo-dropdownlist", inputs: ["customIconClass", "showStickyHeader", "icon", "svgIcon", "loading", "data", "value", "textField", "valueField", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "defaultItem", "disabled", "itemDisabled", "readonly", "filterable", "virtual", "ignoreCase", "delay", "valuePrimitive", "tabindex", "tabIndex", "size", "rounded", "fillMode", "leftRightArrowsNavigation", "id"], outputs: ["valueChange", "filterChange", "selectionChange", "open", "opened", "close", "closed", "focus", "blur"], exportAs: ["kendoDropDownList"] }, { kind: "component", type: CkTypeSelectorInputComponent, selector: "mm-ck-type-selector-input", inputs: ["placeholder", "minSearchLength", "maxResults", "debounceMs", "ckModelIds", "allowAbstract", "dialogTitle", "advancedSearchLabel", "derivedFromRtCkTypeId", "messages", "dialogMessages", "disabled", "required"], outputs: ["ckTypeSelected", "ckTypeCleared"] }, { kind: "component", type: EntitySelectInputComponent, selector: "mm-entity-select-input", inputs: ["dataSource", "placeholder", "minSearchLength", "maxResults", "debounceMs", "prefix", "initialDisplayValue", "dialogDataSource", "dialogTitle", "multiSelect", "advancedSearchLabel", "dialogMessages", "messages", "disabled", "required"], outputs: ["entitySelected", "entityCleared", "entitiesSelected"] }, { kind: "component", type: LoadingOverlayComponent, selector: "mm-loading-overlay", inputs: ["loading"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
4406
4599
  }
4407
4600
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: EntityCardConfigDialogComponent, decorators: [{
4408
4601
  type: Component,
@@ -4411,6 +4604,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
4411
4604
  FormsModule,
4412
4605
  ButtonsModule,
4413
4606
  InputsModule,
4607
+ DropDownsModule,
4414
4608
  CkTypeSelectorInputComponent,
4415
4609
  EntitySelectInputComponent,
4416
4610
  LoadingOverlayComponent
@@ -4419,53 +4613,112 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
4419
4613
 
4420
4614
  <div class="config-form" [class.loading]="isLoadingInitial">
4421
4615
  <mm-loading-overlay [loading]="isLoadingInitial" />
4616
+
4422
4617
  <div class="form-field">
4423
- <label>Runtime Entities</label>
4424
- <mm-ck-type-selector-input
4425
- #ckTypeSelector
4426
- placeholder="Select Runtime Entities..."
4427
- [minSearchLength]="2"
4428
- dialogTitle="Select Runtime Entities"
4429
- [ngModel]="selectedCkType"
4430
- (ckTypeSelected)="onCkTypeSelected($event)"
4431
- (ckTypeCleared)="onCkTypeCleared()">
4432
- </mm-ck-type-selector-input>
4433
- <p class="field-hint">Select the type of entities to choose from.</p>
4618
+ <label>Entity source</label>
4619
+ <ul class="k-radio-list">
4620
+ <li class="k-radio-item">
4621
+ <input type="radio" kendoRadioButton name="bindingMode" value="fixed"
4622
+ [(ngModel)]="bindingMode" />
4623
+ <span class="radio-label">Fixed entity</span>
4624
+ </li>
4625
+ <li class="k-radio-item">
4626
+ <input type="radio" kendoRadioButton name="bindingMode" value="selector"
4627
+ [(ngModel)]="bindingMode" [disabled]="availableSelectors.length === 0" />
4628
+ <span class="radio-label">Entity selector{{ availableSelectors.length === 0 ? ' (none configured)' : '' }}</span>
4629
+ </li>
4630
+ <li class="k-radio-item">
4631
+ <input type="radio" kendoRadioButton name="bindingMode" value="variable"
4632
+ [(ngModel)]="bindingMode" />
4633
+ <span class="radio-label">Variable</span>
4634
+ </li>
4635
+ </ul>
4636
+ <p class="field-hint">
4637
+ Choose a fixed entity, follow a board-level entity selector, or bind the
4638
+ type and rtId to MeshBoard variables (e.g. <code>$mp_rtCkTypeId</code> / <code>$mp_rtId</code>).
4639
+ </p>
4434
4640
  </div>
4435
4641
 
4436
- <div class="form-field" [class.disabled]="!selectedCkType">
4437
- <label>Entity</label>
4438
- @if (selectedCkType && entityDataSource) {
4439
- <mm-entity-select-input
4440
- #entitySelector
4441
- [dataSource]="entityDataSource"
4442
- [dialogDataSource]="entityDialogDataSource"
4443
- placeholder="Search for an entity..."
4444
- dialogTitle="Select Entity"
4445
- [minSearchLength]="1"
4446
- [ngModel]="selectedEntity"
4447
- (entitySelected)="onEntitySelected($event)"
4448
- (entityCleared)="onEntityCleared()">
4449
- </mm-entity-select-input>
4450
- } @else {
4451
- <kendo-textbox
4452
- [disabled]="true"
4453
- placeholder="First select Runtime Entities...">
4454
- </kendo-textbox>
4455
- }
4456
- <p class="field-hint">Select the specific entity to display in this widget.</p>
4457
- </div>
4642
+ @if (bindingMode === 'fixed') {
4643
+ <div class="form-field">
4644
+ <label>Runtime Entities</label>
4645
+ <mm-ck-type-selector-input
4646
+ #ckTypeSelector
4647
+ placeholder="Select Runtime Entities..."
4648
+ [minSearchLength]="2"
4649
+ dialogTitle="Select Runtime Entities"
4650
+ [ngModel]="selectedCkType"
4651
+ (ckTypeSelected)="onCkTypeSelected($event)"
4652
+ (ckTypeCleared)="onCkTypeCleared()">
4653
+ </mm-ck-type-selector-input>
4654
+ <p class="field-hint">Select the type of entities to choose from.</p>
4655
+ </div>
4458
4656
 
4459
- @if (selectedEntity) {
4460
- <div class="selection-preview">
4461
- <h4>Selected Entity</h4>
4462
- <div class="preview-content">
4463
- <p><strong>Type:</strong> {{ selectedCkType?.rtCkTypeId }}</p>
4464
- <p><strong>RT-ID:</strong> {{ selectedEntity.rtId }}</p>
4465
- @if (selectedEntity.rtWellKnownName) {
4466
- <p><strong>Name:</strong> {{ selectedEntity.rtWellKnownName }}</p>
4467
- }
4657
+ <div class="form-field" [class.disabled]="!selectedCkType">
4658
+ <label>Entity</label>
4659
+ @if (selectedCkType && entityDataSource) {
4660
+ <mm-entity-select-input
4661
+ #entitySelector
4662
+ [dataSource]="entityDataSource"
4663
+ [dialogDataSource]="entityDialogDataSource"
4664
+ placeholder="Search for an entity..."
4665
+ dialogTitle="Select Entity"
4666
+ [minSearchLength]="1"
4667
+ [ngModel]="selectedEntity"
4668
+ (entitySelected)="onEntitySelected($event)"
4669
+ (entityCleared)="onEntityCleared()">
4670
+ </mm-entity-select-input>
4671
+ } @else {
4672
+ <kendo-textbox
4673
+ [disabled]="true"
4674
+ placeholder="First select Runtime Entities...">
4675
+ </kendo-textbox>
4676
+ }
4677
+ <p class="field-hint">Select the specific entity to display in this widget.</p>
4678
+ </div>
4679
+
4680
+ @if (selectedEntity) {
4681
+ <div class="selection-preview">
4682
+ <h4>Selected Entity</h4>
4683
+ <div class="preview-content">
4684
+ <p><strong>Type:</strong> {{ selectedCkType?.rtCkTypeId }}</p>
4685
+ <p><strong>RT-ID:</strong> {{ selectedEntity.rtId }}</p>
4686
+ @if (selectedEntity.rtWellKnownName) {
4687
+ <p><strong>Name:</strong> {{ selectedEntity.rtWellKnownName }}</p>
4688
+ }
4689
+ </div>
4468
4690
  </div>
4691
+ }
4692
+ }
4693
+
4694
+ @if (bindingMode === 'selector') {
4695
+ <div class="form-field">
4696
+ <label>Bind to entity selector</label>
4697
+ <kendo-dropdownlist
4698
+ [data]="availableSelectors"
4699
+ textField="label"
4700
+ valueField="id"
4701
+ [valuePrimitive]="true"
4702
+ [defaultItem]="{ id: '', label: '— Select —' }"
4703
+ [(ngModel)]="entitySelectorId">
4704
+ </kendo-dropdownlist>
4705
+ <p class="field-hint">
4706
+ The card follows the asset picked in this selector at board level —
4707
+ its rtId and CK type are resolved from the current selection.
4708
+ </p>
4709
+ </div>
4710
+ }
4711
+
4712
+ @if (bindingMode === 'variable') {
4713
+ <div class="form-field">
4714
+ <label>CK Type variable</label>
4715
+ <kendo-textbox [(ngModel)]="variableCkTypeId" placeholder="$mp_rtCkTypeId"></kendo-textbox>
4716
+ <p class="field-hint">CK type id, or a variable like <code>$mp_rtCkTypeId</code>.</p>
4717
+ </div>
4718
+ <div class="form-field">
4719
+ <label>rtId variable</label>
4720
+ <kendo-textbox [(ngModel)]="variableRtId" placeholder="$mp_rtId"></kendo-textbox>
4721
+ <p class="field-hint">Runtime id, or a variable like <code>$mp_rtId</code>.</p>
4469
4722
  </div>
4470
4723
  }
4471
4724
 
@@ -4492,7 +4745,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
4492
4745
  </button>
4493
4746
  </div>
4494
4747
  </div>
4495
- `, changeDetection: ChangeDetectionStrategy.Eager, styles: [":host{display:block;height:100%}.config-container{display:flex;flex-direction:column;height:100%}.config-form{flex:1;overflow-y:auto;display:flex;flex-direction:column;gap:20px;padding:16px;position:relative}.config-form.loading{pointer-events:none}.form-field{display:flex;flex-direction:column;gap:6px}.form-field.disabled{opacity:.6}.form-field label{font-weight:600;font-size:.9rem;color:var(--kendo-color-on-app-surface, #212529)}.field-hint{margin:0;font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.selection-preview{padding:12px;background:var(--kendo-color-surface-alt, #f8f9fa);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px}.selection-preview h4{margin:0 0 8px;font-size:.9rem;color:var(--kendo-color-primary, #0d6efd)}.preview-content p{margin:4px 0;font-size:.85rem}.checkbox-row{display:flex;align-items:center;gap:8px;cursor:pointer;font-weight:600;font-size:.9rem;color:var(--kendo-color-on-app-surface, #212529)}.checkbox-row input{margin:0}.action-bar{display:flex;justify-content:flex-end;gap:8px;padding:8px 16px;border-top:1px solid var(--kendo-color-border, #dee2e6)}\n"] }]
4748
+ `, changeDetection: ChangeDetectionStrategy.Eager, styles: [":host{display:block;height:100%}.config-container{display:flex;flex-direction:column;height:100%}.config-form{flex:1;overflow-y:auto;display:flex;flex-direction:column;gap:20px;padding:16px;position:relative}.config-form.loading{pointer-events:none}.form-field{display:flex;flex-direction:column;gap:6px}.form-field.disabled{opacity:.6}.form-field label{font-weight:600;font-size:.9rem;color:var(--kendo-color-on-app-surface, #212529)}.field-hint{margin:0;font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.selection-preview{padding:12px;background:var(--kendo-color-surface-alt, #f8f9fa);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px}.selection-preview h4{margin:0 0 8px;font-size:.9rem;color:var(--kendo-color-primary, #0d6efd)}.preview-content p{margin:4px 0;font-size:.85rem}.checkbox-row{display:flex;align-items:center;gap:8px;cursor:pointer;font-weight:600;font-size:.9rem;color:var(--kendo-color-on-app-surface, #212529)}.checkbox-row input{margin:0}.k-radio-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:6px}.radio-label{margin-left:8px;font-size:.9rem}.field-hint code{font-size:.78rem}.action-bar{display:flex;justify-content:flex-end;gap:8px;padding:8px 16px;border-top:1px solid var(--kendo-color-border, #dee2e6)}\n"] }]
4496
4749
  }], propDecorators: { ckTypeSelectorInput: [{
4497
4750
  type: ViewChild,
4498
4751
  args: ['ckTypeSelector']
@@ -4503,6 +4756,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
4503
4756
  type: Input
4504
4757
  }], initialRtId: [{
4505
4758
  type: Input
4759
+ }], initialEntitySelectorId: [{
4760
+ type: Input
4506
4761
  }], initialHideEmptyAttributes: [{
4507
4762
  type: Input
4508
4763
  }] } });
@@ -4514,7 +4769,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
4514
4769
  /**
4515
4770
  * Supported row types for query processing.
4516
4771
  */
4517
- const SUPPORTED_ROW_TYPES = [
4772
+ const SUPPORTED_ROW_TYPES$1 = [
4518
4773
  'RtSimpleQueryRow',
4519
4774
  'RtAggregationQueryRow',
4520
4775
  'RtGroupingAggregationQueryRow'
@@ -4615,7 +4870,7 @@ function parseNumericValue(value) {
4615
4870
  function extractAggregationValue(queryResult, valueField) {
4616
4871
  const rows = queryResult.rows?.items ?? [];
4617
4872
  // Get the first supported row
4618
- const firstRow = rows.find(row => row && SUPPORTED_ROW_TYPES.includes(row.__typename ?? ''));
4873
+ const firstRow = rows.find(row => row && SUPPORTED_ROW_TYPES$1.includes(row.__typename ?? ''));
4619
4874
  if (!firstRow)
4620
4875
  return 0;
4621
4876
  const cells = firstRow.cells?.items ?? [];
@@ -4650,7 +4905,7 @@ function extractGroupedAggregationValue(queryResult, categoryField, categoryValu
4650
4905
  const rows = queryResult.rows?.items ?? [];
4651
4906
  // Find the row where category matches
4652
4907
  for (const row of rows) {
4653
- if (!row || !SUPPORTED_ROW_TYPES.includes(row.__typename ?? ''))
4908
+ if (!row || !SUPPORTED_ROW_TYPES$1.includes(row.__typename ?? ''))
4654
4909
  continue;
4655
4910
  const cells = row.cells?.items ?? [];
4656
4911
  let categoryMatch = false;
@@ -4687,7 +4942,7 @@ function processStaticSeriesData(rows, categoryField, seriesConfigs) {
4687
4942
  seriesMap.set(seriesConfig.field, []);
4688
4943
  }
4689
4944
  for (const row of rows) {
4690
- if (!SUPPORTED_ROW_TYPES.includes(row.__typename ?? ''))
4945
+ if (!SUPPORTED_ROW_TYPES$1.includes(row.__typename ?? ''))
4691
4946
  continue;
4692
4947
  const cells = row.cells?.items ?? [];
4693
4948
  let categoryValue = '';
@@ -4737,7 +4992,7 @@ function processDynamicSeriesData(rows, categoryField, seriesGroupField, valueFi
4737
4992
  const allCategories = new Set();
4738
4993
  const allSeriesGroups = new Set();
4739
4994
  for (const row of rows) {
4740
- if (!SUPPORTED_ROW_TYPES.includes(row.__typename ?? ''))
4995
+ if (!SUPPORTED_ROW_TYPES$1.includes(row.__typename ?? ''))
4741
4996
  continue;
4742
4997
  const cells = row.cells?.items ?? [];
4743
4998
  let categoryValue = '';
@@ -4790,7 +5045,7 @@ function processDynamicSeriesData(rows, categoryField, seriesGroupField, valueFi
4790
5045
  function processPieChartData(rows, categoryField, valueField) {
4791
5046
  const result = [];
4792
5047
  for (const row of rows) {
4793
- if (!SUPPORTED_ROW_TYPES.includes(row.__typename ?? ''))
5048
+ if (!SUPPORTED_ROW_TYPES$1.includes(row.__typename ?? ''))
4794
5049
  continue;
4795
5050
  const cells = row.cells?.items ?? [];
4796
5051
  let categoryValue = '';
@@ -7006,8 +7261,148 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
7006
7261
  type: Output
7007
7262
  }] } });
7008
7263
 
7264
+ /**
7265
+ * Centralized, timezone-aware datetime formatting for MeshBoard widgets.
7266
+ *
7267
+ * Every widget that renders a timestamp routes through here so the board's
7268
+ * {@link MeshBoardTimeZoneMode} governs display consistently — matching the
7269
+ * basis the time-filter boundaries are computed on. In `'local'` mode values
7270
+ * render in the browser's timezone; in `'utc'` mode they render in UTC.
7271
+ */
7272
+ /** Locale used for all MeshBoard datetime rendering (matches the app's LOCALE_ID family). */
7273
+ const MESHBOARD_LOCALE = 'de-AT';
7274
+ /**
7275
+ * Strict ISO-8601 *date-time* matcher (date + time, optional fractional seconds
7276
+ * and `Z`/offset). Plain dates (`2026-01-01`) are intentionally excluded so they
7277
+ * are not reinterpreted across a timezone boundary.
7278
+ */
7279
+ const ISO_DATE_TIME_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2})?(\.\d+)?(Z|[+-]\d{2}:\d{2})?$/;
7280
+ /** True when the value is a string in strict ISO-8601 date-time form. */
7281
+ function isIsoDateTime(value) {
7282
+ return typeof value === 'string' && ISO_DATE_TIME_RE.test(value);
7283
+ }
7284
+ /** Parses a value into a valid `Date`, or returns `null` when it is not a usable instant. */
7285
+ function toInstant(value) {
7286
+ if (value instanceof Date) {
7287
+ return isNaN(value.getTime()) ? null : value;
7288
+ }
7289
+ if (typeof value === 'string' || typeof value === 'number') {
7290
+ const date = new Date(value);
7291
+ return isNaN(date.getTime()) ? null : date;
7292
+ }
7293
+ return null;
7294
+ }
7295
+ /** Maps the board mode to the `Intl` `timeZone` option (`undefined` = browser local). */
7296
+ function timeZoneOption(mode) {
7297
+ return mode === 'utc' ? 'UTC' : undefined;
7298
+ }
7299
+ /**
7300
+ * Cached `Intl.DateTimeFormat` per board mode. Bucketing widgets call
7301
+ * {@link getZonedDateParts} once per data row, so re-creating the (relatively
7302
+ * expensive) formatter each time would be wasteful.
7303
+ */
7304
+ const partsFormatterCache = new Map();
7305
+ function partsFormatter(mode) {
7306
+ let formatter = partsFormatterCache.get(mode);
7307
+ if (!formatter) {
7308
+ formatter = new Intl.DateTimeFormat('en-US', {
7309
+ year: 'numeric',
7310
+ month: '2-digit',
7311
+ day: '2-digit',
7312
+ hour: '2-digit',
7313
+ minute: '2-digit',
7314
+ hourCycle: 'h23',
7315
+ timeZone: timeZoneOption(mode)
7316
+ });
7317
+ partsFormatterCache.set(mode, formatter);
7318
+ }
7319
+ return formatter;
7320
+ }
7321
+ /**
7322
+ * Decomposes an instant into its calendar/clock components **in the board's
7323
+ * timezone**. In `'local'` mode the parts are the browser-local wall-clock
7324
+ * values; in `'utc'` mode they are the UTC values.
7325
+ *
7326
+ * This is the bucketing counterpart to {@link formatInstant}: widgets that group
7327
+ * data by hour-of-day / day (e.g. the heatmap) must derive those buckets on the
7328
+ * same timezone basis the time-filter boundaries use, otherwise a UTC+offset
7329
+ * tenant sees rows shifted into the wrong hour/day. Returns `null` for
7330
+ * non-instants so callers can skip the row.
7331
+ */
7332
+ function getZonedDateParts(value, mode) {
7333
+ const date = toInstant(value);
7334
+ if (!date) {
7335
+ return null;
7336
+ }
7337
+ const lookup = new Map();
7338
+ for (const part of partsFormatter(mode).formatToParts(date)) {
7339
+ lookup.set(part.type, part.value);
7340
+ }
7341
+ const year = Number(lookup.get('year'));
7342
+ const month = Number(lookup.get('month'));
7343
+ const day = Number(lookup.get('day'));
7344
+ // `h23` yields 0–23, but some engines emit '24' for midnight — normalize it.
7345
+ const hour = Number(lookup.get('hour')) % 24;
7346
+ const minute = Number(lookup.get('minute'));
7347
+ if ([year, month, day, hour, minute].some(Number.isNaN)) {
7348
+ return null;
7349
+ }
7350
+ return { year, month, day, hour, minute };
7351
+ }
7352
+ /** Zero-padded `yyyy-MM-dd` key for {@link ZonedDateParts} (stable for sorting). */
7353
+ function zonedDateKey(parts) {
7354
+ const month = parts.month.toString().padStart(2, '0');
7355
+ const day = parts.day.toString().padStart(2, '0');
7356
+ return `${parts.year}-${month}-${day}`;
7357
+ }
7358
+ /**
7359
+ * Formats an instant honoring the board's timezone mode, with caller-supplied
7360
+ * `Intl.DateTimeFormatOptions`. The `timeZone` option is injected from `mode`
7361
+ * and overrides any passed in. Returns `null` when the value is not a valid
7362
+ * instant, so callers can fall back to their own rendering.
7363
+ */
7364
+ function formatInstant(value, mode, options) {
7365
+ const date = toInstant(value);
7366
+ if (!date) {
7367
+ return null;
7368
+ }
7369
+ return date.toLocaleString(MESHBOARD_LOCALE, { ...options, timeZone: timeZoneOption(mode) });
7370
+ }
7371
+ /** Full date with `dd.MM.yyyy` in the board's timezone, or `null`. */
7372
+ function formatBoardDate(value, mode) {
7373
+ return formatInstant(value, mode, { day: '2-digit', month: '2-digit', year: 'numeric' });
7374
+ }
7375
+ /**
7376
+ * Full date + time with seconds (`dd.MM.yyyy HH:mm:ss`) in the board's timezone.
7377
+ * This is the table-widget cell format. Returns `null` for non-instants.
7378
+ *
7379
+ * Date and time parts are formatted separately and joined with a single space so
7380
+ * the output is exactly `dd.MM.yyyy HH:mm:ss` — the locale's combined format
7381
+ * would otherwise insert a comma (`dd.MM.yyyy, HH:mm:ss`).
7382
+ */
7383
+ function formatBoardDateTime(value, mode) {
7384
+ const datePart = formatInstant(value, mode, { day: '2-digit', month: '2-digit', year: 'numeric' });
7385
+ const timePart = formatInstant(value, mode, { hour: '2-digit', minute: '2-digit', second: '2-digit' });
7386
+ if (datePart === null || timePart === null) {
7387
+ return null;
7388
+ }
7389
+ return `${datePart} ${timePart}`;
7390
+ }
7391
+ /**
7392
+ * Table-cell formatter: ISO-8601 date-time strings are rendered in the board's
7393
+ * timezone (`dd.MM.yyyy HH:mm:ss`); every other value passes through unchanged.
7394
+ * Safe to attach to all columns — non-datetime cells are returned via `String`.
7395
+ */
7396
+ function formatTableCellValue(value, mode) {
7397
+ if (isIsoDateTime(value)) {
7398
+ return formatBoardDateTime(value, mode) ?? String(value);
7399
+ }
7400
+ return value === null || value === undefined ? '' : String(value);
7401
+ }
7402
+
7009
7403
  class EntityAssociationsWidgetComponent {
7010
7404
  dataService = inject(DashboardDataService);
7405
+ stateService = inject(MeshBoardStateService);
7011
7406
  config;
7012
7407
  arrowRightIcon = arrowRightIcon;
7013
7408
  arrowLeftIcon = arrowLeftIcon;
@@ -7218,14 +7613,13 @@ class EntityAssociationsWidgetComponent {
7218
7613
  maximumFractionDigits: 2
7219
7614
  });
7220
7615
  }
7616
+ const mode = this.stateService.timeZoneMode();
7221
7617
  if (value instanceof Date)
7222
- return value.toLocaleDateString('de-AT');
7618
+ return formatBoardDate(value, mode) ?? String(value);
7223
7619
  if (typeof value === 'string') {
7224
- // Try to detect ISO date strings
7225
- if (/^\d{4}-\d{2}-\d{2}T/.test(value)) {
7226
- const date = new Date(value);
7227
- if (!isNaN(date.getTime()))
7228
- return date.toLocaleDateString('de-AT');
7620
+ // Format ISO date-time strings on the board's timezone basis
7621
+ if (isIsoDateTime(value)) {
7622
+ return formatBoardDate(value, mode) ?? value;
7229
7623
  }
7230
7624
  return value;
7231
7625
  }
@@ -8514,6 +8908,7 @@ function resolveStatusMapping(config) {
8514
8908
  class TableWidgetComponent {
8515
8909
  config;
8516
8910
  dataSource;
8911
+ stateService = inject(MeshBoardStateService);
8517
8912
  // Widget state signals
8518
8913
  _isLoading = signal(false, /* @ts-ignore */
8519
8914
  ...(ngDevMode ? [{ debugName: "_isLoading" }] : /* istanbul ignore next */ []));
@@ -8545,9 +8940,25 @@ class TableWidgetComponent {
8545
8940
  * For persistent queries, uses dynamically derived columns from the query response.
8546
8941
  */
8547
8942
  listViewColumns = computed(() => {
8943
+ // Read the board's timezone mode so datetime cells (e.g. archive
8944
+ // window_start/window_end, which arrive as raw ISO-8601 strings) render on
8945
+ // the same basis the time filter is computed on. Reading it here makes the
8946
+ // columns recompute when the mode changes.
8947
+ const mode = this.stateService.timeZoneMode();
8948
+ // Only plain-text columns get the datetime formatter — columns the user
8949
+ // explicitly typed ('date', 'numeric', 'iso8601', …) keep their own
8950
+ // rendering, and any value that is not an ISO-8601 date-time passes through
8951
+ // unchanged. Derived stream-data columns are all 'text', so they're covered.
8952
+ const withDateFormatter = (column) => {
8953
+ const isPlainText = !column.dataType || column.dataType === 'text';
8954
+ if (!isPlainText || column.formatter) {
8955
+ return column;
8956
+ }
8957
+ return { ...column, formatter: (value) => formatTableCellValue(value, mode) };
8958
+ };
8548
8959
  // If explicit columns are configured, use them (works for both data source types)
8549
8960
  if (this.config?.columns && this.config.columns.length > 0) {
8550
- return this.config.columns.map(col => ({
8961
+ return this.config.columns.map(col => withDateFormatter({
8551
8962
  field: col.field,
8552
8963
  displayName: col.title,
8553
8964
  dataType: (col.dataType ?? 'text'),
@@ -8559,7 +8970,7 @@ class TableWidgetComponent {
8559
8970
  if (this.config?.dataSource?.type === 'persistentQuery') {
8560
8971
  const queryColumns = this._queryColumnsForView();
8561
8972
  if (queryColumns.length > 0) {
8562
- return queryColumns;
8973
+ return queryColumns.map(withDateFormatter);
8563
8974
  }
8564
8975
  return [];
8565
8976
  }
@@ -12781,18 +13192,18 @@ class BarChartWidgetComponent {
12781
13192
  return defaultColor;
12782
13193
  }
12783
13194
  formatCategoryValue(value) {
12784
- const str = String(value ?? '');
12785
- // Detect ISO 8601 timestamps and format as readable date/time
12786
- if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/.test(str)) {
12787
- const date = new Date(str);
12788
- if (!isNaN(date.getTime())) {
12789
- return date.toLocaleString('de-AT', {
12790
- day: '2-digit', month: '2-digit',
12791
- hour: '2-digit', minute: '2-digit'
12792
- });
13195
+ // ISO-8601 date-time values are formatted on the board's timezone basis;
13196
+ // everything else passes through as a string.
13197
+ if (isIsoDateTime(value)) {
13198
+ const formatted = formatInstant(value, this.stateService.timeZoneMode(), {
13199
+ day: '2-digit', month: '2-digit',
13200
+ hour: '2-digit', minute: '2-digit'
13201
+ });
13202
+ if (formatted !== null) {
13203
+ return formatted;
12793
13204
  }
12794
13205
  }
12795
- return str;
13206
+ return String(value ?? '');
12796
13207
  }
12797
13208
  /**
12798
13209
  * Converts widget filter configuration to GraphQL FieldFilterDto format.
@@ -14147,8 +14558,11 @@ class LineChartWidgetComponent {
14147
14558
  // Sort categories chronologically
14148
14559
  const sortedCategoryEntries = Array.from(allCategories.entries())
14149
14560
  .sort((a, b) => a[1].getTime() - b[1].getTime());
14150
- // Detect if we need time precision (multiple data points per day)
14151
- const dateOnlySet = new Set(sortedCategoryEntries.map(([, date]) => date.toLocaleDateString('de-AT')));
14561
+ // Detect if we need time precision (multiple data points per day). The
14562
+ // day-key is computed on the board's timezone basis so the date axis stays
14563
+ // consistent with the time filter and the rest of the board.
14564
+ const mode = this.stateService.timeZoneMode();
14565
+ const dateOnlySet = new Set(sortedCategoryEntries.map(([, date]) => formatInstant(date, mode, { day: '2-digit', month: '2-digit', year: 'numeric' }) ?? '?'));
14152
14566
  const needsTime = dateOnlySet.size < sortedCategoryEntries.length;
14153
14567
  const categories = sortedCategoryEntries.map(([, date]) => needsTime ? this.formatDateTime(date) : this.formatDate(date));
14154
14568
  const categoryKeys = sortedCategoryEntries.map(([key]) => key);
@@ -14194,27 +14608,24 @@ class LineChartWidgetComponent {
14194
14608
  * Formats a date for display on the category axis (date only).
14195
14609
  */
14196
14610
  formatDate(date) {
14197
- if (isNaN(date.getTime()))
14198
- return '?';
14199
- return date.toLocaleDateString('de-AT', {
14611
+ return formatInstant(date, this.stateService.timeZoneMode(), {
14200
14612
  day: '2-digit',
14201
14613
  month: '2-digit',
14202
14614
  year: 'numeric'
14203
- });
14615
+ }) ?? '?';
14204
14616
  }
14205
14617
  /**
14206
14618
  * Formats a date with time for display on the category axis.
14619
+ * Date and time parts are formatted separately (joined with a space) to keep
14620
+ * the compact axis label; both honor the board's timezone mode.
14207
14621
  */
14208
14622
  formatDateTime(date) {
14209
- if (isNaN(date.getTime()))
14623
+ const mode = this.stateService.timeZoneMode();
14624
+ const datePart = formatInstant(date, mode, { day: '2-digit', month: '2-digit' });
14625
+ const timePart = formatInstant(date, mode, { hour: '2-digit', minute: '2-digit' });
14626
+ if (datePart === null || timePart === null)
14210
14627
  return '?';
14211
- return date.toLocaleDateString('de-AT', {
14212
- day: '2-digit',
14213
- month: '2-digit'
14214
- }) + ' ' + date.toLocaleTimeString('de-AT', {
14215
- hour: '2-digit',
14216
- minute: '2-digit'
14217
- });
14628
+ return `${datePart} ${timePart}`;
14218
14629
  }
14219
14630
  sanitizeAxisName(name) {
14220
14631
  return name.replace(/[^a-zA-Z0-9]/g, '_');
@@ -14303,6 +14714,7 @@ class LineChartWidgetComponent {
14303
14714
  [axis]="series.axisName ?? ''"
14304
14715
  [color]="series.color"
14305
14716
  [opacity]="0.18"
14717
+ [visibleInLegend]="false"
14306
14718
  [markers]="{ visible: false }">
14307
14719
  </kendo-chart-series-item>
14308
14720
  }
@@ -14335,7 +14747,7 @@ class LineChartWidgetComponent {
14335
14747
  </kendo-chart>
14336
14748
  }
14337
14749
  </div>
14338
- `, isInline: true, styles: [":host{display:block;width:100%;height:100%}.line-chart-widget{position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:8px;box-sizing:border-box;overflow:hidden}.data-count{position:absolute;top:2px;right:6px;z-index:2;font-size:.7rem;line-height:1;opacity:.55;color:var(--kendo-color-subtle, #6c757d);pointer-events:none;-webkit-user-select:none;user-select:none;font-variant-numeric:tabular-nums}.line-chart-widget.loading,.line-chart-widget.error{opacity:.7}.loading-indicator,.error-message{display:flex;align-items:center;justify-content:center;height:100%;width:100%}.loading-indicator span{font-size:1.5rem;color:var(--kendo-color-subtle, #6c757d)}.error-message span{color:var(--kendo-color-error, #dc3545);font-size:.875rem}.chart-container{width:100%;height:100%}kendo-chart{width:100%;height:100%}.chart-tooltip{padding:4px 8px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ChartsModule }, { kind: "component", type: i1$7.ChartComponent, selector: "kendo-chart", inputs: ["pannable", "renderAs", "seriesColors", "subtitle", "title", "noData", "observeStyles", "transitions", "zoomable", "axisDefaults", "categoryAxis", "chartArea", "legend", "panes", "paneDefaults", "plotArea", "series", "seriesDefaults", "tooltip", "valueAxis", "xAxis", "yAxis", "resizeRateLimit", "popupSettings", "drilldownLevel"], outputs: ["axisLabelClick", "drag", "dragEnd", "dragStart", "legendItemHover", "legendItemLeave", "noteClick", "noteHover", "noteLeave", "paneRender", "plotAreaClick", "plotAreaHover", "plotAreaLeave", "render", "select", "selectEnd", "selectStart", "seriesClick", "drilldown", "seriesHover", "seriesOver", "seriesLeave", "zoom", "zoomEnd", "zoomStart", "legendItemClick", "drilldownLevelChange"], exportAs: ["kendoChart"] }, { kind: "directive", type: i1$7.SeriesTooltipTemplateDirective, selector: "[kendoChartSeriesTooltipTemplate]" }, { kind: "component", type: i1$7.CategoryAxisComponent, selector: "kendo-chart-category-axis" }, { kind: "component", type: i1$7.CategoryAxisItemComponent, selector: "kendo-chart-category-axis-item", inputs: ["autoBaseUnitSteps", "axisCrossingValue", "background", "baseUnit", "baseUnitStep", "categories", "color", "justified", "line", "majorGridLines", "majorTicks", "max", "maxDateGroups", "maxDivisions", "min", "minorGridLines", "minorTicks", "name", "pane", "plotBands", "reverse", "roundToBaseUnit", "startAngle", "type", "visible", "weekStartDay", "crosshair", "labels", "notes", "select", "title", "rangeLabels", "highlight"] }, { kind: "component", type: i1$7.CategoryAxisLabelsComponent, selector: "kendo-chart-category-axis-item-labels", inputs: ["background", "border", "color", "content", "culture", "dateFormats", "font", "format", "margin", "mirror", "padding", "position", "rotation", "skip", "step", "visible", "visual"] }, { kind: "component", type: i1$7.ChartAreaComponent, selector: "kendo-chart-area", inputs: ["background", "border", "height", "margin", "opacity", "width"] }, { kind: "component", type: i1$7.LegendComponent, selector: "kendo-chart-legend", inputs: ["align", "background", "border", "height", "labels", "margin", "offsetX", "offsetY", "orientation", "padding", "position", "reverse", "visible", "width", "markers", "spacing", "inactiveItems", "item", "title", "focusHighlight"] }, { kind: "component", type: i1$7.SeriesComponent, selector: "kendo-chart-series" }, { kind: "component", type: i1$7.SeriesItemComponent, selector: "kendo-chart-series-item", inputs: ["aggregate", "autoFit", "axis", "border", "categoryAxis", "categoryField", "closeField", "color", "colorField", "connectors", "currentField", "dashType", "data", "downColor", "downColorField", "drilldownField", "dynamicHeight", "dynamicSlope", "errorHighField", "errorLowField", "explodeField", "field", "fromField", "gap", "highField", "holeSize", "line", "lowField", "lowerField", "margin", "maxSize", "mean", "meanField", "median", "medianField", "minSize", "missingValues", "name", "neckRatio", "negativeColor", "negativeValues", "noteTextField", "opacity", "openField", "outliersField", "overlay", "padding", "q1Field", "q3Field", "segmentSpacing", "size", "sizeField", "spacing", "stack", "startAngle", "lineStyle", "summaryField", "target", "toField", "type", "upperField", "visible", "visibleInLegend", "visibleInLegendField", "visual", "width", "whiskers", "xAxis", "xErrorHighField", "xErrorLowField", "xField", "yAxis", "yErrorHighField", "yErrorLowField", "yField", "zIndex", "trendline", "for", "legendItem", "pattern", "patternField", "errorBars", "extremes", "highlight", "labels", "markers", "notes", "outliers", "tooltip"] }, { kind: "component", type: i1$7.TooltipComponent, selector: "kendo-chart-tooltip", inputs: ["background", "border", "color", "font", "format", "opacity", "padding", "shared", "visible"] }, { kind: "component", type: i1$7.ValueAxisComponent, selector: "kendo-chart-value-axis" }, { kind: "component", type: i1$7.ValueAxisItemComponent, selector: "kendo-chart-value-axis-item", inputs: ["axisCrossingValue", "background", "color", "line", "majorGridLines", "majorTicks", "majorUnit", "max", "min", "minorGridLines", "minorTicks", "minorUnit", "name", "narrowRange", "pane", "plotBands", "reverse", "type", "visible", "crosshair", "labels", "notes", "title"] }, { kind: "component", type: WidgetNotConfiguredComponent, selector: "mm-widget-not-configured" }], changeDetection: i0.ChangeDetectionStrategy.Eager });
14750
+ `, isInline: true, styles: [":host{display:block;width:100%;height:100%}.line-chart-widget{position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:8px;box-sizing:border-box;overflow:hidden}.data-count{position:absolute;top:4px;right:6px;z-index:2;max-width:calc(100% - 12px);padding:1px 7px;border-radius:9px;font-size:.7rem;line-height:1.5;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--kendo-color-on-app-surface, #6c757d);background:color-mix(in srgb,var(--kendo-color-app-surface, #888) 12%,transparent);pointer-events:none;-webkit-user-select:none;user-select:none;font-variant-numeric:tabular-nums}.line-chart-widget.loading,.line-chart-widget.error{opacity:.7}.loading-indicator,.error-message{display:flex;align-items:center;justify-content:center;height:100%;width:100%}.loading-indicator span{font-size:1.5rem;color:var(--kendo-color-subtle, #6c757d)}.error-message span{color:var(--kendo-color-error, #dc3545);font-size:.875rem}.chart-container{width:100%;height:100%}kendo-chart{width:100%;height:100%}.chart-tooltip{padding:4px 8px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ChartsModule }, { kind: "component", type: i1$7.ChartComponent, selector: "kendo-chart", inputs: ["pannable", "renderAs", "seriesColors", "subtitle", "title", "noData", "observeStyles", "transitions", "zoomable", "axisDefaults", "categoryAxis", "chartArea", "legend", "panes", "paneDefaults", "plotArea", "series", "seriesDefaults", "tooltip", "valueAxis", "xAxis", "yAxis", "resizeRateLimit", "popupSettings", "drilldownLevel"], outputs: ["axisLabelClick", "drag", "dragEnd", "dragStart", "legendItemHover", "legendItemLeave", "noteClick", "noteHover", "noteLeave", "paneRender", "plotAreaClick", "plotAreaHover", "plotAreaLeave", "render", "select", "selectEnd", "selectStart", "seriesClick", "drilldown", "seriesHover", "seriesOver", "seriesLeave", "zoom", "zoomEnd", "zoomStart", "legendItemClick", "drilldownLevelChange"], exportAs: ["kendoChart"] }, { kind: "directive", type: i1$7.SeriesTooltipTemplateDirective, selector: "[kendoChartSeriesTooltipTemplate]" }, { kind: "component", type: i1$7.CategoryAxisComponent, selector: "kendo-chart-category-axis" }, { kind: "component", type: i1$7.CategoryAxisItemComponent, selector: "kendo-chart-category-axis-item", inputs: ["autoBaseUnitSteps", "axisCrossingValue", "background", "baseUnit", "baseUnitStep", "categories", "color", "justified", "line", "majorGridLines", "majorTicks", "max", "maxDateGroups", "maxDivisions", "min", "minorGridLines", "minorTicks", "name", "pane", "plotBands", "reverse", "roundToBaseUnit", "startAngle", "type", "visible", "weekStartDay", "crosshair", "labels", "notes", "select", "title", "rangeLabels", "highlight"] }, { kind: "component", type: i1$7.CategoryAxisLabelsComponent, selector: "kendo-chart-category-axis-item-labels", inputs: ["background", "border", "color", "content", "culture", "dateFormats", "font", "format", "margin", "mirror", "padding", "position", "rotation", "skip", "step", "visible", "visual"] }, { kind: "component", type: i1$7.ChartAreaComponent, selector: "kendo-chart-area", inputs: ["background", "border", "height", "margin", "opacity", "width"] }, { kind: "component", type: i1$7.LegendComponent, selector: "kendo-chart-legend", inputs: ["align", "background", "border", "height", "labels", "margin", "offsetX", "offsetY", "orientation", "padding", "position", "reverse", "visible", "width", "markers", "spacing", "inactiveItems", "item", "title", "focusHighlight"] }, { kind: "component", type: i1$7.SeriesComponent, selector: "kendo-chart-series" }, { kind: "component", type: i1$7.SeriesItemComponent, selector: "kendo-chart-series-item", inputs: ["aggregate", "autoFit", "axis", "border", "categoryAxis", "categoryField", "closeField", "color", "colorField", "connectors", "currentField", "dashType", "data", "downColor", "downColorField", "drilldownField", "dynamicHeight", "dynamicSlope", "errorHighField", "errorLowField", "explodeField", "field", "fromField", "gap", "highField", "holeSize", "line", "lowField", "lowerField", "margin", "maxSize", "mean", "meanField", "median", "medianField", "minSize", "missingValues", "name", "neckRatio", "negativeColor", "negativeValues", "noteTextField", "opacity", "openField", "outliersField", "overlay", "padding", "q1Field", "q3Field", "segmentSpacing", "size", "sizeField", "spacing", "stack", "startAngle", "lineStyle", "summaryField", "target", "toField", "type", "upperField", "visible", "visibleInLegend", "visibleInLegendField", "visual", "width", "whiskers", "xAxis", "xErrorHighField", "xErrorLowField", "xField", "yAxis", "yErrorHighField", "yErrorLowField", "yField", "zIndex", "trendline", "for", "legendItem", "pattern", "patternField", "errorBars", "extremes", "highlight", "labels", "markers", "notes", "outliers", "tooltip"] }, { kind: "component", type: i1$7.TooltipComponent, selector: "kendo-chart-tooltip", inputs: ["background", "border", "color", "font", "format", "opacity", "padding", "shared", "visible"] }, { kind: "component", type: i1$7.ValueAxisComponent, selector: "kendo-chart-value-axis" }, { kind: "component", type: i1$7.ValueAxisItemComponent, selector: "kendo-chart-value-axis-item", inputs: ["axisCrossingValue", "background", "color", "line", "majorGridLines", "majorTicks", "majorUnit", "max", "min", "minorGridLines", "minorTicks", "minorUnit", "name", "narrowRange", "pane", "plotBands", "reverse", "type", "visible", "crosshair", "labels", "notes", "title"] }, { kind: "component", type: WidgetNotConfiguredComponent, selector: "mm-widget-not-configured" }], changeDetection: i0.ChangeDetectionStrategy.Eager });
14339
14751
  }
14340
14752
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: LineChartWidgetComponent, decorators: [{
14341
14753
  type: Component,
@@ -14415,6 +14827,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
14415
14827
  [axis]="series.axisName ?? ''"
14416
14828
  [color]="series.color"
14417
14829
  [opacity]="0.18"
14830
+ [visibleInLegend]="false"
14418
14831
  [markers]="{ visible: false }">
14419
14832
  </kendo-chart-series-item>
14420
14833
  }
@@ -14447,7 +14860,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
14447
14860
  </kendo-chart>
14448
14861
  }
14449
14862
  </div>
14450
- `, changeDetection: ChangeDetectionStrategy.Eager, styles: [":host{display:block;width:100%;height:100%}.line-chart-widget{position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:8px;box-sizing:border-box;overflow:hidden}.data-count{position:absolute;top:2px;right:6px;z-index:2;font-size:.7rem;line-height:1;opacity:.55;color:var(--kendo-color-subtle, #6c757d);pointer-events:none;-webkit-user-select:none;user-select:none;font-variant-numeric:tabular-nums}.line-chart-widget.loading,.line-chart-widget.error{opacity:.7}.loading-indicator,.error-message{display:flex;align-items:center;justify-content:center;height:100%;width:100%}.loading-indicator span{font-size:1.5rem;color:var(--kendo-color-subtle, #6c757d)}.error-message span{color:var(--kendo-color-error, #dc3545);font-size:.875rem}.chart-container{width:100%;height:100%}kendo-chart{width:100%;height:100%}.chart-tooltip{padding:4px 8px}\n"] }]
14863
+ `, changeDetection: ChangeDetectionStrategy.Eager, styles: [":host{display:block;width:100%;height:100%}.line-chart-widget{position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:8px;box-sizing:border-box;overflow:hidden}.data-count{position:absolute;top:4px;right:6px;z-index:2;max-width:calc(100% - 12px);padding:1px 7px;border-radius:9px;font-size:.7rem;line-height:1.5;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--kendo-color-on-app-surface, #6c757d);background:color-mix(in srgb,var(--kendo-color-app-surface, #888) 12%,transparent);pointer-events:none;-webkit-user-select:none;user-select:none;font-variant-numeric:tabular-nums}.line-chart-widget.loading,.line-chart-widget.error{opacity:.7}.loading-indicator,.error-message{display:flex;align-items:center;justify-content:center;height:100%;width:100%}.loading-indicator span{font-size:1.5rem;color:var(--kendo-color-subtle, #6c757d)}.error-message span{color:var(--kendo-color-error, #dc3545);font-size:.875rem}.chart-container{width:100%;height:100%}kendo-chart{width:100%;height:100%}.chart-tooltip{padding:4px 8px}\n"] }]
14451
14864
  }], propDecorators: { config: [{
14452
14865
  type: Input
14453
14866
  }] } });
@@ -15380,11 +15793,15 @@ class HeatmapWidgetComponent {
15380
15793
  // Detect interval width in minutes from the first row that has both from and to
15381
15794
  const intervalMinutes = this.detectIntervalMinutes(parsedRows);
15382
15795
  const hasSubHourIntervals = dateEndField && intervalMinutes > 0 && intervalMinutes < 60;
15796
+ // Bucket on the board's timezone basis so hour-of-day / day axes match the
15797
+ // time-filter boundaries (default 'local'); using UTC here shifts a UTC+N
15798
+ // tenant's rows into the wrong hour/day.
15799
+ const mode = this.stateService.timeZoneMode();
15383
15800
  if (hasSubHourIntervals) {
15384
- this.processSubHourData(parsedRows, intervalMinutes, aggregation);
15801
+ this.processSubHourData(parsedRows, intervalMinutes, aggregation, mode);
15385
15802
  }
15386
15803
  else {
15387
- this.processHourlyData(parsedRows, aggregation);
15804
+ this.processHourlyData(parsedRows, aggregation, mode);
15388
15805
  }
15389
15806
  }
15390
15807
  /**
@@ -15404,11 +15821,14 @@ class HeatmapWidgetComponent {
15404
15821
  /**
15405
15822
  * Processes data into hourly buckets (original behavior).
15406
15823
  */
15407
- processHourlyData(rows, aggregation) {
15824
+ processHourlyData(rows, aggregation, mode) {
15408
15825
  const buckets = new Map();
15409
15826
  for (const { dateFrom, numericValue } of rows) {
15410
- const dateKey = this.formatDateKey(dateFrom);
15411
- const hour = dateFrom.getUTCHours();
15827
+ const parts = getZonedDateParts(dateFrom, mode);
15828
+ if (!parts)
15829
+ continue;
15830
+ const dateKey = zonedDateKey(parts);
15831
+ const hour = parts.hour;
15412
15832
  if (!buckets.has(dateKey))
15413
15833
  buckets.set(dateKey, new Map());
15414
15834
  const hourMap = buckets.get(dateKey);
@@ -15438,16 +15858,19 @@ class HeatmapWidgetComponent {
15438
15858
  * Y-axis: hours (00:00..23:00), X-axis: interval labels (e.g. "00-15", "15-30", ...).
15439
15859
  * For multi-day data, X-axis labels are prefixed with the date.
15440
15860
  */
15441
- processSubHourData(rows, intervalMinutes, aggregation) {
15861
+ processSubHourData(rows, intervalMinutes, aggregation, mode) {
15442
15862
  const intervalsPerHour = Math.floor(60 / intervalMinutes);
15443
15863
  // Bucket: xKey (date+interval) -> hour -> values[]
15444
15864
  const buckets = new Map();
15445
15865
  const allDates = new Set();
15446
15866
  for (const { dateFrom, numericValue } of rows) {
15447
- const dateKey = this.formatDateKey(dateFrom);
15867
+ const parts = getZonedDateParts(dateFrom, mode);
15868
+ if (!parts)
15869
+ continue;
15870
+ const dateKey = zonedDateKey(parts);
15448
15871
  allDates.add(dateKey);
15449
- const hour = dateFrom.getUTCHours();
15450
- const minute = dateFrom.getUTCMinutes();
15872
+ const hour = parts.hour;
15873
+ const minute = parts.minute;
15451
15874
  const intervalIndex = Math.floor(minute / intervalMinutes);
15452
15875
  const intervalStart = intervalIndex * intervalMinutes;
15453
15876
  const intervalEnd = intervalStart + intervalMinutes;
@@ -15540,12 +15963,6 @@ class HeatmapWidgetComponent {
15540
15963
  const date = new Date(String(value));
15541
15964
  return isNaN(date.getTime()) ? null : date;
15542
15965
  }
15543
- formatDateKey(date) {
15544
- const year = date.getUTCFullYear();
15545
- const month = (date.getUTCMonth() + 1).toString().padStart(2, '0');
15546
- const day = date.getUTCDate().toString().padStart(2, '0');
15547
- return `${year}-${month}-${day}`;
15548
- }
15549
15966
  convertFiltersToDto(filters) {
15550
15967
  const variables = this.stateService.getVariables();
15551
15968
  return this.variableService.convertToFieldFilterDto(filters, variables);
@@ -16413,8 +16830,72 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
16413
16830
  type: Input
16414
16831
  }] } });
16415
16832
 
16833
+ /**
16834
+ * Row `__typename`s the value extraction recognises. Runtime queries
16835
+ * discriminate by kind; stream-data queries collapse every kind
16836
+ * (simple / aggregation / grouped / downsampling) into `StreamDataQueryRow`.
16837
+ */
16838
+ const SUPPORTED_ROW_TYPES = new Set([
16839
+ 'RtAggregationQueryRow',
16840
+ 'RtGroupingAggregationQueryRow',
16841
+ 'StreamDataQueryRow'
16842
+ ]);
16843
+ function extractAggregation(result, valueField) {
16844
+ const firstRow = result.rows.find(row => SUPPORTED_ROW_TYPES.has(row.__typename ?? ''));
16845
+ if (!firstRow)
16846
+ return 0;
16847
+ if (valueField) {
16848
+ for (const cell of firstRow.cells) {
16849
+ if (matchesAttributePath(cell.attributePath, valueField)) {
16850
+ return parseNumericValue(cell.value);
16851
+ }
16852
+ }
16853
+ }
16854
+ // Fallback: first cell value when no specific field is configured.
16855
+ return firstRow.cells.length > 0 ? parseNumericValue(firstRow.cells[0].value) : 0;
16856
+ }
16857
+ function extractGroupedAggregation(result, categoryField, categoryValue, valueField) {
16858
+ if (!categoryField || !categoryValue || !valueField)
16859
+ return 0;
16860
+ for (const row of result.rows) {
16861
+ if (!SUPPORTED_ROW_TYPES.has(row.__typename ?? ''))
16862
+ continue;
16863
+ let categoryMatch = false;
16864
+ let value = 0;
16865
+ for (const cell of row.cells) {
16866
+ if (matchesAttributePath(cell.attributePath, categoryField) && String(cell.value) === categoryValue) {
16867
+ categoryMatch = true;
16868
+ }
16869
+ if (matchesAttributePath(cell.attributePath, valueField)) {
16870
+ value = parseNumericValue(cell.value);
16871
+ }
16872
+ }
16873
+ if (categoryMatch)
16874
+ return value;
16875
+ }
16876
+ return 0;
16877
+ }
16878
+ /**
16879
+ * Reduces a unified query result to the single numeric value a cell displays,
16880
+ * according to the source's `queryMode`. Mirrors the KPI widget extraction.
16881
+ */
16882
+ function extractPersistentQueryCellValue(result, source) {
16883
+ switch (source.queryMode ?? 'simpleCount') {
16884
+ case 'aggregation':
16885
+ return extractAggregation(result, source.queryValueField);
16886
+ case 'groupedAggregation':
16887
+ return extractGroupedAggregation(result, source.queryCategoryField, source.queryCategoryValue, source.queryValueField);
16888
+ case 'simpleCount':
16889
+ default:
16890
+ return result.totalCount;
16891
+ }
16892
+ }
16893
+
16416
16894
  class StatsGridWidgetComponent {
16417
16895
  dataService = inject(MeshBoardDataService);
16896
+ queryExecutor = inject(QueryExecutorService);
16897
+ stateService = inject(MeshBoardStateService);
16898
+ variableService = inject(MeshBoardVariableService);
16418
16899
  config;
16419
16900
  _isLoading = signal(false, /* @ts-ignore */
16420
16901
  ...(ngDevMode ? [{ debugName: "_isLoading" }] : /* istanbul ignore next */ []));
@@ -16461,26 +16942,22 @@ class StatsGridWidgetComponent {
16461
16942
  this._isLoading.set(true);
16462
16943
  this._error.set(null);
16463
16944
  try {
16945
+ // Runtime aggregation queries are only needed for stats WITHOUT a
16946
+ // per-stat persistent-query source — those run through the executor below.
16464
16947
  const dataSource = this.config.dataSource;
16465
- if (dataSource?.type !== 'aggregation' || !dataSource.queries?.length) {
16466
- // No aggregation queries configured - show zeros
16467
- this._statValues.set(initialValues.map(v => ({ ...v, value: 0, isLoading: false })));
16468
- return;
16469
- }
16470
- // Load all aggregation queries
16471
- const results = await this.dataService.fetchAggregations(dataSource.queries);
16472
- // Map results to stat values
16473
- const updatedValues = this.config.stats.map(stat => {
16474
- const result = results.get(stat.queryId);
16475
- return {
16476
- label: stat.label,
16477
- value: result ?? null,
16478
- color: this.getColorClass(stat.color),
16479
- prefix: stat.prefix,
16480
- suffix: stat.suffix,
16481
- isLoading: false
16482
- };
16483
- });
16948
+ const aggregationQueries = dataSource?.type === 'aggregation' ? (dataSource.queries ?? []) : [];
16949
+ const aggregationResults = aggregationQueries.length > 0
16950
+ ? await this.dataService.fetchAggregations(aggregationQueries)
16951
+ : new Map();
16952
+ // Resolve every stat's value (persistent-query stats execute in parallel).
16953
+ const updatedValues = await Promise.all(this.config.stats.map(async (stat) => ({
16954
+ label: stat.label,
16955
+ value: await this.resolveStatValue(stat, aggregationResults),
16956
+ color: this.getColorClass(stat.color),
16957
+ prefix: stat.prefix,
16958
+ suffix: stat.suffix,
16959
+ isLoading: false
16960
+ })));
16484
16961
  this._statValues.set(updatedValues);
16485
16962
  }
16486
16963
  catch (err) {
@@ -16492,6 +16969,41 @@ class StatsGridWidgetComponent {
16492
16969
  this._isLoading.set(false);
16493
16970
  }
16494
16971
  }
16972
+ /**
16973
+ * Resolves a single stat's value: a per-stat persistent query (runtime or
16974
+ * stream-data) when configured, otherwise the runtime aggregation result.
16975
+ */
16976
+ async resolveStatValue(stat, aggregationResults) {
16977
+ if (stat.persistentQuerySource) {
16978
+ return this.loadPersistentQueryValue(stat.persistentQuerySource);
16979
+ }
16980
+ return aggregationResults.get(stat.queryId) ?? null;
16981
+ }
16982
+ async loadPersistentQueryValue(source) {
16983
+ try {
16984
+ const result = await firstValueFrom(this.queryExecutor.execute(source.queryFamily, source.queryRtId, {
16985
+ fieldFilter: this.convertFiltersToDto(source.filters),
16986
+ streamDataArgs: this.buildStreamDataArgs(source)
16987
+ }));
16988
+ return extractPersistentQueryCellValue(result, source);
16989
+ }
16990
+ catch (err) {
16991
+ console.error('Error loading stat persistent-query data:', err);
16992
+ return null;
16993
+ }
16994
+ }
16995
+ buildStreamDataArgs(source) {
16996
+ const timeArgs = this.stateService.resolveStreamDataTimeArgs(source.ignoreTimeFilter);
16997
+ const rtIds = this.stateService.resolveStreamDataRtIds(source.entitySelectorId);
16998
+ if (!timeArgs && !rtIds) {
16999
+ return undefined;
17000
+ }
17001
+ return { ...timeArgs, rtIds };
17002
+ }
17003
+ convertFiltersToDto(filters) {
17004
+ const variables = this.stateService.getVariables();
17005
+ return this.variableService.convertToFieldFilterDto(filters, variables) ?? undefined;
17006
+ }
16495
17007
  getColorClass(color) {
16496
17008
  switch (color) {
16497
17009
  case 'mint': return 'stat-mint';
@@ -16526,11 +17038,259 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
16526
17038
  type: Input
16527
17039
  }] } });
16528
17040
 
17041
+ /**
17042
+ * Reusable editor for a {@link PersistentQueryCellSource} (a single-value
17043
+ * persistent-query data source). Shared by the stats-grid stat editor and the
17044
+ * summary-card tile editor so both surface the identical runtime/stream-data
17045
+ * configuration UX (query picker, SD time-filter opt-out + asset-scope, query
17046
+ * mode and value/category fields). The runtime ↔ stream-data choice is derived
17047
+ * from the picked query's family — the SD-only controls hide for runtime queries.
17048
+ */
17049
+ class PersistentQueryCellEditorComponent {
17050
+ getQueriesGQL = inject(GetSystemPersistentQueriesDtoGQL);
17051
+ initialSource;
17052
+ selectors = [];
17053
+ sourceChange = new EventEmitter();
17054
+ queries = [];
17055
+ selectedQuery = null;
17056
+ isLoading = false;
17057
+ ignoreTimeFilter = false;
17058
+ entitySelectorId;
17059
+ queryMode = 'simpleCount';
17060
+ queryValueField;
17061
+ queryCategoryField;
17062
+ queryCategoryValue;
17063
+ queryModeOptions = [
17064
+ { value: 'simpleCount', label: 'Simple (total count)' },
17065
+ { value: 'aggregation', label: 'Aggregation (single value)' },
17066
+ { value: 'groupedAggregation', label: 'Grouped aggregation' }
17067
+ ];
17068
+ /** Family of the picked query, derived from its own CK type. */
17069
+ get family() {
17070
+ return queryFamily(this.selectedQuery?.ckTypeId) ?? undefined;
17071
+ }
17072
+ async ngOnInit() {
17073
+ const src = this.initialSource;
17074
+ if (src) {
17075
+ this.ignoreTimeFilter = src.ignoreTimeFilter ?? false;
17076
+ this.entitySelectorId = src.entitySelectorId;
17077
+ this.queryMode = src.queryMode ?? 'simpleCount';
17078
+ this.queryValueField = src.queryValueField;
17079
+ this.queryCategoryField = src.queryCategoryField;
17080
+ this.queryCategoryValue = src.queryCategoryValue;
17081
+ }
17082
+ await this.loadQueries();
17083
+ if (src?.queryRtId) {
17084
+ this.selectedQuery = this.queries.find(q => q.rtId === src.queryRtId)
17085
+ ?? { rtId: src.queryRtId, name: src.queryName ?? src.queryRtId, ckTypeId: undefined, queryCkTypeId: undefined };
17086
+ }
17087
+ }
17088
+ async onFilter(text) {
17089
+ await this.loadQueries(text);
17090
+ }
17091
+ onQuerySelected(query) {
17092
+ this.selectedQuery = query;
17093
+ // Reset SD-only state when the picked query is not a stream-data query.
17094
+ if (this.family !== 'streamData') {
17095
+ this.ignoreTimeFilter = false;
17096
+ this.entitySelectorId = undefined;
17097
+ }
17098
+ this.emit();
17099
+ }
17100
+ emit() {
17101
+ if (!this.selectedQuery) {
17102
+ // Emit an empty-rtId source so the parent can flag the cell invalid.
17103
+ this.sourceChange.emit({ queryRtId: '', queryMode: this.queryMode });
17104
+ return;
17105
+ }
17106
+ const family = this.family;
17107
+ const source = {
17108
+ queryRtId: this.selectedQuery.rtId,
17109
+ queryName: this.selectedQuery.name,
17110
+ queryFamily: family,
17111
+ queryMode: this.queryMode,
17112
+ queryValueField: this.queryValueField || undefined,
17113
+ queryCategoryField: this.queryCategoryField || undefined,
17114
+ queryCategoryValue: this.queryCategoryValue || undefined,
17115
+ filters: this.initialSource?.filters
17116
+ };
17117
+ if (family === 'streamData') {
17118
+ if (this.ignoreTimeFilter)
17119
+ source.ignoreTimeFilter = true;
17120
+ if (this.entitySelectorId)
17121
+ source.entitySelectorId = this.entitySelectorId;
17122
+ }
17123
+ this.sourceChange.emit(source);
17124
+ }
17125
+ async loadQueries(searchText) {
17126
+ this.isLoading = true;
17127
+ try {
17128
+ const result = await firstValueFrom(this.getQueriesGQL.fetch({
17129
+ first: 100,
17130
+ searchFilter: searchText ? { searchTerm: searchText, language: 'de' } : undefined
17131
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Apollo fetch requires flexible variable typing
17132
+ }));
17133
+ this.queries = (result.data?.runtime?.systemPersistentQuery?.items ?? [])
17134
+ .filter((item) => item !== null)
17135
+ .map(item => ({
17136
+ rtId: item.rtId,
17137
+ name: item.name,
17138
+ description: item.description,
17139
+ ckTypeId: item.ckTypeId,
17140
+ queryCkTypeId: item.queryCkTypeId
17141
+ }));
17142
+ }
17143
+ finally {
17144
+ this.isLoading = false;
17145
+ }
17146
+ }
17147
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: PersistentQueryCellEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
17148
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: PersistentQueryCellEditorComponent, isStandalone: true, selector: "mm-persistent-query-cell-editor", inputs: { initialSource: "initialSource", selectors: "selectors" }, outputs: { sourceChange: "sourceChange" }, ngImport: i0, template: `
17149
+ <div class="pq-editor">
17150
+ <div class="pq-field">
17151
+ <label>Persistent Query <span class="required">*</span></label>
17152
+ <kendo-combobox
17153
+ [data]="queries"
17154
+ [textField]="'name'"
17155
+ [valueField]="'rtId'"
17156
+ [valuePrimitive]="false"
17157
+ [(ngModel)]="selectedQuery"
17158
+ [filterable]="true"
17159
+ (filterChange)="onFilter($event)"
17160
+ (valueChange)="onQuerySelected($event)"
17161
+ [loading]="isLoading"
17162
+ placeholder="Select a query...">
17163
+ </kendo-combobox>
17164
+ </div>
17165
+
17166
+ <!-- Stream-data-only options (hidden for runtime queries) -->
17167
+ <mm-sd-time-filter-toggle
17168
+ [family]="family"
17169
+ [(ignoreTimeFilter)]="ignoreTimeFilter"
17170
+ (ignoreTimeFilterChange)="emit()">
17171
+ </mm-sd-time-filter-toggle>
17172
+ <mm-entity-selector-scope-picker
17173
+ [family]="family"
17174
+ [selectors]="selectors"
17175
+ [(entitySelectorId)]="entitySelectorId"
17176
+ (entitySelectorIdChange)="emit()">
17177
+ </mm-entity-selector-scope-picker>
17178
+
17179
+ <div class="pq-field">
17180
+ <label>Query Mode</label>
17181
+ <kendo-dropdownlist
17182
+ [data]="queryModeOptions"
17183
+ textField="label"
17184
+ valueField="value"
17185
+ [valuePrimitive]="true"
17186
+ [(ngModel)]="queryMode"
17187
+ (valueChange)="emit()">
17188
+ </kendo-dropdownlist>
17189
+ </div>
17190
+
17191
+ @if (queryMode === 'aggregation' || queryMode === 'groupedAggregation') {
17192
+ <div class="pq-field">
17193
+ <label>Value Field</label>
17194
+ <kendo-textbox [(ngModel)]="queryValueField" (valueChange)="emit()" placeholder="e.g. amountvalue_sum"></kendo-textbox>
17195
+ </div>
17196
+ }
17197
+
17198
+ @if (queryMode === 'groupedAggregation') {
17199
+ <div class="pq-field">
17200
+ <label>Category Field</label>
17201
+ <kendo-textbox [(ngModel)]="queryCategoryField" (valueChange)="emit()" placeholder="e.g. operatingstatus"></kendo-textbox>
17202
+ </div>
17203
+ <div class="pq-field">
17204
+ <label>Category Value</label>
17205
+ <kendo-textbox [(ngModel)]="queryCategoryValue" (valueChange)="emit()" placeholder="value to match"></kendo-textbox>
17206
+ </div>
17207
+ }
17208
+ </div>
17209
+ `, isInline: true, styles: [".pq-editor{display:flex;flex-direction:column;gap:8px}.pq-field{display:flex;flex-direction:column;gap:4px}.pq-field label{font-weight:600;font-size:.85rem}.required{color:var(--kendo-color-error, #dc3545)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: InputsModule }, { kind: "component", type: i3.TextBoxComponent, selector: "kendo-textbox", inputs: ["focusableId", "title", "type", "disabled", "readonly", "tabindex", "value", "selectOnFocus", "showSuccessIcon", "showErrorIcon", "clearButton", "successIcon", "successSvgIcon", "errorIcon", "errorSvgIcon", "clearButtonIcon", "clearButtonSvgIcon", "size", "rounded", "fillMode", "tabIndex", "placeholder", "maxlength", "inputAttributes"], outputs: ["valueChange", "inputFocus", "inputBlur", "focus", "blur"], exportAs: ["kendoTextBox"] }, { kind: "ngmodule", type: DropDownsModule }, { kind: "component", type: i4.ComboBoxComponent, selector: "kendo-combobox", inputs: ["icon", "svgIcon", "inputAttributes", "showStickyHeader", "focusableId", "allowCustom", "data", "value", "textField", "valueField", "valuePrimitive", "valueNormalizer", "placeholder", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "loading", "suggest", "clearButton", "disabled", "itemDisabled", "readonly", "tabindex", "tabIndex", "filterable", "virtual", "size", "rounded", "fillMode"], outputs: ["valueChange", "selectionChange", "filterChange", "open", "opened", "close", "closed", "focus", "blur", "inputFocus", "inputBlur", "escape"], exportAs: ["kendoComboBox"] }, { kind: "component", type: i4.DropDownListComponent, selector: "kendo-dropdownlist", inputs: ["customIconClass", "showStickyHeader", "icon", "svgIcon", "loading", "data", "value", "textField", "valueField", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "defaultItem", "disabled", "itemDisabled", "readonly", "filterable", "virtual", "ignoreCase", "delay", "valuePrimitive", "tabindex", "tabIndex", "size", "rounded", "fillMode", "leftRightArrowsNavigation", "id"], outputs: ["valueChange", "filterChange", "selectionChange", "open", "opened", "close", "closed", "focus", "blur"], exportAs: ["kendoDropDownList"] }, { kind: "component", type: SdTimeFilterToggleComponent, selector: "mm-sd-time-filter-toggle", inputs: ["family", "ignoreTimeFilter"], outputs: ["ignoreTimeFilterChange"] }, { kind: "component", type: EntitySelectorScopePickerComponent, selector: "mm-entity-selector-scope-picker", inputs: ["family", "entitySelectorId", "selectors"], outputs: ["entitySelectorIdChange"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
17210
+ }
17211
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: PersistentQueryCellEditorComponent, decorators: [{
17212
+ type: Component,
17213
+ args: [{ selector: 'mm-persistent-query-cell-editor', standalone: true, imports: [CommonModule, FormsModule, InputsModule, DropDownsModule, SdTimeFilterToggleComponent, EntitySelectorScopePickerComponent], changeDetection: ChangeDetectionStrategy.Eager, template: `
17214
+ <div class="pq-editor">
17215
+ <div class="pq-field">
17216
+ <label>Persistent Query <span class="required">*</span></label>
17217
+ <kendo-combobox
17218
+ [data]="queries"
17219
+ [textField]="'name'"
17220
+ [valueField]="'rtId'"
17221
+ [valuePrimitive]="false"
17222
+ [(ngModel)]="selectedQuery"
17223
+ [filterable]="true"
17224
+ (filterChange)="onFilter($event)"
17225
+ (valueChange)="onQuerySelected($event)"
17226
+ [loading]="isLoading"
17227
+ placeholder="Select a query...">
17228
+ </kendo-combobox>
17229
+ </div>
17230
+
17231
+ <!-- Stream-data-only options (hidden for runtime queries) -->
17232
+ <mm-sd-time-filter-toggle
17233
+ [family]="family"
17234
+ [(ignoreTimeFilter)]="ignoreTimeFilter"
17235
+ (ignoreTimeFilterChange)="emit()">
17236
+ </mm-sd-time-filter-toggle>
17237
+ <mm-entity-selector-scope-picker
17238
+ [family]="family"
17239
+ [selectors]="selectors"
17240
+ [(entitySelectorId)]="entitySelectorId"
17241
+ (entitySelectorIdChange)="emit()">
17242
+ </mm-entity-selector-scope-picker>
17243
+
17244
+ <div class="pq-field">
17245
+ <label>Query Mode</label>
17246
+ <kendo-dropdownlist
17247
+ [data]="queryModeOptions"
17248
+ textField="label"
17249
+ valueField="value"
17250
+ [valuePrimitive]="true"
17251
+ [(ngModel)]="queryMode"
17252
+ (valueChange)="emit()">
17253
+ </kendo-dropdownlist>
17254
+ </div>
17255
+
17256
+ @if (queryMode === 'aggregation' || queryMode === 'groupedAggregation') {
17257
+ <div class="pq-field">
17258
+ <label>Value Field</label>
17259
+ <kendo-textbox [(ngModel)]="queryValueField" (valueChange)="emit()" placeholder="e.g. amountvalue_sum"></kendo-textbox>
17260
+ </div>
17261
+ }
17262
+
17263
+ @if (queryMode === 'groupedAggregation') {
17264
+ <div class="pq-field">
17265
+ <label>Category Field</label>
17266
+ <kendo-textbox [(ngModel)]="queryCategoryField" (valueChange)="emit()" placeholder="e.g. operatingstatus"></kendo-textbox>
17267
+ </div>
17268
+ <div class="pq-field">
17269
+ <label>Category Value</label>
17270
+ <kendo-textbox [(ngModel)]="queryCategoryValue" (valueChange)="emit()" placeholder="value to match"></kendo-textbox>
17271
+ </div>
17272
+ }
17273
+ </div>
17274
+ `, styles: [".pq-editor{display:flex;flex-direction:column;gap:8px}.pq-field{display:flex;flex-direction:column;gap:4px}.pq-field label{font-weight:600;font-size:.85rem}.required{color:var(--kendo-color-error, #dc3545)}\n"] }]
17275
+ }], propDecorators: { initialSource: [{
17276
+ type: Input
17277
+ }], selectors: [{
17278
+ type: Input
17279
+ }], sourceChange: [{
17280
+ type: Output
17281
+ }] } });
17282
+
16529
17283
  class StatsGridConfigDialogComponent {
16530
17284
  windowRef = inject(WindowRef);
17285
+ stateService = inject(MeshBoardStateService);
16531
17286
  initialStats;
16532
17287
  initialQueries;
16533
17288
  initialColumns;
17289
+ entitySelectors = [];
17290
+ sourceTypeOptions = [
17291
+ { value: 'aggregation', label: 'Runtime aggregation' },
17292
+ { value: 'persistentQuery', label: 'Persistent query' }
17293
+ ];
16534
17294
  colorOptions = [
16535
17295
  { value: 'mint', label: 'Mint', color: '#98d9c2' },
16536
17296
  { value: 'cyan', label: 'Cyan', color: '#5bc0be' },
@@ -16558,20 +17318,29 @@ class StatsGridConfigDialogComponent {
16558
17318
  get isValid() {
16559
17319
  if (this.form.stats.length === 0)
16560
17320
  return false;
16561
- return this.form.stats.every(stat => stat.label?.trim() && stat.ckTypeId?.trim());
17321
+ return this.form.stats.every(stat => {
17322
+ if (!stat.label?.trim())
17323
+ return false;
17324
+ return stat.sourceType === 'persistentQuery'
17325
+ ? !!stat.persistentQuerySource?.queryRtId
17326
+ : !!stat.ckTypeId?.trim();
17327
+ });
16562
17328
  }
16563
17329
  ngOnInit() {
16564
17330
  this.filteredCkTypeOptions = [...this.ckTypeOptions];
16565
17331
  this.form.columns = this.initialColumns ?? 3;
17332
+ this.entitySelectors = this.stateService.getEntitySelectors();
16566
17333
  // Convert initial stats and queries to editable format
16567
- if (this.initialStats && this.initialQueries) {
17334
+ if (this.initialStats) {
16568
17335
  this.form.stats = this.initialStats.map(stat => {
16569
17336
  const query = this.initialQueries?.find(q => q.id === stat.queryId);
16570
17337
  return {
16571
17338
  id: `stat-${this.nextId++}`,
16572
17339
  label: stat.label,
17340
+ sourceType: stat.persistentQuerySource ? 'persistentQuery' : 'aggregation',
16573
17341
  ckTypeId: query?.ckTypeId ?? '',
16574
17342
  aggregation: query?.aggregation ?? 'count',
17343
+ persistentQuerySource: stat.persistentQuerySource,
16575
17344
  color: stat.color ?? 'default',
16576
17345
  prefix: stat.prefix,
16577
17346
  suffix: stat.suffix
@@ -16588,6 +17357,7 @@ class StatsGridConfigDialogComponent {
16588
17357
  this.form.stats.push({
16589
17358
  id: `stat-${this.nextId++}`,
16590
17359
  label: '',
17360
+ sourceType: 'aggregation',
16591
17361
  ckTypeId: 'ConstructionKit/CkType',
16592
17362
  aggregation: 'count',
16593
17363
  color: 'default'
@@ -16602,6 +17372,18 @@ class StatsGridConfigDialogComponent {
16602
17372
  const queries = [];
16603
17373
  for (const stat of this.form.stats) {
16604
17374
  const queryId = `query-${stat.id}`;
17375
+ if (stat.sourceType === 'persistentQuery') {
17376
+ // Persistent-query stat — value comes from the executor, no aggregation query.
17377
+ stats.push({
17378
+ label: stat.label,
17379
+ queryId,
17380
+ persistentQuerySource: stat.persistentQuerySource,
17381
+ color: stat.color,
17382
+ prefix: stat.prefix,
17383
+ suffix: stat.suffix
17384
+ });
17385
+ continue;
17386
+ }
16605
17387
  stats.push({
16606
17388
  label: stat.label,
16607
17389
  queryId,
@@ -16705,28 +17487,53 @@ class StatsGridConfigDialogComponent {
16705
17487
  </div>
16706
17488
 
16707
17489
  <div class="form-row">
16708
- <div class="form-field flex-2">
16709
- <label>CK Type <span class="required">*</span></label>
17490
+ <div class="form-field flex-1">
17491
+ <label>Data Source</label>
16710
17492
  <kendo-dropdownlist
16711
- [data]="ckTypeOptions"
17493
+ [data]="sourceTypeOptions"
16712
17494
  textField="label"
16713
17495
  valueField="value"
16714
17496
  [valuePrimitive]="true"
16715
- [(ngModel)]="stat.ckTypeId"
16716
- [filterable]="true"
16717
- (filterChange)="onCkTypeFilter($event)">
16718
- </kendo-dropdownlist>
16719
- </div>
16720
- <div class="form-field flex-1">
16721
- <label>Aggregation</label>
16722
- <kendo-dropdownlist
16723
- [data]="aggregationOptions"
16724
- [valuePrimitive]="true"
16725
- [(ngModel)]="stat.aggregation">
17497
+ [(ngModel)]="stat.sourceType">
16726
17498
  </kendo-dropdownlist>
16727
17499
  </div>
16728
17500
  </div>
16729
17501
 
17502
+ @if (stat.sourceType !== 'persistentQuery') {
17503
+ <div class="form-row">
17504
+ <div class="form-field flex-2">
17505
+ <label>CK Type <span class="required">*</span></label>
17506
+ <kendo-dropdownlist
17507
+ [data]="ckTypeOptions"
17508
+ textField="label"
17509
+ valueField="value"
17510
+ [valuePrimitive]="true"
17511
+ [(ngModel)]="stat.ckTypeId"
17512
+ [filterable]="true"
17513
+ (filterChange)="onCkTypeFilter($event)">
17514
+ </kendo-dropdownlist>
17515
+ </div>
17516
+ <div class="form-field flex-1">
17517
+ <label>Aggregation</label>
17518
+ <kendo-dropdownlist
17519
+ [data]="aggregationOptions"
17520
+ [valuePrimitive]="true"
17521
+ [(ngModel)]="stat.aggregation">
17522
+ </kendo-dropdownlist>
17523
+ </div>
17524
+ </div>
17525
+ } @else {
17526
+ <div class="form-row">
17527
+ <div class="form-field flex-1">
17528
+ <mm-persistent-query-cell-editor
17529
+ [initialSource]="stat.persistentQuerySource"
17530
+ [selectors]="entitySelectors"
17531
+ (sourceChange)="stat.persistentQuerySource = $event">
17532
+ </mm-persistent-query-cell-editor>
17533
+ </div>
17534
+ </div>
17535
+ }
17536
+
16730
17537
  <div class="form-row">
16731
17538
  <div class="form-field flex-1">
16732
17539
  <label>Prefix</label>
@@ -16760,7 +17567,7 @@ class StatsGridConfigDialogComponent {
16760
17567
  </button>
16761
17568
  </div>
16762
17569
  </div>
16763
- `, isInline: true, styles: [":host{display:block;height:100%}.config-container{display:flex;flex-direction:column;height:100%}.action-bar{display:flex;justify-content:flex-end;gap:8px;padding:8px 16px;border-top:1px solid var(--kendo-color-border, #dee2e6)}.config-form{display:flex;flex-direction:column;gap:20px;padding:16px;flex:1;overflow-y:auto}.form-field{display:flex;flex-direction:column;gap:6px}.form-field.flex-1{flex:1}.form-field.flex-2{flex:2}.form-field label{font-weight:600;font-size:.9rem;color:var(--kendo-color-on-app-surface, #212529)}.field-hint{margin:0;font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.form-section{padding:16px;background:var(--kendo-color-surface-alt, #f8f9fa);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px}.section-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px}.section-header h4{margin:0;font-size:.95rem;color:var(--kendo-color-primary, #0d6efd)}.form-row{display:flex;gap:16px}.required{color:var(--kendo-color-error, #dc3545)}.empty-state{text-align:center;padding:24px;color:var(--kendo-color-subtle, #6c757d);font-style:italic}.stat-item{padding:16px;margin-bottom:12px;background:var(--kendo-color-surface, #ffffff);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px}.stat-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}.stat-number{font-weight:600;color:var(--kendo-color-primary, #0d6efd)}.stat-form{display:flex;flex-direction:column;gap:12px}.color-item{display:flex;align-items:center;gap:8px}.color-swatch{width:16px;height:16px;border-radius:3px;border:1px solid rgba(0,0,0,.1)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ButtonsModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: InputsModule }, { kind: "component", type: i3.TextBoxComponent, selector: "kendo-textbox", inputs: ["focusableId", "title", "type", "disabled", "readonly", "tabindex", "value", "selectOnFocus", "showSuccessIcon", "showErrorIcon", "clearButton", "successIcon", "successSvgIcon", "errorIcon", "errorSvgIcon", "clearButtonIcon", "clearButtonSvgIcon", "size", "rounded", "fillMode", "tabIndex", "placeholder", "maxlength", "inputAttributes"], outputs: ["valueChange", "inputFocus", "inputBlur", "focus", "blur"], exportAs: ["kendoTextBox"] }, { kind: "component", type: i3.NumericTextBoxComponent, selector: "kendo-numerictextbox", inputs: ["focusableId", "disabled", "readonly", "title", "autoCorrect", "format", "max", "min", "decimals", "placeholder", "step", "spinners", "rangeValidation", "tabindex", "tabIndex", "changeValueOnScroll", "selectOnFocus", "value", "maxlength", "size", "rounded", "fillMode", "inputAttributes"], outputs: ["valueChange", "focus", "blur", "inputFocus", "inputBlur"], exportAs: ["kendoNumericTextBox"] }, { kind: "ngmodule", type: DropDownsModule }, { kind: "directive", type: i4.ItemTemplateDirective, selector: "[kendoDropDownListItemTemplate],[kendoComboBoxItemTemplate],[kendoAutoCompleteItemTemplate],[kendoMultiSelectItemTemplate]" }, { kind: "component", type: i4.DropDownListComponent, selector: "kendo-dropdownlist", inputs: ["customIconClass", "showStickyHeader", "icon", "svgIcon", "loading", "data", "value", "textField", "valueField", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "defaultItem", "disabled", "itemDisabled", "readonly", "filterable", "virtual", "ignoreCase", "delay", "valuePrimitive", "tabindex", "tabIndex", "size", "rounded", "fillMode", "leftRightArrowsNavigation", "id"], outputs: ["valueChange", "filterChange", "selectionChange", "open", "opened", "close", "closed", "focus", "blur"], exportAs: ["kendoDropDownList"] }, { kind: "directive", type: i4.ValueTemplateDirective, selector: "[kendoDropDownListValueTemplate],[kendoDropDownTreeValueTemplate]" }], changeDetection: i0.ChangeDetectionStrategy.Eager });
17570
+ `, isInline: true, styles: [":host{display:block;height:100%}.config-container{display:flex;flex-direction:column;height:100%}.action-bar{display:flex;justify-content:flex-end;gap:8px;padding:8px 16px;border-top:1px solid var(--kendo-color-border, #dee2e6)}.config-form{display:flex;flex-direction:column;gap:20px;padding:16px;flex:1;overflow-y:auto}.form-field{display:flex;flex-direction:column;gap:6px}.form-field.flex-1{flex:1}.form-field.flex-2{flex:2}.form-field label{font-weight:600;font-size:.9rem;color:var(--kendo-color-on-app-surface, #212529)}.field-hint{margin:0;font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.form-section{padding:16px;background:var(--kendo-color-surface-alt, #f8f9fa);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px}.section-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px}.section-header h4{margin:0;font-size:.95rem;color:var(--kendo-color-primary, #0d6efd)}.form-row{display:flex;gap:16px}.required{color:var(--kendo-color-error, #dc3545)}.empty-state{text-align:center;padding:24px;color:var(--kendo-color-subtle, #6c757d);font-style:italic}.stat-item{padding:16px;margin-bottom:12px;background:var(--kendo-color-surface, #ffffff);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px}.stat-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}.stat-number{font-weight:600;color:var(--kendo-color-primary, #0d6efd)}.stat-form{display:flex;flex-direction:column;gap:12px}.color-item{display:flex;align-items:center;gap:8px}.color-swatch{width:16px;height:16px;border-radius:3px;border:1px solid rgba(0,0,0,.1)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ButtonsModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: InputsModule }, { kind: "component", type: i3.TextBoxComponent, selector: "kendo-textbox", inputs: ["focusableId", "title", "type", "disabled", "readonly", "tabindex", "value", "selectOnFocus", "showSuccessIcon", "showErrorIcon", "clearButton", "successIcon", "successSvgIcon", "errorIcon", "errorSvgIcon", "clearButtonIcon", "clearButtonSvgIcon", "size", "rounded", "fillMode", "tabIndex", "placeholder", "maxlength", "inputAttributes"], outputs: ["valueChange", "inputFocus", "inputBlur", "focus", "blur"], exportAs: ["kendoTextBox"] }, { kind: "component", type: i3.NumericTextBoxComponent, selector: "kendo-numerictextbox", inputs: ["focusableId", "disabled", "readonly", "title", "autoCorrect", "format", "max", "min", "decimals", "placeholder", "step", "spinners", "rangeValidation", "tabindex", "tabIndex", "changeValueOnScroll", "selectOnFocus", "value", "maxlength", "size", "rounded", "fillMode", "inputAttributes"], outputs: ["valueChange", "focus", "blur", "inputFocus", "inputBlur"], exportAs: ["kendoNumericTextBox"] }, { kind: "ngmodule", type: DropDownsModule }, { kind: "directive", type: i4.ItemTemplateDirective, selector: "[kendoDropDownListItemTemplate],[kendoComboBoxItemTemplate],[kendoAutoCompleteItemTemplate],[kendoMultiSelectItemTemplate]" }, { kind: "component", type: i4.DropDownListComponent, selector: "kendo-dropdownlist", inputs: ["customIconClass", "showStickyHeader", "icon", "svgIcon", "loading", "data", "value", "textField", "valueField", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "defaultItem", "disabled", "itemDisabled", "readonly", "filterable", "virtual", "ignoreCase", "delay", "valuePrimitive", "tabindex", "tabIndex", "size", "rounded", "fillMode", "leftRightArrowsNavigation", "id"], outputs: ["valueChange", "filterChange", "selectionChange", "open", "opened", "close", "closed", "focus", "blur"], exportAs: ["kendoDropDownList"] }, { kind: "directive", type: i4.ValueTemplateDirective, selector: "[kendoDropDownListValueTemplate],[kendoDropDownTreeValueTemplate]" }, { kind: "component", type: PersistentQueryCellEditorComponent, selector: "mm-persistent-query-cell-editor", inputs: ["initialSource", "selectors"], outputs: ["sourceChange"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
16764
17571
  }
16765
17572
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: StatsGridConfigDialogComponent, decorators: [{
16766
17573
  type: Component,
@@ -16769,7 +17576,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
16769
17576
  FormsModule,
16770
17577
  ButtonsModule,
16771
17578
  InputsModule,
16772
- DropDownsModule
17579
+ DropDownsModule,
17580
+ PersistentQueryCellEditorComponent
16773
17581
  ], template: `
16774
17582
  <div class="config-container">
16775
17583
 
@@ -16849,28 +17657,53 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
16849
17657
  </div>
16850
17658
 
16851
17659
  <div class="form-row">
16852
- <div class="form-field flex-2">
16853
- <label>CK Type <span class="required">*</span></label>
17660
+ <div class="form-field flex-1">
17661
+ <label>Data Source</label>
16854
17662
  <kendo-dropdownlist
16855
- [data]="ckTypeOptions"
17663
+ [data]="sourceTypeOptions"
16856
17664
  textField="label"
16857
17665
  valueField="value"
16858
17666
  [valuePrimitive]="true"
16859
- [(ngModel)]="stat.ckTypeId"
16860
- [filterable]="true"
16861
- (filterChange)="onCkTypeFilter($event)">
16862
- </kendo-dropdownlist>
16863
- </div>
16864
- <div class="form-field flex-1">
16865
- <label>Aggregation</label>
16866
- <kendo-dropdownlist
16867
- [data]="aggregationOptions"
16868
- [valuePrimitive]="true"
16869
- [(ngModel)]="stat.aggregation">
17667
+ [(ngModel)]="stat.sourceType">
16870
17668
  </kendo-dropdownlist>
16871
17669
  </div>
16872
17670
  </div>
16873
17671
 
17672
+ @if (stat.sourceType !== 'persistentQuery') {
17673
+ <div class="form-row">
17674
+ <div class="form-field flex-2">
17675
+ <label>CK Type <span class="required">*</span></label>
17676
+ <kendo-dropdownlist
17677
+ [data]="ckTypeOptions"
17678
+ textField="label"
17679
+ valueField="value"
17680
+ [valuePrimitive]="true"
17681
+ [(ngModel)]="stat.ckTypeId"
17682
+ [filterable]="true"
17683
+ (filterChange)="onCkTypeFilter($event)">
17684
+ </kendo-dropdownlist>
17685
+ </div>
17686
+ <div class="form-field flex-1">
17687
+ <label>Aggregation</label>
17688
+ <kendo-dropdownlist
17689
+ [data]="aggregationOptions"
17690
+ [valuePrimitive]="true"
17691
+ [(ngModel)]="stat.aggregation">
17692
+ </kendo-dropdownlist>
17693
+ </div>
17694
+ </div>
17695
+ } @else {
17696
+ <div class="form-row">
17697
+ <div class="form-field flex-1">
17698
+ <mm-persistent-query-cell-editor
17699
+ [initialSource]="stat.persistentQuerySource"
17700
+ [selectors]="entitySelectors"
17701
+ (sourceChange)="stat.persistentQuerySource = $event">
17702
+ </mm-persistent-query-cell-editor>
17703
+ </div>
17704
+ </div>
17705
+ }
17706
+
16874
17707
  <div class="form-row">
16875
17708
  <div class="form-field flex-1">
16876
17709
  <label>Prefix</label>
@@ -19607,6 +20440,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
19607
20440
  class SummaryCardWidgetComponent {
19608
20441
  entityGQL = inject(GetDashboardEntityDtoGQL);
19609
20442
  dataService = inject(DashboardDataService);
20443
+ queryExecutor = inject(QueryExecutorService);
20444
+ stateService = inject(MeshBoardStateService);
20445
+ variableService = inject(MeshBoardVariableService);
19610
20446
  config;
19611
20447
  _isLoading = signal(false, /* @ts-ignore */
19612
20448
  ...(ngDevMode ? [{ debugName: "_isLoading" }] : /* istanbul ignore next */ []));
@@ -19660,6 +20496,9 @@ class SummaryCardWidgetComponent {
19660
20496
  }
19661
20497
  }
19662
20498
  async fetchTileValue(tile, entityCache) {
20499
+ if (tile.persistentQuerySource) {
20500
+ return this.fetchPersistentQueryValue(tile.persistentQuerySource);
20501
+ }
19663
20502
  if (tile.entitySource) {
19664
20503
  const { rtId, ckTypeId, attributePath } = tile.entitySource;
19665
20504
  if (!entityCache.has(rtId)) {
@@ -19681,6 +20520,25 @@ class SummaryCardWidgetComponent {
19681
20520
  }
19682
20521
  return null;
19683
20522
  }
20523
+ async fetchPersistentQueryValue(source) {
20524
+ const result = await firstValueFrom(this.queryExecutor.execute(source.queryFamily, source.queryRtId, {
20525
+ fieldFilter: this.convertFiltersToDto(source.filters),
20526
+ streamDataArgs: this.buildStreamDataArgs(source)
20527
+ }));
20528
+ return extractPersistentQueryCellValue(result, source);
20529
+ }
20530
+ buildStreamDataArgs(source) {
20531
+ const timeArgs = this.stateService.resolveStreamDataTimeArgs(source.ignoreTimeFilter);
20532
+ const rtIds = this.stateService.resolveStreamDataRtIds(source.entitySelectorId);
20533
+ if (!timeArgs && !rtIds) {
20534
+ return undefined;
20535
+ }
20536
+ return { ...timeArgs, rtIds };
20537
+ }
20538
+ convertFiltersToDto(filters) {
20539
+ const variables = this.stateService.getVariables();
20540
+ return this.variableService.convertToFieldFilterDto(filters, variables) ?? undefined;
20541
+ }
19684
20542
  async fetchEntityAttributes(rtId, ckTypeId) {
19685
20543
  const result = await firstValueFrom(this.entityGQL.fetch({
19686
20544
  variables: { rtId, ckTypeId }
@@ -19755,21 +20613,28 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
19755
20613
 
19756
20614
  class SummaryCardConfigDialogComponent {
19757
20615
  windowRef = inject(WindowRef);
20616
+ stateService = inject(MeshBoardStateService);
19758
20617
  initialColumns;
19759
20618
  initialTiles;
20619
+ entitySelectors = [];
19760
20620
  form = {
19761
20621
  columns: 2,
19762
20622
  tiles: []
19763
20623
  };
19764
20624
  colorOptions = ['default', 'primary', 'success', 'warning', 'error'];
19765
20625
  sizeOptions = ['normal', 'full'];
19766
- sourceTypeOptions = ['entity', 'aggregation'];
20626
+ sourceTypeOptions = ['entity', 'aggregation', 'persistentQuery'];
19767
20627
  aggregationOptions = ['count', 'sum', 'avg', 'min', 'max'];
19768
20628
  get isValid() {
19769
- return this.form.tiles.length > 0 && this.form.tiles.every(t => t.label && t.ckTypeId);
20629
+ return this.form.tiles.length > 0 && this.form.tiles.every(t => {
20630
+ if (!t.label)
20631
+ return false;
20632
+ return t.sourceType === 'persistentQuery' ? !!t.persistentQuerySource?.queryRtId : !!t.ckTypeId;
20633
+ });
19770
20634
  }
19771
20635
  ngOnInit() {
19772
20636
  this.form.columns = this.initialColumns ?? 2;
20637
+ this.entitySelectors = this.stateService.getEntitySelectors();
19773
20638
  if (this.initialTiles) {
19774
20639
  this.form.tiles = this.initialTiles.map((t, i) => ({
19775
20640
  id: t.id || `tile-${i}`,
@@ -19778,12 +20643,13 @@ class SummaryCardConfigDialogComponent {
19778
20643
  suffix: t.suffix ?? '',
19779
20644
  color: t.color ?? 'default',
19780
20645
  size: t.size ?? 'normal',
19781
- sourceType: t.entitySource ? 'entity' : 'aggregation',
20646
+ sourceType: t.persistentQuerySource ? 'persistentQuery' : t.entitySource ? 'entity' : 'aggregation',
19782
20647
  rtId: t.entitySource?.rtId ?? '',
19783
20648
  ckTypeId: t.entitySource?.ckTypeId ?? t.aggregationSource?.ckTypeId ?? '',
19784
20649
  attributePath: t.entitySource?.attributePath ?? '',
19785
20650
  aggregation: t.aggregationSource?.aggregation ?? 'count',
19786
- aggAttribute: t.aggregationSource?.attribute ?? ''
20651
+ aggAttribute: t.aggregationSource?.attribute ?? '',
20652
+ persistentQuerySource: t.persistentQuerySource
19787
20653
  }));
19788
20654
  }
19789
20655
  }
@@ -19810,7 +20676,10 @@ class SummaryCardConfigDialogComponent {
19810
20676
  color: t.color || undefined,
19811
20677
  size: t.size || undefined
19812
20678
  };
19813
- if (t.sourceType === 'entity') {
20679
+ if (t.sourceType === 'persistentQuery') {
20680
+ tile.persistentQuerySource = t.persistentQuerySource;
20681
+ }
20682
+ else if (t.sourceType === 'entity') {
19814
20683
  tile.entitySource = { rtId: t.rtId, ckTypeId: t.ckTypeId, attributePath: t.attributePath };
19815
20684
  }
19816
20685
  else {
@@ -19871,6 +20740,16 @@ class SummaryCardConfigDialogComponent {
19871
20740
  <kendo-textbox [(ngModel)]="tile.aggAttribute" placeholder="Attribute (for sum/avg)" style="flex: 1;"></kendo-textbox>
19872
20741
  </div>
19873
20742
  }
20743
+ @if (tile.sourceType === 'persistentQuery') {
20744
+ <div class="tile-row">
20745
+ <mm-persistent-query-cell-editor
20746
+ style="flex: 1;"
20747
+ [initialSource]="tile.persistentQuerySource"
20748
+ [selectors]="entitySelectors"
20749
+ (sourceChange)="tile.persistentQuerySource = $event">
20750
+ </mm-persistent-query-cell-editor>
20751
+ </div>
20752
+ }
19874
20753
  </div>
19875
20754
  }
19876
20755
  <button kendoButton fillMode="flat" (click)="addTile()">+ Add Tile</button>
@@ -19881,11 +20760,11 @@ class SummaryCardConfigDialogComponent {
19881
20760
  <button kendoButton themeColor="primary" [disabled]="!isValid" (click)="onSave()">Save</button>
19882
20761
  </div>
19883
20762
  </div>
19884
- `, isInline: true, styles: [":host{display:block;height:100%}.config-container{display:flex;flex-direction:column;height:100%}.config-form{flex:1;overflow-y:auto;padding:16px;display:flex;flex-direction:column;gap:12px}.action-bar{display:flex;justify-content:flex-end;gap:8px;padding:8px 16px;border-top:1px solid var(--kendo-color-border, #dee2e6)}.form-field{display:flex;flex-direction:column;gap:6px}.form-field label{font-weight:600;font-size:.9rem}h4{margin:8px 0 4px;font-size:.95rem}.tile-config{border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px;padding:10px;margin-bottom:8px;display:flex;flex-direction:column;gap:6px}.tile-header{display:flex;justify-content:space-between;align-items:center}.tile-row{display:flex;gap:6px;align-items:center}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ButtonsModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: InputsModule }, { kind: "component", type: i3.TextBoxComponent, selector: "kendo-textbox", inputs: ["focusableId", "title", "type", "disabled", "readonly", "tabindex", "value", "selectOnFocus", "showSuccessIcon", "showErrorIcon", "clearButton", "successIcon", "successSvgIcon", "errorIcon", "errorSvgIcon", "clearButtonIcon", "clearButtonSvgIcon", "size", "rounded", "fillMode", "tabIndex", "placeholder", "maxlength", "inputAttributes"], outputs: ["valueChange", "inputFocus", "inputBlur", "focus", "blur"], exportAs: ["kendoTextBox"] }, { kind: "component", type: i3.NumericTextBoxComponent, selector: "kendo-numerictextbox", inputs: ["focusableId", "disabled", "readonly", "title", "autoCorrect", "format", "max", "min", "decimals", "placeholder", "step", "spinners", "rangeValidation", "tabindex", "tabIndex", "changeValueOnScroll", "selectOnFocus", "value", "maxlength", "size", "rounded", "fillMode", "inputAttributes"], outputs: ["valueChange", "focus", "blur", "inputFocus", "inputBlur"], exportAs: ["kendoNumericTextBox"] }, { kind: "ngmodule", type: DropDownsModule }, { kind: "component", type: i4.DropDownListComponent, selector: "kendo-dropdownlist", inputs: ["customIconClass", "showStickyHeader", "icon", "svgIcon", "loading", "data", "value", "textField", "valueField", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "defaultItem", "disabled", "itemDisabled", "readonly", "filterable", "virtual", "ignoreCase", "delay", "valuePrimitive", "tabindex", "tabIndex", "size", "rounded", "fillMode", "leftRightArrowsNavigation", "id"], outputs: ["valueChange", "filterChange", "selectionChange", "open", "opened", "close", "closed", "focus", "blur"], exportAs: ["kendoDropDownList"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
20763
+ `, isInline: true, styles: [":host{display:block;height:100%}.config-container{display:flex;flex-direction:column;height:100%}.config-form{flex:1;overflow-y:auto;padding:16px;display:flex;flex-direction:column;gap:12px}.action-bar{display:flex;justify-content:flex-end;gap:8px;padding:8px 16px;border-top:1px solid var(--kendo-color-border, #dee2e6)}.form-field{display:flex;flex-direction:column;gap:6px}.form-field label{font-weight:600;font-size:.9rem}h4{margin:8px 0 4px;font-size:.95rem}.tile-config{border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px;padding:10px;margin-bottom:8px;display:flex;flex-direction:column;gap:6px}.tile-header{display:flex;justify-content:space-between;align-items:center}.tile-row{display:flex;gap:6px;align-items:center}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ButtonsModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: InputsModule }, { kind: "component", type: i3.TextBoxComponent, selector: "kendo-textbox", inputs: ["focusableId", "title", "type", "disabled", "readonly", "tabindex", "value", "selectOnFocus", "showSuccessIcon", "showErrorIcon", "clearButton", "successIcon", "successSvgIcon", "errorIcon", "errorSvgIcon", "clearButtonIcon", "clearButtonSvgIcon", "size", "rounded", "fillMode", "tabIndex", "placeholder", "maxlength", "inputAttributes"], outputs: ["valueChange", "inputFocus", "inputBlur", "focus", "blur"], exportAs: ["kendoTextBox"] }, { kind: "component", type: i3.NumericTextBoxComponent, selector: "kendo-numerictextbox", inputs: ["focusableId", "disabled", "readonly", "title", "autoCorrect", "format", "max", "min", "decimals", "placeholder", "step", "spinners", "rangeValidation", "tabindex", "tabIndex", "changeValueOnScroll", "selectOnFocus", "value", "maxlength", "size", "rounded", "fillMode", "inputAttributes"], outputs: ["valueChange", "focus", "blur", "inputFocus", "inputBlur"], exportAs: ["kendoNumericTextBox"] }, { kind: "ngmodule", type: DropDownsModule }, { kind: "component", type: i4.DropDownListComponent, selector: "kendo-dropdownlist", inputs: ["customIconClass", "showStickyHeader", "icon", "svgIcon", "loading", "data", "value", "textField", "valueField", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "defaultItem", "disabled", "itemDisabled", "readonly", "filterable", "virtual", "ignoreCase", "delay", "valuePrimitive", "tabindex", "tabIndex", "size", "rounded", "fillMode", "leftRightArrowsNavigation", "id"], outputs: ["valueChange", "filterChange", "selectionChange", "open", "opened", "close", "closed", "focus", "blur"], exportAs: ["kendoDropDownList"] }, { kind: "component", type: PersistentQueryCellEditorComponent, selector: "mm-persistent-query-cell-editor", inputs: ["initialSource", "selectors"], outputs: ["sourceChange"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
19885
20764
  }
19886
20765
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: SummaryCardConfigDialogComponent, decorators: [{
19887
20766
  type: Component,
19888
- args: [{ selector: 'mm-summary-card-config-dialog', standalone: true, imports: [CommonModule, FormsModule, ButtonsModule, InputsModule, DropDownsModule], template: `
20767
+ args: [{ selector: 'mm-summary-card-config-dialog', standalone: true, imports: [CommonModule, FormsModule, ButtonsModule, InputsModule, DropDownsModule, PersistentQueryCellEditorComponent], template: `
19889
20768
  <div class="config-container">
19890
20769
  <div class="config-form">
19891
20770
  <div class="form-field">
@@ -19924,6 +20803,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
19924
20803
  <kendo-textbox [(ngModel)]="tile.aggAttribute" placeholder="Attribute (for sum/avg)" style="flex: 1;"></kendo-textbox>
19925
20804
  </div>
19926
20805
  }
20806
+ @if (tile.sourceType === 'persistentQuery') {
20807
+ <div class="tile-row">
20808
+ <mm-persistent-query-cell-editor
20809
+ style="flex: 1;"
20810
+ [initialSource]="tile.persistentQuerySource"
20811
+ [selectors]="entitySelectors"
20812
+ (sourceChange)="tile.persistentQuerySource = $event">
20813
+ </mm-persistent-query-cell-editor>
20814
+ </div>
20815
+ }
19927
20816
  </div>
19928
20817
  }
19929
20818
  <button kendoButton fillMode="flat" (click)="addTile()">+ Add Tile</button>
@@ -20219,6 +21108,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
20219
21108
 
20220
21109
  class AlertListWidgetComponent {
20221
21110
  getEntitiesByCkTypeGQL = inject(GetEntitiesByCkTypeDtoGQL);
21111
+ stateService = inject(MeshBoardStateService);
20222
21112
  config;
20223
21113
  _isLoading = signal(false, /* @ts-ignore */
20224
21114
  ...(ngDevMode ? [{ debugName: "_isLoading" }] : /* istanbul ignore next */ []));
@@ -20251,13 +21141,9 @@ class AlertListWidgetComponent {
20251
21141
  return getAlertSeverityIcon(level);
20252
21142
  }
20253
21143
  formatTime(timestamp) {
20254
- try {
20255
- const date = new Date(timestamp);
20256
- return date.toLocaleString('de-AT', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' });
20257
- }
20258
- catch {
20259
- return '';
20260
- }
21144
+ return formatInstant(timestamp, this.stateService.timeZoneMode(), {
21145
+ day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit'
21146
+ }) ?? '';
20261
21147
  }
20262
21148
  async loadData() {
20263
21149
  this._isLoading.set(true);
@@ -20831,7 +21717,7 @@ class ProcessDataService {
20831
21717
  getProcessDiagramsGQL = inject(GetProcessDiagramsDtoGQL);
20832
21718
  createProcessDiagramGQL = inject(CreateProcessDiagramDtoGQL);
20833
21719
  updateProcessDiagramGQL = inject(UpdateProcessDiagramDtoGQL);
20834
- executeRuntimeQueryGQL = inject(ExecuteRuntimeQueryDtoGQL);
21720
+ queryExecutor = inject(QueryExecutorService);
20835
21721
  /**
20836
21722
  * Loads a list of available process diagrams from the backend
20837
21723
  *
@@ -21145,10 +22031,20 @@ class ProcessDataService {
21145
22031
  if (!queryRtId) {
21146
22032
  throw new Error('Persistent query binding requires queryRtId');
21147
22033
  }
21148
- // TODO: Implement persistent query execution
21149
- // For now, return undefined
21150
- console.warn('Persistent query data source not yet implemented');
21151
- return undefined;
22034
+ // Execute via the unified executor (family resolved lazily). The active time
22035
+ // filter is applied by default for stream-data queries; runtime queries
22036
+ // ignore it. The element binding has no asset-scope, so no rtIds override.
22037
+ const result = await firstValueFrom(this.queryExecutor.execute(undefined, queryRtId, {
22038
+ first: 1,
22039
+ streamDataArgs: this.stateService.resolveStreamDataTimeArgs()
22040
+ }));
22041
+ // Extract the bound column from the first row.
22042
+ const firstRow = result.rows[0];
22043
+ if (!firstRow) {
22044
+ return undefined;
22045
+ }
22046
+ const cell = firstRow.cells.find(c => c.attributePath === binding.attributePath);
22047
+ return cell?.value;
21152
22048
  }
21153
22049
  /**
21154
22050
  * Resolves variable references in a string
@@ -21327,60 +22223,67 @@ class ProcessDataService {
21327
22223
  * Loads data from a persistent query binding
21328
22224
  */
21329
22225
  async loadBoundQueryData(config) {
21330
- const { bindingQueryRtId, bindingFilters } = config;
22226
+ const { bindingQueryRtId, bindingFilters, bindingQueryFamily } = config;
21331
22227
  if (!bindingQueryRtId) {
21332
22228
  throw new Error('Persistent query binding requires a Query rtId');
21333
22229
  }
21334
22230
  // Convert widget filters to GraphQL format with variable resolution
21335
22231
  const fieldFilters = this.convertFiltersToDto(bindingFilters);
21336
- // Execute the query
21337
- const result = await firstValueFrom(this.executeRuntimeQueryGQL.fetch({
21338
- variables: {
21339
- rtId: bindingQueryRtId,
21340
- first: 1000, // Reasonable limit for process diagram data
21341
- fieldFilter: fieldFilters
21342
- }
22232
+ // Execute via the unified executor — it routes runtime vs stream-data from
22233
+ // the persisted family (or a one-time lookup when absent). Stream-data
22234
+ // queries additionally consume the time-filter / asset-scope overrides built
22235
+ // below; runtime queries ignore `streamDataArgs`.
22236
+ const result = await firstValueFrom(this.queryExecutor.execute(bindingQueryFamily, bindingQueryRtId, {
22237
+ first: 1000, // Reasonable limit for process diagram data
22238
+ fieldFilter: fieldFilters,
22239
+ streamDataArgs: this.buildBindingStreamDataArgs(config)
21343
22240
  }));
21344
- // Parse the query result
21345
- const queryData = result.data?.runtime?.runtimeQuery?.items?.[0];
21346
- if (!queryData) {
21347
- throw new Error(`Query not found or returned no data: ${bindingQueryRtId}`);
21348
- }
21349
- // Map the result to our format
21350
- const queryResult = this.mapQueryResult(queryData);
21351
22241
  return {
21352
22242
  type: 'query',
21353
- queryResult,
22243
+ queryResult: this.mapExecutionResult(result),
21354
22244
  loadedAt: new Date()
21355
22245
  };
21356
22246
  }
21357
22247
  /**
21358
- * Maps GraphQL query result to QueryResultData format
22248
+ * Builds the stream-data overrides (time range + asset scope) for the bound
22249
+ * persistent query from the active MeshBoard time filter and entity selector.
22250
+ * Returns `undefined` when neither override applies (e.g. runtime query, no
22251
+ * time filter, opted out). Runtime queries ignore the result.
21359
22252
  */
21360
- mapQueryResult(queryData) {
21361
- const columns = queryData.columns.map(col => ({
22253
+ buildBindingStreamDataArgs(config) {
22254
+ const timeArgs = this.stateService.resolveStreamDataTimeArgs(config.bindingIgnoreTimeFilter);
22255
+ const rtIds = this.stateService.resolveStreamDataRtIds(config.bindingEntitySelectorId);
22256
+ if (!timeArgs && !rtIds) {
22257
+ return undefined;
22258
+ }
22259
+ return { ...timeArgs, rtIds };
22260
+ }
22261
+ /**
22262
+ * Maps a unified {@link QueryExecutionResult} (runtime or stream-data) to the
22263
+ * Process Widget's {@link QueryResultData} shape used by column mappings.
22264
+ */
22265
+ mapExecutionResult(result) {
22266
+ const columns = result.columns.map(col => ({
21362
22267
  attributePath: col.attributePath ?? '',
21363
22268
  attributeValueType: col.attributeValueType ?? undefined
21364
22269
  }));
21365
- const rows = (queryData.rows?.items ?? []).map(row => {
22270
+ const rows = result.rows.map(row => {
21366
22271
  const cells = new Map();
21367
- for (const cell of row?.cells?.items ?? []) {
21368
- if (cell?.attributePath) {
22272
+ for (const cell of row.cells) {
22273
+ if (cell.attributePath) {
21369
22274
  cells.set(cell.attributePath, cell.value);
21370
22275
  }
21371
22276
  }
21372
- // Handle different row types
21373
- const typedRow = row;
21374
22277
  return {
21375
- rtId: typedRow.rtId ?? undefined,
21376
- ckTypeId: typedRow.ckTypeId ?? undefined,
22278
+ rtId: row.rtId ?? undefined,
22279
+ ckTypeId: row.ckTypeId ?? undefined,
21377
22280
  cells
21378
22281
  };
21379
22282
  });
21380
22283
  return {
21381
22284
  columns,
21382
22285
  rows,
21383
- totalCount: queryData.rows?.totalCount ?? 0
22286
+ totalCount: result.totalCount
21384
22287
  };
21385
22288
  }
21386
22289
  /**
@@ -25121,6 +26024,9 @@ class ProcessConfigDialogComponent {
25121
26024
  initialBindingRtId;
25122
26025
  initialBindingQueryRtId;
25123
26026
  initialBindingQueryName;
26027
+ initialBindingQueryFamily;
26028
+ initialBindingIgnoreTimeFilter;
26029
+ initialBindingEntitySelectorId;
25124
26030
  initialBindingFilters;
25125
26031
  initialPropertyMappings;
25126
26032
  // State - Diagram
@@ -25137,6 +26043,14 @@ class ProcessConfigDialogComponent {
25137
26043
  // State - Persistent Query Binding
25138
26044
  persistentQueries = [];
25139
26045
  selectedPersistentQuery = null;
26046
+ // Stream-data binding state (only relevant when the picked query is a stream-data query)
26047
+ bindingIgnoreTimeFilter = false;
26048
+ bindingEntitySelectorId;
26049
+ entitySelectors = [];
26050
+ /** Family of the picked persistent query, derived from its own CK type. */
26051
+ get selectedQueryFamily() {
26052
+ return queryFamily(this.selectedPersistentQuery?.ckTypeId) ?? undefined;
26053
+ }
25140
26054
  isLoadingQueries = false;
25141
26055
  // State - Filters
25142
26056
  filters = [];
@@ -25196,6 +26110,10 @@ class ProcessConfigDialogComponent {
25196
26110
  this.form.initialZoom = this.initialInitialZoom ?? 1;
25197
26111
  // Initialize data binding mode
25198
26112
  this.dataBindingMode = this.initialDataBindingMode ?? 'none';
26113
+ // Initialize stream-data binding state + available entity selectors (for scope picker)
26114
+ this.bindingIgnoreTimeFilter = this.initialBindingIgnoreTimeFilter ?? false;
26115
+ this.bindingEntitySelectorId = this.initialBindingEntitySelectorId;
26116
+ this.entitySelectors = this.stateService.getEntitySelectors();
25199
26117
  // Initialize filter variables from MeshBoard
25200
26118
  this.initFilterVariables();
25201
26119
  // Load data in parallel
@@ -25425,6 +26343,12 @@ class ProcessConfigDialogComponent {
25425
26343
  }
25426
26344
  onQuerySelected(query) {
25427
26345
  this.selectedPersistentQuery = query;
26346
+ // Reset stream-data-only binding state when the picked query is not a
26347
+ // stream-data query (the toggle/scope picker are hidden in that case).
26348
+ if (this.selectedQueryFamily !== 'streamData') {
26349
+ this.bindingIgnoreTimeFilter = false;
26350
+ this.bindingEntitySelectorId = undefined;
26351
+ }
25428
26352
  // Load attributes if query has associated CK type
25429
26353
  if (query?.queryCkTypeId) {
25430
26354
  this.loadAttributesForCkType(query.queryCkTypeId);
@@ -25447,6 +26371,7 @@ class ProcessConfigDialogComponent {
25447
26371
  rtId: item.rtId,
25448
26372
  name: item.name,
25449
26373
  description: item.description,
26374
+ ckTypeId: item.ckTypeId,
25450
26375
  queryCkTypeId: item.queryCkTypeId
25451
26376
  }));
25452
26377
  }
@@ -25518,6 +26443,17 @@ class ProcessConfigDialogComponent {
25518
26443
  if (this.dataBindingMode === 'persistentQuery' && this.selectedPersistentQuery) {
25519
26444
  result.bindingQueryRtId = this.selectedPersistentQuery.rtId;
25520
26445
  result.bindingQueryName = this.selectedPersistentQuery.name;
26446
+ const family = this.selectedQueryFamily;
26447
+ result.bindingQueryFamily = family;
26448
+ // Stream-data-only overrides — persisted only when they apply.
26449
+ if (family === 'streamData') {
26450
+ if (this.bindingIgnoreTimeFilter) {
26451
+ result.bindingIgnoreTimeFilter = true;
26452
+ }
26453
+ if (this.bindingEntitySelectorId) {
26454
+ result.bindingEntitySelectorId = this.bindingEntitySelectorId;
26455
+ }
26456
+ }
25521
26457
  }
25522
26458
  // Filters
25523
26459
  if (this.filters.length > 0 && this.dataBindingMode !== 'none') {
@@ -25540,7 +26476,7 @@ class ProcessConfigDialogComponent {
25540
26476
  this.windowRef.close();
25541
26477
  }
25542
26478
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ProcessConfigDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
25543
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: ProcessConfigDialogComponent, isStandalone: true, selector: "mm-process-config-dialog", inputs: { initialProcessDiagramRtId: "initialProcessDiagramRtId", initialFitToBounds: "initialFitToBounds", initialAllowZoom: "initialAllowZoom", initialAllowPan: "initialAllowPan", initialShowToolbar: "initialShowToolbar", initialInitialZoom: "initialInitialZoom", initialDataBindingMode: "initialDataBindingMode", initialBindingCkTypeId: "initialBindingCkTypeId", initialBindingRtId: "initialBindingRtId", initialBindingQueryRtId: "initialBindingQueryRtId", initialBindingQueryName: "initialBindingQueryName", initialBindingFilters: "initialBindingFilters", initialPropertyMappings: "initialPropertyMappings" }, providers: [ProcessDataService], ngImport: i0, template: `
26479
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: ProcessConfigDialogComponent, isStandalone: true, selector: "mm-process-config-dialog", inputs: { initialProcessDiagramRtId: "initialProcessDiagramRtId", initialFitToBounds: "initialFitToBounds", initialAllowZoom: "initialAllowZoom", initialAllowPan: "initialAllowPan", initialShowToolbar: "initialShowToolbar", initialInitialZoom: "initialInitialZoom", initialDataBindingMode: "initialDataBindingMode", initialBindingCkTypeId: "initialBindingCkTypeId", initialBindingRtId: "initialBindingRtId", initialBindingQueryRtId: "initialBindingQueryRtId", initialBindingQueryName: "initialBindingQueryName", initialBindingQueryFamily: "initialBindingQueryFamily", initialBindingIgnoreTimeFilter: "initialBindingIgnoreTimeFilter", initialBindingEntitySelectorId: "initialBindingEntitySelectorId", initialBindingFilters: "initialBindingFilters", initialPropertyMappings: "initialPropertyMappings" }, providers: [ProcessDataService], ngImport: i0, template: `
25544
26480
  <div class="config-container">
25545
26481
 
25546
26482
  <div class="config-form">
@@ -25706,6 +26642,17 @@ class ProcessConfigDialogComponent {
25706
26642
  <p class="field-hint warning">No queries found.</p>
25707
26643
  }
25708
26644
  </div>
26645
+
26646
+ <!-- Stream-data-only options (hidden for runtime queries) -->
26647
+ <mm-sd-time-filter-toggle
26648
+ [family]="selectedQueryFamily"
26649
+ [(ignoreTimeFilter)]="bindingIgnoreTimeFilter">
26650
+ </mm-sd-time-filter-toggle>
26651
+ <mm-entity-selector-scope-picker
26652
+ [family]="selectedQueryFamily"
26653
+ [selectors]="entitySelectors"
26654
+ [(entitySelectorId)]="bindingEntitySelectorId">
26655
+ </mm-entity-selector-scope-picker>
25709
26656
  }
25710
26657
 
25711
26658
  <!-- Filters Section -->
@@ -25860,7 +26807,7 @@ class ProcessConfigDialogComponent {
25860
26807
  </button>
25861
26808
  </div>
25862
26809
  </div>
25863
- `, isInline: true, styles: [":host{display:block;height:100%}.config-container{display:flex;flex-direction:column;height:100%}.action-bar{display:flex;justify-content:flex-end;gap:8px;padding:8px 16px;border-top:1px solid var(--kendo-color-border, #dee2e6)}.config-form{flex:1;overflow-y:auto;padding:16px}.tab-content{padding:16px;min-height:200px;position:relative}.tab-content.loading{pointer-events:none}.section-hint{margin:0 0 16px;font-size:.85rem;color:var(--kendo-color-subtle, #6c757d)}.empty-hint{text-align:center;padding:24px;color:var(--kendo-color-subtle, #6c757d);font-style:italic}.form-field{display:flex;flex-direction:column;gap:6px;margin-bottom:16px}.form-field:last-child{margin-bottom:0}.form-field.disabled{opacity:.6}.form-field label{font-weight:600;font-size:.9rem;color:var(--kendo-color-on-app-surface, #212529)}.required{color:var(--kendo-color-error, #dc3545)}.field-hint{margin:0;font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.field-hint.warning{color:var(--kendo-color-warning, #ffc107)}.form-row{display:flex;gap:24px;margin-bottom:12px}.form-row:last-child{margin-bottom:0}.mode-toggle{display:flex;gap:8px}.checkbox-field{flex-direction:row;align-items:flex-start}.checkbox-field label{display:flex;align-items:center;gap:8px;cursor:pointer;font-weight:400}.checkbox-field .field-hint{margin-top:4px;margin-left:28px}.preview-section{margin-top:16px;padding:12px;background:var(--kendo-color-surface-alt, #f8f9fa);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px}.preview-info{display:flex;flex-direction:column;gap:6px}.preview-item{display:flex;justify-content:space-between;padding:6px 10px;background:var(--kendo-color-surface, #ffffff);border-radius:4px}.preview-label{font-weight:500;font-size:.85rem;color:var(--kendo-color-subtle, #6c757d)}.preview-value{font-weight:600;font-size:.85rem;color:var(--kendo-color-on-app-surface, #212529)}.diagram-item{display:flex;justify-content:space-between;gap:16px}.diagram-name{font-weight:500}.diagram-size{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.query-item{display:flex;flex-direction:column;gap:2px}.query-name{font-weight:500}.query-description{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.property-mappings-table{display:flex;flex-direction:column;gap:6px}.mapping-header{display:grid;grid-template-columns:minmax(80px,1fr) 55px 85px minmax(80px,1fr) minmax(90px,1.2fr);gap:6px;padding:6px 10px;background:var(--kendo-color-surface-alt, #f8f9fa);border-radius:4px;font-weight:600;font-size:.7rem;color:var(--kendo-color-subtle, #6c757d);text-transform:uppercase}.mapping-row{display:grid;grid-template-columns:minmax(80px,1fr) 55px 85px minmax(80px,1fr) minmax(90px,1.2fr);gap:6px;padding:6px 10px;background:var(--kendo-color-surface, #ffffff);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px;align-items:center}.col-property{font-weight:500;font-size:.85rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.col-type.type-badge{font-size:.65rem;padding:2px 4px;background:var(--kendo-color-surface-alt, #f8f9fa);border-radius:10px;text-align:center;text-transform:uppercase;color:var(--kendo-color-subtle, #6c757d)}.col-expression kendo-textbox{font-family:Consolas,Monaco,monospace;font-size:.8rem}.expression-help{margin-top:12px}.expression-help code{background:var(--kendo-color-surface-alt, #f8f9fa);padding:1px 4px;border-radius:3px;font-size:.75rem}.loading-hint{font-style:italic;color:var(--kendo-color-subtle, #6c757d);padding:8px;text-align:center}:host ::ng-deep kendo-tabstrip{border:none}:host ::ng-deep .k-tabstrip-items-wrapper{border-bottom:1px solid var(--kendo-color-border, #dee2e6)}:host ::ng-deep .k-tabstrip-content{border:none;padding:0}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox]:not([ngNoCva])[formControlName],input[type=checkbox]:not([ngNoCva])[formControl],input[type=checkbox]:not([ngNoCva])[ngModel]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ButtonsModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: InputsModule }, { kind: "component", type: i3.TextBoxComponent, selector: "kendo-textbox", inputs: ["focusableId", "title", "type", "disabled", "readonly", "tabindex", "value", "selectOnFocus", "showSuccessIcon", "showErrorIcon", "clearButton", "successIcon", "successSvgIcon", "errorIcon", "errorSvgIcon", "clearButtonIcon", "clearButtonSvgIcon", "size", "rounded", "fillMode", "tabIndex", "placeholder", "maxlength", "inputAttributes"], outputs: ["valueChange", "inputFocus", "inputBlur", "focus", "blur"], exportAs: ["kendoTextBox"] }, { kind: "directive", type: i3.CheckBoxDirective, selector: "input[kendoCheckBox]", inputs: ["size", "rounded"] }, { kind: "ngmodule", type: DropDownsModule }, { kind: "directive", type: i4.ItemTemplateDirective, selector: "[kendoDropDownListItemTemplate],[kendoComboBoxItemTemplate],[kendoAutoCompleteItemTemplate],[kendoMultiSelectItemTemplate]" }, { kind: "component", type: i4.ComboBoxComponent, selector: "kendo-combobox", inputs: ["icon", "svgIcon", "inputAttributes", "showStickyHeader", "focusableId", "allowCustom", "data", "value", "textField", "valueField", "valuePrimitive", "valueNormalizer", "placeholder", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "loading", "suggest", "clearButton", "disabled", "itemDisabled", "readonly", "tabindex", "tabIndex", "filterable", "virtual", "size", "rounded", "fillMode"], outputs: ["valueChange", "selectionChange", "filterChange", "open", "opened", "close", "closed", "focus", "blur", "inputFocus", "inputBlur", "escape"], exportAs: ["kendoComboBox"] }, { kind: "component", type: i4.DropDownListComponent, selector: "kendo-dropdownlist", inputs: ["customIconClass", "showStickyHeader", "icon", "svgIcon", "loading", "data", "value", "textField", "valueField", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "defaultItem", "disabled", "itemDisabled", "readonly", "filterable", "virtual", "ignoreCase", "delay", "valuePrimitive", "tabindex", "tabIndex", "size", "rounded", "fillMode", "leftRightArrowsNavigation", "id"], outputs: ["valueChange", "filterChange", "selectionChange", "open", "opened", "close", "closed", "focus", "blur"], exportAs: ["kendoDropDownList"] }, { kind: "ngmodule", type: LayoutModule }, { kind: "component", type: i5.TabStripComponent, selector: "kendo-tabstrip", inputs: ["height", "animate", "tabAlignment", "tabPosition", "keepTabContent", "closable", "scrollable", "size", "closeIcon", "closeIconClass", "closeSVGIcon", "showContentArea"], outputs: ["tabSelect", "tabClose", "tabScroll"], exportAs: ["kendoTabStrip"] }, { kind: "component", type: i5.TabStripTabComponent, selector: "kendo-tabstrip-tab", inputs: ["title", "disabled", "cssClass", "cssStyle", "selected", "closable", "closeIcon", "closeIconClass", "closeSVGIcon"], exportAs: ["kendoTabStripTab"] }, { kind: "directive", type: i5.TabContentDirective, selector: "[kendoTabContent]" }, { kind: "component", type: CkTypeSelectorInputComponent, selector: "mm-ck-type-selector-input", inputs: ["placeholder", "minSearchLength", "maxResults", "debounceMs", "ckModelIds", "allowAbstract", "dialogTitle", "advancedSearchLabel", "derivedFromRtCkTypeId", "messages", "dialogMessages", "disabled", "required"], outputs: ["ckTypeSelected", "ckTypeCleared"] }, { kind: "component", type: EntitySelectInputComponent, selector: "mm-entity-select-input", inputs: ["dataSource", "placeholder", "minSearchLength", "maxResults", "debounceMs", "prefix", "initialDisplayValue", "dialogDataSource", "dialogTitle", "multiSelect", "advancedSearchLabel", "dialogMessages", "messages", "disabled", "required"], outputs: ["entitySelected", "entityCleared", "entitiesSelected"] }, { kind: "component", type: FieldFilterEditorComponent, selector: "mm-field-filter-editor", inputs: ["availableAttributes", "ckTypeId", "hideNavigationProperties", "attributePaths", "enableVariables", "availableVariables", "filters"], outputs: ["filtersChange"] }, { kind: "component", type: LoadingOverlayComponent, selector: "mm-loading-overlay", inputs: ["loading"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
26810
+ `, isInline: true, styles: [":host{display:block;height:100%}.config-container{display:flex;flex-direction:column;height:100%}.action-bar{display:flex;justify-content:flex-end;gap:8px;padding:8px 16px;border-top:1px solid var(--kendo-color-border, #dee2e6)}.config-form{flex:1;overflow-y:auto;padding:16px}.tab-content{padding:16px;min-height:200px;position:relative}.tab-content.loading{pointer-events:none}.section-hint{margin:0 0 16px;font-size:.85rem;color:var(--kendo-color-subtle, #6c757d)}.empty-hint{text-align:center;padding:24px;color:var(--kendo-color-subtle, #6c757d);font-style:italic}.form-field{display:flex;flex-direction:column;gap:6px;margin-bottom:16px}.form-field:last-child{margin-bottom:0}.form-field.disabled{opacity:.6}.form-field label{font-weight:600;font-size:.9rem;color:var(--kendo-color-on-app-surface, #212529)}.required{color:var(--kendo-color-error, #dc3545)}.field-hint{margin:0;font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.field-hint.warning{color:var(--kendo-color-warning, #ffc107)}.form-row{display:flex;gap:24px;margin-bottom:12px}.form-row:last-child{margin-bottom:0}.mode-toggle{display:flex;gap:8px}.checkbox-field{flex-direction:row;align-items:flex-start}.checkbox-field label{display:flex;align-items:center;gap:8px;cursor:pointer;font-weight:400}.checkbox-field .field-hint{margin-top:4px;margin-left:28px}.preview-section{margin-top:16px;padding:12px;background:var(--kendo-color-surface-alt, #f8f9fa);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px}.preview-info{display:flex;flex-direction:column;gap:6px}.preview-item{display:flex;justify-content:space-between;padding:6px 10px;background:var(--kendo-color-surface, #ffffff);border-radius:4px}.preview-label{font-weight:500;font-size:.85rem;color:var(--kendo-color-subtle, #6c757d)}.preview-value{font-weight:600;font-size:.85rem;color:var(--kendo-color-on-app-surface, #212529)}.diagram-item{display:flex;justify-content:space-between;gap:16px}.diagram-name{font-weight:500}.diagram-size{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.query-item{display:flex;flex-direction:column;gap:2px}.query-name{font-weight:500}.query-description{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.property-mappings-table{display:flex;flex-direction:column;gap:6px}.mapping-header{display:grid;grid-template-columns:minmax(80px,1fr) 55px 85px minmax(80px,1fr) minmax(90px,1.2fr);gap:6px;padding:6px 10px;background:var(--kendo-color-surface-alt, #f8f9fa);border-radius:4px;font-weight:600;font-size:.7rem;color:var(--kendo-color-subtle, #6c757d);text-transform:uppercase}.mapping-row{display:grid;grid-template-columns:minmax(80px,1fr) 55px 85px minmax(80px,1fr) minmax(90px,1.2fr);gap:6px;padding:6px 10px;background:var(--kendo-color-surface, #ffffff);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px;align-items:center}.col-property{font-weight:500;font-size:.85rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.col-type.type-badge{font-size:.65rem;padding:2px 4px;background:var(--kendo-color-surface-alt, #f8f9fa);border-radius:10px;text-align:center;text-transform:uppercase;color:var(--kendo-color-subtle, #6c757d)}.col-expression kendo-textbox{font-family:Consolas,Monaco,monospace;font-size:.8rem}.expression-help{margin-top:12px}.expression-help code{background:var(--kendo-color-surface-alt, #f8f9fa);padding:1px 4px;border-radius:3px;font-size:.75rem}.loading-hint{font-style:italic;color:var(--kendo-color-subtle, #6c757d);padding:8px;text-align:center}:host ::ng-deep kendo-tabstrip{border:none}:host ::ng-deep .k-tabstrip-items-wrapper{border-bottom:1px solid var(--kendo-color-border, #dee2e6)}:host ::ng-deep .k-tabstrip-content{border:none;padding:0}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox]:not([ngNoCva])[formControlName],input[type=checkbox]:not([ngNoCva])[formControl],input[type=checkbox]:not([ngNoCva])[ngModel]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ButtonsModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: InputsModule }, { kind: "component", type: i3.TextBoxComponent, selector: "kendo-textbox", inputs: ["focusableId", "title", "type", "disabled", "readonly", "tabindex", "value", "selectOnFocus", "showSuccessIcon", "showErrorIcon", "clearButton", "successIcon", "successSvgIcon", "errorIcon", "errorSvgIcon", "clearButtonIcon", "clearButtonSvgIcon", "size", "rounded", "fillMode", "tabIndex", "placeholder", "maxlength", "inputAttributes"], outputs: ["valueChange", "inputFocus", "inputBlur", "focus", "blur"], exportAs: ["kendoTextBox"] }, { kind: "directive", type: i3.CheckBoxDirective, selector: "input[kendoCheckBox]", inputs: ["size", "rounded"] }, { kind: "ngmodule", type: DropDownsModule }, { kind: "directive", type: i4.ItemTemplateDirective, selector: "[kendoDropDownListItemTemplate],[kendoComboBoxItemTemplate],[kendoAutoCompleteItemTemplate],[kendoMultiSelectItemTemplate]" }, { kind: "component", type: i4.ComboBoxComponent, selector: "kendo-combobox", inputs: ["icon", "svgIcon", "inputAttributes", "showStickyHeader", "focusableId", "allowCustom", "data", "value", "textField", "valueField", "valuePrimitive", "valueNormalizer", "placeholder", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "loading", "suggest", "clearButton", "disabled", "itemDisabled", "readonly", "tabindex", "tabIndex", "filterable", "virtual", "size", "rounded", "fillMode"], outputs: ["valueChange", "selectionChange", "filterChange", "open", "opened", "close", "closed", "focus", "blur", "inputFocus", "inputBlur", "escape"], exportAs: ["kendoComboBox"] }, { kind: "component", type: i4.DropDownListComponent, selector: "kendo-dropdownlist", inputs: ["customIconClass", "showStickyHeader", "icon", "svgIcon", "loading", "data", "value", "textField", "valueField", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "defaultItem", "disabled", "itemDisabled", "readonly", "filterable", "virtual", "ignoreCase", "delay", "valuePrimitive", "tabindex", "tabIndex", "size", "rounded", "fillMode", "leftRightArrowsNavigation", "id"], outputs: ["valueChange", "filterChange", "selectionChange", "open", "opened", "close", "closed", "focus", "blur"], exportAs: ["kendoDropDownList"] }, { kind: "ngmodule", type: LayoutModule }, { kind: "component", type: i5.TabStripComponent, selector: "kendo-tabstrip", inputs: ["height", "animate", "tabAlignment", "tabPosition", "keepTabContent", "closable", "scrollable", "size", "closeIcon", "closeIconClass", "closeSVGIcon", "showContentArea"], outputs: ["tabSelect", "tabClose", "tabScroll"], exportAs: ["kendoTabStrip"] }, { kind: "component", type: i5.TabStripTabComponent, selector: "kendo-tabstrip-tab", inputs: ["title", "disabled", "cssClass", "cssStyle", "selected", "closable", "closeIcon", "closeIconClass", "closeSVGIcon"], exportAs: ["kendoTabStripTab"] }, { kind: "directive", type: i5.TabContentDirective, selector: "[kendoTabContent]" }, { kind: "component", type: CkTypeSelectorInputComponent, selector: "mm-ck-type-selector-input", inputs: ["placeholder", "minSearchLength", "maxResults", "debounceMs", "ckModelIds", "allowAbstract", "dialogTitle", "advancedSearchLabel", "derivedFromRtCkTypeId", "messages", "dialogMessages", "disabled", "required"], outputs: ["ckTypeSelected", "ckTypeCleared"] }, { kind: "component", type: EntitySelectInputComponent, selector: "mm-entity-select-input", inputs: ["dataSource", "placeholder", "minSearchLength", "maxResults", "debounceMs", "prefix", "initialDisplayValue", "dialogDataSource", "dialogTitle", "multiSelect", "advancedSearchLabel", "dialogMessages", "messages", "disabled", "required"], outputs: ["entitySelected", "entityCleared", "entitiesSelected"] }, { kind: "component", type: FieldFilterEditorComponent, selector: "mm-field-filter-editor", inputs: ["availableAttributes", "ckTypeId", "hideNavigationProperties", "attributePaths", "enableVariables", "availableVariables", "filters"], outputs: ["filtersChange"] }, { kind: "component", type: LoadingOverlayComponent, selector: "mm-loading-overlay", inputs: ["loading"] }, { kind: "component", type: SdTimeFilterToggleComponent, selector: "mm-sd-time-filter-toggle", inputs: ["family", "ignoreTimeFilter"], outputs: ["ignoreTimeFilterChange"] }, { kind: "component", type: EntitySelectorScopePickerComponent, selector: "mm-entity-selector-scope-picker", inputs: ["family", "entitySelectorId", "selectors"], outputs: ["entitySelectorIdChange"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
25864
26811
  }
25865
26812
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ProcessConfigDialogComponent, decorators: [{
25866
26813
  type: Component,
@@ -25874,7 +26821,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
25874
26821
  CkTypeSelectorInputComponent,
25875
26822
  EntitySelectInputComponent,
25876
26823
  FieldFilterEditorComponent,
25877
- LoadingOverlayComponent
26824
+ LoadingOverlayComponent,
26825
+ SdTimeFilterToggleComponent,
26826
+ EntitySelectorScopePickerComponent
25878
26827
  ], providers: [ProcessDataService], template: `
25879
26828
  <div class="config-container">
25880
26829
 
@@ -26041,6 +26990,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
26041
26990
  <p class="field-hint warning">No queries found.</p>
26042
26991
  }
26043
26992
  </div>
26993
+
26994
+ <!-- Stream-data-only options (hidden for runtime queries) -->
26995
+ <mm-sd-time-filter-toggle
26996
+ [family]="selectedQueryFamily"
26997
+ [(ignoreTimeFilter)]="bindingIgnoreTimeFilter">
26998
+ </mm-sd-time-filter-toggle>
26999
+ <mm-entity-selector-scope-picker
27000
+ [family]="selectedQueryFamily"
27001
+ [selectors]="entitySelectors"
27002
+ [(entitySelectorId)]="bindingEntitySelectorId">
27003
+ </mm-entity-selector-scope-picker>
26044
27004
  }
26045
27005
 
26046
27006
  <!-- Filters Section -->
@@ -26218,6 +27178,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
26218
27178
  type: Input
26219
27179
  }], initialBindingQueryName: [{
26220
27180
  type: Input
27181
+ }], initialBindingQueryFamily: [{
27182
+ type: Input
27183
+ }], initialBindingIgnoreTimeFilter: [{
27184
+ type: Input
27185
+ }], initialBindingEntitySelectorId: [{
27186
+ type: Input
26221
27187
  }], initialBindingFilters: [{
26222
27188
  type: Input
26223
27189
  }], initialPropertyMappings: [{
@@ -26283,6 +27249,9 @@ function registerProcessWidget(registry) {
26283
27249
  initialBindingRtId: widget.bindingRtId,
26284
27250
  initialBindingQueryRtId: widget.bindingQueryRtId,
26285
27251
  initialBindingQueryName: widget.bindingQueryName,
27252
+ initialBindingQueryFamily: widget.bindingQueryFamily,
27253
+ initialBindingIgnoreTimeFilter: widget.bindingIgnoreTimeFilter,
27254
+ initialBindingEntitySelectorId: widget.bindingEntitySelectorId,
26286
27255
  initialBindingFilters: widget.bindingFilters,
26287
27256
  initialPropertyMappings: widget.propertyMappings
26288
27257
  }),
@@ -26303,6 +27272,9 @@ function registerProcessWidget(registry) {
26303
27272
  bindingRtId: result.bindingRtId,
26304
27273
  bindingQueryRtId: result.bindingQueryRtId,
26305
27274
  bindingQueryName: result.bindingQueryName,
27275
+ bindingQueryFamily: result.bindingQueryFamily,
27276
+ bindingIgnoreTimeFilter: result.bindingIgnoreTimeFilter,
27277
+ bindingEntitySelectorId: result.bindingEntitySelectorId,
26306
27278
  bindingFilters: result.bindingFilters,
26307
27279
  propertyMappings: result.propertyMappings
26308
27280
  }),
@@ -26341,6 +27313,10 @@ function registerProcessWidget(registry) {
26341
27313
  bindingRtId: widget.bindingRtId,
26342
27314
  bindingQueryRtId: widget.bindingQueryRtId,
26343
27315
  bindingQueryName: widget.bindingQueryName,
27316
+ // Stream-data binding — written only when set.
27317
+ ...(widget.bindingQueryFamily && { bindingQueryFamily: widget.bindingQueryFamily }),
27318
+ ...(widget.bindingIgnoreTimeFilter && { bindingIgnoreTimeFilter: true }),
27319
+ ...(widget.bindingEntitySelectorId && { bindingEntitySelectorId: widget.bindingEntitySelectorId }),
26344
27320
  bindingFilters: widget.bindingFilters,
26345
27321
  propertyMappings: widget.propertyMappings
26346
27322
  }
@@ -26371,6 +27347,9 @@ function registerProcessWidget(registry) {
26371
27347
  bindingRtId: config['bindingRtId'],
26372
27348
  bindingQueryRtId: config['bindingQueryRtId'],
26373
27349
  bindingQueryName: config['bindingQueryName'],
27350
+ bindingQueryFamily: config['bindingQueryFamily'],
27351
+ bindingIgnoreTimeFilter: config['bindingIgnoreTimeFilter'],
27352
+ bindingEntitySelectorId: config['bindingEntitySelectorId'],
26374
27353
  bindingFilters: config['bindingFilters'],
26375
27354
  propertyMappings: config['propertyMappings']
26376
27355
  };
@@ -26498,14 +27477,25 @@ function registerDefaultWidgets(registry) {
26498
27477
  configDialogTitle: 'Entity Configuration',
26499
27478
  defaultSize: { colSpan: 2, rowSpan: 2 },
26500
27479
  supportedDataSources: ['runtimeEntity'],
26501
- getInitialConfig: (widget) => ({
26502
- initialCkTypeId: getDataSourceInfo(widget).ckTypeId,
26503
- initialRtId: getDataSourceInfo(widget).rtId,
26504
- initialHideEmptyAttributes: widget.hideEmptyAttributes ?? false
26505
- }),
27480
+ getInitialConfig: (widget) => {
27481
+ const ds = widget.dataSource;
27482
+ const isRuntime = ds.type === 'runtimeEntity';
27483
+ return {
27484
+ initialCkTypeId: isRuntime ? ds.ckTypeId : undefined,
27485
+ initialRtId: isRuntime ? ds.rtId : undefined,
27486
+ initialEntitySelectorId: isRuntime ? ds.entitySelectorId : undefined,
27487
+ initialHideEmptyAttributes: widget.hideEmptyAttributes ?? false
27488
+ };
27489
+ },
26506
27490
  applyConfigResult: (widget, result) => ({
26507
27491
  ...widget,
26508
- dataSource: createDataSource(result, true),
27492
+ dataSource: {
27493
+ type: 'runtimeEntity',
27494
+ ckTypeId: result.ckTypeId || undefined,
27495
+ rtId: result.rtId || undefined,
27496
+ entitySelectorId: result.entitySelectorId || undefined,
27497
+ includeAssociations: true
27498
+ },
26509
27499
  hideEmptyAttributes: result.hideEmptyAttributes
26510
27500
  }),
26511
27501
  // SOLID: Factory function
@@ -26527,7 +27517,8 @@ function registerDefaultWidgets(registry) {
26527
27517
  showHeader: widget.showHeader,
26528
27518
  showAttributes: widget.showAttributes,
26529
27519
  attributeFilter: widget.attributeFilter,
26530
- hideEmptyAttributes: widget.hideEmptyAttributes
27520
+ hideEmptyAttributes: widget.hideEmptyAttributes,
27521
+ entitySelectorId: widget.dataSource.type === 'runtimeEntity' ? widget.dataSource.entitySelectorId : undefined
26531
27522
  }
26532
27523
  }),
26533
27524
  // SOLID: Deserialization from persistence
@@ -26540,7 +27531,8 @@ function registerDefaultWidgets(registry) {
26540
27531
  dataSource: {
26541
27532
  type: 'runtimeEntity',
26542
27533
  ckTypeId: data.dataSourceCkTypeId ?? undefined,
26543
- rtId: data.dataSourceRtId ?? undefined
27534
+ rtId: data.dataSourceRtId ?? undefined,
27535
+ entitySelectorId: config['entitySelectorId'] || undefined
26544
27536
  },
26545
27537
  showHeader: config['showHeader'] ?? true,
26546
27538
  showAttributes: config['showAttributes'] ?? true,
@@ -29144,7 +30136,8 @@ class MeshBoardSettingsResult {
29144
30136
  rtWellKnownName;
29145
30137
  entitySelectors;
29146
30138
  autoRefreshSeconds;
29147
- constructor(name, description, columns, rowHeight, gap, variables, timeFilter, rtWellKnownName, entitySelectors, autoRefreshSeconds) {
30139
+ timeZoneMode;
30140
+ constructor(name, description, columns, rowHeight, gap, variables, timeFilter, rtWellKnownName, entitySelectors, autoRefreshSeconds, timeZoneMode) {
29148
30141
  this.name = name;
29149
30142
  this.description = description;
29150
30143
  this.columns = columns;
@@ -29155,6 +30148,7 @@ class MeshBoardSettingsResult {
29155
30148
  this.rtWellKnownName = rtWellKnownName;
29156
30149
  this.entitySelectors = entitySelectors;
29157
30150
  this.autoRefreshSeconds = autoRefreshSeconds;
30151
+ this.timeZoneMode = timeZoneMode;
29158
30152
  }
29159
30153
  }
29160
30154
  /**
@@ -29182,6 +30176,11 @@ class MeshBoardSettingsDialogComponent {
29182
30176
  timeFilterEnabled = false;
29183
30177
  defaultSelection;
29184
30178
  initialDefaultSelection;
30179
+ /**
30180
+ * Timezone basis for time-filter boundaries and datetime display across all
30181
+ * widgets. Defaults to `'local'` (browser timezone).
30182
+ */
30183
+ timeZoneMode = DEFAULT_TIME_ZONE_MODE;
29185
30184
  /** Static and time filter variable names for duplicate detection in entity selector editor */
29186
30185
  get staticVariableNames() {
29187
30186
  return this.variables
@@ -29207,6 +30206,7 @@ class MeshBoardSettingsDialogComponent {
29207
30206
  this.rowHeight = settings.rowHeight;
29208
30207
  this.gap = settings.gap;
29209
30208
  this.autoRefreshSeconds = settings.autoRefreshSeconds ?? 0;
30209
+ this.timeZoneMode = settings.timeZoneMode ?? DEFAULT_TIME_ZONE_MODE;
29210
30210
  this.variables = settings.variables ? [...settings.variables] : [];
29211
30211
  this.entitySelectors = settings.entitySelectors ? settings.entitySelectors.map(es => ({ ...es })) : [];
29212
30212
  this.timeFilterEnabled = settings.timeFilter?.enabled ?? false;
@@ -29248,7 +30248,7 @@ class MeshBoardSettingsDialogComponent {
29248
30248
  enabled: this.timeFilterEnabled,
29249
30249
  defaultSelection: this.timeFilterEnabled ? this.defaultSelection : undefined
29250
30250
  };
29251
- const result = new MeshBoardSettingsResult(this.name.trim(), this.description.trim(), this.columns, this.rowHeight, this.gap, this.variables, timeFilter, this.rtWellKnownName.trim() || undefined, this.entitySelectors.length > 0 ? this.entitySelectors : undefined, this.autoRefreshSeconds > 0 ? this.autoRefreshSeconds : undefined);
30251
+ const result = new MeshBoardSettingsResult(this.name.trim(), this.description.trim(), this.columns, this.rowHeight, this.gap, this.variables, timeFilter, this.rtWellKnownName.trim() || undefined, this.entitySelectors.length > 0 ? this.entitySelectors : undefined, this.autoRefreshSeconds > 0 ? this.autoRefreshSeconds : undefined, this.timeZoneMode);
29252
30252
  this.windowRef.close(result);
29253
30253
  }
29254
30254
  /**
@@ -29258,7 +30258,7 @@ class MeshBoardSettingsDialogComponent {
29258
30258
  this.windowRef.close();
29259
30259
  }
29260
30260
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: MeshBoardSettingsDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
29261
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: MeshBoardSettingsDialogComponent, isStandalone: true, selector: "mm-meshboard-settings-dialog", ngImport: i0, template: "<div class=\"meshboard-settings-dialog\">\n <kendo-tabstrip [animate]=\"false\">\n <!-- General Tab -->\n <kendo-tabstrip-tab [title]=\"'General'\" [selected]=\"true\">\n <ng-template kendoTabContent>\n <div class=\"tab-content\">\n <form class=\"settings-form\">\n <!-- Name Field -->\n <kendo-formfield>\n <kendo-label [for]=\"nameInput\" text=\"Name *\"></kendo-label>\n <kendo-textbox\n #nameInput\n [(ngModel)]=\"name\"\n name=\"name\"\n placeholder=\"Enter MeshBoard name\"\n required>\n </kendo-textbox>\n @if (name.trim().length === 0) {\n <kendo-formerror>Name is required</kendo-formerror>\n }\n </kendo-formfield>\n\n <!-- Description Field -->\n <kendo-formfield>\n <kendo-label [for]=\"descriptionInput\" text=\"Description\"></kendo-label>\n <kendo-textarea\n #descriptionInput\n [(ngModel)]=\"description\"\n name=\"description\"\n placeholder=\"Enter MeshBoard description (optional)\"\n [rows]=\"3\">\n </kendo-textarea>\n </kendo-formfield>\n\n <!-- Well-Known Name Field -->\n <kendo-formfield>\n <kendo-label [for]=\"wellKnownNameInput\" text=\"Well-Known Name\"></kendo-label>\n <kendo-textbox\n #wellKnownNameInput\n [(ngModel)]=\"rtWellKnownName\"\n name=\"rtWellKnownName\"\n placeholder=\"e.g., cockpit, dashboard-main\">\n </kendo-textbox>\n <kendo-formhint>Unique identifier for routing. Use lowercase with hyphens (e.g., 'cockpit', 'sales-dashboard').</kendo-formhint>\n </kendo-formfield>\n\n <!-- Layout Settings -->\n <div class=\"section-title\">Layout Settings</div>\n\n <!-- Columns Field -->\n <kendo-formfield>\n <kendo-label [for]=\"columnsInput\" text=\"Columns *\"></kendo-label>\n <kendo-numerictextbox\n #columnsInput\n [(ngModel)]=\"columns\"\n name=\"columns\"\n [min]=\"1\"\n [max]=\"12\"\n [decimals]=\"0\"\n [format]=\"'n0'\"\n [spinners]=\"true\"\n required>\n </kendo-numerictextbox>\n <kendo-formhint>Number of columns in the grid (1-12)</kendo-formhint>\n @if (columns < 1 || columns > 12) {\n <kendo-formerror>Columns must be between 1 and 12</kendo-formerror>\n }\n </kendo-formfield>\n\n <!-- Row Height Field -->\n <kendo-formfield>\n <kendo-label [for]=\"rowHeightInput\" text=\"Row Height *\"></kendo-label>\n <kendo-numerictextbox\n #rowHeightInput\n [(ngModel)]=\"rowHeight\"\n name=\"rowHeight\"\n [min]=\"100\"\n [max]=\"1000\"\n [decimals]=\"0\"\n [format]=\"'n0'\"\n [spinners]=\"true\"\n required>\n </kendo-numerictextbox>\n <kendo-formhint>Height of each row in pixels (100-1000)</kendo-formhint>\n @if (rowHeight < 100 || rowHeight > 1000) {\n <kendo-formerror>Row height must be between 100 and 1000</kendo-formerror>\n }\n </kendo-formfield>\n\n <!-- Gap Field -->\n <kendo-formfield>\n <kendo-label [for]=\"gapInput\" text=\"Gap *\"></kendo-label>\n <kendo-numerictextbox\n #gapInput\n [(ngModel)]=\"gap\"\n name=\"gap\"\n [min]=\"0\"\n [max]=\"100\"\n [decimals]=\"0\"\n [format]=\"'n0'\"\n [spinners]=\"true\"\n required>\n </kendo-numerictextbox>\n <kendo-formhint>Space between widgets in pixels (0-100)</kendo-formhint>\n @if (gap < 0 || gap > 100) {\n <kendo-formerror>Gap must be between 0 and 100</kendo-formerror>\n }\n </kendo-formfield>\n\n <!-- Auto-Refresh Field -->\n <kendo-formfield>\n <kendo-label [for]=\"autoRefreshInput\" text=\"Auto-refresh\"></kendo-label>\n <kendo-numerictextbox\n #autoRefreshInput\n [(ngModel)]=\"autoRefreshSeconds\"\n name=\"autoRefreshSeconds\"\n [min]=\"0\"\n [max]=\"3600\"\n [decimals]=\"0\"\n [format]=\"'n0'\"\n [spinners]=\"true\">\n </kendo-numerictextbox>\n <kendo-formhint>Refresh interval in seconds (0 = disabled, max 3600). Pauses while the tab is hidden.</kendo-formhint>\n @if (autoRefreshSeconds < 0 || autoRefreshSeconds > 3600) {\n <kendo-formerror>Auto-refresh must be between 0 and 3600 seconds</kendo-formerror>\n }\n </kendo-formfield>\n </form>\n </div>\n </ng-template>\n </kendo-tabstrip-tab>\n\n <!-- Variables Tab -->\n <kendo-tabstrip-tab [title]=\"'Variables'\">\n <ng-template kendoTabContent>\n <div class=\"tab-content\">\n <mm-variables-editor\n [(variables)]=\"variables\">\n </mm-variables-editor>\n </div>\n </ng-template>\n </kendo-tabstrip-tab>\n\n <!-- Time Filter Tab -->\n <kendo-tabstrip-tab [title]=\"'Time Filter'\">\n <ng-template kendoTabContent>\n <div class=\"tab-content\">\n <form class=\"settings-form\">\n <kendo-formfield>\n <div class=\"checkbox-wrapper\">\n <input\n type=\"checkbox\"\n kendoCheckBox\n #timeFilterCheckbox\n [(ngModel)]=\"timeFilterEnabled\"\n name=\"timeFilterEnabled\"\n id=\"timeFilterEnabled\"/>\n <kendo-label\n [for]=\"timeFilterCheckbox\"\n text=\"Enable Time Filter\"\n class=\"checkbox-label\">\n </kendo-label>\n </div>\n <kendo-formhint>\n Shows a time range picker in the toolbar. Sets $timeRangeFrom and $timeRangeTo variables.\n </kendo-formhint>\n </kendo-formfield>\n\n @if (timeFilterEnabled) {\n <kendo-formfield>\n <kendo-label text=\"Default Selection\"></kendo-label>\n <mm-time-range-picker\n [initialSelection]=\"initialDefaultSelection\"\n (selectionChange)=\"onDefaultSelectionChange($event)\">\n </mm-time-range-picker>\n <kendo-formhint>Initial time filter shown when no URL parameters are set. Note: User-selected filters override this default. Use the reset button in the toolbar to revert to the default.</kendo-formhint>\n </kendo-formfield>\n }\n </form>\n </div>\n </ng-template>\n </kendo-tabstrip-tab>\n\n <!-- Entity Selectors Tab -->\n <kendo-tabstrip-tab [title]=\"'Entity Selectors'\">\n <ng-template kendoTabContent>\n <div class=\"tab-content\">\n <mm-entity-selector-editor\n [(entitySelectors)]=\"entitySelectors\"\n [existingVariableNames]=\"staticVariableNames\"\n (editingStateChange)=\"entitySelectorEditing = $event\">\n </mm-entity-selector-editor>\n </div>\n </ng-template>\n </kendo-tabstrip-tab>\n </kendo-tabstrip>\n\n <!-- Dialog Actions -->\n @if (!entitySelectorEditing) {\n <div class=\"dialog-actions mm-dialog-actions\">\n <button kendoButton (click)=\"cancel()\" fillMode=\"flat\">\n Cancel\n </button>\n <button\n kendoButton\n (click)=\"save()\"\n [disabled]=\"!isValid\"\n themeColor=\"primary\">\n Save\n </button>\n </div>\n }\n</div>\n", styles: [".meshboard-settings-dialog{display:flex;flex-direction:column;height:100%;overflow:hidden}.meshboard-settings-dialog kendo-tabstrip{flex:1;min-height:0;display:flex;flex-direction:column}.meshboard-settings-dialog kendo-tabstrip ::ng-deep .k-tabstrip-content{flex:1;min-height:0;overflow:hidden}.meshboard-settings-dialog .tab-content{height:100%;overflow-y:auto;padding:1.5rem}.meshboard-settings-dialog .settings-form{display:flex;flex-direction:column;gap:1.25rem}.meshboard-settings-dialog .settings-form kendo-formfield{display:flex;flex-direction:column;gap:.5rem}.meshboard-settings-dialog .settings-form kendo-formfield kendo-label{font-weight:500;color:var(--kendo-color-on-app-surface, #424242)}.meshboard-settings-dialog .settings-form kendo-formfield kendo-textbox,.meshboard-settings-dialog .settings-form kendo-formfield kendo-textarea,.meshboard-settings-dialog .settings-form kendo-formfield kendo-numerictextbox{width:100%}.meshboard-settings-dialog .settings-form kendo-formfield kendo-formhint{font-size:.75rem;color:var(--kendo-color-subtle, #757575)}.meshboard-settings-dialog .settings-form kendo-formfield kendo-formerror{font-size:.75rem;color:var(--kendo-color-error, #f44336)}.meshboard-settings-dialog .settings-form .section-title{font-size:.875rem;font-weight:600;color:var(--kendo-color-on-app-surface, #424242);text-transform:uppercase;letter-spacing:.5px;margin-top:.5rem;padding-bottom:.5rem;border-bottom:1px solid var(--kendo-color-border, #e0e0e0)}.meshboard-settings-dialog .settings-form .checkbox-wrapper{display:flex;align-items:center;gap:.5rem}.meshboard-settings-dialog .settings-form .checkbox-wrapper .checkbox-label{font-weight:400;cursor:pointer}.meshboard-settings-dialog .dialog-actions{display:flex;justify-content:flex-end;gap:.75rem;padding:1rem 1.5rem;border-top:1px solid var(--kendo-color-border, #e0e0e0);flex-shrink:0;background-color:var(--kendo-color-surface-alt, white)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox]:not([ngNoCva])[formControlName],input[type=checkbox]:not([ngNoCva])[formControl],input[type=checkbox]:not([ngNoCva])[ngModel]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1$1.NgForm, selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: InputsModule }, { kind: "component", type: i3.TextBoxComponent, selector: "kendo-textbox", inputs: ["focusableId", "title", "type", "disabled", "readonly", "tabindex", "value", "selectOnFocus", "showSuccessIcon", "showErrorIcon", "clearButton", "successIcon", "successSvgIcon", "errorIcon", "errorSvgIcon", "clearButtonIcon", "clearButtonSvgIcon", "size", "rounded", "fillMode", "tabIndex", "placeholder", "maxlength", "inputAttributes"], outputs: ["valueChange", "inputFocus", "inputBlur", "focus", "blur"], exportAs: ["kendoTextBox"] }, { kind: "component", type: i3.NumericTextBoxComponent, selector: "kendo-numerictextbox", inputs: ["focusableId", "disabled", "readonly", "title", "autoCorrect", "format", "max", "min", "decimals", "placeholder", "step", "spinners", "rangeValidation", "tabindex", "tabIndex", "changeValueOnScroll", "selectOnFocus", "value", "maxlength", "size", "rounded", "fillMode", "inputAttributes"], outputs: ["valueChange", "focus", "blur", "inputFocus", "inputBlur"], exportAs: ["kendoNumericTextBox"] }, { kind: "component", type: i3.TextAreaComponent, selector: "kendo-textarea", inputs: ["focusableId", "flow", "inputAttributes", "adornmentsOrientation", "rows", "cols", "maxlength", "maxResizableRows", "tabindex", "tabIndex", "resizable", "size", "rounded", "fillMode", "showPrefixSeparator", "showSuffixSeparator"], outputs: ["focus", "blur", "valueChange"], exportAs: ["kendoTextArea"] }, { kind: "directive", type: i3.CheckBoxDirective, selector: "input[kendoCheckBox]", inputs: ["size", "rounded"] }, { kind: "component", type: i3.FormFieldComponent, selector: "kendo-formfield", inputs: ["showHints", "orientation", "showErrors", "colSpan"] }, { kind: "component", type: i3.HintComponent, selector: "kendo-formhint", inputs: ["align"] }, { kind: "component", type: i3.ErrorComponent, selector: "kendo-formerror", inputs: ["align"] }, { kind: "ngmodule", type: CheckBoxModule }, { kind: "ngmodule", type: LabelModule }, { kind: "component", type: i4$1.LabelComponent, selector: "kendo-label", inputs: ["text", "for", "optional", "labelCssStyle", "labelCssClass"], exportAs: ["kendoLabel"] }, { kind: "ngmodule", type: FormFieldModule }, { kind: "ngmodule", type: TabStripModule }, { kind: "component", type: i5.TabStripComponent, selector: "kendo-tabstrip", inputs: ["height", "animate", "tabAlignment", "tabPosition", "keepTabContent", "closable", "scrollable", "size", "closeIcon", "closeIconClass", "closeSVGIcon", "showContentArea"], outputs: ["tabSelect", "tabClose", "tabScroll"], exportAs: ["kendoTabStrip"] }, { kind: "component", type: i5.TabStripTabComponent, selector: "kendo-tabstrip-tab", inputs: ["title", "disabled", "cssClass", "cssStyle", "selected", "closable", "closeIcon", "closeIconClass", "closeSVGIcon"], exportAs: ["kendoTabStripTab"] }, { kind: "directive", type: i5.TabContentDirective, selector: "[kendoTabContent]" }, { kind: "component", type: VariablesEditorComponent, selector: "mm-variables-editor", inputs: ["variables"], outputs: ["variablesChange"] }, { kind: "component", type: EntitySelectorEditorComponent, selector: "mm-entity-selector-editor", inputs: ["entitySelectors", "existingVariableNames"], outputs: ["entitySelectorsChange", "editingStateChange"] }, { kind: "component", type: TimeRangePickerComponent, selector: "mm-time-range-picker", inputs: ["config", "labels", "initialSelection"], outputs: ["rangeChange", "rangeChangeISO", "selectionChange"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
30261
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: MeshBoardSettingsDialogComponent, isStandalone: true, selector: "mm-meshboard-settings-dialog", ngImport: i0, template: "<div class=\"meshboard-settings-dialog\">\n <kendo-tabstrip [animate]=\"false\">\n <!-- General Tab -->\n <kendo-tabstrip-tab [title]=\"'General'\" [selected]=\"true\">\n <ng-template kendoTabContent>\n <div class=\"tab-content\">\n <form class=\"settings-form\">\n <!-- Name Field -->\n <kendo-formfield>\n <kendo-label [for]=\"nameInput\" text=\"Name *\"></kendo-label>\n <kendo-textbox\n #nameInput\n [(ngModel)]=\"name\"\n name=\"name\"\n placeholder=\"Enter MeshBoard name\"\n required>\n </kendo-textbox>\n @if (name.trim().length === 0) {\n <kendo-formerror>Name is required</kendo-formerror>\n }\n </kendo-formfield>\n\n <!-- Description Field -->\n <kendo-formfield>\n <kendo-label [for]=\"descriptionInput\" text=\"Description\"></kendo-label>\n <kendo-textarea\n #descriptionInput\n [(ngModel)]=\"description\"\n name=\"description\"\n placeholder=\"Enter MeshBoard description (optional)\"\n [rows]=\"3\">\n </kendo-textarea>\n </kendo-formfield>\n\n <!-- Well-Known Name Field -->\n <kendo-formfield>\n <kendo-label [for]=\"wellKnownNameInput\" text=\"Well-Known Name\"></kendo-label>\n <kendo-textbox\n #wellKnownNameInput\n [(ngModel)]=\"rtWellKnownName\"\n name=\"rtWellKnownName\"\n placeholder=\"e.g., cockpit, dashboard-main\">\n </kendo-textbox>\n <kendo-formhint>Unique identifier for routing. Use lowercase with hyphens (e.g., 'cockpit', 'sales-dashboard').</kendo-formhint>\n </kendo-formfield>\n\n <!-- Layout Settings -->\n <div class=\"section-title\">Layout Settings</div>\n\n <!-- Columns Field -->\n <kendo-formfield>\n <kendo-label [for]=\"columnsInput\" text=\"Columns *\"></kendo-label>\n <kendo-numerictextbox\n #columnsInput\n [(ngModel)]=\"columns\"\n name=\"columns\"\n [min]=\"1\"\n [max]=\"12\"\n [decimals]=\"0\"\n [format]=\"'n0'\"\n [spinners]=\"true\"\n required>\n </kendo-numerictextbox>\n <kendo-formhint>Number of columns in the grid (1-12)</kendo-formhint>\n @if (columns < 1 || columns > 12) {\n <kendo-formerror>Columns must be between 1 and 12</kendo-formerror>\n }\n </kendo-formfield>\n\n <!-- Row Height Field -->\n <kendo-formfield>\n <kendo-label [for]=\"rowHeightInput\" text=\"Row Height *\"></kendo-label>\n <kendo-numerictextbox\n #rowHeightInput\n [(ngModel)]=\"rowHeight\"\n name=\"rowHeight\"\n [min]=\"100\"\n [max]=\"1000\"\n [decimals]=\"0\"\n [format]=\"'n0'\"\n [spinners]=\"true\"\n required>\n </kendo-numerictextbox>\n <kendo-formhint>Height of each row in pixels (100-1000)</kendo-formhint>\n @if (rowHeight < 100 || rowHeight > 1000) {\n <kendo-formerror>Row height must be between 100 and 1000</kendo-formerror>\n }\n </kendo-formfield>\n\n <!-- Gap Field -->\n <kendo-formfield>\n <kendo-label [for]=\"gapInput\" text=\"Gap *\"></kendo-label>\n <kendo-numerictextbox\n #gapInput\n [(ngModel)]=\"gap\"\n name=\"gap\"\n [min]=\"0\"\n [max]=\"100\"\n [decimals]=\"0\"\n [format]=\"'n0'\"\n [spinners]=\"true\"\n required>\n </kendo-numerictextbox>\n <kendo-formhint>Space between widgets in pixels (0-100)</kendo-formhint>\n @if (gap < 0 || gap > 100) {\n <kendo-formerror>Gap must be between 0 and 100</kendo-formerror>\n }\n </kendo-formfield>\n\n <!-- Auto-Refresh Field -->\n <kendo-formfield>\n <kendo-label [for]=\"autoRefreshInput\" text=\"Auto-refresh\"></kendo-label>\n <kendo-numerictextbox\n #autoRefreshInput\n [(ngModel)]=\"autoRefreshSeconds\"\n name=\"autoRefreshSeconds\"\n [min]=\"0\"\n [max]=\"3600\"\n [decimals]=\"0\"\n [format]=\"'n0'\"\n [spinners]=\"true\">\n </kendo-numerictextbox>\n <kendo-formhint>Refresh interval in seconds (0 = disabled, max 3600). Pauses while the tab is hidden.</kendo-formhint>\n @if (autoRefreshSeconds < 0 || autoRefreshSeconds > 3600) {\n <kendo-formerror>Auto-refresh must be between 0 and 3600 seconds</kendo-formerror>\n }\n </kendo-formfield>\n </form>\n </div>\n </ng-template>\n </kendo-tabstrip-tab>\n\n <!-- Variables Tab -->\n <kendo-tabstrip-tab [title]=\"'Variables'\">\n <ng-template kendoTabContent>\n <div class=\"tab-content\">\n <mm-variables-editor\n [(variables)]=\"variables\">\n </mm-variables-editor>\n </div>\n </ng-template>\n </kendo-tabstrip-tab>\n\n <!-- Time Filter Tab -->\n <kendo-tabstrip-tab [title]=\"'Time Filter'\">\n <ng-template kendoTabContent>\n <div class=\"tab-content\">\n <form class=\"settings-form\">\n <kendo-formfield>\n <kendo-label text=\"Timezone\"></kendo-label>\n <div class=\"radio-group\">\n <div class=\"radio-wrapper\">\n <input\n type=\"radio\"\n kendoRadioButton\n #tzLocal\n [(ngModel)]=\"timeZoneMode\"\n name=\"timeZoneMode\"\n value=\"local\"\n id=\"timeZoneModeLocal\"/>\n <kendo-label [for]=\"tzLocal\" text=\"Local time (browser)\" class=\"radio-label\"></kendo-label>\n </div>\n <div class=\"radio-wrapper\">\n <input\n type=\"radio\"\n kendoRadioButton\n #tzUtc\n [(ngModel)]=\"timeZoneMode\"\n name=\"timeZoneMode\"\n value=\"utc\"\n id=\"timeZoneModeUtc\"/>\n <kendo-label [for]=\"tzUtc\" text=\"UTC\" class=\"radio-label\"></kendo-label>\n </div>\n </div>\n <kendo-formhint>\n Basis for the time filter and for how all widgets display dates and times. Local time is the default.\n </kendo-formhint>\n </kendo-formfield>\n\n <kendo-formfield>\n <div class=\"checkbox-wrapper\">\n <input\n type=\"checkbox\"\n kendoCheckBox\n #timeFilterCheckbox\n [(ngModel)]=\"timeFilterEnabled\"\n name=\"timeFilterEnabled\"\n id=\"timeFilterEnabled\"/>\n <kendo-label\n [for]=\"timeFilterCheckbox\"\n text=\"Enable Time Filter\"\n class=\"checkbox-label\">\n </kendo-label>\n </div>\n <kendo-formhint>\n Shows a time range picker in the toolbar. Sets $timeRangeFrom and $timeRangeTo variables.\n </kendo-formhint>\n </kendo-formfield>\n\n @if (timeFilterEnabled) {\n <kendo-formfield>\n <kendo-label text=\"Default Selection\"></kendo-label>\n <mm-time-range-picker\n [initialSelection]=\"initialDefaultSelection\"\n (selectionChange)=\"onDefaultSelectionChange($event)\">\n </mm-time-range-picker>\n <kendo-formhint>Initial time filter shown when no URL parameters are set. Note: User-selected filters override this default. Use the reset button in the toolbar to revert to the default.</kendo-formhint>\n </kendo-formfield>\n }\n </form>\n </div>\n </ng-template>\n </kendo-tabstrip-tab>\n\n <!-- Entity Selectors Tab -->\n <kendo-tabstrip-tab [title]=\"'Entity Selectors'\">\n <ng-template kendoTabContent>\n <div class=\"tab-content\">\n <mm-entity-selector-editor\n [(entitySelectors)]=\"entitySelectors\"\n [existingVariableNames]=\"staticVariableNames\"\n (editingStateChange)=\"entitySelectorEditing = $event\">\n </mm-entity-selector-editor>\n </div>\n </ng-template>\n </kendo-tabstrip-tab>\n </kendo-tabstrip>\n\n <!-- Dialog Actions -->\n @if (!entitySelectorEditing) {\n <div class=\"dialog-actions mm-dialog-actions\">\n <button kendoButton (click)=\"cancel()\" fillMode=\"flat\">\n Cancel\n </button>\n <button\n kendoButton\n (click)=\"save()\"\n [disabled]=\"!isValid\"\n themeColor=\"primary\">\n Save\n </button>\n </div>\n }\n</div>\n", styles: [".meshboard-settings-dialog{display:flex;flex-direction:column;height:100%;overflow:hidden}.meshboard-settings-dialog kendo-tabstrip{flex:1;min-height:0;display:flex;flex-direction:column}.meshboard-settings-dialog kendo-tabstrip ::ng-deep .k-tabstrip-content{flex:1;min-height:0;overflow:hidden}.meshboard-settings-dialog .tab-content{height:100%;overflow-y:auto;padding:1.5rem}.meshboard-settings-dialog .settings-form{display:flex;flex-direction:column;gap:1.25rem}.meshboard-settings-dialog .settings-form kendo-formfield{display:flex;flex-direction:column;gap:.5rem}.meshboard-settings-dialog .settings-form kendo-formfield kendo-label{font-weight:500;color:var(--kendo-color-on-app-surface, #424242)}.meshboard-settings-dialog .settings-form kendo-formfield kendo-textbox,.meshboard-settings-dialog .settings-form kendo-formfield kendo-textarea,.meshboard-settings-dialog .settings-form kendo-formfield kendo-numerictextbox{width:100%}.meshboard-settings-dialog .settings-form kendo-formfield kendo-formhint{font-size:.75rem;color:var(--kendo-color-subtle, #757575)}.meshboard-settings-dialog .settings-form kendo-formfield kendo-formerror{font-size:.75rem;color:var(--kendo-color-error, #f44336)}.meshboard-settings-dialog .settings-form .section-title{font-size:.875rem;font-weight:600;color:var(--kendo-color-on-app-surface, #424242);text-transform:uppercase;letter-spacing:.5px;margin-top:.5rem;padding-bottom:.5rem;border-bottom:1px solid var(--kendo-color-border, #e0e0e0)}.meshboard-settings-dialog .settings-form .checkbox-wrapper,.meshboard-settings-dialog .settings-form .radio-wrapper{display:flex;align-items:center;gap:.5rem}.meshboard-settings-dialog .settings-form .checkbox-wrapper .checkbox-label,.meshboard-settings-dialog .settings-form .checkbox-wrapper .radio-label,.meshboard-settings-dialog .settings-form .radio-wrapper .checkbox-label,.meshboard-settings-dialog .settings-form .radio-wrapper .radio-label{font-weight:400;cursor:pointer}.meshboard-settings-dialog .settings-form .radio-group{display:flex;flex-direction:column;gap:.5rem;margin-top:.25rem}.meshboard-settings-dialog .dialog-actions{display:flex;justify-content:flex-end;gap:.75rem;padding:1rem 1.5rem;border-top:1px solid var(--kendo-color-border, #e0e0e0);flex-shrink:0;background-color:var(--kendo-color-surface-alt, white)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox]:not([ngNoCva])[formControlName],input[type=checkbox]:not([ngNoCva])[formControl],input[type=checkbox]:not([ngNoCva])[ngModel]" }, { kind: "directive", type: i1$1.RadioControlValueAccessor, selector: "input[type=radio]:not([ngNoCva])[formControlName],input[type=radio]:not([ngNoCva])[formControl],input[type=radio]:not([ngNoCva])[ngModel]", inputs: ["name", "formControlName", "value"] }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1$1.NgForm, selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: InputsModule }, { kind: "component", type: i3.TextBoxComponent, selector: "kendo-textbox", inputs: ["focusableId", "title", "type", "disabled", "readonly", "tabindex", "value", "selectOnFocus", "showSuccessIcon", "showErrorIcon", "clearButton", "successIcon", "successSvgIcon", "errorIcon", "errorSvgIcon", "clearButtonIcon", "clearButtonSvgIcon", "size", "rounded", "fillMode", "tabIndex", "placeholder", "maxlength", "inputAttributes"], outputs: ["valueChange", "inputFocus", "inputBlur", "focus", "blur"], exportAs: ["kendoTextBox"] }, { kind: "component", type: i3.NumericTextBoxComponent, selector: "kendo-numerictextbox", inputs: ["focusableId", "disabled", "readonly", "title", "autoCorrect", "format", "max", "min", "decimals", "placeholder", "step", "spinners", "rangeValidation", "tabindex", "tabIndex", "changeValueOnScroll", "selectOnFocus", "value", "maxlength", "size", "rounded", "fillMode", "inputAttributes"], outputs: ["valueChange", "focus", "blur", "inputFocus", "inputBlur"], exportAs: ["kendoNumericTextBox"] }, { kind: "component", type: i3.TextAreaComponent, selector: "kendo-textarea", inputs: ["focusableId", "flow", "inputAttributes", "adornmentsOrientation", "rows", "cols", "maxlength", "maxResizableRows", "tabindex", "tabIndex", "resizable", "size", "rounded", "fillMode", "showPrefixSeparator", "showSuffixSeparator"], outputs: ["focus", "blur", "valueChange"], exportAs: ["kendoTextArea"] }, { kind: "directive", type: i3.CheckBoxDirective, selector: "input[kendoCheckBox]", inputs: ["size", "rounded"] }, { kind: "directive", type: i3.RadioButtonDirective, selector: "input[kendoRadioButton]", inputs: ["size"] }, { kind: "component", type: i3.FormFieldComponent, selector: "kendo-formfield", inputs: ["showHints", "orientation", "showErrors", "colSpan"] }, { kind: "component", type: i3.HintComponent, selector: "kendo-formhint", inputs: ["align"] }, { kind: "component", type: i3.ErrorComponent, selector: "kendo-formerror", inputs: ["align"] }, { kind: "ngmodule", type: CheckBoxModule }, { kind: "ngmodule", type: LabelModule }, { kind: "component", type: i4$1.LabelComponent, selector: "kendo-label", inputs: ["text", "for", "optional", "labelCssStyle", "labelCssClass"], exportAs: ["kendoLabel"] }, { kind: "ngmodule", type: FormFieldModule }, { kind: "ngmodule", type: TabStripModule }, { kind: "component", type: i5.TabStripComponent, selector: "kendo-tabstrip", inputs: ["height", "animate", "tabAlignment", "tabPosition", "keepTabContent", "closable", "scrollable", "size", "closeIcon", "closeIconClass", "closeSVGIcon", "showContentArea"], outputs: ["tabSelect", "tabClose", "tabScroll"], exportAs: ["kendoTabStrip"] }, { kind: "component", type: i5.TabStripTabComponent, selector: "kendo-tabstrip-tab", inputs: ["title", "disabled", "cssClass", "cssStyle", "selected", "closable", "closeIcon", "closeIconClass", "closeSVGIcon"], exportAs: ["kendoTabStripTab"] }, { kind: "directive", type: i5.TabContentDirective, selector: "[kendoTabContent]" }, { kind: "component", type: VariablesEditorComponent, selector: "mm-variables-editor", inputs: ["variables"], outputs: ["variablesChange"] }, { kind: "component", type: EntitySelectorEditorComponent, selector: "mm-entity-selector-editor", inputs: ["entitySelectors", "existingVariableNames"], outputs: ["entitySelectorsChange", "editingStateChange"] }, { kind: "component", type: TimeRangePickerComponent, selector: "mm-time-range-picker", inputs: ["config", "labels", "initialSelection"], outputs: ["rangeChange", "rangeChangeISO", "selectionChange"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
29262
30262
  }
29263
30263
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: MeshBoardSettingsDialogComponent, decorators: [{
29264
30264
  type: Component,
@@ -29274,7 +30274,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
29274
30274
  VariablesEditorComponent,
29275
30275
  EntitySelectorEditorComponent,
29276
30276
  TimeRangePickerComponent
29277
- ], changeDetection: ChangeDetectionStrategy.Eager, template: "<div class=\"meshboard-settings-dialog\">\n <kendo-tabstrip [animate]=\"false\">\n <!-- General Tab -->\n <kendo-tabstrip-tab [title]=\"'General'\" [selected]=\"true\">\n <ng-template kendoTabContent>\n <div class=\"tab-content\">\n <form class=\"settings-form\">\n <!-- Name Field -->\n <kendo-formfield>\n <kendo-label [for]=\"nameInput\" text=\"Name *\"></kendo-label>\n <kendo-textbox\n #nameInput\n [(ngModel)]=\"name\"\n name=\"name\"\n placeholder=\"Enter MeshBoard name\"\n required>\n </kendo-textbox>\n @if (name.trim().length === 0) {\n <kendo-formerror>Name is required</kendo-formerror>\n }\n </kendo-formfield>\n\n <!-- Description Field -->\n <kendo-formfield>\n <kendo-label [for]=\"descriptionInput\" text=\"Description\"></kendo-label>\n <kendo-textarea\n #descriptionInput\n [(ngModel)]=\"description\"\n name=\"description\"\n placeholder=\"Enter MeshBoard description (optional)\"\n [rows]=\"3\">\n </kendo-textarea>\n </kendo-formfield>\n\n <!-- Well-Known Name Field -->\n <kendo-formfield>\n <kendo-label [for]=\"wellKnownNameInput\" text=\"Well-Known Name\"></kendo-label>\n <kendo-textbox\n #wellKnownNameInput\n [(ngModel)]=\"rtWellKnownName\"\n name=\"rtWellKnownName\"\n placeholder=\"e.g., cockpit, dashboard-main\">\n </kendo-textbox>\n <kendo-formhint>Unique identifier for routing. Use lowercase with hyphens (e.g., 'cockpit', 'sales-dashboard').</kendo-formhint>\n </kendo-formfield>\n\n <!-- Layout Settings -->\n <div class=\"section-title\">Layout Settings</div>\n\n <!-- Columns Field -->\n <kendo-formfield>\n <kendo-label [for]=\"columnsInput\" text=\"Columns *\"></kendo-label>\n <kendo-numerictextbox\n #columnsInput\n [(ngModel)]=\"columns\"\n name=\"columns\"\n [min]=\"1\"\n [max]=\"12\"\n [decimals]=\"0\"\n [format]=\"'n0'\"\n [spinners]=\"true\"\n required>\n </kendo-numerictextbox>\n <kendo-formhint>Number of columns in the grid (1-12)</kendo-formhint>\n @if (columns < 1 || columns > 12) {\n <kendo-formerror>Columns must be between 1 and 12</kendo-formerror>\n }\n </kendo-formfield>\n\n <!-- Row Height Field -->\n <kendo-formfield>\n <kendo-label [for]=\"rowHeightInput\" text=\"Row Height *\"></kendo-label>\n <kendo-numerictextbox\n #rowHeightInput\n [(ngModel)]=\"rowHeight\"\n name=\"rowHeight\"\n [min]=\"100\"\n [max]=\"1000\"\n [decimals]=\"0\"\n [format]=\"'n0'\"\n [spinners]=\"true\"\n required>\n </kendo-numerictextbox>\n <kendo-formhint>Height of each row in pixels (100-1000)</kendo-formhint>\n @if (rowHeight < 100 || rowHeight > 1000) {\n <kendo-formerror>Row height must be between 100 and 1000</kendo-formerror>\n }\n </kendo-formfield>\n\n <!-- Gap Field -->\n <kendo-formfield>\n <kendo-label [for]=\"gapInput\" text=\"Gap *\"></kendo-label>\n <kendo-numerictextbox\n #gapInput\n [(ngModel)]=\"gap\"\n name=\"gap\"\n [min]=\"0\"\n [max]=\"100\"\n [decimals]=\"0\"\n [format]=\"'n0'\"\n [spinners]=\"true\"\n required>\n </kendo-numerictextbox>\n <kendo-formhint>Space between widgets in pixels (0-100)</kendo-formhint>\n @if (gap < 0 || gap > 100) {\n <kendo-formerror>Gap must be between 0 and 100</kendo-formerror>\n }\n </kendo-formfield>\n\n <!-- Auto-Refresh Field -->\n <kendo-formfield>\n <kendo-label [for]=\"autoRefreshInput\" text=\"Auto-refresh\"></kendo-label>\n <kendo-numerictextbox\n #autoRefreshInput\n [(ngModel)]=\"autoRefreshSeconds\"\n name=\"autoRefreshSeconds\"\n [min]=\"0\"\n [max]=\"3600\"\n [decimals]=\"0\"\n [format]=\"'n0'\"\n [spinners]=\"true\">\n </kendo-numerictextbox>\n <kendo-formhint>Refresh interval in seconds (0 = disabled, max 3600). Pauses while the tab is hidden.</kendo-formhint>\n @if (autoRefreshSeconds < 0 || autoRefreshSeconds > 3600) {\n <kendo-formerror>Auto-refresh must be between 0 and 3600 seconds</kendo-formerror>\n }\n </kendo-formfield>\n </form>\n </div>\n </ng-template>\n </kendo-tabstrip-tab>\n\n <!-- Variables Tab -->\n <kendo-tabstrip-tab [title]=\"'Variables'\">\n <ng-template kendoTabContent>\n <div class=\"tab-content\">\n <mm-variables-editor\n [(variables)]=\"variables\">\n </mm-variables-editor>\n </div>\n </ng-template>\n </kendo-tabstrip-tab>\n\n <!-- Time Filter Tab -->\n <kendo-tabstrip-tab [title]=\"'Time Filter'\">\n <ng-template kendoTabContent>\n <div class=\"tab-content\">\n <form class=\"settings-form\">\n <kendo-formfield>\n <div class=\"checkbox-wrapper\">\n <input\n type=\"checkbox\"\n kendoCheckBox\n #timeFilterCheckbox\n [(ngModel)]=\"timeFilterEnabled\"\n name=\"timeFilterEnabled\"\n id=\"timeFilterEnabled\"/>\n <kendo-label\n [for]=\"timeFilterCheckbox\"\n text=\"Enable Time Filter\"\n class=\"checkbox-label\">\n </kendo-label>\n </div>\n <kendo-formhint>\n Shows a time range picker in the toolbar. Sets $timeRangeFrom and $timeRangeTo variables.\n </kendo-formhint>\n </kendo-formfield>\n\n @if (timeFilterEnabled) {\n <kendo-formfield>\n <kendo-label text=\"Default Selection\"></kendo-label>\n <mm-time-range-picker\n [initialSelection]=\"initialDefaultSelection\"\n (selectionChange)=\"onDefaultSelectionChange($event)\">\n </mm-time-range-picker>\n <kendo-formhint>Initial time filter shown when no URL parameters are set. Note: User-selected filters override this default. Use the reset button in the toolbar to revert to the default.</kendo-formhint>\n </kendo-formfield>\n }\n </form>\n </div>\n </ng-template>\n </kendo-tabstrip-tab>\n\n <!-- Entity Selectors Tab -->\n <kendo-tabstrip-tab [title]=\"'Entity Selectors'\">\n <ng-template kendoTabContent>\n <div class=\"tab-content\">\n <mm-entity-selector-editor\n [(entitySelectors)]=\"entitySelectors\"\n [existingVariableNames]=\"staticVariableNames\"\n (editingStateChange)=\"entitySelectorEditing = $event\">\n </mm-entity-selector-editor>\n </div>\n </ng-template>\n </kendo-tabstrip-tab>\n </kendo-tabstrip>\n\n <!-- Dialog Actions -->\n @if (!entitySelectorEditing) {\n <div class=\"dialog-actions mm-dialog-actions\">\n <button kendoButton (click)=\"cancel()\" fillMode=\"flat\">\n Cancel\n </button>\n <button\n kendoButton\n (click)=\"save()\"\n [disabled]=\"!isValid\"\n themeColor=\"primary\">\n Save\n </button>\n </div>\n }\n</div>\n", styles: [".meshboard-settings-dialog{display:flex;flex-direction:column;height:100%;overflow:hidden}.meshboard-settings-dialog kendo-tabstrip{flex:1;min-height:0;display:flex;flex-direction:column}.meshboard-settings-dialog kendo-tabstrip ::ng-deep .k-tabstrip-content{flex:1;min-height:0;overflow:hidden}.meshboard-settings-dialog .tab-content{height:100%;overflow-y:auto;padding:1.5rem}.meshboard-settings-dialog .settings-form{display:flex;flex-direction:column;gap:1.25rem}.meshboard-settings-dialog .settings-form kendo-formfield{display:flex;flex-direction:column;gap:.5rem}.meshboard-settings-dialog .settings-form kendo-formfield kendo-label{font-weight:500;color:var(--kendo-color-on-app-surface, #424242)}.meshboard-settings-dialog .settings-form kendo-formfield kendo-textbox,.meshboard-settings-dialog .settings-form kendo-formfield kendo-textarea,.meshboard-settings-dialog .settings-form kendo-formfield kendo-numerictextbox{width:100%}.meshboard-settings-dialog .settings-form kendo-formfield kendo-formhint{font-size:.75rem;color:var(--kendo-color-subtle, #757575)}.meshboard-settings-dialog .settings-form kendo-formfield kendo-formerror{font-size:.75rem;color:var(--kendo-color-error, #f44336)}.meshboard-settings-dialog .settings-form .section-title{font-size:.875rem;font-weight:600;color:var(--kendo-color-on-app-surface, #424242);text-transform:uppercase;letter-spacing:.5px;margin-top:.5rem;padding-bottom:.5rem;border-bottom:1px solid var(--kendo-color-border, #e0e0e0)}.meshboard-settings-dialog .settings-form .checkbox-wrapper{display:flex;align-items:center;gap:.5rem}.meshboard-settings-dialog .settings-form .checkbox-wrapper .checkbox-label{font-weight:400;cursor:pointer}.meshboard-settings-dialog .dialog-actions{display:flex;justify-content:flex-end;gap:.75rem;padding:1rem 1.5rem;border-top:1px solid var(--kendo-color-border, #e0e0e0);flex-shrink:0;background-color:var(--kendo-color-surface-alt, white)}\n"] }]
30277
+ ], changeDetection: ChangeDetectionStrategy.Eager, template: "<div class=\"meshboard-settings-dialog\">\n <kendo-tabstrip [animate]=\"false\">\n <!-- General Tab -->\n <kendo-tabstrip-tab [title]=\"'General'\" [selected]=\"true\">\n <ng-template kendoTabContent>\n <div class=\"tab-content\">\n <form class=\"settings-form\">\n <!-- Name Field -->\n <kendo-formfield>\n <kendo-label [for]=\"nameInput\" text=\"Name *\"></kendo-label>\n <kendo-textbox\n #nameInput\n [(ngModel)]=\"name\"\n name=\"name\"\n placeholder=\"Enter MeshBoard name\"\n required>\n </kendo-textbox>\n @if (name.trim().length === 0) {\n <kendo-formerror>Name is required</kendo-formerror>\n }\n </kendo-formfield>\n\n <!-- Description Field -->\n <kendo-formfield>\n <kendo-label [for]=\"descriptionInput\" text=\"Description\"></kendo-label>\n <kendo-textarea\n #descriptionInput\n [(ngModel)]=\"description\"\n name=\"description\"\n placeholder=\"Enter MeshBoard description (optional)\"\n [rows]=\"3\">\n </kendo-textarea>\n </kendo-formfield>\n\n <!-- Well-Known Name Field -->\n <kendo-formfield>\n <kendo-label [for]=\"wellKnownNameInput\" text=\"Well-Known Name\"></kendo-label>\n <kendo-textbox\n #wellKnownNameInput\n [(ngModel)]=\"rtWellKnownName\"\n name=\"rtWellKnownName\"\n placeholder=\"e.g., cockpit, dashboard-main\">\n </kendo-textbox>\n <kendo-formhint>Unique identifier for routing. Use lowercase with hyphens (e.g., 'cockpit', 'sales-dashboard').</kendo-formhint>\n </kendo-formfield>\n\n <!-- Layout Settings -->\n <div class=\"section-title\">Layout Settings</div>\n\n <!-- Columns Field -->\n <kendo-formfield>\n <kendo-label [for]=\"columnsInput\" text=\"Columns *\"></kendo-label>\n <kendo-numerictextbox\n #columnsInput\n [(ngModel)]=\"columns\"\n name=\"columns\"\n [min]=\"1\"\n [max]=\"12\"\n [decimals]=\"0\"\n [format]=\"'n0'\"\n [spinners]=\"true\"\n required>\n </kendo-numerictextbox>\n <kendo-formhint>Number of columns in the grid (1-12)</kendo-formhint>\n @if (columns < 1 || columns > 12) {\n <kendo-formerror>Columns must be between 1 and 12</kendo-formerror>\n }\n </kendo-formfield>\n\n <!-- Row Height Field -->\n <kendo-formfield>\n <kendo-label [for]=\"rowHeightInput\" text=\"Row Height *\"></kendo-label>\n <kendo-numerictextbox\n #rowHeightInput\n [(ngModel)]=\"rowHeight\"\n name=\"rowHeight\"\n [min]=\"100\"\n [max]=\"1000\"\n [decimals]=\"0\"\n [format]=\"'n0'\"\n [spinners]=\"true\"\n required>\n </kendo-numerictextbox>\n <kendo-formhint>Height of each row in pixels (100-1000)</kendo-formhint>\n @if (rowHeight < 100 || rowHeight > 1000) {\n <kendo-formerror>Row height must be between 100 and 1000</kendo-formerror>\n }\n </kendo-formfield>\n\n <!-- Gap Field -->\n <kendo-formfield>\n <kendo-label [for]=\"gapInput\" text=\"Gap *\"></kendo-label>\n <kendo-numerictextbox\n #gapInput\n [(ngModel)]=\"gap\"\n name=\"gap\"\n [min]=\"0\"\n [max]=\"100\"\n [decimals]=\"0\"\n [format]=\"'n0'\"\n [spinners]=\"true\"\n required>\n </kendo-numerictextbox>\n <kendo-formhint>Space between widgets in pixels (0-100)</kendo-formhint>\n @if (gap < 0 || gap > 100) {\n <kendo-formerror>Gap must be between 0 and 100</kendo-formerror>\n }\n </kendo-formfield>\n\n <!-- Auto-Refresh Field -->\n <kendo-formfield>\n <kendo-label [for]=\"autoRefreshInput\" text=\"Auto-refresh\"></kendo-label>\n <kendo-numerictextbox\n #autoRefreshInput\n [(ngModel)]=\"autoRefreshSeconds\"\n name=\"autoRefreshSeconds\"\n [min]=\"0\"\n [max]=\"3600\"\n [decimals]=\"0\"\n [format]=\"'n0'\"\n [spinners]=\"true\">\n </kendo-numerictextbox>\n <kendo-formhint>Refresh interval in seconds (0 = disabled, max 3600). Pauses while the tab is hidden.</kendo-formhint>\n @if (autoRefreshSeconds < 0 || autoRefreshSeconds > 3600) {\n <kendo-formerror>Auto-refresh must be between 0 and 3600 seconds</kendo-formerror>\n }\n </kendo-formfield>\n </form>\n </div>\n </ng-template>\n </kendo-tabstrip-tab>\n\n <!-- Variables Tab -->\n <kendo-tabstrip-tab [title]=\"'Variables'\">\n <ng-template kendoTabContent>\n <div class=\"tab-content\">\n <mm-variables-editor\n [(variables)]=\"variables\">\n </mm-variables-editor>\n </div>\n </ng-template>\n </kendo-tabstrip-tab>\n\n <!-- Time Filter Tab -->\n <kendo-tabstrip-tab [title]=\"'Time Filter'\">\n <ng-template kendoTabContent>\n <div class=\"tab-content\">\n <form class=\"settings-form\">\n <kendo-formfield>\n <kendo-label text=\"Timezone\"></kendo-label>\n <div class=\"radio-group\">\n <div class=\"radio-wrapper\">\n <input\n type=\"radio\"\n kendoRadioButton\n #tzLocal\n [(ngModel)]=\"timeZoneMode\"\n name=\"timeZoneMode\"\n value=\"local\"\n id=\"timeZoneModeLocal\"/>\n <kendo-label [for]=\"tzLocal\" text=\"Local time (browser)\" class=\"radio-label\"></kendo-label>\n </div>\n <div class=\"radio-wrapper\">\n <input\n type=\"radio\"\n kendoRadioButton\n #tzUtc\n [(ngModel)]=\"timeZoneMode\"\n name=\"timeZoneMode\"\n value=\"utc\"\n id=\"timeZoneModeUtc\"/>\n <kendo-label [for]=\"tzUtc\" text=\"UTC\" class=\"radio-label\"></kendo-label>\n </div>\n </div>\n <kendo-formhint>\n Basis for the time filter and for how all widgets display dates and times. Local time is the default.\n </kendo-formhint>\n </kendo-formfield>\n\n <kendo-formfield>\n <div class=\"checkbox-wrapper\">\n <input\n type=\"checkbox\"\n kendoCheckBox\n #timeFilterCheckbox\n [(ngModel)]=\"timeFilterEnabled\"\n name=\"timeFilterEnabled\"\n id=\"timeFilterEnabled\"/>\n <kendo-label\n [for]=\"timeFilterCheckbox\"\n text=\"Enable Time Filter\"\n class=\"checkbox-label\">\n </kendo-label>\n </div>\n <kendo-formhint>\n Shows a time range picker in the toolbar. Sets $timeRangeFrom and $timeRangeTo variables.\n </kendo-formhint>\n </kendo-formfield>\n\n @if (timeFilterEnabled) {\n <kendo-formfield>\n <kendo-label text=\"Default Selection\"></kendo-label>\n <mm-time-range-picker\n [initialSelection]=\"initialDefaultSelection\"\n (selectionChange)=\"onDefaultSelectionChange($event)\">\n </mm-time-range-picker>\n <kendo-formhint>Initial time filter shown when no URL parameters are set. Note: User-selected filters override this default. Use the reset button in the toolbar to revert to the default.</kendo-formhint>\n </kendo-formfield>\n }\n </form>\n </div>\n </ng-template>\n </kendo-tabstrip-tab>\n\n <!-- Entity Selectors Tab -->\n <kendo-tabstrip-tab [title]=\"'Entity Selectors'\">\n <ng-template kendoTabContent>\n <div class=\"tab-content\">\n <mm-entity-selector-editor\n [(entitySelectors)]=\"entitySelectors\"\n [existingVariableNames]=\"staticVariableNames\"\n (editingStateChange)=\"entitySelectorEditing = $event\">\n </mm-entity-selector-editor>\n </div>\n </ng-template>\n </kendo-tabstrip-tab>\n </kendo-tabstrip>\n\n <!-- Dialog Actions -->\n @if (!entitySelectorEditing) {\n <div class=\"dialog-actions mm-dialog-actions\">\n <button kendoButton (click)=\"cancel()\" fillMode=\"flat\">\n Cancel\n </button>\n <button\n kendoButton\n (click)=\"save()\"\n [disabled]=\"!isValid\"\n themeColor=\"primary\">\n Save\n </button>\n </div>\n }\n</div>\n", styles: [".meshboard-settings-dialog{display:flex;flex-direction:column;height:100%;overflow:hidden}.meshboard-settings-dialog kendo-tabstrip{flex:1;min-height:0;display:flex;flex-direction:column}.meshboard-settings-dialog kendo-tabstrip ::ng-deep .k-tabstrip-content{flex:1;min-height:0;overflow:hidden}.meshboard-settings-dialog .tab-content{height:100%;overflow-y:auto;padding:1.5rem}.meshboard-settings-dialog .settings-form{display:flex;flex-direction:column;gap:1.25rem}.meshboard-settings-dialog .settings-form kendo-formfield{display:flex;flex-direction:column;gap:.5rem}.meshboard-settings-dialog .settings-form kendo-formfield kendo-label{font-weight:500;color:var(--kendo-color-on-app-surface, #424242)}.meshboard-settings-dialog .settings-form kendo-formfield kendo-textbox,.meshboard-settings-dialog .settings-form kendo-formfield kendo-textarea,.meshboard-settings-dialog .settings-form kendo-formfield kendo-numerictextbox{width:100%}.meshboard-settings-dialog .settings-form kendo-formfield kendo-formhint{font-size:.75rem;color:var(--kendo-color-subtle, #757575)}.meshboard-settings-dialog .settings-form kendo-formfield kendo-formerror{font-size:.75rem;color:var(--kendo-color-error, #f44336)}.meshboard-settings-dialog .settings-form .section-title{font-size:.875rem;font-weight:600;color:var(--kendo-color-on-app-surface, #424242);text-transform:uppercase;letter-spacing:.5px;margin-top:.5rem;padding-bottom:.5rem;border-bottom:1px solid var(--kendo-color-border, #e0e0e0)}.meshboard-settings-dialog .settings-form .checkbox-wrapper,.meshboard-settings-dialog .settings-form .radio-wrapper{display:flex;align-items:center;gap:.5rem}.meshboard-settings-dialog .settings-form .checkbox-wrapper .checkbox-label,.meshboard-settings-dialog .settings-form .checkbox-wrapper .radio-label,.meshboard-settings-dialog .settings-form .radio-wrapper .checkbox-label,.meshboard-settings-dialog .settings-form .radio-wrapper .radio-label{font-weight:400;cursor:pointer}.meshboard-settings-dialog .settings-form .radio-group{display:flex;flex-direction:column;gap:.5rem;margin-top:.25rem}.meshboard-settings-dialog .dialog-actions{display:flex;justify-content:flex-end;gap:.75rem;padding:1rem 1.5rem;border-top:1px solid var(--kendo-color-border, #e0e0e0);flex-shrink:0;background-color:var(--kendo-color-surface-alt, white)}\n"] }]
29278
30278
  }] });
29279
30279
 
29280
30280
  /**
@@ -30444,7 +31444,7 @@ class MeshBoardViewComponent {
30444
31444
  // Update state so widgets use the URL-derived time range
30445
31445
  const sharedSelection = this.toSharedSelection(urlSelection);
30446
31446
  const showTime = timeFilter.pickerConfig?.showTime ?? false;
30447
- const range = TimeRangeUtils.getTimeRangeFromSelection(sharedSelection, showTime);
31447
+ const range = TimeRangeUtils.getTimeRangeFromSelection(sharedSelection, showTime, this.stateService.timeZoneMode());
30448
31448
  if (range) {
30449
31449
  const rangeISO = TimeRangeUtils.toISO(range);
30450
31450
  this.stateService.updateTimeFilterSelection(urlSelection, rangeISO.from, rangeISO.to);
@@ -30461,7 +31461,7 @@ class MeshBoardViewComponent {
30461
31461
  this._urlTimeSelection.set(selection);
30462
31462
  const sharedSelection = this.toSharedSelection(selection);
30463
31463
  const showTime = timeFilter.pickerConfig?.showTime ?? false;
30464
- const range = TimeRangeUtils.getTimeRangeFromSelection(sharedSelection, showTime);
31464
+ const range = TimeRangeUtils.getTimeRangeFromSelection(sharedSelection, showTime, this.stateService.timeZoneMode());
30465
31465
  if (range) {
30466
31466
  const rangeISO = TimeRangeUtils.toISO(range);
30467
31467
  this.stateService.updateTimeFilterSelection(selection, rangeISO.from, rangeISO.to);
@@ -30470,7 +31470,21 @@ class MeshBoardViewComponent {
30470
31470
  }
30471
31471
  const sharedSelection = this.toSharedSelection(selection);
30472
31472
  const showTime = timeFilter.pickerConfig?.showTime ?? false;
30473
- const range = TimeRangeUtils.getTimeRangeFromSelection(sharedSelection, showTime);
31473
+ const range = TimeRangeUtils.getTimeRangeFromSelection(sharedSelection, showTime, this.stateService.timeZoneMode());
31474
+ if (!range) {
31475
+ return;
31476
+ }
31477
+ const rangeISO = TimeRangeUtils.toISO(range);
31478
+ this.stateService.setTimeFilterVariables(rangeISO.from, rangeISO.to);
31479
+ }
31480
+ /**
31481
+ * Recomputes the active time-filter range from the current selection on the
31482
+ * board's (possibly just-changed) timezone mode and republishes the
31483
+ * `$timeRangeFrom`/`$timeRangeTo` variables. No-op when the filter is
31484
+ * disabled or no selection is active.
31485
+ */
31486
+ reapplyTimeFilterRange() {
31487
+ const range = this.stateService.resolveCurrentTimeRange();
30474
31488
  if (!range) {
30475
31489
  return;
30476
31490
  }
@@ -30540,6 +31554,11 @@ class MeshBoardViewComponent {
30540
31554
  ...settingsResult,
30541
31555
  entitySelectors: settingsResult.entitySelectors
30542
31556
  });
31557
+ // The timezone mode and/or default selection may have changed — recompute
31558
+ // the active time-filter range on the new basis so $timeRangeFrom/To and
31559
+ // every widget's display stay consistent, then reload widget data.
31560
+ this.reapplyTimeFilterRange();
31561
+ await this.refresh();
30543
31562
  // Enter edit mode if not already in it
30544
31563
  if (!this.isEditMode()) {
30545
31564
  this.editModeService.enterEditMode(this.stateService.getConfig());
@@ -30730,7 +31749,7 @@ class MeshBoardViewComponent {
30730
31749
  }
30731
31750
  // Calculate the time range from the selection
30732
31751
  const showTime = this.stateService.getTimeFilterConfig()?.pickerConfig?.showTime ?? false;
30733
- const range = TimeRangeUtils.getTimeRangeFromSelection(sharedSelection, showTime);
31752
+ const range = TimeRangeUtils.getTimeRangeFromSelection(sharedSelection, showTime, this.stateService.timeZoneMode());
30734
31753
  if (!range)
30735
31754
  return;
30736
31755
  // Convert to ISO strings
@@ -30756,7 +31775,7 @@ class MeshBoardViewComponent {
30756
31775
  const defaultSel = config.defaultSelection;
30757
31776
  const sharedSelection = this.toSharedSelection(defaultSel);
30758
31777
  const showTime = config.pickerConfig?.showTime ?? false;
30759
- const range = TimeRangeUtils.getTimeRangeFromSelection(sharedSelection, showTime);
31778
+ const range = TimeRangeUtils.getTimeRangeFromSelection(sharedSelection, showTime, this.stateService.timeZoneMode());
30760
31779
  if (!range)
30761
31780
  return;
30762
31781
  const rangeISO = TimeRangeUtils.toISO(range);
@@ -31096,7 +32115,9 @@ class MeshBoardViewComponent {
31096
32115
  isWidgetUnconfigured(widget) {
31097
32116
  const dataSource = widget.dataSource;
31098
32117
  if (dataSource.type === 'runtimeEntity') {
31099
- return !dataSource.rtId && !dataSource.ckTypeId;
32118
+ // An entity-selector binding (or a variable-bearing rtId/ckTypeId) counts
32119
+ // as configured — the entity is resolved live from a board-level selection.
32120
+ return !dataSource.entitySelectorId && !dataSource.rtId && !dataSource.ckTypeId;
31100
32121
  }
31101
32122
  if (dataSource.type === 'persistentQuery') {
31102
32123
  return !dataSource.queryRtId;
@@ -31240,6 +32261,16 @@ class MeshBoardViewComponent {
31240
32261
  if (!values.some(v => v.name === rtIdVariableName)) {
31241
32262
  values.push({ name: rtIdVariableName, value: rtId, type: 'string' });
31242
32263
  }
32264
+ // Auto-expose the selected entity's CK type as `$<selectorId>_rtCkTypeId`
32265
+ // so entity-display widgets (e.g. the entity card) can resolve both the
32266
+ // rtId and its type from a single board-level selection. Prefer the picked
32267
+ // entity's actual type (it may be a subtype of the selector's configured
32268
+ // `ckTypeId`); fall back to the selector's type. An explicit mapping to the
32269
+ // same variable name wins.
32270
+ const rtCkTypeIdVariableName = `${selector.id}_rtCkTypeId`;
32271
+ if (!values.some(v => v.name === rtCkTypeIdVariableName)) {
32272
+ values.push({ name: rtCkTypeIdVariableName, value: entity.ckTypeId ?? selector.ckTypeId, type: 'string' });
32273
+ }
31243
32274
  this.stateService.setEntitySelectorVariables(selector.id, values);
31244
32275
  // Resolve the stream-data scope rtIds for this selection (one-hop
31245
32276
  // childScope traversal, or the picked entity itself). Runs alongside the
@@ -31377,5 +32408,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
31377
32408
  * Generated bundle index. Do not edit.
31378
32409
  */
31379
32410
 
31380
- export { AddWidgetDialogComponent, AiInsightsConfigDialogComponent, AiInsightsService, AiInsightsWidgetComponent, AlertBannerConfigDialogComponent, AlertBannerWidgetComponent, AlertListConfigDialogComponent, AlertListWidgetComponent, AssociationsConfigDialogComponent, AutoRefreshTimerService, BarChartConfigDialogComponent, BarChartWidgetComponent, DashboardDataService, DashboardGridService, EditModeStateService, EditWidgetDialogComponent, EntityAssociationsWidgetComponent, EntityCardConfigDialogComponent, EntityCardWidgetComponent, EntityDetailDialogComponent, EntitySelectorEditorComponent, EntitySelectorToolbarComponent, GaugeConfigDialogComponent, GaugeWidgetComponent, HeatmapConfigDialogComponent, HeatmapWidgetComponent, KpiConfigDialogComponent, KpiWidgetComponent, LineChartConfigDialogComponent, LineChartWidgetComponent, MESHBOARD_OPTIONS, MESHBOARD_TENANT_ID_PROVIDER, MarkdownConfigDialogComponent, MarkdownWidgetComponent, MeshBoardDataService, MeshBoardGridService, MeshBoardManagerDialogComponent, MeshBoardPersistenceService, MeshBoardSettingsDialogComponent, MeshBoardSettingsResult, MeshBoardStateService, MeshBoardViewComponent, PieChartConfigDialogComponent, PieChartWidgetComponent, QueryExecutorService, QuerySelectorComponent, RuntimeEntitySelectorComponent, ServiceHealthConfigDialogComponent, ServiceHealthWidgetComponent, StatsGridConfigDialogComponent, StatsGridWidgetComponent, StatusIndicatorConfigDialogComponent, StatusIndicatorWidgetComponent, StatusListConfigDialogComponent, StatusListWidgetComponent, SummaryCardConfigDialogComponent, SummaryCardWidgetComponent, TableConfigDialogComponent, TableWidgetComponent, TableWidgetDataSourceDirective, WidgetFactoryService, WidgetGroupComponent, WidgetGroupConfigDialogComponent, WidgetNotConfiguredComponent, WidgetRegistryService, classifyQuery, provideDefaultWidgets, provideMeshBoard, provideProcessWidget, provideWidgetRegistrations, queryFamily, registerDefaultWidgets, registerProcessWidget };
32411
+ export { AddWidgetDialogComponent, AiInsightsConfigDialogComponent, AiInsightsService, AiInsightsWidgetComponent, AlertBannerConfigDialogComponent, AlertBannerWidgetComponent, AlertListConfigDialogComponent, AlertListWidgetComponent, AssociationsConfigDialogComponent, AutoRefreshTimerService, BarChartConfigDialogComponent, BarChartWidgetComponent, DEFAULT_TIME_ZONE_MODE, DashboardDataService, DashboardGridService, EditModeStateService, EditWidgetDialogComponent, EntityAssociationsWidgetComponent, EntityCardConfigDialogComponent, EntityCardWidgetComponent, EntityDetailDialogComponent, EntitySelectorEditorComponent, EntitySelectorToolbarComponent, GaugeConfigDialogComponent, GaugeWidgetComponent, HeatmapConfigDialogComponent, HeatmapWidgetComponent, KpiConfigDialogComponent, KpiWidgetComponent, LineChartConfigDialogComponent, LineChartWidgetComponent, MESHBOARD_OPTIONS, MESHBOARD_TENANT_ID_PROVIDER, MarkdownConfigDialogComponent, MarkdownWidgetComponent, MeshBoardDataService, MeshBoardGridService, MeshBoardManagerDialogComponent, MeshBoardPersistenceService, MeshBoardSettingsDialogComponent, MeshBoardSettingsResult, MeshBoardStateService, MeshBoardViewComponent, PieChartConfigDialogComponent, PieChartWidgetComponent, QueryExecutorService, QuerySelectorComponent, RuntimeEntitySelectorComponent, ServiceHealthConfigDialogComponent, ServiceHealthWidgetComponent, StatsGridConfigDialogComponent, StatsGridWidgetComponent, StatusIndicatorConfigDialogComponent, StatusIndicatorWidgetComponent, StatusListConfigDialogComponent, StatusListWidgetComponent, SummaryCardConfigDialogComponent, SummaryCardWidgetComponent, TableConfigDialogComponent, TableWidgetComponent, TableWidgetDataSourceDirective, WidgetFactoryService, WidgetGroupComponent, WidgetGroupConfigDialogComponent, WidgetNotConfiguredComponent, WidgetRegistryService, classifyQuery, provideDefaultWidgets, provideMeshBoard, provideProcessWidget, provideWidgetRegistrations, queryFamily, registerDefaultWidgets, registerProcessWidget };
31381
32412
  //# sourceMappingURL=meshmakers-octo-meshboard.mjs.map