@meshmakers/octo-meshboard 3.5.500 → 3.5.510

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,20 +1,20 @@
1
1
  import { FieldFilterOperatorsDto, CkTypeSelectorService, RuntimeEntitySelectDataSource, RuntimeEntityDialogDataSource, DeleteStrategiesDto, AssociationModOptionsDto, CkModelService, TENANT_ID_PROVIDER, QueryModeDto, GraphDirectionDto, AttributeSelectorService, GraphQL, GetCkTypeAvailableQueryColumnsDtoGQL, SortOrdersDto, SeriesResolutionSignalDto, HealthService, AssetRepoService, JobManagementService, CONFIGURATION_SERVICE, SystemCommunicationDeploymentStateDto, SystemCommunicationConfigurationStateDto, HealthStatus } from '@meshmakers/octo-services';
2
2
  export { RuntimeEntityDialogDataSource, RuntimeEntitySelectDataSource, TENANT_ID_PROVIDER } from '@meshmakers/octo-services';
3
3
  import * as i0 from '@angular/core';
4
- 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';
4
+ import { Injectable, inject, EventEmitter, forwardRef, Output, Input, ViewChild, ChangeDetectionStrategy, Component, Injector, EnvironmentInjector, ApplicationRef, signal, computed, effect, untracked, Directive, InjectionToken, isSignal, LOCALE_ID, NgZone, DestroyRef, ElementRef, input, createEnvironmentInjector, ViewContainerRef, makeEnvironmentProviders, provideAppInitializer, ChangeDetectorRef, ViewChildren, 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, NEVER, interval, filter, combineLatest, defer, EMPTY } from 'rxjs';
7
+ import { firstValueFrom, from, map, Observable, of, switchMap as switchMap$1, forkJoin, catchError, NEVER, interval, Subject, filter, combineLatest, defer, EMPTY } from 'rxjs';
8
8
  import * as i1$8 from '@meshmakers/shared-ui';
9
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';
13
- import { CommonModule, NgTemplateOutlet } from '@angular/common';
13
+ import { CommonModule, formatDate, formatNumber, NgTemplateOutlet } from '@angular/common';
14
14
  import { CkTypeSelectorInputComponent, SecretSafeAttributeNamesService, AttributeValueTypeDto, PropertyValueDisplayComponent, FieldFilterEditorComponent, PropertyGridComponent, OctoGraphQlDataSource, AttributeSelectorDialogService, AttributeSortSelectorDialogService } from '@meshmakers/octo-ui';
15
15
  import * as i1$5 from '@progress/kendo-angular-dialog';
16
16
  import { WindowService, WindowCloseResult, WindowRef, DialogsModule, DialogRef, DialogModule, DialogService } from '@progress/kendo-angular-dialog';
17
- import { switchMap, map as map$1, catchError as catchError$1, startWith, take, shareReplay, tap } from 'rxjs/operators';
17
+ import { switchMap, map as map$1, catchError as catchError$1, debounceTime, startWith, take, shareReplay, tap } from 'rxjs/operators';
18
18
  import * as i2 from '@progress/kendo-angular-buttons';
19
19
  import { ButtonsModule, ButtonModule } from '@progress/kendo-angular-buttons';
20
20
  import * as i3 from '@progress/kendo-angular-inputs';
@@ -39,6 +39,7 @@ import { LabelModule, KENDO_LABELS } from '@progress/kendo-angular-label';
39
39
  import { DomSanitizer } from '@angular/platform-browser';
40
40
  import * as i5 from '@progress/kendo-angular-layout';
41
41
  import { LayoutModule, TabStripModule, TileLayoutModule, TileLayoutComponent } from '@progress/kendo-angular-layout';
42
+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
42
43
  import * as i4$2 from '@progress/kendo-angular-dateinputs';
43
44
  import { DatePickerModule, DateTimePickerModule } from '@progress/kendo-angular-dateinputs';
44
45
  import { BreadCrumbService } from '@meshmakers/shared-services';
@@ -58,6 +59,8 @@ function isCockpitWidgetType(type) {
58
59
  }
59
60
  /** The board-level default timezone mode applied when none is persisted. */
60
61
  const DEFAULT_TIME_ZONE_MODE = 'local';
62
+ /** Maximum number of hops of a child-scope path, enforced by editor and runtime (AB#7723). */
63
+ const MAX_SCOPE_HOPS = 5;
61
64
 
62
65
  // Re-export from octo-services (moved there for reuse by octo-ui)
63
66
 
@@ -2199,10 +2202,17 @@ class MeshBoardPersistenceService {
2199
2202
  attributeMappings: es.attributeMappings,
2200
2203
  showInToolbar: es.showInToolbar,
2201
2204
  defaultRtId: es.defaultRtId,
2205
+ // Carried verbatim: the one-hop form and the multi-hop `path`
2206
+ // (AB#7723) keep their keys and order, so older boards encode unchanged.
2202
2207
  childScope: es.childScope,
2203
2208
  // AB#7722 — omitted (undefined → dropped by JSON.stringify) for
2204
2209
  // selectors that follow nobody, so older boards encode unchanged.
2205
- followsSelectorId: es.followsSelectorId || undefined
2210
+ followsSelectorId: es.followsSelectorId || undefined,
2211
+ // AB#7724 — omitted when empty, so older boards encode unchanged;
2212
+ // rows are reduced to the persisted filter fields (no editor state).
2213
+ filters: es.filters?.length
2214
+ ? es.filters.map(f => ({ attributePath: f.attributePath, operator: f.operator, comparisonValue: f.comparisonValue }))
2215
+ : undefined
2206
2216
  }));
2207
2217
  }
2208
2218
  const dataJson = JSON.stringify(data);
@@ -2448,13 +2458,20 @@ class MeshBoardStateService {
2448
2458
  /**
2449
2459
  * Transient scope state of each entity selector's current selection (keyed
2450
2460
  * 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
2461
+ * with the source rtIds (childScope hop or path, or the picked entity's
2452
2462
  * own rtId; possibly none), or `failed`. No entry = no active pick. Not
2453
2463
  * persisted — derived fresh from each selection, cleared on board switch
2454
2464
  * (AB#7721).
2455
2465
  */
2456
2466
  _entitySelectorScopes = signal({}, /* @ts-ignore */
2457
2467
  ...(ngDevMode ? [{ debugName: "_entitySelectorScopes" }] : /* istanbul ignore next */ []));
2468
+ /**
2469
+ * Period set by the page that embeds the board (`MeshBoardViewComponent.timeRange`, AB#7582):
2470
+ * exact instants, or `invalid` when the host passed an unusable range. `null` = the board's own
2471
+ * selection applies. Transient — never persisted, never written into the board config.
2472
+ */
2473
+ _hostTimeRange = signal(null, /* @ts-ignore */
2474
+ ...(ngDevMode ? [{ debugName: "_hostTimeRange" }] : /* istanbul ignore next */ []));
2458
2475
  /**
2459
2476
  * Transient variables published by widgets (KPI output variables, AB#5364).
2460
2477
  * Kept apart from `_meshBoardConfig` so publishing triggers no persistence and
@@ -3068,10 +3085,22 @@ class MeshBoardStateService {
3068
3085
  * Boundaries are resolved on the board's {@link timeZoneMode} basis (local by
3069
3086
  * default, or UTC), the same basis widgets format their datetime display on —
3070
3087
  * so the selected range and the displayed timestamps stay consistent.
3088
+ *
3089
+ * While an embedding host has set a period ({@link setHostTimeRange}, AB#7582)
3090
+ * and the board's time filter is enabled, the host's instants are returned
3091
+ * unchanged (no day snapping, no zone conversion) and the stored selection is
3092
+ * ignored; an invalid host period resolves to `null`.
3071
3093
  */
3072
3094
  resolveCurrentTimeRange() {
3073
3095
  const config = this._meshBoardConfig().timeFilter;
3074
- if (!config?.enabled || !config.selection) {
3096
+ if (!config?.enabled) {
3097
+ return null;
3098
+ }
3099
+ const host = this._hostTimeRange();
3100
+ if (host) {
3101
+ return host.status === 'valid' ? { from: new Date(host.from), to: new Date(host.to) } : null;
3102
+ }
3103
+ if (!config.selection) {
3075
3104
  return null;
3076
3105
  }
3077
3106
  const showTime = config.pickerConfig?.showTime ?? false;
@@ -3140,7 +3169,7 @@ class MeshBoardStateService {
3140
3169
  /**
3141
3170
  * Stores the source rtIds resolved from an entity selector's current
3142
3171
  * selection. Called by the view component after a selection resolves (the
3143
- * one-hop childScope traversal, or the picked entity's own rtId when the
3172
+ * childScope traversal (one hop or a path), or the picked entity's own rtId when the
3144
3173
  * selector has no childScope). An empty array is a resolved EMPTY scope:
3145
3174
  * bound widgets show no data and send no query (AB#7721). Transient — not
3146
3175
  * persisted.
@@ -3184,20 +3213,38 @@ class MeshBoardStateService {
3184
3213
  .map(([id]) => id), /* @ts-ignore */
3185
3214
  ...(ngDevMode ? [{ debugName: "failedEntitySelectorScopeIds" }] : /* istanbul ignore next */ []));
3186
3215
  /**
3187
- * Decides how a widget bound to an entity selector runs its stream-data query
3188
- * (AB#7721):
3216
+ * Decides how a widget runs its persistent query — the entry point every
3217
+ * built-in widget calls with its binding, query family and time-filter opt-out:
3189
3218
  *
3190
- * - `unscoped` — no binding, a runtime query (`family === 'runtime'`, the
3191
- * binding is ignored there), or no active pick: keep the persisted scope.
3219
+ * 1. Host gate (AB#7582): while the embedding host's period is invalid, every
3220
+ * stream-data query bound to the time filter is `empty` (`invalidTimeRange`),
3221
+ * whatever its selector binding; pass the widget's `ignoreTimeFilter` so an
3222
+ * opted-out widget, whose data does not depend on the period, keeps querying.
3223
+ * Runtime queries have no time window and are not gated. A legacy config
3224
+ * without `queryFamily` is treated as stream data here.
3225
+ * 2. Entity-selector scope ({@link resolveEntitySelectorScope}, AB#7721) — since
3226
+ * AB#7725 for runtime queries too: `unscoped` / `pending` / `scoped` / `empty`.
3227
+ */
3228
+ resolveStreamDataScope(entitySelectorId, family, ignoreTimeFilter) {
3229
+ if (family !== 'runtime' && this.isStreamDataBlockedByHostTimeRange(ignoreTimeFilter)) {
3230
+ return { kind: 'empty', reason: 'invalidTimeRange' };
3231
+ }
3232
+ return this.resolveEntitySelectorScope(entitySelectorId);
3233
+ }
3234
+ /**
3235
+ * The entity-selector part of {@link resolveStreamDataScope}, without the host
3236
+ * gate — stream data (AB#7721) and runtime queries alike (AB#7725):
3237
+ *
3238
+ * - `unscoped` — no binding, or no active pick: keep the query as saved.
3192
3239
  * - `pending` — the pick is still resolving: show loading, send no query.
3193
- * - `scoped` — query with the resolved source rtIds.
3240
+ * - `scoped` — query restricted to the resolved rtIds: stream data via
3241
+ * `streamDataArgs.rtIds`, runtime queries via the `rtId IN` field filter
3242
+ * (`QueryExecutionOptions.scopeRtIds`).
3194
3243
  * - `empty` — the pick has nothing in scope or the lookup failed: show the
3195
3244
  * 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
3245
  */
3199
- resolveStreamDataScope(entitySelectorId, family) {
3200
- if (!entitySelectorId || family === 'runtime') {
3246
+ resolveEntitySelectorScope(entitySelectorId) {
3247
+ if (!entitySelectorId) {
3201
3248
  return { kind: 'unscoped' };
3202
3249
  }
3203
3250
  const state = this._entitySelectorScopes()[entitySelectorId];
@@ -3225,12 +3272,46 @@ class MeshBoardStateService {
3225
3272
  * skip the query for `pending` / `empty` (AB#7721).
3226
3273
  */
3227
3274
  resolveStreamDataRtIds(entitySelectorId) {
3228
- const scope = this.resolveStreamDataScope(entitySelectorId);
3275
+ const scope = this.resolveEntitySelectorScope(entitySelectorId);
3229
3276
  return scope.kind === 'scoped' ? scope.rtIds : undefined;
3230
3277
  }
3231
3278
  setEntitySelectorScope(selectorId, state) {
3232
3279
  this._entitySelectorScopes.update(map => ({ ...map, [selectorId]: state }));
3233
3280
  }
3281
+ /**
3282
+ * Sets (or with `null` clears) the period of the page embedding the board
3283
+ * (AB#7582). Called by `MeshBoardViewComponent` for its `timeRange` input; a
3284
+ * host normally sets the input instead of calling this. Transient: not
3285
+ * persisted, not written into the board config, so it never causes unsaved
3286
+ * changes. Only takes effect while the board's time filter is enabled.
3287
+ */
3288
+ setHostTimeRange(state, owner) {
3289
+ this._hostTimeRange.set(state);
3290
+ this.hostTimeRangeOwner = state ? owner ?? null : null;
3291
+ }
3292
+ /**
3293
+ * Clears the host period only if `owner` set it (AB#7582). The service is
3294
+ * root-provided, so a view that is destroyed (or drops its `timeRange`) must not
3295
+ * clear a period another view set in the meantime.
3296
+ */
3297
+ clearHostTimeRange(owner) {
3298
+ if (this.hostTimeRangeOwner === owner) {
3299
+ this.setHostTimeRange(null);
3300
+ }
3301
+ }
3302
+ /** The view (or other caller) that set the current host period, if it said so. */
3303
+ hostTimeRangeOwner = null;
3304
+ /** The period set by the embedding host, or `null` when the board's own selection applies. */
3305
+ hostTimeRange = this._hostTimeRange.asReadonly();
3306
+ /**
3307
+ * True when an invalid host period blocks a stream-data query that is bound to
3308
+ * the time filter (AB#7582): the period is invalid, the board's time filter is
3309
+ * enabled and the caller did not opt out with `ignoreTimeFilter`. Such a query
3310
+ * must not be sent — without a window it would read the whole archive.
3311
+ */
3312
+ isStreamDataBlockedByHostTimeRange(ignoreTimeFilter) {
3313
+ return !ignoreTimeFilter && this._hostTimeRange()?.status === 'invalid' && this.isTimeFilterEnabled();
3314
+ }
3234
3315
  /**
3235
3316
  * Updates the time filter configuration.
3236
3317
  */
@@ -3747,7 +3828,7 @@ class QueryExecutorService {
3747
3828
  rtId: queryRtId,
3748
3829
  first: options.first ?? undefined,
3749
3830
  after: options.after ?? undefined,
3750
- fieldFilter: options.fieldFilter ?? undefined
3831
+ fieldFilter: QueryExecutorService.withRtIdScope(options.fieldFilter, options.scopeRtIds)
3751
3832
  },
3752
3833
  fetchPolicy: options.forceRefresh ? 'network-only' : 'cache-first'
3753
3834
  }).pipe(map$1(result => {
@@ -3768,7 +3849,8 @@ class QueryExecutorService {
3768
3849
  }));
3769
3850
  }
3770
3851
  executeStreamData(queryRtId, options = {}) {
3771
- const arg = options.streamDataArgs ? this.buildStreamDataArg(options.streamDataArgs) : undefined;
3852
+ const streamDataArgs = QueryExecutorService.withStreamDataScope(options.streamDataArgs, options.scopeRtIds);
3853
+ const arg = streamDataArgs ? this.buildStreamDataArg(streamDataArgs) : undefined;
3772
3854
  return this.streamDataGql.fetch({
3773
3855
  variables: {
3774
3856
  rtId: queryRtId,
@@ -3945,6 +4027,29 @@ class QueryExecutorService {
3945
4027
  }
3946
4028
  return groups;
3947
4029
  }
4030
+ /**
4031
+ * Runtime entity-selector scope (AB#7725): appends `rtId IN (scopeRtIds)` to the
4032
+ * caller's field filters. The backend AND-combines all runtime field filters with
4033
+ * the persisted query's filters before the database query, so simple rows as well
4034
+ * as aggregation / grouping results cover the scoped entities only. Returns the
4035
+ * caller's filters unchanged (or `undefined`) when there is no scope.
4036
+ */
4037
+ static withRtIdScope(fieldFilter, scopeRtIds) {
4038
+ if (!scopeRtIds || scopeRtIds.length === 0) {
4039
+ return fieldFilter ?? undefined;
4040
+ }
4041
+ return [
4042
+ ...(fieldFilter ?? []),
4043
+ { attributePath: 'rtId', operator: FieldFilterOperatorsDto.InDto, comparisonValue: scopeRtIds }
4044
+ ];
4045
+ }
4046
+ /** Stream-data entity-selector scope: `scopeRtIds` fill `rtIds` unless the caller set them. */
4047
+ static withStreamDataScope(args, scopeRtIds) {
4048
+ if (!scopeRtIds || scopeRtIds.length === 0 || (args?.rtIds && args.rtIds.length > 0)) {
4049
+ return args;
4050
+ }
4051
+ return { ...args, rtIds: scopeRtIds };
4052
+ }
3948
4053
  buildStreamDataArg(args) {
3949
4054
  // Skip the entire `arg` field when the caller has nothing to override —
3950
4055
  // the persisted query then runs with its intrinsic from/to/limit and the
@@ -4086,6 +4191,45 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
4086
4191
  }]
4087
4192
  }], ctorParameters: () => [{ type: i1.Apollo }] });
4088
4193
 
4194
+ const GetAssociationTargetRtIdsDocumentDto = gql `
4195
+ query getAssociationTargetRtIds($rtIds: [OctoObjectId]!, $ckTypeId: String!, $targetCkTypeId: String!, $roleId: String!, $direction: GraphDirection!, $first: Int!, $targetsFirst: Int!) {
4196
+ runtime {
4197
+ runtimeEntities(rtIds: $rtIds, ckId: $ckTypeId, first: $first) {
4198
+ items {
4199
+ rtId
4200
+ associations {
4201
+ targets(
4202
+ ckId: $targetCkTypeId
4203
+ roleId: $roleId
4204
+ direction: $direction
4205
+ first: $targetsFirst
4206
+ ) {
4207
+ totalCount
4208
+ items {
4209
+ rtId
4210
+ }
4211
+ }
4212
+ }
4213
+ }
4214
+ }
4215
+ }
4216
+ }
4217
+ `;
4218
+ class GetAssociationTargetRtIdsDtoGQL extends i1.Query {
4219
+ document = GetAssociationTargetRtIdsDocumentDto;
4220
+ constructor(apollo) {
4221
+ super(apollo);
4222
+ }
4223
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: GetAssociationTargetRtIdsDtoGQL, deps: [{ token: i1.Apollo }], target: i0.ɵɵFactoryTarget.Injectable });
4224
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: GetAssociationTargetRtIdsDtoGQL, providedIn: 'root' });
4225
+ }
4226
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: GetAssociationTargetRtIdsDtoGQL, decorators: [{
4227
+ type: Injectable,
4228
+ args: [{
4229
+ providedIn: 'root'
4230
+ }]
4231
+ }], ctorParameters: () => [{ type: i1.Apollo }] });
4232
+
4089
4233
  const GetCkTypeAttributesForMeshboardDocumentDto = gql `
4090
4234
  query getCkTypeAttributesForMeshboard($ckTypeId: String!, $first: Int) {
4091
4235
  constructionKit {
@@ -4308,12 +4452,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
4308
4452
  }]
4309
4453
  }] });
4310
4454
 
4455
+ /** Source rtIds per request of one batched association hop (AB#7723). */
4456
+ const ASSOCIATION_HOP_SOURCE_BATCH = 100;
4457
+ /** Targets read per source entity in one association hop (AB#7723). */
4458
+ const ASSOCIATION_HOP_TARGETS_PER_SOURCE = 1000;
4311
4459
  class MeshBoardDataService {
4312
4460
  getDashboardEntityGQL = inject(GetDashboardEntityDtoGQL);
4313
4461
  getCkModelsWithStateGQL = inject(GetCkModelsWithStateDtoGQL);
4314
4462
  getEntitiesByCkTypeGQL = inject(GetEntitiesByCkTypeDtoGQL);
4315
4463
  queryExecutor = inject(QueryExecutorService);
4316
4464
  getAssociationTargetsGQL = inject(GetAssociationTargetsDtoGQL);
4465
+ getAssociationTargetRtIdsGQL = inject(GetAssociationTargetRtIdsDtoGQL);
4317
4466
  secretSafeNames = inject(SecretSafeAttributeNamesService);
4318
4467
  getCkTypeAttributesGQL = inject(GetCkTypeAttributesForMeshboardDtoGQL);
4319
4468
  apollo = inject(Apollo);
@@ -4552,6 +4701,13 @@ class MeshBoardDataService {
4552
4701
  */
4553
4702
  async fetchRepeaterFromQuery(family, queryRtId, maxItems) {
4554
4703
  try {
4704
+ // AB#7582: while the embedding host's period is invalid, a stream-data
4705
+ // repeater query is not sent (it would read the whole archive); the group
4706
+ // shows no items. Runtime queries are unaffected.
4707
+ if (this.stateService.isStreamDataBlockedByHostTimeRange()
4708
+ && (family ?? await this.queryExecutor.resolveFamily(queryRtId)) === 'streamData') {
4709
+ return [];
4710
+ }
4555
4711
  const streamDataArgs = this.buildRepeaterStreamDataArgs();
4556
4712
  const result = await firstValueFrom(this.queryExecutor.execute(family, queryRtId, {
4557
4713
  first: maxItems,
@@ -4676,6 +4832,56 @@ class MeshBoardDataService {
4676
4832
  }));
4677
4833
  }));
4678
4834
  }
4835
+ /**
4836
+ * One association hop for MANY source entities (AB#7723, multi-hop child
4837
+ * scope): the rtIds of the `targetCkTypeId` entities reached from each of
4838
+ * `sourceRtIds` (all of `sourceCkTypeId`) over `roleId` in `direction`.
4839
+ *
4840
+ * Query cost: the sources go out in batches of
4841
+ * {@link ASSOCIATION_HOP_SOURCE_BATCH} rtIds per request (`runtimeEntities(rtIds:)`
4842
+ * with the association targets nested), the batches of one hop in parallel —
4843
+ * never one request per entity. Only rtIds are selected. Per source at most
4844
+ * {@link ASSOCIATION_HOP_TARGETS_PER_SOURCE} targets are read; a truncated
4845
+ * source is logged as a warning. Duplicates are not removed here.
4846
+ * Errors propagate (the caller turns them into a failed scope).
4847
+ */
4848
+ fetchAssociationTargetRtIds(sourceRtIds, sourceCkTypeId, targetCkTypeId, roleId, direction) {
4849
+ if (sourceRtIds.length === 0) {
4850
+ return of([]);
4851
+ }
4852
+ const graphDirection = direction === 'out' ? GraphDirectionDto.OutboundDto : GraphDirectionDto.InboundDto;
4853
+ const batches = [];
4854
+ for (let i = 0; i < sourceRtIds.length; i += ASSOCIATION_HOP_SOURCE_BATCH) {
4855
+ batches.push(sourceRtIds.slice(i, i + ASSOCIATION_HOP_SOURCE_BATCH));
4856
+ }
4857
+ return forkJoin(batches.map(batch => this.getAssociationTargetRtIdsGQL.fetch({
4858
+ variables: {
4859
+ rtIds: batch,
4860
+ ckTypeId: sourceCkTypeId,
4861
+ targetCkTypeId,
4862
+ roleId,
4863
+ direction: graphDirection,
4864
+ first: batch.length,
4865
+ targetsFirst: ASSOCIATION_HOP_TARGETS_PER_SOURCE
4866
+ }
4867
+ }).pipe(map(result => {
4868
+ const rtIds = [];
4869
+ for (const source of result.data?.runtime?.runtimeEntities?.items ?? []) {
4870
+ const targets = source?.associations?.targets;
4871
+ const items = targets?.items ?? [];
4872
+ if ((targets?.totalCount ?? 0) > items.length) {
4873
+ console.warn(`Child scope hop ${roleId} → ${targetCkTypeId}: source ${String(source?.rtId)} has `
4874
+ + `${targets?.totalCount} targets, only the first ${items.length} are used.`);
4875
+ }
4876
+ for (const target of items) {
4877
+ if (target?.rtId !== null && target?.rtId !== undefined) {
4878
+ rtIds.push(String(target.rtId));
4879
+ }
4880
+ }
4881
+ }
4882
+ return rtIds;
4883
+ })))).pipe(map(perBatch => perBatch.flat()));
4884
+ }
4679
4885
  /**
4680
4886
  * Fetches CK type attribute definitions (name + type) for config dialog.
4681
4887
  */
@@ -6614,7 +6820,7 @@ class KpiWidgetComponent {
6614
6820
  // Entity-selector scope (AB#7721): pending → stay in the loading state and
6615
6821
  // send no query (the view refreshes once the scope is known); empty → show
6616
6822
  // the no-data value '-' without querying the persisted (unscoped) scope.
6617
- const scope = this.stateService.resolveStreamDataScope(dataSource.entitySelectorId, dataSource.queryFamily);
6823
+ const scope = this.stateService.resolveStreamDataScope(dataSource.entitySelectorId, dataSource.queryFamily, dataSource.ignoreTimeFilter);
6618
6824
  if (scope.kind === 'pending') {
6619
6825
  return;
6620
6826
  }
@@ -6626,7 +6832,9 @@ class KpiWidgetComponent {
6626
6832
  const streamDataArgs = this.buildStreamDataArgs();
6627
6833
  const result = await firstValueFrom(this.queryExecutor.execute(dataSource.queryFamily, dataSource.queryRtId, {
6628
6834
  fieldFilter: fieldFilter ?? undefined,
6629
- streamDataArgs
6835
+ streamDataArgs,
6836
+ // Runtime queries: `rtId IN` field filter (AB#7725); stream data: arg.rtIds.
6837
+ scopeRtIds: scope.kind === 'scoped' ? scope.rtIds : undefined
6630
6838
  }).pipe(catchError(err => {
6631
6839
  console.error('Error loading KPI query data:', err);
6632
6840
  throw err;
@@ -6866,19 +7074,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
6866
7074
  }] } });
6867
7075
 
6868
7076
  /**
6869
- * Per-widget asset-scope binding for stream-data persistent-query widgets.
7077
+ * Per-widget asset-scope binding for persistent-query widgets — stream-data and
7078
+ * runtime queries (AB#7725).
6870
7079
  *
6871
- * Stream-data archives are keyed by their source rtIds, so a widget is scoped
6872
- * to a picked asset by passing those source rtIds as `streamDataArgs.rtIds`
6873
- * (see `MeshBoardStateService.resolveStreamDataRtIds` and the backend
6874
- * `StreamDataArguments.rtIds` override). This control lets the designer bind a
6875
- * single widget to one of the MeshBoard's entity selectors: when the selector's
6876
- * entity is picked, its resolved source rtIds (the selector's `childScope`
6877
- * one-hop, or the picked entity itself) scope this widget's query.
7080
+ * This control lets the designer bind a single widget to one of the MeshBoard's
7081
+ * entity selectors: when the selector's entity is picked, its resolved rtIds
7082
+ * (the selector's `childScope` hop or path, or the picked entity itself) scope
7083
+ * this widget's query on the server (see
7084
+ * `MeshBoardStateService.resolveEntitySelectorScope` and
7085
+ * `QueryExecutionOptions.scopeRtIds`):
7086
+ * - stream-data queries get the rtIds as `streamDataArgs.rtIds` (the backend
7087
+ * `StreamDataArguments.rtIds` override) — simple, aggregation, grouping and
7088
+ * downsampling queries alike;
7089
+ * - runtime queries (SimpleRtQuery, AggregationRtQuery,
7090
+ * GroupingAggregationRtQuery) get the field filter `rtId IN (rtIds)`,
7091
+ * AND-combined with the widget's and the saved query's own filters.
6878
7092
  *
6879
- * The control renders only when the selected query is a stream-data query
6880
- * (`family === 'streamData'`); for runtime queries the binding has no effect and
6881
- * the picker stays hidden (mirrors `SdTimeFilterToggleComponent`).
7093
+ * The control renders once a query is selected (`family` is known); without a
7094
+ * query there is nothing to scope.
6882
7095
  *
6883
7096
  * Two-way bindable:
6884
7097
  * ```html
