@meshmakers/octo-meshboard 3.5.480 → 3.5.500
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -4,9 +4,9 @@ import * as i0 from '@angular/core';
|
|
|
4
4
|
import { Injectable, inject, EventEmitter, forwardRef, Output, Input, ViewChild, ChangeDetectionStrategy, Component, Injector, EnvironmentInjector, ApplicationRef, signal, computed, effect, untracked, Directive, InjectionToken, isSignal, NgZone, DestroyRef, ElementRef, input, createEnvironmentInjector, ViewContainerRef, makeEnvironmentProviders, provideAppInitializer, output, viewChild } from '@angular/core';
|
|
5
5
|
import * as i1$1 from '@angular/forms';
|
|
6
6
|
import { NG_VALUE_ACCESSOR, FormsModule } from '@angular/forms';
|
|
7
|
-
import { firstValueFrom, from, map, Observable, of, switchMap as switchMap$1, catchError, interval, filter, combineLatest, defer, EMPTY } from 'rxjs';
|
|
7
|
+
import { firstValueFrom, from, map, Observable, of, switchMap as switchMap$1, catchError, NEVER, interval, filter, combineLatest, defer, EMPTY } from 'rxjs';
|
|
8
8
|
import * as i1$8 from '@meshmakers/shared-ui';
|
|
9
|
-
import { EntitySelectInputComponent, WindowStateService, TimeRangeUtils, MmDialogActionsDirective, ListViewComponent, FetchResultTyped, DataSourceBase, MM_ACTION_ICONS,
|
|
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' };
|
|
3202
|
+
}
|
|
3203
|
+
const state = this._entitySelectorScopes()[entitySelectorId];
|
|
3204
|
+
if (!state) {
|
|
3205
|
+
return { kind: 'unscoped' };
|
|
3139
3206
|
}
|
|
3140
|
-
|
|
3141
|
-
|
|
3207
|
+
switch (state.status) {
|
|
3208
|
+
case 'pending':
|
|
3209
|
+
return { kind: 'pending' };
|
|
3210
|
+
case 'failed':
|
|
3211
|
+
return { kind: 'empty', reason: 'failed' };
|
|
3212
|
+
case 'resolved':
|
|
3213
|
+
return state.rtIds.length > 0
|
|
3214
|
+
? { kind: 'scoped', rtIds: state.rtIds }
|
|
3215
|
+
: { kind: 'empty', reason: 'noTargets' };
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
/**
|
|
3219
|
+
* Resolves the stream-data `rtIds` for a persistent-query widget bound to an
|
|
3220
|
+
* entity selector: the resolved source rtIds when the scope is `scoped`,
|
|
3221
|
+
* otherwise `undefined`.
|
|
3222
|
+
*
|
|
3223
|
+
* `undefined` does NOT mean "query with the persisted scope" for a pending or
|
|
3224
|
+
* empty scope — callers must check {@link resolveStreamDataScope} first and
|
|
3225
|
+
* skip the query for `pending` / `empty` (AB#7721).
|
|
3226
|
+
*/
|
|
3227
|
+
resolveStreamDataRtIds(entitySelectorId) {
|
|
3228
|
+
const scope = this.resolveStreamDataScope(entitySelectorId);
|
|
3229
|
+
return scope.kind === 'scoped' ? scope.rtIds : undefined;
|
|
3230
|
+
}
|
|
3231
|
+
setEntitySelectorScope(selectorId, state) {
|
|
3232
|
+
this._entitySelectorScopes.update(map => ({ ...map, [selectorId]: state }));
|
|
3142
3233
|
}
|
|
3143
3234
|
/**
|
|
3144
3235
|
* Updates the time filter configuration.
|
|
@@ -5942,6 +6033,23 @@ function applyValueMultiplier(value, multiplier) {
|
|
|
5942
6033
|
}
|
|
5943
6034
|
return value * multiplier;
|
|
5944
6035
|
}
|
|
6036
|
+
const WIDGET_NUMBER_LOCALE = 'de-AT';
|
|
6037
|
+
const numberFormats = new Map();
|
|
6038
|
+
function getNumberFormat(locale, minimumFractionDigits, maximumFractionDigits, useGrouping) {
|
|
6039
|
+
const key = `${locale}|${minimumFractionDigits}|${maximumFractionDigits}|${useGrouping}`;
|
|
6040
|
+
let format = numberFormats.get(key);
|
|
6041
|
+
if (!format) {
|
|
6042
|
+
format = new Intl.NumberFormat(locale, useGrouping === undefined
|
|
6043
|
+
? { minimumFractionDigits, maximumFractionDigits }
|
|
6044
|
+
: { minimumFractionDigits, maximumFractionDigits, useGrouping });
|
|
6045
|
+
numberFormats.set(key, format);
|
|
6046
|
+
}
|
|
6047
|
+
return format;
|
|
6048
|
+
}
|
|
6049
|
+
function formatWidgetNumber(value, fractionDigits, mode) {
|
|
6050
|
+
const minimumFractionDigits = mode === 'exact' ? fractionDigits : 0;
|
|
6051
|
+
return getNumberFormat(WIDGET_NUMBER_LOCALE, minimumFractionDigits, fractionDigits).format(value);
|
|
6052
|
+
}
|
|
5945
6053
|
|
|
5946
6054
|
const VARIABLE_REFERENCE = /\$\{([^}]+)\}|\$([a-zA-Z_]\w*)/g;
|
|
5947
6055
|
const IDENTIFIER_PREFIX = '__v_';
|
|
@@ -6093,6 +6201,14 @@ function findVariableCycles(widgets, configuredVariableNames = []) {
|
|
|
6093
6201
|
return inCycle;
|
|
6094
6202
|
}
|
|
6095
6203
|
|
|
6204
|
+
const KPI_MAX_DECIMAL_PLACES = 3;
|
|
6205
|
+
function normalizeKpiDecimalPlaces(value) {
|
|
6206
|
+
if (typeof value !== 'number' || !Number.isInteger(value)) {
|
|
6207
|
+
return undefined;
|
|
6208
|
+
}
|
|
6209
|
+
return value >= 0 && value <= KPI_MAX_DECIMAL_PLACES ? value : undefined;
|
|
6210
|
+
}
|
|
6211
|
+
|
|
6096
6212
|
class KpiWidgetComponent {
|
|
6097
6213
|
dataService = inject(DashboardDataService);
|
|
6098
6214
|
queryExecutor = inject(QueryExecutorService);
|
|
@@ -6222,7 +6338,7 @@ class KpiWidgetComponent {
|
|
|
6222
6338
|
attributeName = '_queryValue';
|
|
6223
6339
|
}
|
|
6224
6340
|
else if (config?.dataSource?.type === 'static') {
|
|
6225
|
-
attributeName = '_staticValue';
|
|
6341
|
+
attributeName = config.dataSource.data ? config.valueAttribute : '_staticValue';
|
|
6226
6342
|
}
|
|
6227
6343
|
else {
|
|
6228
6344
|
attributeName = config?.valueAttribute;
|
|
@@ -6299,6 +6415,10 @@ class KpiWidgetComponent {
|
|
|
6299
6415
|
if (strValue === '') {
|
|
6300
6416
|
return '-';
|
|
6301
6417
|
}
|
|
6418
|
+
const decimals = normalizeKpiDecimalPlaces(this.currentConfig()?.decimalPlaces);
|
|
6419
|
+
if (decimals !== undefined) {
|
|
6420
|
+
return this.formatWithDecimals(value, strValue, decimals);
|
|
6421
|
+
}
|
|
6302
6422
|
const numValue = typeof value === 'number' ? value : parseFloat(strValue);
|
|
6303
6423
|
if (isNaN(numValue)) {
|
|
6304
6424
|
// Not a number, return as string
|
|
@@ -6309,6 +6429,19 @@ class KpiWidgetComponent {
|
|
|
6309
6429
|
maximumFractionDigits: 2
|
|
6310
6430
|
});
|
|
6311
6431
|
}
|
|
6432
|
+
static strictNumber(text) {
|
|
6433
|
+
const trimmed = text.trim();
|
|
6434
|
+
return trimmed === '' ? NaN : Number(trimmed);
|
|
6435
|
+
}
|
|
6436
|
+
formatWithDecimals(value, strValue, decimals) {
|
|
6437
|
+
const numValue = typeof value === 'number' ? value : KpiWidgetComponent.strictNumber(strValue);
|
|
6438
|
+
if (typeof value === 'number' ? isNaN(numValue) : !Number.isFinite(numValue)) {
|
|
6439
|
+
return strValue;
|
|
6440
|
+
}
|
|
6441
|
+
const negative = numValue < 0 || Object.is(numValue, -0);
|
|
6442
|
+
const shown = negative && Number(getNumberFormat('en-US', 0, decimals, false).format(numValue)) === 0 ? 0 : numValue;
|
|
6443
|
+
return formatWidgetNumber(shown, decimals, 'exact');
|
|
6444
|
+
}
|
|
6312
6445
|
label = computed(() => {
|
|
6313
6446
|
const data = this._data();
|
|
6314
6447
|
if (!this.config?.labelAttribute || !data)
|
|
@@ -6388,6 +6521,11 @@ class KpiWidgetComponent {
|
|
|
6388
6521
|
this._error.set(null);
|
|
6389
6522
|
return;
|
|
6390
6523
|
}
|
|
6524
|
+
if (dataSource.type === 'static' && dataSource.data) {
|
|
6525
|
+
this._data.set(dataSource.data);
|
|
6526
|
+
this._error.set(null);
|
|
6527
|
+
return;
|
|
6528
|
+
}
|
|
6391
6529
|
if (dataSource.type === 'static') {
|
|
6392
6530
|
// Resolve static value with variable substitution
|
|
6393
6531
|
const staticValue = this.config.staticValue ?? '';
|
|
@@ -6473,6 +6611,18 @@ class KpiWidgetComponent {
|
|
|
6473
6611
|
// queryFamily may be undefined for legacy widget configs — the executor
|
|
6474
6612
|
// falls back to a one-time lookup by rtId. streamDataArgs is sent
|
|
6475
6613
|
// unconditionally because the runtime path ignores it.
|
|
6614
|
+
// Entity-selector scope (AB#7721): pending → stay in the loading state and
|
|
6615
|
+
// send no query (the view refreshes once the scope is known); empty → show
|
|
6616
|
+
// the no-data value '-' without querying the persisted (unscoped) scope.
|
|
6617
|
+
const scope = this.stateService.resolveStreamDataScope(dataSource.entitySelectorId, dataSource.queryFamily);
|
|
6618
|
+
if (scope.kind === 'pending') {
|
|
6619
|
+
return;
|
|
6620
|
+
}
|
|
6621
|
+
if (scope.kind === 'empty') {
|
|
6622
|
+
this._data.set(null);
|
|
6623
|
+
this._isLoading.set(false);
|
|
6624
|
+
return;
|
|
6625
|
+
}
|
|
6476
6626
|
const streamDataArgs = this.buildStreamDataArgs();
|
|
6477
6627
|
const result = await firstValueFrom(this.queryExecutor.execute(dataSource.queryFamily, dataSource.queryRtId, {
|
|
6478
6628
|
fieldFilter: fieldFilter ?? undefined,
|
|
@@ -6571,6 +6721,10 @@ class KpiWidgetComponent {
|
|
|
6571
6721
|
if (typeof value === 'number')
|
|
6572
6722
|
return value;
|
|
6573
6723
|
if (typeof value === 'string') {
|
|
6724
|
+
if (normalizeKpiDecimalPlaces(this.currentConfig()?.decimalPlaces) !== undefined) {
|
|
6725
|
+
const strict = KpiWidgetComponent.strictNumber(value);
|
|
6726
|
+
return Number.isFinite(strict) ? strict : value;
|
|
6727
|
+
}
|
|
6574
6728
|
// Try to parse as number first
|
|
6575
6729
|
const parsed = parseFloat(value);
|
|
6576
6730
|
// If it's a valid number, return as number; otherwise return the string
|
|
@@ -6852,6 +7006,7 @@ class KpiConfigDialogComponent {
|
|
|
6852
7006
|
initialSuffix;
|
|
6853
7007
|
initialTrend;
|
|
6854
7008
|
initialComparisonText;
|
|
7009
|
+
initialDecimalPlaces;
|
|
6855
7010
|
// Initial values for editing - Persistent Query
|
|
6856
7011
|
initialDataSourceType;
|
|
6857
7012
|
initialQueryRtId;
|
|
@@ -6918,6 +7073,7 @@ class KpiConfigDialogComponent {
|
|
|
6918
7073
|
suffix: '',
|
|
6919
7074
|
trend: undefined,
|
|
6920
7075
|
comparisonText: '',
|
|
7076
|
+
decimalPlaces: null,
|
|
6921
7077
|
// Query-specific form fields
|
|
6922
7078
|
queryValueField: '',
|
|
6923
7079
|
queryCategoryField: '',
|
|
@@ -7021,6 +7177,9 @@ class KpiConfigDialogComponent {
|
|
|
7021
7177
|
: null;
|
|
7022
7178
|
}
|
|
7023
7179
|
variableSyntaxHint = '${variableName}';
|
|
7180
|
+
get maxDecimalPlaces() {
|
|
7181
|
+
return KPI_MAX_DECIMAL_PLACES;
|
|
7182
|
+
}
|
|
7024
7183
|
trendOptions = [
|
|
7025
7184
|
{ value: undefined, label: 'None' },
|
|
7026
7185
|
{ value: 'up', label: 'Up (positive)' },
|
|
@@ -7076,6 +7235,7 @@ class KpiConfigDialogComponent {
|
|
|
7076
7235
|
this.form.suffix = this.initialSuffix || '';
|
|
7077
7236
|
this.form.trend = this.initialTrend;
|
|
7078
7237
|
this.form.comparisonText = this.initialComparisonText || '';
|
|
7238
|
+
this.form.decimalPlaces = this.initialDecimalPlaces ?? null;
|
|
7079
7239
|
this.form.queryValueField = this.initialQueryValueField || '';
|
|
7080
7240
|
this.form.queryCategoryField = this.initialQueryCategoryField || '';
|
|
7081
7241
|
this.form.queryCategoryValue = this.initialQueryCategoryValue || '';
|
|
@@ -7252,11 +7412,7 @@ class KpiConfigDialogComponent {
|
|
|
7252
7412
|
ckTypeId: '',
|
|
7253
7413
|
valueAttribute: '',
|
|
7254
7414
|
formula: this.form.formula.trim(),
|
|
7255
|
-
|
|
7256
|
-
suffix: this.form.suffix || undefined,
|
|
7257
|
-
trend: this.form.trend,
|
|
7258
|
-
comparisonText: this.form.comparisonText || undefined,
|
|
7259
|
-
outputVariableName: this.form.outputVariableName.trim() || undefined
|
|
7415
|
+
...this.displayFields()
|
|
7260
7416
|
});
|
|
7261
7417
|
return;
|
|
7262
7418
|
}
|
|
@@ -7266,11 +7422,7 @@ class KpiConfigDialogComponent {
|
|
|
7266
7422
|
ckTypeId: '',
|
|
7267
7423
|
valueAttribute: '',
|
|
7268
7424
|
staticValue: this.form.staticValue,
|
|
7269
|
-
|
|
7270
|
-
suffix: this.form.suffix || undefined,
|
|
7271
|
-
trend: this.form.trend,
|
|
7272
|
-
comparisonText: this.form.comparisonText || undefined,
|
|
7273
|
-
outputVariableName: this.form.outputVariableName.trim() || undefined
|
|
7425
|
+
...this.displayFields()
|
|
7274
7426
|
});
|
|
7275
7427
|
return;
|
|
7276
7428
|
}
|
|
@@ -7290,11 +7442,7 @@ class KpiConfigDialogComponent {
|
|
|
7290
7442
|
queryValueField: this.form.queryValueField || undefined,
|
|
7291
7443
|
queryCategoryField: this.form.queryCategoryField || undefined,
|
|
7292
7444
|
queryCategoryValue: this.form.queryCategoryValue || undefined,
|
|
7293
|
-
|
|
7294
|
-
suffix: this.form.suffix || undefined,
|
|
7295
|
-
trend: this.form.trend,
|
|
7296
|
-
comparisonText: this.form.comparisonText || undefined,
|
|
7297
|
-
outputVariableName: this.form.outputVariableName.trim() || undefined,
|
|
7445
|
+
...this.displayFields(),
|
|
7298
7446
|
filters: filtersDto
|
|
7299
7447
|
});
|
|
7300
7448
|
}
|
|
@@ -7305,15 +7453,21 @@ class KpiConfigDialogComponent {
|
|
|
7305
7453
|
rtId: this.isCountMode ? undefined : this.selectedEntity?.rtId,
|
|
7306
7454
|
valueAttribute: this.isCountMode ? '_count' : this.form.valueAttribute,
|
|
7307
7455
|
labelAttribute: this.form.labelAttribute || undefined,
|
|
7308
|
-
|
|
7309
|
-
suffix: this.form.suffix || undefined,
|
|
7310
|
-
trend: this.form.trend,
|
|
7311
|
-
comparisonText: this.form.comparisonText || undefined,
|
|
7312
|
-
outputVariableName: this.form.outputVariableName.trim() || undefined,
|
|
7456
|
+
...this.displayFields(),
|
|
7313
7457
|
filters: filtersDto
|
|
7314
7458
|
});
|
|
7315
7459
|
}
|
|
7316
7460
|
}
|
|
7461
|
+
displayFields() {
|
|
7462
|
+
return {
|
|
7463
|
+
prefix: this.form.prefix || undefined,
|
|
7464
|
+
suffix: this.form.suffix || undefined,
|
|
7465
|
+
trend: this.form.trend,
|
|
7466
|
+
comparisonText: this.form.comparisonText || undefined,
|
|
7467
|
+
outputVariableName: this.form.outputVariableName.trim() || undefined,
|
|
7468
|
+
decimalPlaces: normalizeKpiDecimalPlaces(this.form.decimalPlaces)
|
|
7469
|
+
};
|
|
7470
|
+
}
|
|
7317
7471
|
onCancel() {
|
|
7318
7472
|
this.windowRef.close();
|
|
7319
7473
|
}
|
|
@@ -7458,7 +7612,7 @@ class KpiConfigDialogComponent {
|
|
|
7458
7612
|
this.filters = updatedFilters;
|
|
7459
7613
|
}
|
|
7460
7614
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: KpiConfigDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
7461
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: KpiConfigDialogComponent, isStandalone: true, selector: "mm-kpi-config-dialog", inputs: { initialCkTypeId: "initialCkTypeId", initialRtId: "initialRtId", initialValueAttribute: "initialValueAttribute", initialLabelAttribute: "initialLabelAttribute", initialPrefix: "initialPrefix", initialSuffix: "initialSuffix", initialTrend: "initialTrend", initialComparisonText: "initialComparisonText", initialDataSourceType: "initialDataSourceType", initialQueryRtId: "initialQueryRtId", initialQueryName: "initialQueryName", initialQueryFamily: "initialQueryFamily", initialIgnoreTimeFilter: "initialIgnoreTimeFilter", initialEntitySelectorId: "initialEntitySelectorId", initialQueryMode: "initialQueryMode", initialQueryValueField: "initialQueryValueField", initialQueryCategoryField: "initialQueryCategoryField", initialQueryCategoryValue: "initialQueryCategoryValue", initialStaticValue: "initialStaticValue", initialWidgetId: "initialWidgetId", initialFormula: "initialFormula", initialOutputVariableName: "initialOutputVariableName", initialFilters: "initialFilters" }, viewQueries: [{ propertyName: "ckTypeSelectorInput", first: true, predicate: ["ckTypeSelector"], descendants: true }, { propertyName: "entitySelectorInput", first: true, predicate: ["entitySelector"], descendants: true }, { propertyName: "querySelector", first: true, predicate: ["querySelector"], descendants: true }], ngImport: i0, template: `
|
|
7615
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: KpiConfigDialogComponent, isStandalone: true, selector: "mm-kpi-config-dialog", inputs: { initialCkTypeId: "initialCkTypeId", initialRtId: "initialRtId", initialValueAttribute: "initialValueAttribute", initialLabelAttribute: "initialLabelAttribute", initialPrefix: "initialPrefix", initialSuffix: "initialSuffix", initialTrend: "initialTrend", initialComparisonText: "initialComparisonText", initialDecimalPlaces: "initialDecimalPlaces", initialDataSourceType: "initialDataSourceType", initialQueryRtId: "initialQueryRtId", initialQueryName: "initialQueryName", initialQueryFamily: "initialQueryFamily", initialIgnoreTimeFilter: "initialIgnoreTimeFilter", initialEntitySelectorId: "initialEntitySelectorId", initialQueryMode: "initialQueryMode", initialQueryValueField: "initialQueryValueField", initialQueryCategoryField: "initialQueryCategoryField", initialQueryCategoryValue: "initialQueryCategoryValue", initialStaticValue: "initialStaticValue", initialWidgetId: "initialWidgetId", initialFormula: "initialFormula", initialOutputVariableName: "initialOutputVariableName", initialFilters: "initialFilters" }, viewQueries: [{ propertyName: "ckTypeSelectorInput", first: true, predicate: ["ckTypeSelector"], descendants: true }, { propertyName: "entitySelectorInput", first: true, predicate: ["entitySelector"], descendants: true }, { propertyName: "querySelector", first: true, predicate: ["querySelector"], descendants: true }], ngImport: i0, template: `
|
|
7462
7616
|
<div class="config-container">
|
|
7463
7617
|
|
|
7464
7618
|
<div class="config-form" [class.loading]="isLoadingInitial">
|
|
@@ -7871,6 +8025,19 @@ class KpiConfigDialogComponent {
|
|
|
7871
8025
|
<kendo-textbox [(ngModel)]="form.comparisonText" placeholder="e.g. +3,1% vs. last week"></kendo-textbox>
|
|
7872
8026
|
<p class="section-hint">Displayed below the value in the trend color. Supports {{ variableSyntaxHint }}</p>
|
|
7873
8027
|
</div>
|
|
8028
|
+
<div class="form-field">
|
|
8029
|
+
<label>Decimal Places</label>
|
|
8030
|
+
<kendo-numerictextbox
|
|
8031
|
+
[(ngModel)]="form.decimalPlaces"
|
|
8032
|
+
[min]="0"
|
|
8033
|
+
[max]="maxDecimalPlaces"
|
|
8034
|
+
[step]="1"
|
|
8035
|
+
[decimals]="0"
|
|
8036
|
+
format="n0"
|
|
8037
|
+
placeholder="Automatic">
|
|
8038
|
+
</kendo-numerictextbox>
|
|
8039
|
+
<p class="section-hint">Fixed number of decimal places for numeric values (0-3). Leave empty for automatic (two for a fractional value, none for a whole number).</p>
|
|
8040
|
+
</div>
|
|
7874
8041
|
</div>
|
|
7875
8042
|
|
|
7876
8043
|
<!-- Filters Section -->
|
|
@@ -7900,7 +8067,7 @@ class KpiConfigDialogComponent {
|
|
|
7900
8067
|
</button>
|
|
7901
8068
|
</div>
|
|
7902
8069
|
</div>
|
|
7903
|
-
`, isInline: true, styles: [":host{display:block;height:100%}.config-container{display:flex;flex-direction:column;height:100%}.config-form{display:flex;flex-direction:column;gap:20px;flex:1;overflow-y:auto;padding:16px;position:relative}.config-form.loading{pointer-events:none}.form-field{display:flex;flex-direction:column;gap:6px}.form-field.disabled{opacity:.6}.form-field.flex-1{flex:1}.form-field label{font-weight:600;font-size:.9rem;color:var(--kendo-color-on-app-surface, #212529)}.field-hint{margin:0;font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.form-section{padding:16px;background:var(--kendo-color-surface-alt, #f8f9fa);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px}.form-section h4{margin:0 0 16px;font-size:.95rem;color:var(--kendo-color-primary, #0d6efd)}.section-hint{margin:0 0 12px;font-size:.85rem;color:var(--kendo-color-subtle, #6c757d)}.form-row{display:flex;gap:16px}.mode-toggle{display:flex;gap:8px}.mode-toggle button{flex:1}.attribute-item{display:flex;justify-content:space-between;align-items:center;gap:8px;width:100%}.attribute-path{flex:1}.attribute-type{font-size:.75rem;color:var(--kendo-color-subtle, #6c757d);background:var(--kendo-color-surface-alt, #f8f9fa);padding:2px 6px;border-radius:3px}.required{color:var(--kendo-color-error, #dc3545)}.field-error{margin:0;font-size:.8rem;color:var(--kendo-color-error, #dc3545)}.variable-list{display:flex;flex-wrap:wrap;gap:4px}.variable-source{color:var(--kendo-color-subtle, #6c757d)}.query-item{display:flex;flex-direction:column;gap:2px}.query-name{font-weight:500}.query-description{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.column-item{display:flex;justify-content:space-between;gap:16px}.column-path{font-weight:500}.column-type{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ButtonsModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: InputsModule }, { kind: "component", type: i3.TextBoxComponent, selector: "kendo-textbox", inputs: ["focusableId", "title", "type", "disabled", "readonly", "tabindex", "value", "selectOnFocus", "showSuccessIcon", "showErrorIcon", "clearButton", "successIcon", "successSvgIcon", "errorIcon", "errorSvgIcon", "clearButtonIcon", "clearButtonSvgIcon", "size", "rounded", "fillMode", "tabIndex", "placeholder", "maxlength", "inputAttributes"], outputs: ["valueChange", "inputFocus", "inputBlur", "focus", "blur"], exportAs: ["kendoTextBox"] }, { kind: "ngmodule", type: DropDownsModule }, { kind: "directive", type: i4.ItemTemplateDirective, selector: "[kendoDropDownListItemTemplate],[kendoComboBoxItemTemplate],[kendoAutoCompleteItemTemplate],[kendoMultiSelectItemTemplate]" }, { kind: "component", type: i4.ComboBoxComponent, selector: "kendo-combobox", inputs: ["icon", "svgIcon", "inputAttributes", "showStickyHeader", "focusableId", "allowCustom", "data", "value", "textField", "valueField", "valuePrimitive", "valueNormalizer", "placeholder", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "loading", "suggest", "clearButton", "disabled", "itemDisabled", "readonly", "tabindex", "tabIndex", "filterable", "virtual", "size", "rounded", "fillMode"], outputs: ["valueChange", "selectionChange", "filterChange", "open", "opened", "close", "closed", "focus", "blur", "inputFocus", "inputBlur", "escape"], exportAs: ["kendoComboBox"] }, { kind: "component", type: i4.DropDownListComponent, selector: "kendo-dropdownlist", inputs: ["customIconClass", "showStickyHeader", "icon", "svgIcon", "loading", "data", "value", "textField", "valueField", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "defaultItem", "disabled", "itemDisabled", "readonly", "filterable", "virtual", "ignoreCase", "delay", "valuePrimitive", "tabindex", "tabIndex", "size", "rounded", "fillMode", "leftRightArrowsNavigation", "id"], outputs: ["valueChange", "filterChange", "selectionChange", "open", "opened", "close", "closed", "focus", "blur"], exportAs: ["kendoDropDownList"] }, { kind: "component", type: CkTypeSelectorInputComponent, selector: "mm-ck-type-selector-input", inputs: ["placeholder", "minSearchLength", "maxResults", "debounceMs", "ckModelIds", "allowAbstract", "dialogTitle", "advancedSearchLabel", "derivedFromRtCkTypeId", "messages", "dialogMessages", "disabled", "required"], outputs: ["ckTypeSelected", "ckTypeCleared"] }, { kind: "component", type: EntitySelectInputComponent, selector: "mm-entity-select-input", inputs: ["dataSource", "placeholder", "minSearchLength", "maxResults", "debounceMs", "prefix", "initialDisplayValue", "dialogDataSource", "dialogTitle", "multiSelect", "advancedSearchLabel", "dialogMessages", "messages", "disabled", "required"], outputs: ["entitySelected", "entityCleared", "entitiesSelected"] }, { kind: "component", type: FieldFilterEditorComponent, selector: "mm-field-filter-editor", inputs: ["availableAttributes", "ckTypeId", "hideNavigationProperties", "attributePaths", "enableVariables", "availableVariables", "filters"], outputs: ["filtersChange"] }, { kind: "component", type: QuerySelectorComponent, selector: "mm-query-selector", inputs: ["placeholder", "hint", "disabled", "acceptFamilies"], outputs: ["querySelected", "queriesLoaded"] }, { kind: "component", type: SdTimeFilterToggleComponent, selector: "mm-sd-time-filter-toggle", inputs: ["family", "ignoreTimeFilter"], outputs: ["ignoreTimeFilterChange"] }, { kind: "component", type: EntitySelectorScopePickerComponent, selector: "mm-entity-selector-scope-picker", inputs: ["family", "entitySelectorId", "selectors"], outputs: ["entitySelectorIdChange"] }, { kind: "component", type: LoadingOverlayComponent, selector: "mm-loading-overlay", inputs: ["loading"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
|
|
8070
|
+
`, isInline: true, styles: [":host{display:block;height:100%}.config-container{display:flex;flex-direction:column;height:100%}.config-form{display:flex;flex-direction:column;gap:20px;flex:1;overflow-y:auto;padding:16px;position:relative}.config-form.loading{pointer-events:none}.form-field{display:flex;flex-direction:column;gap:6px}.form-field.disabled{opacity:.6}.form-field.flex-1{flex:1}.form-field label{font-weight:600;font-size:.9rem;color:var(--kendo-color-on-app-surface, #212529)}.field-hint{margin:0;font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.form-section{padding:16px;background:var(--kendo-color-surface-alt, #f8f9fa);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px}.form-section h4{margin:0 0 16px;font-size:.95rem;color:var(--kendo-color-primary, #0d6efd)}.section-hint{margin:0 0 12px;font-size:.85rem;color:var(--kendo-color-subtle, #6c757d)}.form-row{display:flex;gap:16px}.mode-toggle{display:flex;gap:8px}.mode-toggle button{flex:1}.attribute-item{display:flex;justify-content:space-between;align-items:center;gap:8px;width:100%}.attribute-path{flex:1}.attribute-type{font-size:.75rem;color:var(--kendo-color-subtle, #6c757d);background:var(--kendo-color-surface-alt, #f8f9fa);padding:2px 6px;border-radius:3px}.required{color:var(--kendo-color-error, #dc3545)}.field-error{margin:0;font-size:.8rem;color:var(--kendo-color-error, #dc3545)}.variable-list{display:flex;flex-wrap:wrap;gap:4px}.variable-source{color:var(--kendo-color-subtle, #6c757d)}.query-item{display:flex;flex-direction:column;gap:2px}.query-name{font-weight:500}.query-description{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.column-item{display:flex;justify-content:space-between;gap:16px}.column-path{font-weight:500}.column-type{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ButtonsModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: InputsModule }, { kind: "component", type: i3.TextBoxComponent, selector: "kendo-textbox", inputs: ["focusableId", "title", "type", "disabled", "readonly", "tabindex", "value", "selectOnFocus", "showSuccessIcon", "showErrorIcon", "clearButton", "successIcon", "successSvgIcon", "errorIcon", "errorSvgIcon", "clearButtonIcon", "clearButtonSvgIcon", "size", "rounded", "fillMode", "tabIndex", "placeholder", "maxlength", "inputAttributes"], outputs: ["valueChange", "inputFocus", "inputBlur", "focus", "blur"], exportAs: ["kendoTextBox"] }, { kind: "component", type: i3.NumericTextBoxComponent, selector: "kendo-numerictextbox", inputs: ["focusableId", "disabled", "readonly", "title", "autoCorrect", "format", "max", "min", "decimals", "placeholder", "step", "spinners", "rangeValidation", "tabindex", "tabIndex", "changeValueOnScroll", "selectOnFocus", "value", "maxlength", "size", "rounded", "fillMode", "inputAttributes"], outputs: ["valueChange", "focus", "blur", "inputFocus", "inputBlur"], exportAs: ["kendoNumericTextBox"] }, { kind: "ngmodule", type: DropDownsModule }, { kind: "directive", type: i4.ItemTemplateDirective, selector: "[kendoDropDownListItemTemplate],[kendoComboBoxItemTemplate],[kendoAutoCompleteItemTemplate],[kendoMultiSelectItemTemplate]" }, { kind: "component", type: i4.ComboBoxComponent, selector: "kendo-combobox", inputs: ["icon", "svgIcon", "inputAttributes", "showStickyHeader", "focusableId", "allowCustom", "data", "value", "textField", "valueField", "valuePrimitive", "valueNormalizer", "placeholder", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "loading", "suggest", "clearButton", "disabled", "itemDisabled", "readonly", "tabindex", "tabIndex", "filterable", "virtual", "size", "rounded", "fillMode"], outputs: ["valueChange", "selectionChange", "filterChange", "open", "opened", "close", "closed", "focus", "blur", "inputFocus", "inputBlur", "escape"], exportAs: ["kendoComboBox"] }, { kind: "component", type: i4.DropDownListComponent, selector: "kendo-dropdownlist", inputs: ["customIconClass", "showStickyHeader", "icon", "svgIcon", "loading", "data", "value", "textField", "valueField", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "defaultItem", "disabled", "itemDisabled", "readonly", "filterable", "virtual", "ignoreCase", "delay", "valuePrimitive", "tabindex", "tabIndex", "size", "rounded", "fillMode", "leftRightArrowsNavigation", "id"], outputs: ["valueChange", "filterChange", "selectionChange", "open", "opened", "close", "closed", "focus", "blur"], exportAs: ["kendoDropDownList"] }, { kind: "component", type: CkTypeSelectorInputComponent, selector: "mm-ck-type-selector-input", inputs: ["placeholder", "minSearchLength", "maxResults", "debounceMs", "ckModelIds", "allowAbstract", "dialogTitle", "advancedSearchLabel", "derivedFromRtCkTypeId", "messages", "dialogMessages", "disabled", "required"], outputs: ["ckTypeSelected", "ckTypeCleared"] }, { kind: "component", type: EntitySelectInputComponent, selector: "mm-entity-select-input", inputs: ["dataSource", "placeholder", "minSearchLength", "maxResults", "debounceMs", "prefix", "initialDisplayValue", "dialogDataSource", "dialogTitle", "multiSelect", "advancedSearchLabel", "dialogMessages", "messages", "disabled", "required"], outputs: ["entitySelected", "entityCleared", "entitiesSelected"] }, { kind: "component", type: FieldFilterEditorComponent, selector: "mm-field-filter-editor", inputs: ["availableAttributes", "ckTypeId", "hideNavigationProperties", "attributePaths", "enableVariables", "availableVariables", "filters"], outputs: ["filtersChange"] }, { kind: "component", type: QuerySelectorComponent, selector: "mm-query-selector", inputs: ["placeholder", "hint", "disabled", "acceptFamilies"], outputs: ["querySelected", "queriesLoaded"] }, { kind: "component", type: SdTimeFilterToggleComponent, selector: "mm-sd-time-filter-toggle", inputs: ["family", "ignoreTimeFilter"], outputs: ["ignoreTimeFilterChange"] }, { kind: "component", type: EntitySelectorScopePickerComponent, selector: "mm-entity-selector-scope-picker", inputs: ["family", "entitySelectorId", "selectors"], outputs: ["entitySelectorIdChange"] }, { kind: "component", type: LoadingOverlayComponent, selector: "mm-loading-overlay", inputs: ["loading"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
|
|
7904
8071
|
}
|
|
7905
8072
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: KpiConfigDialogComponent, decorators: [{
|
|
7906
8073
|
type: Component,
|
|
@@ -8330,6 +8497,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
8330
8497
|
<kendo-textbox [(ngModel)]="form.comparisonText" placeholder="e.g. +3,1% vs. last week"></kendo-textbox>
|
|
8331
8498
|
<p class="section-hint">Displayed below the value in the trend color. Supports {{ variableSyntaxHint }}</p>
|
|
8332
8499
|
</div>
|
|
8500
|
+
<div class="form-field">
|
|
8501
|
+
<label>Decimal Places</label>
|
|
8502
|
+
<kendo-numerictextbox
|
|
8503
|
+
[(ngModel)]="form.decimalPlaces"
|
|
8504
|
+
[min]="0"
|
|
8505
|
+
[max]="maxDecimalPlaces"
|
|
8506
|
+
[step]="1"
|
|
8507
|
+
[decimals]="0"
|
|
8508
|
+
format="n0"
|
|
8509
|
+
placeholder="Automatic">
|
|
8510
|
+
</kendo-numerictextbox>
|
|
8511
|
+
<p class="section-hint">Fixed number of decimal places for numeric values (0-3). Leave empty for automatic (two for a fractional value, none for a whole number).</p>
|
|
8512
|
+
</div>
|
|
8333
8513
|
</div>
|
|
8334
8514
|
|
|
8335
8515
|
<!-- Filters Section -->
|
|
@@ -8385,6 +8565,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
8385
8565
|
type: Input
|
|
8386
8566
|
}], initialComparisonText: [{
|
|
8387
8567
|
type: Input
|
|
8568
|
+
}], initialDecimalPlaces: [{
|
|
8569
|
+
type: Input
|
|
8388
8570
|
}], initialDataSourceType: [{
|
|
8389
8571
|
type: Input
|
|
8390
8572
|
}], initialQueryRtId: [{
|
|
@@ -10080,6 +10262,17 @@ class TableWidgetDataSourceDirective extends OctoGraphQlDataSource {
|
|
|
10080
10262
|
* `'runtime'` for legacy configs that predate stream-data support.
|
|
10081
10263
|
*/
|
|
10082
10264
|
fetchPersistentQueryData(dataSource, queryOptions) {
|
|
10265
|
+
// Entity-selector scope (AB#7721): pending → keep the list in its loading
|
|
10266
|
+
// state (an observable that never emits; the next refresh after the scope
|
|
10267
|
+
// resolves cancels it) and send no query; empty → no rows, no query — never
|
|
10268
|
+
// the persisted (unscoped) scope.
|
|
10269
|
+
const scope = this.stateService.resolveStreamDataScope(dataSource.entitySelectorId, dataSource.queryFamily);
|
|
10270
|
+
if (scope.kind === 'pending') {
|
|
10271
|
+
return NEVER;
|
|
10272
|
+
}
|
|
10273
|
+
if (scope.kind === 'empty') {
|
|
10274
|
+
return of(new FetchResultTyped([], 0));
|
|
10275
|
+
}
|
|
10083
10276
|
const fieldFilter = this.convertFiltersToDto(this._config?.filters);
|
|
10084
10277
|
// queryFamily may be undefined for legacy widget configs — the executor
|
|
10085
10278
|
// falls back to a one-time lookup by rtId. streamDataArgs is sent
|
|
@@ -10223,6 +10416,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
10223
10416
|
/** Default texts of {@link MeshBoardMessages} (the list texts default inside `mm-list-view`). */
|
|
10224
10417
|
const DEFAULT_MESHBOARD_MESSAGES = {
|
|
10225
10418
|
tableNotConfigured: 'Table not configured',
|
|
10419
|
+
scopeResolutionFailed: 'Could not resolve the scope of "{label}" - widgets bound to it show no data.',
|
|
10226
10420
|
};
|
|
10227
10421
|
/**
|
|
10228
10422
|
* Optional: the host's translations of the MeshBoard widget texts (English without it). Set it
|
|
@@ -10247,6 +10441,7 @@ function resolveMeshBoardMessages(messages) {
|
|
|
10247
10441
|
...DEFAULT_MESHBOARD_MESSAGES,
|
|
10248
10442
|
...(messages?.tableList ? { tableList: messages.tableList } : {}),
|
|
10249
10443
|
...(messages?.tableNotConfigured ? { tableNotConfigured: messages.tableNotConfigured } : {}),
|
|
10444
|
+
...(messages?.scopeResolutionFailed ? { scopeResolutionFailed: messages.scopeResolutionFailed } : {}),
|
|
10250
10445
|
};
|
|
10251
10446
|
}
|
|
10252
10447
|
/** The resolved MeshBoard messages of {@link MESHBOARD_MESSAGES} as a signal. Call in an injection context. */
|
|
@@ -11367,6 +11562,9 @@ class GaugeWidgetComponent {
|
|
|
11367
11562
|
}, /* @ts-ignore */
|
|
11368
11563
|
...(ngDevMode ? [{ debugName: "numericValue" }] : /* istanbul ignore next */ []));
|
|
11369
11564
|
formattedValue = computed(() => {
|
|
11565
|
+
// Nothing loaded (e.g. an empty entity-selector scope, AB#7721): no value, not "0".
|
|
11566
|
+
if (!this._data())
|
|
11567
|
+
return '-';
|
|
11370
11568
|
const value = this.numericValue();
|
|
11371
11569
|
return value.toLocaleString('de-AT', {
|
|
11372
11570
|
minimumFractionDigits: value % 1 !== 0 ? 1 : 0,
|
|
@@ -11438,6 +11636,18 @@ class GaugeWidgetComponent {
|
|
|
11438
11636
|
// queryFamily may be undefined for legacy widget configs — the executor
|
|
11439
11637
|
// falls back to a one-time lookup by rtId. streamDataArgs is sent
|
|
11440
11638
|
// unconditionally because the runtime path ignores it.
|
|
11639
|
+
// Entity-selector scope (AB#7721): pending → stay in the loading state and
|
|
11640
|
+
// send no query (the view refreshes once the scope is known); empty → show
|
|
11641
|
+
// no value ('-') without querying the persisted (unscoped) scope.
|
|
11642
|
+
const scope = this.stateService.resolveStreamDataScope(dataSource.entitySelectorId, dataSource.queryFamily);
|
|
11643
|
+
if (scope.kind === 'pending') {
|
|
11644
|
+
return;
|
|
11645
|
+
}
|
|
11646
|
+
if (scope.kind === 'empty') {
|
|
11647
|
+
this._data.set(null);
|
|
11648
|
+
this._isLoading.set(false);
|
|
11649
|
+
return;
|
|
11650
|
+
}
|
|
11441
11651
|
const streamDataArgs = this.buildStreamDataArgs();
|
|
11442
11652
|
const result = await firstValueFrom(this.queryExecutor.execute(dataSource.queryFamily, dataSource.queryRtId, {
|
|
11443
11653
|
fieldFilter: fieldFilter ?? undefined,
|
|
@@ -13540,6 +13750,18 @@ class PieChartWidgetComponent {
|
|
|
13540
13750
|
* Note: isNotConfigured() check in loadData() ensures queryRtId is set.
|
|
13541
13751
|
*/
|
|
13542
13752
|
async loadPersistentQueryData(dataSource) {
|
|
13753
|
+
// Entity-selector scope (AB#7721): pending → stay in the loading state and
|
|
13754
|
+
// send no query (the view refreshes once the scope is known); empty → render
|
|
13755
|
+
// the empty chart without querying the persisted (unscoped) scope.
|
|
13756
|
+
const scope = this.stateService.resolveStreamDataScope(dataSource.entitySelectorId, dataSource.queryFamily);
|
|
13757
|
+
if (scope.kind === 'pending') {
|
|
13758
|
+
return;
|
|
13759
|
+
}
|
|
13760
|
+
if (scope.kind === 'empty') {
|
|
13761
|
+
this._rawData.set([]);
|
|
13762
|
+
this._isLoading.set(false);
|
|
13763
|
+
return;
|
|
13764
|
+
}
|
|
13543
13765
|
const fieldFilter = this.convertFiltersToDto(this.config.filters);
|
|
13544
13766
|
// queryFamily may be undefined for legacy widget configs — the executor
|
|
13545
13767
|
// falls back to a one-time lookup by rtId. streamDataArgs is sent
|
|
@@ -14754,14 +14976,23 @@ class BarChartWidgetComponent {
|
|
|
14754
14976
|
// queryFamily may be undefined for legacy widget configs — the executor
|
|
14755
14977
|
// falls back to a one-time lookup by rtId. streamDataArgs is sent
|
|
14756
14978
|
// unconditionally because the runtime path ignores it.
|
|
14979
|
+
// Entity-selector scope (AB#7721): pending → stay in the loading state and
|
|
14980
|
+
// send no query (the view refreshes once the scope is known); empty → render
|
|
14981
|
+
// the empty chart without querying the persisted (unscoped) scope.
|
|
14982
|
+
const scope = this.stateService.resolveStreamDataScope(queryDataSource.entitySelectorId, queryDataSource.queryFamily);
|
|
14983
|
+
if (scope.kind === 'pending') {
|
|
14984
|
+
return;
|
|
14985
|
+
}
|
|
14757
14986
|
const streamDataArgs = this.buildStreamDataArgs();
|
|
14758
|
-
const result =
|
|
14759
|
-
|
|
14760
|
-
|
|
14761
|
-
|
|
14762
|
-
|
|
14763
|
-
|
|
14764
|
-
|
|
14987
|
+
const result = scope.kind === 'empty'
|
|
14988
|
+
? emptyQueryExecutionResult(queryDataSource.queryFamily)
|
|
14989
|
+
: await firstValueFrom(this.queryExecutor.execute(queryDataSource.queryFamily, queryDataSource.queryRtId, {
|
|
14990
|
+
fieldFilter: fieldFilter ?? undefined,
|
|
14991
|
+
streamDataArgs
|
|
14992
|
+
}).pipe(catchError(err => {
|
|
14993
|
+
console.error('Error loading Bar Chart data:', err);
|
|
14994
|
+
throw err;
|
|
14995
|
+
})));
|
|
14765
14996
|
const filteredRows = result.rows.filter(row => BarChartWidgetComponent.SUPPORTED_ROW_TYPES.has(row.__typename ?? ''));
|
|
14766
14997
|
if (this.isDynamicSeriesMode()) {
|
|
14767
14998
|
this.processDynamicSeriesData(filteredRows);
|
|
@@ -16306,6 +16537,21 @@ class LineChartWidgetComponent {
|
|
|
16306
16537
|
this._isLoading.set(true);
|
|
16307
16538
|
this._error.set(null);
|
|
16308
16539
|
try {
|
|
16540
|
+
// Entity-selector scope (AB#7721): pending → stay in the loading state and
|
|
16541
|
+
// send no query (the view refreshes once the scope is known); empty → render
|
|
16542
|
+
// the empty chart without querying the persisted (unscoped) scope.
|
|
16543
|
+
const scope = this.stateService.resolveStreamDataScope(queryDataSource.entitySelectorId, queryDataSource.queryFamily);
|
|
16544
|
+
if (scope.kind === 'pending') {
|
|
16545
|
+
return;
|
|
16546
|
+
}
|
|
16547
|
+
if (scope.kind === 'empty') {
|
|
16548
|
+
this._resolutionSignal.set(null);
|
|
16549
|
+
this._resolutionTarget.set(null);
|
|
16550
|
+
this.processData([], false);
|
|
16551
|
+
this._dataInfo.set({ rows: 0, points: 0, total: 0 });
|
|
16552
|
+
this._isLoading.set(false);
|
|
16553
|
+
return;
|
|
16554
|
+
}
|
|
16309
16555
|
const fieldFilter = this.convertFiltersToDto(this.config.filters);
|
|
16310
16556
|
// Resolution-aware routing (AB#4290): let the server pick the archive/rollup for the visible
|
|
16311
16557
|
// window + target point count, then downsample each source series against it. Requires a
|
|
@@ -17840,10 +18086,7 @@ class HeatmapWidgetComponent {
|
|
|
17840
18086
|
if (this.config.compactNumbers) {
|
|
17841
18087
|
return this.formatSI(displayValue, decimals);
|
|
17842
18088
|
}
|
|
17843
|
-
return displayValue
|
|
17844
|
-
minimumFractionDigits: 0,
|
|
17845
|
-
maximumFractionDigits: decimals
|
|
17846
|
-
});
|
|
18089
|
+
return formatWidgetNumber(displayValue, decimals, 'maximum');
|
|
17847
18090
|
}
|
|
17848
18091
|
/**
|
|
17849
18092
|
* Formats a number using SI prefixes (k, M, G, T).
|
|
@@ -17860,16 +18103,10 @@ class HeatmapWidgetComponent {
|
|
|
17860
18103
|
for (const [threshold, prefix] of siPrefixes) {
|
|
17861
18104
|
if (abs >= threshold) {
|
|
17862
18105
|
const scaled = value / threshold;
|
|
17863
|
-
return scaled
|
|
17864
|
-
minimumFractionDigits: 0,
|
|
17865
|
-
maximumFractionDigits: decimals
|
|
17866
|
-
}) + prefix;
|
|
18106
|
+
return formatWidgetNumber(scaled, decimals, 'maximum') + prefix;
|
|
17867
18107
|
}
|
|
17868
18108
|
}
|
|
17869
|
-
return value
|
|
17870
|
-
minimumFractionDigits: 0,
|
|
17871
|
-
maximumFractionDigits: decimals
|
|
17872
|
-
});
|
|
18109
|
+
return formatWidgetNumber(value, decimals, 'maximum');
|
|
17873
18110
|
}
|
|
17874
18111
|
cellLabelContent = (e) => {
|
|
17875
18112
|
if (e.value === 0)
|
|
@@ -17896,14 +18133,23 @@ class HeatmapWidgetComponent {
|
|
|
17896
18133
|
// queryFamily may be undefined for legacy widget configs — the executor
|
|
17897
18134
|
// falls back to a one-time lookup by rtId. streamDataArgs is sent
|
|
17898
18135
|
// unconditionally because the runtime path ignores it.
|
|
18136
|
+
// Entity-selector scope (AB#7721): pending → stay in the loading state and
|
|
18137
|
+
// send no query (the view refreshes once the scope is known); empty → render
|
|
18138
|
+
// the empty grid without querying the persisted (unscoped) scope.
|
|
18139
|
+
const scope = this.stateService.resolveStreamDataScope(queryDataSource.entitySelectorId, queryDataSource.queryFamily);
|
|
18140
|
+
if (scope.kind === 'pending') {
|
|
18141
|
+
return;
|
|
18142
|
+
}
|
|
17899
18143
|
const streamDataArgs = this.buildStreamDataArgs();
|
|
17900
|
-
const result =
|
|
17901
|
-
|
|
17902
|
-
|
|
17903
|
-
|
|
17904
|
-
|
|
17905
|
-
|
|
17906
|
-
|
|
18144
|
+
const result = scope.kind === 'empty'
|
|
18145
|
+
? emptyQueryExecutionResult(queryDataSource.queryFamily)
|
|
18146
|
+
: await firstValueFrom(this.queryExecutor.execute(queryDataSource.queryFamily, queryDataSource.queryRtId, {
|
|
18147
|
+
fieldFilter: fieldFilter ?? undefined,
|
|
18148
|
+
streamDataArgs
|
|
18149
|
+
}).pipe(catchError(err => {
|
|
18150
|
+
console.error('Error loading Heatmap data:', err);
|
|
18151
|
+
throw err;
|
|
18152
|
+
})));
|
|
17907
18153
|
const filteredRows = result.rows.filter(row => HeatmapWidgetComponent.SUPPORTED_ROW_TYPES.has(row.__typename ?? ''));
|
|
17908
18154
|
this.processHeatmapData(filteredRows);
|
|
17909
18155
|
this._isLoading.set(false);
|
|
@@ -19238,6 +19484,13 @@ class StatsGridWidgetComponent {
|
|
|
19238
19484
|
this._statValues.set(initialValues);
|
|
19239
19485
|
this._isLoading.set(true);
|
|
19240
19486
|
this._error.set(null);
|
|
19487
|
+
// Entity-selector scope (AB#7721): while a bound stat's scope is still
|
|
19488
|
+
// resolving, stay in the loading state and send no query at all; the view
|
|
19489
|
+
// refreshes the widget once the scope is known.
|
|
19490
|
+
if (this.config.stats.some(stat => stat.persistentQuerySource
|
|
19491
|
+
&& this.scopeOf(stat.persistentQuerySource).kind === 'pending')) {
|
|
19492
|
+
return;
|
|
19493
|
+
}
|
|
19241
19494
|
try {
|
|
19242
19495
|
// Runtime aggregation queries are only needed for stats WITHOUT a
|
|
19243
19496
|
// per-stat persistent-query source — those run through the executor below.
|
|
@@ -19277,6 +19530,11 @@ class StatsGridWidgetComponent {
|
|
|
19277
19530
|
return aggregationResults.get(stat.queryId) ?? null;
|
|
19278
19531
|
}
|
|
19279
19532
|
async loadPersistentQueryValue(source) {
|
|
19533
|
+
// Empty scope (nothing in scope or lookup failed, AB#7721): no value, no
|
|
19534
|
+
// query — never the persisted (unscoped) scope.
|
|
19535
|
+
if (this.scopeOf(source).kind === 'empty') {
|
|
19536
|
+
return null;
|
|
19537
|
+
}
|
|
19280
19538
|
try {
|
|
19281
19539
|
const result = await firstValueFrom(this.queryExecutor.execute(source.queryFamily, source.queryRtId, {
|
|
19282
19540
|
fieldFilter: this.convertFiltersToDto(source.filters),
|
|
@@ -19289,6 +19547,9 @@ class StatsGridWidgetComponent {
|
|
|
19289
19547
|
return null;
|
|
19290
19548
|
}
|
|
19291
19549
|
}
|
|
19550
|
+
scopeOf(source) {
|
|
19551
|
+
return this.stateService.resolveStreamDataScope(source.entitySelectorId, source.queryFamily);
|
|
19552
|
+
}
|
|
19292
19553
|
buildStreamDataArgs(source) {
|
|
19293
19554
|
const timeArgs = this.stateService.resolveStreamDataTimeArgs(source.ignoreTimeFilter);
|
|
19294
19555
|
const rtIds = this.stateService.resolveStreamDataRtIds(source.entitySelectorId);
|
|
@@ -21005,7 +21266,8 @@ class WidgetGroupComponent {
|
|
|
21005
21266
|
prefix: staticConfig?.prefix,
|
|
21006
21267
|
suffix: staticConfig?.suffix,
|
|
21007
21268
|
trend: staticConfig?.trend,
|
|
21008
|
-
icon: staticConfig?.icon
|
|
21269
|
+
icon: staticConfig?.icon,
|
|
21270
|
+
decimalPlaces: normalizeKpiDecimalPlaces(staticConfig?.decimalPlaces)
|
|
21009
21271
|
};
|
|
21010
21272
|
}
|
|
21011
21273
|
/**
|
|
@@ -21138,6 +21400,9 @@ class WidgetGroupConfigDialogComponent {
|
|
|
21138
21400
|
initialMinChildWidth;
|
|
21139
21401
|
initialGap;
|
|
21140
21402
|
initialEmptyMessage;
|
|
21403
|
+
get maxDecimalPlaces() {
|
|
21404
|
+
return KPI_MAX_DECIMAL_PLACES;
|
|
21405
|
+
}
|
|
21141
21406
|
// Data source mode
|
|
21142
21407
|
dataSourceMode = 'persistentQuery';
|
|
21143
21408
|
// Query mode state
|
|
@@ -21160,6 +21425,7 @@ class WidgetGroupConfigDialogComponent {
|
|
|
21160
21425
|
valueAttribute: '',
|
|
21161
21426
|
prefix: '',
|
|
21162
21427
|
suffix: '',
|
|
21428
|
+
decimalPlaces: null,
|
|
21163
21429
|
gaugeType: 'arc',
|
|
21164
21430
|
gaugeMin: 0,
|
|
21165
21431
|
gaugeMax: 100,
|
|
@@ -21229,6 +21495,8 @@ class WidgetGroupConfigDialogComponent {
|
|
|
21229
21495
|
this.form.prefix = staticConfig.prefix ?? '';
|
|
21230
21496
|
if ('suffix' in staticConfig)
|
|
21231
21497
|
this.form.suffix = staticConfig.suffix ?? '';
|
|
21498
|
+
if ('decimalPlaces' in staticConfig)
|
|
21499
|
+
this.form.decimalPlaces = normalizeKpiDecimalPlaces(staticConfig.decimalPlaces) ?? null;
|
|
21232
21500
|
if ('gaugeType' in staticConfig)
|
|
21233
21501
|
this.form.gaugeType = staticConfig.gaugeType ?? 'arc';
|
|
21234
21502
|
if ('min' in staticConfig)
|
|
@@ -21448,7 +21716,8 @@ class WidgetGroupConfigDialogComponent {
|
|
|
21448
21716
|
case 'kpi':
|
|
21449
21717
|
return {
|
|
21450
21718
|
prefix: this.form.prefix || undefined,
|
|
21451
|
-
suffix: this.form.suffix || undefined
|
|
21719
|
+
suffix: this.form.suffix || undefined,
|
|
21720
|
+
decimalPlaces: normalizeKpiDecimalPlaces(this.form.decimalPlaces)
|
|
21452
21721
|
};
|
|
21453
21722
|
case 'gauge':
|
|
21454
21723
|
return {
|
|
@@ -21675,6 +21944,19 @@ class WidgetGroupConfigDialogComponent {
|
|
|
21675
21944
|
</kendo-textbox>
|
|
21676
21945
|
</div>
|
|
21677
21946
|
</div>
|
|
21947
|
+
<div class="form-field">
|
|
21948
|
+
<label>Decimal Places</label>
|
|
21949
|
+
<kendo-numerictextbox
|
|
21950
|
+
[(ngModel)]="form.decimalPlaces"
|
|
21951
|
+
[min]="0"
|
|
21952
|
+
[max]="maxDecimalPlaces"
|
|
21953
|
+
[step]="1"
|
|
21954
|
+
[decimals]="0"
|
|
21955
|
+
format="n0"
|
|
21956
|
+
placeholder="Automatic">
|
|
21957
|
+
</kendo-numerictextbox>
|
|
21958
|
+
<p class="field-hint">Fixed number of decimal places for numeric values (0-3). Leave empty for automatic (two for a fractional value, none for a whole number).</p>
|
|
21959
|
+
</div>
|
|
21678
21960
|
}
|
|
21679
21961
|
</div>
|
|
21680
21962
|
|
|
@@ -22009,6 +22291,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
22009
22291
|
</kendo-textbox>
|
|
22010
22292
|
</div>
|
|
22011
22293
|
</div>
|
|
22294
|
+
<div class="form-field">
|
|
22295
|
+
<label>Decimal Places</label>
|
|
22296
|
+
<kendo-numerictextbox
|
|
22297
|
+
[(ngModel)]="form.decimalPlaces"
|
|
22298
|
+
[min]="0"
|
|
22299
|
+
[max]="maxDecimalPlaces"
|
|
22300
|
+
[step]="1"
|
|
22301
|
+
[decimals]="0"
|
|
22302
|
+
format="n0"
|
|
22303
|
+
placeholder="Automatic">
|
|
22304
|
+
</kendo-numerictextbox>
|
|
22305
|
+
<p class="field-hint">Fixed number of decimal places for numeric values (0-3). Leave empty for automatic (two for a fractional value, none for a whole number).</p>
|
|
22306
|
+
</div>
|
|
22012
22307
|
}
|
|
22013
22308
|
</div>
|
|
22014
22309
|
|
|
@@ -22879,6 +23174,13 @@ class SummaryCardWidgetComponent {
|
|
|
22879
23174
|
return;
|
|
22880
23175
|
this._isLoading.set(true);
|
|
22881
23176
|
this._error.set(null);
|
|
23177
|
+
// Entity-selector scope (AB#7721): while a bound tile's scope is still
|
|
23178
|
+
// resolving, stay in the loading state and send no query at all; the view
|
|
23179
|
+
// refreshes the widget once the scope is known.
|
|
23180
|
+
if (this.config.tiles.some(tile => tile.persistentQuerySource
|
|
23181
|
+
&& this.scopeOf(tile.persistentQuerySource).kind === 'pending')) {
|
|
23182
|
+
return;
|
|
23183
|
+
}
|
|
22882
23184
|
try {
|
|
22883
23185
|
// Cache entities by rtId to avoid duplicate fetches
|
|
22884
23186
|
const entityCache = new Map();
|
|
@@ -22927,12 +23229,20 @@ class SummaryCardWidgetComponent {
|
|
|
22927
23229
|
return null;
|
|
22928
23230
|
}
|
|
22929
23231
|
async fetchPersistentQueryValue(source) {
|
|
23232
|
+
// Empty scope (nothing in scope or lookup failed, AB#7721): no value, no
|
|
23233
|
+
// query — never the persisted (unscoped) scope.
|
|
23234
|
+
if (this.scopeOf(source).kind === 'empty') {
|
|
23235
|
+
return null;
|
|
23236
|
+
}
|
|
22930
23237
|
const result = await firstValueFrom(this.queryExecutor.execute(source.queryFamily, source.queryRtId, {
|
|
22931
23238
|
fieldFilter: this.convertFiltersToDto(source.filters),
|
|
22932
23239
|
streamDataArgs: this.buildStreamDataArgs(source)
|
|
22933
23240
|
}));
|
|
22934
23241
|
return extractPersistentQueryCellValue(result, source);
|
|
22935
23242
|
}
|
|
23243
|
+
scopeOf(source) {
|
|
23244
|
+
return this.stateService.resolveStreamDataScope(source.entitySelectorId, source.queryFamily);
|
|
23245
|
+
}
|
|
22936
23246
|
buildStreamDataArgs(source) {
|
|
22937
23247
|
const timeArgs = this.stateService.resolveStreamDataTimeArgs(source.ignoreTimeFilter);
|
|
22938
23248
|
const rtIds = this.stateService.resolveStreamDataRtIds(source.entitySelectorId);
|
|
@@ -24639,6 +24949,21 @@ class ProcessDataService {
|
|
|
24639
24949
|
if (!bindingQueryRtId) {
|
|
24640
24950
|
throw new Error('Persistent query binding requires a Query rtId');
|
|
24641
24951
|
}
|
|
24952
|
+
// Entity-selector scope (AB#7721): NO query is sent while the scope is pending
|
|
24953
|
+
// or empty — never the persisted (unscoped) scope. Pending → a `pending` result
|
|
24954
|
+
// (the widget stays loading; a refresh follows once the scope resolves);
|
|
24955
|
+
// empty → no rows.
|
|
24956
|
+
const scope = this.stateService.resolveStreamDataScope(config.bindingEntitySelectorId, bindingQueryFamily);
|
|
24957
|
+
if (scope.kind === 'pending') {
|
|
24958
|
+
return { type: 'query', pending: true, loadedAt: new Date() };
|
|
24959
|
+
}
|
|
24960
|
+
if (scope.kind === 'empty') {
|
|
24961
|
+
return {
|
|
24962
|
+
type: 'query',
|
|
24963
|
+
queryResult: this.mapExecutionResult(emptyQueryExecutionResult(bindingQueryFamily)),
|
|
24964
|
+
loadedAt: new Date()
|
|
24965
|
+
};
|
|
24966
|
+
}
|
|
24642
24967
|
// Convert widget filters to GraphQL format with variable resolution
|
|
24643
24968
|
const fieldFilters = this.convertFiltersToDto(bindingFilters);
|
|
24644
24969
|
// Execute via the unified executor — it routes runtime vs stream-data from
|
|
@@ -24867,13 +25192,16 @@ class ProcessWidgetComponent {
|
|
|
24867
25192
|
this._isLoading.set(true);
|
|
24868
25193
|
this._error.set(null);
|
|
24869
25194
|
this._boundData.set(null);
|
|
25195
|
+
// A bound entity-selector scope that is still resolving keeps the widget loading
|
|
25196
|
+
// until the refresh that follows the resolution (AB#7721).
|
|
25197
|
+
let scopePending = false;
|
|
24870
25198
|
try {
|
|
24871
25199
|
// Check for inline config first
|
|
24872
25200
|
if (this.config.inlineConfig) {
|
|
24873
25201
|
this._diagramConfig.set(this.config.inlineConfig);
|
|
24874
25202
|
await this.loadSymbolDefinitions(this.config.inlineConfig);
|
|
24875
25203
|
await this.loadRuntimeData();
|
|
24876
|
-
await this.loadBoundData();
|
|
25204
|
+
scopePending = await this.loadBoundData();
|
|
24877
25205
|
return;
|
|
24878
25206
|
}
|
|
24879
25207
|
// Load from backend by rtId
|
|
@@ -24882,7 +25210,7 @@ class ProcessWidgetComponent {
|
|
|
24882
25210
|
this._diagramConfig.set(diagram);
|
|
24883
25211
|
await this.loadSymbolDefinitions(diagram);
|
|
24884
25212
|
await this.loadRuntimeData();
|
|
24885
|
-
await this.loadBoundData();
|
|
25213
|
+
scopePending = await this.loadBoundData();
|
|
24886
25214
|
return;
|
|
24887
25215
|
}
|
|
24888
25216
|
// No configuration
|
|
@@ -24893,25 +25221,32 @@ class ProcessWidgetComponent {
|
|
|
24893
25221
|
this._error.set('Failed to load process diagram');
|
|
24894
25222
|
}
|
|
24895
25223
|
finally {
|
|
24896
|
-
|
|
25224
|
+
if (!scopePending) {
|
|
25225
|
+
this._isLoading.set(false);
|
|
25226
|
+
}
|
|
24897
25227
|
}
|
|
24898
25228
|
}
|
|
24899
25229
|
/**
|
|
24900
|
-
* Loads bound data based on widget data binding configuration
|
|
25230
|
+
* Loads bound data based on widget data binding configuration.
|
|
25231
|
+
* Returns `true` when the bound entity-selector scope is still resolving (AB#7721).
|
|
24901
25232
|
*/
|
|
24902
25233
|
async loadBoundData() {
|
|
24903
25234
|
// Only load if data binding is configured
|
|
24904
25235
|
if (!this.config.dataBindingMode || this.config.dataBindingMode === 'none') {
|
|
24905
|
-
return;
|
|
25236
|
+
return false;
|
|
24906
25237
|
}
|
|
24907
25238
|
try {
|
|
24908
25239
|
const boundData = await this.dataService.loadBoundData(this.config);
|
|
25240
|
+
if (boundData?.pending) {
|
|
25241
|
+
return true;
|
|
25242
|
+
}
|
|
24909
25243
|
this._boundData.set(boundData);
|
|
24910
25244
|
}
|
|
24911
25245
|
catch (err) {
|
|
24912
25246
|
console.error('Error loading bound data:', err);
|
|
24913
25247
|
// Don't set error - diagram still displays, just without bound data
|
|
24914
25248
|
}
|
|
25249
|
+
return false;
|
|
24915
25250
|
}
|
|
24916
25251
|
/**
|
|
24917
25252
|
* Loads symbol definitions for all symbol instances in the diagram
|
|
@@ -29796,6 +30131,27 @@ function createDefaultPersistentQueryDataSource() {
|
|
|
29796
30131
|
* Registers all built-in widget types with the registry.
|
|
29797
30132
|
* Called during app initialization.
|
|
29798
30133
|
*/
|
|
30134
|
+
function kpiDisplayFieldsFromResult(result) {
|
|
30135
|
+
return {
|
|
30136
|
+
outputVariableName: result.outputVariableName,
|
|
30137
|
+
prefix: result.prefix,
|
|
30138
|
+
suffix: result.suffix,
|
|
30139
|
+
trend: result.trend,
|
|
30140
|
+
comparisonText: result.comparisonText,
|
|
30141
|
+
decimalPlaces: result.decimalPlaces
|
|
30142
|
+
};
|
|
30143
|
+
}
|
|
30144
|
+
function kpiDisplayFieldsFromPersisted(config) {
|
|
30145
|
+
return {
|
|
30146
|
+
prefix: config['prefix'],
|
|
30147
|
+
suffix: config['suffix'],
|
|
30148
|
+
icon: config['icon'],
|
|
30149
|
+
trend: config['trend'],
|
|
30150
|
+
comparisonText: config['comparisonText'],
|
|
30151
|
+
outputVariableName: config['outputVariableName'],
|
|
30152
|
+
decimalPlaces: normalizeKpiDecimalPlaces(config['decimalPlaces'])
|
|
30153
|
+
};
|
|
30154
|
+
}
|
|
29799
30155
|
function registerDefaultWidgets(registry) {
|
|
29800
30156
|
// Entity Card Widget
|
|
29801
30157
|
registry.registerWidget({
|
|
@@ -29915,6 +30271,7 @@ function registerDefaultWidgets(registry) {
|
|
|
29915
30271
|
initialSuffix: kpiWidget.suffix,
|
|
29916
30272
|
initialTrend: kpiWidget.trend,
|
|
29917
30273
|
initialComparisonText: kpiWidget.comparisonText,
|
|
30274
|
+
initialDecimalPlaces: normalizeKpiDecimalPlaces(kpiWidget.decimalPlaces),
|
|
29918
30275
|
// Filters
|
|
29919
30276
|
initialFilters: kpiWidget.filters
|
|
29920
30277
|
};
|
|
@@ -29932,7 +30289,6 @@ function registerDefaultWidgets(registry) {
|
|
|
29932
30289
|
dataSource: { type: 'static' },
|
|
29933
30290
|
valueMode: 'formula',
|
|
29934
30291
|
formula: result.formula,
|
|
29935
|
-
outputVariableName: result.outputVariableName,
|
|
29936
30292
|
valueAttribute: '',
|
|
29937
30293
|
staticValue: undefined,
|
|
29938
30294
|
labelAttribute: undefined,
|
|
@@ -29940,10 +30296,7 @@ function registerDefaultWidgets(registry) {
|
|
|
29940
30296
|
queryValueField: undefined,
|
|
29941
30297
|
queryCategoryField: undefined,
|
|
29942
30298
|
queryCategoryValue: undefined,
|
|
29943
|
-
|
|
29944
|
-
suffix: result.suffix,
|
|
29945
|
-
trend: result.trend,
|
|
29946
|
-
comparisonText: result.comparisonText,
|
|
30299
|
+
...kpiDisplayFieldsFromResult(result),
|
|
29947
30300
|
filters: undefined
|
|
29948
30301
|
};
|
|
29949
30302
|
}
|
|
@@ -29952,7 +30305,6 @@ function registerDefaultWidgets(registry) {
|
|
|
29952
30305
|
...widget,
|
|
29953
30306
|
valueMode: undefined,
|
|
29954
30307
|
formula: undefined,
|
|
29955
|
-
outputVariableName: result.outputVariableName,
|
|
29956
30308
|
dataSource: { type: 'static' },
|
|
29957
30309
|
valueAttribute: '',
|
|
29958
30310
|
staticValue: result.staticValue,
|
|
@@ -29961,10 +30313,7 @@ function registerDefaultWidgets(registry) {
|
|
|
29961
30313
|
queryValueField: undefined,
|
|
29962
30314
|
queryCategoryField: undefined,
|
|
29963
30315
|
queryCategoryValue: undefined,
|
|
29964
|
-
|
|
29965
|
-
suffix: result.suffix,
|
|
29966
|
-
trend: result.trend,
|
|
29967
|
-
comparisonText: result.comparisonText,
|
|
30316
|
+
...kpiDisplayFieldsFromResult(result),
|
|
29968
30317
|
filters: undefined
|
|
29969
30318
|
};
|
|
29970
30319
|
}
|
|
@@ -29982,7 +30331,6 @@ function registerDefaultWidgets(registry) {
|
|
|
29982
30331
|
...widget,
|
|
29983
30332
|
valueMode: undefined,
|
|
29984
30333
|
formula: undefined,
|
|
29985
|
-
outputVariableName: result.outputVariableName,
|
|
29986
30334
|
dataSource,
|
|
29987
30335
|
valueAttribute: '',
|
|
29988
30336
|
staticValue: undefined,
|
|
@@ -29990,10 +30338,7 @@ function registerDefaultWidgets(registry) {
|
|
|
29990
30338
|
queryValueField: result.queryValueField,
|
|
29991
30339
|
queryCategoryField: result.queryCategoryField,
|
|
29992
30340
|
queryCategoryValue: result.queryCategoryValue,
|
|
29993
|
-
|
|
29994
|
-
suffix: result.suffix,
|
|
29995
|
-
trend: result.trend,
|
|
29996
|
-
comparisonText: result.comparisonText,
|
|
30341
|
+
...kpiDisplayFieldsFromResult(result),
|
|
29997
30342
|
filters
|
|
29998
30343
|
};
|
|
29999
30344
|
}
|
|
@@ -30003,7 +30348,6 @@ function registerDefaultWidgets(registry) {
|
|
|
30003
30348
|
...widget,
|
|
30004
30349
|
valueMode: undefined,
|
|
30005
30350
|
formula: undefined,
|
|
30006
|
-
outputVariableName: result.outputVariableName,
|
|
30007
30351
|
dataSource: createDataSource(result, false),
|
|
30008
30352
|
valueAttribute: result.valueAttribute,
|
|
30009
30353
|
labelAttribute: result.labelAttribute,
|
|
@@ -30012,10 +30356,7 @@ function registerDefaultWidgets(registry) {
|
|
|
30012
30356
|
queryValueField: undefined,
|
|
30013
30357
|
queryCategoryField: undefined,
|
|
30014
30358
|
queryCategoryValue: undefined,
|
|
30015
|
-
|
|
30016
|
-
suffix: result.suffix,
|
|
30017
|
-
trend: result.trend,
|
|
30018
|
-
comparisonText: result.comparisonText,
|
|
30359
|
+
...kpiDisplayFieldsFromResult(result),
|
|
30019
30360
|
filters
|
|
30020
30361
|
};
|
|
30021
30362
|
}
|
|
@@ -30048,6 +30389,7 @@ function registerDefaultWidgets(registry) {
|
|
|
30048
30389
|
icon: widget.icon,
|
|
30049
30390
|
trend: widget.trend,
|
|
30050
30391
|
comparisonText: widget.comparisonText,
|
|
30392
|
+
decimalPlaces: widget.decimalPlaces,
|
|
30051
30393
|
staticValue: widget.staticValue,
|
|
30052
30394
|
valueMode: widget.valueMode,
|
|
30053
30395
|
formula: widget.formula,
|
|
@@ -30088,12 +30430,7 @@ function registerDefaultWidgets(registry) {
|
|
|
30088
30430
|
staticValue: config['staticValue'],
|
|
30089
30431
|
valueMode: config['valueMode'] === 'formula' ? 'formula' : undefined,
|
|
30090
30432
|
formula: config['formula'],
|
|
30091
|
-
|
|
30092
|
-
suffix: config['suffix'],
|
|
30093
|
-
icon: config['icon'],
|
|
30094
|
-
trend: config['trend'],
|
|
30095
|
-
comparisonText: config['comparisonText'],
|
|
30096
|
-
outputVariableName: config['outputVariableName']
|
|
30433
|
+
...kpiDisplayFieldsFromPersisted(config)
|
|
30097
30434
|
};
|
|
30098
30435
|
}
|
|
30099
30436
|
if (dataSource.type === 'persistentQuery') {
|
|
@@ -30108,12 +30445,7 @@ function registerDefaultWidgets(registry) {
|
|
|
30108
30445
|
queryCategoryField: config['queryCategoryField'],
|
|
30109
30446
|
queryCategoryValue: config['queryCategoryValue'],
|
|
30110
30447
|
valueMultiplier: config['valueMultiplier'],
|
|
30111
|
-
|
|
30112
|
-
suffix: config['suffix'],
|
|
30113
|
-
icon: config['icon'],
|
|
30114
|
-
trend: config['trend'],
|
|
30115
|
-
comparisonText: config['comparisonText'],
|
|
30116
|
-
outputVariableName: config['outputVariableName'],
|
|
30448
|
+
...kpiDisplayFieldsFromPersisted(config),
|
|
30117
30449
|
filters: config['filters']
|
|
30118
30450
|
};
|
|
30119
30451
|
}
|
|
@@ -30124,12 +30456,7 @@ function registerDefaultWidgets(registry) {
|
|
|
30124
30456
|
dataSource: dataSource,
|
|
30125
30457
|
valueAttribute: config['valueAttribute'] ?? '_count',
|
|
30126
30458
|
labelAttribute: config['labelAttribute'],
|
|
30127
|
-
|
|
30128
|
-
suffix: config['suffix'],
|
|
30129
|
-
icon: config['icon'],
|
|
30130
|
-
trend: config['trend'],
|
|
30131
|
-
comparisonText: config['comparisonText'],
|
|
30132
|
-
outputVariableName: config['outputVariableName'],
|
|
30459
|
+
...kpiDisplayFieldsFromPersisted(config),
|
|
30133
30460
|
filters: config['filters']
|
|
30134
30461
|
};
|
|
30135
30462
|
}
|
|
@@ -32009,6 +32336,96 @@ function provideWidgetRegistrations(registrations) {
|
|
|
32009
32336
|
]);
|
|
32010
32337
|
}
|
|
32011
32338
|
|
|
32339
|
+
/** True when the selector is configured to follow another selector. */
|
|
32340
|
+
function isFollowerSelector(selector) {
|
|
32341
|
+
return !!selector.followsSelectorId;
|
|
32342
|
+
}
|
|
32343
|
+
/** True when the selector renders a picker in the toolbar. Followers never do. */
|
|
32344
|
+
function showsInToolbar(selector) {
|
|
32345
|
+
return selector.showInToolbar !== false && !isFollowerSelector(selector);
|
|
32346
|
+
}
|
|
32347
|
+
/** Resolves the leader of `selector` among `selectors`. */
|
|
32348
|
+
function resolveFollowLeader(selectors, selector) {
|
|
32349
|
+
const leaderId = selector.followsSelectorId;
|
|
32350
|
+
if (!leaderId) {
|
|
32351
|
+
return { kind: 'none' };
|
|
32352
|
+
}
|
|
32353
|
+
if (leaderId === selector.id) {
|
|
32354
|
+
return { kind: 'invalid', problem: 'self', leaderId };
|
|
32355
|
+
}
|
|
32356
|
+
const leader = selectors.find(s => s.id === leaderId);
|
|
32357
|
+
if (!leader) {
|
|
32358
|
+
return { kind: 'invalid', problem: 'unknown', leaderId };
|
|
32359
|
+
}
|
|
32360
|
+
if (isFollowerSelector(leader)) {
|
|
32361
|
+
return { kind: 'invalid', problem: 'chained', leaderId };
|
|
32362
|
+
}
|
|
32363
|
+
return { kind: 'leader', leader };
|
|
32364
|
+
}
|
|
32365
|
+
/**
|
|
32366
|
+
* The valid followers of `leaderId`: selectors whose `followsSelectorId` is
|
|
32367
|
+
* `leaderId`, provided the leader exists and is not itself a follower.
|
|
32368
|
+
*/
|
|
32369
|
+
function followersOf(selectors, leaderId) {
|
|
32370
|
+
return selectors.filter(s => s.followsSelectorId === leaderId && resolveFollowLeader(selectors, s).kind === 'leader');
|
|
32371
|
+
}
|
|
32372
|
+
/**
|
|
32373
|
+
* Selectors that `selectorId` may follow (the editor's "Follows" list): every
|
|
32374
|
+
* other selector that does not follow one itself.
|
|
32375
|
+
*/
|
|
32376
|
+
function eligibleLeaders(selectors, selectorId) {
|
|
32377
|
+
return selectors.filter(s => s.id !== selectorId && !isFollowerSelector(s));
|
|
32378
|
+
}
|
|
32379
|
+
/** Ids of the selectors that name `selectorId` as their leader (valid or not). */
|
|
32380
|
+
function selectorsFollowing(selectors, selectorId) {
|
|
32381
|
+
return selectors.filter(s => s.id !== selectorId && s.followsSelectorId === selectorId);
|
|
32382
|
+
}
|
|
32383
|
+
/**
|
|
32384
|
+
* The selector as it is resolved at runtime: a follower with a valid leader
|
|
32385
|
+
* picks from the leader's CK type (the editor fixes it to the leader's anyway;
|
|
32386
|
+
* this keeps an out-of-date stored type from breaking the entity fetch).
|
|
32387
|
+
*/
|
|
32388
|
+
function effectiveSelector(selectors, selector) {
|
|
32389
|
+
const resolution = resolveFollowLeader(selectors, selector);
|
|
32390
|
+
if (resolution.kind === 'leader' && resolution.leader.ckTypeId !== selector.ckTypeId) {
|
|
32391
|
+
return { ...selector, ckTypeId: resolution.leader.ckTypeId };
|
|
32392
|
+
}
|
|
32393
|
+
return selector;
|
|
32394
|
+
}
|
|
32395
|
+
/**
|
|
32396
|
+
* Editor validation of a "Follows" choice for the selector `selectorId`.
|
|
32397
|
+
* Returns the message that tells the author why the choice is not allowed, or
|
|
32398
|
+
* `null` when it is valid (including "follows nobody").
|
|
32399
|
+
*/
|
|
32400
|
+
function followValidationError(selectors, selectorId, followsSelectorId) {
|
|
32401
|
+
if (!followsSelectorId) {
|
|
32402
|
+
return null;
|
|
32403
|
+
}
|
|
32404
|
+
if (followsSelectorId === selectorId) {
|
|
32405
|
+
return 'A selector cannot follow itself.';
|
|
32406
|
+
}
|
|
32407
|
+
const followedBy = selectorsFollowing(selectors, selectorId);
|
|
32408
|
+
if (followedBy.length > 0) {
|
|
32409
|
+
return `Followed by ${labelList(followedBy)} — a selector that is followed cannot follow another one.`;
|
|
32410
|
+
}
|
|
32411
|
+
const leader = selectors.find(s => s.id === followsSelectorId);
|
|
32412
|
+
if (!leader) {
|
|
32413
|
+
return `The leader '${followsSelectorId}' does not exist — pick another leader or none.`;
|
|
32414
|
+
}
|
|
32415
|
+
if (isFollowerSelector(leader)) {
|
|
32416
|
+
return `The leader '${leader.label || leader.id}' follows another selector — pick another leader or none.`;
|
|
32417
|
+
}
|
|
32418
|
+
return null;
|
|
32419
|
+
}
|
|
32420
|
+
/** "A, B and C" from the selectors' labels (falling back to their ids). */
|
|
32421
|
+
function labelList(selectors) {
|
|
32422
|
+
const labels = selectors.map(s => s.label || s.id);
|
|
32423
|
+
if (labels.length <= 1) {
|
|
32424
|
+
return labels.join('');
|
|
32425
|
+
}
|
|
32426
|
+
return `${labels.slice(0, -1).join(', ')} and ${labels[labels.length - 1]}`;
|
|
32427
|
+
}
|
|
32428
|
+
|
|
32012
32429
|
/**
|
|
32013
32430
|
* Component for editing entity selector configurations in MeshBoard settings.
|
|
32014
32431
|
* Allows adding, editing, and removing entity selectors.
|
|
@@ -32019,6 +32436,7 @@ class EntitySelectorEditorComponent {
|
|
|
32019
32436
|
attributeSelectorService = inject(AttributeSelectorDialogService);
|
|
32020
32437
|
variableService = inject(MeshBoardVariableService);
|
|
32021
32438
|
getEntitiesByCkTypeGQL = inject(GetEntitiesByCkTypeDtoGQL);
|
|
32439
|
+
confirmationService = inject(ConfirmationService, { optional: true });
|
|
32022
32440
|
entitySelectors = [];
|
|
32023
32441
|
/** Existing variable names from other sources (static variables, other selectors) for duplicate detection */
|
|
32024
32442
|
existingVariableNames = [];
|
|
@@ -32047,6 +32465,10 @@ class EntitySelectorEditorComponent {
|
|
|
32047
32465
|
// the association to its parent), so 'in' is the correct default for a
|
|
32048
32466
|
// childScope that resolves a picked parent to its children.
|
|
32049
32467
|
editChildScopeDirection = 'in';
|
|
32468
|
+
// Leader/follower (AB#7722): id of the selector this one follows ('' = none).
|
|
32469
|
+
editFollowsSelectorId = '';
|
|
32470
|
+
/** Why choosing the leader changed the form (CK type fixed, mappings/default cleared). */
|
|
32471
|
+
followNotice = '';
|
|
32050
32472
|
// Data sources for the default entity picker (created when CK type is set)
|
|
32051
32473
|
defaultEntityDataSource = null;
|
|
32052
32474
|
defaultEntityDialogDataSource = null;
|
|
@@ -32066,6 +32488,8 @@ class EntitySelectorEditorComponent {
|
|
|
32066
32488
|
this.editChildScopeTargetCkTypeItem = null;
|
|
32067
32489
|
this.editChildScopeRoleId = 'System/ParentChild';
|
|
32068
32490
|
this.editChildScopeDirection = 'in';
|
|
32491
|
+
this.editFollowsSelectorId = '';
|
|
32492
|
+
this.followNotice = '';
|
|
32069
32493
|
this.defaultEntityDataSource = null;
|
|
32070
32494
|
this.defaultEntityDialogDataSource = null;
|
|
32071
32495
|
this.isEditing = true;
|
|
@@ -32101,6 +32525,14 @@ class EntitySelectorEditorComponent {
|
|
|
32101
32525
|
: null;
|
|
32102
32526
|
this.editChildScopeRoleId = selector.childScope?.roleId ?? 'System/ParentChild';
|
|
32103
32527
|
this.editChildScopeDirection = selector.childScope?.direction ?? 'in';
|
|
32528
|
+
this.editFollowsSelectorId = selector.followsSelectorId ?? '';
|
|
32529
|
+
this.followNotice = '';
|
|
32530
|
+
// A follower picks from its leader's CK type; re-sync it in case the leader's
|
|
32531
|
+
// type changed after the follower was saved.
|
|
32532
|
+
const leader = resolveFollowLeader(this.entitySelectors, selector);
|
|
32533
|
+
if (leader.kind === 'leader' && leader.leader.ckTypeId !== this.editCkTypeId) {
|
|
32534
|
+
this.applyLeaderCkType(leader.leader.ckTypeId);
|
|
32535
|
+
}
|
|
32104
32536
|
this.updateDefaultEntityDataSources();
|
|
32105
32537
|
this.isEditing = true;
|
|
32106
32538
|
this.editingStateChange.emit(true);
|
|
@@ -32108,10 +32540,82 @@ class EntitySelectorEditorComponent {
|
|
|
32108
32540
|
/**
|
|
32109
32541
|
* Removes an entity selector at the given index.
|
|
32110
32542
|
*/
|
|
32111
|
-
removeSelector(index) {
|
|
32543
|
+
async removeSelector(index) {
|
|
32544
|
+
const selector = this.entitySelectors[index];
|
|
32545
|
+
const followers = selector ? selectorsFollowing(this.entitySelectors, selector.id) : [];
|
|
32546
|
+
if (followers.length > 0 && this.confirmationService) {
|
|
32547
|
+
// AB#7722: removing a leader orphans its followers — name them.
|
|
32548
|
+
const name = selector.label || selector.id;
|
|
32549
|
+
const confirmed = await this.confirmationService.showDangerConfirm({
|
|
32550
|
+
title: `Remove selector ${name}?`,
|
|
32551
|
+
targetName: name,
|
|
32552
|
+
consequence: `${labelList(followers)} ${followers.length === 1 ? 'follows' : 'follow'} this selector. `
|
|
32553
|
+
+ 'Without a leader they behave as if the leader had no pick until you choose another leader.',
|
|
32554
|
+
confirmText: 'Remove selector'
|
|
32555
|
+
});
|
|
32556
|
+
if (!confirmed)
|
|
32557
|
+
return;
|
|
32558
|
+
}
|
|
32112
32559
|
this.entitySelectors = this.entitySelectors.filter((_, i) => i !== index);
|
|
32113
32560
|
this.emitChange();
|
|
32114
32561
|
}
|
|
32562
|
+
/** Selectors the edited selector may follow (non-followers, never itself). */
|
|
32563
|
+
get leaderOptions() {
|
|
32564
|
+
return eligibleLeaders(this.entitySelectors, this.editId);
|
|
32565
|
+
}
|
|
32566
|
+
/** Selectors that follow the edited selector (it then cannot follow one itself). */
|
|
32567
|
+
get followedBy() {
|
|
32568
|
+
return this.editingIndex === null ? [] : selectorsFollowing(this.entitySelectors, this.editId);
|
|
32569
|
+
}
|
|
32570
|
+
/** True when the stored leader is not among the selectable leaders (deleted or a follower). */
|
|
32571
|
+
get isLeaderUnavailable() {
|
|
32572
|
+
return !!this.editFollowsSelectorId && !this.leaderOptions.some(l => l.id === this.editFollowsSelectorId);
|
|
32573
|
+
}
|
|
32574
|
+
/** "A, B and C" of the selectors following the edited one. */
|
|
32575
|
+
get followedByLabels() {
|
|
32576
|
+
return labelList(this.followedBy);
|
|
32577
|
+
}
|
|
32578
|
+
/** Validation message of the "Follows" choice, or '' when it is valid. */
|
|
32579
|
+
get followError() {
|
|
32580
|
+
return followValidationError(this.entitySelectors, this.editId, this.editFollowsSelectorId || undefined) ?? '';
|
|
32581
|
+
}
|
|
32582
|
+
/** True while the edited selector follows another one. */
|
|
32583
|
+
get isFollower() {
|
|
32584
|
+
return !!this.editFollowsSelectorId;
|
|
32585
|
+
}
|
|
32586
|
+
/**
|
|
32587
|
+
* Handles a change of the "Follows" choice. Choosing a leader fixes the CK type
|
|
32588
|
+
* to the leader's (clearing default entity and mappings with a notice when the
|
|
32589
|
+
* type changes) and hides the toolbar option; back to none makes the CK type
|
|
32590
|
+
* editable again with "Show in Toolbar" unchecked.
|
|
32591
|
+
*/
|
|
32592
|
+
onFollowsChanged(leaderId) {
|
|
32593
|
+
this.editFollowsSelectorId = leaderId;
|
|
32594
|
+
this.followNotice = '';
|
|
32595
|
+
this.editShowInToolbar = false;
|
|
32596
|
+
if (!leaderId)
|
|
32597
|
+
return;
|
|
32598
|
+
const leader = this.entitySelectors.find(s => s.id === leaderId);
|
|
32599
|
+
if (leader && leader.ckTypeId !== this.editCkTypeId) {
|
|
32600
|
+
const cleared = this.editMappings.length > 0 || !!this.editDefaultRtId;
|
|
32601
|
+
this.applyLeaderCkType(leader.ckTypeId);
|
|
32602
|
+
this.updateDefaultEntityDataSources();
|
|
32603
|
+
if (cleared) {
|
|
32604
|
+
this.followNotice = `The CK type is now the leader's (${leader.ckTypeId}); `
|
|
32605
|
+
+ 'the default entity and attribute mappings of the previous type were cleared.';
|
|
32606
|
+
}
|
|
32607
|
+
}
|
|
32608
|
+
}
|
|
32609
|
+
/** True when the selector names a leader that does not exist or is itself a follower. */
|
|
32610
|
+
isOrphanFollower(selector) {
|
|
32611
|
+
return resolveFollowLeader(this.entitySelectors, selector).kind === 'invalid';
|
|
32612
|
+
}
|
|
32613
|
+
applyLeaderCkType(ckTypeId) {
|
|
32614
|
+
this.editCkTypeId = ckTypeId;
|
|
32615
|
+
this.editCkTypeItem = { fullName: ckTypeId, rtCkTypeId: ckTypeId, isAbstract: false, isFinal: false };
|
|
32616
|
+
this.editMappings = [];
|
|
32617
|
+
this.editDefaultRtId = '';
|
|
32618
|
+
}
|
|
32115
32619
|
/**
|
|
32116
32620
|
* Handles CK type selection in the editor form.
|
|
32117
32621
|
*/
|
|
@@ -32201,7 +32705,9 @@ class EntitySelectorEditorComponent {
|
|
|
32201
32705
|
attributeMappings: this.editMappings,
|
|
32202
32706
|
showInToolbar: this.editShowInToolbar,
|
|
32203
32707
|
defaultRtId: this.editDefaultRtId.trim() || undefined,
|
|
32204
|
-
childScope
|
|
32708
|
+
childScope,
|
|
32709
|
+
// A follower never renders a picker (AB#7722).
|
|
32710
|
+
...(this.editFollowsSelectorId ? { showInToolbar: false, followsSelectorId: this.editFollowsSelectorId } : {})
|
|
32205
32711
|
};
|
|
32206
32712
|
if (this.editingIndex !== null) {
|
|
32207
32713
|
this.entitySelectors = this.entitySelectors.map((es, i) => i === this.editingIndex ? selector : es);
|
|
@@ -32229,13 +32735,18 @@ class EntitySelectorEditorComponent {
|
|
|
32229
32735
|
const hasRequiredFields = this.editId.trim().length > 0 &&
|
|
32230
32736
|
this.editLabel.trim().length > 0 &&
|
|
32231
32737
|
this.editCkTypeId.length > 0 &&
|
|
32232
|
-
|
|
32738
|
+
// A follower mostly exists for its child scope: mappings are optional (AB#7722).
|
|
32739
|
+
(this.isFollower || this.editMappings.length > 0) &&
|
|
32233
32740
|
this.editMappings.every(m => m.variableName.trim().length > 0 &&
|
|
32234
32741
|
this.isValidVariableName(m.variableName) &&
|
|
32235
32742
|
!this.isDuplicateVariableName(m.variableName, m.attributePath)) &&
|
|
32236
32743
|
!this.isDuplicateId(this.editId, this.editingIndex);
|
|
32237
|
-
// If not shown in toolbar, a default entity must be selected
|
|
32238
|
-
|
|
32744
|
+
// If not shown in toolbar, a default entity must be selected — except for a
|
|
32745
|
+
// follower, which takes its leader's pick (AB#7722).
|
|
32746
|
+
if (!this.isFollower && !this.editShowInToolbar && !this.editDefaultRtId.trim()) {
|
|
32747
|
+
return false;
|
|
32748
|
+
}
|
|
32749
|
+
if (this.followError) {
|
|
32239
32750
|
return false;
|
|
32240
32751
|
}
|
|
32241
32752
|
return hasRequiredFields;
|
|
@@ -32258,6 +32769,13 @@ class EntitySelectorEditorComponent {
|
|
|
32258
32769
|
* Gets a display label indicating the selector mode.
|
|
32259
32770
|
*/
|
|
32260
32771
|
getModeLabel(selector) {
|
|
32772
|
+
const leader = resolveFollowLeader(this.entitySelectors, selector);
|
|
32773
|
+
if (leader.kind === 'leader') {
|
|
32774
|
+
return `Follows: ${leader.leader.label || leader.leader.id}`;
|
|
32775
|
+
}
|
|
32776
|
+
if (leader.kind === 'invalid') {
|
|
32777
|
+
return 'Follows: missing';
|
|
32778
|
+
}
|
|
32261
32779
|
return selector.showInToolbar !== false ? 'Toolbar' : 'Fixed';
|
|
32262
32780
|
}
|
|
32263
32781
|
/**
|
|
@@ -32321,7 +32839,7 @@ class EntitySelectorEditorComponent {
|
|
|
32321
32839
|
this.entitySelectorsChange.emit([...this.entitySelectors]);
|
|
32322
32840
|
}
|
|
32323
32841
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: EntitySelectorEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
32324
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: EntitySelectorEditorComponent, isStandalone: true, selector: "mm-entity-selector-editor", inputs: { entitySelectors: "entitySelectors", existingVariableNames: "existingVariableNames" }, outputs: { entitySelectorsChange: "entitySelectorsChange", editingStateChange: "editingStateChange" }, ngImport: i0, template: "<div class=\"entity-selector-editor\">\n @if (!isEditing) {\n <!-- Selector List -->\n <div class=\"selector-list\">\n @for (selector of entitySelectors; track selector.id; let i = $index) {\n <div class=\"selector-item\">\n <div class=\"selector-info\">\n <span class=\"selector-label\">{{ selector.label }}</span>\n <span class=\"selector-mode\">{{ getModeLabel(selector) }}</span>\n <span class=\"selector-ck-type\">{{ selector.ckTypeId }}</span>\n <span class=\"selector-mappings\">{{ getMappingSummary(selector) }}</span>\n </div>\n <div class=\"selector-actions\">\n <button\n kendoButton\n [svgIcon]=\"pencilIcon\"\n (click)=\"editSelector(i)\"\n fillMode=\"flat\"\n size=\"small\"\n title=\"Edit selector\"\n [attr.aria-label]=\"'Edit selector ' + (selector.label || selector.ckTypeId)\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"trashIcon\"\n (click)=\"removeSelector(i)\"\n fillMode=\"flat\"\n size=\"small\"\n themeColor=\"error\"\n title=\"Remove selector\"\n [attr.aria-label]=\"'Remove selector ' + (selector.label || selector.ckTypeId)\"\n class=\"remove-btn\">\n </button>\n </div>\n </div>\n }\n\n @if (entitySelectors.length === 0) {\n <div class=\"empty-state\">\n No entity selectors configured. Add one to enable entity-based variables.\n </div>\n }\n </div>\n\n <div class=\"add-button-row\">\n <button\n kendoButton\n [svgIcon]=\"plusIcon\"\n (click)=\"addSelector()\"\n themeColor=\"primary\">\n Add Entity Selector\n </button>\n </div>\n } @else {\n <!-- Edit Form -->\n <div class=\"edit-panel\">\n <div class=\"edit-panel-header\">\n {{ editingIndex !== null ? 'Edit Entity Selector' : 'New Entity Selector' }}\n </div>\n <div class=\"edit-form\">\n <div class=\"form-field\">\n <label class=\"field-label\">ID *</label>\n <kendo-textbox\n [(ngModel)]=\"editId\"\n placeholder=\"e.g., mp\"\n [readonly]=\"editingIndex !== null\">\n </kendo-textbox>\n @if (isDuplicateId(editId, editingIndex)) {\n <span class=\"field-error\">ID must be unique</span>\n }\n <span class=\"field-hint\">Used in URL parameters (es_<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 });
|
|
32325
32843
|
}
|
|
32326
32844
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: EntitySelectorEditorComponent, decorators: [{
|
|
32327
32845
|
type: Component,
|
|
@@ -32334,7 +32852,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
32334
32852
|
SVGIconModule,
|
|
32335
32853
|
CkTypeSelectorInputComponent,
|
|
32336
32854
|
EntitySelectInputComponent
|
|
32337
|
-
], changeDetection: ChangeDetectionStrategy.Eager, template: "<div class=\"entity-selector-editor\">\n @if (!isEditing) {\n <!-- Selector List -->\n <div class=\"selector-list\">\n @for (selector of entitySelectors; track selector.id; let i = $index) {\n <div class=\"selector-item\">\n <div class=\"selector-info\">\n <span class=\"selector-label\">{{ selector.label }}</span>\n <span class=\"selector-mode\">{{ getModeLabel(selector) }}</span>\n <span class=\"selector-ck-type\">{{ selector.ckTypeId }}</span>\n <span class=\"selector-mappings\">{{ getMappingSummary(selector) }}</span>\n </div>\n <div class=\"selector-actions\">\n <button\n kendoButton\n [svgIcon]=\"pencilIcon\"\n (click)=\"editSelector(i)\"\n fillMode=\"flat\"\n size=\"small\"\n title=\"Edit selector\"\n [attr.aria-label]=\"'Edit selector ' + (selector.label || selector.ckTypeId)\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"trashIcon\"\n (click)=\"removeSelector(i)\"\n fillMode=\"flat\"\n size=\"small\"\n themeColor=\"error\"\n title=\"Remove selector\"\n [attr.aria-label]=\"'Remove selector ' + (selector.label || selector.ckTypeId)\"\n class=\"remove-btn\">\n </button>\n </div>\n </div>\n }\n\n @if (entitySelectors.length === 0) {\n <div class=\"empty-state\">\n No entity selectors configured. Add one to enable entity-based variables.\n </div>\n }\n </div>\n\n <div class=\"add-button-row\">\n <button\n kendoButton\n [svgIcon]=\"plusIcon\"\n (click)=\"addSelector()\"\n themeColor=\"primary\">\n Add Entity Selector\n </button>\n </div>\n } @else {\n <!-- Edit Form -->\n <div class=\"edit-panel\">\n <div class=\"edit-panel-header\">\n {{ editingIndex !== null ? 'Edit Entity Selector' : 'New Entity Selector' }}\n </div>\n <div class=\"edit-form\">\n <div class=\"form-field\">\n <label class=\"field-label\">ID *</label>\n <kendo-textbox\n [(ngModel)]=\"editId\"\n placeholder=\"e.g., mp\"\n [readonly]=\"editingIndex !== null\">\n </kendo-textbox>\n @if (isDuplicateId(editId, editingIndex)) {\n <span class=\"field-error\">ID must be unique</span>\n }\n <span class=\"field-hint\">Used in URL parameters (es_<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"] }]
|
|
32338
32856
|
}], propDecorators: { entitySelectors: [{
|
|
32339
32857
|
type: Input
|
|
32340
32858
|
}], existingVariableNames: [{
|
|
@@ -32365,6 +32883,13 @@ class EntitySelectorToolbarComponent {
|
|
|
32365
32883
|
this.updateDataSources();
|
|
32366
32884
|
}
|
|
32367
32885
|
}
|
|
32886
|
+
/**
|
|
32887
|
+
* True when the selector renders a picker. Hidden selectors and followers
|
|
32888
|
+
* (AB#7722) never do; a follower's pick is derived from its leader.
|
|
32889
|
+
*/
|
|
32890
|
+
showsInToolbar(selector) {
|
|
32891
|
+
return showsInToolbar(selector);
|
|
32892
|
+
}
|
|
32368
32893
|
/**
|
|
32369
32894
|
* Returns the data source for a selector.
|
|
32370
32895
|
*/
|
|
@@ -32406,7 +32931,7 @@ class EntitySelectorToolbarComponent {
|
|
|
32406
32931
|
}
|
|
32407
32932
|
}
|
|
32408
32933
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: EntitySelectorToolbarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
32409
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: EntitySelectorToolbarComponent, isStandalone: true, selector: "mm-entity-selector-toolbar", inputs: { entitySelectors: "entitySelectors" }, outputs: { entitySelected: "entitySelected", entityCleared: "entityCleared" }, usesOnChanges: true, ngImport: i0, template: "<div class=\"entity-selector-toolbar\">\n @for (selector of entitySelectors; track selector.id) {\n @if (selector
|
|
32934
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: EntitySelectorToolbarComponent, isStandalone: true, selector: "mm-entity-selector-toolbar", inputs: { entitySelectors: "entitySelectors" }, outputs: { entitySelected: "entitySelected", entityCleared: "entityCleared" }, usesOnChanges: true, ngImport: i0, template: "<div class=\"entity-selector-toolbar\">\n @for (selector of entitySelectors; track selector.id) {\n @if (showsInToolbar(selector)) {\n <div class=\"entity-selector-item\">\n <kendo-label [text]=\"selector.label\">\n <mm-entity-select-input\n [dataSource]=\"getDataSource(selector)\"\n [dialogDataSource]=\"getDialogDataSource(selector)\"\n [initialDisplayValue]=\"selector.selectedDisplayName\"\n [placeholder]=\"'Select ' + selector.label + '...'\"\n [dialogTitle]=\"'Select ' + selector.label\"\n (entitySelected)=\"onEntitySelected(selector.id, $event)\"\n (entityCleared)=\"onEntityCleared(selector.id)\">\n </mm-entity-select-input>\n </kendo-label>\n </div>\n }\n }\n</div>\n", styles: [".entity-selector-toolbar{display:flex;align-items:flex-end;gap:1rem;flex-wrap:wrap}.entity-selector-item{display:flex;flex-direction:column}.entity-selector-item kendo-label{display:flex;flex-direction:column;gap:.25rem}.entity-selector-item mm-entity-select-input{min-width:200px;max-width:300px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: i4$1.LabelComponent, selector: "kendo-label", inputs: ["text", "for", "optional", "labelCssStyle", "labelCssClass"], exportAs: ["kendoLabel"] }, { kind: "component", type: EntitySelectInputComponent, selector: "mm-entity-select-input", inputs: ["dataSource", "placeholder", "minSearchLength", "maxResults", "debounceMs", "prefix", "initialDisplayValue", "dialogDataSource", "dialogTitle", "multiSelect", "advancedSearchLabel", "dialogMessages", "messages", "disabled", "required"], outputs: ["entitySelected", "entityCleared", "entitiesSelected"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
|
|
32410
32935
|
}
|
|
32411
32936
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: EntitySelectorToolbarComponent, decorators: [{
|
|
32412
32937
|
type: Component,
|
|
@@ -32414,7 +32939,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
32414
32939
|
CommonModule,
|
|
32415
32940
|
KENDO_LABELS,
|
|
32416
32941
|
EntitySelectInputComponent
|
|
32417
|
-
], changeDetection: ChangeDetectionStrategy.Eager, template: "<div class=\"entity-selector-toolbar\">\n @for (selector of entitySelectors; track selector.id) {\n @if (selector
|
|
32942
|
+
], changeDetection: ChangeDetectionStrategy.Eager, template: "<div class=\"entity-selector-toolbar\">\n @for (selector of entitySelectors; track selector.id) {\n @if (showsInToolbar(selector)) {\n <div class=\"entity-selector-item\">\n <kendo-label [text]=\"selector.label\">\n <mm-entity-select-input\n [dataSource]=\"getDataSource(selector)\"\n [dialogDataSource]=\"getDialogDataSource(selector)\"\n [initialDisplayValue]=\"selector.selectedDisplayName\"\n [placeholder]=\"'Select ' + selector.label + '...'\"\n [dialogTitle]=\"'Select ' + selector.label\"\n (entitySelected)=\"onEntitySelected(selector.id, $event)\"\n (entityCleared)=\"onEntityCleared(selector.id)\">\n </mm-entity-select-input>\n </kendo-label>\n </div>\n }\n }\n</div>\n", styles: [".entity-selector-toolbar{display:flex;align-items:flex-end;gap:1rem;flex-wrap:wrap}.entity-selector-item{display:flex;flex-direction:column}.entity-selector-item kendo-label{display:flex;flex-direction:column;gap:.25rem}.entity-selector-item mm-entity-select-input{min-width:200px;max-width:300px}\n"] }]
|
|
32418
32943
|
}], propDecorators: { entitySelectors: [{
|
|
32419
32944
|
type: Input
|
|
32420
32945
|
}], entitySelected: [{
|
|
@@ -34262,13 +34787,13 @@ class MeshBoardViewComponent {
|
|
|
34262
34787
|
// Entity Selector computed signals
|
|
34263
34788
|
entitySelectorsConfig = computed(() => this.stateService.getEntitySelectors(), /* @ts-ignore */
|
|
34264
34789
|
...(ngDevMode ? [{ debugName: "entitySelectorsConfig" }] : /* istanbul ignore next */ []));
|
|
34265
|
-
hasEntitySelectors = computed(() => !this.hideEntitySelectors() && (this.entitySelectorsConfig()?.some(es => es
|
|
34790
|
+
hasEntitySelectors = computed(() => !this.hideEntitySelectors() && (this.entitySelectorsConfig()?.some(es => showsInToolbar(es)) ?? false), /* @ts-ignore */
|
|
34266
34791
|
...(ngDevMode ? [{ debugName: "hasEntitySelectors" }] : /* istanbul ignore next */ []));
|
|
34267
34792
|
unselectedToolbarSelectors = computed(() => {
|
|
34268
34793
|
if (this.hideEntitySelectors())
|
|
34269
34794
|
return [];
|
|
34270
34795
|
const selectors = this.entitySelectorsConfig() ?? [];
|
|
34271
|
-
return selectors.filter(es => es
|
|
34796
|
+
return selectors.filter(es => showsInToolbar(es) && !es.selectedRtId);
|
|
34272
34797
|
}, /* @ts-ignore */
|
|
34273
34798
|
...(ngDevMode ? [{ debugName: "unselectedToolbarSelectors" }] : /* istanbul ignore next */ []));
|
|
34274
34799
|
/**
|
|
@@ -34285,6 +34810,7 @@ class MeshBoardViewComponent {
|
|
|
34285
34810
|
}, /* @ts-ignore */
|
|
34286
34811
|
...(ngDevMode ? [{ debugName: "canResetTimeFilter" }] : /* istanbul ignore next */ []));
|
|
34287
34812
|
constructor() {
|
|
34813
|
+
this.messages = injectMeshBoardMessages();
|
|
34288
34814
|
// Effect to sync URL when dashboard changes (after initial load)
|
|
34289
34815
|
effect(() => {
|
|
34290
34816
|
const currentRtId = this.stateService.persistedMeshBoardId();
|
|
@@ -34399,6 +34925,7 @@ class MeshBoardViewComponent {
|
|
|
34399
34925
|
// Load MeshBoard by well-known name from route data
|
|
34400
34926
|
const widgets = await this.stateService.switchToMeshBoardByWellKnownName(wellKnownName);
|
|
34401
34927
|
if (widgets) {
|
|
34928
|
+
this.primeEntitySelectorScopes();
|
|
34402
34929
|
await this.preloadWidgetData(widgets);
|
|
34403
34930
|
}
|
|
34404
34931
|
else {
|
|
@@ -34409,6 +34936,7 @@ class MeshBoardViewComponent {
|
|
|
34409
34936
|
else {
|
|
34410
34937
|
// Load initial MeshBoard (first available)
|
|
34411
34938
|
const widgets = await this.stateService.loadInitialMeshBoard();
|
|
34939
|
+
this.primeEntitySelectorScopes();
|
|
34412
34940
|
await this.preloadWidgetData(widgets);
|
|
34413
34941
|
}
|
|
34414
34942
|
// Initialize time filter variables if enabled with stored selection
|
|
@@ -34470,11 +34998,13 @@ class MeshBoardViewComponent {
|
|
|
34470
34998
|
if (exists) {
|
|
34471
34999
|
// Switch to the requested MeshBoard
|
|
34472
35000
|
const switchedWidgets = await this.stateService.switchToMeshBoard(rtId);
|
|
35001
|
+
this.primeEntitySelectorScopes();
|
|
34473
35002
|
await this.preloadWidgetData(switchedWidgets);
|
|
34474
35003
|
}
|
|
34475
35004
|
else {
|
|
34476
35005
|
// MeshBoard not found - use whatever was loaded initially
|
|
34477
35006
|
console.warn(`MeshBoard with rtId ${rtId} not found, using default`);
|
|
35007
|
+
this.primeEntitySelectorScopes();
|
|
34478
35008
|
await this.preloadWidgetData(widgets);
|
|
34479
35009
|
}
|
|
34480
35010
|
}
|
|
@@ -34781,6 +35311,11 @@ class MeshBoardViewComponent {
|
|
|
34781
35311
|
console.warn(`Entity selector '${selectorId}' not found`);
|
|
34782
35312
|
return;
|
|
34783
35313
|
}
|
|
35314
|
+
if (isFollowerSelector(selector)) {
|
|
35315
|
+
// A follower's pick is derived from its leader (AB#7722).
|
|
35316
|
+
console.warn(`Entity selector '${selectorId}' follows '${selector.followsSelectorId}' and cannot be set directly`);
|
|
35317
|
+
return;
|
|
35318
|
+
}
|
|
34784
35319
|
// Update selection state
|
|
34785
35320
|
this.stateService.updateEntitySelectorSelection(selectorId, rtId, displayName);
|
|
34786
35321
|
// Fetch entity and set variables from attribute mappings
|
|
@@ -34795,12 +35330,80 @@ class MeshBoardViewComponent {
|
|
|
34795
35330
|
* Programmatically clears an entity selector value from outside the component.
|
|
34796
35331
|
*/
|
|
34797
35332
|
clearEntitySelector(selectorId) {
|
|
35333
|
+
const selector = this.stateService.getEntitySelector(selectorId);
|
|
35334
|
+
if (selector && isFollowerSelector(selector)) {
|
|
35335
|
+
console.warn(`Entity selector '${selectorId}' follows '${selector.followsSelectorId}' and cannot be cleared directly`);
|
|
35336
|
+
return;
|
|
35337
|
+
}
|
|
35338
|
+
this.clearSelectorPick(selectorId);
|
|
35339
|
+
// Followers fall back to their own defaultRtId, or to no scope (AB#7722).
|
|
35340
|
+
const followers = this.resetFollowersOf(selectorId);
|
|
35341
|
+
this.stateService.triggerRefresh();
|
|
35342
|
+
if (followers) {
|
|
35343
|
+
void followers.then(() => this.stateService.triggerRefresh());
|
|
35344
|
+
}
|
|
35345
|
+
}
|
|
35346
|
+
/**
|
|
35347
|
+
* Clears one selector's pick, variables and scope state. Without a pick bound
|
|
35348
|
+
* widgets fall back to the persisted scope (AB#7721).
|
|
35349
|
+
*/
|
|
35350
|
+
clearSelectorPick(selectorId) {
|
|
34798
35351
|
this.stateService.updateEntitySelectorSelection(selectorId, undefined);
|
|
34799
35352
|
this.stateService.clearEntitySelectorVariables(selectorId);
|
|
34800
|
-
this.
|
|
35353
|
+
this.scopeRequestSeq.set(selectorId, (this.scopeRequestSeq.get(selectorId) ?? 0) + 1);
|
|
35354
|
+
this.stateService.clearEntitySelectorRtIds(selectorId);
|
|
35355
|
+
}
|
|
35356
|
+
/**
|
|
35357
|
+
* The leader `leaderId` has no pick any more: each of its followers resolves
|
|
35358
|
+
* its own `defaultRtId`, or — without one — has no pick and no scope
|
|
35359
|
+
* (AB#7722). Followers with a default are marked pending synchronously.
|
|
35360
|
+
* Returns a promise for the default resolutions, or `null` when there are none.
|
|
35361
|
+
*/
|
|
35362
|
+
resetFollowersOf(leaderId) {
|
|
35363
|
+
const followers = followersOf(this.stateService.getEntitySelectors(), leaderId);
|
|
35364
|
+
const resolutions = [];
|
|
35365
|
+
for (const follower of followers) {
|
|
35366
|
+
if (follower.defaultRtId) {
|
|
35367
|
+
const rtId = follower.defaultRtId;
|
|
35368
|
+
const resolution = this.resolveEntitySelectorVariables(follower, rtId);
|
|
35369
|
+
// The resolution marked the follower pending synchronously; remember
|
|
35370
|
+
// its sequence so a leader pick made meanwhile keeps its selection.
|
|
35371
|
+
const seq = this.scopeRequestSeq.get(follower.id);
|
|
35372
|
+
resolutions.push(resolution.then(displayName => {
|
|
35373
|
+
if (this.scopeRequestSeq.get(follower.id) === seq) {
|
|
35374
|
+
this.stateService.updateEntitySelectorSelection(follower.id, rtId, displayName);
|
|
35375
|
+
}
|
|
35376
|
+
}));
|
|
35377
|
+
}
|
|
35378
|
+
else {
|
|
35379
|
+
this.clearSelectorPick(follower.id);
|
|
35380
|
+
}
|
|
35381
|
+
}
|
|
35382
|
+
return resolutions.length > 0 ? Promise.all(resolutions).then(() => undefined) : null;
|
|
34801
35383
|
}
|
|
34802
35384
|
/** Resolution errors for runtime entity variables */
|
|
34803
35385
|
variableResolutionErrors = this.stateService.variableResolutionErrors;
|
|
35386
|
+
// Assigned in the constructor: a field initialised from a relative import breaks under the
|
|
35387
|
+
// Vitest module runner (see the frontend-libraries CLAUDE.md).
|
|
35388
|
+
messages;
|
|
35389
|
+
/**
|
|
35390
|
+
* One notice per entity selector whose stream-data scope could not be
|
|
35391
|
+
* resolved (AB#7721), so "nothing in scope" (empty widgets, no notice) and
|
|
35392
|
+
* "lookup failed" (empty widgets plus this notice) are distinguishable.
|
|
35393
|
+
*/
|
|
35394
|
+
scopeResolutionErrors = computed(() => {
|
|
35395
|
+
const template = this.messages().scopeResolutionFailed;
|
|
35396
|
+
return this.stateService.failedEntitySelectorScopeIds().map(id => {
|
|
35397
|
+
const label = this.stateService.getEntitySelector(id)?.label || id;
|
|
35398
|
+
return { selectorId: id, message: template.split('{label}').join(label) };
|
|
35399
|
+
});
|
|
35400
|
+
}, /* @ts-ignore */
|
|
35401
|
+
...(ngDevMode ? [{ debugName: "scopeResolutionErrors" }] : /* istanbul ignore next */ []));
|
|
35402
|
+
/**
|
|
35403
|
+
* Per-selector sequence number of the latest scope resolution, so a slower
|
|
35404
|
+
* resolution of an earlier pick never overwrites the scope of a newer pick.
|
|
35405
|
+
*/
|
|
35406
|
+
scopeRequestSeq = new Map();
|
|
34804
35407
|
// ============================================================================
|
|
34805
35408
|
// Time Filter Event Handlers
|
|
34806
35409
|
// ============================================================================
|
|
@@ -35234,7 +35837,8 @@ class MeshBoardViewComponent {
|
|
|
35234
35837
|
async onEntitySelectorSelected(event) {
|
|
35235
35838
|
const { selectorId, entity } = event;
|
|
35236
35839
|
const selector = this.stateService.getEntitySelector(selectorId);
|
|
35237
|
-
|
|
35840
|
+
// Followers render no picker; their pick is derived from the leader (AB#7722).
|
|
35841
|
+
if (!selector || isFollowerSelector(selector))
|
|
35238
35842
|
return;
|
|
35239
35843
|
// Update selector selection state
|
|
35240
35844
|
this.stateService.updateEntitySelectorSelection(selectorId, entity.rtId, entity.displayName);
|
|
@@ -35251,12 +35855,14 @@ class MeshBoardViewComponent {
|
|
|
35251
35855
|
*/
|
|
35252
35856
|
async onEntitySelectorCleared(event) {
|
|
35253
35857
|
const { selectorId } = event;
|
|
35254
|
-
|
|
35255
|
-
|
|
35256
|
-
|
|
35257
|
-
|
|
35258
|
-
//
|
|
35259
|
-
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);
|
|
35260
35866
|
// Sync to URL
|
|
35261
35867
|
this.writeEntitySelectorsToUrl();
|
|
35262
35868
|
// Refresh widgets
|
|
@@ -35266,84 +35872,179 @@ class MeshBoardViewComponent {
|
|
|
35266
35872
|
* Initializes entity selectors from URL params, route data, or defaults.
|
|
35267
35873
|
*/
|
|
35268
35874
|
async initializeEntitySelectors() {
|
|
35875
|
+
const picks = this.initialEntitySelectorPicks(true);
|
|
35876
|
+
if (picks.length === 0)
|
|
35877
|
+
return;
|
|
35878
|
+
// Mark every picked scope pending before the first await, so no bound
|
|
35879
|
+
// widget queries the persisted scope while the picks resolve (AB#7721).
|
|
35880
|
+
this.primeEntitySelectorScopes();
|
|
35881
|
+
for (const { selector, rtId, viaLeader } of picks) {
|
|
35882
|
+
// A follower whose leader has a pick is resolved together with the
|
|
35883
|
+
// leader (one entity fetch, AB#7722).
|
|
35884
|
+
if (viaLeader)
|
|
35885
|
+
continue;
|
|
35886
|
+
const displayName = await this.resolveEntitySelectorVariables(selector, rtId);
|
|
35887
|
+
this.stateService.updateEntitySelectorSelection(selector.id, rtId, displayName);
|
|
35888
|
+
}
|
|
35889
|
+
// Widgets reload only when their config reference changes. Bound widgets
|
|
35890
|
+
// rendered while their scope was pending must now load with the resolved
|
|
35891
|
+
// scope (AB#7721).
|
|
35892
|
+
this.stateService.triggerRefresh();
|
|
35893
|
+
}
|
|
35894
|
+
/**
|
|
35895
|
+
* The initial pick of each entity selector. Priority: route data > URL
|
|
35896
|
+
* params > defaultRtId. Selectors without a pick are left out.
|
|
35897
|
+
*
|
|
35898
|
+
* Followers (AB#7722) never read route data or the URL: a follower takes its
|
|
35899
|
+
* leader's initial pick (`viaLeader: true`, resolved together with the
|
|
35900
|
+
* leader) and falls back to its own `defaultRtId` only when the leader has
|
|
35901
|
+
* none. An unknown or chained leader counts as "leader has no pick"; with
|
|
35902
|
+
* `warn` set this logs a console warning.
|
|
35903
|
+
*/
|
|
35904
|
+
initialEntitySelectorPicks(warn = false) {
|
|
35269
35905
|
const selectors = this.stateService.getEntitySelectors();
|
|
35270
35906
|
if (selectors.length === 0)
|
|
35271
|
-
return;
|
|
35272
|
-
// Read URL params
|
|
35907
|
+
return [];
|
|
35273
35908
|
const urlSelections = this.readEntitySelectorsFromUrl();
|
|
35274
|
-
// Read route data
|
|
35275
35909
|
const routeData = this.route.snapshot.data['entitySelectors'];
|
|
35910
|
+
const ownPick = (selector) => routeData?.[selector.id] ?? urlSelections.get(selector.id) ?? selector.defaultRtId;
|
|
35911
|
+
const picks = [];
|
|
35276
35912
|
for (const selector of selectors) {
|
|
35277
|
-
|
|
35278
|
-
|
|
35279
|
-
|
|
35280
|
-
|
|
35281
|
-
|
|
35282
|
-
|
|
35283
|
-
|
|
35913
|
+
const leader = resolveFollowLeader(selectors, selector);
|
|
35914
|
+
if (leader.kind === 'none') {
|
|
35915
|
+
const rtId = ownPick(selector);
|
|
35916
|
+
if (rtId)
|
|
35917
|
+
picks.push({ selector, rtId, viaLeader: false });
|
|
35918
|
+
continue;
|
|
35919
|
+
}
|
|
35920
|
+
if (leader.kind === 'invalid' && warn) {
|
|
35921
|
+
console.warn(`Entity selector '${selector.id}' follows '${leader.leaderId}', which ${leader.problem === 'unknown' ? 'does not exist' : leader.problem === 'self' ? 'is itself' : 'follows another selector'} — treated as "leader has no pick"`);
|
|
35922
|
+
}
|
|
35923
|
+
const leaderRtId = leader.kind === 'leader' ? ownPick(leader.leader) : undefined;
|
|
35924
|
+
if (leaderRtId) {
|
|
35925
|
+
picks.push({ selector, rtId: leaderRtId, viaLeader: true });
|
|
35926
|
+
}
|
|
35927
|
+
else if (selector.defaultRtId) {
|
|
35928
|
+
picks.push({ selector, rtId: selector.defaultRtId, viaLeader: false });
|
|
35929
|
+
}
|
|
35930
|
+
}
|
|
35931
|
+
return picks;
|
|
35932
|
+
}
|
|
35933
|
+
/**
|
|
35934
|
+
* Marks the scope of every selector that has an initial pick as pending
|
|
35935
|
+
* (AB#7721). Called synchronously right after a board is loaded, before
|
|
35936
|
+
* change detection renders its widgets, so a bound stream-data widget starts
|
|
35937
|
+
* in its loading state instead of querying the persisted (unscoped) scope.
|
|
35938
|
+
* Selectors without a pick stay without scope state (persisted scope).
|
|
35939
|
+
*/
|
|
35940
|
+
primeEntitySelectorScopes() {
|
|
35941
|
+
for (const { selector } of this.initialEntitySelectorPicks()) {
|
|
35942
|
+
if (!this.stateService.getEntitySelectorScopeState(selector.id)) {
|
|
35943
|
+
this.stateService.setEntitySelectorScopePending(selector.id);
|
|
35284
35944
|
}
|
|
35285
35945
|
}
|
|
35286
35946
|
}
|
|
35287
35947
|
/**
|
|
35288
|
-
* Resolves variables for an entity selector by fetching entity attributes
|
|
35948
|
+
* Resolves variables for an entity selector by fetching entity attributes,
|
|
35949
|
+
* then its stream-data scope.
|
|
35950
|
+
*
|
|
35951
|
+
* A leader's pick is also the pick of all its followers (AB#7722): they are
|
|
35952
|
+
* marked pending together with the leader, the picked entity is fetched
|
|
35953
|
+
* once, and each follower gets its own variables (its attribute mappings,
|
|
35954
|
+
* `<id>_rtId`, `<id>_rtCkTypeId`), its own scope (its `childScope`) and the
|
|
35955
|
+
* leader's selection. Returns the picked entity's display name.
|
|
35289
35956
|
*/
|
|
35290
35957
|
async resolveEntitySelectorVariables(selector, rtId) {
|
|
35958
|
+
const selectors = this.stateService.getEntitySelectors();
|
|
35959
|
+
const resolved = effectiveSelector(selectors, selector);
|
|
35960
|
+
const followers = followersOf(selectors, selector.id).map(f => effectiveSelector(selectors, f));
|
|
35961
|
+
const targets = [resolved, ...followers];
|
|
35962
|
+
// The scope of the new pick is unknown until it resolves: bound widgets
|
|
35963
|
+
// must neither keep the previous pick's scope nor fall back to the
|
|
35964
|
+
// persisted one (AB#7721). Followers are pending while the leader is.
|
|
35965
|
+
const seqs = targets.map(t => this.beginScopeResolution(t.id));
|
|
35966
|
+
const failAll = () => targets.forEach((t, i) => this.failScopeResolution(t.id, seqs[i]));
|
|
35291
35967
|
try {
|
|
35292
35968
|
const entity = await firstValueFrom(this.dataService.fetchRuntimeEntity({
|
|
35293
35969
|
type: 'runtimeEntity',
|
|
35294
|
-
ckTypeId:
|
|
35970
|
+
ckTypeId: resolved.ckTypeId,
|
|
35295
35971
|
rtId
|
|
35296
35972
|
}));
|
|
35297
35973
|
if (!entity) {
|
|
35298
35974
|
console.warn(`Entity not found for selector '${selector.id}': ${rtId}`);
|
|
35975
|
+
// The pick cannot be resolved: bound widgets show no data instead of
|
|
35976
|
+
// waiting forever or falling back to the persisted scope (AB#7721).
|
|
35977
|
+
failAll();
|
|
35299
35978
|
return undefined;
|
|
35300
35979
|
}
|
|
35301
|
-
|
|
35302
|
-
|
|
35303
|
-
|
|
35304
|
-
|
|
35305
|
-
|
|
35306
|
-
|
|
35307
|
-
|
|
35308
|
-
: 'string'
|
|
35309
|
-
};
|
|
35980
|
+
// A newer pick (of the leader, or a follower's own default) may have
|
|
35981
|
+
// started meanwhile; never overwrite its variables or selection.
|
|
35982
|
+
const isCurrent = (i) => this.scopeRequestSeq.get(targets[i].id) === seqs[i];
|
|
35983
|
+
targets.forEach((target, i) => {
|
|
35984
|
+
if (isCurrent(i)) {
|
|
35985
|
+
this.stateService.setEntitySelectorVariables(target.id, this.selectorVariables(target, entity, rtId));
|
|
35986
|
+
}
|
|
35310
35987
|
});
|
|
35311
|
-
// Auto-expose the selected entity's rtId as `$<selectorId>_rtId` so SD/runtime
|
|
35312
|
-
// widgets can scope to the picked asset (parent-association filter) without the
|
|
35313
|
-
// designer adding a fake attribute mapping. An explicit mapping to the same
|
|
35314
|
-
// variable name wins (skip the synthetic one if already present).
|
|
35315
|
-
const rtIdVariableName = `${selector.id}_rtId`;
|
|
35316
|
-
if (!values.some(v => v.name === rtIdVariableName)) {
|
|
35317
|
-
values.push({ name: rtIdVariableName, value: rtId, type: 'string' });
|
|
35318
|
-
}
|
|
35319
|
-
// Auto-expose the selected entity's CK type as `$<selectorId>_rtCkTypeId`
|
|
35320
|
-
// so entity-display widgets (e.g. the entity card) can resolve both the
|
|
35321
|
-
// rtId and its type from a single board-level selection. Prefer the picked
|
|
35322
|
-
// entity's actual type (it may be a subtype of the selector's configured
|
|
35323
|
-
// `ckTypeId`); fall back to the selector's type. An explicit mapping to the
|
|
35324
|
-
// same variable name wins.
|
|
35325
|
-
const rtCkTypeIdVariableName = `${selector.id}_rtCkTypeId`;
|
|
35326
|
-
if (!values.some(v => v.name === rtCkTypeIdVariableName)) {
|
|
35327
|
-
values.push({ name: rtCkTypeIdVariableName, value: entity.ckTypeId ?? selector.ckTypeId, type: 'string' });
|
|
35328
|
-
}
|
|
35329
|
-
this.stateService.setEntitySelectorVariables(selector.id, values);
|
|
35330
|
-
// Resolve the stream-data scope rtIds for this selection (one-hop
|
|
35331
|
-
// childScope traversal, or the picked entity itself). Runs alongside the
|
|
35332
|
-
// variable resolution so widgets bound to this selector can scope their
|
|
35333
|
-
// stream-data query by the resolved source rtIds.
|
|
35334
|
-
await this.resolveEntitySelectorScopeRtIds(selector, rtId);
|
|
35335
35988
|
// Label the selection the way the pick dialog does: by the engine's
|
|
35336
35989
|
// display name (the type's displayNameRule — "2026" for a fiscal year),
|
|
35337
35990
|
// not the well-known name, which most entities do not carry. Without it
|
|
35338
35991
|
// a board opened on its defaultRtId showed the raw rtId in the toolbar
|
|
35339
35992
|
// until the user re-picked the very same entity.
|
|
35340
|
-
|
|
35993
|
+
const displayName = entity.rtDisplayName || entity.rtWellKnownName || entity.rtId;
|
|
35994
|
+
followers.forEach((follower, i) => {
|
|
35995
|
+
if (isCurrent(i + 1)) {
|
|
35996
|
+
this.stateService.updateEntitySelectorSelection(follower.id, rtId, displayName);
|
|
35997
|
+
}
|
|
35998
|
+
});
|
|
35999
|
+
// Resolve the stream-data scope rtIds for this selection (one-hop
|
|
36000
|
+
// childScope traversal, or the picked entity itself), per selector, so
|
|
36001
|
+
// widgets bound to the leader or any follower scope their stream-data
|
|
36002
|
+
// query by the resolved source rtIds.
|
|
36003
|
+
await Promise.all(targets.map((t, i) => this.resolveEntitySelectorScopeRtIds(t, rtId, seqs[i])));
|
|
36004
|
+
return displayName;
|
|
35341
36005
|
}
|
|
35342
36006
|
catch (error) {
|
|
35343
36007
|
console.error(`Error resolving entity selector '${selector.id}':`, error);
|
|
36008
|
+
// Same as above: a failed pick is an empty scope, never the persisted one.
|
|
36009
|
+
failAll();
|
|
35344
36010
|
return undefined;
|
|
35345
36011
|
}
|
|
35346
36012
|
}
|
|
36013
|
+
/**
|
|
36014
|
+
* The variables one selector derives from the picked entity: its attribute
|
|
36015
|
+
* mappings plus the auto-exposed `<id>_rtId` and `<id>_rtCkTypeId`.
|
|
36016
|
+
*/
|
|
36017
|
+
selectorVariables(selector, entity, rtId) {
|
|
36018
|
+
const values = selector.attributeMappings.map(mapping => {
|
|
36019
|
+
const attr = entity.attributes.find(a => a.attributeName === mapping.attributePath);
|
|
36020
|
+
return {
|
|
36021
|
+
name: mapping.variableName,
|
|
36022
|
+
value: attr ? String(attr.value ?? '') : '',
|
|
36023
|
+
type: mapping.attributeValueType
|
|
36024
|
+
? this.variableService.mapAttributeTypeToVariableType(mapping.attributeValueType)
|
|
36025
|
+
: 'string'
|
|
36026
|
+
};
|
|
36027
|
+
});
|
|
36028
|
+
// Auto-expose the selected entity's rtId as `$<selectorId>_rtId` so SD/runtime
|
|
36029
|
+
// widgets can scope to the picked asset (parent-association filter) without the
|
|
36030
|
+
// designer adding a fake attribute mapping. An explicit mapping to the same
|
|
36031
|
+
// variable name wins (skip the synthetic one if already present).
|
|
36032
|
+
const rtIdVariableName = `${selector.id}_rtId`;
|
|
36033
|
+
if (!values.some(v => v.name === rtIdVariableName)) {
|
|
36034
|
+
values.push({ name: rtIdVariableName, value: rtId, type: 'string' });
|
|
36035
|
+
}
|
|
36036
|
+
// Auto-expose the selected entity's CK type as `$<selectorId>_rtCkTypeId`
|
|
36037
|
+
// so entity-display widgets (e.g. the entity card) can resolve both the
|
|
36038
|
+
// rtId and its type from a single board-level selection. Prefer the picked
|
|
36039
|
+
// entity's actual type (it may be a subtype of the selector's configured
|
|
36040
|
+
// `ckTypeId`); fall back to the selector's type. An explicit mapping to the
|
|
36041
|
+
// same variable name wins.
|
|
36042
|
+
const rtCkTypeIdVariableName = `${selector.id}_rtCkTypeId`;
|
|
36043
|
+
if (!values.some(v => v.name === rtCkTypeIdVariableName)) {
|
|
36044
|
+
values.push({ name: rtCkTypeIdVariableName, value: entity.ckTypeId ?? selector.ckTypeId, type: 'string' });
|
|
36045
|
+
}
|
|
36046
|
+
return values;
|
|
36047
|
+
}
|
|
35347
36048
|
/**
|
|
35348
36049
|
* Resolves the set of source rtIds that a stream-data widget bound to this
|
|
35349
36050
|
* selector should be scoped to, and caches them on the state service.
|
|
@@ -35353,7 +36054,10 @@ class MeshBoardViewComponent {
|
|
|
35353
36054
|
* become the scope (e.g. MeteringPoint → its EnergyMeasurement rtIds). With
|
|
35354
36055
|
* no childScope, the picked entity's own rtId is the scope (direct keying).
|
|
35355
36056
|
*/
|
|
35356
|
-
async resolveEntitySelectorScopeRtIds(selector, rtId) {
|
|
36057
|
+
async resolveEntitySelectorScopeRtIds(selector, rtId, seq) {
|
|
36058
|
+
const isCurrent = () => this.scopeRequestSeq.get(selector.id) === seq;
|
|
36059
|
+
if (!isCurrent())
|
|
36060
|
+
return;
|
|
35357
36061
|
const childScope = selector.childScope;
|
|
35358
36062
|
if (!childScope?.targetCkTypeId || !childScope.roleId) {
|
|
35359
36063
|
this.stateService.setEntitySelectorRtIds(selector.id, [rtId]);
|
|
@@ -35364,16 +36068,39 @@ class MeshBoardViewComponent {
|
|
|
35364
36068
|
// Parent → children via System/ParentChild is an INBOUND hop (the child
|
|
35365
36069
|
// owns the association to its parent), so 'in' is the default.
|
|
35366
36070
|
childScope.direction ?? 'in'));
|
|
36071
|
+
if (!isCurrent())
|
|
36072
|
+
return;
|
|
35367
36073
|
const childRtIds = (targets ?? [])
|
|
35368
36074
|
.map(t => String(t.rtId))
|
|
35369
36075
|
.filter(id => id.length > 0);
|
|
36076
|
+
// An empty list is a resolved EMPTY scope: bound widgets show no data and
|
|
36077
|
+
// send no query — never the persisted scope, which would show rows of
|
|
36078
|
+
// other entities (AB#7721).
|
|
35370
36079
|
this.stateService.setEntitySelectorRtIds(selector.id, childRtIds);
|
|
35371
36080
|
}
|
|
35372
36081
|
catch (error) {
|
|
36082
|
+
if (!isCurrent())
|
|
36083
|
+
return;
|
|
36084
|
+
// Logged once per failed resolution; the view shows a notice and bound
|
|
36085
|
+
// widgets show no data (AB#7721).
|
|
35373
36086
|
console.error(`Error resolving child scope for selector '${selector.id}':`, error);
|
|
35374
|
-
|
|
35375
|
-
|
|
35376
|
-
|
|
36087
|
+
this.stateService.setEntitySelectorScopeFailed(selector.id);
|
|
36088
|
+
}
|
|
36089
|
+
}
|
|
36090
|
+
/**
|
|
36091
|
+
* Starts the scope resolution of a new pick: marks the scope pending and
|
|
36092
|
+
* returns the sequence number that identifies this resolution (AB#7721).
|
|
36093
|
+
*/
|
|
36094
|
+
beginScopeResolution(selectorId) {
|
|
36095
|
+
const seq = (this.scopeRequestSeq.get(selectorId) ?? 0) + 1;
|
|
36096
|
+
this.scopeRequestSeq.set(selectorId, seq);
|
|
36097
|
+
this.stateService.setEntitySelectorScopePending(selectorId);
|
|
36098
|
+
return seq;
|
|
36099
|
+
}
|
|
36100
|
+
/** Marks the scope failed — unless a newer pick has started resolving meanwhile. */
|
|
36101
|
+
failScopeResolution(selectorId, seq) {
|
|
36102
|
+
if (this.scopeRequestSeq.get(selectorId) === seq) {
|
|
36103
|
+
this.stateService.setEntitySelectorScopeFailed(selectorId);
|
|
35377
36104
|
}
|
|
35378
36105
|
}
|
|
35379
36106
|
/**
|
|
@@ -35384,6 +36111,9 @@ class MeshBoardViewComponent {
|
|
|
35384
36111
|
const selectors = this.stateService.getEntitySelectors();
|
|
35385
36112
|
const params = {};
|
|
35386
36113
|
for (const selector of selectors) {
|
|
36114
|
+
// Followers are derived from their leader and never serialized (AB#7722).
|
|
36115
|
+
if (isFollowerSelector(selector))
|
|
36116
|
+
continue;
|
|
35387
36117
|
params[`es_${selector.id}`] = selector.selectedRtId ?? null;
|
|
35388
36118
|
}
|
|
35389
36119
|
// Same single-URL serialization as writeTimeFilterToUrl: no relative
|
|
@@ -35437,7 +36167,7 @@ class MeshBoardViewComponent {
|
|
|
35437
36167
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: MeshBoardViewComponent, isStandalone: true, selector: "mm-meshboard-view", inputs: { headerMode: { classPropertyName: "headerMode", publicName: "headerMode", isSignal: true, isRequired: false, transformFunction: null }, chrome: { classPropertyName: "chrome", publicName: "chrome", isSignal: true, isRequired: false, transformFunction: null } }, providers: [
|
|
35438
36168
|
{ provide: HAS_UNSAVED_CHANGES, useExisting: MeshBoardViewComponent },
|
|
35439
36169
|
AutoRefreshTimerService
|
|
35440
|
-
], viewQueries: [{ propertyName: "tileLayout", first: true, predicate: TileLayoutComponent, descendants: true }], hostDirectives: [{ directive: i1$8.UnsavedChangesDirective }], ngImport: i0, template: "<div class=\"meshboard-view\" [class.chrome-plain]=\"effectiveChrome() === 'plain'\">\n @if (isLoading()) {\n <div class=\"loading-container\">\n <div class=\"k-loading-mask\">\n <span class=\"k-loading-text\">Loading MeshBoard...</span>\n <div class=\"k-loading-image\"></div>\n <div class=\"k-loading-color\"></div>\n </div>\n </div>\n } @else if (isModelAvailable() === false) {\n <div class=\"model-unavailable\">\n <div class=\"message-card\">\n <kendo-svg-icon [icon]=\"xIcon\" size=\"xlarge\"></kendo-svg-icon>\n <h2>MeshBoard Not Available</h2>\n <p>The MeshBoard feature requires the CK model 'System.UI' version 1.0.1 or higher.</p>\n <p>Please install the required model in your tenant to use this feature.</p>\n </div>\n </div>\n } @else if (notFoundError()) {\n <div class=\"model-unavailable\">\n <div class=\"message-card\">\n <kendo-svg-icon [icon]=\"xIcon\" size=\"xlarge\"></kendo-svg-icon>\n <h2>MeshBoard Not Found</h2>\n <p>{{ notFoundError() }}</p>\n <p>To create a MeshBoard with a Well-Known Name:</p>\n <ol>\n <li>Go to the <a [routerLink]=\"meshBoardPageLink()\">MeshBoard</a> page</li>\n <li>Open Settings</li>\n <li>Set the \"Well-Known Name\" field</li>\n <li>Save the MeshBoard</li>\n </ol>\n </div>\n </div>\n } @else if (isInitialized()) {\n <!-- Toolbar (headerMode: full = name + description + controls, compact = controls only, none = no row) -->\n @if (effectiveHeaderMode() !== 'none') {\n <div class=\"meshboard-toolbar\" [class.compact]=\"effectiveHeaderMode() === 'compact'\">\n @if (effectiveHeaderMode() === 'full') {\n <div class=\"toolbar-left\">\n <h2 class=\"meshboard-title\">{{ config().name }}</h2>\n @if (config().description) {\n <span class=\"meshboard-description\">{{ config().description }}</span>\n }\n </div>\n }\n\n <!-- Time Filter (center) -->\n @if (isTimeFilterEnabled()) {\n <div class=\"toolbar-center\">\n <mm-time-range-picker\n [config]=\"timeFilterConfig()?.pickerConfig ?? {}\"\n [labels]=\"timeRangeLabels()\"\n [initialSelection]=\"initialTimeSelection()\"\n (rangeChange)=\"onTimeRangeChange($event)\"\n (selectionChange)=\"onTimeSelectionChange($event)\">\n </mm-time-range-picker>\n @if (canResetTimeFilter()) {\n <button\n kendoButton\n [svgIcon]=\"undoIcon\"\n (click)=\"resetTimeFilterToDefault()\"\n title=\"Reset to default time filter\"\n aria-label=\"Reset to default time filter\"\n fillMode=\"flat\"\n size=\"small\">\n </button>\n }\n </div>\n }\n\n <!-- Entity Selectors -->\n @if (hasEntitySelectors()) {\n @if (isTimeFilterEnabled()) {\n <div class=\"toolbar-separator\"></div>\n }\n <div class=\"toolbar-entity-selectors\">\n <mm-entity-selector-toolbar\n [entitySelectors]=\"entitySelectorsConfig()\"\n (entitySelected)=\"onEntitySelectorSelected($event)\"\n (entityCleared)=\"onEntitySelectorCleared($event)\">\n </mm-entity-selector-toolbar>\n </div>\n }\n\n @if (isTimeFilterEnabled() || hasEntitySelectors()) {\n <div class=\"toolbar-separator\"></div>\n }\n <div class=\"toolbar-right mm-toolbar-actions\">\n @if (!isReadonly()) {\n <!-- Manager Button (icon only) -->\n <button\n kendoButton\n [svgIcon]=\"gridLayoutIcon\"\n (click)=\"openManager()\"\n title=\"Manage MeshBoards\"\n aria-label=\"Manage MeshBoards\"\n fillMode=\"flat\">\n </button>\n\n <!-- Settings Button (icon only) -->\n <button\n kendoButton\n [svgIcon]=\"gearIcon\"\n (click)=\"openSettings()\"\n title=\"MeshBoard Settings\"\n aria-label=\"MeshBoard Settings\"\n fillMode=\"flat\">\n </button>\n }\n\n <!-- Refresh Button (icon only) -->\n <button\n kendoButton\n [svgIcon]=\"arrowRotateCwIcon\"\n (click)=\"refresh()\"\n title=\"Refresh All Widgets\"\n aria-label=\"Refresh All Widgets\"\n fillMode=\"flat\">\n </button>\n\n @if (!isReadonly()) {\n <!-- Edit Mode Toggle (icon only) -->\n @if (!isEditMode()) {\n <button\n kendoButton\n [svgIcon]=\"pencilIcon\"\n (click)=\"toggleEditMode()\"\n title=\"Enter Edit Mode\"\n aria-label=\"Enter Edit Mode\"\n fillMode=\"flat\">\n </button>\n } @else {\n <button\n kendoButton\n [svgIcon]=\"xIcon\"\n (click)=\"cancelEdit()\"\n title=\"Cancel Edit Mode\"\n aria-label=\"Cancel Edit Mode\"\n fillMode=\"flat\">\n </button>\n }\n\n <!-- Add Widget Button (with text) -->\n <button\n kendoButton\n [svgIcon]=\"plusIcon\"\n (click)=\"openAddWidget()\"\n title=\"Add Widget\"\n themeColor=\"primary\">\n Add Widget\n </button>\n\n <!-- Save Button (with text) -->\n @if (canSave()) {\n <button\n kendoButton\n [svgIcon]=\"saveIcon\"\n (click)=\"save()\"\n [disabled]=\"isSaving()\"\n title=\"Save MeshBoard\"\n themeColor=\"primary\">\n {{ isSaving() ? 'Saving...' : 'Save' }}\n </button>\n }\n }\n </div>\n </div>\n }\n\n <!-- Entity Selector Hint -->\n @if (unselectedToolbarSelectors().length > 0) {\n <div class=\"entity-selector-hint\">\n <kendo-svg-icon [icon]=\"infoCircleIcon\" size=\"medium\"></kendo-svg-icon>\n <span>\n Please select\n @for (selector of unselectedToolbarSelectors(); track selector.id; let last = $last) {\n <strong>{{ selector.label }}</strong>@if (!last) {, }\n }\n to display dashboard data.\n </span>\n </div>\n }\n\n <!-- Variable Resolution Errors -->\n @if (variableResolutionErrors().length > 0) {\n <div class=\"variable-resolution-errors\">\n <strong>Variable resolution errors:</strong>\n @for (error of variableResolutionErrors(); track error.variableName) {\n <span class=\"error-item\">{{ error.variableName }}: {{ error.message }}</span>\n }\n </div>\n }\n\n <!-- Banner Zone: widgets placed in the banner stack above the grid -->\n @if (isEditMode()) {\n <div class=\"zone-label banner-zone-label\">Banner Zone</div>\n }\n @for (widget of bannerWidgets(); track trackByWidgetId($index, widget); let first = $first; let last = $last) {\n <div class=\"banner-zone-item\" [class.banner-edit]=\"isEditMode()\">\n @if (isEditMode()) {\n <div class=\"banner-edit-header\">\n <span class=\"widget-title\">{{ widget.title }}</span>\n <div class=\"widget-actions\">\n <button\n kendoButton\n [svgIcon]=\"arrowUpIcon\"\n (click)=\"moveBannerUp(widget.id)\"\n fillMode=\"flat\"\n size=\"small\"\n [disabled]=\"first\"\n title=\"Move up\"\n [attr.aria-label]=\"'Move up ' + widget.title\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"arrowDownIcon\"\n (click)=\"moveBannerDown(widget.id)\"\n fillMode=\"flat\"\n size=\"small\"\n [disabled]=\"last\"\n title=\"Move down\"\n [attr.aria-label]=\"'Move down ' + widget.title\">\n </button>\n @if (supportsConfiguration(widget)) {\n <button\n kendoButton\n [svgIcon]=\"linkIcon\"\n (click)=\"openWidgetConfig(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"config-widget-btn\"\n [class.needs-config]=\"isWidgetUnconfigured(widget)\"\n [title]=\"isWidgetUnconfigured(widget) ? 'Configure data source (required)' : 'Configure data source'\"\n [attr.aria-label]=\"(isWidgetUnconfigured(widget) ? 'Configure data source (required) ' : 'Configure data source ') + widget.title\">\n </button>\n }\n <button\n kendoButton\n [svgIcon]=\"gearIcon\"\n (click)=\"openEditWidgetDialog(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"edit-widget-btn\"\n title=\"Edit widget\"\n [attr.aria-label]=\"'Edit widget ' + widget.title\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"trashIcon\"\n (click)=\"removeWidget(widget.id)\"\n fillMode=\"flat\"\n themeColor=\"error\"\n size=\"small\"\n class=\"remove-widget-btn\"\n title=\"Delete widget\"\n [attr.aria-label]=\"'Delete widget ' + widget.title\">\n </button>\n </div>\n </div>\n }\n @if (getWidgetComponentType(widget.type); as componentType) {\n <ng-container *ngComponentOutlet=\"componentType; inputs: { config: widget }\"></ng-container>\n }\n </div>\n }\n\n <!-- MeshBoard Grid -->\n @if (isEditMode()) {\n <div class=\"zone-label grid-zone-label\">Grid Zone</div>\n }\n @if (hasGridWidgets()) {\n <kendo-tilelayout\n [columns]=\"displayColumns()\"\n [autoFlow]=\"displayAutoFlow()\"\n [rowHeight]=\"config().rowHeight\"\n [gap]=\"config().gap\"\n [class.has-banners]=\"bannerWidgets().length > 0 && !isEditMode()\"\n [resizable]=\"canEditLayout()\"\n [reorderable]=\"canEditLayout()\"\n (reorder)=\"onReorder($event)\"\n (resize)=\"onResize($event)\">\n @for (placement of displayGridWidgets(); track placement.widget.id) {\n @let widget = placement.widget;\n <kendo-tilelayout-item\n [col]=\"$any(placement.col)\"\n [row]=\"$any(placement.row)\"\n [colSpan]=\"placement.colSpan\"\n [rowSpan]=\"placement.rowSpan\">\n <kendo-tilelayout-item-header>\n <div class=\"widget-header\"\n [class.unconfigured]=\"isWidgetUnconfigured(widget)\"\n [class.chromeless-header]=\"widget.chromeless && !isEditMode()\">\n @if (!(widget.chromeless && !isEditMode())) {\n <span class=\"widget-title\">{{ widget.title }}</span>\n @if (isWidgetUnconfigured(widget)) {\n <span class=\"unconfigured-badge\" title=\"Widget needs configuration\">!</span>\n }\n }\n @if (isEditMode()) {\n <div class=\"widget-actions\" (pointerdown)=\"$event.stopPropagation()\">\n @if (supportsConfiguration(widget)) {\n <button\n kendoButton\n [svgIcon]=\"linkIcon\"\n (click)=\"openWidgetConfig(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"config-widget-btn\"\n [class.needs-config]=\"isWidgetUnconfigured(widget)\"\n [title]=\"isWidgetUnconfigured(widget) ? 'Configure data source (required)' : 'Configure data source'\"\n [attr.aria-label]=\"(isWidgetUnconfigured(widget) ? 'Configure data source (required) ' : 'Configure data source ') + widget.title\">\n </button>\n }\n <button\n kendoButton\n [svgIcon]=\"gearIcon\"\n (click)=\"openEditWidgetDialog(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"edit-widget-btn\"\n title=\"Edit position\"\n [attr.aria-label]=\"'Edit position ' + widget.title\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"copyIcon\"\n (click)=\"duplicateWidget(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"duplicate-widget-btn\"\n title=\"Duplicate widget\"\n [attr.aria-label]=\"'Duplicate widget ' + widget.title\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"trashIcon\"\n (click)=\"removeWidget(widget.id)\"\n fillMode=\"flat\"\n themeColor=\"error\"\n size=\"small\"\n class=\"remove-widget-btn\"\n title=\"Delete widget\"\n [attr.aria-label]=\"'Delete widget ' + widget.title\">\n </button>\n </div>\n }\n </div>\n </kendo-tilelayout-item-header>\n <kendo-tilelayout-item-body>\n @if (getWidgetComponentType(widget.type); as componentType) {\n <ng-container *ngComponentOutlet=\"componentType; inputs: { config: widget }\"></ng-container>\n } @else {\n <div class=\"widget-error\">\n <p>Unknown widget type: {{ widget.type }}</p>\n </div>\n }\n </kendo-tilelayout-item-body>\n </kendo-tilelayout-item>\n }\n </kendo-tilelayout>\n } @else {\n <mm-meshboard-empty-state [readonly]=\"isReadonly()\" (addWidget)=\"openAddWidget()\"></mm-meshboard-empty-state>\n }\n\n <!-- Edit Widget Dialog -->\n @if (showEditWidgetDialog && editingWidget) {\n <mm-edit-widget-dialog\n [widget]=\"editingWidget\"\n [widgets]=\"config().widgets\"\n [maxColumns]=\"config().columns\"\n [gridService]=\"gridService\"\n (save)=\"onEditWidgetSave($event)\"\n (cancelled)=\"closeEditWidgetDialog()\">\n </mm-edit-widget-dialog>\n }\n }\n</div>\n", styles: [":host{display:flex;flex-direction:column;flex:1;min-height:0;height:100%;width:100%}.meshboard-view{display:flex;flex-direction:column;flex:1;min-height:0;width:100%;background-color:var(--kendo-color-surface, #f5f5f5)}.loading-container{display:flex;align-items:center;justify-content:center;height:100%;width:100%}.entity-selector-hint{display:flex;align-items:center;gap:8px;padding:10px 16px;margin-bottom:8px;background:color-mix(in srgb,var(--kendo-color-info, #2196f3) 10%,var(--kendo-color-surface-alt, #fafafa));border:1px solid color-mix(in srgb,var(--kendo-color-info, #2196f3) 40%,transparent);border-radius:4px;font-size:13px;color:var(--kendo-color-on-app-surface, #424242)}.entity-selector-hint strong{color:var(--kendo-color-info, #2196f3)}.variable-resolution-errors{display:flex;flex-wrap:wrap;align-items:center;gap:8px;padding:8px 16px;margin-bottom:8px;background:color-mix(in srgb,var(--kendo-color-error, #f44336) 10%,var(--kendo-color-surface-alt, #fafafa));border:1px solid var(--kendo-color-error, #f44336);border-radius:4px;font-size:13px;color:var(--kendo-color-on-app-surface, #424242)}.variable-resolution-errors strong{color:var(--kendo-color-error, #f44336)}.variable-resolution-errors .error-item{padding:2px 8px;background:color-mix(in srgb,var(--kendo-color-error, #f44336) 8%,transparent);border-radius:3px;font-family:var(--theme-font-mono);font-size:12px}.model-unavailable{display:flex;align-items:center;justify-content:center;height:100%;width:100%;padding:2rem;overflow:hidden;box-sizing:border-box}.model-unavailable .message-card{text-align:center;max-width:500px;padding:2rem;background:var(--kendo-color-surface-alt, #fafafa);border:1px solid var(--kendo-color-border, #e0e0e0);border-radius:8px;overflow:hidden}.model-unavailable .message-card kendo-svg-icon{color:var(--kendo-color-warning, #ff9800);margin-bottom:1rem}.model-unavailable .message-card h2{margin:1rem 0;color:var(--kendo-color-on-app-surface, #424242)}.model-unavailable .message-card p{margin:.5rem 0;color:var(--kendo-color-subtle, #757575);line-height:1.5}.model-unavailable .message-card ol{text-align:left;margin:1rem 0;padding-left:1.5rem;color:var(--kendo-color-subtle, #757575);line-height:1.8}.model-unavailable .message-card ol li{margin:.25rem 0}.model-unavailable .message-card ol a{color:var(--kendo-color-primary, #3f51b5);text-decoration:none}.model-unavailable .message-card ol a:hover{text-decoration:underline}.meshboard-toolbar{display:flex;justify-content:space-between;align-items:flex-end;padding:1rem 1.5rem;background-color:var(--kendo-color-surface-alt, white);border-bottom:1px solid var(--kendo-color-border, #e0e0e0);gap:1rem;flex-shrink:0}.meshboard-toolbar .toolbar-left{display:flex;align-items:baseline;gap:1rem;flex:1;min-width:0;align-self:center}.meshboard-toolbar .toolbar-left .meshboard-title{margin:0;font-size:1.5rem;font-weight:500;color:var(--kendo-color-on-app-surface, #212121);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.meshboard-toolbar .toolbar-left .meshboard-description{color:var(--kendo-color-subtle, #757575);font-size:.875rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.meshboard-toolbar .toolbar-center{display:flex;justify-content:center;align-items:flex-end;flex-shrink:0}.meshboard-toolbar .toolbar-center mm-time-range-picker ::ng-deep .time-range-picker{gap:.5rem}.meshboard-toolbar .toolbar-separator{width:1px;align-self:stretch;background-color:var(--kendo-color-border, #e0e0e0);flex-shrink:0}.meshboard-toolbar .toolbar-entity-selectors{display:flex;align-items:flex-end;flex-shrink:0}.meshboard-toolbar .toolbar-right{display:flex;gap:.5rem;align-items:center;flex-shrink:0}.meshboard-toolbar.compact{justify-content:flex-end;align-items:center;padding:.25rem 1.5rem 0;background-color:transparent;border-bottom:none}kendo-tilelayout{padding:1.5rem;overflow:auto;flex:1;min-height:0}.widget-header{display:flex;justify-content:space-between;align-items:center;width:100%;gap:.5rem}.widget-header.unconfigured{background-color:#ff98001a}.widget-header .widget-title{flex:1;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.widget-header .unconfigured-badge{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:50%;background-color:var(--kendo-color-warning, #ff9800);color:#fff;font-size:12px;font-weight:700;flex-shrink:0}.widget-header .widget-actions{display:flex;align-items:center;gap:.25rem;flex-shrink:0}.widget-header .config-widget-btn,.widget-header .edit-widget-btn,.widget-header .remove-widget-btn{opacity:.6;transition:opacity .2s}.widget-header .config-widget-btn:hover,.widget-header .edit-widget-btn:hover,.widget-header .remove-widget-btn:hover{opacity:1}.widget-header .config-widget-btn.needs-config{color:var(--kendo-color-warning, #ff9800);opacity:1}.widget-header .remove-widget-btn{color:var(--kendo-color-error, #f44336)}.widget-error{display:flex;align-items:center;justify-content:center;height:100%;padding:1rem;color:var(--kendo-color-error, #f44336);text-align:center}.meshboard-view.edit-mode kendo-tilelayout-item:hover{box-shadow:0 0 0 2px var(--kendo-color-primary, #3f51b5)}@media(max-width:768px){.meshboard-toolbar{flex-direction:column;align-items:stretch;gap:.75rem}.meshboard-toolbar .toolbar-left{flex-direction:column;align-items:flex-start;gap:.25rem}.meshboard-toolbar .toolbar-right{flex-wrap:wrap;justify-content:flex-end}.meshboard-toolbar.compact{align-items:flex-end;padding:.25rem 1rem 0}kendo-tilelayout{padding:1rem}}:host ::ng-deep kendo-tilelayout.has-banners{padding-top:0!important}:host ::ng-deep kendo-tilelayout{padding-bottom:1.5rem!important}.widget-header.chromeless-header{display:none}:host ::ng-deep .k-tilelayout-item-header:has(.chromeless-header){display:none}:host ::ng-deep kendo-tilelayout-item:has(.chromeless-header).k-tilelayout-item,:host ::ng-deep kendo-tilelayout-item:has(.chromeless-header).k-card{border:none;box-shadow:none;background:transparent}:host ::ng-deep kendo-tilelayout-item:has(kendo-dialog),:host ::ng-deep kendo-tilelayout-item:has(kendo-window){z-index:9999}.zone-label{font-size:.65rem;opacity:.4;padding:6px 1rem 2px;font-weight:600}.banner-zone-item{margin:0;padding:8px 1rem}.banner-zone-item.banner-edit{border:1px dashed var(--kendo-color-border, rgba(255, 255, 255, .2));border-radius:4px;margin:0 1rem 12px;padding:4px 0}.banner-edit-header{display:flex;align-items:center;justify-content:space-between;padding:4px 8px;opacity:.7}.banner-edit-header .widget-title{font-size:.75rem}.banner-edit-header .widget-actions{display:flex;gap:2px}.meshboard-view.chrome-plain{background:transparent!important}.meshboard-view.chrome-plain .meshboard-toolbar{padding:0 0 var(--theme-space-3, 12px);background:transparent!important;border-bottom:none!important}.meshboard-view.chrome-plain kendo-tilelayout{padding:0!important}.meshboard-view.chrome-plain .banner-zone-item:not(.banner-edit){padding-left:0;padding-right:0}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$3.NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i3$1.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }, { kind: "ngmodule", type: TileLayoutModule }, { kind: "component", type: i5.TileLayoutComponent, selector: "kendo-tilelayout", inputs: ["columns", "columnWidth", "gap", "reorderable", "resizable", "rowHeight", "autoFlow", "navigable"], outputs: ["reorder", "resize"] }, { kind: "component", type: i5.TileLayoutItemBodyComponent, selector: "kendo-tilelayout-item-body" }, { kind: "component", type: i5.TileLayoutItemComponent, selector: "kendo-tilelayout-item", inputs: ["title", "rowSpan", "colSpan", "order", "col", "row", "reorderable", "resizable"] }, { kind: "component", type: i5.TileLayoutItemHeaderComponent, selector: "kendo-tilelayout-item-header" }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: DialogModule }, { kind: "ngmodule", type: SVGIconModule }, { kind: "component", type: i1$4.SVGIconComponent, selector: "kendo-svg-icon, kendo-svgicon", inputs: ["icon", "variant"], exportAs: ["kendoSVGIcon"] }, { kind: "component", type: EditWidgetDialogComponent, selector: "mm-edit-widget-dialog", inputs: ["widget", "widgets", "maxColumns", "gridService"], outputs: ["save", "cancelled"] }, { kind: "component", type: TimeRangePickerComponent, selector: "mm-time-range-picker", inputs: ["config", "labels", "initialSelection"], outputs: ["rangeChange", "rangeChangeISO", "selectionChange"] }, { kind: "component", type: EntitySelectorToolbarComponent, selector: "mm-entity-selector-toolbar", inputs: ["entitySelectors"], outputs: ["entitySelected", "entityCleared"] }, { kind: "component", type: MeshBoardEmptyStateComponent, selector: "mm-meshboard-empty-state", inputs: ["readonly"], outputs: ["addWidget"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
|
|
36170
|
+
], viewQueries: [{ propertyName: "tileLayout", first: true, predicate: TileLayoutComponent, descendants: true }], hostDirectives: [{ directive: i1$8.UnsavedChangesDirective }], ngImport: i0, template: "<div class=\"meshboard-view\" [class.chrome-plain]=\"effectiveChrome() === 'plain'\">\n @if (isLoading()) {\n <div class=\"loading-container\">\n <div class=\"k-loading-mask\">\n <span class=\"k-loading-text\">Loading MeshBoard...</span>\n <div class=\"k-loading-image\"></div>\n <div class=\"k-loading-color\"></div>\n </div>\n </div>\n } @else if (isModelAvailable() === false) {\n <div class=\"model-unavailable\">\n <div class=\"message-card\">\n <kendo-svg-icon [icon]=\"xIcon\" size=\"xlarge\"></kendo-svg-icon>\n <h2>MeshBoard Not Available</h2>\n <p>The MeshBoard feature requires the CK model 'System.UI' version 1.0.1 or higher.</p>\n <p>Please install the required model in your tenant to use this feature.</p>\n </div>\n </div>\n } @else if (notFoundError()) {\n <div class=\"model-unavailable\">\n <div class=\"message-card\">\n <kendo-svg-icon [icon]=\"xIcon\" size=\"xlarge\"></kendo-svg-icon>\n <h2>MeshBoard Not Found</h2>\n <p>{{ notFoundError() }}</p>\n <p>To create a MeshBoard with a Well-Known Name:</p>\n <ol>\n <li>Go to the <a [routerLink]=\"meshBoardPageLink()\">MeshBoard</a> page</li>\n <li>Open Settings</li>\n <li>Set the \"Well-Known Name\" field</li>\n <li>Save the MeshBoard</li>\n </ol>\n </div>\n </div>\n } @else if (isInitialized()) {\n <!-- Toolbar (headerMode: full = name + description + controls, compact = controls only, none = no row) -->\n @if (effectiveHeaderMode() !== 'none') {\n <div class=\"meshboard-toolbar\" [class.compact]=\"effectiveHeaderMode() === 'compact'\">\n @if (effectiveHeaderMode() === 'full') {\n <div class=\"toolbar-left\">\n <h2 class=\"meshboard-title\">{{ config().name }}</h2>\n @if (config().description) {\n <span class=\"meshboard-description\">{{ config().description }}</span>\n }\n </div>\n }\n\n <!-- Time Filter (center) -->\n @if (isTimeFilterEnabled()) {\n <div class=\"toolbar-center\">\n <mm-time-range-picker\n [config]=\"timeFilterConfig()?.pickerConfig ?? {}\"\n [labels]=\"timeRangeLabels()\"\n [initialSelection]=\"initialTimeSelection()\"\n (rangeChange)=\"onTimeRangeChange($event)\"\n (selectionChange)=\"onTimeSelectionChange($event)\">\n </mm-time-range-picker>\n @if (canResetTimeFilter()) {\n <button\n kendoButton\n [svgIcon]=\"undoIcon\"\n (click)=\"resetTimeFilterToDefault()\"\n title=\"Reset to default time filter\"\n aria-label=\"Reset to default time filter\"\n fillMode=\"flat\"\n size=\"small\">\n </button>\n }\n </div>\n }\n\n <!-- Entity Selectors -->\n @if (hasEntitySelectors()) {\n @if (isTimeFilterEnabled()) {\n <div class=\"toolbar-separator\"></div>\n }\n <div class=\"toolbar-entity-selectors\">\n <mm-entity-selector-toolbar\n [entitySelectors]=\"entitySelectorsConfig()\"\n (entitySelected)=\"onEntitySelectorSelected($event)\"\n (entityCleared)=\"onEntitySelectorCleared($event)\">\n </mm-entity-selector-toolbar>\n </div>\n }\n\n @if (isTimeFilterEnabled() || hasEntitySelectors()) {\n <div class=\"toolbar-separator\"></div>\n }\n <div class=\"toolbar-right mm-toolbar-actions\">\n @if (!isReadonly()) {\n <!-- Manager Button (icon only) -->\n <button\n kendoButton\n [svgIcon]=\"gridLayoutIcon\"\n (click)=\"openManager()\"\n title=\"Manage MeshBoards\"\n aria-label=\"Manage MeshBoards\"\n fillMode=\"flat\">\n </button>\n\n <!-- Settings Button (icon only) -->\n <button\n kendoButton\n [svgIcon]=\"gearIcon\"\n (click)=\"openSettings()\"\n title=\"MeshBoard Settings\"\n aria-label=\"MeshBoard Settings\"\n fillMode=\"flat\">\n </button>\n }\n\n <!-- Refresh Button (icon only) -->\n <button\n kendoButton\n [svgIcon]=\"arrowRotateCwIcon\"\n (click)=\"refresh()\"\n title=\"Refresh All Widgets\"\n aria-label=\"Refresh All Widgets\"\n fillMode=\"flat\">\n </button>\n\n @if (!isReadonly()) {\n <!-- Edit Mode Toggle (icon only) -->\n @if (!isEditMode()) {\n <button\n kendoButton\n [svgIcon]=\"pencilIcon\"\n (click)=\"toggleEditMode()\"\n title=\"Enter Edit Mode\"\n aria-label=\"Enter Edit Mode\"\n fillMode=\"flat\">\n </button>\n } @else {\n <button\n kendoButton\n [svgIcon]=\"xIcon\"\n (click)=\"cancelEdit()\"\n title=\"Cancel Edit Mode\"\n aria-label=\"Cancel Edit Mode\"\n fillMode=\"flat\">\n </button>\n }\n\n <!-- Add Widget Button (with text) -->\n <button\n kendoButton\n [svgIcon]=\"plusIcon\"\n (click)=\"openAddWidget()\"\n title=\"Add Widget\"\n themeColor=\"primary\">\n Add Widget\n </button>\n\n <!-- Save Button (with text) -->\n @if (canSave()) {\n <button\n kendoButton\n [svgIcon]=\"saveIcon\"\n (click)=\"save()\"\n [disabled]=\"isSaving()\"\n title=\"Save MeshBoard\"\n themeColor=\"primary\">\n {{ isSaving() ? 'Saving...' : 'Save' }}\n </button>\n }\n }\n </div>\n </div>\n }\n\n <!-- Entity Selector Hint -->\n @if (unselectedToolbarSelectors().length > 0) {\n <div class=\"entity-selector-hint\">\n <kendo-svg-icon [icon]=\"infoCircleIcon\" size=\"medium\"></kendo-svg-icon>\n <span>\n Please select\n @for (selector of unselectedToolbarSelectors(); track selector.id; let last = $last) {\n <strong>{{ selector.label }}</strong>@if (!last) {, }\n }\n to display dashboard data.\n </span>\n </div>\n }\n\n <!-- Variable Resolution Errors -->\n @if (variableResolutionErrors().length > 0) {\n <div class=\"variable-resolution-errors\">\n <strong>Variable resolution errors:</strong>\n @for (error of variableResolutionErrors(); track error.variableName) {\n <span class=\"error-item\">{{ error.variableName }}: {{ error.message }}</span>\n }\n </div>\n }\n\n <!-- Entity selector scopes that could not be resolved (AB#7721) -->\n @if (scopeResolutionErrors().length > 0) {\n <div class=\"variable-resolution-errors scope-resolution-errors\" role=\"status\">\n @for (error of scopeResolutionErrors(); track error.selectorId) {\n <span class=\"error-item\">{{ error.message }}</span>\n }\n </div>\n }\n\n <!-- Banner Zone: widgets placed in the banner stack above the grid -->\n @if (isEditMode()) {\n <div class=\"zone-label banner-zone-label\">Banner Zone</div>\n }\n @for (widget of bannerWidgets(); track trackByWidgetId($index, widget); let first = $first; let last = $last) {\n <div class=\"banner-zone-item\" [class.banner-edit]=\"isEditMode()\">\n @if (isEditMode()) {\n <div class=\"banner-edit-header\">\n <span class=\"widget-title\">{{ widget.title }}</span>\n <div class=\"widget-actions\">\n <button\n kendoButton\n [svgIcon]=\"arrowUpIcon\"\n (click)=\"moveBannerUp(widget.id)\"\n fillMode=\"flat\"\n size=\"small\"\n [disabled]=\"first\"\n title=\"Move up\"\n [attr.aria-label]=\"'Move up ' + widget.title\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"arrowDownIcon\"\n (click)=\"moveBannerDown(widget.id)\"\n fillMode=\"flat\"\n size=\"small\"\n [disabled]=\"last\"\n title=\"Move down\"\n [attr.aria-label]=\"'Move down ' + widget.title\">\n </button>\n @if (supportsConfiguration(widget)) {\n <button\n kendoButton\n [svgIcon]=\"linkIcon\"\n (click)=\"openWidgetConfig(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"config-widget-btn\"\n [class.needs-config]=\"isWidgetUnconfigured(widget)\"\n [title]=\"isWidgetUnconfigured(widget) ? 'Configure data source (required)' : 'Configure data source'\"\n [attr.aria-label]=\"(isWidgetUnconfigured(widget) ? 'Configure data source (required) ' : 'Configure data source ') + widget.title\">\n </button>\n }\n <button\n kendoButton\n [svgIcon]=\"gearIcon\"\n (click)=\"openEditWidgetDialog(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"edit-widget-btn\"\n title=\"Edit widget\"\n [attr.aria-label]=\"'Edit widget ' + widget.title\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"trashIcon\"\n (click)=\"removeWidget(widget.id)\"\n fillMode=\"flat\"\n themeColor=\"error\"\n size=\"small\"\n class=\"remove-widget-btn\"\n title=\"Delete widget\"\n [attr.aria-label]=\"'Delete widget ' + widget.title\">\n </button>\n </div>\n </div>\n }\n @if (getWidgetComponentType(widget.type); as componentType) {\n <ng-container *ngComponentOutlet=\"componentType; inputs: { config: widget }\"></ng-container>\n }\n </div>\n }\n\n <!-- MeshBoard Grid -->\n @if (isEditMode()) {\n <div class=\"zone-label grid-zone-label\">Grid Zone</div>\n }\n @if (hasGridWidgets()) {\n <kendo-tilelayout\n [columns]=\"displayColumns()\"\n [autoFlow]=\"displayAutoFlow()\"\n [rowHeight]=\"config().rowHeight\"\n [gap]=\"config().gap\"\n [class.has-banners]=\"bannerWidgets().length > 0 && !isEditMode()\"\n [resizable]=\"canEditLayout()\"\n [reorderable]=\"canEditLayout()\"\n (reorder)=\"onReorder($event)\"\n (resize)=\"onResize($event)\">\n @for (placement of displayGridWidgets(); track placement.widget.id) {\n @let widget = placement.widget;\n <kendo-tilelayout-item\n [col]=\"$any(placement.col)\"\n [row]=\"$any(placement.row)\"\n [colSpan]=\"placement.colSpan\"\n [rowSpan]=\"placement.rowSpan\">\n <kendo-tilelayout-item-header>\n <div class=\"widget-header\"\n [class.unconfigured]=\"isWidgetUnconfigured(widget)\"\n [class.chromeless-header]=\"widget.chromeless && !isEditMode()\">\n @if (!(widget.chromeless && !isEditMode())) {\n <span class=\"widget-title\">{{ widget.title }}</span>\n @if (isWidgetUnconfigured(widget)) {\n <span class=\"unconfigured-badge\" title=\"Widget needs configuration\">!</span>\n }\n }\n @if (isEditMode()) {\n <div class=\"widget-actions\" (pointerdown)=\"$event.stopPropagation()\">\n @if (supportsConfiguration(widget)) {\n <button\n kendoButton\n [svgIcon]=\"linkIcon\"\n (click)=\"openWidgetConfig(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"config-widget-btn\"\n [class.needs-config]=\"isWidgetUnconfigured(widget)\"\n [title]=\"isWidgetUnconfigured(widget) ? 'Configure data source (required)' : 'Configure data source'\"\n [attr.aria-label]=\"(isWidgetUnconfigured(widget) ? 'Configure data source (required) ' : 'Configure data source ') + widget.title\">\n </button>\n }\n <button\n kendoButton\n [svgIcon]=\"gearIcon\"\n (click)=\"openEditWidgetDialog(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"edit-widget-btn\"\n title=\"Edit position\"\n [attr.aria-label]=\"'Edit position ' + widget.title\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"copyIcon\"\n (click)=\"duplicateWidget(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"duplicate-widget-btn\"\n title=\"Duplicate widget\"\n [attr.aria-label]=\"'Duplicate widget ' + widget.title\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"trashIcon\"\n (click)=\"removeWidget(widget.id)\"\n fillMode=\"flat\"\n themeColor=\"error\"\n size=\"small\"\n class=\"remove-widget-btn\"\n title=\"Delete widget\"\n [attr.aria-label]=\"'Delete widget ' + widget.title\">\n </button>\n </div>\n }\n </div>\n </kendo-tilelayout-item-header>\n <kendo-tilelayout-item-body>\n @if (getWidgetComponentType(widget.type); as componentType) {\n <ng-container *ngComponentOutlet=\"componentType; inputs: { config: widget }\"></ng-container>\n } @else {\n <div class=\"widget-error\">\n <p>Unknown widget type: {{ widget.type }}</p>\n </div>\n }\n </kendo-tilelayout-item-body>\n </kendo-tilelayout-item>\n }\n </kendo-tilelayout>\n } @else {\n <mm-meshboard-empty-state [readonly]=\"isReadonly()\" (addWidget)=\"openAddWidget()\"></mm-meshboard-empty-state>\n }\n\n <!-- Edit Widget Dialog -->\n @if (showEditWidgetDialog && editingWidget) {\n <mm-edit-widget-dialog\n [widget]=\"editingWidget\"\n [widgets]=\"config().widgets\"\n [maxColumns]=\"config().columns\"\n [gridService]=\"gridService\"\n (save)=\"onEditWidgetSave($event)\"\n (cancelled)=\"closeEditWidgetDialog()\">\n </mm-edit-widget-dialog>\n }\n }\n</div>\n", styles: [":host{display:flex;flex-direction:column;flex:1;min-height:0;height:100%;width:100%}.meshboard-view{display:flex;flex-direction:column;flex:1;min-height:0;width:100%;background-color:var(--kendo-color-surface, #f5f5f5)}.loading-container{display:flex;align-items:center;justify-content:center;height:100%;width:100%}.entity-selector-hint{display:flex;align-items:center;gap:8px;padding:10px 16px;margin-bottom:8px;background:color-mix(in srgb,var(--kendo-color-info, #2196f3) 10%,var(--kendo-color-surface-alt, #fafafa));border:1px solid color-mix(in srgb,var(--kendo-color-info, #2196f3) 40%,transparent);border-radius:4px;font-size:13px;color:var(--kendo-color-on-app-surface, #424242)}.entity-selector-hint strong{color:var(--kendo-color-info, #2196f3)}.variable-resolution-errors{display:flex;flex-wrap:wrap;align-items:center;gap:8px;padding:8px 16px;margin-bottom:8px;background:color-mix(in srgb,var(--kendo-color-error, #f44336) 10%,var(--kendo-color-surface-alt, #fafafa));border:1px solid var(--kendo-color-error, #f44336);border-radius:4px;font-size:13px;color:var(--kendo-color-on-app-surface, #424242)}.variable-resolution-errors strong{color:var(--kendo-color-error, #f44336)}.variable-resolution-errors .error-item{padding:2px 8px;background:color-mix(in srgb,var(--kendo-color-error, #f44336) 8%,transparent);border-radius:3px;font-family:var(--theme-font-mono);font-size:12px}.scope-resolution-errors .error-item{font-family:inherit}.model-unavailable{display:flex;align-items:center;justify-content:center;height:100%;width:100%;padding:2rem;overflow:hidden;box-sizing:border-box}.model-unavailable .message-card{text-align:center;max-width:500px;padding:2rem;background:var(--kendo-color-surface-alt, #fafafa);border:1px solid var(--kendo-color-border, #e0e0e0);border-radius:8px;overflow:hidden}.model-unavailable .message-card kendo-svg-icon{color:var(--kendo-color-warning, #ff9800);margin-bottom:1rem}.model-unavailable .message-card h2{margin:1rem 0;color:var(--kendo-color-on-app-surface, #424242)}.model-unavailable .message-card p{margin:.5rem 0;color:var(--kendo-color-subtle, #757575);line-height:1.5}.model-unavailable .message-card ol{text-align:left;margin:1rem 0;padding-left:1.5rem;color:var(--kendo-color-subtle, #757575);line-height:1.8}.model-unavailable .message-card ol li{margin:.25rem 0}.model-unavailable .message-card ol a{color:var(--kendo-color-primary, #3f51b5);text-decoration:none}.model-unavailable .message-card ol a:hover{text-decoration:underline}.meshboard-toolbar{display:flex;justify-content:space-between;align-items:flex-end;padding:1rem 1.5rem;background-color:var(--kendo-color-surface-alt, white);border-bottom:1px solid var(--kendo-color-border, #e0e0e0);gap:1rem;flex-shrink:0}.meshboard-toolbar .toolbar-left{display:flex;align-items:baseline;gap:1rem;flex:1;min-width:0;align-self:center}.meshboard-toolbar .toolbar-left .meshboard-title{margin:0;font-size:1.5rem;font-weight:500;color:var(--kendo-color-on-app-surface, #212121);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.meshboard-toolbar .toolbar-left .meshboard-description{color:var(--kendo-color-subtle, #757575);font-size:.875rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.meshboard-toolbar .toolbar-center{display:flex;justify-content:center;align-items:flex-end;flex-shrink:0}.meshboard-toolbar .toolbar-center mm-time-range-picker ::ng-deep .time-range-picker{gap:.5rem}.meshboard-toolbar .toolbar-separator{width:1px;align-self:stretch;background-color:var(--kendo-color-border, #e0e0e0);flex-shrink:0}.meshboard-toolbar .toolbar-entity-selectors{display:flex;align-items:flex-end;flex-shrink:0}.meshboard-toolbar .toolbar-right{display:flex;gap:.5rem;align-items:center;flex-shrink:0}.meshboard-toolbar.compact{justify-content:flex-end;align-items:center;padding:.25rem 1.5rem 0;background-color:transparent;border-bottom:none}kendo-tilelayout{padding:1.5rem;overflow:auto;flex:1;min-height:0}.widget-header{display:flex;justify-content:space-between;align-items:center;width:100%;gap:.5rem}.widget-header.unconfigured{background-color:#ff98001a}.widget-header .widget-title{flex:1;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.widget-header .unconfigured-badge{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:50%;background-color:var(--kendo-color-warning, #ff9800);color:#fff;font-size:12px;font-weight:700;flex-shrink:0}.widget-header .widget-actions{display:flex;align-items:center;gap:.25rem;flex-shrink:0}.widget-header .config-widget-btn,.widget-header .edit-widget-btn,.widget-header .remove-widget-btn{opacity:.6;transition:opacity .2s}.widget-header .config-widget-btn:hover,.widget-header .edit-widget-btn:hover,.widget-header .remove-widget-btn:hover{opacity:1}.widget-header .config-widget-btn.needs-config{color:var(--kendo-color-warning, #ff9800);opacity:1}.widget-header .remove-widget-btn{color:var(--kendo-color-error, #f44336)}.widget-error{display:flex;align-items:center;justify-content:center;height:100%;padding:1rem;color:var(--kendo-color-error, #f44336);text-align:center}.meshboard-view.edit-mode kendo-tilelayout-item:hover{box-shadow:0 0 0 2px var(--kendo-color-primary, #3f51b5)}@media(max-width:768px){.meshboard-toolbar{flex-direction:column;align-items:stretch;gap:.75rem}.meshboard-toolbar .toolbar-left{flex-direction:column;align-items:flex-start;gap:.25rem}.meshboard-toolbar .toolbar-right{flex-wrap:wrap;justify-content:flex-end}.meshboard-toolbar.compact{align-items:flex-end;padding:.25rem 1rem 0}kendo-tilelayout{padding:1rem}}:host ::ng-deep kendo-tilelayout.has-banners{padding-top:0!important}:host ::ng-deep kendo-tilelayout{padding-bottom:1.5rem!important}.widget-header.chromeless-header{display:none}:host ::ng-deep .k-tilelayout-item-header:has(.chromeless-header){display:none}:host ::ng-deep kendo-tilelayout-item:has(.chromeless-header).k-tilelayout-item,:host ::ng-deep kendo-tilelayout-item:has(.chromeless-header).k-card{border:none;box-shadow:none;background:transparent}:host ::ng-deep kendo-tilelayout-item:has(kendo-dialog),:host ::ng-deep kendo-tilelayout-item:has(kendo-window){z-index:9999}.zone-label{font-size:.65rem;opacity:.4;padding:6px 1rem 2px;font-weight:600}.banner-zone-item{margin:0;padding:8px 1rem}.banner-zone-item.banner-edit{border:1px dashed var(--kendo-color-border, rgba(255, 255, 255, .2));border-radius:4px;margin:0 1rem 12px;padding:4px 0}.banner-edit-header{display:flex;align-items:center;justify-content:space-between;padding:4px 8px;opacity:.7}.banner-edit-header .widget-title{font-size:.75rem}.banner-edit-header .widget-actions{display:flex;gap:2px}.meshboard-view.chrome-plain{background:transparent!important}.meshboard-view.chrome-plain .meshboard-toolbar{padding:0 0 var(--theme-space-3, 12px);background:transparent!important;border-bottom:none!important}.meshboard-view.chrome-plain kendo-tilelayout{padding:0!important}.meshboard-view.chrome-plain .banner-zone-item:not(.banner-edit){padding-left:0;padding-right:0}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$3.NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i3$1.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }, { kind: "ngmodule", type: TileLayoutModule }, { kind: "component", type: i5.TileLayoutComponent, selector: "kendo-tilelayout", inputs: ["columns", "columnWidth", "gap", "reorderable", "resizable", "rowHeight", "autoFlow", "navigable"], outputs: ["reorder", "resize"] }, { kind: "component", type: i5.TileLayoutItemBodyComponent, selector: "kendo-tilelayout-item-body" }, { kind: "component", type: i5.TileLayoutItemComponent, selector: "kendo-tilelayout-item", inputs: ["title", "rowSpan", "colSpan", "order", "col", "row", "reorderable", "resizable"] }, { kind: "component", type: i5.TileLayoutItemHeaderComponent, selector: "kendo-tilelayout-item-header" }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: DialogModule }, { kind: "ngmodule", type: SVGIconModule }, { kind: "component", type: i1$4.SVGIconComponent, selector: "kendo-svg-icon, kendo-svgicon", inputs: ["icon", "variant"], exportAs: ["kendoSVGIcon"] }, { kind: "component", type: EditWidgetDialogComponent, selector: "mm-edit-widget-dialog", inputs: ["widget", "widgets", "maxColumns", "gridService"], outputs: ["save", "cancelled"] }, { kind: "component", type: TimeRangePickerComponent, selector: "mm-time-range-picker", inputs: ["config", "labels", "initialSelection"], outputs: ["rangeChange", "rangeChangeISO", "selectionChange"] }, { kind: "component", type: EntitySelectorToolbarComponent, selector: "mm-entity-selector-toolbar", inputs: ["entitySelectors"], outputs: ["entitySelected", "entityCleared"] }, { kind: "component", type: MeshBoardEmptyStateComponent, selector: "mm-meshboard-empty-state", inputs: ["readonly"], outputs: ["addWidget"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
|
|
35441
36171
|
}
|
|
35442
36172
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: MeshBoardViewComponent, decorators: [{
|
|
35443
36173
|
type: Component,
|
|
@@ -35455,7 +36185,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
35455
36185
|
], hostDirectives: [UnsavedChangesDirective], providers: [
|
|
35456
36186
|
{ provide: HAS_UNSAVED_CHANGES, useExisting: MeshBoardViewComponent },
|
|
35457
36187
|
AutoRefreshTimerService
|
|
35458
|
-
], changeDetection: ChangeDetectionStrategy.Eager, template: "<div class=\"meshboard-view\" [class.chrome-plain]=\"effectiveChrome() === 'plain'\">\n @if (isLoading()) {\n <div class=\"loading-container\">\n <div class=\"k-loading-mask\">\n <span class=\"k-loading-text\">Loading MeshBoard...</span>\n <div class=\"k-loading-image\"></div>\n <div class=\"k-loading-color\"></div>\n </div>\n </div>\n } @else if (isModelAvailable() === false) {\n <div class=\"model-unavailable\">\n <div class=\"message-card\">\n <kendo-svg-icon [icon]=\"xIcon\" size=\"xlarge\"></kendo-svg-icon>\n <h2>MeshBoard Not Available</h2>\n <p>The MeshBoard feature requires the CK model 'System.UI' version 1.0.1 or higher.</p>\n <p>Please install the required model in your tenant to use this feature.</p>\n </div>\n </div>\n } @else if (notFoundError()) {\n <div class=\"model-unavailable\">\n <div class=\"message-card\">\n <kendo-svg-icon [icon]=\"xIcon\" size=\"xlarge\"></kendo-svg-icon>\n <h2>MeshBoard Not Found</h2>\n <p>{{ notFoundError() }}</p>\n <p>To create a MeshBoard with a Well-Known Name:</p>\n <ol>\n <li>Go to the <a [routerLink]=\"meshBoardPageLink()\">MeshBoard</a> page</li>\n <li>Open Settings</li>\n <li>Set the \"Well-Known Name\" field</li>\n <li>Save the MeshBoard</li>\n </ol>\n </div>\n </div>\n } @else if (isInitialized()) {\n <!-- Toolbar (headerMode: full = name + description + controls, compact = controls only, none = no row) -->\n @if (effectiveHeaderMode() !== 'none') {\n <div class=\"meshboard-toolbar\" [class.compact]=\"effectiveHeaderMode() === 'compact'\">\n @if (effectiveHeaderMode() === 'full') {\n <div class=\"toolbar-left\">\n <h2 class=\"meshboard-title\">{{ config().name }}</h2>\n @if (config().description) {\n <span class=\"meshboard-description\">{{ config().description }}</span>\n }\n </div>\n }\n\n <!-- Time Filter (center) -->\n @if (isTimeFilterEnabled()) {\n <div class=\"toolbar-center\">\n <mm-time-range-picker\n [config]=\"timeFilterConfig()?.pickerConfig ?? {}\"\n [labels]=\"timeRangeLabels()\"\n [initialSelection]=\"initialTimeSelection()\"\n (rangeChange)=\"onTimeRangeChange($event)\"\n (selectionChange)=\"onTimeSelectionChange($event)\">\n </mm-time-range-picker>\n @if (canResetTimeFilter()) {\n <button\n kendoButton\n [svgIcon]=\"undoIcon\"\n (click)=\"resetTimeFilterToDefault()\"\n title=\"Reset to default time filter\"\n aria-label=\"Reset to default time filter\"\n fillMode=\"flat\"\n size=\"small\">\n </button>\n }\n </div>\n }\n\n <!-- Entity Selectors -->\n @if (hasEntitySelectors()) {\n @if (isTimeFilterEnabled()) {\n <div class=\"toolbar-separator\"></div>\n }\n <div class=\"toolbar-entity-selectors\">\n <mm-entity-selector-toolbar\n [entitySelectors]=\"entitySelectorsConfig()\"\n (entitySelected)=\"onEntitySelectorSelected($event)\"\n (entityCleared)=\"onEntitySelectorCleared($event)\">\n </mm-entity-selector-toolbar>\n </div>\n }\n\n @if (isTimeFilterEnabled() || hasEntitySelectors()) {\n <div class=\"toolbar-separator\"></div>\n }\n <div class=\"toolbar-right mm-toolbar-actions\">\n @if (!isReadonly()) {\n <!-- Manager Button (icon only) -->\n <button\n kendoButton\n [svgIcon]=\"gridLayoutIcon\"\n (click)=\"openManager()\"\n title=\"Manage MeshBoards\"\n aria-label=\"Manage MeshBoards\"\n fillMode=\"flat\">\n </button>\n\n <!-- Settings Button (icon only) -->\n <button\n kendoButton\n [svgIcon]=\"gearIcon\"\n (click)=\"openSettings()\"\n title=\"MeshBoard Settings\"\n aria-label=\"MeshBoard Settings\"\n fillMode=\"flat\">\n </button>\n }\n\n <!-- Refresh Button (icon only) -->\n <button\n kendoButton\n [svgIcon]=\"arrowRotateCwIcon\"\n (click)=\"refresh()\"\n title=\"Refresh All Widgets\"\n aria-label=\"Refresh All Widgets\"\n fillMode=\"flat\">\n </button>\n\n @if (!isReadonly()) {\n <!-- Edit Mode Toggle (icon only) -->\n @if (!isEditMode()) {\n <button\n kendoButton\n [svgIcon]=\"pencilIcon\"\n (click)=\"toggleEditMode()\"\n title=\"Enter Edit Mode\"\n aria-label=\"Enter Edit Mode\"\n fillMode=\"flat\">\n </button>\n } @else {\n <button\n kendoButton\n [svgIcon]=\"xIcon\"\n (click)=\"cancelEdit()\"\n title=\"Cancel Edit Mode\"\n aria-label=\"Cancel Edit Mode\"\n fillMode=\"flat\">\n </button>\n }\n\n <!-- Add Widget Button (with text) -->\n <button\n kendoButton\n [svgIcon]=\"plusIcon\"\n (click)=\"openAddWidget()\"\n title=\"Add Widget\"\n themeColor=\"primary\">\n Add Widget\n </button>\n\n <!-- Save Button (with text) -->\n @if (canSave()) {\n <button\n kendoButton\n [svgIcon]=\"saveIcon\"\n (click)=\"save()\"\n [disabled]=\"isSaving()\"\n title=\"Save MeshBoard\"\n themeColor=\"primary\">\n {{ isSaving() ? 'Saving...' : 'Save' }}\n </button>\n }\n }\n </div>\n </div>\n }\n\n <!-- Entity Selector Hint -->\n @if (unselectedToolbarSelectors().length > 0) {\n <div class=\"entity-selector-hint\">\n <kendo-svg-icon [icon]=\"infoCircleIcon\" size=\"medium\"></kendo-svg-icon>\n <span>\n Please select\n @for (selector of unselectedToolbarSelectors(); track selector.id; let last = $last) {\n <strong>{{ selector.label }}</strong>@if (!last) {, }\n }\n to display dashboard data.\n </span>\n </div>\n }\n\n <!-- Variable Resolution Errors -->\n @if (variableResolutionErrors().length > 0) {\n <div class=\"variable-resolution-errors\">\n <strong>Variable resolution errors:</strong>\n @for (error of variableResolutionErrors(); track error.variableName) {\n <span class=\"error-item\">{{ error.variableName }}: {{ error.message }}</span>\n }\n </div>\n }\n\n <!-- Banner Zone: widgets placed in the banner stack above the grid -->\n @if (isEditMode()) {\n <div class=\"zone-label banner-zone-label\">Banner Zone</div>\n }\n @for (widget of bannerWidgets(); track trackByWidgetId($index, widget); let first = $first; let last = $last) {\n <div class=\"banner-zone-item\" [class.banner-edit]=\"isEditMode()\">\n @if (isEditMode()) {\n <div class=\"banner-edit-header\">\n <span class=\"widget-title\">{{ widget.title }}</span>\n <div class=\"widget-actions\">\n <button\n kendoButton\n [svgIcon]=\"arrowUpIcon\"\n (click)=\"moveBannerUp(widget.id)\"\n fillMode=\"flat\"\n size=\"small\"\n [disabled]=\"first\"\n title=\"Move up\"\n [attr.aria-label]=\"'Move up ' + widget.title\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"arrowDownIcon\"\n (click)=\"moveBannerDown(widget.id)\"\n fillMode=\"flat\"\n size=\"small\"\n [disabled]=\"last\"\n title=\"Move down\"\n [attr.aria-label]=\"'Move down ' + widget.title\">\n </button>\n @if (supportsConfiguration(widget)) {\n <button\n kendoButton\n [svgIcon]=\"linkIcon\"\n (click)=\"openWidgetConfig(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"config-widget-btn\"\n [class.needs-config]=\"isWidgetUnconfigured(widget)\"\n [title]=\"isWidgetUnconfigured(widget) ? 'Configure data source (required)' : 'Configure data source'\"\n [attr.aria-label]=\"(isWidgetUnconfigured(widget) ? 'Configure data source (required) ' : 'Configure data source ') + widget.title\">\n </button>\n }\n <button\n kendoButton\n [svgIcon]=\"gearIcon\"\n (click)=\"openEditWidgetDialog(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"edit-widget-btn\"\n title=\"Edit widget\"\n [attr.aria-label]=\"'Edit widget ' + widget.title\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"trashIcon\"\n (click)=\"removeWidget(widget.id)\"\n fillMode=\"flat\"\n themeColor=\"error\"\n size=\"small\"\n class=\"remove-widget-btn\"\n title=\"Delete widget\"\n [attr.aria-label]=\"'Delete widget ' + widget.title\">\n </button>\n </div>\n </div>\n }\n @if (getWidgetComponentType(widget.type); as componentType) {\n <ng-container *ngComponentOutlet=\"componentType; inputs: { config: widget }\"></ng-container>\n }\n </div>\n }\n\n <!-- MeshBoard Grid -->\n @if (isEditMode()) {\n <div class=\"zone-label grid-zone-label\">Grid Zone</div>\n }\n @if (hasGridWidgets()) {\n <kendo-tilelayout\n [columns]=\"displayColumns()\"\n [autoFlow]=\"displayAutoFlow()\"\n [rowHeight]=\"config().rowHeight\"\n [gap]=\"config().gap\"\n [class.has-banners]=\"bannerWidgets().length > 0 && !isEditMode()\"\n [resizable]=\"canEditLayout()\"\n [reorderable]=\"canEditLayout()\"\n (reorder)=\"onReorder($event)\"\n (resize)=\"onResize($event)\">\n @for (placement of displayGridWidgets(); track placement.widget.id) {\n @let widget = placement.widget;\n <kendo-tilelayout-item\n [col]=\"$any(placement.col)\"\n [row]=\"$any(placement.row)\"\n [colSpan]=\"placement.colSpan\"\n [rowSpan]=\"placement.rowSpan\">\n <kendo-tilelayout-item-header>\n <div class=\"widget-header\"\n [class.unconfigured]=\"isWidgetUnconfigured(widget)\"\n [class.chromeless-header]=\"widget.chromeless && !isEditMode()\">\n @if (!(widget.chromeless && !isEditMode())) {\n <span class=\"widget-title\">{{ widget.title }}</span>\n @if (isWidgetUnconfigured(widget)) {\n <span class=\"unconfigured-badge\" title=\"Widget needs configuration\">!</span>\n }\n }\n @if (isEditMode()) {\n <div class=\"widget-actions\" (pointerdown)=\"$event.stopPropagation()\">\n @if (supportsConfiguration(widget)) {\n <button\n kendoButton\n [svgIcon]=\"linkIcon\"\n (click)=\"openWidgetConfig(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"config-widget-btn\"\n [class.needs-config]=\"isWidgetUnconfigured(widget)\"\n [title]=\"isWidgetUnconfigured(widget) ? 'Configure data source (required)' : 'Configure data source'\"\n [attr.aria-label]=\"(isWidgetUnconfigured(widget) ? 'Configure data source (required) ' : 'Configure data source ') + widget.title\">\n </button>\n }\n <button\n kendoButton\n [svgIcon]=\"gearIcon\"\n (click)=\"openEditWidgetDialog(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"edit-widget-btn\"\n title=\"Edit position\"\n [attr.aria-label]=\"'Edit position ' + widget.title\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"copyIcon\"\n (click)=\"duplicateWidget(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"duplicate-widget-btn\"\n title=\"Duplicate widget\"\n [attr.aria-label]=\"'Duplicate widget ' + widget.title\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"trashIcon\"\n (click)=\"removeWidget(widget.id)\"\n fillMode=\"flat\"\n themeColor=\"error\"\n size=\"small\"\n class=\"remove-widget-btn\"\n title=\"Delete widget\"\n [attr.aria-label]=\"'Delete widget ' + widget.title\">\n </button>\n </div>\n }\n </div>\n </kendo-tilelayout-item-header>\n <kendo-tilelayout-item-body>\n @if (getWidgetComponentType(widget.type); as componentType) {\n <ng-container *ngComponentOutlet=\"componentType; inputs: { config: widget }\"></ng-container>\n } @else {\n <div class=\"widget-error\">\n <p>Unknown widget type: {{ widget.type }}</p>\n </div>\n }\n </kendo-tilelayout-item-body>\n </kendo-tilelayout-item>\n }\n </kendo-tilelayout>\n } @else {\n <mm-meshboard-empty-state [readonly]=\"isReadonly()\" (addWidget)=\"openAddWidget()\"></mm-meshboard-empty-state>\n }\n\n <!-- Edit Widget Dialog -->\n @if (showEditWidgetDialog && editingWidget) {\n <mm-edit-widget-dialog\n [widget]=\"editingWidget\"\n [widgets]=\"config().widgets\"\n [maxColumns]=\"config().columns\"\n [gridService]=\"gridService\"\n (save)=\"onEditWidgetSave($event)\"\n (cancelled)=\"closeEditWidgetDialog()\">\n </mm-edit-widget-dialog>\n }\n }\n</div>\n", styles: [":host{display:flex;flex-direction:column;flex:1;min-height:0;height:100%;width:100%}.meshboard-view{display:flex;flex-direction:column;flex:1;min-height:0;width:100%;background-color:var(--kendo-color-surface, #f5f5f5)}.loading-container{display:flex;align-items:center;justify-content:center;height:100%;width:100%}.entity-selector-hint{display:flex;align-items:center;gap:8px;padding:10px 16px;margin-bottom:8px;background:color-mix(in srgb,var(--kendo-color-info, #2196f3) 10%,var(--kendo-color-surface-alt, #fafafa));border:1px solid color-mix(in srgb,var(--kendo-color-info, #2196f3) 40%,transparent);border-radius:4px;font-size:13px;color:var(--kendo-color-on-app-surface, #424242)}.entity-selector-hint strong{color:var(--kendo-color-info, #2196f3)}.variable-resolution-errors{display:flex;flex-wrap:wrap;align-items:center;gap:8px;padding:8px 16px;margin-bottom:8px;background:color-mix(in srgb,var(--kendo-color-error, #f44336) 10%,var(--kendo-color-surface-alt, #fafafa));border:1px solid var(--kendo-color-error, #f44336);border-radius:4px;font-size:13px;color:var(--kendo-color-on-app-surface, #424242)}.variable-resolution-errors strong{color:var(--kendo-color-error, #f44336)}.variable-resolution-errors .error-item{padding:2px 8px;background:color-mix(in srgb,var(--kendo-color-error, #f44336) 8%,transparent);border-radius:3px;font-family:var(--theme-font-mono);font-size:12px}.model-unavailable{display:flex;align-items:center;justify-content:center;height:100%;width:100%;padding:2rem;overflow:hidden;box-sizing:border-box}.model-unavailable .message-card{text-align:center;max-width:500px;padding:2rem;background:var(--kendo-color-surface-alt, #fafafa);border:1px solid var(--kendo-color-border, #e0e0e0);border-radius:8px;overflow:hidden}.model-unavailable .message-card kendo-svg-icon{color:var(--kendo-color-warning, #ff9800);margin-bottom:1rem}.model-unavailable .message-card h2{margin:1rem 0;color:var(--kendo-color-on-app-surface, #424242)}.model-unavailable .message-card p{margin:.5rem 0;color:var(--kendo-color-subtle, #757575);line-height:1.5}.model-unavailable .message-card ol{text-align:left;margin:1rem 0;padding-left:1.5rem;color:var(--kendo-color-subtle, #757575);line-height:1.8}.model-unavailable .message-card ol li{margin:.25rem 0}.model-unavailable .message-card ol a{color:var(--kendo-color-primary, #3f51b5);text-decoration:none}.model-unavailable .message-card ol a:hover{text-decoration:underline}.meshboard-toolbar{display:flex;justify-content:space-between;align-items:flex-end;padding:1rem 1.5rem;background-color:var(--kendo-color-surface-alt, white);border-bottom:1px solid var(--kendo-color-border, #e0e0e0);gap:1rem;flex-shrink:0}.meshboard-toolbar .toolbar-left{display:flex;align-items:baseline;gap:1rem;flex:1;min-width:0;align-self:center}.meshboard-toolbar .toolbar-left .meshboard-title{margin:0;font-size:1.5rem;font-weight:500;color:var(--kendo-color-on-app-surface, #212121);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.meshboard-toolbar .toolbar-left .meshboard-description{color:var(--kendo-color-subtle, #757575);font-size:.875rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.meshboard-toolbar .toolbar-center{display:flex;justify-content:center;align-items:flex-end;flex-shrink:0}.meshboard-toolbar .toolbar-center mm-time-range-picker ::ng-deep .time-range-picker{gap:.5rem}.meshboard-toolbar .toolbar-separator{width:1px;align-self:stretch;background-color:var(--kendo-color-border, #e0e0e0);flex-shrink:0}.meshboard-toolbar .toolbar-entity-selectors{display:flex;align-items:flex-end;flex-shrink:0}.meshboard-toolbar .toolbar-right{display:flex;gap:.5rem;align-items:center;flex-shrink:0}.meshboard-toolbar.compact{justify-content:flex-end;align-items:center;padding:.25rem 1.5rem 0;background-color:transparent;border-bottom:none}kendo-tilelayout{padding:1.5rem;overflow:auto;flex:1;min-height:0}.widget-header{display:flex;justify-content:space-between;align-items:center;width:100%;gap:.5rem}.widget-header.unconfigured{background-color:#ff98001a}.widget-header .widget-title{flex:1;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.widget-header .unconfigured-badge{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:50%;background-color:var(--kendo-color-warning, #ff9800);color:#fff;font-size:12px;font-weight:700;flex-shrink:0}.widget-header .widget-actions{display:flex;align-items:center;gap:.25rem;flex-shrink:0}.widget-header .config-widget-btn,.widget-header .edit-widget-btn,.widget-header .remove-widget-btn{opacity:.6;transition:opacity .2s}.widget-header .config-widget-btn:hover,.widget-header .edit-widget-btn:hover,.widget-header .remove-widget-btn:hover{opacity:1}.widget-header .config-widget-btn.needs-config{color:var(--kendo-color-warning, #ff9800);opacity:1}.widget-header .remove-widget-btn{color:var(--kendo-color-error, #f44336)}.widget-error{display:flex;align-items:center;justify-content:center;height:100%;padding:1rem;color:var(--kendo-color-error, #f44336);text-align:center}.meshboard-view.edit-mode kendo-tilelayout-item:hover{box-shadow:0 0 0 2px var(--kendo-color-primary, #3f51b5)}@media(max-width:768px){.meshboard-toolbar{flex-direction:column;align-items:stretch;gap:.75rem}.meshboard-toolbar .toolbar-left{flex-direction:column;align-items:flex-start;gap:.25rem}.meshboard-toolbar .toolbar-right{flex-wrap:wrap;justify-content:flex-end}.meshboard-toolbar.compact{align-items:flex-end;padding:.25rem 1rem 0}kendo-tilelayout{padding:1rem}}:host ::ng-deep kendo-tilelayout.has-banners{padding-top:0!important}:host ::ng-deep kendo-tilelayout{padding-bottom:1.5rem!important}.widget-header.chromeless-header{display:none}:host ::ng-deep .k-tilelayout-item-header:has(.chromeless-header){display:none}:host ::ng-deep kendo-tilelayout-item:has(.chromeless-header).k-tilelayout-item,:host ::ng-deep kendo-tilelayout-item:has(.chromeless-header).k-card{border:none;box-shadow:none;background:transparent}:host ::ng-deep kendo-tilelayout-item:has(kendo-dialog),:host ::ng-deep kendo-tilelayout-item:has(kendo-window){z-index:9999}.zone-label{font-size:.65rem;opacity:.4;padding:6px 1rem 2px;font-weight:600}.banner-zone-item{margin:0;padding:8px 1rem}.banner-zone-item.banner-edit{border:1px dashed var(--kendo-color-border, rgba(255, 255, 255, .2));border-radius:4px;margin:0 1rem 12px;padding:4px 0}.banner-edit-header{display:flex;align-items:center;justify-content:space-between;padding:4px 8px;opacity:.7}.banner-edit-header .widget-title{font-size:.75rem}.banner-edit-header .widget-actions{display:flex;gap:2px}.meshboard-view.chrome-plain{background:transparent!important}.meshboard-view.chrome-plain .meshboard-toolbar{padding:0 0 var(--theme-space-3, 12px);background:transparent!important;border-bottom:none!important}.meshboard-view.chrome-plain kendo-tilelayout{padding:0!important}.meshboard-view.chrome-plain .banner-zone-item:not(.banner-edit){padding-left:0;padding-right:0}\n"] }]
|
|
36188
|
+
], changeDetection: ChangeDetectionStrategy.Eager, template: "<div class=\"meshboard-view\" [class.chrome-plain]=\"effectiveChrome() === 'plain'\">\n @if (isLoading()) {\n <div class=\"loading-container\">\n <div class=\"k-loading-mask\">\n <span class=\"k-loading-text\">Loading MeshBoard...</span>\n <div class=\"k-loading-image\"></div>\n <div class=\"k-loading-color\"></div>\n </div>\n </div>\n } @else if (isModelAvailable() === false) {\n <div class=\"model-unavailable\">\n <div class=\"message-card\">\n <kendo-svg-icon [icon]=\"xIcon\" size=\"xlarge\"></kendo-svg-icon>\n <h2>MeshBoard Not Available</h2>\n <p>The MeshBoard feature requires the CK model 'System.UI' version 1.0.1 or higher.</p>\n <p>Please install the required model in your tenant to use this feature.</p>\n </div>\n </div>\n } @else if (notFoundError()) {\n <div class=\"model-unavailable\">\n <div class=\"message-card\">\n <kendo-svg-icon [icon]=\"xIcon\" size=\"xlarge\"></kendo-svg-icon>\n <h2>MeshBoard Not Found</h2>\n <p>{{ notFoundError() }}</p>\n <p>To create a MeshBoard with a Well-Known Name:</p>\n <ol>\n <li>Go to the <a [routerLink]=\"meshBoardPageLink()\">MeshBoard</a> page</li>\n <li>Open Settings</li>\n <li>Set the \"Well-Known Name\" field</li>\n <li>Save the MeshBoard</li>\n </ol>\n </div>\n </div>\n } @else if (isInitialized()) {\n <!-- Toolbar (headerMode: full = name + description + controls, compact = controls only, none = no row) -->\n @if (effectiveHeaderMode() !== 'none') {\n <div class=\"meshboard-toolbar\" [class.compact]=\"effectiveHeaderMode() === 'compact'\">\n @if (effectiveHeaderMode() === 'full') {\n <div class=\"toolbar-left\">\n <h2 class=\"meshboard-title\">{{ config().name }}</h2>\n @if (config().description) {\n <span class=\"meshboard-description\">{{ config().description }}</span>\n }\n </div>\n }\n\n <!-- Time Filter (center) -->\n @if (isTimeFilterEnabled()) {\n <div class=\"toolbar-center\">\n <mm-time-range-picker\n [config]=\"timeFilterConfig()?.pickerConfig ?? {}\"\n [labels]=\"timeRangeLabels()\"\n [initialSelection]=\"initialTimeSelection()\"\n (rangeChange)=\"onTimeRangeChange($event)\"\n (selectionChange)=\"onTimeSelectionChange($event)\">\n </mm-time-range-picker>\n @if (canResetTimeFilter()) {\n <button\n kendoButton\n [svgIcon]=\"undoIcon\"\n (click)=\"resetTimeFilterToDefault()\"\n title=\"Reset to default time filter\"\n aria-label=\"Reset to default time filter\"\n fillMode=\"flat\"\n size=\"small\">\n </button>\n }\n </div>\n }\n\n <!-- Entity Selectors -->\n @if (hasEntitySelectors()) {\n @if (isTimeFilterEnabled()) {\n <div class=\"toolbar-separator\"></div>\n }\n <div class=\"toolbar-entity-selectors\">\n <mm-entity-selector-toolbar\n [entitySelectors]=\"entitySelectorsConfig()\"\n (entitySelected)=\"onEntitySelectorSelected($event)\"\n (entityCleared)=\"onEntitySelectorCleared($event)\">\n </mm-entity-selector-toolbar>\n </div>\n }\n\n @if (isTimeFilterEnabled() || hasEntitySelectors()) {\n <div class=\"toolbar-separator\"></div>\n }\n <div class=\"toolbar-right mm-toolbar-actions\">\n @if (!isReadonly()) {\n <!-- Manager Button (icon only) -->\n <button\n kendoButton\n [svgIcon]=\"gridLayoutIcon\"\n (click)=\"openManager()\"\n title=\"Manage MeshBoards\"\n aria-label=\"Manage MeshBoards\"\n fillMode=\"flat\">\n </button>\n\n <!-- Settings Button (icon only) -->\n <button\n kendoButton\n [svgIcon]=\"gearIcon\"\n (click)=\"openSettings()\"\n title=\"MeshBoard Settings\"\n aria-label=\"MeshBoard Settings\"\n fillMode=\"flat\">\n </button>\n }\n\n <!-- Refresh Button (icon only) -->\n <button\n kendoButton\n [svgIcon]=\"arrowRotateCwIcon\"\n (click)=\"refresh()\"\n title=\"Refresh All Widgets\"\n aria-label=\"Refresh All Widgets\"\n fillMode=\"flat\">\n </button>\n\n @if (!isReadonly()) {\n <!-- Edit Mode Toggle (icon only) -->\n @if (!isEditMode()) {\n <button\n kendoButton\n [svgIcon]=\"pencilIcon\"\n (click)=\"toggleEditMode()\"\n title=\"Enter Edit Mode\"\n aria-label=\"Enter Edit Mode\"\n fillMode=\"flat\">\n </button>\n } @else {\n <button\n kendoButton\n [svgIcon]=\"xIcon\"\n (click)=\"cancelEdit()\"\n title=\"Cancel Edit Mode\"\n aria-label=\"Cancel Edit Mode\"\n fillMode=\"flat\">\n </button>\n }\n\n <!-- Add Widget Button (with text) -->\n <button\n kendoButton\n [svgIcon]=\"plusIcon\"\n (click)=\"openAddWidget()\"\n title=\"Add Widget\"\n themeColor=\"primary\">\n Add Widget\n </button>\n\n <!-- Save Button (with text) -->\n @if (canSave()) {\n <button\n kendoButton\n [svgIcon]=\"saveIcon\"\n (click)=\"save()\"\n [disabled]=\"isSaving()\"\n title=\"Save MeshBoard\"\n themeColor=\"primary\">\n {{ isSaving() ? 'Saving...' : 'Save' }}\n </button>\n }\n }\n </div>\n </div>\n }\n\n <!-- Entity Selector Hint -->\n @if (unselectedToolbarSelectors().length > 0) {\n <div class=\"entity-selector-hint\">\n <kendo-svg-icon [icon]=\"infoCircleIcon\" size=\"medium\"></kendo-svg-icon>\n <span>\n Please select\n @for (selector of unselectedToolbarSelectors(); track selector.id; let last = $last) {\n <strong>{{ selector.label }}</strong>@if (!last) {, }\n }\n to display dashboard data.\n </span>\n </div>\n }\n\n <!-- Variable Resolution Errors -->\n @if (variableResolutionErrors().length > 0) {\n <div class=\"variable-resolution-errors\">\n <strong>Variable resolution errors:</strong>\n @for (error of variableResolutionErrors(); track error.variableName) {\n <span class=\"error-item\">{{ error.variableName }}: {{ error.message }}</span>\n }\n </div>\n }\n\n <!-- Entity selector scopes that could not be resolved (AB#7721) -->\n @if (scopeResolutionErrors().length > 0) {\n <div class=\"variable-resolution-errors scope-resolution-errors\" role=\"status\">\n @for (error of scopeResolutionErrors(); track error.selectorId) {\n <span class=\"error-item\">{{ error.message }}</span>\n }\n </div>\n }\n\n <!-- Banner Zone: widgets placed in the banner stack above the grid -->\n @if (isEditMode()) {\n <div class=\"zone-label banner-zone-label\">Banner Zone</div>\n }\n @for (widget of bannerWidgets(); track trackByWidgetId($index, widget); let first = $first; let last = $last) {\n <div class=\"banner-zone-item\" [class.banner-edit]=\"isEditMode()\">\n @if (isEditMode()) {\n <div class=\"banner-edit-header\">\n <span class=\"widget-title\">{{ widget.title }}</span>\n <div class=\"widget-actions\">\n <button\n kendoButton\n [svgIcon]=\"arrowUpIcon\"\n (click)=\"moveBannerUp(widget.id)\"\n fillMode=\"flat\"\n size=\"small\"\n [disabled]=\"first\"\n title=\"Move up\"\n [attr.aria-label]=\"'Move up ' + widget.title\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"arrowDownIcon\"\n (click)=\"moveBannerDown(widget.id)\"\n fillMode=\"flat\"\n size=\"small\"\n [disabled]=\"last\"\n title=\"Move down\"\n [attr.aria-label]=\"'Move down ' + widget.title\">\n </button>\n @if (supportsConfiguration(widget)) {\n <button\n kendoButton\n [svgIcon]=\"linkIcon\"\n (click)=\"openWidgetConfig(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"config-widget-btn\"\n [class.needs-config]=\"isWidgetUnconfigured(widget)\"\n [title]=\"isWidgetUnconfigured(widget) ? 'Configure data source (required)' : 'Configure data source'\"\n [attr.aria-label]=\"(isWidgetUnconfigured(widget) ? 'Configure data source (required) ' : 'Configure data source ') + widget.title\">\n </button>\n }\n <button\n kendoButton\n [svgIcon]=\"gearIcon\"\n (click)=\"openEditWidgetDialog(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"edit-widget-btn\"\n title=\"Edit widget\"\n [attr.aria-label]=\"'Edit widget ' + widget.title\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"trashIcon\"\n (click)=\"removeWidget(widget.id)\"\n fillMode=\"flat\"\n themeColor=\"error\"\n size=\"small\"\n class=\"remove-widget-btn\"\n title=\"Delete widget\"\n [attr.aria-label]=\"'Delete widget ' + widget.title\">\n </button>\n </div>\n </div>\n }\n @if (getWidgetComponentType(widget.type); as componentType) {\n <ng-container *ngComponentOutlet=\"componentType; inputs: { config: widget }\"></ng-container>\n }\n </div>\n }\n\n <!-- MeshBoard Grid -->\n @if (isEditMode()) {\n <div class=\"zone-label grid-zone-label\">Grid Zone</div>\n }\n @if (hasGridWidgets()) {\n <kendo-tilelayout\n [columns]=\"displayColumns()\"\n [autoFlow]=\"displayAutoFlow()\"\n [rowHeight]=\"config().rowHeight\"\n [gap]=\"config().gap\"\n [class.has-banners]=\"bannerWidgets().length > 0 && !isEditMode()\"\n [resizable]=\"canEditLayout()\"\n [reorderable]=\"canEditLayout()\"\n (reorder)=\"onReorder($event)\"\n (resize)=\"onResize($event)\">\n @for (placement of displayGridWidgets(); track placement.widget.id) {\n @let widget = placement.widget;\n <kendo-tilelayout-item\n [col]=\"$any(placement.col)\"\n [row]=\"$any(placement.row)\"\n [colSpan]=\"placement.colSpan\"\n [rowSpan]=\"placement.rowSpan\">\n <kendo-tilelayout-item-header>\n <div class=\"widget-header\"\n [class.unconfigured]=\"isWidgetUnconfigured(widget)\"\n [class.chromeless-header]=\"widget.chromeless && !isEditMode()\">\n @if (!(widget.chromeless && !isEditMode())) {\n <span class=\"widget-title\">{{ widget.title }}</span>\n @if (isWidgetUnconfigured(widget)) {\n <span class=\"unconfigured-badge\" title=\"Widget needs configuration\">!</span>\n }\n }\n @if (isEditMode()) {\n <div class=\"widget-actions\" (pointerdown)=\"$event.stopPropagation()\">\n @if (supportsConfiguration(widget)) {\n <button\n kendoButton\n [svgIcon]=\"linkIcon\"\n (click)=\"openWidgetConfig(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"config-widget-btn\"\n [class.needs-config]=\"isWidgetUnconfigured(widget)\"\n [title]=\"isWidgetUnconfigured(widget) ? 'Configure data source (required)' : 'Configure data source'\"\n [attr.aria-label]=\"(isWidgetUnconfigured(widget) ? 'Configure data source (required) ' : 'Configure data source ') + widget.title\">\n </button>\n }\n <button\n kendoButton\n [svgIcon]=\"gearIcon\"\n (click)=\"openEditWidgetDialog(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"edit-widget-btn\"\n title=\"Edit position\"\n [attr.aria-label]=\"'Edit position ' + widget.title\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"copyIcon\"\n (click)=\"duplicateWidget(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"duplicate-widget-btn\"\n title=\"Duplicate widget\"\n [attr.aria-label]=\"'Duplicate widget ' + widget.title\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"trashIcon\"\n (click)=\"removeWidget(widget.id)\"\n fillMode=\"flat\"\n themeColor=\"error\"\n size=\"small\"\n class=\"remove-widget-btn\"\n title=\"Delete widget\"\n [attr.aria-label]=\"'Delete widget ' + widget.title\">\n </button>\n </div>\n }\n </div>\n </kendo-tilelayout-item-header>\n <kendo-tilelayout-item-body>\n @if (getWidgetComponentType(widget.type); as componentType) {\n <ng-container *ngComponentOutlet=\"componentType; inputs: { config: widget }\"></ng-container>\n } @else {\n <div class=\"widget-error\">\n <p>Unknown widget type: {{ widget.type }}</p>\n </div>\n }\n </kendo-tilelayout-item-body>\n </kendo-tilelayout-item>\n }\n </kendo-tilelayout>\n } @else {\n <mm-meshboard-empty-state [readonly]=\"isReadonly()\" (addWidget)=\"openAddWidget()\"></mm-meshboard-empty-state>\n }\n\n <!-- Edit Widget Dialog -->\n @if (showEditWidgetDialog && editingWidget) {\n <mm-edit-widget-dialog\n [widget]=\"editingWidget\"\n [widgets]=\"config().widgets\"\n [maxColumns]=\"config().columns\"\n [gridService]=\"gridService\"\n (save)=\"onEditWidgetSave($event)\"\n (cancelled)=\"closeEditWidgetDialog()\">\n </mm-edit-widget-dialog>\n }\n }\n</div>\n", styles: [":host{display:flex;flex-direction:column;flex:1;min-height:0;height:100%;width:100%}.meshboard-view{display:flex;flex-direction:column;flex:1;min-height:0;width:100%;background-color:var(--kendo-color-surface, #f5f5f5)}.loading-container{display:flex;align-items:center;justify-content:center;height:100%;width:100%}.entity-selector-hint{display:flex;align-items:center;gap:8px;padding:10px 16px;margin-bottom:8px;background:color-mix(in srgb,var(--kendo-color-info, #2196f3) 10%,var(--kendo-color-surface-alt, #fafafa));border:1px solid color-mix(in srgb,var(--kendo-color-info, #2196f3) 40%,transparent);border-radius:4px;font-size:13px;color:var(--kendo-color-on-app-surface, #424242)}.entity-selector-hint strong{color:var(--kendo-color-info, #2196f3)}.variable-resolution-errors{display:flex;flex-wrap:wrap;align-items:center;gap:8px;padding:8px 16px;margin-bottom:8px;background:color-mix(in srgb,var(--kendo-color-error, #f44336) 10%,var(--kendo-color-surface-alt, #fafafa));border:1px solid var(--kendo-color-error, #f44336);border-radius:4px;font-size:13px;color:var(--kendo-color-on-app-surface, #424242)}.variable-resolution-errors strong{color:var(--kendo-color-error, #f44336)}.variable-resolution-errors .error-item{padding:2px 8px;background:color-mix(in srgb,var(--kendo-color-error, #f44336) 8%,transparent);border-radius:3px;font-family:var(--theme-font-mono);font-size:12px}.scope-resolution-errors .error-item{font-family:inherit}.model-unavailable{display:flex;align-items:center;justify-content:center;height:100%;width:100%;padding:2rem;overflow:hidden;box-sizing:border-box}.model-unavailable .message-card{text-align:center;max-width:500px;padding:2rem;background:var(--kendo-color-surface-alt, #fafafa);border:1px solid var(--kendo-color-border, #e0e0e0);border-radius:8px;overflow:hidden}.model-unavailable .message-card kendo-svg-icon{color:var(--kendo-color-warning, #ff9800);margin-bottom:1rem}.model-unavailable .message-card h2{margin:1rem 0;color:var(--kendo-color-on-app-surface, #424242)}.model-unavailable .message-card p{margin:.5rem 0;color:var(--kendo-color-subtle, #757575);line-height:1.5}.model-unavailable .message-card ol{text-align:left;margin:1rem 0;padding-left:1.5rem;color:var(--kendo-color-subtle, #757575);line-height:1.8}.model-unavailable .message-card ol li{margin:.25rem 0}.model-unavailable .message-card ol a{color:var(--kendo-color-primary, #3f51b5);text-decoration:none}.model-unavailable .message-card ol a:hover{text-decoration:underline}.meshboard-toolbar{display:flex;justify-content:space-between;align-items:flex-end;padding:1rem 1.5rem;background-color:var(--kendo-color-surface-alt, white);border-bottom:1px solid var(--kendo-color-border, #e0e0e0);gap:1rem;flex-shrink:0}.meshboard-toolbar .toolbar-left{display:flex;align-items:baseline;gap:1rem;flex:1;min-width:0;align-self:center}.meshboard-toolbar .toolbar-left .meshboard-title{margin:0;font-size:1.5rem;font-weight:500;color:var(--kendo-color-on-app-surface, #212121);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.meshboard-toolbar .toolbar-left .meshboard-description{color:var(--kendo-color-subtle, #757575);font-size:.875rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.meshboard-toolbar .toolbar-center{display:flex;justify-content:center;align-items:flex-end;flex-shrink:0}.meshboard-toolbar .toolbar-center mm-time-range-picker ::ng-deep .time-range-picker{gap:.5rem}.meshboard-toolbar .toolbar-separator{width:1px;align-self:stretch;background-color:var(--kendo-color-border, #e0e0e0);flex-shrink:0}.meshboard-toolbar .toolbar-entity-selectors{display:flex;align-items:flex-end;flex-shrink:0}.meshboard-toolbar .toolbar-right{display:flex;gap:.5rem;align-items:center;flex-shrink:0}.meshboard-toolbar.compact{justify-content:flex-end;align-items:center;padding:.25rem 1.5rem 0;background-color:transparent;border-bottom:none}kendo-tilelayout{padding:1.5rem;overflow:auto;flex:1;min-height:0}.widget-header{display:flex;justify-content:space-between;align-items:center;width:100%;gap:.5rem}.widget-header.unconfigured{background-color:#ff98001a}.widget-header .widget-title{flex:1;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.widget-header .unconfigured-badge{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:50%;background-color:var(--kendo-color-warning, #ff9800);color:#fff;font-size:12px;font-weight:700;flex-shrink:0}.widget-header .widget-actions{display:flex;align-items:center;gap:.25rem;flex-shrink:0}.widget-header .config-widget-btn,.widget-header .edit-widget-btn,.widget-header .remove-widget-btn{opacity:.6;transition:opacity .2s}.widget-header .config-widget-btn:hover,.widget-header .edit-widget-btn:hover,.widget-header .remove-widget-btn:hover{opacity:1}.widget-header .config-widget-btn.needs-config{color:var(--kendo-color-warning, #ff9800);opacity:1}.widget-header .remove-widget-btn{color:var(--kendo-color-error, #f44336)}.widget-error{display:flex;align-items:center;justify-content:center;height:100%;padding:1rem;color:var(--kendo-color-error, #f44336);text-align:center}.meshboard-view.edit-mode kendo-tilelayout-item:hover{box-shadow:0 0 0 2px var(--kendo-color-primary, #3f51b5)}@media(max-width:768px){.meshboard-toolbar{flex-direction:column;align-items:stretch;gap:.75rem}.meshboard-toolbar .toolbar-left{flex-direction:column;align-items:flex-start;gap:.25rem}.meshboard-toolbar .toolbar-right{flex-wrap:wrap;justify-content:flex-end}.meshboard-toolbar.compact{align-items:flex-end;padding:.25rem 1rem 0}kendo-tilelayout{padding:1rem}}:host ::ng-deep kendo-tilelayout.has-banners{padding-top:0!important}:host ::ng-deep kendo-tilelayout{padding-bottom:1.5rem!important}.widget-header.chromeless-header{display:none}:host ::ng-deep .k-tilelayout-item-header:has(.chromeless-header){display:none}:host ::ng-deep kendo-tilelayout-item:has(.chromeless-header).k-tilelayout-item,:host ::ng-deep kendo-tilelayout-item:has(.chromeless-header).k-card{border:none;box-shadow:none;background:transparent}:host ::ng-deep kendo-tilelayout-item:has(kendo-dialog),:host ::ng-deep kendo-tilelayout-item:has(kendo-window){z-index:9999}.zone-label{font-size:.65rem;opacity:.4;padding:6px 1rem 2px;font-weight:600}.banner-zone-item{margin:0;padding:8px 1rem}.banner-zone-item.banner-edit{border:1px dashed var(--kendo-color-border, rgba(255, 255, 255, .2));border-radius:4px;margin:0 1rem 12px;padding:4px 0}.banner-edit-header{display:flex;align-items:center;justify-content:space-between;padding:4px 8px;opacity:.7}.banner-edit-header .widget-title{font-size:.75rem}.banner-edit-header .widget-actions{display:flex;gap:2px}.meshboard-view.chrome-plain{background:transparent!important}.meshboard-view.chrome-plain .meshboard-toolbar{padding:0 0 var(--theme-space-3, 12px);background:transparent!important;border-bottom:none!important}.meshboard-view.chrome-plain kendo-tilelayout{padding:0!important}.meshboard-view.chrome-plain .banner-zone-item:not(.banner-edit){padding-left:0;padding-right:0}\n"] }]
|
|
35459
36189
|
}], ctorParameters: () => [], propDecorators: { tileLayout: [{
|
|
35460
36190
|
type: ViewChild,
|
|
35461
36191
|
args: [TileLayoutComponent]
|
|
@@ -38905,5 +39635,5 @@ function provideCockpitWidgets() {
|
|
|
38905
39635
|
* Generated bundle index. Do not edit.
|
|
38906
39636
|
*/
|
|
38907
39637
|
|
|
38908
|
-
export { ADAPTER_OFFLINE_GRACE_MS, ATTENTION_SEVERITY_ORDER, AdaptersAttentionProvider, AddWidgetDialogComponent, AiInsightsConfigDialogComponent, AiInsightsService, AiInsightsWidgetComponent, AlertBannerConfigDialogComponent, AlertBannerWidgetComponent, AlertListConfigDialogComponent, AlertListWidgetComponent, AssociationsConfigDialogComponent, AttentionListConfigDialogComponent, AttentionListWidgetComponent, AutoRefreshTimerService, BUILT_IN_ATTENTION_PROVIDERS, BarChartConfigDialogComponent, BarChartWidgetComponent, BlueprintUpdatesAttentionProvider, COCKPIT_ADAPTER_LIMIT, COCKPIT_ATTENTION_PROVIDERS, COCKPIT_BLUEPRINT_CATALOG_LIMIT, COCKPIT_CHILD_TENANT_IDS, COCKPIT_DATA_FLOW_LIMIT, COCKPIT_EXPLAIN_HANDLER, COCKPIT_FAILED_EXECUTIONS_OPTIONS, COCKPIT_KPI_GATES, COCKPIT_KPI_SLOW_LOADING_MS, COCKPIT_LINK_RESOLVER, COCKPIT_RECENT_ITEMS, COCKPIT_ROLES, COCKPIT_SERVICE_NAMES, COCKPIT_VERSION_SOURCE, COCKPIT_VIEWER_ACCESS, COCKPIT_WIDGET_MESSAGES, COCKPIT_WIDGET_TYPES, CkModelsResolveFailedAttentionProvider, CockpitAdapterStatesService, CockpitAttentionService, CockpitBlueprintStatusService, CockpitChildTenantsService, CockpitCkModelStatesService, CockpitContextService, CockpitDataFlowExecutionsService, CockpitKpiConfigDialogComponent, CockpitKpiService, CockpitKpiWidgetComponent, CockpitServiceHealthService, DAY_MS, DEFAULT_ATTENTION_MAX_ITEMS, DEFAULT_COCKPIT_WIDGET_MESSAGES, DEFAULT_FAILED_EXECUTIONS_OPTIONS, DEFAULT_MESHBOARD_MESSAGES, DEFAULT_RECENT_ITEMS_MAX, DEFAULT_TIME_ZONE_MODE, DashboardDataService, DashboardGridService, EditModeStateService, EditWidgetDialogComponent, EntityAssociationsWidgetComponent, EntityCardConfigDialogComponent, EntityCardWidgetComponent, EntityDetailDialogComponent, EntitySelectorEditorComponent, EntitySelectorToolbarComponent, FALLBACK_SERIES_COLORS, FailedExecutionsAttentionProvider, FeaturesNotInstalledAttentionProvider, GaugeConfigDialogComponent, GaugeWidgetComponent, HISTOGRAM_HOURS, HOUR_MS, HeatmapConfigDialogComponent, HeatmapWidgetComponent, KPI_ERROR_TEXT, KpiConfigDialogComponent, KpiWidgetComponent, LazyMarkdownComponent, LineChartConfigDialogComponent, LineChartWidgetComponent, MAX_RECENT_ITEMS, MESHBOARD_CHROME_ROUTE_DATA, MESHBOARD_DESCRIPTION_MARKER, MESHBOARD_EDIT_QUERY_PARAM, MESHBOARD_HEADER_MODE_ROUTE_DATA, MESHBOARD_LABEL_RESOLVER, MESHBOARD_MESSAGES, MESHBOARD_OPTIONS, MESHBOARD_TENANT_ID_PROVIDER, MarkdownConfigDialogComponent, MarkdownWidgetComponent, MeshBoardDataService, MeshBoardGridService, MeshBoardManagerDialogComponent, MeshBoardPersistenceService, MeshBoardSettingsDialogComponent, MeshBoardSettingsResult, MeshBoardStateService, MeshBoardViewComponent, NOT_AVAILABLE_TEXT, PieChartConfigDialogComponent, PieChartWidgetComponent, QueryExecutorService, QuerySelectorComponent, RecentItemsConfigDialogComponent, RecentItemsWidgetComponent, RuntimeEntitySelectorComponent, SERVICE_MANAGED_BLUEPRINT_PREFIX, STATE_STATUS_BY_KEY, ServiceHealthConfigDialogComponent, ServiceHealthWidgetComponent, ServicesHealthAttentionProvider, StatsGridConfigDialogComponent, StatsGridWidgetComponent, StatusIndicatorConfigDialogComponent, StatusIndicatorWidgetComponent, StatusListConfigDialogComponent, StatusListWidgetComponent, SummaryCardConfigDialogComponent, SummaryCardWidgetComponent, TableConfigDialogComponent, TableWidgetComponent, TableWidgetDataSourceDirective, UnregisteredPoolsAttentionProvider, WidgetFactoryService, WidgetGroupComponent, WidgetGroupConfigDialogComponent, WidgetNotConfiguredComponent, WidgetRegistryService, adapterKpi, adaptersInError, adaptersOffline, blueprintUpdatesKpi, buildHourlyHistogram, categoryStatus, chartSeriesColors, ckModelKpi, classifyQuery, compareBlueprintVersions, compareMeshBoardNavigation, countFlowExecutions, countLabel, countPipelineExecutions, enabledButNotInstalled, executionKpi, failedExecutionsSeverity, formatCockpitMessage, formatCount, humanizeCategory, isAdapterAtRest, isAdapterExpectedToRun, isAdapterOnline, isCockpitWidgetType, isMeshBoardChrome, isMeshBoardHeaderMode, isServiceConfigured, joinEncodedDescription, kpiKindOf, latestStatisticsUpdate, legacyCockpitLinkTarget, loadNgxMarkdown, nameList, newMeshBoardConfig, normalizeNavigation, observeThemeChanges, pipelineExecutionInputs, plural, provideCockpitWidgetHost, provideCockpitWidgets, provideDefaultWidgets, provideMeshBoard, provideMeshBoardMessages, provideProcessWidget, provideWidgetRegistrations, queryFamily, readMeshBoardNavigation, recentRelativeTime, registerCockpitWidgets, registerDefaultWidgets, registerProcessWidget, resolveChartColor, resolveCockpitWidgetMessages, resolveMeshBoardChrome, resolveMeshBoardHeaderMode, resolveMeshBoardLabel, resolveMeshBoardMessages, selectProviders, serviceStateText, servicesHealthKpi, sortAttentionFindings, sparklineGeometry, splitBlueprintId, splitEncodedDescription, statusColor, summarizeAdapterOnline, tenantCountKpi, toBlueprintStatus, toServiceHealthStatus, truncationNote, versionKpi, withMeshBoardNavigation };
|
|
39638
|
+
export { ADAPTER_OFFLINE_GRACE_MS, ATTENTION_SEVERITY_ORDER, AdaptersAttentionProvider, AddWidgetDialogComponent, AiInsightsConfigDialogComponent, AiInsightsService, AiInsightsWidgetComponent, AlertBannerConfigDialogComponent, AlertBannerWidgetComponent, AlertListConfigDialogComponent, AlertListWidgetComponent, AssociationsConfigDialogComponent, AttentionListConfigDialogComponent, AttentionListWidgetComponent, AutoRefreshTimerService, BUILT_IN_ATTENTION_PROVIDERS, BarChartConfigDialogComponent, BarChartWidgetComponent, BlueprintUpdatesAttentionProvider, COCKPIT_ADAPTER_LIMIT, COCKPIT_ATTENTION_PROVIDERS, COCKPIT_BLUEPRINT_CATALOG_LIMIT, COCKPIT_CHILD_TENANT_IDS, COCKPIT_DATA_FLOW_LIMIT, COCKPIT_EXPLAIN_HANDLER, COCKPIT_FAILED_EXECUTIONS_OPTIONS, COCKPIT_KPI_GATES, COCKPIT_KPI_SLOW_LOADING_MS, COCKPIT_LINK_RESOLVER, COCKPIT_RECENT_ITEMS, COCKPIT_ROLES, COCKPIT_SERVICE_NAMES, COCKPIT_VERSION_SOURCE, COCKPIT_VIEWER_ACCESS, COCKPIT_WIDGET_MESSAGES, COCKPIT_WIDGET_TYPES, CkModelsResolveFailedAttentionProvider, CockpitAdapterStatesService, CockpitAttentionService, CockpitBlueprintStatusService, CockpitChildTenantsService, CockpitCkModelStatesService, CockpitContextService, CockpitDataFlowExecutionsService, CockpitKpiConfigDialogComponent, CockpitKpiService, CockpitKpiWidgetComponent, CockpitServiceHealthService, DAY_MS, DEFAULT_ATTENTION_MAX_ITEMS, DEFAULT_COCKPIT_WIDGET_MESSAGES, DEFAULT_FAILED_EXECUTIONS_OPTIONS, DEFAULT_MESHBOARD_MESSAGES, DEFAULT_RECENT_ITEMS_MAX, DEFAULT_TIME_ZONE_MODE, DashboardDataService, DashboardGridService, EditModeStateService, EditWidgetDialogComponent, EntityAssociationsWidgetComponent, EntityCardConfigDialogComponent, EntityCardWidgetComponent, EntityDetailDialogComponent, EntitySelectorEditorComponent, EntitySelectorToolbarComponent, FALLBACK_SERIES_COLORS, FailedExecutionsAttentionProvider, FeaturesNotInstalledAttentionProvider, GaugeConfigDialogComponent, GaugeWidgetComponent, HISTOGRAM_HOURS, HOUR_MS, HeatmapConfigDialogComponent, HeatmapWidgetComponent, KPI_ERROR_TEXT, KpiConfigDialogComponent, KpiWidgetComponent, LazyMarkdownComponent, LineChartConfigDialogComponent, LineChartWidgetComponent, MAX_RECENT_ITEMS, MESHBOARD_CHROME_ROUTE_DATA, MESHBOARD_DESCRIPTION_MARKER, MESHBOARD_EDIT_QUERY_PARAM, MESHBOARD_HEADER_MODE_ROUTE_DATA, MESHBOARD_LABEL_RESOLVER, MESHBOARD_MESSAGES, MESHBOARD_OPTIONS, MESHBOARD_TENANT_ID_PROVIDER, MarkdownConfigDialogComponent, MarkdownWidgetComponent, MeshBoardDataService, MeshBoardGridService, MeshBoardManagerDialogComponent, MeshBoardPersistenceService, MeshBoardSettingsDialogComponent, MeshBoardSettingsResult, MeshBoardStateService, MeshBoardViewComponent, NOT_AVAILABLE_TEXT, PieChartConfigDialogComponent, PieChartWidgetComponent, QueryExecutorService, QuerySelectorComponent, RecentItemsConfigDialogComponent, RecentItemsWidgetComponent, RuntimeEntitySelectorComponent, SERVICE_MANAGED_BLUEPRINT_PREFIX, STATE_STATUS_BY_KEY, ServiceHealthConfigDialogComponent, ServiceHealthWidgetComponent, ServicesHealthAttentionProvider, StatsGridConfigDialogComponent, StatsGridWidgetComponent, StatusIndicatorConfigDialogComponent, StatusIndicatorWidgetComponent, StatusListConfigDialogComponent, StatusListWidgetComponent, SummaryCardConfigDialogComponent, SummaryCardWidgetComponent, TableConfigDialogComponent, TableWidgetComponent, TableWidgetDataSourceDirective, UnregisteredPoolsAttentionProvider, WidgetFactoryService, WidgetGroupComponent, WidgetGroupConfigDialogComponent, WidgetNotConfiguredComponent, WidgetRegistryService, adapterKpi, adaptersInError, adaptersOffline, blueprintUpdatesKpi, buildHourlyHistogram, categoryStatus, chartSeriesColors, ckModelKpi, classifyQuery, compareBlueprintVersions, compareMeshBoardNavigation, countFlowExecutions, countLabel, countPipelineExecutions, emptyQueryExecutionResult, enabledButNotInstalled, executionKpi, failedExecutionsSeverity, formatCockpitMessage, formatCount, humanizeCategory, isAdapterAtRest, isAdapterExpectedToRun, isAdapterOnline, isCockpitWidgetType, isMeshBoardChrome, isMeshBoardHeaderMode, isServiceConfigured, joinEncodedDescription, kpiKindOf, latestStatisticsUpdate, legacyCockpitLinkTarget, loadNgxMarkdown, nameList, newMeshBoardConfig, normalizeNavigation, observeThemeChanges, pipelineExecutionInputs, plural, provideCockpitWidgetHost, provideCockpitWidgets, provideDefaultWidgets, provideMeshBoard, provideMeshBoardMessages, provideProcessWidget, provideWidgetRegistrations, queryFamily, readMeshBoardNavigation, recentRelativeTime, registerCockpitWidgets, registerDefaultWidgets, registerProcessWidget, resolveChartColor, resolveCockpitWidgetMessages, resolveMeshBoardChrome, resolveMeshBoardHeaderMode, resolveMeshBoardLabel, resolveMeshBoardMessages, selectProviders, serviceStateText, servicesHealthKpi, sortAttentionFindings, sparklineGeometry, splitBlueprintId, splitEncodedDescription, statusColor, summarizeAdapterOnline, tenantCountKpi, toBlueprintStatus, toServiceHealthStatus, truncationNote, versionKpi, withMeshBoardNavigation };
|
|
38909
39639
|
//# sourceMappingURL=meshmakers-octo-meshboard.mjs.map
|