@meshmakers/octo-meshboard 3.5.490 → 3.5.500

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.
@@ -4,9 +4,9 @@ import * as i0 from '@angular/core';
4
4
  import { Injectable, inject, EventEmitter, forwardRef, Output, Input, ViewChild, ChangeDetectionStrategy, Component, Injector, EnvironmentInjector, ApplicationRef, signal, computed, effect, untracked, Directive, InjectionToken, isSignal, NgZone, DestroyRef, ElementRef, input, createEnvironmentInjector, ViewContainerRef, makeEnvironmentProviders, provideAppInitializer, output, viewChild } from '@angular/core';
5
5
  import * as i1$1 from '@angular/forms';
6
6
  import { NG_VALUE_ACCESSOR, FormsModule } from '@angular/forms';
7
- import { firstValueFrom, from, map, Observable, of, switchMap as switchMap$1, catchError, interval, filter, combineLatest, defer, EMPTY } from 'rxjs';
7
+ import { firstValueFrom, from, map, Observable, of, switchMap as switchMap$1, catchError, NEVER, interval, filter, combineLatest, defer, EMPTY } from 'rxjs';
8
8
  import * as i1$8 from '@meshmakers/shared-ui';
9
- import { EntitySelectInputComponent, WindowStateService, TimeRangeUtils, MmDialogActionsDirective, ListViewComponent, FetchResultTyped, DataSourceBase, MM_ACTION_ICONS, TimeRangePickerComponent, ConfirmationService, ImportStrategyDialogService, HAS_UNSAVED_CHANGES, UnsavedChangesDirective } from '@meshmakers/shared-ui';
9
+ import { EntitySelectInputComponent, WindowStateService, TimeRangeUtils, MmDialogActionsDirective, ListViewComponent, FetchResultTyped, DataSourceBase, MM_ACTION_ICONS, ConfirmationService, 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';
@@ -492,6 +492,24 @@ function queryFamily(queryCkTypeId) {
492
492
  return classifyQuery(queryCkTypeId)?.family ?? null;
493
493
  }
494
494
 
495
+ /**
496
+ * The result a widget renders instead of executing its query when its scope is
497
+ * `empty` (AB#7721). Shape-compatible with a real empty response, so each
498
+ * widget's existing no-data rendering applies unchanged.
499
+ */
500
+ function emptyQueryExecutionResult(family) {
501
+ return {
502
+ family: family ?? 'streamData',
503
+ queryRtId: null,
504
+ associatedCkTypeId: null,
505
+ columns: [],
506
+ rows: [],
507
+ totalCount: 0,
508
+ hasNextPage: false,
509
+ endCursor: null
510
+ };
511
+ }
512
+
495
513
  const GetSystemPersistentQueriesDocumentDto = gql `
496
514
  query getSystemPersistentQueries($after: String, $first: Int, $searchFilter: SearchFilter, $fieldFilters: [FieldFilter], $sort: [Sort]) {
497
515
  runtime {
@@ -2181,7 +2199,10 @@ class MeshBoardPersistenceService {
2181
2199
  attributeMappings: es.attributeMappings,
2182
2200
  showInToolbar: es.showInToolbar,
2183
2201
  defaultRtId: es.defaultRtId,
2184
- childScope: es.childScope
2202
+ childScope: es.childScope,
2203
+ // AB#7722 — omitted (undefined → dropped by JSON.stringify) for
2204
+ // selectors that follow nobody, so older boards encode unchanged.
2205
+ followsSelectorId: es.followsSelectorId || undefined
2185
2206
  }));
2186
2207
  }
2187
2208
  const dataJson = JSON.stringify(data);
@@ -2425,13 +2446,15 @@ class MeshBoardStateService {
2425
2446
  _variableResolutionErrors = signal([], /* @ts-ignore */
2426
2447
  ...(ngDevMode ? [{ debugName: "_variableResolutionErrors" }] : /* istanbul ignore next */ []));
2427
2448
  /**
2428
- * Transient cache of the source rtIds resolved from each entity selector's
2429
- * current selection (keyed by selector id). Populated by the view component
2430
- * when a selection resolves (one-hop childScope traversal, or the picked
2431
- * entity's own rtId). Not persisted — derived fresh from each selection.
2449
+ * Transient scope state of each entity selector's current selection (keyed
2450
+ * by selector id): `pending` while the view resolves the pick, `resolved`
2451
+ * with the source rtIds (one-hop childScope traversal, or the picked entity's
2452
+ * own rtId; possibly none), or `failed`. No entry = no active pick. Not
2453
+ * persisted — derived fresh from each selection, cleared on board switch
2454
+ * (AB#7721).
2432
2455
  */
2433
- _entitySelectorRtIds = signal({}, /* @ts-ignore */
2434
- ...(ngDevMode ? [{ debugName: "_entitySelectorRtIds" }] : /* istanbul ignore next */ []));
2456
+ _entitySelectorScopes = signal({}, /* @ts-ignore */
2457
+ ...(ngDevMode ? [{ debugName: "_entitySelectorScopes" }] : /* istanbul ignore next */ []));
2435
2458
  /**
2436
2459
  * Transient variables published by widgets (KPI output variables, AB#5364).
2437
2460
  * Kept apart from `_meshBoardConfig` so publishing triggers no persistence and
@@ -2565,6 +2588,7 @@ class MeshBoardStateService {
2565
2588
  console.warn(`Resolved ${movedWidgets.length} overlapping widget(s)`);
2566
2589
  }
2567
2590
  this._widgetVariables.set([]);
2591
+ this._entitySelectorScopes.set({});
2568
2592
  this._hiddenForViewer.set(new Set());
2569
2593
  this._widgetContentHeights.set(new Map());
2570
2594
  this._loadedTenantId.set(tenantId);
@@ -3104,21 +3128,41 @@ class MeshBoardStateService {
3104
3128
  }
3105
3129
  return mode;
3106
3130
  }
3131
+ /**
3132
+ * Marks an entity selector's scope as being resolved (AB#7721). Bound
3133
+ * stream-data widgets show their loading state and send no query until the
3134
+ * scope is resolved, failed or cleared — so they never flash rows of the
3135
+ * query's persisted (unscoped) scope while a pick is resolving.
3136
+ */
3137
+ setEntitySelectorScopePending(selectorId) {
3138
+ this.setEntitySelectorScope(selectorId, { status: 'pending' });
3139
+ }
3107
3140
  /**
3108
3141
  * Stores the source rtIds resolved from an entity selector's current
3109
3142
  * selection. Called by the view component after a selection resolves (the
3110
3143
  * one-hop childScope traversal, or the picked entity's own rtId when the
3111
- * selector has no childScope). Transient — not persisted.
3144
+ * selector has no childScope). An empty array is a resolved EMPTY scope:
3145
+ * bound widgets show no data and send no query (AB#7721). Transient — not
3146
+ * persisted.
3112
3147
  */
3113
3148
  setEntitySelectorRtIds(selectorId, rtIds) {
3114
- this._entitySelectorRtIds.update(map => ({ ...map, [selectorId]: rtIds }));
3149
+ this.setEntitySelectorScope(selectorId, { status: 'resolved', rtIds: [...rtIds] });
3150
+ }
3151
+ /**
3152
+ * Marks an entity selector's scope resolution as failed (e.g. the association
3153
+ * fetch threw). Bound widgets treat it like an empty scope — no data, no
3154
+ * query — and the view tells the user the scope could not be resolved
3155
+ * (AB#7721).
3156
+ */
3157
+ setEntitySelectorScopeFailed(selectorId) {
3158
+ this.setEntitySelectorScope(selectorId, { status: 'failed' });
3115
3159
  }
3116
3160
  /**
3117
- * Clears the cached source rtIds for an entity selector (e.g. when its
3118
- * selection is cleared).
3161
+ * Clears the scope state for an entity selector (its selection was cleared).
3162
+ * Bound widgets then query with the persisted scope again.
3119
3163
  */
3120
3164
  clearEntitySelectorRtIds(selectorId) {
3121
- this._entitySelectorRtIds.update(map => {
3165
+ this._entitySelectorScopes.update(map => {
3122
3166
  if (!(selectorId in map)) {
3123
3167
  return map;
3124
3168
  }
@@ -3127,18 +3171,65 @@ class MeshBoardStateService {
3127
3171
  return next;
3128
3172
  });
3129
3173
  }
3174
+ /** The raw scope state of a selector, or `undefined` when it has no active pick. */
3175
+ getEntitySelectorScopeState(selectorId) {
3176
+ return this._entitySelectorScopes()[selectorId];
3177
+ }
3130
3178
  /**
3131
- * Resolves the stream-data `rtIds` scope for a persistent-query widget bound
3132
- * to an entity selector. Returns the source rtIds cached from the selector's
3133
- * current selection, or `undefined` when the selector id is absent, unknown,
3134
- * or has no active selection (so the query keeps its persisted scope).
3179
+ * Ids of the selectors whose scope resolution failed (for the view's
3180
+ * "could not resolve the scope" notice, AB#7721).
3135
3181
  */
3136
- resolveStreamDataRtIds(entitySelectorId) {
3137
- if (!entitySelectorId) {
3138
- return undefined;
3182
+ failedEntitySelectorScopeIds = computed(() => Object.entries(this._entitySelectorScopes())
3183
+ .filter(([, state]) => state.status === 'failed')
3184
+ .map(([id]) => id), /* @ts-ignore */
3185
+ ...(ngDevMode ? [{ debugName: "failedEntitySelectorScopeIds" }] : /* istanbul ignore next */ []));
3186
+ /**
3187
+ * Decides how a widget bound to an entity selector runs its stream-data query
3188
+ * (AB#7721):
3189
+ *
3190
+ * - `unscoped` — no binding, a runtime query (`family === 'runtime'`, the
3191
+ * binding is ignored there), or no active pick: keep the persisted scope.
3192
+ * - `pending` — the pick is still resolving: show loading, send no query.
3193
+ * - `scoped` — query with the resolved source rtIds.
3194
+ * - `empty` — the pick has nothing in scope or the lookup failed: show the
3195
+ * no-data state and send no query. Never fall back to the persisted scope.
3196
+ *
3197
+ * A legacy config without `queryFamily` is treated as stream data.
3198
+ */
3199
+ resolveStreamDataScope(entitySelectorId, family) {
3200
+ if (!entitySelectorId || family === 'runtime') {
3201
+ return { kind: 'unscoped' };
3139
3202
  }
3140
- const rtIds = this._entitySelectorRtIds()[entitySelectorId];
3141
- return rtIds && rtIds.length > 0 ? rtIds : undefined;
3203
+ const state = this._entitySelectorScopes()[entitySelectorId];
3204
+ if (!state) {
3205
+ return { kind: 'unscoped' };
3206
+ }
3207
+ switch (state.status) {
3208
+ case 'pending':
3209
+ return { kind: 'pending' };
3210
+ case 'failed':
3211
+ return { kind: 'empty', reason: 'failed' };
3212
+ case 'resolved':
3213
+ return state.rtIds.length > 0
3214
+ ? { kind: 'scoped', rtIds: state.rtIds }
3215
+ : { kind: 'empty', reason: 'noTargets' };
3216
+ }
3217
+ }
3218
+ /**
3219
+ * Resolves the stream-data `rtIds` for a persistent-query widget bound to an
3220
+ * entity selector: the resolved source rtIds when the scope is `scoped`,
3221
+ * otherwise `undefined`.
3222
+ *
3223
+ * `undefined` does NOT mean "query with the persisted scope" for a pending or
3224
+ * empty scope — callers must check {@link resolveStreamDataScope} first and
3225
+ * skip the query for `pending` / `empty` (AB#7721).
3226
+ */
3227
+ resolveStreamDataRtIds(entitySelectorId) {
3228
+ const scope = this.resolveStreamDataScope(entitySelectorId);
3229
+ return scope.kind === 'scoped' ? scope.rtIds : undefined;
3230
+ }
3231
+ setEntitySelectorScope(selectorId, state) {
3232
+ this._entitySelectorScopes.update(map => ({ ...map, [selectorId]: state }));
3142
3233
  }
3143
3234
  /**
3144
3235
  * Updates the time filter configuration.
@@ -6520,6 +6611,18 @@ class KpiWidgetComponent {
6520
6611
  // queryFamily may be undefined for legacy widget configs — the executor
6521
6612
  // falls back to a one-time lookup by rtId. streamDataArgs is sent
6522
6613
  // unconditionally because the runtime path ignores it.
6614
+ // Entity-selector scope (AB#7721): pending → stay in the loading state and
6615
+ // send no query (the view refreshes once the scope is known); empty → show
6616
+ // the no-data value '-' without querying the persisted (unscoped) scope.
6617
+ const scope = this.stateService.resolveStreamDataScope(dataSource.entitySelectorId, dataSource.queryFamily);
6618
+ if (scope.kind === 'pending') {
6619
+ return;
6620
+ }
6621
+ if (scope.kind === 'empty') {
6622
+ this._data.set(null);
6623
+ this._isLoading.set(false);
6624
+ return;
6625
+ }
6523
6626
  const streamDataArgs = this.buildStreamDataArgs();
6524
6627
  const result = await firstValueFrom(this.queryExecutor.execute(dataSource.queryFamily, dataSource.queryRtId, {
6525
6628
  fieldFilter: fieldFilter ?? undefined,
@@ -10159,6 +10262,17 @@ class TableWidgetDataSourceDirective extends OctoGraphQlDataSource {
10159
10262
  * `'runtime'` for legacy configs that predate stream-data support.
10160
10263
  */
10161
10264
  fetchPersistentQueryData(dataSource, queryOptions) {
10265
+ // Entity-selector scope (AB#7721): pending → keep the list in its loading
10266
+ // state (an observable that never emits; the next refresh after the scope
10267
+ // resolves cancels it) and send no query; empty → no rows, no query — never
10268
+ // the persisted (unscoped) scope.
10269
+ const scope = this.stateService.resolveStreamDataScope(dataSource.entitySelectorId, dataSource.queryFamily);
10270
+ if (scope.kind === 'pending') {
10271
+ return NEVER;
10272
+ }
10273
+ if (scope.kind === 'empty') {
10274
+ return of(new FetchResultTyped([], 0));
10275
+ }
10162
10276
  const fieldFilter = this.convertFiltersToDto(this._config?.filters);
10163
10277
  // queryFamily may be undefined for legacy widget configs — the executor
10164
10278
  // falls back to a one-time lookup by rtId. streamDataArgs is sent
@@ -10302,6 +10416,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
10302
10416
  /** Default texts of {@link MeshBoardMessages} (the list texts default inside `mm-list-view`). */
10303
10417
  const DEFAULT_MESHBOARD_MESSAGES = {
10304
10418
  tableNotConfigured: 'Table not configured',
10419
+ scopeResolutionFailed: 'Could not resolve the scope of "{label}" - widgets bound to it show no data.',
10305
10420
  };
10306
10421
  /**
10307
10422
  * Optional: the host's translations of the MeshBoard widget texts (English without it). Set it
@@ -10326,6 +10441,7 @@ function resolveMeshBoardMessages(messages) {
10326
10441
  ...DEFAULT_MESHBOARD_MESSAGES,
10327
10442
  ...(messages?.tableList ? { tableList: messages.tableList } : {}),
10328
10443
  ...(messages?.tableNotConfigured ? { tableNotConfigured: messages.tableNotConfigured } : {}),
10444
+ ...(messages?.scopeResolutionFailed ? { scopeResolutionFailed: messages.scopeResolutionFailed } : {}),
10329
10445
  };
10330
10446
  }
10331
10447
  /** The resolved MeshBoard messages of {@link MESHBOARD_MESSAGES} as a signal. Call in an injection context. */
@@ -11446,6 +11562,9 @@ class GaugeWidgetComponent {
11446
11562
  }, /* @ts-ignore */
11447
11563
  ...(ngDevMode ? [{ debugName: "numericValue" }] : /* istanbul ignore next */ []));
11448
11564
  formattedValue = computed(() => {
11565
+ // Nothing loaded (e.g. an empty entity-selector scope, AB#7721): no value, not "0".
11566
+ if (!this._data())
11567
+ return '-';
11449
11568
  const value = this.numericValue();
11450
11569
  return value.toLocaleString('de-AT', {
11451
11570
  minimumFractionDigits: value % 1 !== 0 ? 1 : 0,
@@ -11517,6 +11636,18 @@ class GaugeWidgetComponent {
11517
11636
  // queryFamily may be undefined for legacy widget configs — the executor
11518
11637
  // falls back to a one-time lookup by rtId. streamDataArgs is sent
11519
11638
  // unconditionally because the runtime path ignores it.
11639
+ // Entity-selector scope (AB#7721): pending → stay in the loading state and
11640
+ // send no query (the view refreshes once the scope is known); empty → show
11641
+ // no value ('-') without querying the persisted (unscoped) scope.
11642
+ const scope = this.stateService.resolveStreamDataScope(dataSource.entitySelectorId, dataSource.queryFamily);
11643
+ if (scope.kind === 'pending') {
11644
+ return;
11645
+ }
11646
+ if (scope.kind === 'empty') {
11647
+ this._data.set(null);
11648
+ this._isLoading.set(false);
11649
+ return;
11650
+ }
11520
11651
  const streamDataArgs = this.buildStreamDataArgs();
11521
11652
  const result = await firstValueFrom(this.queryExecutor.execute(dataSource.queryFamily, dataSource.queryRtId, {
11522
11653
  fieldFilter: fieldFilter ?? undefined,
@@ -13619,6 +13750,18 @@ class PieChartWidgetComponent {
13619
13750
  * Note: isNotConfigured() check in loadData() ensures queryRtId is set.
13620
13751
  */
13621
13752
  async loadPersistentQueryData(dataSource) {
13753
+ // Entity-selector scope (AB#7721): pending → stay in the loading state and
13754
+ // send no query (the view refreshes once the scope is known); empty → render
13755
+ // the empty chart without querying the persisted (unscoped) scope.
13756
+ const scope = this.stateService.resolveStreamDataScope(dataSource.entitySelectorId, dataSource.queryFamily);
13757
+ if (scope.kind === 'pending') {
13758
+ return;
13759
+ }
13760
+ if (scope.kind === 'empty') {
13761
+ this._rawData.set([]);
13762
+ this._isLoading.set(false);
13763
+ return;
13764
+ }
13622
13765
  const fieldFilter = this.convertFiltersToDto(this.config.filters);
13623
13766
  // queryFamily may be undefined for legacy widget configs — the executor
13624
13767
  // falls back to a one-time lookup by rtId. streamDataArgs is sent
@@ -14833,14 +14976,23 @@ class BarChartWidgetComponent {
14833
14976
  // queryFamily may be undefined for legacy widget configs — the executor
14834
14977
  // falls back to a one-time lookup by rtId. streamDataArgs is sent
14835
14978
  // unconditionally because the runtime path ignores it.
14979
+ // Entity-selector scope (AB#7721): pending → stay in the loading state and
14980
+ // send no query (the view refreshes once the scope is known); empty → render
14981
+ // the empty chart without querying the persisted (unscoped) scope.
14982
+ const scope = this.stateService.resolveStreamDataScope(queryDataSource.entitySelectorId, queryDataSource.queryFamily);
14983
+ if (scope.kind === 'pending') {
14984
+ return;
14985
+ }
14836
14986
  const streamDataArgs = this.buildStreamDataArgs();
14837
- const result = await firstValueFrom(this.queryExecutor.execute(queryDataSource.queryFamily, queryDataSource.queryRtId, {
14838
- fieldFilter: fieldFilter ?? undefined,
14839
- streamDataArgs
14840
- }).pipe(catchError(err => {
14841
- console.error('Error loading Bar Chart data:', err);
14842
- throw err;
14843
- })));
14987
+ const result = scope.kind === 'empty'
14988
+ ? emptyQueryExecutionResult(queryDataSource.queryFamily)
14989
+ : await firstValueFrom(this.queryExecutor.execute(queryDataSource.queryFamily, queryDataSource.queryRtId, {
14990
+ fieldFilter: fieldFilter ?? undefined,
14991
+ streamDataArgs
14992
+ }).pipe(catchError(err => {
14993
+ console.error('Error loading Bar Chart data:', err);
14994
+ throw err;
14995
+ })));
14844
14996
  const filteredRows = result.rows.filter(row => BarChartWidgetComponent.SUPPORTED_ROW_TYPES.has(row.__typename ?? ''));
14845
14997
  if (this.isDynamicSeriesMode()) {
14846
14998
  this.processDynamicSeriesData(filteredRows);
@@ -16385,6 +16537,21 @@ class LineChartWidgetComponent {
16385
16537
  this._isLoading.set(true);
16386
16538
  this._error.set(null);
16387
16539
  try {
16540
+ // Entity-selector scope (AB#7721): pending → stay in the loading state and
16541
+ // send no query (the view refreshes once the scope is known); empty → render
16542
+ // the empty chart without querying the persisted (unscoped) scope.
16543
+ const scope = this.stateService.resolveStreamDataScope(queryDataSource.entitySelectorId, queryDataSource.queryFamily);
16544
+ if (scope.kind === 'pending') {
16545
+ return;
16546
+ }
16547
+ if (scope.kind === 'empty') {
16548
+ this._resolutionSignal.set(null);
16549
+ this._resolutionTarget.set(null);
16550
+ this.processData([], false);
16551
+ this._dataInfo.set({ rows: 0, points: 0, total: 0 });
16552
+ this._isLoading.set(false);
16553
+ return;
16554
+ }
16388
16555
  const fieldFilter = this.convertFiltersToDto(this.config.filters);
16389
16556
  // Resolution-aware routing (AB#4290): let the server pick the archive/rollup for the visible
16390
16557
  // window + target point count, then downsample each source series against it. Requires a
@@ -17966,14 +18133,23 @@ class HeatmapWidgetComponent {
17966
18133
  // queryFamily may be undefined for legacy widget configs — the executor
17967
18134
  // falls back to a one-time lookup by rtId. streamDataArgs is sent
17968
18135
  // unconditionally because the runtime path ignores it.
18136
+ // Entity-selector scope (AB#7721): pending → stay in the loading state and
18137
+ // send no query (the view refreshes once the scope is known); empty → render
18138
+ // the empty grid without querying the persisted (unscoped) scope.
18139
+ const scope = this.stateService.resolveStreamDataScope(queryDataSource.entitySelectorId, queryDataSource.queryFamily);
18140
+ if (scope.kind === 'pending') {
18141
+ return;
18142
+ }
17969
18143
  const streamDataArgs = this.buildStreamDataArgs();
17970
- const result = await firstValueFrom(this.queryExecutor.execute(queryDataSource.queryFamily, queryDataSource.queryRtId, {
17971
- fieldFilter: fieldFilter ?? undefined,
17972
- streamDataArgs
17973
- }).pipe(catchError(err => {
17974
- console.error('Error loading Heatmap data:', err);
17975
- throw err;
17976
- })));
18144
+ const result = scope.kind === 'empty'
18145
+ ? emptyQueryExecutionResult(queryDataSource.queryFamily)
18146
+ : await firstValueFrom(this.queryExecutor.execute(queryDataSource.queryFamily, queryDataSource.queryRtId, {
18147
+ fieldFilter: fieldFilter ?? undefined,
18148
+ streamDataArgs
18149
+ }).pipe(catchError(err => {
18150
+ console.error('Error loading Heatmap data:', err);
18151
+ throw err;
18152
+ })));
17977
18153
  const filteredRows = result.rows.filter(row => HeatmapWidgetComponent.SUPPORTED_ROW_TYPES.has(row.__typename ?? ''));
17978
18154
  this.processHeatmapData(filteredRows);
17979
18155
  this._isLoading.set(false);
@@ -19308,6 +19484,13 @@ class StatsGridWidgetComponent {
19308
19484
  this._statValues.set(initialValues);
19309
19485
  this._isLoading.set(true);
19310
19486
  this._error.set(null);
19487
+ // Entity-selector scope (AB#7721): while a bound stat's scope is still
19488
+ // resolving, stay in the loading state and send no query at all; the view
19489
+ // refreshes the widget once the scope is known.
19490
+ if (this.config.stats.some(stat => stat.persistentQuerySource
19491
+ && this.scopeOf(stat.persistentQuerySource).kind === 'pending')) {
19492
+ return;
19493
+ }
19311
19494
  try {
19312
19495
  // Runtime aggregation queries are only needed for stats WITHOUT a
19313
19496
  // per-stat persistent-query source — those run through the executor below.
@@ -19347,6 +19530,11 @@ class StatsGridWidgetComponent {
19347
19530
  return aggregationResults.get(stat.queryId) ?? null;
19348
19531
  }
19349
19532
  async loadPersistentQueryValue(source) {
19533
+ // Empty scope (nothing in scope or lookup failed, AB#7721): no value, no
19534
+ // query — never the persisted (unscoped) scope.
19535
+ if (this.scopeOf(source).kind === 'empty') {
19536
+ return null;
19537
+ }
19350
19538
  try {
19351
19539
  const result = await firstValueFrom(this.queryExecutor.execute(source.queryFamily, source.queryRtId, {
19352
19540
  fieldFilter: this.convertFiltersToDto(source.filters),
@@ -19359,6 +19547,9 @@ class StatsGridWidgetComponent {
19359
19547
  return null;
19360
19548
  }
19361
19549
  }
19550
+ scopeOf(source) {
19551
+ return this.stateService.resolveStreamDataScope(source.entitySelectorId, source.queryFamily);
19552
+ }
19362
19553
  buildStreamDataArgs(source) {
19363
19554
  const timeArgs = this.stateService.resolveStreamDataTimeArgs(source.ignoreTimeFilter);
19364
19555
  const rtIds = this.stateService.resolveStreamDataRtIds(source.entitySelectorId);
@@ -22983,6 +23174,13 @@ class SummaryCardWidgetComponent {
22983
23174
  return;
22984
23175
  this._isLoading.set(true);
22985
23176
  this._error.set(null);
23177
+ // Entity-selector scope (AB#7721): while a bound tile's scope is still
23178
+ // resolving, stay in the loading state and send no query at all; the view
23179
+ // refreshes the widget once the scope is known.
23180
+ if (this.config.tiles.some(tile => tile.persistentQuerySource
23181
+ && this.scopeOf(tile.persistentQuerySource).kind === 'pending')) {
23182
+ return;
23183
+ }
22986
23184
  try {
22987
23185
  // Cache entities by rtId to avoid duplicate fetches
22988
23186
  const entityCache = new Map();
@@ -23031,12 +23229,20 @@ class SummaryCardWidgetComponent {
23031
23229
  return null;
23032
23230
  }
23033
23231
  async fetchPersistentQueryValue(source) {
23232
+ // Empty scope (nothing in scope or lookup failed, AB#7721): no value, no
23233
+ // query — never the persisted (unscoped) scope.
23234
+ if (this.scopeOf(source).kind === 'empty') {
23235
+ return null;
23236
+ }
23034
23237
  const result = await firstValueFrom(this.queryExecutor.execute(source.queryFamily, source.queryRtId, {
23035
23238
  fieldFilter: this.convertFiltersToDto(source.filters),
23036
23239
  streamDataArgs: this.buildStreamDataArgs(source)
23037
23240
  }));
23038
23241
  return extractPersistentQueryCellValue(result, source);
23039
23242
  }
23243
+ scopeOf(source) {
23244
+ return this.stateService.resolveStreamDataScope(source.entitySelectorId, source.queryFamily);
23245
+ }
23040
23246
  buildStreamDataArgs(source) {
23041
23247
  const timeArgs = this.stateService.resolveStreamDataTimeArgs(source.ignoreTimeFilter);
23042
23248
  const rtIds = this.stateService.resolveStreamDataRtIds(source.entitySelectorId);
@@ -24743,6 +24949,21 @@ class ProcessDataService {
24743
24949
  if (!bindingQueryRtId) {
24744
24950
  throw new Error('Persistent query binding requires a Query rtId');
24745
24951
  }
24952
+ // Entity-selector scope (AB#7721): NO query is sent while the scope is pending
24953
+ // or empty — never the persisted (unscoped) scope. Pending → a `pending` result
24954
+ // (the widget stays loading; a refresh follows once the scope resolves);
24955
+ // empty → no rows.
24956
+ const scope = this.stateService.resolveStreamDataScope(config.bindingEntitySelectorId, bindingQueryFamily);
24957
+ if (scope.kind === 'pending') {
24958
+ return { type: 'query', pending: true, loadedAt: new Date() };
24959
+ }
24960
+ if (scope.kind === 'empty') {
24961
+ return {
24962
+ type: 'query',
24963
+ queryResult: this.mapExecutionResult(emptyQueryExecutionResult(bindingQueryFamily)),
24964
+ loadedAt: new Date()
24965
+ };
24966
+ }
24746
24967
  // Convert widget filters to GraphQL format with variable resolution
24747
24968
  const fieldFilters = this.convertFiltersToDto(bindingFilters);
24748
24969
  // Execute via the unified executor — it routes runtime vs stream-data from
@@ -24971,13 +25192,16 @@ class ProcessWidgetComponent {
24971
25192
  this._isLoading.set(true);
24972
25193
  this._error.set(null);
24973
25194
  this._boundData.set(null);
25195
+ // A bound entity-selector scope that is still resolving keeps the widget loading
25196
+ // until the refresh that follows the resolution (AB#7721).
25197
+ let scopePending = false;
24974
25198
  try {
24975
25199
  // Check for inline config first
24976
25200
  if (this.config.inlineConfig) {
24977
25201
  this._diagramConfig.set(this.config.inlineConfig);
24978
25202
  await this.loadSymbolDefinitions(this.config.inlineConfig);
24979
25203
  await this.loadRuntimeData();
24980
- await this.loadBoundData();
25204
+ scopePending = await this.loadBoundData();
24981
25205
  return;
24982
25206
  }
24983
25207
  // Load from backend by rtId
@@ -24986,7 +25210,7 @@ class ProcessWidgetComponent {
24986
25210
  this._diagramConfig.set(diagram);
24987
25211
  await this.loadSymbolDefinitions(diagram);
24988
25212
  await this.loadRuntimeData();
24989
- await this.loadBoundData();
25213
+ scopePending = await this.loadBoundData();
24990
25214
  return;
24991
25215
  }
24992
25216
  // No configuration
@@ -24997,25 +25221,32 @@ class ProcessWidgetComponent {
24997
25221
  this._error.set('Failed to load process diagram');
24998
25222
  }
24999
25223
  finally {
25000
- this._isLoading.set(false);
25224
+ if (!scopePending) {
25225
+ this._isLoading.set(false);
25226
+ }
25001
25227
  }
25002
25228
  }
25003
25229
  /**
25004
- * Loads bound data based on widget data binding configuration
25230
+ * Loads bound data based on widget data binding configuration.
25231
+ * Returns `true` when the bound entity-selector scope is still resolving (AB#7721).
25005
25232
  */
25006
25233
  async loadBoundData() {
25007
25234
  // Only load if data binding is configured
25008
25235
  if (!this.config.dataBindingMode || this.config.dataBindingMode === 'none') {
25009
- return;
25236
+ return false;
25010
25237
  }
25011
25238
  try {
25012
25239
  const boundData = await this.dataService.loadBoundData(this.config);
25240
+ if (boundData?.pending) {
25241
+ return true;
25242
+ }
25013
25243
  this._boundData.set(boundData);
25014
25244
  }
25015
25245
  catch (err) {
25016
25246
  console.error('Error loading bound data:', err);
25017
25247
  // Don't set error - diagram still displays, just without bound data
25018
25248
  }
25249
+ return false;
25019
25250
  }
25020
25251
  /**
25021
25252
  * Loads symbol definitions for all symbol instances in the diagram
@@ -32105,6 +32336,96 @@ function provideWidgetRegistrations(registrations) {
32105
32336
  ]);
32106
32337
  }
32107
32338
 
32339
+ /** True when the selector is configured to follow another selector. */
32340
+ function isFollowerSelector(selector) {
32341
+ return !!selector.followsSelectorId;
32342
+ }
32343
+ /** True when the selector renders a picker in the toolbar. Followers never do. */
32344
+ function showsInToolbar(selector) {
32345
+ return selector.showInToolbar !== false && !isFollowerSelector(selector);
32346
+ }
32347
+ /** Resolves the leader of `selector` among `selectors`. */
32348
+ function resolveFollowLeader(selectors, selector) {
32349
+ const leaderId = selector.followsSelectorId;
32350
+ if (!leaderId) {
32351
+ return { kind: 'none' };
32352
+ }
32353
+ if (leaderId === selector.id) {
32354
+ return { kind: 'invalid', problem: 'self', leaderId };
32355
+ }
32356
+ const leader = selectors.find(s => s.id === leaderId);
32357
+ if (!leader) {
32358
+ return { kind: 'invalid', problem: 'unknown', leaderId };
32359
+ }
32360
+ if (isFollowerSelector(leader)) {
32361
+ return { kind: 'invalid', problem: 'chained', leaderId };
32362
+ }
32363
+ return { kind: 'leader', leader };
32364
+ }
32365
+ /**
32366
+ * The valid followers of `leaderId`: selectors whose `followsSelectorId` is
32367
+ * `leaderId`, provided the leader exists and is not itself a follower.
32368
+ */
32369
+ function followersOf(selectors, leaderId) {
32370
+ return selectors.filter(s => s.followsSelectorId === leaderId && resolveFollowLeader(selectors, s).kind === 'leader');
32371
+ }
32372
+ /**
32373
+ * Selectors that `selectorId` may follow (the editor's "Follows" list): every
32374
+ * other selector that does not follow one itself.
32375
+ */
32376
+ function eligibleLeaders(selectors, selectorId) {
32377
+ return selectors.filter(s => s.id !== selectorId && !isFollowerSelector(s));
32378
+ }
32379
+ /** Ids of the selectors that name `selectorId` as their leader (valid or not). */
32380
+ function selectorsFollowing(selectors, selectorId) {
32381
+ return selectors.filter(s => s.id !== selectorId && s.followsSelectorId === selectorId);
32382
+ }
32383
+ /**
32384
+ * The selector as it is resolved at runtime: a follower with a valid leader
32385
+ * picks from the leader's CK type (the editor fixes it to the leader's anyway;
32386
+ * this keeps an out-of-date stored type from breaking the entity fetch).
32387
+ */
32388
+ function effectiveSelector(selectors, selector) {
32389
+ const resolution = resolveFollowLeader(selectors, selector);
32390
+ if (resolution.kind === 'leader' && resolution.leader.ckTypeId !== selector.ckTypeId) {
32391
+ return { ...selector, ckTypeId: resolution.leader.ckTypeId };
32392
+ }
32393
+ return selector;
32394
+ }
32395
+ /**
32396
+ * Editor validation of a "Follows" choice for the selector `selectorId`.
32397
+ * Returns the message that tells the author why the choice is not allowed, or
32398
+ * `null` when it is valid (including "follows nobody").
32399
+ */
32400
+ function followValidationError(selectors, selectorId, followsSelectorId) {
32401
+ if (!followsSelectorId) {
32402
+ return null;
32403
+ }
32404
+ if (followsSelectorId === selectorId) {
32405
+ return 'A selector cannot follow itself.';
32406
+ }
32407
+ const followedBy = selectorsFollowing(selectors, selectorId);
32408
+ if (followedBy.length > 0) {
32409
+ return `Followed by ${labelList(followedBy)} — a selector that is followed cannot follow another one.`;
32410
+ }
32411
+ const leader = selectors.find(s => s.id === followsSelectorId);
32412
+ if (!leader) {
32413
+ return `The leader '${followsSelectorId}' does not exist — pick another leader or none.`;
32414
+ }
32415
+ if (isFollowerSelector(leader)) {
32416
+ return `The leader '${leader.label || leader.id}' follows another selector — pick another leader or none.`;
32417
+ }
32418
+ return null;
32419
+ }
32420
+ /** "A, B and C" from the selectors' labels (falling back to their ids). */
32421
+ function labelList(selectors) {
32422
+ const labels = selectors.map(s => s.label || s.id);
32423
+ if (labels.length <= 1) {
32424
+ return labels.join('');
32425
+ }
32426
+ return `${labels.slice(0, -1).join(', ')} and ${labels[labels.length - 1]}`;
32427
+ }
32428
+
32108
32429
  /**
32109
32430
  * Component for editing entity selector configurations in MeshBoard settings.
32110
32431
  * Allows adding, editing, and removing entity selectors.
@@ -32115,6 +32436,7 @@ class EntitySelectorEditorComponent {
32115
32436
  attributeSelectorService = inject(AttributeSelectorDialogService);
32116
32437
  variableService = inject(MeshBoardVariableService);
32117
32438
  getEntitiesByCkTypeGQL = inject(GetEntitiesByCkTypeDtoGQL);
32439
+ confirmationService = inject(ConfirmationService, { optional: true });
32118
32440
  entitySelectors = [];
32119
32441
  /** Existing variable names from other sources (static variables, other selectors) for duplicate detection */
32120
32442
  existingVariableNames = [];
@@ -32143,6 +32465,10 @@ class EntitySelectorEditorComponent {
32143
32465
  // the association to its parent), so 'in' is the correct default for a
32144
32466
  // childScope that resolves a picked parent to its children.
32145
32467
  editChildScopeDirection = 'in';
32468
+ // Leader/follower (AB#7722): id of the selector this one follows ('' = none).
32469
+ editFollowsSelectorId = '';
32470
+ /** Why choosing the leader changed the form (CK type fixed, mappings/default cleared). */
32471
+ followNotice = '';
32146
32472
  // Data sources for the default entity picker (created when CK type is set)
32147
32473
  defaultEntityDataSource = null;
32148
32474
  defaultEntityDialogDataSource = null;
@@ -32162,6 +32488,8 @@ class EntitySelectorEditorComponent {
32162
32488
  this.editChildScopeTargetCkTypeItem = null;
32163
32489
  this.editChildScopeRoleId = 'System/ParentChild';
32164
32490
  this.editChildScopeDirection = 'in';
32491
+ this.editFollowsSelectorId = '';
32492
+ this.followNotice = '';
32165
32493
  this.defaultEntityDataSource = null;
32166
32494
  this.defaultEntityDialogDataSource = null;
32167
32495
  this.isEditing = true;
@@ -32197,6 +32525,14 @@ class EntitySelectorEditorComponent {
32197
32525
  : null;
32198
32526
  this.editChildScopeRoleId = selector.childScope?.roleId ?? 'System/ParentChild';
32199
32527
  this.editChildScopeDirection = selector.childScope?.direction ?? 'in';
32528
+ this.editFollowsSelectorId = selector.followsSelectorId ?? '';
32529
+ this.followNotice = '';
32530
+ // A follower picks from its leader's CK type; re-sync it in case the leader's
32531
+ // type changed after the follower was saved.
32532
+ const leader = resolveFollowLeader(this.entitySelectors, selector);
32533
+ if (leader.kind === 'leader' && leader.leader.ckTypeId !== this.editCkTypeId) {
32534
+ this.applyLeaderCkType(leader.leader.ckTypeId);
32535
+ }
32200
32536
  this.updateDefaultEntityDataSources();
32201
32537
  this.isEditing = true;
32202
32538
  this.editingStateChange.emit(true);
@@ -32204,10 +32540,82 @@ class EntitySelectorEditorComponent {
32204
32540
  /**
32205
32541
  * Removes an entity selector at the given index.
32206
32542
  */
32207
- removeSelector(index) {
32543
+ async removeSelector(index) {
32544
+ const selector = this.entitySelectors[index];
32545
+ const followers = selector ? selectorsFollowing(this.entitySelectors, selector.id) : [];
32546
+ if (followers.length > 0 && this.confirmationService) {
32547
+ // AB#7722: removing a leader orphans its followers — name them.
32548
+ const name = selector.label || selector.id;
32549
+ const confirmed = await this.confirmationService.showDangerConfirm({
32550
+ title: `Remove selector ${name}?`,
32551
+ targetName: name,
32552
+ consequence: `${labelList(followers)} ${followers.length === 1 ? 'follows' : 'follow'} this selector. `
32553
+ + 'Without a leader they behave as if the leader had no pick until you choose another leader.',
32554
+ confirmText: 'Remove selector'
32555
+ });
32556
+ if (!confirmed)
32557
+ return;
32558
+ }
32208
32559
  this.entitySelectors = this.entitySelectors.filter((_, i) => i !== index);
32209
32560
  this.emitChange();
32210
32561
  }
32562
+ /** Selectors the edited selector may follow (non-followers, never itself). */
32563
+ get leaderOptions() {
32564
+ return eligibleLeaders(this.entitySelectors, this.editId);
32565
+ }
32566
+ /** Selectors that follow the edited selector (it then cannot follow one itself). */
32567
+ get followedBy() {
32568
+ return this.editingIndex === null ? [] : selectorsFollowing(this.entitySelectors, this.editId);
32569
+ }
32570
+ /** True when the stored leader is not among the selectable leaders (deleted or a follower). */
32571
+ get isLeaderUnavailable() {
32572
+ return !!this.editFollowsSelectorId && !this.leaderOptions.some(l => l.id === this.editFollowsSelectorId);
32573
+ }
32574
+ /** "A, B and C" of the selectors following the edited one. */
32575
+ get followedByLabels() {
32576
+ return labelList(this.followedBy);
32577
+ }
32578
+ /** Validation message of the "Follows" choice, or '' when it is valid. */
32579
+ get followError() {
32580
+ return followValidationError(this.entitySelectors, this.editId, this.editFollowsSelectorId || undefined) ?? '';
32581
+ }
32582
+ /** True while the edited selector follows another one. */
32583
+ get isFollower() {
32584
+ return !!this.editFollowsSelectorId;
32585
+ }
32586
+ /**
32587
+ * Handles a change of the "Follows" choice. Choosing a leader fixes the CK type
32588
+ * to the leader's (clearing default entity and mappings with a notice when the
32589
+ * type changes) and hides the toolbar option; back to none makes the CK type
32590
+ * editable again with "Show in Toolbar" unchecked.
32591
+ */
32592
+ onFollowsChanged(leaderId) {
32593
+ this.editFollowsSelectorId = leaderId;
32594
+ this.followNotice = '';
32595
+ this.editShowInToolbar = false;
32596
+ if (!leaderId)
32597
+ return;
32598
+ const leader = this.entitySelectors.find(s => s.id === leaderId);
32599
+ if (leader && leader.ckTypeId !== this.editCkTypeId) {
32600
+ const cleared = this.editMappings.length > 0 || !!this.editDefaultRtId;
32601
+ this.applyLeaderCkType(leader.ckTypeId);
32602
+ this.updateDefaultEntityDataSources();
32603
+ if (cleared) {
32604
+ this.followNotice = `The CK type is now the leader's (${leader.ckTypeId}); `
32605
+ + 'the default entity and attribute mappings of the previous type were cleared.';
32606
+ }
32607
+ }
32608
+ }
32609
+ /** True when the selector names a leader that does not exist or is itself a follower. */
32610
+ isOrphanFollower(selector) {
32611
+ return resolveFollowLeader(this.entitySelectors, selector).kind === 'invalid';
32612
+ }
32613
+ applyLeaderCkType(ckTypeId) {
32614
+ this.editCkTypeId = ckTypeId;
32615
+ this.editCkTypeItem = { fullName: ckTypeId, rtCkTypeId: ckTypeId, isAbstract: false, isFinal: false };
32616
+ this.editMappings = [];
32617
+ this.editDefaultRtId = '';
32618
+ }
32211
32619
  /**
32212
32620
  * Handles CK type selection in the editor form.
32213
32621
  */
