@meshmakers/octo-meshboard 3.4.160 → 3.4.180

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.
@@ -1,4 +1,4 @@
1
- import { FieldFilterOperatorsDto, CkTypeSelectorService, RuntimeEntitySelectDataSource as RuntimeEntitySelectDataSource$2, RuntimeEntityDialogDataSource as RuntimeEntityDialogDataSource$2, DeleteStrategiesDto, AssociationModOptionsDto, CkModelService, GraphDirectionDto, AttributeSelectorService, GraphQL, GetCkTypeAvailableQueryColumnsDtoGQL, SortOrdersDto, HealthService, TENANT_ID_PROVIDER, AssetRepoService, JobManagementService } from '@meshmakers/octo-services';
1
+ import { FieldFilterOperatorsDto, CkTypeSelectorService, RuntimeEntitySelectDataSource as RuntimeEntitySelectDataSource$2, RuntimeEntityDialogDataSource as RuntimeEntityDialogDataSource$2, DeleteStrategiesDto, AssociationModOptionsDto, CkModelService, QueryModeDto, GraphDirectionDto, AttributeSelectorService, GraphQL, GetCkTypeAvailableQueryColumnsDtoGQL, SortOrdersDto, HealthService, TENANT_ID_PROVIDER, AssetRepoService, JobManagementService } from '@meshmakers/octo-services';
2
2
  export { RuntimeEntityDialogDataSource, RuntimeEntitySelectDataSource, TENANT_ID_PROVIDER } from '@meshmakers/octo-services';
3
3
  import * as i0 from '@angular/core';
4
4
  import { Injectable, inject, EventEmitter, forwardRef, Output, Input, ViewChild, Component, Injector, EnvironmentInjector, ApplicationRef, signal, computed, Directive, makeEnvironmentProviders, provideAppInitializer, InjectionToken, effect } from '@angular/core';
@@ -6,7 +6,7 @@ import * as i1$1 from '@angular/forms';
6
6
  import { NG_VALUE_ACCESSOR, FormsModule } from '@angular/forms';
7
7
  import { firstValueFrom, from, map, Observable, of, catchError, interval, filter } from 'rxjs';
8
8
  import * as i1$8 from '@meshmakers/shared-ui';
9
- import { EntitySelectInputComponent, WindowStateService, ListViewComponent, FetchResultTyped, DataSourceBase, TimeRangePickerComponent, ImportStrategyDialogService, TimeRangeUtils, HAS_UNSAVED_CHANGES, UnsavedChangesDirective } from '@meshmakers/shared-ui';
9
+ import { EntitySelectInputComponent, WindowStateService, TimeRangeUtils, ListViewComponent, FetchResultTyped, DataSourceBase, TimeRangePickerComponent, ImportStrategyDialogService, HAS_UNSAVED_CHANGES, UnsavedChangesDirective } from '@meshmakers/shared-ui';
10
10
  import * as i1 from 'apollo-angular';
11
11
  import { gql, Apollo } from 'apollo-angular';
12
12
  import * as i1$3 from '@angular/common';
@@ -14,6 +14,7 @@ import { CommonModule } from '@angular/common';
14
14
  import { CkTypeSelectorInputComponent, AttributeValueTypeDto, PropertyValueDisplayComponent, FieldFilterEditorComponent, PropertyGridComponent, OctoGraphQlDataSource, AttributeSelectorDialogService, AttributeSortSelectorDialogService } from '@meshmakers/octo-ui';
15
15
  import * as i1$5 from '@progress/kendo-angular-dialog';
16
16
  import { WindowService, WindowCloseResult, WindowRef, DialogsModule, DialogRef, DialogModule, DialogService } from '@progress/kendo-angular-dialog';
17
+ import { switchMap, map as map$1, catchError as catchError$1 } from 'rxjs/operators';
17
18
  import * as i2 from '@progress/kendo-angular-buttons';
18
19
  import { ButtonsModule, ButtonModule } from '@progress/kendo-angular-buttons';
19
20
  import * as i3 from '@progress/kendo-angular-inputs';
@@ -25,7 +26,6 @@ import { SVGIconModule } from '@progress/kendo-angular-icons';
25
26
  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';
26
27
  import * as i4 from '@progress/kendo-angular-dropdowns';
27
28
  import { DropDownsModule, DropDownListModule } from '@progress/kendo-angular-dropdowns';
28
- import { map as map$1, catchError as catchError$1 } from 'rxjs/operators';
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';
@@ -52,6 +52,56 @@ import { BreadCrumbService } from '@meshmakers/shared-services';
52
52
 
53
53
  // Re-export from octo-services (moved there for reuse by octo-ui)
54
54
 
55
+ /**
56
+ * Persistent-query family classification.
57
+ *
58
+ * The query builder persists both runtime-data and stream-data queries as
59
+ * `systemPersistentQuery` entities. They are distinguished by the
60
+ * `queryCkTypeId` field, which carries a substring matching one of the
61
+ * known kinds below.
62
+ *
63
+ * Ordering matters: `GroupingAggregationSdQuery` contains `AggregationSdQuery`
64
+ * as a substring, so the grouping variant must be tested first.
65
+ */
66
+ const STREAM_DATA_RULES = [
67
+ { marker: 'DownsamplingSdQuery', kind: 'downsampling' },
68
+ { marker: 'GroupingAggregationSdQuery', kind: 'groupingAggregation' },
69
+ { marker: 'AggregationSdQuery', kind: 'aggregation' },
70
+ { marker: 'SimpleSdQuery', kind: 'simple' }
71
+ ];
72
+ const RUNTIME_RULES = [
73
+ { marker: 'GroupingAggregationRtQuery', kind: 'groupingAggregation' },
74
+ { marker: 'AggregationRtQuery', kind: 'aggregation' },
75
+ { marker: 'SimpleRtQuery', kind: 'simple' }
76
+ ];
77
+ /**
78
+ * Classify a persistent query by its `queryCkTypeId`.
79
+ * Returns `null` for unknown / legacy values; callers decide how to handle them.
80
+ */
81
+ function classifyQuery(queryCkTypeId) {
82
+ if (!queryCkTypeId) {
83
+ return null;
84
+ }
85
+ for (const rule of STREAM_DATA_RULES) {
86
+ if (queryCkTypeId.includes(rule.marker)) {
87
+ return { family: 'streamData', kind: rule.kind };
88
+ }
89
+ }
90
+ for (const rule of RUNTIME_RULES) {
91
+ if (queryCkTypeId.includes(rule.marker)) {
92
+ return { family: 'runtime', kind: rule.kind };
93
+ }
94
+ }
95
+ return null;
96
+ }
97
+ /**
98
+ * Convenience accessor — returns just the family ('runtime' | 'streamData')
99
+ * or null when the query type is unrecognised.
100
+ */
101
+ function queryFamily(queryCkTypeId) {
102
+ return classifyQuery(queryCkTypeId)?.family ?? null;
103
+ }
104
+
55
105
  const GetSystemPersistentQueriesDocumentDto = gql `
56
106
  query getSystemPersistentQueries($after: String, $first: Int, $searchFilter: SearchFilter, $fieldFilters: [FieldFilter], $sort: [Sort]) {
57
107
  runtime {
@@ -89,14 +139,39 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
89
139
  }]
90
140
  }], ctorParameters: () => [{ type: i1.Apollo }] });
91
141
 
142
+ /**
143
+ * Filter the result list down to queries whose family is in the accept list.
144
+ * Family is classified by the persistent-query entity's own CK type
145
+ * (`ckTypeId`, e.g. `RtSimpleSdQuery`) — NOT the target type
146
+ * (`queryCkTypeId`, e.g. `Basic.Energy/EnergyMeasurement`), which has nothing
147
+ * to do with runtime vs stream-data.
148
+ *
149
+ * `null` family (unrecognised legacy query type) is kept only when 'runtime'
150
+ * is among the accepted families — historical behavior treated everything as
151
+ * runtime-compatible.
152
+ */
153
+ function filterByFamily(items, accept) {
154
+ if (accept.length === 0) {
155
+ return items;
156
+ }
157
+ return items.filter(item => {
158
+ const family = queryFamily(item.ckTypeId);
159
+ if (family === null) {
160
+ return accept.includes('runtime');
161
+ }
162
+ return accept.includes(family);
163
+ });
164
+ }
92
165
  /**
93
166
  * Autocomplete data source for persistent query selection.
94
- * Filters queries by search text using GraphQL.
167
+ * Filters queries by search text using GraphQL, then narrows by family on the client.
95
168
  */
96
169
  class PersistentQueryAutocompleteDataSource {
97
170
  gql;
98
- constructor(gql) {
171
+ acceptFamilies;
172
+ constructor(gql, acceptFamilies = ['runtime', 'streamData']) {
99
173
  this.gql = gql;
174
+ this.acceptFamilies = acceptFamilies;
100
175
  }
101
176
  async onFilter(filter, take = 50) {
102
177
  const result = await firstValueFrom(this.gql.fetch({
@@ -105,14 +180,16 @@ class PersistentQueryAutocompleteDataSource {
105
180
  fieldFilters: filter ? [{ attributePath: 'name', operator: FieldFilterOperatorsDto.LikeDto, comparisonValue: filter }] : undefined
106
181
  }
107
182
  }));
108
- const items = (result.data?.runtime?.systemPersistentQuery?.items ?? [])
183
+ const rawItems = (result.data?.runtime?.systemPersistentQuery?.items ?? [])
109
184
  .filter((item) => item !== null)
110
185
  .map(item => ({
111
186
  rtId: item.rtId,
112
187
  name: item.name ?? '',
113
188
  description: item.description,
189
+ ckTypeId: item.ckTypeId,
114
190
  queryCkTypeId: item.queryCkTypeId
115
191
  }));
192
+ const items = filterByFamily(rawItems, this.acceptFamilies);
116
193
  return {
117
194
  totalCount: result.data?.runtime?.systemPersistentQuery?.totalCount ?? 0,
118
195
  items
@@ -127,12 +204,14 @@ class PersistentQueryAutocompleteDataSource {
127
204
  }
128
205
  /**
129
206
  * Dialog data source for persistent query selection grid.
130
- * Provides columns and paginated data for the entity select dialog.
207
+ * Provides columns and paginated data for the entity select dialog, narrowed by family on the client.
131
208
  */
132
209
  class PersistentQueryDialogDataSource {
133
210
  gql;
134
- constructor(gql) {
211
+ acceptFamilies;
212
+ constructor(gql, acceptFamilies = ['runtime', 'streamData']) {
135
213
  this.gql = gql;
214
+ this.acceptFamilies = acceptFamilies;
136
215
  }
137
216
  getColumns() {
138
217
  return [
@@ -149,7 +228,7 @@ class PersistentQueryDialogDataSource {
149
228
  fieldFilters: options.textSearch ? [{ attributePath: 'name', operator: FieldFilterOperatorsDto.LikeDto, comparisonValue: options.textSearch }] : undefined
150
229
  }
151
230
  })).pipe(map(result => {
152
- const items = (result.data?.runtime?.systemPersistentQuery?.items ?? [])
231
+ const rawItems = (result.data?.runtime?.systemPersistentQuery?.items ?? [])
153
232
  .filter((item) => item !== null)
154
233
  .map(item => ({
155
234
  rtId: item.rtId,
@@ -157,6 +236,7 @@ class PersistentQueryDialogDataSource {
157
236
  description: item.description,
158
237
  queryCkTypeId: item.queryCkTypeId
159
238
  }));
239
+ const items = filterByFamily(rawItems, this.acceptFamilies);
160
240
  return {
161
241
  data: items,
162
242
  totalCount: result.data?.runtime?.systemPersistentQuery?.totalCount ?? 0
@@ -194,6 +274,17 @@ class QuerySelectorComponent {
194
274
  hint;
195
275
  /** Whether the component is disabled */
196
276
  disabled = false;
277
+ /**
278
+ * Which query families to show in the picker.
279
+ * Default: both runtime and stream-data.
280
+ * Set to `['runtime']` for legacy widgets that cannot consume stream-data,
281
+ * or `['streamData']` for stream-data-only pickers.
282
+ *
283
+ * Filtering happens client-side after a server-side fetch — small per-tenant
284
+ * query counts make this acceptable; a server-side filter would require a
285
+ * backend change.
286
+ */
287
+ acceptFamilies = ['runtime', 'streamData'];
197
288
  /** Emitted when a query is selected */
198
289
  querySelected = new EventEmitter();
199
290
  /** Emitted when queries are loaded (emits the selected query in a single-item array for compatibility) */
@@ -205,8 +296,16 @@ class QuerySelectorComponent {
205
296
  onChange = () => { };
206
297
  onTouched = () => { };
207
298
  constructor() {
208
- this.queryDataSource = new PersistentQueryAutocompleteDataSource(this.getSystemPersistentQueriesGQL);
209
- this.queryDialogDataSource = new PersistentQueryDialogDataSource(this.getSystemPersistentQueriesGQL);
299
+ this.rebuildDataSources();
300
+ }
301
+ ngOnChanges(changes) {
302
+ if (changes['acceptFamilies']) {
303
+ this.rebuildDataSources();
304
+ }
305
+ }
306
+ rebuildDataSources() {
307
+ this.queryDataSource = new PersistentQueryAutocompleteDataSource(this.getSystemPersistentQueriesGQL, this.acceptFamilies);
308
+ this.queryDialogDataSource = new PersistentQueryDialogDataSource(this.getSystemPersistentQueriesGQL, this.acceptFamilies);
210
309
  }
211
310
  ngAfterViewInit() {
212
311
  // Forward any value that was set via writeValue() before the ViewChild was available
@@ -250,6 +349,7 @@ class QuerySelectorComponent {
250
349
  rtId: item.rtId,
251
350
  name: item.name ?? '',
252
351
  description: item.description,
352
+ ckTypeId: item.ckTypeId,
253
353
  queryCkTypeId: item.queryCkTypeId
254
354
  }));
255
355
  const query = items[0] ?? null;
@@ -277,13 +377,13 @@ class QuerySelectorComponent {
277
377
  this.disabled = isDisabled;
278
378
  }
279
379
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QuerySelectorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
280
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: QuerySelectorComponent, isStandalone: true, selector: "mm-query-selector", inputs: { placeholder: "placeholder", hint: "hint", disabled: "disabled" }, outputs: { querySelected: "querySelected", queriesLoaded: "queriesLoaded" }, providers: [
380
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: QuerySelectorComponent, isStandalone: true, selector: "mm-query-selector", inputs: { placeholder: "placeholder", hint: "hint", disabled: "disabled", acceptFamilies: "acceptFamilies" }, outputs: { querySelected: "querySelected", queriesLoaded: "queriesLoaded" }, providers: [
281
381
  {
282
382
  provide: NG_VALUE_ACCESSOR,
283
383
  useExisting: forwardRef(() => QuerySelectorComponent),
284
384
  multi: true
285
385
  }
286
- ], viewQueries: [{ propertyName: "entitySelect", first: true, predicate: ["entitySelect"], descendants: true }], ngImport: i0, template: `
386
+ ], viewQueries: [{ propertyName: "entitySelect", first: true, predicate: ["entitySelect"], descendants: true }], usesOnChanges: true, ngImport: i0, template: `
287
387
  <div class="query-selector">
288
388
  <mm-entity-select-input
289
389
  #entitySelect
@@ -341,6 +441,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
341
441
  type: Input
342
442
  }], disabled: [{
343
443
  type: Input
444
+ }], acceptFamilies: [{
445
+ type: Input
344
446
  }], querySelected: [{
345
447
  type: Output
346
448
  }], queriesLoaded: [{
@@ -1401,7 +1503,7 @@ class MeshBoardPersistenceService {
1401
1503
  */
1402
1504
  async createMeshBoard(config) {
1403
1505
  // Encode variables, timeFilter, and entitySelectors in description field (temporary until backend adds config field)
1404
- const encodedDescription = this.encodeVariablesInDescription(config.description ?? '', config.variables, config.timeFilter, config.entitySelectors);
1506
+ const encodedDescription = this.encodeVariablesInDescription(config.description ?? '', config.variables, config.timeFilter, config.entitySelectors, config.autoRefreshSeconds);
1405
1507
  const dashboardInput = {
1406
1508
  name: config.name,
1407
1509
  description: encodedDescription,
@@ -1433,7 +1535,7 @@ class MeshBoardPersistenceService {
1433
1535
  async updateMeshBoard(rtId, config, existingWidgetRtIds = []) {
1434
1536
  const result = { createdWidgets: [] };
1435
1537
  // Encode variables, timeFilter, and entitySelectors in description field (temporary until backend adds config field)
1436
- const encodedDescription = this.encodeVariablesInDescription(config.description ?? '', config.variables, config.timeFilter, config.entitySelectors);
1538
+ const encodedDescription = this.encodeVariablesInDescription(config.description ?? '', config.variables, config.timeFilter, config.entitySelectors, config.autoRefreshSeconds);
1437
1539
  const dashboardItem = {
1438
1540
  name: config.name,
1439
1541
  description: encodedDescription,
@@ -1495,8 +1597,9 @@ class MeshBoardPersistenceService {
1495
1597
  * Converts a persisted MeshBoard to MeshBoardConfig
1496
1598
  */
1497
1599
  toMeshBoardConfig(meshBoard, widgets) {
1498
- // Decode variables, timeFilter, and entitySelectors from description field (temporary until backend adds config field)
1499
- const { description, variables, timeFilter, entitySelectors } = this.decodeVariablesFromDescription(meshBoard.description);
1600
+ // Decode variables, timeFilter, entitySelectors, and autoRefresh from description field
1601
+ // (temporary until backend adds first-class config field)
1602
+ const { description, variables, timeFilter, entitySelectors, autoRefreshSeconds } = this.decodeVariablesFromDescription(meshBoard.description);
1500
1603
  return {
1501
1604
  id: meshBoard.rtId,
1502
1605
  name: meshBoard.name,
@@ -1508,6 +1611,7 @@ class MeshBoardPersistenceService {
1508
1611
  variables,
1509
1612
  timeFilter,
1510
1613
  entitySelectors,
1614
+ autoRefreshSeconds,
1511
1615
  widgets: widgets.map(w => this.toWidgetConfig(w))
1512
1616
  };
1513
1617
  }
@@ -1629,12 +1733,13 @@ class MeshBoardPersistenceService {
1629
1733
  * Note: Only static variables are persisted. TimeFilter variables are derived
1630
1734
  * from the timeFilter selection when the MeshBoard is loaded.
1631
1735
  */
1632
- encodeVariablesInDescription(description, variables, timeFilter, entitySelectors) {
1736
+ encodeVariablesInDescription(description, variables, timeFilter, entitySelectors, autoRefreshSeconds) {
1633
1737
  // Filter out timeFilter and entitySelector variables (they are derived, not persisted directly)
1634
1738
  const staticVariables = variables?.filter(v => v.source !== 'timeFilter' && v.source !== 'entitySelector');
1635
1739
  // Check if there's anything to encode
1636
1740
  const hasEntitySelectors = entitySelectors && entitySelectors.length > 0;
1637
- if ((!staticVariables || staticVariables.length === 0) && !timeFilter?.enabled && !hasEntitySelectors) {
1741
+ const hasAutoRefresh = !!autoRefreshSeconds && autoRefreshSeconds > 0;
1742
+ if ((!staticVariables || staticVariables.length === 0) && !timeFilter?.enabled && !hasEntitySelectors && !hasAutoRefresh) {
1638
1743
  return description;
1639
1744
  }
1640
1745
  try {
@@ -1645,6 +1750,9 @@ class MeshBoardPersistenceService {
1645
1750
  if (timeFilter?.enabled) {
1646
1751
  data.timeFilter = timeFilter;
1647
1752
  }
1753
+ if (hasAutoRefresh) {
1754
+ data.autoRefreshSeconds = autoRefreshSeconds;
1755
+ }
1648
1756
  if (hasEntitySelectors) {
1649
1757
  // Strip transient fields before persisting
1650
1758
  data.entitySelectors = entitySelectors.map(es => ({
@@ -1691,12 +1799,13 @@ class MeshBoardPersistenceService {
1691
1799
  if (Array.isArray(parsed)) {
1692
1800
  return { description, variables: parsed };
1693
1801
  }
1694
- // New format: object with variables, timeFilter, and entitySelectors
1802
+ // New format: object with variables, timeFilter, entitySelectors, and autoRefreshSeconds
1695
1803
  return {
1696
1804
  description,
1697
1805
  variables: parsed.variables ?? [],
1698
1806
  timeFilter: parsed.timeFilter,
1699
- entitySelectors: parsed.entitySelectors
1807
+ entitySelectors: parsed.entitySelectors,
1808
+ autoRefreshSeconds: typeof parsed.autoRefreshSeconds === 'number' ? parsed.autoRefreshSeconds : undefined
1700
1809
  };
1701
1810
  }
1702
1811
  catch (error) {
@@ -2144,7 +2253,8 @@ class MeshBoardStateService {
2144
2253
  selection: settings.timeFilter.defaultSelection ?? config.timeFilter?.selection
2145
2254
  }
2146
2255
  : config.timeFilter,
2147
- entitySelectors: settings.entitySelectors ?? config.entitySelectors
2256
+ entitySelectors: settings.entitySelectors ?? config.entitySelectors,
2257
+ autoRefreshSeconds: settings.autoRefreshSeconds
2148
2258
  }));
2149
2259
  // If time filter is disabled, clear the time filter variables
2150
2260
  if (settings.timeFilter && !settings.timeFilter.enabled) {
@@ -2165,7 +2275,8 @@ class MeshBoardStateService {
2165
2275
  gap: config.gap,
2166
2276
  variables: config.variables ?? [],
2167
2277
  timeFilter: config.timeFilter,
2168
- entitySelectors: config.entitySelectors
2278
+ entitySelectors: config.entitySelectors,
2279
+ autoRefreshSeconds: config.autoRefreshSeconds
2169
2280
  };
2170
2281
  }
2171
2282
  /**
@@ -2331,6 +2442,33 @@ class MeshBoardStateService {
2331
2442
  getTimeFilterConfig() {
2332
2443
  return this._meshBoardConfig().timeFilter;
2333
2444
  }
2445
+ /**
2446
+ * Resolves the current time-filter selection to a concrete UTC `{from, to}`
2447
+ * range. Returns `null` when the filter is disabled, no selection exists,
2448
+ * or the selection is incomplete.
2449
+ *
2450
+ * Stream-data persistent queries consume this to bound their result set;
2451
+ * runtime queries ignore it.
2452
+ *
2453
+ * IANA-timezone-aware bucket boundaries are tracked separately (AB#4190);
2454
+ * this helper currently returns UTC boundaries derived from the picker.
2455
+ */
2456
+ resolveCurrentTimeRange() {
2457
+ const config = this._meshBoardConfig().timeFilter;
2458
+ if (!config?.enabled || !config.selection) {
2459
+ return null;
2460
+ }
2461
+ const showTime = config.pickerConfig?.showTime ?? false;
2462
+ // The model stores customFrom/customTo as ISO strings for JSON persistence,
2463
+ // shared-ui's TimeRangeUtils expects Date objects — convert before delegating.
2464
+ const selection = config.selection;
2465
+ const sharedSelection = {
2466
+ ...selection,
2467
+ customFrom: selection.customFrom ? new Date(selection.customFrom) : undefined,
2468
+ customTo: selection.customTo ? new Date(selection.customTo) : undefined
2469
+ };
2470
+ return TimeRangeUtils.getTimeRangeFromSelection(sharedSelection, showTime);
2471
+ }
2334
2472
  /**
2335
2473
  * Updates the time filter configuration.
2336
2474
  */
@@ -2587,6 +2725,279 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
2587
2725
  }]
2588
2726
  }], ctorParameters: () => [{ type: i1.Apollo }] });
2589
2727
 
2728
+ const ExecuteStreamDataQueryDocumentDto = gql `
2729
+ query executeStreamDataQuery($rtId: OctoObjectId!, $arg: StreamDataArguments, $first: Int, $after: String, $sortOrder: [Sort], $fieldFilter: [FieldFilter]) {
2730
+ streamData {
2731
+ streamDataQuery(rtId: $rtId) {
2732
+ items {
2733
+ queryRtId
2734
+ associatedCkTypeId
2735
+ columns {
2736
+ attributePath
2737
+ attributeValueType
2738
+ aggregationType
2739
+ }
2740
+ rows(
2741
+ arg: $arg
2742
+ first: $first
2743
+ after: $after
2744
+ sortOrder: $sortOrder
2745
+ fieldFilter: $fieldFilter
2746
+ ) {
2747
+ totalCount
2748
+ pageInfo {
2749
+ hasNextPage
2750
+ endCursor
2751
+ }
2752
+ items {
2753
+ rtId
2754
+ ckTypeId
2755
+ timestamp
2756
+ rtWellKnownName
2757
+ rtCreationDateTime
2758
+ rtChangedDateTime
2759
+ cells {
2760
+ items {
2761
+ attributePath
2762
+ value
2763
+ }
2764
+ }
2765
+ }
2766
+ }
2767
+ }
2768
+ }
2769
+ }
2770
+ }
2771
+ `;
2772
+ class ExecuteStreamDataQueryDtoGQL extends i1.Query {
2773
+ document = ExecuteStreamDataQueryDocumentDto;
2774
+ constructor(apollo) {
2775
+ super(apollo);
2776
+ }
2777
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ExecuteStreamDataQueryDtoGQL, deps: [{ token: i1.Apollo }], target: i0.ɵɵFactoryTarget.Injectable });
2778
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ExecuteStreamDataQueryDtoGQL, providedIn: 'root' });
2779
+ }
2780
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ExecuteStreamDataQueryDtoGQL, decorators: [{
2781
+ type: Injectable,
2782
+ args: [{
2783
+ providedIn: 'root'
2784
+ }]
2785
+ }], ctorParameters: () => [{ type: i1.Apollo }] });
2786
+
2787
+ const EMPTY_RESULT_BASE = Object.freeze({
2788
+ queryRtId: null,
2789
+ associatedCkTypeId: null,
2790
+ columns: [],
2791
+ rows: [],
2792
+ totalCount: 0,
2793
+ hasNextPage: false,
2794
+ endCursor: null
2795
+ });
2796
+ /**
2797
+ * Executes persistent queries by rtId and returns a unified result shape
2798
+ * regardless of whether the underlying query is runtime-data or stream-data.
2799
+ *
2800
+ * Widgets consume `QueryExecutionResult` and stay agnostic about which family
2801
+ * the saved query belongs to — switching a widget's data source between
2802
+ * runtime and stream-data is purely a configuration change.
2803
+ */
2804
+ class QueryExecutorService {
2805
+ runtimeGql = inject(ExecuteRuntimeQueryDtoGQL);
2806
+ streamDataGql = inject(ExecuteStreamDataQueryDtoGQL);
2807
+ persistentQueriesGql = inject(GetSystemPersistentQueriesDtoGQL);
2808
+ /**
2809
+ * Cache of resolved query families, keyed by query rtId. Filled lazily for
2810
+ * legacy widget configs that pre-date the `queryFamily` field — saves a
2811
+ * round-trip on every refresh.
2812
+ */
2813
+ familyCache = new Map();
2814
+ execute(family, queryRtId, options = {}) {
2815
+ if (family) {
2816
+ return family === 'streamData'
2817
+ ? this.executeStreamData(queryRtId, options)
2818
+ : this.executeRuntime(queryRtId, options);
2819
+ }
2820
+ // Family unknown — look it up from the persistent-query entity once and
2821
+ // route accordingly. Legacy widget configs (saved before queryFamily was
2822
+ // persisted) hit this path; subsequent calls in the same session use the
2823
+ // cached family.
2824
+ return from(this.resolveFamily(queryRtId)).pipe(switchMap(resolved => resolved === 'streamData'
2825
+ ? this.executeStreamData(queryRtId, options)
2826
+ : this.executeRuntime(queryRtId, options)));
2827
+ }
2828
+ /**
2829
+ * Resolves the family of a persistent query by rtId. Falls back to
2830
+ * `'runtime'` when the query type cannot be classified — this matches the
2831
+ * pre-Phase-1 behavior.
2832
+ */
2833
+ async resolveFamily(queryRtId) {
2834
+ const cached = this.familyCache.get(queryRtId);
2835
+ if (cached)
2836
+ return cached;
2837
+ try {
2838
+ const result = await firstValueFrom(this.persistentQueriesGql.fetch({
2839
+ variables: {
2840
+ first: 1,
2841
+ fieldFilters: [{ attributePath: 'rtId', operator: FieldFilterOperatorsDto.EqualsDto, comparisonValue: queryRtId }]
2842
+ }
2843
+ }));
2844
+ const item = result.data?.runtime?.systemPersistentQuery?.items?.[0];
2845
+ const resolved = queryFamily(item?.ckTypeId ?? null) ?? 'runtime';
2846
+ this.familyCache.set(queryRtId, resolved);
2847
+ return resolved;
2848
+ }
2849
+ catch (error) {
2850
+ console.warn('QueryExecutorService: family lookup failed for', queryRtId, '— defaulting to runtime', error);
2851
+ return 'runtime';
2852
+ }
2853
+ }
2854
+ executeRuntime(queryRtId, options = {}) {
2855
+ return this.runtimeGql.fetch({
2856
+ variables: {
2857
+ rtId: queryRtId,
2858
+ first: options.first ?? undefined,
2859
+ after: options.after ?? undefined,
2860
+ fieldFilter: options.fieldFilter ?? undefined
2861
+ },
2862
+ fetchPolicy: options.forceRefresh ? 'network-only' : 'cache-first'
2863
+ }).pipe(map$1(result => {
2864
+ const queryItem = result.data?.runtime?.runtimeQuery?.items?.[0];
2865
+ if (!queryItem) {
2866
+ return { family: 'runtime', ...EMPTY_RESULT_BASE };
2867
+ }
2868
+ return {
2869
+ family: 'runtime',
2870
+ queryRtId: queryItem.queryRtId ?? null,
2871
+ associatedCkTypeId: queryItem.associatedCkTypeId ?? null,
2872
+ columns: this.mapColumns(queryItem.columns),
2873
+ rows: this.mapRuntimeRows(queryItem.rows?.items),
2874
+ totalCount: queryItem.rows?.totalCount ?? 0,
2875
+ hasNextPage: queryItem.rows?.pageInfo?.hasNextPage ?? false,
2876
+ endCursor: queryItem.rows?.pageInfo?.endCursor ?? null
2877
+ };
2878
+ }));
2879
+ }
2880
+ executeStreamData(queryRtId, options = {}) {
2881
+ const arg = options.streamDataArgs ? this.buildStreamDataArg(options.streamDataArgs) : undefined;
2882
+ return this.streamDataGql.fetch({
2883
+ variables: {
2884
+ rtId: queryRtId,
2885
+ first: options.first ?? undefined,
2886
+ after: options.after ?? undefined,
2887
+ sortOrder: options.sortOrder ?? undefined,
2888
+ fieldFilter: options.fieldFilter ?? undefined,
2889
+ arg
2890
+ },
2891
+ fetchPolicy: options.forceRefresh ? 'network-only' : 'cache-first'
2892
+ }).pipe(map$1(result => {
2893
+ const queryItem = result.data?.streamData?.streamDataQuery?.items?.[0];
2894
+ if (!queryItem) {
2895
+ return { family: 'streamData', ...EMPTY_RESULT_BASE };
2896
+ }
2897
+ return {
2898
+ family: 'streamData',
2899
+ queryRtId: queryItem.queryRtId ?? null,
2900
+ associatedCkTypeId: queryItem.associatedCkTypeId ?? null,
2901
+ columns: this.mapColumns(queryItem.columns),
2902
+ rows: this.mapStreamDataRows(queryItem.rows?.items),
2903
+ totalCount: queryItem.rows?.totalCount ?? 0,
2904
+ hasNextPage: queryItem.rows?.pageInfo?.hasNextPage ?? false,
2905
+ endCursor: queryItem.rows?.pageInfo?.endCursor ?? null
2906
+ };
2907
+ }));
2908
+ }
2909
+ buildStreamDataArg(args) {
2910
+ // Skip the entire `arg` field when the caller has nothing to override —
2911
+ // the persisted query then runs with its intrinsic from/to/limit and the
2912
+ // GraphQL request stays minimal.
2913
+ const hasOverride = args.from != null || args.to != null || args.interval != null || args.limit != null || args.queryMode != null;
2914
+ if (!hasOverride) {
2915
+ return undefined;
2916
+ }
2917
+ // `queryMode` defaults to Default because the schema requires it. The
2918
+ // backend dispatcher ignores it (variant comes from the persisted entity's
2919
+ // CK subtype); see the type-level doc comment for the full story.
2920
+ return {
2921
+ from: args.from ?? undefined,
2922
+ to: args.to ?? undefined,
2923
+ interval: args.interval ?? undefined,
2924
+ limit: args.limit ?? undefined,
2925
+ queryMode: args.queryMode ?? QueryModeDto.DefaultDto
2926
+ };
2927
+ }
2928
+ mapColumns(columns) {
2929
+ if (!columns)
2930
+ return [];
2931
+ const result = [];
2932
+ for (const col of columns) {
2933
+ if (!col?.attributePath)
2934
+ continue;
2935
+ result.push({
2936
+ attributePath: col.attributePath,
2937
+ attributeValueType: col.attributeValueType ?? null,
2938
+ aggregationType: col.aggregationType ?? null
2939
+ });
2940
+ }
2941
+ return result;
2942
+ }
2943
+ mapRuntimeRows(rows) {
2944
+ if (!rows)
2945
+ return [];
2946
+ const result = [];
2947
+ for (const row of rows) {
2948
+ if (!row)
2949
+ continue;
2950
+ const r = row;
2951
+ result.push({
2952
+ __typename: r.__typename,
2953
+ rtId: r.rtId ?? null,
2954
+ ckTypeId: r.ckTypeId ?? null,
2955
+ cells: this.mapCells(r.cells?.items)
2956
+ });
2957
+ }
2958
+ return result;
2959
+ }
2960
+ mapStreamDataRows(rows) {
2961
+ if (!rows)
2962
+ return [];
2963
+ const result = [];
2964
+ for (const row of rows) {
2965
+ if (!row)
2966
+ continue;
2967
+ const r = row;
2968
+ const ckTypeId = typeof r.ckTypeId === 'string' ? r.ckTypeId : (r.ckTypeId?.fullName ?? null);
2969
+ result.push({
2970
+ __typename: r.__typename ?? 'StreamDataQueryRow',
2971
+ rtId: r.rtId ?? null,
2972
+ ckTypeId,
2973
+ timestamp: r.timestamp ?? null,
2974
+ rtWellKnownName: r.rtWellKnownName ?? null,
2975
+ rtCreationDateTime: r.rtCreationDateTime ?? null,
2976
+ rtChangedDateTime: r.rtChangedDateTime ?? null,
2977
+ cells: this.mapCells(r.cells?.items)
2978
+ });
2979
+ }
2980
+ return result;
2981
+ }
2982
+ mapCells(cells) {
2983
+ if (!cells)
2984
+ return [];
2985
+ const result = [];
2986
+ for (const cell of cells) {
2987
+ if (!cell?.attributePath)
2988
+ continue;
2989
+ result.push({ attributePath: cell.attributePath, value: cell.value });
2990
+ }
2991
+ return result;
2992
+ }
2993
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QueryExecutorService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
2994
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QueryExecutorService, providedIn: 'root' });
2995
+ }
2996
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QueryExecutorService, decorators: [{
2997
+ type: Injectable,
2998
+ args: [{ providedIn: 'root' }]
2999
+ }] });
3000
+
2590
3001
  const GetAssociationTargetsDocumentDto = gql `
