@meshmakers/octo-meshboard 3.4.160 → 3.4.170

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,273 @@ 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
+ const hasOverride = args.from != null || args.to != null || args.interval != null || args.limit != null || args.queryMode != null;
2911
+ if (!hasOverride) {
2912
+ return undefined;
2913
+ }
2914
+ return {
2915
+ from: args.from ?? undefined,
2916
+ to: args.to ?? undefined,
2917
+ interval: args.interval ?? undefined,
2918
+ limit: args.limit ?? undefined,
2919
+ queryMode: args.queryMode ?? QueryModeDto.DefaultDto
2920
+ };
2921
+ }
2922
+ mapColumns(columns) {
2923
+ if (!columns)
2924
+ return [];
2925
+ const result = [];
2926
+ for (const col of columns) {
2927
+ if (!col?.attributePath)
2928
+ continue;
2929
+ result.push({
2930
+ attributePath: col.attributePath,
2931
+ attributeValueType: col.attributeValueType ?? null,
2932
+ aggregationType: col.aggregationType ?? null
2933
+ });
2934
+ }
2935
+ return result;
2936
+ }
2937
+ mapRuntimeRows(rows) {
2938
+ if (!rows)
2939
+ return [];
2940
+ const result = [];
2941
+ for (const row of rows) {
2942
+ if (!row)
2943
+ continue;
2944
+ const r = row;
2945
+ result.push({
2946
+ __typename: r.__typename,
2947
+ rtId: r.rtId ?? null,
2948
+ ckTypeId: r.ckTypeId ?? null,
2949
+ cells: this.mapCells(r.cells?.items)
2950
+ });
2951
+ }
2952
+ return result;
2953
+ }
2954
+ mapStreamDataRows(rows) {
2955
+ if (!rows)
2956
+ return [];
2957
+ const result = [];
2958
+ for (const row of rows) {
2959
+ if (!row)
2960
+ continue;
2961
+ const r = row;
2962
+ const ckTypeId = typeof r.ckTypeId === 'string' ? r.ckTypeId : (r.ckTypeId?.fullName ?? null);
2963
+ result.push({
2964
+ __typename: r.__typename ?? 'StreamDataQueryRow',
2965
+ rtId: r.rtId ?? null,
2966
+ ckTypeId,
2967
+ timestamp: r.timestamp ?? null,
2968
+ rtWellKnownName: r.rtWellKnownName ?? null,
2969
+ rtCreationDateTime: r.rtCreationDateTime ?? null,
2970
+ rtChangedDateTime: r.rtChangedDateTime ?? null,
2971
+ cells: this.mapCells(r.cells?.items)
2972
+ });
2973
+ }
2974
+ return result;
2975
+ }
2976
+ mapCells(cells) {
2977
+ if (!cells)
2978
+ return [];
2979
+ const result = [];
2980
+ for (const cell of cells) {
2981
+ if (!cell?.attributePath)
2982
+ continue;
2983
+ result.push({ attributePath: cell.attributePath, value: cell.value });
2984
+ }
2985
+ return result;
2986
+ }
2987
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QueryExecutorService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
2988
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QueryExecutorService, providedIn: 'root' });
2989
+ }
2990
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QueryExecutorService, decorators: [{
2991
+ type: Injectable,
2992
+ args: [{ providedIn: 'root' }]
2993
+ }] });
2994
+
2590
2995
  const GetAssociationTargetsDocumentDto = gql `
2591
2996
  query getAssociationTargets($rtId: OctoObjectId!, $ckTypeId: String!, $targetCkTypeId: String!, $roleId: String!, $direction: GraphDirection!, $first: Int, $attributeNames: [String]) {
2592
2997
  runtime {
@@ -2859,7 +3264,7 @@ class MeshBoardDataService {
2859
3264
  getDashboardEntityGQL = inject(GetDashboardEntityDtoGQL);
2860
3265
  getCkModelsWithStateGQL = inject(GetCkModelsWithStateDtoGQL);
2861
3266
  getEntitiesByCkTypeGQL = inject(GetEntitiesByCkTypeDtoGQL);
2862
- executeRuntimeQueryGQL = inject(ExecuteRuntimeQueryDtoGQL);
3267
+ queryExecutor = inject(QueryExecutorService);
2863
3268
  getAssociationTargetsGQL = inject(GetAssociationTargetsDtoGQL);
2864
3269
  getCkTypeAttributesGQL = inject(GetCkTypeAttributesForMeshboardDtoGQL);
2865
3270
  apollo = inject(Apollo);
@@ -3070,8 +3475,10 @@ class MeshBoardDataService {
3070
3475
  async fetchRepeaterData(dataSource) {
3071
3476
  const maxItems = dataSource.maxItems ?? 50;
3072
3477
  if (dataSource.queryRtId) {
3073
- // Query Mode: Execute persistent query
3074
- return this.fetchRepeaterFromQuery(dataSource.queryRtId, maxItems);
3478
+ // Query Mode: Execute persistent query (runtime or stream-data).
3479
+ // `queryFamily` may be undefined for legacy configs — the executor falls
3480
+ // back to a one-time lookup keyed by rtId.
3481
+ return this.fetchRepeaterFromQuery(dataSource.queryFamily, dataSource.queryRtId, maxItems);
3075
3482
  }
3076
3483
  else if (dataSource.ckTypeId) {
3077
3484
  // Entity Mode: Load entities by CK type
@@ -3081,49 +3488,33 @@ class MeshBoardDataService {
3081
3488
  return [];
3082
3489
  }
3083
3490
  /**
3084
- * Fetches repeater data from a persistent query.
3085
- * Maps query rows to RepeaterDataItem objects.
3491
+ * Fetches repeater data from a persistent query (runtime or stream-data).
3492
+ * Always sends `streamDataArgs` when a time filter is active — the runtime
3493
+ * path ignores the field, so this is safe regardless of the resolved family.
3086
3494
  */
3087
- async fetchRepeaterFromQuery(queryRtId, maxItems) {
3495
+ async fetchRepeaterFromQuery(family, queryRtId, maxItems) {
3088
3496
  try {
3089
- const result = await firstValueFrom(this.executeRuntimeQueryGQL.fetch({
3090
- variables: {
3091
- rtId: queryRtId,
3092
- first: maxItems
3093
- }
3497
+ const streamDataArgs = this.buildRepeaterStreamDataArgs();
3498
+ const result = await firstValueFrom(this.queryExecutor.execute(family, queryRtId, {
3499
+ first: maxItems,
3500
+ streamDataArgs
3094
3501
  }));
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
3502
  const items = [];
3105
- for (const row of rows) {
3106
- if (!row)
3107
- continue;
3108
- // Extract rtId from RtSimpleQueryRow if available
3503
+ for (const row of result.rows) {
3109
3504
  const rtId = row.rtId ?? `row-${items.length}`;
3110
- const ckTypeId = row.ckTypeId ?? queryResult.associatedCkTypeId ?? '';
3111
- // Build attributes map from cells
3505
+ const ckTypeId = row.ckTypeId ?? result.associatedCkTypeId ?? '';
3506
+ // Build attributes map from cells; expose both sanitised (`a_b`) and
3507
+ // original (`a.b`) keys so widget configs can address either form.
3112
3508
  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)
3509
+ for (const cell of row.cells) {
3118
3510
  const sanitizedPath = cell.attributePath.replace(/\./g, '_');
3119
3511
  attributes.set(sanitizedPath, cell.value);
3120
- // Also store with original path for flexibility
3121
3512
  attributes.set(cell.attributePath, cell.value);
3122
3513
  }
3123
3514
  items.push({
3124
3515
  rtId,
3125
- ckTypeId,
3126
- rtWellKnownName: attributes.get('rtWellKnownName'),
3516
+ ckTypeId: ckTypeId ?? '',
3517
+ rtWellKnownName: row.rtWellKnownName ?? attributes.get('rtWellKnownName'),
3127
3518
  attributes
3128
3519
  });
3129
3520
  }
@@ -3134,6 +3525,13 @@ class MeshBoardDataService {
3134
3525
  return [];
3135
3526
  }
3136
3527
  }
3528
+ buildRepeaterStreamDataArgs() {
3529
+ const range = this.stateService.resolveCurrentTimeRange();
3530
+ if (!range) {
3531
+ return undefined;
3532
+ }
3533
+ return { from: range.from, to: range.to };
3534
+ }
3137
3535
  /**
3138
3536
  * Fetches repeater data from entities by CK type.
3139
3537
  * Maps entities to RepeaterDataItem objects.
@@ -4252,9 +4650,19 @@ function processPieChartData(rows, categoryField, valueField) {
4252
4650
 
4253
4651
  class KpiWidgetComponent {
4254
4652
  dataService = inject(DashboardDataService);
4255
- executeRuntimeQueryGQL = inject(ExecuteRuntimeQueryDtoGQL);
4653
+ queryExecutor = inject(QueryExecutorService);
4256
4654
  stateService = inject(MeshBoardStateService);
4257
4655
  variableService = inject(MeshBoardVariableService);
4656
+ /**
4657
+ * Row __typenames KPI extraction recognises.
4658
+ * Runtime queries discriminate; stream-data queries collapse all kinds
4659
+ * (simple / aggregation / grouped / downsampling) into `StreamDataQueryRow`.
4660
+ */
4661
+ static SUPPORTED_ROW_TYPES = new Set([
4662
+ 'RtAggregationQueryRow',
4663
+ 'RtGroupingAggregationQueryRow',
4664
+ 'StreamDataQueryRow'
4665
+ ]);
4258
4666
  config;
4259
4667
  arrowUpIcon = arrowUpIcon;
4260
4668
  arrowDownIcon = arrowDownIcon;
@@ -4506,43 +4914,29 @@ class KpiWidgetComponent {
4506
4914
  this._isLoading.set(true);
4507
4915
  this._error.set(null);
4508
4916
  try {
4509
- // Convert widget filters to GraphQL format
4510
4917
  const fieldFilter = this.convertFiltersToDto(this.config.filters);
4511
- const result = await firstValueFrom(this.executeRuntimeQueryGQL.fetch({
4512
- variables: {
4513
- rtId: dataSource.queryRtId,
4514
- fieldFilter
4515
- }
4918
+ // queryFamily may be undefined for legacy widget configs — the executor
4919
+ // falls back to a one-time lookup by rtId. streamDataArgs is sent
4920
+ // unconditionally because the runtime path ignores it.
4921
+ const streamDataArgs = this.buildStreamDataArgs();
4922
+ const result = await firstValueFrom(this.queryExecutor.execute(dataSource.queryFamily, dataSource.queryRtId, {
4923
+ fieldFilter: fieldFilter ?? undefined,
4924
+ streamDataArgs
4516
4925
  }).pipe(catchError(err => {
4517
4926
  console.error('Error loading KPI query data:', err);
4518
4927
  throw err;
4519
4928
  })));
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
4929
  let value = 0;
4533
4930
  const queryMode = this.config.queryMode ?? 'simpleCount';
4534
4931
  switch (queryMode) {
4535
4932
  case 'simpleCount':
4536
- // Use totalCount from the query
4537
- value = queryResult.rows?.totalCount ?? 0;
4933
+ value = result.totalCount;
4538
4934
  break;
4539
4935
  case 'aggregation':
4540
- // Get the single value from aggregation query (1 row, 1 column)
4541
- value = this.extractAggregationValue(queryResult);
4936
+ value = this.extractAggregationValue(result);
4542
4937
  break;
4543
4938
  case 'groupedAggregation':
4544
- // Find the row matching the selected category and get its value
4545
- value = this.extractGroupedAggregationValue(queryResult);
4939
+ value = this.extractGroupedAggregationValue(result);
4546
4940
  break;
4547
4941
  }
4548
4942
  // Create a synthetic entity with the value
@@ -4561,48 +4955,39 @@ class KpiWidgetComponent {
4561
4955
  this._isLoading.set(false);
4562
4956
  }
4563
4957
  }
4958
+ buildStreamDataArgs() {
4959
+ const range = this.stateService.resolveCurrentTimeRange();
4960
+ if (!range) {
4961
+ return undefined;
4962
+ }
4963
+ return { from: range.from, to: range.to };
4964
+ }
4564
4965
  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 ?? ''));
4966
+ const firstRow = queryResult.rows.find(row => KpiWidgetComponent.SUPPORTED_ROW_TYPES.has(row.__typename ?? ''));
4569
4967
  if (!firstRow)
4570
4968
  return 0;
4571
- const queryRow = firstRow;
4572
- const cells = queryRow.cells?.items ?? [];
4573
- // Find the value field or use the first cell
4574
4969
  const valueField = this.config.queryValueField;
4575
- for (const cell of cells) {
4576
- if (!cell?.attributePath)
4577
- continue;
4970
+ for (const cell of firstRow.cells) {
4578
4971
  if (valueField && matchesAttributePath(cell.attributePath, valueField)) {
4579
4972
  return this.extractCellValue(cell.value);
4580
4973
  }
4581
4974
  }
4582
4975
  // 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;
4976
+ return firstRow.cells.length > 0 ? this.extractCellValue(firstRow.cells[0].value) : 0;
4585
4977
  }
4586
4978
  extractGroupedAggregationValue(queryResult) {
4587
- const rows = queryResult.rows?.items ?? [];
4588
- const supportedRowTypes = ['RtGroupingAggregationQueryRow', 'RtAggregationQueryRow'];
4589
4979
  const categoryField = this.config.queryCategoryField;
4590
4980
  const categoryValue = this.config.queryCategoryValue;
4591
4981
  const valueField = this.config.queryValueField;
4592
4982
  if (!categoryField || !categoryValue || !valueField) {
4593
4983
  return 0;
4594
4984
  }
4595
- // Find the row where category matches
4596
- for (const row of rows) {
4597
- if (!row || !supportedRowTypes.includes(row.__typename ?? ''))
4985
+ for (const row of queryResult.rows) {
4986
+ if (!KpiWidgetComponent.SUPPORTED_ROW_TYPES.has(row.__typename ?? ''))
4598
4987
  continue;
4599
- const queryRow = row;
4600
- const cells = queryRow.cells?.items ?? [];
4601
4988
  let categoryMatch = false;
4602
4989
  let value = 0;
4603
- for (const cell of cells) {
4604
- if (!cell?.attributePath)
4605
- continue;
4990
+ for (const cell of row.cells) {
4606
4991
  if (matchesAttributePath(cell.attributePath, categoryField) && String(cell.value) === categoryValue) {
4607
4992
  categoryMatch = true;
4608
4993
  }
@@ -4692,8 +5077,18 @@ class KpiConfigDialogComponent {
4692
5077
  getEntitiesByCkTypeGQL = inject(GetEntitiesByCkTypeDtoGQL);
4693
5078
  ckTypeSelectorService = inject(CkTypeSelectorService);
4694
5079
  attributeSelectorService = inject(AttributeSelectorService);
4695
- executeRuntimeQueryGQL = inject(ExecuteRuntimeQueryDtoGQL);
4696
5080
  getRuntimeQueryColumnsGQL = inject(GetRuntimeQueryColumnsDtoGQL);
5081
+ queryExecutor = inject(QueryExecutorService);
5082
+ /**
5083
+ * Row __typenames the dialog recognises when collecting distinct category
5084
+ * values from a query result for the grouped-aggregation category picker.
5085
+ */
5086
+ static INTROSPECTION_ROW_TYPES = new Set([
5087
+ 'RtSimpleQueryRow',
5088
+ 'RtAggregationQueryRow',
5089
+ 'RtGroupingAggregationQueryRow',
5090
+ 'StreamDataQueryRow'
5091
+ ]);
4697
5092
  meshBoardStateService = inject(MeshBoardStateService);
4698
5093
  windowRef = inject(WindowRef);
4699
5094
  ckTypeSelectorInput;
@@ -4712,6 +5107,7 @@ class KpiConfigDialogComponent {
4712
5107
  initialDataSourceType;
4713
5108
  initialQueryRtId;
4714
5109
  initialQueryName;
5110
+ initialQueryFamily;
4715
5111
  initialQueryMode;
4716
5112
  initialQueryValueField;
4717
5113
  initialQueryCategoryField;
@@ -4985,6 +5381,7 @@ class KpiConfigDialogComponent {
4985
5381
  return;
4986
5382
  }
4987
5383
  if (this.dataSourceType === 'persistentQuery' && this.selectedPersistentQuery) {
5384
+ const family = queryFamily(this.selectedPersistentQuery.ckTypeId) ?? this.initialQueryFamily ?? undefined;
4988
5385
  this.windowRef.close({
4989
5386
  dataSourceType: 'persistentQuery',
4990
5387
  ckTypeId: '',
@@ -4992,6 +5389,7 @@ class KpiConfigDialogComponent {
4992
5389
  valueAttribute: '',
4993
5390
  queryRtId: this.selectedPersistentQuery.rtId,
4994
5391
  queryName: this.selectedPersistentQuery.name,
5392
+ queryFamily: family,
4995
5393
  queryMode: this.queryMode,
4996
5394
  queryValueField: this.form.queryValueField || undefined,
4997
5395
  queryCategoryField: this.form.queryCategoryField || undefined,
@@ -5058,35 +5456,16 @@ class KpiConfigDialogComponent {
5058
5456
  const rtId = queryRtId || this.selectedPersistentQuery?.rtId;
5059
5457
  if (!rtId)
5060
5458
  return;
5459
+ // queryFamily may be undefined when the selected query metadata is missing —
5460
+ // fetchColumnsForFamily resolves it via the executor's one-time lookup.
5461
+ const family = queryFamily(this.selectedPersistentQuery?.ckTypeId) ?? this.initialQueryFamily;
5061
5462
  this.isLoadingQueryColumns = true;
5062
5463
  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
- }
5464
+ this.queryColumns = await this.fetchColumnsForFamily(family, rtId);
5465
+ // Category values for grouped aggregation are loaded on-demand by
5466
+ // loadCategoryValuesForField only when a categoryField is actually selected.
5467
+ if (this.queryColumns.length > 0 && this.queryMode === 'groupedAggregation' && this.form.queryCategoryField) {
5468
+ await this.loadCategoryValuesForField(rtId, this.form.queryCategoryField);
5090
5469
  }
5091
5470
  }
5092
5471
  catch (error) {
@@ -5097,6 +5476,40 @@ class KpiConfigDialogComponent {
5097
5476
  this.isLoadingQueryColumns = false;
5098
5477
  }
5099
5478
  }
5479
+ /**
5480
+ * Loads column metadata for the picker. Runtime queries use the
5481
+ * metadata-only resolver (no aggregation executed); stream-data queries
5482
+ * fall back to executing the query with `first: 1`. When `family` is
5483
+ * unknown (legacy configs), the executor resolves it once by rtId lookup.
5484
+ */
5485
+ async fetchColumnsForFamily(family, rtId) {
5486
+ const resolvedFamily = family ?? await this.queryExecutor.resolveFamily(rtId);
5487
+ if (resolvedFamily === 'runtime') {
5488
+ const result = await firstValueFrom(this.getRuntimeQueryColumnsGQL.fetch({
5489
+ variables: { rtId }
5490
+ }));
5491
+ const queryItem = result.data?.runtime?.runtimeQuery?.items?.[0];
5492
+ if (!queryItem)
5493
+ return [];
5494
+ return (queryItem.columns ?? [])
5495
+ .filter((c) => c !== null)
5496
+ // Column AttributePath is already in the engine's wire form for aggregation /
5497
+ // grouping columns (e.g. `quantity_sum`, `operatingstatus`) so picker entries
5498
+ // double as both the visible label and the stored config value.
5499
+ .map(c => ({
5500
+ attributePath: c.attributePath ?? '',
5501
+ attributeValueType: c.attributeValueType ?? '',
5502
+ aggregationType: c.aggregationType ?? null
5503
+ }));
5504
+ }
5505
+ // Stream-data: execute with a tiny page just to surface columns.
5506
+ const sdResult = await firstValueFrom(this.queryExecutor.executeStreamData(rtId, { first: 1 }));
5507
+ return sdResult.columns.map(c => ({
5508
+ attributePath: c.attributePath,
5509
+ attributeValueType: c.attributeValueType ?? '',
5510
+ aggregationType: c.aggregationType ?? null
5511
+ }));
5512
+ }
5100
5513
  async onCategoryFieldChange(categoryField) {
5101
5514
  this.form.queryCategoryField = categoryField;
5102
5515
  this.form.queryCategoryValue = '';
@@ -5108,36 +5521,23 @@ class KpiConfigDialogComponent {
5108
5521
  async loadCategoryValuesForField(queryRtId, categoryField) {
5109
5522
  this.isLoadingCategoryValues = true;
5110
5523
  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
- }
5524
+ // family may be undefined here — the executor falls back to a lookup.
5525
+ const family = queryFamily(this.selectedPersistentQuery?.ckTypeId) ?? this.initialQueryFamily;
5526
+ const result = await firstValueFrom(this.queryExecutor.execute(family, queryRtId, { first: 100 }));
5527
+ const values = new Set();
5528
+ for (const row of result.rows) {
5529
+ if (!KpiConfigDialogComponent.INTROSPECTION_ROW_TYPES.has(row.__typename ?? ''))
5530
+ continue;
5531
+ for (const cell of row.cells) {
5532
+ if (matchesAttributePath(cell.attributePath, categoryField) && cell.value !== null && cell.value !== undefined) {
5533
+ values.add(String(cell.value));
5134
5534
  }
5135
5535
  }
5136
- this.categoryValues = Array.from(values).map(v => ({
5137
- value: v,
5138
- displayValue: v
5139
- }));
5140
5536
  }
5537
+ this.categoryValues = Array.from(values).map(v => ({
5538
+ value: v,
5539
+ displayValue: v
5540
+ }));
5141
5541
  }
5142
5542
  catch (error) {
5143
5543
  console.error('Error loading category values:', error);
@@ -5154,7 +5554,7 @@ class KpiConfigDialogComponent {
5154
5554
  this.filters = updatedFilters;
5155
5555
  }
5156
5556
  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: `
