@meshmakers/octo-meshboard 3.5.480 → 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' };
3202
+ }
3203
+ const state = this._entitySelectorScopes()[entitySelectorId];
3204
+ if (!state) {
3205
+ return { kind: 'unscoped' };
3139
3206
  }
3140
- const rtIds = this._entitySelectorRtIds()[entitySelectorId];
3141
- return rtIds && rtIds.length > 0 ? rtIds : undefined;
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.
@@ -5942,6 +6033,23 @@ function applyValueMultiplier(value, multiplier) {
5942
6033
  }
5943
6034
  return value * multiplier;
5944
6035
  }
6036
+ const WIDGET_NUMBER_LOCALE = 'de-AT';
6037
+ const numberFormats = new Map();
6038
+ function getNumberFormat(locale, minimumFractionDigits, maximumFractionDigits, useGrouping) {
6039
+ const key = `${locale}|${minimumFractionDigits}|${maximumFractionDigits}|${useGrouping}`;
6040
+ let format = numberFormats.get(key);
6041
+ if (!format) {
6042
+ format = new Intl.NumberFormat(locale, useGrouping === undefined
6043
+ ? { minimumFractionDigits, maximumFractionDigits }
6044
+ : { minimumFractionDigits, maximumFractionDigits, useGrouping });
6045
+ numberFormats.set(key, format);
6046
+ }
6047
+ return format;
6048
+ }
6049
+ function formatWidgetNumber(value, fractionDigits, mode) {
6050
+ const minimumFractionDigits = mode === 'exact' ? fractionDigits : 0;
6051
+ return getNumberFormat(WIDGET_NUMBER_LOCALE, minimumFractionDigits, fractionDigits).format(value);
6052
+ }
5945
6053
 
5946
6054
  const VARIABLE_REFERENCE = /\$\{([^}]+)\}|\$([a-zA-Z_]\w*)/g;
5947
6055
  const IDENTIFIER_PREFIX = '__v_';
@@ -6093,6 +6201,14 @@ function findVariableCycles(widgets, configuredVariableNames = []) {
6093
6201
  return inCycle;
6094
6202
  }
6095
6203
 