@@ -6890,7 +7103,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
6890
7103
  * ```
6891
7104
  */
6892
7105
  class EntitySelectorScopePickerComponent {
6893
- /** Family of the currently selected query. Picker is shown only for 'streamData'. */
7106
+ /** Family of the currently selected query. Picker is shown once a query (either family) is selected. */
6894
7107
  family;
6895
7108
  /** Currently bound entity selector id (`undefined` ⇒ no binding). */
6896
7109
  entitySelectorId;
@@ -6905,7 +7118,7 @@ class EntitySelectorScopePickerComponent {
6905
7118
  /** Emits on change (enables `[(entitySelectorId)]` two-way binding). */
6906
7119
  entitySelectorIdChange = new EventEmitter();
6907
7120
  get visible() {
6908
- return this.family === 'streamData';
7121
+ return this.family === 'streamData' || this.family === 'runtime';
6909
7122
  }
6910
7123
  /** "None" row first, then one row per available selector. */
6911
7124
  get options() {
@@ -6932,7 +7145,7 @@ class EntitySelectorScopePickerComponent {
6932
7145
  (valueChange)="onSelect($event)">
6933
7146
  </kendo-dropdownlist>
6934
7147
  <p class="field-hint">
6935
- Scopes this widget's stream-data query to the rtIds resolved from the
7148
+ Restricts this widget's query to the entities resolved from the
6936
7149
  selected asset. Leave unset to use the saved query's own scope.
6937
7150
  </p>
6938
7151
  </div>
@@ -6954,7 +7167,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
6954
7167
  (valueChange)="onSelect($event)">
6955
7168
  </kendo-dropdownlist>
6956
7169
  <p class="field-hint">
6957
- Scopes this widget's stream-data query to the rtIds resolved from the
7170
+ Restricts this widget's query to the entities resolved from the
6958
7171
  selected asset. Leave unset to use the saved query's own scope.
6959
7172
  </p>
6960
7173
  </div>
@@ -8871,7 +9084,7 @@ const ISO_NAIVE_DATE_TIME_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2})?(\.\d+)?$
8871
9084
  * instant. A naive ISO date-time string (no `Z`/offset) is treated as UTC — the
8872
9085
  * wire's serialization of an instant — never as browser-local time (AB#4818).
8873
9086
  */
8874
- function toInstant(value) {
9087
+ function toInstant$1(value) {
8875
9088
  if (value instanceof Date) {
8876
9089
  return isNaN(value.getTime()) ? null : value;
8877
9090
  }
@@ -8929,7 +9142,7 @@ function partsFormatter(mode) {
8929
9142
  * non-instants so callers can skip the row.
8930
9143
  */
8931
9144
  function getZonedDateParts(value, mode) {
8932
- const date = toInstant(value);
9145
+ const date = toInstant$1(value);
8933
9146
  if (!date) {
8934
9147
  return null;
8935
9148
  }
@@ -8961,7 +9174,7 @@ function zonedDateKey(parts) {
8961
9174
  * instant, so callers can fall back to their own rendering.
8962
9175
  */
8963
9176
  function formatInstant(value, mode, options) {
8964
- const date = toInstant(value);
9177
+ const date = toInstant$1(value);
8965
9178
  if (!date) {
8966
9179
  return null;
8967
9180
  }
@@ -10266,7 +10479,7 @@ class TableWidgetDataSourceDirective extends OctoGraphQlDataSource {
10266
10479
  // state (an observable that never emits; the next refresh after the scope
10267
10480
  // resolves cancels it) and send no query; empty → no rows, no query — never
10268
10481
  // the persisted (unscoped) scope.
10269
- const scope = this.stateService.resolveStreamDataScope(dataSource.entitySelectorId, dataSource.queryFamily);
10482
+ const scope = this.stateService.resolveStreamDataScope(dataSource.entitySelectorId, dataSource.queryFamily, dataSource.ignoreTimeFilter);
10270
10483
  if (scope.kind === 'pending') {
10271
10484
  return NEVER;
10272
10485
  }
@@ -10285,7 +10498,9 @@ class TableWidgetDataSourceDirective extends OctoGraphQlDataSource {
10285
10498
  first: queryOptions.state.take ?? this._config?.pageSize ?? 10,
10286
10499
  after: GraphQL.offsetToCursor(queryOptions.state.skip ?? 0),
10287
10500
  fieldFilter: fieldFilter ?? undefined,
10288
- streamDataArgs
10501
+ streamDataArgs,
10502
+ // Runtime queries: `rtId IN` field filter (AB#7725); stream data: arg.rtIds.
10503
+ scopeRtIds: scope.kind === 'scoped' ? scope.rtIds : undefined
10289
10504
  }).pipe(map$1(result => {
10290
10505
  // Extract columns from query response and update signal.
10291
10506
  // Replace dots with underscores for grid compatibility (Kendo treats dots as nested paths).
@@ -10472,6 +10687,286 @@ function resolveMeshBoardLabel(resolver, request) {
10472
10687
  return typeof text === 'string' && text !== '' ? text : request.defaultText;
10473
10688
  }
10474
10689
 
10690
+ /**
10691
+ * Optional: the host's current UI language. MeshBoard does not derive it from `LOCALE_ID` (that is
10692
+ * static in many apps); a host that wants per-language texts in board configurations (table column
10693
+ * `titles`, `valueLabels`) hands its language over once, via `provideMeshBoard({ language })`,
10694
+ * `provideMeshBoardLanguage(language)` or this token directly. Not provided = the configured default
10695
+ * texts (e.g. the column `title`).
10696
+ *
10697
+ * ```ts
10698
+ * providers: [provideMeshBoard({ language: languageService.currentLanguage })] // Signal<string>
10699
+ * ```
10700
+ */
10701
+ const MESHBOARD_LANGUAGE = new InjectionToken('MESHBOARD_LANGUAGE');
10702
+ /** Reads a {@link MeshBoardLanguageSource} (fixed or signal) as a normalised culture code, or `null`. */
10703
+ function readMeshBoardLanguageSource(source) {
10704
+ if (!source) {
10705
+ return null;
10706
+ }
10707
+ const value = isSignal(source) ? source() : source;
10708
+ if (typeof value !== 'string') {
10709
+ return null;
10710
+ }
10711
+ const normalised = value.trim().replace(/_/g, '-');
10712
+ return normalised === '' ? null : normalised;
10713
+ }
10714
+ /** The host language of {@link MESHBOARD_LANGUAGE} as a signal (`null` without one). Call in an injection context. */
10715
+ function injectMeshBoardLanguage() {
10716
+ const source = inject(MESHBOARD_LANGUAGE, { optional: true });
10717
+ return computed(() => readMeshBoardLanguageSource(source));
10718
+ }
10719
+ /**
10720
+ * Well-formed culture key of a per-language text map: a 2-3 letter language, optionally followed by
10721
+ * subtags (`de`, `de-AT`, `en-GB`, `zh-Hant-TW`). Keys such as `german` or `de_AT` are not culture codes.
10722
+ */
10723
+ const CULTURE_KEY = /^[A-Za-z]{2,3}(-[A-Za-z0-9]{1,8})*$/;
10724
+ /** Whether `key` is a well-formed culture key of a per-language text map. */
10725
+ function isCultureKey(key) {
10726
+ return CULTURE_KEY.test(key);
10727
+ }
10728
+ /**
10729
+ * Picks the text for `language` from a per-language map (AB#7655). Lookup order:
10730
+ * 1. the exact culture or language (`de-AT`, `de`; case-insensitive);
10731
+ * 2. a culture (`de-AT`) without an exact key: a key equal to its language part (`de`). A culture
10732
+ * never picks a sibling culture (`de-DE` does not take `de-AT`);
10733
+ * 3. a bare language (`de`) without an exact key: the first authored key of that language
10734
+ * (`de-AT` in `{ 'en-GB': …, 'de-AT': …, 'de-DE': … }`), in the map's key order.
10735
+ * Returns `undefined` when there is no language, no map or no match, so the caller applies its own
10736
+ * fallback (e.g. the column `title`).
10737
+ */
10738
+ function pickLocalizedText(texts, language) {
10739
+ if (!texts || !language) {
10740
+ return undefined;
10741
+ }
10742
+ const wanted = language.toLowerCase();
10743
+ const isBareLanguage = !wanted.includes('-');
10744
+ const languagePart = wanted.split('-')[0];
10745
+ let languageMatch;
10746
+ for (const [key, text] of Object.entries(texts)) {
10747
+ if (typeof text !== 'string' || text === '') {
10748
+ continue;
10749
+ }
10750
+ const candidate = key.toLowerCase();
10751
+ if (candidate === wanted) {
10752
+ return text;
10753
+ }
10754
+ if (languageMatch !== undefined) {
10755
+ continue;
10756
+ }
10757
+ if (isBareLanguage ? candidate.split('-')[0] === wanted : candidate === languagePart) {
10758
+ languageMatch = text;
10759
+ }
10760
+ }
10761
+ return languageMatch;
10762
+ }
10763
+
10764
+ /** An empty problems record. */
10765
+ function emptyLabelProblems() {
10766
+ return { unknownCultureKeys: [], malformedValueKeys: [], malformedTitleKeys: [], statusMappingIgnored: false };
10767
+ }
10768
+ function isPlainObject(value) {
10769
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
10770
+ }
10771
+ /**
10772
+ * Keeps the usable entries of a per-language text map: culture keys with a non-empty string text.
10773
+ * Bad culture keys are reported; the keys of entries with a non-string or empty text are returned
10774
+ * as `malformedKeys`.
10775
+ */
10776
+ function cleanLocalizedTexts(texts, unknownCultureKeys) {
10777
+ const clean = {};
10778
+ const malformedKeys = [];
10779
+ for (const [key, text] of Object.entries(texts)) {
10780
+ if (!isCultureKey(key)) {
10781
+ unknownCultureKeys.push(key);
10782
+ continue;
10783
+ }
10784
+ if (typeof text !== 'string' || text === '') {
10785
+ malformedKeys.push(key);
10786
+ continue;
10787
+ }
10788
+ clean[key] = text;
10789
+ }
10790
+ return { texts: clean, malformedKeys };
10791
+ }
10792
+ /**
10793
+ * The header text of a column: `titles` looked up with {@link pickLocalizedText} (exact culture,
10794
+ * then language part; a bare language takes the first key of that language), then `title`.
10795
+ * Without a language (no `MESHBOARD_LANGUAGE`) it is always `title`, as before AB#7655.
10796
+ * Unknown culture keys and malformed entries (non-string or empty text, a `titles` that is no
10797
+ * object) are reported in `problems` and never matched.
10798
+ */
10799
+ function resolveColumnTitle(column, language, problems) {
10800
+ const titles = column.titles;
10801
+ if (!isPlainObject(titles)) {
10802
+ if (titles !== undefined && titles !== null) {
10803
+ problems?.malformedTitleKeys?.push('*');
10804
+ }
10805
+ return column.title;
10806
+ }
10807
+ const { texts, malformedKeys } = cleanLocalizedTexts(titles, problems?.unknownCultureKeys ?? []);
10808
+ problems?.malformedTitleKeys?.push(...malformedKeys);
10809
+ return pickLocalizedText(texts, language) ?? column.title;
10810
+ }
10811
+ /**
10812
+ * Resolves a column's `valueLabels` for the current language into a lookup by raw value (`"1"`,
10813
+ * `"2"`, ...). A label given per language follows the language like `titles`; without a match that
10814
+ * entry is left out, so the cell shows the value in the column's normal rendering. Malformed
10815
+ * entries and unknown culture keys are ignored and reported in `problems`. Returns `null` when no
10816
+ * label applies for the current language (no usable `valueLabels`, or only per-language labels
10817
+ * without a match), so the column keeps its `dataType` / `format` rendering and the label resolver.
10818
+ */
10819
+ function resolveValueLabels(column, language, problems) {
10820
+ const valueLabels = column.valueLabels;
10821
+ if (valueLabels === undefined || valueLabels === null) {
10822
+ return null;
10823
+ }
10824
+ const report = problems ?? { unknownCultureKeys: [], malformedValueKeys: [] };
10825
+ if (!isPlainObject(valueLabels)) {
10826
+ report.malformedValueKeys.push('*');
10827
+ return null;
10828
+ }
10829
+ const resolved = new Map();
10830
+ for (const [key, entry] of Object.entries(valueLabels)) {
10831
+ if (!isPlainObject(entry)) {
10832
+ report.malformedValueKeys.push(key);
10833
+ continue;
10834
+ }
10835
+ const { label, color } = entry;
10836
+ if (color !== undefined && typeof color !== 'string') {
10837
+ report.malformedValueKeys.push(key);
10838
+ continue;
10839
+ }
10840
+ let text;
10841
+ if (typeof label === 'string' && label !== '') {
10842
+ text = label;
10843
+ }
10844
+ else if (isPlainObject(label)) {
10845
+ const cleaned = cleanLocalizedTexts(label, report.unknownCultureKeys);
10846
+ if (cleaned.malformedKeys.length > 0 || Object.keys(label).length === 0) {
10847
+ report.malformedValueKeys.push(key);
10848
+ continue;
10849
+ }
10850
+ text = pickLocalizedText(cleaned.texts, language);
10851
+ }
10852
+ else {
10853
+ report.malformedValueKeys.push(key);
10854
+ continue;
10855
+ }
10856
+ if (text !== undefined) {
10857
+ resolved.set(key, { text, ...(color ? { color } : {}) });
10858
+ }
10859
+ }
10860
+ return resolved.size > 0 ? resolved : null;
10861
+ }
10862
+ /**
10863
+ * Text of a value-label cell whose value has no label: the value as the list view renders the
10864
+ * column's `dataType` - `numeric` with `format` like the `number` pipe, `iso8601` with `format`
10865
+ * like the `date` pipe (both with the app's `locale`), plain text through `textFormatter` (the
10866
+ * board's date-time formatter), anything else as string. A value the pipe cannot format shows as is.
10867
+ */
10868
+ function formatUnlabelledValue(value, column, locale, textFormatter) {
10869
+ const format = column.format || undefined;
10870
+ try {
10871
+ switch (column.dataType) {
10872
+ case 'numeric': {
10873
+ const number = typeof value === 'number'
10874
+ ? value
10875
+ : typeof value === 'string' && value.trim() !== '' ? Number(value) : Number.NaN;
10876
+ return Number.isFinite(number) ? formatNumber(number, locale, format) : String(value);
10877
+ }
10878
+ case 'iso8601':
10879
+ return typeof value === 'string' || typeof value === 'number' || value instanceof Date
10880
+ ? formatDate(value, format ?? 'mediumDate', locale)
10881
+ : String(value);
10882
+ case undefined:
10883
+ case 'text':
10884
+ return textFormatter(value);
10885
+ default:
10886
+ return String(value);
10887
+ }
10888
+ }
10889
+ catch {
10890
+ return String(value);
10891
+ }
10892
+ }
10893
+ /** Whether a problems record holds anything worth a warning. */
10894
+ function hasLabelProblems(problems) {
10895
+ return problems.unknownCultureKeys.length > 0
10896
+ || problems.malformedValueKeys.length > 0
10897
+ || (problems.malformedTitleKeys?.length ?? 0) > 0
10898
+ || problems.statusMappingIgnored === true;
10899
+ }
10900
+ /**
10901
+ * Columns the table config dialog saves (AB#7655 AC 6). The dialog edits which fields are shown,
10902
+ * not their presentation, so it must not erase hand-authored column properties (`titles`,
10903
+ * `valueLabels`, `width`, `format`, `statusMapping`, ...):
10904
+ * - same data source (same CK type / same persistent query): a field that stays selected keeps its
10905
+ * existing column object as is; a newly selected field gets a fresh column with `defaultTitle(field)`;
10906
+ * - persistent query: see {@link mergePersistentQueryColumns}.
10907
+ */
10908
+ function mergeRuntimeEntityColumns(selectedFields, initialColumns, sameDataSource, defaultTitle) {
10909
+ const existing = new Map();
10910
+ if (sameDataSource) {
10911
+ for (const column of initialColumns ?? []) {
10912
+ if (column && typeof column.field === 'string' && !existing.has(column.field)) {
10913
+ existing.set(column.field, column);
10914
+ }
10915
+ }
10916
+ }
10917
+ return selectedFields.map(field => existing.get(field) ?? { field, title: defaultTitle(field), width: undefined });
10918
+ }
10919
+ /**
10920
+ * Explicit columns of a persistent-query table after the dialog saved (AB#7655):
10921
+ * - a different query, or no explicit columns: `[]` (columns are derived from the query at run time);
10922
+ * - the same query with known `queryFields` (the columns the query returns now): the explicit columns
10923
+ * whose field the query still returns, unchanged and in their order, followed by the query's other
10924
+ * fields as derived columns (`defaultTitle(field)`, like the widget derives them);
10925
+ * - the same query, `queryFields` unknown (`null`, empty, or the lookup failed): the explicit columns unchanged.
10926
+ */
10927
+ function mergePersistentQueryColumns(initialColumns, sameQuery, queryFields, defaultTitle = field => field) {
10928
+ if (!sameQuery || !initialColumns || initialColumns.length === 0) {
10929
+ return [];
10930
+ }
10931
+ if (!queryFields || queryFields.length === 0) {
10932
+ return [...initialColumns];
10933
+ }
10934
+ const returned = new Set(queryFields);
10935
+ const kept = initialColumns.filter(column => column && typeof column.field === 'string' && returned.has(column.field));
10936
+ const keptFields = new Set(kept.map(column => column.field));
10937
+ const added = [...returned]
10938
+ .filter(field => !keptFields.has(field))
10939
+ .map(field => ({ field, title: defaultTitle(field) }));
10940
+ return [...kept, ...added];
10941
+ }
10942
+
10943
+ /**
10944
+ * Cell of a table widget column with `valueLabels` (AB#7655): plain text like every other cell,
10945
+ * optionally in the configured colour. Without a colour it inherits the theme text colour; an
10946
+ * empty text renders an empty cell.
10947
+ */
10948
+ class TableValueLabelCellComponent {
10949
+ /** Text of the cell: the label, or the raw value when unmapped; `''` for null / missing. */
10950
+ text = '';
10951
+ /** Optional CSS colour of the text. */
10952
+ color;
10953
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: TableValueLabelCellComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
10954
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: TableValueLabelCellComponent, isStandalone: true, selector: "mm-table-value-label-cell", inputs: { text: "text", color: "color" }, ngImport: i0, template: `@if (text) {<span class="mm-table-value-label" [style.color]="color || null">{{ text }}</span>}`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
10955
+ }
10956
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: TableValueLabelCellComponent, decorators: [{
10957
+ type: Component,
10958
+ args: [{
10959
+ selector: 'mm-table-value-label-cell',
10960
+ standalone: true,
10961
+ changeDetection: ChangeDetectionStrategy.OnPush,
10962
+ template: `@if (text) {<span class="mm-table-value-label" [style.color]="color || null">{{ text }}</span>}`
10963
+ }]
10964
+ }], propDecorators: { text: [{
10965
+ type: Input
10966
+ }], color: [{
10967
+ type: Input
10968
+ }] } });
10969
+
10475
10970
  const ICON_MAP = {
10476
10971
  'check-circle': checkCircleIcon,
10477
10972
  'x-circle': xCircleIcon,
@@ -10487,6 +10982,35 @@ function resolveStatusMapping(config) {
10487
10982
  { icon: ICON_MAP[val.icon] ?? questionCircleIcon, tooltip: val.tooltip, color: val.color }
10488
10983
  ]));
10489
10984
  }
10985
+ /** Per-instance fallback for the warning key of widgets without an id. */
10986
+ let nextTableWidgetInstance = 0;
10987
+ /** Short, stable hash of a text (FNV-1a, base 36) for warning keys. */
10988
+ function shortHash(text) {
10989
+ let hash = 0x811c9dc5;
10990
+ for (let i = 0; i < text.length; i++) {
10991
+ hash ^= text.charCodeAt(i);
10992
+ hash = Math.imul(hash, 0x01000193);
10993
+ }
10994
+ return (hash >>> 0).toString(36);
10995
+ }
10996
+ /** A row's value for a column field, read like `mm-list-view` does (flat key first, then nested path). */
10997
+ function readCellValue(item, field) {
10998
+ if (item === null || typeof item !== 'object') {
10999
+ return undefined;
11000
+ }
11001
+ const row = item;
11002
+ if (!field.includes('.') || row[field] !== undefined) {
11003
+ return row[field];
11004
+ }
11005
+ let value = row;
11006
+ for (const key of field.split('.')) {
11007
+ if (value === null || value === undefined || typeof value !== 'object') {
11008
+ return undefined;
11009
+ }
11010
+ value = value[key];
11011
+ }
11012
+ return value;
11013
+ }
10490
11014
  class TableWidgetComponent {
10491
11015
  config;
10492
11016
  dataSource;
@@ -10498,6 +11022,27 @@ class TableWidgetComponent {
10498
11022
  /** CK value type per derived column field (persistent queries), for the label resolver. */
10499
11023
  _queryColumnValueTypes = signal(new Map(), /* @ts-ignore */
10500
11024
  ...(ngDevMode ? [{ debugName: "_queryColumnValueTypes" }] : /* istanbul ignore next */ []));
11025
+ /** Host UI language (`MESHBOARD_LANGUAGE`, AB#7655) for column `titles` / `valueLabels`; `null` = `title` / raw texts. */
11026
+ language;
11027
+ /**
11028
+ * Problems already warned about (`<widget id or instance>|<field>|<problem hash>`), so a bad config
11029
+ * logs once, not on every language change, while a new problem after a config edit is reported again.
11030
+ */
11031
+ warnedColumns = new Set();
11032
+ /** Fallback warning key of a widget without an id. */
11033
+ instanceKey = `#${++nextTableWidgetInstance}`;
11034
+ /** App locale for the `number` / `date` rendering of unlabelled values in value-label columns. */
11035
+ locale = inject(LOCALE_ID);
11036
+ /**
11037
+ * Bumped on every `config` change: `config` is a plain `@Input`, so the computeds that read it
11038
+ * depend on this signal to pick up a new config on the same widget instance (AB#7655).
11039
+ */
11040
+ _configVersion = signal(0, /* @ts-ignore */
11041
+ ...(ngDevMode ? [{ debugName: "_configVersion" }] : /* istanbul ignore next */ []));
11042
+ constructor() {
11043
+ // Assigned here, not as a field initialiser: relative-import initialisers break under the Vitest runner (CLAUDE.md).
11044
+ this.language = injectMeshBoardLanguage();
11045
+ }
10501
11046
  // Widget state signals
10502
11047
  _isLoading = signal(false, /* @ts-ignore */
10503
11048
  ...(ngDevMode ? [{ debugName: "_isLoading" }] : /* istanbul ignore next */ []));