5557
+ 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
5558
  <div class="config-container">
5159
5559
 
5160
5560
  <div class="config-form" [class.loading]="isLoadingInitial">
@@ -5524,7 +5924,7 @@ class KpiConfigDialogComponent {
5524
5924
  </button>
5525
5925
  </div>
5526
5926
  </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"] }] });
5927
+ `, 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
5928
  }
5529
5929
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: KpiConfigDialogComponent, decorators: [{
5530
5930
  type: Component,
@@ -5941,6 +6341,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
5941
6341
  type: Input
5942
6342
  }], initialQueryName: [{
5943
6343
  type: Input
6344
+ }], initialQueryFamily: [{
6345
+ type: Input
5944
6346
  }], initialQueryMode: [{
5945
6347
  type: Input
5946
6348
  }], initialQueryValueField: [{
@@ -7364,7 +7766,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
7364
7766
  */
7365
7767
  class TableWidgetDataSourceDirective extends OctoGraphQlDataSource {
7366
7768
  getEntitiesByCkTypeGQL = inject(GetEntitiesByCkTypeDtoGQL);
7367
- executeRuntimeQueryGQL = inject(ExecuteRuntimeQueryDtoGQL);
7769
+ queryExecutor = inject(QueryExecutorService);
7368
7770
  stateService = inject(MeshBoardStateService);
7369
7771
  variableService = inject(MeshBoardVariableService);
7370
7772
  _config = null;
@@ -7493,86 +7895,81 @@ class TableWidgetDataSourceDirective extends OctoGraphQlDataSource {
7493
7895
  }
7494
7896
  }
7495
7897
  /**
7496
- * Fetches data from a persistent query.
7497
- * Extracts columns from the query response and transforms cells to flat records.
7898
+ * Row __typenames the table widget knows how to flatten. Runtime queries
7899
+ * use three discriminated variants; stream-data queries collapse all
7900
+ * kinds (simple / aggregation / grouped / downsampling) into a single
7901
+ * `StreamDataQueryRow` type.
7902
+ */
7903
+ static SUPPORTED_ROW_TYPES = new Set([
7904
+ 'RtSimpleQueryRow',
7905
+ 'RtAggregationQueryRow',
7906
+ 'RtGroupingAggregationQueryRow',
7907
+ 'StreamDataQueryRow'
7908
+ ]);
7909
+ /**
7910
+ * Fetches data from a persistent query (runtime or stream-data).
7911
+ * Family is determined from the cached `queryFamily` on the data source
7912
+ * (set by the config dialog when the user picks a query) and defaults to
7913
+ * `'runtime'` for legacy configs that predate stream-data support.
7498
7914
  */
7499
7915
  fetchPersistentQueryData(dataSource, queryOptions) {
7500
- // Convert widget-configured filters to GraphQL format (with variable resolution)
7501
7916
  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
- }
7917
+ // queryFamily may be undefined for legacy widget configs — the executor
7918
+ // falls back to a one-time lookup by rtId. streamDataArgs is sent
7919
+ // unconditionally because the runtime path ignores it.
7920
+ //
7921
+ // Precedence: MeshBoard time filter > query's intrinsic time bounds.
7922
+ // When no time filter is active, the persistent query uses its own bounds.
7923
+ const streamDataArgs = this.buildStreamDataArgs();
7924
+ return this.queryExecutor.execute(dataSource.queryFamily, dataSource.queryRtId, {
7925
+ first: queryOptions.state.take ?? this._config?.pageSize ?? 10,
7926
+ after: GraphQL.offsetToCursor(queryOptions.state.skip ?? 0),
7927
+ fieldFilter: fieldFilter ?? undefined,
7928
+ streamDataArgs
7509
7929
  }).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 ?? ''),
7930
+ // Extract columns from query response and update signal.
7931
+ // Replace dots with underscores for grid compatibility (Kendo treats dots as nested paths).
7932
+ const columns = result.columns.map(c => ({
7933
+ attributePath: this.sanitizeFieldName(c.attributePath),
7524
7934
  attributeValueType: c.attributeValueType ?? ''
7525
7935
  }));
7526
7936
  this._queryColumns.set(columns);
7527
- // Emit event to notify component that columns have been loaded
7528
7937
  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
7938
  const columnPaths = new Set(columns.map(c => c.attributePath));
7534
7939
  const hasRtIdColumn = columnPaths.has('rtId');
7535
7940
  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);
7941
+ const data = result.rows
7942
+ .filter(row => TableWidgetDataSourceDirective.SUPPORTED_ROW_TYPES.has(row.__typename ?? ''))
7943
+ .map((row, index) => this.queryRowToRecord(row, columns, hasRtIdColumn, hasCkTypeIdColumn, index));
7944
+ return new FetchResultTyped(data, result.totalCount);
7571
7945
  }), catchError$1(err => {
7572
7946
  console.error('Error fetching query data:', err);
7573
7947
  return of(new FetchResultTyped([], 0));
7574
7948
  }));
7575
7949
  }
7950
+ /**
7951
+ * Flattens a unified `QueryResultRow` into a Kendo-grid-friendly record.
7952
+ * Cells are stored under their matching column's `attributePath` rather than
7953
+ * the cell's own — the engine emits cell paths in wire-form with a function
7954
+ * suffix (e.g. cell path `meterreading_count` for column path `meterReading`).
7955
+ * `matchesAttributePath` reconciles both forms.
7956
+ */
7957
+ queryRowToRecord(row, columns, hasRtIdColumn, hasCkTypeIdColumn, index) {
7958
+ const record = {};
7959
+ if (hasRtIdColumn) {
7960
+ record['rtId'] = row.rtId ?? `agg-${index}`;
7961
+ }
7962
+ if (hasCkTypeIdColumn) {
7963
+ record['ckTypeId'] = row.ckTypeId ?? '';
7964
+ }
7965
+ for (const cell of row.cells) {
7966
+ const matchingColumn = columns.find(col => matchesAttributePath(cell.attributePath, col.attributePath));
7967
+ if (matchingColumn) {
7968
+ record[matchingColumn.attributePath] = cell.value;
7969
+ }
7970
+ }
7971
+ return record;
7972
+ }
7576
7973
  /**
7577
7974
  * Converts query columns to TableColumn format for display.
7578
7975
  */
@@ -7615,6 +8012,18 @@ class TableWidgetDataSourceDirective extends OctoGraphQlDataSource {
7615
8012
  const variables = this.stateService.getVariables();
7616
8013
  return this.variableService.convertToFieldFilterDto(filters, variables);
7617
8014
  }
8015
+ /**
8016
+ * Builds `StreamDataExecutionArgs` from the MeshBoard's current time filter.
8017
+ * Returns `undefined` when no filter is active so the persistent query's
8018
+ * own bounds apply.
8019
+ */
8020
+ buildStreamDataArgs() {
8021
+ const range = this.stateService.resolveCurrentTimeRange();
8022
+ if (!range) {
8023
+ return undefined;
8024
+ }
8025
+ return { from: range.from, to: range.to };
8026
+ }
7618
8027
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: TableWidgetDataSourceDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
7619
8028
  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
8029
  {
@@ -7850,6 +8259,7 @@ class TableConfigDialogComponent {
7850
8259
  initialSortable;
7851
8260
  initialQueryRtId;
7852
8261
  initialQueryName;
8262
+ initialQueryFamily;
7853
8263
  columnsIcon = columnsIcon;
7854
8264
  sortIcon = sortAscIcon;
7855
8265
  filterIcon = filterIcon;
@@ -8071,6 +8481,9 @@ class TableConfigDialogComponent {
8071
8481
  operator: f.operator,
8072
8482
  comparisonValue: f.comparisonValue
8073
8483
  }));
8484
+ // Derive family from the selected query's CK type so the runtime executor
8485
+ // can route correctly without an extra lookup.
8486
+ const family = queryFamily(this.selectedPersistentQuery.ckTypeId) ?? this.initialQueryFamily ?? undefined;
8074
8487
  this.windowRef.close({
8075
8488
  dataSourceType: 'persistentQuery',
8076
8489
  ckTypeId: '', // Not used for persistent query
@@ -8079,6 +8492,7 @@ class TableConfigDialogComponent {
8079
8492
  filters: queryFilterDtos,
8080
8493
  queryRtId: this.selectedPersistentQuery.rtId,
8081
8494
  queryName: this.selectedPersistentQuery.name,
8495
+ queryFamily: family,
8082
8496
  pageSize: this.form.pageSize,
8083
8497
  sortable: this.form.sortable
8084
8498
  });
@@ -8095,7 +8509,7 @@ class TableConfigDialogComponent {
8095
8509
  this.windowRef.close();
8096
8510
  }
8097
8511
  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: [
8512
+ 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
8513
  AttributeSelectorDialogService,
8100
8514
  AttributeSortSelectorDialogService
8101
8515
  ], 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 +8712,7 @@ class TableConfigDialogComponent {
8298
8712
  </button>
8299
8713
  </div>
8300
8714
  </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"] }] });
8715
+ `, 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
8716
  }