6204
+ const KPI_MAX_DECIMAL_PLACES = 3;
6205
+ function normalizeKpiDecimalPlaces(value) {
6206
+ if (typeof value !== 'number' || !Number.isInteger(value)) {
6207
+ return undefined;
6208
+ }
6209
+ return value >= 0 && value <= KPI_MAX_DECIMAL_PLACES ? value : undefined;
6210
+ }
6211
+
6096
6212
  class KpiWidgetComponent {
6097
6213
  dataService = inject(DashboardDataService);
6098
6214
  queryExecutor = inject(QueryExecutorService);
@@ -6222,7 +6338,7 @@ class KpiWidgetComponent {
6222
6338
  attributeName = '_queryValue';
6223
6339
  }
6224
6340
  else if (config?.dataSource?.type === 'static') {
6225
- attributeName = '_staticValue';
6341
+ attributeName = config.dataSource.data ? config.valueAttribute : '_staticValue';
6226
6342
  }
6227
6343
  else {
6228
6344
  attributeName = config?.valueAttribute;
@@ -6299,6 +6415,10 @@ class KpiWidgetComponent {
6299
6415
  if (strValue === '') {
6300
6416
  return '-';
6301
6417
  }
6418
+ const decimals = normalizeKpiDecimalPlaces(this.currentConfig()?.decimalPlaces);
6419
+ if (decimals !== undefined) {
6420
+ return this.formatWithDecimals(value, strValue, decimals);
6421
+ }
6302
6422
  const numValue = typeof value === 'number' ? value : parseFloat(strValue);
6303
6423
  if (isNaN(numValue)) {
6304
6424
  // Not a number, return as string
@@ -6309,6 +6429,19 @@ class KpiWidgetComponent {
6309
6429
  maximumFractionDigits: 2
6310
6430
  });
6311
6431
  }
6432
+ static strictNumber(text) {
6433
+ const trimmed = text.trim();
6434
+ return trimmed === '' ? NaN : Number(trimmed);
6435
+ }
6436
+ formatWithDecimals(value, strValue, decimals) {
6437
+ const numValue = typeof value === 'number' ? value : KpiWidgetComponent.strictNumber(strValue);
6438
+ if (typeof value === 'number' ? isNaN(numValue) : !Number.isFinite(numValue)) {
6439
+ return strValue;
6440
+ }
6441
+ const negative = numValue < 0 || Object.is(numValue, -0);
6442
+ const shown = negative && Number(getNumberFormat('en-US', 0, decimals, false).format(numValue)) === 0 ? 0 : numValue;
6443
+ return formatWidgetNumber(shown, decimals, 'exact');
6444
+ }
6312
6445
  label = computed(() => {
6313
6446
  const data = this._data();
6314
6447
  if (!this.config?.labelAttribute || !data)
@@ -6388,6 +6521,11 @@ class KpiWidgetComponent {
6388
6521
  this._error.set(null);
6389
6522
  return;
6390
6523
  }
6524
+ if (dataSource.type === 'static' && dataSource.data) {
6525
+ this._data.set(dataSource.data);
6526
+ this._error.set(null);
6527
+ return;
6528
+ }
6391
6529
  if (dataSource.type === 'static') {
6392
6530
  // Resolve static value with variable substitution
6393
6531
  const staticValue = this.config.staticValue ?? '';
@@ -6473,6 +6611,18 @@ class KpiWidgetComponent {
6473
6611
  // queryFamily may be undefined for legacy widget configs — the executor
6474
6612
  // falls back to a one-time lookup by rtId. streamDataArgs is sent
6475
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
+ }
6476
6626
  const streamDataArgs = this.buildStreamDataArgs();
6477
6627
  const result = await firstValueFrom(this.queryExecutor.execute(dataSource.queryFamily, dataSource.queryRtId, {
6478
6628
  fieldFilter: fieldFilter ?? undefined,
@@ -6571,6 +6721,10 @@ class KpiWidgetComponent {
6571
6721
  if (typeof value === 'number')
6572
6722
  return value;
6573
6723
  if (typeof value === 'string') {
6724
+ if (normalizeKpiDecimalPlaces(this.currentConfig()?.decimalPlaces) !== undefined) {
6725
+ const strict = KpiWidgetComponent.strictNumber(value);
6726
+ return Number.isFinite(strict) ? strict : value;
6727
+ }
6574
6728
  // Try to parse as number first
6575
6729
  const parsed = parseFloat(value);
6576
6730
  // If it's a valid number, return as number; otherwise return the string
@@ -6852,6 +7006,7 @@ class KpiConfigDialogComponent {
6852
7006
  initialSuffix;
6853
7007
  initialTrend;
6854
7008
  initialComparisonText;
7009
+ initialDecimalPlaces;
6855
7010
  // Initial values for editing - Persistent Query
6856
7011
  initialDataSourceType;
6857
7012
  initialQueryRtId;
@@ -6918,6 +7073,7 @@ class KpiConfigDialogComponent {
6918
7073
  suffix: '',
6919
7074
  trend: undefined,
6920
7075
  comparisonText: '',
7076
+ decimalPlaces: null,
6921
7077
  // Query-specific form fields
6922
7078
  queryValueField: '',
6923
7079
  queryCategoryField: '',
@@ -7021,6 +7177,9 @@ class KpiConfigDialogComponent {
7021
7177
  : null;
7022
7178
  }
7023
7179
  variableSyntaxHint = '${variableName}';
7180
+ get maxDecimalPlaces() {
7181
+ return KPI_MAX_DECIMAL_PLACES;
7182
+ }
7024
7183
  trendOptions = [
7025
7184
  { value: undefined, label: 'None' },
7026
7185
  { value: 'up', label: 'Up (positive)' },
@@ -7076,6 +7235,7 @@ class KpiConfigDialogComponent {
7076
7235
  this.form.suffix = this.initialSuffix || '';
7077
7236
  this.form.trend = this.initialTrend;
7078
7237
  this.form.comparisonText = this.initialComparisonText || '';
7238
+ this.form.decimalPlaces = this.initialDecimalPlaces ?? null;
7079
7239
  this.form.queryValueField = this.initialQueryValueField || '';
7080
7240
  this.form.queryCategoryField = this.initialQueryCategoryField || '';
7081
7241
  this.form.queryCategoryValue = this.initialQueryCategoryValue || '';
@@ -7252,11 +7412,7 @@ class KpiConfigDialogComponent {
7252
7412
  ckTypeId: '',
7253
7413
  valueAttribute: '',
7254
7414
  formula: this.form.formula.trim(),
7255
- prefix: this.form.prefix || undefined,
7256
- suffix: this.form.suffix || undefined,
7257
- trend: this.form.trend,
7258
- comparisonText: this.form.comparisonText || undefined,
7259
- outputVariableName: this.form.outputVariableName.trim() || undefined
7415
+ ...this.displayFields()
7260
7416
  });
7261
7417
  return;
7262
7418
  }
@@ -7266,11 +7422,7 @@ class KpiConfigDialogComponent {
7266
7422
  ckTypeId: '',
7267
7423
  valueAttribute: '',
7268
7424
  staticValue: this.form.staticValue,
7269
- prefix: this.form.prefix || undefined,
7270
- suffix: this.form.suffix || undefined,
7271
- trend: this.form.trend,
7272
- comparisonText: this.form.comparisonText || undefined,
7273
- outputVariableName: this.form.outputVariableName.trim() || undefined
7425
+ ...this.displayFields()
7274
7426
  });
7275
7427
  return;
7276
7428
  }
@@ -7290,11 +7442,7 @@ class KpiConfigDialogComponent {
7290
7442
  queryValueField: this.form.queryValueField || undefined,
7291
7443
  queryCategoryField: this.form.queryCategoryField || undefined,
7292
7444
  queryCategoryValue: this.form.queryCategoryValue || undefined,
7293
- prefix: this.form.prefix || undefined,
7294
- suffix: this.form.suffix || undefined,
7295
- trend: this.form.trend,
7296
- comparisonText: this.form.comparisonText || undefined,
7297
- outputVariableName: this.form.outputVariableName.trim() || undefined,
7445
+ ...this.displayFields(),
7298
7446
  filters: filtersDto
7299
7447
  });
7300
7448
  }
@@ -7305,15 +7453,21 @@ class KpiConfigDialogComponent {
7305
7453
  rtId: this.isCountMode ? undefined : this.selectedEntity?.rtId,
7306
7454
  valueAttribute: this.isCountMode ? '_count' : this.form.valueAttribute,
7307
7455
  labelAttribute: this.form.labelAttribute || undefined,
7308
- prefix: this.form.prefix || undefined,
7309
- suffix: this.form.suffix || undefined,
7310
- trend: this.form.trend,
7311
- comparisonText: this.form.comparisonText || undefined,
7312
- outputVariableName: this.form.outputVariableName.trim() || undefined,
7456
+ ...this.displayFields(),
7313
7457
  filters: filtersDto
7314
7458
  });
7315
7459
  }
7316
7460
  }
7461
+ displayFields() {
7462
+ return {
7463
+ prefix: this.form.prefix || undefined,
7464
+ suffix: this.form.suffix || undefined,
7465
+ trend: this.form.trend,
7466
+ comparisonText: this.form.comparisonText || undefined,
7467
+ outputVariableName: this.form.outputVariableName.trim() || undefined,
7468
+ decimalPlaces: normalizeKpiDecimalPlaces(this.form.decimalPlaces)
7469
+ };
7470
+ }
7317
7471
  onCancel() {
7318
7472
  this.windowRef.close();
7319
7473
  }
@@ -7458,7 +7612,7 @@ class KpiConfigDialogComponent {
7458
7612
  this.filters = updatedFilters;
7459
7613
  }
7460
7614
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: KpiConfigDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
7461
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: KpiConfigDialogComponent, isStandalone: true, selector: "mm-kpi-config-dialog", inputs: { initialCkTypeId: "initialCkTypeId", initialRtId: "initialRtId", initialValueAttribute: "initialValueAttribute", initialLabelAttribute: "initialLabelAttribute", initialPrefix: "initialPrefix", initialSuffix: "initialSuffix", initialTrend: "initialTrend", initialComparisonText: "initialComparisonText", initialDataSourceType: "initialDataSourceType", initialQueryRtId: "initialQueryRtId", initialQueryName: "initialQueryName", initialQueryFamily: "initialQueryFamily", initialIgnoreTimeFilter: "initialIgnoreTimeFilter", initialEntitySelectorId: "initialEntitySelectorId", initialQueryMode: "initialQueryMode", initialQueryValueField: "initialQueryValueField", initialQueryCategoryField: "initialQueryCategoryField", initialQueryCategoryValue: "initialQueryCategoryValue", initialStaticValue: "initialStaticValue", initialWidgetId: "initialWidgetId", initialFormula: "initialFormula", initialOutputVariableName: "initialOutputVariableName", initialFilters: "initialFilters" }, viewQueries: [{ propertyName: "ckTypeSelectorInput", first: true, predicate: ["ckTypeSelector"], descendants: true }, { propertyName: "entitySelectorInput", first: true, predicate: ["entitySelector"], descendants: true }, { propertyName: "querySelector", first: true, predicate: ["querySelector"], descendants: true }], ngImport: i0, template: `
7615
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: KpiConfigDialogComponent, isStandalone: true, selector: "mm-kpi-config-dialog", inputs: { initialCkTypeId: "initialCkTypeId", initialRtId: "initialRtId", initialValueAttribute: "initialValueAttribute", initialLabelAttribute: "initialLabelAttribute", initialPrefix: "initialPrefix", initialSuffix: "initialSuffix", initialTrend: "initialTrend", initialComparisonText: "initialComparisonText", initialDecimalPlaces: "initialDecimalPlaces", initialDataSourceType: "initialDataSourceType", initialQueryRtId: "initialQueryRtId", initialQueryName: "initialQueryName", initialQueryFamily: "initialQueryFamily", initialIgnoreTimeFilter: "initialIgnoreTimeFilter", initialEntitySelectorId: "initialEntitySelectorId", initialQueryMode: "initialQueryMode", initialQueryValueField: "initialQueryValueField", initialQueryCategoryField: "initialQueryCategoryField", initialQueryCategoryValue: "initialQueryCategoryValue", initialStaticValue: "initialStaticValue", initialWidgetId: "initialWidgetId", initialFormula: "initialFormula", initialOutputVariableName: "initialOutputVariableName", initialFilters: "initialFilters" }, viewQueries: [{ propertyName: "ckTypeSelectorInput", first: true, predicate: ["ckTypeSelector"], descendants: true }, { propertyName: "entitySelectorInput", first: true, predicate: ["entitySelector"], descendants: true }, { propertyName: "querySelector", first: true, predicate: ["querySelector"], descendants: true }], ngImport: i0, template: `
7462
7616
  <div class="config-container">
7463
7617
 
7464
7618
  <div class="config-form" [class.loading]="isLoadingInitial">
@@ -7871,6 +8025,19 @@ class KpiConfigDialogComponent {
7871
8025
  <kendo-textbox [(ngModel)]="form.comparisonText" placeholder="e.g. +3,1% vs. last week"></kendo-textbox>
7872
8026
  <p class="section-hint">Displayed below the value in the trend color. Supports {{ variableSyntaxHint }}</p>
7873
8027
  </div>
8028
+ <div class="form-field">
8029
+ <label>Decimal Places</label>
8030
+ <kendo-numerictextbox
8031
+ [(ngModel)]="form.decimalPlaces"
8032
+ [min]="0"
8033
+ [max]="maxDecimalPlaces"
8034
+ [step]="1"
8035
+ [decimals]="0"
8036
+ format="n0"
8037
+ placeholder="Automatic">
8038
+ </kendo-numerictextbox>
8039
+ <p class="section-hint">Fixed number of decimal places for numeric values (0-3). Leave empty for automatic (two for a fractional value, none for a whole number).</p>
8040
+ </div>
7874
8041
  </div>
7875
8042
 
7876
8043
  <!-- Filters Section -->
@@ -7900,7 +8067,7 @@ class KpiConfigDialogComponent {
7900
8067
  </button>
7901
8068
  </div>
7902
8069
  </div>
7903
- `, isInline: true, styles: [":host{display:block;height:100%}.config-container{display:flex;flex-direction:column;height:100%}.config-form{display:flex;flex-direction:column;gap:20px;flex:1;overflow-y:auto;padding:16px;position:relative}.config-form.loading{pointer-events:none}.form-field{display:flex;flex-direction:column;gap:6px}.form-field.disabled{opacity:.6}.form-field.flex-1{flex:1}.form-field label{font-weight:600;font-size:.9rem;color:var(--kendo-color-on-app-surface, #212529)}.field-hint{margin:0;font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.form-section{padding:16px;background:var(--kendo-color-surface-alt, #f8f9fa);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px}.form-section h4{margin:0 0 16px;font-size:.95rem;color:var(--kendo-color-primary, #0d6efd)}.section-hint{margin:0 0 12px;font-size:.85rem;color:var(--kendo-color-subtle, #6c757d)}.form-row{display:flex;gap:16px}.mode-toggle{display:flex;gap:8px}.mode-toggle button{flex:1}.attribute-item{display:flex;justify-content:space-between;align-items:center;gap:8px;width:100%}.attribute-path{flex:1}.attribute-type{font-size:.75rem;color:var(--kendo-color-subtle, #6c757d);background:var(--kendo-color-surface-alt, #f8f9fa);padding:2px 6px;border-radius:3px}.required{color:var(--kendo-color-error, #dc3545)}.field-error{margin:0;font-size:.8rem;color:var(--kendo-color-error, #dc3545)}.variable-list{display:flex;flex-wrap:wrap;gap:4px}.variable-source{color:var(--kendo-color-subtle, #6c757d)}.query-item{display:flex;flex-direction:column;gap:2px}.query-name{font-weight:500}.query-description{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.column-item{display:flex;justify-content:space-between;gap:16px}.column-path{font-weight:500}.column-type{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ButtonsModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "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: "ngmodule", type: DropDownsModule }, { kind: "directive", type: i4.ItemTemplateDirective, selector: "[kendoDropDownListItemTemplate],[kendoComboBoxItemTemplate],[kendoAutoCompleteItemTemplate],[kendoMultiSelectItemTemplate]" }, { kind: "component", type: i4.ComboBoxComponent, selector: "kendo-combobox", inputs: ["icon", "svgIcon", "inputAttributes", "showStickyHeader", "focusableId", "allowCustom", "data", "value", "textField", "valueField", "valuePrimitive", "valueNormalizer", "placeholder", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "loading", "suggest", "clearButton", "disabled", "itemDisabled", "readonly", "tabindex", "tabIndex", "filterable", "virtual", "size", "rounded", "fillMode"], outputs: ["valueChange", "selectionChange", "filterChange", "open", "opened", "close", "closed", "focus", "blur", "inputFocus", "inputBlur", "escape"], exportAs: ["kendoComboBox"] }, { kind: "component", type: i4.DropDownListComponent, selector: "kendo-dropdownlist", inputs: ["customIconClass", "showStickyHeader", "icon", "svgIcon", "loading", "data", "value", "textField", "valueField", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "defaultItem", "disabled", "itemDisabled", "readonly", "filterable", "virtual", "ignoreCase", "delay", "valuePrimitive", "tabindex", "tabIndex", "size", "rounded", "fillMode", "leftRightArrowsNavigation", "id"], outputs: ["valueChange", "filterChange", "selectionChange", "open", "opened", "close", "closed", "focus", "blur"], exportAs: ["kendoDropDownList"] }, { kind: "component", type: CkTypeSelectorInputComponent, selector: "mm-ck-type-selector-input", inputs: ["placeholder", "minSearchLength", "maxResults", "debounceMs", "ckModelIds", "allowAbstract", "dialogTitle", "advancedSearchLabel", "derivedFromRtCkTypeId", "messages", "dialogMessages", "disabled", "required"], outputs: ["ckTypeSelected", "ckTypeCleared"] }, { kind: "component", type: EntitySelectInputComponent, selector: "mm-entity-select-input", inputs: ["dataSource", "placeholder", "minSearchLength", "maxResults", "debounceMs", "prefix", "initialDisplayValue", "dialogDataSource", "dialogTitle", "multiSelect", "advancedSearchLabel", "dialogMessages", "messages", "disabled", "required"], outputs: ["entitySelected", "entityCleared", "entitiesSelected"] }, { kind: "component", type: FieldFilterEditorComponent, selector: "mm-field-filter-editor", inputs: ["availableAttributes", "ckTypeId", "hideNavigationProperties", "attributePaths", "enableVariables", "availableVariables", "filters"], outputs: ["filtersChange"] }, { kind: "component", type: QuerySelectorComponent, selector: "mm-query-selector", inputs: ["placeholder", "hint", "disabled", "acceptFamilies"], outputs: ["querySelected", "queriesLoaded"] }, { kind: "component", type: SdTimeFilterToggleComponent, selector: "mm-sd-time-filter-toggle", inputs: ["family", "ignoreTimeFilter"], outputs: ["ignoreTimeFilterChange"] }, { kind: "component", type: EntitySelectorScopePickerComponent, selector: "mm-entity-selector-scope-picker", inputs: ["family", "entitySelectorId", "selectors"], outputs: ["entitySelectorIdChange"] }, { kind: "component", type: LoadingOverlayComponent, selector: "mm-loading-overlay", inputs: ["loading"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
8070
+ `, isInline: true, styles: [":host{display:block;height:100%}.config-container{display:flex;flex-direction:column;height:100%}.config-form{display:flex;flex-direction:column;gap:20px;flex:1;overflow-y:auto;padding:16px;position:relative}.config-form.loading{pointer-events:none}.form-field{display:flex;flex-direction:column;gap:6px}.form-field.disabled{opacity:.6}.form-field.flex-1{flex:1}.form-field label{font-weight:600;font-size:.9rem;color:var(--kendo-color-on-app-surface, #212529)}.field-hint{margin:0;font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.form-section{padding:16px;background:var(--kendo-color-surface-alt, #f8f9fa);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px}.form-section h4{margin:0 0 16px;font-size:.95rem;color:var(--kendo-color-primary, #0d6efd)}.section-hint{margin:0 0 12px;font-size:.85rem;color:var(--kendo-color-subtle, #6c757d)}.form-row{display:flex;gap:16px}.mode-toggle{display:flex;gap:8px}.mode-toggle button{flex:1}.attribute-item{display:flex;justify-content:space-between;align-items:center;gap:8px;width:100%}.attribute-path{flex:1}.attribute-type{font-size:.75rem;color:var(--kendo-color-subtle, #6c757d);background:var(--kendo-color-surface-alt, #f8f9fa);padding:2px 6px;border-radius:3px}.required{color:var(--kendo-color-error, #dc3545)}.field-error{margin:0;font-size:.8rem;color:var(--kendo-color-error, #dc3545)}.variable-list{display:flex;flex-wrap:wrap;gap:4px}.variable-source{color:var(--kendo-color-subtle, #6c757d)}.query-item{display:flex;flex-direction:column;gap:2px}.query-name{font-weight:500}.query-description{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.column-item{display:flex;justify-content:space-between;gap:16px}.column-path{font-weight:500}.column-type{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ButtonsModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: InputsModule }, { kind: "component", type: i3.TextBoxComponent, selector: "kendo-textbox", inputs: ["focusableId", "title", "type", "disabled", "readonly", "tabindex", "value", "selectOnFocus", "showSuccessIcon", "showErrorIcon", "clearButton", "successIcon", "successSvgIcon", "errorIcon", "errorSvgIcon", "clearButtonIcon", "clearButtonSvgIcon", "size", "rounded", "fillMode", "tabIndex", "placeholder", "maxlength", "inputAttributes"], outputs: ["valueChange", "inputFocus", "inputBlur", "focus", "blur"], exportAs: ["kendoTextBox"] }, { kind: "component", type: i3.NumericTextBoxComponent, selector: "kendo-numerictextbox", inputs: ["focusableId", "disabled", "readonly", "title", "autoCorrect", "format", "max", "min", "decimals", "placeholder", "step", "spinners", "rangeValidation", "tabindex", "tabIndex", "changeValueOnScroll", "selectOnFocus", "value", "maxlength", "size", "rounded", "fillMode", "inputAttributes"], outputs: ["valueChange", "focus", "blur", "inputFocus", "inputBlur"], exportAs: ["kendoNumericTextBox"] }, { kind: "ngmodule", type: DropDownsModule }, { kind: "directive", type: i4.ItemTemplateDirective, selector: "[kendoDropDownListItemTemplate],[kendoComboBoxItemTemplate],[kendoAutoCompleteItemTemplate],[kendoMultiSelectItemTemplate]" }, { kind: "component", type: i4.ComboBoxComponent, selector: "kendo-combobox", inputs: ["icon", "svgIcon", "inputAttributes", "showStickyHeader", "focusableId", "allowCustom", "data", "value", "textField", "valueField", "valuePrimitive", "valueNormalizer", "placeholder", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "loading", "suggest", "clearButton", "disabled", "itemDisabled", "readonly", "tabindex", "tabIndex", "filterable", "virtual", "size", "rounded", "fillMode"], outputs: ["valueChange", "selectionChange", "filterChange", "open", "opened", "close", "closed", "focus", "blur", "inputFocus", "inputBlur", "escape"], exportAs: ["kendoComboBox"] }, { kind: "component", type: i4.DropDownListComponent, selector: "kendo-dropdownlist", inputs: ["customIconClass", "showStickyHeader", "icon", "svgIcon", "loading", "data", "value", "textField", "valueField", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "defaultItem", "disabled", "itemDisabled", "readonly", "filterable", "virtual", "ignoreCase", "delay", "valuePrimitive", "tabindex", "tabIndex", "size", "rounded", "fillMode", "leftRightArrowsNavigation", "id"], outputs: ["valueChange", "filterChange", "selectionChange", "open", "opened", "close", "closed", "focus", "blur"], exportAs: ["kendoDropDownList"] }, { kind: "component", type: CkTypeSelectorInputComponent, selector: "mm-ck-type-selector-input", inputs: ["placeholder", "minSearchLength", "maxResults", "debounceMs", "ckModelIds", "allowAbstract", "dialogTitle", "advancedSearchLabel", "derivedFromRtCkTypeId", "messages", "dialogMessages", "disabled", "required"], outputs: ["ckTypeSelected", "ckTypeCleared"] }, { kind: "component", type: EntitySelectInputComponent, selector: "mm-entity-select-input", inputs: ["dataSource", "placeholder", "minSearchLength", "maxResults", "debounceMs", "prefix", "initialDisplayValue", "dialogDataSource", "dialogTitle", "multiSelect", "advancedSearchLabel", "dialogMessages", "messages", "disabled", "required"], outputs: ["entitySelected", "entityCleared", "entitiesSelected"] }, { kind: "component", type: FieldFilterEditorComponent, selector: "mm-field-filter-editor", inputs: ["availableAttributes", "ckTypeId", "hideNavigationProperties", "attributePaths", "enableVariables", "availableVariables", "filters"], outputs: ["filtersChange"] }, { kind: "component", type: QuerySelectorComponent, selector: "mm-query-selector", inputs: ["placeholder", "hint", "disabled", "acceptFamilies"], outputs: ["querySelected", "queriesLoaded"] }, { kind: "component", type: SdTimeFilterToggleComponent, selector: "mm-sd-time-filter-toggle", inputs: ["family", "ignoreTimeFilter"], outputs: ["ignoreTimeFilterChange"] }, { kind: "component", type: EntitySelectorScopePickerComponent, selector: "mm-entity-selector-scope-picker", inputs: ["family", "entitySelectorId", "selectors"], outputs: ["entitySelectorIdChange"] }, { kind: "component", type: LoadingOverlayComponent, selector: "mm-loading-overlay", inputs: ["loading"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
7904
8071
  }
7905
8072
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: KpiConfigDialogComponent, decorators: [{
7906
8073
  type: Component,
@@ -8330,6 +8497,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
8330
8497
  <kendo-textbox [(ngModel)]="form.comparisonText" placeholder="e.g. +3,1% vs. last week"></kendo-textbox>
8331
8498
  <p class="section-hint">Displayed below the value in the trend color. Supports {{ variableSyntaxHint }}</p>
8332
8499
  </div>
8500
+ <div class="form-field">
8501
+ <label>Decimal Places</label>
8502
+ <kendo-numerictextbox
8503
+ [(ngModel)]="form.decimalPlaces"
8504
+ [min]="0"
8505
+ [max]="maxDecimalPlaces"
8506
+ [step]="1"
8507
+ [decimals]="0"
8508
+ format="n0"
8509
+ placeholder="Automatic">
8510
+ </kendo-numerictextbox>
8511
+ <p class="section-hint">Fixed number of decimal places for numeric values (0-3). Leave empty for automatic (two for a fractional value, none for a whole number).</p>
8512
+ </div>
8333
8513
  </div>
8334
8514
 
8335
8515
  <!-- Filters Section -->
@@ -8385,6 +8565,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
8385
8565
  type: Input
8386
8566
  }], initialComparisonText: [{
8387
8567
  type: Input
8568
+ }], initialDecimalPlaces: [{
8569
+ type: Input
8388
8570
  }], initialDataSourceType: [{
8389
8571
  type: Input
8390
8572
  }], initialQueryRtId: [{
@@ -10080,6 +10262,17 @@ class TableWidgetDataSourceDirective extends OctoGraphQlDataSource {
10080
10262
  * `'runtime'` for legacy configs that predate stream-data support.
10081
10263
  */
10082
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
+ }
10083
10276
  const fieldFilter = this.convertFiltersToDto(this._config?.filters);
10084
10277
  // queryFamily may be undefined for legacy widget configs — the executor
10085
10278
  // falls back to a one-time lookup by rtId. streamDataArgs is sent
@@ -10223,6 +10416,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
10223
10416
  /** Default texts of {@link MeshBoardMessages} (the list texts default inside `mm-list-view`). */
10224
10417
  const DEFAULT_MESHBOARD_MESSAGES = {
10225
10418
  tableNotConfigured: 'Table not configured',
10419
+ scopeResolutionFailed: 'Could not resolve the scope of "{label}" - widgets bound to it show no data.',
10226
10420
  };
10227
10421
  /**
10228
10422
  * Optional: the host's translations of the MeshBoard widget texts (English without it). Set it
@@ -10247,6 +10441,7 @@ function resolveMeshBoardMessages(messages) {
10247
10441
  ...DEFAULT_MESHBOARD_MESSAGES,
10248
10442
  ...(messages?.tableList ? { tableList: messages.tableList } : {}),
10249
10443
  ...(messages?.tableNotConfigured ? { tableNotConfigured: messages.tableNotConfigured } : {}),
10444
+ ...(messages?.scopeResolutionFailed ? { scopeResolutionFailed: messages.scopeResolutionFailed } : {}),
10250
10445
  };
10251
10446
  }
10252
10447
  /** The resolved MeshBoard messages of {@link MESHBOARD_MESSAGES} as a signal. Call in an injection context. */
@@ -11367,6 +11562,9 @@ class GaugeWidgetComponent {
11367
11562
  }, /* @ts-ignore */
11368
11563
  ...(ngDevMode ? [{ debugName: "numericValue" }] : /* istanbul ignore next */ []));
11369
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 '-';
11370
11568
  const value = this.numericValue();
11371
11569
  return value.toLocaleString('de-AT', {
11372
11570
  minimumFractionDigits: value % 1 !== 0 ? 1 : 0,
@@ -11438,6 +11636,18 @@ class GaugeWidgetComponent {
11438
11636
  // queryFamily may be undefined for legacy widget configs — the executor
11439
11637
  // falls back to a one-time lookup by rtId. streamDataArgs is sent
11440
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
+ }
11441
11651
  const streamDataArgs = this.buildStreamDataArgs();
11442
11652
  const result = await firstValueFrom(this.queryExecutor.execute(dataSource.queryFamily, dataSource.queryRtId, {
11443
11653
  fieldFilter: fieldFilter ?? undefined,
@@ -13540,6 +13750,18 @@ class PieChartWidgetComponent {
13540
13750
  * Note: isNotConfigured() check in loadData() ensures queryRtId is set.
13541
13751
  */
13542
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
+ }
13543
13765
  const fieldFilter = this.convertFiltersToDto(this.config.filters);
13544
13766
  // queryFamily may be undefined for legacy widget configs — the executor
13545
13767
  // falls back to a one-time lookup by rtId. streamDataArgs is sent
@@ -14754,14 +14976,23 @@ class BarChartWidgetComponent {
14754
14976
  // queryFamily may be undefined for legacy widget configs — the executor
14755
14977
  // falls back to a one-time lookup by rtId. streamDataArgs is sent
14756
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
+ }
14757
14986
  const streamDataArgs = this.buildStreamDataArgs();
14758
- const result = await firstValueFrom(this.queryExecutor.execute(queryDataSource.queryFamily, queryDataSource.queryRtId, {
14759
- fieldFilter: fieldFilter ?? undefined,
14760
- streamDataArgs
14761
- }).pipe(catchError(err => {
14762
- console.error('Error loading Bar Chart data:', err);
14763
- throw err;
14764
- })));
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
+ })));
14765
14996
  const filteredRows = result.rows.filter(row => BarChartWidgetComponent.SUPPORTED_ROW_TYPES.has(row.__typename ?? ''));
14766
14997
  if (this.isDynamicSeriesMode()) {
14767
14998
  this.processDynamicSeriesData(filteredRows);
@@ -16306,6 +16537,21 @@ class LineChartWidgetComponent {
16306
16537
  this._isLoading.set(true);
16307
16538
  this._error.set(null);
16308
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
+ }
16309
16555
  const fieldFilter = this.convertFiltersToDto(this.config.filters);
16310
16556
  // Resolution-aware routing (AB#4290): let the server pick the archive/rollup for the visible
16311
16557
  // window + target point count, then downsample each source series against it. Requires a
@@ -17840,10 +18086,7 @@ class HeatmapWidgetComponent {
17840
18086
  if (this.config.compactNumbers) {
17841
18087
  return this.formatSI(displayValue, decimals);
17842
18088
  }
17843
- return displayValue.toLocaleString('de-AT', {
17844
- minimumFractionDigits: 0,
17845
- maximumFractionDigits: decimals
17846
- });
18089
+ return formatWidgetNumber(displayValue, decimals, 'maximum');
17847
18090
  }
17848
18091
  /**
17849
18092
  * Formats a number using SI prefixes (k, M, G, T).
@@ -17860,16 +18103,10 @@ class HeatmapWidgetComponent {
17860
18103
  for (const [threshold, prefix] of siPrefixes) {
17861
18104
  if (abs >= threshold) {
17862
18105
  const scaled = value / threshold;
17863
- return scaled.toLocaleString('de-AT', {
17864
- minimumFractionDigits: 0,
17865
- maximumFractionDigits: decimals
17866
- }) + prefix;
18106
+ return formatWidgetNumber(scaled, decimals, 'maximum') + prefix;
17867
18107
  }
17868
18108
  }
17869
- return value.toLocaleString('de-AT', {
17870
- minimumFractionDigits: 0,
17871
- maximumFractionDigits: decimals
17872
- });
18109
+ return formatWidgetNumber(value, decimals, 'maximum');
17873
18110
  }
17874
18111
  cellLabelContent = (e) => {
17875
18112
  if (e.value === 0)
@@ -17896,14 +18133,23 @@ class HeatmapWidgetComponent {
17896
18133
  // queryFamily may be undefined for legacy widget configs — the executor
17897
18134
  // falls back to a one-time lookup by rtId. streamDataArgs is sent
17898
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
+ }
17899
18143
  const streamDataArgs = this.buildStreamDataArgs();
17900
- const result = await firstValueFrom(this.queryExecutor.execute(queryDataSource.queryFamily, queryDataSource.queryRtId, {
17901
- fieldFilter: fieldFilter ?? undefined,
17902
- streamDataArgs
17903
- }).pipe(catchError(err => {
17904
- console.error('Error loading Heatmap data:', err);
17905
- throw err;
17906
- })));
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
+ })));
17907
18153
  const filteredRows = result.rows.filter(row => HeatmapWidgetComponent.SUPPORTED_ROW_TYPES.has(row.__typename ?? ''));
17908
18154
  this.processHeatmapData(filteredRows);
17909
18155
  this._isLoading.set(false);
@@ -19238,6 +19484,13 @@ class StatsGridWidgetComponent {
19238
19484
  this._statValues.set(initialValues);
19239
19485
  this._isLoading.set(true);
19240
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
+ }
19241
19494
  try {
19242
19495
  // Runtime aggregation queries are only needed for stats WITHOUT a
19243
19496
  // per-stat persistent-query source — those run through the executor below.
@@ -19277,6 +19530,11 @@ class StatsGridWidgetComponent {
19277
19530
  return aggregationResults.get(stat.queryId) ?? null;
19278
19531
  }
19279
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
+ }
19280
19538
  try {
19281
19539
  const result = await firstValueFrom(this.queryExecutor.execute(source.queryFamily, source.queryRtId, {
19282
19540
  fieldFilter: this.convertFiltersToDto(source.filters),
@@ -19289,6 +19547,9 @@ class StatsGridWidgetComponent {
19289
19547
  return null;
19290
19548
  }
19291
19549
  }
19550
+ scopeOf(source) {
19551
+ return this.stateService.resolveStreamDataScope(source.entitySelectorId, source.queryFamily);
19552
+ }
19292
19553
  buildStreamDataArgs(source) {
19293
19554
  const timeArgs = this.stateService.resolveStreamDataTimeArgs(source.ignoreTimeFilter);
19294
19555
  const rtIds = this.stateService.resolveStreamDataRtIds(source.entitySelectorId);
@@ -21005,7 +21266,8 @@ class WidgetGroupComponent {
21005
21266
  prefix: staticConfig?.prefix,
21006
21267
  suffix: staticConfig?.suffix,
21007
21268
  trend: staticConfig?.trend,
21008
- icon: staticConfig?.icon
21269
+ icon: staticConfig?.icon,
21270
+ decimalPlaces: normalizeKpiDecimalPlaces(staticConfig?.decimalPlaces)
21009
21271
  };
21010
21272
  }
21011
21273
  /**
@@ -21138,6 +21400,9 @@ class WidgetGroupConfigDialogComponent {
21138
21400
  initialMinChildWidth;
21139
21401
  initialGap;
21140
21402
  initialEmptyMessage;
21403
+ get maxDecimalPlaces() {
21404
+ return KPI_MAX_DECIMAL_PLACES;
21405
+ }
21141
21406
  // Data source mode
21142
21407
  dataSourceMode = 'persistentQuery';
21143
21408
  // Query mode state
@@ -21160,6 +21425,7 @@ class WidgetGroupConfigDialogComponent {
21160
21425
  valueAttribute: '',
21161
21426
  prefix: '',
21162
21427
  suffix: '',
21428
+ decimalPlaces: null,
21163
21429
  gaugeType: 'arc',
21164
21430
  gaugeMin: 0,
21165
21431
  gaugeMax: 100,
@@ -21229,6 +21495,8 @@ class WidgetGroupConfigDialogComponent {
21229
21495
  this.form.prefix = staticConfig.prefix ?? '';
21230
21496
  if ('suffix' in staticConfig)
21231
21497
  this.form.suffix = staticConfig.suffix ?? '';
21498
+ if ('decimalPlaces' in staticConfig)
21499
+ this.form.decimalPlaces = normalizeKpiDecimalPlaces(staticConfig.decimalPlaces) ?? null;
21232
21500
  if ('gaugeType' in staticConfig)
21233
21501
  this.form.gaugeType = staticConfig.gaugeType ?? 'arc';
21234
21502
  if ('min' in staticConfig)
@@ -21448,7 +21716,8 @@ class WidgetGroupConfigDialogComponent {
21448
21716
  case 'kpi':
21449
21717
  return {
21450
21718
  prefix: this.form.prefix || undefined,
21451
- suffix: this.form.suffix || undefined
21719
+ suffix: this.form.suffix || undefined,
21720
+ decimalPlaces: normalizeKpiDecimalPlaces(this.form.decimalPlaces)
21452
21721
  };
21453
21722
  case 'gauge':
21454
21723
  return {
@@ -21675,6 +21944,19 @@ class WidgetGroupConfigDialogComponent {
21675
21944
  </kendo-textbox>
21676
21945
  </div>
21677
21946
  </div>
21947
+ <div class="form-field">
21948
+ <label>Decimal Places</label>
21949
+ <kendo-numerictextbox
21950
+ [(ngModel)]="form.decimalPlaces"
21951
+ [min]="0"
21952
+ [max]="maxDecimalPlaces"
21953
+ [step]="1"
21954
+ [decimals]="0"
21955
+ format="n0"
21956
+ placeholder="Automatic">
21957
+ </kendo-numerictextbox>
21958
+ <p class="field-hint">Fixed number of decimal places for numeric values (0-3). Leave empty for automatic (two for a fractional value, none for a whole number).</p>
21959
+ </div>
21678
21960
  }
21679
21961
  </div>
21680
21962
 
@@ -22009,6 +22291,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
22009
22291
  </kendo-textbox>
22010
22292
  </div>
22011
22293
  </div>
22294
+ <div class="form-field">
22295
+ <label>Decimal Places</label>
22296
+ <kendo-numerictextbox
22297
+ [(ngModel)]="form.decimalPlaces"
22298
+ [min]="0"
22299
+ [max]="maxDecimalPlaces"
22300
+ [step]="1"
22301
+ [decimals]="0"
22302
+ format="n0"
22303
+ placeholder="Automatic">
22304
+ </kendo-numerictextbox>
22305
+ <p class="field-hint">Fixed number of decimal places for numeric values (0-3). Leave empty for automatic (two for a fractional value, none for a whole number).</p>
22306
+ </div>
22012
22307
  }
22013
22308
  </div>
22014
22309
 
@@ -22879,6 +23174,13 @@ class SummaryCardWidgetComponent {
22879
23174
  return;
22880
23175
  this._isLoading.set(true);
22881
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
+ }
22882
23184
  try {
22883
23185
  // Cache entities by rtId to avoid duplicate fetches
22884
23186
  const entityCache = new Map();
@@ -22927,12 +23229,20 @@ class SummaryCardWidgetComponent {
22927
23229
  return null;
22928
23230
  }
22929
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
+ }
22930
23237
  const result = await firstValueFrom(this.queryExecutor.execute(source.queryFamily, source.queryRtId, {
22931
23238
  fieldFilter: this.convertFiltersToDto(source.filters),
22932
23239
  streamDataArgs: this.buildStreamDataArgs(source)
22933
23240
  }));
22934
23241
  return extractPersistentQueryCellValue(result, source);
22935
23242
  }
23243
+ scopeOf(source) {
23244
+ return this.stateService.resolveStreamDataScope(source.entitySelectorId, source.queryFamily);
23245
+ }
22936
23246
  buildStreamDataArgs(source) {
22937
23247
  const timeArgs = this.stateService.resolveStreamDataTimeArgs(source.ignoreTimeFilter);
22938
23248
  const rtIds = this.stateService.resolveStreamDataRtIds(source.entitySelectorId);
@@ -24639,6 +24949,21 @@ class ProcessDataService {
24639
24949
  if (!bindingQueryRtId) {
24640
24950
  throw new Error('Persistent query binding requires a Query rtId');
24641
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
+ }
24642
24967
  // Convert widget filters to GraphQL format with variable resolution
24643
24968
  const fieldFilters = this.convertFiltersToDto(bindingFilters);
24644
24969
  // Execute via the unified executor — it routes runtime vs stream-data from
@@ -24867,13 +25192,16 @@ class ProcessWidgetComponent {
24867
25192
  this._isLoading.set(true);
24868
25193
  this._error.set(null);
24869
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;
24870
25198
  try {
24871
25199
  // Check for inline config first
24872
25200
  if (this.config.inlineConfig) {
24873
25201
  this._diagramConfig.set(this.config.inlineConfig);
24874
25202
  await this.loadSymbolDefinitions(this.config.inlineConfig);
24875
25203
  await this.loadRuntimeData();
24876
- await this.loadBoundData();
25204
+ scopePending = await this.loadBoundData();
24877
25205
  return;
24878
25206
  }
24879
25207
  // Load from backend by rtId
@@ -24882,7 +25210,7 @@ class ProcessWidgetComponent {
24882
25210
  this._diagramConfig.set(diagram);
24883
25211
  await this.loadSymbolDefinitions(diagram);
24884
25212
  await this.loadRuntimeData();
24885
- await this.loadBoundData();
25213
+ scopePending = await this.loadBoundData();
24886
25214
  return;
24887
25215
  }
24888
25216
  // No configuration
@@ -24893,25 +25221,32 @@ class ProcessWidgetComponent {
24893
25221
  this._error.set('Failed to load process diagram');
24894
25222
  }
24895
25223
  finally {
24896
- this._isLoading.set(false);
25224
+ if (!scopePending) {
25225
+ this._isLoading.set(false);
25226
+ }
24897
25227
  }
24898
25228
  }
24899
25229
  /**
24900
- * 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).
24901
25232
  */
24902
25233
  async loadBoundData() {
24903
25234
  // Only load if data binding is configured
24904
25235
  if (!this.config.dataBindingMode || this.config.dataBindingMode === 'none') {
24905
- return;
25236
+ return false;
24906
25237
  }
24907
25238
  try {
24908
25239
  const boundData = await this.dataService.loadBoundData(this.config);
25240
+ if (boundData?.pending) {
25241
+ return true;
25242
+ }
24909
25243
  this._boundData.set(boundData);
24910
25244
  }
24911
25245
  catch (err) {
24912
25246
  console.error('Error loading bound data:', err);
24913
25247
  // Don't set error - diagram still displays, just without bound data
24914
25248
  }
25249
+ return false;
24915
25250
  }
24916
25251
  /**
24917
25252
  * Loads symbol definitions for all symbol instances in the diagram
@@ -29796,6 +30131,27 @@ function createDefaultPersistentQueryDataSource() {
29796
30131
  * Registers all built-in widget types with the registry.
29797
30132
  * Called during app initialization.
29798
30133
  */
30134
+ function kpiDisplayFieldsFromResult(result) {
30135
+ return {
30136
+ outputVariableName: result.outputVariableName,
30137
+ prefix: result.prefix,
30138
+ suffix: result.suffix,
30139
+ trend: result.trend,
30140
+ comparisonText: result.comparisonText,
30141
+ decimalPlaces: result.decimalPlaces
30142
+ };
30143
+ }
30144
+ function kpiDisplayFieldsFromPersisted(config) {
30145
+ return {
30146
+ prefix: config['prefix'],
30147
+ suffix: config['suffix'],
30148
+ icon: config['icon'],
30149
+ trend: config['trend'],
30150
+ comparisonText: config['comparisonText'],
30151
+ outputVariableName: config['outputVariableName'],
30152
+ decimalPlaces: normalizeKpiDecimalPlaces(config['decimalPlaces'])
30153
+ };
30154
+ }
29799
30155
  function registerDefaultWidgets(registry) {
29800
30156
  // Entity Card Widget
29801
30157
  registry.registerWidget({
@@ -29915,6 +30271,7 @@ function registerDefaultWidgets(registry) {
29915
30271
  initialSuffix: kpiWidget.suffix,
29916
30272
  initialTrend: kpiWidget.trend,
29917
30273
  initialComparisonText: kpiWidget.comparisonText,
30274
+ initialDecimalPlaces: normalizeKpiDecimalPlaces(kpiWidget.decimalPlaces),
29918
30275
  // Filters
29919
30276
  initialFilters: kpiWidget.filters
29920
30277
  };
@@ -29932,7 +30289,6 @@ function registerDefaultWidgets(registry) {
29932
30289
  dataSource: { type: 'static' },
29933
30290
  valueMode: 'formula',
29934
30291
  formula: result.formula,
29935
- outputVariableName: result.outputVariableName,
29936
30292
  valueAttribute: '',
29937
30293
  staticValue: undefined,
29938
30294
  labelAttribute: undefined,
@@ -29940,10 +30296,7 @@ function registerDefaultWidgets(registry) {
29940
30296
  queryValueField: undefined,
29941
30297
  queryCategoryField: undefined,
29942
30298
  queryCategoryValue: undefined,
29943
- prefix: result.prefix,
29944
- suffix: result.suffix,
29945
- trend: result.trend,
29946
- comparisonText: result.comparisonText,
30299
+ ...kpiDisplayFieldsFromResult(result),
29947
30300
  filters: undefined
29948
30301
  };
29949
30302
  }
@@ -29952,7 +30305,6 @@ function registerDefaultWidgets(registry) {
29952
30305
  ...widget,
29953
30306
  valueMode: undefined,
29954
30307
  formula: undefined,
29955
- outputVariableName: result.outputVariableName,
29956
30308
  dataSource: { type: 'static' },
29957
30309
  valueAttribute: '',
29958
30310
  staticValue: result.staticValue,
@@ -29961,10 +30313,7 @@ function registerDefaultWidgets(registry) {
29961
30313
  queryValueField: undefined,
29962
30314
  queryCategoryField: undefined,
29963
30315
  queryCategoryValue: undefined,
29964
- prefix: result.prefix,
29965
- suffix: result.suffix,
29966
- trend: result.trend,
29967
- comparisonText: result.comparisonText,
30316
+ ...kpiDisplayFieldsFromResult(result),
29968
30317
  filters: undefined
29969
30318
  };
29970
30319
  }
@@ -29982,7 +30331,6 @@ function registerDefaultWidgets(registry) {
29982
30331
  ...widget,
29983
30332
  valueMode: undefined,
29984
30333
  formula: undefined,
29985
- outputVariableName: result.outputVariableName,
29986
30334
  dataSource,
29987
30335
  valueAttribute: '',
29988
30336
  staticValue: undefined,
@@ -29990,10 +30338,7 @@ function registerDefaultWidgets(registry) {
29990
30338
  queryValueField: result.queryValueField,
29991
30339
  queryCategoryField: result.queryCategoryField,
29992
30340
  queryCategoryValue: result.queryCategoryValue,
29993
- prefix: result.prefix,
29994
- suffix: result.suffix,
29995
- trend: result.trend,
29996
- comparisonText: result.comparisonText,
30341
+ ...kpiDisplayFieldsFromResult(result),
29997
30342
  filters
29998
30343
  };
29999
30344
  }
@@ -30003,7 +30348,6 @@ function registerDefaultWidgets(registry) {
30003
30348
  ...widget,
30004
30349
  valueMode: undefined,
30005
30350
  formula: undefined,
30006
- outputVariableName: result.outputVariableName,
30007
30351
  dataSource: createDataSource(result, false),
30008
30352
  valueAttribute: result.valueAttribute,
30009
30353
  labelAttribute: result.labelAttribute,
@@ -30012,10 +30356,7 @@ function registerDefaultWidgets(registry) {
30012
30356
  queryValueField: undefined,
30013
30357
  queryCategoryField: undefined,
30014
30358
  queryCategoryValue: undefined,
30015
- prefix: result.prefix,
30016
- suffix: result.suffix,
30017
- trend: result.trend,
30018
- comparisonText: result.comparisonText,
30359
+ ...kpiDisplayFieldsFromResult(result),
30019
30360
  filters
30020
30361
  };
30021
30362
  }
@@ -30048,6 +30389,7 @@ function registerDefaultWidgets(registry) {
30048
30389
  icon: widget.icon,
30049
30390
  trend: widget.trend,
30050
30391
  comparisonText: widget.comparisonText,
30392
+ decimalPlaces: widget.decimalPlaces,
30051
30393
  staticValue: widget.staticValue,
30052
30394
  valueMode: widget.valueMode,
30053
30395
  formula: widget.formula,
@@ -30088,12 +30430,7 @@ function registerDefaultWidgets(registry) {
30088
30430
  staticValue: config['staticValue'],
30089
30431
  valueMode: config['valueMode'] === 'formula' ? 'formula' : undefined,
30090
30432
  formula: config['formula'],
30091
- prefix: config['prefix'],
30092
- suffix: config['suffix'],
30093
- icon: config['icon'],
30094
- trend: config['trend'],
30095
- comparisonText: config['comparisonText'],
30096
- outputVariableName: config['outputVariableName']
30433
+ ...kpiDisplayFieldsFromPersisted(config)
30097
30434
  };
30098
30435
  }
30099
30436
  if (dataSource.type === 'persistentQuery') {
@@ -30108,12 +30445,7 @@ function registerDefaultWidgets(registry) {
30108
30445
  queryCategoryField: config['queryCategoryField'],
30109
30446
  queryCategoryValue: config['queryCategoryValue'],
30110
30447
  valueMultiplier: config['valueMultiplier'],
30111
- prefix: config['prefix'],
30112
- suffix: config['suffix'],
30113
- icon: config['icon'],
30114
- trend: config['trend'],
30115
- comparisonText: config['comparisonText'],
30116
- outputVariableName: config['outputVariableName'],
30448
+ ...kpiDisplayFieldsFromPersisted(config),
30117
30449
  filters: config['filters']
30118
30450
  };
30119
30451
  }
@@ -30124,12 +30456,7 @@ function registerDefaultWidgets(registry) {
30124
30456
  dataSource: dataSource,
30125
30457
  valueAttribute: config['valueAttribute'] ?? '_count',
30126
30458
  labelAttribute: config['labelAttribute'],
30127
- prefix: config['prefix'],
30128
- suffix: config['suffix'],
30129
- icon: config['icon'],
30130
- trend: config['trend'],
30131
- comparisonText: config['comparisonText'],
30132
- outputVariableName: config['outputVariableName'],
30459
+ ...kpiDisplayFieldsFromPersisted(config),
30133
30460
  filters: config['filters']
30134
30461
  };
30135
30462
  }
@@ -32009,6 +32336,96 @@ function provideWidgetRegistrations(registrations) {
32009
32336
  ]);
32010
32337
  }
32011
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
+
32012
32429
  /**
32013
32430
  * Component for editing entity selector configurations in MeshBoard settings.
32014
32431
  * Allows adding, editing, and removing entity selectors.
@@ -32019,6 +32436,7 @@ class EntitySelectorEditorComponent {
32019
32436
  attributeSelectorService = inject(AttributeSelectorDialogService);
32020
32437
  variableService = inject(MeshBoardVariableService);
32021
32438
  getEntitiesByCkTypeGQL = inject(GetEntitiesByCkTypeDtoGQL);
32439
+ confirmationService = inject(ConfirmationService, { optional: true });
32022
32440
  entitySelectors = [];
32023
32441
  /** Existing variable names from other sources (static variables, other selectors) for duplicate detection */
32024
32442
  existingVariableNames = [];
@@ -32047,6 +32465,10 @@ class EntitySelectorEditorComponent {
32047
32465
  // the association to its parent), so 'in' is the correct default for a
32048
32466
  // childScope that resolves a picked parent to its children.
32049
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 = '';
32050
32472
  // Data sources for the default entity picker (created when CK type is set)
32051
32473
  defaultEntityDataSource = null;
32052
32474
  defaultEntityDialogDataSource = null;
@@ -32066,6 +32488,8 @@ class EntitySelectorEditorComponent {
32066
32488
  this.editChildScopeTargetCkTypeItem = null;
32067
32489
  this.editChildScopeRoleId = 'System/ParentChild';
32068
32490
  this.editChildScopeDirection = 'in';
32491
+ this.editFollowsSelectorId = '';
32492
+ this.followNotice = '';
32069
32493
  this.defaultEntityDataSource = null;
32070
32494
  this.defaultEntityDialogDataSource = null;
32071
32495
  this.isEditing = true;
@@ -32101,6 +32525,14 @@ class EntitySelectorEditorComponent {
32101
32525
  : null;
32102
32526
  this.editChildScopeRoleId = selector.childScope?.roleId ?? 'System/ParentChild';
32103
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
+ }
32104
32536
  this.updateDefaultEntityDataSources();
32105
32537
  this.isEditing = true;
32106
32538
  this.editingStateChange.emit(true);
@@ -32108,10 +32540,82 @@ class EntitySelectorEditorComponent {
32108
32540
  /**
32109
32541
  * Removes an entity selector at the given index.
32110
32542
  */
32111
- 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
+ }
32112
32559
  this.entitySelectors = this.entitySelectors.filter((_, i) => i !== index);
32113
32560
  this.emitChange();
32114
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
+ }
32115
32619
  /**
32116
32620
  * Handles CK type selection in the editor form.
32117
32621
  */
@@ -32201,7 +32705,9 @@ class EntitySelectorEditorComponent {
32201
32705
  attributeMappings: this.editMappings,
32202
32706
  showInToolbar: this.editShowInToolbar,
32203
32707
  defaultRtId: this.editDefaultRtId.trim() || undefined,
32204
- childScope
32708
+ childScope,
32709
+ // A follower never renders a picker (AB#7722).
32710
+ ...(this.editFollowsSelectorId ? { showInToolbar: false, followsSelectorId: this.editFollowsSelectorId } : {})
32205
32711
  };
32206
32712
  if (this.editingIndex !== null) {
32207
32713
  this.entitySelectors = this.entitySelectors.map((es, i) => i === this.editingIndex ? selector : es);
@@ -32229,13 +32735,18 @@ class EntitySelectorEditorComponent {
32229
32735
  const hasRequiredFields = this.editId.trim().length > 0 &&
32230
32736
  this.editLabel.trim().length > 0 &&
32231
32737
  this.editCkTypeId.length > 0 &&
32232
- 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) &&
32233
32740
  this.editMappings.every(m => m.variableName.trim().length > 0 &&
32234
32741
  this.isValidVariableName(m.variableName) &&
32235
32742
  !this.isDuplicateVariableName(m.variableName, m.attributePath)) &&
32236
32743
  !this.isDuplicateId(this.editId, this.editingIndex);
32237
- // If not shown in toolbar, a default entity must be selected
32238
- 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) {
32239
32750
  return false;
32240
32751
  }
32241
32752
  return hasRequiredFields;
@@ -32258,6 +32769,13 @@ class EntitySelectorEditorComponent {
32258
32769
  * Gets a display label indicating the selector mode.
32259
32770
  */
32260
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
+ }
32261
32779
  return selector.showInToolbar !== false ? 'Toolbar' : 'Fixed';
32262
32780
  }
32263
32781
  /**
@@ -32321,7 +32839,7 @@ class EntitySelectorEditorComponent {
32321
32839
  this.entitySelectorsChange.emit([...this.entitySelectors]);
32322
32840
  }
32323
32841
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: EntitySelectorEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
32324
- 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 });
32325
32843
  }
32326
32844
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: EntitySelectorEditorComponent, decorators: [{
32327
32845
  type: Component,
@@ -32334,7 +32852,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
32334
32852
  SVGIconModule,
32335
32853
  CkTypeSelectorInputComponent,
32336
32854
  EntitySelectInputComponent
32337
- ], 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"] }]
32338
32856
  }], propDecorators: { entitySelectors: [{
32339
32857
  type: Input
32340
32858
  }], existingVariableNames: [{
@@ -32365,6 +32883,13 @@ class EntitySelectorToolbarComponent {
32365
32883
  this.updateDataSources();
32366
32884
  }
32367
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
+ }
32368
32893
  /**
32369
32894
  * Returns the data source for a selector.
32370
32895
  */
@@ -32406,7 +32931,7 @@ class EntitySelectorToolbarComponent {
32406
32931
  }
32407
32932
  }
32408
32933
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: EntitySelectorToolbarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
32409
- 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 });
32410
32935
  }
32411
32936
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: EntitySelectorToolbarComponent, decorators: [{
32412
32937
  type: Component,
@@ -32414,7 +32939,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
32414
32939
  CommonModule,
32415
32940
  KENDO_LABELS,
32416
32941
  EntitySelectInputComponent
32417
- ], 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"] }]
32418
32943
  }], propDecorators: { entitySelectors: [{
32419
32944
  type: Input
32420
32945
  }], entitySelected: [{
@@ -34262,13 +34787,13 @@ class MeshBoardViewComponent {
34262
34787
  // Entity Selector computed signals
34263
34788
  entitySelectorsConfig = computed(() => this.stateService.getEntitySelectors(), /* @ts-ignore */
34264
34789
  ...(ngDevMode ? [{ debugName: "entitySelectorsConfig" }] : /* istanbul ignore next */ []));
34265
- 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 */
34266
34791
  ...(ngDevMode ? [{ debugName: "hasEntitySelectors" }] : /* istanbul ignore next */ []));
34267
34792
  unselectedToolbarSelectors = computed(() => {
34268
34793
  if (this.hideEntitySelectors())
34269
34794
  return [];
34270
34795
  const selectors = this.entitySelectorsConfig() ?? [];
34271
- return selectors.filter(es => es.showInToolbar !== false && !es.selectedRtId);
34796
+ return selectors.filter(es => showsInToolbar(es) && !es.selectedRtId);
34272
34797
  }, /* @ts-ignore */
34273
34798
  ...(ngDevMode ? [{ debugName: "unselectedToolbarSelectors" }] : /* istanbul ignore next */ []));
34274
34799
  /**
@@ -34285,6 +34810,7 @@ class MeshBoardViewComponent {
34285
34810
  }, /* @ts-ignore */
34286
34811
  ...(ngDevMode ? [{ debugName: "canResetTimeFilter" }] : /* istanbul ignore next */ []));
34287
34812
  constructor() {
34813
+ this.messages = injectMeshBoardMessages();
34288
34814
  // Effect to sync URL when dashboard changes (after initial load)
34289
34815
  effect(() => {
34290
34816
  const currentRtId = this.stateService.persistedMeshBoardId();
@@ -34399,6 +34925,7 @@ class MeshBoardViewComponent {
34399
34925
  // Load MeshBoard by well-known name from route data
34400
34926
  const widgets = await this.stateService.switchToMeshBoardByWellKnownName(wellKnownName);
34401
34927
  if (widgets) {
34928
+ this.primeEntitySelectorScopes();
34402
34929
  await this.preloadWidgetData(widgets);
34403
34930
  }
34404
34931
  else {
@@ -34409,6 +34936,7 @@ class MeshBoardViewComponent {
34409
34936
  else {
34410
34937
  // Load initial MeshBoard (first available)
34411
34938
  const widgets = await this.stateService.loadInitialMeshBoard();
34939
+ this.primeEntitySelectorScopes();
34412
34940
  await this.preloadWidgetData(widgets);
34413
34941
  }
34414
34942
  // Initialize time filter variables if enabled with stored selection
@@ -34470,11 +34998,13 @@ class MeshBoardViewComponent {
34470
34998
  if (exists) {
34471
34999
  // Switch to the requested MeshBoard
34472
35000
  const switchedWidgets = await this.stateService.switchToMeshBoard(rtId);
35001
+ this.primeEntitySelectorScopes();
34473
35002
  await this.preloadWidgetData(switchedWidgets);
34474
35003
  }
34475
35004
  else {
34476
35005
  // MeshBoard not found - use whatever was loaded initially
34477
35006
  console.warn(`MeshBoard with rtId ${rtId} not found, using default`);
35007
+ this.primeEntitySelectorScopes();
34478
35008
  await this.preloadWidgetData(widgets);
34479
35009
  }
34480
35010
  }
@@ -34781,6 +35311,11 @@ class MeshBoardViewComponent {
34781
35311
  console.warn(`Entity selector '${selectorId}' not found`);
34782
35312
  return;
34783
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
+ }
34784
35319
  // Update selection state
34785
35320
  this.stateService.updateEntitySelectorSelection(selectorId, rtId, displayName);
34786
35321
  // Fetch entity and set variables from attribute mappings
@@ -34795,12 +35330,80 @@ class MeshBoardViewComponent {
34795
35330
  * Programmatically clears an entity selector value from outside the component.
34796
35331
  */
34797
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) {
34798
35351
  this.stateService.updateEntitySelectorSelection(selectorId, undefined);
34799
35352
  this.stateService.clearEntitySelectorVariables(selectorId);
34800
- 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;
34801
35383
  }
34802
35384
  /** Resolution errors for runtime entity variables */
34803
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();
34804
35407
  // ============================================================================
34805
35408
  // Time Filter Event Handlers
34806
35409
  // ============================================================================
@@ -35234,7 +35837,8 @@ class MeshBoardViewComponent {
35234
35837
  async onEntitySelectorSelected(event) {
35235
35838
  const { selectorId, entity } = event;
35236
35839
  const selector = this.stateService.getEntitySelector(selectorId);
35237
- if (!selector)
35840
+ // Followers render no picker; their pick is derived from the leader (AB#7722).
35841
+ if (!selector || isFollowerSelector(selector))
35238
35842
  return;
35239
35843
  // Update selector selection state
35240
35844
  this.stateService.updateEntitySelectorSelection(selectorId, entity.rtId, entity.displayName);
@@ -35251,12 +35855,14 @@ class MeshBoardViewComponent {
35251
35855
  */
35252
35856
  async onEntitySelectorCleared(event) {
35253
35857
  const { selectorId } = event;
35254
- // Clear selection state
35255
- this.stateService.updateEntitySelectorSelection(selectorId, undefined);
35256
- // Clear variables for this selector
35257
- this.stateService.clearEntitySelectorVariables(selectorId);
35258
- // Clear the resolved stream-data scope rtIds for this selector
35259
- 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);
35260
35866
  // Sync to URL
35261
35867
  this.writeEntitySelectorsToUrl();
35262
35868
  // Refresh widgets
@@ -35266,84 +35872,179 @@ class MeshBoardViewComponent {
35266
35872
  * Initializes entity selectors from URL params, route data, or defaults.
35267
35873
  */
35268
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) {
35269
35905
  const selectors = this.stateService.getEntitySelectors();
35270
35906
  if (selectors.length === 0)
35271
- return;
35272
- // Read URL params
35907
+ return [];
35273
35908
  const urlSelections = this.readEntitySelectorsFromUrl();
35274
- // Read route data
35275
35909
  const routeData = this.route.snapshot.data['entitySelectors'];
35910
+ const ownPick = (selector) => routeData?.[selector.id] ?? urlSelections.get(selector.id) ?? selector.defaultRtId;
35911
+ const picks = [];
35276
35912
  for (const selector of selectors) {
35277
- // Priority: route data > URL params > defaultRtId
35278
- const rtId = routeData?.[selector.id]
35279
- ?? urlSelections.get(selector.id)
35280
- ?? selector.defaultRtId;
35281
- if (rtId) {
35282
- const displayName = await this.resolveEntitySelectorVariables(selector, rtId);
35283
- 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);
35284
35944
  }
35285
35945
  }
35286
35946
  }
35287
35947
  /**
35288
- * 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.
35289
35956
  */
35290
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]));
35291
35967
  try {
35292
35968
  const entity = await firstValueFrom(this.dataService.fetchRuntimeEntity({
35293
35969
  type: 'runtimeEntity',
35294
- ckTypeId: selector.ckTypeId,
35970
+ ckTypeId: resolved.ckTypeId,
35295
35971
  rtId
35296
35972
  }));
35297
35973
  if (!entity) {
35298
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();
35299
35978
  return undefined;
35300
35979
  }
35301
- const values = selector.attributeMappings.map(mapping => {
35302
- const attr = entity.attributes.find(a => a.attributeName === mapping.attributePath);
35303
- return {
35304
- name: mapping.variableName,
35305
- value: attr ? String(attr.value ?? '') : '',
35306
- type: mapping.attributeValueType
35307
- ? this.variableService.mapAttributeTypeToVariableType(mapping.attributeValueType)
35308
- : 'string'
35309
- };
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
+ }
35310
35987
  });
35311
- // Auto-expose the selected entity's rtId as `$<selectorId>_rtId` so SD/runtime
35312
- // widgets can scope to the picked asset (parent-association filter) without the
35313
- // designer adding a fake attribute mapping. An explicit mapping to the same
35314
- // variable name wins (skip the synthetic one if already present).
35315
- const rtIdVariableName = `${selector.id}_rtId`;
35316
- if (!values.some(v => v.name === rtIdVariableName)) {
35317
- values.push({ name: rtIdVariableName, value: rtId, type: 'string' });
35318
- }
35319
- // Auto-expose the selected entity's CK type as `$<selectorId>_rtCkTypeId`
35320
- // so entity-display widgets (e.g. the entity card) can resolve both the
35321
- // rtId and its type from a single board-level selection. Prefer the picked
35322
- // entity's actual type (it may be a subtype of the selector's configured
35323
- // `ckTypeId`); fall back to the selector's type. An explicit mapping to the
35324
- // same variable name wins.
35325
- const rtCkTypeIdVariableName = `${selector.id}_rtCkTypeId`;
35326
- if (!values.some(v => v.name === rtCkTypeIdVariableName)) {
35327
- values.push({ name: rtCkTypeIdVariableName, value: entity.ckTypeId ?? selector.ckTypeId, type: 'string' });
35328
- }
35329
- this.stateService.setEntitySelectorVariables(selector.id, values);
35330
- // Resolve the stream-data scope rtIds for this selection (one-hop
35331
- // childScope traversal, or the picked entity itself). Runs alongside the
35332
- // variable resolution so widgets bound to this selector can scope their
35333
- // stream-data query by the resolved source rtIds.
35334
- await this.resolveEntitySelectorScopeRtIds(selector, rtId);
35335
35988
  // Label the selection the way the pick dialog does: by the engine's
35336
35989
  // display name (the type's displayNameRule — "2026" for a fiscal year),
35337
35990
  // not the well-known name, which most entities do not carry. Without it
35338
35991
  // a board opened on its defaultRtId showed the raw rtId in the toolbar
35339
35992
  // until the user re-picked the very same entity.
35340
- 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;
35341
36005
  }
35342
36006
  catch (error) {
35343
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();
35344
36010
  return undefined;
35345
36011
  }
35346
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
+ }
35347
36048
  /**
35348
36049
  * Resolves the set of source rtIds that a stream-data widget bound to this
35349
36050
  * selector should be scoped to, and caches them on the state service.
@@ -35353,7 +36054,10 @@ class MeshBoardViewComponent {
35353
36054
  * become the scope (e.g. MeteringPoint → its EnergyMeasurement rtIds). With
35354
36055
  * no childScope, the picked entity's own rtId is the scope (direct keying).
35355
36056
  */
35356
- async resolveEntitySelectorScopeRtIds(selector, rtId) {
36057
+ async resolveEntitySelectorScopeRtIds(selector, rtId, seq) {
36058
+ const isCurrent = () => this.scopeRequestSeq.get(selector.id) === seq;
36059
+ if (!isCurrent())
36060
+ return;
35357
36061
  const childScope = selector.childScope;
35358
36062
  if (!childScope?.targetCkTypeId || !childScope.roleId) {
35359
36063
  this.stateService.setEntitySelectorRtIds(selector.id, [rtId]);
@@ -35364,16 +36068,39 @@ class MeshBoardViewComponent {
35364
36068
  // Parent → children via System/ParentChild is an INBOUND hop (the child
35365
36069
  // owns the association to its parent), so 'in' is the default.
35366
36070
  childScope.direction ?? 'in'));
36071
+ if (!isCurrent())
36072
+ return;
35367
36073
  const childRtIds = (targets ?? [])
35368
36074
  .map(t => String(t.rtId))
35369
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).
35370
36079
  this.stateService.setEntitySelectorRtIds(selector.id, childRtIds);
35371
36080
  }
35372
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).
35373
36086
  console.error(`Error resolving child scope for selector '${selector.id}':`, error);
35374
- // Empty scope is safer than a stale one: the bound widget falls back to
35375
- // its persisted scope rather than querying the wrong asset.
35376
- 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);
35377
36104
  }
35378
36105
  }
35379
36106
  /**
@@ -35384,6 +36111,9 @@ class MeshBoardViewComponent {
35384
36111
  const selectors = this.stateService.getEntitySelectors();
35385
36112
  const params = {};
35386
36113
  for (const selector of selectors) {
36114
+ // Followers are derived from their leader and never serialized (AB#7722).
36115
+ if (isFollowerSelector(selector))
36116
+ continue;
35387
36117
  params[`es_${selector.id}`] = selector.selectedRtId ?? null;
35388
36118
  }
35389
36119
  // Same single-URL serialization as writeTimeFilterToUrl: no relative
@@ -35437,7 +36167,7 @@ class MeshBoardViewComponent {
35437
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: [
35438
36168
  { provide: HAS_UNSAVED_CHANGES, useExisting: MeshBoardViewComponent },
35439
36169
  AutoRefreshTimerService
35440
- ], 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 });
35441
36171
  }
35442
36172
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: MeshBoardViewComponent, decorators: [{
35443
36173
  type: Component,
@@ -35455,7 +36185,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
35455
36185
  ], hostDirectives: [UnsavedChangesDirective], providers: [
35456
36186
  { provide: HAS_UNSAVED_CHANGES, useExisting: MeshBoardViewComponent },
35457
36187
  AutoRefreshTimerService
35458
- ], 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"] }]
35459
36189
  }], ctorParameters: () => [], propDecorators: { tileLayout: [{
35460
36190
  type: ViewChild,
35461
36191
  args: [TileLayoutComponent]
@@ -38905,5 +39635,5 @@ function provideCockpitWidgets() {
38905
39635
  * Generated bundle index. Do not edit.
38906
39636
  */
38907
39637
 
38908
- 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 };
38909
39639
  //# sourceMappingURL=meshmakers-octo-meshboard.mjs.map