@@ -10530,6 +11075,8 @@ class TableWidgetComponent {
10530
11075
  * For persistent queries, uses dynamically derived columns from the query response.
10531
11076
  */
10532
11077
  listViewColumns = computed(() => {
11078
+ // A new config on the same instance recomputes the columns (titles, valueLabels, columns).
11079
+ this._configVersion();
10533
11080
  // Read the board's timezone mode so datetime cells (e.g. archive
10534
11081
  // window_start/window_end, which arrive as raw ISO-8601 strings) render on
10535
11082
  // the same basis the time filter is computed on. Reading it here makes the
@@ -10561,30 +11108,66 @@ class TableWidgetComponent {
10561
11108
  ...column,
10562
11109
  formatter: (value) => {
10563
11110
  const text = formatTableCellValue(value, mode);
10564
- if (typeof value !== 'string' || value === '') {
11111
+ // String cells as before; numeric cells only when the query reports an enum value
11112
+ // type (numeric enum codes, AB#7655) — the resolver gets the number as string.
11113
+ const isEnumNumber = typeof value === 'number' && Number.isFinite(value) && !!valueType && /ENUM/i.test(valueType);
11114
+ if (!isEnumNumber && (typeof value !== 'string' || value === '')) {
10565
11115
  return text;
10566
11116
  }
10567
11117
  return resolveMeshBoardLabel(resolver, {
10568
11118
  kind: 'tableCell',
10569
11119
  ...(ckTypeId ? { ckTypeId } : {}),
10570
11120
  attribute: column.field,
10571
- value,
11121
+ value: String(value),
10572
11122
  ...(valueType ? { valueType } : {}),
10573
11123
  defaultText: text
10574
11124
  });
10575
11125
  }
10576
11126
  };
10577
11127
  };
10578
- // If explicit columns are configured, use them (works for both data source types)
11128
+ // If explicit columns are configured, use them (works for both data source types).
11129
+ // Header texts and value labels follow the host language (AB#7655); reading the
11130
+ // language signal here re-labels the columns without refetching data.
10579
11131
  if (this.config?.columns && this.config.columns.length > 0) {
10580
- return this.config.columns.map(col => withDateFormatter({
10581
- field: col.field,
10582
- displayName: col.title,
10583
- dataType: (col.dataType ?? 'text'),
10584
- width: col.width,
10585
- ...(col.format ? { format: col.format } : {}),
10586
- ...(col.statusMapping ? { statusMapping: resolveStatusMapping(col.statusMapping) } : {})
10587
- }));
11132
+ const language = this.language();
11133
+ const textFormatter = (value) => formatTableCellValue(value, mode);
11134
+ return this.config.columns.map(col => {
11135
+ const problems = emptyLabelProblems();
11136
+ const base = {
11137
+ field: col.field,
11138
+ displayName: resolveColumnTitle(col, language, problems),
11139
+ dataType: (col.dataType ?? 'text'),
11140
+ width: col.width,
11141
+ ...(col.format ? { format: col.format } : {}),
11142
+ ...(col.statusMapping ? { statusMapping: resolveStatusMapping(col.statusMapping) } : {})
11143
+ };
11144
+ const labels = resolveValueLabels(col, language, problems);
11145
+ if (labels && (col.statusMapping || col.dataType === 'statusIcons')) {
11146
+ // valueLabels together with statusMapping (status icons) is not supported: the labels win.
11147
+ problems.statusMappingIgnored = true;
11148
+ }
11149
+ this.warnOnce(col, problems);
11150
+ if (!labels) {
11151
+ return withDateFormatter(base);
11152
+ }
11153
+ // valueLabels win over the label resolver: a plain-text cell with the label (and its optional
11154
+ // colour); an unmapped value keeps the column's dataType / format rendering; empty for null.
11155
+ return {
11156
+ ...base,
11157
+ dataType: 'component',
11158
+ cellComponent: TableValueLabelCellComponent,
11159
+ cellInputs: (item) => {
11160
+ const value = readCellValue(item, col.field);
11161
+ if (value === null || value === undefined || value === '') {
11162
+ return { text: '' };
11163
+ }
11164
+ const label = labels.get(String(value));
11165
+ return label
11166
+ ? { text: label.text, color: label.color }
11167
+ : { text: formatUnlabelledValue(value, col, this.locale, textFormatter) };
11168
+ }
11169
+ };
11170
+ });
10588
11171
  }
10589
11172
  // For persistent query without explicit columns, use derived columns
10590
11173
  if (this.config?.dataSource?.type === 'persistentQuery') {
@@ -10619,6 +11202,7 @@ class TableWidgetComponent {
10619
11202
  * Returns true once columns are available.
10620
11203
  */
10621
11204
  columnsReady = computed(() => {
11205
+ this._configVersion();
10622
11206
  if (!this.config?.dataSource)
10623
11207
  return false;
10624
11208
  if (this.config.dataSource.type === 'persistentQuery') {
@@ -10629,6 +11213,38 @@ class TableWidgetComponent {
10629
11213
  return (this.config.columns?.length ?? 0) > 0;
10630
11214
  }, /* @ts-ignore */
10631
11215
  ...(ngDevMode ? [{ debugName: "columnsReady" }] : /* istanbul ignore next */ []));
11216
+ /**
11217
+ * Logs one console warning per widget, column and set of problems for ignored `titles` /
11218
+ * `valueLabels` entries (unknown culture keys, malformed titles or value labels, `valueLabels`
11219
+ * together with `statusMapping`, AB#7655). The table still renders. The key carries a hash of the
11220
+ * problem list, so a different problem after a config edit is reported again; widgets without an
11221
+ * id use a per-instance key.
11222
+ */
11223
+ warnOnce(column, problems) {
11224
+ if (!hasLabelProblems(problems)) {
11225
+ return;
11226
+ }
11227
+ const parts = [];
11228
+ const quoted = (keys) => keys.map(k => `"${k}"`).join(', ');
11229
+ if (problems.unknownCultureKeys.length > 0) {
11230
+ parts.push(`unknown culture keys ${quoted(problems.unknownCultureKeys)}`);
11231
+ }
11232
+ if (problems.malformedTitleKeys && problems.malformedTitleKeys.length > 0) {
11233
+ parts.push(`malformed titles entries ${quoted(problems.malformedTitleKeys)}`);
11234
+ }
11235
+ if (problems.malformedValueKeys.length > 0) {
11236
+ parts.push(`malformed valueLabels entries ${quoted(problems.malformedValueKeys)}`);
11237
+ }
11238
+ if (problems.statusMappingIgnored) {
11239
+ parts.push('statusMapping (valueLabels together with status icons are not supported; the labels are shown)');
11240
+ }
11241
+ const key = `${this.config?.id || this.instanceKey}|${column.field}|${shortHash(parts.join(';'))}`;
11242
+ if (this.warnedColumns.has(key)) {
11243
+ return;
11244
+ }
11245
+ this.warnedColumns.add(key);
11246
+ console.warn(`octo-meshboard: table widget "${this.config?.title ?? ''}" (${this.config?.id ?? ''}), column "${column.field}": ignoring ${parts.join('; ')}`);
11247
+ }
10632
11248
  formatColumnTitle(field) {
10633
11249
  return field
10634
11250
  .replace(/([A-Z])/g, ' $1')
@@ -10636,6 +11252,9 @@ class TableWidgetComponent {
10636
11252
  .trim();
10637
11253
  }
10638
11254
  ngOnChanges(changes) {
11255
+ if (changes['config']) {
11256
+ this._configVersion.update(version => version + 1);
11257
+ }
10639
11258
  if (changes['config'] && !changes['config'].firstChange) {
10640
11259
  // Clear cached query columns when config changes (for persistent queries)
10641
11260
  if (this.config?.dataSource?.type === 'persistentQuery') {
@@ -10713,7 +11332,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
10713
11332
  }
10714
11333
  </div>
10715
11334
  `, changeDetection: ChangeDetectionStrategy.Eager, styles: [".table-widget{height:100%;width:100%;display:flex;flex-direction:column}.no-config-overlay{display:flex;align-items:center;justify-content:center;height:100%;color:var(--kendo-color-subtle, #6c757d);font-style:italic}:host ::ng-deep mm-list-view{height:100%}:host ::ng-deep .k-grid{height:100%}\n"] }]
10716
- }], propDecorators: { config: [{
11335
+ }], ctorParameters: () => [], propDecorators: { config: [{
10717
11336
  type: Input
10718
11337
  }], dataSource: [{
10719
11338
  type: ViewChild,
@@ -10732,6 +11351,10 @@ class TableConfigDialogComponent {
10732
11351
  getCkTypeAvailableQueryColumnsGQL = inject(GetCkTypeAvailableQueryColumnsDtoGQL);
10733
11352
  stateService = inject(MeshBoardStateService);
10734
11353
  windowRef = inject(WindowRef);
11354
+ getRuntimeQueryColumnsGQL = inject(GetRuntimeQueryColumnsDtoGQL);
11355
+ queryExecutor = inject(QueryExecutorService);
11356
+ /** Fields the saved query returns now, loaded once per query rtId (AB#7655, kept explicit columns). */
11357
+ queryFieldsLoad;
10735
11358
  ckTypeSelectorInput;
10736
11359
  filterEditor;
10737
11360
  querySelector;
@@ -10949,14 +11572,48 @@ class TableConfigDialogComponent {
10949
11572
  return 'No sorting configured';
10950
11573
  return this.sorting.map(s => `${s.attributePath} ${s.sortOrder === SortOrdersDto.AscendingDto ? '↑' : '↓'}`).join(', ');
10951
11574
  }
10952
- onSave() {
11575
+ /**
11576
+ * The fields a persistent query returns now (`null` when unknown or the lookup failed). Runtime
11577
+ * queries use the metadata-only column resolver; stream-data queries execute with `first: 1`
11578
+ * (no column-introspection endpoint), like the chart dialogs.
11579
+ */
11580
+ loadQueryFields(rtId) {
11581
+ if (this.queryFieldsLoad?.rtId === rtId) {
11582
+ return this.queryFieldsLoad.fields;
11583
+ }
11584
+ const fields = (async () => {
11585
+ try {
11586
+ const family = queryFamily(this.selectedPersistentQuery?.ckTypeId) ?? this.initialQueryFamily
11587
+ ?? await this.queryExecutor.resolveFamily(rtId);
11588
+ if (family === 'runtime') {
11589
+ // network-only: the query may have changed since its columns were cached.
11590
+ const result = await firstValueFrom(this.getRuntimeQueryColumnsGQL.fetch({
11591
+ variables: { rtId },
11592
+ fetchPolicy: 'network-only'
11593
+ }));
11594
+ const columns = result.data?.runtime?.runtimeQuery?.items?.[0]?.columns ?? [];
11595
+ return columns
11596
+ .map(c => c?.attributePath ?? '')
11597
+ .filter(path => path !== '');
11598
+ }
11599
+ const sdResult = await firstValueFrom(this.queryExecutor.executeStreamData(rtId, { first: 1, forceRefresh: true }));
11600
+ return sdResult.columns.map(c => c.attributePath).filter(path => !!path);
11601
+ }
11602
+ catch (error) {
11603
+ console.error('Error loading query columns:', error);
11604
+ return null;
11605
+ }
11606
+ })();
11607
+ this.queryFieldsLoad = { rtId, fields };
11608
+ return fields;
11609
+ }
11610
+ async onSave() {
10953
11611
  if (this.dataSourceType === 'runtimeEntity' && this.selectedCkType) {
10954
- // Convert selected columns to TableColumn format
10955
- const columns = this.selectedColumns.map(field => ({
10956
- field,
10957
- title: this.formatColumnTitle(field),
10958
- width: undefined
10959
- }));
11612
+ // Convert selected columns to TableColumn format. Columns that stay selected on the same
11613
+ // CK type keep their authored properties (titles, valueLabels, width, format, ... — AB#7655).
11614
+ const sameDataSource = this.initialDataSourceType !== 'persistentQuery'
11615
+ && !!this.initialCkTypeId && this.selectedCkType.rtCkTypeId === this.initialCkTypeId;
11616
+ const columns = mergeRuntimeEntityColumns(this.selectedColumns, this.initialColumns, sameDataSource, field => this.formatColumnTitle(field));
10960
11617
  // Convert filters (remove IDs)
10961
11618
  const filterDtos = this.filters.map(f => ({
10962
11619
  attributePath: f.attributePath,
@@ -10982,15 +11639,24 @@ class TableConfigDialogComponent {
10982
11639
  }));
10983
11640
  // Derive family from the selected query's CK type so the runtime executor
10984
11641
  // can route correctly without an extra lookup.
10985
- const family = queryFamily(this.selectedPersistentQuery.ckTypeId) ?? this.initialQueryFamily ?? undefined;
11642
+ const query = this.selectedPersistentQuery;
11643
+ const family = queryFamily(query.ckTypeId) ?? this.initialQueryFamily ?? undefined;
11644
+ // Explicit (hand-authored) columns survive a save on the same query (AB#7655), minus the ones
11645
+ // the query no longer returns, plus the query's other columns as derived columns; a new query
11646
+ // starts without explicit columns (derived from the query at run time).
11647
+ const sameQuery = this.initialDataSourceType === 'persistentQuery' && query.rtId === this.initialQueryRtId;
11648
+ const queryFields = sameQuery && (this.initialColumns?.length ?? 0) > 0
11649
+ ? await this.loadQueryFields(query.rtId)
11650
+ : null;
11651
+ const columns = mergePersistentQueryColumns(this.initialColumns, sameQuery, queryFields, field => this.formatColumnTitle(field));
10986
11652
  this.windowRef.close({
10987
11653
  dataSourceType: 'persistentQuery',
10988
11654
  ckTypeId: '', // Not used for persistent query
10989
- columns: [], // Will be derived from query
11655
+ columns,
10990
11656
  sorting: [],
10991
11657
  filters: queryFilterDtos,
10992
- queryRtId: this.selectedPersistentQuery.rtId,
10993
- queryName: this.selectedPersistentQuery.name,
11658
+ queryRtId: query.rtId,
11659
+ queryName: query.name,
10994
11660
  queryFamily: family,
10995
11661
  ignoreTimeFilter: this.ignoreTimeFilter,
10996
11662
  entitySelectorId: this.entitySelectorId,
@@ -11639,7 +12305,7 @@ class GaugeWidgetComponent {
11639
12305
  // Entity-selector scope (AB#7721): pending → stay in the loading state and
11640
12306
  // send no query (the view refreshes once the scope is known); empty → show
11641
12307
  // no value ('-') without querying the persisted (unscoped) scope.
11642
- const scope = this.stateService.resolveStreamDataScope(dataSource.entitySelectorId, dataSource.queryFamily);
12308
+ const scope = this.stateService.resolveStreamDataScope(dataSource.entitySelectorId, dataSource.queryFamily, dataSource.ignoreTimeFilter);
11643
12309
  if (scope.kind === 'pending') {
11644
12310
  return;
11645
12311
  }
@@ -11651,7 +12317,9 @@ class GaugeWidgetComponent {
11651
12317
  const streamDataArgs = this.buildStreamDataArgs();
11652
12318
  const result = await firstValueFrom(this.queryExecutor.execute(dataSource.queryFamily, dataSource.queryRtId, {
11653
12319
  fieldFilter: fieldFilter ?? undefined,
11654
- streamDataArgs
12320
+ streamDataArgs,
12321
+ // Runtime queries: `rtId IN` field filter (AB#7725); stream data: arg.rtIds.
12322
+ scopeRtIds: scope.kind === 'scoped' ? scope.rtIds : undefined
11655
12323
  }).pipe(catchError(err => {
11656
12324
  console.error('Error loading Gauge query data:', err);
11657
12325
  throw err;
@@ -13753,7 +14421,7 @@ class PieChartWidgetComponent {
13753
14421
  // Entity-selector scope (AB#7721): pending → stay in the loading state and
13754
14422
  // send no query (the view refreshes once the scope is known); empty → render
13755
14423
  // the empty chart without querying the persisted (unscoped) scope.
13756
- const scope = this.stateService.resolveStreamDataScope(dataSource.entitySelectorId, dataSource.queryFamily);
14424
+ const scope = this.stateService.resolveStreamDataScope(dataSource.entitySelectorId, dataSource.queryFamily, dataSource.ignoreTimeFilter);
13757
14425
  if (scope.kind === 'pending') {
13758
14426
  return;
13759
14427
  }
@@ -13769,7 +14437,9 @@ class PieChartWidgetComponent {
13769
14437
  const streamDataArgs = this.buildStreamDataArgs();
13770
14438
  const result = await firstValueFrom(this.queryExecutor.execute(dataSource.queryFamily, dataSource.queryRtId, {
13771
14439
  fieldFilter: fieldFilter ?? undefined,
13772
- streamDataArgs
14440
+ streamDataArgs,
14441
+ // Runtime queries: `rtId IN` field filter (AB#7725); stream data: arg.rtIds.
14442
+ scopeRtIds: scope.kind === 'scoped' ? scope.rtIds : undefined
13773
14443
  }).pipe(catchError(err => {
13774
14444
  console.error('Error loading Pie Chart data:', err);
13775
14445
  throw err;
@@ -14979,7 +15649,7 @@ class BarChartWidgetComponent {
14979
15649
  // Entity-selector scope (AB#7721): pending → stay in the loading state and
14980
15650
  // send no query (the view refreshes once the scope is known); empty → render
14981
15651
  // the empty chart without querying the persisted (unscoped) scope.
14982
- const scope = this.stateService.resolveStreamDataScope(queryDataSource.entitySelectorId, queryDataSource.queryFamily);
15652
+ const scope = this.stateService.resolveStreamDataScope(queryDataSource.entitySelectorId, queryDataSource.queryFamily, queryDataSource.ignoreTimeFilter);
14983
15653
  if (scope.kind === 'pending') {
14984
15654
  return;
14985
15655
  }
@@ -14988,7 +15658,9 @@ class BarChartWidgetComponent {
14988
15658
  ? emptyQueryExecutionResult(queryDataSource.queryFamily)
14989
15659
  : await firstValueFrom(this.queryExecutor.execute(queryDataSource.queryFamily, queryDataSource.queryRtId, {
14990
15660
  fieldFilter: fieldFilter ?? undefined,
14991
- streamDataArgs
15661
+ streamDataArgs,
15662
+ // Runtime queries: `rtId IN` field filter (AB#7725); stream data: arg.rtIds.
15663
+ scopeRtIds: scope.kind === 'scoped' ? scope.rtIds : undefined
14992
15664
  }).pipe(catchError(err => {
14993
15665
  console.error('Error loading Bar Chart data:', err);
14994
15666
  throw err;
@@ -16540,7 +17212,7 @@ class LineChartWidgetComponent {
16540
17212
  // Entity-selector scope (AB#7721): pending → stay in the loading state and
16541
17213
  // send no query (the view refreshes once the scope is known); empty → render
16542
17214
  // the empty chart without querying the persisted (unscoped) scope.
16543
- const scope = this.stateService.resolveStreamDataScope(queryDataSource.entitySelectorId, queryDataSource.queryFamily);
17215
+ const scope = this.stateService.resolveStreamDataScope(queryDataSource.entitySelectorId, queryDataSource.queryFamily, queryDataSource.ignoreTimeFilter);
16544
17216
  if (scope.kind === 'pending') {
16545
17217
  return;
16546
17218
  }
@@ -16580,7 +17252,9 @@ class LineChartWidgetComponent {
16580
17252
  this.lastLimit = streamDataArgs?.limit ?? 0;
16581
17253
  const result = await firstValueFrom(this.queryExecutor.execute(queryDataSource.queryFamily, queryDataSource.queryRtId, {
16582
17254
  fieldFilter: fieldFilter ?? undefined,
16583
- streamDataArgs
17255
+ streamDataArgs,
17256
+ // Runtime queries: `rtId IN` field filter (AB#7725); stream data: arg.rtIds.
17257
+ scopeRtIds: scope.kind === 'scoped' ? scope.rtIds : undefined
16584
17258
  }).pipe(catchError(err => {
16585
17259
  console.error('Error loading Line Chart data:', err);
16586
17260
  throw err;
@@ -16796,7 +17470,7 @@ class LineChartWidgetComponent {
16796
17470
  // as UTC — a bare `new Date(...)` would reinterpret it as browser-local time and shift
16797
17471
  // the axis by the UTC offset (AB#4818).
16798
17472
  if (!allCategories.has(categoryValue)) {
16799
- allCategories.set(categoryValue, toInstant(categoryValue) ?? new Date(NaN));
17473
+ allCategories.set(categoryValue, toInstant$1(categoryValue) ?? new Date(NaN));
16800
17474
  }
16801
17475
  allSeriesGroups.add(seriesGroupValue);
16802
17476
  if (!dataMap.has(categoryValue)) {
@@ -18136,7 +18810,7 @@ class HeatmapWidgetComponent {
18136
18810
  // Entity-selector scope (AB#7721): pending → stay in the loading state and
18137
18811
  // send no query (the view refreshes once the scope is known); empty → render
18138
18812
  // the empty grid without querying the persisted (unscoped) scope.
18139
- const scope = this.stateService.resolveStreamDataScope(queryDataSource.entitySelectorId, queryDataSource.queryFamily);
18813
+ const scope = this.stateService.resolveStreamDataScope(queryDataSource.entitySelectorId, queryDataSource.queryFamily, queryDataSource.ignoreTimeFilter);
18140
18814
  if (scope.kind === 'pending') {
18141
18815
  return;
18142
18816
  }
@@ -18145,7 +18819,9 @@ class HeatmapWidgetComponent {
18145
18819
  ? emptyQueryExecutionResult(queryDataSource.queryFamily)
18146
18820
  : await firstValueFrom(this.queryExecutor.execute(queryDataSource.queryFamily, queryDataSource.queryRtId, {
18147
18821
  fieldFilter: fieldFilter ?? undefined,
18148
- streamDataArgs
18822
+ streamDataArgs,
18823
+ // Runtime queries: `rtId IN` field filter (AB#7725); stream data: arg.rtIds.
18824
+ scopeRtIds: scope.kind === 'scoped' ? scope.rtIds : undefined
18149
18825
  }).pipe(catchError(err => {
18150
18826
  console.error('Error loading Heatmap data:', err);
18151
18827
  throw err;
@@ -18413,7 +19089,7 @@ class HeatmapWidgetComponent {
18413
19089
  parseDate(value) {
18414
19090
  if (!value)
18415
19091
  return null;
18416
- return toInstant(value);
19092
+ return toInstant$1(value);
18417
19093
  }
18418
19094
  convertFiltersToDto(filters) {
18419
19095
  const variables = this.stateService.getVariables();
@@ -19530,15 +20206,19 @@ class StatsGridWidgetComponent {
19530
20206
  return aggregationResults.get(stat.queryId) ?? null;
19531
20207
  }
19532
20208
  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') {
20209
+ // Pending or empty scope (still resolving, nothing in scope or lookup failed,
20210
+ // AB#7721/AB#7725): no value, no query — never the persisted (unscoped) scope.
20211
+ // The view refreshes the widget once a pending scope is resolved.
20212
+ const scope = this.scopeOf(source);
20213
+ if (scope.kind === 'pending' || scope.kind === 'empty') {
19536
20214
  return null;
19537
20215
  }
19538
20216
  try {
19539
20217
  const result = await firstValueFrom(this.queryExecutor.execute(source.queryFamily, source.queryRtId, {
19540
20218
  fieldFilter: this.convertFiltersToDto(source.filters),
19541
- streamDataArgs: this.buildStreamDataArgs(source)
20219
+ streamDataArgs: this.buildStreamDataArgs(source),
20220
+ // Runtime queries: `rtId IN` field filter (AB#7725); stream data: arg.rtIds.
20221
+ scopeRtIds: scope.kind === 'scoped' ? scope.rtIds : undefined
19542
20222
  }));
19543
20223
  return extractPersistentQueryCellValue(result, source);
19544
20224
  }
@@ -19548,7 +20228,7 @@ class StatsGridWidgetComponent {
19548
20228
  }
19549
20229
  }
19550
20230
  scopeOf(source) {
19551
- return this.stateService.resolveStreamDataScope(source.entitySelectorId, source.queryFamily);
20231
+ return this.stateService.resolveStreamDataScope(source.entitySelectorId, source.queryFamily, source.ignoreTimeFilter);
19552
20232
  }
19553
20233
  buildStreamDataArgs(source) {
19554
20234
  const timeArgs = this.stateService.resolveStreamDataTimeArgs(source.ignoreTimeFilter);
@@ -19721,7 +20401,7 @@ class PersistentQueryCellEditorComponent {
19721
20401
  </kendo-combobox>
19722
20402
  </div>
19723
20403
 
19724
- <!-- Stream-data-only options (hidden for runtime queries) -->
20404
+ <!-- Time filter: stream data only. Scope binding: both families (AB#7725). -->
19725
20405
  <mm-sd-time-filter-toggle
19726
20406
  [family]="family"
19727
20407
  [(ignoreTimeFilter)]="ignoreTimeFilter"
@@ -19786,7 +20466,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
19786
20466
  </kendo-combobox>
19787
20467
  </div>
19788
20468
 
19789
- <!-- Stream-data-only options (hidden for runtime queries) -->
20469
+ <!-- Time filter: stream data only. Scope binding: both families (AB#7725). -->
19790
20470
  <mm-sd-time-filter-toggle
19791
20471
  [family]="family"
19792
20472
  [(ignoreTimeFilter)]="ignoreTimeFilter"
@@ -23229,19 +23909,23 @@ class SummaryCardWidgetComponent {
23229
23909
  return null;
23230
23910
  }
23231
23911
  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') {
23912
+ // Pending or empty scope (still resolving, nothing in scope or lookup failed,
23913
+ // AB#7721/AB#7725): no value, no query — never the persisted (unscoped) scope.
23914
+ // The view refreshes the widget once a pending scope is resolved.
23915
+ const scope = this.scopeOf(source);
23916
+ if (scope.kind === 'pending' || scope.kind === 'empty') {
23235
23917
  return null;
23236
23918
  }
23237
23919
  const result = await firstValueFrom(this.queryExecutor.execute(source.queryFamily, source.queryRtId, {
23238
23920
  fieldFilter: this.convertFiltersToDto(source.filters),
23239
- streamDataArgs: this.buildStreamDataArgs(source)
23921
+ streamDataArgs: this.buildStreamDataArgs(source),
23922
+ // Runtime queries: `rtId IN` field filter (AB#7725); stream data: arg.rtIds.
23923
+ scopeRtIds: scope.kind === 'scoped' ? scope.rtIds : undefined
23240
23924
  }));
23241
23925
  return extractPersistentQueryCellValue(result, source);
23242
23926
  }
23243
23927
  scopeOf(source) {
23244
- return this.stateService.resolveStreamDataScope(source.entitySelectorId, source.queryFamily);
23928
+ return this.stateService.resolveStreamDataScope(source.entitySelectorId, source.queryFamily, source.ignoreTimeFilter);
23245
23929
  }
23246
23930
  buildStreamDataArgs(source) {
23247
23931
  const timeArgs = this.stateService.resolveStreamDataTimeArgs(source.ignoreTimeFilter);
@@ -24753,6 +25437,13 @@ class ProcessDataService {
24753
25437
  if (!queryRtId) {
24754
25438
  throw new Error('Persistent query binding requires queryRtId');
24755
25439
  }
25440
+ // AB#7582: while the embedding host's period is invalid, a stream-data query
25441
+ // bound to the time filter is not sent (it would read the whole archive); the
25442
+ // binding gets no value. Runtime queries are unaffected.
25443
+ if (this.stateService.isStreamDataBlockedByHostTimeRange()
25444
+ && await this.queryExecutor.resolveFamily(queryRtId) === 'streamData') {
25445
+ return undefined;
25446
+ }
24756
25447
  // Execute via the unified executor (family resolved lazily). The active time
24757
25448
  // filter is applied by default for stream-data queries; runtime queries
24758
25449
  // ignore it. The element binding has no asset-scope, so no rtIds override.
@@ -24953,7 +25644,7 @@ class ProcessDataService {
24953
25644
  // or empty — never the persisted (unscoped) scope. Pending → a `pending` result
24954
25645
  // (the widget stays loading; a refresh follows once the scope resolves);
24955
25646
  // empty → no rows.
24956
- const scope = this.stateService.resolveStreamDataScope(config.bindingEntitySelectorId, bindingQueryFamily);
25647
+ const scope = this.stateService.resolveStreamDataScope(config.bindingEntitySelectorId, bindingQueryFamily, config.bindingIgnoreTimeFilter);
24957
25648
  if (scope.kind === 'pending') {
24958
25649
  return { type: 'query', pending: true, loadedAt: new Date() };
24959
25650
  }
@@ -24973,7 +25664,9 @@ class ProcessDataService {
24973
25664
  const result = await firstValueFrom(this.queryExecutor.execute(bindingQueryFamily, bindingQueryRtId, {
24974
25665
  first: 1000, // Reasonable limit for process diagram data
24975
25666
  fieldFilter: fieldFilters,
24976
- streamDataArgs: this.buildBindingStreamDataArgs(config)
25667
+ streamDataArgs: this.buildBindingStreamDataArgs(config),
25668
+ // Runtime queries: `rtId IN` field filter (AB#7725); stream data: arg.rtIds.
25669
+ scopeRtIds: scope.kind === 'scoped' ? scope.rtIds : undefined
24977
25670
  }));
24978
25671
  return {
24979
25672
  type: 'query',
@@ -30592,7 +31285,8 @@ function registerDefaultWidgets(registry) {
30592
31285
  return {
30593
31286
  ...widget,
30594
31287
  dataSource,
30595
- columns: [], // Will be derived from query at runtime
31288
+ // Explicit columns the dialog kept (same query, AB#7655); [] = derived from the query at runtime
31289
+ columns: result.columns ?? [],
30596
31290
  sorting: [],
30597
31291
  filters,
30598
31292
  pageSize: result.pageSize,
@@ -32279,6 +32973,9 @@ function provideMeshBoard(options) {
32279
32973
  if (opts.messages) {
32280
32974
  providers.push({ provide: MESHBOARD_MESSAGES, useValue: opts.messages });
32281
32975
  }
32976
+ if (opts.language) {
32977
+ providers.push({ provide: MESHBOARD_LANGUAGE, useValue: opts.language });
32978
+ }
32282
32979
  // Include default widgets unless explicitly disabled
32283
32980
  if (opts.includeDefaultWidgets !== false) {
32284
32981
  providers.push(provideAppInitializer(() => {
@@ -32295,6 +32992,13 @@ function provideMeshBoard(options) {
32295
32992
  function provideMeshBoardMessages(messages) {
32296
32993
  return makeEnvironmentProviders([{ provide: MESHBOARD_MESSAGES, useValue: messages }]);
32297
32994
  }
32995
+ /**
32996
+ * Provides the host's UI language (`MESHBOARD_LANGUAGE`, AB#7655) without `provideMeshBoard()` —
32997
+ * for apps that register the widgets lazily. A string or a signal (the widgets follow it).
32998
+ */
32999
+ function provideMeshBoardLanguage(language) {
33000
+ return makeEnvironmentProviders([{ provide: MESHBOARD_LANGUAGE, useValue: language }]);
33001
+ }
32298
33002
  /**
32299
33003
  * Provider function for registering custom widgets.
32300
33004
  * Use this to add your own widget types to MeshBoard.
@@ -32426,6 +33130,506 @@ function labelList(selectors) {
32426
33130
  return `${labels.slice(0, -1).join(', ')} and ${labels[labels.length - 1]}`;
32427
33131
  }
32428
33132
 
33133
+ /** Turns a selector's child scope into its resolution plan. */
33134
+ function scopePlan(childScope) {
33135
+ if (!childScope) {
33136
+ return { kind: 'none' };
33137
+ }
33138
+ const path = childScope.path;
33139
+ if (Array.isArray(path) && path.length > 0) {
33140
+ if (path.length > MAX_SCOPE_HOPS) {
33141
+ return { kind: 'invalid', reason: `the path has ${path.length} hops (at most ${MAX_SCOPE_HOPS})` };
33142
+ }
33143
+ const hops = [];
33144
+ for (let i = 0; i < path.length; i++) {
33145
+ const hop = normalizeHop(path[i]);
33146
+ if (!hop) {
33147
+ return { kind: 'invalid', reason: `hop ${i + 1} needs an association role and a target type` };
33148
+ }
33149
+ hops.push(hop);
33150
+ }
33151
+ return { kind: 'path', hops };
33152
+ }
33153
+ // Original one-hop form; incomplete top-level fields mean "no child scope",
33154
+ // exactly as before AB#7723.
33155
+ if (!childScope.targetCkTypeId || !childScope.roleId) {
33156
+ return { kind: 'none' };
33157
+ }
33158
+ return {
33159
+ kind: 'single',
33160
+ hop: {
33161
+ roleId: childScope.roleId,
33162
+ direction: childScope.direction ?? 'in',
33163
+ targetCkTypeId: childScope.targetCkTypeId
33164
+ }
33165
+ };
33166
+ }
33167
+ /** The hops of a child scope as the editor shows them (one-hop form = one hop). */
33168
+ function scopeHopsOf(childScope) {
33169
+ if (!childScope) {
33170
+ return [];
33171
+ }
33172
+ if (Array.isArray(childScope.path) && childScope.path.length > 0) {
33173
+ return childScope.path.map(h => ({ roleId: h.roleId, direction: h.direction ?? 'in', targetCkTypeId: h.targetCkTypeId }));
33174
+ }
33175
+ // Incomplete top-level fields are "no child scope" at runtime; show them as none.
33176
+ if (childScope.targetCkTypeId && childScope.roleId) {
33177
+ return [{ roleId: childScope.roleId, direction: childScope.direction ?? 'in', targetCkTypeId: childScope.targetCkTypeId }];
33178
+ }
33179
+ return [];
33180
+ }
33181
+ /**
33182
+ * Editor validation of a hop list: the message for the first hop without role
33183
+ * or target type, or for more than {@link MAX_SCOPE_HOPS} hops; `null` when the
33184
+ * list can be saved (an empty list = no child scope).
33185
+ */
33186
+ function scopeHopsValidationError(hops) {
33187
+ if (hops.length > MAX_SCOPE_HOPS) {
33188
+ return `At most ${MAX_SCOPE_HOPS} hops.`;
33189
+ }
33190
+ const index = hops.findIndex(h => !h.roleId?.trim() || !h.targetCkTypeId?.trim());
33191
+ return index < 0 ? null : `Hop ${index + 1} needs an association and a target type.`;
33192
+ }
33193
+ /**
33194
+ * The child scope the editor saves for a list of hops: none for zero hops, the
33195
+ * original top-level form for one hop (older library versions keep reading
33196
+ * it), and `path` only for two or more hops. Roles are trimmed.
33197
+ */
33198
+ function childScopeFromHops(hops) {
33199
+ const clean = hops.map(h => ({
33200
+ roleId: h.roleId.trim(),
33201
+ direction: h.direction ?? 'in',
33202
+ targetCkTypeId: h.targetCkTypeId.trim()
33203
+ }));
33204
+ if (clean.length === 0) {
33205
+ return undefined;
33206
+ }
33207
+ if (clean.length === 1) {
33208
+ const [hop] = clean;
33209
+ return { targetCkTypeId: hop.targetCkTypeId, roleId: hop.roleId, direction: hop.direction };
33210
+ }
33211
+ return { path: clean };
33212
+ }
33213
+ /**
33214
+ * Resolves a scope path hop by hop from the picked entity: each hop is ONE
33215
+ * batched step over all entities the previous hop reached (the fetcher decides
33216
+ * how to batch), the source type of hop n is the target type of hop n-1, and
33217
+ * the result is the distinct rtIds of the last hop.
33218
+ *
33219
+ * - An empty hop ends the resolution: no further hop is requested and the
33220
+ * scope is empty (AB#7721 empty-scope rule).
33221
+ * - A failing hop rejects; the caller turns that into a failed scope.
33222
+ * - `isCurrent` is checked after every hop: when a newer pick has started, the
33223
+ * resolution stops and returns `null` (stale — must not be applied).
33224
+ */
33225
+ async function resolveScopePath(pickedRtId, pickedCkTypeId, hops, fetchHop, isCurrent = () => true) {
33226
+ let sources = [pickedRtId];
33227
+ let sourceCkTypeId = pickedCkTypeId;
33228
+ for (const hop of hops) {
33229
+ const targets = await fetchHop(sources, sourceCkTypeId, hop);
33230
+ if (!isCurrent()) {
33231
+ return null;
33232
+ }
33233
+ sources = distinctRtIds(targets);
33234
+ if (sources.length === 0) {
33235
+ return [];
33236
+ }
33237
+ sourceCkTypeId = hop.targetCkTypeId;
33238
+ }
33239
+ return sources;
33240
+ }
33241
+ /** Non-empty rtIds in first-seen order, without duplicates. */
33242
+ function distinctRtIds(rtIds) {
33243
+ const seen = new Set();
33244
+ for (const id of rtIds) {
33245
+ const value = id === null || id === undefined ? '' : String(id);
33246
+ if (value.length > 0) {
33247
+ seen.add(value);
33248
+ }
33249
+ }
33250
+ return [...seen];
33251
+ }
33252
+ /** "Test/Site → Test/System → Test/Device" for the editor's path line. */
33253
+ function scopePathLabel(startCkTypeId, hops) {
33254
+ return [startCkTypeId, ...hops.map(h => h.targetCkTypeId || '?')].join(' → ');
33255
+ }
33256
+ function normalizeHop(hop) {
33257
+ const roleId = typeof hop?.roleId === 'string' ? hop.roleId.trim() : '';
33258
+ const targetCkTypeId = typeof hop?.targetCkTypeId === 'string' ? hop.targetCkTypeId.trim() : '';
33259
+ if (!roleId || !targetCkTypeId) {
33260
+ return null;
33261
+ }
33262
+ return { roleId, direction: hop?.direction === 'out' ? 'out' : 'in', targetCkTypeId };
33263
+ }
33264
+
33265
+ /**
33266
+ * Picker filter of an entity selector (AB#7724).
33267
+ *
33268
+ * `EntitySelectorConfig.filters` narrows what the selector's pickers list. The
33269
+ * rows are converted exactly like widget filters (variables substituted, AND)
33270
+ * and sent as GraphQL `fieldFilter` of the `runtimeEntities` query, so the
33271
+ * server filters — the picker never loads all entities of the type. Pure
33272
+ * helpers, shared by the toolbar (runtime) and the selector editor.
33273
+ */
33274
+ /** True when the selector carries at least one filter row that applies (never for followers). */
33275
+ function hasPickerFilter(selector) {
33276
+ return !isFollowerSelector(selector) && (selector.filters?.length ?? 0) > 0;
33277
+ }
33278
+ /**
33279
+ * The GraphQL field filters of the selector's picker, with board variables
33280
+ * substituted; `undefined` when the selector has no filter or is a follower
33281
+ * (a follower has no picker and ignores a filter).
33282
+ */
33283
+ function pickerFieldFilters(selector, variables, variableService) {
33284
+ if (!hasPickerFilter(selector)) {
33285
+ return undefined;
33286
+ }
33287
+ return variableService.convertToFieldFilterDto(selector.filters, variables);
33288
+ }
33289
+ /**
33290
+ * Wraps the picker query so every fetch carries `baseFilters` in front of the
33291
+ * fetch's own field filters (the text search of the dropdown and the dialog).
33292
+ * GraphQL combines field filters with AND, so the search works on top of the
33293
+ * selector filter. Without base filters the original query is returned
33294
+ * unchanged — an unfiltered picker sends exactly the query it sent before.
33295
+ *
33296
+ * The wrapper inherits from the original service object, so it is usable
33297
+ * wherever the picker data sources (octo-services) expect the query service.
33298
+ */
33299
+ function withPickerFilters(gql, baseFilters) {
33300
+ if (!baseFilters?.length) {
33301
+ return gql;
33302
+ }
33303
+ const wrapped = Object.create(gql);
33304
+ wrapped.fetch = ((options) => {
33305
+ const variables = options?.variables;
33306
+ const own = variables?.fieldFilters;
33307
+ const ownList = own === undefined || own === null ? [] : Array.isArray(own) ? own : [own];
33308
+ return gql.fetch({
33309
+ ...options,
33310
+ variables: { ...variables, fieldFilters: [...baseFilters, ...ownList] }
33311
+ });
33312
+ });
33313
+ return wrapped;
33314
+ }
33315
+ /**
33316
+ * Variable names a filter row still references after substitution — a board
33317
+ * variable without a value. Such a value is sent literally, exactly like in
33318
+ * widget filters (`MeshBoardVariableService.resolveVariables` keeps the
33319
+ * placeholder), so it usually matches nothing; the picker names the variable
33320
+ * in its empty-list text instead of showing an unexplained empty list.
33321
+ */
33322
+ function unresolvedFilterVariables(filters) {
33323
+ const names = new Set();
33324
+ for (const filter of filters ?? []) {
33325
+ const value = typeof filter?.comparisonValue === 'string' ? filter.comparisonValue : '';
33326
+ for (const match of value.matchAll(/\$\{([^}]+)\}|\$([a-zA-Z_]\w*)/g)) {
33327
+ names.add(match[1] ?? match[2]);
33328
+ }
33329
+ }
33330
+ return [...names];
33331
+ }
33332
+ /**
33333
+ * Empty-list text of a filtered picker (wording agreed with the Studio PO):
33334
+ * the search alone may have found nothing, so it names the filter as a
33335
+ * possible reason without claiming it is the reason.
33336
+ */
33337
+ function filteredNoEntitiesMessage(label, unresolvedVariables = []) {
33338
+ const name = label || 'entity';
33339
+ const base = `No ${name} found. This board only lists ${name} that match its filter.`;
33340
+ if (unresolvedVariables.length === 0) {
33341
+ return base;
33342
+ }
33343
+ const vars = unresolvedVariables.map(v => `$${v}`).join(', ');
33344
+ return `${base} The filter uses ${vars}, which has no value yet.`;
33345
+ }
33346
+ /** Title of the picker's search dialog when a filter is set. */
33347
+ function filteredDialogTitle(label) {
33348
+ return `Select ${label} (filtered)`;
33349
+ }
33350
+
33351
+ /** Key of the "Other role…" entry: free-text role and direction. */
33352
+ const OTHER_ASSOCIATION = '__other__';
33353
+ /**
33354
+ * Hop list of an entity selector's child scope (AB#7723; wireframe agreed by
33355
+ * Freya, AB#7723 comment 6625315). Each hop picks an association of its source
33356
+ * type from the CK metadata (role + direction + base target in one entry, or
33357
+ * "Other role…" for free text) and a target type restricted to types derived
33358
+ * from the association's target. The source type of hop 1 is the selector's
33359
+ * CK type, of every later hop the previous hop's target type. Hops can be
33360
+ * added (at most {@link MAX_SCOPE_HOPS}), removed and moved up/down.
33361
+ *
33362
+ * Two-way bindable; emits the hops as configured (validation is the host's,
33363
+ * see `scopeHopsValidationError`):
33364
+ * ```html
33365
+ * <mm-entity-selector-scope-path-editor
33366
+ * [sourceCkTypeId]="ckTypeId"
33367
+ * [(hops)]="hops">
33368
+ * </mm-entity-selector-scope-path-editor>
33369
+ * ```
33370
+ */
33371
+ class EntitySelectorScopePathEditorComponent {
33372
+ rolesGQL = inject(GetCkTypeAssociationRolesDtoGQL);
33373
+ cdr = inject(ChangeDetectorRef);
33374
+ hopsChange = new EventEmitter();
33375
+ hopCards;
33376
+ rows = [];
33377
+ plusIcon = plusIcon;
33378
+ arrowUpIcon = arrowUpIcon;
33379
+ arrowDownIcon = arrowDownIcon;
33380
+ trashIcon = MM_ACTION_ICONS.delete;
33381
+ _sourceCkTypeId = '';
33382
+ lastEmitted = null;
33383
+ associations = new Map();
33384
+ /** CK type of the picked entity — the source type of hop 1. */
33385
+ set sourceCkTypeId(value) {
33386
+ this._sourceCkTypeId = value ?? '';
33387
+ this.syncAssociationKeys();
33388
+ }
33389
+ get sourceCkTypeId() {
33390
+ return this._sourceCkTypeId;
33391
+ }
33392
+ /** The configured hops (one-hop form = one hop). */
33393
+ set hops(value) {
33394
+ // Our own emission coming back through the two-way binding: keep the rows
33395
+ // (free-text mode) as they are.
33396
+ if (value && value === this.lastEmitted)
33397
+ return;
33398
+ this.rows = (value ?? []).map(h => ({
33399
+ roleId: h.roleId ?? '',
33400
+ direction: h.direction ?? 'in',
33401
+ targetCkTypeId: h.targetCkTypeId ?? '',
33402
+ targetItem: h.targetCkTypeId ? ckTypeItem(h.targetCkTypeId) : null,
33403
+ associationKey: ''
33404
+ }));
33405
+ this.syncAssociationKeys();
33406
+ }
33407
+ /** Maximum hop count (getter: relative imports must not initialise fields, see CLAUDE.md). */
33408
+ get maxHops() {
33409
+ return MAX_SCOPE_HOPS;
33410
+ }
33411
+ get canAddHop() {
33412
+ return this.rows.length < MAX_SCOPE_HOPS;
33413
+ }
33414
+ /** "Test/Site → Test/System → Test/Device". */
33415
+ get pathLabel() {
33416
+ return scopePathLabel(this.sourceCkTypeId || '?', this.rows);
33417
+ }
33418
+ /** True when the association metadata of any shown source type could not be loaded. */
33419
+ get metadataFailed() {
33420
+ return this.rows.some((_, i) => this.loadOf(i)?.state === 'failed');
33421
+ }
33422
+ /** Source type of hop `index`. */
33423
+ sourceTypeOf(index) {
33424
+ return index === 0 ? this.sourceCkTypeId : this.rows[index - 1]?.targetCkTypeId ?? '';
33425
+ }
33426
+ /** Dropdown entries of hop `index`: the source type's associations plus "Other role…". */
33427
+ associationOptions(index) {
33428
+ const load = this.loadOf(index);
33429
+ const options = load?.state === 'loaded' ? load.options : [];
33430
+ return [...options, {
33431
+ key: OTHER_ASSOCIATION,
33432
+ roleId: '',
33433
+ direction: 'in',
33434
+ baseTargetCkTypeId: '',
33435
+ label: 'Other role…'
33436
+ }];
33437
+ }
33438
+ /** True when hop `index` offers the association dropdown (metadata loaded with ≥1 association). */
33439
+ hasAssociationList(index) {
33440
+ const load = this.loadOf(index);
33441
+ return load?.state === 'loaded' && load.options.length > 0;
33442
+ }
33443
+ /** True when hop `index` shows the free-text role and direction fields. */
33444
+ isFreeText(index) {
33445
+ return !this.hasAssociationList(index) || this.rows[index].associationKey === OTHER_ASSOCIATION;
33446
+ }
33447
+ /** Base type the target-type picker of hop `index` is restricted to (undefined = any type). */
33448
+ baseTargetOf(index) {
33449
+ const row = this.rows[index];
33450
+ const option = this.associationOptions(index).find(o => o.key === row.associationKey && o.key !== OTHER_ASSOCIATION);
33451
+ return option?.baseTargetCkTypeId || undefined;
33452
+ }
33453
+ /**
33454
+ * Warning of hop `index` when its role is not an association of its (new)
33455
+ * source type — after a reorder, a removal or a changed earlier target. The
33456
+ * values are kept; it does not block saving.
33457
+ */
33458
+ associationWarning(index) {
33459
+ const row = this.rows[index];
33460
+ if (!row.roleId.trim() || !this.hasAssociationList(index))
33461
+ return '';
33462
+ const load = this.loadOf(index);
33463
+ const defined = load?.state === 'loaded'
33464
+ && load.options.some(o => o.roleId === row.roleId.trim() && o.direction === row.direction);
33465
+ return defined ? '' : `Association ${row.roleId.trim()} is not defined on ${this.sourceTypeOf(index) || 'the source type'}.`;
33466
+ }
33467
+ /** Validation message of hop `index`: an incomplete hop blocks saving (see `scopeHopsValidationError`). */
33468
+ hopError(index) {
33469
+ const row = this.rows[index];
33470
+ return !row.roleId.trim() || !row.targetCkTypeId.trim()
33471
+ ? `Hop ${index + 1} needs an association and a target type.`
33472
+ : '';
33473
+ }
33474
+ isRoleInvalid(index) {
33475
+ return !this.rows[index].roleId.trim();
33476
+ }
33477
+ isTargetInvalid(index) {
33478
+ return !this.rows[index].targetCkTypeId.trim();
33479
+ }
33480
+ addHop() {
33481
+ if (!this.canAddHop)
33482
+ return;
33483
+ this.rows = [...this.rows, {
33484
+ roleId: 'System/ParentChild',
33485
+ direction: 'in',
33486
+ targetCkTypeId: '',
33487
+ targetItem: null,
33488
+ associationKey: ''
33489
+ }];
33490
+ this.syncAssociationKeys();
33491
+ this.emit();
33492
+ }
33493
+ removeHop(index) {
33494
+ this.rows = this.rows.filter((_, i) => i !== index);
33495
+ this.syncAssociationKeys();
33496
+ this.emit();
33497
+ }
33498
+ moveHop(index, delta) {
33499
+ const target = index + delta;
33500
+ if (target < 0 || target >= this.rows.length)
33501
+ return;
33502
+ const rows = [...this.rows];
33503
+ [rows[index], rows[target]] = [rows[target], rows[index]];
33504
+ this.rows = rows;
33505
+ this.syncAssociationKeys();
33506
+ this.emit();
33507
+ this.focusHop(target);
33508
+ }
33509
+ onAssociationChanged(index, key) {
33510
+ const row = this.rows[index];
33511
+ row.associationKey = key;
33512
+ const option = this.associationOptions(index).find(o => o.key === key);
33513
+ if (option && key !== OTHER_ASSOCIATION) {
33514
+ row.roleId = option.roleId;
33515
+ row.direction = option.direction;
33516
+ }
33517
+ this.emit();
33518
+ }
33519
+ onRoleChanged(index, roleId) {
33520
+ this.rows[index].roleId = roleId;
33521
+ this.emit();
33522
+ }
33523
+ onDirectionChanged(index, direction) {
33524
+ this.rows[index].direction = direction;
33525
+ this.emit();
33526
+ }
33527
+ onTargetSelected(index, ckType) {
33528
+ const row = this.rows[index];
33529
+ row.targetCkTypeId = ckType.rtCkTypeId;
33530
+ row.targetItem = ckType;
33531
+ // Later hops keep their values; their source type changed, so re-match them.
33532
+ this.syncAssociationKeys();
33533
+ this.emit();
33534
+ }
33535
+ onTargetCleared(index) {
33536
+ const row = this.rows[index];
33537
+ row.targetCkTypeId = '';
33538
+ row.targetItem = null;
33539
+ this.syncAssociationKeys();
33540
+ this.emit();
33541
+ }
33542
+ emit() {
33543
+ this.lastEmitted = this.rows.map(r => ({ roleId: r.roleId, direction: r.direction, targetCkTypeId: r.targetCkTypeId }));
33544
+ this.hopsChange.emit(this.lastEmitted);
33545
+ }
33546
+ loadOf(index) {
33547
+ const type = this.sourceTypeOf(index);
33548
+ return type ? this.associations.get(type) : undefined;
33549
+ }
33550
+ /**
33551
+ * Matches every hop's role/direction to an entry of its source type's
33552
+ * associations (loading missing metadata); a hop whose association is not
33553
+ * listed switches to "Other role…" with its values kept.
33554
+ */
33555
+ syncAssociationKeys() {
33556
+ this.rows.forEach((row, index) => {
33557
+ const type = this.sourceTypeOf(index);
33558
+ if (type && !this.associations.has(type)) {
33559
+ void this.loadAssociations(type);
33560
+ }
33561
+ const load = this.loadOf(index);
33562
+ if (load?.state !== 'loaded' || load.options.length === 0) {
33563
+ row.associationKey = '';
33564
+ return;
33565
+ }
33566
+ const match = load.options.find(o => o.roleId === row.roleId.trim() && o.direction === row.direction);
33567
+ row.associationKey = match ? match.key : (row.roleId.trim() ? OTHER_ASSOCIATION : '');
33568
+ });
33569
+ }
33570
+ async loadAssociations(ckTypeId) {
33571
+ this.associations.set(ckTypeId, { state: 'loading' });
33572
+ try {
33573
+ const result = await firstValueFrom(this.rolesGQL.fetch({ variables: { ckTypeId } }));
33574
+ const ckType = result.data?.constructionKit?.types?.items?.[0];
33575
+ const options = [];
33576
+ const add = (direction, all) => {
33577
+ for (const assoc of all ?? []) {
33578
+ const roleId = assoc?.rtRoleId ? String(assoc.rtRoleId) : '';
33579
+ if (!roleId)
33580
+ continue;
33581
+ const base = assoc?.rtTargetCkTypeId ? String(assoc.rtTargetCkTypeId) : '';
33582
+ const key = `${direction}|${roleId}|${base}`;
33583
+ if (options.some(o => o.key === key))
33584
+ continue;
33585
+ options.push({
33586
+ key,
33587
+ roleId,
33588
+ direction,
33589
+ baseTargetCkTypeId: base,
33590
+ label: `${roleId} · ${direction === 'in' ? 'inbound' : 'outbound'}${base ? ` · ${base}` : ''}`
33591
+ });
33592
+ }
33593
+ };
33594
+ add('in', ckType?.associations?.in?.all);
33595
+ add('out', ckType?.associations?.out?.all);
33596
+ this.associations.set(ckTypeId, { state: 'loaded', options });
33597
+ }
33598
+ catch (error) {
33599
+ console.warn(`Could not load the associations of ${ckTypeId}:`, error);
33600
+ this.associations.set(ckTypeId, { state: 'failed' });
33601
+ }
33602
+ this.syncAssociationKeys();
33603
+ this.cdr.markForCheck();
33604
+ }
33605
+ /** Keeps the keyboard focus on a moved hop (wireframe change 4). */
33606
+ focusHop(index) {
33607
+ setTimeout(() => {
33608
+ const card = this.hopCards?.get(index)?.nativeElement;
33609
+ const button = card?.querySelector(`[data-hop-action="${index > 0 ? 'up' : 'down'}"]`);
33610
+ (button ?? card)?.focus();
33611
+ });
33612
+ }
33613
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: EntitySelectorScopePathEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
33614
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: EntitySelectorScopePathEditorComponent, isStandalone: true, selector: "mm-entity-selector-scope-path-editor", inputs: { sourceCkTypeId: "sourceCkTypeId", hops: "hops" }, outputs: { hopsChange: "hopsChange" }, viewQueries: [{ propertyName: "hopCards", predicate: ["hopCard"], descendants: true }], ngImport: i0, template: "<div class=\"scope-path-editor\">\n <span class=\"field-hint\">\n Optional. Path of associations from the picked entity to the entities whose\n rtIds scope bound stream-data widgets (up to {{ maxHops }} hops).\n </span>\n\n @if (rows.length === 0) {\n <p class=\"scope-empty\" data-test=\"scope-empty\">No child scope: widgets use the picked entity's own rtId.</p>\n } @else {\n <span class=\"scope-from\">From: <code>{{ sourceCkTypeId || '?' }}</code> (picked entity)</span>\n @if (metadataFailed) {\n <span class=\"field-hint scope-metadata-failed\" data-test=\"metadata-failed\">\n Associations could not be loaded; enter the role manually.\n </span>\n }\n\n @for (row of rows; track $index; let i = $index; let first = $first; let last = $last) {\n <div class=\"hop-card\" #hopCard tabindex=\"-1\" [attr.data-test]=\"'hop-' + (i + 1)\">\n <div class=\"hop-header\">\n <span class=\"hop-title\">\n Hop {{ i + 1 }}\n @if (i > 0) {\n <span class=\"hop-source\">(from <code>{{ sourceTypeOf(i) || '?' }}</code>)</span>\n }\n </span>\n <span class=\"hop-actions\">\n @if (!first) {\n <button kendoButton fillMode=\"flat\" size=\"small\" [svgIcon]=\"arrowUpIcon\"\n data-hop-action=\"up\"\n [attr.aria-label]=\"'Move hop ' + (i + 1) + ' up'\" [title]=\"'Move hop ' + (i + 1) + ' up'\"\n (click)=\"moveHop(i, -1)\"></button>\n }\n @if (!last) {\n <button kendoButton fillMode=\"flat\" size=\"small\" [svgIcon]=\"arrowDownIcon\"\n data-hop-action=\"down\"\n [attr.aria-label]=\"'Move hop ' + (i + 1) + ' down'\" [title]=\"'Move hop ' + (i + 1) + ' down'\"\n (click)=\"moveHop(i, 1)\"></button>\n }\n <button kendoButton fillMode=\"flat\" size=\"small\" [svgIcon]=\"trashIcon\" class=\"hop-remove\"\n data-hop-action=\"remove\"\n [attr.aria-label]=\"'Remove hop ' + (i + 1)\" [title]=\"'Remove hop ' + (i + 1)\"\n (click)=\"removeHop(i)\"></button>\n </span>\n </div>\n\n <div class=\"hop-field\">\n <label class=\"field-label\" [attr.for]=\"'hop-assoc-' + i\">Association</label>\n @if (hasAssociationList(i)) {\n <kendo-dropdownlist\n [id]=\"'hop-assoc-' + i\"\n [data]=\"associationOptions(i)\"\n textField=\"label\"\n valueField=\"key\"\n [valuePrimitive]=\"true\"\n [value]=\"row.associationKey || null\"\n [defaultItem]=\"{ key: '', label: 'Select an association\u2026' }\"\n [class.k-invalid]=\"isRoleInvalid(i)\"\n (valueChange)=\"onAssociationChanged(i, $event)\">\n <ng-template kendoDropDownListValueTemplate let-item>\n <span class=\"assoc-text\" [title]=\"item?.label ?? ''\">{{ item?.label }}</span>\n </ng-template>\n <ng-template kendoDropDownListItemTemplate let-item>\n <span class=\"assoc-text\" [title]=\"item.label\">{{ item.label }}</span>\n </ng-template>\n </kendo-dropdownlist>\n }\n @if (isFreeText(i)) {\n <div class=\"hop-free-text\">\n <kendo-textbox\n [id]=\"hasAssociationList(i) ? null : 'hop-assoc-' + i\"\n [value]=\"row.roleId\"\n placeholder=\"Association role (e.g. System/ParentChild)\"\n [class.k-invalid]=\"isRoleInvalid(i)\"\n (valueChange)=\"onRoleChanged(i, $event)\">\n </kendo-textbox>\n <select class=\"hop-direction\" [attr.aria-label]=\"'Direction of hop ' + (i + 1)\"\n [ngModel]=\"row.direction\" (ngModelChange)=\"onDirectionChanged(i, $event)\">\n <option value=\"in\">Inbound</option>\n <option value=\"out\">Outbound</option>\n </select>\n </div>\n }\n @if (associationWarning(i); as warning) {\n <span class=\"field-warning\" data-test=\"association-warning\">{{ warning }}</span>\n }\n </div>\n\n <div class=\"hop-field\">\n <label class=\"field-label\">Target type</label>\n <mm-ck-type-selector-input\n [ngModel]=\"row.targetItem\"\n [derivedFromRtCkTypeId]=\"baseTargetOf(i)\"\n [placeholder]=\"'Target CK type\u2026'\"\n [class.k-invalid]=\"isTargetInvalid(i)\"\n (ckTypeSelected)=\"onTargetSelected(i, $event)\"\n (ckTypeCleared)=\"onTargetCleared(i)\">\n </mm-ck-type-selector-input>\n </div>\n\n @if (hopError(i); as error) {\n <span class=\"field-error\" data-test=\"hop-error\">{{ error }}</span>\n }\n </div>\n }\n }\n\n <div class=\"scope-footer\">\n <button kendoButton fillMode=\"flat\" [svgIcon]=\"plusIcon\" [disabled]=\"!canAddHop\"\n data-test=\"add-hop\" (click)=\"addHop()\">Add hop</button>\n @if (!canAddHop) {\n <span class=\"field-hint\">At most {{ maxHops }} hops.</span>\n }\n </div>\n\n @if (rows.length > 0) {\n <span class=\"scope-path\" data-test=\"scope-path\">Path: {{ pathLabel }}</span>\n }\n</div>\n", styles: [".scope-path-editor{display:flex;flex-direction:column;gap:.5rem;min-width:0}.field-label{font-weight:500;font-size:.875rem;color:var(--kendo-color-on-app-surface, #424242)}.field-hint,.scope-from,.scope-empty{font-size:.75rem;color:var(--kendo-color-subtle, #757575);margin:0}.field-error{font-size:.75rem;color:var(--kendo-color-error, #f44336)}.field-warning{font-size:.75rem;color:var(--kendo-color-warning, #b26a00)}code{font-family:var(--theme-font-mono, monospace)}.hop-card{display:flex;flex-direction:column;gap:.5rem;padding:.5rem .75rem .75rem;border:1px solid var(--kendo-color-border, #e0e0e0);border-radius:4px;background:var(--kendo-color-surface-alt, #fafafa);min-width:0}.hop-header{display:flex;align-items:center;justify-content:space-between;gap:.5rem}.hop-title{font-weight:500;font-size:.875rem;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.hop-source{font-weight:400;color:var(--kendo-color-subtle, #757575)}.hop-actions{display:flex;flex-wrap:nowrap;flex-shrink:0}.hop-remove{color:var(--kendo-color-error, #f44336)}.hop-field{display:flex;flex-direction:column;gap:.25rem;min-width:0}.hop-field kendo-dropdownlist{width:100%;min-width:0}.assoc-text{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.hop-free-text{display:flex;gap:.5rem;min-width:0}.hop-free-text kendo-textbox{flex:1 1 auto;min-width:0}.hop-direction{flex:0 0 auto}.scope-footer{display:flex;align-items:center;gap:.5rem}.scope-path{font-size:.75rem;font-family:var(--theme-font-mono, monospace);color:var(--kendo-color-on-app-surface, #424242);overflow-wrap:break-word}\n"], dependencies: [{ 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.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: DropDownsModule }, { kind: "directive", type: i4.ItemTemplateDirective, selector: "[kendoDropDownListItemTemplate],[kendoComboBoxItemTemplate],[kendoAutoCompleteItemTemplate],[kendoMultiSelectItemTemplate]" }, { kind: "component", type: i4.DropDownListComponent, selector: "kendo-dropdownlist", inputs: ["customIconClass", "showStickyHeader", "icon", "svgIcon", "loading", "data", "value", "textField", "valueField", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "defaultItem", "disabled", "itemDisabled", "readonly", "filterable", "virtual", "ignoreCase", "delay", "valuePrimitive", "tabindex", "tabIndex", "size", "rounded", "fillMode", "leftRightArrowsNavigation", "id"], outputs: ["valueChange", "filterChange", "selectionChange", "open", "opened", "close", "closed", "focus", "blur"], exportAs: ["kendoDropDownList"] }, { kind: "directive", type: i4.ValueTemplateDirective, selector: "[kendoDropDownListValueTemplate],[kendoDropDownTreeValueTemplate]" }, { kind: "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: CkTypeSelectorInputComponent, selector: "mm-ck-type-selector-input", inputs: ["placeholder", "minSearchLength", "maxResults", "debounceMs", "ckModelIds", "allowAbstract", "dialogTitle", "advancedSearchLabel", "derivedFromRtCkTypeId", "messages", "dialogMessages", "disabled", "required"], outputs: ["ckTypeSelected", "ckTypeCleared"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
33615
+ }
33616
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: EntitySelectorScopePathEditorComponent, decorators: [{
33617
+ type: Component,
33618
+ args: [{ selector: 'mm-entity-selector-scope-path-editor', standalone: true, imports: [FormsModule, ButtonModule, DropDownsModule, InputsModule, CkTypeSelectorInputComponent], changeDetection: ChangeDetectionStrategy.Eager, template: "<div class=\"scope-path-editor\">\n <span class=\"field-hint\">\n Optional. Path of associations from the picked entity to the entities whose\n rtIds scope bound stream-data widgets (up to {{ maxHops }} hops).\n </span>\n\n @if (rows.length === 0) {\n <p class=\"scope-empty\" data-test=\"scope-empty\">No child scope: widgets use the picked entity's own rtId.</p>\n } @else {\n <span class=\"scope-from\">From: <code>{{ sourceCkTypeId || '?' }}</code> (picked entity)</span>\n @if (metadataFailed) {\n <span class=\"field-hint scope-metadata-failed\" data-test=\"metadata-failed\">\n Associations could not be loaded; enter the role manually.\n </span>\n }\n\n @for (row of rows; track $index; let i = $index; let first = $first; let last = $last) {\n <div class=\"hop-card\" #hopCard tabindex=\"-1\" [attr.data-test]=\"'hop-' + (i + 1)\">\n <div class=\"hop-header\">\n <span class=\"hop-title\">\n Hop {{ i + 1 }}\n @if (i > 0) {\n <span class=\"hop-source\">(from <code>{{ sourceTypeOf(i) || '?' }}</code>)</span>\n }\n </span>\n <span class=\"hop-actions\">\n @if (!first) {\n <button kendoButton fillMode=\"flat\" size=\"small\" [svgIcon]=\"arrowUpIcon\"\n data-hop-action=\"up\"\n [attr.aria-label]=\"'Move hop ' + (i + 1) + ' up'\" [title]=\"'Move hop ' + (i + 1) + ' up'\"\n (click)=\"moveHop(i, -1)\"></button>\n }\n @if (!last) {\n <button kendoButton fillMode=\"flat\" size=\"small\" [svgIcon]=\"arrowDownIcon\"\n data-hop-action=\"down\"\n [attr.aria-label]=\"'Move hop ' + (i + 1) + ' down'\" [title]=\"'Move hop ' + (i + 1) + ' down'\"\n (click)=\"moveHop(i, 1)\"></button>\n }\n <button kendoButton fillMode=\"flat\" size=\"small\" [svgIcon]=\"trashIcon\" class=\"hop-remove\"\n data-hop-action=\"remove\"\n [attr.aria-label]=\"'Remove hop ' + (i + 1)\" [title]=\"'Remove hop ' + (i + 1)\"\n (click)=\"removeHop(i)\"></button>\n </span>\n </div>\n\n <div class=\"hop-field\">\n <label class=\"field-label\" [attr.for]=\"'hop-assoc-' + i\">Association</label>\n @if (hasAssociationList(i)) {\n <kendo-dropdownlist\n [id]=\"'hop-assoc-' + i\"\n [data]=\"associationOptions(i)\"\n textField=\"label\"\n valueField=\"key\"\n [valuePrimitive]=\"true\"\n [value]=\"row.associationKey || null\"\n [defaultItem]=\"{ key: '', label: 'Select an association\u2026' }\"\n [class.k-invalid]=\"isRoleInvalid(i)\"\n (valueChange)=\"onAssociationChanged(i, $event)\">\n <ng-template kendoDropDownListValueTemplate let-item>\n <span class=\"assoc-text\" [title]=\"item?.label ?? ''\">{{ item?.label }}</span>\n </ng-template>\n <ng-template kendoDropDownListItemTemplate let-item>\n <span class=\"assoc-text\" [title]=\"item.label\">{{ item.label }}</span>\n </ng-template>\n </kendo-dropdownlist>\n }\n @if (isFreeText(i)) {\n <div class=\"hop-free-text\">\n <kendo-textbox\n [id]=\"hasAssociationList(i) ? null : 'hop-assoc-' + i\"\n [value]=\"row.roleId\"\n placeholder=\"Association role (e.g. System/ParentChild)\"\n [class.k-invalid]=\"isRoleInvalid(i)\"\n (valueChange)=\"onRoleChanged(i, $event)\">\n </kendo-textbox>\n <select class=\"hop-direction\" [attr.aria-label]=\"'Direction of hop ' + (i + 1)\"\n [ngModel]=\"row.direction\" (ngModelChange)=\"onDirectionChanged(i, $event)\">\n <option value=\"in\">Inbound</option>\n <option value=\"out\">Outbound</option>\n </select>\n </div>\n }\n @if (associationWarning(i); as warning) {\n <span class=\"field-warning\" data-test=\"association-warning\">{{ warning }}</span>\n }\n </div>\n\n <div class=\"hop-field\">\n <label class=\"field-label\">Target type</label>\n <mm-ck-type-selector-input\n [ngModel]=\"row.targetItem\"\n [derivedFromRtCkTypeId]=\"baseTargetOf(i)\"\n [placeholder]=\"'Target CK type\u2026'\"\n [class.k-invalid]=\"isTargetInvalid(i)\"\n (ckTypeSelected)=\"onTargetSelected(i, $event)\"\n (ckTypeCleared)=\"onTargetCleared(i)\">\n </mm-ck-type-selector-input>\n </div>\n\n @if (hopError(i); as error) {\n <span class=\"field-error\" data-test=\"hop-error\">{{ error }}</span>\n }\n </div>\n }\n }\n\n <div class=\"scope-footer\">\n <button kendoButton fillMode=\"flat\" [svgIcon]=\"plusIcon\" [disabled]=\"!canAddHop\"\n data-test=\"add-hop\" (click)=\"addHop()\">Add hop</button>\n @if (!canAddHop) {\n <span class=\"field-hint\">At most {{ maxHops }} hops.</span>\n }\n </div>\n\n @if (rows.length > 0) {\n <span class=\"scope-path\" data-test=\"scope-path\">Path: {{ pathLabel }}</span>\n }\n</div>\n", styles: [".scope-path-editor{display:flex;flex-direction:column;gap:.5rem;min-width:0}.field-label{font-weight:500;font-size:.875rem;color:var(--kendo-color-on-app-surface, #424242)}.field-hint,.scope-from,.scope-empty{font-size:.75rem;color:var(--kendo-color-subtle, #757575);margin:0}.field-error{font-size:.75rem;color:var(--kendo-color-error, #f44336)}.field-warning{font-size:.75rem;color:var(--kendo-color-warning, #b26a00)}code{font-family:var(--theme-font-mono, monospace)}.hop-card{display:flex;flex-direction:column;gap:.5rem;padding:.5rem .75rem .75rem;border:1px solid var(--kendo-color-border, #e0e0e0);border-radius:4px;background:var(--kendo-color-surface-alt, #fafafa);min-width:0}.hop-header{display:flex;align-items:center;justify-content:space-between;gap:.5rem}.hop-title{font-weight:500;font-size:.875rem;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.hop-source{font-weight:400;color:var(--kendo-color-subtle, #757575)}.hop-actions{display:flex;flex-wrap:nowrap;flex-shrink:0}.hop-remove{color:var(--kendo-color-error, #f44336)}.hop-field{display:flex;flex-direction:column;gap:.25rem;min-width:0}.hop-field kendo-dropdownlist{width:100%;min-width:0}.assoc-text{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.hop-free-text{display:flex;gap:.5rem;min-width:0}.hop-free-text kendo-textbox{flex:1 1 auto;min-width:0}.hop-direction{flex:0 0 auto}.scope-footer{display:flex;align-items:center;gap:.5rem}.scope-path{font-size:.75rem;font-family:var(--theme-font-mono, monospace);color:var(--kendo-color-on-app-surface, #424242);overflow-wrap:break-word}\n"] }]
33619
+ }], propDecorators: { hopsChange: [{
33620
+ type: Output
33621
+ }], hopCards: [{
33622
+ type: ViewChildren,
33623
+ args: ['hopCard']
33624
+ }], sourceCkTypeId: [{
33625
+ type: Input
33626
+ }], hops: [{
33627
+ type: Input
33628
+ }] } });
33629
+ function ckTypeItem(ckTypeId) {
33630
+ return { fullName: ckTypeId, rtCkTypeId: ckTypeId, isAbstract: false, isFinal: false };
33631
+ }
33632
+
32429
33633
  /**
32430
33634
  * Component for editing entity selector configurations in MeshBoard settings.
32431
33635
  * Allows adding, editing, and removing entity selectors.
@@ -32437,9 +33641,16 @@ class EntitySelectorEditorComponent {
32437
33641
  variableService = inject(MeshBoardVariableService);
32438
33642
  getEntitiesByCkTypeGQL = inject(GetEntitiesByCkTypeDtoGQL);
32439
33643
  confirmationService = inject(ConfirmationService, { optional: true });
33644
+ cdr = inject(ChangeDetectorRef);
33645
+ destroyRef = inject(DestroyRef);
32440
33646
  entitySelectors = [];
32441
33647
  /** Existing variable names from other sources (static variables, other selectors) for duplicate detection */
32442
33648
  existingVariableNames = [];
33649
+ /**
33650
+ * The board's static variables (AB#7724): offered in the picker filter and
33651
+ * substituted when the editor lists default entities and checks the default.
33652
+ */
33653
+ variables = [];
32443
33654
  entitySelectorsChange = new EventEmitter();
32444
33655
  /** Emits true when entering edit mode, false when leaving — allows parent to hide its own buttons */
32445
33656
  editingStateChange = new EventEmitter();
@@ -32456,22 +33667,32 @@ class EntitySelectorEditorComponent {
32456
33667
  editMappings = [];
32457
33668
  editShowInToolbar = true;
32458
33669
  editDefaultRtId = '';
32459
- // Child scope (stream-data): optional one-hop traversal resolving the picked
32460
- // entity to the child source rtIds a stream-data widget scopes by.
32461
- editChildScopeTargetCkTypeId = '';
32462
- editChildScopeTargetCkTypeItem = null;
32463
- editChildScopeRoleId = '';
32464
- // Parent → children via System/ParentChild is reached INBOUND (the child holds
32465
- // the association to its parent), so 'in' is the correct default for a
32466
- // childScope that resolves a picked parent to its children.
32467
- editChildScopeDirection = 'in';
33670
+ // Child scope (stream-data, AB#7723): the hops from the picked entity to the
33671
+ // entities whose rtIds scope bound widgets. Empty = the picked entity itself.
33672
+ // One hop saves in the original top-level form, two or more as `path`.
33673
+ editScopeHops = [];
32468
33674
  // Leader/follower (AB#7722): id of the selector this one follows ('' = none).
32469
33675
  editFollowsSelectorId = '';
32470
33676
  /** Why choosing the leader changed the form (CK type fixed, mappings/default cleared). */
32471
33677
  followNotice = '';
33678
+ // Picker filter (AB#7724): attribute rows on the CK type, AND-combined.
33679
+ editFilters = [];
33680
+ /** Filter rows a follower carries from hand-editing; ignored and dropped on save. */
33681
+ followerFilterCount = 0;
33682
+ /** Result of the server check whether the default entity matches the filter. */
33683
+ defaultFilterCheck = { state: 'none' };
33684
+ defaultCheckRequests = new Subject();
32472
33685
  // Data sources for the default entity picker (created when CK type is set)
32473
33686
  defaultEntityDataSource = null;
32474
33687
  defaultEntityDialogDataSource = null;
33688
+ constructor() {
33689
+ // Debounced so typing in the filter grid does not fire a request per key;
33690
+ // switchMap drops the answer of an outdated check.
33691
+ this.defaultCheckRequests.pipe(debounceTime(DEFAULT_CHECK_DEBOUNCE_MS), switchMap(request => 'state' in request ? of(request) : this.checkDefaultEntity(request)), takeUntilDestroyed(this.destroyRef)).subscribe(check => {
33692
+ this.defaultFilterCheck = check;
33693
+ this.cdr.markForCheck();
33694
+ });
33695
+ }
32475
33696
  /**
32476
33697
  * Starts adding a new entity selector.
32477
33698
  */
@@ -32484,12 +33705,12 @@ class EntitySelectorEditorComponent {
32484
33705
  this.editMappings = [];
32485
33706
  this.editShowInToolbar = true;
32486
33707
  this.editDefaultRtId = '';
32487
- this.editChildScopeTargetCkTypeId = '';
32488
- this.editChildScopeTargetCkTypeItem = null;
32489
- this.editChildScopeRoleId = 'System/ParentChild';
32490
- this.editChildScopeDirection = 'in';
33708
+ this.editScopeHops = [];
32491
33709
  this.editFollowsSelectorId = '';
32492
33710
  this.followNotice = '';
33711
+ this.editFilters = [];
33712
+ this.followerFilterCount = 0;
33713
+ this.defaultFilterCheck = { state: 'none' };
32493
33714
  this.defaultEntityDataSource = null;
32494
33715
  this.defaultEntityDialogDataSource = null;
32495
33716
  this.isEditing = true;
@@ -32514,19 +33735,17 @@ class EntitySelectorEditorComponent {
32514
33735
  this.editMappings = [...selector.attributeMappings];
32515
33736
  this.editShowInToolbar = selector.showInToolbar !== false;
32516
33737
  this.editDefaultRtId = selector.defaultRtId ?? '';
32517
- this.editChildScopeTargetCkTypeId = selector.childScope?.targetCkTypeId ?? '';
32518
- this.editChildScopeTargetCkTypeItem = selector.childScope?.targetCkTypeId
32519
- ? {
32520
- fullName: selector.childScope.targetCkTypeId,
32521
- rtCkTypeId: selector.childScope.targetCkTypeId,
32522
- isAbstract: false,
32523
- isFinal: false
32524
- }
32525
- : null;
32526
- this.editChildScopeRoleId = selector.childScope?.roleId ?? 'System/ParentChild';
32527
- this.editChildScopeDirection = selector.childScope?.direction ?? 'in';
33738
+ this.editScopeHops = scopeHopsOf(selector.childScope);
32528
33739
  this.editFollowsSelectorId = selector.followsSelectorId ?? '';
32529
33740
  this.followNotice = '';
33741
+ this.editFilters = (selector.filters ?? []).map((f, i) => ({
33742
+ id: i + 1,
33743
+ attributePath: f.attributePath,
33744
+ operator: f.operator,
33745
+ comparisonValue: f.comparisonValue
33746
+ }));
33747
+ this.followerFilterCount = selector.followsSelectorId ? this.editFilters.length : 0;
33748
+ this.defaultFilterCheck = { state: 'none' };
32530
33749
  // A follower picks from its leader's CK type; re-sync it in case the leader's
32531
33750
  // type changed after the follower was saved.
32532
33751
  const leader = resolveFollowLeader(this.entitySelectors, selector);
@@ -32534,6 +33753,7 @@ class EntitySelectorEditorComponent {
32534
33753
  this.applyLeaderCkType(leader.leader.ckTypeId);
32535
33754
  }
32536
33755
  this.updateDefaultEntityDataSources();
33756
+ this.requestDefaultCheck();
32537
33757
  this.isEditing = true;
32538
33758
  this.editingStateChange.emit(true);
32539
33759
  }
@@ -32593,6 +33813,10 @@ class EntitySelectorEditorComponent {
32593
33813
  this.editFollowsSelectorId = leaderId;
32594
33814
  this.followNotice = '';
32595
33815
  this.editShowInToolbar = false;
33816
+ // A follower has no picker: its filter rows are ignored and dropped on save (AB#7724).
33817
+ this.followerFilterCount = leaderId ? this.editFilters.length : 0;
33818
+ this.requestDefaultCheck();
33819
+ this.updateDefaultEntityDataSources();
32596
33820
  if (!leaderId)
32597
33821
  return;
32598
33822
  const leader = this.entitySelectors.find(s => s.id === leaderId);
@@ -32615,6 +33839,8 @@ class EntitySelectorEditorComponent {
32615
33839
  this.editCkTypeItem = { fullName: ckTypeId, rtCkTypeId: ckTypeId, isAbstract: false, isFinal: false };
32616
33840
  this.editMappings = [];
32617
33841
  this.editDefaultRtId = '';
33842
+ this.editFilters = [];
33843
+ this.followerFilterCount = 0;
32618
33844
  }
32619
33845
  /**
32620
33846
  * Handles CK type selection in the editor form.
@@ -32622,10 +33848,12 @@ class EntitySelectorEditorComponent {
32622
33848
  onCkTypeSelected(ckType) {
32623
33849
  this.editCkTypeId = ckType.rtCkTypeId;
32624
33850
  this.editCkTypeItem = ckType;
32625
- // Clear mappings and default entity when CK type changes
33851
+ // Clear mappings, default entity and filter rows when CK type changes
32626
33852
  this.editMappings = [];
32627
33853
  this.editDefaultRtId = '';
33854
+ this.editFilters = [];
32628
33855
  this.updateDefaultEntityDataSources();
33856
+ this.requestDefaultCheck();
32629
33857
  }
32630
33858
  /**
32631
33859
  * Handles CK type cleared in the editor form.
@@ -32635,8 +33863,10 @@ class EntitySelectorEditorComponent {
32635
33863
  this.editCkTypeItem = null;
32636
33864
  this.editMappings = [];
32637
33865
  this.editDefaultRtId = '';
33866
+ this.editFilters = [];
32638
33867
  this.defaultEntityDataSource = null;
32639
33868
  this.defaultEntityDialogDataSource = null;
33869
+ this.requestDefaultCheck();
32640
33870
  }
32641
33871
  /**
32642
33872
  * Opens the attribute selector dialog to pick attributes for variable mappings.
@@ -32657,31 +33887,70 @@ class EntitySelectorEditorComponent {
32657
33887
  };
32658
33888
  });
32659
33889
  }
32660
- /**
32661
- * Handles child-scope target CK type selection.
32662
- */
32663
- onChildScopeCkTypeSelected(ckType) {
32664
- this.editChildScopeTargetCkTypeId = ckType.rtCkTypeId;
32665
- this.editChildScopeTargetCkTypeItem = ckType;
33890
+ /** Takes the hop list of the child-scope editor (AB#7723). */
33891
+ onScopeHopsChanged(hops) {
33892
+ this.editScopeHops = hops;
32666
33893
  }
32667
- /**
32668
- * Handles child-scope target CK type cleared.
32669
- */
32670
- onChildScopeCkTypeCleared() {
32671
- this.editChildScopeTargetCkTypeId = '';
32672
- this.editChildScopeTargetCkTypeItem = null;
33894
+ /** Validation message of the hop list, or '' when it can be saved. */
33895
+ get scopeHopsError() {
33896
+ return scopeHopsValidationError(this.editScopeHops) ?? '';
32673
33897
  }
32674
33898
  /**
32675
33899
  * Handles default entity selection from the entity picker.
32676
33900
  */
32677
33901
  onDefaultEntitySelected(entity) {
32678
33902
  this.editDefaultRtId = entity.rtId;
33903
+ this.requestDefaultCheck();
32679
33904
  }
32680
33905
  /**
32681
33906
  * Handles default entity cleared.
32682
33907
  */
32683
33908
  onDefaultEntityCleared() {
32684
33909
  this.editDefaultRtId = '';
33910
+ this.requestDefaultCheck();
33911
+ }
33912
+ /** Takes the rows of the picker filter editor (AB#7724). */
33913
+ onFiltersChange(filters) {
33914
+ this.editFilters = filters;
33915
+ this.updateDefaultEntityDataSources();
33916
+ this.requestDefaultCheck();
33917
+ }
33918
+ /**
33919
+ * Variables offered in filter values: the board's static variables and the
33920
+ * variables other selectors map. The edited selector's own mappings are
33921
+ * left out — they are filled from its own pick.
33922
+ */
33923
+ get filterVariables() {
33924
+ const result = new Map();
33925
+ for (const v of this.variables) {
33926
+ result.set(v.name, { name: v.name, label: v.label || v.name, type: v.type });
33927
+ }
33928
+ this.entitySelectors
33929
+ .filter((_, i) => i !== this.editingIndex)
33930
+ .forEach(es => es.attributeMappings.forEach(m => {
33931
+ if (!result.has(m.variableName)) {
33932
+ result.set(m.variableName, { name: m.variableName, label: `${m.variableName} (${es.label || es.id})` });
33933
+ }
33934
+ }));
33935
+ return [...result.values()];
33936
+ }
33937
+ /** "2 filters" chip of a selector in the list, or '' without filter. */
33938
+ getFilterSummary(selector) {
33939
+ const count = selector.followsSelectorId ? 0 : selector.filters?.length ?? 0;
33940
+ if (count === 0)
33941
+ return '';
33942
+ return count === 1 ? '1 filter' : `${count} filters`;
33943
+ }
33944
+ /** "$a, $b" of the variables the default-entity check cannot substitute. */
33945
+ get uncheckedVariablesText() {
33946
+ return this.defaultFilterCheck.state === 'unchecked'
33947
+ ? this.defaultFilterCheck.variables.map(v => `$${v}`).join(', ')
33948
+ : '';
33949
+ }
33950
+ /** Label of the leader the edited follower takes its pick from. */
33951
+ get leaderLabel() {
33952
+ const leader = this.entitySelectors.find(s => s.id === this.editFollowsSelectorId);
33953
+ return leader ? (leader.label || leader.id) : this.editFollowsSelectorId;
32685
33954
  }
32686
33955
  /**
32687
33956
  * Saves the current entity selector being edited.
@@ -32689,15 +33958,10 @@ class EntitySelectorEditorComponent {
32689
33958
  saveEdit() {
32690
33959
  if (!this.isEditValid())
32691
33960
  return;
32692
- // Only persist a childScope when both the target type and role are set;
32693
- // otherwise leave it undefined (the picked entity itself is the scope).
32694
- const childScope = this.editChildScopeTargetCkTypeId && this.editChildScopeRoleId.trim()
32695
- ? {
32696
- targetCkTypeId: this.editChildScopeTargetCkTypeId,
32697
- roleId: this.editChildScopeRoleId.trim(),
32698
- direction: this.editChildScopeDirection
32699
- }
32700
- : undefined;
33961
+ // No hops = no childScope (the picked entity itself is the scope); one hop
33962
+ // in the original top-level form (older library versions keep reading it);
33963
+ // two or more hops as `path` (AB#7723).
33964
+ const childScope = childScopeFromHops(this.editScopeHops);
32701
33965
  const selector = {
32702
33966
  id: this.editId,
32703
33967
  label: this.editLabel.trim(),
@@ -32706,6 +33970,8 @@ class EntitySelectorEditorComponent {
32706
33970
  showInToolbar: this.editShowInToolbar,
32707
33971
  defaultRtId: this.editDefaultRtId.trim() || undefined,
32708
33972
  childScope,
33973
+ // Picker filter (AB#7724); omitted when empty and never written for a follower.
33974
+ filters: !this.editFollowsSelectorId && this.editFilters.length > 0 ? this.toFilterConfigs(this.editFilters) : undefined,
32709
33975
  // A follower never renders a picker (AB#7722).
32710
33976
  ...(this.editFollowsSelectorId ? { showInToolbar: false, followsSelectorId: this.editFollowsSelectorId } : {})
32711
33977
  };
@@ -32749,6 +34015,10 @@ class EntitySelectorEditorComponent {
32749
34015
  if (this.followError) {
32750
34016
  return false;
32751
34017
  }
34018
+ // Every hop needs an association and a target type; at most 5 hops (AB#7723).
34019
+ if (this.scopeHopsError) {
34020
+ return false;
34021
+ }
32752
34022
  return hasRequiredFields;
32753
34023
  }
32754
34024
  /**
@@ -32827,19 +34097,74 @@ class EntitySelectorEditorComponent {
32827
34097
  }
32828
34098
  updateDefaultEntityDataSources() {
32829
34099
  if (this.editCkTypeId) {
32830
- this.defaultEntityDataSource = new RuntimeEntitySelectDataSource(this.getEntitiesByCkTypeGQL, this.editCkTypeId);
32831
- this.defaultEntityDialogDataSource = new RuntimeEntityDialogDataSource(this.getEntitiesByCkTypeGQL, this.editCkTypeId);
34100
+ // The default-entity picker lists what the toolbar picker lists (AB#7724).
34101
+ const gql = withPickerFilters(this.getEntitiesByCkTypeGQL, this.resolvedEditorFilters().filters);
34102
+ this.defaultEntityDataSource = new RuntimeEntitySelectDataSource(gql, this.editCkTypeId);
34103
+ this.defaultEntityDialogDataSource = new RuntimeEntityDialogDataSource(gql, this.editCkTypeId);
32832
34104
  }
32833
34105
  else {
32834
34106
  this.defaultEntityDataSource = null;
32835
34107
  this.defaultEntityDialogDataSource = null;
32836
34108
  }
32837
34109
  }
34110
+ toFilterConfigs(rows) {
34111
+ return rows.map(f => ({
34112
+ attributePath: f.attributePath,
34113
+ operator: f.operator,
34114
+ comparisonValue: f.comparisonValue == null ? '' : String(f.comparisonValue)
34115
+ }));
34116
+ }
34117
+ /**
34118
+ * The filter rows that apply in the editor, with the board's static
34119
+ * variables substituted. Rows whose value still names a variable without a
34120
+ * value (e.g. one a selector pick fills at view time) are left out and
34121
+ * reported in `unresolved`; the editor cannot know their value.
34122
+ */
34123
+ resolvedEditorFilters() {
34124
+ if (this.isFollower || this.editFilters.length === 0) {
34125
+ return { filters: undefined, unresolved: [] };
34126
+ }
34127
+ const converted = this.variableService.convertToFieldFilterDto(this.toFilterConfigs(this.editFilters), this.variables) ?? [];
34128
+ const unresolved = unresolvedFilterVariables(converted);
34129
+ const applicable = converted.filter(f => unresolvedFilterVariables([f]).length === 0);
34130
+ return { filters: applicable.length > 0 ? applicable : undefined, unresolved };
34131
+ }
34132
+ /** Queues the (debounced) check whether the default entity matches the filter. */
34133
+ requestDefaultCheck() {
34134
+ const rtId = this.editDefaultRtId.trim();
34135
+ if (!rtId || !this.editCkTypeId || this.isFollower || this.editFilters.length === 0) {
34136
+ this.defaultCheckRequests.next({ state: 'none' });
34137
+ return;
34138
+ }
34139
+ const { filters, unresolved } = this.resolvedEditorFilters();
34140
+ if (unresolved.length > 0) {
34141
+ this.defaultCheckRequests.next({ state: 'unchecked', variables: unresolved });
34142
+ return;
34143
+ }
34144
+ this.defaultCheckRequests.next({ ckTypeId: this.editCkTypeId, rtId, filters: filters ?? [] });
34145
+ }
34146
+ /** Asks the server whether the entity is among the filtered ones (rtId + filter, first 1). */
34147
+ checkDefaultEntity(request) {
34148
+ return this.getEntitiesByCkTypeGQL.fetch({
34149
+ variables: {
34150
+ ckTypeId: request.ckTypeId,
34151
+ attributeNames: [],
34152
+ rtId: request.rtId,
34153
+ first: 1,
34154
+ fieldFilters: request.filters
34155
+ },
34156
+ fetchPolicy: 'network-only'
34157
+ }).pipe(map$1(result => {
34158
+ const count = result.data?.runtime?.runtimeEntities?.totalCount
34159
+ ?? result.data?.runtime?.runtimeEntities?.items?.length ?? 0;
34160
+ return (count > 0 ? { state: 'match' } : { state: 'mismatch' });
34161
+ }), catchError$1(() => of({ state: 'error' })));
34162
+ }
32838
34163
  emitChange() {
32839
34164
  this.entitySelectorsChange.emit([...this.entitySelectors]);
32840
34165
  }
32841
34166
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: EntitySelectorEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
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 });
34167
+ 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", variables: "variables" }, 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 @if (getFilterSummary(selector); as filterSummary) {\n <span class=\"selector-mode selector-filter-chip\">{{ filterSummary }}</span>\n }\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 @if (editCkTypeId) {\n <div class=\"form-field picker-filter-field\">\n <label class=\"field-label\">Picker filter</label>\n @if (isFollower) {\n <span class=\"field-hint\">A follower takes the leader's pick. Set the filter on {{ leaderLabel }}.</span>\n @if (followerFilterCount > 0) {\n <span class=\"field-hint follow-notice\">Filters on a follower are ignored and are removed when you save.</span>\n }\n } @else {\n <span class=\"field-hint\">\n Only entities that match all rows are listed in the toolbar picker and in the default-entity picker below.\n </span>\n <mm-field-filter-editor\n [ckTypeId]=\"editCkTypeId\"\n [filters]=\"editFilters\"\n [enableVariables]=\"filterVariables.length > 0\"\n [availableVariables]=\"filterVariables\"\n (filtersChange)=\"onFiltersChange($event)\">\n </mm-field-filter-editor>\n }\n </div>\n }\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 @switch (defaultFilterCheck.state) {\n @case ('mismatch') {\n <span class=\"field-warning default-filter-warning\">The default entity does not match the picker filter. It still loads, but users cannot pick it again.</span>\n }\n @case ('error') {\n <span class=\"field-hint default-filter-warning\">Could not check the default entity.</span>\n }\n @case ('unchecked') {\n <span class=\"field-hint default-filter-warning\">Cannot check the default entity: the filter uses {{ uncheckedVariablesText }}, which is filled at view time.</span>\n }\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 <mm-entity-selector-scope-path-editor\n [sourceCkTypeId]=\"editCkTypeId\"\n [hops]=\"editScopeHops\"\n (hopsChange)=\"onScopeHopsChanged($event)\">\n </mm-entity-selector-scope-path-editor>\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-filter-chip{color:var(--kendo-color-subtle, #757575)}.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 .field-warning{font-size:.75rem;color:var(--kendo-color-warning, #f57c00)}.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"] }, { kind: "component", type: EntitySelectorScopePathEditorComponent, selector: "mm-entity-selector-scope-path-editor", inputs: ["sourceCkTypeId", "hops"], outputs: ["hopsChange"] }, { kind: "component", type: FieldFilterEditorComponent, selector: "mm-field-filter-editor", inputs: ["availableAttributes", "ckTypeId", "hideNavigationProperties", "attributePaths", "enableVariables", "availableVariables", "filters"], outputs: ["filtersChange"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
32843
34168
  }
32844
34169
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: EntitySelectorEditorComponent, decorators: [{
32845
34170
  type: Component,
@@ -32851,38 +34176,60 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
32851
34176
  LabelModule,
32852
34177
  SVGIconModule,
32853
34178
  CkTypeSelectorInputComponent,
32854
- EntitySelectInputComponent
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"] }]
32856
- }], propDecorators: { entitySelectors: [{
34179
+ EntitySelectInputComponent,
34180
+ EntitySelectorScopePathEditorComponent,
34181
+ FieldFilterEditorComponent
34182
+ ], 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 @if (getFilterSummary(selector); as filterSummary) {\n <span class=\"selector-mode selector-filter-chip\">{{ filterSummary }}</span>\n }\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 @if (editCkTypeId) {\n <div class=\"form-field picker-filter-field\">\n <label class=\"field-label\">Picker filter</label>\n @if (isFollower) {\n <span class=\"field-hint\">A follower takes the leader's pick. Set the filter on {{ leaderLabel }}.</span>\n @if (followerFilterCount > 0) {\n <span class=\"field-hint follow-notice\">Filters on a follower are ignored and are removed when you save.</span>\n }\n } @else {\n <span class=\"field-hint\">\n Only entities that match all rows are listed in the toolbar picker and in the default-entity picker below.\n </span>\n <mm-field-filter-editor\n [ckTypeId]=\"editCkTypeId\"\n [filters]=\"editFilters\"\n [enableVariables]=\"filterVariables.length > 0\"\n [availableVariables]=\"filterVariables\"\n (filtersChange)=\"onFiltersChange($event)\">\n </mm-field-filter-editor>\n }\n </div>\n }\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 @switch (defaultFilterCheck.state) {\n @case ('mismatch') {\n <span class=\"field-warning default-filter-warning\">The default entity does not match the picker filter. It still loads, but users cannot pick it again.</span>\n }\n @case ('error') {\n <span class=\"field-hint default-filter-warning\">Could not check the default entity.</span>\n }\n @case ('unchecked') {\n <span class=\"field-hint default-filter-warning\">Cannot check the default entity: the filter uses {{ uncheckedVariablesText }}, which is filled at view time.</span>\n }\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 <mm-entity-selector-scope-path-editor\n [sourceCkTypeId]=\"editCkTypeId\"\n [hops]=\"editScopeHops\"\n (hopsChange)=\"onScopeHopsChanged($event)\">\n </mm-entity-selector-scope-path-editor>\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-filter-chip{color:var(--kendo-color-subtle, #757575)}.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 .field-warning{font-size:.75rem;color:var(--kendo-color-warning, #f57c00)}.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"] }]
34183
+ }], ctorParameters: () => [], propDecorators: { entitySelectors: [{
32857
34184
  type: Input
32858
34185
  }], existingVariableNames: [{
32859
34186
  type: Input
34187
+ }], variables: [{
34188
+ type: Input
32860
34189
  }], entitySelectorsChange: [{
32861
34190
  type: Output
32862
34191
  }], editingStateChange: [{
32863
34192
  type: Output
32864
34193
  }] } });
34194
+ /** Debounce of the default-entity filter check (ms). */
34195
+ const DEFAULT_CHECK_DEBOUNCE_MS = 400;
32865
34196
 
32866
34197
  /**
32867
34198
  * Toolbar component that renders entity select inputs for each configured entity selector.
32868
- * Creates data sources per CK type and emits selection/clear events.
34199
+ * Creates data sources per CK type and picker filter (AB#7724) and emits selection/clear events.
32869
34200
  */
32870
34201
  class EntitySelectorToolbarComponent {
32871
34202
  getEntitiesByCkTypeGQL = inject(GetEntitiesByCkTypeDtoGQL);
34203
+ variableService = inject(MeshBoardVariableService);
32872
34204
  entitySelectors = [];
34205
+ /**
34206
+ * Board variables substituted into the selectors' picker filters (AB#7724),
34207
+ * the same substitution widget filters get.
34208
+ */
34209
+ variables = [];
32873
34210
  entitySelected = new EventEmitter();
32874
34211
  entityCleared = new EventEmitter();
32875
- // Cached data sources per CK type
34212
+ // Cached data sources per CK type and resolved picker filter
32876
34213
  dataSourceCache = new Map();
32877
34214
  dialogDataSourceCache = new Map();
32878
34215
  // Map of selector ID -> data sources for template
32879
34216
  selectorDataSources = new Map();
32880
34217
  selectorDialogDataSources = new Map();
34218
+ // Picker texts of filtered selectors (absent ⇒ the input's defaults)
34219
+ selectorMessages = new Map();
32881
34220
  ngOnChanges(changes) {
32882
- if (changes['entitySelectors']) {
34221
+ if (changes['entitySelectors'] || changes['variables']) {
32883
34222
  this.updateDataSources();
32884
34223
  }
32885
34224
  }
34225
+ /** Texts of the selector's picker: a filtered picker explains its empty list (AB#7724). */
34226
+ getMessages(selector) {
34227
+ return this.selectorMessages.get(selector.id) ?? NO_MESSAGES;
34228
+ }
34229
+ /** Title of the selector's search dialog; marks a filtered picker (AB#7724). */
34230
+ getDialogTitle(selector) {
34231
+ return this.selectorMessages.has(selector.id) ? filteredDialogTitle(selector.label) : 'Select ' + selector.label;
34232
+ }
32886
34233
  /**
32887
34234
  * True when the selector renders a picker. Hidden selectors and followers
32888
34235
  * (AB#7722) never do; a follower's pick is derived from its leader.
@@ -32915,23 +34262,45 @@ class EntitySelectorToolbarComponent {
32915
34262
  this.entityCleared.emit({ selectorId });
32916
34263
  }
32917
34264
  /**
32918
- * Creates or retrieves cached data sources for each selector.
34265
+ * Creates or retrieves cached data sources for each selector. Selectors of
34266
+ * the same CK type and the same resolved filter share their data sources; a
34267
+ * selector without filter uses the plain query, exactly as before AB#7724.
34268
+ * Entries no selector uses any more (e.g. after a filter variable changed)
34269
+ * are dropped, so the caches never outgrow the current selectors.
32919
34270
  */
32920
34271
  updateDataSources() {
34272
+ this.selectorMessages = new Map();
34273
+ const usedKeys = new Set();
32921
34274
  for (const selector of this.entitySelectors) {
32922
- // Reuse cached data source if same CK type
32923
- if (!this.dataSourceCache.has(selector.ckTypeId)) {
32924
- this.dataSourceCache.set(selector.ckTypeId, new RuntimeEntitySelectDataSource(this.getEntitiesByCkTypeGQL, selector.ckTypeId));
34275
+ const filters = pickerFieldFilters(selector, this.variables, this.variableService);
34276
+ const key = filters ? `${selector.ckTypeId}|${JSON.stringify(filters)}` : selector.ckTypeId;
34277
+ usedKeys.add(key);
34278
+ if (!this.dataSourceCache.has(key) || !this.dialogDataSourceCache.has(key)) {
34279
+ const gql = withPickerFilters(this.getEntitiesByCkTypeGQL, filters);
34280
+ this.dataSourceCache.set(key, new RuntimeEntitySelectDataSource(gql, selector.ckTypeId));
34281
+ this.dialogDataSourceCache.set(key, new RuntimeEntityDialogDataSource(gql, selector.ckTypeId));
34282
+ }
34283
+ this.selectorDataSources.set(selector.id, this.dataSourceCache.get(key));
34284
+ this.selectorDialogDataSources.set(selector.id, this.dialogDataSourceCache.get(key));
34285
+ if (filters) {
34286
+ this.selectorMessages.set(selector.id, {
34287
+ noEntitiesFound: filteredNoEntitiesMessage(selector.label, unresolvedFilterVariables(filters))
34288
+ });
32925
34289
  }
32926
- if (!this.dialogDataSourceCache.has(selector.ckTypeId)) {
32927
- this.dialogDataSourceCache.set(selector.ckTypeId, new RuntimeEntityDialogDataSource(this.getEntitiesByCkTypeGQL, selector.ckTypeId));
34290
+ }
34291
+ for (const key of [...this.dataSourceCache.keys()]) {
34292
+ if (!usedKeys.has(key)) {
34293
+ this.dataSourceCache.delete(key);
34294
+ this.dialogDataSourceCache.delete(key);
32928
34295
  }
32929
- this.selectorDataSources.set(selector.id, this.dataSourceCache.get(selector.ckTypeId));
32930
- this.selectorDialogDataSources.set(selector.id, this.dialogDataSourceCache.get(selector.ckTypeId));
32931
34296
  }
32932
34297
  }
34298
+ /** Number of cached data source pairs (for tests). */
34299
+ get cachedDataSourceCount() {
34300
+ return this.dataSourceCache.size;
34301
+ }
32933
34302
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: EntitySelectorToolbarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
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 });
34303
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: EntitySelectorToolbarComponent, isStandalone: true, selector: "mm-entity-selector-toolbar", inputs: { entitySelectors: "entitySelectors", variables: "variables" }, 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]=\"getDialogTitle(selector)\"\n [messages]=\"getMessages(selector)\"\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 });
32935
34304
  }
32936
34305
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: EntitySelectorToolbarComponent, decorators: [{
32937
34306
  type: Component,
@@ -32939,14 +34308,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
32939
34308
  CommonModule,
32940
34309
  KENDO_LABELS,
32941
34310
  EntitySelectInputComponent
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"] }]
34311
+ ], 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]=\"getDialogTitle(selector)\"\n [messages]=\"getMessages(selector)\"\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"] }]
32943
34312
  }], propDecorators: { entitySelectors: [{
32944
34313
  type: Input
34314
+ }], variables: [{
34315
+ type: Input
32945
34316
  }], entitySelected: [{
32946
34317
  type: Output
32947
34318
  }], entityCleared: [{
32948
34319
  type: Output
32949
34320
  }] } });
34321
+ const NO_MESSAGES = {};
32950
34322
 
32951
34323
  /** Below this container width the board stacks in a single column. */
32952
34324
  const PHONE_MAX_WIDTH = 700;
@@ -33294,6 +34666,90 @@ function applyTimeFilterParams(url, selection) {
33294
34666
  return applyQueryParams(url, timeFilterQueryParams(selection));
33295
34667
  }
33296
34668
 
34669
+ /**
34670
+ * RFC 3339 date-time: `YYYY-MM-DDTHH:MM:SS`, optional 1-3 fraction digits, then `Z` or `±HH:MM`.
34671
+ * Uppercase `T`/`Z` only, seconds required, no compact offset (`+0100`): those forms parse
34672
+ * differently across browsers (Safari), and a string without an offset would be read in the
34673
+ * browser's own zone.
34674
+ */
34675
+ const RFC3339 = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,3}))?(?:(Z)|([+-])(\d{2}):(\d{2}))$/;
34676
+ function daysInMonth(year, month) {
34677
+ return new Date(Date.UTC(year, month, 0)).getUTCDate();
34678
+ }
34679
+ /**
34680
+ * The instant of an RFC 3339 string, computed from its fields (never from `Date.parse`, so every
34681
+ * browser gets the same result), or `null` when the string is not RFC 3339 or names a date or time
34682
+ * that does not exist (`2026-02-30`, `24:00`, `23:60`, leap second `:60`, offset hour > 23).
34683
+ */
34684
+ function parseRfc3339(value) {
34685
+ const m = RFC3339.exec(value);
34686
+ if (!m) {
34687
+ return null;
34688
+ }
34689
+ const [year, month, day, hour, minute, second] = m.slice(1, 7).map(Number);
34690
+ const millis = m[7] ? Number(m[7].padEnd(3, '0')) : 0;
34691
+ if (month < 1 || month > 12 || day < 1 || day > daysInMonth(year, month)
34692
+ || hour > 23 || minute > 59 || second > 59) {
34693
+ return null;
34694
+ }
34695
+ let offsetMinutes = 0;
34696
+ if (!m[8]) {
34697
+ const offsetHours = Number(m[10]);
34698
+ const offsetMins = Number(m[11]);
34699
+ if (offsetHours > 23 || offsetMins > 59) {
34700
+ return null;
34701
+ }
34702
+ offsetMinutes = (m[9] === '-' ? -1 : 1) * (offsetHours * 60 + offsetMins);
34703
+ }
34704
+ // setUTCFullYear (not Date.UTC, which maps years 0-99 to 1900-1999) for the wall-clock fields,
34705
+ // then the offset: local wall clock = UTC + offset.
34706
+ const date = new Date(0);
34707
+ date.setUTCFullYear(year, month - 1, day);
34708
+ date.setUTCHours(hour, minute, second, millis);
34709
+ date.setTime(date.getTime() - offsetMinutes * 60_000);
34710
+ return Number.isNaN(date.getTime()) ? null : date;
34711
+ }
34712
+ function toInstant(value) {
34713
+ if (value instanceof Date) {
34714
+ return Number.isNaN(value.getTime()) ? null : new Date(value.getTime());
34715
+ }
34716
+ return typeof value === 'string' ? parseRfc3339(value) : null;
34717
+ }
34718
+ /**
34719
+ * Parses the host's `timeRange` input. `undefined`/`null` = the host sets no period.
34720
+ * Never snaps or converts: the instants are taken as given. `Date` objects are accepted as they
34721
+ * are; strings must be RFC 3339 with an offset, e.g. `2026-03-28T05:00:00Z` or
34722
+ * `2026-03-28T06:00:00+01:00` (see {@link RFC3339}).
34723
+ */
34724
+ function parseHostTimeRange(value) {
34725
+ if (value === undefined || value === null) {
34726
+ return undefined;
34727
+ }
34728
+ const from = toInstant(value.from);
34729
+ const to = toInstant(value.to);
34730
+ if (!from || !to) {
34731
+ return {
34732
+ status: 'invalid',
34733
+ reason: 'from and to must be Date objects or RFC 3339 date-times with an offset '
34734
+ + '(YYYY-MM-DDTHH:MM:SS[.sss] followed by Z or ±HH:MM)'
34735
+ };
34736
+ }
34737
+ if (from.getTime() >= to.getTime()) {
34738
+ return { status: 'invalid', reason: 'from must be before to' };
34739
+ }
34740
+ return { status: 'valid', from, to };
34741
+ }
34742
+ /** True when two parsed ranges mean the same period (same instants, or both invalid for the same reason). */
34743
+ function sameHostTimeRange(a, b) {
34744
+ if (!a || !b) {
34745
+ return a === b;
34746
+ }
34747
+ if (a.status === 'valid' && b.status === 'valid') {
34748
+ return a.from.getTime() === b.from.getTime() && a.to.getTime() === b.to.getTime();
34749
+ }
34750
+ return a.status === 'invalid' && b.status === 'invalid' && a.reason === b.reason;
34751
+ }
34752
+
33297
34753
  /**
33298
34754
  * Component for editing MeshBoard variables.
33299
34755
  * Allows adding, editing, and removing static variables with type-specific input controls.
@@ -33630,7 +35086,7 @@ class MeshBoardSettingsDialogComponent {
33630
35086
  this.windowRef.close();
33631
35087
  }
33632
35088
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: MeshBoardSettingsDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
33633
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: MeshBoardSettingsDialogComponent, isStandalone: true, selector: "mm-meshboard-settings-dialog", ngImport: i0, template: "<div class=\"meshboard-settings-dialog\">\n <kendo-tabstrip [animate]=\"false\">\n <!-- General Tab -->\n <kendo-tabstrip-tab [title]=\"'General'\" [selected]=\"true\">\n <ng-template kendoTabContent>\n <div class=\"tab-content\">\n <form class=\"settings-form\">\n <!-- Name Field -->\n <kendo-formfield>\n <kendo-label [for]=\"nameInput\" text=\"Name *\"></kendo-label>\n <kendo-textbox\n #nameInput\n [(ngModel)]=\"name\"\n name=\"name\"\n placeholder=\"Enter MeshBoard name\"\n required>\n </kendo-textbox>\n @if (name.trim().length === 0) {\n <kendo-formerror>Name is required</kendo-formerror>\n }\n </kendo-formfield>\n\n <!-- Description Field -->\n <kendo-formfield>\n <kendo-label [for]=\"descriptionInput\" text=\"Description\"></kendo-label>\n <kendo-textarea\n #descriptionInput\n [(ngModel)]=\"description\"\n name=\"description\"\n placeholder=\"Enter MeshBoard description (optional)\"\n [rows]=\"3\">\n </kendo-textarea>\n </kendo-formfield>\n\n <!-- Well-Known Name Field -->\n <kendo-formfield>\n <kendo-label [for]=\"wellKnownNameInput\" text=\"Well-Known Name\"></kendo-label>\n <kendo-textbox\n #wellKnownNameInput\n [(ngModel)]=\"rtWellKnownName\"\n name=\"rtWellKnownName\"\n placeholder=\"e.g., cockpit, dashboard-main\">\n </kendo-textbox>\n <kendo-formhint>Unique identifier for routing. Use lowercase with hyphens (e.g., 'cockpit', 'sales-dashboard').</kendo-formhint>\n </kendo-formfield>\n\n <!-- Layout Settings -->\n <div class=\"section-title\">Layout Settings</div>\n\n <!-- Columns Field -->\n <kendo-formfield>\n <kendo-label [for]=\"columnsInput\" text=\"Columns *\"></kendo-label>\n <kendo-numerictextbox\n #columnsInput\n [(ngModel)]=\"columns\"\n name=\"columns\"\n [min]=\"1\"\n [max]=\"12\"\n [decimals]=\"0\"\n [format]=\"'n0'\"\n [spinners]=\"true\"\n required>\n </kendo-numerictextbox>\n <kendo-formhint>Number of columns in the grid (1-12)</kendo-formhint>\n @if (columns < 1 || columns > 12) {\n <kendo-formerror>Columns must be between 1 and 12</kendo-formerror>\n }\n </kendo-formfield>\n\n <!-- Row Height Field -->\n <kendo-formfield>\n <kendo-label [for]=\"rowHeightInput\" text=\"Row Height *\"></kendo-label>\n <kendo-numerictextbox\n #rowHeightInput\n [(ngModel)]=\"rowHeight\"\n name=\"rowHeight\"\n [min]=\"100\"\n [max]=\"1000\"\n [decimals]=\"0\"\n [format]=\"'n0'\"\n [spinners]=\"true\"\n required>\n </kendo-numerictextbox>\n <kendo-formhint>Height of each row in pixels (100-1000)</kendo-formhint>\n @if (rowHeight < 100 || rowHeight > 1000) {\n <kendo-formerror>Row height must be between 100 and 1000</kendo-formerror>\n }\n </kendo-formfield>\n\n <!-- Gap Field -->\n <kendo-formfield>\n <kendo-label [for]=\"gapInput\" text=\"Gap *\"></kendo-label>\n <kendo-numerictextbox\n #gapInput\n [(ngModel)]=\"gap\"\n name=\"gap\"\n [min]=\"0\"\n [max]=\"100\"\n [decimals]=\"0\"\n [format]=\"'n0'\"\n [spinners]=\"true\"\n required>\n </kendo-numerictextbox>\n <kendo-formhint>Space between widgets in pixels (0-100)</kendo-formhint>\n @if (gap < 0 || gap > 100) {\n <kendo-formerror>Gap must be between 0 and 100</kendo-formerror>\n }\n </kendo-formfield>\n\n <!-- Auto-Refresh Field -->\n <kendo-formfield>\n <kendo-label [for]=\"autoRefreshInput\" text=\"Auto-refresh\"></kendo-label>\n <kendo-numerictextbox\n #autoRefreshInput\n [(ngModel)]=\"autoRefreshSeconds\"\n name=\"autoRefreshSeconds\"\n [min]=\"0\"\n [max]=\"3600\"\n [decimals]=\"0\"\n [format]=\"'n0'\"\n [spinners]=\"true\">\n </kendo-numerictextbox>\n <kendo-formhint>Refresh interval in seconds (0 = disabled, max 3600). Pauses while the tab is hidden.</kendo-formhint>\n @if (autoRefreshSeconds < 0 || autoRefreshSeconds > 3600) {\n <kendo-formerror>Auto-refresh must be between 0 and 3600 seconds</kendo-formerror>\n }\n </kendo-formfield>\n </form>\n </div>\n </ng-template>\n </kendo-tabstrip-tab>\n\n <!-- Variables Tab -->\n <kendo-tabstrip-tab [title]=\"'Variables'\">\n <ng-template kendoTabContent>\n <div class=\"tab-content\">\n <mm-variables-editor\n [(variables)]=\"variables\">\n </mm-variables-editor>\n </div>\n </ng-template>\n </kendo-tabstrip-tab>\n\n <!-- Time Filter Tab -->\n <kendo-tabstrip-tab [title]=\"'Time Filter'\">\n <ng-template kendoTabContent>\n <div class=\"tab-content\">\n <form class=\"settings-form\">\n <kendo-formfield>\n <kendo-label text=\"Timezone\"></kendo-label>\n <div class=\"radio-group\">\n <div class=\"radio-wrapper\">\n <input\n type=\"radio\"\n kendoRadioButton\n #tzLocal\n [(ngModel)]=\"timeZoneMode\"\n name=\"timeZoneMode\"\n value=\"local\"\n id=\"timeZoneModeLocal\"/>\n <kendo-label [for]=\"tzLocal\" text=\"Local time (browser)\" class=\"radio-label\"></kendo-label>\n </div>\n <div class=\"radio-wrapper\">\n <input\n type=\"radio\"\n kendoRadioButton\n #tzUtc\n [(ngModel)]=\"timeZoneMode\"\n name=\"timeZoneMode\"\n value=\"utc\"\n id=\"timeZoneModeUtc\"/>\n <kendo-label [for]=\"tzUtc\" text=\"UTC\" class=\"radio-label\"></kendo-label>\n </div>\n <div class=\"radio-wrapper\">\n <input\n type=\"radio\"\n kendoRadioButton\n #tzZone\n [(ngModel)]=\"timeZoneMode\"\n name=\"timeZoneMode\"\n [value]=\"zoneSelection\"\n id=\"timeZoneModeZone\"/>\n <kendo-label [for]=\"tzZone\" text=\"Specific time zone\" class=\"radio-label\"></kendo-label>\n <kendo-dropdownlist\n class=\"tz-zone-select\"\n [data]=\"commonTimeZones\"\n [value]=\"zoneSelection\"\n (valueChange)=\"onZoneSelectionChange($event)\"\n [disabled]=\"!isSpecificZone\"\n [filterable]=\"true\"\n aria-label=\"IANA time zone\">\n </kendo-dropdownlist>\n </div>\n </div>\n <kendo-formhint>\n Basis for the time filter and for how all widgets display dates and times. Local time is the default.\n A specific time zone resolves civil days (today / yesterday / this month) DST-correctly in that\n zone and selects matching calendar rollups.\n </kendo-formhint>\n </kendo-formfield>\n\n <kendo-formfield>\n <div class=\"checkbox-wrapper\">\n <input\n type=\"checkbox\"\n kendoCheckBox\n #timeFilterCheckbox\n [(ngModel)]=\"timeFilterEnabled\"\n name=\"timeFilterEnabled\"\n id=\"timeFilterEnabled\"/>\n <kendo-label\n [for]=\"timeFilterCheckbox\"\n text=\"Enable Time Filter\"\n class=\"checkbox-label\">\n </kendo-label>\n </div>\n <kendo-formhint>\n Shows a time range picker in the toolbar. Sets $timeRangeFrom and $timeRangeTo variables.\n </kendo-formhint>\n </kendo-formfield>\n\n @if (timeFilterEnabled) {\n <kendo-formfield>\n <kendo-label text=\"Default Selection\"></kendo-label>\n <mm-time-range-picker\n [initialSelection]=\"initialDefaultSelection\"\n (selectionChange)=\"onDefaultSelectionChange($event)\">\n </mm-time-range-picker>\n <kendo-formhint>Initial time filter shown when no URL parameters are set. Note: User-selected filters override this default. Use the reset button in the toolbar to revert to the default.</kendo-formhint>\n </kendo-formfield>\n }\n </form>\n </div>\n </ng-template>\n </kendo-tabstrip-tab>\n\n <!-- Entity Selectors Tab -->\n <kendo-tabstrip-tab [title]=\"'Entity Selectors'\">\n <ng-template kendoTabContent>\n <div class=\"tab-content\">\n <mm-entity-selector-editor\n [(entitySelectors)]=\"entitySelectors\"\n [existingVariableNames]=\"staticVariableNames\"\n (editingStateChange)=\"entitySelectorEditing = $event\">\n </mm-entity-selector-editor>\n </div>\n </ng-template>\n </kendo-tabstrip-tab>\n </kendo-tabstrip>\n\n <!-- Dialog Actions -->\n @if (!entitySelectorEditing) {\n <div class=\"dialog-actions mm-dialog-actions\">\n <button kendoButton (click)=\"cancel()\" fillMode=\"flat\">\n Cancel\n </button>\n <button\n kendoButton\n (click)=\"save()\"\n [disabled]=\"!isValid\"\n themeColor=\"primary\">\n Save\n </button>\n </div>\n }\n</div>\n", styles: [".meshboard-settings-dialog{display:flex;flex-direction:column;height:100%;overflow:hidden}.meshboard-settings-dialog kendo-tabstrip{flex:1;min-height:0;display:flex;flex-direction:column}.meshboard-settings-dialog kendo-tabstrip ::ng-deep .k-tabstrip-content{flex:1;min-height:0;overflow:hidden}.meshboard-settings-dialog .tab-content{height:100%;overflow-y:auto;padding:1.5rem}.meshboard-settings-dialog .settings-form{display:flex;flex-direction:column;gap:1.25rem}.meshboard-settings-dialog .settings-form kendo-formfield{display:flex;flex-direction:column;gap:.5rem}.meshboard-settings-dialog .settings-form kendo-formfield kendo-label{font-weight:500;color:var(--kendo-color-on-app-surface, #424242)}.meshboard-settings-dialog .settings-form kendo-formfield kendo-textbox,.meshboard-settings-dialog .settings-form kendo-formfield kendo-textarea,.meshboard-settings-dialog .settings-form kendo-formfield kendo-numerictextbox{width:100%}.meshboard-settings-dialog .settings-form kendo-formfield kendo-formhint{font-size:.75rem;color:var(--kendo-color-subtle, #757575)}.meshboard-settings-dialog .settings-form kendo-formfield kendo-formerror{font-size:.75rem;color:var(--kendo-color-error, #f44336)}.meshboard-settings-dialog .settings-form .section-title{font-size:.875rem;font-weight:600;color:var(--kendo-color-on-app-surface, #424242);margin-top:.5rem;padding-bottom:.5rem;border-bottom:1px solid var(--kendo-color-border, #e0e0e0)}.meshboard-settings-dialog .settings-form .checkbox-wrapper,.meshboard-settings-dialog .settings-form .radio-wrapper{display:flex;align-items:center;gap:.5rem}.meshboard-settings-dialog .settings-form .checkbox-wrapper .checkbox-label,.meshboard-settings-dialog .settings-form .checkbox-wrapper .radio-label,.meshboard-settings-dialog .settings-form .radio-wrapper .checkbox-label,.meshboard-settings-dialog .settings-form .radio-wrapper .radio-label{font-weight:400;cursor:pointer}.meshboard-settings-dialog .settings-form .checkbox-wrapper .tz-zone-select,.meshboard-settings-dialog .settings-form .radio-wrapper .tz-zone-select{min-width:12rem}.meshboard-settings-dialog .settings-form .radio-group{display:flex;flex-direction:column;gap:.5rem;margin-top:.25rem}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox]:not([ngNoCva])[formControlName],input[type=checkbox]:not([ngNoCva])[formControl],input[type=checkbox]:not([ngNoCva])[ngModel]" }, { kind: "directive", type: i1$1.RadioControlValueAccessor, selector: "input[type=radio]:not([ngNoCva])[formControlName],input[type=radio]:not([ngNoCva])[formControl],input[type=radio]:not([ngNoCva])[ngModel]", inputs: ["name", "formControlName", "value"] }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1$1.NgForm, selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: InputsModule }, { kind: "component", type: i3.TextBoxComponent, selector: "kendo-textbox", inputs: ["focusableId", "title", "type", "disabled", "readonly", "tabindex", "value", "selectOnFocus", "showSuccessIcon", "showErrorIcon", "clearButton", "successIcon", "successSvgIcon", "errorIcon", "errorSvgIcon", "clearButtonIcon", "clearButtonSvgIcon", "size", "rounded", "fillMode", "tabIndex", "placeholder", "maxlength", "inputAttributes"], outputs: ["valueChange", "inputFocus", "inputBlur", "focus", "blur"], exportAs: ["kendoTextBox"] }, { kind: "component", type: i3.NumericTextBoxComponent, selector: "kendo-numerictextbox", inputs: ["focusableId", "disabled", "readonly", "title", "autoCorrect", "format", "max", "min", "decimals", "placeholder", "step", "spinners", "rangeValidation", "tabindex", "tabIndex", "changeValueOnScroll", "selectOnFocus", "value", "maxlength", "size", "rounded", "fillMode", "inputAttributes"], outputs: ["valueChange", "focus", "blur", "inputFocus", "inputBlur"], exportAs: ["kendoNumericTextBox"] }, { kind: "component", type: i3.TextAreaComponent, selector: "kendo-textarea", inputs: ["focusableId", "flow", "inputAttributes", "adornmentsOrientation", "rows", "cols", "maxlength", "maxResizableRows", "tabindex", "tabIndex", "resizable", "size", "rounded", "fillMode", "showPrefixSeparator", "showSuffixSeparator"], outputs: ["focus", "blur", "valueChange"], exportAs: ["kendoTextArea"] }, { kind: "directive", type: i3.CheckBoxDirective, selector: "input[kendoCheckBox]", inputs: ["size", "rounded"] }, { kind: "directive", type: i3.RadioButtonDirective, selector: "input[kendoRadioButton]", inputs: ["size"] }, { kind: "component", type: i3.FormFieldComponent, selector: "kendo-formfield", inputs: ["showHints", "orientation", "showErrors", "colSpan"] }, { kind: "component", type: i3.HintComponent, selector: "kendo-formhint", inputs: ["align"] }, { kind: "component", type: i3.ErrorComponent, selector: "kendo-formerror", inputs: ["align"] }, { kind: "ngmodule", type: CheckBoxModule }, { kind: "ngmodule", type: LabelModule }, { kind: "component", type: i4$1.LabelComponent, selector: "kendo-label", inputs: ["text", "for", "optional", "labelCssStyle", "labelCssClass"], exportAs: ["kendoLabel"] }, { kind: "ngmodule", type: FormFieldModule }, { kind: "ngmodule", type: TabStripModule }, { kind: "component", type: i5.TabStripComponent, selector: "kendo-tabstrip", inputs: ["height", "animate", "tabAlignment", "tabPosition", "keepTabContent", "closable", "scrollable", "size", "closeIcon", "closeIconClass", "closeSVGIcon", "showContentArea"], outputs: ["tabSelect", "tabClose", "tabScroll"], exportAs: ["kendoTabStrip"] }, { kind: "component", type: i5.TabStripTabComponent, selector: "kendo-tabstrip-tab", inputs: ["title", "disabled", "cssClass", "cssStyle", "selected", "closable", "closeIcon", "closeIconClass", "closeSVGIcon"], exportAs: ["kendoTabStripTab"] }, { kind: "directive", type: i5.TabContentDirective, selector: "[kendoTabContent]" }, { kind: "ngmodule", type: DropDownListModule }, { 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: VariablesEditorComponent, selector: "mm-variables-editor", inputs: ["variables"], outputs: ["variablesChange"] }, { kind: "component", type: EntitySelectorEditorComponent, selector: "mm-entity-selector-editor", inputs: ["entitySelectors", "existingVariableNames"], outputs: ["entitySelectorsChange", "editingStateChange"] }, { kind: "component", type: TimeRangePickerComponent, selector: "mm-time-range-picker", inputs: ["config", "labels", "initialSelection"], outputs: ["rangeChange", "rangeChangeISO", "selectionChange"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
35089
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: MeshBoardSettingsDialogComponent, isStandalone: true, selector: "mm-meshboard-settings-dialog", ngImport: i0, template: "<div class=\"meshboard-settings-dialog\">\n <kendo-tabstrip [animate]=\"false\">\n <!-- General Tab -->\n <kendo-tabstrip-tab [title]=\"'General'\" [selected]=\"true\">\n <ng-template kendoTabContent>\n <div class=\"tab-content\">\n <form class=\"settings-form\">\n <!-- Name Field -->\n <kendo-formfield>\n <kendo-label [for]=\"nameInput\" text=\"Name *\"></kendo-label>\n <kendo-textbox\n #nameInput\n [(ngModel)]=\"name\"\n name=\"name\"\n placeholder=\"Enter MeshBoard name\"\n required>\n </kendo-textbox>\n @if (name.trim().length === 0) {\n <kendo-formerror>Name is required</kendo-formerror>\n }\n </kendo-formfield>\n\n <!-- Description Field -->\n <kendo-formfield>\n <kendo-label [for]=\"descriptionInput\" text=\"Description\"></kendo-label>\n <kendo-textarea\n #descriptionInput\n [(ngModel)]=\"description\"\n name=\"description\"\n placeholder=\"Enter MeshBoard description (optional)\"\n [rows]=\"3\">\n </kendo-textarea>\n </kendo-formfield>\n\n <!-- Well-Known Name Field -->\n <kendo-formfield>\n <kendo-label [for]=\"wellKnownNameInput\" text=\"Well-Known Name\"></kendo-label>\n <kendo-textbox\n #wellKnownNameInput\n [(ngModel)]=\"rtWellKnownName\"\n name=\"rtWellKnownName\"\n placeholder=\"e.g., cockpit, dashboard-main\">\n </kendo-textbox>\n <kendo-formhint>Unique identifier for routing. Use lowercase with hyphens (e.g., 'cockpit', 'sales-dashboard').</kendo-formhint>\n </kendo-formfield>\n\n <!-- Layout Settings -->\n <div class=\"section-title\">Layout Settings</div>\n\n <!-- Columns Field -->\n <kendo-formfield>\n <kendo-label [for]=\"columnsInput\" text=\"Columns *\"></kendo-label>\n <kendo-numerictextbox\n #columnsInput\n [(ngModel)]=\"columns\"\n name=\"columns\"\n [min]=\"1\"\n [max]=\"12\"\n [decimals]=\"0\"\n [format]=\"'n0'\"\n [spinners]=\"true\"\n required>\n </kendo-numerictextbox>\n <kendo-formhint>Number of columns in the grid (1-12)</kendo-formhint>\n @if (columns < 1 || columns > 12) {\n <kendo-formerror>Columns must be between 1 and 12</kendo-formerror>\n }\n </kendo-formfield>\n\n <!-- Row Height Field -->\n <kendo-formfield>\n <kendo-label [for]=\"rowHeightInput\" text=\"Row Height *\"></kendo-label>\n <kendo-numerictextbox\n #rowHeightInput\n [(ngModel)]=\"rowHeight\"\n name=\"rowHeight\"\n [min]=\"100\"\n [max]=\"1000\"\n [decimals]=\"0\"\n [format]=\"'n0'\"\n [spinners]=\"true\"\n required>\n </kendo-numerictextbox>\n <kendo-formhint>Height of each row in pixels (100-1000)</kendo-formhint>\n @if (rowHeight < 100 || rowHeight > 1000) {\n <kendo-formerror>Row height must be between 100 and 1000</kendo-formerror>\n }\n </kendo-formfield>\n\n <!-- Gap Field -->\n <kendo-formfield>\n <kendo-label [for]=\"gapInput\" text=\"Gap *\"></kendo-label>\n <kendo-numerictextbox\n #gapInput\n [(ngModel)]=\"gap\"\n name=\"gap\"\n [min]=\"0\"\n [max]=\"100\"\n [decimals]=\"0\"\n [format]=\"'n0'\"\n [spinners]=\"true\"\n required>\n </kendo-numerictextbox>\n <kendo-formhint>Space between widgets in pixels (0-100)</kendo-formhint>\n @if (gap < 0 || gap > 100) {\n <kendo-formerror>Gap must be between 0 and 100</kendo-formerror>\n }\n </kendo-formfield>\n\n <!-- Auto-Refresh Field -->\n <kendo-formfield>\n <kendo-label [for]=\"autoRefreshInput\" text=\"Auto-refresh\"></kendo-label>\n <kendo-numerictextbox\n #autoRefreshInput\n [(ngModel)]=\"autoRefreshSeconds\"\n name=\"autoRefreshSeconds\"\n [min]=\"0\"\n [max]=\"3600\"\n [decimals]=\"0\"\n [format]=\"'n0'\"\n [spinners]=\"true\">\n </kendo-numerictextbox>\n <kendo-formhint>Refresh interval in seconds (0 = disabled, max 3600). Pauses while the tab is hidden.</kendo-formhint>\n @if (autoRefreshSeconds < 0 || autoRefreshSeconds > 3600) {\n <kendo-formerror>Auto-refresh must be between 0 and 3600 seconds</kendo-formerror>\n }\n </kendo-formfield>\n </form>\n </div>\n </ng-template>\n </kendo-tabstrip-tab>\n\n <!-- Variables Tab -->\n <kendo-tabstrip-tab [title]=\"'Variables'\">\n <ng-template kendoTabContent>\n <div class=\"tab-content\">\n <mm-variables-editor\n [(variables)]=\"variables\">\n </mm-variables-editor>\n </div>\n </ng-template>\n </kendo-tabstrip-tab>\n\n <!-- Time Filter Tab -->\n <kendo-tabstrip-tab [title]=\"'Time Filter'\">\n <ng-template kendoTabContent>\n <div class=\"tab-content\">\n <form class=\"settings-form\">\n <kendo-formfield>\n <kendo-label text=\"Timezone\"></kendo-label>\n <div class=\"radio-group\">\n <div class=\"radio-wrapper\">\n <input\n type=\"radio\"\n kendoRadioButton\n #tzLocal\n [(ngModel)]=\"timeZoneMode\"\n name=\"timeZoneMode\"\n value=\"local\"\n id=\"timeZoneModeLocal\"/>\n <kendo-label [for]=\"tzLocal\" text=\"Local time (browser)\" class=\"radio-label\"></kendo-label>\n </div>\n <div class=\"radio-wrapper\">\n <input\n type=\"radio\"\n kendoRadioButton\n #tzUtc\n [(ngModel)]=\"timeZoneMode\"\n name=\"timeZoneMode\"\n value=\"utc\"\n id=\"timeZoneModeUtc\"/>\n <kendo-label [for]=\"tzUtc\" text=\"UTC\" class=\"radio-label\"></kendo-label>\n </div>\n <div class=\"radio-wrapper\">\n <input\n type=\"radio\"\n kendoRadioButton\n #tzZone\n [(ngModel)]=\"timeZoneMode\"\n name=\"timeZoneMode\"\n [value]=\"zoneSelection\"\n id=\"timeZoneModeZone\"/>\n <kendo-label [for]=\"tzZone\" text=\"Specific time zone\" class=\"radio-label\"></kendo-label>\n <kendo-dropdownlist\n class=\"tz-zone-select\"\n [data]=\"commonTimeZones\"\n [value]=\"zoneSelection\"\n (valueChange)=\"onZoneSelectionChange($event)\"\n [disabled]=\"!isSpecificZone\"\n [filterable]=\"true\"\n aria-label=\"IANA time zone\">\n </kendo-dropdownlist>\n </div>\n </div>\n <kendo-formhint>\n Basis for the time filter and for how all widgets display dates and times. Local time is the default.\n A specific time zone resolves civil days (today / yesterday / this month) DST-correctly in that\n zone and selects matching calendar rollups.\n </kendo-formhint>\n </kendo-formfield>\n\n <kendo-formfield>\n <div class=\"checkbox-wrapper\">\n <input\n type=\"checkbox\"\n kendoCheckBox\n #timeFilterCheckbox\n [(ngModel)]=\"timeFilterEnabled\"\n name=\"timeFilterEnabled\"\n id=\"timeFilterEnabled\"/>\n <kendo-label\n [for]=\"timeFilterCheckbox\"\n text=\"Enable Time Filter\"\n class=\"checkbox-label\">\n </kendo-label>\n </div>\n <kendo-formhint>\n Shows a time range picker in the toolbar. Sets $timeRangeFrom and $timeRangeTo variables.\n </kendo-formhint>\n </kendo-formfield>\n\n @if (timeFilterEnabled) {\n <kendo-formfield>\n <kendo-label text=\"Default Selection\"></kendo-label>\n <mm-time-range-picker\n [initialSelection]=\"initialDefaultSelection\"\n (selectionChange)=\"onDefaultSelectionChange($event)\">\n </mm-time-range-picker>\n <kendo-formhint>Initial time filter shown when no URL parameters are set. Note: User-selected filters override this default. Use the reset button in the toolbar to revert to the default.</kendo-formhint>\n </kendo-formfield>\n }\n </form>\n </div>\n </ng-template>\n </kendo-tabstrip-tab>\n\n <!-- Entity Selectors Tab -->\n <kendo-tabstrip-tab [title]=\"'Entity Selectors'\">\n <ng-template kendoTabContent>\n <div class=\"tab-content\">\n <mm-entity-selector-editor\n [(entitySelectors)]=\"entitySelectors\"\n [existingVariableNames]=\"staticVariableNames\"\n [variables]=\"variables\"\n (editingStateChange)=\"entitySelectorEditing = $event\">\n </mm-entity-selector-editor>\n </div>\n </ng-template>\n </kendo-tabstrip-tab>\n </kendo-tabstrip>\n\n <!-- Dialog Actions -->\n @if (!entitySelectorEditing) {\n <div class=\"dialog-actions mm-dialog-actions\">\n <button kendoButton (click)=\"cancel()\" fillMode=\"flat\">\n Cancel\n </button>\n <button\n kendoButton\n (click)=\"save()\"\n [disabled]=\"!isValid\"\n themeColor=\"primary\">\n Save\n </button>\n </div>\n }\n</div>\n", styles: [".meshboard-settings-dialog{display:flex;flex-direction:column;height:100%;overflow:hidden}.meshboard-settings-dialog kendo-tabstrip{flex:1;min-height:0;display:flex;flex-direction:column}.meshboard-settings-dialog kendo-tabstrip ::ng-deep .k-tabstrip-content{flex:1;min-height:0;overflow:hidden}.meshboard-settings-dialog .tab-content{height:100%;overflow-y:auto;padding:1.5rem}.meshboard-settings-dialog .settings-form{display:flex;flex-direction:column;gap:1.25rem}.meshboard-settings-dialog .settings-form kendo-formfield{display:flex;flex-direction:column;gap:.5rem}.meshboard-settings-dialog .settings-form kendo-formfield kendo-label{font-weight:500;color:var(--kendo-color-on-app-surface, #424242)}.meshboard-settings-dialog .settings-form kendo-formfield kendo-textbox,.meshboard-settings-dialog .settings-form kendo-formfield kendo-textarea,.meshboard-settings-dialog .settings-form kendo-formfield kendo-numerictextbox{width:100%}.meshboard-settings-dialog .settings-form kendo-formfield kendo-formhint{font-size:.75rem;color:var(--kendo-color-subtle, #757575)}.meshboard-settings-dialog .settings-form kendo-formfield kendo-formerror{font-size:.75rem;color:var(--kendo-color-error, #f44336)}.meshboard-settings-dialog .settings-form .section-title{font-size:.875rem;font-weight:600;color:var(--kendo-color-on-app-surface, #424242);margin-top:.5rem;padding-bottom:.5rem;border-bottom:1px solid var(--kendo-color-border, #e0e0e0)}.meshboard-settings-dialog .settings-form .checkbox-wrapper,.meshboard-settings-dialog .settings-form .radio-wrapper{display:flex;align-items:center;gap:.5rem}.meshboard-settings-dialog .settings-form .checkbox-wrapper .checkbox-label,.meshboard-settings-dialog .settings-form .checkbox-wrapper .radio-label,.meshboard-settings-dialog .settings-form .radio-wrapper .checkbox-label,.meshboard-settings-dialog .settings-form .radio-wrapper .radio-label{font-weight:400;cursor:pointer}.meshboard-settings-dialog .settings-form .checkbox-wrapper .tz-zone-select,.meshboard-settings-dialog .settings-form .radio-wrapper .tz-zone-select{min-width:12rem}.meshboard-settings-dialog .settings-form .radio-group{display:flex;flex-direction:column;gap:.5rem;margin-top:.25rem}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox]:not([ngNoCva])[formControlName],input[type=checkbox]:not([ngNoCva])[formControl],input[type=checkbox]:not([ngNoCva])[ngModel]" }, { kind: "directive", type: i1$1.RadioControlValueAccessor, selector: "input[type=radio]:not([ngNoCva])[formControlName],input[type=radio]:not([ngNoCva])[formControl],input[type=radio]:not([ngNoCva])[ngModel]", inputs: ["name", "formControlName", "value"] }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1$1.NgForm, selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: InputsModule }, { kind: "component", type: i3.TextBoxComponent, selector: "kendo-textbox", inputs: ["focusableId", "title", "type", "disabled", "readonly", "tabindex", "value", "selectOnFocus", "showSuccessIcon", "showErrorIcon", "clearButton", "successIcon", "successSvgIcon", "errorIcon", "errorSvgIcon", "clearButtonIcon", "clearButtonSvgIcon", "size", "rounded", "fillMode", "tabIndex", "placeholder", "maxlength", "inputAttributes"], outputs: ["valueChange", "inputFocus", "inputBlur", "focus", "blur"], exportAs: ["kendoTextBox"] }, { kind: "component", type: i3.NumericTextBoxComponent, selector: "kendo-numerictextbox", inputs: ["focusableId", "disabled", "readonly", "title", "autoCorrect", "format", "max", "min", "decimals", "placeholder", "step", "spinners", "rangeValidation", "tabindex", "tabIndex", "changeValueOnScroll", "selectOnFocus", "value", "maxlength", "size", "rounded", "fillMode", "inputAttributes"], outputs: ["valueChange", "focus", "blur", "inputFocus", "inputBlur"], exportAs: ["kendoNumericTextBox"] }, { kind: "component", type: i3.TextAreaComponent, selector: "kendo-textarea", inputs: ["focusableId", "flow", "inputAttributes", "adornmentsOrientation", "rows", "cols", "maxlength", "maxResizableRows", "tabindex", "tabIndex", "resizable", "size", "rounded", "fillMode", "showPrefixSeparator", "showSuffixSeparator"], outputs: ["focus", "blur", "valueChange"], exportAs: ["kendoTextArea"] }, { kind: "directive", type: i3.CheckBoxDirective, selector: "input[kendoCheckBox]", inputs: ["size", "rounded"] }, { kind: "directive", type: i3.RadioButtonDirective, selector: "input[kendoRadioButton]", inputs: ["size"] }, { kind: "component", type: i3.FormFieldComponent, selector: "kendo-formfield", inputs: ["showHints", "orientation", "showErrors", "colSpan"] }, { kind: "component", type: i3.HintComponent, selector: "kendo-formhint", inputs: ["align"] }, { kind: "component", type: i3.ErrorComponent, selector: "kendo-formerror", inputs: ["align"] }, { kind: "ngmodule", type: CheckBoxModule }, { kind: "ngmodule", type: LabelModule }, { kind: "component", type: i4$1.LabelComponent, selector: "kendo-label", inputs: ["text", "for", "optional", "labelCssStyle", "labelCssClass"], exportAs: ["kendoLabel"] }, { kind: "ngmodule", type: FormFieldModule }, { kind: "ngmodule", type: TabStripModule }, { kind: "component", type: i5.TabStripComponent, selector: "kendo-tabstrip", inputs: ["height", "animate", "tabAlignment", "tabPosition", "keepTabContent", "closable", "scrollable", "size", "closeIcon", "closeIconClass", "closeSVGIcon", "showContentArea"], outputs: ["tabSelect", "tabClose", "tabScroll"], exportAs: ["kendoTabStrip"] }, { kind: "component", type: i5.TabStripTabComponent, selector: "kendo-tabstrip-tab", inputs: ["title", "disabled", "cssClass", "cssStyle", "selected", "closable", "closeIcon", "closeIconClass", "closeSVGIcon"], exportAs: ["kendoTabStripTab"] }, { kind: "directive", type: i5.TabContentDirective, selector: "[kendoTabContent]" }, { kind: "ngmodule", type: DropDownListModule }, { 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: VariablesEditorComponent, selector: "mm-variables-editor", inputs: ["variables"], outputs: ["variablesChange"] }, { kind: "component", type: EntitySelectorEditorComponent, selector: "mm-entity-selector-editor", inputs: ["entitySelectors", "existingVariableNames", "variables"], outputs: ["entitySelectorsChange", "editingStateChange"] }, { kind: "component", type: TimeRangePickerComponent, selector: "mm-time-range-picker", inputs: ["config", "labels", "initialSelection"], outputs: ["rangeChange", "rangeChangeISO", "selectionChange"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
33634
35090
  }
33635
35091
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: MeshBoardSettingsDialogComponent, decorators: [{
33636
35092
  type: Component,
@@ -33647,7 +35103,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
33647
35103
  VariablesEditorComponent,
33648
35104
  EntitySelectorEditorComponent,
33649
35105
  TimeRangePickerComponent
33650
- ], changeDetection: ChangeDetectionStrategy.Eager, template: "<div class=\"meshboard-settings-dialog\">\n <kendo-tabstrip [animate]=\"false\">\n <!-- General Tab -->\n <kendo-tabstrip-tab [title]=\"'General'\" [selected]=\"true\">\n <ng-template kendoTabContent>\n <div class=\"tab-content\">\n <form class=\"settings-form\">\n <!-- Name Field -->\n <kendo-formfield>\n <kendo-label [for]=\"nameInput\" text=\"Name *\"></kendo-label>\n <kendo-textbox\n #nameInput\n [(ngModel)]=\"name\"\n name=\"name\"\n placeholder=\"Enter MeshBoard name\"\n required>\n </kendo-textbox>\n @if (name.trim().length === 0) {\n <kendo-formerror>Name is required</kendo-formerror>\n }\n </kendo-formfield>\n\n <!-- Description Field -->\n <kendo-formfield>\n <kendo-label [for]=\"descriptionInput\" text=\"Description\"></kendo-label>\n <kendo-textarea\n #descriptionInput\n [(ngModel)]=\"description\"\n name=\"description\"\n placeholder=\"Enter MeshBoard description (optional)\"\n [rows]=\"3\">\n </kendo-textarea>\n </kendo-formfield>\n\n <!-- Well-Known Name Field -->\n <kendo-formfield>\n <kendo-label [for]=\"wellKnownNameInput\" text=\"Well-Known Name\"></kendo-label>\n <kendo-textbox\n #wellKnownNameInput\n [(ngModel)]=\"rtWellKnownName\"\n name=\"rtWellKnownName\"\n placeholder=\"e.g., cockpit, dashboard-main\">\n </kendo-textbox>\n <kendo-formhint>Unique identifier for routing. Use lowercase with hyphens (e.g., 'cockpit', 'sales-dashboard').</kendo-formhint>\n </kendo-formfield>\n\n <!-- Layout Settings -->\n <div class=\"section-title\">Layout Settings</div>\n\n <!-- Columns Field -->\n <kendo-formfield>\n <kendo-label [for]=\"columnsInput\" text=\"Columns *\"></kendo-label>\n <kendo-numerictextbox\n #columnsInput\n [(ngModel)]=\"columns\"\n name=\"columns\"\n [min]=\"1\"\n [max]=\"12\"\n [decimals]=\"0\"\n [format]=\"'n0'\"\n [spinners]=\"true\"\n required>\n </kendo-numerictextbox>\n <kendo-formhint>Number of columns in the grid (1-12)</kendo-formhint>\n @if (columns < 1 || columns > 12) {\n <kendo-formerror>Columns must be between 1 and 12</kendo-formerror>\n }\n </kendo-formfield>\n\n <!-- Row Height Field -->\n <kendo-formfield>\n <kendo-label [for]=\"rowHeightInput\" text=\"Row Height *\"></kendo-label>\n <kendo-numerictextbox\n #rowHeightInput\n [(ngModel)]=\"rowHeight\"\n name=\"rowHeight\"\n [min]=\"100\"\n [max]=\"1000\"\n [decimals]=\"0\"\n [format]=\"'n0'\"\n [spinners]=\"true\"\n required>\n </kendo-numerictextbox>\n <kendo-formhint>Height of each row in pixels (100-1000)</kendo-formhint>\n @if (rowHeight < 100 || rowHeight > 1000) {\n <kendo-formerror>Row height must be between 100 and 1000</kendo-formerror>\n }\n </kendo-formfield>\n\n <!-- Gap Field -->\n <kendo-formfield>\n <kendo-label [for]=\"gapInput\" text=\"Gap *\"></kendo-label>\n <kendo-numerictextbox\n #gapInput\n [(ngModel)]=\"gap\"\n name=\"gap\"\n [min]=\"0\"\n [max]=\"100\"\n [decimals]=\"0\"\n [format]=\"'n0'\"\n [spinners]=\"true\"\n required>\n </kendo-numerictextbox>\n <kendo-formhint>Space between widgets in pixels (0-100)</kendo-formhint>\n @if (gap < 0 || gap > 100) {\n <kendo-formerror>Gap must be between 0 and 100</kendo-formerror>\n }\n </kendo-formfield>\n\n <!-- Auto-Refresh Field -->\n <kendo-formfield>\n <kendo-label [for]=\"autoRefreshInput\" text=\"Auto-refresh\"></kendo-label>\n <kendo-numerictextbox\n #autoRefreshInput\n [(ngModel)]=\"autoRefreshSeconds\"\n name=\"autoRefreshSeconds\"\n [min]=\"0\"\n [max]=\"3600\"\n [decimals]=\"0\"\n [format]=\"'n0'\"\n [spinners]=\"true\">\n </kendo-numerictextbox>\n <kendo-formhint>Refresh interval in seconds (0 = disabled, max 3600). Pauses while the tab is hidden.</kendo-formhint>\n @if (autoRefreshSeconds < 0 || autoRefreshSeconds > 3600) {\n <kendo-formerror>Auto-refresh must be between 0 and 3600 seconds</kendo-formerror>\n }\n </kendo-formfield>\n </form>\n </div>\n </ng-template>\n </kendo-tabstrip-tab>\n\n <!-- Variables Tab -->\n <kendo-tabstrip-tab [title]=\"'Variables'\">\n <ng-template kendoTabContent>\n <div class=\"tab-content\">\n <mm-variables-editor\n [(variables)]=\"variables\">\n </mm-variables-editor>\n </div>\n </ng-template>\n </kendo-tabstrip-tab>\n\n <!-- Time Filter Tab -->\n <kendo-tabstrip-tab [title]=\"'Time Filter'\">\n <ng-template kendoTabContent>\n <div class=\"tab-content\">\n <form class=\"settings-form\">\n <kendo-formfield>\n <kendo-label text=\"Timezone\"></kendo-label>\n <div class=\"radio-group\">\n <div class=\"radio-wrapper\">\n <input\n type=\"radio\"\n kendoRadioButton\n #tzLocal\n [(ngModel)]=\"timeZoneMode\"\n name=\"timeZoneMode\"\n value=\"local\"\n id=\"timeZoneModeLocal\"/>\n <kendo-label [for]=\"tzLocal\" text=\"Local time (browser)\" class=\"radio-label\"></kendo-label>\n </div>\n <div class=\"radio-wrapper\">\n <input\n type=\"radio\"\n kendoRadioButton\n #tzUtc\n [(ngModel)]=\"timeZoneMode\"\n name=\"timeZoneMode\"\n value=\"utc\"\n id=\"timeZoneModeUtc\"/>\n <kendo-label [for]=\"tzUtc\" text=\"UTC\" class=\"radio-label\"></kendo-label>\n </div>\n <div class=\"radio-wrapper\">\n <input\n type=\"radio\"\n kendoRadioButton\n #tzZone\n [(ngModel)]=\"timeZoneMode\"\n name=\"timeZoneMode\"\n [value]=\"zoneSelection\"\n id=\"timeZoneModeZone\"/>\n <kendo-label [for]=\"tzZone\" text=\"Specific time zone\" class=\"radio-label\"></kendo-label>\n <kendo-dropdownlist\n class=\"tz-zone-select\"\n [data]=\"commonTimeZones\"\n [value]=\"zoneSelection\"\n (valueChange)=\"onZoneSelectionChange($event)\"\n [disabled]=\"!isSpecificZone\"\n [filterable]=\"true\"\n aria-label=\"IANA time zone\">\n </kendo-dropdownlist>\n </div>\n </div>\n <kendo-formhint>\n Basis for the time filter and for how all widgets display dates and times. Local time is the default.\n A specific time zone resolves civil days (today / yesterday / this month) DST-correctly in that\n zone and selects matching calendar rollups.\n </kendo-formhint>\n </kendo-formfield>\n\n <kendo-formfield>\n <div class=\"checkbox-wrapper\">\n <input\n type=\"checkbox\"\n kendoCheckBox\n #timeFilterCheckbox\n [(ngModel)]=\"timeFilterEnabled\"\n name=\"timeFilterEnabled\"\n id=\"timeFilterEnabled\"/>\n <kendo-label\n [for]=\"timeFilterCheckbox\"\n text=\"Enable Time Filter\"\n class=\"checkbox-label\">\n </kendo-label>\n </div>\n <kendo-formhint>\n Shows a time range picker in the toolbar. Sets $timeRangeFrom and $timeRangeTo variables.\n </kendo-formhint>\n </kendo-formfield>\n\n @if (timeFilterEnabled) {\n <kendo-formfield>\n <kendo-label text=\"Default Selection\"></kendo-label>\n <mm-time-range-picker\n [initialSelection]=\"initialDefaultSelection\"\n (selectionChange)=\"onDefaultSelectionChange($event)\">\n </mm-time-range-picker>\n <kendo-formhint>Initial time filter shown when no URL parameters are set. Note: User-selected filters override this default. Use the reset button in the toolbar to revert to the default.</kendo-formhint>\n </kendo-formfield>\n }\n </form>\n </div>\n </ng-template>\n </kendo-tabstrip-tab>\n\n <!-- Entity Selectors Tab -->\n <kendo-tabstrip-tab [title]=\"'Entity Selectors'\">\n <ng-template kendoTabContent>\n <div class=\"tab-content\">\n <mm-entity-selector-editor\n [(entitySelectors)]=\"entitySelectors\"\n [existingVariableNames]=\"staticVariableNames\"\n (editingStateChange)=\"entitySelectorEditing = $event\">\n </mm-entity-selector-editor>\n </div>\n </ng-template>\n </kendo-tabstrip-tab>\n </kendo-tabstrip>\n\n <!-- Dialog Actions -->\n @if (!entitySelectorEditing) {\n <div class=\"dialog-actions mm-dialog-actions\">\n <button kendoButton (click)=\"cancel()\" fillMode=\"flat\">\n Cancel\n </button>\n <button\n kendoButton\n (click)=\"save()\"\n [disabled]=\"!isValid\"\n themeColor=\"primary\">\n Save\n </button>\n </div>\n }\n</div>\n", styles: [".meshboard-settings-dialog{display:flex;flex-direction:column;height:100%;overflow:hidden}.meshboard-settings-dialog kendo-tabstrip{flex:1;min-height:0;display:flex;flex-direction:column}.meshboard-settings-dialog kendo-tabstrip ::ng-deep .k-tabstrip-content{flex:1;min-height:0;overflow:hidden}.meshboard-settings-dialog .tab-content{height:100%;overflow-y:auto;padding:1.5rem}.meshboard-settings-dialog .settings-form{display:flex;flex-direction:column;gap:1.25rem}.meshboard-settings-dialog .settings-form kendo-formfield{display:flex;flex-direction:column;gap:.5rem}.meshboard-settings-dialog .settings-form kendo-formfield kendo-label{font-weight:500;color:var(--kendo-color-on-app-surface, #424242)}.meshboard-settings-dialog .settings-form kendo-formfield kendo-textbox,.meshboard-settings-dialog .settings-form kendo-formfield kendo-textarea,.meshboard-settings-dialog .settings-form kendo-formfield kendo-numerictextbox{width:100%}.meshboard-settings-dialog .settings-form kendo-formfield kendo-formhint{font-size:.75rem;color:var(--kendo-color-subtle, #757575)}.meshboard-settings-dialog .settings-form kendo-formfield kendo-formerror{font-size:.75rem;color:var(--kendo-color-error, #f44336)}.meshboard-settings-dialog .settings-form .section-title{font-size:.875rem;font-weight:600;color:var(--kendo-color-on-app-surface, #424242);margin-top:.5rem;padding-bottom:.5rem;border-bottom:1px solid var(--kendo-color-border, #e0e0e0)}.meshboard-settings-dialog .settings-form .checkbox-wrapper,.meshboard-settings-dialog .settings-form .radio-wrapper{display:flex;align-items:center;gap:.5rem}.meshboard-settings-dialog .settings-form .checkbox-wrapper .checkbox-label,.meshboard-settings-dialog .settings-form .checkbox-wrapper .radio-label,.meshboard-settings-dialog .settings-form .radio-wrapper .checkbox-label,.meshboard-settings-dialog .settings-form .radio-wrapper .radio-label{font-weight:400;cursor:pointer}.meshboard-settings-dialog .settings-form .checkbox-wrapper .tz-zone-select,.meshboard-settings-dialog .settings-form .radio-wrapper .tz-zone-select{min-width:12rem}.meshboard-settings-dialog .settings-form .radio-group{display:flex;flex-direction:column;gap:.5rem;margin-top:.25rem}\n"] }]
35106
+ ], changeDetection: ChangeDetectionStrategy.Eager, template: "<div class=\"meshboard-settings-dialog\">\n <kendo-tabstrip [animate]=\"false\">\n <!-- General Tab -->\n <kendo-tabstrip-tab [title]=\"'General'\" [selected]=\"true\">\n <ng-template kendoTabContent>\n <div class=\"tab-content\">\n <form class=\"settings-form\">\n <!-- Name Field -->\n <kendo-formfield>\n <kendo-label [for]=\"nameInput\" text=\"Name *\"></kendo-label>\n <kendo-textbox\n #nameInput\n [(ngModel)]=\"name\"\n name=\"name\"\n placeholder=\"Enter MeshBoard name\"\n required>\n </kendo-textbox>\n @if (name.trim().length === 0) {\n <kendo-formerror>Name is required</kendo-formerror>\n }\n </kendo-formfield>\n\n <!-- Description Field -->\n <kendo-formfield>\n <kendo-label [for]=\"descriptionInput\" text=\"Description\"></kendo-label>\n <kendo-textarea\n #descriptionInput\n [(ngModel)]=\"description\"\n name=\"description\"\n placeholder=\"Enter MeshBoard description (optional)\"\n [rows]=\"3\">\n </kendo-textarea>\n </kendo-formfield>\n\n <!-- Well-Known Name Field -->\n <kendo-formfield>\n <kendo-label [for]=\"wellKnownNameInput\" text=\"Well-Known Name\"></kendo-label>\n <kendo-textbox\n #wellKnownNameInput\n [(ngModel)]=\"rtWellKnownName\"\n name=\"rtWellKnownName\"\n placeholder=\"e.g., cockpit, dashboard-main\">\n </kendo-textbox>\n <kendo-formhint>Unique identifier for routing. Use lowercase with hyphens (e.g., 'cockpit', 'sales-dashboard').</kendo-formhint>\n </kendo-formfield>\n\n <!-- Layout Settings -->\n <div class=\"section-title\">Layout Settings</div>\n\n <!-- Columns Field -->\n <kendo-formfield>\n <kendo-label [for]=\"columnsInput\" text=\"Columns *\"></kendo-label>\n <kendo-numerictextbox\n #columnsInput\n [(ngModel)]=\"columns\"\n name=\"columns\"\n [min]=\"1\"\n [max]=\"12\"\n [decimals]=\"0\"\n [format]=\"'n0'\"\n [spinners]=\"true\"\n required>\n </kendo-numerictextbox>\n <kendo-formhint>Number of columns in the grid (1-12)</kendo-formhint>\n @if (columns < 1 || columns > 12) {\n <kendo-formerror>Columns must be between 1 and 12</kendo-formerror>\n }\n </kendo-formfield>\n\n <!-- Row Height Field -->\n <kendo-formfield>\n <kendo-label [for]=\"rowHeightInput\" text=\"Row Height *\"></kendo-label>\n <kendo-numerictextbox\n #rowHeightInput\n [(ngModel)]=\"rowHeight\"\n name=\"rowHeight\"\n [min]=\"100\"\n [max]=\"1000\"\n [decimals]=\"0\"\n [format]=\"'n0'\"\n [spinners]=\"true\"\n required>\n </kendo-numerictextbox>\n <kendo-formhint>Height of each row in pixels (100-1000)</kendo-formhint>\n @if (rowHeight < 100 || rowHeight > 1000) {\n <kendo-formerror>Row height must be between 100 and 1000</kendo-formerror>\n }\n </kendo-formfield>\n\n <!-- Gap Field -->\n <kendo-formfield>\n <kendo-label [for]=\"gapInput\" text=\"Gap *\"></kendo-label>\n <kendo-numerictextbox\n #gapInput\n [(ngModel)]=\"gap\"\n name=\"gap\"\n [min]=\"0\"\n [max]=\"100\"\n [decimals]=\"0\"\n [format]=\"'n0'\"\n [spinners]=\"true\"\n required>\n </kendo-numerictextbox>\n <kendo-formhint>Space between widgets in pixels (0-100)</kendo-formhint>\n @if (gap < 0 || gap > 100) {\n <kendo-formerror>Gap must be between 0 and 100</kendo-formerror>\n }\n </kendo-formfield>\n\n <!-- Auto-Refresh Field -->\n <kendo-formfield>\n <kendo-label [for]=\"autoRefreshInput\" text=\"Auto-refresh\"></kendo-label>\n <kendo-numerictextbox\n #autoRefreshInput\n [(ngModel)]=\"autoRefreshSeconds\"\n name=\"autoRefreshSeconds\"\n [min]=\"0\"\n [max]=\"3600\"\n [decimals]=\"0\"\n [format]=\"'n0'\"\n [spinners]=\"true\">\n </kendo-numerictextbox>\n <kendo-formhint>Refresh interval in seconds (0 = disabled, max 3600). Pauses while the tab is hidden.</kendo-formhint>\n @if (autoRefreshSeconds < 0 || autoRefreshSeconds > 3600) {\n <kendo-formerror>Auto-refresh must be between 0 and 3600 seconds</kendo-formerror>\n }\n </kendo-formfield>\n </form>\n </div>\n </ng-template>\n </kendo-tabstrip-tab>\n\n <!-- Variables Tab -->\n <kendo-tabstrip-tab [title]=\"'Variables'\">\n <ng-template kendoTabContent>\n <div class=\"tab-content\">\n <mm-variables-editor\n [(variables)]=\"variables\">\n </mm-variables-editor>\n </div>\n </ng-template>\n </kendo-tabstrip-tab>\n\n <!-- Time Filter Tab -->\n <kendo-tabstrip-tab [title]=\"'Time Filter'\">\n <ng-template kendoTabContent>\n <div class=\"tab-content\">\n <form class=\"settings-form\">\n <kendo-formfield>\n <kendo-label text=\"Timezone\"></kendo-label>\n <div class=\"radio-group\">\n <div class=\"radio-wrapper\">\n <input\n type=\"radio\"\n kendoRadioButton\n #tzLocal\n [(ngModel)]=\"timeZoneMode\"\n name=\"timeZoneMode\"\n value=\"local\"\n id=\"timeZoneModeLocal\"/>\n <kendo-label [for]=\"tzLocal\" text=\"Local time (browser)\" class=\"radio-label\"></kendo-label>\n </div>\n <div class=\"radio-wrapper\">\n <input\n type=\"radio\"\n kendoRadioButton\n #tzUtc\n [(ngModel)]=\"timeZoneMode\"\n name=\"timeZoneMode\"\n value=\"utc\"\n id=\"timeZoneModeUtc\"/>\n <kendo-label [for]=\"tzUtc\" text=\"UTC\" class=\"radio-label\"></kendo-label>\n </div>\n <div class=\"radio-wrapper\">\n <input\n type=\"radio\"\n kendoRadioButton\n #tzZone\n [(ngModel)]=\"timeZoneMode\"\n name=\"timeZoneMode\"\n [value]=\"zoneSelection\"\n id=\"timeZoneModeZone\"/>\n <kendo-label [for]=\"tzZone\" text=\"Specific time zone\" class=\"radio-label\"></kendo-label>\n <kendo-dropdownlist\n class=\"tz-zone-select\"\n [data]=\"commonTimeZones\"\n [value]=\"zoneSelection\"\n (valueChange)=\"onZoneSelectionChange($event)\"\n [disabled]=\"!isSpecificZone\"\n [filterable]=\"true\"\n aria-label=\"IANA time zone\">\n </kendo-dropdownlist>\n </div>\n </div>\n <kendo-formhint>\n Basis for the time filter and for how all widgets display dates and times. Local time is the default.\n A specific time zone resolves civil days (today / yesterday / this month) DST-correctly in that\n zone and selects matching calendar rollups.\n </kendo-formhint>\n </kendo-formfield>\n\n <kendo-formfield>\n <div class=\"checkbox-wrapper\">\n <input\n type=\"checkbox\"\n kendoCheckBox\n #timeFilterCheckbox\n [(ngModel)]=\"timeFilterEnabled\"\n name=\"timeFilterEnabled\"\n id=\"timeFilterEnabled\"/>\n <kendo-label\n [for]=\"timeFilterCheckbox\"\n text=\"Enable Time Filter\"\n class=\"checkbox-label\">\n </kendo-label>\n </div>\n <kendo-formhint>\n Shows a time range picker in the toolbar. Sets $timeRangeFrom and $timeRangeTo variables.\n </kendo-formhint>\n </kendo-formfield>\n\n @if (timeFilterEnabled) {\n <kendo-formfield>\n <kendo-label text=\"Default Selection\"></kendo-label>\n <mm-time-range-picker\n [initialSelection]=\"initialDefaultSelection\"\n (selectionChange)=\"onDefaultSelectionChange($event)\">\n </mm-time-range-picker>\n <kendo-formhint>Initial time filter shown when no URL parameters are set. Note: User-selected filters override this default. Use the reset button in the toolbar to revert to the default.</kendo-formhint>\n </kendo-formfield>\n }\n </form>\n </div>\n </ng-template>\n </kendo-tabstrip-tab>\n\n <!-- Entity Selectors Tab -->\n <kendo-tabstrip-tab [title]=\"'Entity Selectors'\">\n <ng-template kendoTabContent>\n <div class=\"tab-content\">\n <mm-entity-selector-editor\n [(entitySelectors)]=\"entitySelectors\"\n [existingVariableNames]=\"staticVariableNames\"\n [variables]=\"variables\"\n (editingStateChange)=\"entitySelectorEditing = $event\">\n </mm-entity-selector-editor>\n </div>\n </ng-template>\n </kendo-tabstrip-tab>\n </kendo-tabstrip>\n\n <!-- Dialog Actions -->\n @if (!entitySelectorEditing) {\n <div class=\"dialog-actions mm-dialog-actions\">\n <button kendoButton (click)=\"cancel()\" fillMode=\"flat\">\n Cancel\n </button>\n <button\n kendoButton\n (click)=\"save()\"\n [disabled]=\"!isValid\"\n themeColor=\"primary\">\n Save\n </button>\n </div>\n }\n</div>\n", styles: [".meshboard-settings-dialog{display:flex;flex-direction:column;height:100%;overflow:hidden}.meshboard-settings-dialog kendo-tabstrip{flex:1;min-height:0;display:flex;flex-direction:column}.meshboard-settings-dialog kendo-tabstrip ::ng-deep .k-tabstrip-content{flex:1;min-height:0;overflow:hidden}.meshboard-settings-dialog .tab-content{height:100%;overflow-y:auto;padding:1.5rem}.meshboard-settings-dialog .settings-form{display:flex;flex-direction:column;gap:1.25rem}.meshboard-settings-dialog .settings-form kendo-formfield{display:flex;flex-direction:column;gap:.5rem}.meshboard-settings-dialog .settings-form kendo-formfield kendo-label{font-weight:500;color:var(--kendo-color-on-app-surface, #424242)}.meshboard-settings-dialog .settings-form kendo-formfield kendo-textbox,.meshboard-settings-dialog .settings-form kendo-formfield kendo-textarea,.meshboard-settings-dialog .settings-form kendo-formfield kendo-numerictextbox{width:100%}.meshboard-settings-dialog .settings-form kendo-formfield kendo-formhint{font-size:.75rem;color:var(--kendo-color-subtle, #757575)}.meshboard-settings-dialog .settings-form kendo-formfield kendo-formerror{font-size:.75rem;color:var(--kendo-color-error, #f44336)}.meshboard-settings-dialog .settings-form .section-title{font-size:.875rem;font-weight:600;color:var(--kendo-color-on-app-surface, #424242);margin-top:.5rem;padding-bottom:.5rem;border-bottom:1px solid var(--kendo-color-border, #e0e0e0)}.meshboard-settings-dialog .settings-form .checkbox-wrapper,.meshboard-settings-dialog .settings-form .radio-wrapper{display:flex;align-items:center;gap:.5rem}.meshboard-settings-dialog .settings-form .checkbox-wrapper .checkbox-label,.meshboard-settings-dialog .settings-form .checkbox-wrapper .radio-label,.meshboard-settings-dialog .settings-form .radio-wrapper .checkbox-label,.meshboard-settings-dialog .settings-form .radio-wrapper .radio-label{font-weight:400;cursor:pointer}.meshboard-settings-dialog .settings-form .checkbox-wrapper .tz-zone-select,.meshboard-settings-dialog .settings-form .radio-wrapper .tz-zone-select{min-width:12rem}.meshboard-settings-dialog .settings-form .radio-group{display:flex;flex-direction:column;gap:.5rem;margin-top:.25rem}\n"] }]
33651
35107
  }], ctorParameters: () => [] });
33652
35108
 
33653
35109
  /**
@@ -34706,6 +36162,46 @@ class MeshBoardViewComponent {
34706
36162
  ...(ngDevMode ? [{ debugName: "routeChrome" }] : /* istanbul ignore next */ []));
34707
36163
  effectiveChrome = computed(() => resolveMeshBoardChrome(this.chrome(), this.routeChrome()), /* @ts-ignore */
34708
36164
  ...(ngDevMode ? [{ debugName: "effectiveChrome" }] : /* istanbul ignore next */ []));
36165
+ /**
36166
+ * The period set by the host page (AB#7582): two exact instants, half-open `[from, to)`, as
36167
+ * `Date` objects or ISO 8601 strings with a zone designator. While set (and the board's time
36168
+ * filter is enabled) the view publishes exactly these instants as `$timeRangeFrom` /
36169
+ * `$timeRangeTo` and as the stream-data window - no day snapping, no zone conversion - and
36170
+ * ignores the `tf_*` query parameters and the stored/default selection. It writes no `tf_*`
36171
+ * parameters and causes no unsaved changes. A change reloads the widgets once; `undefined`
36172
+ * returns the board to its own selection. Day rules (e.g. a 06:00 gas day) belong to the host.
36173
+ * An invalid range (unparseable, or `from >= to`) sends no stream-data query and logs one
36174
+ * console error. A board with the time filter disabled ignores the input.
36175
+ */
36176
+ timeRange = input(undefined, /* @ts-ignore */
36177
+ ...(ngDevMode ? [{ debugName: "timeRange" }] : /* istanbul ignore next */ []));
36178
+ /**
36179
+ * Who controls the period: `board` (default, unchanged behaviour) shows the board's time-range
36180
+ * picker; `host` hides the picker and its reset button - entity selectors and actions stay.
36181
+ * While `timeRange` is set the picker is hidden and its events are ignored either way.
36182
+ */
36183
+ timeFilterControl = input('board', /* @ts-ignore */
36184
+ ...(ngDevMode ? [{ debugName: "timeFilterControl" }] : /* istanbul ignore next */ []));
36185
+ /** The parsed `timeRange` input; `undefined` = the host sets no period. */
36186
+ parsedHostTimeRange = computed(() => parseHostTimeRange(this.timeRange()), /* @ts-ignore */
36187
+ ...(ngDevMode ? [{ debugName: "parsedHostTimeRange" }] : /* istanbul ignore next */ []));
36188
+ /** True while the host owns the period (a valid or an invalid `timeRange` is set). */
36189
+ isHostTimeRangeActive = computed(() => this.parsedHostTimeRange() !== undefined, /* @ts-ignore */
36190
+ ...(ngDevMode ? [{ debugName: "isHostTimeRangeActive" }] : /* istanbul ignore next */ []));
36191
+ /** Whether the board's own time-range picker (and its reset button) is rendered. */
36192
+ showTimeRangePicker = computed(() => this.isTimeFilterEnabled() && this.timeFilterControl() !== 'host' && !this.isHostTimeRangeActive(), /* @ts-ignore */
36193
+ ...(ngDevMode ? [{ debugName: "showTimeRangePicker" }] : /* istanbul ignore next */ []));
36194
+ /** The host range last applied to the state service (to reload only on a real change). */
36195
+ appliedHostTimeRange;
36196
+ /** The invalid host range already reported, so a board switch does not log it again. */
36197
+ reportedInvalidHostTimeRange;
36198
+ /**
36199
+ * Set once the host has owned the period: the `tf_*` parameters of the URL this view was opened
36200
+ * with are stale from then on and are not applied again when `timeRange` goes back to undefined.
36201
+ */
36202
+ hostHasOwnedPeriod = false;
36203
+ /** Set in ngOnDestroy; the async initial load stops touching shared state after it (AB#7582). */
36204
+ destroyed = false;
34709
36205
  // Computed link to MeshBoard page with tenant
34710
36206
  meshBoardPageLink = computed(() => {
34711
36207
  // Get tenant from route parameters (synchronously from snapshot)
@@ -34787,6 +36283,9 @@ class MeshBoardViewComponent {
34787
36283
  // Entity Selector computed signals
34788
36284
  entitySelectorsConfig = computed(() => this.stateService.getEntitySelectors(), /* @ts-ignore */
34789
36285
  ...(ngDevMode ? [{ debugName: "entitySelectorsConfig" }] : /* istanbul ignore next */ []));
36286
+ /** Board variables substituted into the selectors' picker filters (AB#7724). */
36287
+ boardVariables = computed(() => this.stateService.getVariables(), /* @ts-ignore */
36288
+ ...(ngDevMode ? [{ debugName: "boardVariables" }] : /* istanbul ignore next */ []));
34790
36289
  hasEntitySelectors = computed(() => !this.hideEntitySelectors() && (this.entitySelectorsConfig()?.some(es => showsInToolbar(es)) ?? false), /* @ts-ignore */
34791
36290
  ...(ngDevMode ? [{ debugName: "hasEntitySelectors" }] : /* istanbul ignore next */ []));
34792
36291
  unselectedToolbarSelectors = computed(() => {
@@ -34828,6 +36327,15 @@ class MeshBoardViewComponent {
34828
36327
  }
34829
36328
  }
34830
36329
  });
36330
+ // Host period (AB#7582): apply every change of the timeRange input after the initial load
36331
+ // (ngOnInit applies the initial value) and reload the widgets once.
36332
+ effect(() => {
36333
+ const parsed = this.parsedHostTimeRange();
36334
+ if (!this._isInitialized()) {
36335
+ return;
36336
+ }
36337
+ untracked(() => this.onHostTimeRangeChanged(parsed));
36338
+ });
34831
36339
  // Effect to re-evaluate the auto-refresh timer whenever the MeshBoard
34832
36340
  // config changes (e.g. user edits autoRefreshSeconds in settings, or a
34833
36341
  // different board is loaded). Re-reads on the same interval skip the
@@ -34881,8 +36389,16 @@ class MeshBoardViewComponent {
34881
36389
  this.router.navigateByUrl(applyTimeFilterParams(targetUrl, this.activeTimeFilterSelection()), { replaceUrl: true });
34882
36390
  }
34883
36391
  }
34884
- /** The active time-filter selection to mirror into the URL, or null when the filter is off. */
36392
+ /**
36393
+ * The active time-filter selection to mirror into the URL, or null when the filter is off or
36394
+ * the host owns the period or controls the filter (AB#7582: the view then writes no tf_*
36395
+ * parameters; with `timeFilterControl='host'` and no `timeRange` the board selection still
36396
+ * applies, but the URL is the host's).
36397
+ */
34885
36398
  activeTimeFilterSelection() {
36399
+ if (this.isHostTimeRangeActive() || this.timeFilterControl() === 'host') {
36400
+ return null;
36401
+ }
34886
36402
  const config = this.stateService.getTimeFilterConfig();
34887
36403
  return config?.enabled ? config.selection ?? null : null;
34888
36404
  }
@@ -34939,10 +36455,18 @@ class MeshBoardViewComponent {
34939
36455
  this.primeEntitySelectorScopes();
34940
36456
  await this.preloadWidgetData(widgets);
34941
36457
  }
36458
+ // The view may have been destroyed while the board loaded (e.g. a fast switch on the host
36459
+ // page): it must not publish its host period or selection into the root-provided state.
36460
+ if (this.destroyed) {
36461
+ return;
36462
+ }
34942
36463
  // Initialize time filter variables if enabled with stored selection
34943
36464
  this.initializeTimeFilterVariables();
34944
36465
  // Initialize entity selectors from URL params, route data, or defaults
34945
36466
  await this.initializeEntitySelectors();
36467
+ if (this.destroyed) {
36468
+ return;
36469
+ }
34946
36470
  // Mark initial load as complete so the effect can handle subsequent board switches
34947
36471
  this.initialLoadComplete = true;
34948
36472
  // `?edit=1` (e.g. "Edit" in a host's board list) opens the board in edit mode;
@@ -35009,6 +36533,14 @@ class MeshBoardViewComponent {
35009
36533
  }
35010
36534
  }
35011
36535
  ngOnDestroy() {
36536
+ this.destroyed = true;
36537
+ // The state service is root-provided: a host period must not outlive the view that set it.
36538
+ // Owner-aware, so a period another view set in the meantime stays. Only a view that set a
36539
+ // period clears one (the default path never touches it); a view destroyed while its board was
36540
+ // still loading never set one (see the guards after the awaits in ngOnInit).
36541
+ if (this.hostHasOwnedPeriod) {
36542
+ this.stateService.clearHostTimeRange(this);
36543
+ }
35012
36544
  this.resizeObserver?.disconnect();
35013
36545
  this.closeConfigDialog();
35014
36546
  this.navigationSubscription?.unsubscribe();
@@ -35051,14 +36583,19 @@ class MeshBoardViewComponent {
35051
36583
  /**
35052
36584
  * Initializes time filter variables based on URL query params or stored selection.
35053
36585
  * URL params take precedence over stored selection to support page reload.
36586
+ * A period set by the host (`timeRange`, AB#7582) takes precedence over both.
35054
36587
  */
35055
36588
  initializeTimeFilterVariables() {
36589
+ if (this.applyHostTimeRange(this.parsedHostTimeRange())) {
36590
+ return;
36591
+ }
35056
36592
  const timeFilter = this.stateService.getTimeFilterConfig();
35057
36593
  if (!timeFilter?.enabled) {
35058
36594
  return;
35059
36595
  }
35060
- // Check URL query params first (takes precedence over stored selection)
35061
- const urlSelection = this.readTimeFilterFromUrl();
36596
+ // Check URL query params first (takes precedence over stored selection). Once the host has
36597
+ // owned the period, the tf_* of the opening URL are stale and are not applied again.
36598
+ const urlSelection = this.hostHasOwnedPeriod ? null : this.readTimeFilterFromUrl();
35062
36599
  if (urlSelection) {
35063
36600
  // Signal the URL selection so the picker picks it up
35064
36601
  this._urlTimeSelection.set(urlSelection);
@@ -35098,6 +36635,59 @@ class MeshBoardViewComponent {
35098
36635
  const rangeISO = TimeRangeUtils.toISO(range);
35099
36636
  this.stateService.setTimeFilterVariables(rangeISO.from, rangeISO.to);
35100
36637
  }
36638
+ /**
36639
+ * Hands the host period to the state service and publishes `$timeRangeFrom` / `$timeRangeTo`
36640
+ * as exactly its instants (AB#7582). Returns true while the host owns the period, so the
36641
+ * caller skips the board's own selection. With the time filter disabled the period is only
36642
+ * stored (the state service ignores it then) and nothing is logged.
36643
+ */
36644
+ applyHostTimeRange(parsed) {
36645
+ if (this.destroyed) {
36646
+ return parsed !== undefined;
36647
+ }
36648
+ const previous = this.appliedHostTimeRange;
36649
+ this.appliedHostTimeRange = parsed;
36650
+ if (!parsed) {
36651
+ // Without a host period nothing changes for the board (default path untouched). When the
36652
+ // host drops its period, its variables go too; the board's own selection (if any) re-sets them.
36653
+ if (previous) {
36654
+ this.stateService.clearHostTimeRange(this);
36655
+ this.stateService.clearTimeFilterVariables();
36656
+ }
36657
+ return false;
36658
+ }
36659
+ this.hostHasOwnedPeriod = true;
36660
+ this.stateService.setHostTimeRange(parsed, this);
36661
+ if (!this.stateService.isTimeFilterEnabled()) {
36662
+ return true;
36663
+ }
36664
+ if (parsed.status === 'valid') {
36665
+ this.stateService.setTimeFilterVariables(parsed.from.toISOString(), parsed.to.toISOString());
36666
+ }
36667
+ else {
36668
+ this.stateService.clearTimeFilterVariables();
36669
+ if (this.reportedInvalidHostTimeRange !== parsed) {
36670
+ this.reportedInvalidHostTimeRange = parsed;
36671
+ console.error(`mm-meshboard-view: input "timeRange" is invalid (${parsed.reason}); `
36672
+ + 'the board sends no stream-data query until the host sets a valid range.');
36673
+ }
36674
+ }
36675
+ return true;
36676
+ }
36677
+ /**
36678
+ * Applies a change of the `timeRange` input after the initial load (AB#7582): the host period,
36679
+ * or the board's own selection again when the input went back to `undefined`. Reloads the
36680
+ * widgets once; an equal period (same instants) changes nothing.
36681
+ */
36682
+ onHostTimeRangeChanged(parsed) {
36683
+ if (sameHostTimeRange(parsed, this.appliedHostTimeRange)) {
36684
+ return;
36685
+ }
36686
+ this.initializeTimeFilterVariables();
36687
+ if (this.stateService.isTimeFilterEnabled()) {
36688
+ this.stateService.triggerRefresh();
36689
+ }
36690
+ }
35101
36691
  /**
35102
36692
  * Recomputes the active time-filter range from the current selection on the
35103
36693
  * board's (possibly just-changed) timezone mode and republishes the
@@ -35105,6 +36695,11 @@ class MeshBoardViewComponent {
35105
36695
  * disabled or no selection is active.
35106
36696
  */
35107
36697
  reapplyTimeFilterRange() {
36698
+ // A host period (valid or invalid) takes the same path as on every other change.
36699
+ if (this.isHostTimeRangeActive()) {
36700
+ this.applyHostTimeRange(this.parsedHostTimeRange());
36701
+ return;
36702
+ }
35108
36703
  const range = this.stateService.resolveCurrentTimeRange();
35109
36704
  if (!range) {
35110
36705
  return;
@@ -35421,6 +37016,10 @@ class MeshBoardViewComponent {
35421
37016
  * Updates the time filter variables, persists the selection, syncs to URL, and triggers refresh.
35422
37017
  */
35423
37018
  async onTimeSelectionChange(sharedSelection) {
37019
+ // The host owns the period (AB#7582): no selection, URL or refresh from the picker.
37020
+ if (this.isHostTimeRangeActive()) {
37021
+ return;
37022
+ }
35424
37023
  // Convert shared-ui selection to our model
35425
37024
  const selection = {
35426
37025
  type: sharedSelection.type,
@@ -35463,6 +37062,9 @@ class MeshBoardViewComponent {
35463
37062
  * Clears the stored selection and applies the default.
35464
37063
  */
35465
37064
  resetTimeFilterToDefault() {
37065
+ if (this.isHostTimeRangeActive()) {
37066
+ return;
37067
+ }
35466
37068
  const config = this.timeFilterConfig();
35467
37069
  if (!config?.defaultSelection)
35468
37070
  return;
@@ -35996,8 +37598,8 @@ class MeshBoardViewComponent {
35996
37598
  this.stateService.updateEntitySelectorSelection(follower.id, rtId, displayName);
35997
37599
  }
35998
37600
  });
35999
- // Resolve the stream-data scope rtIds for this selection (one-hop
36000
- // childScope traversal, or the picked entity itself), per selector, so
37601
+ // Resolve the stream-data scope rtIds for this selection (childScope
37602
+ // hop or path, or the picked entity itself), per selector, so
36001
37603
  // widgets bound to the leader or any follower scope their stream-data
36002
37604
  // query by the resolved source rtIds.
36003
37605
  await Promise.all(targets.map((t, i) => this.resolveEntitySelectorScopeRtIds(t, rtId, seqs[i])));
@@ -36049,34 +37651,52 @@ class MeshBoardViewComponent {
36049
37651
  * Resolves the set of source rtIds that a stream-data widget bound to this
36050
37652
  * selector should be scoped to, and caches them on the state service.
36051
37653
  *
36052
- * When the selector defines a {@link EntitySelectorChildScope}, the picked
36053
- * entity's children of `targetCkTypeId` reached via `roleId`/`direction`
36054
- * become the scope (e.g. MeteringPoint → its EnergyMeasurement rtIds). With
36055
- * no childScope, the picked entity's own rtId is the scope (direct keying).
37654
+ * - No {@link EntitySelectorChildScope}: the picked entity's own rtId is the
37655
+ * scope (direct keying).
37656
+ * - One-hop form (top-level fields): the picked entity's targets of
37657
+ * `targetCkTypeId` reached via `roleId`/`direction` (e.g. MeteringPoint →
37658
+ * its EnergyMeasurement rtIds) — exactly as before AB#7723.
37659
+ * - `path` (AB#7723): hop by hop, each hop one batched request over all
37660
+ * entities the previous hop reached; the scope is the distinct rtIds of the
37661
+ * last hop. An empty hop ends the resolution with an empty scope; a failed
37662
+ * hop, or an invalid path (more than 5 hops, a hop without role or target
37663
+ * type), is a failed scope (AB#7721).
36056
37664
  */
36057
37665
  async resolveEntitySelectorScopeRtIds(selector, rtId, seq) {
36058
37666
  const isCurrent = () => this.scopeRequestSeq.get(selector.id) === seq;
36059
37667
  if (!isCurrent())
36060
37668
  return;
36061
- const childScope = selector.childScope;
36062
- if (!childScope?.targetCkTypeId || !childScope.roleId) {
37669
+ const plan = scopePlan(selector.childScope);
37670
+ if (plan.kind === 'none') {
36063
37671
  this.stateService.setEntitySelectorRtIds(selector.id, [rtId]);
36064
37672
  return;
36065
37673
  }
37674
+ if (plan.kind === 'invalid') {
37675
+ console.error(`Invalid child scope path for selector '${selector.id}': ${plan.reason}`);
37676
+ this.stateService.setEntitySelectorScopeFailed(selector.id);
37677
+ return;
37678
+ }
36066
37679
  try {
36067
- const targets = await firstValueFrom(this.dataService.fetchAssociationTargets(rtId, selector.ckTypeId, childScope.targetCkTypeId, childScope.roleId,
36068
- // Parent → children via System/ParentChild is an INBOUND hop (the child
36069
- // owns the association to its parent), so 'in' is the default.
36070
- childScope.direction ?? 'in'));
36071
- if (!isCurrent())
37680
+ let scopeRtIds;
37681
+ if (plan.kind === 'single') {
37682
+ // The original one-hop request, unchanged (AB#7723 keeps old boards byte-identical).
37683
+ const targets = await firstValueFrom(this.dataService.fetchAssociationTargets(rtId, selector.ckTypeId, plan.hop.targetCkTypeId, plan.hop.roleId,
37684
+ // Parent → children via System/ParentChild is an INBOUND hop (the child
37685
+ // owns the association to its parent), so 'in' is the default.
37686
+ plan.hop.direction));
37687
+ scopeRtIds = isCurrent()
37688
+ ? (targets ?? []).map(t => String(t.rtId)).filter(id => id.length > 0)
37689
+ : null;
37690
+ }
37691
+ else {
37692
+ scopeRtIds = await resolveScopePath(rtId, selector.ckTypeId, plan.hops, (sources, sourceCkTypeId, hop) => firstValueFrom(this.dataService.fetchAssociationTargetRtIds(sources, sourceCkTypeId, hop.targetCkTypeId, hop.roleId, hop.direction)), isCurrent);
37693
+ }
37694
+ if (scopeRtIds === null || !isCurrent())
36072
37695
  return;
36073
- const childRtIds = (targets ?? [])
36074
- .map(t => String(t.rtId))
36075
- .filter(id => id.length > 0);
36076
37696
  // An empty list is a resolved EMPTY scope: bound widgets show no data and
36077
37697
  // send no query — never the persisted scope, which would show rows of
36078
37698
  // other entities (AB#7721).
36079
- this.stateService.setEntitySelectorRtIds(selector.id, childRtIds);
37699
+ this.stateService.setEntitySelectorRtIds(selector.id, scopeRtIds);
36080
37700
  }
36081
37701
  catch (error) {
36082
37702
  if (!isCurrent())
@@ -36164,10 +37784,10 @@ class MeshBoardViewComponent {
36164
37784
  }
36165
37785
  }
36166
37786
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: MeshBoardViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
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: [
37787
+ 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 }, timeRange: { classPropertyName: "timeRange", publicName: "timeRange", isSignal: true, isRequired: false, transformFunction: null }, timeFilterControl: { classPropertyName: "timeFilterControl", publicName: "timeFilterControl", isSignal: true, isRequired: false, transformFunction: null } }, providers: [
36168
37788
  { provide: HAS_UNSAVED_CHANGES, useExisting: MeshBoardViewComponent },
36169
37789
  AutoRefreshTimerService
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 });
37790
+ ], 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); hidden when the host owns the period (AB#7582) -->\n @if (showTimeRangePicker()) {\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 (showTimeRangePicker()) {\n <div class=\"toolbar-separator\"></div>\n }\n <div class=\"toolbar-entity-selectors\">\n <mm-entity-selector-toolbar\n [entitySelectors]=\"entitySelectorsConfig()\"\n [variables]=\"boardVariables()\"\n (entitySelected)=\"onEntitySelectorSelected($event)\"\n (entityCleared)=\"onEntitySelectorCleared($event)\">\n </mm-entity-selector-toolbar>\n </div>\n }\n\n @if (showTimeRangePicker() || 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", "variables"], outputs: ["entitySelected", "entityCleared"] }, { kind: "component", type: MeshBoardEmptyStateComponent, selector: "mm-meshboard-empty-state", inputs: ["readonly"], outputs: ["addWidget"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
36171
37791
  }
36172
37792
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: MeshBoardViewComponent, decorators: [{
36173
37793
  type: Component,
@@ -36185,11 +37805,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
36185
37805
  ], hostDirectives: [UnsavedChangesDirective], providers: [
36186
37806
  { provide: HAS_UNSAVED_CHANGES, useExisting: MeshBoardViewComponent },
36187
37807
  AutoRefreshTimerService
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"] }]
37808
+ ], 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); hidden when the host owns the period (AB#7582) -->\n @if (showTimeRangePicker()) {\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 (showTimeRangePicker()) {\n <div class=\"toolbar-separator\"></div>\n }\n <div class=\"toolbar-entity-selectors\">\n <mm-entity-selector-toolbar\n [entitySelectors]=\"entitySelectorsConfig()\"\n [variables]=\"boardVariables()\"\n (entitySelected)=\"onEntitySelectorSelected($event)\"\n (entityCleared)=\"onEntitySelectorCleared($event)\">\n </mm-entity-selector-toolbar>\n </div>\n }\n\n @if (showTimeRangePicker() || 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"] }]
36189
37809
  }], ctorParameters: () => [], propDecorators: { tileLayout: [{
36190
37810
  type: ViewChild,
36191
37811
  args: [TileLayoutComponent]
36192
- }], headerMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "headerMode", required: false }] }], chrome: [{ type: i0.Input, args: [{ isSignal: true, alias: "chrome", required: false }] }] } });
37812
+ }], headerMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "headerMode", required: false }] }], chrome: [{ type: i0.Input, args: [{ isSignal: true, alias: "chrome", required: false }] }], timeRange: [{ type: i0.Input, args: [{ isSignal: true, alias: "timeRange", required: false }] }], timeFilterControl: [{ type: i0.Input, args: [{ isSignal: true, alias: "timeFilterControl", required: false }] }] } });
36193
37813
 
36194
37814
  /** English defaults — today's texts. Complete: every member (optional ones too) has a value. */
36195
37815
  const DEFAULT_COCKPIT_WIDGET_MESSAGES = {
@@ -39635,5 +41255,5 @@ function provideCockpitWidgets() {
39635
41255
  * Generated bundle index. Do not edit.
39636
41256
  */
39637
41257
 
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 };
41258
+ export { ADAPTER_OFFLINE_GRACE_MS, ASSOCIATION_HOP_SOURCE_BATCH, ASSOCIATION_HOP_TARGETS_PER_SOURCE, 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, EntitySelectorScopePathEditorComponent, 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, MAX_SCOPE_HOPS, MESHBOARD_CHROME_ROUTE_DATA, MESHBOARD_DESCRIPTION_MARKER, MESHBOARD_EDIT_QUERY_PARAM, MESHBOARD_HEADER_MODE_ROUTE_DATA, MESHBOARD_LABEL_RESOLVER, MESHBOARD_LANGUAGE, 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, parseHostTimeRange, pickLocalizedText, pipelineExecutionInputs, plural, provideCockpitWidgetHost, provideCockpitWidgets, provideDefaultWidgets, provideMeshBoard, provideMeshBoardLanguage, 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 };
39639
41259
  //# sourceMappingURL=meshmakers-octo-meshboard.mjs.map