2591
3002
  query getAssociationTargets($rtId: OctoObjectId!, $ckTypeId: String!, $targetCkTypeId: String!, $roleId: String!, $direction: GraphDirection!, $first: Int, $attributeNames: [String]) {
2592
3003
  runtime {
@@ -2859,7 +3270,7 @@ class MeshBoardDataService {
2859
3270
  getDashboardEntityGQL = inject(GetDashboardEntityDtoGQL);
2860
3271
  getCkModelsWithStateGQL = inject(GetCkModelsWithStateDtoGQL);
2861
3272
  getEntitiesByCkTypeGQL = inject(GetEntitiesByCkTypeDtoGQL);
2862
- executeRuntimeQueryGQL = inject(ExecuteRuntimeQueryDtoGQL);
3273
+ queryExecutor = inject(QueryExecutorService);
2863
3274
  getAssociationTargetsGQL = inject(GetAssociationTargetsDtoGQL);
2864
3275
  getCkTypeAttributesGQL = inject(GetCkTypeAttributesForMeshboardDtoGQL);
2865
3276
  apollo = inject(Apollo);
@@ -3070,8 +3481,10 @@ class MeshBoardDataService {
3070
3481
  async fetchRepeaterData(dataSource) {
3071
3482
  const maxItems = dataSource.maxItems ?? 50;
3072
3483
  if (dataSource.queryRtId) {
3073
- // Query Mode: Execute persistent query
3074
- return this.fetchRepeaterFromQuery(dataSource.queryRtId, maxItems);
3484
+ // Query Mode: Execute persistent query (runtime or stream-data).
3485
+ // `queryFamily` may be undefined for legacy configs — the executor falls
3486
+ // back to a one-time lookup keyed by rtId.
3487
+ return this.fetchRepeaterFromQuery(dataSource.queryFamily, dataSource.queryRtId, maxItems);
3075
3488
  }
3076
3489
  else if (dataSource.ckTypeId) {
3077
3490
  // Entity Mode: Load entities by CK type
@@ -3081,49 +3494,33 @@ class MeshBoardDataService {
3081
3494
  return [];
3082
3495
  }
3083
3496
  /**
3084
- * Fetches repeater data from a persistent query.
3085
- * Maps query rows to RepeaterDataItem objects.
3497
+ * Fetches repeater data from a persistent query (runtime or stream-data).
3498
+ * Always sends `streamDataArgs` when a time filter is active — the runtime
3499
+ * path ignores the field, so this is safe regardless of the resolved family.
3086
3500
  */
3087
- async fetchRepeaterFromQuery(queryRtId, maxItems) {
3501
+ async fetchRepeaterFromQuery(family, queryRtId, maxItems) {
3088
3502
  try {
3089
- const result = await firstValueFrom(this.executeRuntimeQueryGQL.fetch({
3090
- variables: {
3091
- rtId: queryRtId,
3092
- first: maxItems
3093
- }
3503
+ const streamDataArgs = this.buildRepeaterStreamDataArgs();
3504
+ const result = await firstValueFrom(this.queryExecutor.execute(family, queryRtId, {
3505
+ first: maxItems,
3506
+ streamDataArgs
3094
3507
  }));
3095
- const queryItems = result.data?.runtime?.runtimeQuery?.items ?? [];
3096
- if (queryItems.length === 0) {
3097
- return [];
3098
- }
3099
- const queryResult = queryItems[0];
3100
- if (!queryResult) {
3101
- return [];
3102
- }
3103
- const rows = queryResult.rows?.items ?? [];
3104
3508
  const items = [];
3105
- for (const row of rows) {
3106
- if (!row)
3107
- continue;
3108
- // Extract rtId from RtSimpleQueryRow if available
3509
+ for (const row of result.rows) {
3109
3510
  const rtId = row.rtId ?? `row-${items.length}`;
3110
- const ckTypeId = row.ckTypeId ?? queryResult.associatedCkTypeId ?? '';
3111
- // Build attributes map from cells
3511
+ const ckTypeId = row.ckTypeId ?? result.associatedCkTypeId ?? '';
3512
+ // Build attributes map from cells; expose both sanitised (`a_b`) and
3513
+ // original (`a.b`) keys so widget configs can address either form.
3112
3514
  const attributes = new Map();
3113
- const cells = row.cells?.items ?? [];
3114
- for (const cell of cells) {
3115
- if (!cell?.attributePath)
3116
- continue;
3117
- // Sanitize the attribute path (replace dots with underscores)
3515
+ for (const cell of row.cells) {
3118
3516
  const sanitizedPath = cell.attributePath.replace(/\./g, '_');
3119
3517
  attributes.set(sanitizedPath, cell.value);
3120
- // Also store with original path for flexibility
3121
3518
  attributes.set(cell.attributePath, cell.value);
3122
3519
  }
3123
3520
  items.push({
3124
3521
  rtId,
3125
- ckTypeId,
3126
- rtWellKnownName: attributes.get('rtWellKnownName'),
3522
+ ckTypeId: ckTypeId ?? '',
3523
+ rtWellKnownName: row.rtWellKnownName ?? attributes.get('rtWellKnownName'),
3127
3524
  attributes
3128
3525
  });
3129
3526
  }
@@ -3134,6 +3531,13 @@ class MeshBoardDataService {
3134
3531
  return [];
3135
3532
  }
3136
3533
  }
3534
+ buildRepeaterStreamDataArgs() {
3535
+ const range = this.stateService.resolveCurrentTimeRange();
3536
+ if (!range) {
3537
+ return undefined;
3538
+ }
3539
+ return { from: range.from, to: range.to };
3540
+ }
3137
3541
  /**
3138
3542
  * Fetches repeater data from entities by CK type.
3139
3543
  * Maps entities to RepeaterDataItem objects.
@@ -4252,9 +4656,19 @@ function processPieChartData(rows, categoryField, valueField) {
4252
4656
 
4253
4657
  class KpiWidgetComponent {
4254
4658
  dataService = inject(DashboardDataService);
4255
- executeRuntimeQueryGQL = inject(ExecuteRuntimeQueryDtoGQL);
4659
+ queryExecutor = inject(QueryExecutorService);
4256
4660
  stateService = inject(MeshBoardStateService);
4257
4661
  variableService = inject(MeshBoardVariableService);
4662
+ /**
4663
+ * Row __typenames KPI extraction recognises.
4664
+ * Runtime queries discriminate; stream-data queries collapse all kinds
4665
+ * (simple / aggregation / grouped / downsampling) into `StreamDataQueryRow`.
4666
+ */
4667
+ static SUPPORTED_ROW_TYPES = new Set([
4668
+ 'RtAggregationQueryRow',
4669
+ 'RtGroupingAggregationQueryRow',
4670
+ 'StreamDataQueryRow'
4671
+ ]);
4258
4672
  config;
4259
4673
  arrowUpIcon = arrowUpIcon;
4260
4674
  arrowDownIcon = arrowDownIcon;
@@ -4506,43 +4920,29 @@ class KpiWidgetComponent {
4506
4920
  this._isLoading.set(true);
4507
4921
  this._error.set(null);
4508
4922
  try {
4509
- // Convert widget filters to GraphQL format
4510
4923
  const fieldFilter = this.convertFiltersToDto(this.config.filters);
4511
- const result = await firstValueFrom(this.executeRuntimeQueryGQL.fetch({
4512
- variables: {
4513
- rtId: dataSource.queryRtId,
4514
- fieldFilter
4515
- }
4924
+ // queryFamily may be undefined for legacy widget configs — the executor
4925
+ // falls back to a one-time lookup by rtId. streamDataArgs is sent
4926
+ // unconditionally because the runtime path ignores it.
4927
+ const streamDataArgs = this.buildStreamDataArgs();
4928
+ const result = await firstValueFrom(this.queryExecutor.execute(dataSource.queryFamily, dataSource.queryRtId, {
4929
+ fieldFilter: fieldFilter ?? undefined,
4930
+ streamDataArgs
4516
4931
  }).pipe(catchError(err => {
4517
4932
  console.error('Error loading KPI query data:', err);
4518
4933
  throw err;
4519
4934
  })));
4520
- const queryItems = result.data?.runtime?.runtimeQuery?.items ?? [];
4521
- if (queryItems.length === 0) {
4522
- this._error.set('Query returned no results');
4523
- this._isLoading.set(false);
4524
- return;
4525
- }
4526
- const queryResult = queryItems[0];
4527
- if (!queryResult) {
4528
- this._error.set('Query returned no results');
4529
- this._isLoading.set(false);
4530
- return;
4531
- }
4532
4935
  let value = 0;
4533
4936
  const queryMode = this.config.queryMode ?? 'simpleCount';
4534
4937
  switch (queryMode) {
4535
4938
  case 'simpleCount':
4536
- // Use totalCount from the query
4537
- value = queryResult.rows?.totalCount ?? 0;
4939
+ value = result.totalCount;
4538
4940
  break;
4539
4941
  case 'aggregation':
4540
- // Get the single value from aggregation query (1 row, 1 column)
4541
- value = this.extractAggregationValue(queryResult);
4942
+ value = this.extractAggregationValue(result);
4542
4943
  break;
4543
4944
  case 'groupedAggregation':
4544
- // Find the row matching the selected category and get its value
4545
- value = this.extractGroupedAggregationValue(queryResult);
4945
+ value = this.extractGroupedAggregationValue(result);
4546
4946
  break;
4547
4947
  }
4548
4948
  // Create a synthetic entity with the value
@@ -4561,48 +4961,39 @@ class KpiWidgetComponent {
4561
4961
  this._isLoading.set(false);
4562
4962
  }
4563
4963
  }
4964
+ buildStreamDataArgs() {
4965
+ const range = this.stateService.resolveCurrentTimeRange();
4966
+ if (!range) {
4967
+ return undefined;
4968
+ }
4969
+ return { from: range.from, to: range.to };
4970
+ }
4564
4971
  extractAggregationValue(queryResult) {
4565
- const rows = queryResult.rows?.items ?? [];
4566
- const supportedRowTypes = ['RtAggregationQueryRow', 'RtGroupingAggregationQueryRow'];
4567
- // Get the first row
4568
- const firstRow = rows.find(row => row && supportedRowTypes.includes(row.__typename ?? ''));
4972
+ const firstRow = queryResult.rows.find(row => KpiWidgetComponent.SUPPORTED_ROW_TYPES.has(row.__typename ?? ''));
4569
4973
  if (!firstRow)
4570
4974
  return 0;
4571
- const queryRow = firstRow;
4572
- const cells = queryRow.cells?.items ?? [];
4573
- // Find the value field or use the first cell
4574
4975
  const valueField = this.config.queryValueField;
4575
- for (const cell of cells) {
4576
- if (!cell?.attributePath)
4577
- continue;
4976
+ for (const cell of firstRow.cells) {
4578
4977
  if (valueField && matchesAttributePath(cell.attributePath, valueField)) {
4579
4978
  return this.extractCellValue(cell.value);
4580
4979
  }
4581
4980
  }
4582
4981
  // Fallback: return first cell value if no specific field configured
4583
- const firstCell = cells.find(c => c !== null);
4584
- return firstCell ? this.extractCellValue(firstCell.value) : 0;
4982
+ return firstRow.cells.length > 0 ? this.extractCellValue(firstRow.cells[0].value) : 0;
4585
4983
  }
4586
4984
  extractGroupedAggregationValue(queryResult) {
4587
- const rows = queryResult.rows?.items ?? [];
4588
- const supportedRowTypes = ['RtGroupingAggregationQueryRow', 'RtAggregationQueryRow'];
4589
4985
  const categoryField = this.config.queryCategoryField;
4590
4986
  const categoryValue = this.config.queryCategoryValue;
4591
4987
  const valueField = this.config.queryValueField;
4592
4988
  if (!categoryField || !categoryValue || !valueField) {
4593
4989
  return 0;
4594
4990
  }
4595
- // Find the row where category matches
4596
- for (const row of rows) {
4597
- if (!row || !supportedRowTypes.includes(row.__typename ?? ''))
4991
+ for (const row of queryResult.rows) {
4992
+ if (!KpiWidgetComponent.SUPPORTED_ROW_TYPES.has(row.__typename ?? ''))
4598
4993
  continue;
4599
- const queryRow = row;
4600
- const cells = queryRow.cells?.items ?? [];
4601
4994
  let categoryMatch = false;
4602
4995
  let value = 0;
4603
- for (const cell of cells) {
4604
- if (!cell?.attributePath)
4605
- continue;
4996
+ for (const cell of row.cells) {
4606
4997
  if (matchesAttributePath(cell.attributePath, categoryField) && String(cell.value) === categoryValue) {
4607
4998
  categoryMatch = true;
4608
4999
  }
@@ -4692,8 +5083,18 @@ class KpiConfigDialogComponent {
4692
5083
  getEntitiesByCkTypeGQL = inject(GetEntitiesByCkTypeDtoGQL);
4693
5084
  ckTypeSelectorService = inject(CkTypeSelectorService);
4694
5085
  attributeSelectorService = inject(AttributeSelectorService);
4695
- executeRuntimeQueryGQL = inject(ExecuteRuntimeQueryDtoGQL);
4696
5086
  getRuntimeQueryColumnsGQL = inject(GetRuntimeQueryColumnsDtoGQL);
5087
+ queryExecutor = inject(QueryExecutorService);
5088
+ /**
5089
+ * Row __typenames the dialog recognises when collecting distinct category
5090
+ * values from a query result for the grouped-aggregation category picker.
5091
+ */
5092
+ static INTROSPECTION_ROW_TYPES = new Set([
5093
+ 'RtSimpleQueryRow',
5094
+ 'RtAggregationQueryRow',
5095
+ 'RtGroupingAggregationQueryRow',
5096
+ 'StreamDataQueryRow'
5097
+ ]);
4697
5098
  meshBoardStateService = inject(MeshBoardStateService);
4698
5099
  windowRef = inject(WindowRef);
4699
5100
  ckTypeSelectorInput;
@@ -4712,6 +5113,7 @@ class KpiConfigDialogComponent {
4712
5113
  initialDataSourceType;
4713
5114
  initialQueryRtId;
4714
5115
  initialQueryName;
5116
+ initialQueryFamily;
4715
5117
  initialQueryMode;
4716
5118
  initialQueryValueField;
4717
5119
  initialQueryCategoryField;
@@ -4985,6 +5387,7 @@ class KpiConfigDialogComponent {
4985
5387
  return;
4986
5388
  }
4987
5389
  if (this.dataSourceType === 'persistentQuery' && this.selectedPersistentQuery) {
5390
+ const family = queryFamily(this.selectedPersistentQuery.ckTypeId) ?? this.initialQueryFamily ?? undefined;
4988
5391
  this.windowRef.close({
4989
5392
  dataSourceType: 'persistentQuery',
4990
5393
  ckTypeId: '',
@@ -4992,6 +5395,7 @@ class KpiConfigDialogComponent {
4992
5395
  valueAttribute: '',
4993
5396
  queryRtId: this.selectedPersistentQuery.rtId,
4994
5397
  queryName: this.selectedPersistentQuery.name,
5398
+ queryFamily: family,
4995
5399
  queryMode: this.queryMode,
4996
5400
  queryValueField: this.form.queryValueField || undefined,
4997
5401
  queryCategoryField: this.form.queryCategoryField || undefined,
@@ -5058,35 +5462,16 @@ class KpiConfigDialogComponent {
5058
5462
  const rtId = queryRtId || this.selectedPersistentQuery?.rtId;
5059
5463
  if (!rtId)
5060
5464
  return;
5465
+ // queryFamily may be undefined when the selected query metadata is missing —
5466
+ // fetchColumnsForFamily resolves it via the executor's one-time lookup.
5467
+ const family = queryFamily(this.selectedPersistentQuery?.ckTypeId) ?? this.initialQueryFamily;
5061
5468
  this.isLoadingQueryColumns = true;
5062
5469
  try {
5063
- // Metadata-only fetch column resolver runs off the cached query definition
5064
- // without executing the underlying aggregation, so the dialog opens fast even
5065
- // for queries that aggregate over large data sets.
5066
- const result = await firstValueFrom(this.getRuntimeQueryColumnsGQL.fetch({
5067
- variables: {
5068
- rtId: rtId
5069
- }
5070
- }));
5071
- const queryItems = result.data?.runtime?.runtimeQuery?.items ?? [];
5072
- if (queryItems.length > 0 && queryItems[0]) {
5073
- const columns = queryItems[0].columns ?? [];
5074
- const filteredColumns = columns
5075
- .filter((c) => c !== null);
5076
- // Column AttributePath is already in the engine's wire form for aggregation /
5077
- // grouping columns (e.g. `quantity_sum`, `operatingstatus`) so picker entries
5078
- // double as both the visible label and the stored config value, and MIN + MAX
5079
- // of the same source path show up as two distinct entries.
5080
- this.queryColumns = filteredColumns.map(c => ({
5081
- attributePath: c.attributePath ?? '',
5082
- attributeValueType: c.attributeValueType ?? '',
5083
- aggregationType: c.aggregationType ?? null
5084
- }));
5085
- // Category values for grouped aggregation are loaded on-demand by
5086
- // loadCategoryValuesForField — only when a categoryField is actually selected.
5087
- if (this.queryMode === 'groupedAggregation' && this.form.queryCategoryField) {
5088
- await this.loadCategoryValuesForField(rtId, this.form.queryCategoryField);
5089
- }
5470
+ this.queryColumns = await this.fetchColumnsForFamily(family, rtId);
5471
+ // Category values for grouped aggregation are loaded on-demand by
5472
+ // loadCategoryValuesForField only when a categoryField is actually selected.
5473
+ if (this.queryColumns.length > 0 && this.queryMode === 'groupedAggregation' && this.form.queryCategoryField) {
5474
+ await this.loadCategoryValuesForField(rtId, this.form.queryCategoryField);
5090
5475
  }
5091
5476
  }
5092
5477
  catch (error) {
@@ -5097,6 +5482,40 @@ class KpiConfigDialogComponent {
5097
5482
  this.isLoadingQueryColumns = false;
5098
5483
  }
5099
5484
  }
5485
+ /**
5486
+ * Loads column metadata for the picker. Runtime queries use the
5487
+ * metadata-only resolver (no aggregation executed); stream-data queries
5488
+ * fall back to executing the query with `first: 1`. When `family` is
5489
+ * unknown (legacy configs), the executor resolves it once by rtId lookup.
5490
+ */
5491
+ async fetchColumnsForFamily(family, rtId) {
5492
+ const resolvedFamily = family ?? await this.queryExecutor.resolveFamily(rtId);
5493
+ if (resolvedFamily === 'runtime') {
5494
+ const result = await firstValueFrom(this.getRuntimeQueryColumnsGQL.fetch({
5495
+ variables: { rtId }
5496
+ }));
5497
+ const queryItem = result.data?.runtime?.runtimeQuery?.items?.[0];
5498
+ if (!queryItem)
5499
+ return [];
5500
+ return (queryItem.columns ?? [])
5501
+ .filter((c) => c !== null)
5502
+ // Column AttributePath is already in the engine's wire form for aggregation /
5503
+ // grouping columns (e.g. `quantity_sum`, `operatingstatus`) so picker entries
5504
+ // double as both the visible label and the stored config value.
5505
+ .map(c => ({
5506
+ attributePath: c.attributePath ?? '',
5507
+ attributeValueType: c.attributeValueType ?? '',
5508
+ aggregationType: c.aggregationType ?? null
5509
+ }));
5510
+ }
5511
+ // Stream-data: execute with a tiny page just to surface columns.
5512
+ const sdResult = await firstValueFrom(this.queryExecutor.executeStreamData(rtId, { first: 1 }));
5513
+ return sdResult.columns.map(c => ({
5514
+ attributePath: c.attributePath,
5515
+ attributeValueType: c.attributeValueType ?? '',
5516
+ aggregationType: c.aggregationType ?? null
5517
+ }));
5518
+ }
5100
5519
  async onCategoryFieldChange(categoryField) {
5101
5520
  this.form.queryCategoryField = categoryField;
5102
5521
  this.form.queryCategoryValue = '';
@@ -5108,36 +5527,23 @@ class KpiConfigDialogComponent {
5108
5527
  async loadCategoryValuesForField(queryRtId, categoryField) {
5109
5528
  this.isLoadingCategoryValues = true;
5110
5529
  try {
5111
- const result = await firstValueFrom(this.executeRuntimeQueryGQL.fetch({
5112
- variables: {
5113
- rtId: queryRtId,
5114
- first: 100
5115
- }
5116
- }));
5117
- const queryItems = result.data?.runtime?.runtimeQuery?.items ?? [];
5118
- if (queryItems.length > 0 && queryItems[0]) {
5119
- const queryResult = queryItems[0];
5120
- const rows = queryResult.rows?.items ?? [];
5121
- const supportedRowTypes = ['RtSimpleQueryRow', 'RtAggregationQueryRow', 'RtGroupingAggregationQueryRow'];
5122
- const values = new Set();
5123
- for (const row of rows) {
5124
- if (!row || !supportedRowTypes.includes(row.__typename ?? ''))
5125
- continue;
5126
- const queryRow = row;
5127
- const cells = queryRow.cells?.items ?? [];
5128
- for (const cell of cells) {
5129
- if (!cell?.attributePath)
5130
- continue;
5131
- if (matchesAttributePath(cell.attributePath, categoryField) && cell.value !== null && cell.value !== undefined) {
5132
- values.add(String(cell.value));
5133
- }
5530
+ // family may be undefined here — the executor falls back to a lookup.
5531
+ const family = queryFamily(this.selectedPersistentQuery?.ckTypeId) ?? this.initialQueryFamily;
5532
+ const result = await firstValueFrom(this.queryExecutor.execute(family, queryRtId, { first: 100 }));
5533
+ const values = new Set();
5534
+ for (const row of result.rows) {
5535
+ if (!KpiConfigDialogComponent.INTROSPECTION_ROW_TYPES.has(row.__typename ?? ''))
5536
+ continue;
5537
+ for (const cell of row.cells) {
5538
+ if (matchesAttributePath(cell.attributePath, categoryField) && cell.value !== null && cell.value !== undefined) {
5539
+ values.add(String(cell.value));
5134
5540
  }
5135
5541
  }
5136
- this.categoryValues = Array.from(values).map(v => ({
5137
- value: v,
5138
- displayValue: v
5139
- }));
5140
5542
  }
5543
+ this.categoryValues = Array.from(values).map(v => ({
5544
+ value: v,
5545
+ displayValue: v
5546
+ }));
5141
5547
  }
5142
5548
  catch (error) {
5143
5549
  console.error('Error loading category values:', error);
@@ -5154,7 +5560,7 @@ class KpiConfigDialogComponent {
5154
5560
  this.filters = updatedFilters;
5155
5561
  }
5156
5562
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: KpiConfigDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5157
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: KpiConfigDialogComponent, isStandalone: true, selector: "mm-kpi-config-dialog", inputs: { initialCkTypeId: "initialCkTypeId", initialRtId: "initialRtId", initialValueAttribute: "initialValueAttribute", initialLabelAttribute: "initialLabelAttribute", initialPrefix: "initialPrefix", initialSuffix: "initialSuffix", initialTrend: "initialTrend", initialComparisonText: "initialComparisonText", initialDataSourceType: "initialDataSourceType", initialQueryRtId: "initialQueryRtId", initialQueryName: "initialQueryName", initialQueryMode: "initialQueryMode", initialQueryValueField: "initialQueryValueField", initialQueryCategoryField: "initialQueryCategoryField", initialQueryCategoryValue: "initialQueryCategoryValue", initialStaticValue: "initialStaticValue", initialFilters: "initialFilters" }, viewQueries: [{ propertyName: "ckTypeSelectorInput", first: true, predicate: ["ckTypeSelector"], descendants: true }, { propertyName: "entitySelectorInput", first: true, predicate: ["entitySelector"], descendants: true }, { propertyName: "querySelector", first: true, predicate: ["querySelector"], descendants: true }], ngImport: i0, template: `
5563
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: KpiConfigDialogComponent, isStandalone: true, selector: "mm-kpi-config-dialog", inputs: { initialCkTypeId: "initialCkTypeId", initialRtId: "initialRtId", initialValueAttribute: "initialValueAttribute", initialLabelAttribute: "initialLabelAttribute", initialPrefix: "initialPrefix", initialSuffix: "initialSuffix", initialTrend: "initialTrend", initialComparisonText: "initialComparisonText", initialDataSourceType: "initialDataSourceType", initialQueryRtId: "initialQueryRtId", initialQueryName: "initialQueryName", initialQueryFamily: "initialQueryFamily", initialQueryMode: "initialQueryMode", initialQueryValueField: "initialQueryValueField", initialQueryCategoryField: "initialQueryCategoryField", initialQueryCategoryValue: "initialQueryCategoryValue", initialStaticValue: "initialStaticValue", initialFilters: "initialFilters" }, viewQueries: [{ propertyName: "ckTypeSelectorInput", first: true, predicate: ["ckTypeSelector"], descendants: true }, { propertyName: "entitySelectorInput", first: true, predicate: ["entitySelector"], descendants: true }, { propertyName: "querySelector", first: true, predicate: ["querySelector"], descendants: true }], ngImport: i0, template: `
5158
5564
  <div class="config-container">
5159
5565
 
5160
5566
  <div class="config-form" [class.loading]="isLoadingInitial">
@@ -5524,7 +5930,7 @@ class KpiConfigDialogComponent {
5524
5930
  </button>
5525
5931
  </div>
5526
5932
  </div>
5527
- `, 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;flex:1;overflow-y:auto;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.flex-1{flex:1}.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}.form-section h4{margin:0 0 16px;font-size:.95rem;color:var(--kendo-color-primary, #0d6efd)}.section-hint{margin:0 0 12px;font-size:.85rem;color:var(--kendo-color-subtle, #6c757d)}.form-row{display:flex;gap:16px}.mode-toggle{display:flex;gap:8px}.mode-toggle button{flex:1}.attribute-item{display:flex;justify-content:space-between;align-items:center;gap:8px;width:100%}.attribute-path{flex:1}.attribute-type{font-size:.75rem;color:var(--kendo-color-subtle, #6c757d);background:var(--kendo-color-surface-alt, #f8f9fa);padding:2px 6px;border-radius:3px}.required{color:var(--kendo-color-error, #dc3545)}.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)}.column-item{display:flex;justify-content:space-between;gap:16px}.column-path{font-weight:500}.column-type{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}\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", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon", "primary", "look"], 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: "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: "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: QuerySelectorComponent, selector: "mm-query-selector", inputs: ["placeholder", "hint", "disabled"], outputs: ["querySelected", "queriesLoaded"] }, { kind: "component", type: LoadingOverlayComponent, selector: "mm-loading-overlay", inputs: ["loading"] }] });
5933
+ `, 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;flex:1;overflow-y:auto;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.flex-1{flex:1}.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}.form-section h4{margin:0 0 16px;font-size:.95rem;color:var(--kendo-color-primary, #0d6efd)}.section-hint{margin:0 0 12px;font-size:.85rem;color:var(--kendo-color-subtle, #6c757d)}.form-row{display:flex;gap:16px}.mode-toggle{display:flex;gap:8px}.mode-toggle button{flex:1}.attribute-item{display:flex;justify-content:space-between;align-items:center;gap:8px;width:100%}.attribute-path{flex:1}.attribute-type{font-size:.75rem;color:var(--kendo-color-subtle, #6c757d);background:var(--kendo-color-surface-alt, #f8f9fa);padding:2px 6px;border-radius:3px}.required{color:var(--kendo-color-error, #dc3545)}.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)}.column-item{display:flex;justify-content:space-between;gap:16px}.column-path{font-weight:500}.column-type{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}\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", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon", "primary", "look"], 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: "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: "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: QuerySelectorComponent, selector: "mm-query-selector", inputs: ["placeholder", "hint", "disabled", "acceptFamilies"], outputs: ["querySelected", "queriesLoaded"] }, { kind: "component", type: LoadingOverlayComponent, selector: "mm-loading-overlay", inputs: ["loading"] }] });
5528
5934
  }
5529
5935
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: KpiConfigDialogComponent, decorators: [{
5530
5936
  type: Component,
@@ -5941,6 +6347,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
5941
6347
  type: Input
5942
6348
  }], initialQueryName: [{
5943
6349
  type: Input
6350
+ }], initialQueryFamily: [{
6351
+ type: Input
5944
6352
  }], initialQueryMode: [{
5945
6353
  type: Input
5946
6354
  }], initialQueryValueField: [{
@@ -7364,7 +7772,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
7364
7772
  */
7365
7773
  class TableWidgetDataSourceDirective extends OctoGraphQlDataSource {
7366
7774
  getEntitiesByCkTypeGQL = inject(GetEntitiesByCkTypeDtoGQL);
7367
- executeRuntimeQueryGQL = inject(ExecuteRuntimeQueryDtoGQL);
7775
+ queryExecutor = inject(QueryExecutorService);
7368
7776
  stateService = inject(MeshBoardStateService);
7369
7777
  variableService = inject(MeshBoardVariableService);
7370
7778
  _config = null;
@@ -7493,86 +7901,81 @@ class TableWidgetDataSourceDirective extends OctoGraphQlDataSource {
7493
7901
  }
7494
7902
  }
7495
7903
  /**
7496
- * Fetches data from a persistent query.
7497
- * Extracts columns from the query response and transforms cells to flat records.
7904
+ * Row __typenames the table widget knows how to flatten. Runtime queries
7905
+ * use three discriminated variants; stream-data queries collapse all
7906
+ * kinds (simple / aggregation / grouped / downsampling) into a single
7907
+ * `StreamDataQueryRow` type.
7908
+ */
7909
+ static SUPPORTED_ROW_TYPES = new Set([
7910
+ 'RtSimpleQueryRow',
7911
+ 'RtAggregationQueryRow',
7912
+ 'RtGroupingAggregationQueryRow',
7913
+ 'StreamDataQueryRow'
7914
+ ]);
7915
+ /**
7916
+ * Fetches data from a persistent query (runtime or stream-data).
7917
+ * Family is determined from the cached `queryFamily` on the data source
7918
+ * (set by the config dialog when the user picks a query) and defaults to
7919
+ * `'runtime'` for legacy configs that predate stream-data support.
7498
7920
  */
7499
7921
  fetchPersistentQueryData(dataSource, queryOptions) {
7500
- // Convert widget-configured filters to GraphQL format (with variable resolution)
7501
7922
  const fieldFilter = this.convertFiltersToDto(this._config?.filters);
7502
- return this.executeRuntimeQueryGQL.fetch({
7503
- variables: {
7504
- rtId: dataSource.queryRtId,
7505
- first: queryOptions.state.take ?? this._config?.pageSize ?? 10,
7506
- after: GraphQL.offsetToCursor(queryOptions.state.skip ?? 0),
7507
- fieldFilter
7508
- }
7923
+ // queryFamily may be undefined for legacy widget configs — the executor
7924
+ // falls back to a one-time lookup by rtId. streamDataArgs is sent
7925
+ // unconditionally because the runtime path ignores it.
7926
+ //
7927
+ // Precedence: MeshBoard time filter > query's intrinsic time bounds.
7928
+ // When no time filter is active, the persistent query uses its own bounds.
7929
+ const streamDataArgs = this.buildStreamDataArgs();
7930
+ return this.queryExecutor.execute(dataSource.queryFamily, dataSource.queryRtId, {
7931
+ first: queryOptions.state.take ?? this._config?.pageSize ?? 10,
7932
+ after: GraphQL.offsetToCursor(queryOptions.state.skip ?? 0),
7933
+ fieldFilter: fieldFilter ?? undefined,
7934
+ streamDataArgs
7509
7935
  }).pipe(map$1(result => {
7510
- const queryItems = result.data?.runtime?.runtimeQuery?.items ?? [];
7511
- if (queryItems.length === 0) {
7512
- return new FetchResultTyped([], 0);
7513
- }
7514
- const queryResult = queryItems[0];
7515
- if (!queryResult) {
7516
- return new FetchResultTyped([], 0);
7517
- }
7518
- // Extract columns from query response and update signal
7519
- // Replace dots with underscores for grid compatibility (Kendo treats dots as nested paths)
7520
- const columns = (queryResult.columns ?? [])
7521
- .filter((c) => c !== null)
7522
- .map(c => ({
7523
- attributePath: this.sanitizeFieldName(c.attributePath ?? ''),
7936
+ // Extract columns from query response and update signal.
7937
+ // Replace dots with underscores for grid compatibility (Kendo treats dots as nested paths).
7938
+ const columns = result.columns.map(c => ({
7939
+ attributePath: this.sanitizeFieldName(c.attributePath),
7524
7940
  attributeValueType: c.attributeValueType ?? ''
7525
7941
  }));
7526
7942
  this._queryColumns.set(columns);
7527
- // Emit event to notify component that columns have been loaded
7528
7943
  this.queryColumnsLoaded.emit(columns);
7529
- // Extract rows
7530
- const rows = queryResult.rows?.items ?? [];
7531
- const totalCount = queryResult.rows?.totalCount ?? 0;
7532
- // Check which standard fields are in the query columns
7533
7944
  const columnPaths = new Set(columns.map(c => c.attributePath));
7534
7945
  const hasRtIdColumn = columnPaths.has('rtId');
7535
7946
  const hasCkTypeIdColumn = columnPaths.has('ckTypeId');
7536
- // Transform rows to flat records (handle union type by checking __typename)
7537
- // Support RtSimpleQueryRow, RtAggregationQueryRow, and RtGroupingAggregationQueryRow
7538
- const supportedRowTypes = ['RtSimpleQueryRow', 'RtAggregationQueryRow', 'RtGroupingAggregationQueryRow'];
7539
- const data = rows
7540
- .filter((row) => row !== null)
7541
- .filter(row => supportedRowTypes.includes(row.__typename ?? ''))
7542
- .map((row, index) => {
7543
- // Both row types have ckTypeId and cells, but only RtSimpleQueryRow has rtId
7544
- const queryRow = row;
7545
- const record = {};
7546
- // Only add rtId/ckTypeId if they're explicitly in the query columns
7547
- if (hasRtIdColumn) {
7548
- record['rtId'] = queryRow.rtId ?? `agg-${index}`;
7549
- }
7550
- if (hasCkTypeIdColumn) {
7551
- record['ckTypeId'] = queryRow.ckTypeId ?? '';
7552
- }
7553
- // Flatten cells into the record. Each cell is stored under the COLUMN's
7554
- // attributePath (which is what the Kendo grid uses as `field`) rather than
7555
- // the cell's own attributePath — the two may differ now that the engine
7556
- // emits cell paths in wire-form with a function suffix
7557
- // (e.g. cell path `meterreading_count` for column path `meterReading`).
7558
- // `matchesAttributePath` reconciles both forms.
7559
- const cells = queryRow.cells?.items ?? [];
7560
- for (const cell of cells) {
7561
- if (!cell?.attributePath)
7562
- continue;
7563
- const matchingColumn = columns.find(col => matchesAttributePath(cell.attributePath, col.attributePath));
7564
- if (matchingColumn) {
7565
- record[matchingColumn.attributePath] = cell.value;
7566
- }
7567
- }
7568
- return record;
7569
- });
7570
- return new FetchResultTyped(data, totalCount);
7947
+ const data = result.rows
7948
+ .filter(row => TableWidgetDataSourceDirective.SUPPORTED_ROW_TYPES.has(row.__typename ?? ''))
7949
+ .map((row, index) => this.queryRowToRecord(row, columns, hasRtIdColumn, hasCkTypeIdColumn, index));
7950
+ return new FetchResultTyped(data, result.totalCount);
7571
7951
  }), catchError$1(err => {
7572
7952
  console.error('Error fetching query data:', err);
7573
7953
  return of(new FetchResultTyped([], 0));
7574
7954
  }));
7575
7955
  }
7956
+ /**
7957
+ * Flattens a unified `QueryResultRow` into a Kendo-grid-friendly record.
7958
+ * Cells are stored under their matching column's `attributePath` rather than
7959
+ * the cell's own — the engine emits cell paths in wire-form with a function
7960
+ * suffix (e.g. cell path `meterreading_count` for column path `meterReading`).
7961
+ * `matchesAttributePath` reconciles both forms.
7962
+ */
7963
+ queryRowToRecord(row, columns, hasRtIdColumn, hasCkTypeIdColumn, index) {
7964
+ const record = {};
7965
+ if (hasRtIdColumn) {
7966
+ record['rtId'] = row.rtId ?? `agg-${index}`;
7967
+ }
7968
+ if (hasCkTypeIdColumn) {
7969
+ record['ckTypeId'] = row.ckTypeId ?? '';
7970
+ }
7971
+ for (const cell of row.cells) {
7972
+ const matchingColumn = columns.find(col => matchesAttributePath(cell.attributePath, col.attributePath));
7973
+ if (matchingColumn) {
7974
+ record[matchingColumn.attributePath] = cell.value;
7975
+ }
7976
+ }
7977
+ return record;
7978
+ }
7576
7979
  /**
7577
7980
  * Converts query columns to TableColumn format for display.
7578
7981
  */
@@ -7615,6 +8018,18 @@ class TableWidgetDataSourceDirective extends OctoGraphQlDataSource {
7615
8018
  const variables = this.stateService.getVariables();
7616
8019
  return this.variableService.convertToFieldFilterDto(filters, variables);
7617
8020
  }
8021
+ /**
8022
+ * Builds `StreamDataExecutionArgs` from the MeshBoard's current time filter.
8023
+ * Returns `undefined` when no filter is active so the persistent query's
8024
+ * own bounds apply.
8025
+ */
8026
+ buildStreamDataArgs() {
8027
+ const range = this.stateService.resolveCurrentTimeRange();
8028
+ if (!range) {
8029
+ return undefined;
8030
+ }
8031
+ return { from: range.from, to: range.to };
8032
+ }
7618
8033
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: TableWidgetDataSourceDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
7619
8034
  static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.17", type: TableWidgetDataSourceDirective, isStandalone: true, selector: "[mmTableWidgetDataSource]", inputs: { config: "config" }, outputs: { queryColumnsLoaded: "queryColumnsLoaded" }, providers: [
7620
8035
  {
@@ -7850,6 +8265,7 @@ class TableConfigDialogComponent {
7850
8265
  initialSortable;
7851
8266
  initialQueryRtId;
7852
8267
  initialQueryName;
8268
+ initialQueryFamily;
7853
8269
  columnsIcon = columnsIcon;
7854
8270
  sortIcon = sortAscIcon;
7855
8271
  filterIcon = filterIcon;
@@ -8071,6 +8487,9 @@ class TableConfigDialogComponent {
8071
8487
  operator: f.operator,
8072
8488
  comparisonValue: f.comparisonValue
8073
8489
  }));
8490
+ // Derive family from the selected query's CK type so the runtime executor
8491
+ // can route correctly without an extra lookup.
8492
+ const family = queryFamily(this.selectedPersistentQuery.ckTypeId) ?? this.initialQueryFamily ?? undefined;
8074
8493
  this.windowRef.close({
8075
8494
  dataSourceType: 'persistentQuery',
8076
8495
  ckTypeId: '', // Not used for persistent query
@@ -8079,6 +8498,7 @@ class TableConfigDialogComponent {
8079
8498
  filters: queryFilterDtos,
8080
8499
  queryRtId: this.selectedPersistentQuery.rtId,
8081
8500
  queryName: this.selectedPersistentQuery.name,
8501
+ queryFamily: family,
8082
8502
  pageSize: this.form.pageSize,
8083
8503
  sortable: this.form.sortable
8084
8504
  });
@@ -8095,7 +8515,7 @@ class TableConfigDialogComponent {
8095
8515
  this.windowRef.close();
8096
8516
  }
8097
8517
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: TableConfigDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
8098
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: TableConfigDialogComponent, isStandalone: true, selector: "mm-table-config-dialog", inputs: { initialDataSourceType: "initialDataSourceType", initialCkTypeId: "initialCkTypeId", initialColumns: "initialColumns", initialSorting: "initialSorting", initialFilters: "initialFilters", initialPageSize: "initialPageSize", initialSortable: "initialSortable", initialQueryRtId: "initialQueryRtId", initialQueryName: "initialQueryName" }, providers: [
8518
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: TableConfigDialogComponent, isStandalone: true, selector: "mm-table-config-dialog", inputs: { initialDataSourceType: "initialDataSourceType", initialCkTypeId: "initialCkTypeId", initialColumns: "initialColumns", initialSorting: "initialSorting", initialFilters: "initialFilters", initialPageSize: "initialPageSize", initialSortable: "initialSortable", initialQueryRtId: "initialQueryRtId", initialQueryName: "initialQueryName", initialQueryFamily: "initialQueryFamily" }, providers: [
8099
8519
  AttributeSelectorDialogService,
8100
8520
  AttributeSortSelectorDialogService
8101
8521
  ], viewQueries: [{ propertyName: "ckTypeSelectorInput", first: true, predicate: ["ckTypeSelector"], descendants: true }, { propertyName: "filterEditor", first: true, predicate: ["filterEditor"], descendants: true }, { propertyName: "querySelector", first: true, predicate: ["querySelector"], descendants: true }], ngImport: i0, template: `
@@ -8298,7 +8718,7 @@ class TableConfigDialogComponent {
8298
8718
  </button>
8299
8719
  </div>
8300
8720
  </div>
8301
- `, 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:16px;flex:1;overflow-y:auto;padding:16px;position:relative}.config-form.loading{pointer-events:none}.form-field{display:flex;flex-direction:column;gap:6px}.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)}.config-card{padding:12px 16px;background:var(--kendo-color-surface-alt, #f8f9fa);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px}.card-header{display:flex;align-items:center;gap:8px;margin-bottom:8px}.card-header kendo-svgicon{color:var(--kendo-color-primary, #0d6efd)}.card-title{font-weight:600;font-size:.95rem;color:var(--kendo-color-primary, #0d6efd)}.card-count{color:var(--kendo-color-subtle, #6c757d);font-size:.85rem}.card-content{display:flex;align-items:center;justify-content:space-between;gap:16px}.config-summary{margin:0;font-size:.85rem;color:var(--kendo-color-on-app-surface, #212529);flex:1}.filters-card .card-content{flex-direction:column;align-items:stretch}.filter-content{width:100%}.options-card .options-content{display:flex;gap:24px;align-items:flex-end}.checkbox-field{flex-direction:row;align-items:center}.checkbox-field label{display:flex;align-items:center;gap:8px;cursor:pointer;color:var(--kendo-color-on-app-surface, #212529)}.data-source-type .radio-group{display:flex;gap:24px}.radio-label{display:flex;align-items:center;gap:8px;cursor:pointer;font-weight:400;color:var(--kendo-color-on-app-surface, #212529)}.radio-label span{color:var(--kendo-color-on-app-surface, #212529)}.query-info{flex-direction:column;align-items:stretch;gap:8px}.info-row{display:flex;gap:8px}.info-label{font-weight:600;min-width:100px;color:var(--kendo-color-subtle, #6c757d)}.info-value{flex:1;color:var(--kendo-color-on-app-surface, #212529)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1$1.RadioControlValueAccessor, selector: "input[type=radio][formControlName],input[type=radio][formControl],input[type=radio][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", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon", "primary", "look"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: InputsModule }, { 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: "directive", type: i3.CheckBoxDirective, selector: "input[kendoCheckBox]", inputs: ["size", "rounded"] }, { kind: "directive", type: i3.RadioButtonDirective, selector: "input[kendoRadioButton]", inputs: ["size"] }, { kind: "ngmodule", type: NumericTextBoxModule }, { kind: "ngmodule", type: DropDownsModule }, { kind: "ngmodule", type: SVGIconModule }, { kind: "component", type: i1$4.SVGIconComponent, selector: "kendo-svg-icon, kendo-svgicon", inputs: ["icon"], exportAs: ["kendoSVGIcon"] }, { 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: FieldFilterEditorComponent, selector: "mm-field-filter-editor", inputs: ["availableAttributes", "ckTypeId", "hideNavigationProperties", "attributePaths", "enableVariables", "availableVariables", "filters"], outputs: ["filtersChange"] }, { kind: "component", type: QuerySelectorComponent, selector: "mm-query-selector", inputs: ["placeholder", "hint", "disabled"], outputs: ["querySelected", "queriesLoaded"] }, { kind: "component", type: LoadingOverlayComponent, selector: "mm-loading-overlay", inputs: ["loading"] }] });
8721
+ `, 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:16px;flex:1;overflow-y:auto;padding:16px;position:relative}.config-form.loading{pointer-events:none}.form-field{display:flex;flex-direction:column;gap:6px}.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)}.config-card{padding:12px 16px;background:var(--kendo-color-surface-alt, #f8f9fa);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px}.card-header{display:flex;align-items:center;gap:8px;margin-bottom:8px}.card-header kendo-svgicon{color:var(--kendo-color-primary, #0d6efd)}.card-title{font-weight:600;font-size:.95rem;color:var(--kendo-color-primary, #0d6efd)}.card-count{color:var(--kendo-color-subtle, #6c757d);font-size:.85rem}.card-content{display:flex;align-items:center;justify-content:space-between;gap:16px}.config-summary{margin:0;font-size:.85rem;color:var(--kendo-color-on-app-surface, #212529);flex:1}.filters-card .card-content{flex-direction:column;align-items:stretch}.filter-content{width:100%}.options-card .options-content{display:flex;gap:24px;align-items:flex-end}.checkbox-field{flex-direction:row;align-items:center}.checkbox-field label{display:flex;align-items:center;gap:8px;cursor:pointer;color:var(--kendo-color-on-app-surface, #212529)}.data-source-type .radio-group{display:flex;gap:24px}.radio-label{display:flex;align-items:center;gap:8px;cursor:pointer;font-weight:400;color:var(--kendo-color-on-app-surface, #212529)}.radio-label span{color:var(--kendo-color-on-app-surface, #212529)}.query-info{flex-direction:column;align-items:stretch;gap:8px}.info-row{display:flex;gap:8px}.info-label{font-weight:600;min-width:100px;color:var(--kendo-color-subtle, #6c757d)}.info-value{flex:1;color:var(--kendo-color-on-app-surface, #212529)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1$1.RadioControlValueAccessor, selector: "input[type=radio][formControlName],input[type=radio][formControl],input[type=radio][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", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon", "primary", "look"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: InputsModule }, { 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: "directive", type: i3.CheckBoxDirective, selector: "input[kendoCheckBox]", inputs: ["size", "rounded"] }, { kind: "directive", type: i3.RadioButtonDirective, selector: "input[kendoRadioButton]", inputs: ["size"] }, { kind: "ngmodule", type: NumericTextBoxModule }, { kind: "ngmodule", type: DropDownsModule }, { kind: "ngmodule", type: SVGIconModule }, { kind: "component", type: i1$4.SVGIconComponent, selector: "kendo-svg-icon, kendo-svgicon", inputs: ["icon"], exportAs: ["kendoSVGIcon"] }, { 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: FieldFilterEditorComponent, selector: "mm-field-filter-editor", inputs: ["availableAttributes", "ckTypeId", "hideNavigationProperties", "attributePaths", "enableVariables", "availableVariables", "filters"], outputs: ["filtersChange"] }, { kind: "component", type: QuerySelectorComponent, selector: "mm-query-selector", inputs: ["placeholder", "hint", "disabled", "acceptFamilies"], outputs: ["querySelected", "queriesLoaded"] }, { kind: "component", type: LoadingOverlayComponent, selector: "mm-loading-overlay", inputs: ["loading"] }] });
8302
8722
  }
8303
8723
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: TableConfigDialogComponent, decorators: [{
8304
8724
  type: Component,
@@ -8545,6 +8965,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
8545
8965
  type: Input
8546
8966
  }], initialQueryName: [{
8547
8967
  type: Input
8968
+ }], initialQueryFamily: [{
8969
+ type: Input
8548
8970
  }] } });
8549
8971
 
8550
8972
  class GaugeWidgetComponent {
@@ -8563,7 +8985,12 @@ class GaugeWidgetComponent {
8563
8985
  },
8564
8986
  ];
8565
8987
  dataService = inject(DashboardDataService);
8566
- executeRuntimeQueryGQL = inject(ExecuteRuntimeQueryDtoGQL);
8988
+ queryExecutor = inject(QueryExecutorService);
8989
+ static SUPPORTED_ROW_TYPES = new Set([
8990
+ 'RtAggregationQueryRow',
8991
+ 'RtGroupingAggregationQueryRow',
8992
+ 'StreamDataQueryRow'
8993
+ ]);
8567
8994
  stateService = inject(MeshBoardStateService);
8568
8995
  variableService = inject(MeshBoardVariableService);
8569
8996
  config;
@@ -8673,43 +9100,29 @@ class GaugeWidgetComponent {
8673
9100
  this._isLoading.set(true);
8674
9101
  this._error.set(null);
8675
9102
  try {
8676
- // Convert widget filters to GraphQL format
8677
9103
  const fieldFilter = this.convertFiltersToDto(this.config.filters);
8678
- const result = await firstValueFrom(this.executeRuntimeQueryGQL.fetch({
8679
- variables: {
8680
- rtId: dataSource.queryRtId,
8681
- fieldFilter
8682
- }
9104
+ // queryFamily may be undefined for legacy widget configs — the executor
9105
+ // falls back to a one-time lookup by rtId. streamDataArgs is sent
9106
+ // unconditionally because the runtime path ignores it.
9107
+ const streamDataArgs = this.buildStreamDataArgs();
9108
+ const result = await firstValueFrom(this.queryExecutor.execute(dataSource.queryFamily, dataSource.queryRtId, {
9109
+ fieldFilter: fieldFilter ?? undefined,
9110
+ streamDataArgs
8683
9111
  }).pipe(catchError(err => {
8684
9112
  console.error('Error loading Gauge query data:', err);
8685
9113
  throw err;
8686
9114
  })));
8687
- const queryItems = result.data?.runtime?.runtimeQuery?.items ?? [];
8688
- if (queryItems.length === 0) {
8689
- this._error.set('Query returned no results');
8690
- this._isLoading.set(false);
8691
- return;
8692
- }
8693
- const queryResult = queryItems[0];
8694
- if (!queryResult) {
8695
- this._error.set('Query returned no results');
8696
- this._isLoading.set(false);
8697
- return;
8698
- }
8699
9115
  let value = 0;
8700
9116
  const queryMode = this.config.queryMode ?? 'simpleCount';
8701
9117
  switch (queryMode) {
8702
9118
  case 'simpleCount':
8703
- // Use totalCount from the query
8704
- value = queryResult.rows?.totalCount ?? 0;
9119
+ value = result.totalCount;
8705
9120
  break;
8706
9121
  case 'aggregation':
8707
- // Get the single value from aggregation query (1 row, 1 column)
8708
- value = this.extractAggregationValue(queryResult);
9122
+ value = this.extractAggregationValue(result);
8709
9123
  break;
8710
9124
  case 'groupedAggregation':
8711
- // Find the row matching the selected category and get its value
8712
- value = this.extractGroupedAggregationValue(queryResult);
9125
+ value = this.extractGroupedAggregationValue(result);
8713
9126
  break;
8714
9127
  }
8715
9128
  // Create a synthetic entity with the value
@@ -8728,48 +9141,38 @@ class GaugeWidgetComponent {
8728
9141
  this._isLoading.set(false);
8729
9142
  }
8730
9143
  }
9144
+ buildStreamDataArgs() {
9145
+ const range = this.stateService.resolveCurrentTimeRange();
9146
+ if (!range) {
9147
+ return undefined;
9148
+ }
9149
+ return { from: range.from, to: range.to };
9150
+ }
8731
9151
  extractAggregationValue(queryResult) {
8732
- const rows = queryResult.rows?.items ?? [];
8733
- const supportedRowTypes = ['RtAggregationQueryRow', 'RtGroupingAggregationQueryRow'];
8734
- // Get the first row
8735
- const firstRow = rows.find(row => row && supportedRowTypes.includes(row.__typename ?? ''));
9152
+ const firstRow = queryResult.rows.find(row => GaugeWidgetComponent.SUPPORTED_ROW_TYPES.has(row.__typename ?? ''));
8736
9153
  if (!firstRow)
8737
9154
  return 0;
8738
- const queryRow = firstRow;
8739
- const cells = queryRow.cells?.items ?? [];
8740
- // Find the value field or use the first numeric cell
8741
9155
  const valueField = this.config.queryValueField;
8742
- for (const cell of cells) {
8743
- if (!cell?.attributePath)
8744
- continue;
9156
+ for (const cell of firstRow.cells) {
8745
9157
  if (valueField && matchesAttributePath(cell.attributePath, valueField)) {
8746
9158
  return this.parseNumericValue(cell.value);
8747
9159
  }
8748
9160
  }
8749
- // Fallback: return first cell value if no specific field configured
8750
- const firstCell = cells.find(c => c !== null);
8751
- return firstCell ? this.parseNumericValue(firstCell.value) : 0;
9161
+ return firstRow.cells.length > 0 ? this.parseNumericValue(firstRow.cells[0].value) : 0;
8752
9162
  }
8753
9163
  extractGroupedAggregationValue(queryResult) {
8754
- const rows = queryResult.rows?.items ?? [];
8755
- const supportedRowTypes = ['RtGroupingAggregationQueryRow', 'RtAggregationQueryRow'];
8756
9164
  const categoryField = this.config.queryCategoryField;
8757
9165
  const categoryValue = this.config.queryCategoryValue;
8758
9166
  const valueField = this.config.queryValueField;
8759
9167
  if (!categoryField || !categoryValue || !valueField) {
8760
9168
  return 0;
8761
9169
  }
8762
- // Find the row where category matches
8763
- for (const row of rows) {
8764
- if (!row || !supportedRowTypes.includes(row.__typename ?? ''))
9170
+ for (const row of queryResult.rows) {
9171
+ if (!GaugeWidgetComponent.SUPPORTED_ROW_TYPES.has(row.__typename ?? ''))
8765
9172
  continue;
8766
- const queryRow = row;
8767
- const cells = queryRow.cells?.items ?? [];
8768
9173
  let categoryMatch = false;
8769
9174
  let value = 0;
8770
- for (const cell of cells) {
8771
- if (!cell?.attributePath)
8772
- continue;
9175
+ for (const cell of row.cells) {
8773
9176
  if (matchesAttributePath(cell.attributePath, categoryField) && String(cell.value) === categoryValue) {
8774
9177
  categoryMatch = true;
8775
9178
  }
@@ -9157,8 +9560,18 @@ class GaugeConfigDialogComponent {
9157
9560
  getEntitiesByCkTypeGQL = inject(GetEntitiesByCkTypeDtoGQL);
9158
9561
  ckTypeSelectorService = inject(CkTypeSelectorService);
9159
9562
  attributeSelectorService = inject(AttributeSelectorService);
9160
- executeRuntimeQueryGQL = inject(ExecuteRuntimeQueryDtoGQL);
9161
9563
  getRuntimeQueryColumnsGQL = inject(GetRuntimeQueryColumnsDtoGQL);
9564
+ queryExecutor = inject(QueryExecutorService);
9565
+ /**
9566
+ * Row __typenames the dialog recognises when collecting distinct category
9567
+ * values from a query result for the grouped-aggregation category picker.
9568
+ */
9569
+ static INTROSPECTION_ROW_TYPES = new Set([
9570
+ 'RtSimpleQueryRow',
9571
+ 'RtAggregationQueryRow',
9572
+ 'RtGroupingAggregationQueryRow',
9573
+ 'StreamDataQueryRow'
9574
+ ]);
9162
9575
  meshBoardStateService = inject(MeshBoardStateService);
9163
9576
  windowRef = inject(WindowRef);
9164
9577
  ckTypeSelectorInput;
@@ -9181,6 +9594,7 @@ class GaugeConfigDialogComponent {
9181
9594
  initialDataSourceType;
9182
9595
  initialQueryRtId;
9183
9596
  initialQueryName;
9597
+ initialQueryFamily;
9184
9598
  initialQueryMode;
9185
9599
  initialQueryValueField;
9186
9600
  initialQueryCategoryField;
@@ -9428,6 +9842,7 @@ class GaugeConfigDialogComponent {
9428
9842
  }))
9429
9843
  : undefined;
9430
9844
  if (this.dataSourceType === 'persistentQuery' && this.selectedPersistentQuery) {
9845
+ const family = queryFamily(this.selectedPersistentQuery.ckTypeId) ?? this.initialQueryFamily ?? undefined;
9431
9846
  this.windowRef.close({
9432
9847
  dataSourceType: 'persistentQuery',
9433
9848
  ckTypeId: '',
@@ -9435,6 +9850,7 @@ class GaugeConfigDialogComponent {
9435
9850
  valueAttribute: '',
9436
9851
  queryRtId: this.selectedPersistentQuery.rtId,
9437
9852
  queryName: this.selectedPersistentQuery.name,
9853
+ queryFamily: family,
9438
9854
  queryMode: this.queryMode,
9439
9855
  queryValueField: this.form.queryValueField || undefined,
9440
9856
  queryCategoryField: this.form.queryCategoryField || undefined,
@@ -9507,31 +9923,13 @@ class GaugeConfigDialogComponent {
9507
9923
  }
9508
9924
  async loadQueryColumnsAndValues(queryRtId) {
9509
9925
  this.isLoadingQueryColumns = true;
9926
+ // queryFamily may be undefined when the selected query metadata is missing —
9927
+ // fetchColumnsForFamily resolves it via the executor's one-time lookup.
9928
+ const family = queryFamily(this.selectedPersistentQuery?.ckTypeId) ?? this.initialQueryFamily;
9510
9929
  try {
9511
- // Metadata-only fetch skips backend aggregation execution so the dialog opens
9512
- // fast even for large data sets.
9513
- const result = await firstValueFrom(this.getRuntimeQueryColumnsGQL.fetch({
9514
- variables: {
9515
- rtId: queryRtId
9516
- }
9517
- }));
9518
- const queryItems = result.data?.runtime?.runtimeQuery?.items ?? [];
9519
- if (queryItems.length > 0 && queryItems[0]) {
9520
- const columns = queryItems[0].columns ?? [];
9521
- const filteredColumns = columns
9522
- .filter((c) => c !== null);
9523
- // Engine emits column attributePath in wire form for aggregation / grouping
9524
- // columns, so the picker can use it as both label and stored value verbatim.
9525
- this.queryColumns = filteredColumns.map(c => ({
9526
- attributePath: c.attributePath ?? '',
9527
- attributeValueType: c.attributeValueType ?? '',
9528
- aggregationType: c.aggregationType ?? null
9529
- }));
9530
- // Category values for grouped aggregation are loaded on-demand by
9531
- // loadCategoryValuesForField — only when a categoryField is actually selected.
9532
- if (this.queryMode === 'groupedAggregation' && this.form.queryCategoryField) {
9533
- await this.loadCategoryValuesForField(queryRtId, this.form.queryCategoryField);
9534
- }
9930
+ this.queryColumns = await this.fetchColumnsForFamily(family, queryRtId);
9931
+ if (this.queryColumns.length > 0 && this.queryMode === 'groupedAggregation' && this.form.queryCategoryField) {
9932
+ await this.loadCategoryValuesForField(queryRtId, this.form.queryCategoryField);
9535
9933
  }
9536
9934
  }
9537
9935
  catch (error) {
@@ -9542,6 +9940,36 @@ class GaugeConfigDialogComponent {
9542
9940
  this.isLoadingQueryColumns = false;
9543
9941
  }
9544
9942
  }
9943
+ /**
9944
+ * Runtime queries use the metadata-only resolver (no aggregation executed);
9945
+ * stream-data queries fall back to executing the query with `first: 1`.
9946
+ * When `family` is unknown (legacy configs), the executor resolves it once
9947
+ * by rtId lookup.
9948
+ */
9949
+ async fetchColumnsForFamily(family, rtId) {
9950
+ const resolvedFamily = family ?? await this.queryExecutor.resolveFamily(rtId);
9951
+ if (resolvedFamily === 'runtime') {
9952
+ const result = await firstValueFrom(this.getRuntimeQueryColumnsGQL.fetch({
9953
+ variables: { rtId }
9954
+ }));
9955
+ const queryItem = result.data?.runtime?.runtimeQuery?.items?.[0];
9956
+ if (!queryItem)
9957
+ return [];
9958
+ return (queryItem.columns ?? [])
9959
+ .filter((c) => c !== null)
9960
+ .map(c => ({
9961
+ attributePath: c.attributePath ?? '',
9962
+ attributeValueType: c.attributeValueType ?? '',
9963
+ aggregationType: c.aggregationType ?? null
9964
+ }));
9965
+ }
9966
+ const sdResult = await firstValueFrom(this.queryExecutor.executeStreamData(rtId, { first: 1 }));
9967
+ return sdResult.columns.map(c => ({
9968
+ attributePath: c.attributePath,
9969
+ attributeValueType: c.attributeValueType ?? '',
9970
+ aggregationType: c.aggregationType ?? null
9971
+ }));
9972
+ }
9545
9973
  async onCategoryFieldChange(categoryField) {
9546
9974
  this.form.queryCategoryField = categoryField;
9547
9975
  this.form.queryCategoryValue = '';
@@ -9552,37 +9980,24 @@ class GaugeConfigDialogComponent {
9552
9980
  }
9553
9981
  async loadCategoryValuesForField(queryRtId, categoryField) {
9554
9982
  this.isLoadingCategoryValues = true;
9983
+ // family may be undefined — the executor falls back to a lookup.
9984
+ const family = queryFamily(this.selectedPersistentQuery?.ckTypeId) ?? this.initialQueryFamily;
9555
9985
  try {
9556
- const result = await firstValueFrom(this.executeRuntimeQueryGQL.fetch({
9557
- variables: {
9558
- rtId: queryRtId,
9559
- first: 100
9560
- }
9561
- }));
9562
- const queryItems = result.data?.runtime?.runtimeQuery?.items ?? [];
9563
- if (queryItems.length > 0 && queryItems[0]) {
9564
- const queryResult = queryItems[0];
9565
- const rows = queryResult.rows?.items ?? [];
9566
- const supportedRowTypes = ['RtSimpleQueryRow', 'RtAggregationQueryRow', 'RtGroupingAggregationQueryRow'];
9567
- const values = new Set();
9568
- for (const row of rows) {
9569
- if (!row || !supportedRowTypes.includes(row.__typename ?? ''))
9570
- continue;
9571
- const queryRow = row;
9572
- const cells = queryRow.cells?.items ?? [];
9573
- for (const cell of cells) {
9574
- if (!cell?.attributePath)
9575
- continue;
9576
- if (matchesAttributePath(cell.attributePath, categoryField) && cell.value !== null && cell.value !== undefined) {
9577
- values.add(String(cell.value));
9578
- }
9986
+ const result = await firstValueFrom(this.queryExecutor.execute(family, queryRtId, { first: 100 }));
9987
+ const values = new Set();
9988
+ for (const row of result.rows) {
9989
+ if (!GaugeConfigDialogComponent.INTROSPECTION_ROW_TYPES.has(row.__typename ?? ''))
9990
+ continue;
9991
+ for (const cell of row.cells) {
9992
+ if (matchesAttributePath(cell.attributePath, categoryField) && cell.value !== null && cell.value !== undefined) {
9993
+ values.add(String(cell.value));
9579
9994
  }
9580
9995
  }
9581
- this.categoryValues = Array.from(values).map(v => ({
9582
- value: v,
9583
- displayValue: v
9584
- }));
9585
9996
  }
9997
+ this.categoryValues = Array.from(values).map(v => ({
9998
+ value: v,
9999
+ displayValue: v
10000
+ }));
9586
10001
  }
9587
10002
  catch (error) {
9588
10003
  console.error('Error loading category values:', error);
@@ -9599,7 +10014,7 @@ class GaugeConfigDialogComponent {
9599
10014
  this.filters = updatedFilters;
9600
10015
  }
9601
10016
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: GaugeConfigDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
9602
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: GaugeConfigDialogComponent, isStandalone: true, selector: "mm-gauge-config-dialog", inputs: { initialCkTypeId: "initialCkTypeId", initialRtId: "initialRtId", initialGaugeType: "initialGaugeType", initialValueAttribute: "initialValueAttribute", initialLabelAttribute: "initialLabelAttribute", initialMin: "initialMin", initialMax: "initialMax", initialRanges: "initialRanges", initialShowLabel: "initialShowLabel", initialPrefix: "initialPrefix", initialSuffix: "initialSuffix", initialReverse: "initialReverse", initialDataSourceType: "initialDataSourceType", initialQueryRtId: "initialQueryRtId", initialQueryName: "initialQueryName", initialQueryMode: "initialQueryMode", initialQueryValueField: "initialQueryValueField", initialQueryCategoryField: "initialQueryCategoryField", initialQueryCategoryValue: "initialQueryCategoryValue", initialFilters: "initialFilters" }, viewQueries: [{ propertyName: "ckTypeSelectorInput", first: true, predicate: ["ckTypeSelector"], descendants: true }, { propertyName: "entitySelectorInput", first: true, predicate: ["entitySelector"], descendants: true }, { propertyName: "querySelector", first: true, predicate: ["querySelector"], descendants: true }], ngImport: i0, template: `
10017
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: GaugeConfigDialogComponent, isStandalone: true, selector: "mm-gauge-config-dialog", inputs: { initialCkTypeId: "initialCkTypeId", initialRtId: "initialRtId", initialGaugeType: "initialGaugeType", initialValueAttribute: "initialValueAttribute", initialLabelAttribute: "initialLabelAttribute", initialMin: "initialMin", initialMax: "initialMax", initialRanges: "initialRanges", initialShowLabel: "initialShowLabel", initialPrefix: "initialPrefix", initialSuffix: "initialSuffix", initialReverse: "initialReverse", initialDataSourceType: "initialDataSourceType", initialQueryRtId: "initialQueryRtId", initialQueryName: "initialQueryName", initialQueryFamily: "initialQueryFamily", initialQueryMode: "initialQueryMode", initialQueryValueField: "initialQueryValueField", initialQueryCategoryField: "initialQueryCategoryField", initialQueryCategoryValue: "initialQueryCategoryValue", initialFilters: "initialFilters" }, viewQueries: [{ propertyName: "ckTypeSelectorInput", first: true, predicate: ["ckTypeSelector"], descendants: true }, { propertyName: "entitySelectorInput", first: true, predicate: ["entitySelector"], descendants: true }, { propertyName: "querySelector", first: true, predicate: ["querySelector"], descendants: true }], ngImport: i0, template: `
9603
10018
  <div class="config-container">
9604
10019
 
9605
10020
  <div class="config-form" [class.loading]="isLoadingInitial">
@@ -9997,7 +10412,7 @@ class GaugeConfigDialogComponent {
9997
10412
  </button>
9998
10413
  </div>
9999
10414
  </div>
10000
- `, 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;position:relative;flex:1;overflow-y:auto}.config-form.loading{pointer-events:none}.form-field{display:flex;flex-direction:column;gap:6px}.form-field.disabled{opacity:.6}.form-field.flex-1{flex:1}.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}.form-section h4{margin:0 0 16px;font-size:.95rem;color:var(--kendo-color-primary, #0d6efd)}.section-hint{margin:0 0 12px;font-size:.85rem;color:var(--kendo-color-subtle, #6c757d)}.form-row{display:flex;gap:16px}.checkbox-label{margin-left:8px;font-weight:400}.gauge-type-item{display:flex;flex-direction:column;gap:2px}.gauge-type-label{font-weight:500}.gauge-type-desc{font-size:.75rem;color:var(--kendo-color-subtle, #6c757d)}.attribute-item{display:flex;justify-content:space-between;align-items:center;gap:8px;width:100%}.attribute-path{flex:1}.attribute-type{font-size:.75rem;color:var(--kendo-color-subtle, #6c757d);background:var(--kendo-color-surface-alt, #f8f9fa);padding:2px 6px;border-radius:3px}.range-row{display:flex;align-items:center;gap:8px;margin-bottom:8px}.range-input{width:80px}.range-separator{color:var(--kendo-color-subtle, #6c757d)}.color-picker{width:40px;height:32px;padding:2px;border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px;cursor:pointer}.mode-toggle{display:flex;gap:8px}.mode-toggle button{flex:1}.required{color:var(--kendo-color-error, #dc3545)}.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)}.column-item{display:flex;justify-content:space-between;gap:16px}.column-path{font-weight:500}.column-type{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][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", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon", "primary", "look"], 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: "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: "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: QuerySelectorComponent, selector: "mm-query-selector", inputs: ["placeholder", "hint", "disabled"], outputs: ["querySelected", "queriesLoaded"] }, { kind: "component", type: LoadingOverlayComponent, selector: "mm-loading-overlay", inputs: ["loading"] }] });
10415
+ `, 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;position:relative;flex:1;overflow-y:auto}.config-form.loading{pointer-events:none}.form-field{display:flex;flex-direction:column;gap:6px}.form-field.disabled{opacity:.6}.form-field.flex-1{flex:1}.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}.form-section h4{margin:0 0 16px;font-size:.95rem;color:var(--kendo-color-primary, #0d6efd)}.section-hint{margin:0 0 12px;font-size:.85rem;color:var(--kendo-color-subtle, #6c757d)}.form-row{display:flex;gap:16px}.checkbox-label{margin-left:8px;font-weight:400}.gauge-type-item{display:flex;flex-direction:column;gap:2px}.gauge-type-label{font-weight:500}.gauge-type-desc{font-size:.75rem;color:var(--kendo-color-subtle, #6c757d)}.attribute-item{display:flex;justify-content:space-between;align-items:center;gap:8px;width:100%}.attribute-path{flex:1}.attribute-type{font-size:.75rem;color:var(--kendo-color-subtle, #6c757d);background:var(--kendo-color-surface-alt, #f8f9fa);padding:2px 6px;border-radius:3px}.range-row{display:flex;align-items:center;gap:8px;margin-bottom:8px}.range-input{width:80px}.range-separator{color:var(--kendo-color-subtle, #6c757d)}.color-picker{width:40px;height:32px;padding:2px;border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px;cursor:pointer}.mode-toggle{display:flex;gap:8px}.mode-toggle button{flex:1}.required{color:var(--kendo-color-error, #dc3545)}.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)}.column-item{display:flex;justify-content:space-between;gap:16px}.column-path{font-weight:500}.column-type{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][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", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon", "primary", "look"], 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: "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: "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: QuerySelectorComponent, selector: "mm-query-selector", inputs: ["placeholder", "hint", "disabled", "acceptFamilies"], outputs: ["querySelected", "queriesLoaded"] }, { kind: "component", type: LoadingOverlayComponent, selector: "mm-loading-overlay", inputs: ["loading"] }] });
10001
10416
  }
10002
10417
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: GaugeConfigDialogComponent, decorators: [{
10003
10418
  type: Component,
@@ -10450,6 +10865,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
10450
10865
  type: Input
10451
10866
  }], initialQueryName: [{
10452
10867
  type: Input
10868
+ }], initialQueryFamily: [{
10869
+ type: Input
10453
10870
  }], initialQueryMode: [{
10454
10871
  type: Input
10455
10872
  }], initialQueryValueField: [{
@@ -10463,7 +10880,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
10463
10880
  }] } });
10464
10881
 
10465
10882
  class PieChartWidgetComponent {
10466
- executeRuntimeQueryGQL = inject(ExecuteRuntimeQueryDtoGQL);
10883
+ queryExecutor = inject(QueryExecutorService);
10884
+ static SUPPORTED_ROW_TYPES = new Set([
10885
+ 'RtSimpleQueryRow',
10886
+ 'RtAggregationQueryRow',
10887
+ 'RtGroupingAggregationQueryRow',
10888
+ 'StreamDataQueryRow'
10889
+ ]);
10467
10890
  dataService = inject(MeshBoardDataService);
10468
10891
  stateService = inject(MeshBoardStateService);
10469
10892
  variableService = inject(MeshBoardVariableService);
@@ -10588,35 +11011,22 @@ class PieChartWidgetComponent {
10588
11011
  * Note: isNotConfigured() check in loadData() ensures queryRtId is set.
10589
11012
  */
10590
11013
  async loadPersistentQueryData(dataSource) {
10591
- // Convert widget filters to GraphQL format
10592
11014
  const fieldFilter = this.convertFiltersToDto(this.config.filters);
10593
- const result = await firstValueFrom(this.executeRuntimeQueryGQL.fetch({
10594
- variables: {
10595
- rtId: dataSource.queryRtId,
10596
- fieldFilter
10597
- }
11015
+ // queryFamily may be undefined for legacy widget configs — the executor
11016
+ // falls back to a one-time lookup by rtId. streamDataArgs is sent
11017
+ // unconditionally because the runtime path ignores it.
11018
+ const streamDataArgs = this.buildStreamDataArgs();
11019
+ const result = await firstValueFrom(this.queryExecutor.execute(dataSource.queryFamily, dataSource.queryRtId, {
11020
+ fieldFilter: fieldFilter ?? undefined,
11021
+ streamDataArgs
10598
11022
  }).pipe(catchError(err => {
10599
11023
  console.error('Error loading Pie Chart data:', err);
10600
11024
  throw err;
10601
11025
  })));
10602
- const queryItems = result.data?.runtime?.runtimeQuery?.items ?? [];
10603
- if (queryItems.length === 0) {
10604
- this._chartData.set([]);
10605
- this._isLoading.set(false);
10606
- return;
10607
- }
10608
- const queryResult = queryItems[0];
10609
- if (!queryResult) {
10610
- this._chartData.set([]);
10611
- this._isLoading.set(false);
10612
- return;
10613
- }
10614
11026
  // Extract columns to verify configured fields are present. Both forms (original CK
10615
11027
  // path and engine wire-form) are accepted so saved configs survive the engine's
10616
11028
  // switch to wire-form keys without a migration.
10617
- const columnPaths = (queryResult.columns ?? [])
10618
- .filter((c) => c !== null)
10619
- .map(c => c.attributePath ?? '');
11029
+ const columnPaths = result.columns.map(c => c.attributePath);
10620
11030
  const categoryFieldPresent = columnPaths.some(p => matchesAttributePath(p, this.config.categoryField));
10621
11031
  const valueFieldPresent = columnPaths.some(p => matchesAttributePath(p, this.config.valueField));
10622
11032
  if (!categoryFieldPresent || !valueFieldPresent) {
@@ -10624,20 +11034,12 @@ class PieChartWidgetComponent {
10624
11034
  this._isLoading.set(false);
10625
11035
  return;
10626
11036
  }
10627
- // Extract rows and transform to chart data
10628
- const rows = queryResult.rows?.items ?? [];
10629
- const supportedRowTypes = ['RtSimpleQueryRow', 'RtAggregationQueryRow', 'RtGroupingAggregationQueryRow'];
10630
- const chartData = rows
10631
- .filter((row) => row !== null)
10632
- .filter(row => supportedRowTypes.includes(row.__typename ?? ''))
11037
+ const chartData = result.rows
11038
+ .filter(row => PieChartWidgetComponent.SUPPORTED_ROW_TYPES.has(row.__typename ?? ''))
10633
11039
  .map(row => {
10634
- const queryRow = row;
10635
- const cells = queryRow.cells?.items ?? [];
10636
11040
  let category = '';
10637
11041
  let value = 0;
10638
- for (const cell of cells) {
10639
- if (!cell?.attributePath)
10640
- continue;
11042
+ for (const cell of row.cells) {
10641
11043
  if (matchesAttributePath(cell.attributePath, this.config.categoryField)) {
10642
11044
  category = String(cell.value ?? '');
10643
11045
  }
@@ -10652,6 +11054,13 @@ class PieChartWidgetComponent {
10652
11054
  this._chartData.set(chartData);
10653
11055
  this._isLoading.set(false);
10654
11056
  }
11057
+ buildStreamDataArgs() {
11058
+ const range = this.stateService.resolveCurrentTimeRange();
11059
+ if (!range) {
11060
+ return undefined;
11061
+ }
11062
+ return { from: range.from, to: range.to };
11063
+ }
10655
11064
  /**
10656
11065
  * Converts widget filter configuration to GraphQL FieldFilterDto format.
10657
11066
  * Resolves MeshBoard variables in filter values before conversion.
@@ -10755,6 +11164,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
10755
11164
  */
10756
11165
  class PieChartConfigDialogComponent {
10757
11166
  getRuntimeQueryColumnsGQL = inject(GetRuntimeQueryColumnsDtoGQL);
11167
+ queryExecutor = inject(QueryExecutorService);
10758
11168
  stateService = inject(MeshBoardStateService);
10759
11169
  windowRef = inject(WindowRef);
10760
11170
  querySelector;
@@ -10762,6 +11172,7 @@ class PieChartConfigDialogComponent {
10762
11172
  initialDataSourceType;
10763
11173
  initialQueryRtId;
10764
11174
  initialQueryName;
11175
+ initialQueryFamily;
10765
11176
  initialChartType;
10766
11177
  initialCategoryField;
10767
11178
  initialValueField;
@@ -10922,37 +11333,19 @@ class PieChartConfigDialogComponent {
10922
11333
  }
10923
11334
  async loadQueryColumns(queryRtId) {
10924
11335
  this.isLoadingColumns = true;
11336
+ // family may be undefined when the selected query metadata is missing —
11337
+ // fetchColumnsForFamily resolves it via the executor's one-time lookup.
11338
+ const family = queryFamily(this.selectedPersistentQuery?.ckTypeId) ?? this.initialQueryFamily;
10925
11339
  try {
10926
- // Metadata-only query: column resolver runs off the cached query definition without
10927
- // touching the row execution path, so the dialog opens fast even when the underlying
10928
- // persistent query aggregates over a large data set.
10929
- const result = await firstValueFrom(this.getRuntimeQueryColumnsGQL.fetch({
10930
- variables: {
10931
- rtId: queryRtId
10932
- }
10933
- }));
10934
- const queryItems = result.data?.runtime?.runtimeQuery?.items ?? [];
10935
- if (queryItems.length > 0 && queryItems[0]) {
10936
- const columns = queryItems[0].columns ?? [];
10937
- const filteredColumns = columns
10938
- .filter((c) => c !== null);
10939
- // Engine emits column attributePath in wire form for aggregation / grouping
10940
- // columns (e.g. `quantity_sum`, `operatingstatus`); picker uses it verbatim.
10941
- this.queryColumns = filteredColumns.map(c => ({
10942
- attributePath: c.attributePath ?? '',
10943
- attributeValueType: c.attributeValueType ?? '',
10944
- aggregationType: c.aggregationType ?? null
10945
- }));
10946
- // Auto-select fields if only 2 columns (typical for grouped aggregations)
10947
- if (this.queryColumns.length === 2 && !this.form.categoryField && !this.form.valueField) {
10948
- // Assume first column is category, second is value (typical pattern)
10949
- const numericTypes = ['INTEGER', 'FLOAT', 'DOUBLE', 'DECIMAL', 'LONG'];
10950
- const valueColumn = this.queryColumns.find(c => numericTypes.includes(c.attributeValueType));
10951
- const categoryColumn = this.queryColumns.find(c => c !== valueColumn);
10952
- if (valueColumn && categoryColumn) {
10953
- this.form.valueField = valueColumn.attributePath;
10954
- this.form.categoryField = categoryColumn.attributePath;
10955
- }
11340
+ this.queryColumns = await this.fetchColumnsForFamily(family, queryRtId);
11341
+ // Auto-select fields if only 2 columns (typical for grouped aggregations)
11342
+ if (this.queryColumns.length === 2 && !this.form.categoryField && !this.form.valueField) {
11343
+ const numericTypes = ['INTEGER', 'FLOAT', 'DOUBLE', 'DECIMAL', 'LONG'];
11344
+ const valueColumn = this.queryColumns.find(c => numericTypes.includes(c.attributeValueType));
11345
+ const categoryColumn = this.queryColumns.find(c => c !== valueColumn);
11346
+ if (valueColumn && categoryColumn) {
11347
+ this.form.valueField = valueColumn.attributePath;
11348
+ this.form.categoryField = categoryColumn.attributePath;
10956
11349
  }
10957
11350
  }
10958
11351
  }
@@ -10964,6 +11357,35 @@ class PieChartConfigDialogComponent {
10964
11357
  this.isLoadingColumns = false;
10965
11358
  }
10966
11359
  }
11360
+ /**
11361
+ * Runtime queries use the metadata-only resolver (no aggregation executed);
11362
+ * stream-data queries fall back to executing the query with `first: 1`
11363
+ * because the SD path has no dedicated column-introspection endpoint today.
11364
+ */
11365
+ async fetchColumnsForFamily(family, rtId) {
11366
+ const resolvedFamily = family ?? await this.queryExecutor.resolveFamily(rtId);
11367
+ if (resolvedFamily === 'runtime') {
11368
+ const result = await firstValueFrom(this.getRuntimeQueryColumnsGQL.fetch({
11369
+ variables: { rtId }
11370
+ }));
11371
+ const queryItem = result.data?.runtime?.runtimeQuery?.items?.[0];
11372
+ if (!queryItem)
11373
+ return [];
11374
+ return (queryItem.columns ?? [])
11375
+ .filter((c) => c !== null)
11376
+ .map(c => ({
11377
+ attributePath: c.attributePath ?? '',
11378
+ attributeValueType: c.attributeValueType ?? '',
11379
+ aggregationType: c.aggregationType ?? null
11380
+ }));
11381
+ }
11382
+ const sdResult = await firstValueFrom(this.queryExecutor.executeStreamData(rtId, { first: 1 }));
11383
+ return sdResult.columns.map(c => ({
11384
+ attributePath: c.attributePath,
11385
+ attributeValueType: c.attributeValueType ?? '',
11386
+ aggregationType: c.aggregationType ?? null
11387
+ }));
11388
+ }
10967
11389
  onFiltersChange(updatedFilters) {
10968
11390
  this.filters = updatedFilters;
10969
11391
  }
@@ -10993,6 +11415,7 @@ class PieChartConfigDialogComponent {
10993
11415
  return;
10994
11416
  result.queryRtId = this.selectedPersistentQuery.rtId;
10995
11417
  result.queryName = this.selectedPersistentQuery.name;
11418
+ result.queryFamily = queryFamily(this.selectedPersistentQuery.ckTypeId) ?? this.initialQueryFamily ?? undefined;
10996
11419
  }
10997
11420
  else if (this.form.dataSourceType === 'constructionKitQuery') {
10998
11421
  result.ckQueryTarget = this.form.ckQueryTarget;
@@ -11004,7 +11427,7 @@ class PieChartConfigDialogComponent {
11004
11427
  this.windowRef.close();
11005
11428
  }
11006
11429
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: PieChartConfigDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
11007
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: PieChartConfigDialogComponent, isStandalone: true, selector: "mm-pie-chart-config-dialog", inputs: { initialDataSourceType: "initialDataSourceType", initialQueryRtId: "initialQueryRtId", initialQueryName: "initialQueryName", initialChartType: "initialChartType", initialCategoryField: "initialCategoryField", initialValueField: "initialValueField", initialShowLabels: "initialShowLabels", initialShowLegend: "initialShowLegend", initialLegendPosition: "initialLegendPosition", initialCkQueryTarget: "initialCkQueryTarget", initialCkGroupBy: "initialCkGroupBy", initialFilters: "initialFilters" }, viewQueries: [{ propertyName: "querySelector", first: true, predicate: ["querySelector"], descendants: true }], ngImport: i0, template: `
11430
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: PieChartConfigDialogComponent, isStandalone: true, selector: "mm-pie-chart-config-dialog", inputs: { initialDataSourceType: "initialDataSourceType", initialQueryRtId: "initialQueryRtId", initialQueryName: "initialQueryName", initialQueryFamily: "initialQueryFamily", initialChartType: "initialChartType", initialCategoryField: "initialCategoryField", initialValueField: "initialValueField", initialShowLabels: "initialShowLabels", initialShowLegend: "initialShowLegend", initialLegendPosition: "initialLegendPosition", initialCkQueryTarget: "initialCkQueryTarget", initialCkGroupBy: "initialCkGroupBy", initialFilters: "initialFilters" }, viewQueries: [{ propertyName: "querySelector", first: true, predicate: ["querySelector"], descendants: true }], ngImport: i0, template: `
11008
11431
  <div class="config-container">
11009
11432
 
11010
11433
  <div class="config-form" [class.loading]="isLoadingInitial">
@@ -11202,7 +11625,7 @@ class PieChartConfigDialogComponent {
11202
11625
  </button>
11203
11626
  </div>
11204
11627
  </div>
11205
- `, 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;flex:1;overflow-y:auto;gap:20px;padding:16px;position:relative}.config-form.loading{pointer-events:none}.config-section{padding:16px;background:var(--kendo-color-surface-alt, #f8f9fa);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px}.section-title{margin:0 0 16px;font-size:1rem;font-weight:600;color:var(--kendo-color-primary, #0d6efd)}.section-hint{margin:0 0 12px;font-size:.85rem;color:var(--kendo-color-subtle, #6c757d)}.form-field{display:flex;flex-direction:column;gap:6px;margin-bottom:12px}.form-field:last-child{margin-bottom:0}.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)}.radio-group{display:flex;gap:24px}.radio-label{display:flex;align-items:center;gap:8px;cursor:pointer;font-weight:400}.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)}.column-item{display:flex;justify-content:space-between;gap:16px}.column-path{font-weight:500}.column-type{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.RadioControlValueAccessor, selector: "input[type=radio][formControlName],input[type=radio][formControl],input[type=radio][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", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon", "primary", "look"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: InputsModule }, { kind: "directive", type: i3.RadioButtonDirective, selector: "input[kendoRadioButton]", inputs: ["size"] }, { 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: SVGIconModule }, { kind: "component", type: FieldFilterEditorComponent, selector: "mm-field-filter-editor", inputs: ["availableAttributes", "ckTypeId", "hideNavigationProperties", "attributePaths", "enableVariables", "availableVariables", "filters"], outputs: ["filtersChange"] }, { kind: "component", type: QuerySelectorComponent, selector: "mm-query-selector", inputs: ["placeholder", "hint", "disabled"], outputs: ["querySelected", "queriesLoaded"] }, { kind: "component", type: LoadingOverlayComponent, selector: "mm-loading-overlay", inputs: ["loading"] }] });
11628
+ `, 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;flex:1;overflow-y:auto;gap:20px;padding:16px;position:relative}.config-form.loading{pointer-events:none}.config-section{padding:16px;background:var(--kendo-color-surface-alt, #f8f9fa);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px}.section-title{margin:0 0 16px;font-size:1rem;font-weight:600;color:var(--kendo-color-primary, #0d6efd)}.section-hint{margin:0 0 12px;font-size:.85rem;color:var(--kendo-color-subtle, #6c757d)}.form-field{display:flex;flex-direction:column;gap:6px;margin-bottom:12px}.form-field:last-child{margin-bottom:0}.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)}.radio-group{display:flex;gap:24px}.radio-label{display:flex;align-items:center;gap:8px;cursor:pointer;font-weight:400}.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)}.column-item{display:flex;justify-content:space-between;gap:16px}.column-path{font-weight:500}.column-type{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.RadioControlValueAccessor, selector: "input[type=radio][formControlName],input[type=radio][formControl],input[type=radio][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", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon", "primary", "look"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: InputsModule }, { kind: "directive", type: i3.RadioButtonDirective, selector: "input[kendoRadioButton]", inputs: ["size"] }, { 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: SVGIconModule }, { kind: "component", type: FieldFilterEditorComponent, selector: "mm-field-filter-editor", inputs: ["availableAttributes", "ckTypeId", "hideNavigationProperties", "attributePaths", "enableVariables", "availableVariables", "filters"], outputs: ["filtersChange"] }, { kind: "component", type: QuerySelectorComponent, selector: "mm-query-selector", inputs: ["placeholder", "hint", "disabled", "acceptFamilies"], outputs: ["querySelected", "queriesLoaded"] }, { kind: "component", type: LoadingOverlayComponent, selector: "mm-loading-overlay", inputs: ["loading"] }] });
11206
11629
  }
11207
11630
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: PieChartConfigDialogComponent, decorators: [{
11208
11631
  type: Component,
@@ -11424,6 +11847,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
11424
11847
  type: Input
11425
11848
  }], initialQueryName: [{
11426
11849
  type: Input
11850
+ }], initialQueryFamily: [{
11851
+ type: Input
11427
11852
  }], initialChartType: [{
11428
11853
  type: Input
11429
11854
  }], initialCategoryField: [{
@@ -11453,7 +11878,13 @@ const CHART_TYPE_MAPPING = {
11453
11878
  stackedBar100: { type: 'bar', stack: '100%' }
11454
11879
  };
11455
11880
  class BarChartWidgetComponent {
11456
- executeRuntimeQueryGQL = inject(ExecuteRuntimeQueryDtoGQL);
11881
+ queryExecutor = inject(QueryExecutorService);
11882
+ static SUPPORTED_ROW_TYPES = new Set([
11883
+ 'RtSimpleQueryRow',
11884
+ 'RtAggregationQueryRow',
11885
+ 'RtGroupingAggregationQueryRow',
11886
+ 'StreamDataQueryRow'
11887
+ ]);
11457
11888
  stateService = inject(MeshBoardStateService);
11458
11889
  variableService = inject(MeshBoardVariableService);
11459
11890
  config;
@@ -11562,38 +11993,19 @@ class BarChartWidgetComponent {
11562
11993
  this._isLoading.set(true);
11563
11994
  this._error.set(null);
11564
11995
  try {
11565
- // Convert widget filters to GraphQL format
11566
11996
  const fieldFilter = this.convertFiltersToDto(this.config.filters);
11567
- const result = await firstValueFrom(this.executeRuntimeQueryGQL.fetch({
11568
- variables: {
11569
- rtId: queryDataSource.queryRtId,
11570
- fieldFilter
11571
- }
11997
+ // queryFamily may be undefined for legacy widget configs — the executor
11998
+ // falls back to a one-time lookup by rtId. streamDataArgs is sent
11999
+ // unconditionally because the runtime path ignores it.
12000
+ const streamDataArgs = this.buildStreamDataArgs();
12001
+ const result = await firstValueFrom(this.queryExecutor.execute(queryDataSource.queryFamily, queryDataSource.queryRtId, {
12002
+ fieldFilter: fieldFilter ?? undefined,
12003
+ streamDataArgs
11572
12004
  }).pipe(catchError(err => {
11573
12005
  console.error('Error loading Bar Chart data:', err);
11574
12006
  throw err;
11575
12007
  })));
11576
- const queryItems = result.data?.runtime?.runtimeQuery?.items ?? [];
11577
- if (queryItems.length === 0) {
11578
- this._categories.set([]);
11579
- this._seriesData.set([]);
11580
- this._isLoading.set(false);
11581
- return;
11582
- }
11583
- const queryResult = queryItems[0];
11584
- if (!queryResult) {
11585
- this._categories.set([]);
11586
- this._seriesData.set([]);
11587
- this._isLoading.set(false);
11588
- return;
11589
- }
11590
- // Extract rows
11591
- const rows = queryResult.rows?.items ?? [];
11592
- const supportedRowTypes = ['RtSimpleQueryRow', 'RtAggregationQueryRow', 'RtGroupingAggregationQueryRow'];
11593
- const filteredRows = rows
11594
- .filter((row) => row !== null)
11595
- .filter(row => supportedRowTypes.includes(row.__typename ?? ''));
11596
- // Process data based on mode
12008
+ const filteredRows = result.rows.filter(row => BarChartWidgetComponent.SUPPORTED_ROW_TYPES.has(row.__typename ?? ''));
11597
12009
  if (this.isDynamicSeriesMode()) {
11598
12010
  this.processDynamicSeriesData(filteredRows);
11599
12011
  }
@@ -11608,6 +12020,13 @@ class BarChartWidgetComponent {
11608
12020
  this._isLoading.set(false);
11609
12021
  }
11610
12022
  }
12023
+ buildStreamDataArgs() {
12024
+ const range = this.stateService.resolveCurrentTimeRange();
12025
+ if (!range) {
12026
+ return undefined;
12027
+ }
12028
+ return { from: range.from, to: range.to };
12029
+ }
11611
12030
  /**
11612
12031
  * Processes data in Static Series Mode.
11613
12032
  * Each series in config.series corresponds to a separate numeric field.
@@ -11620,13 +12039,9 @@ class BarChartWidgetComponent {
11620
12039
  seriesMap.set(seriesConfig.field, []);
11621
12040
  }
11622
12041
  for (const row of filteredRows) {
11623
- const queryRow = row;
11624
- const cells = queryRow.cells?.items ?? [];
11625
12042
  let categoryValue = '';
11626
12043
  const rowValues = new Map();
11627
- for (const cell of cells) {
11628
- if (!cell?.attributePath)
11629
- continue;
12044
+ for (const cell of row.cells) {
11630
12045
  if (matchesAttributePath(cell.attributePath, this.config.categoryField)) {
11631
12046
  categoryValue = this.formatCategoryValue(cell.value);
11632
12047
  }
@@ -11683,14 +12098,10 @@ class BarChartWidgetComponent {
11683
12098
  const allCategories = new Set();
11684
12099
  const allSeriesGroups = new Set();
11685
12100
  for (const row of filteredRows) {
11686
- const queryRow = row;
11687
- const cells = queryRow.cells?.items ?? [];
11688
12101
  let categoryValue = '';
11689
12102
  let seriesGroupValue = '';
11690
12103
  let numericValue = 0;
11691
- for (const cell of cells) {
11692
- if (!cell?.attributePath)
11693
- continue;
12104
+ for (const cell of row.cells) {
11694
12105
  if (matchesAttributePath(cell.attributePath, categoryField)) {
11695
12106
  categoryValue = this.formatCategoryValue(cell.value);
11696
12107
  }
@@ -11941,12 +12352,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
11941
12352
  */
11942
12353
  class BarChartConfigDialogComponent {
11943
12354
  getRuntimeQueryColumnsGQL = inject(GetRuntimeQueryColumnsDtoGQL);
12355
+ queryExecutor = inject(QueryExecutorService);
11944
12356
  stateService = inject(MeshBoardStateService);
11945
12357
  windowRef = inject(WindowRef);
11946
12358
  querySelector;
11947
12359
  // Initial values for editing
11948
12360
  initialQueryRtId;
11949
12361
  initialQueryName;
12362
+ initialQueryFamily;
11950
12363
  initialChartType;
11951
12364
  initialCategoryField;
11952
12365
  initialSeries;
@@ -12078,41 +12491,23 @@ class BarChartConfigDialogComponent {
12078
12491
  }
12079
12492
  async loadQueryColumns(queryRtId) {
12080
12493
  this.isLoadingColumns = true;
12494
+ // family may be undefined when the selected query metadata is missing —
12495
+ // fetchColumnsForFamily resolves it via the executor's one-time lookup.
12496
+ const family = queryFamily(this.selectedPersistentQuery?.ckTypeId) ?? this.initialQueryFamily;
12081
12497
  try {
12082
- // Metadata-only column resolver runs off the cached query definition and skips
12083
- // the row execution path, so the dialog opens fast even on large aggregations.
12084
- const result = await firstValueFrom(this.getRuntimeQueryColumnsGQL.fetch({
12085
- variables: {
12086
- rtId: queryRtId
12498
+ this.queryColumns = await this.fetchColumnsForFamily(family, queryRtId);
12499
+ // Filter numeric and non-numeric columns
12500
+ const numericTypes = ['INTEGER', 'FLOAT', 'DOUBLE', 'DECIMAL', 'LONG'];
12501
+ this.numericColumns = this.queryColumns.filter(c => numericTypes.includes(c.attributeValueType));
12502
+ this.nonNumericColumns = this.queryColumns.filter(c => !numericTypes.includes(c.attributeValueType));
12503
+ // Auto-select fields if possible and not editing
12504
+ if (!this.initialQueryRtId && this.queryColumns.length >= 2) {
12505
+ const categoryColumn = this.queryColumns.find(c => !numericTypes.includes(c.attributeValueType));
12506
+ if (categoryColumn) {
12507
+ this.form.categoryField = categoryColumn.attributePath;
12087
12508
  }
12088
- }));
12089
- const queryItems = result.data?.runtime?.runtimeQuery?.items ?? [];
12090
- if (queryItems.length > 0 && queryItems[0]) {
12091
- const columns = queryItems[0].columns ?? [];
12092
- const filteredColumns = columns
12093
- .filter((c) => c !== null);
12094
- // Engine emits column attributePath in wire form for aggregation / grouping
12095
- // columns (e.g. `quantity_sum`, `operatingstatus`); picker uses it verbatim.
12096
- this.queryColumns = filteredColumns.map(c => ({
12097
- attributePath: c.attributePath ?? '',
12098
- attributeValueType: c.attributeValueType ?? '',
12099
- aggregationType: c.aggregationType ?? null
12100
- }));
12101
- // Filter numeric and non-numeric columns
12102
- const numericTypes = ['INTEGER', 'FLOAT', 'DOUBLE', 'DECIMAL', 'LONG'];
12103
- this.numericColumns = this.queryColumns.filter(c => numericTypes.includes(c.attributeValueType));
12104
- this.nonNumericColumns = this.queryColumns.filter(c => !numericTypes.includes(c.attributeValueType));
12105
- // Auto-select fields if possible and not editing
12106
- if (!this.initialQueryRtId && this.queryColumns.length >= 2) {
12107
- // Find first non-numeric column for category
12108
- const categoryColumn = this.queryColumns.find(c => !numericTypes.includes(c.attributeValueType));
12109
- if (categoryColumn) {
12110
- this.form.categoryField = categoryColumn.attributePath;
12111
- }
12112
- // Auto-select all numeric columns as series
12113
- if (this.numericColumns.length > 0) {
12114
- this.selectedSeriesFields = this.numericColumns.map(c => c.attributePath);
12115
- }
12509
+ if (this.numericColumns.length > 0) {
12510
+ this.selectedSeriesFields = this.numericColumns.map(c => c.attributePath);
12116
12511
  }
12117
12512
  }
12118
12513
  }
@@ -12126,6 +12521,35 @@ class BarChartConfigDialogComponent {
12126
12521
  this.isLoadingColumns = false;
12127
12522
  }
12128
12523
  }
12524
+ /**
12525
+ * Runtime queries use the metadata-only resolver (no aggregation executed);
12526
+ * stream-data queries fall back to executing the query with `first: 1`
12527
+ * because the SD path has no dedicated column-introspection endpoint today.
12528
+ */
12529
+ async fetchColumnsForFamily(family, rtId) {
12530
+ const resolvedFamily = family ?? await this.queryExecutor.resolveFamily(rtId);
12531
+ if (resolvedFamily === 'runtime') {
12532
+ const result = await firstValueFrom(this.getRuntimeQueryColumnsGQL.fetch({
12533
+ variables: { rtId }
12534
+ }));
12535
+ const queryItem = result.data?.runtime?.runtimeQuery?.items?.[0];
12536
+ if (!queryItem)
12537
+ return [];
12538
+ return (queryItem.columns ?? [])
12539
+ .filter((c) => c !== null)
12540
+ .map(c => ({
12541
+ attributePath: c.attributePath ?? '',
12542
+ attributeValueType: c.attributeValueType ?? '',
12543
+ aggregationType: c.aggregationType ?? null
12544
+ }));
12545
+ }
12546
+ const sdResult = await firstValueFrom(this.queryExecutor.executeStreamData(rtId, { first: 1 }));
12547
+ return sdResult.columns.map(c => ({
12548
+ attributePath: c.attributePath,
12549
+ attributeValueType: c.attributeValueType ?? '',
12550
+ aggregationType: c.aggregationType ?? null
12551
+ }));
12552
+ }
12129
12553
  onSeriesFieldsChange(fields) {
12130
12554
  this.selectedSeriesFields = fields;
12131
12555
  }
@@ -12155,11 +12579,13 @@ class BarChartConfigDialogComponent {
12155
12579
  comparisonValue: f.comparisonValue
12156
12580
  }))
12157
12581
  : undefined;
12582
+ const family = queryFamily(this.selectedPersistentQuery.ckTypeId) ?? this.initialQueryFamily ?? undefined;
12158
12583
  const result = {
12159
12584
  ckTypeId: '',
12160
12585
  rtId: '',
12161
12586
  queryRtId: this.selectedPersistentQuery.rtId,
12162
12587
  queryName: this.selectedPersistentQuery.name,
12588
+ queryFamily: family,
12163
12589
  chartType: this.form.chartType,
12164
12590
  categoryField: this.form.categoryField,
12165
12591
  series,
@@ -12187,7 +12613,7 @@ class BarChartConfigDialogComponent {
12187
12613
  this.windowRef.close();
12188
12614
  }
12189
12615
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: BarChartConfigDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
12190
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: BarChartConfigDialogComponent, isStandalone: true, selector: "mm-bar-chart-config-dialog", inputs: { initialQueryRtId: "initialQueryRtId", initialQueryName: "initialQueryName", initialChartType: "initialChartType", initialCategoryField: "initialCategoryField", initialSeries: "initialSeries", initialSeriesGroupField: "initialSeriesGroupField", initialValueField: "initialValueField", initialShowLegend: "initialShowLegend", initialLegendPosition: "initialLegendPosition", initialShowDataLabels: "initialShowDataLabels", initialColorThresholds: "initialColorThresholds", initialDefaultBarColor: "initialDefaultBarColor", initialFilters: "initialFilters" }, viewQueries: [{ propertyName: "querySelector", first: true, predicate: ["querySelector"], descendants: true }], ngImport: i0, template: `
12616
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: BarChartConfigDialogComponent, isStandalone: true, selector: "mm-bar-chart-config-dialog", inputs: { initialQueryRtId: "initialQueryRtId", initialQueryName: "initialQueryName", initialQueryFamily: "initialQueryFamily", initialChartType: "initialChartType", initialCategoryField: "initialCategoryField", initialSeries: "initialSeries", initialSeriesGroupField: "initialSeriesGroupField", initialValueField: "initialValueField", initialShowLegend: "initialShowLegend", initialLegendPosition: "initialLegendPosition", initialShowDataLabels: "initialShowDataLabels", initialColorThresholds: "initialColorThresholds", initialDefaultBarColor: "initialDefaultBarColor", initialFilters: "initialFilters" }, viewQueries: [{ propertyName: "querySelector", first: true, predicate: ["querySelector"], descendants: true }], ngImport: i0, template: `
12191
12617
  <div class="config-container">
12192
12618
 
12193
12619
  <div class="config-form" [class.loading]="isLoadingInitial">
@@ -12448,7 +12874,7 @@ class BarChartConfigDialogComponent {
12448
12874
  </button>
12449
12875
  </div>
12450
12876
  </div>
12451
- `, 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;flex:1;overflow-y:auto;padding:16px;position:relative}.config-form.loading{pointer-events:none}.config-section{padding:16px;background:var(--kendo-color-surface-alt, #f8f9fa);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px}.section-title{margin:0 0 16px;font-size:1rem;font-weight:600;color:var(--kendo-color-primary, #0d6efd)}.section-hint{margin:0 0 12px;font-size:.85rem;color:var(--kendo-color-subtle, #6c757d)}.form-field{display:flex;flex-direction:column;gap:6px;margin-bottom:12px}.form-field:last-child{margin-bottom:0}.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)}.chart-type-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px}.radio-label{display:flex;align-items:center;gap:8px;cursor:pointer;font-weight:400}.form-row{display:flex;gap:24px}.checkbox-field{flex-direction:row;align-items:center}.checkbox-field label{display:flex;align-items:center;gap:8px;cursor:pointer;font-weight:400}.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)}.column-item{display:flex;justify-content:space-between;gap:16px}.column-path{font-weight:500}.column-type{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.form-section{margin-top:8px}.form-section h4{margin:0 0 4px;font-size:.95rem;font-weight:600}.threshold-row{display:flex;gap:8px;align-items:center;margin-bottom:8px}.threshold-row label{font-size:.85rem;min-width:70px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1$1.RadioControlValueAccessor, selector: "input[type=radio][formControlName],input[type=radio][formControl],input[type=radio][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", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon", "primary", "look"], 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: "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: "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: "component", type: i4.MultiSelectComponent, selector: "kendo-multiselect", inputs: ["showStickyHeader", "focusableId", "autoClose", "loading", "data", "value", "valueField", "textField", "tabindex", "tabIndex", "size", "rounded", "fillMode", "placeholder", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "disabled", "itemDisabled", "checkboxes", "readonly", "filterable", "virtual", "popupSettings", "listHeight", "valuePrimitive", "clearButton", "tagMapper", "allowCustom", "valueNormalizer", "inputAttributes"], outputs: ["filterChange", "valueChange", "open", "opened", "close", "closed", "focus", "blur", "inputFocus", "inputBlur", "removeTag"], exportAs: ["kendoMultiSelect"] }, { kind: "ngmodule", type: SVGIconModule }, { kind: "component", type: FieldFilterEditorComponent, selector: "mm-field-filter-editor", inputs: ["availableAttributes", "ckTypeId", "hideNavigationProperties", "attributePaths", "enableVariables", "availableVariables", "filters"], outputs: ["filtersChange"] }, { kind: "component", type: QuerySelectorComponent, selector: "mm-query-selector", inputs: ["placeholder", "hint", "disabled"], outputs: ["querySelected", "queriesLoaded"] }, { kind: "component", type: LoadingOverlayComponent, selector: "mm-loading-overlay", inputs: ["loading"] }] });
12877
+ `, 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;flex:1;overflow-y:auto;padding:16px;position:relative}.config-form.loading{pointer-events:none}.config-section{padding:16px;background:var(--kendo-color-surface-alt, #f8f9fa);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px}.section-title{margin:0 0 16px;font-size:1rem;font-weight:600;color:var(--kendo-color-primary, #0d6efd)}.section-hint{margin:0 0 12px;font-size:.85rem;color:var(--kendo-color-subtle, #6c757d)}.form-field{display:flex;flex-direction:column;gap:6px;margin-bottom:12px}.form-field:last-child{margin-bottom:0}.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)}.chart-type-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px}.radio-label{display:flex;align-items:center;gap:8px;cursor:pointer;font-weight:400}.form-row{display:flex;gap:24px}.checkbox-field{flex-direction:row;align-items:center}.checkbox-field label{display:flex;align-items:center;gap:8px;cursor:pointer;font-weight:400}.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)}.column-item{display:flex;justify-content:space-between;gap:16px}.column-path{font-weight:500}.column-type{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.form-section{margin-top:8px}.form-section h4{margin:0 0 4px;font-size:.95rem;font-weight:600}.threshold-row{display:flex;gap:8px;align-items:center;margin-bottom:8px}.threshold-row label{font-size:.85rem;min-width:70px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1$1.RadioControlValueAccessor, selector: "input[type=radio][formControlName],input[type=radio][formControl],input[type=radio][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", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon", "primary", "look"], 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: "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: "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: "component", type: i4.MultiSelectComponent, selector: "kendo-multiselect", inputs: ["showStickyHeader", "focusableId", "autoClose", "loading", "data", "value", "valueField", "textField", "tabindex", "tabIndex", "size", "rounded", "fillMode", "placeholder", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "disabled", "itemDisabled", "checkboxes", "readonly", "filterable", "virtual", "popupSettings", "listHeight", "valuePrimitive", "clearButton", "tagMapper", "allowCustom", "valueNormalizer", "inputAttributes"], outputs: ["filterChange", "valueChange", "open", "opened", "close", "closed", "focus", "blur", "inputFocus", "inputBlur", "removeTag"], exportAs: ["kendoMultiSelect"] }, { kind: "ngmodule", type: SVGIconModule }, { kind: "component", type: FieldFilterEditorComponent, selector: "mm-field-filter-editor", inputs: ["availableAttributes", "ckTypeId", "hideNavigationProperties", "attributePaths", "enableVariables", "availableVariables", "filters"], outputs: ["filtersChange"] }, { kind: "component", type: QuerySelectorComponent, selector: "mm-query-selector", inputs: ["placeholder", "hint", "disabled", "acceptFamilies"], outputs: ["querySelected", "queriesLoaded"] }, { kind: "component", type: LoadingOverlayComponent, selector: "mm-loading-overlay", inputs: ["loading"] }] });
12452
12878
  }
12453
12879
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: BarChartConfigDialogComponent, decorators: [{
12454
12880
  type: Component,
@@ -12731,6 +13157,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
12731
13157
  type: Input
12732
13158
  }], initialQueryName: [{
12733
13159
  type: Input
13160
+ }], initialQueryFamily: [{
13161
+ type: Input
12734
13162
  }], initialChartType: [{
12735
13163
  type: Input
12736
13164
  }], initialCategoryField: [{
@@ -12756,7 +13184,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
12756
13184
  }] } });
12757
13185
 
12758
13186
  class LineChartWidgetComponent {
12759
- executeRuntimeQueryGQL = inject(ExecuteRuntimeQueryDtoGQL);
13187
+ queryExecutor = inject(QueryExecutorService);
13188
+ static SUPPORTED_ROW_TYPES = new Set([
13189
+ 'RtSimpleQueryRow',
13190
+ 'RtAggregationQueryRow',
13191
+ 'RtGroupingAggregationQueryRow',
13192
+ 'StreamDataQueryRow'
13193
+ ]);
12760
13194
  stateService = inject(MeshBoardStateService);
12761
13195
  variableService = inject(MeshBoardVariableService);
12762
13196
  config;
@@ -12868,36 +13302,18 @@ class LineChartWidgetComponent {
12868
13302
  this._error.set(null);
12869
13303
  try {
12870
13304
  const fieldFilter = this.convertFiltersToDto(this.config.filters);
12871
- const result = await firstValueFrom(this.executeRuntimeQueryGQL.fetch({
12872
- variables: {
12873
- rtId: queryDataSource.queryRtId,
12874
- fieldFilter
12875
- }
13305
+ // queryFamily may be undefined for legacy widget configs — the executor
13306
+ // falls back to a one-time lookup by rtId. streamDataArgs is sent
13307
+ // unconditionally because the runtime path ignores it.
13308
+ const streamDataArgs = this.buildStreamDataArgs();
13309
+ const result = await firstValueFrom(this.queryExecutor.execute(queryDataSource.queryFamily, queryDataSource.queryRtId, {
13310
+ fieldFilter: fieldFilter ?? undefined,
13311
+ streamDataArgs
12876
13312
  }).pipe(catchError(err => {
12877
13313
  console.error('Error loading Line Chart data:', err);
12878
13314
  throw err;
12879
13315
  })));
12880
- const queryItems = result.data?.runtime?.runtimeQuery?.items ?? [];
12881
- if (queryItems.length === 0) {
12882
- this._categories.set([]);
12883
- this._seriesData.set([]);
12884
- this._valueAxes.set([]);
12885
- this._isLoading.set(false);
12886
- return;
12887
- }
12888
- const queryResult = queryItems[0];
12889
- if (!queryResult) {
12890
- this._categories.set([]);
12891
- this._seriesData.set([]);
12892
- this._valueAxes.set([]);
12893
- this._isLoading.set(false);
12894
- return;
12895
- }
12896
- const rows = queryResult.rows?.items ?? [];
12897
- const supportedRowTypes = ['RtSimpleQueryRow', 'RtAggregationQueryRow', 'RtGroupingAggregationQueryRow'];
12898
- const filteredRows = rows
12899
- .filter((row) => row !== null)
12900
- .filter(row => supportedRowTypes.includes(row.__typename ?? ''));
13316
+ const filteredRows = result.rows.filter(row => LineChartWidgetComponent.SUPPORTED_ROW_TYPES.has(row.__typename ?? ''));
12901
13317
  this.processData(filteredRows);
12902
13318
  this._isLoading.set(false);
12903
13319
  }
@@ -12907,6 +13323,13 @@ class LineChartWidgetComponent {
12907
13323
  this._isLoading.set(false);
12908
13324
  }
12909
13325
  }
13326
+ buildStreamDataArgs() {
13327
+ const range = this.stateService.resolveCurrentTimeRange();
13328
+ if (!range) {
13329
+ return undefined;
13330
+ }
13331
+ return { from: range.from, to: range.to };
13332
+ }
12910
13333
  /**
12911
13334
  * Processes query rows into line chart data.
12912
13335
  * Groups by seriesGroupField, orders by categoryField (date), supports multi-axis by unitField.
@@ -12922,15 +13345,11 @@ class LineChartWidgetComponent {
12922
13345
  const allSeriesGroups = new Set();
12923
13346
  const seriesUnitMap = new Map(); // seriesGroup -> unit
12924
13347
  for (const row of filteredRows) {
12925
- const queryRow = row;
12926
- const cells = queryRow.cells?.items ?? [];
12927
13348
  let categoryValue = '';
12928
13349
  let seriesGroupValue = '';
12929
13350
  let numericValue = 0;
12930
13351
  let unitValue = '';
12931
- for (const cell of cells) {
12932
- if (!cell?.attributePath)
12933
- continue;
13352
+ for (const cell of row.cells) {
12934
13353
  if (matchesAttributePath(cell.attributePath, categoryField)) {
12935
13354
  categoryValue = String(cell.value ?? '');
12936
13355
  }
@@ -13226,12 +13645,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
13226
13645
  */
13227
13646
  class LineChartConfigDialogComponent {
13228
13647
  getRuntimeQueryColumnsGQL = inject(GetRuntimeQueryColumnsDtoGQL);
13648
+ queryExecutor = inject(QueryExecutorService);
13229
13649
  stateService = inject(MeshBoardStateService);
13230
13650
  windowRef = inject(WindowRef);
13231
13651
  querySelector;
13232
13652
  // Initial values for editing
13233
13653
  initialQueryRtId;
13234
13654
  initialQueryName;
13655
+ initialQueryFamily;
13235
13656
  initialChartType;
13236
13657
  initialCategoryField;
13237
13658
  initialSeriesGroupField;
@@ -13335,27 +13756,14 @@ class LineChartConfigDialogComponent {
13335
13756
  }
13336
13757
  async loadQueryColumns(queryRtId) {
13337
13758
  this.isLoadingColumns = true;
13759
+ // family may be undefined when the selected query metadata is missing —
13760
+ // fetchColumnsForFamily resolves it via the executor's one-time lookup.
13761
+ const family = queryFamily(this.selectedPersistentQuery?.ckTypeId) ?? this.initialQueryFamily;
13338
13762
  try {
13339
- // Metadata-only skips the row execution path on the backend.
13340
- const result = await firstValueFrom(this.getRuntimeQueryColumnsGQL.fetch({
13341
- variables: {
13342
- rtId: queryRtId
13343
- }
13344
- }));
13345
- const queryItems = result.data?.runtime?.runtimeQuery?.items ?? [];
13346
- if (queryItems.length > 0 && queryItems[0]) {
13347
- const columns = queryItems[0].columns ?? [];
13348
- const filteredColumns = columns
13349
- .filter((c) => c !== null);
13350
- this.queryColumns = filteredColumns.map(c => ({
13351
- attributePath: c.attributePath ?? '',
13352
- attributeValueType: c.attributeValueType ?? '',
13353
- aggregationType: c.aggregationType ?? null
13354
- }));
13355
- const numericTypes = ['INTEGER', 'FLOAT', 'DOUBLE', 'DECIMAL', 'LONG'];
13356
- this.numericColumns = this.queryColumns.filter(c => numericTypes.includes(c.attributeValueType));
13357
- this.nonNumericColumns = this.queryColumns.filter(c => !numericTypes.includes(c.attributeValueType));
13358
- }
13763
+ this.queryColumns = await this.fetchColumnsForFamily(family, queryRtId);
13764
+ const numericTypes = ['INTEGER', 'FLOAT', 'DOUBLE', 'DECIMAL', 'LONG'];
13765
+ this.numericColumns = this.queryColumns.filter(c => numericTypes.includes(c.attributeValueType));
13766
+ this.nonNumericColumns = this.queryColumns.filter(c => !numericTypes.includes(c.attributeValueType));
13359
13767
  }
13360
13768
  catch (error) {
13361
13769
  console.error('Error loading query columns:', error);
@@ -13367,6 +13775,35 @@ class LineChartConfigDialogComponent {
13367
13775
  this.isLoadingColumns = false;
13368
13776
  }
13369
13777
  }
13778
+ /**
13779
+ * Runtime queries use the metadata-only resolver (no aggregation executed);
13780
+ * stream-data queries fall back to executing the query with `first: 1`
13781
+ * because the SD path has no dedicated column-introspection endpoint today.
13782
+ */
13783
+ async fetchColumnsForFamily(family, rtId) {
13784
+ const resolvedFamily = family ?? await this.queryExecutor.resolveFamily(rtId);
13785
+ if (resolvedFamily === 'runtime') {
13786
+ const result = await firstValueFrom(this.getRuntimeQueryColumnsGQL.fetch({
13787
+ variables: { rtId }
13788
+ }));
13789
+ const queryItem = result.data?.runtime?.runtimeQuery?.items?.[0];
13790
+ if (!queryItem)
13791
+ return [];
13792
+ return (queryItem.columns ?? [])
13793
+ .filter((c) => c !== null)
13794
+ .map(c => ({
13795
+ attributePath: c.attributePath ?? '',
13796
+ attributeValueType: c.attributeValueType ?? '',
13797
+ aggregationType: c.aggregationType ?? null
13798
+ }));
13799
+ }
13800
+ const sdResult = await firstValueFrom(this.queryExecutor.executeStreamData(rtId, { first: 1 }));
13801
+ return sdResult.columns.map(c => ({
13802
+ attributePath: c.attributePath,
13803
+ attributeValueType: c.attributeValueType ?? '',
13804
+ aggregationType: c.aggregationType ?? null
13805
+ }));
13806
+ }
13370
13807
  onFiltersChange(updatedFilters) {
13371
13808
  this.filters = updatedFilters;
13372
13809
  }
@@ -13380,11 +13817,13 @@ class LineChartConfigDialogComponent {
13380
13817
  comparisonValue: f.comparisonValue
13381
13818
  }))
13382
13819
  : undefined;
13820
+ const family = queryFamily(this.selectedPersistentQuery.ckTypeId) ?? this.initialQueryFamily ?? undefined;
13383
13821
  const result = {
13384
13822
  ckTypeId: '',
13385
13823
  rtId: '',
13386
13824
  queryRtId: this.selectedPersistentQuery.rtId,
13387
13825
  queryName: this.selectedPersistentQuery.name,
13826
+ queryFamily: family,
13388
13827
  chartType: this.form.chartType,
13389
13828
  categoryField: this.form.categoryField,
13390
13829
  seriesGroupField: this.form.seriesGroupField,
@@ -13408,7 +13847,7 @@ class LineChartConfigDialogComponent {
13408
13847
  this.windowRef.close();
13409
13848
  }
13410
13849
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: LineChartConfigDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
13411
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: LineChartConfigDialogComponent, isStandalone: true, selector: "mm-line-chart-config-dialog", inputs: { initialQueryRtId: "initialQueryRtId", initialQueryName: "initialQueryName", initialChartType: "initialChartType", initialCategoryField: "initialCategoryField", initialSeriesGroupField: "initialSeriesGroupField", initialValueField: "initialValueField", initialUnitField: "initialUnitField", initialShowLegend: "initialShowLegend", initialLegendPosition: "initialLegendPosition", initialShowMarkers: "initialShowMarkers", initialReferenceLines: "initialReferenceLines", initialFilters: "initialFilters" }, viewQueries: [{ propertyName: "querySelector", first: true, predicate: ["querySelector"], descendants: true }], ngImport: i0, template: `
13850
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: LineChartConfigDialogComponent, isStandalone: true, selector: "mm-line-chart-config-dialog", inputs: { initialQueryRtId: "initialQueryRtId", initialQueryName: "initialQueryName", initialQueryFamily: "initialQueryFamily", initialChartType: "initialChartType", initialCategoryField: "initialCategoryField", initialSeriesGroupField: "initialSeriesGroupField", initialValueField: "initialValueField", initialUnitField: "initialUnitField", initialShowLegend: "initialShowLegend", initialLegendPosition: "initialLegendPosition", initialShowMarkers: "initialShowMarkers", initialReferenceLines: "initialReferenceLines", initialFilters: "initialFilters" }, viewQueries: [{ propertyName: "querySelector", first: true, predicate: ["querySelector"], descendants: true }], ngImport: i0, template: `
13412
13851
  <div class="config-container">
13413
13852
 
13414
13853
  <div class="config-form" [class.loading]="isLoadingInitial">
@@ -13611,7 +14050,7 @@ class LineChartConfigDialogComponent {
13611
14050
  </button>
13612
14051
  </div>
13613
14052
  </div>
13614
- `, 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;flex:1;overflow-y:auto;padding:16px;position:relative}.config-form.loading{pointer-events:none}.config-section{padding:16px;background:var(--kendo-color-surface-alt, #f8f9fa);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px}.section-title{margin:0 0 16px;font-size:1rem;font-weight:600;color:var(--kendo-color-primary, #0d6efd)}.section-hint{margin:0 0 12px;font-size:.85rem;color:var(--kendo-color-subtle, #6c757d)}.form-field{display:flex;flex-direction:column;gap:6px;margin-bottom:12px}.form-field:last-child{margin-bottom:0}.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)}.chart-type-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px}.radio-label{display:flex;align-items:center;gap:8px;cursor:pointer;font-weight:400}.form-row{display:flex;gap:24px;align-items:center}.checkbox-field{flex-direction:row;align-items:center;margin-bottom:0}.checkbox-field label{display:flex;align-items:center;gap:8px;cursor:pointer;font-weight:400}.column-item{display:flex;justify-content:space-between;gap:16px}.column-path{font-weight:500}.column-type{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.form-section{margin-top:8px}.form-section h4{margin:0 0 4px;font-size:.95rem;font-weight:600}.reference-line-row{display:flex;gap:8px;align-items:center;margin-bottom:8px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1$1.RadioControlValueAccessor, selector: "input[type=radio][formControlName],input[type=radio][formControl],input[type=radio][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", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon", "primary", "look"], 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: "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: "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: SVGIconModule }, { kind: "component", type: FieldFilterEditorComponent, selector: "mm-field-filter-editor", inputs: ["availableAttributes", "ckTypeId", "hideNavigationProperties", "attributePaths", "enableVariables", "availableVariables", "filters"], outputs: ["filtersChange"] }, { kind: "component", type: QuerySelectorComponent, selector: "mm-query-selector", inputs: ["placeholder", "hint", "disabled"], outputs: ["querySelected", "queriesLoaded"] }, { kind: "component", type: LoadingOverlayComponent, selector: "mm-loading-overlay", inputs: ["loading"] }] });
14053
+ `, 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;flex:1;overflow-y:auto;padding:16px;position:relative}.config-form.loading{pointer-events:none}.config-section{padding:16px;background:var(--kendo-color-surface-alt, #f8f9fa);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px}.section-title{margin:0 0 16px;font-size:1rem;font-weight:600;color:var(--kendo-color-primary, #0d6efd)}.section-hint{margin:0 0 12px;font-size:.85rem;color:var(--kendo-color-subtle, #6c757d)}.form-field{display:flex;flex-direction:column;gap:6px;margin-bottom:12px}.form-field:last-child{margin-bottom:0}.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)}.chart-type-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px}.radio-label{display:flex;align-items:center;gap:8px;cursor:pointer;font-weight:400}.form-row{display:flex;gap:24px;align-items:center}.checkbox-field{flex-direction:row;align-items:center;margin-bottom:0}.checkbox-field label{display:flex;align-items:center;gap:8px;cursor:pointer;font-weight:400}.column-item{display:flex;justify-content:space-between;gap:16px}.column-path{font-weight:500}.column-type{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.form-section{margin-top:8px}.form-section h4{margin:0 0 4px;font-size:.95rem;font-weight:600}.reference-line-row{display:flex;gap:8px;align-items:center;margin-bottom:8px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1$1.RadioControlValueAccessor, selector: "input[type=radio][formControlName],input[type=radio][formControl],input[type=radio][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", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon", "primary", "look"], 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: "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: "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: SVGIconModule }, { kind: "component", type: FieldFilterEditorComponent, selector: "mm-field-filter-editor", inputs: ["availableAttributes", "ckTypeId", "hideNavigationProperties", "attributePaths", "enableVariables", "availableVariables", "filters"], outputs: ["filtersChange"] }, { kind: "component", type: QuerySelectorComponent, selector: "mm-query-selector", inputs: ["placeholder", "hint", "disabled", "acceptFamilies"], outputs: ["querySelected", "queriesLoaded"] }, { kind: "component", type: LoadingOverlayComponent, selector: "mm-loading-overlay", inputs: ["loading"] }] });
13615
14054
  }
13616
14055
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: LineChartConfigDialogComponent, decorators: [{
13617
14056
  type: Component,
@@ -13836,6 +14275,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
13836
14275
  type: Input
13837
14276
  }], initialQueryName: [{
13838
14277
  type: Input
14278
+ }], initialQueryFamily: [{
14279
+ type: Input
13839
14280
  }], initialChartType: [{
13840
14281
  type: Input
13841
14282
  }], initialCategoryField: [{
@@ -13876,7 +14317,13 @@ function buildGradientRanges(min, max, colors) {
13876
14317
  }));
13877
14318
  }
13878
14319
  class HeatmapWidgetComponent {
13879
- executeRuntimeQueryGQL = inject(ExecuteRuntimeQueryDtoGQL);
14320
+ queryExecutor = inject(QueryExecutorService);
14321
+ static SUPPORTED_ROW_TYPES = new Set([
14322
+ 'RtSimpleQueryRow',
14323
+ 'RtAggregationQueryRow',
14324
+ 'RtGroupingAggregationQueryRow',
14325
+ 'StreamDataQueryRow'
14326
+ ]);
13880
14327
  stateService = inject(MeshBoardStateService);
13881
14328
  variableService = inject(MeshBoardVariableService);
13882
14329
  config;
@@ -13996,36 +14443,18 @@ class HeatmapWidgetComponent {
13996
14443
  this._error.set(null);
13997
14444
  try {
13998
14445
  const fieldFilter = this.convertFiltersToDto(this.config.filters);
13999
- const result = await firstValueFrom(this.executeRuntimeQueryGQL.fetch({
14000
- variables: {
14001
- rtId: queryDataSource.queryRtId,
14002
- fieldFilter
14003
- }
14446
+ // queryFamily may be undefined for legacy widget configs — the executor
14447
+ // falls back to a one-time lookup by rtId. streamDataArgs is sent
14448
+ // unconditionally because the runtime path ignores it.
14449
+ const streamDataArgs = this.buildStreamDataArgs();
14450
+ const result = await firstValueFrom(this.queryExecutor.execute(queryDataSource.queryFamily, queryDataSource.queryRtId, {
14451
+ fieldFilter: fieldFilter ?? undefined,
14452
+ streamDataArgs
14004
14453
  }).pipe(catchError(err => {
14005
14454
  console.error('Error loading Heatmap data:', err);
14006
14455
  throw err;
14007
14456
  })));
14008
- const queryItems = result.data?.runtime?.runtimeQuery?.items ?? [];
14009
- if (queryItems.length === 0) {
14010
- this._heatmapData.set([]);
14011
- this._xCategories.set([]);
14012
- this._yCategories.set([]);
14013
- this._isLoading.set(false);
14014
- return;
14015
- }
14016
- const queryResult = queryItems[0];
14017
- if (!queryResult) {
14018
- this._heatmapData.set([]);
14019
- this._xCategories.set([]);
14020
- this._yCategories.set([]);
14021
- this._isLoading.set(false);
14022
- return;
14023
- }
14024
- const rows = queryResult.rows?.items ?? [];
14025
- const supportedRowTypes = ['RtSimpleQueryRow', 'RtAggregationQueryRow', 'RtGroupingAggregationQueryRow'];
14026
- const filteredRows = rows
14027
- .filter((row) => row !== null)
14028
- .filter(row => supportedRowTypes.includes(row.__typename ?? ''));
14457
+ const filteredRows = result.rows.filter(row => HeatmapWidgetComponent.SUPPORTED_ROW_TYPES.has(row.__typename ?? ''));
14029
14458
  this.processHeatmapData(filteredRows);
14030
14459
  this._isLoading.set(false);
14031
14460
  }
@@ -14035,6 +14464,13 @@ class HeatmapWidgetComponent {
14035
14464
  this._isLoading.set(false);
14036
14465
  }
14037
14466
  }
14467
+ buildStreamDataArgs() {
14468
+ const range = this.stateService.resolveCurrentTimeRange();
14469
+ if (!range) {
14470
+ return undefined;
14471
+ }
14472
+ return { from: range.from, to: range.to };
14473
+ }
14038
14474
  /**
14039
14475
  * Processes query rows into heatmap data.
14040
14476
  * When dateEndField is configured, auto-detects the interval width and shows sub-hour columns.
@@ -14047,14 +14483,10 @@ class HeatmapWidgetComponent {
14047
14483
  const aggregation = this.config.aggregation ?? 'count';
14048
14484
  const parsedRows = [];
14049
14485
  for (const row of filteredRows) {
14050
- const queryRow = row;
14051
- const cells = queryRow.cells?.items ?? [];
14052
14486
  let dateFrom = null;
14053
14487
  let dateTo = null;
14054
14488
  let numericValue = 1; // default for count
14055
- for (const cell of cells) {
14056
- if (!cell?.attributePath)
14057
- continue;
14489
+ for (const cell of row.cells) {
14058
14490
  if (matchesAttributePath(cell.attributePath, dateField)) {
14059
14491
  dateFrom = this.parseDate(cell.value);
14060
14492
  }
@@ -14381,12 +14813,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
14381
14813
 
14382
14814
  class HeatmapConfigDialogComponent {
14383
14815
  getRuntimeQueryColumnsGQL = inject(GetRuntimeQueryColumnsDtoGQL);
14816
+ queryExecutor = inject(QueryExecutorService);
14384
14817
  stateService = inject(MeshBoardStateService);
14385
14818
  windowRef = inject(WindowRef);
14386
14819
  querySelector;
14387
14820
  // Initial values for editing
14388
14821
  initialQueryRtId;
14389
14822
  initialQueryName;
14823
+ initialQueryFamily;
14390
14824
  initialDateField;
14391
14825
  initialDateEndField;
14392
14826
  initialValueField;
@@ -14508,36 +14942,22 @@ class HeatmapConfigDialogComponent {
14508
14942
  }
14509
14943
  async loadQueryColumns(queryRtId) {
14510
14944
  this.isLoadingColumns = true;
14945
+ // family may be undefined when the selected query metadata is missing —
14946
+ // fetchColumnsForFamily resolves it via the executor's one-time lookup.
14947
+ const family = queryFamily(this.selectedPersistentQuery?.ckTypeId) ?? this.initialQueryFamily;
14511
14948
  try {
14512
- // Metadata-only skips the row execution path on the backend.
14513
- const result = await firstValueFrom(this.getRuntimeQueryColumnsGQL.fetch({
14514
- variables: {
14515
- rtId: queryRtId
14516
- }
14517
- }));
14518
- const queryItems = result.data?.runtime?.runtimeQuery?.items ?? [];
14519
- if (queryItems.length > 0 && queryItems[0]) {
14520
- const columns = queryItems[0].columns ?? [];
14521
- const filteredColumns = columns
14522
- .filter((c) => c !== null);
14523
- this.queryColumns = filteredColumns.map(c => ({
14524
- attributePath: c.attributePath ?? '',
14525
- attributeValueType: c.attributeValueType ?? '',
14526
- aggregationType: c.aggregationType ?? null
14527
- }));
14528
- const numericTypes = ['INTEGER', 'FLOAT', 'DOUBLE', 'DECIMAL', 'LONG'];
14529
- const dateTimeTypes = ['DATE_TIME', 'DATETIME', 'DATE'];
14530
- this.numericColumns = this.queryColumns.filter(c => numericTypes.includes(c.attributeValueType));
14531
- this.dateTimeColumns = this.queryColumns.filter(c => dateTimeTypes.includes(c.attributeValueType));
14532
- // If no explicit datetime columns found, also allow string columns
14533
- // (datetime values are sometimes returned as strings)
14534
- if (this.dateTimeColumns.length === 0) {
14535
- this.dateTimeColumns = this.queryColumns;
14536
- }
14537
- // Auto-select first datetime column if not editing
14538
- if (!this.initialQueryRtId && this.dateTimeColumns.length > 0) {
14539
- this.form.dateField = this.dateTimeColumns[0].attributePath;
14540
- }
14949
+ this.queryColumns = await this.fetchColumnsForFamily(family, queryRtId);
14950
+ const numericTypes = ['INTEGER', 'FLOAT', 'DOUBLE', 'DECIMAL', 'LONG'];
14951
+ const dateTimeTypes = ['DATE_TIME', 'DATETIME', 'DATE'];
14952
+ this.numericColumns = this.queryColumns.filter(c => numericTypes.includes(c.attributeValueType));
14953
+ this.dateTimeColumns = this.queryColumns.filter(c => dateTimeTypes.includes(c.attributeValueType));
14954
+ // If no explicit datetime columns found, also allow string columns
14955
+ // (datetime values are sometimes returned as strings)
14956
+ if (this.dateTimeColumns.length === 0) {
14957
+ this.dateTimeColumns = this.queryColumns;
14958
+ }
14959
+ if (!this.initialQueryRtId && this.dateTimeColumns.length > 0) {
14960
+ this.form.dateField = this.dateTimeColumns[0].attributePath;
14541
14961
  }
14542
14962
  }
14543
14963
  catch (error) {
@@ -14550,6 +14970,35 @@ class HeatmapConfigDialogComponent {
14550
14970
  this.isLoadingColumns = false;
14551
14971
  }
14552
14972
  }
14973
+ /**
14974
+ * Runtime queries use the metadata-only resolver (no aggregation executed);
14975
+ * stream-data queries fall back to executing the query with `first: 1`
14976
+ * because the SD path has no dedicated column-introspection endpoint today.
14977
+ */
14978
+ async fetchColumnsForFamily(family, rtId) {
14979
+ const resolvedFamily = family ?? await this.queryExecutor.resolveFamily(rtId);
14980
+ if (resolvedFamily === 'runtime') {
14981
+ const result = await firstValueFrom(this.getRuntimeQueryColumnsGQL.fetch({
14982
+ variables: { rtId }
14983
+ }));
14984
+ const queryItem = result.data?.runtime?.runtimeQuery?.items?.[0];
14985
+ if (!queryItem)
14986
+ return [];
14987
+ return (queryItem.columns ?? [])
14988
+ .filter((c) => c !== null)
14989
+ .map(c => ({
14990
+ attributePath: c.attributePath ?? '',
14991
+ attributeValueType: c.attributeValueType ?? '',
14992
+ aggregationType: c.aggregationType ?? null
14993
+ }));
14994
+ }
14995
+ const sdResult = await firstValueFrom(this.queryExecutor.executeStreamData(rtId, { first: 1 }));
14996
+ return sdResult.columns.map(c => ({
14997
+ attributePath: c.attributePath,
14998
+ attributeValueType: c.attributeValueType ?? '',
14999
+ aggregationType: c.aggregationType ?? null
15000
+ }));
15001
+ }
14553
15002
  onFiltersChange(updatedFilters) {
14554
15003
  this.filters = updatedFilters;
14555
15004
  }
@@ -14563,11 +15012,13 @@ class HeatmapConfigDialogComponent {
14563
15012
  comparisonValue: f.comparisonValue
14564
15013
  }))
14565
15014
  : undefined;
15015
+ const family = queryFamily(this.selectedPersistentQuery.ckTypeId) ?? this.initialQueryFamily ?? undefined;
14566
15016
  const result = {
14567
15017
  ckTypeId: '',
14568
15018
  rtId: '',
14569
15019
  queryRtId: this.selectedPersistentQuery.rtId,
14570
15020
  queryName: this.selectedPersistentQuery.name,
15021
+ queryFamily: family,
14571
15022
  dateField: this.form.dateField,
14572
15023
  dateEndField: this.form.dateEndField || undefined,
14573
15024
  valueField: this.form.valueField || undefined,
@@ -14586,7 +15037,7 @@ class HeatmapConfigDialogComponent {
14586
15037
  this.windowRef.close();
14587
15038
  }
14588
15039
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: HeatmapConfigDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
14589
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: HeatmapConfigDialogComponent, isStandalone: true, selector: "mm-heatmap-config-dialog", inputs: { initialQueryRtId: "initialQueryRtId", initialQueryName: "initialQueryName", initialDateField: "initialDateField", initialDateEndField: "initialDateEndField", initialValueField: "initialValueField", initialAggregation: "initialAggregation", initialColorScheme: "initialColorScheme", initialShowLegend: "initialShowLegend", initialLegendPosition: "initialLegendPosition", initialDecimalPlaces: "initialDecimalPlaces", initialCompactNumbers: "initialCompactNumbers", initialValueMultiplier: "initialValueMultiplier", initialFilters: "initialFilters" }, viewQueries: [{ propertyName: "querySelector", first: true, predicate: ["querySelector"], descendants: true }], ngImport: i0, template: `
15040
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: HeatmapConfigDialogComponent, isStandalone: true, selector: "mm-heatmap-config-dialog", inputs: { initialQueryRtId: "initialQueryRtId", initialQueryName: "initialQueryName", initialQueryFamily: "initialQueryFamily", initialDateField: "initialDateField", initialDateEndField: "initialDateEndField", initialValueField: "initialValueField", initialAggregation: "initialAggregation", initialColorScheme: "initialColorScheme", initialShowLegend: "initialShowLegend", initialLegendPosition: "initialLegendPosition", initialDecimalPlaces: "initialDecimalPlaces", initialCompactNumbers: "initialCompactNumbers", initialValueMultiplier: "initialValueMultiplier", initialFilters: "initialFilters" }, viewQueries: [{ propertyName: "querySelector", first: true, predicate: ["querySelector"], descendants: true }], ngImport: i0, template: `
14590
15041
  <div class="config-container">
14591
15042
 
14592
15043
  <div class="config-form" [class.loading]="isLoadingInitial">
@@ -14795,7 +15246,7 @@ class HeatmapConfigDialogComponent {
14795
15246
  </button>
14796
15247
  </div>
14797
15248
  </div>
14798
- `, 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;flex:1;overflow-y:auto;padding:16px;position:relative}.config-form.loading{pointer-events:none}.config-section{padding:16px;background:var(--kendo-color-surface-alt, #f8f9fa);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px}.section-title{margin:0 0 16px;font-size:1rem;font-weight:600;color:var(--kendo-color-primary, #0d6efd)}.section-hint{margin:0 0 12px;font-size:.85rem;color:var(--kendo-color-subtle, #6c757d)}.form-field{display:flex;flex-direction:column;gap:6px;margin-bottom:12px}.form-field:last-child{margin-bottom:0}.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)}.color-scheme-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px}.radio-label{display:flex;align-items:center;gap:8px;cursor:pointer;font-weight:400}.color-scheme-preview{display:flex;align-items:center;gap:6px}.color-swatch{display:inline-block;width:16px;height:16px;border-radius:3px;border:1px solid var(--kendo-color-border, #dee2e6)}.form-row{display:flex;gap:24px}.checkbox-field{flex-direction:row;align-items:center}.checkbox-field label{display:flex;align-items:center;gap:8px;cursor:pointer;font-weight:400}.column-item{display:flex;justify-content:space-between;gap:16px}.column-path{font-weight:500}.column-type{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1$1.RadioControlValueAccessor, selector: "input[type=radio][formControlName],input[type=radio][formControl],input[type=radio][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", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon", "primary", "look"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: InputsModule }, { 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: "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: "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: SVGIconModule }, { kind: "component", type: FieldFilterEditorComponent, selector: "mm-field-filter-editor", inputs: ["availableAttributes", "ckTypeId", "hideNavigationProperties", "attributePaths", "enableVariables", "availableVariables", "filters"], outputs: ["filtersChange"] }, { kind: "component", type: QuerySelectorComponent, selector: "mm-query-selector", inputs: ["placeholder", "hint", "disabled"], outputs: ["querySelected", "queriesLoaded"] }, { kind: "component", type: LoadingOverlayComponent, selector: "mm-loading-overlay", inputs: ["loading"] }] });
15249
+ `, 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;flex:1;overflow-y:auto;padding:16px;position:relative}.config-form.loading{pointer-events:none}.config-section{padding:16px;background:var(--kendo-color-surface-alt, #f8f9fa);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px}.section-title{margin:0 0 16px;font-size:1rem;font-weight:600;color:var(--kendo-color-primary, #0d6efd)}.section-hint{margin:0 0 12px;font-size:.85rem;color:var(--kendo-color-subtle, #6c757d)}.form-field{display:flex;flex-direction:column;gap:6px;margin-bottom:12px}.form-field:last-child{margin-bottom:0}.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)}.color-scheme-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px}.radio-label{display:flex;align-items:center;gap:8px;cursor:pointer;font-weight:400}.color-scheme-preview{display:flex;align-items:center;gap:6px}.color-swatch{display:inline-block;width:16px;height:16px;border-radius:3px;border:1px solid var(--kendo-color-border, #dee2e6)}.form-row{display:flex;gap:24px}.checkbox-field{flex-direction:row;align-items:center}.checkbox-field label{display:flex;align-items:center;gap:8px;cursor:pointer;font-weight:400}.column-item{display:flex;justify-content:space-between;gap:16px}.column-path{font-weight:500}.column-type{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1$1.RadioControlValueAccessor, selector: "input[type=radio][formControlName],input[type=radio][formControl],input[type=radio][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", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon", "primary", "look"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: InputsModule }, { 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: "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: "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: SVGIconModule }, { kind: "component", type: FieldFilterEditorComponent, selector: "mm-field-filter-editor", inputs: ["availableAttributes", "ckTypeId", "hideNavigationProperties", "attributePaths", "enableVariables", "availableVariables", "filters"], outputs: ["filtersChange"] }, { kind: "component", type: QuerySelectorComponent, selector: "mm-query-selector", inputs: ["placeholder", "hint", "disabled", "acceptFamilies"], outputs: ["querySelected", "queriesLoaded"] }, { kind: "component", type: LoadingOverlayComponent, selector: "mm-loading-overlay", inputs: ["loading"] }] });
14799
15250
  }
14800
15251
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: HeatmapConfigDialogComponent, decorators: [{
14801
15252
  type: Component,
@@ -15026,6 +15477,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
15026
15477
  type: Input
15027
15478
  }], initialQueryName: [{
15028
15479
  type: Input
15480
+ }], initialQueryFamily: [{
15481
+ type: Input
15029
15482
  }], initialDateField: [{
15030
15483
  type: Input
15031
15484
  }], initialDateEndField: [{
@@ -16603,6 +17056,7 @@ class WidgetGroupConfigDialogComponent {
16603
17056
  ckTypeSelectorService = inject(CkTypeSelectorService);
16604
17057
  attributeSelectorService = inject(AttributeSelectorService);
16605
17058
  getRuntimeQueryColumnsGQL = inject(GetRuntimeQueryColumnsDtoGQL);
17059
+ queryExecutor = inject(QueryExecutorService);
16606
17060
  getCkTypeAvailableQueryColumnsGQL = inject(GetCkTypeAvailableQueryColumnsDtoGQL);
16607
17061
  meshBoardStateService = inject(MeshBoardStateService);
16608
17062
  windowRef = inject(WindowRef);
@@ -16611,6 +17065,7 @@ class WidgetGroupConfigDialogComponent {
16611
17065
  initialDataSourceMode;
16612
17066
  initialQueryRtId;
16613
17067
  initialQueryName;
17068
+ initialQueryFamily;
16614
17069
  initialCkTypeId;
16615
17070
  initialFilters;
16616
17071
  initialMaxItems;
@@ -16767,26 +17222,12 @@ class WidgetGroupConfigDialogComponent {
16767
17222
  }
16768
17223
  async loadQueryColumns(queryRtId) {
16769
17224
  this.isLoadingColumns = true;
17225
+ // family may be undefined when the selected query metadata is missing —
17226
+ // fetchColumnsForFamily resolves it via the executor's one-time lookup.
17227
+ const family = queryFamily(this.selectedPersistentQuery?.ckTypeId) ?? this.initialQueryFamily;
16770
17228
  try {
16771
- // Metadata-only skips the row execution path on the backend.
16772
- const result = await firstValueFrom(this.getRuntimeQueryColumnsGQL.fetch({
16773
- variables: {
16774
- rtId: queryRtId
16775
- }
16776
- }));
16777
- const queryItems = result.data?.runtime?.runtimeQuery?.items ?? [];
16778
- if (queryItems.length > 0 && queryItems[0]) {
16779
- const queryResult = queryItems[0];
16780
- const columns = queryResult.columns ?? [];
16781
- this.availableColumns = columns
16782
- .filter((c) => c !== null)
16783
- .map(c => ({
16784
- attributePath: c.attributePath ?? '',
16785
- attributeValueType: c.attributeValueType ?? '',
16786
- aggregationType: c.aggregationType ?? null
16787
- }));
16788
- this.filteredColumns.set(this.availableColumns);
16789
- }
17229
+ this.availableColumns = await this.fetchColumnsForFamily(family, queryRtId);
17230
+ this.filteredColumns.set(this.availableColumns);
16790
17231
  }
16791
17232
  catch (error) {
16792
17233
  console.error('Error loading query columns:', error);
@@ -16797,6 +17238,35 @@ class WidgetGroupConfigDialogComponent {
16797
17238
  this.isLoadingColumns = false;
16798
17239
  }
16799
17240
  }
17241
+ /**
17242
+ * Runtime queries use the metadata-only resolver (no aggregation executed);
17243
+ * stream-data queries fall back to executing the query with `first: 1`
17244
+ * because the SD path has no dedicated column-introspection endpoint today.
17245
+ */
17246
+ async fetchColumnsForFamily(family, rtId) {
17247
+ const resolvedFamily = family ?? await this.queryExecutor.resolveFamily(rtId);
17248
+ if (resolvedFamily === 'runtime') {
17249
+ const result = await firstValueFrom(this.getRuntimeQueryColumnsGQL.fetch({
17250
+ variables: { rtId }
17251
+ }));
17252
+ const queryItem = result.data?.runtime?.runtimeQuery?.items?.[0];
17253
+ if (!queryItem)
17254
+ return [];
17255
+ return (queryItem.columns ?? [])
17256
+ .filter((c) => c !== null)
17257
+ .map(c => ({
17258
+ attributePath: c.attributePath ?? '',
17259
+ attributeValueType: c.attributeValueType ?? '',
17260
+ aggregationType: c.aggregationType ?? null
17261
+ }));
17262
+ }
17263
+ const sdResult = await firstValueFrom(this.queryExecutor.executeStreamData(rtId, { first: 1 }));
17264
+ return sdResult.columns.map(c => ({
17265
+ attributePath: c.attributePath,
17266
+ attributeValueType: c.attributeValueType ?? '',
17267
+ aggregationType: c.aggregationType ?? null
17268
+ }));
17269
+ }
16800
17270
  // ============================================================================
16801
17271
  // CK Type Methods
16802
17272
  // ============================================================================
@@ -16889,11 +17359,15 @@ class WidgetGroupConfigDialogComponent {
16889
17359
  comparisonValue: f.comparisonValue
16890
17360
  }))
16891
17361
  : undefined;
17362
+ const family = this.dataSourceMode === 'persistentQuery' && this.selectedPersistentQuery
17363
+ ? queryFamily(this.selectedPersistentQuery.ckTypeId) ?? this.initialQueryFamily ?? undefined
17364
+ : undefined;
16892
17365
  const result = {
16893
17366
  ckTypeId: this.dataSourceMode === 'ckType' ? (this.selectedCkType?.rtCkTypeId ?? '') : '',
16894
17367
  dataSourceMode: this.dataSourceMode,
16895
17368
  queryRtId: this.dataSourceMode === 'persistentQuery' ? this.selectedPersistentQuery?.rtId : undefined,
16896
17369
  queryName: this.dataSourceMode === 'persistentQuery' ? this.selectedPersistentQuery?.name : undefined,
17370
+ queryFamily: family,
16897
17371
  filters: this.dataSourceMode === 'ckType' ? filtersDto : undefined,
16898
17372
  maxItems: this.form.maxItems,
16899
17373
  childTemplate,
@@ -16931,7 +17405,7 @@ class WidgetGroupConfigDialogComponent {
16931
17405
  this.windowRef.close();
16932
17406
  }
16933
17407
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: WidgetGroupConfigDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
16934
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: WidgetGroupConfigDialogComponent, isStandalone: true, selector: "mm-widget-group-config-dialog", inputs: { initialDataSourceMode: "initialDataSourceMode", initialQueryRtId: "initialQueryRtId", initialQueryName: "initialQueryName", initialCkTypeId: "initialCkTypeId", initialFilters: "initialFilters", initialMaxItems: "initialMaxItems", initialChildTemplate: "initialChildTemplate", initialLayout: "initialLayout", initialGridColumns: "initialGridColumns", initialMinChildWidth: "initialMinChildWidth", initialGap: "initialGap", initialEmptyMessage: "initialEmptyMessage" }, viewQueries: [{ propertyName: "querySelector", first: true, predicate: ["querySelector"], descendants: true }], ngImport: i0, template: `
17408
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: WidgetGroupConfigDialogComponent, isStandalone: true, selector: "mm-widget-group-config-dialog", inputs: { initialDataSourceMode: "initialDataSourceMode", initialQueryRtId: "initialQueryRtId", initialQueryName: "initialQueryName", initialQueryFamily: "initialQueryFamily", initialCkTypeId: "initialCkTypeId", initialFilters: "initialFilters", initialMaxItems: "initialMaxItems", initialChildTemplate: "initialChildTemplate", initialLayout: "initialLayout", initialGridColumns: "initialGridColumns", initialMinChildWidth: "initialMinChildWidth", initialGap: "initialGap", initialEmptyMessage: "initialEmptyMessage" }, viewQueries: [{ propertyName: "querySelector", first: true, predicate: ["querySelector"], descendants: true }], ngImport: i0, template: `
16935
17409
  <div class="config-container">
16936
17410
 
16937
17411
  <div class="config-form" [class.loading]="isLoadingInitial">
@@ -17251,7 +17725,7 @@ class WidgetGroupConfigDialogComponent {
17251
17725
  </button>
17252
17726
  </div>
17253
17727
  </div>
17254
- `, 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;position:relative;flex:1;overflow-y:auto}.config-form.loading{pointer-events:none}.form-section{padding:16px;background:var(--kendo-color-surface-alt, #f8f9fa);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px}.form-section h4{margin:0 0 16px;font-size:.95rem;color:var(--kendo-color-primary, #0d6efd)}.form-field{display:flex;flex-direction:column;gap:6px;margin-bottom:12px}.form-field:last-child{margin-bottom:0}.form-field.flex-1{flex:1}.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-row{display:flex;gap:16px}.mode-toggle{display:flex;gap:8px}.mode-toggle button{flex:1}.required{color:var(--kendo-color-error, #dc3545)}.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)}.column-item{display:flex;justify-content:space-between;gap:16px}.column-path{font-weight:500}.column-type{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}\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", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon", "primary", "look"], 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.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: 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: FieldFilterEditorComponent, selector: "mm-field-filter-editor", inputs: ["availableAttributes", "ckTypeId", "hideNavigationProperties", "attributePaths", "enableVariables", "availableVariables", "filters"], outputs: ["filtersChange"] }, { kind: "component", type: QuerySelectorComponent, selector: "mm-query-selector", inputs: ["placeholder", "hint", "disabled"], outputs: ["querySelected", "queriesLoaded"] }, { kind: "component", type: LoadingOverlayComponent, selector: "mm-loading-overlay", inputs: ["loading"] }] });
17728
+ `, 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;position:relative;flex:1;overflow-y:auto}.config-form.loading{pointer-events:none}.form-section{padding:16px;background:var(--kendo-color-surface-alt, #f8f9fa);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px}.form-section h4{margin:0 0 16px;font-size:.95rem;color:var(--kendo-color-primary, #0d6efd)}.form-field{display:flex;flex-direction:column;gap:6px;margin-bottom:12px}.form-field:last-child{margin-bottom:0}.form-field.flex-1{flex:1}.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-row{display:flex;gap:16px}.mode-toggle{display:flex;gap:8px}.mode-toggle button{flex:1}.required{color:var(--kendo-color-error, #dc3545)}.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)}.column-item{display:flex;justify-content:space-between;gap:16px}.column-path{font-weight:500}.column-type{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}\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", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon", "primary", "look"], 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.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: 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: FieldFilterEditorComponent, selector: "mm-field-filter-editor", inputs: ["availableAttributes", "ckTypeId", "hideNavigationProperties", "attributePaths", "enableVariables", "availableVariables", "filters"], outputs: ["filtersChange"] }, { kind: "component", type: QuerySelectorComponent, selector: "mm-query-selector", inputs: ["placeholder", "hint", "disabled", "acceptFamilies"], outputs: ["querySelected", "queriesLoaded"] }, { kind: "component", type: LoadingOverlayComponent, selector: "mm-loading-overlay", inputs: ["loading"] }] });
17255
17729
  }
17256
17730
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: WidgetGroupConfigDialogComponent, decorators: [{
17257
17731
  type: Component,
@@ -17595,6 +18069,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
17595
18069
  type: Input
17596
18070
  }], initialQueryName: [{
17597
18071
  type: Input
18072
+ }], initialQueryFamily: [{
18073
+ type: Input
17598
18074
  }], initialCkTypeId: [{
17599
18075
  type: Input
17600
18076
  }], initialFilters: [{
@@ -24996,11 +25472,16 @@ function parseConfig(data) {
24996
25472
  */
24997
25473
  function buildDataSourceFromPersisted(data, config) {
24998
25474
  if (data.dataSourceType === 'systemQuery' || data.dataSourceType === 'persistentQuery') {
24999
- return {
25475
+ const persisted = {
25000
25476
  type: 'persistentQuery',
25001
25477
  queryRtId: data.dataSourceRtId ?? config['queryRtId'] ?? '',
25002
25478
  queryName: config['queryName']
25003
25479
  };
25480
+ const persistedFamily = config['queryFamily'];
25481
+ if (persistedFamily === 'runtime' || persistedFamily === 'streamData') {
25482
+ persisted.queryFamily = persistedFamily;
25483
+ }
25484
+ return persisted;
25004
25485
  }
25005
25486
  if (data.dataSourceType === 'static') {
25006
25487
  return { type: 'static' };
@@ -25120,6 +25601,7 @@ function registerDefaultWidgets(registry) {
25120
25601
  initialDataSourceType: dataSourceType,
25121
25602
  initialQueryRtId: isPersistentQuery ? kpiWidget.dataSource.queryRtId : undefined,
25122
25603
  initialQueryName: isPersistentQuery ? kpiWidget.dataSource.queryName : undefined,
25604
+ initialQueryFamily: isPersistentQuery ? kpiWidget.dataSource.queryFamily : undefined,
25123
25605
  initialQueryMode: kpiWidget.queryMode,
25124
25606
  initialQueryValueField: kpiWidget.queryValueField,
25125
25607
  initialQueryCategoryField: kpiWidget.queryCategoryField,
@@ -25164,7 +25646,8 @@ function registerDefaultWidgets(registry) {
25164
25646
  const dataSource = {
25165
25647
  type: 'persistentQuery',
25166
25648
  queryRtId: result.queryRtId,
25167
- queryName: result.queryName
25649
+ queryName: result.queryName,
25650
+ queryFamily: result.queryFamily
25168
25651
  };
25169
25652
  return {
25170
25653
  ...widget,
@@ -25235,7 +25718,10 @@ function registerDefaultWidgets(registry) {
25235
25718
  filters: widget.filters,
25236
25719
  ...(isPersistentQuery && {
25237
25720
  queryName: widget.dataSource.queryName,
25238
- queryRtId: widget.dataSource.queryRtId
25721
+ queryRtId: widget.dataSource.queryRtId,
25722
+ ...(widget.dataSource.queryFamily && {
25723
+ queryFamily: widget.dataSource.queryFamily
25724
+ })
25239
25725
  })
25240
25726
  }
25241
25727
  };
@@ -25399,7 +25885,8 @@ function registerDefaultWidgets(registry) {
25399
25885
  initialPageSize: tableWidget.pageSize,
25400
25886
  initialSortable: tableWidget.sortable,
25401
25887
  initialQueryRtId: isPersistentQuery ? dataSource.queryRtId : undefined,
25402
- initialQueryName: isPersistentQuery ? dataSource.queryName : undefined
25888
+ initialQueryName: isPersistentQuery ? dataSource.queryName : undefined,
25889
+ initialQueryFamily: isPersistentQuery ? dataSource.queryFamily : undefined
25403
25890
  };
25404
25891
  },
25405
25892
  applyConfigResult: (widget, result) => {
@@ -25408,7 +25895,8 @@ function registerDefaultWidgets(registry) {
25408
25895
  const dataSource = {
25409
25896
  type: 'persistentQuery',
25410
25897
  queryRtId: result.queryRtId,
25411
- queryName: result.queryName
25898
+ queryName: result.queryName,
25899
+ queryFamily: result.queryFamily
25412
25900
  };
25413
25901
  // Convert filters from DTO to widget format
25414
25902
  const filters = result.filters?.map(f => ({
@@ -25462,11 +25950,12 @@ function registerDefaultWidgets(registry) {
25462
25950
  // SOLID: Serialization for persistence
25463
25951
  toPersistedConfig: (widget) => {
25464
25952
  const isPersistentQuery = widget.dataSource.type === 'persistentQuery';
25953
+ const queryDataSource = isPersistentQuery ? widget.dataSource : null;
25465
25954
  return {
25466
25955
  dataSourceType: isPersistentQuery ? 'persistentQuery' : 'runtimeEntity',
25467
25956
  dataSourceCkTypeId: widget.dataSource.type === 'runtimeEntity' ? widget.dataSource.ckTypeId : undefined,
25468
25957
  dataSourceRtId: isPersistentQuery
25469
- ? widget.dataSource.queryRtId
25958
+ ? queryDataSource.queryRtId
25470
25959
  : (widget.dataSource.type === 'runtimeEntity' ? widget.dataSource.rtId : undefined),
25471
25960
  config: {
25472
25961
  columns: widget.columns,
@@ -25474,9 +25963,10 @@ function registerDefaultWidgets(registry) {
25474
25963
  filters: widget.filters,
25475
25964
  pageSize: widget.pageSize,
25476
25965
  sortable: widget.sortable,
25477
- ...(isPersistentQuery && {
25478
- queryName: widget.dataSource.queryName,
25479
- queryRtId: widget.dataSource.queryRtId
25966
+ ...(queryDataSource && {
25967
+ queryName: queryDataSource.queryName,
25968
+ queryRtId: queryDataSource.queryRtId,
25969
+ ...(queryDataSource.queryFamily && { queryFamily: queryDataSource.queryFamily })
25480
25970
  })
25481
25971
  }
25482
25972
  };
@@ -25521,6 +26011,7 @@ function registerDefaultWidgets(registry) {
25521
26011
  initialDataSourceType: isPersistentQuery ? 'persistentQuery' : 'runtimeEntity',
25522
26012
  initialQueryRtId: isPersistentQuery ? gaugeWidget.dataSource.queryRtId : undefined,
25523
26013
  initialQueryName: isPersistentQuery ? gaugeWidget.dataSource.queryName : undefined,
26014
+ initialQueryFamily: isPersistentQuery ? gaugeWidget.dataSource.queryFamily : undefined,
25524
26015
  initialQueryMode: gaugeWidget.queryMode,
25525
26016
  initialQueryValueField: gaugeWidget.queryValueField,
25526
26017
  initialQueryCategoryField: gaugeWidget.queryCategoryField,
@@ -25550,7 +26041,8 @@ function registerDefaultWidgets(registry) {
25550
26041
  const dataSource = {
25551
26042
  type: 'persistentQuery',
25552
26043
  queryRtId: result.queryRtId,
25553
- queryName: result.queryName
26044
+ queryName: result.queryName,
26045
+ queryFamily: result.queryFamily
25554
26046
  };
25555
26047
  return {
25556
26048
  ...widget,
@@ -25632,7 +26124,10 @@ function registerDefaultWidgets(registry) {
25632
26124
  filters: widget.filters,
25633
26125
  ...(isPersistentQuery && {
25634
26126
  queryName: widget.dataSource.queryName,
25635
- queryRtId: widget.dataSource.queryRtId
26127
+ queryRtId: widget.dataSource.queryRtId,
26128
+ ...(widget.dataSource.queryFamily && {
26129
+ queryFamily: widget.dataSource.queryFamily
26130
+ })
25636
26131
  })
25637
26132
  }
25638
26133
  };
@@ -25701,6 +26196,7 @@ function registerDefaultWidgets(registry) {
25701
26196
  initialDataSourceType: dataSource.type,
25702
26197
  initialQueryRtId: isPersistentQuery ? dataSource.queryRtId : undefined,
25703
26198
  initialQueryName: isPersistentQuery ? dataSource.queryName : undefined,
26199
+ initialQueryFamily: isPersistentQuery ? dataSource.queryFamily : undefined,
25704
26200
  initialCkQueryTarget: isCkQuery ? dataSource.queryTarget : undefined,
25705
26201
  initialCkGroupBy: isCkQuery ? dataSource.groupBy : undefined,
25706
26202
  initialChartType: pieWidget.chartType,
@@ -25733,7 +26229,8 @@ function registerDefaultWidgets(registry) {
25733
26229
  dataSource = {
25734
26230
  type: 'persistentQuery',
25735
26231
  queryRtId: result.queryRtId ?? '',
25736
- queryName: result.queryName
26232
+ queryName: result.queryName,
26233
+ queryFamily: result.queryFamily
25737
26234
  };
25738
26235
  }
25739
26236
  return {
@@ -25795,6 +26292,9 @@ function registerDefaultWidgets(registry) {
25795
26292
  legendPosition: widget.legendPosition,
25796
26293
  queryName: dataSource.queryName,
25797
26294
  queryRtId: dataSource.queryRtId,
26295
+ ...(dataSource.queryFamily && {
26296
+ queryFamily: dataSource.queryFamily
26297
+ }),
25798
26298
  filters: widget.filters
25799
26299
  }
25800
26300
  };
@@ -25856,6 +26356,7 @@ function registerDefaultWidgets(registry) {
25856
26356
  return {
25857
26357
  initialQueryRtId: isPersistentQuery ? dataSource.queryRtId : undefined,
25858
26358
  initialQueryName: isPersistentQuery ? dataSource.queryName : undefined,
26359
+ initialQueryFamily: isPersistentQuery ? dataSource.queryFamily : undefined,
25859
26360
  initialChartType: barWidget.chartType,
25860
26361
  initialCategoryField: barWidget.categoryField,
25861
26362
  initialSeries: barWidget.series,
@@ -25873,7 +26374,8 @@ function registerDefaultWidgets(registry) {
25873
26374
  const dataSource = {
25874
26375
  type: 'persistentQuery',
25875
26376
  queryRtId: result.queryRtId,
25876
- queryName: result.queryName
26377
+ queryName: result.queryName,
26378
+ queryFamily: result.queryFamily
25877
26379
  };
25878
26380
  // Convert filters from DTO to widget format
25879
26381
  const filters = result.filters?.map(f => ({
@@ -25929,6 +26431,9 @@ function registerDefaultWidgets(registry) {
25929
26431
  defaultBarColor: widget.defaultBarColor,
25930
26432
  queryName: widget.dataSource.queryName,
25931
26433
  queryRtId: widget.dataSource.queryRtId,
26434
+ ...(widget.dataSource.queryFamily && {
26435
+ queryFamily: widget.dataSource.queryFamily
26436
+ }),
25932
26437
  filters: widget.filters
25933
26438
  }
25934
26439
  }),
@@ -25973,6 +26478,7 @@ function registerDefaultWidgets(registry) {
25973
26478
  return {
25974
26479
  initialQueryRtId: isPersistentQuery ? dataSource.queryRtId : undefined,
25975
26480
  initialQueryName: isPersistentQuery ? dataSource.queryName : undefined,
26481
+ initialQueryFamily: isPersistentQuery ? dataSource.queryFamily : undefined,
25976
26482
  initialChartType: lineWidget.chartType,
25977
26483
  initialCategoryField: lineWidget.categoryField,
25978
26484
  initialSeriesGroupField: lineWidget.seriesGroupField,
@@ -25989,7 +26495,8 @@ function registerDefaultWidgets(registry) {
25989
26495
  const dataSource = {
25990
26496
  type: 'persistentQuery',
25991
26497
  queryRtId: result.queryRtId,
25992
- queryName: result.queryName
26498
+ queryName: result.queryName,
26499
+ queryFamily: result.queryFamily
25993
26500
  };
25994
26501
  const filters = result.filters?.map(f => ({
25995
26502
  attributePath: f.attributePath,
@@ -26040,6 +26547,9 @@ function registerDefaultWidgets(registry) {
26040
26547
  referenceLines: widget.referenceLines,
26041
26548
  queryName: widget.dataSource.queryName,
26042
26549
  queryRtId: widget.dataSource.queryRtId,
26550
+ ...(widget.dataSource.queryFamily && {
26551
+ queryFamily: widget.dataSource.queryFamily
26552
+ }),
26043
26553
  filters: widget.filters
26044
26554
  }
26045
26555
  }),
@@ -26316,6 +26826,7 @@ function registerDefaultWidgets(registry) {
26316
26826
  initialDataSourceMode: hasQuery ? 'persistentQuery' : (hasCkType ? 'ckType' : 'persistentQuery'),
26317
26827
  initialQueryRtId: hasQuery ? dataSource.queryRtId : undefined,
26318
26828
  initialQueryName: hasQuery ? dataSource.queryName : undefined,
26829
+ initialQueryFamily: hasQuery ? dataSource.queryFamily : undefined,
26319
26830
  initialCkTypeId: hasCkType ? dataSource.ckTypeId : undefined,
26320
26831
  initialFilters: hasCkType ? dataSource.filters : undefined,
26321
26832
  initialMaxItems: dataSource.type === 'repeaterQuery' ? dataSource.maxItems : undefined,
@@ -26334,6 +26845,7 @@ function registerDefaultWidgets(registry) {
26334
26845
  type: 'repeaterQuery',
26335
26846
  queryRtId: result.dataSourceMode === 'persistentQuery' ? result.queryRtId : undefined,
26336
26847
  queryName: result.dataSourceMode === 'persistentQuery' ? result.queryName : undefined,
26848
+ queryFamily: result.dataSourceMode === 'persistentQuery' ? result.queryFamily : undefined,
26337
26849
  ckTypeId: useCkType ? result.ckTypeId : undefined,
26338
26850
  filters: useCkType && result.filters ? result.filters.map(f => ({
26339
26851
  attributePath: f.attributePath,
@@ -26384,6 +26896,7 @@ function registerDefaultWidgets(registry) {
26384
26896
  dataSourceCkTypeId: !hasQuery ? dataSource.ckTypeId : undefined,
26385
26897
  config: {
26386
26898
  queryName: dataSource.queryName,
26899
+ ...(dataSource.queryFamily && { queryFamily: dataSource.queryFamily }),
26387
26900
  maxItems: dataSource.maxItems,
26388
26901
  filters: dataSource.filters,
26389
26902
  childTemplate: widget.childTemplate,
@@ -26399,10 +26912,14 @@ function registerDefaultWidgets(registry) {
26399
26912
  fromPersistedConfig: (data, base) => {
26400
26913
  const config = parseConfig(data);
26401
26914
  const hasQuery = !!data.dataSourceRtId;
26915
+ const persistedFamily = config['queryFamily'];
26402
26916
  const dataSource = {
26403
26917
  type: 'repeaterQuery',
26404
26918
  queryRtId: hasQuery ? data.dataSourceRtId ?? undefined : undefined,
26405
26919
  queryName: hasQuery ? config['queryName'] : undefined,
26920
+ queryFamily: hasQuery && (persistedFamily === 'runtime' || persistedFamily === 'streamData')
26921
+ ? persistedFamily
26922
+ : undefined,
26406
26923
  ckTypeId: !hasQuery ? data.dataSourceCkTypeId ?? undefined : undefined,
26407
26924
  filters: !hasQuery ? config['filters'] : undefined,
26408
26925
  maxItems: config['maxItems']
@@ -26505,6 +27022,7 @@ function registerDefaultWidgets(registry) {
26505
27022
  return {
26506
27023
  initialQueryRtId: isPersistentQuery ? dataSource.queryRtId : undefined,
26507
27024
  initialQueryName: isPersistentQuery ? dataSource.queryName : undefined,
27025
+ initialQueryFamily: isPersistentQuery ? dataSource.queryFamily : undefined,
26508
27026
  initialDateField: heatmapWidget.dateField,
26509
27027
  initialDateEndField: heatmapWidget.dateEndField,
26510
27028
  initialValueField: heatmapWidget.valueField,
@@ -26522,7 +27040,8 @@ function registerDefaultWidgets(registry) {
26522
27040
  const dataSource = {
26523
27041
  type: 'persistentQuery',
26524
27042
  queryRtId: result.queryRtId,
26525
- queryName: result.queryName
27043
+ queryName: result.queryName,
27044
+ queryFamily: result.queryFamily
26526
27045
  };
26527
27046
  const filters = result.filters?.map(f => ({
26528
27047
  attributePath: f.attributePath,
@@ -26575,6 +27094,9 @@ function registerDefaultWidgets(registry) {
26575
27094
  valueMultiplier: widget.valueMultiplier,
26576
27095
  queryName: widget.dataSource.queryName,
26577
27096
  queryRtId: widget.dataSource.queryRtId,
27097
+ ...(widget.dataSource.queryFamily && {
27098
+ queryFamily: widget.dataSource.queryFamily
27099
+ }),
26578
27100
  filters: widget.filters
26579
27101
  }
26580
27102
  }),
@@ -27534,7 +28056,8 @@ class MeshBoardSettingsResult {
27534
28056
  timeFilter;
27535
28057
  rtWellKnownName;
27536
28058
  entitySelectors;
27537
- constructor(name, description, columns, rowHeight, gap, variables, timeFilter, rtWellKnownName, entitySelectors) {
28059
+ autoRefreshSeconds;
28060
+ constructor(name, description, columns, rowHeight, gap, variables, timeFilter, rtWellKnownName, entitySelectors, autoRefreshSeconds) {
27538
28061
  this.name = name;
27539
28062
  this.description = description;
27540
28063
  this.columns = columns;
@@ -27544,6 +28067,7 @@ class MeshBoardSettingsResult {
27544
28067
  this.timeFilter = timeFilter;
27545
28068
  this.rtWellKnownName = rtWellKnownName;
27546
28069
  this.entitySelectors = entitySelectors;
28070
+ this.autoRefreshSeconds = autoRefreshSeconds;
27547
28071
  }
27548
28072
  }
27549
28073
  /**
@@ -27559,6 +28083,12 @@ class MeshBoardSettingsDialogComponent {
27559
28083
  columns = 6;
27560
28084
  rowHeight = 200;
27561
28085
  gap = 16;
28086
+ /**
28087
+ * Auto-refresh interval in seconds. `0` disables auto-refresh and is the default.
28088
+ * When > 0 the MeshBoard view re-polls all widgets at this interval while the
28089
+ * tab is visible.
28090
+ */
28091
+ autoRefreshSeconds = 0;
27562
28092
  variables = [];
27563
28093
  entitySelectors = [];
27564
28094
  entitySelectorEditing = false;
@@ -27576,7 +28106,8 @@ class MeshBoardSettingsDialogComponent {
27576
28106
  return (this.name.trim().length > 0 &&
27577
28107
  this.columns >= 1 && this.columns <= 12 &&
27578
28108
  this.rowHeight >= 100 && this.rowHeight <= 1000 &&
27579
- this.gap >= 0 && this.gap <= 100);
28109
+ this.gap >= 0 && this.gap <= 100 &&
28110
+ this.autoRefreshSeconds >= 0 && this.autoRefreshSeconds <= 3600);
27580
28111
  }
27581
28112
  /**
27582
28113
  * Sets the initial values for the form fields.
@@ -27588,6 +28119,7 @@ class MeshBoardSettingsDialogComponent {
27588
28119
  this.columns = settings.columns;
27589
28120
  this.rowHeight = settings.rowHeight;
27590
28121
  this.gap = settings.gap;
28122
+ this.autoRefreshSeconds = settings.autoRefreshSeconds ?? 0;
27591
28123
  this.variables = settings.variables ? [...settings.variables] : [];
27592
28124
  this.entitySelectors = settings.entitySelectors ? settings.entitySelectors.map(es => ({ ...es })) : [];
27593
28125
  this.timeFilterEnabled = settings.timeFilter?.enabled ?? false;
@@ -27629,7 +28161,7 @@ class MeshBoardSettingsDialogComponent {
27629
28161
  enabled: this.timeFilterEnabled,
27630
28162
  defaultSelection: this.timeFilterEnabled ? this.defaultSelection : undefined
27631
28163
  };
27632
- 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);
28164
+ 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);
27633
28165
  this.windowRef.close(result);
27634
28166
  }
27635
28167
  /**
@@ -27639,7 +28171,7 @@ class MeshBoardSettingsDialogComponent {
27639
28171
  this.windowRef.close();
27640
28172
  }
27641
28173
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: MeshBoardSettingsDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
27642
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", 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 </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][formControlName],input[type=checkbox][formControl],input[type=checkbox][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", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon", "primary", "look"], 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"] }] });
28174
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", 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][formControlName],input[type=checkbox][formControl],input[type=checkbox][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", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon", "primary", "look"], 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"] }] });
27643
28175
  }
27644
28176
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: MeshBoardSettingsDialogComponent, decorators: [{
27645
28177
  type: Component,
@@ -27655,7 +28187,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
27655
28187
  VariablesEditorComponent,
27656
28188
  EntitySelectorEditorComponent,
27657
28189
  TimeRangePickerComponent
27658
- ], 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 </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"] }]
28190
+ ], 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"] }]
27659
28191
  }] });
27660
28192
 
27661
28193
  /**
@@ -28520,6 +29052,10 @@ class MeshBoardViewComponent {
28520
29052
  // Config dialog state
28521
29053
  configDialogSubscription = null;
28522
29054
  navigationSubscription = null;
29055
+ // Auto-refresh polling
29056
+ autoRefreshTimerId = null;
29057
+ autoRefreshActiveSeconds = 0;
29058
+ visibilityListener = () => this.evaluateAutoRefresh();
28523
29059
  // State signals
28524
29060
  config = this.stateService.meshBoardConfig;
28525
29061
  isEditMode = this.editModeService.isEditMode;
@@ -28601,6 +29137,16 @@ class MeshBoardViewComponent {
28601
29137
  }
28602
29138
  }
28603
29139
  });
29140
+ // Effect to re-evaluate the auto-refresh timer whenever the MeshBoard
29141
+ // config changes (e.g. user edits autoRefreshSeconds in settings, or a
29142
+ // different board is loaded). Re-reads on the same interval skip the
29143
+ // restart inside evaluateAutoRefresh().
29144
+ effect(() => {
29145
+ // Touch the signals we depend on so Angular re-fires the effect.
29146
+ const seconds = this.config().autoRefreshSeconds ?? 0;
29147
+ void seconds;
29148
+ this.evaluateAutoRefresh();
29149
+ });
28604
29150
  // Update breadcrumb after each navigation (BreadCrumbService recreates items on NavigationEnd)
28605
29151
  if (this.breadCrumbService) {
28606
29152
  this.navigationSubscription = this.router.events
@@ -28687,6 +29233,13 @@ class MeshBoardViewComponent {
28687
29233
  this.initialLoadComplete = true;
28688
29234
  // Update breadcrumb with MeshBoard name
28689
29235
  this.updateBreadcrumb();
29236
+ // Wire visibility-aware auto-refresh. The config-watching effect already
29237
+ // re-evaluates on signal changes; this listener handles the tab going
29238
+ // background → foreground without a config change.
29239
+ if (typeof document !== 'undefined') {
29240
+ document.addEventListener('visibilitychange', this.visibilityListener);
29241
+ }
29242
+ this.evaluateAutoRefresh();
28690
29243
  this._isInitialized.set(true);
28691
29244
  }
28692
29245
  catch (err) {
@@ -28717,6 +29270,41 @@ class MeshBoardViewComponent {
28717
29270
  ngOnDestroy() {
28718
29271
  this.closeConfigDialog();
28719
29272
  this.navigationSubscription?.unsubscribe();
29273
+ this.stopAutoRefresh();
29274
+ if (typeof document !== 'undefined') {
29275
+ document.removeEventListener('visibilitychange', this.visibilityListener);
29276
+ }
29277
+ }
29278
+ /**
29279
+ * Starts, restarts, or stops the auto-refresh timer based on the MeshBoard
29280
+ * config and document visibility. Called on init, whenever config changes,
29281
+ * and whenever the tab visibility changes.
29282
+ *
29283
+ * Pause-on-hidden saves bandwidth and avoids Apollo cache thrashing when
29284
+ * the user has the tab in the background.
29285
+ */
29286
+ evaluateAutoRefresh() {
29287
+ const seconds = this.config().autoRefreshSeconds ?? 0;
29288
+ const tabVisible = typeof document === 'undefined' || !document.hidden;
29289
+ if (seconds <= 0 || !tabVisible) {
29290
+ this.stopAutoRefresh();
29291
+ return;
29292
+ }
29293
+ if (this.autoRefreshTimerId !== null && this.autoRefreshActiveSeconds === seconds) {
29294
+ return; // already running with the same interval
29295
+ }
29296
+ this.stopAutoRefresh();
29297
+ this.autoRefreshActiveSeconds = seconds;
29298
+ this.autoRefreshTimerId = setInterval(() => {
29299
+ void this.refresh();
29300
+ }, seconds * 1000);
29301
+ }
29302
+ stopAutoRefresh() {
29303
+ if (this.autoRefreshTimerId !== null) {
29304
+ clearInterval(this.autoRefreshTimerId);
29305
+ this.autoRefreshTimerId = null;
29306
+ }
29307
+ this.autoRefreshActiveSeconds = 0;
28720
29308
  }
28721
29309
  /**
28722
29310
  * Preloads data for all widgets to improve initial rendering performance.
@@ -29633,5 +30221,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
29633
30221
  * Generated bundle index. Do not edit.
29634
30222
  */
29635
30223
 
29636
- export { AddWidgetDialogComponent, AiInsightsConfigDialogComponent, AiInsightsService, AiInsightsWidgetComponent, AlertBannerConfigDialogComponent, AlertBannerWidgetComponent, AlertListConfigDialogComponent, AlertListWidgetComponent, AssociationsConfigDialogComponent, 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, QuerySelectorComponent, RuntimeEntitySelectorComponent, ServiceHealthConfigDialogComponent, ServiceHealthWidgetComponent, StatsGridConfigDialogComponent, StatsGridWidgetComponent, StatusIndicatorConfigDialogComponent, StatusIndicatorWidgetComponent, StatusListConfigDialogComponent, StatusListWidgetComponent, SummaryCardConfigDialogComponent, SummaryCardWidgetComponent, TableConfigDialogComponent, TableWidgetComponent, TableWidgetDataSourceDirective, WidgetFactoryService, WidgetGroupComponent, WidgetGroupConfigDialogComponent, WidgetNotConfiguredComponent, WidgetRegistryService, provideDefaultWidgets, provideMeshBoard, provideProcessWidget, provideWidgetRegistrations, registerDefaultWidgets, registerProcessWidget };
30224
+ export { AddWidgetDialogComponent, AiInsightsConfigDialogComponent, AiInsightsService, AiInsightsWidgetComponent, AlertBannerConfigDialogComponent, AlertBannerWidgetComponent, AlertListConfigDialogComponent, AlertListWidgetComponent, AssociationsConfigDialogComponent, 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 };
29637
30225
  //# sourceMappingURL=meshmakers-octo-meshboard.mjs.map