8303
8717
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: TableConfigDialogComponent, decorators: [{
8304
8718
  type: Component,
@@ -8545,6 +8959,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
8545
8959
  type: Input
8546
8960
  }], initialQueryName: [{
8547
8961
  type: Input
8962
+ }], initialQueryFamily: [{
8963
+ type: Input
8548
8964
  }] } });
8549
8965
 
8550
8966
  class GaugeWidgetComponent {
@@ -8563,7 +8979,12 @@ class GaugeWidgetComponent {
8563
8979
  },
8564
8980
  ];
8565
8981
  dataService = inject(DashboardDataService);
8566
- executeRuntimeQueryGQL = inject(ExecuteRuntimeQueryDtoGQL);
8982
+ queryExecutor = inject(QueryExecutorService);
8983
+ static SUPPORTED_ROW_TYPES = new Set([
8984
+ 'RtAggregationQueryRow',
8985
+ 'RtGroupingAggregationQueryRow',
8986
+ 'StreamDataQueryRow'
8987
+ ]);
8567
8988
  stateService = inject(MeshBoardStateService);
8568
8989
  variableService = inject(MeshBoardVariableService);
8569
8990
  config;
@@ -8673,43 +9094,29 @@ class GaugeWidgetComponent {
8673
9094
  this._isLoading.set(true);
8674
9095
  this._error.set(null);
8675
9096
  try {
8676
- // Convert widget filters to GraphQL format
8677
9097
  const fieldFilter = this.convertFiltersToDto(this.config.filters);
8678
- const result = await firstValueFrom(this.executeRuntimeQueryGQL.fetch({
8679
- variables: {
8680
- rtId: dataSource.queryRtId,
8681
- fieldFilter
8682
- }
9098
+ // queryFamily may be undefined for legacy widget configs — the executor
9099
+ // falls back to a one-time lookup by rtId. streamDataArgs is sent
9100
+ // unconditionally because the runtime path ignores it.
9101
+ const streamDataArgs = this.buildStreamDataArgs();
9102
+ const result = await firstValueFrom(this.queryExecutor.execute(dataSource.queryFamily, dataSource.queryRtId, {
9103
+ fieldFilter: fieldFilter ?? undefined,
9104
+ streamDataArgs
8683
9105
  }).pipe(catchError(err => {
8684
9106
  console.error('Error loading Gauge query data:', err);
8685
9107
  throw err;
8686
9108
  })));
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
9109
  let value = 0;
8700
9110
  const queryMode = this.config.queryMode ?? 'simpleCount';
8701
9111
  switch (queryMode) {
8702
9112
  case 'simpleCount':
8703
- // Use totalCount from the query
8704
- value = queryResult.rows?.totalCount ?? 0;
9113
+ value = result.totalCount;
8705
9114
  break;
8706
9115
  case 'aggregation':
8707
- // Get the single value from aggregation query (1 row, 1 column)
8708
- value = this.extractAggregationValue(queryResult);
9116
+ value = this.extractAggregationValue(result);
8709
9117
  break;
8710
9118
  case 'groupedAggregation':
8711
- // Find the row matching the selected category and get its value
8712
- value = this.extractGroupedAggregationValue(queryResult);
9119
+ value = this.extractGroupedAggregationValue(result);
8713
9120
  break;
8714
9121
  }
8715
9122
  // Create a synthetic entity with the value
@@ -8728,48 +9135,38 @@ class GaugeWidgetComponent {
8728
9135
  this._isLoading.set(false);
8729
9136
  }
8730
9137
  }
9138
+ buildStreamDataArgs() {
9139
+ const range = this.stateService.resolveCurrentTimeRange();
9140
+ if (!range) {
9141
+ return undefined;
9142
+ }
9143
+ return { from: range.from, to: range.to };
9144
+ }
8731
9145
  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 ?? ''));
9146
+ const firstRow = queryResult.rows.find(row => GaugeWidgetComponent.SUPPORTED_ROW_TYPES.has(row.__typename ?? ''));
8736
9147
  if (!firstRow)
8737
9148
  return 0;
8738
- const queryRow = firstRow;
8739
- const cells = queryRow.cells?.items ?? [];
8740
- // Find the value field or use the first numeric cell
8741
9149
  const valueField = this.config.queryValueField;
8742
- for (const cell of cells) {
8743
- if (!cell?.attributePath)
8744
- continue;
9150
+ for (const cell of firstRow.cells) {
8745
9151
  if (valueField && matchesAttributePath(cell.attributePath, valueField)) {
8746
9152
  return this.parseNumericValue(cell.value);
8747
9153
  }
8748
9154
  }
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;
9155
+ return firstRow.cells.length > 0 ? this.parseNumericValue(firstRow.cells[0].value) : 0;
8752
9156
  }
8753
9157
  extractGroupedAggregationValue(queryResult) {
8754
- const rows = queryResult.rows?.items ?? [];
8755
- const supportedRowTypes = ['RtGroupingAggregationQueryRow', 'RtAggregationQueryRow'];
8756
9158
  const categoryField = this.config.queryCategoryField;
8757
9159
  const categoryValue = this.config.queryCategoryValue;
8758
9160
  const valueField = this.config.queryValueField;
8759
9161
  if (!categoryField || !categoryValue || !valueField) {
8760
9162
  return 0;
8761
9163
  }
8762
- // Find the row where category matches
8763
- for (const row of rows) {
8764
- if (!row || !supportedRowTypes.includes(row.__typename ?? ''))
9164
+ for (const row of queryResult.rows) {
9165
+ if (!GaugeWidgetComponent.SUPPORTED_ROW_TYPES.has(row.__typename ?? ''))
8765
9166
  continue;
8766
- const queryRow = row;
8767
- const cells = queryRow.cells?.items ?? [];
8768
9167
  let categoryMatch = false;
8769
9168
  let value = 0;
8770
- for (const cell of cells) {
8771
- if (!cell?.attributePath)
8772
- continue;
9169
+ for (const cell of row.cells) {
8773
9170
  if (matchesAttributePath(cell.attributePath, categoryField) && String(cell.value) === categoryValue) {
8774
9171
  categoryMatch = true;
8775
9172
  }
@@ -9157,8 +9554,18 @@ class GaugeConfigDialogComponent {
9157
9554
  getEntitiesByCkTypeGQL = inject(GetEntitiesByCkTypeDtoGQL);
9158
9555
  ckTypeSelectorService = inject(CkTypeSelectorService);
9159
9556
  attributeSelectorService = inject(AttributeSelectorService);
9160
- executeRuntimeQueryGQL = inject(ExecuteRuntimeQueryDtoGQL);
9161
9557
  getRuntimeQueryColumnsGQL = inject(GetRuntimeQueryColumnsDtoGQL);
9558
+ queryExecutor = inject(QueryExecutorService);
9559
+ /**
9560
+ * Row __typenames the dialog recognises when collecting distinct category
9561
+ * values from a query result for the grouped-aggregation category picker.
9562
+ */
9563
+ static INTROSPECTION_ROW_TYPES = new Set([
9564
+ 'RtSimpleQueryRow',
9565
+ 'RtAggregationQueryRow',
9566
+ 'RtGroupingAggregationQueryRow',
9567
+ 'StreamDataQueryRow'
9568
+ ]);
9162
9569
  meshBoardStateService = inject(MeshBoardStateService);
9163
9570
  windowRef = inject(WindowRef);
9164
9571
  ckTypeSelectorInput;
@@ -9181,6 +9588,7 @@ class GaugeConfigDialogComponent {
9181
9588
  initialDataSourceType;
9182
9589
  initialQueryRtId;
9183
9590
  initialQueryName;
9591
+ initialQueryFamily;
9184
9592
  initialQueryMode;
9185
9593
  initialQueryValueField;
9186
9594
  initialQueryCategoryField;
@@ -9428,6 +9836,7 @@ class GaugeConfigDialogComponent {
9428
9836
  }))
9429
9837
  : undefined;
9430
9838
  if (this.dataSourceType === 'persistentQuery' && this.selectedPersistentQuery) {
9839
+ const family = queryFamily(this.selectedPersistentQuery.ckTypeId) ?? this.initialQueryFamily ?? undefined;
9431
9840
  this.windowRef.close({
9432
9841
  dataSourceType: 'persistentQuery',
9433
9842
  ckTypeId: '',
@@ -9435,6 +9844,7 @@ class GaugeConfigDialogComponent {
9435
9844
  valueAttribute: '',
9436
9845
  queryRtId: this.selectedPersistentQuery.rtId,
9437
9846
  queryName: this.selectedPersistentQuery.name,
9847
+ queryFamily: family,
9438
9848
  queryMode: this.queryMode,
9439
9849
  queryValueField: this.form.queryValueField || undefined,
9440
9850
  queryCategoryField: this.form.queryCategoryField || undefined,
@@ -9507,31 +9917,13 @@ class GaugeConfigDialogComponent {
9507
9917
  }
9508
9918
  async loadQueryColumnsAndValues(queryRtId) {
9509
9919
  this.isLoadingQueryColumns = true;
9920
+ // queryFamily may be undefined when the selected query metadata is missing —
9921
+ // fetchColumnsForFamily resolves it via the executor's one-time lookup.
9922
+ const family = queryFamily(this.selectedPersistentQuery?.ckTypeId) ?? this.initialQueryFamily;
9510
9923
  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
- }
9924
+ this.queryColumns = await this.fetchColumnsForFamily(family, queryRtId);
9925
+ if (this.queryColumns.length > 0 && this.queryMode === 'groupedAggregation' && this.form.queryCategoryField) {
9926
+ await this.loadCategoryValuesForField(queryRtId, this.form.queryCategoryField);
9535
9927
  }
9536
9928
  }
9537
9929
  catch (error) {
@@ -9542,6 +9934,36 @@ class GaugeConfigDialogComponent {
9542
9934
  this.isLoadingQueryColumns = false;
9543
9935
  }
9544
9936
  }
9937
+ /**
9938
+ * Runtime queries use the metadata-only resolver (no aggregation executed);
9939
+ * stream-data queries fall back to executing the query with `first: 1`.
9940
+ * When `family` is unknown (legacy configs), the executor resolves it once
9941
+ * by rtId lookup.
9942
+ */
9943
+ async fetchColumnsForFamily(family, rtId) {
9944
+ const resolvedFamily = family ?? await this.queryExecutor.resolveFamily(rtId);
9945
+ if (resolvedFamily === 'runtime') {
9946
+ const result = await firstValueFrom(this.getRuntimeQueryColumnsGQL.fetch({
9947
+ variables: { rtId }
9948
+ }));
9949
+ const queryItem = result.data?.runtime?.runtimeQuery?.items?.[0];
9950
+ if (!queryItem)
9951
+ return [];
9952
+ return (queryItem.columns ?? [])
9953
+ .filter((c) => c !== null)
9954
+ .map(c => ({
9955
+ attributePath: c.attributePath ?? '',
9956
+ attributeValueType: c.attributeValueType ?? '',
9957
+ aggregationType: c.aggregationType ?? null
9958
+ }));
9959
+ }
9960
+ const sdResult = await firstValueFrom(this.queryExecutor.executeStreamData(rtId, { first: 1 }));
9961
+ return sdResult.columns.map(c => ({
9962
+ attributePath: c.attributePath,
9963
+ attributeValueType: c.attributeValueType ?? '',
9964
+ aggregationType: c.aggregationType ?? null
9965
+ }));
9966
+ }
9545
9967
  async onCategoryFieldChange(categoryField) {
9546
9968
  this.form.queryCategoryField = categoryField;
9547
9969
  this.form.queryCategoryValue = '';
@@ -9552,37 +9974,24 @@ class GaugeConfigDialogComponent {
9552
9974
  }
9553
9975
  async loadCategoryValuesForField(queryRtId, categoryField) {
9554
9976
  this.isLoadingCategoryValues = true;
9977
+ // family may be undefined — the executor falls back to a lookup.
9978
+ const family = queryFamily(this.selectedPersistentQuery?.ckTypeId) ?? this.initialQueryFamily;
9555
9979
  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
- }
9980
+ const result = await firstValueFrom(this.queryExecutor.execute(family, queryRtId, { first: 100 }));
9981
+ const values = new Set();
9982
+ for (const row of result.rows) {
9983
+ if (!GaugeConfigDialogComponent.INTROSPECTION_ROW_TYPES.has(row.__typename ?? ''))
9984
+ continue;
9985
+ for (const cell of row.cells) {
9986
+ if (matchesAttributePath(cell.attributePath, categoryField) && cell.value !== null && cell.value !== undefined) {
9987
+ values.add(String(cell.value));
9579
9988
  }
9580
9989
  }
9581
- this.categoryValues = Array.from(values).map(v => ({
9582
- value: v,
9583
- displayValue: v
9584
- }));
9585
9990
  }
9991
+ this.categoryValues = Array.from(values).map(v => ({
9992
+ value: v,
9993
+ displayValue: v
9994
+ }));
9586
9995
  }
9587
9996
  catch (error) {
9588
9997
  console.error('Error loading category values:', error);
@@ -9599,7 +10008,7 @@ class GaugeConfigDialogComponent {
9599
10008
  this.filters = updatedFilters;
9600
10009
  }
9601
10010
  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: `
10011
+ 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
10012
  <div class="config-container">
9604
10013
 
9605
10014
  <div class="config-form" [class.loading]="isLoadingInitial">
@@ -9997,7 +10406,7 @@ class GaugeConfigDialogComponent {
9997
10406
  </button>
9998
10407
  </div>
9999
10408
  </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"] }] });
10409
+ `, 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
10410
  }
10002
10411
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: GaugeConfigDialogComponent, decorators: [{
10003
10412
  type: Component,
@@ -10450,6 +10859,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
10450
10859
  type: Input
10451
10860
  }], initialQueryName: [{
10452
10861
  type: Input
10862
+ }], initialQueryFamily: [{
10863
+ type: Input
10453
10864
  }], initialQueryMode: [{
10454
10865
  type: Input
10455
10866
  }], initialQueryValueField: [{
@@ -10463,7 +10874,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
10463
10874
  }] } });
10464
10875
 
10465
10876
  class PieChartWidgetComponent {
10466
- executeRuntimeQueryGQL = inject(ExecuteRuntimeQueryDtoGQL);
10877
+ queryExecutor = inject(QueryExecutorService);
10878
+ static SUPPORTED_ROW_TYPES = new Set([
10879
+ 'RtSimpleQueryRow',
10880
+ 'RtAggregationQueryRow',
10881
+ 'RtGroupingAggregationQueryRow',
10882
+ 'StreamDataQueryRow'
10883
+ ]);
10467
10884
  dataService = inject(MeshBoardDataService);
10468
10885
  stateService = inject(MeshBoardStateService);
10469
10886
  variableService = inject(MeshBoardVariableService);
@@ -10588,35 +11005,22 @@ class PieChartWidgetComponent {
10588
11005
  * Note: isNotConfigured() check in loadData() ensures queryRtId is set.
10589
11006
  */
10590
11007
  async loadPersistentQueryData(dataSource) {
10591
- // Convert widget filters to GraphQL format
10592
11008
  const fieldFilter = this.convertFiltersToDto(this.config.filters);
10593
- const result = await firstValueFrom(this.executeRuntimeQueryGQL.fetch({
10594
- variables: {
10595
- rtId: dataSource.queryRtId,
10596
- fieldFilter
10597
- }
11009
+ // queryFamily may be undefined for legacy widget configs — the executor
11010
+ // falls back to a one-time lookup by rtId. streamDataArgs is sent
11011
+ // unconditionally because the runtime path ignores it.
11012
+ const streamDataArgs = this.buildStreamDataArgs();
11013
+ const result = await firstValueFrom(this.queryExecutor.execute(dataSource.queryFamily, dataSource.queryRtId, {
11014
+ fieldFilter: fieldFilter ?? undefined,
11015
+ streamDataArgs
10598
11016
  }).pipe(catchError(err => {
10599
11017
  console.error('Error loading Pie Chart data:', err);
10600
11018
  throw err;
10601
11019
  })));
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
11020
  // Extract columns to verify configured fields are present. Both forms (original CK
10615
11021
  // path and engine wire-form) are accepted so saved configs survive the engine's
10616
11022
  // switch to wire-form keys without a migration.
10617
- const columnPaths = (queryResult.columns ?? [])
10618
- .filter((c) => c !== null)
10619
- .map(c => c.attributePath ?? '');
11023
+ const columnPaths = result.columns.map(c => c.attributePath);
10620
11024
  const categoryFieldPresent = columnPaths.some(p => matchesAttributePath(p, this.config.categoryField));
10621
11025
  const valueFieldPresent = columnPaths.some(p => matchesAttributePath(p, this.config.valueField));
10622
11026
  if (!categoryFieldPresent || !valueFieldPresent) {
@@ -10624,20 +11028,12 @@ class PieChartWidgetComponent {
10624
11028
  this._isLoading.set(false);
10625
11029
  return;
10626
11030
  }
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 ?? ''))
11031
+ const chartData = result.rows
11032
+ .filter(row => PieChartWidgetComponent.SUPPORTED_ROW_TYPES.has(row.__typename ?? ''))
10633
11033
  .map(row => {
10634
- const queryRow = row;
10635
- const cells = queryRow.cells?.items ?? [];
10636
11034
  let category = '';
10637
11035
  let value = 0;
10638
- for (const cell of cells) {
10639
- if (!cell?.attributePath)
10640
- continue;
11036
+ for (const cell of row.cells) {
10641
11037
  if (matchesAttributePath(cell.attributePath, this.config.categoryField)) {
10642
11038
  category = String(cell.value ?? '');
10643
11039
  }
@@ -10652,6 +11048,13 @@ class PieChartWidgetComponent {
10652
11048
  this._chartData.set(chartData);
10653
11049
  this._isLoading.set(false);
10654
11050
  }
11051
+ buildStreamDataArgs() {
11052
+ const range = this.stateService.resolveCurrentTimeRange();
11053
+ if (!range) {
11054
+ return undefined;
11055
+ }
11056
+ return { from: range.from, to: range.to };
11057
+ }
10655
11058
  /**
10656
11059
  * Converts widget filter configuration to GraphQL FieldFilterDto format.
10657
11060
  * Resolves MeshBoard variables in filter values before conversion.
@@ -10755,6 +11158,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
10755
11158
  */
10756
11159
  class PieChartConfigDialogComponent {
10757
11160
  getRuntimeQueryColumnsGQL = inject(GetRuntimeQueryColumnsDtoGQL);
11161
+ queryExecutor = inject(QueryExecutorService);
10758
11162
  stateService = inject(MeshBoardStateService);
10759
11163
  windowRef = inject(WindowRef);
10760
11164
  querySelector;
@@ -10762,6 +11166,7 @@ class PieChartConfigDialogComponent {
10762
11166
  initialDataSourceType;
10763
11167
  initialQueryRtId;
10764
11168
  initialQueryName;
11169
+ initialQueryFamily;
10765
11170
  initialChartType;
10766
11171
  initialCategoryField;
10767
11172
  initialValueField;
@@ -10922,37 +11327,19 @@ class PieChartConfigDialogComponent {
10922
11327
  }
10923
11328
  async loadQueryColumns(queryRtId) {
10924
11329
  this.isLoadingColumns = true;
11330
+ // family may be undefined when the selected query metadata is missing —
11331
+ // fetchColumnsForFamily resolves it via the executor's one-time lookup.
11332
+ const family = queryFamily(this.selectedPersistentQuery?.ckTypeId) ?? this.initialQueryFamily;
10925
11333
  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
- }
11334
+ this.queryColumns = await this.fetchColumnsForFamily(family, queryRtId);
11335
+ // Auto-select fields if only 2 columns (typical for grouped aggregations)
11336
+ if (this.queryColumns.length === 2 && !this.form.categoryField && !this.form.valueField) {
11337
+ const numericTypes = ['INTEGER', 'FLOAT', 'DOUBLE', 'DECIMAL', 'LONG'];
11338
+ const valueColumn = this.queryColumns.find(c => numericTypes.includes(c.attributeValueType));
11339
+ const categoryColumn = this.queryColumns.find(c => c !== valueColumn);
11340
+ if (valueColumn && categoryColumn) {
11341
+ this.form.valueField = valueColumn.attributePath;
11342
+ this.form.categoryField = categoryColumn.attributePath;
10956
11343
  }
10957
11344
  }
10958
11345
  }
@@ -10964,6 +11351,35 @@ class PieChartConfigDialogComponent {
10964
11351
  this.isLoadingColumns = false;
10965
11352
  }
10966
11353
  }
11354
+ /**
11355
+ * Runtime queries use the metadata-only resolver (no aggregation executed);
11356
+ * stream-data queries fall back to executing the query with `first: 1`
11357
+ * because the SD path has no dedicated column-introspection endpoint today.
11358
+ */
11359
+ async fetchColumnsForFamily(family, rtId) {
11360
+ const resolvedFamily = family ?? await this.queryExecutor.resolveFamily(rtId);
11361
+ if (resolvedFamily === 'runtime') {
11362
+ const result = await firstValueFrom(this.getRuntimeQueryColumnsGQL.fetch({
11363
+ variables: { rtId }
11364
+ }));
11365
+ const queryItem = result.data?.runtime?.runtimeQuery?.items?.[0];
11366
+ if (!queryItem)
11367
+ return [];
11368
+ return (queryItem.columns ?? [])
11369
+ .filter((c) => c !== null)
11370
+ .map(c => ({
11371
+ attributePath: c.attributePath ?? '',
11372
+ attributeValueType: c.attributeValueType ?? '',
11373
+ aggregationType: c.aggregationType ?? null
11374
+ }));
11375
+ }
11376
+ const sdResult = await firstValueFrom(this.queryExecutor.executeStreamData(rtId, { first: 1 }));
11377
+ return sdResult.columns.map(c => ({
11378
+ attributePath: c.attributePath,
11379
+ attributeValueType: c.attributeValueType ?? '',
11380
+ aggregationType: c.aggregationType ?? null
11381
+ }));
11382
+ }
10967
11383
  onFiltersChange(updatedFilters) {
10968
11384
  this.filters = updatedFilters;
10969
11385
  }
@@ -10993,6 +11409,7 @@ class PieChartConfigDialogComponent {
10993
11409
  return;
10994
11410
  result.queryRtId = this.selectedPersistentQuery.rtId;
10995
11411
  result.queryName = this.selectedPersistentQuery.name;
11412
+ result.queryFamily = queryFamily(this.selectedPersistentQuery.ckTypeId) ?? this.initialQueryFamily ?? undefined;
10996
11413
  }
10997
11414
  else if (this.form.dataSourceType === 'constructionKitQuery') {
10998
11415
  result.ckQueryTarget = this.form.ckQueryTarget;
@@ -11004,7 +11421,7 @@ class PieChartConfigDialogComponent {
11004
11421
  this.windowRef.close();
11005
11422
  }
11006
11423
  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: `
11424
+ 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
11425
  <div class="config-container">
11009
11426
 
11010
11427
  <div class="config-form" [class.loading]="isLoadingInitial">
@@ -11202,7 +11619,7 @@ class PieChartConfigDialogComponent {
11202
11619
  </button>
11203
11620
  </div>
11204
11621
  </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"] }] });
11622
+ `, 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
11623
  }
11207
11624
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: PieChartConfigDialogComponent, decorators: [{
11208
11625
  type: Component,
@@ -11424,6 +11841,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
11424
11841
  type: Input
11425
11842
  }], initialQueryName: [{
11426
11843
  type: Input
11844
+ }], initialQueryFamily: [{
11845
+ type: Input
11427
11846
  }], initialChartType: [{
11428
11847
  type: Input
11429
11848
  }], initialCategoryField: [{
@@ -11453,7 +11872,13 @@ const CHART_TYPE_MAPPING = {
11453
11872
  stackedBar100: { type: 'bar', stack: '100%' }
11454
11873
  };
11455
11874
  class BarChartWidgetComponent {
11456
- executeRuntimeQueryGQL = inject(ExecuteRuntimeQueryDtoGQL);
11875
+ queryExecutor = inject(QueryExecutorService);
11876
+ static SUPPORTED_ROW_TYPES = new Set([
11877
+ 'RtSimpleQueryRow',
11878
+ 'RtAggregationQueryRow',
11879
+ 'RtGroupingAggregationQueryRow',
11880
+ 'StreamDataQueryRow'
11881
+ ]);
11457
11882
  stateService = inject(MeshBoardStateService);
11458
11883
  variableService = inject(MeshBoardVariableService);
11459
11884
  config;
@@ -11562,38 +11987,19 @@ class BarChartWidgetComponent {
11562
11987
  this._isLoading.set(true);
11563
11988
  this._error.set(null);
11564
11989
  try {
11565
- // Convert widget filters to GraphQL format
11566
11990
  const fieldFilter = this.convertFiltersToDto(this.config.filters);
11567
- const result = await firstValueFrom(this.executeRuntimeQueryGQL.fetch({
11568
- variables: {
11569
- rtId: queryDataSource.queryRtId,
11570
- fieldFilter
11571
- }
11991
+ // queryFamily may be undefined for legacy widget configs — the executor
11992
+ // falls back to a one-time lookup by rtId. streamDataArgs is sent
11993
+ // unconditionally because the runtime path ignores it.
11994
+ const streamDataArgs = this.buildStreamDataArgs();
11995
+ const result = await firstValueFrom(this.queryExecutor.execute(queryDataSource.queryFamily, queryDataSource.queryRtId, {
11996
+ fieldFilter: fieldFilter ?? undefined,
11997
+ streamDataArgs
11572
11998
  }).pipe(catchError(err => {
11573
11999
  console.error('Error loading Bar Chart data:', err);
11574
12000
  throw err;
11575
12001
  })));
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
12002
+ const filteredRows = result.rows.filter(row => BarChartWidgetComponent.SUPPORTED_ROW_TYPES.has(row.__typename ?? ''));
11597
12003
  if (this.isDynamicSeriesMode()) {
11598
12004
  this.processDynamicSeriesData(filteredRows);
11599
12005
  }
@@ -11608,6 +12014,13 @@ class BarChartWidgetComponent {
11608
12014
  this._isLoading.set(false);
11609
12015
  }
11610
12016
  }
12017
+ buildStreamDataArgs() {
12018
+ const range = this.stateService.resolveCurrentTimeRange();
12019
+ if (!range) {
12020
+ return undefined;
12021
+ }
12022
+ return { from: range.from, to: range.to };
12023
+ }
11611
12024
  /**
11612
12025
  * Processes data in Static Series Mode.
11613
12026
  * Each series in config.series corresponds to a separate numeric field.
@@ -11620,13 +12033,9 @@ class BarChartWidgetComponent {
11620
12033
  seriesMap.set(seriesConfig.field, []);
11621
12034
  }
11622
12035
  for (const row of filteredRows) {
11623
- const queryRow = row;
11624
- const cells = queryRow.cells?.items ?? [];
11625
12036
  let categoryValue = '';
11626
12037
  const rowValues = new Map();
11627
- for (const cell of cells) {
11628
- if (!cell?.attributePath)
11629
- continue;
12038
+ for (const cell of row.cells) {
11630
12039
  if (matchesAttributePath(cell.attributePath, this.config.categoryField)) {
11631
12040
  categoryValue = this.formatCategoryValue(cell.value);
11632
12041
  }
@@ -11683,14 +12092,10 @@ class BarChartWidgetComponent {
11683
12092
  const allCategories = new Set();
11684
12093
  const allSeriesGroups = new Set();
11685
12094
  for (const row of filteredRows) {
11686
- const queryRow = row;
11687
- const cells = queryRow.cells?.items ?? [];
11688
12095
  let categoryValue = '';
11689
12096
  let seriesGroupValue = '';
11690
12097
  let numericValue = 0;
11691
- for (const cell of cells) {
11692
- if (!cell?.attributePath)
11693
- continue;
12098
+ for (const cell of row.cells) {
11694
12099
  if (matchesAttributePath(cell.attributePath, categoryField)) {
11695
12100
  categoryValue = this.formatCategoryValue(cell.value);
11696
12101
  }
@@ -11941,12 +12346,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
11941
12346
  */
11942
12347
  class BarChartConfigDialogComponent {
11943
12348
  getRuntimeQueryColumnsGQL = inject(GetRuntimeQueryColumnsDtoGQL);
12349
+ queryExecutor = inject(QueryExecutorService);
11944
12350
  stateService = inject(MeshBoardStateService);
11945
12351
  windowRef = inject(WindowRef);
11946
12352
  querySelector;
11947
12353
  // Initial values for editing
11948
12354
  initialQueryRtId;
11949
12355
  initialQueryName;
12356
+ initialQueryFamily;
11950
12357
  initialChartType;
11951
12358
  initialCategoryField;
11952
12359
  initialSeries;
@@ -12078,41 +12485,23 @@ class BarChartConfigDialogComponent {
12078
12485
  }
12079
12486
  async loadQueryColumns(queryRtId) {
12080
12487
  this.isLoadingColumns = true;
12488
+ // family may be undefined when the selected query metadata is missing —
12489
+ // fetchColumnsForFamily resolves it via the executor's one-time lookup.
12490
+ const family = queryFamily(this.selectedPersistentQuery?.ckTypeId) ?? this.initialQueryFamily;
12081
12491
  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
12492
+ this.queryColumns = await this.fetchColumnsForFamily(family, queryRtId);
12493
+ // Filter numeric and non-numeric columns
12494
+ const numericTypes = ['INTEGER', 'FLOAT', 'DOUBLE', 'DECIMAL', 'LONG'];
12495
+ this.numericColumns = this.queryColumns.filter(c => numericTypes.includes(c.attributeValueType));
12496
+ this.nonNumericColumns = this.queryColumns.filter(c => !numericTypes.includes(c.attributeValueType));
12497
+ // Auto-select fields if possible and not editing
12498
+ if (!this.initialQueryRtId && this.queryColumns.length >= 2) {
12499
+ const categoryColumn = this.queryColumns.find(c => !numericTypes.includes(c.attributeValueType));
12500
+ if (categoryColumn) {
12501
+ this.form.categoryField = categoryColumn.attributePath;
12087
12502
  }
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
- }
12503
+ if (this.numericColumns.length > 0) {
12504
+ this.selectedSeriesFields = this.numericColumns.map(c => c.attributePath);
12116
12505
  }
12117
12506
  }
12118
12507
  }
@@ -12126,6 +12515,35 @@ class BarChartConfigDialogComponent {
12126
12515
  this.isLoadingColumns = false;
12127
12516
  }
12128
12517
  }
12518
+ /**
12519
+ * Runtime queries use the metadata-only resolver (no aggregation executed);
12520
+ * stream-data queries fall back to executing the query with `first: 1`
12521
+ * because the SD path has no dedicated column-introspection endpoint today.
12522
+ */
12523
+ async fetchColumnsForFamily(family, rtId) {
12524
+ const resolvedFamily = family ?? await this.queryExecutor.resolveFamily(rtId);
12525
+ if (resolvedFamily === 'runtime') {
12526
+ const result = await firstValueFrom(this.getRuntimeQueryColumnsGQL.fetch({
12527
+ variables: { rtId }
12528
+ }));
12529
+ const queryItem = result.data?.runtime?.runtimeQuery?.items?.[0];
12530
+ if (!queryItem)
12531
+ return [];
12532
+ return (queryItem.columns ?? [])
12533
+ .filter((c) => c !== null)
12534
+ .map(c => ({
12535
+ attributePath: c.attributePath ?? '',
12536
+ attributeValueType: c.attributeValueType ?? '',
12537
+ aggregationType: c.aggregationType ?? null
12538
+ }));
12539
+ }
12540
+ const sdResult = await firstValueFrom(this.queryExecutor.executeStreamData(rtId, { first: 1 }));
12541
+ return sdResult.columns.map(c => ({
12542
+ attributePath: c.attributePath,
12543
+ attributeValueType: c.attributeValueType ?? '',
12544
+ aggregationType: c.aggregationType ?? null
12545
+ }));
12546
+ }
12129
12547
  onSeriesFieldsChange(fields) {
12130
12548
  this.selectedSeriesFields = fields;
12131
12549
  }
@@ -12155,11 +12573,13 @@ class BarChartConfigDialogComponent {
12155
12573
  comparisonValue: f.comparisonValue
12156
12574
  }))
12157
12575
  : undefined;
12576
+ const family = queryFamily(this.selectedPersistentQuery.ckTypeId) ?? this.initialQueryFamily ?? undefined;
12158
12577
  const result = {
12159
12578
  ckTypeId: '',
12160
12579
  rtId: '',
12161
12580
  queryRtId: this.selectedPersistentQuery.rtId,
12162
12581
  queryName: this.selectedPersistentQuery.name,
12582
+ queryFamily: family,
12163
12583
  chartType: this.form.chartType,
12164
12584
  categoryField: this.form.categoryField,
12165
12585
  series,
@@ -12187,7 +12607,7 @@ class BarChartConfigDialogComponent {
12187
12607
  this.windowRef.close();
12188
12608
  }
12189
12609
  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: `
12610
+ 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
12611
  <div class="config-container">
12192
12612
 
12193
12613
  <div class="config-form" [class.loading]="isLoadingInitial">
@@ -12448,7 +12868,7 @@ class BarChartConfigDialogComponent {
12448
12868
  </button>
12449
12869
  </div>
12450
12870
  </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"] }] });
12871
+ `, 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
12872
  }
12453
12873
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: BarChartConfigDialogComponent, decorators: [{
12454
12874
  type: Component,
@@ -12731,6 +13151,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
12731
13151
  type: Input
12732
13152
  }], initialQueryName: [{
12733
13153
  type: Input
13154
+ }], initialQueryFamily: [{
13155
+ type: Input
12734
13156
  }], initialChartType: [{
12735
13157
  type: Input
12736
13158
  }], initialCategoryField: [{
@@ -12756,7 +13178,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
12756
13178
  }] } });
12757
13179
 
12758
13180
  class LineChartWidgetComponent {
12759
- executeRuntimeQueryGQL = inject(ExecuteRuntimeQueryDtoGQL);
13181
+ queryExecutor = inject(QueryExecutorService);
13182
+ static SUPPORTED_ROW_TYPES = new Set([
13183
+ 'RtSimpleQueryRow',
13184
+ 'RtAggregationQueryRow',
13185
+ 'RtGroupingAggregationQueryRow',
13186
+ 'StreamDataQueryRow'
13187
+ ]);
12760
13188
  stateService = inject(MeshBoardStateService);
12761
13189
  variableService = inject(MeshBoardVariableService);
12762
13190
  config;
@@ -12868,36 +13296,18 @@ class LineChartWidgetComponent {
12868
13296
  this._error.set(null);
12869
13297
  try {
12870
13298
  const fieldFilter = this.convertFiltersToDto(this.config.filters);
12871
- const result = await firstValueFrom(this.executeRuntimeQueryGQL.fetch({
12872
- variables: {
12873
- rtId: queryDataSource.queryRtId,
12874
- fieldFilter
12875
- }
13299
+ // queryFamily may be undefined for legacy widget configs — the executor
13300
+ // falls back to a one-time lookup by rtId. streamDataArgs is sent
13301
+ // unconditionally because the runtime path ignores it.
13302
+ const streamDataArgs = this.buildStreamDataArgs();
13303
+ const result = await firstValueFrom(this.queryExecutor.execute(queryDataSource.queryFamily, queryDataSource.queryRtId, {
13304
+ fieldFilter: fieldFilter ?? undefined,
13305
+ streamDataArgs
12876
13306
  }).pipe(catchError(err => {
12877
13307
  console.error('Error loading Line Chart data:', err);
12878
13308
  throw err;
12879
13309
  })));
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 ?? ''));
13310
+ const filteredRows = result.rows.filter(row => LineChartWidgetComponent.SUPPORTED_ROW_TYPES.has(row.__typename ?? ''));
12901
13311
  this.processData(filteredRows);
12902
13312
  this._isLoading.set(false);
12903
13313
  }
@@ -12907,6 +13317,13 @@ class LineChartWidgetComponent {
12907
13317
  this._isLoading.set(false);
12908
13318
  }
12909
13319
  }
13320
+ buildStreamDataArgs() {
13321
+ const range = this.stateService.resolveCurrentTimeRange();
13322
+ if (!range) {
13323
+ return undefined;
13324
+ }
13325
+ return { from: range.from, to: range.to };
13326
+ }
12910
13327
  /**
12911
13328
  * Processes query rows into line chart data.
12912
13329
  * Groups by seriesGroupField, orders by categoryField (date), supports multi-axis by unitField.
@@ -12922,15 +13339,11 @@ class LineChartWidgetComponent {
12922
13339
  const allSeriesGroups = new Set();
12923
13340
  const seriesUnitMap = new Map(); // seriesGroup -> unit
12924
13341
  for (const row of filteredRows) {
12925
- const queryRow = row;
12926
- const cells = queryRow.cells?.items ?? [];
12927
13342
  let categoryValue = '';
12928
13343
  let seriesGroupValue = '';
12929
13344
  let numericValue = 0;
12930
13345
  let unitValue = '';
12931
- for (const cell of cells) {
12932
- if (!cell?.attributePath)
12933
- continue;
13346
+ for (const cell of row.cells) {
12934
13347
  if (matchesAttributePath(cell.attributePath, categoryField)) {
12935
13348
  categoryValue = String(cell.value ?? '');
12936
13349
  }
@@ -13226,12 +13639,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
13226
13639
  */
13227
13640
  class LineChartConfigDialogComponent {
13228
13641
  getRuntimeQueryColumnsGQL = inject(GetRuntimeQueryColumnsDtoGQL);
13642
+ queryExecutor = inject(QueryExecutorService);
13229
13643
  stateService = inject(MeshBoardStateService);
13230
13644
  windowRef = inject(WindowRef);
13231
13645
  querySelector;
13232
13646
  // Initial values for editing
13233
13647
  initialQueryRtId;
13234
13648
  initialQueryName;
13649
+ initialQueryFamily;
13235
13650
  initialChartType;
13236
13651
  initialCategoryField;
13237
13652
  initialSeriesGroupField;
@@ -13335,27 +13750,14 @@ class LineChartConfigDialogComponent {
13335
13750
  }
13336
13751
  async loadQueryColumns(queryRtId) {
13337
13752
  this.isLoadingColumns = true;
13753
+ // family may be undefined when the selected query metadata is missing —
13754
+ // fetchColumnsForFamily resolves it via the executor's one-time lookup.
13755
+ const family = queryFamily(this.selectedPersistentQuery?.ckTypeId) ?? this.initialQueryFamily;
13338
13756
  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
- }
13757
+ this.queryColumns = await this.fetchColumnsForFamily(family, queryRtId);
13758
+ const numericTypes = ['INTEGER', 'FLOAT', 'DOUBLE', 'DECIMAL', 'LONG'];
13759
+ this.numericColumns = this.queryColumns.filter(c => numericTypes.includes(c.attributeValueType));
13760
+ this.nonNumericColumns = this.queryColumns.filter(c => !numericTypes.includes(c.attributeValueType));
13359
13761
  }
13360
13762
  catch (error) {
13361
13763
  console.error('Error loading query columns:', error);
@@ -13367,6 +13769,35 @@ class LineChartConfigDialogComponent {
13367
13769
  this.isLoadingColumns = false;
13368
13770
  }
13369
13771
  }
13772
+ /**
13773
+ * Runtime queries use the metadata-only resolver (no aggregation executed);
13774
+ * stream-data queries fall back to executing the query with `first: 1`
13775
+ * because the SD path has no dedicated column-introspection endpoint today.
13776
+ */
13777
+ async fetchColumnsForFamily(family, rtId) {
13778
+ const resolvedFamily = family ?? await this.queryExecutor.resolveFamily(rtId);
13779
+ if (resolvedFamily === 'runtime') {
13780
+ const result = await firstValueFrom(this.getRuntimeQueryColumnsGQL.fetch({
13781
+ variables: { rtId }
13782
+ }));
13783
+ const queryItem = result.data?.runtime?.runtimeQuery?.items?.[0];
13784
+ if (!queryItem)
13785
+ return [];
13786
+ return (queryItem.columns ?? [])
13787
+ .filter((c) => c !== null)
13788
+ .map(c => ({
13789
+ attributePath: c.attributePath ?? '',
13790
+ attributeValueType: c.attributeValueType ?? '',
13791
+ aggregationType: c.aggregationType ?? null
13792
+ }));
13793
+ }
13794
+ const sdResult = await firstValueFrom(this.queryExecutor.executeStreamData(rtId, { first: 1 }));
13795
+ return sdResult.columns.map(c => ({
13796
+ attributePath: c.attributePath,
13797
+ attributeValueType: c.attributeValueType ?? '',
13798
+ aggregationType: c.aggregationType ?? null
13799
+ }));
13800
+ }
13370
13801
  onFiltersChange(updatedFilters) {
13371
13802
  this.filters = updatedFilters;
13372
13803
  }
@@ -13380,11 +13811,13 @@ class LineChartConfigDialogComponent {
13380
13811
  comparisonValue: f.comparisonValue
13381
13812
  }))
13382
13813
  : undefined;
13814
+ const family = queryFamily(this.selectedPersistentQuery.ckTypeId) ?? this.initialQueryFamily ?? undefined;
13383
13815
  const result = {
13384
13816
  ckTypeId: '',
13385
13817
  rtId: '',
13386
13818
  queryRtId: this.selectedPersistentQuery.rtId,
13387
13819
  queryName: this.selectedPersistentQuery.name,
13820
+ queryFamily: family,
13388
13821
  chartType: this.form.chartType,
13389
13822
  categoryField: this.form.categoryField,
13390
13823
  seriesGroupField: this.form.seriesGroupField,
@@ -13408,7 +13841,7 @@ class LineChartConfigDialogComponent {
13408
13841
  this.windowRef.close();
13409
13842
  }
13410
13843
  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: `
13844
+ 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
13845
  <div class="config-container">
13413
13846
 
13414
13847
  <div class="config-form" [class.loading]="isLoadingInitial">
@@ -13611,7 +14044,7 @@ class LineChartConfigDialogComponent {
13611
14044
  </button>
13612
14045
  </div>
13613
14046
  </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"] }] });
14047
+ `, 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
14048
  }
13616
14049
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: LineChartConfigDialogComponent, decorators: [{
13617
14050
  type: Component,
@@ -13836,6 +14269,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
13836
14269
  type: Input
13837
14270
  }], initialQueryName: [{
13838
14271
  type: Input
14272
+ }], initialQueryFamily: [{
14273
+ type: Input
13839
14274
  }], initialChartType: [{
13840
14275
  type: Input
13841
14276
  }], initialCategoryField: [{
@@ -13876,7 +14311,13 @@ function buildGradientRanges(min, max, colors) {
13876
14311
  }));
13877
14312
  }
13878
14313
  class HeatmapWidgetComponent {
13879
- executeRuntimeQueryGQL = inject(ExecuteRuntimeQueryDtoGQL);
14314
+ queryExecutor = inject(QueryExecutorService);
14315
+ static SUPPORTED_ROW_TYPES = new Set([
14316
+ 'RtSimpleQueryRow',
14317
+ 'RtAggregationQueryRow',
14318
+ 'RtGroupingAggregationQueryRow',
14319
+ 'StreamDataQueryRow'
14320
+ ]);
13880
14321
  stateService = inject(MeshBoardStateService);
13881
14322
  variableService = inject(MeshBoardVariableService);
13882
14323
  config;
@@ -13996,36 +14437,18 @@ class HeatmapWidgetComponent {
13996
14437
  this._error.set(null);
13997
14438
  try {
13998
14439
  const fieldFilter = this.convertFiltersToDto(this.config.filters);
13999
- const result = await firstValueFrom(this.executeRuntimeQueryGQL.fetch({
14000
- variables: {
14001
- rtId: queryDataSource.queryRtId,
14002
- fieldFilter
14003
- }
14440
+ // queryFamily may be undefined for legacy widget configs — the executor
14441
+ // falls back to a one-time lookup by rtId. streamDataArgs is sent
14442
+ // unconditionally because the runtime path ignores it.
14443
+ const streamDataArgs = this.buildStreamDataArgs();
14444
+ const result = await firstValueFrom(this.queryExecutor.execute(queryDataSource.queryFamily, queryDataSource.queryRtId, {
14445
+ fieldFilter: fieldFilter ?? undefined,
14446
+ streamDataArgs
14004
14447
  }).pipe(catchError(err => {
14005
14448
  console.error('Error loading Heatmap data:', err);
14006
14449
  throw err;
14007
14450
  })));
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 ?? ''));
14451
+ const filteredRows = result.rows.filter(row => HeatmapWidgetComponent.SUPPORTED_ROW_TYPES.has(row.__typename ?? ''));
14029
14452
  this.processHeatmapData(filteredRows);
14030
14453
  this._isLoading.set(false);
14031
14454
  }
@@ -14035,6 +14458,13 @@ class HeatmapWidgetComponent {
14035
14458
  this._isLoading.set(false);
14036
14459
  }
14037
14460
  }
14461
+ buildStreamDataArgs() {
14462
+ const range = this.stateService.resolveCurrentTimeRange();
14463
+ if (!range) {
14464
+ return undefined;
14465
+ }
14466
+ return { from: range.from, to: range.to };
14467
+ }
14038
14468
  /**
14039
14469
  * Processes query rows into heatmap data.
14040
14470
  * When dateEndField is configured, auto-detects the interval width and shows sub-hour columns.
@@ -14047,14 +14477,10 @@ class HeatmapWidgetComponent {
14047
14477
  const aggregation = this.config.aggregation ?? 'count';
14048
14478
  const parsedRows = [];
14049
14479
  for (const row of filteredRows) {
14050
- const queryRow = row;
14051
- const cells = queryRow.cells?.items ?? [];
14052
14480
  let dateFrom = null;
14053
14481
  let dateTo = null;
14054
14482
  let numericValue = 1; // default for count
14055
- for (const cell of cells) {
14056
- if (!cell?.attributePath)
14057
- continue;
14483
+ for (const cell of row.cells) {
14058
14484
  if (matchesAttributePath(cell.attributePath, dateField)) {
14059
14485
  dateFrom = this.parseDate(cell.value);
14060
14486
  }
@@ -14381,12 +14807,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
14381
14807
 
14382
14808
  class HeatmapConfigDialogComponent {
14383
14809
  getRuntimeQueryColumnsGQL = inject(GetRuntimeQueryColumnsDtoGQL);
14810
+ queryExecutor = inject(QueryExecutorService);
14384
14811
  stateService = inject(MeshBoardStateService);
14385
14812
  windowRef = inject(WindowRef);
14386
14813
  querySelector;
14387
14814
  // Initial values for editing
14388
14815
  initialQueryRtId;
14389
14816
  initialQueryName;
14817
+ initialQueryFamily;
14390
14818
  initialDateField;
14391
14819
  initialDateEndField;
14392
14820
  initialValueField;
@@ -14508,36 +14936,22 @@ class HeatmapConfigDialogComponent {
14508
14936
  }
14509
14937
  async loadQueryColumns(queryRtId) {
14510
14938
  this.isLoadingColumns = true;
14939
+ // family may be undefined when the selected query metadata is missing —
14940
+ // fetchColumnsForFamily resolves it via the executor's one-time lookup.
14941
+ const family = queryFamily(this.selectedPersistentQuery?.ckTypeId) ?? this.initialQueryFamily;
14511
14942
  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
- }
14943
+ this.queryColumns = await this.fetchColumnsForFamily(family, queryRtId);
14944
+ const numericTypes = ['INTEGER', 'FLOAT', 'DOUBLE', 'DECIMAL', 'LONG'];
14945
+ const dateTimeTypes = ['DATE_TIME', 'DATETIME', 'DATE'];
14946
+ this.numericColumns = this.queryColumns.filter(c => numericTypes.includes(c.attributeValueType));
14947
+ this.dateTimeColumns = this.queryColumns.filter(c => dateTimeTypes.includes(c.attributeValueType));
14948
+ // If no explicit datetime columns found, also allow string columns
14949
+ // (datetime values are sometimes returned as strings)
14950
+ if (this.dateTimeColumns.length === 0) {
14951
+ this.dateTimeColumns = this.queryColumns;
14952
+ }
14953
+ if (!this.initialQueryRtId && this.dateTimeColumns.length > 0) {
14954
+ this.form.dateField = this.dateTimeColumns[0].attributePath;
14541
14955
  }
14542
14956
  }
14543
14957
  catch (error) {
@@ -14550,6 +14964,35 @@ class HeatmapConfigDialogComponent {
14550
14964
  this.isLoadingColumns = false;
14551
14965
  }
14552
14966
  }
14967
+ /**
14968
+ * Runtime queries use the metadata-only resolver (no aggregation executed);
14969
+ * stream-data queries fall back to executing the query with `first: 1`
14970
+ * because the SD path has no dedicated column-introspection endpoint today.
14971
+ */
14972
+ async fetchColumnsForFamily(family, rtId) {
14973
+ const resolvedFamily = family ?? await this.queryExecutor.resolveFamily(rtId);
14974
+ if (resolvedFamily === 'runtime') {
14975
+ const result = await firstValueFrom(this.getRuntimeQueryColumnsGQL.fetch({
14976
+ variables: { rtId }
14977
+ }));
14978
+ const queryItem = result.data?.runtime?.runtimeQuery?.items?.[0];
14979
+ if (!queryItem)
14980
+ return [];
14981
+ return (queryItem.columns ?? [])
14982
+ .filter((c) => c !== null)
14983
+ .map(c => ({
14984
+ attributePath: c.attributePath ?? '',
14985
+ attributeValueType: c.attributeValueType ?? '',
14986
+ aggregationType: c.aggregationType ?? null
14987
+ }));
14988
+ }
14989
+ const sdResult = await firstValueFrom(this.queryExecutor.executeStreamData(rtId, { first: 1 }));
14990
+ return sdResult.columns.map(c => ({
14991
+ attributePath: c.attributePath,
14992
+ attributeValueType: c.attributeValueType ?? '',
14993
+ aggregationType: c.aggregationType ?? null
14994
+ }));
14995
+ }
14553
14996
  onFiltersChange(updatedFilters) {
14554
14997
  this.filters = updatedFilters;
14555
14998
  }
@@ -14563,11 +15006,13 @@ class HeatmapConfigDialogComponent {
14563
15006
  comparisonValue: f.comparisonValue
14564
15007
  }))
14565
15008
  : undefined;
15009
+ const family = queryFamily(this.selectedPersistentQuery.ckTypeId) ?? this.initialQueryFamily ?? undefined;
14566
15010
  const result = {
14567
15011
  ckTypeId: '',
14568
15012
  rtId: '',
14569
15013
  queryRtId: this.selectedPersistentQuery.rtId,
14570
15014
  queryName: this.selectedPersistentQuery.name,
15015
+ queryFamily: family,
14571
15016
  dateField: this.form.dateField,
14572
15017
  dateEndField: this.form.dateEndField || undefined,
14573
15018
  valueField: this.form.valueField || undefined,
@@ -14586,7 +15031,7 @@ class HeatmapConfigDialogComponent {
14586
15031
  this.windowRef.close();
14587
15032
  }
14588
15033
  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: `
15034
+ 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
15035
  <div class="config-container">
14591
15036
 
14592
15037
  <div class="config-form" [class.loading]="isLoadingInitial">
@@ -14795,7 +15240,7 @@ class HeatmapConfigDialogComponent {
14795
15240
  </button>
14796
15241
  </div>
14797
15242
  </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"] }] });
15243
+ `, 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
15244
  }
14800
15245
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: HeatmapConfigDialogComponent, decorators: [{
14801
15246
  type: Component,
@@ -15026,6 +15471,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
15026
15471
  type: Input
15027
15472
  }], initialQueryName: [{
15028
15473
  type: Input
15474
+ }], initialQueryFamily: [{
15475
+ type: Input
15029
15476
  }], initialDateField: [{
15030
15477
  type: Input
15031
15478
  }], initialDateEndField: [{
@@ -16603,6 +17050,7 @@ class WidgetGroupConfigDialogComponent {
16603
17050
  ckTypeSelectorService = inject(CkTypeSelectorService);
16604
17051
  attributeSelectorService = inject(AttributeSelectorService);
16605
17052
  getRuntimeQueryColumnsGQL = inject(GetRuntimeQueryColumnsDtoGQL);
17053
+ queryExecutor = inject(QueryExecutorService);
16606
17054
  getCkTypeAvailableQueryColumnsGQL = inject(GetCkTypeAvailableQueryColumnsDtoGQL);
16607
17055
  meshBoardStateService = inject(MeshBoardStateService);
16608
17056
  windowRef = inject(WindowRef);
@@ -16611,6 +17059,7 @@ class WidgetGroupConfigDialogComponent {
16611
17059
  initialDataSourceMode;
16612
17060
  initialQueryRtId;
16613
17061
  initialQueryName;
17062
+ initialQueryFamily;
16614
17063
  initialCkTypeId;
16615
17064
  initialFilters;
16616
17065
  initialMaxItems;
@@ -16767,26 +17216,12 @@ class WidgetGroupConfigDialogComponent {
16767
17216
  }
16768
17217
  async loadQueryColumns(queryRtId) {
16769
17218
  this.isLoadingColumns = true;
17219
+ // family may be undefined when the selected query metadata is missing —
17220
+ // fetchColumnsForFamily resolves it via the executor's one-time lookup.
17221
+ const family = queryFamily(this.selectedPersistentQuery?.ckTypeId) ?? this.initialQueryFamily;
16770
17222
  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
- }
17223
+ this.availableColumns = await this.fetchColumnsForFamily(family, queryRtId);
17224
+ this.filteredColumns.set(this.availableColumns);
16790
17225
  }
16791
17226
  catch (error) {
16792
17227
  console.error('Error loading query columns:', error);
@@ -16797,6 +17232,35 @@ class WidgetGroupConfigDialogComponent {
16797
17232
  this.isLoadingColumns = false;
16798
17233
  }
16799
17234
  }
17235
+ /**
17236
+ * Runtime queries use the metadata-only resolver (no aggregation executed);
17237
+ * stream-data queries fall back to executing the query with `first: 1`
17238
+ * because the SD path has no dedicated column-introspection endpoint today.
17239
+ */
17240
+ async fetchColumnsForFamily(family, rtId) {
17241
+ const resolvedFamily = family ?? await this.queryExecutor.resolveFamily(rtId);
17242
+ if (resolvedFamily === 'runtime') {
17243
+ const result = await firstValueFrom(this.getRuntimeQueryColumnsGQL.fetch({
17244
+ variables: { rtId }
17245
+ }));
17246
+ const queryItem = result.data?.runtime?.runtimeQuery?.items?.[0];
17247
+ if (!queryItem)
17248
+ return [];
17249
+ return (queryItem.columns ?? [])
17250
+ .filter((c) => c !== null)
17251
+ .map(c => ({
17252
+ attributePath: c.attributePath ?? '',
17253
+ attributeValueType: c.attributeValueType ?? '',
17254
+ aggregationType: c.aggregationType ?? null
17255
+ }));
17256
+ }
17257
+ const sdResult = await firstValueFrom(this.queryExecutor.executeStreamData(rtId, { first: 1 }));
17258
+ return sdResult.columns.map(c => ({
17259
+ attributePath: c.attributePath,
17260
+ attributeValueType: c.attributeValueType ?? '',
17261
+ aggregationType: c.aggregationType ?? null
17262
+ }));
17263
+ }
16800
17264
  // ============================================================================
16801
17265
  // CK Type Methods
16802
17266
  // ============================================================================
@@ -16889,11 +17353,15 @@ class WidgetGroupConfigDialogComponent {
16889
17353
  comparisonValue: f.comparisonValue
16890
17354
  }))
16891
17355
  : undefined;
17356
+ const family = this.dataSourceMode === 'persistentQuery' && this.selectedPersistentQuery
17357
+ ? queryFamily(this.selectedPersistentQuery.ckTypeId) ?? this.initialQueryFamily ?? undefined
17358
+ : undefined;
16892
17359
  const result = {
16893
17360
  ckTypeId: this.dataSourceMode === 'ckType' ? (this.selectedCkType?.rtCkTypeId ?? '') : '',
16894
17361
  dataSourceMode: this.dataSourceMode,
16895
17362
  queryRtId: this.dataSourceMode === 'persistentQuery' ? this.selectedPersistentQuery?.rtId : undefined,
16896
17363
  queryName: this.dataSourceMode === 'persistentQuery' ? this.selectedPersistentQuery?.name : undefined,
17364
+ queryFamily: family,
16897
17365
  filters: this.dataSourceMode === 'ckType' ? filtersDto : undefined,
16898
17366
  maxItems: this.form.maxItems,
16899
17367
  childTemplate,
@@ -16931,7 +17399,7 @@ class WidgetGroupConfigDialogComponent {
16931
17399
  this.windowRef.close();
16932
17400
  }
16933
17401
  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: `
17402
+ 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
17403
  <div class="config-container">
16936
17404
 
16937
17405
  <div class="config-form" [class.loading]="isLoadingInitial">
@@ -17251,7 +17719,7 @@ class WidgetGroupConfigDialogComponent {
17251
17719
  </button>
17252
17720
  </div>
17253
17721
  </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"] }] });
17722
+ `, 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
17723
  }
17256
17724
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: WidgetGroupConfigDialogComponent, decorators: [{
17257
17725
  type: Component,
@@ -17595,6 +18063,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
17595
18063
  type: Input
17596
18064
  }], initialQueryName: [{
17597
18065
  type: Input
18066
+ }], initialQueryFamily: [{
18067
+ type: Input
17598
18068
  }], initialCkTypeId: [{
17599
18069
  type: Input
17600
18070
  }], initialFilters: [{
@@ -24996,11 +25466,16 @@ function parseConfig(data) {
24996
25466
  */
24997
25467
  function buildDataSourceFromPersisted(data, config) {
24998
25468
  if (data.dataSourceType === 'systemQuery' || data.dataSourceType === 'persistentQuery') {
24999
- return {
25469
+ const persisted = {
25000
25470
  type: 'persistentQuery',
25001
25471
  queryRtId: data.dataSourceRtId ?? config['queryRtId'] ?? '',
25002
25472
  queryName: config['queryName']
25003
25473
  };
25474
+ const persistedFamily = config['queryFamily'];
25475
+ if (persistedFamily === 'runtime' || persistedFamily === 'streamData') {
25476
+ persisted.queryFamily = persistedFamily;
25477
+ }
25478
+ return persisted;
25004
25479
  }
25005
25480
  if (data.dataSourceType === 'static') {
25006
25481
  return { type: 'static' };
@@ -25120,6 +25595,7 @@ function registerDefaultWidgets(registry) {
25120
25595
  initialDataSourceType: dataSourceType,
25121
25596
  initialQueryRtId: isPersistentQuery ? kpiWidget.dataSource.queryRtId : undefined,
25122
25597
  initialQueryName: isPersistentQuery ? kpiWidget.dataSource.queryName : undefined,
25598
+ initialQueryFamily: isPersistentQuery ? kpiWidget.dataSource.queryFamily : undefined,
25123
25599
  initialQueryMode: kpiWidget.queryMode,
25124
25600
  initialQueryValueField: kpiWidget.queryValueField,
25125
25601
  initialQueryCategoryField: kpiWidget.queryCategoryField,
@@ -25164,7 +25640,8 @@ function registerDefaultWidgets(registry) {
25164
25640
  const dataSource = {
25165
25641
  type: 'persistentQuery',
25166
25642
  queryRtId: result.queryRtId,
25167
- queryName: result.queryName
25643
+ queryName: result.queryName,
25644
+ queryFamily: result.queryFamily
25168
25645
  };
25169
25646
  return {
25170
25647
  ...widget,
@@ -25235,7 +25712,10 @@ function registerDefaultWidgets(registry) {
25235
25712
  filters: widget.filters,
25236
25713
  ...(isPersistentQuery && {
25237
25714
  queryName: widget.dataSource.queryName,
25238
- queryRtId: widget.dataSource.queryRtId
25715
+ queryRtId: widget.dataSource.queryRtId,
25716
+ ...(widget.dataSource.queryFamily && {
25717
+ queryFamily: widget.dataSource.queryFamily
25718
+ })
25239
25719
  })
25240
25720
  }
25241
25721
  };
@@ -25399,7 +25879,8 @@ function registerDefaultWidgets(registry) {
25399
25879
  initialPageSize: tableWidget.pageSize,
25400
25880
  initialSortable: tableWidget.sortable,
25401
25881
  initialQueryRtId: isPersistentQuery ? dataSource.queryRtId : undefined,
25402
- initialQueryName: isPersistentQuery ? dataSource.queryName : undefined
25882
+ initialQueryName: isPersistentQuery ? dataSource.queryName : undefined,
25883
+ initialQueryFamily: isPersistentQuery ? dataSource.queryFamily : undefined
25403
25884
  };
25404
25885
  },
25405
25886
  applyConfigResult: (widget, result) => {
@@ -25408,7 +25889,8 @@ function registerDefaultWidgets(registry) {
25408
25889
  const dataSource = {
25409
25890
  type: 'persistentQuery',
25410
25891
  queryRtId: result.queryRtId,
25411
- queryName: result.queryName
25892
+ queryName: result.queryName,
25893
+ queryFamily: result.queryFamily
25412
25894
  };
25413
25895
  // Convert filters from DTO to widget format
25414
25896
  const filters = result.filters?.map(f => ({
@@ -25462,11 +25944,12 @@ function registerDefaultWidgets(registry) {
25462
25944
  // SOLID: Serialization for persistence
25463
25945
  toPersistedConfig: (widget) => {
25464
25946
  const isPersistentQuery = widget.dataSource.type === 'persistentQuery';
25947
+ const queryDataSource = isPersistentQuery ? widget.dataSource : null;
25465
25948
  return {
25466
25949
  dataSourceType: isPersistentQuery ? 'persistentQuery' : 'runtimeEntity',
25467
25950
  dataSourceCkTypeId: widget.dataSource.type === 'runtimeEntity' ? widget.dataSource.ckTypeId : undefined,
25468
25951
  dataSourceRtId: isPersistentQuery
25469
- ? widget.dataSource.queryRtId
25952
+ ? queryDataSource.queryRtId
25470
25953
  : (widget.dataSource.type === 'runtimeEntity' ? widget.dataSource.rtId : undefined),
25471
25954
  config: {
25472
25955
  columns: widget.columns,
@@ -25474,9 +25957,10 @@ function registerDefaultWidgets(registry) {
25474
25957
  filters: widget.filters,
25475
25958
  pageSize: widget.pageSize,
25476
25959
  sortable: widget.sortable,
25477
- ...(isPersistentQuery && {
25478
- queryName: widget.dataSource.queryName,
25479
- queryRtId: widget.dataSource.queryRtId
25960
+ ...(queryDataSource && {
25961
+ queryName: queryDataSource.queryName,
25962
+ queryRtId: queryDataSource.queryRtId,
25963
+ ...(queryDataSource.queryFamily && { queryFamily: queryDataSource.queryFamily })
25480
25964
  })
25481
25965
  }
25482
25966
  };
@@ -25521,6 +26005,7 @@ function registerDefaultWidgets(registry) {
25521
26005
  initialDataSourceType: isPersistentQuery ? 'persistentQuery' : 'runtimeEntity',
25522
26006
  initialQueryRtId: isPersistentQuery ? gaugeWidget.dataSource.queryRtId : undefined,
25523
26007
  initialQueryName: isPersistentQuery ? gaugeWidget.dataSource.queryName : undefined,
26008
+ initialQueryFamily: isPersistentQuery ? gaugeWidget.dataSource.queryFamily : undefined,
25524
26009
  initialQueryMode: gaugeWidget.queryMode,
25525
26010
  initialQueryValueField: gaugeWidget.queryValueField,
25526
26011
  initialQueryCategoryField: gaugeWidget.queryCategoryField,
@@ -25550,7 +26035,8 @@ function registerDefaultWidgets(registry) {
25550
26035
  const dataSource = {
25551
26036
  type: 'persistentQuery',
25552
26037
  queryRtId: result.queryRtId,
25553
- queryName: result.queryName
26038
+ queryName: result.queryName,
26039
+ queryFamily: result.queryFamily
25554
26040
  };
25555
26041
  return {
25556
26042
  ...widget,
@@ -25632,7 +26118,10 @@ function registerDefaultWidgets(registry) {
25632
26118
  filters: widget.filters,
25633
26119
  ...(isPersistentQuery && {
25634
26120
  queryName: widget.dataSource.queryName,
25635
- queryRtId: widget.dataSource.queryRtId
26121
+ queryRtId: widget.dataSource.queryRtId,
26122
+ ...(widget.dataSource.queryFamily && {
26123
+ queryFamily: widget.dataSource.queryFamily
26124
+ })
25636
26125
  })
25637
26126
  }
25638
26127
  };
@@ -25701,6 +26190,7 @@ function registerDefaultWidgets(registry) {
25701
26190
  initialDataSourceType: dataSource.type,
25702
26191
  initialQueryRtId: isPersistentQuery ? dataSource.queryRtId : undefined,
25703
26192
  initialQueryName: isPersistentQuery ? dataSource.queryName : undefined,
26193
+ initialQueryFamily: isPersistentQuery ? dataSource.queryFamily : undefined,
25704
26194
  initialCkQueryTarget: isCkQuery ? dataSource.queryTarget : undefined,
25705
26195
  initialCkGroupBy: isCkQuery ? dataSource.groupBy : undefined,
25706
26196
  initialChartType: pieWidget.chartType,
@@ -25733,7 +26223,8 @@ function registerDefaultWidgets(registry) {
25733
26223
  dataSource = {
25734
26224
  type: 'persistentQuery',
25735
26225
  queryRtId: result.queryRtId ?? '',
25736
- queryName: result.queryName
26226
+ queryName: result.queryName,
26227
+ queryFamily: result.queryFamily
25737
26228
  };
25738
26229
  }
25739
26230
  return {
@@ -25795,6 +26286,9 @@ function registerDefaultWidgets(registry) {
25795
26286
  legendPosition: widget.legendPosition,
25796
26287
  queryName: dataSource.queryName,
25797
26288
  queryRtId: dataSource.queryRtId,
26289
+ ...(dataSource.queryFamily && {
26290
+ queryFamily: dataSource.queryFamily
26291
+ }),
25798
26292
  filters: widget.filters
25799
26293
  }
25800
26294
  };
@@ -25856,6 +26350,7 @@ function registerDefaultWidgets(registry) {
25856
26350
  return {
25857
26351
  initialQueryRtId: isPersistentQuery ? dataSource.queryRtId : undefined,
25858
26352
  initialQueryName: isPersistentQuery ? dataSource.queryName : undefined,
26353
+ initialQueryFamily: isPersistentQuery ? dataSource.queryFamily : undefined,
25859
26354
  initialChartType: barWidget.chartType,
25860
26355
  initialCategoryField: barWidget.categoryField,
25861
26356
  initialSeries: barWidget.series,
@@ -25873,7 +26368,8 @@ function registerDefaultWidgets(registry) {
25873
26368
  const dataSource = {
25874
26369
  type: 'persistentQuery',
25875
26370
  queryRtId: result.queryRtId,
25876
- queryName: result.queryName
26371
+ queryName: result.queryName,
26372
+ queryFamily: result.queryFamily
25877
26373
  };
25878
26374
  // Convert filters from DTO to widget format
25879
26375
  const filters = result.filters?.map(f => ({
@@ -25929,6 +26425,9 @@ function registerDefaultWidgets(registry) {
25929
26425
  defaultBarColor: widget.defaultBarColor,
25930
26426
  queryName: widget.dataSource.queryName,
25931
26427
  queryRtId: widget.dataSource.queryRtId,
26428
+ ...(widget.dataSource.queryFamily && {
26429
+ queryFamily: widget.dataSource.queryFamily
26430
+ }),
25932
26431
  filters: widget.filters
25933
26432
  }
25934
26433
  }),
@@ -25973,6 +26472,7 @@ function registerDefaultWidgets(registry) {
25973
26472
  return {
25974
26473
  initialQueryRtId: isPersistentQuery ? dataSource.queryRtId : undefined,
25975
26474
  initialQueryName: isPersistentQuery ? dataSource.queryName : undefined,
26475
+ initialQueryFamily: isPersistentQuery ? dataSource.queryFamily : undefined,
25976
26476
  initialChartType: lineWidget.chartType,
25977
26477
  initialCategoryField: lineWidget.categoryField,
25978
26478
  initialSeriesGroupField: lineWidget.seriesGroupField,
@@ -25989,7 +26489,8 @@ function registerDefaultWidgets(registry) {
25989
26489
  const dataSource = {
25990
26490
  type: 'persistentQuery',
25991
26491
  queryRtId: result.queryRtId,
25992
- queryName: result.queryName
26492
+ queryName: result.queryName,
26493
+ queryFamily: result.queryFamily
25993
26494
  };
25994
26495
  const filters = result.filters?.map(f => ({
25995
26496
  attributePath: f.attributePath,
@@ -26040,6 +26541,9 @@ function registerDefaultWidgets(registry) {
26040
26541
  referenceLines: widget.referenceLines,
26041
26542
  queryName: widget.dataSource.queryName,
26042
26543
  queryRtId: widget.dataSource.queryRtId,
26544
+ ...(widget.dataSource.queryFamily && {
26545
+ queryFamily: widget.dataSource.queryFamily
26546
+ }),
26043
26547
  filters: widget.filters
26044
26548
  }
26045
26549
  }),
@@ -26316,6 +26820,7 @@ function registerDefaultWidgets(registry) {
26316
26820
  initialDataSourceMode: hasQuery ? 'persistentQuery' : (hasCkType ? 'ckType' : 'persistentQuery'),
26317
26821
  initialQueryRtId: hasQuery ? dataSource.queryRtId : undefined,
26318
26822
  initialQueryName: hasQuery ? dataSource.queryName : undefined,
26823
+ initialQueryFamily: hasQuery ? dataSource.queryFamily : undefined,
26319
26824
  initialCkTypeId: hasCkType ? dataSource.ckTypeId : undefined,
26320
26825
  initialFilters: hasCkType ? dataSource.filters : undefined,
26321
26826
  initialMaxItems: dataSource.type === 'repeaterQuery' ? dataSource.maxItems : undefined,
@@ -26334,6 +26839,7 @@ function registerDefaultWidgets(registry) {
26334
26839
  type: 'repeaterQuery',
26335
26840
  queryRtId: result.dataSourceMode === 'persistentQuery' ? result.queryRtId : undefined,
26336
26841
  queryName: result.dataSourceMode === 'persistentQuery' ? result.queryName : undefined,
26842
+ queryFamily: result.dataSourceMode === 'persistentQuery' ? result.queryFamily : undefined,
26337
26843
  ckTypeId: useCkType ? result.ckTypeId : undefined,
26338
26844
  filters: useCkType && result.filters ? result.filters.map(f => ({
26339
26845
  attributePath: f.attributePath,
@@ -26384,6 +26890,7 @@ function registerDefaultWidgets(registry) {
26384
26890
  dataSourceCkTypeId: !hasQuery ? dataSource.ckTypeId : undefined,
26385
26891
  config: {
26386
26892
  queryName: dataSource.queryName,
26893
+ ...(dataSource.queryFamily && { queryFamily: dataSource.queryFamily }),
26387
26894
  maxItems: dataSource.maxItems,
26388
26895
  filters: dataSource.filters,
26389
26896
  childTemplate: widget.childTemplate,
@@ -26399,10 +26906,14 @@ function registerDefaultWidgets(registry) {
26399
26906
  fromPersistedConfig: (data, base) => {
26400
26907
  const config = parseConfig(data);
26401
26908
  const hasQuery = !!data.dataSourceRtId;
26909
+ const persistedFamily = config['queryFamily'];
26402
26910
  const dataSource = {
26403
26911
  type: 'repeaterQuery',
26404
26912
  queryRtId: hasQuery ? data.dataSourceRtId ?? undefined : undefined,
26405
26913
  queryName: hasQuery ? config['queryName'] : undefined,
26914
+ queryFamily: hasQuery && (persistedFamily === 'runtime' || persistedFamily === 'streamData')
26915
+ ? persistedFamily
26916
+ : undefined,
26406
26917
  ckTypeId: !hasQuery ? data.dataSourceCkTypeId ?? undefined : undefined,
26407
26918
  filters: !hasQuery ? config['filters'] : undefined,
26408
26919
  maxItems: config['maxItems']
@@ -26505,6 +27016,7 @@ function registerDefaultWidgets(registry) {
26505
27016
  return {
26506
27017
  initialQueryRtId: isPersistentQuery ? dataSource.queryRtId : undefined,
26507
27018
  initialQueryName: isPersistentQuery ? dataSource.queryName : undefined,
27019
+ initialQueryFamily: isPersistentQuery ? dataSource.queryFamily : undefined,
26508
27020
  initialDateField: heatmapWidget.dateField,
26509
27021
  initialDateEndField: heatmapWidget.dateEndField,
26510
27022
  initialValueField: heatmapWidget.valueField,
@@ -26522,7 +27034,8 @@ function registerDefaultWidgets(registry) {
26522
27034
  const dataSource = {
26523
27035
  type: 'persistentQuery',
26524
27036
  queryRtId: result.queryRtId,
26525
- queryName: result.queryName
27037
+ queryName: result.queryName,
27038
+ queryFamily: result.queryFamily
26526
27039
  };
26527
27040
  const filters = result.filters?.map(f => ({
26528
27041
  attributePath: f.attributePath,
@@ -26575,6 +27088,9 @@ function registerDefaultWidgets(registry) {
26575
27088
  valueMultiplier: widget.valueMultiplier,
26576
27089
  queryName: widget.dataSource.queryName,
26577
27090
  queryRtId: widget.dataSource.queryRtId,
27091
+ ...(widget.dataSource.queryFamily && {
27092
+ queryFamily: widget.dataSource.queryFamily
27093
+ }),
26578
27094
  filters: widget.filters
26579
27095
  }
26580
27096
  }),
@@ -27534,7 +28050,8 @@ class MeshBoardSettingsResult {
27534
28050
  timeFilter;
27535
28051
  rtWellKnownName;
27536
28052
  entitySelectors;
27537
- constructor(name, description, columns, rowHeight, gap, variables, timeFilter, rtWellKnownName, entitySelectors) {
28053
+ autoRefreshSeconds;
28054
+ constructor(name, description, columns, rowHeight, gap, variables, timeFilter, rtWellKnownName, entitySelectors, autoRefreshSeconds) {
27538
28055
  this.name = name;
27539
28056
  this.description = description;
27540
28057
  this.columns = columns;
@@ -27544,6 +28061,7 @@ class MeshBoardSettingsResult {
27544
28061
  this.timeFilter = timeFilter;
27545
28062
  this.rtWellKnownName = rtWellKnownName;
27546
28063
  this.entitySelectors = entitySelectors;
28064
+ this.autoRefreshSeconds = autoRefreshSeconds;
27547
28065
  }
27548
28066
  }
27549
28067
  /**
@@ -27559,6 +28077,12 @@ class MeshBoardSettingsDialogComponent {
27559
28077
  columns = 6;
27560
28078
  rowHeight = 200;
27561
28079
  gap = 16;
28080
+ /**
28081
+ * Auto-refresh interval in seconds. `0` disables auto-refresh and is the default.
28082
+ * When > 0 the MeshBoard view re-polls all widgets at this interval while the
28083
+ * tab is visible.
28084
+ */
28085
+ autoRefreshSeconds = 0;
27562
28086
  variables = [];
27563
28087
  entitySelectors = [];
27564
28088
  entitySelectorEditing = false;
@@ -27576,7 +28100,8 @@ class MeshBoardSettingsDialogComponent {
27576
28100
  return (this.name.trim().length > 0 &&
27577
28101
  this.columns >= 1 && this.columns <= 12 &&
27578
28102
  this.rowHeight >= 100 && this.rowHeight <= 1000 &&
27579
- this.gap >= 0 && this.gap <= 100);
28103
+ this.gap >= 0 && this.gap <= 100 &&
28104
+ this.autoRefreshSeconds >= 0 && this.autoRefreshSeconds <= 3600);
27580
28105
  }
27581
28106
  /**
27582
28107
  * Sets the initial values for the form fields.
@@ -27588,6 +28113,7 @@ class MeshBoardSettingsDialogComponent {
27588
28113
  this.columns = settings.columns;
27589
28114
  this.rowHeight = settings.rowHeight;
27590
28115
  this.gap = settings.gap;
28116
+ this.autoRefreshSeconds = settings.autoRefreshSeconds ?? 0;
27591
28117
  this.variables = settings.variables ? [...settings.variables] : [];
27592
28118
  this.entitySelectors = settings.entitySelectors ? settings.entitySelectors.map(es => ({ ...es })) : [];
27593
28119
  this.timeFilterEnabled = settings.timeFilter?.enabled ?? false;
@@ -27629,7 +28155,7 @@ class MeshBoardSettingsDialogComponent {
27629
28155
  enabled: this.timeFilterEnabled,
27630
28156
  defaultSelection: this.timeFilterEnabled ? this.defaultSelection : undefined
27631
28157
  };
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);
28158
+ 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
28159
  this.windowRef.close(result);
27634
28160
  }
27635
28161
  /**
@@ -27639,7 +28165,7 @@ class MeshBoardSettingsDialogComponent {
27639
28165
  this.windowRef.close();
27640
28166
  }
27641
28167
  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"] }] });
28168
+ 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
28169
  }
27644
28170
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: MeshBoardSettingsDialogComponent, decorators: [{
27645
28171
  type: Component,
@@ -27655,7 +28181,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
27655
28181
  VariablesEditorComponent,
27656
28182
  EntitySelectorEditorComponent,
27657
28183
  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"] }]
28184
+ ], 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
28185
  }] });
27660
28186
 
27661
28187
  /**
@@ -28520,6 +29046,10 @@ class MeshBoardViewComponent {
28520
29046
  // Config dialog state
28521
29047
  configDialogSubscription = null;
28522
29048
  navigationSubscription = null;
29049
+ // Auto-refresh polling
29050
+ autoRefreshTimerId = null;
29051
+ autoRefreshActiveSeconds = 0;
29052
+ visibilityListener = () => this.evaluateAutoRefresh();
28523
29053
  // State signals
28524
29054
  config = this.stateService.meshBoardConfig;
28525
29055
  isEditMode = this.editModeService.isEditMode;
@@ -28601,6 +29131,16 @@ class MeshBoardViewComponent {
28601
29131
  }
28602
29132
  }
28603
29133
  });
29134
+ // Effect to re-evaluate the auto-refresh timer whenever the MeshBoard
29135
+ // config changes (e.g. user edits autoRefreshSeconds in settings, or a
29136
+ // different board is loaded). Re-reads on the same interval skip the
29137
+ // restart inside evaluateAutoRefresh().
29138
+ effect(() => {
29139
+ // Touch the signals we depend on so Angular re-fires the effect.
29140
+ const seconds = this.config().autoRefreshSeconds ?? 0;
29141
+ void seconds;
29142
+ this.evaluateAutoRefresh();
29143
+ });
28604
29144
  // Update breadcrumb after each navigation (BreadCrumbService recreates items on NavigationEnd)
28605
29145
  if (this.breadCrumbService) {
28606
29146
  this.navigationSubscription = this.router.events
@@ -28687,6 +29227,13 @@ class MeshBoardViewComponent {
28687
29227
  this.initialLoadComplete = true;
28688
29228
  // Update breadcrumb with MeshBoard name
28689
29229
  this.updateBreadcrumb();
29230
+ // Wire visibility-aware auto-refresh. The config-watching effect already
29231
+ // re-evaluates on signal changes; this listener handles the tab going
29232
+ // background → foreground without a config change.
29233
+ if (typeof document !== 'undefined') {
29234
+ document.addEventListener('visibilitychange', this.visibilityListener);
29235
+ }
29236
+ this.evaluateAutoRefresh();
28690
29237
  this._isInitialized.set(true);
28691
29238
  }
28692
29239
  catch (err) {
@@ -28717,6 +29264,41 @@ class MeshBoardViewComponent {
28717
29264
  ngOnDestroy() {
28718
29265
  this.closeConfigDialog();
28719
29266
  this.navigationSubscription?.unsubscribe();
29267
+ this.stopAutoRefresh();
29268
+ if (typeof document !== 'undefined') {
29269
+ document.removeEventListener('visibilitychange', this.visibilityListener);
29270
+ }
29271
+ }
29272
+ /**
29273
+ * Starts, restarts, or stops the auto-refresh timer based on the MeshBoard
29274
+ * config and document visibility. Called on init, whenever config changes,
29275
+ * and whenever the tab visibility changes.
29276
+ *
29277
+ * Pause-on-hidden saves bandwidth and avoids Apollo cache thrashing when
29278
+ * the user has the tab in the background.
29279
+ */
29280
+ evaluateAutoRefresh() {
29281
+ const seconds = this.config().autoRefreshSeconds ?? 0;
29282
+ const tabVisible = typeof document === 'undefined' || !document.hidden;
29283
+ if (seconds <= 0 || !tabVisible) {
29284
+ this.stopAutoRefresh();
29285
+ return;
29286
+ }
29287
+ if (this.autoRefreshTimerId !== null && this.autoRefreshActiveSeconds === seconds) {
29288
+ return; // already running with the same interval
29289
+ }
29290
+ this.stopAutoRefresh();
29291
+ this.autoRefreshActiveSeconds = seconds;
29292
+ this.autoRefreshTimerId = setInterval(() => {
29293
+ void this.refresh();
29294
+ }, seconds * 1000);
29295
+ }
29296
+ stopAutoRefresh() {
29297
+ if (this.autoRefreshTimerId !== null) {
29298
+ clearInterval(this.autoRefreshTimerId);
29299
+ this.autoRefreshTimerId = null;
29300
+ }
29301
+ this.autoRefreshActiveSeconds = 0;
28720
29302
  }
28721
29303
  /**
28722
29304
  * Preloads data for all widgets to improve initial rendering performance.
@@ -29633,5 +30215,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
29633
30215
  * Generated bundle index. Do not edit.
29634
30216
  */
29635
30217
 
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 };
30218
+ 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
30219
  //# sourceMappingURL=meshmakers-octo-meshboard.mjs.map