@@ -32297,7 +32705,9 @@ class EntitySelectorEditorComponent {
32297
32705
  attributeMappings: this.editMappings,
32298
32706
  showInToolbar: this.editShowInToolbar,
32299
32707
  defaultRtId: this.editDefaultRtId.trim() || undefined,
32300
- childScope
32708
+ childScope,
32709
+ // A follower never renders a picker (AB#7722).
32710
+ ...(this.editFollowsSelectorId ? { showInToolbar: false, followsSelectorId: this.editFollowsSelectorId } : {})
32301
32711
  };
32302
32712
  if (this.editingIndex !== null) {
32303
32713
  this.entitySelectors = this.entitySelectors.map((es, i) => i === this.editingIndex ? selector : es);
@@ -32325,13 +32735,18 @@ class EntitySelectorEditorComponent {
32325
32735
  const hasRequiredFields = this.editId.trim().length > 0 &&
32326
32736
  this.editLabel.trim().length > 0 &&
32327
32737
  this.editCkTypeId.length > 0 &&
32328
- this.editMappings.length > 0 &&
32738
+ // A follower mostly exists for its child scope: mappings are optional (AB#7722).
32739
+ (this.isFollower || this.editMappings.length > 0) &&
32329
32740
  this.editMappings.every(m => m.variableName.trim().length > 0 &&
32330
32741
  this.isValidVariableName(m.variableName) &&
32331
32742
  !this.isDuplicateVariableName(m.variableName, m.attributePath)) &&
32332
32743
  !this.isDuplicateId(this.editId, this.editingIndex);
32333
- // If not shown in toolbar, a default entity must be selected
32334
- if (!this.editShowInToolbar && !this.editDefaultRtId.trim()) {
32744
+ // If not shown in toolbar, a default entity must be selected — except for a
32745
+ // follower, which takes its leader's pick (AB#7722).
32746
+ if (!this.isFollower && !this.editShowInToolbar && !this.editDefaultRtId.trim()) {
32747
+ return false;
32748
+ }
32749
+ if (this.followError) {
32335
32750
  return false;
32336
32751
  }
32337
32752
  return hasRequiredFields;
@@ -32354,6 +32769,13 @@ class EntitySelectorEditorComponent {
32354
32769
  * Gets a display label indicating the selector mode.
32355
32770
  */
32356
32771
  getModeLabel(selector) {
32772
+ const leader = resolveFollowLeader(this.entitySelectors, selector);
32773
+ if (leader.kind === 'leader') {
32774
+ return `Follows: ${leader.leader.label || leader.leader.id}`;
32775
+ }
32776
+ if (leader.kind === 'invalid') {
32777
+ return 'Follows: missing';
32778
+ }
32357
32779
  return selector.showInToolbar !== false ? 'Toolbar' : 'Fixed';
32358
32780
  }
32359
32781
  /**
@@ -32417,7 +32839,7 @@ class EntitySelectorEditorComponent {
32417
32839
  this.entitySelectorsChange.emit([...this.entitySelectors]);
32418
32840
  }
32419
32841
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: EntitySelectorEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
32420
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: EntitySelectorEditorComponent, isStandalone: true, selector: "mm-entity-selector-editor", inputs: { entitySelectors: "entitySelectors", existingVariableNames: "existingVariableNames" }, outputs: { entitySelectorsChange: "entitySelectorsChange", editingStateChange: "editingStateChange" }, ngImport: i0, template: "<div class=\"entity-selector-editor\">\n @if (!isEditing) {\n <!-- Selector List -->\n <div class=\"selector-list\">\n @for (selector of entitySelectors; track selector.id; let i = $index) {\n <div class=\"selector-item\">\n <div class=\"selector-info\">\n <span class=\"selector-label\">{{ selector.label }}</span>\n <span class=\"selector-mode\">{{ getModeLabel(selector) }}</span>\n <span class=\"selector-ck-type\">{{ selector.ckTypeId }}</span>\n <span class=\"selector-mappings\">{{ getMappingSummary(selector) }}</span>\n </div>\n <div class=\"selector-actions\">\n <button\n kendoButton\n [svgIcon]=\"pencilIcon\"\n (click)=\"editSelector(i)\"\n fillMode=\"flat\"\n size=\"small\"\n title=\"Edit selector\"\n [attr.aria-label]=\"'Edit selector ' + (selector.label || selector.ckTypeId)\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"trashIcon\"\n (click)=\"removeSelector(i)\"\n fillMode=\"flat\"\n size=\"small\"\n themeColor=\"error\"\n title=\"Remove selector\"\n [attr.aria-label]=\"'Remove selector ' + (selector.label || selector.ckTypeId)\"\n class=\"remove-btn\">\n </button>\n </div>\n </div>\n }\n\n @if (entitySelectors.length === 0) {\n <div class=\"empty-state\">\n No entity selectors configured. Add one to enable entity-based variables.\n </div>\n }\n </div>\n\n <div class=\"add-button-row\">\n <button\n kendoButton\n [svgIcon]=\"plusIcon\"\n (click)=\"addSelector()\"\n themeColor=\"primary\">\n Add Entity Selector\n </button>\n </div>\n } @else {\n <!-- Edit Form -->\n <div class=\"edit-panel\">\n <div class=\"edit-panel-header\">\n {{ editingIndex !== null ? 'Edit Entity Selector' : 'New Entity Selector' }}\n </div>\n <div class=\"edit-form\">\n <div class=\"form-field\">\n <label class=\"field-label\">ID *</label>\n <kendo-textbox\n [(ngModel)]=\"editId\"\n placeholder=\"e.g., mp\"\n [readonly]=\"editingIndex !== null\">\n </kendo-textbox>\n @if (isDuplicateId(editId, editingIndex)) {\n <span class=\"field-error\">ID must be unique</span>\n }\n <span class=\"field-hint\">Used in URL parameters (es_&lt;id&gt;=&lt;rtId&gt;)</span>\n </div>\n\n <div class=\"form-field\">\n <label class=\"field-label\">Label *</label>\n <kendo-textbox\n [(ngModel)]=\"editLabel\"\n placeholder=\"e.g., Metering Point\">\n </kendo-textbox>\n </div>\n\n <div class=\"form-field\">\n <label class=\"field-label\">CK Type *</label>\n <mm-ck-type-selector-input\n [ngModel]=\"editCkTypeItem\"\n [placeholder]=\"'Select CK type...'\"\n (ckTypeSelected)=\"onCkTypeSelected($event)\"\n (ckTypeCleared)=\"onCkTypeCleared()\">\n </mm-ck-type-selector-input>\n </div>\n\n <div class=\"form-field\">\n <label class=\"field-label\">Attribute Mappings *</label>\n <button\n kendoButton\n (click)=\"selectAttributes()\"\n [disabled]=\"!editCkTypeId\"\n fillMode=\"outline\"\n size=\"small\">\n Select Attributes...\n </button>\n\n @if (editMappings.length > 0) {\n <div class=\"mappings-list\">\n <div class=\"mappings-header\">\n <span class=\"mappings-header-attr\">Attribute</span>\n <span class=\"mappings-header-var\">Variable Name</span>\n </div>\n @for (mapping of editMappings; track mapping.attributePath; let j = $index) {\n <div class=\"mapping-item\">\n <span class=\"mapping-path\">{{ mapping.attributePath }}</span>\n <span class=\"mapping-arrow\">&rarr;</span>\n <div class=\"mapping-var-field\">\n <div class=\"mapping-var-input-wrapper\">\n <span class=\"mapping-var-prefix\">$</span>\n <kendo-textbox\n [(ngModel)]=\"mapping.variableName\"\n size=\"small\"\n class=\"mapping-var-input\"\n placeholder=\"variableName\"\n [class.invalid]=\"getVariableNameError(mapping)\">\n </kendo-textbox>\n </div>\n @if (getVariableNameError(mapping); as error) {\n <span class=\"field-error\">{{ error }}</span>\n }\n </div>\n </div>\n }\n </div>\n }\n </div>\n\n <div class=\"form-field\">\n <div class=\"checkbox-row\">\n <input\n type=\"checkbox\"\n kendoCheckBox\n [(ngModel)]=\"editShowInToolbar\"\n id=\"showInToolbar\" />\n <label class=\"field-label checkbox-label\" for=\"showInToolbar\">Show in Toolbar</label>\n </div>\n <span class=\"field-hint\">\n @if (editShowInToolbar) {\n Users can select an entity from a dropdown in the toolbar.\n } @else {\n Hidden selector \u2014 variables are resolved from the default entity below.\n }\n </span>\n </div>\n\n <div class=\"form-field\">\n <label class=\"field-label\">Default Entity{{ !editShowInToolbar ? ' *' : '' }}</label>\n @if (defaultEntityDataSource) {\n <mm-entity-select-input\n [dataSource]=\"defaultEntityDataSource\"\n [dialogDataSource]=\"defaultEntityDialogDataSource!\"\n [placeholder]=\"'Select default entity...'\"\n [dialogTitle]=\"'Select Default ' + editLabel\"\n (entitySelected)=\"onDefaultEntitySelected($event)\"\n (entityCleared)=\"onDefaultEntityCleared()\">\n </mm-entity-select-input>\n } @else {\n <span class=\"field-hint\">Select a CK type first to pick a default entity.</span>\n }\n @if (editDefaultRtId) {\n <span class=\"field-hint\">Selected: {{ editDefaultRtId }}</span>\n }\n @if (!editShowInToolbar && !editDefaultRtId) {\n <span class=\"field-error\">A default entity is required when not shown in toolbar.</span>\n }\n <span class=\"field-hint\">\n @if (editShowInToolbar) {\n Optional \u2014 pre-selected entity when the MeshBoard loads.\n } @else {\n This entity's attributes will be used to populate the variables.\n }\n </span>\n </div>\n\n <div class=\"form-field\">\n <label class=\"field-label\">Child Scope (stream-data)</label>\n <span class=\"field-hint\">\n Optional. Resolves the picked entity to its child source rtIds (one\n association hop) so a stream-data widget can scope to them \u2014 e.g. a\n MeteringPoint resolved to its EnergyMeasurement rtIds. Leave empty to\n scope by the picked entity's own rtId.\n </span>\n <mm-ck-type-selector-input\n [ngModel]=\"editChildScopeTargetCkTypeItem\"\n [placeholder]=\"'Child CK type (e.g. EnergyMeasurement)...'\"\n (ckTypeSelected)=\"onChildScopeCkTypeSelected($event)\"\n (ckTypeCleared)=\"onChildScopeCkTypeCleared()\">\n </mm-ck-type-selector-input>\n <kendo-textbox\n [(ngModel)]=\"editChildScopeRoleId\"\n placeholder=\"Association role (e.g. System/ParentChild)\">\n </kendo-textbox>\n <label class=\"field-label\">Direction</label>\n <select [(ngModel)]=\"editChildScopeDirection\" class=\"child-scope-direction\">\n <option value=\"out\">Outbound (parent \u2192 child)</option>\n <option value=\"in\">Inbound (child \u2192 parent)</option>\n </select>\n </div>\n\n <div class=\"edit-actions mm-dialog-actions\">\n <button kendoButton (click)=\"cancelEdit()\" fillMode=\"flat\">\n Cancel\n </button>\n <button\n kendoButton\n (click)=\"saveEdit()\"\n [disabled]=\"!isEditValid()\"\n themeColor=\"primary\">\n {{ editingIndex !== null ? 'Update' : 'Add' }}\n </button>\n </div>\n </div>\n </div>\n }\n</div>\n", styles: [".entity-selector-editor{display:flex;flex-direction:column;gap:1.25rem}.selector-list{display:flex;flex-direction:column;gap:.5rem}.selector-item{display:flex;justify-content:space-between;align-items:center;padding:.75rem 1rem;background:var(--kendo-color-surface-alt, #fafafa);border:1px solid var(--kendo-color-border, #e0e0e0);border-radius:4px}.selector-item .selector-info{display:flex;flex-direction:column;gap:.25rem;min-width:0;flex:1}.selector-item .selector-label{font-weight:500;color:var(--kendo-color-on-app-surface, #424242)}.selector-item .selector-mode{font-size:.75rem;font-weight:500;color:var(--kendo-color-info, #0288d1)}.selector-item .selector-ck-type{font-size:.8rem;color:var(--kendo-color-subtle, #757575);font-family:var(--theme-font-mono)}.selector-item .selector-mappings{font-size:.8rem;color:var(--kendo-color-primary, #3f51b5);font-family:var(--theme-font-mono)}.selector-item .selector-actions{display:flex;gap:.25rem;flex-shrink:0}.selector-item .remove-btn{color:var(--kendo-color-error, #f44336)}.empty-state{padding:1.5rem;text-align:center;color:var(--kendo-color-subtle, #757575);font-size:.875rem}.add-button-row{display:flex;justify-content:flex-start}.edit-panel{border:1px solid var(--kendo-color-border, #e0e0e0);border-radius:4px;overflow:hidden}.edit-panel-header{padding:.625rem 1rem;font-weight:600;font-size:.875rem;color:var(--kendo-color-on-primary, #ffffff);background:var(--kendo-color-primary, #3f51b5)}.edit-form{display:flex;flex-direction:column;gap:1.25rem;padding:1.25rem}.edit-form .form-field{display:flex;flex-direction:column;gap:.5rem}.edit-form .field-label{font-weight:500;font-size:.875rem;color:var(--kendo-color-on-app-surface, #424242)}.edit-form .field-hint{font-size:.75rem;color:var(--kendo-color-subtle, #757575)}.edit-form .field-error{font-size:.75rem;color:var(--kendo-color-error, #f44336)}.edit-form .checkbox-row{display:flex;align-items:center;gap:.5rem}.edit-form .checkbox-row .checkbox-label{margin:0;cursor:pointer}.mappings-list{display:flex;flex-direction:column;gap:.5rem;margin-top:.5rem;padding:.75rem;background:var(--kendo-color-surface-alt, #fafafa);border:1px solid var(--kendo-color-border, #e0e0e0);border-radius:4px}.mappings-header{display:flex;align-items:center;gap:.5rem;font-size:.75rem;font-weight:600;color:var(--kendo-color-subtle, #757575);padding-bottom:.25rem;border-bottom:1px solid var(--kendo-color-border, #e0e0e0)}.mappings-header .mappings-header-attr{min-width:150px}.mappings-header .mappings-header-var{margin-left:1.75rem}.mapping-item{display:flex;align-items:flex-start;gap:.5rem}.mapping-item .mapping-path{font-size:.8rem;font-family:var(--theme-font-mono);color:var(--kendo-color-subtle, #757575);min-width:150px;padding-top:5px}.mapping-item .mapping-arrow{color:var(--kendo-color-subtle, #757575);flex-shrink:0;padding-top:5px}.mapping-item .mapping-var-field{display:flex;flex-direction:column;gap:2px;flex:1;min-width:100px}.mapping-item .mapping-var-input-wrapper{display:flex;align-items:center;gap:0}.mapping-item .mapping-var-prefix{font-family:var(--theme-font-mono);font-size:.875rem;font-weight:600;color:var(--kendo-color-primary, #3f51b5);padding-right:2px;flex-shrink:0}.mapping-item .mapping-var-input{flex:1}.mapping-item .mapping-var-input.invalid ::ng-deep .k-input{border-color:var(--kendo-color-error, #f44336)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox]:not([ngNoCva])[formControlName],input[type=checkbox]:not([ngNoCva])[formControl],input[type=checkbox]:not([ngNoCva])[ngModel]" }, { kind: "directive", type: i1$1.SelectControlValueAccessor, selector: "select:not([multiple]):not([ngNoCva])[formControlName],select:not([multiple]):not([ngNoCva])[formControl],select:not([multiple]):not([ngNoCva])[ngModel]", inputs: ["compareWith"] }, { 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: ButtonModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: InputsModule }, { kind: "component", type: i3.TextBoxComponent, selector: "kendo-textbox", inputs: ["focusableId", "title", "type", "disabled", "readonly", "tabindex", "value", "selectOnFocus", "showSuccessIcon", "showErrorIcon", "clearButton", "successIcon", "successSvgIcon", "errorIcon", "errorSvgIcon", "clearButtonIcon", "clearButtonSvgIcon", "size", "rounded", "fillMode", "tabIndex", "placeholder", "maxlength", "inputAttributes"], outputs: ["valueChange", "inputFocus", "inputBlur", "focus", "blur"], exportAs: ["kendoTextBox"] }, { kind: "directive", type: i3.CheckBoxDirective, selector: "input[kendoCheckBox]", inputs: ["size", "rounded"] }, { kind: "ngmodule", type: LabelModule }, { kind: "directive", type: i4$1.LabelDirective, selector: "label[for]", inputs: ["for", "labelClass"] }, { kind: "ngmodule", type: SVGIconModule }, { 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"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
32842
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: EntitySelectorEditorComponent, isStandalone: true, selector: "mm-entity-selector-editor", inputs: { entitySelectors: "entitySelectors", existingVariableNames: "existingVariableNames" }, outputs: { entitySelectorsChange: "entitySelectorsChange", editingStateChange: "editingStateChange" }, ngImport: i0, template: "<div class=\"entity-selector-editor\">\n @if (!isEditing) {\n <!-- Selector List -->\n <div class=\"selector-list\">\n @for (selector of entitySelectors; track selector.id; let i = $index) {\n <div class=\"selector-item\">\n <div class=\"selector-info\">\n <span class=\"selector-label\">{{ selector.label }}</span>\n <span class=\"selector-mode\" [class.selector-mode-error]=\"isOrphanFollower(selector)\">{{ getModeLabel(selector) }}</span>\n <span class=\"selector-ck-type\">{{ selector.ckTypeId }}</span>\n <span class=\"selector-mappings\">{{ getMappingSummary(selector) }}</span>\n </div>\n <div class=\"selector-actions\">\n <button\n kendoButton\n [svgIcon]=\"pencilIcon\"\n (click)=\"editSelector(i)\"\n fillMode=\"flat\"\n size=\"small\"\n title=\"Edit selector\"\n [attr.aria-label]=\"'Edit selector ' + (selector.label || selector.ckTypeId)\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"trashIcon\"\n (click)=\"removeSelector(i)\"\n fillMode=\"flat\"\n size=\"small\"\n themeColor=\"error\"\n title=\"Remove selector\"\n [attr.aria-label]=\"'Remove selector ' + (selector.label || selector.ckTypeId)\"\n class=\"remove-btn\">\n </button>\n </div>\n </div>\n }\n\n @if (entitySelectors.length === 0) {\n <div class=\"empty-state\">\n No entity selectors configured. Add one to enable entity-based variables.\n </div>\n }\n </div>\n\n <div class=\"add-button-row\">\n <button\n kendoButton\n [svgIcon]=\"plusIcon\"\n (click)=\"addSelector()\"\n themeColor=\"primary\">\n Add Entity Selector\n </button>\n </div>\n } @else {\n <!-- Edit Form -->\n <div class=\"edit-panel\">\n <div class=\"edit-panel-header\">\n {{ editingIndex !== null ? 'Edit Entity Selector' : 'New Entity Selector' }}\n </div>\n <div class=\"edit-form\">\n <div class=\"form-field\">\n <label class=\"field-label\">ID *</label>\n <kendo-textbox\n [(ngModel)]=\"editId\"\n placeholder=\"e.g., mp\"\n [readonly]=\"editingIndex !== null\">\n </kendo-textbox>\n @if (isDuplicateId(editId, editingIndex)) {\n <span class=\"field-error\">ID must be unique</span>\n }\n <span class=\"field-hint\">Used in URL parameters (es_&lt;id&gt;=&lt;rtId&gt;)</span>\n </div>\n\n <div class=\"form-field\">\n <label class=\"field-label\">Label *</label>\n <kendo-textbox\n [(ngModel)]=\"editLabel\"\n placeholder=\"e.g., Metering Point\">\n </kendo-textbox>\n </div>\n\n <div class=\"form-field\">\n <label class=\"field-label\" for=\"followsSelectorId\">Follows</label>\n <select\n id=\"followsSelectorId\"\n class=\"follows-select\"\n [ngModel]=\"editFollowsSelectorId\"\n (ngModelChange)=\"onFollowsChanged($event)\"\n [disabled]=\"followedBy.length > 0 && !editFollowsSelectorId\">\n <option value=\"\">\u2014 none (own picker / default entity) \u2014</option>\n @for (leader of leaderOptions; track leader.id) {\n <option [value]=\"leader.id\">{{ leader.label || leader.id }} ({{ leader.id }})</option>\n }\n @if (isLeaderUnavailable) {\n <option [value]=\"editFollowsSelectorId\">{{ editFollowsSelectorId }} (unavailable)</option>\n }\n </select>\n @if (followError) {\n <span class=\"field-error\">{{ followError }}</span>\n } @else if (followedBy.length > 0) {\n <span class=\"field-hint\">Followed by {{ followedByLabels }} \u2014 a selector that is followed cannot follow another one.</span>\n }\n @if (followNotice) {\n <span class=\"field-hint follow-notice\">{{ followNotice }}</span>\n }\n <span class=\"field-hint\">\n Takes the pick of the selected selector. The CK type is fixed to the leader's.\n A follower never shows a picker and is not written to the URL.\n </span>\n </div>\n\n <div class=\"form-field\">\n <label class=\"field-label\">CK Type *</label>\n @if (isFollower) {\n <kendo-textbox [value]=\"editCkTypeId\" [readonly]=\"true\"></kendo-textbox>\n <span class=\"field-hint\">Fixed to the leader's CK type.</span>\n } @else {\n <mm-ck-type-selector-input\n [ngModel]=\"editCkTypeItem\"\n [placeholder]=\"'Select CK type...'\"\n (ckTypeSelected)=\"onCkTypeSelected($event)\"\n (ckTypeCleared)=\"onCkTypeCleared()\">\n </mm-ck-type-selector-input>\n }\n </div>\n\n <div class=\"form-field\">\n <label class=\"field-label\">Attribute Mappings{{ isFollower ? '' : ' *' }}</label>\n <button\n kendoButton\n (click)=\"selectAttributes()\"\n [disabled]=\"!editCkTypeId\"\n fillMode=\"outline\"\n size=\"small\">\n Select Attributes...\n </button>\n\n @if (editMappings.length > 0) {\n <div class=\"mappings-list\">\n <div class=\"mappings-header\">\n <span class=\"mappings-header-attr\">Attribute</span>\n <span class=\"mappings-header-var\">Variable Name</span>\n </div>\n @for (mapping of editMappings; track mapping.attributePath; let j = $index) {\n <div class=\"mapping-item\">\n <span class=\"mapping-path\">{{ mapping.attributePath }}</span>\n <span class=\"mapping-arrow\">&rarr;</span>\n <div class=\"mapping-var-field\">\n <div class=\"mapping-var-input-wrapper\">\n <span class=\"mapping-var-prefix\">$</span>\n <kendo-textbox\n [(ngModel)]=\"mapping.variableName\"\n size=\"small\"\n class=\"mapping-var-input\"\n placeholder=\"variableName\"\n [class.invalid]=\"getVariableNameError(mapping)\">\n </kendo-textbox>\n </div>\n @if (getVariableNameError(mapping); as error) {\n <span class=\"field-error\">{{ error }}</span>\n }\n </div>\n </div>\n }\n </div>\n }\n </div>\n\n @if (!isFollower) {\n <div class=\"form-field\">\n <div class=\"checkbox-row\">\n <input\n type=\"checkbox\"\n kendoCheckBox\n [(ngModel)]=\"editShowInToolbar\"\n id=\"showInToolbar\" />\n <label class=\"field-label checkbox-label\" for=\"showInToolbar\">Show in Toolbar</label>\n </div>\n <span class=\"field-hint\">\n @if (editShowInToolbar) {\n Users can select an entity from a dropdown in the toolbar.\n } @else {\n Hidden selector \u2014 variables are resolved from the default entity below.\n }\n </span>\n </div>\n }\n\n <div class=\"form-field\">\n <label class=\"field-label\">Default Entity{{ !editShowInToolbar && !isFollower ? ' *' : '' }}</label>\n @if (defaultEntityDataSource) {\n <mm-entity-select-input\n [dataSource]=\"defaultEntityDataSource\"\n [dialogDataSource]=\"defaultEntityDialogDataSource!\"\n [placeholder]=\"'Select default entity...'\"\n [dialogTitle]=\"'Select Default ' + editLabel\"\n (entitySelected)=\"onDefaultEntitySelected($event)\"\n (entityCleared)=\"onDefaultEntityCleared()\">\n </mm-entity-select-input>\n } @else {\n <span class=\"field-hint\">Select a CK type first to pick a default entity.</span>\n }\n @if (editDefaultRtId) {\n <span class=\"field-hint\">Selected: {{ editDefaultRtId }}</span>\n }\n @if (!editShowInToolbar && !isFollower && !editDefaultRtId) {\n <span class=\"field-error\">A default entity is required when not shown in toolbar.</span>\n }\n <span class=\"field-hint\">\n @if (isFollower) {\n Optional \u2014 used only while the leader has no pick.\n } @else if (editShowInToolbar) {\n Optional \u2014 pre-selected entity when the MeshBoard loads.\n } @else {\n This entity's attributes will be used to populate the variables.\n }\n </span>\n </div>\n\n <div class=\"form-field\">\n <label class=\"field-label\">Child Scope (stream-data)</label>\n <span class=\"field-hint\">\n Optional. Resolves the picked entity to its child source rtIds (one\n association hop) so a stream-data widget can scope to them \u2014 e.g. a\n MeteringPoint resolved to its EnergyMeasurement rtIds. Leave empty to\n scope by the picked entity's own rtId.\n </span>\n <mm-ck-type-selector-input\n [ngModel]=\"editChildScopeTargetCkTypeItem\"\n [placeholder]=\"'Child CK type (e.g. EnergyMeasurement)...'\"\n (ckTypeSelected)=\"onChildScopeCkTypeSelected($event)\"\n (ckTypeCleared)=\"onChildScopeCkTypeCleared()\">\n </mm-ck-type-selector-input>\n <kendo-textbox\n [(ngModel)]=\"editChildScopeRoleId\"\n placeholder=\"Association role (e.g. System/ParentChild)\">\n </kendo-textbox>\n <label class=\"field-label\">Direction</label>\n <select [(ngModel)]=\"editChildScopeDirection\" class=\"child-scope-direction\">\n <option value=\"out\">Outbound (parent \u2192 child)</option>\n <option value=\"in\">Inbound (child \u2192 parent)</option>\n </select>\n </div>\n\n <div class=\"edit-actions mm-dialog-actions\">\n <button kendoButton (click)=\"cancelEdit()\" fillMode=\"flat\">\n Cancel\n </button>\n <button\n kendoButton\n (click)=\"saveEdit()\"\n [disabled]=\"!isEditValid()\"\n themeColor=\"primary\">\n {{ editingIndex !== null ? 'Update' : 'Add' }}\n </button>\n </div>\n </div>\n </div>\n }\n</div>\n", styles: [".entity-selector-editor{display:flex;flex-direction:column;gap:1.25rem}.selector-list{display:flex;flex-direction:column;gap:.5rem}.selector-item{display:flex;justify-content:space-between;align-items:center;padding:.75rem 1rem;background:var(--kendo-color-surface-alt, #fafafa);border:1px solid var(--kendo-color-border, #e0e0e0);border-radius:4px}.selector-item .selector-info{display:flex;flex-direction:column;gap:.25rem;min-width:0;flex:1}.selector-item .selector-label{font-weight:500;color:var(--kendo-color-on-app-surface, #424242)}.selector-item .selector-mode{font-size:.75rem;font-weight:500;color:var(--kendo-color-info, #0288d1)}.selector-item .selector-mode-error{color:var(--kendo-color-error, #d32f2f)}.selector-item .selector-ck-type{font-size:.8rem;color:var(--kendo-color-subtle, #757575);font-family:var(--theme-font-mono)}.selector-item .selector-mappings{font-size:.8rem;color:var(--kendo-color-primary, #3f51b5);font-family:var(--theme-font-mono)}.selector-item .selector-actions{display:flex;gap:.25rem;flex-shrink:0}.selector-item .remove-btn{color:var(--kendo-color-error, #f44336)}.empty-state{padding:1.5rem;text-align:center;color:var(--kendo-color-subtle, #757575);font-size:.875rem}.add-button-row{display:flex;justify-content:flex-start}.edit-panel{border:1px solid var(--kendo-color-border, #e0e0e0);border-radius:4px;overflow:hidden}.edit-panel-header{padding:.625rem 1rem;font-weight:600;font-size:.875rem;color:var(--kendo-color-on-primary, #ffffff);background:var(--kendo-color-primary, #3f51b5)}.edit-form{display:flex;flex-direction:column;gap:1.25rem;padding:1.25rem}.edit-form .form-field{display:flex;flex-direction:column;gap:.5rem}.edit-form .field-label{font-weight:500;font-size:.875rem;color:var(--kendo-color-on-app-surface, #424242)}.edit-form .field-hint{font-size:.75rem;color:var(--kendo-color-subtle, #757575)}.edit-form .field-error{font-size:.75rem;color:var(--kendo-color-error, #f44336)}.edit-form .checkbox-row{display:flex;align-items:center;gap:.5rem}.edit-form .checkbox-row .checkbox-label{margin:0;cursor:pointer}.mappings-list{display:flex;flex-direction:column;gap:.5rem;margin-top:.5rem;padding:.75rem;background:var(--kendo-color-surface-alt, #fafafa);border:1px solid var(--kendo-color-border, #e0e0e0);border-radius:4px}.mappings-header{display:flex;align-items:center;gap:.5rem;font-size:.75rem;font-weight:600;color:var(--kendo-color-subtle, #757575);padding-bottom:.25rem;border-bottom:1px solid var(--kendo-color-border, #e0e0e0)}.mappings-header .mappings-header-attr{min-width:150px}.mappings-header .mappings-header-var{margin-left:1.75rem}.mapping-item{display:flex;align-items:flex-start;gap:.5rem}.mapping-item .mapping-path{font-size:.8rem;font-family:var(--theme-font-mono);color:var(--kendo-color-subtle, #757575);min-width:150px;padding-top:5px}.mapping-item .mapping-arrow{color:var(--kendo-color-subtle, #757575);flex-shrink:0;padding-top:5px}.mapping-item .mapping-var-field{display:flex;flex-direction:column;gap:2px;flex:1;min-width:100px}.mapping-item .mapping-var-input-wrapper{display:flex;align-items:center;gap:0}.mapping-item .mapping-var-prefix{font-family:var(--theme-font-mono);font-size:.875rem;font-weight:600;color:var(--kendo-color-primary, #3f51b5);padding-right:2px;flex-shrink:0}.mapping-item .mapping-var-input{flex:1}.mapping-item .mapping-var-input.invalid ::ng-deep .k-input{border-color:var(--kendo-color-error, #f44336)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox]:not([ngNoCva])[formControlName],input[type=checkbox]:not([ngNoCva])[formControl],input[type=checkbox]:not([ngNoCva])[ngModel]" }, { kind: "directive", type: i1$1.SelectControlValueAccessor, selector: "select:not([multiple]):not([ngNoCva])[formControlName],select:not([multiple]):not([ngNoCva])[formControl],select:not([multiple]):not([ngNoCva])[ngModel]", inputs: ["compareWith"] }, { 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: ButtonModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: InputsModule }, { kind: "component", type: i3.TextBoxComponent, selector: "kendo-textbox", inputs: ["focusableId", "title", "type", "disabled", "readonly", "tabindex", "value", "selectOnFocus", "showSuccessIcon", "showErrorIcon", "clearButton", "successIcon", "successSvgIcon", "errorIcon", "errorSvgIcon", "clearButtonIcon", "clearButtonSvgIcon", "size", "rounded", "fillMode", "tabIndex", "placeholder", "maxlength", "inputAttributes"], outputs: ["valueChange", "inputFocus", "inputBlur", "focus", "blur"], exportAs: ["kendoTextBox"] }, { kind: "directive", type: i3.CheckBoxDirective, selector: "input[kendoCheckBox]", inputs: ["size", "rounded"] }, { kind: "ngmodule", type: LabelModule }, { kind: "directive", type: i4$1.LabelDirective, selector: "label[for]", inputs: ["for", "labelClass"] }, { kind: "ngmodule", type: SVGIconModule }, { 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"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
32421
32843
  }
32422
32844
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: EntitySelectorEditorComponent, decorators: [{
32423
32845
  type: Component,
@@ -32430,7 +32852,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
32430
32852
  SVGIconModule,
32431
32853
  CkTypeSelectorInputComponent,
32432
32854
  EntitySelectInputComponent
32433
- ], changeDetection: ChangeDetectionStrategy.Eager, template: "<div class=\"entity-selector-editor\">\n @if (!isEditing) {\n <!-- Selector List -->\n <div class=\"selector-list\">\n @for (selector of entitySelectors; track selector.id; let i = $index) {\n <div class=\"selector-item\">\n <div class=\"selector-info\">\n <span class=\"selector-label\">{{ selector.label }}</span>\n <span class=\"selector-mode\">{{ getModeLabel(selector) }}</span>\n <span class=\"selector-ck-type\">{{ selector.ckTypeId }}</span>\n <span class=\"selector-mappings\">{{ getMappingSummary(selector) }}</span>\n </div>\n <div class=\"selector-actions\">\n <button\n kendoButton\n [svgIcon]=\"pencilIcon\"\n (click)=\"editSelector(i)\"\n fillMode=\"flat\"\n size=\"small\"\n title=\"Edit selector\"\n [attr.aria-label]=\"'Edit selector ' + (selector.label || selector.ckTypeId)\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"trashIcon\"\n (click)=\"removeSelector(i)\"\n fillMode=\"flat\"\n size=\"small\"\n themeColor=\"error\"\n title=\"Remove selector\"\n [attr.aria-label]=\"'Remove selector ' + (selector.label || selector.ckTypeId)\"\n class=\"remove-btn\">\n </button>\n </div>\n </div>\n }\n\n @if (entitySelectors.length === 0) {\n <div class=\"empty-state\">\n No entity selectors configured. Add one to enable entity-based variables.\n </div>\n }\n </div>\n\n <div class=\"add-button-row\">\n <button\n kendoButton\n [svgIcon]=\"plusIcon\"\n (click)=\"addSelector()\"\n themeColor=\"primary\">\n Add Entity Selector\n </button>\n </div>\n } @else {\n <!-- Edit Form -->\n <div class=\"edit-panel\">\n <div class=\"edit-panel-header\">\n {{ editingIndex !== null ? 'Edit Entity Selector' : 'New Entity Selector' }}\n </div>\n <div class=\"edit-form\">\n <div class=\"form-field\">\n <label class=\"field-label\">ID *</label>\n <kendo-textbox\n [(ngModel)]=\"editId\"\n placeholder=\"e.g., mp\"\n [readonly]=\"editingIndex !== null\">\n </kendo-textbox>\n @if (isDuplicateId(editId, editingIndex)) {\n <span class=\"field-error\">ID must be unique</span>\n }\n <span class=\"field-hint\">Used in URL parameters (es_&lt;id&gt;=&lt;rtId&gt;)</span>\n </div>\n\n <div class=\"form-field\">\n <label class=\"field-label\">Label *</label>\n <kendo-textbox\n [(ngModel)]=\"editLabel\"\n placeholder=\"e.g., Metering Point\">\n </kendo-textbox>\n </div>\n\n <div class=\"form-field\">\n <label class=\"field-label\">CK Type *</label>\n <mm-ck-type-selector-input\n [ngModel]=\"editCkTypeItem\"\n [placeholder]=\"'Select CK type...'\"\n (ckTypeSelected)=\"onCkTypeSelected($event)\"\n (ckTypeCleared)=\"onCkTypeCleared()\">\n </mm-ck-type-selector-input>\n </div>\n\n <div class=\"form-field\">\n <label class=\"field-label\">Attribute Mappings *</label>\n <button\n kendoButton\n (click)=\"selectAttributes()\"\n [disabled]=\"!editCkTypeId\"\n fillMode=\"outline\"\n size=\"small\">\n Select Attributes...\n </button>\n\n @if (editMappings.length > 0) {\n <div class=\"mappings-list\">\n <div class=\"mappings-header\">\n <span class=\"mappings-header-attr\">Attribute</span>\n <span class=\"mappings-header-var\">Variable Name</span>\n </div>\n @for (mapping of editMappings; track mapping.attributePath; let j = $index) {\n <div class=\"mapping-item\">\n <span class=\"mapping-path\">{{ mapping.attributePath }}</span>\n <span class=\"mapping-arrow\">&rarr;</span>\n <div class=\"mapping-var-field\">\n <div class=\"mapping-var-input-wrapper\">\n <span class=\"mapping-var-prefix\">$</span>\n <kendo-textbox\n [(ngModel)]=\"mapping.variableName\"\n size=\"small\"\n class=\"mapping-var-input\"\n placeholder=\"variableName\"\n [class.invalid]=\"getVariableNameError(mapping)\">\n </kendo-textbox>\n </div>\n @if (getVariableNameError(mapping); as error) {\n <span class=\"field-error\">{{ error }}</span>\n }\n </div>\n </div>\n }\n </div>\n }\n </div>\n\n <div class=\"form-field\">\n <div class=\"checkbox-row\">\n <input\n type=\"checkbox\"\n kendoCheckBox\n [(ngModel)]=\"editShowInToolbar\"\n id=\"showInToolbar\" />\n <label class=\"field-label checkbox-label\" for=\"showInToolbar\">Show in Toolbar</label>\n </div>\n <span class=\"field-hint\">\n @if (editShowInToolbar) {\n Users can select an entity from a dropdown in the toolbar.\n } @else {\n Hidden selector \u2014 variables are resolved from the default entity below.\n }\n </span>\n </div>\n\n <div class=\"form-field\">\n <label class=\"field-label\">Default Entity{{ !editShowInToolbar ? ' *' : '' }}</label>\n @if (defaultEntityDataSource) {\n <mm-entity-select-input\n [dataSource]=\"defaultEntityDataSource\"\n [dialogDataSource]=\"defaultEntityDialogDataSource!\"\n [placeholder]=\"'Select default entity...'\"\n [dialogTitle]=\"'Select Default ' + editLabel\"\n (entitySelected)=\"onDefaultEntitySelected($event)\"\n (entityCleared)=\"onDefaultEntityCleared()\">\n </mm-entity-select-input>\n } @else {\n <span class=\"field-hint\">Select a CK type first to pick a default entity.</span>\n }\n @if (editDefaultRtId) {\n <span class=\"field-hint\">Selected: {{ editDefaultRtId }}</span>\n }\n @if (!editShowInToolbar && !editDefaultRtId) {\n <span class=\"field-error\">A default entity is required when not shown in toolbar.</span>\n }\n <span class=\"field-hint\">\n @if (editShowInToolbar) {\n Optional \u2014 pre-selected entity when the MeshBoard loads.\n } @else {\n This entity's attributes will be used to populate the variables.\n }\n </span>\n </div>\n\n <div class=\"form-field\">\n <label class=\"field-label\">Child Scope (stream-data)</label>\n <span class=\"field-hint\">\n Optional. Resolves the picked entity to its child source rtIds (one\n association hop) so a stream-data widget can scope to them \u2014 e.g. a\n MeteringPoint resolved to its EnergyMeasurement rtIds. Leave empty to\n scope by the picked entity's own rtId.\n </span>\n <mm-ck-type-selector-input\n [ngModel]=\"editChildScopeTargetCkTypeItem\"\n [placeholder]=\"'Child CK type (e.g. EnergyMeasurement)...'\"\n (ckTypeSelected)=\"onChildScopeCkTypeSelected($event)\"\n (ckTypeCleared)=\"onChildScopeCkTypeCleared()\">\n </mm-ck-type-selector-input>\n <kendo-textbox\n [(ngModel)]=\"editChildScopeRoleId\"\n placeholder=\"Association role (e.g. System/ParentChild)\">\n </kendo-textbox>\n <label class=\"field-label\">Direction</label>\n <select [(ngModel)]=\"editChildScopeDirection\" class=\"child-scope-direction\">\n <option value=\"out\">Outbound (parent \u2192 child)</option>\n <option value=\"in\">Inbound (child \u2192 parent)</option>\n </select>\n </div>\n\n <div class=\"edit-actions mm-dialog-actions\">\n <button kendoButton (click)=\"cancelEdit()\" fillMode=\"flat\">\n Cancel\n </button>\n <button\n kendoButton\n (click)=\"saveEdit()\"\n [disabled]=\"!isEditValid()\"\n themeColor=\"primary\">\n {{ editingIndex !== null ? 'Update' : 'Add' }}\n </button>\n </div>\n </div>\n </div>\n }\n</div>\n", styles: [".entity-selector-editor{display:flex;flex-direction:column;gap:1.25rem}.selector-list{display:flex;flex-direction:column;gap:.5rem}.selector-item{display:flex;justify-content:space-between;align-items:center;padding:.75rem 1rem;background:var(--kendo-color-surface-alt, #fafafa);border:1px solid var(--kendo-color-border, #e0e0e0);border-radius:4px}.selector-item .selector-info{display:flex;flex-direction:column;gap:.25rem;min-width:0;flex:1}.selector-item .selector-label{font-weight:500;color:var(--kendo-color-on-app-surface, #424242)}.selector-item .selector-mode{font-size:.75rem;font-weight:500;color:var(--kendo-color-info, #0288d1)}.selector-item .selector-ck-type{font-size:.8rem;color:var(--kendo-color-subtle, #757575);font-family:var(--theme-font-mono)}.selector-item .selector-mappings{font-size:.8rem;color:var(--kendo-color-primary, #3f51b5);font-family:var(--theme-font-mono)}.selector-item .selector-actions{display:flex;gap:.25rem;flex-shrink:0}.selector-item .remove-btn{color:var(--kendo-color-error, #f44336)}.empty-state{padding:1.5rem;text-align:center;color:var(--kendo-color-subtle, #757575);font-size:.875rem}.add-button-row{display:flex;justify-content:flex-start}.edit-panel{border:1px solid var(--kendo-color-border, #e0e0e0);border-radius:4px;overflow:hidden}.edit-panel-header{padding:.625rem 1rem;font-weight:600;font-size:.875rem;color:var(--kendo-color-on-primary, #ffffff);background:var(--kendo-color-primary, #3f51b5)}.edit-form{display:flex;flex-direction:column;gap:1.25rem;padding:1.25rem}.edit-form .form-field{display:flex;flex-direction:column;gap:.5rem}.edit-form .field-label{font-weight:500;font-size:.875rem;color:var(--kendo-color-on-app-surface, #424242)}.edit-form .field-hint{font-size:.75rem;color:var(--kendo-color-subtle, #757575)}.edit-form .field-error{font-size:.75rem;color:var(--kendo-color-error, #f44336)}.edit-form .checkbox-row{display:flex;align-items:center;gap:.5rem}.edit-form .checkbox-row .checkbox-label{margin:0;cursor:pointer}.mappings-list{display:flex;flex-direction:column;gap:.5rem;margin-top:.5rem;padding:.75rem;background:var(--kendo-color-surface-alt, #fafafa);border:1px solid var(--kendo-color-border, #e0e0e0);border-radius:4px}.mappings-header{display:flex;align-items:center;gap:.5rem;font-size:.75rem;font-weight:600;color:var(--kendo-color-subtle, #757575);padding-bottom:.25rem;border-bottom:1px solid var(--kendo-color-border, #e0e0e0)}.mappings-header .mappings-header-attr{min-width:150px}.mappings-header .mappings-header-var{margin-left:1.75rem}.mapping-item{display:flex;align-items:flex-start;gap:.5rem}.mapping-item .mapping-path{font-size:.8rem;font-family:var(--theme-font-mono);color:var(--kendo-color-subtle, #757575);min-width:150px;padding-top:5px}.mapping-item .mapping-arrow{color:var(--kendo-color-subtle, #757575);flex-shrink:0;padding-top:5px}.mapping-item .mapping-var-field{display:flex;flex-direction:column;gap:2px;flex:1;min-width:100px}.mapping-item .mapping-var-input-wrapper{display:flex;align-items:center;gap:0}.mapping-item .mapping-var-prefix{font-family:var(--theme-font-mono);font-size:.875rem;font-weight:600;color:var(--kendo-color-primary, #3f51b5);padding-right:2px;flex-shrink:0}.mapping-item .mapping-var-input{flex:1}.mapping-item .mapping-var-input.invalid ::ng-deep .k-input{border-color:var(--kendo-color-error, #f44336)}\n"] }]
32855
+ ], changeDetection: ChangeDetectionStrategy.Eager, template: "<div class=\"entity-selector-editor\">\n @if (!isEditing) {\n <!-- Selector List -->\n <div class=\"selector-list\">\n @for (selector of entitySelectors; track selector.id; let i = $index) {\n <div class=\"selector-item\">\n <div class=\"selector-info\">\n <span class=\"selector-label\">{{ selector.label }}</span>\n <span class=\"selector-mode\" [class.selector-mode-error]=\"isOrphanFollower(selector)\">{{ getModeLabel(selector) }}</span>\n <span class=\"selector-ck-type\">{{ selector.ckTypeId }}</span>\n <span class=\"selector-mappings\">{{ getMappingSummary(selector) }}</span>\n </div>\n <div class=\"selector-actions\">\n <button\n kendoButton\n [svgIcon]=\"pencilIcon\"\n (click)=\"editSelector(i)\"\n fillMode=\"flat\"\n size=\"small\"\n title=\"Edit selector\"\n [attr.aria-label]=\"'Edit selector ' + (selector.label || selector.ckTypeId)\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"trashIcon\"\n (click)=\"removeSelector(i)\"\n fillMode=\"flat\"\n size=\"small\"\n themeColor=\"error\"\n title=\"Remove selector\"\n [attr.aria-label]=\"'Remove selector ' + (selector.label || selector.ckTypeId)\"\n class=\"remove-btn\">\n </button>\n </div>\n </div>\n }\n\n @if (entitySelectors.length === 0) {\n <div class=\"empty-state\">\n No entity selectors configured. Add one to enable entity-based variables.\n </div>\n }\n </div>\n\n <div class=\"add-button-row\">\n <button\n kendoButton\n [svgIcon]=\"plusIcon\"\n (click)=\"addSelector()\"\n themeColor=\"primary\">\n Add Entity Selector\n </button>\n </div>\n } @else {\n <!-- Edit Form -->\n <div class=\"edit-panel\">\n <div class=\"edit-panel-header\">\n {{ editingIndex !== null ? 'Edit Entity Selector' : 'New Entity Selector' }}\n </div>\n <div class=\"edit-form\">\n <div class=\"form-field\">\n <label class=\"field-label\">ID *</label>\n <kendo-textbox\n [(ngModel)]=\"editId\"\n placeholder=\"e.g., mp\"\n [readonly]=\"editingIndex !== null\">\n </kendo-textbox>\n @if (isDuplicateId(editId, editingIndex)) {\n <span class=\"field-error\">ID must be unique</span>\n }\n <span class=\"field-hint\">Used in URL parameters (es_&lt;id&gt;=&lt;rtId&gt;)</span>\n </div>\n\n <div class=\"form-field\">\n <label class=\"field-label\">Label *</label>\n <kendo-textbox\n [(ngModel)]=\"editLabel\"\n placeholder=\"e.g., Metering Point\">\n </kendo-textbox>\n </div>\n\n <div class=\"form-field\">\n <label class=\"field-label\" for=\"followsSelectorId\">Follows</label>\n <select\n id=\"followsSelectorId\"\n class=\"follows-select\"\n [ngModel]=\"editFollowsSelectorId\"\n (ngModelChange)=\"onFollowsChanged($event)\"\n [disabled]=\"followedBy.length > 0 && !editFollowsSelectorId\">\n <option value=\"\">\u2014 none (own picker / default entity) \u2014</option>\n @for (leader of leaderOptions; track leader.id) {\n <option [value]=\"leader.id\">{{ leader.label || leader.id }} ({{ leader.id }})</option>\n }\n @if (isLeaderUnavailable) {\n <option [value]=\"editFollowsSelectorId\">{{ editFollowsSelectorId }} (unavailable)</option>\n }\n </select>\n @if (followError) {\n <span class=\"field-error\">{{ followError }}</span>\n } @else if (followedBy.length > 0) {\n <span class=\"field-hint\">Followed by {{ followedByLabels }} \u2014 a selector that is followed cannot follow another one.</span>\n }\n @if (followNotice) {\n <span class=\"field-hint follow-notice\">{{ followNotice }}</span>\n }\n <span class=\"field-hint\">\n Takes the pick of the selected selector. The CK type is fixed to the leader's.\n A follower never shows a picker and is not written to the URL.\n </span>\n </div>\n\n <div class=\"form-field\">\n <label class=\"field-label\">CK Type *</label>\n @if (isFollower) {\n <kendo-textbox [value]=\"editCkTypeId\" [readonly]=\"true\"></kendo-textbox>\n <span class=\"field-hint\">Fixed to the leader's CK type.</span>\n } @else {\n <mm-ck-type-selector-input\n [ngModel]=\"editCkTypeItem\"\n [placeholder]=\"'Select CK type...'\"\n (ckTypeSelected)=\"onCkTypeSelected($event)\"\n (ckTypeCleared)=\"onCkTypeCleared()\">\n </mm-ck-type-selector-input>\n }\n </div>\n\n <div class=\"form-field\">\n <label class=\"field-label\">Attribute Mappings{{ isFollower ? '' : ' *' }}</label>\n <button\n kendoButton\n (click)=\"selectAttributes()\"\n [disabled]=\"!editCkTypeId\"\n fillMode=\"outline\"\n size=\"small\">\n Select Attributes...\n </button>\n\n @if (editMappings.length > 0) {\n <div class=\"mappings-list\">\n <div class=\"mappings-header\">\n <span class=\"mappings-header-attr\">Attribute</span>\n <span class=\"mappings-header-var\">Variable Name</span>\n </div>\n @for (mapping of editMappings; track mapping.attributePath; let j = $index) {\n <div class=\"mapping-item\">\n <span class=\"mapping-path\">{{ mapping.attributePath }}</span>\n <span class=\"mapping-arrow\">&rarr;</span>\n <div class=\"mapping-var-field\">\n <div class=\"mapping-var-input-wrapper\">\n <span class=\"mapping-var-prefix\">$</span>\n <kendo-textbox\n [(ngModel)]=\"mapping.variableName\"\n size=\"small\"\n class=\"mapping-var-input\"\n placeholder=\"variableName\"\n [class.invalid]=\"getVariableNameError(mapping)\">\n </kendo-textbox>\n </div>\n @if (getVariableNameError(mapping); as error) {\n <span class=\"field-error\">{{ error }}</span>\n }\n </div>\n </div>\n }\n </div>\n }\n </div>\n\n @if (!isFollower) {\n <div class=\"form-field\">\n <div class=\"checkbox-row\">\n <input\n type=\"checkbox\"\n kendoCheckBox\n [(ngModel)]=\"editShowInToolbar\"\n id=\"showInToolbar\" />\n <label class=\"field-label checkbox-label\" for=\"showInToolbar\">Show in Toolbar</label>\n </div>\n <span class=\"field-hint\">\n @if (editShowInToolbar) {\n Users can select an entity from a dropdown in the toolbar.\n } @else {\n Hidden selector \u2014 variables are resolved from the default entity below.\n }\n </span>\n </div>\n }\n\n <div class=\"form-field\">\n <label class=\"field-label\">Default Entity{{ !editShowInToolbar && !isFollower ? ' *' : '' }}</label>\n @if (defaultEntityDataSource) {\n <mm-entity-select-input\n [dataSource]=\"defaultEntityDataSource\"\n [dialogDataSource]=\"defaultEntityDialogDataSource!\"\n [placeholder]=\"'Select default entity...'\"\n [dialogTitle]=\"'Select Default ' + editLabel\"\n (entitySelected)=\"onDefaultEntitySelected($event)\"\n (entityCleared)=\"onDefaultEntityCleared()\">\n </mm-entity-select-input>\n } @else {\n <span class=\"field-hint\">Select a CK type first to pick a default entity.</span>\n }\n @if (editDefaultRtId) {\n <span class=\"field-hint\">Selected: {{ editDefaultRtId }}</span>\n }\n @if (!editShowInToolbar && !isFollower && !editDefaultRtId) {\n <span class=\"field-error\">A default entity is required when not shown in toolbar.</span>\n }\n <span class=\"field-hint\">\n @if (isFollower) {\n Optional \u2014 used only while the leader has no pick.\n } @else if (editShowInToolbar) {\n Optional \u2014 pre-selected entity when the MeshBoard loads.\n } @else {\n This entity's attributes will be used to populate the variables.\n }\n </span>\n </div>\n\n <div class=\"form-field\">\n <label class=\"field-label\">Child Scope (stream-data)</label>\n <span class=\"field-hint\">\n Optional. Resolves the picked entity to its child source rtIds (one\n association hop) so a stream-data widget can scope to them \u2014 e.g. a\n MeteringPoint resolved to its EnergyMeasurement rtIds. Leave empty to\n scope by the picked entity's own rtId.\n </span>\n <mm-ck-type-selector-input\n [ngModel]=\"editChildScopeTargetCkTypeItem\"\n [placeholder]=\"'Child CK type (e.g. EnergyMeasurement)...'\"\n (ckTypeSelected)=\"onChildScopeCkTypeSelected($event)\"\n (ckTypeCleared)=\"onChildScopeCkTypeCleared()\">\n </mm-ck-type-selector-input>\n <kendo-textbox\n [(ngModel)]=\"editChildScopeRoleId\"\n placeholder=\"Association role (e.g. System/ParentChild)\">\n </kendo-textbox>\n <label class=\"field-label\">Direction</label>\n <select [(ngModel)]=\"editChildScopeDirection\" class=\"child-scope-direction\">\n <option value=\"out\">Outbound (parent \u2192 child)</option>\n <option value=\"in\">Inbound (child \u2192 parent)</option>\n </select>\n </div>\n\n <div class=\"edit-actions mm-dialog-actions\">\n <button kendoButton (click)=\"cancelEdit()\" fillMode=\"flat\">\n Cancel\n </button>\n <button\n kendoButton\n (click)=\"saveEdit()\"\n [disabled]=\"!isEditValid()\"\n themeColor=\"primary\">\n {{ editingIndex !== null ? 'Update' : 'Add' }}\n </button>\n </div>\n </div>\n </div>\n }\n</div>\n", styles: [".entity-selector-editor{display:flex;flex-direction:column;gap:1.25rem}.selector-list{display:flex;flex-direction:column;gap:.5rem}.selector-item{display:flex;justify-content:space-between;align-items:center;padding:.75rem 1rem;background:var(--kendo-color-surface-alt, #fafafa);border:1px solid var(--kendo-color-border, #e0e0e0);border-radius:4px}.selector-item .selector-info{display:flex;flex-direction:column;gap:.25rem;min-width:0;flex:1}.selector-item .selector-label{font-weight:500;color:var(--kendo-color-on-app-surface, #424242)}.selector-item .selector-mode{font-size:.75rem;font-weight:500;color:var(--kendo-color-info, #0288d1)}.selector-item .selector-mode-error{color:var(--kendo-color-error, #d32f2f)}.selector-item .selector-ck-type{font-size:.8rem;color:var(--kendo-color-subtle, #757575);font-family:var(--theme-font-mono)}.selector-item .selector-mappings{font-size:.8rem;color:var(--kendo-color-primary, #3f51b5);font-family:var(--theme-font-mono)}.selector-item .selector-actions{display:flex;gap:.25rem;flex-shrink:0}.selector-item .remove-btn{color:var(--kendo-color-error, #f44336)}.empty-state{padding:1.5rem;text-align:center;color:var(--kendo-color-subtle, #757575);font-size:.875rem}.add-button-row{display:flex;justify-content:flex-start}.edit-panel{border:1px solid var(--kendo-color-border, #e0e0e0);border-radius:4px;overflow:hidden}.edit-panel-header{padding:.625rem 1rem;font-weight:600;font-size:.875rem;color:var(--kendo-color-on-primary, #ffffff);background:var(--kendo-color-primary, #3f51b5)}.edit-form{display:flex;flex-direction:column;gap:1.25rem;padding:1.25rem}.edit-form .form-field{display:flex;flex-direction:column;gap:.5rem}.edit-form .field-label{font-weight:500;font-size:.875rem;color:var(--kendo-color-on-app-surface, #424242)}.edit-form .field-hint{font-size:.75rem;color:var(--kendo-color-subtle, #757575)}.edit-form .field-error{font-size:.75rem;color:var(--kendo-color-error, #f44336)}.edit-form .checkbox-row{display:flex;align-items:center;gap:.5rem}.edit-form .checkbox-row .checkbox-label{margin:0;cursor:pointer}.mappings-list{display:flex;flex-direction:column;gap:.5rem;margin-top:.5rem;padding:.75rem;background:var(--kendo-color-surface-alt, #fafafa);border:1px solid var(--kendo-color-border, #e0e0e0);border-radius:4px}.mappings-header{display:flex;align-items:center;gap:.5rem;font-size:.75rem;font-weight:600;color:var(--kendo-color-subtle, #757575);padding-bottom:.25rem;border-bottom:1px solid var(--kendo-color-border, #e0e0e0)}.mappings-header .mappings-header-attr{min-width:150px}.mappings-header .mappings-header-var{margin-left:1.75rem}.mapping-item{display:flex;align-items:flex-start;gap:.5rem}.mapping-item .mapping-path{font-size:.8rem;font-family:var(--theme-font-mono);color:var(--kendo-color-subtle, #757575);min-width:150px;padding-top:5px}.mapping-item .mapping-arrow{color:var(--kendo-color-subtle, #757575);flex-shrink:0;padding-top:5px}.mapping-item .mapping-var-field{display:flex;flex-direction:column;gap:2px;flex:1;min-width:100px}.mapping-item .mapping-var-input-wrapper{display:flex;align-items:center;gap:0}.mapping-item .mapping-var-prefix{font-family:var(--theme-font-mono);font-size:.875rem;font-weight:600;color:var(--kendo-color-primary, #3f51b5);padding-right:2px;flex-shrink:0}.mapping-item .mapping-var-input{flex:1}.mapping-item .mapping-var-input.invalid ::ng-deep .k-input{border-color:var(--kendo-color-error, #f44336)}\n"] }]
32434
32856
  }], propDecorators: { entitySelectors: [{
32435
32857
  type: Input
32436
32858
  }], existingVariableNames: [{
@@ -32461,6 +32883,13 @@ class EntitySelectorToolbarComponent {
32461
32883
  this.updateDataSources();
32462
32884
  }
32463
32885
  }
32886
+ /**
32887
+ * True when the selector renders a picker. Hidden selectors and followers
32888
+ * (AB#7722) never do; a follower's pick is derived from its leader.
32889
+ */
32890
+ showsInToolbar(selector) {
32891
+ return showsInToolbar(selector);
32892
+ }
32464
32893
  /**
32465
32894
  * Returns the data source for a selector.
32466
32895
  */
@@ -32502,7 +32931,7 @@ class EntitySelectorToolbarComponent {
32502
32931
  }
32503
32932
  }
32504
32933
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: EntitySelectorToolbarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
32505
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: EntitySelectorToolbarComponent, isStandalone: true, selector: "mm-entity-selector-toolbar", inputs: { entitySelectors: "entitySelectors" }, outputs: { entitySelected: "entitySelected", entityCleared: "entityCleared" }, usesOnChanges: true, ngImport: i0, template: "<div class=\"entity-selector-toolbar\">\n @for (selector of entitySelectors; track selector.id) {\n @if (selector.showInToolbar !== false) {\n <div class=\"entity-selector-item\">\n <kendo-label [text]=\"selector.label\">\n <mm-entity-select-input\n [dataSource]=\"getDataSource(selector)\"\n [dialogDataSource]=\"getDialogDataSource(selector)\"\n [initialDisplayValue]=\"selector.selectedDisplayName\"\n [placeholder]=\"'Select ' + selector.label + '...'\"\n [dialogTitle]=\"'Select ' + selector.label\"\n (entitySelected)=\"onEntitySelected(selector.id, $event)\"\n (entityCleared)=\"onEntityCleared(selector.id)\">\n </mm-entity-select-input>\n </kendo-label>\n </div>\n }\n }\n</div>\n", styles: [".entity-selector-toolbar{display:flex;align-items:flex-end;gap:1rem;flex-wrap:wrap}.entity-selector-item{display:flex;flex-direction:column}.entity-selector-item kendo-label{display:flex;flex-direction:column;gap:.25rem}.entity-selector-item mm-entity-select-input{min-width:200px;max-width:300px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: i4$1.LabelComponent, selector: "kendo-label", inputs: ["text", "for", "optional", "labelCssStyle", "labelCssClass"], exportAs: ["kendoLabel"] }, { 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"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
32934
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: EntitySelectorToolbarComponent, isStandalone: true, selector: "mm-entity-selector-toolbar", inputs: { entitySelectors: "entitySelectors" }, outputs: { entitySelected: "entitySelected", entityCleared: "entityCleared" }, usesOnChanges: true, ngImport: i0, template: "<div class=\"entity-selector-toolbar\">\n @for (selector of entitySelectors; track selector.id) {\n @if (showsInToolbar(selector)) {\n <div class=\"entity-selector-item\">\n <kendo-label [text]=\"selector.label\">\n <mm-entity-select-input\n [dataSource]=\"getDataSource(selector)\"\n [dialogDataSource]=\"getDialogDataSource(selector)\"\n [initialDisplayValue]=\"selector.selectedDisplayName\"\n [placeholder]=\"'Select ' + selector.label + '...'\"\n [dialogTitle]=\"'Select ' + selector.label\"\n (entitySelected)=\"onEntitySelected(selector.id, $event)\"\n (entityCleared)=\"onEntityCleared(selector.id)\">\n </mm-entity-select-input>\n </kendo-label>\n </div>\n }\n }\n</div>\n", styles: [".entity-selector-toolbar{display:flex;align-items:flex-end;gap:1rem;flex-wrap:wrap}.entity-selector-item{display:flex;flex-direction:column}.entity-selector-item kendo-label{display:flex;flex-direction:column;gap:.25rem}.entity-selector-item mm-entity-select-input{min-width:200px;max-width:300px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: i4$1.LabelComponent, selector: "kendo-label", inputs: ["text", "for", "optional", "labelCssStyle", "labelCssClass"], exportAs: ["kendoLabel"] }, { 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"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
32506
32935
  }
32507
32936
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: EntitySelectorToolbarComponent, decorators: [{
32508
32937
  type: Component,
@@ -32510,7 +32939,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
32510
32939
  CommonModule,
32511
32940
  KENDO_LABELS,
32512
32941
  EntitySelectInputComponent
32513
- ], changeDetection: ChangeDetectionStrategy.Eager, template: "<div class=\"entity-selector-toolbar\">\n @for (selector of entitySelectors; track selector.id) {\n @if (selector.showInToolbar !== false) {\n <div class=\"entity-selector-item\">\n <kendo-label [text]=\"selector.label\">\n <mm-entity-select-input\n [dataSource]=\"getDataSource(selector)\"\n [dialogDataSource]=\"getDialogDataSource(selector)\"\n [initialDisplayValue]=\"selector.selectedDisplayName\"\n [placeholder]=\"'Select ' + selector.label + '...'\"\n [dialogTitle]=\"'Select ' + selector.label\"\n (entitySelected)=\"onEntitySelected(selector.id, $event)\"\n (entityCleared)=\"onEntityCleared(selector.id)\">\n </mm-entity-select-input>\n </kendo-label>\n </div>\n }\n }\n</div>\n", styles: [".entity-selector-toolbar{display:flex;align-items:flex-end;gap:1rem;flex-wrap:wrap}.entity-selector-item{display:flex;flex-direction:column}.entity-selector-item kendo-label{display:flex;flex-direction:column;gap:.25rem}.entity-selector-item mm-entity-select-input{min-width:200px;max-width:300px}\n"] }]
32942
+ ], changeDetection: ChangeDetectionStrategy.Eager, template: "<div class=\"entity-selector-toolbar\">\n @for (selector of entitySelectors; track selector.id) {\n @if (showsInToolbar(selector)) {\n <div class=\"entity-selector-item\">\n <kendo-label [text]=\"selector.label\">\n <mm-entity-select-input\n [dataSource]=\"getDataSource(selector)\"\n [dialogDataSource]=\"getDialogDataSource(selector)\"\n [initialDisplayValue]=\"selector.selectedDisplayName\"\n [placeholder]=\"'Select ' + selector.label + '...'\"\n [dialogTitle]=\"'Select ' + selector.label\"\n (entitySelected)=\"onEntitySelected(selector.id, $event)\"\n (entityCleared)=\"onEntityCleared(selector.id)\">\n </mm-entity-select-input>\n </kendo-label>\n </div>\n }\n }\n</div>\n", styles: [".entity-selector-toolbar{display:flex;align-items:flex-end;gap:1rem;flex-wrap:wrap}.entity-selector-item{display:flex;flex-direction:column}.entity-selector-item kendo-label{display:flex;flex-direction:column;gap:.25rem}.entity-selector-item mm-entity-select-input{min-width:200px;max-width:300px}\n"] }]
32514
32943
  }], propDecorators: { entitySelectors: [{
32515
32944
  type: Input
32516
32945
  }], entitySelected: [{
@@ -34358,13 +34787,13 @@ class MeshBoardViewComponent {
34358
34787
  // Entity Selector computed signals
34359
34788
  entitySelectorsConfig = computed(() => this.stateService.getEntitySelectors(), /* @ts-ignore */
34360
34789
  ...(ngDevMode ? [{ debugName: "entitySelectorsConfig" }] : /* istanbul ignore next */ []));
34361
- hasEntitySelectors = computed(() => !this.hideEntitySelectors() && (this.entitySelectorsConfig()?.some(es => es.showInToolbar !== false) ?? false), /* @ts-ignore */
34790
+ hasEntitySelectors = computed(() => !this.hideEntitySelectors() && (this.entitySelectorsConfig()?.some(es => showsInToolbar(es)) ?? false), /* @ts-ignore */
34362
34791
  ...(ngDevMode ? [{ debugName: "hasEntitySelectors" }] : /* istanbul ignore next */ []));
34363
34792
  unselectedToolbarSelectors = computed(() => {
34364
34793
  if (this.hideEntitySelectors())
34365
34794
  return [];
34366
34795
  const selectors = this.entitySelectorsConfig() ?? [];
34367
- return selectors.filter(es => es.showInToolbar !== false && !es.selectedRtId);
34796
+ return selectors.filter(es => showsInToolbar(es) && !es.selectedRtId);
34368
34797
  }, /* @ts-ignore */
34369
34798
  ...(ngDevMode ? [{ debugName: "unselectedToolbarSelectors" }] : /* istanbul ignore next */ []));
34370
34799
  /**
@@ -34381,6 +34810,7 @@ class MeshBoardViewComponent {
34381
34810
  }, /* @ts-ignore */
34382
34811
  ...(ngDevMode ? [{ debugName: "canResetTimeFilter" }] : /* istanbul ignore next */ []));
34383
34812
  constructor() {
34813
+ this.messages = injectMeshBoardMessages();
34384
34814
  // Effect to sync URL when dashboard changes (after initial load)
34385
34815
  effect(() => {
34386
34816
  const currentRtId = this.stateService.persistedMeshBoardId();
@@ -34495,6 +34925,7 @@ class MeshBoardViewComponent {
34495
34925
  // Load MeshBoard by well-known name from route data
34496
34926
  const widgets = await this.stateService.switchToMeshBoardByWellKnownName(wellKnownName);
34497
34927
  if (widgets) {
34928
+ this.primeEntitySelectorScopes();
34498
34929
  await this.preloadWidgetData(widgets);
34499
34930
  }
34500
34931
  else {
@@ -34505,6 +34936,7 @@ class MeshBoardViewComponent {
34505
34936
  else {
34506
34937
  // Load initial MeshBoard (first available)
34507
34938
  const widgets = await this.stateService.loadInitialMeshBoard();
34939
+ this.primeEntitySelectorScopes();
34508
34940
  await this.preloadWidgetData(widgets);
34509
34941
  }
34510
34942
  // Initialize time filter variables if enabled with stored selection
@@ -34566,11 +34998,13 @@ class MeshBoardViewComponent {
34566
34998
  if (exists) {
34567
34999
  // Switch to the requested MeshBoard
34568
35000
  const switchedWidgets = await this.stateService.switchToMeshBoard(rtId);
35001
+ this.primeEntitySelectorScopes();
34569
35002
  await this.preloadWidgetData(switchedWidgets);
34570
35003
  }
34571
35004
  else {
34572
35005
  // MeshBoard not found - use whatever was loaded initially
34573
35006
  console.warn(`MeshBoard with rtId ${rtId} not found, using default`);
35007
+ this.primeEntitySelectorScopes();
34574
35008
  await this.preloadWidgetData(widgets);
34575
35009
  }
34576
35010
  }
@@ -34877,6 +35311,11 @@ class MeshBoardViewComponent {
34877
35311
  console.warn(`Entity selector '${selectorId}' not found`);
34878
35312
  return;
34879
35313
  }
35314
+ if (isFollowerSelector(selector)) {
35315
+ // A follower's pick is derived from its leader (AB#7722).
35316
+ console.warn(`Entity selector '${selectorId}' follows '${selector.followsSelectorId}' and cannot be set directly`);
35317
+ return;
35318
+ }
34880
35319
  // Update selection state
34881
35320
  this.stateService.updateEntitySelectorSelection(selectorId, rtId, displayName);
34882
35321
  // Fetch entity and set variables from attribute mappings
@@ -34891,12 +35330,80 @@ class MeshBoardViewComponent {
34891
35330
  * Programmatically clears an entity selector value from outside the component.
34892
35331
  */
34893
35332
  clearEntitySelector(selectorId) {
35333
+ const selector = this.stateService.getEntitySelector(selectorId);
35334
+ if (selector && isFollowerSelector(selector)) {
35335
+ console.warn(`Entity selector '${selectorId}' follows '${selector.followsSelectorId}' and cannot be cleared directly`);
35336
+ return;
35337
+ }
35338
+ this.clearSelectorPick(selectorId);
35339
+ // Followers fall back to their own defaultRtId, or to no scope (AB#7722).
35340
+ const followers = this.resetFollowersOf(selectorId);
35341
+ this.stateService.triggerRefresh();
35342
+ if (followers) {
35343
+ void followers.then(() => this.stateService.triggerRefresh());
35344
+ }
35345
+ }
35346
+ /**
35347
+ * Clears one selector's pick, variables and scope state. Without a pick bound
35348
+ * widgets fall back to the persisted scope (AB#7721).
35349
+ */
35350
+ clearSelectorPick(selectorId) {
34894
35351
  this.stateService.updateEntitySelectorSelection(selectorId, undefined);
34895
35352
  this.stateService.clearEntitySelectorVariables(selectorId);
34896
- this.stateService.triggerRefresh();
35353
+ this.scopeRequestSeq.set(selectorId, (this.scopeRequestSeq.get(selectorId) ?? 0) + 1);
35354
+ this.stateService.clearEntitySelectorRtIds(selectorId);
35355
+ }
35356
+ /**
35357
+ * The leader `leaderId` has no pick any more: each of its followers resolves
35358
+ * its own `defaultRtId`, or — without one — has no pick and no scope
35359
+ * (AB#7722). Followers with a default are marked pending synchronously.
35360
+ * Returns a promise for the default resolutions, or `null` when there are none.
35361
+ */
35362
+ resetFollowersOf(leaderId) {
35363
+ const followers = followersOf(this.stateService.getEntitySelectors(), leaderId);
35364
+ const resolutions = [];
35365
+ for (const follower of followers) {
35366
+ if (follower.defaultRtId) {
35367
+ const rtId = follower.defaultRtId;
35368
+ const resolution = this.resolveEntitySelectorVariables(follower, rtId);
35369
+ // The resolution marked the follower pending synchronously; remember
35370
+ // its sequence so a leader pick made meanwhile keeps its selection.
35371
+ const seq = this.scopeRequestSeq.get(follower.id);
35372
+ resolutions.push(resolution.then(displayName => {
35373
+ if (this.scopeRequestSeq.get(follower.id) === seq) {
35374
+ this.stateService.updateEntitySelectorSelection(follower.id, rtId, displayName);
35375
+ }
35376
+ }));
35377
+ }
35378
+ else {
35379
+ this.clearSelectorPick(follower.id);
35380
+ }
35381
+ }
35382
+ return resolutions.length > 0 ? Promise.all(resolutions).then(() => undefined) : null;
34897
35383
  }
34898
35384
  /** Resolution errors for runtime entity variables */
34899
35385
  variableResolutionErrors = this.stateService.variableResolutionErrors;
35386
+ // Assigned in the constructor: a field initialised from a relative import breaks under the
35387
+ // Vitest module runner (see the frontend-libraries CLAUDE.md).
35388
+ messages;
35389
+ /**
35390
+ * One notice per entity selector whose stream-data scope could not be
35391
+ * resolved (AB#7721), so "nothing in scope" (empty widgets, no notice) and
35392
+ * "lookup failed" (empty widgets plus this notice) are distinguishable.
35393
+ */
35394
+ scopeResolutionErrors = computed(() => {
35395
+ const template = this.messages().scopeResolutionFailed;
35396
+ return this.stateService.failedEntitySelectorScopeIds().map(id => {
35397
+ const label = this.stateService.getEntitySelector(id)?.label || id;
35398
+ return { selectorId: id, message: template.split('{label}').join(label) };
35399
+ });
35400
+ }, /* @ts-ignore */
35401
+ ...(ngDevMode ? [{ debugName: "scopeResolutionErrors" }] : /* istanbul ignore next */ []));
35402
+ /**
35403
+ * Per-selector sequence number of the latest scope resolution, so a slower
35404
+ * resolution of an earlier pick never overwrites the scope of a newer pick.
35405
+ */
35406
+ scopeRequestSeq = new Map();
34900
35407
  // ============================================================================
34901
35408
  // Time Filter Event Handlers
34902
35409
  // ============================================================================
@@ -35330,7 +35837,8 @@ class MeshBoardViewComponent {
35330
35837
  async onEntitySelectorSelected(event) {
35331
35838
  const { selectorId, entity } = event;
35332
35839
  const selector = this.stateService.getEntitySelector(selectorId);
35333
- if (!selector)
35840
+ // Followers render no picker; their pick is derived from the leader (AB#7722).
35841
+ if (!selector || isFollowerSelector(selector))
35334
35842
  return;
35335
35843
  // Update selector selection state
35336
35844
  this.stateService.updateEntitySelectorSelection(selectorId, entity.rtId, entity.displayName);
@@ -35347,12 +35855,14 @@ class MeshBoardViewComponent {
35347
35855
  */
35348
35856
  async onEntitySelectorCleared(event) {
35349
35857
  const { selectorId } = event;
35350
- // Clear selection state
35351
- this.stateService.updateEntitySelectorSelection(selectorId, undefined);
35352
- // Clear variables for this selector
35353
- this.stateService.clearEntitySelectorVariables(selectorId);
35354
- // Clear the resolved stream-data scope rtIds for this selector
35355
- this.stateService.clearEntitySelectorRtIds(selectorId);
35858
+ const selector = this.stateService.getEntitySelector(selectorId);
35859
+ if (selector && isFollowerSelector(selector))
35860
+ return;
35861
+ // Clear selection, variables and the resolved stream-data scope: without a
35862
+ // pick bound widgets fall back to the persisted scope (AB#7721).
35863
+ this.clearSelectorPick(selectorId);
35864
+ // Followers fall back to their own defaultRtId, or to no scope (AB#7722).
35865
+ await this.resetFollowersOf(selectorId);
35356
35866
  // Sync to URL
35357
35867
  this.writeEntitySelectorsToUrl();
35358
35868
  // Refresh widgets
@@ -35362,84 +35872,179 @@ class MeshBoardViewComponent {
35362
35872
  * Initializes entity selectors from URL params, route data, or defaults.
35363
35873
  */
35364
35874
  async initializeEntitySelectors() {
35875
+ const picks = this.initialEntitySelectorPicks(true);
35876
+ if (picks.length === 0)
35877
+ return;
35878
+ // Mark every picked scope pending before the first await, so no bound
35879
+ // widget queries the persisted scope while the picks resolve (AB#7721).
35880
+ this.primeEntitySelectorScopes();
35881
+ for (const { selector, rtId, viaLeader } of picks) {
35882
+ // A follower whose leader has a pick is resolved together with the
35883
+ // leader (one entity fetch, AB#7722).
35884
+ if (viaLeader)
35885
+ continue;
35886
+ const displayName = await this.resolveEntitySelectorVariables(selector, rtId);
35887
+ this.stateService.updateEntitySelectorSelection(selector.id, rtId, displayName);
35888
+ }
35889
+ // Widgets reload only when their config reference changes. Bound widgets
35890
+ // rendered while their scope was pending must now load with the resolved
35891
+ // scope (AB#7721).
35892
+ this.stateService.triggerRefresh();
35893
+ }
35894
+ /**
35895
+ * The initial pick of each entity selector. Priority: route data > URL
35896
+ * params > defaultRtId. Selectors without a pick are left out.
35897
+ *
35898
+ * Followers (AB#7722) never read route data or the URL: a follower takes its
35899
+ * leader's initial pick (`viaLeader: true`, resolved together with the
35900
+ * leader) and falls back to its own `defaultRtId` only when the leader has
35901
+ * none. An unknown or chained leader counts as "leader has no pick"; with
35902
+ * `warn` set this logs a console warning.
35903
+ */
35904
+ initialEntitySelectorPicks(warn = false) {
35365
35905
  const selectors = this.stateService.getEntitySelectors();
35366
35906
  if (selectors.length === 0)
35367
- return;
35368
- // Read URL params
35907
+ return [];
35369
35908
  const urlSelections = this.readEntitySelectorsFromUrl();
35370
- // Read route data
35371
35909
  const routeData = this.route.snapshot.data['entitySelectors'];
35910
+ const ownPick = (selector) => routeData?.[selector.id] ?? urlSelections.get(selector.id) ?? selector.defaultRtId;
35911
+ const picks = [];
35372
35912
  for (const selector of selectors) {
35373
- // Priority: route data > URL params > defaultRtId
35374
- const rtId = routeData?.[selector.id]
35375
- ?? urlSelections.get(selector.id)
35376
- ?? selector.defaultRtId;
35377
- if (rtId) {
35378
- const displayName = await this.resolveEntitySelectorVariables(selector, rtId);
35379
- this.stateService.updateEntitySelectorSelection(selector.id, rtId, displayName);
35913
+ const leader = resolveFollowLeader(selectors, selector);
35914
+ if (leader.kind === 'none') {
35915
+ const rtId = ownPick(selector);
35916
+ if (rtId)
35917
+ picks.push({ selector, rtId, viaLeader: false });
35918
+ continue;
35919
+ }
35920
+ if (leader.kind === 'invalid' && warn) {
35921
+ console.warn(`Entity selector '${selector.id}' follows '${leader.leaderId}', which ${leader.problem === 'unknown' ? 'does not exist' : leader.problem === 'self' ? 'is itself' : 'follows another selector'} — treated as "leader has no pick"`);
35922
+ }
35923
+ const leaderRtId = leader.kind === 'leader' ? ownPick(leader.leader) : undefined;
35924
+ if (leaderRtId) {
35925
+ picks.push({ selector, rtId: leaderRtId, viaLeader: true });
35926
+ }
35927
+ else if (selector.defaultRtId) {
35928
+ picks.push({ selector, rtId: selector.defaultRtId, viaLeader: false });
35929
+ }
35930
+ }
35931
+ return picks;
35932
+ }
35933
+ /**
35934
+ * Marks the scope of every selector that has an initial pick as pending
35935
+ * (AB#7721). Called synchronously right after a board is loaded, before
35936
+ * change detection renders its widgets, so a bound stream-data widget starts
35937
+ * in its loading state instead of querying the persisted (unscoped) scope.
35938
+ * Selectors without a pick stay without scope state (persisted scope).
35939
+ */
35940
+ primeEntitySelectorScopes() {
35941
+ for (const { selector } of this.initialEntitySelectorPicks()) {
35942
+ if (!this.stateService.getEntitySelectorScopeState(selector.id)) {
35943
+ this.stateService.setEntitySelectorScopePending(selector.id);
35380
35944
  }
35381
35945
  }
35382
35946
  }
35383
35947
  /**
35384
- * Resolves variables for an entity selector by fetching entity attributes.
35948
+ * Resolves variables for an entity selector by fetching entity attributes,
35949
+ * then its stream-data scope.
35950
+ *
35951
+ * A leader's pick is also the pick of all its followers (AB#7722): they are
35952
+ * marked pending together with the leader, the picked entity is fetched
35953
+ * once, and each follower gets its own variables (its attribute mappings,
35954
+ * `<id>_rtId`, `<id>_rtCkTypeId`), its own scope (its `childScope`) and the
35955
+ * leader's selection. Returns the picked entity's display name.
35385
35956
  */
35386
35957
  async resolveEntitySelectorVariables(selector, rtId) {
35958
+ const selectors = this.stateService.getEntitySelectors();
35959
+ const resolved = effectiveSelector(selectors, selector);
35960
+ const followers = followersOf(selectors, selector.id).map(f => effectiveSelector(selectors, f));
35961
+ const targets = [resolved, ...followers];
35962
+ // The scope of the new pick is unknown until it resolves: bound widgets
35963
+ // must neither keep the previous pick's scope nor fall back to the
35964
+ // persisted one (AB#7721). Followers are pending while the leader is.
35965
+ const seqs = targets.map(t => this.beginScopeResolution(t.id));
35966
+ const failAll = () => targets.forEach((t, i) => this.failScopeResolution(t.id, seqs[i]));
35387
35967
  try {
35388
35968
  const entity = await firstValueFrom(this.dataService.fetchRuntimeEntity({
35389
35969
  type: 'runtimeEntity',
35390
- ckTypeId: selector.ckTypeId,
35970
+ ckTypeId: resolved.ckTypeId,
35391
35971
  rtId
35392
35972
  }));
35393
35973
  if (!entity) {
35394
35974
  console.warn(`Entity not found for selector '${selector.id}': ${rtId}`);
35975
+ // The pick cannot be resolved: bound widgets show no data instead of
35976
+ // waiting forever or falling back to the persisted scope (AB#7721).
35977
+ failAll();
35395
35978
  return undefined;
35396
35979
  }
35397
- const values = selector.attributeMappings.map(mapping => {
35398
- const attr = entity.attributes.find(a => a.attributeName === mapping.attributePath);
35399
- return {
35400
- name: mapping.variableName,
35401
- value: attr ? String(attr.value ?? '') : '',
35402
- type: mapping.attributeValueType
35403
- ? this.variableService.mapAttributeTypeToVariableType(mapping.attributeValueType)
35404
- : 'string'
35405
- };
35980
+ // A newer pick (of the leader, or a follower's own default) may have
35981
+ // started meanwhile; never overwrite its variables or selection.
35982
+ const isCurrent = (i) => this.scopeRequestSeq.get(targets[i].id) === seqs[i];
35983
+ targets.forEach((target, i) => {
35984
+ if (isCurrent(i)) {
35985
+ this.stateService.setEntitySelectorVariables(target.id, this.selectorVariables(target, entity, rtId));
35986
+ }
35406
35987
  });
35407
- // Auto-expose the selected entity's rtId as `$<selectorId>_rtId` so SD/runtime
35408
- // widgets can scope to the picked asset (parent-association filter) without the
35409
- // designer adding a fake attribute mapping. An explicit mapping to the same
35410
- // variable name wins (skip the synthetic one if already present).
35411
- const rtIdVariableName = `${selector.id}_rtId`;
35412
- if (!values.some(v => v.name === rtIdVariableName)) {
35413
- values.push({ name: rtIdVariableName, value: rtId, type: 'string' });
35414
- }
35415
- // Auto-expose the selected entity's CK type as `$<selectorId>_rtCkTypeId`
35416
- // so entity-display widgets (e.g. the entity card) can resolve both the
35417
- // rtId and its type from a single board-level selection. Prefer the picked
35418
- // entity's actual type (it may be a subtype of the selector's configured
35419
- // `ckTypeId`); fall back to the selector's type. An explicit mapping to the
35420
- // same variable name wins.
35421
- const rtCkTypeIdVariableName = `${selector.id}_rtCkTypeId`;
35422
- if (!values.some(v => v.name === rtCkTypeIdVariableName)) {
35423
- values.push({ name: rtCkTypeIdVariableName, value: entity.ckTypeId ?? selector.ckTypeId, type: 'string' });
35424
- }
35425
- this.stateService.setEntitySelectorVariables(selector.id, values);
35426
- // Resolve the stream-data scope rtIds for this selection (one-hop
35427
- // childScope traversal, or the picked entity itself). Runs alongside the
35428
- // variable resolution so widgets bound to this selector can scope their
35429
- // stream-data query by the resolved source rtIds.
35430
- await this.resolveEntitySelectorScopeRtIds(selector, rtId);
35431
35988
  // Label the selection the way the pick dialog does: by the engine's
35432
35989
  // display name (the type's displayNameRule — "2026" for a fiscal year),
35433
35990
  // not the well-known name, which most entities do not carry. Without it
35434
35991
  // a board opened on its defaultRtId showed the raw rtId in the toolbar
35435
35992
  // until the user re-picked the very same entity.
35436
- return entity.rtDisplayName || entity.rtWellKnownName || entity.rtId;
35993
+ const displayName = entity.rtDisplayName || entity.rtWellKnownName || entity.rtId;
35994
+ followers.forEach((follower, i) => {
35995
+ if (isCurrent(i + 1)) {
35996
+ this.stateService.updateEntitySelectorSelection(follower.id, rtId, displayName);
35997
+ }
35998
+ });
35999
+ // Resolve the stream-data scope rtIds for this selection (one-hop
36000
+ // childScope traversal, or the picked entity itself), per selector, so
36001
+ // widgets bound to the leader or any follower scope their stream-data
36002
+ // query by the resolved source rtIds.
36003
+ await Promise.all(targets.map((t, i) => this.resolveEntitySelectorScopeRtIds(t, rtId, seqs[i])));
36004
+ return displayName;
35437
36005
  }
35438
36006
  catch (error) {
35439
36007
  console.error(`Error resolving entity selector '${selector.id}':`, error);
36008
+ // Same as above: a failed pick is an empty scope, never the persisted one.
36009
+ failAll();
35440
36010
  return undefined;
35441
36011
  }
35442
36012
  }
36013
+ /**
36014
+ * The variables one selector derives from the picked entity: its attribute
36015
+ * mappings plus the auto-exposed `<id>_rtId` and `<id>_rtCkTypeId`.
36016
+ */
36017
+ selectorVariables(selector, entity, rtId) {
36018
+ const values = selector.attributeMappings.map(mapping => {
36019
+ const attr = entity.attributes.find(a => a.attributeName === mapping.attributePath);
36020
+ return {
36021
+ name: mapping.variableName,
36022
+ value: attr ? String(attr.value ?? '') : '',
36023
+ type: mapping.attributeValueType
36024
+ ? this.variableService.mapAttributeTypeToVariableType(mapping.attributeValueType)
36025
+ : 'string'
36026
+ };
36027
+ });
36028
+ // Auto-expose the selected entity's rtId as `$<selectorId>_rtId` so SD/runtime
36029
+ // widgets can scope to the picked asset (parent-association filter) without the
36030
+ // designer adding a fake attribute mapping. An explicit mapping to the same
36031
+ // variable name wins (skip the synthetic one if already present).
36032
+ const rtIdVariableName = `${selector.id}_rtId`;
36033
+ if (!values.some(v => v.name === rtIdVariableName)) {
36034
+ values.push({ name: rtIdVariableName, value: rtId, type: 'string' });
36035
+ }
36036
+ // Auto-expose the selected entity's CK type as `$<selectorId>_rtCkTypeId`
36037
+ // so entity-display widgets (e.g. the entity card) can resolve both the
36038
+ // rtId and its type from a single board-level selection. Prefer the picked
36039
+ // entity's actual type (it may be a subtype of the selector's configured
36040
+ // `ckTypeId`); fall back to the selector's type. An explicit mapping to the
36041
+ // same variable name wins.
36042
+ const rtCkTypeIdVariableName = `${selector.id}_rtCkTypeId`;
36043
+ if (!values.some(v => v.name === rtCkTypeIdVariableName)) {
36044
+ values.push({ name: rtCkTypeIdVariableName, value: entity.ckTypeId ?? selector.ckTypeId, type: 'string' });
36045
+ }
36046
+ return values;
36047
+ }
35443
36048
  /**
35444
36049
  * Resolves the set of source rtIds that a stream-data widget bound to this
35445
36050
  * selector should be scoped to, and caches them on the state service.
@@ -35449,7 +36054,10 @@ class MeshBoardViewComponent {
35449
36054
  * become the scope (e.g. MeteringPoint → its EnergyMeasurement rtIds). With
35450
36055
  * no childScope, the picked entity's own rtId is the scope (direct keying).
35451
36056
  */
35452
- async resolveEntitySelectorScopeRtIds(selector, rtId) {
36057
+ async resolveEntitySelectorScopeRtIds(selector, rtId, seq) {
36058
+ const isCurrent = () => this.scopeRequestSeq.get(selector.id) === seq;
36059
+ if (!isCurrent())
36060
+ return;
35453
36061
  const childScope = selector.childScope;
35454
36062
  if (!childScope?.targetCkTypeId || !childScope.roleId) {
35455
36063
  this.stateService.setEntitySelectorRtIds(selector.id, [rtId]);
@@ -35460,16 +36068,39 @@ class MeshBoardViewComponent {
35460
36068
  // Parent → children via System/ParentChild is an INBOUND hop (the child
35461
36069
  // owns the association to its parent), so 'in' is the default.
35462
36070
  childScope.direction ?? 'in'));
36071
+ if (!isCurrent())
36072
+ return;
35463
36073
  const childRtIds = (targets ?? [])
35464
36074
  .map(t => String(t.rtId))
35465
36075
  .filter(id => id.length > 0);
36076
+ // An empty list is a resolved EMPTY scope: bound widgets show no data and
36077
+ // send no query — never the persisted scope, which would show rows of
36078
+ // other entities (AB#7721).
35466
36079
  this.stateService.setEntitySelectorRtIds(selector.id, childRtIds);
35467
36080
  }
35468
36081
  catch (error) {
36082
+ if (!isCurrent())
36083
+ return;
36084
+ // Logged once per failed resolution; the view shows a notice and bound
36085
+ // widgets show no data (AB#7721).
35469
36086
  console.error(`Error resolving child scope for selector '${selector.id}':`, error);
35470
- // Empty scope is safer than a stale one: the bound widget falls back to
35471
- // its persisted scope rather than querying the wrong asset.
35472
- this.stateService.setEntitySelectorRtIds(selector.id, []);
36087
+ this.stateService.setEntitySelectorScopeFailed(selector.id);
36088
+ }
36089
+ }
36090
+ /**
36091
+ * Starts the scope resolution of a new pick: marks the scope pending and
36092
+ * returns the sequence number that identifies this resolution (AB#7721).
36093
+ */
36094
+ beginScopeResolution(selectorId) {
36095
+ const seq = (this.scopeRequestSeq.get(selectorId) ?? 0) + 1;
36096
+ this.scopeRequestSeq.set(selectorId, seq);
36097
+ this.stateService.setEntitySelectorScopePending(selectorId);
36098
+ return seq;
36099
+ }
36100
+ /** Marks the scope failed — unless a newer pick has started resolving meanwhile. */
36101
+ failScopeResolution(selectorId, seq) {
36102
+ if (this.scopeRequestSeq.get(selectorId) === seq) {
36103
+ this.stateService.setEntitySelectorScopeFailed(selectorId);
35473
36104
  }
35474
36105
  }
35475
36106
  /**
@@ -35480,6 +36111,9 @@ class MeshBoardViewComponent {
35480
36111
  const selectors = this.stateService.getEntitySelectors();
35481
36112
  const params = {};
35482
36113
  for (const selector of selectors) {
36114
+ // Followers are derived from their leader and never serialized (AB#7722).
36115
+ if (isFollowerSelector(selector))
36116
+ continue;
35483
36117
  params[`es_${selector.id}`] = selector.selectedRtId ?? null;
35484
36118
  }
35485
36119
  // Same single-URL serialization as writeTimeFilterToUrl: no relative
@@ -35533,7 +36167,7 @@ class MeshBoardViewComponent {
35533
36167
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: MeshBoardViewComponent, isStandalone: true, selector: "mm-meshboard-view", inputs: { headerMode: { classPropertyName: "headerMode", publicName: "headerMode", isSignal: true, isRequired: false, transformFunction: null }, chrome: { classPropertyName: "chrome", publicName: "chrome", isSignal: true, isRequired: false, transformFunction: null } }, providers: [
35534
36168
  { provide: HAS_UNSAVED_CHANGES, useExisting: MeshBoardViewComponent },
35535
36169
  AutoRefreshTimerService
35536
- ], viewQueries: [{ propertyName: "tileLayout", first: true, predicate: TileLayoutComponent, descendants: true }], hostDirectives: [{ directive: i1$8.UnsavedChangesDirective }], ngImport: i0, template: "<div class=\"meshboard-view\" [class.chrome-plain]=\"effectiveChrome() === 'plain'\">\n @if (isLoading()) {\n <div class=\"loading-container\">\n <div class=\"k-loading-mask\">\n <span class=\"k-loading-text\">Loading MeshBoard...</span>\n <div class=\"k-loading-image\"></div>\n <div class=\"k-loading-color\"></div>\n </div>\n </div>\n } @else if (isModelAvailable() === false) {\n <div class=\"model-unavailable\">\n <div class=\"message-card\">\n <kendo-svg-icon [icon]=\"xIcon\" size=\"xlarge\"></kendo-svg-icon>\n <h2>MeshBoard Not Available</h2>\n <p>The MeshBoard feature requires the CK model 'System.UI' version 1.0.1 or higher.</p>\n <p>Please install the required model in your tenant to use this feature.</p>\n </div>\n </div>\n } @else if (notFoundError()) {\n <div class=\"model-unavailable\">\n <div class=\"message-card\">\n <kendo-svg-icon [icon]=\"xIcon\" size=\"xlarge\"></kendo-svg-icon>\n <h2>MeshBoard Not Found</h2>\n <p>{{ notFoundError() }}</p>\n <p>To create a MeshBoard with a Well-Known Name:</p>\n <ol>\n <li>Go to the <a [routerLink]=\"meshBoardPageLink()\">MeshBoard</a> page</li>\n <li>Open Settings</li>\n <li>Set the \"Well-Known Name\" field</li>\n <li>Save the MeshBoard</li>\n </ol>\n </div>\n </div>\n } @else if (isInitialized()) {\n <!-- Toolbar (headerMode: full = name + description + controls, compact = controls only, none = no row) -->\n @if (effectiveHeaderMode() !== 'none') {\n <div class=\"meshboard-toolbar\" [class.compact]=\"effectiveHeaderMode() === 'compact'\">\n @if (effectiveHeaderMode() === 'full') {\n <div class=\"toolbar-left\">\n <h2 class=\"meshboard-title\">{{ config().name }}</h2>\n @if (config().description) {\n <span class=\"meshboard-description\">{{ config().description }}</span>\n }\n </div>\n }\n\n <!-- Time Filter (center) -->\n @if (isTimeFilterEnabled()) {\n <div class=\"toolbar-center\">\n <mm-time-range-picker\n [config]=\"timeFilterConfig()?.pickerConfig ?? {}\"\n [labels]=\"timeRangeLabels()\"\n [initialSelection]=\"initialTimeSelection()\"\n (rangeChange)=\"onTimeRangeChange($event)\"\n (selectionChange)=\"onTimeSelectionChange($event)\">\n </mm-time-range-picker>\n @if (canResetTimeFilter()) {\n <button\n kendoButton\n [svgIcon]=\"undoIcon\"\n (click)=\"resetTimeFilterToDefault()\"\n title=\"Reset to default time filter\"\n aria-label=\"Reset to default time filter\"\n fillMode=\"flat\"\n size=\"small\">\n </button>\n }\n </div>\n }\n\n <!-- Entity Selectors -->\n @if (hasEntitySelectors()) {\n @if (isTimeFilterEnabled()) {\n <div class=\"toolbar-separator\"></div>\n }\n <div class=\"toolbar-entity-selectors\">\n <mm-entity-selector-toolbar\n [entitySelectors]=\"entitySelectorsConfig()\"\n (entitySelected)=\"onEntitySelectorSelected($event)\"\n (entityCleared)=\"onEntitySelectorCleared($event)\">\n </mm-entity-selector-toolbar>\n </div>\n }\n\n @if (isTimeFilterEnabled() || hasEntitySelectors()) {\n <div class=\"toolbar-separator\"></div>\n }\n <div class=\"toolbar-right mm-toolbar-actions\">\n @if (!isReadonly()) {\n <!-- Manager Button (icon only) -->\n <button\n kendoButton\n [svgIcon]=\"gridLayoutIcon\"\n (click)=\"openManager()\"\n title=\"Manage MeshBoards\"\n aria-label=\"Manage MeshBoards\"\n fillMode=\"flat\">\n </button>\n\n <!-- Settings Button (icon only) -->\n <button\n kendoButton\n [svgIcon]=\"gearIcon\"\n (click)=\"openSettings()\"\n title=\"MeshBoard Settings\"\n aria-label=\"MeshBoard Settings\"\n fillMode=\"flat\">\n </button>\n }\n\n <!-- Refresh Button (icon only) -->\n <button\n kendoButton\n [svgIcon]=\"arrowRotateCwIcon\"\n (click)=\"refresh()\"\n title=\"Refresh All Widgets\"\n aria-label=\"Refresh All Widgets\"\n fillMode=\"flat\">\n </button>\n\n @if (!isReadonly()) {\n <!-- Edit Mode Toggle (icon only) -->\n @if (!isEditMode()) {\n <button\n kendoButton\n [svgIcon]=\"pencilIcon\"\n (click)=\"toggleEditMode()\"\n title=\"Enter Edit Mode\"\n aria-label=\"Enter Edit Mode\"\n fillMode=\"flat\">\n </button>\n } @else {\n <button\n kendoButton\n [svgIcon]=\"xIcon\"\n (click)=\"cancelEdit()\"\n title=\"Cancel Edit Mode\"\n aria-label=\"Cancel Edit Mode\"\n fillMode=\"flat\">\n </button>\n }\n\n <!-- Add Widget Button (with text) -->\n <button\n kendoButton\n [svgIcon]=\"plusIcon\"\n (click)=\"openAddWidget()\"\n title=\"Add Widget\"\n themeColor=\"primary\">\n Add Widget\n </button>\n\n <!-- Save Button (with text) -->\n @if (canSave()) {\n <button\n kendoButton\n [svgIcon]=\"saveIcon\"\n (click)=\"save()\"\n [disabled]=\"isSaving()\"\n title=\"Save MeshBoard\"\n themeColor=\"primary\">\n {{ isSaving() ? 'Saving...' : 'Save' }}\n </button>\n }\n }\n </div>\n </div>\n }\n\n <!-- Entity Selector Hint -->\n @if (unselectedToolbarSelectors().length > 0) {\n <div class=\"entity-selector-hint\">\n <kendo-svg-icon [icon]=\"infoCircleIcon\" size=\"medium\"></kendo-svg-icon>\n <span>\n Please select\n @for (selector of unselectedToolbarSelectors(); track selector.id; let last = $last) {\n <strong>{{ selector.label }}</strong>@if (!last) {, }\n }\n to display dashboard data.\n </span>\n </div>\n }\n\n <!-- Variable Resolution Errors -->\n @if (variableResolutionErrors().length > 0) {\n <div class=\"variable-resolution-errors\">\n <strong>Variable resolution errors:</strong>\n @for (error of variableResolutionErrors(); track error.variableName) {\n <span class=\"error-item\">{{ error.variableName }}: {{ error.message }}</span>\n }\n </div>\n }\n\n <!-- Banner Zone: widgets placed in the banner stack above the grid -->\n @if (isEditMode()) {\n <div class=\"zone-label banner-zone-label\">Banner Zone</div>\n }\n @for (widget of bannerWidgets(); track trackByWidgetId($index, widget); let first = $first; let last = $last) {\n <div class=\"banner-zone-item\" [class.banner-edit]=\"isEditMode()\">\n @if (isEditMode()) {\n <div class=\"banner-edit-header\">\n <span class=\"widget-title\">{{ widget.title }}</span>\n <div class=\"widget-actions\">\n <button\n kendoButton\n [svgIcon]=\"arrowUpIcon\"\n (click)=\"moveBannerUp(widget.id)\"\n fillMode=\"flat\"\n size=\"small\"\n [disabled]=\"first\"\n title=\"Move up\"\n [attr.aria-label]=\"'Move up ' + widget.title\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"arrowDownIcon\"\n (click)=\"moveBannerDown(widget.id)\"\n fillMode=\"flat\"\n size=\"small\"\n [disabled]=\"last\"\n title=\"Move down\"\n [attr.aria-label]=\"'Move down ' + widget.title\">\n </button>\n @if (supportsConfiguration(widget)) {\n <button\n kendoButton\n [svgIcon]=\"linkIcon\"\n (click)=\"openWidgetConfig(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"config-widget-btn\"\n [class.needs-config]=\"isWidgetUnconfigured(widget)\"\n [title]=\"isWidgetUnconfigured(widget) ? 'Configure data source (required)' : 'Configure data source'\"\n [attr.aria-label]=\"(isWidgetUnconfigured(widget) ? 'Configure data source (required) ' : 'Configure data source ') + widget.title\">\n </button>\n }\n <button\n kendoButton\n [svgIcon]=\"gearIcon\"\n (click)=\"openEditWidgetDialog(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"edit-widget-btn\"\n title=\"Edit widget\"\n [attr.aria-label]=\"'Edit widget ' + widget.title\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"trashIcon\"\n (click)=\"removeWidget(widget.id)\"\n fillMode=\"flat\"\n themeColor=\"error\"\n size=\"small\"\n class=\"remove-widget-btn\"\n title=\"Delete widget\"\n [attr.aria-label]=\"'Delete widget ' + widget.title\">\n </button>\n </div>\n </div>\n }\n @if (getWidgetComponentType(widget.type); as componentType) {\n <ng-container *ngComponentOutlet=\"componentType; inputs: { config: widget }\"></ng-container>\n }\n </div>\n }\n\n <!-- MeshBoard Grid -->\n @if (isEditMode()) {\n <div class=\"zone-label grid-zone-label\">Grid Zone</div>\n }\n @if (hasGridWidgets()) {\n <kendo-tilelayout\n [columns]=\"displayColumns()\"\n [autoFlow]=\"displayAutoFlow()\"\n [rowHeight]=\"config().rowHeight\"\n [gap]=\"config().gap\"\n [class.has-banners]=\"bannerWidgets().length > 0 && !isEditMode()\"\n [resizable]=\"canEditLayout()\"\n [reorderable]=\"canEditLayout()\"\n (reorder)=\"onReorder($event)\"\n (resize)=\"onResize($event)\">\n @for (placement of displayGridWidgets(); track placement.widget.id) {\n @let widget = placement.widget;\n <kendo-tilelayout-item\n [col]=\"$any(placement.col)\"\n [row]=\"$any(placement.row)\"\n [colSpan]=\"placement.colSpan\"\n [rowSpan]=\"placement.rowSpan\">\n <kendo-tilelayout-item-header>\n <div class=\"widget-header\"\n [class.unconfigured]=\"isWidgetUnconfigured(widget)\"\n [class.chromeless-header]=\"widget.chromeless && !isEditMode()\">\n @if (!(widget.chromeless && !isEditMode())) {\n <span class=\"widget-title\">{{ widget.title }}</span>\n @if (isWidgetUnconfigured(widget)) {\n <span class=\"unconfigured-badge\" title=\"Widget needs configuration\">!</span>\n }\n }\n @if (isEditMode()) {\n <div class=\"widget-actions\" (pointerdown)=\"$event.stopPropagation()\">\n @if (supportsConfiguration(widget)) {\n <button\n kendoButton\n [svgIcon]=\"linkIcon\"\n (click)=\"openWidgetConfig(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"config-widget-btn\"\n [class.needs-config]=\"isWidgetUnconfigured(widget)\"\n [title]=\"isWidgetUnconfigured(widget) ? 'Configure data source (required)' : 'Configure data source'\"\n [attr.aria-label]=\"(isWidgetUnconfigured(widget) ? 'Configure data source (required) ' : 'Configure data source ') + widget.title\">\n </button>\n }\n <button\n kendoButton\n [svgIcon]=\"gearIcon\"\n (click)=\"openEditWidgetDialog(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"edit-widget-btn\"\n title=\"Edit position\"\n [attr.aria-label]=\"'Edit position ' + widget.title\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"copyIcon\"\n (click)=\"duplicateWidget(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"duplicate-widget-btn\"\n title=\"Duplicate widget\"\n [attr.aria-label]=\"'Duplicate widget ' + widget.title\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"trashIcon\"\n (click)=\"removeWidget(widget.id)\"\n fillMode=\"flat\"\n themeColor=\"error\"\n size=\"small\"\n class=\"remove-widget-btn\"\n title=\"Delete widget\"\n [attr.aria-label]=\"'Delete widget ' + widget.title\">\n </button>\n </div>\n }\n </div>\n </kendo-tilelayout-item-header>\n <kendo-tilelayout-item-body>\n @if (getWidgetComponentType(widget.type); as componentType) {\n <ng-container *ngComponentOutlet=\"componentType; inputs: { config: widget }\"></ng-container>\n } @else {\n <div class=\"widget-error\">\n <p>Unknown widget type: {{ widget.type }}</p>\n </div>\n }\n </kendo-tilelayout-item-body>\n </kendo-tilelayout-item>\n }\n </kendo-tilelayout>\n } @else {\n <mm-meshboard-empty-state [readonly]=\"isReadonly()\" (addWidget)=\"openAddWidget()\"></mm-meshboard-empty-state>\n }\n\n <!-- Edit Widget Dialog -->\n @if (showEditWidgetDialog && editingWidget) {\n <mm-edit-widget-dialog\n [widget]=\"editingWidget\"\n [widgets]=\"config().widgets\"\n [maxColumns]=\"config().columns\"\n [gridService]=\"gridService\"\n (save)=\"onEditWidgetSave($event)\"\n (cancelled)=\"closeEditWidgetDialog()\">\n </mm-edit-widget-dialog>\n }\n }\n</div>\n", styles: [":host{display:flex;flex-direction:column;flex:1;min-height:0;height:100%;width:100%}.meshboard-view{display:flex;flex-direction:column;flex:1;min-height:0;width:100%;background-color:var(--kendo-color-surface, #f5f5f5)}.loading-container{display:flex;align-items:center;justify-content:center;height:100%;width:100%}.entity-selector-hint{display:flex;align-items:center;gap:8px;padding:10px 16px;margin-bottom:8px;background:color-mix(in srgb,var(--kendo-color-info, #2196f3) 10%,var(--kendo-color-surface-alt, #fafafa));border:1px solid color-mix(in srgb,var(--kendo-color-info, #2196f3) 40%,transparent);border-radius:4px;font-size:13px;color:var(--kendo-color-on-app-surface, #424242)}.entity-selector-hint strong{color:var(--kendo-color-info, #2196f3)}.variable-resolution-errors{display:flex;flex-wrap:wrap;align-items:center;gap:8px;padding:8px 16px;margin-bottom:8px;background:color-mix(in srgb,var(--kendo-color-error, #f44336) 10%,var(--kendo-color-surface-alt, #fafafa));border:1px solid var(--kendo-color-error, #f44336);border-radius:4px;font-size:13px;color:var(--kendo-color-on-app-surface, #424242)}.variable-resolution-errors strong{color:var(--kendo-color-error, #f44336)}.variable-resolution-errors .error-item{padding:2px 8px;background:color-mix(in srgb,var(--kendo-color-error, #f44336) 8%,transparent);border-radius:3px;font-family:var(--theme-font-mono);font-size:12px}.model-unavailable{display:flex;align-items:center;justify-content:center;height:100%;width:100%;padding:2rem;overflow:hidden;box-sizing:border-box}.model-unavailable .message-card{text-align:center;max-width:500px;padding:2rem;background:var(--kendo-color-surface-alt, #fafafa);border:1px solid var(--kendo-color-border, #e0e0e0);border-radius:8px;overflow:hidden}.model-unavailable .message-card kendo-svg-icon{color:var(--kendo-color-warning, #ff9800);margin-bottom:1rem}.model-unavailable .message-card h2{margin:1rem 0;color:var(--kendo-color-on-app-surface, #424242)}.model-unavailable .message-card p{margin:.5rem 0;color:var(--kendo-color-subtle, #757575);line-height:1.5}.model-unavailable .message-card ol{text-align:left;margin:1rem 0;padding-left:1.5rem;color:var(--kendo-color-subtle, #757575);line-height:1.8}.model-unavailable .message-card ol li{margin:.25rem 0}.model-unavailable .message-card ol a{color:var(--kendo-color-primary, #3f51b5);text-decoration:none}.model-unavailable .message-card ol a:hover{text-decoration:underline}.meshboard-toolbar{display:flex;justify-content:space-between;align-items:flex-end;padding:1rem 1.5rem;background-color:var(--kendo-color-surface-alt, white);border-bottom:1px solid var(--kendo-color-border, #e0e0e0);gap:1rem;flex-shrink:0}.meshboard-toolbar .toolbar-left{display:flex;align-items:baseline;gap:1rem;flex:1;min-width:0;align-self:center}.meshboard-toolbar .toolbar-left .meshboard-title{margin:0;font-size:1.5rem;font-weight:500;color:var(--kendo-color-on-app-surface, #212121);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.meshboard-toolbar .toolbar-left .meshboard-description{color:var(--kendo-color-subtle, #757575);font-size:.875rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.meshboard-toolbar .toolbar-center{display:flex;justify-content:center;align-items:flex-end;flex-shrink:0}.meshboard-toolbar .toolbar-center mm-time-range-picker ::ng-deep .time-range-picker{gap:.5rem}.meshboard-toolbar .toolbar-separator{width:1px;align-self:stretch;background-color:var(--kendo-color-border, #e0e0e0);flex-shrink:0}.meshboard-toolbar .toolbar-entity-selectors{display:flex;align-items:flex-end;flex-shrink:0}.meshboard-toolbar .toolbar-right{display:flex;gap:.5rem;align-items:center;flex-shrink:0}.meshboard-toolbar.compact{justify-content:flex-end;align-items:center;padding:.25rem 1.5rem 0;background-color:transparent;border-bottom:none}kendo-tilelayout{padding:1.5rem;overflow:auto;flex:1;min-height:0}.widget-header{display:flex;justify-content:space-between;align-items:center;width:100%;gap:.5rem}.widget-header.unconfigured{background-color:#ff98001a}.widget-header .widget-title{flex:1;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.widget-header .unconfigured-badge{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:50%;background-color:var(--kendo-color-warning, #ff9800);color:#fff;font-size:12px;font-weight:700;flex-shrink:0}.widget-header .widget-actions{display:flex;align-items:center;gap:.25rem;flex-shrink:0}.widget-header .config-widget-btn,.widget-header .edit-widget-btn,.widget-header .remove-widget-btn{opacity:.6;transition:opacity .2s}.widget-header .config-widget-btn:hover,.widget-header .edit-widget-btn:hover,.widget-header .remove-widget-btn:hover{opacity:1}.widget-header .config-widget-btn.needs-config{color:var(--kendo-color-warning, #ff9800);opacity:1}.widget-header .remove-widget-btn{color:var(--kendo-color-error, #f44336)}.widget-error{display:flex;align-items:center;justify-content:center;height:100%;padding:1rem;color:var(--kendo-color-error, #f44336);text-align:center}.meshboard-view.edit-mode kendo-tilelayout-item:hover{box-shadow:0 0 0 2px var(--kendo-color-primary, #3f51b5)}@media(max-width:768px){.meshboard-toolbar{flex-direction:column;align-items:stretch;gap:.75rem}.meshboard-toolbar .toolbar-left{flex-direction:column;align-items:flex-start;gap:.25rem}.meshboard-toolbar .toolbar-right{flex-wrap:wrap;justify-content:flex-end}.meshboard-toolbar.compact{align-items:flex-end;padding:.25rem 1rem 0}kendo-tilelayout{padding:1rem}}:host ::ng-deep kendo-tilelayout.has-banners{padding-top:0!important}:host ::ng-deep kendo-tilelayout{padding-bottom:1.5rem!important}.widget-header.chromeless-header{display:none}:host ::ng-deep .k-tilelayout-item-header:has(.chromeless-header){display:none}:host ::ng-deep kendo-tilelayout-item:has(.chromeless-header).k-tilelayout-item,:host ::ng-deep kendo-tilelayout-item:has(.chromeless-header).k-card{border:none;box-shadow:none;background:transparent}:host ::ng-deep kendo-tilelayout-item:has(kendo-dialog),:host ::ng-deep kendo-tilelayout-item:has(kendo-window){z-index:9999}.zone-label{font-size:.65rem;opacity:.4;padding:6px 1rem 2px;font-weight:600}.banner-zone-item{margin:0;padding:8px 1rem}.banner-zone-item.banner-edit{border:1px dashed var(--kendo-color-border, rgba(255, 255, 255, .2));border-radius:4px;margin:0 1rem 12px;padding:4px 0}.banner-edit-header{display:flex;align-items:center;justify-content:space-between;padding:4px 8px;opacity:.7}.banner-edit-header .widget-title{font-size:.75rem}.banner-edit-header .widget-actions{display:flex;gap:2px}.meshboard-view.chrome-plain{background:transparent!important}.meshboard-view.chrome-plain .meshboard-toolbar{padding:0 0 var(--theme-space-3, 12px);background:transparent!important;border-bottom:none!important}.meshboard-view.chrome-plain kendo-tilelayout{padding:0!important}.meshboard-view.chrome-plain .banner-zone-item:not(.banner-edit){padding-left:0;padding-right:0}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$3.NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i3$1.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }, { kind: "ngmodule", type: TileLayoutModule }, { kind: "component", type: i5.TileLayoutComponent, selector: "kendo-tilelayout", inputs: ["columns", "columnWidth", "gap", "reorderable", "resizable", "rowHeight", "autoFlow", "navigable"], outputs: ["reorder", "resize"] }, { kind: "component", type: i5.TileLayoutItemBodyComponent, selector: "kendo-tilelayout-item-body" }, { kind: "component", type: i5.TileLayoutItemComponent, selector: "kendo-tilelayout-item", inputs: ["title", "rowSpan", "colSpan", "order", "col", "row", "reorderable", "resizable"] }, { kind: "component", type: i5.TileLayoutItemHeaderComponent, selector: "kendo-tilelayout-item-header" }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: DialogModule }, { kind: "ngmodule", type: SVGIconModule }, { kind: "component", type: i1$4.SVGIconComponent, selector: "kendo-svg-icon, kendo-svgicon", inputs: ["icon", "variant"], exportAs: ["kendoSVGIcon"] }, { kind: "component", type: EditWidgetDialogComponent, selector: "mm-edit-widget-dialog", inputs: ["widget", "widgets", "maxColumns", "gridService"], outputs: ["save", "cancelled"] }, { kind: "component", type: TimeRangePickerComponent, selector: "mm-time-range-picker", inputs: ["config", "labels", "initialSelection"], outputs: ["rangeChange", "rangeChangeISO", "selectionChange"] }, { kind: "component", type: EntitySelectorToolbarComponent, selector: "mm-entity-selector-toolbar", inputs: ["entitySelectors"], outputs: ["entitySelected", "entityCleared"] }, { kind: "component", type: MeshBoardEmptyStateComponent, selector: "mm-meshboard-empty-state", inputs: ["readonly"], outputs: ["addWidget"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
36170
+ ], viewQueries: [{ propertyName: "tileLayout", first: true, predicate: TileLayoutComponent, descendants: true }], hostDirectives: [{ directive: i1$8.UnsavedChangesDirective }], ngImport: i0, template: "<div class=\"meshboard-view\" [class.chrome-plain]=\"effectiveChrome() === 'plain'\">\n @if (isLoading()) {\n <div class=\"loading-container\">\n <div class=\"k-loading-mask\">\n <span class=\"k-loading-text\">Loading MeshBoard...</span>\n <div class=\"k-loading-image\"></div>\n <div class=\"k-loading-color\"></div>\n </div>\n </div>\n } @else if (isModelAvailable() === false) {\n <div class=\"model-unavailable\">\n <div class=\"message-card\">\n <kendo-svg-icon [icon]=\"xIcon\" size=\"xlarge\"></kendo-svg-icon>\n <h2>MeshBoard Not Available</h2>\n <p>The MeshBoard feature requires the CK model 'System.UI' version 1.0.1 or higher.</p>\n <p>Please install the required model in your tenant to use this feature.</p>\n </div>\n </div>\n } @else if (notFoundError()) {\n <div class=\"model-unavailable\">\n <div class=\"message-card\">\n <kendo-svg-icon [icon]=\"xIcon\" size=\"xlarge\"></kendo-svg-icon>\n <h2>MeshBoard Not Found</h2>\n <p>{{ notFoundError() }}</p>\n <p>To create a MeshBoard with a Well-Known Name:</p>\n <ol>\n <li>Go to the <a [routerLink]=\"meshBoardPageLink()\">MeshBoard</a> page</li>\n <li>Open Settings</li>\n <li>Set the \"Well-Known Name\" field</li>\n <li>Save the MeshBoard</li>\n </ol>\n </div>\n </div>\n } @else if (isInitialized()) {\n <!-- Toolbar (headerMode: full = name + description + controls, compact = controls only, none = no row) -->\n @if (effectiveHeaderMode() !== 'none') {\n <div class=\"meshboard-toolbar\" [class.compact]=\"effectiveHeaderMode() === 'compact'\">\n @if (effectiveHeaderMode() === 'full') {\n <div class=\"toolbar-left\">\n <h2 class=\"meshboard-title\">{{ config().name }}</h2>\n @if (config().description) {\n <span class=\"meshboard-description\">{{ config().description }}</span>\n }\n </div>\n }\n\n <!-- Time Filter (center) -->\n @if (isTimeFilterEnabled()) {\n <div class=\"toolbar-center\">\n <mm-time-range-picker\n [config]=\"timeFilterConfig()?.pickerConfig ?? {}\"\n [labels]=\"timeRangeLabels()\"\n [initialSelection]=\"initialTimeSelection()\"\n (rangeChange)=\"onTimeRangeChange($event)\"\n (selectionChange)=\"onTimeSelectionChange($event)\">\n </mm-time-range-picker>\n @if (canResetTimeFilter()) {\n <button\n kendoButton\n [svgIcon]=\"undoIcon\"\n (click)=\"resetTimeFilterToDefault()\"\n title=\"Reset to default time filter\"\n aria-label=\"Reset to default time filter\"\n fillMode=\"flat\"\n size=\"small\">\n </button>\n }\n </div>\n }\n\n <!-- Entity Selectors -->\n @if (hasEntitySelectors()) {\n @if (isTimeFilterEnabled()) {\n <div class=\"toolbar-separator\"></div>\n }\n <div class=\"toolbar-entity-selectors\">\n <mm-entity-selector-toolbar\n [entitySelectors]=\"entitySelectorsConfig()\"\n (entitySelected)=\"onEntitySelectorSelected($event)\"\n (entityCleared)=\"onEntitySelectorCleared($event)\">\n </mm-entity-selector-toolbar>\n </div>\n }\n\n @if (isTimeFilterEnabled() || hasEntitySelectors()) {\n <div class=\"toolbar-separator\"></div>\n }\n <div class=\"toolbar-right mm-toolbar-actions\">\n @if (!isReadonly()) {\n <!-- Manager Button (icon only) -->\n <button\n kendoButton\n [svgIcon]=\"gridLayoutIcon\"\n (click)=\"openManager()\"\n title=\"Manage MeshBoards\"\n aria-label=\"Manage MeshBoards\"\n fillMode=\"flat\">\n </button>\n\n <!-- Settings Button (icon only) -->\n <button\n kendoButton\n [svgIcon]=\"gearIcon\"\n (click)=\"openSettings()\"\n title=\"MeshBoard Settings\"\n aria-label=\"MeshBoard Settings\"\n fillMode=\"flat\">\n </button>\n }\n\n <!-- Refresh Button (icon only) -->\n <button\n kendoButton\n [svgIcon]=\"arrowRotateCwIcon\"\n (click)=\"refresh()\"\n title=\"Refresh All Widgets\"\n aria-label=\"Refresh All Widgets\"\n fillMode=\"flat\">\n </button>\n\n @if (!isReadonly()) {\n <!-- Edit Mode Toggle (icon only) -->\n @if (!isEditMode()) {\n <button\n kendoButton\n [svgIcon]=\"pencilIcon\"\n (click)=\"toggleEditMode()\"\n title=\"Enter Edit Mode\"\n aria-label=\"Enter Edit Mode\"\n fillMode=\"flat\">\n </button>\n } @else {\n <button\n kendoButton\n [svgIcon]=\"xIcon\"\n (click)=\"cancelEdit()\"\n title=\"Cancel Edit Mode\"\n aria-label=\"Cancel Edit Mode\"\n fillMode=\"flat\">\n </button>\n }\n\n <!-- Add Widget Button (with text) -->\n <button\n kendoButton\n [svgIcon]=\"plusIcon\"\n (click)=\"openAddWidget()\"\n title=\"Add Widget\"\n themeColor=\"primary\">\n Add Widget\n </button>\n\n <!-- Save Button (with text) -->\n @if (canSave()) {\n <button\n kendoButton\n [svgIcon]=\"saveIcon\"\n (click)=\"save()\"\n [disabled]=\"isSaving()\"\n title=\"Save MeshBoard\"\n themeColor=\"primary\">\n {{ isSaving() ? 'Saving...' : 'Save' }}\n </button>\n }\n }\n </div>\n </div>\n }\n\n <!-- Entity Selector Hint -->\n @if (unselectedToolbarSelectors().length > 0) {\n <div class=\"entity-selector-hint\">\n <kendo-svg-icon [icon]=\"infoCircleIcon\" size=\"medium\"></kendo-svg-icon>\n <span>\n Please select\n @for (selector of unselectedToolbarSelectors(); track selector.id; let last = $last) {\n <strong>{{ selector.label }}</strong>@if (!last) {, }\n }\n to display dashboard data.\n </span>\n </div>\n }\n\n <!-- Variable Resolution Errors -->\n @if (variableResolutionErrors().length > 0) {\n <div class=\"variable-resolution-errors\">\n <strong>Variable resolution errors:</strong>\n @for (error of variableResolutionErrors(); track error.variableName) {\n <span class=\"error-item\">{{ error.variableName }}: {{ error.message }}</span>\n }\n </div>\n }\n\n <!-- Entity selector scopes that could not be resolved (AB#7721) -->\n @if (scopeResolutionErrors().length > 0) {\n <div class=\"variable-resolution-errors scope-resolution-errors\" role=\"status\">\n @for (error of scopeResolutionErrors(); track error.selectorId) {\n <span class=\"error-item\">{{ error.message }}</span>\n }\n </div>\n }\n\n <!-- Banner Zone: widgets placed in the banner stack above the grid -->\n @if (isEditMode()) {\n <div class=\"zone-label banner-zone-label\">Banner Zone</div>\n }\n @for (widget of bannerWidgets(); track trackByWidgetId($index, widget); let first = $first; let last = $last) {\n <div class=\"banner-zone-item\" [class.banner-edit]=\"isEditMode()\">\n @if (isEditMode()) {\n <div class=\"banner-edit-header\">\n <span class=\"widget-title\">{{ widget.title }}</span>\n <div class=\"widget-actions\">\n <button\n kendoButton\n [svgIcon]=\"arrowUpIcon\"\n (click)=\"moveBannerUp(widget.id)\"\n fillMode=\"flat\"\n size=\"small\"\n [disabled]=\"first\"\n title=\"Move up\"\n [attr.aria-label]=\"'Move up ' + widget.title\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"arrowDownIcon\"\n (click)=\"moveBannerDown(widget.id)\"\n fillMode=\"flat\"\n size=\"small\"\n [disabled]=\"last\"\n title=\"Move down\"\n [attr.aria-label]=\"'Move down ' + widget.title\">\n </button>\n @if (supportsConfiguration(widget)) {\n <button\n kendoButton\n [svgIcon]=\"linkIcon\"\n (click)=\"openWidgetConfig(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"config-widget-btn\"\n [class.needs-config]=\"isWidgetUnconfigured(widget)\"\n [title]=\"isWidgetUnconfigured(widget) ? 'Configure data source (required)' : 'Configure data source'\"\n [attr.aria-label]=\"(isWidgetUnconfigured(widget) ? 'Configure data source (required) ' : 'Configure data source ') + widget.title\">\n </button>\n }\n <button\n kendoButton\n [svgIcon]=\"gearIcon\"\n (click)=\"openEditWidgetDialog(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"edit-widget-btn\"\n title=\"Edit widget\"\n [attr.aria-label]=\"'Edit widget ' + widget.title\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"trashIcon\"\n (click)=\"removeWidget(widget.id)\"\n fillMode=\"flat\"\n themeColor=\"error\"\n size=\"small\"\n class=\"remove-widget-btn\"\n title=\"Delete widget\"\n [attr.aria-label]=\"'Delete widget ' + widget.title\">\n </button>\n </div>\n </div>\n }\n @if (getWidgetComponentType(widget.type); as componentType) {\n <ng-container *ngComponentOutlet=\"componentType; inputs: { config: widget }\"></ng-container>\n }\n </div>\n }\n\n <!-- MeshBoard Grid -->\n @if (isEditMode()) {\n <div class=\"zone-label grid-zone-label\">Grid Zone</div>\n }\n @if (hasGridWidgets()) {\n <kendo-tilelayout\n [columns]=\"displayColumns()\"\n [autoFlow]=\"displayAutoFlow()\"\n [rowHeight]=\"config().rowHeight\"\n [gap]=\"config().gap\"\n [class.has-banners]=\"bannerWidgets().length > 0 && !isEditMode()\"\n [resizable]=\"canEditLayout()\"\n [reorderable]=\"canEditLayout()\"\n (reorder)=\"onReorder($event)\"\n (resize)=\"onResize($event)\">\n @for (placement of displayGridWidgets(); track placement.widget.id) {\n @let widget = placement.widget;\n <kendo-tilelayout-item\n [col]=\"$any(placement.col)\"\n [row]=\"$any(placement.row)\"\n [colSpan]=\"placement.colSpan\"\n [rowSpan]=\"placement.rowSpan\">\n <kendo-tilelayout-item-header>\n <div class=\"widget-header\"\n [class.unconfigured]=\"isWidgetUnconfigured(widget)\"\n [class.chromeless-header]=\"widget.chromeless && !isEditMode()\">\n @if (!(widget.chromeless && !isEditMode())) {\n <span class=\"widget-title\">{{ widget.title }}</span>\n @if (isWidgetUnconfigured(widget)) {\n <span class=\"unconfigured-badge\" title=\"Widget needs configuration\">!</span>\n }\n }\n @if (isEditMode()) {\n <div class=\"widget-actions\" (pointerdown)=\"$event.stopPropagation()\">\n @if (supportsConfiguration(widget)) {\n <button\n kendoButton\n [svgIcon]=\"linkIcon\"\n (click)=\"openWidgetConfig(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"config-widget-btn\"\n [class.needs-config]=\"isWidgetUnconfigured(widget)\"\n [title]=\"isWidgetUnconfigured(widget) ? 'Configure data source (required)' : 'Configure data source'\"\n [attr.aria-label]=\"(isWidgetUnconfigured(widget) ? 'Configure data source (required) ' : 'Configure data source ') + widget.title\">\n </button>\n }\n <button\n kendoButton\n [svgIcon]=\"gearIcon\"\n (click)=\"openEditWidgetDialog(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"edit-widget-btn\"\n title=\"Edit position\"\n [attr.aria-label]=\"'Edit position ' + widget.title\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"copyIcon\"\n (click)=\"duplicateWidget(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"duplicate-widget-btn\"\n title=\"Duplicate widget\"\n [attr.aria-label]=\"'Duplicate widget ' + widget.title\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"trashIcon\"\n (click)=\"removeWidget(widget.id)\"\n fillMode=\"flat\"\n themeColor=\"error\"\n size=\"small\"\n class=\"remove-widget-btn\"\n title=\"Delete widget\"\n [attr.aria-label]=\"'Delete widget ' + widget.title\">\n </button>\n </div>\n }\n </div>\n </kendo-tilelayout-item-header>\n <kendo-tilelayout-item-body>\n @if (getWidgetComponentType(widget.type); as componentType) {\n <ng-container *ngComponentOutlet=\"componentType; inputs: { config: widget }\"></ng-container>\n } @else {\n <div class=\"widget-error\">\n <p>Unknown widget type: {{ widget.type }}</p>\n </div>\n }\n </kendo-tilelayout-item-body>\n </kendo-tilelayout-item>\n }\n </kendo-tilelayout>\n } @else {\n <mm-meshboard-empty-state [readonly]=\"isReadonly()\" (addWidget)=\"openAddWidget()\"></mm-meshboard-empty-state>\n }\n\n <!-- Edit Widget Dialog -->\n @if (showEditWidgetDialog && editingWidget) {\n <mm-edit-widget-dialog\n [widget]=\"editingWidget\"\n [widgets]=\"config().widgets\"\n [maxColumns]=\"config().columns\"\n [gridService]=\"gridService\"\n (save)=\"onEditWidgetSave($event)\"\n (cancelled)=\"closeEditWidgetDialog()\">\n </mm-edit-widget-dialog>\n }\n }\n</div>\n", styles: [":host{display:flex;flex-direction:column;flex:1;min-height:0;height:100%;width:100%}.meshboard-view{display:flex;flex-direction:column;flex:1;min-height:0;width:100%;background-color:var(--kendo-color-surface, #f5f5f5)}.loading-container{display:flex;align-items:center;justify-content:center;height:100%;width:100%}.entity-selector-hint{display:flex;align-items:center;gap:8px;padding:10px 16px;margin-bottom:8px;background:color-mix(in srgb,var(--kendo-color-info, #2196f3) 10%,var(--kendo-color-surface-alt, #fafafa));border:1px solid color-mix(in srgb,var(--kendo-color-info, #2196f3) 40%,transparent);border-radius:4px;font-size:13px;color:var(--kendo-color-on-app-surface, #424242)}.entity-selector-hint strong{color:var(--kendo-color-info, #2196f3)}.variable-resolution-errors{display:flex;flex-wrap:wrap;align-items:center;gap:8px;padding:8px 16px;margin-bottom:8px;background:color-mix(in srgb,var(--kendo-color-error, #f44336) 10%,var(--kendo-color-surface-alt, #fafafa));border:1px solid var(--kendo-color-error, #f44336);border-radius:4px;font-size:13px;color:var(--kendo-color-on-app-surface, #424242)}.variable-resolution-errors strong{color:var(--kendo-color-error, #f44336)}.variable-resolution-errors .error-item{padding:2px 8px;background:color-mix(in srgb,var(--kendo-color-error, #f44336) 8%,transparent);border-radius:3px;font-family:var(--theme-font-mono);font-size:12px}.scope-resolution-errors .error-item{font-family:inherit}.model-unavailable{display:flex;align-items:center;justify-content:center;height:100%;width:100%;padding:2rem;overflow:hidden;box-sizing:border-box}.model-unavailable .message-card{text-align:center;max-width:500px;padding:2rem;background:var(--kendo-color-surface-alt, #fafafa);border:1px solid var(--kendo-color-border, #e0e0e0);border-radius:8px;overflow:hidden}.model-unavailable .message-card kendo-svg-icon{color:var(--kendo-color-warning, #ff9800);margin-bottom:1rem}.model-unavailable .message-card h2{margin:1rem 0;color:var(--kendo-color-on-app-surface, #424242)}.model-unavailable .message-card p{margin:.5rem 0;color:var(--kendo-color-subtle, #757575);line-height:1.5}.model-unavailable .message-card ol{text-align:left;margin:1rem 0;padding-left:1.5rem;color:var(--kendo-color-subtle, #757575);line-height:1.8}.model-unavailable .message-card ol li{margin:.25rem 0}.model-unavailable .message-card ol a{color:var(--kendo-color-primary, #3f51b5);text-decoration:none}.model-unavailable .message-card ol a:hover{text-decoration:underline}.meshboard-toolbar{display:flex;justify-content:space-between;align-items:flex-end;padding:1rem 1.5rem;background-color:var(--kendo-color-surface-alt, white);border-bottom:1px solid var(--kendo-color-border, #e0e0e0);gap:1rem;flex-shrink:0}.meshboard-toolbar .toolbar-left{display:flex;align-items:baseline;gap:1rem;flex:1;min-width:0;align-self:center}.meshboard-toolbar .toolbar-left .meshboard-title{margin:0;font-size:1.5rem;font-weight:500;color:var(--kendo-color-on-app-surface, #212121);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.meshboard-toolbar .toolbar-left .meshboard-description{color:var(--kendo-color-subtle, #757575);font-size:.875rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.meshboard-toolbar .toolbar-center{display:flex;justify-content:center;align-items:flex-end;flex-shrink:0}.meshboard-toolbar .toolbar-center mm-time-range-picker ::ng-deep .time-range-picker{gap:.5rem}.meshboard-toolbar .toolbar-separator{width:1px;align-self:stretch;background-color:var(--kendo-color-border, #e0e0e0);flex-shrink:0}.meshboard-toolbar .toolbar-entity-selectors{display:flex;align-items:flex-end;flex-shrink:0}.meshboard-toolbar .toolbar-right{display:flex;gap:.5rem;align-items:center;flex-shrink:0}.meshboard-toolbar.compact{justify-content:flex-end;align-items:center;padding:.25rem 1.5rem 0;background-color:transparent;border-bottom:none}kendo-tilelayout{padding:1.5rem;overflow:auto;flex:1;min-height:0}.widget-header{display:flex;justify-content:space-between;align-items:center;width:100%;gap:.5rem}.widget-header.unconfigured{background-color:#ff98001a}.widget-header .widget-title{flex:1;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.widget-header .unconfigured-badge{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:50%;background-color:var(--kendo-color-warning, #ff9800);color:#fff;font-size:12px;font-weight:700;flex-shrink:0}.widget-header .widget-actions{display:flex;align-items:center;gap:.25rem;flex-shrink:0}.widget-header .config-widget-btn,.widget-header .edit-widget-btn,.widget-header .remove-widget-btn{opacity:.6;transition:opacity .2s}.widget-header .config-widget-btn:hover,.widget-header .edit-widget-btn:hover,.widget-header .remove-widget-btn:hover{opacity:1}.widget-header .config-widget-btn.needs-config{color:var(--kendo-color-warning, #ff9800);opacity:1}.widget-header .remove-widget-btn{color:var(--kendo-color-error, #f44336)}.widget-error{display:flex;align-items:center;justify-content:center;height:100%;padding:1rem;color:var(--kendo-color-error, #f44336);text-align:center}.meshboard-view.edit-mode kendo-tilelayout-item:hover{box-shadow:0 0 0 2px var(--kendo-color-primary, #3f51b5)}@media(max-width:768px){.meshboard-toolbar{flex-direction:column;align-items:stretch;gap:.75rem}.meshboard-toolbar .toolbar-left{flex-direction:column;align-items:flex-start;gap:.25rem}.meshboard-toolbar .toolbar-right{flex-wrap:wrap;justify-content:flex-end}.meshboard-toolbar.compact{align-items:flex-end;padding:.25rem 1rem 0}kendo-tilelayout{padding:1rem}}:host ::ng-deep kendo-tilelayout.has-banners{padding-top:0!important}:host ::ng-deep kendo-tilelayout{padding-bottom:1.5rem!important}.widget-header.chromeless-header{display:none}:host ::ng-deep .k-tilelayout-item-header:has(.chromeless-header){display:none}:host ::ng-deep kendo-tilelayout-item:has(.chromeless-header).k-tilelayout-item,:host ::ng-deep kendo-tilelayout-item:has(.chromeless-header).k-card{border:none;box-shadow:none;background:transparent}:host ::ng-deep kendo-tilelayout-item:has(kendo-dialog),:host ::ng-deep kendo-tilelayout-item:has(kendo-window){z-index:9999}.zone-label{font-size:.65rem;opacity:.4;padding:6px 1rem 2px;font-weight:600}.banner-zone-item{margin:0;padding:8px 1rem}.banner-zone-item.banner-edit{border:1px dashed var(--kendo-color-border, rgba(255, 255, 255, .2));border-radius:4px;margin:0 1rem 12px;padding:4px 0}.banner-edit-header{display:flex;align-items:center;justify-content:space-between;padding:4px 8px;opacity:.7}.banner-edit-header .widget-title{font-size:.75rem}.banner-edit-header .widget-actions{display:flex;gap:2px}.meshboard-view.chrome-plain{background:transparent!important}.meshboard-view.chrome-plain .meshboard-toolbar{padding:0 0 var(--theme-space-3, 12px);background:transparent!important;border-bottom:none!important}.meshboard-view.chrome-plain kendo-tilelayout{padding:0!important}.meshboard-view.chrome-plain .banner-zone-item:not(.banner-edit){padding-left:0;padding-right:0}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$3.NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i3$1.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }, { kind: "ngmodule", type: TileLayoutModule }, { kind: "component", type: i5.TileLayoutComponent, selector: "kendo-tilelayout", inputs: ["columns", "columnWidth", "gap", "reorderable", "resizable", "rowHeight", "autoFlow", "navigable"], outputs: ["reorder", "resize"] }, { kind: "component", type: i5.TileLayoutItemBodyComponent, selector: "kendo-tilelayout-item-body" }, { kind: "component", type: i5.TileLayoutItemComponent, selector: "kendo-tilelayout-item", inputs: ["title", "rowSpan", "colSpan", "order", "col", "row", "reorderable", "resizable"] }, { kind: "component", type: i5.TileLayoutItemHeaderComponent, selector: "kendo-tilelayout-item-header" }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: DialogModule }, { kind: "ngmodule", type: SVGIconModule }, { kind: "component", type: i1$4.SVGIconComponent, selector: "kendo-svg-icon, kendo-svgicon", inputs: ["icon", "variant"], exportAs: ["kendoSVGIcon"] }, { kind: "component", type: EditWidgetDialogComponent, selector: "mm-edit-widget-dialog", inputs: ["widget", "widgets", "maxColumns", "gridService"], outputs: ["save", "cancelled"] }, { kind: "component", type: TimeRangePickerComponent, selector: "mm-time-range-picker", inputs: ["config", "labels", "initialSelection"], outputs: ["rangeChange", "rangeChangeISO", "selectionChange"] }, { kind: "component", type: EntitySelectorToolbarComponent, selector: "mm-entity-selector-toolbar", inputs: ["entitySelectors"], outputs: ["entitySelected", "entityCleared"] }, { kind: "component", type: MeshBoardEmptyStateComponent, selector: "mm-meshboard-empty-state", inputs: ["readonly"], outputs: ["addWidget"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
35537
36171
  }
35538
36172
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: MeshBoardViewComponent, decorators: [{
35539
36173
  type: Component,
@@ -35551,7 +36185,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
35551
36185
  ], hostDirectives: [UnsavedChangesDirective], providers: [
35552
36186
  { provide: HAS_UNSAVED_CHANGES, useExisting: MeshBoardViewComponent },
35553
36187
  AutoRefreshTimerService
35554
- ], changeDetection: ChangeDetectionStrategy.Eager, template: "<div class=\"meshboard-view\" [class.chrome-plain]=\"effectiveChrome() === 'plain'\">\n @if (isLoading()) {\n <div class=\"loading-container\">\n <div class=\"k-loading-mask\">\n <span class=\"k-loading-text\">Loading MeshBoard...</span>\n <div class=\"k-loading-image\"></div>\n <div class=\"k-loading-color\"></div>\n </div>\n </div>\n } @else if (isModelAvailable() === false) {\n <div class=\"model-unavailable\">\n <div class=\"message-card\">\n <kendo-svg-icon [icon]=\"xIcon\" size=\"xlarge\"></kendo-svg-icon>\n <h2>MeshBoard Not Available</h2>\n <p>The MeshBoard feature requires the CK model 'System.UI' version 1.0.1 or higher.</p>\n <p>Please install the required model in your tenant to use this feature.</p>\n </div>\n </div>\n } @else if (notFoundError()) {\n <div class=\"model-unavailable\">\n <div class=\"message-card\">\n <kendo-svg-icon [icon]=\"xIcon\" size=\"xlarge\"></kendo-svg-icon>\n <h2>MeshBoard Not Found</h2>\n <p>{{ notFoundError() }}</p>\n <p>To create a MeshBoard with a Well-Known Name:</p>\n <ol>\n <li>Go to the <a [routerLink]=\"meshBoardPageLink()\">MeshBoard</a> page</li>\n <li>Open Settings</li>\n <li>Set the \"Well-Known Name\" field</li>\n <li>Save the MeshBoard</li>\n </ol>\n </div>\n </div>\n } @else if (isInitialized()) {\n <!-- Toolbar (headerMode: full = name + description + controls, compact = controls only, none = no row) -->\n @if (effectiveHeaderMode() !== 'none') {\n <div class=\"meshboard-toolbar\" [class.compact]=\"effectiveHeaderMode() === 'compact'\">\n @if (effectiveHeaderMode() === 'full') {\n <div class=\"toolbar-left\">\n <h2 class=\"meshboard-title\">{{ config().name }}</h2>\n @if (config().description) {\n <span class=\"meshboard-description\">{{ config().description }}</span>\n }\n </div>\n }\n\n <!-- Time Filter (center) -->\n @if (isTimeFilterEnabled()) {\n <div class=\"toolbar-center\">\n <mm-time-range-picker\n [config]=\"timeFilterConfig()?.pickerConfig ?? {}\"\n [labels]=\"timeRangeLabels()\"\n [initialSelection]=\"initialTimeSelection()\"\n (rangeChange)=\"onTimeRangeChange($event)\"\n (selectionChange)=\"onTimeSelectionChange($event)\">\n </mm-time-range-picker>\n @if (canResetTimeFilter()) {\n <button\n kendoButton\n [svgIcon]=\"undoIcon\"\n (click)=\"resetTimeFilterToDefault()\"\n title=\"Reset to default time filter\"\n aria-label=\"Reset to default time filter\"\n fillMode=\"flat\"\n size=\"small\">\n </button>\n }\n </div>\n }\n\n <!-- Entity Selectors -->\n @if (hasEntitySelectors()) {\n @if (isTimeFilterEnabled()) {\n <div class=\"toolbar-separator\"></div>\n }\n <div class=\"toolbar-entity-selectors\">\n <mm-entity-selector-toolbar\n [entitySelectors]=\"entitySelectorsConfig()\"\n (entitySelected)=\"onEntitySelectorSelected($event)\"\n (entityCleared)=\"onEntitySelectorCleared($event)\">\n </mm-entity-selector-toolbar>\n </div>\n }\n\n @if (isTimeFilterEnabled() || hasEntitySelectors()) {\n <div class=\"toolbar-separator\"></div>\n }\n <div class=\"toolbar-right mm-toolbar-actions\">\n @if (!isReadonly()) {\n <!-- Manager Button (icon only) -->\n <button\n kendoButton\n [svgIcon]=\"gridLayoutIcon\"\n (click)=\"openManager()\"\n title=\"Manage MeshBoards\"\n aria-label=\"Manage MeshBoards\"\n fillMode=\"flat\">\n </button>\n\n <!-- Settings Button (icon only) -->\n <button\n kendoButton\n [svgIcon]=\"gearIcon\"\n (click)=\"openSettings()\"\n title=\"MeshBoard Settings\"\n aria-label=\"MeshBoard Settings\"\n fillMode=\"flat\">\n </button>\n }\n\n <!-- Refresh Button (icon only) -->\n <button\n kendoButton\n [svgIcon]=\"arrowRotateCwIcon\"\n (click)=\"refresh()\"\n title=\"Refresh All Widgets\"\n aria-label=\"Refresh All Widgets\"\n fillMode=\"flat\">\n </button>\n\n @if (!isReadonly()) {\n <!-- Edit Mode Toggle (icon only) -->\n @if (!isEditMode()) {\n <button\n kendoButton\n [svgIcon]=\"pencilIcon\"\n (click)=\"toggleEditMode()\"\n title=\"Enter Edit Mode\"\n aria-label=\"Enter Edit Mode\"\n fillMode=\"flat\">\n </button>\n } @else {\n <button\n kendoButton\n [svgIcon]=\"xIcon\"\n (click)=\"cancelEdit()\"\n title=\"Cancel Edit Mode\"\n aria-label=\"Cancel Edit Mode\"\n fillMode=\"flat\">\n </button>\n }\n\n <!-- Add Widget Button (with text) -->\n <button\n kendoButton\n [svgIcon]=\"plusIcon\"\n (click)=\"openAddWidget()\"\n title=\"Add Widget\"\n themeColor=\"primary\">\n Add Widget\n </button>\n\n <!-- Save Button (with text) -->\n @if (canSave()) {\n <button\n kendoButton\n [svgIcon]=\"saveIcon\"\n (click)=\"save()\"\n [disabled]=\"isSaving()\"\n title=\"Save MeshBoard\"\n themeColor=\"primary\">\n {{ isSaving() ? 'Saving...' : 'Save' }}\n </button>\n }\n }\n </div>\n </div>\n }\n\n <!-- Entity Selector Hint -->\n @if (unselectedToolbarSelectors().length > 0) {\n <div class=\"entity-selector-hint\">\n <kendo-svg-icon [icon]=\"infoCircleIcon\" size=\"medium\"></kendo-svg-icon>\n <span>\n Please select\n @for (selector of unselectedToolbarSelectors(); track selector.id; let last = $last) {\n <strong>{{ selector.label }}</strong>@if (!last) {, }\n }\n to display dashboard data.\n </span>\n </div>\n }\n\n <!-- Variable Resolution Errors -->\n @if (variableResolutionErrors().length > 0) {\n <div class=\"variable-resolution-errors\">\n <strong>Variable resolution errors:</strong>\n @for (error of variableResolutionErrors(); track error.variableName) {\n <span class=\"error-item\">{{ error.variableName }}: {{ error.message }}</span>\n }\n </div>\n }\n\n <!-- Banner Zone: widgets placed in the banner stack above the grid -->\n @if (isEditMode()) {\n <div class=\"zone-label banner-zone-label\">Banner Zone</div>\n }\n @for (widget of bannerWidgets(); track trackByWidgetId($index, widget); let first = $first; let last = $last) {\n <div class=\"banner-zone-item\" [class.banner-edit]=\"isEditMode()\">\n @if (isEditMode()) {\n <div class=\"banner-edit-header\">\n <span class=\"widget-title\">{{ widget.title }}</span>\n <div class=\"widget-actions\">\n <button\n kendoButton\n [svgIcon]=\"arrowUpIcon\"\n (click)=\"moveBannerUp(widget.id)\"\n fillMode=\"flat\"\n size=\"small\"\n [disabled]=\"first\"\n title=\"Move up\"\n [attr.aria-label]=\"'Move up ' + widget.title\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"arrowDownIcon\"\n (click)=\"moveBannerDown(widget.id)\"\n fillMode=\"flat\"\n size=\"small\"\n [disabled]=\"last\"\n title=\"Move down\"\n [attr.aria-label]=\"'Move down ' + widget.title\">\n </button>\n @if (supportsConfiguration(widget)) {\n <button\n kendoButton\n [svgIcon]=\"linkIcon\"\n (click)=\"openWidgetConfig(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"config-widget-btn\"\n [class.needs-config]=\"isWidgetUnconfigured(widget)\"\n [title]=\"isWidgetUnconfigured(widget) ? 'Configure data source (required)' : 'Configure data source'\"\n [attr.aria-label]=\"(isWidgetUnconfigured(widget) ? 'Configure data source (required) ' : 'Configure data source ') + widget.title\">\n </button>\n }\n <button\n kendoButton\n [svgIcon]=\"gearIcon\"\n (click)=\"openEditWidgetDialog(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"edit-widget-btn\"\n title=\"Edit widget\"\n [attr.aria-label]=\"'Edit widget ' + widget.title\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"trashIcon\"\n (click)=\"removeWidget(widget.id)\"\n fillMode=\"flat\"\n themeColor=\"error\"\n size=\"small\"\n class=\"remove-widget-btn\"\n title=\"Delete widget\"\n [attr.aria-label]=\"'Delete widget ' + widget.title\">\n </button>\n </div>\n </div>\n }\n @if (getWidgetComponentType(widget.type); as componentType) {\n <ng-container *ngComponentOutlet=\"componentType; inputs: { config: widget }\"></ng-container>\n }\n </div>\n }\n\n <!-- MeshBoard Grid -->\n @if (isEditMode()) {\n <div class=\"zone-label grid-zone-label\">Grid Zone</div>\n }\n @if (hasGridWidgets()) {\n <kendo-tilelayout\n [columns]=\"displayColumns()\"\n [autoFlow]=\"displayAutoFlow()\"\n [rowHeight]=\"config().rowHeight\"\n [gap]=\"config().gap\"\n [class.has-banners]=\"bannerWidgets().length > 0 && !isEditMode()\"\n [resizable]=\"canEditLayout()\"\n [reorderable]=\"canEditLayout()\"\n (reorder)=\"onReorder($event)\"\n (resize)=\"onResize($event)\">\n @for (placement of displayGridWidgets(); track placement.widget.id) {\n @let widget = placement.widget;\n <kendo-tilelayout-item\n [col]=\"$any(placement.col)\"\n [row]=\"$any(placement.row)\"\n [colSpan]=\"placement.colSpan\"\n [rowSpan]=\"placement.rowSpan\">\n <kendo-tilelayout-item-header>\n <div class=\"widget-header\"\n [class.unconfigured]=\"isWidgetUnconfigured(widget)\"\n [class.chromeless-header]=\"widget.chromeless && !isEditMode()\">\n @if (!(widget.chromeless && !isEditMode())) {\n <span class=\"widget-title\">{{ widget.title }}</span>\n @if (isWidgetUnconfigured(widget)) {\n <span class=\"unconfigured-badge\" title=\"Widget needs configuration\">!</span>\n }\n }\n @if (isEditMode()) {\n <div class=\"widget-actions\" (pointerdown)=\"$event.stopPropagation()\">\n @if (supportsConfiguration(widget)) {\n <button\n kendoButton\n [svgIcon]=\"linkIcon\"\n (click)=\"openWidgetConfig(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"config-widget-btn\"\n [class.needs-config]=\"isWidgetUnconfigured(widget)\"\n [title]=\"isWidgetUnconfigured(widget) ? 'Configure data source (required)' : 'Configure data source'\"\n [attr.aria-label]=\"(isWidgetUnconfigured(widget) ? 'Configure data source (required) ' : 'Configure data source ') + widget.title\">\n </button>\n }\n <button\n kendoButton\n [svgIcon]=\"gearIcon\"\n (click)=\"openEditWidgetDialog(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"edit-widget-btn\"\n title=\"Edit position\"\n [attr.aria-label]=\"'Edit position ' + widget.title\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"copyIcon\"\n (click)=\"duplicateWidget(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"duplicate-widget-btn\"\n title=\"Duplicate widget\"\n [attr.aria-label]=\"'Duplicate widget ' + widget.title\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"trashIcon\"\n (click)=\"removeWidget(widget.id)\"\n fillMode=\"flat\"\n themeColor=\"error\"\n size=\"small\"\n class=\"remove-widget-btn\"\n title=\"Delete widget\"\n [attr.aria-label]=\"'Delete widget ' + widget.title\">\n </button>\n </div>\n }\n </div>\n </kendo-tilelayout-item-header>\n <kendo-tilelayout-item-body>\n @if (getWidgetComponentType(widget.type); as componentType) {\n <ng-container *ngComponentOutlet=\"componentType; inputs: { config: widget }\"></ng-container>\n } @else {\n <div class=\"widget-error\">\n <p>Unknown widget type: {{ widget.type }}</p>\n </div>\n }\n </kendo-tilelayout-item-body>\n </kendo-tilelayout-item>\n }\n </kendo-tilelayout>\n } @else {\n <mm-meshboard-empty-state [readonly]=\"isReadonly()\" (addWidget)=\"openAddWidget()\"></mm-meshboard-empty-state>\n }\n\n <!-- Edit Widget Dialog -->\n @if (showEditWidgetDialog && editingWidget) {\n <mm-edit-widget-dialog\n [widget]=\"editingWidget\"\n [widgets]=\"config().widgets\"\n [maxColumns]=\"config().columns\"\n [gridService]=\"gridService\"\n (save)=\"onEditWidgetSave($event)\"\n (cancelled)=\"closeEditWidgetDialog()\">\n </mm-edit-widget-dialog>\n }\n }\n</div>\n", styles: [":host{display:flex;flex-direction:column;flex:1;min-height:0;height:100%;width:100%}.meshboard-view{display:flex;flex-direction:column;flex:1;min-height:0;width:100%;background-color:var(--kendo-color-surface, #f5f5f5)}.loading-container{display:flex;align-items:center;justify-content:center;height:100%;width:100%}.entity-selector-hint{display:flex;align-items:center;gap:8px;padding:10px 16px;margin-bottom:8px;background:color-mix(in srgb,var(--kendo-color-info, #2196f3) 10%,var(--kendo-color-surface-alt, #fafafa));border:1px solid color-mix(in srgb,var(--kendo-color-info, #2196f3) 40%,transparent);border-radius:4px;font-size:13px;color:var(--kendo-color-on-app-surface, #424242)}.entity-selector-hint strong{color:var(--kendo-color-info, #2196f3)}.variable-resolution-errors{display:flex;flex-wrap:wrap;align-items:center;gap:8px;padding:8px 16px;margin-bottom:8px;background:color-mix(in srgb,var(--kendo-color-error, #f44336) 10%,var(--kendo-color-surface-alt, #fafafa));border:1px solid var(--kendo-color-error, #f44336);border-radius:4px;font-size:13px;color:var(--kendo-color-on-app-surface, #424242)}.variable-resolution-errors strong{color:var(--kendo-color-error, #f44336)}.variable-resolution-errors .error-item{padding:2px 8px;background:color-mix(in srgb,var(--kendo-color-error, #f44336) 8%,transparent);border-radius:3px;font-family:var(--theme-font-mono);font-size:12px}.model-unavailable{display:flex;align-items:center;justify-content:center;height:100%;width:100%;padding:2rem;overflow:hidden;box-sizing:border-box}.model-unavailable .message-card{text-align:center;max-width:500px;padding:2rem;background:var(--kendo-color-surface-alt, #fafafa);border:1px solid var(--kendo-color-border, #e0e0e0);border-radius:8px;overflow:hidden}.model-unavailable .message-card kendo-svg-icon{color:var(--kendo-color-warning, #ff9800);margin-bottom:1rem}.model-unavailable .message-card h2{margin:1rem 0;color:var(--kendo-color-on-app-surface, #424242)}.model-unavailable .message-card p{margin:.5rem 0;color:var(--kendo-color-subtle, #757575);line-height:1.5}.model-unavailable .message-card ol{text-align:left;margin:1rem 0;padding-left:1.5rem;color:var(--kendo-color-subtle, #757575);line-height:1.8}.model-unavailable .message-card ol li{margin:.25rem 0}.model-unavailable .message-card ol a{color:var(--kendo-color-primary, #3f51b5);text-decoration:none}.model-unavailable .message-card ol a:hover{text-decoration:underline}.meshboard-toolbar{display:flex;justify-content:space-between;align-items:flex-end;padding:1rem 1.5rem;background-color:var(--kendo-color-surface-alt, white);border-bottom:1px solid var(--kendo-color-border, #e0e0e0);gap:1rem;flex-shrink:0}.meshboard-toolbar .toolbar-left{display:flex;align-items:baseline;gap:1rem;flex:1;min-width:0;align-self:center}.meshboard-toolbar .toolbar-left .meshboard-title{margin:0;font-size:1.5rem;font-weight:500;color:var(--kendo-color-on-app-surface, #212121);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.meshboard-toolbar .toolbar-left .meshboard-description{color:var(--kendo-color-subtle, #757575);font-size:.875rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.meshboard-toolbar .toolbar-center{display:flex;justify-content:center;align-items:flex-end;flex-shrink:0}.meshboard-toolbar .toolbar-center mm-time-range-picker ::ng-deep .time-range-picker{gap:.5rem}.meshboard-toolbar .toolbar-separator{width:1px;align-self:stretch;background-color:var(--kendo-color-border, #e0e0e0);flex-shrink:0}.meshboard-toolbar .toolbar-entity-selectors{display:flex;align-items:flex-end;flex-shrink:0}.meshboard-toolbar .toolbar-right{display:flex;gap:.5rem;align-items:center;flex-shrink:0}.meshboard-toolbar.compact{justify-content:flex-end;align-items:center;padding:.25rem 1.5rem 0;background-color:transparent;border-bottom:none}kendo-tilelayout{padding:1.5rem;overflow:auto;flex:1;min-height:0}.widget-header{display:flex;justify-content:space-between;align-items:center;width:100%;gap:.5rem}.widget-header.unconfigured{background-color:#ff98001a}.widget-header .widget-title{flex:1;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.widget-header .unconfigured-badge{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:50%;background-color:var(--kendo-color-warning, #ff9800);color:#fff;font-size:12px;font-weight:700;flex-shrink:0}.widget-header .widget-actions{display:flex;align-items:center;gap:.25rem;flex-shrink:0}.widget-header .config-widget-btn,.widget-header .edit-widget-btn,.widget-header .remove-widget-btn{opacity:.6;transition:opacity .2s}.widget-header .config-widget-btn:hover,.widget-header .edit-widget-btn:hover,.widget-header .remove-widget-btn:hover{opacity:1}.widget-header .config-widget-btn.needs-config{color:var(--kendo-color-warning, #ff9800);opacity:1}.widget-header .remove-widget-btn{color:var(--kendo-color-error, #f44336)}.widget-error{display:flex;align-items:center;justify-content:center;height:100%;padding:1rem;color:var(--kendo-color-error, #f44336);text-align:center}.meshboard-view.edit-mode kendo-tilelayout-item:hover{box-shadow:0 0 0 2px var(--kendo-color-primary, #3f51b5)}@media(max-width:768px){.meshboard-toolbar{flex-direction:column;align-items:stretch;gap:.75rem}.meshboard-toolbar .toolbar-left{flex-direction:column;align-items:flex-start;gap:.25rem}.meshboard-toolbar .toolbar-right{flex-wrap:wrap;justify-content:flex-end}.meshboard-toolbar.compact{align-items:flex-end;padding:.25rem 1rem 0}kendo-tilelayout{padding:1rem}}:host ::ng-deep kendo-tilelayout.has-banners{padding-top:0!important}:host ::ng-deep kendo-tilelayout{padding-bottom:1.5rem!important}.widget-header.chromeless-header{display:none}:host ::ng-deep .k-tilelayout-item-header:has(.chromeless-header){display:none}:host ::ng-deep kendo-tilelayout-item:has(.chromeless-header).k-tilelayout-item,:host ::ng-deep kendo-tilelayout-item:has(.chromeless-header).k-card{border:none;box-shadow:none;background:transparent}:host ::ng-deep kendo-tilelayout-item:has(kendo-dialog),:host ::ng-deep kendo-tilelayout-item:has(kendo-window){z-index:9999}.zone-label{font-size:.65rem;opacity:.4;padding:6px 1rem 2px;font-weight:600}.banner-zone-item{margin:0;padding:8px 1rem}.banner-zone-item.banner-edit{border:1px dashed var(--kendo-color-border, rgba(255, 255, 255, .2));border-radius:4px;margin:0 1rem 12px;padding:4px 0}.banner-edit-header{display:flex;align-items:center;justify-content:space-between;padding:4px 8px;opacity:.7}.banner-edit-header .widget-title{font-size:.75rem}.banner-edit-header .widget-actions{display:flex;gap:2px}.meshboard-view.chrome-plain{background:transparent!important}.meshboard-view.chrome-plain .meshboard-toolbar{padding:0 0 var(--theme-space-3, 12px);background:transparent!important;border-bottom:none!important}.meshboard-view.chrome-plain kendo-tilelayout{padding:0!important}.meshboard-view.chrome-plain .banner-zone-item:not(.banner-edit){padding-left:0;padding-right:0}\n"] }]
36188
+ ], changeDetection: ChangeDetectionStrategy.Eager, template: "<div class=\"meshboard-view\" [class.chrome-plain]=\"effectiveChrome() === 'plain'\">\n @if (isLoading()) {\n <div class=\"loading-container\">\n <div class=\"k-loading-mask\">\n <span class=\"k-loading-text\">Loading MeshBoard...</span>\n <div class=\"k-loading-image\"></div>\n <div class=\"k-loading-color\"></div>\n </div>\n </div>\n } @else if (isModelAvailable() === false) {\n <div class=\"model-unavailable\">\n <div class=\"message-card\">\n <kendo-svg-icon [icon]=\"xIcon\" size=\"xlarge\"></kendo-svg-icon>\n <h2>MeshBoard Not Available</h2>\n <p>The MeshBoard feature requires the CK model 'System.UI' version 1.0.1 or higher.</p>\n <p>Please install the required model in your tenant to use this feature.</p>\n </div>\n </div>\n } @else if (notFoundError()) {\n <div class=\"model-unavailable\">\n <div class=\"message-card\">\n <kendo-svg-icon [icon]=\"xIcon\" size=\"xlarge\"></kendo-svg-icon>\n <h2>MeshBoard Not Found</h2>\n <p>{{ notFoundError() }}</p>\n <p>To create a MeshBoard with a Well-Known Name:</p>\n <ol>\n <li>Go to the <a [routerLink]=\"meshBoardPageLink()\">MeshBoard</a> page</li>\n <li>Open Settings</li>\n <li>Set the \"Well-Known Name\" field</li>\n <li>Save the MeshBoard</li>\n </ol>\n </div>\n </div>\n } @else if (isInitialized()) {\n <!-- Toolbar (headerMode: full = name + description + controls, compact = controls only, none = no row) -->\n @if (effectiveHeaderMode() !== 'none') {\n <div class=\"meshboard-toolbar\" [class.compact]=\"effectiveHeaderMode() === 'compact'\">\n @if (effectiveHeaderMode() === 'full') {\n <div class=\"toolbar-left\">\n <h2 class=\"meshboard-title\">{{ config().name }}</h2>\n @if (config().description) {\n <span class=\"meshboard-description\">{{ config().description }}</span>\n }\n </div>\n }\n\n <!-- Time Filter (center) -->\n @if (isTimeFilterEnabled()) {\n <div class=\"toolbar-center\">\n <mm-time-range-picker\n [config]=\"timeFilterConfig()?.pickerConfig ?? {}\"\n [labels]=\"timeRangeLabels()\"\n [initialSelection]=\"initialTimeSelection()\"\n (rangeChange)=\"onTimeRangeChange($event)\"\n (selectionChange)=\"onTimeSelectionChange($event)\">\n </mm-time-range-picker>\n @if (canResetTimeFilter()) {\n <button\n kendoButton\n [svgIcon]=\"undoIcon\"\n (click)=\"resetTimeFilterToDefault()\"\n title=\"Reset to default time filter\"\n aria-label=\"Reset to default time filter\"\n fillMode=\"flat\"\n size=\"small\">\n </button>\n }\n </div>\n }\n\n <!-- Entity Selectors -->\n @if (hasEntitySelectors()) {\n @if (isTimeFilterEnabled()) {\n <div class=\"toolbar-separator\"></div>\n }\n <div class=\"toolbar-entity-selectors\">\n <mm-entity-selector-toolbar\n [entitySelectors]=\"entitySelectorsConfig()\"\n (entitySelected)=\"onEntitySelectorSelected($event)\"\n (entityCleared)=\"onEntitySelectorCleared($event)\">\n </mm-entity-selector-toolbar>\n </div>\n }\n\n @if (isTimeFilterEnabled() || hasEntitySelectors()) {\n <div class=\"toolbar-separator\"></div>\n }\n <div class=\"toolbar-right mm-toolbar-actions\">\n @if (!isReadonly()) {\n <!-- Manager Button (icon only) -->\n <button\n kendoButton\n [svgIcon]=\"gridLayoutIcon\"\n (click)=\"openManager()\"\n title=\"Manage MeshBoards\"\n aria-label=\"Manage MeshBoards\"\n fillMode=\"flat\">\n </button>\n\n <!-- Settings Button (icon only) -->\n <button\n kendoButton\n [svgIcon]=\"gearIcon\"\n (click)=\"openSettings()\"\n title=\"MeshBoard Settings\"\n aria-label=\"MeshBoard Settings\"\n fillMode=\"flat\">\n </button>\n }\n\n <!-- Refresh Button (icon only) -->\n <button\n kendoButton\n [svgIcon]=\"arrowRotateCwIcon\"\n (click)=\"refresh()\"\n title=\"Refresh All Widgets\"\n aria-label=\"Refresh All Widgets\"\n fillMode=\"flat\">\n </button>\n\n @if (!isReadonly()) {\n <!-- Edit Mode Toggle (icon only) -->\n @if (!isEditMode()) {\n <button\n kendoButton\n [svgIcon]=\"pencilIcon\"\n (click)=\"toggleEditMode()\"\n title=\"Enter Edit Mode\"\n aria-label=\"Enter Edit Mode\"\n fillMode=\"flat\">\n </button>\n } @else {\n <button\n kendoButton\n [svgIcon]=\"xIcon\"\n (click)=\"cancelEdit()\"\n title=\"Cancel Edit Mode\"\n aria-label=\"Cancel Edit Mode\"\n fillMode=\"flat\">\n </button>\n }\n\n <!-- Add Widget Button (with text) -->\n <button\n kendoButton\n [svgIcon]=\"plusIcon\"\n (click)=\"openAddWidget()\"\n title=\"Add Widget\"\n themeColor=\"primary\">\n Add Widget\n </button>\n\n <!-- Save Button (with text) -->\n @if (canSave()) {\n <button\n kendoButton\n [svgIcon]=\"saveIcon\"\n (click)=\"save()\"\n [disabled]=\"isSaving()\"\n title=\"Save MeshBoard\"\n themeColor=\"primary\">\n {{ isSaving() ? 'Saving...' : 'Save' }}\n </button>\n }\n }\n </div>\n </div>\n }\n\n <!-- Entity Selector Hint -->\n @if (unselectedToolbarSelectors().length > 0) {\n <div class=\"entity-selector-hint\">\n <kendo-svg-icon [icon]=\"infoCircleIcon\" size=\"medium\"></kendo-svg-icon>\n <span>\n Please select\n @for (selector of unselectedToolbarSelectors(); track selector.id; let last = $last) {\n <strong>{{ selector.label }}</strong>@if (!last) {, }\n }\n to display dashboard data.\n </span>\n </div>\n }\n\n <!-- Variable Resolution Errors -->\n @if (variableResolutionErrors().length > 0) {\n <div class=\"variable-resolution-errors\">\n <strong>Variable resolution errors:</strong>\n @for (error of variableResolutionErrors(); track error.variableName) {\n <span class=\"error-item\">{{ error.variableName }}: {{ error.message }}</span>\n }\n </div>\n }\n\n <!-- Entity selector scopes that could not be resolved (AB#7721) -->\n @if (scopeResolutionErrors().length > 0) {\n <div class=\"variable-resolution-errors scope-resolution-errors\" role=\"status\">\n @for (error of scopeResolutionErrors(); track error.selectorId) {\n <span class=\"error-item\">{{ error.message }}</span>\n }\n </div>\n }\n\n <!-- Banner Zone: widgets placed in the banner stack above the grid -->\n @if (isEditMode()) {\n <div class=\"zone-label banner-zone-label\">Banner Zone</div>\n }\n @for (widget of bannerWidgets(); track trackByWidgetId($index, widget); let first = $first; let last = $last) {\n <div class=\"banner-zone-item\" [class.banner-edit]=\"isEditMode()\">\n @if (isEditMode()) {\n <div class=\"banner-edit-header\">\n <span class=\"widget-title\">{{ widget.title }}</span>\n <div class=\"widget-actions\">\n <button\n kendoButton\n [svgIcon]=\"arrowUpIcon\"\n (click)=\"moveBannerUp(widget.id)\"\n fillMode=\"flat\"\n size=\"small\"\n [disabled]=\"first\"\n title=\"Move up\"\n [attr.aria-label]=\"'Move up ' + widget.title\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"arrowDownIcon\"\n (click)=\"moveBannerDown(widget.id)\"\n fillMode=\"flat\"\n size=\"small\"\n [disabled]=\"last\"\n title=\"Move down\"\n [attr.aria-label]=\"'Move down ' + widget.title\">\n </button>\n @if (supportsConfiguration(widget)) {\n <button\n kendoButton\n [svgIcon]=\"linkIcon\"\n (click)=\"openWidgetConfig(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"config-widget-btn\"\n [class.needs-config]=\"isWidgetUnconfigured(widget)\"\n [title]=\"isWidgetUnconfigured(widget) ? 'Configure data source (required)' : 'Configure data source'\"\n [attr.aria-label]=\"(isWidgetUnconfigured(widget) ? 'Configure data source (required) ' : 'Configure data source ') + widget.title\">\n </button>\n }\n <button\n kendoButton\n [svgIcon]=\"gearIcon\"\n (click)=\"openEditWidgetDialog(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"edit-widget-btn\"\n title=\"Edit widget\"\n [attr.aria-label]=\"'Edit widget ' + widget.title\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"trashIcon\"\n (click)=\"removeWidget(widget.id)\"\n fillMode=\"flat\"\n themeColor=\"error\"\n size=\"small\"\n class=\"remove-widget-btn\"\n title=\"Delete widget\"\n [attr.aria-label]=\"'Delete widget ' + widget.title\">\n </button>\n </div>\n </div>\n }\n @if (getWidgetComponentType(widget.type); as componentType) {\n <ng-container *ngComponentOutlet=\"componentType; inputs: { config: widget }\"></ng-container>\n }\n </div>\n }\n\n <!-- MeshBoard Grid -->\n @if (isEditMode()) {\n <div class=\"zone-label grid-zone-label\">Grid Zone</div>\n }\n @if (hasGridWidgets()) {\n <kendo-tilelayout\n [columns]=\"displayColumns()\"\n [autoFlow]=\"displayAutoFlow()\"\n [rowHeight]=\"config().rowHeight\"\n [gap]=\"config().gap\"\n [class.has-banners]=\"bannerWidgets().length > 0 && !isEditMode()\"\n [resizable]=\"canEditLayout()\"\n [reorderable]=\"canEditLayout()\"\n (reorder)=\"onReorder($event)\"\n (resize)=\"onResize($event)\">\n @for (placement of displayGridWidgets(); track placement.widget.id) {\n @let widget = placement.widget;\n <kendo-tilelayout-item\n [col]=\"$any(placement.col)\"\n [row]=\"$any(placement.row)\"\n [colSpan]=\"placement.colSpan\"\n [rowSpan]=\"placement.rowSpan\">\n <kendo-tilelayout-item-header>\n <div class=\"widget-header\"\n [class.unconfigured]=\"isWidgetUnconfigured(widget)\"\n [class.chromeless-header]=\"widget.chromeless && !isEditMode()\">\n @if (!(widget.chromeless && !isEditMode())) {\n <span class=\"widget-title\">{{ widget.title }}</span>\n @if (isWidgetUnconfigured(widget)) {\n <span class=\"unconfigured-badge\" title=\"Widget needs configuration\">!</span>\n }\n }\n @if (isEditMode()) {\n <div class=\"widget-actions\" (pointerdown)=\"$event.stopPropagation()\">\n @if (supportsConfiguration(widget)) {\n <button\n kendoButton\n [svgIcon]=\"linkIcon\"\n (click)=\"openWidgetConfig(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"config-widget-btn\"\n [class.needs-config]=\"isWidgetUnconfigured(widget)\"\n [title]=\"isWidgetUnconfigured(widget) ? 'Configure data source (required)' : 'Configure data source'\"\n [attr.aria-label]=\"(isWidgetUnconfigured(widget) ? 'Configure data source (required) ' : 'Configure data source ') + widget.title\">\n </button>\n }\n <button\n kendoButton\n [svgIcon]=\"gearIcon\"\n (click)=\"openEditWidgetDialog(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"edit-widget-btn\"\n title=\"Edit position\"\n [attr.aria-label]=\"'Edit position ' + widget.title\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"copyIcon\"\n (click)=\"duplicateWidget(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"duplicate-widget-btn\"\n title=\"Duplicate widget\"\n [attr.aria-label]=\"'Duplicate widget ' + widget.title\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"trashIcon\"\n (click)=\"removeWidget(widget.id)\"\n fillMode=\"flat\"\n themeColor=\"error\"\n size=\"small\"\n class=\"remove-widget-btn\"\n title=\"Delete widget\"\n [attr.aria-label]=\"'Delete widget ' + widget.title\">\n </button>\n </div>\n }\n </div>\n </kendo-tilelayout-item-header>\n <kendo-tilelayout-item-body>\n @if (getWidgetComponentType(widget.type); as componentType) {\n <ng-container *ngComponentOutlet=\"componentType; inputs: { config: widget }\"></ng-container>\n } @else {\n <div class=\"widget-error\">\n <p>Unknown widget type: {{ widget.type }}</p>\n </div>\n }\n </kendo-tilelayout-item-body>\n </kendo-tilelayout-item>\n }\n </kendo-tilelayout>\n } @else {\n <mm-meshboard-empty-state [readonly]=\"isReadonly()\" (addWidget)=\"openAddWidget()\"></mm-meshboard-empty-state>\n }\n\n <!-- Edit Widget Dialog -->\n @if (showEditWidgetDialog && editingWidget) {\n <mm-edit-widget-dialog\n [widget]=\"editingWidget\"\n [widgets]=\"config().widgets\"\n [maxColumns]=\"config().columns\"\n [gridService]=\"gridService\"\n (save)=\"onEditWidgetSave($event)\"\n (cancelled)=\"closeEditWidgetDialog()\">\n </mm-edit-widget-dialog>\n }\n }\n</div>\n", styles: [":host{display:flex;flex-direction:column;flex:1;min-height:0;height:100%;width:100%}.meshboard-view{display:flex;flex-direction:column;flex:1;min-height:0;width:100%;background-color:var(--kendo-color-surface, #f5f5f5)}.loading-container{display:flex;align-items:center;justify-content:center;height:100%;width:100%}.entity-selector-hint{display:flex;align-items:center;gap:8px;padding:10px 16px;margin-bottom:8px;background:color-mix(in srgb,var(--kendo-color-info, #2196f3) 10%,var(--kendo-color-surface-alt, #fafafa));border:1px solid color-mix(in srgb,var(--kendo-color-info, #2196f3) 40%,transparent);border-radius:4px;font-size:13px;color:var(--kendo-color-on-app-surface, #424242)}.entity-selector-hint strong{color:var(--kendo-color-info, #2196f3)}.variable-resolution-errors{display:flex;flex-wrap:wrap;align-items:center;gap:8px;padding:8px 16px;margin-bottom:8px;background:color-mix(in srgb,var(--kendo-color-error, #f44336) 10%,var(--kendo-color-surface-alt, #fafafa));border:1px solid var(--kendo-color-error, #f44336);border-radius:4px;font-size:13px;color:var(--kendo-color-on-app-surface, #424242)}.variable-resolution-errors strong{color:var(--kendo-color-error, #f44336)}.variable-resolution-errors .error-item{padding:2px 8px;background:color-mix(in srgb,var(--kendo-color-error, #f44336) 8%,transparent);border-radius:3px;font-family:var(--theme-font-mono);font-size:12px}.scope-resolution-errors .error-item{font-family:inherit}.model-unavailable{display:flex;align-items:center;justify-content:center;height:100%;width:100%;padding:2rem;overflow:hidden;box-sizing:border-box}.model-unavailable .message-card{text-align:center;max-width:500px;padding:2rem;background:var(--kendo-color-surface-alt, #fafafa);border:1px solid var(--kendo-color-border, #e0e0e0);border-radius:8px;overflow:hidden}.model-unavailable .message-card kendo-svg-icon{color:var(--kendo-color-warning, #ff9800);margin-bottom:1rem}.model-unavailable .message-card h2{margin:1rem 0;color:var(--kendo-color-on-app-surface, #424242)}.model-unavailable .message-card p{margin:.5rem 0;color:var(--kendo-color-subtle, #757575);line-height:1.5}.model-unavailable .message-card ol{text-align:left;margin:1rem 0;padding-left:1.5rem;color:var(--kendo-color-subtle, #757575);line-height:1.8}.model-unavailable .message-card ol li{margin:.25rem 0}.model-unavailable .message-card ol a{color:var(--kendo-color-primary, #3f51b5);text-decoration:none}.model-unavailable .message-card ol a:hover{text-decoration:underline}.meshboard-toolbar{display:flex;justify-content:space-between;align-items:flex-end;padding:1rem 1.5rem;background-color:var(--kendo-color-surface-alt, white);border-bottom:1px solid var(--kendo-color-border, #e0e0e0);gap:1rem;flex-shrink:0}.meshboard-toolbar .toolbar-left{display:flex;align-items:baseline;gap:1rem;flex:1;min-width:0;align-self:center}.meshboard-toolbar .toolbar-left .meshboard-title{margin:0;font-size:1.5rem;font-weight:500;color:var(--kendo-color-on-app-surface, #212121);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.meshboard-toolbar .toolbar-left .meshboard-description{color:var(--kendo-color-subtle, #757575);font-size:.875rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.meshboard-toolbar .toolbar-center{display:flex;justify-content:center;align-items:flex-end;flex-shrink:0}.meshboard-toolbar .toolbar-center mm-time-range-picker ::ng-deep .time-range-picker{gap:.5rem}.meshboard-toolbar .toolbar-separator{width:1px;align-self:stretch;background-color:var(--kendo-color-border, #e0e0e0);flex-shrink:0}.meshboard-toolbar .toolbar-entity-selectors{display:flex;align-items:flex-end;flex-shrink:0}.meshboard-toolbar .toolbar-right{display:flex;gap:.5rem;align-items:center;flex-shrink:0}.meshboard-toolbar.compact{justify-content:flex-end;align-items:center;padding:.25rem 1.5rem 0;background-color:transparent;border-bottom:none}kendo-tilelayout{padding:1.5rem;overflow:auto;flex:1;min-height:0}.widget-header{display:flex;justify-content:space-between;align-items:center;width:100%;gap:.5rem}.widget-header.unconfigured{background-color:#ff98001a}.widget-header .widget-title{flex:1;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.widget-header .unconfigured-badge{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:50%;background-color:var(--kendo-color-warning, #ff9800);color:#fff;font-size:12px;font-weight:700;flex-shrink:0}.widget-header .widget-actions{display:flex;align-items:center;gap:.25rem;flex-shrink:0}.widget-header .config-widget-btn,.widget-header .edit-widget-btn,.widget-header .remove-widget-btn{opacity:.6;transition:opacity .2s}.widget-header .config-widget-btn:hover,.widget-header .edit-widget-btn:hover,.widget-header .remove-widget-btn:hover{opacity:1}.widget-header .config-widget-btn.needs-config{color:var(--kendo-color-warning, #ff9800);opacity:1}.widget-header .remove-widget-btn{color:var(--kendo-color-error, #f44336)}.widget-error{display:flex;align-items:center;justify-content:center;height:100%;padding:1rem;color:var(--kendo-color-error, #f44336);text-align:center}.meshboard-view.edit-mode kendo-tilelayout-item:hover{box-shadow:0 0 0 2px var(--kendo-color-primary, #3f51b5)}@media(max-width:768px){.meshboard-toolbar{flex-direction:column;align-items:stretch;gap:.75rem}.meshboard-toolbar .toolbar-left{flex-direction:column;align-items:flex-start;gap:.25rem}.meshboard-toolbar .toolbar-right{flex-wrap:wrap;justify-content:flex-end}.meshboard-toolbar.compact{align-items:flex-end;padding:.25rem 1rem 0}kendo-tilelayout{padding:1rem}}:host ::ng-deep kendo-tilelayout.has-banners{padding-top:0!important}:host ::ng-deep kendo-tilelayout{padding-bottom:1.5rem!important}.widget-header.chromeless-header{display:none}:host ::ng-deep .k-tilelayout-item-header:has(.chromeless-header){display:none}:host ::ng-deep kendo-tilelayout-item:has(.chromeless-header).k-tilelayout-item,:host ::ng-deep kendo-tilelayout-item:has(.chromeless-header).k-card{border:none;box-shadow:none;background:transparent}:host ::ng-deep kendo-tilelayout-item:has(kendo-dialog),:host ::ng-deep kendo-tilelayout-item:has(kendo-window){z-index:9999}.zone-label{font-size:.65rem;opacity:.4;padding:6px 1rem 2px;font-weight:600}.banner-zone-item{margin:0;padding:8px 1rem}.banner-zone-item.banner-edit{border:1px dashed var(--kendo-color-border, rgba(255, 255, 255, .2));border-radius:4px;margin:0 1rem 12px;padding:4px 0}.banner-edit-header{display:flex;align-items:center;justify-content:space-between;padding:4px 8px;opacity:.7}.banner-edit-header .widget-title{font-size:.75rem}.banner-edit-header .widget-actions{display:flex;gap:2px}.meshboard-view.chrome-plain{background:transparent!important}.meshboard-view.chrome-plain .meshboard-toolbar{padding:0 0 var(--theme-space-3, 12px);background:transparent!important;border-bottom:none!important}.meshboard-view.chrome-plain kendo-tilelayout{padding:0!important}.meshboard-view.chrome-plain .banner-zone-item:not(.banner-edit){padding-left:0;padding-right:0}\n"] }]
35555
36189
  }], ctorParameters: () => [], propDecorators: { tileLayout: [{
35556
36190
  type: ViewChild,
35557
36191
  args: [TileLayoutComponent]
@@ -39001,5 +39635,5 @@ function provideCockpitWidgets() {
39001
39635
  * Generated bundle index. Do not edit.
39002
39636
  */
39003
39637
 
39004
- export { ADAPTER_OFFLINE_GRACE_MS, ATTENTION_SEVERITY_ORDER, AdaptersAttentionProvider, AddWidgetDialogComponent, AiInsightsConfigDialogComponent, AiInsightsService, AiInsightsWidgetComponent, AlertBannerConfigDialogComponent, AlertBannerWidgetComponent, AlertListConfigDialogComponent, AlertListWidgetComponent, AssociationsConfigDialogComponent, AttentionListConfigDialogComponent, AttentionListWidgetComponent, AutoRefreshTimerService, BUILT_IN_ATTENTION_PROVIDERS, BarChartConfigDialogComponent, BarChartWidgetComponent, BlueprintUpdatesAttentionProvider, COCKPIT_ADAPTER_LIMIT, COCKPIT_ATTENTION_PROVIDERS, COCKPIT_BLUEPRINT_CATALOG_LIMIT, COCKPIT_CHILD_TENANT_IDS, COCKPIT_DATA_FLOW_LIMIT, COCKPIT_EXPLAIN_HANDLER, COCKPIT_FAILED_EXECUTIONS_OPTIONS, COCKPIT_KPI_GATES, COCKPIT_KPI_SLOW_LOADING_MS, COCKPIT_LINK_RESOLVER, COCKPIT_RECENT_ITEMS, COCKPIT_ROLES, COCKPIT_SERVICE_NAMES, COCKPIT_VERSION_SOURCE, COCKPIT_VIEWER_ACCESS, COCKPIT_WIDGET_MESSAGES, COCKPIT_WIDGET_TYPES, CkModelsResolveFailedAttentionProvider, CockpitAdapterStatesService, CockpitAttentionService, CockpitBlueprintStatusService, CockpitChildTenantsService, CockpitCkModelStatesService, CockpitContextService, CockpitDataFlowExecutionsService, CockpitKpiConfigDialogComponent, CockpitKpiService, CockpitKpiWidgetComponent, CockpitServiceHealthService, DAY_MS, DEFAULT_ATTENTION_MAX_ITEMS, DEFAULT_COCKPIT_WIDGET_MESSAGES, DEFAULT_FAILED_EXECUTIONS_OPTIONS, DEFAULT_MESHBOARD_MESSAGES, DEFAULT_RECENT_ITEMS_MAX, DEFAULT_TIME_ZONE_MODE, DashboardDataService, DashboardGridService, EditModeStateService, EditWidgetDialogComponent, EntityAssociationsWidgetComponent, EntityCardConfigDialogComponent, EntityCardWidgetComponent, EntityDetailDialogComponent, EntitySelectorEditorComponent, EntitySelectorToolbarComponent, FALLBACK_SERIES_COLORS, FailedExecutionsAttentionProvider, FeaturesNotInstalledAttentionProvider, GaugeConfigDialogComponent, GaugeWidgetComponent, HISTOGRAM_HOURS, HOUR_MS, HeatmapConfigDialogComponent, HeatmapWidgetComponent, KPI_ERROR_TEXT, KpiConfigDialogComponent, KpiWidgetComponent, LazyMarkdownComponent, LineChartConfigDialogComponent, LineChartWidgetComponent, MAX_RECENT_ITEMS, MESHBOARD_CHROME_ROUTE_DATA, MESHBOARD_DESCRIPTION_MARKER, MESHBOARD_EDIT_QUERY_PARAM, MESHBOARD_HEADER_MODE_ROUTE_DATA, MESHBOARD_LABEL_RESOLVER, MESHBOARD_MESSAGES, MESHBOARD_OPTIONS, MESHBOARD_TENANT_ID_PROVIDER, MarkdownConfigDialogComponent, MarkdownWidgetComponent, MeshBoardDataService, MeshBoardGridService, MeshBoardManagerDialogComponent, MeshBoardPersistenceService, MeshBoardSettingsDialogComponent, MeshBoardSettingsResult, MeshBoardStateService, MeshBoardViewComponent, NOT_AVAILABLE_TEXT, PieChartConfigDialogComponent, PieChartWidgetComponent, QueryExecutorService, QuerySelectorComponent, RecentItemsConfigDialogComponent, RecentItemsWidgetComponent, RuntimeEntitySelectorComponent, SERVICE_MANAGED_BLUEPRINT_PREFIX, STATE_STATUS_BY_KEY, ServiceHealthConfigDialogComponent, ServiceHealthWidgetComponent, ServicesHealthAttentionProvider, StatsGridConfigDialogComponent, StatsGridWidgetComponent, StatusIndicatorConfigDialogComponent, StatusIndicatorWidgetComponent, StatusListConfigDialogComponent, StatusListWidgetComponent, SummaryCardConfigDialogComponent, SummaryCardWidgetComponent, TableConfigDialogComponent, TableWidgetComponent, TableWidgetDataSourceDirective, UnregisteredPoolsAttentionProvider, WidgetFactoryService, WidgetGroupComponent, WidgetGroupConfigDialogComponent, WidgetNotConfiguredComponent, WidgetRegistryService, adapterKpi, adaptersInError, adaptersOffline, blueprintUpdatesKpi, buildHourlyHistogram, categoryStatus, chartSeriesColors, ckModelKpi, classifyQuery, compareBlueprintVersions, compareMeshBoardNavigation, countFlowExecutions, countLabel, countPipelineExecutions, enabledButNotInstalled, executionKpi, failedExecutionsSeverity, formatCockpitMessage, formatCount, humanizeCategory, isAdapterAtRest, isAdapterExpectedToRun, isAdapterOnline, isCockpitWidgetType, isMeshBoardChrome, isMeshBoardHeaderMode, isServiceConfigured, joinEncodedDescription, kpiKindOf, latestStatisticsUpdate, legacyCockpitLinkTarget, loadNgxMarkdown, nameList, newMeshBoardConfig, normalizeNavigation, observeThemeChanges, pipelineExecutionInputs, plural, provideCockpitWidgetHost, provideCockpitWidgets, provideDefaultWidgets, provideMeshBoard, provideMeshBoardMessages, provideProcessWidget, provideWidgetRegistrations, queryFamily, readMeshBoardNavigation, recentRelativeTime, registerCockpitWidgets, registerDefaultWidgets, registerProcessWidget, resolveChartColor, resolveCockpitWidgetMessages, resolveMeshBoardChrome, resolveMeshBoardHeaderMode, resolveMeshBoardLabel, resolveMeshBoardMessages, selectProviders, serviceStateText, servicesHealthKpi, sortAttentionFindings, sparklineGeometry, splitBlueprintId, splitEncodedDescription, statusColor, summarizeAdapterOnline, tenantCountKpi, toBlueprintStatus, toServiceHealthStatus, truncationNote, versionKpi, withMeshBoardNavigation };
39638
+ export { ADAPTER_OFFLINE_GRACE_MS, ATTENTION_SEVERITY_ORDER, AdaptersAttentionProvider, AddWidgetDialogComponent, AiInsightsConfigDialogComponent, AiInsightsService, AiInsightsWidgetComponent, AlertBannerConfigDialogComponent, AlertBannerWidgetComponent, AlertListConfigDialogComponent, AlertListWidgetComponent, AssociationsConfigDialogComponent, AttentionListConfigDialogComponent, AttentionListWidgetComponent, AutoRefreshTimerService, BUILT_IN_ATTENTION_PROVIDERS, BarChartConfigDialogComponent, BarChartWidgetComponent, BlueprintUpdatesAttentionProvider, COCKPIT_ADAPTER_LIMIT, COCKPIT_ATTENTION_PROVIDERS, COCKPIT_BLUEPRINT_CATALOG_LIMIT, COCKPIT_CHILD_TENANT_IDS, COCKPIT_DATA_FLOW_LIMIT, COCKPIT_EXPLAIN_HANDLER, COCKPIT_FAILED_EXECUTIONS_OPTIONS, COCKPIT_KPI_GATES, COCKPIT_KPI_SLOW_LOADING_MS, COCKPIT_LINK_RESOLVER, COCKPIT_RECENT_ITEMS, COCKPIT_ROLES, COCKPIT_SERVICE_NAMES, COCKPIT_VERSION_SOURCE, COCKPIT_VIEWER_ACCESS, COCKPIT_WIDGET_MESSAGES, COCKPIT_WIDGET_TYPES, CkModelsResolveFailedAttentionProvider, CockpitAdapterStatesService, CockpitAttentionService, CockpitBlueprintStatusService, CockpitChildTenantsService, CockpitCkModelStatesService, CockpitContextService, CockpitDataFlowExecutionsService, CockpitKpiConfigDialogComponent, CockpitKpiService, CockpitKpiWidgetComponent, CockpitServiceHealthService, DAY_MS, DEFAULT_ATTENTION_MAX_ITEMS, DEFAULT_COCKPIT_WIDGET_MESSAGES, DEFAULT_FAILED_EXECUTIONS_OPTIONS, DEFAULT_MESHBOARD_MESSAGES, DEFAULT_RECENT_ITEMS_MAX, DEFAULT_TIME_ZONE_MODE, DashboardDataService, DashboardGridService, EditModeStateService, EditWidgetDialogComponent, EntityAssociationsWidgetComponent, EntityCardConfigDialogComponent, EntityCardWidgetComponent, EntityDetailDialogComponent, EntitySelectorEditorComponent, EntitySelectorToolbarComponent, FALLBACK_SERIES_COLORS, FailedExecutionsAttentionProvider, FeaturesNotInstalledAttentionProvider, GaugeConfigDialogComponent, GaugeWidgetComponent, HISTOGRAM_HOURS, HOUR_MS, HeatmapConfigDialogComponent, HeatmapWidgetComponent, KPI_ERROR_TEXT, KpiConfigDialogComponent, KpiWidgetComponent, LazyMarkdownComponent, LineChartConfigDialogComponent, LineChartWidgetComponent, MAX_RECENT_ITEMS, MESHBOARD_CHROME_ROUTE_DATA, MESHBOARD_DESCRIPTION_MARKER, MESHBOARD_EDIT_QUERY_PARAM, MESHBOARD_HEADER_MODE_ROUTE_DATA, MESHBOARD_LABEL_RESOLVER, MESHBOARD_MESSAGES, MESHBOARD_OPTIONS, MESHBOARD_TENANT_ID_PROVIDER, MarkdownConfigDialogComponent, MarkdownWidgetComponent, MeshBoardDataService, MeshBoardGridService, MeshBoardManagerDialogComponent, MeshBoardPersistenceService, MeshBoardSettingsDialogComponent, MeshBoardSettingsResult, MeshBoardStateService, MeshBoardViewComponent, NOT_AVAILABLE_TEXT, PieChartConfigDialogComponent, PieChartWidgetComponent, QueryExecutorService, QuerySelectorComponent, RecentItemsConfigDialogComponent, RecentItemsWidgetComponent, RuntimeEntitySelectorComponent, SERVICE_MANAGED_BLUEPRINT_PREFIX, STATE_STATUS_BY_KEY, ServiceHealthConfigDialogComponent, ServiceHealthWidgetComponent, ServicesHealthAttentionProvider, StatsGridConfigDialogComponent, StatsGridWidgetComponent, StatusIndicatorConfigDialogComponent, StatusIndicatorWidgetComponent, StatusListConfigDialogComponent, StatusListWidgetComponent, SummaryCardConfigDialogComponent, SummaryCardWidgetComponent, TableConfigDialogComponent, TableWidgetComponent, TableWidgetDataSourceDirective, UnregisteredPoolsAttentionProvider, WidgetFactoryService, WidgetGroupComponent, WidgetGroupConfigDialogComponent, WidgetNotConfiguredComponent, WidgetRegistryService, adapterKpi, adaptersInError, adaptersOffline, blueprintUpdatesKpi, buildHourlyHistogram, categoryStatus, chartSeriesColors, ckModelKpi, classifyQuery, compareBlueprintVersions, compareMeshBoardNavigation, countFlowExecutions, countLabel, countPipelineExecutions, emptyQueryExecutionResult, enabledButNotInstalled, executionKpi, failedExecutionsSeverity, formatCockpitMessage, formatCount, humanizeCategory, isAdapterAtRest, isAdapterExpectedToRun, isAdapterOnline, isCockpitWidgetType, isMeshBoardChrome, isMeshBoardHeaderMode, isServiceConfigured, joinEncodedDescription, kpiKindOf, latestStatisticsUpdate, legacyCockpitLinkTarget, loadNgxMarkdown, nameList, newMeshBoardConfig, normalizeNavigation, observeThemeChanges, pipelineExecutionInputs, plural, provideCockpitWidgetHost, provideCockpitWidgets, provideDefaultWidgets, provideMeshBoard, provideMeshBoardMessages, provideProcessWidget, provideWidgetRegistrations, queryFamily, readMeshBoardNavigation, recentRelativeTime, registerCockpitWidgets, registerDefaultWidgets, registerProcessWidget, resolveChartColor, resolveCockpitWidgetMessages, resolveMeshBoardChrome, resolveMeshBoardHeaderMode, resolveMeshBoardLabel, resolveMeshBoardMessages, selectProviders, serviceStateText, servicesHealthKpi, sortAttentionFindings, sparklineGeometry, splitBlueprintId, splitEncodedDescription, statusColor, summarizeAdapterOnline, tenantCountKpi, toBlueprintStatus, toServiceHealthStatus, truncationNote, versionKpi, withMeshBoardNavigation };
39005
39639
  //# sourceMappingURL=meshmakers-octo-meshboard.mjs.map