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