@meshmakers/octo-meshboard 3.5.500 → 3.5.510
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import { FieldFilterOperatorsDto, CkTypeSelectorService, RuntimeEntitySelectDataSource, RuntimeEntityDialogDataSource, DeleteStrategiesDto, AssociationModOptionsDto, CkModelService, TENANT_ID_PROVIDER, QueryModeDto, GraphDirectionDto, AttributeSelectorService, GraphQL, GetCkTypeAvailableQueryColumnsDtoGQL, SortOrdersDto, SeriesResolutionSignalDto, HealthService, AssetRepoService, JobManagementService, CONFIGURATION_SERVICE, SystemCommunicationDeploymentStateDto, SystemCommunicationConfigurationStateDto, HealthStatus } from '@meshmakers/octo-services';
|
|
2
2
|
export { RuntimeEntityDialogDataSource, RuntimeEntitySelectDataSource, TENANT_ID_PROVIDER } from '@meshmakers/octo-services';
|
|
3
3
|
import * as i0 from '@angular/core';
|
|
4
|
-
import { Injectable, inject, EventEmitter, forwardRef, Output, Input, ViewChild, ChangeDetectionStrategy, Component, Injector, EnvironmentInjector, ApplicationRef, signal, computed, effect, untracked, Directive, InjectionToken, isSignal, NgZone, DestroyRef, ElementRef, input, createEnvironmentInjector, ViewContainerRef, makeEnvironmentProviders, provideAppInitializer, output, viewChild } from '@angular/core';
|
|
4
|
+
import { Injectable, inject, EventEmitter, forwardRef, Output, Input, ViewChild, ChangeDetectionStrategy, Component, Injector, EnvironmentInjector, ApplicationRef, signal, computed, effect, untracked, Directive, InjectionToken, isSignal, LOCALE_ID, NgZone, DestroyRef, ElementRef, input, createEnvironmentInjector, ViewContainerRef, makeEnvironmentProviders, provideAppInitializer, ChangeDetectorRef, ViewChildren, output, viewChild } from '@angular/core';
|
|
5
5
|
import * as i1$1 from '@angular/forms';
|
|
6
6
|
import { NG_VALUE_ACCESSOR, FormsModule } from '@angular/forms';
|
|
7
|
-
import { firstValueFrom, from, map, Observable, of, switchMap as switchMap$1, catchError, NEVER, interval, filter, combineLatest, defer, EMPTY } from 'rxjs';
|
|
7
|
+
import { firstValueFrom, from, map, Observable, of, switchMap as switchMap$1, forkJoin, catchError, NEVER, interval, Subject, filter, combineLatest, defer, EMPTY } from 'rxjs';
|
|
8
8
|
import * as i1$8 from '@meshmakers/shared-ui';
|
|
9
9
|
import { EntitySelectInputComponent, WindowStateService, TimeRangeUtils, MmDialogActionsDirective, ListViewComponent, FetchResultTyped, DataSourceBase, MM_ACTION_ICONS, ConfirmationService, TimeRangePickerComponent, ImportStrategyDialogService, HAS_UNSAVED_CHANGES, UnsavedChangesDirective } from '@meshmakers/shared-ui';
|
|
10
10
|
import * as i1 from 'apollo-angular';
|
|
11
11
|
import { gql, Apollo } from 'apollo-angular';
|
|
12
12
|
import * as i1$3 from '@angular/common';
|
|
13
|
-
import { CommonModule, NgTemplateOutlet } from '@angular/common';
|
|
13
|
+
import { CommonModule, formatDate, formatNumber, NgTemplateOutlet } from '@angular/common';
|
|
14
14
|
import { CkTypeSelectorInputComponent, SecretSafeAttributeNamesService, AttributeValueTypeDto, PropertyValueDisplayComponent, FieldFilterEditorComponent, PropertyGridComponent, OctoGraphQlDataSource, AttributeSelectorDialogService, AttributeSortSelectorDialogService } from '@meshmakers/octo-ui';
|
|
15
15
|
import * as i1$5 from '@progress/kendo-angular-dialog';
|
|
16
16
|
import { WindowService, WindowCloseResult, WindowRef, DialogsModule, DialogRef, DialogModule, DialogService } from '@progress/kendo-angular-dialog';
|
|
17
|
-
import { switchMap, map as map$1, catchError as catchError$1, startWith, take, shareReplay, tap } from 'rxjs/operators';
|
|
17
|
+
import { switchMap, map as map$1, catchError as catchError$1, debounceTime, startWith, take, shareReplay, tap } from 'rxjs/operators';
|
|
18
18
|
import * as i2 from '@progress/kendo-angular-buttons';
|
|
19
19
|
import { ButtonsModule, ButtonModule } from '@progress/kendo-angular-buttons';
|
|
20
20
|
import * as i3 from '@progress/kendo-angular-inputs';
|
|
@@ -39,6 +39,7 @@ import { LabelModule, KENDO_LABELS } from '@progress/kendo-angular-label';
|
|
|
39
39
|
import { DomSanitizer } from '@angular/platform-browser';
|
|
40
40
|
import * as i5 from '@progress/kendo-angular-layout';
|
|
41
41
|
import { LayoutModule, TabStripModule, TileLayoutModule, TileLayoutComponent } from '@progress/kendo-angular-layout';
|
|
42
|
+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
42
43
|
import * as i4$2 from '@progress/kendo-angular-dateinputs';
|
|
43
44
|
import { DatePickerModule, DateTimePickerModule } from '@progress/kendo-angular-dateinputs';
|
|
44
45
|
import { BreadCrumbService } from '@meshmakers/shared-services';
|
|
@@ -58,6 +59,8 @@ function isCockpitWidgetType(type) {
|
|
|
58
59
|
}
|
|
59
60
|
/** The board-level default timezone mode applied when none is persisted. */
|
|
60
61
|
const DEFAULT_TIME_ZONE_MODE = 'local';
|
|
62
|
+
/** Maximum number of hops of a child-scope path, enforced by editor and runtime (AB#7723). */
|
|
63
|
+
const MAX_SCOPE_HOPS = 5;
|
|
61
64
|
|
|
62
65
|
// Re-export from octo-services (moved there for reuse by octo-ui)
|
|
63
66
|
|
|
@@ -2199,10 +2202,17 @@ class MeshBoardPersistenceService {
|
|
|
2199
2202
|
attributeMappings: es.attributeMappings,
|
|
2200
2203
|
showInToolbar: es.showInToolbar,
|
|
2201
2204
|
defaultRtId: es.defaultRtId,
|
|
2205
|
+
// Carried verbatim: the one-hop form and the multi-hop `path`
|
|
2206
|
+
// (AB#7723) keep their keys and order, so older boards encode unchanged.
|
|
2202
2207
|
childScope: es.childScope,
|
|
2203
2208
|
// AB#7722 — omitted (undefined → dropped by JSON.stringify) for
|
|
2204
2209
|
// selectors that follow nobody, so older boards encode unchanged.
|
|
2205
|
-
followsSelectorId: es.followsSelectorId || undefined
|
|
2210
|
+
followsSelectorId: es.followsSelectorId || undefined,
|
|
2211
|
+
// AB#7724 — omitted when empty, so older boards encode unchanged;
|
|
2212
|
+
// rows are reduced to the persisted filter fields (no editor state).
|
|
2213
|
+
filters: es.filters?.length
|
|
2214
|
+
? es.filters.map(f => ({ attributePath: f.attributePath, operator: f.operator, comparisonValue: f.comparisonValue }))
|
|
2215
|
+
: undefined
|
|
2206
2216
|
}));
|
|
2207
2217
|
}
|
|
2208
2218
|
const dataJson = JSON.stringify(data);
|
|
@@ -2448,13 +2458,20 @@ class MeshBoardStateService {
|
|
|
2448
2458
|
/**
|
|
2449
2459
|
* Transient scope state of each entity selector's current selection (keyed
|
|
2450
2460
|
* by selector id): `pending` while the view resolves the pick, `resolved`
|
|
2451
|
-
* with the source rtIds (
|
|
2461
|
+
* with the source rtIds (childScope hop or path, or the picked entity's
|
|
2452
2462
|
* own rtId; possibly none), or `failed`. No entry = no active pick. Not
|
|
2453
2463
|
* persisted — derived fresh from each selection, cleared on board switch
|
|
2454
2464
|
* (AB#7721).
|
|
2455
2465
|
*/
|
|
2456
2466
|
_entitySelectorScopes = signal({}, /* @ts-ignore */
|
|
2457
2467
|
...(ngDevMode ? [{ debugName: "_entitySelectorScopes" }] : /* istanbul ignore next */ []));
|
|
2468
|
+
/**
|
|
2469
|
+
* Period set by the page that embeds the board (`MeshBoardViewComponent.timeRange`, AB#7582):
|
|
2470
|
+
* exact instants, or `invalid` when the host passed an unusable range. `null` = the board's own
|
|
2471
|
+
* selection applies. Transient — never persisted, never written into the board config.
|
|
2472
|
+
*/
|
|
2473
|
+
_hostTimeRange = signal(null, /* @ts-ignore */
|
|
2474
|
+
...(ngDevMode ? [{ debugName: "_hostTimeRange" }] : /* istanbul ignore next */ []));
|
|
2458
2475
|
/**
|
|
2459
2476
|
* Transient variables published by widgets (KPI output variables, AB#5364).
|
|
2460
2477
|
* Kept apart from `_meshBoardConfig` so publishing triggers no persistence and
|
|
@@ -3068,10 +3085,22 @@ class MeshBoardStateService {
|
|
|
3068
3085
|
* Boundaries are resolved on the board's {@link timeZoneMode} basis (local by
|
|
3069
3086
|
* default, or UTC), the same basis widgets format their datetime display on —
|
|
3070
3087
|
* so the selected range and the displayed timestamps stay consistent.
|
|
3088
|
+
*
|
|
3089
|
+
* While an embedding host has set a period ({@link setHostTimeRange}, AB#7582)
|
|
3090
|
+
* and the board's time filter is enabled, the host's instants are returned
|
|
3091
|
+
* unchanged (no day snapping, no zone conversion) and the stored selection is
|
|
3092
|
+
* ignored; an invalid host period resolves to `null`.
|
|
3071
3093
|
*/
|
|
3072
3094
|
resolveCurrentTimeRange() {
|
|
3073
3095
|
const config = this._meshBoardConfig().timeFilter;
|
|
3074
|
-
if (!config?.enabled
|
|
3096
|
+
if (!config?.enabled) {
|
|
3097
|
+
return null;
|
|
3098
|
+
}
|
|
3099
|
+
const host = this._hostTimeRange();
|
|
3100
|
+
if (host) {
|
|
3101
|
+
return host.status === 'valid' ? { from: new Date(host.from), to: new Date(host.to) } : null;
|
|
3102
|
+
}
|
|
3103
|
+
if (!config.selection) {
|
|
3075
3104
|
return null;
|
|
3076
3105
|
}
|
|
3077
3106
|
const showTime = config.pickerConfig?.showTime ?? false;
|
|
@@ -3140,7 +3169,7 @@ class MeshBoardStateService {
|
|
|
3140
3169
|
/**
|
|
3141
3170
|
* Stores the source rtIds resolved from an entity selector's current
|
|
3142
3171
|
* selection. Called by the view component after a selection resolves (the
|
|
3143
|
-
* one
|
|
3172
|
+
* childScope traversal (one hop or a path), or the picked entity's own rtId when the
|
|
3144
3173
|
* selector has no childScope). An empty array is a resolved EMPTY scope:
|
|
3145
3174
|
* bound widgets show no data and send no query (AB#7721). Transient — not
|
|
3146
3175
|
* persisted.
|
|
@@ -3184,20 +3213,38 @@ class MeshBoardStateService {
|
|
|
3184
3213
|
.map(([id]) => id), /* @ts-ignore */
|
|
3185
3214
|
...(ngDevMode ? [{ debugName: "failedEntitySelectorScopeIds" }] : /* istanbul ignore next */ []));
|
|
3186
3215
|
/**
|
|
3187
|
-
* Decides how a widget
|
|
3188
|
-
*
|
|
3216
|
+
* Decides how a widget runs its persistent query — the entry point every
|
|
3217
|
+
* built-in widget calls with its binding, query family and time-filter opt-out:
|
|
3189
3218
|
*
|
|
3190
|
-
*
|
|
3191
|
-
*
|
|
3219
|
+
* 1. Host gate (AB#7582): while the embedding host's period is invalid, every
|
|
3220
|
+
* stream-data query bound to the time filter is `empty` (`invalidTimeRange`),
|
|
3221
|
+
* whatever its selector binding; pass the widget's `ignoreTimeFilter` so an
|
|
3222
|
+
* opted-out widget, whose data does not depend on the period, keeps querying.
|
|
3223
|
+
* Runtime queries have no time window and are not gated. A legacy config
|
|
3224
|
+
* without `queryFamily` is treated as stream data here.
|
|
3225
|
+
* 2. Entity-selector scope ({@link resolveEntitySelectorScope}, AB#7721) — since
|
|
3226
|
+
* AB#7725 for runtime queries too: `unscoped` / `pending` / `scoped` / `empty`.
|
|
3227
|
+
*/
|
|
3228
|
+
resolveStreamDataScope(entitySelectorId, family, ignoreTimeFilter) {
|
|
3229
|
+
if (family !== 'runtime' && this.isStreamDataBlockedByHostTimeRange(ignoreTimeFilter)) {
|
|
3230
|
+
return { kind: 'empty', reason: 'invalidTimeRange' };
|
|
3231
|
+
}
|
|
3232
|
+
return this.resolveEntitySelectorScope(entitySelectorId);
|
|
3233
|
+
}
|
|
3234
|
+
/**
|
|
3235
|
+
* The entity-selector part of {@link resolveStreamDataScope}, without the host
|
|
3236
|
+
* gate — stream data (AB#7721) and runtime queries alike (AB#7725):
|
|
3237
|
+
*
|
|
3238
|
+
* - `unscoped` — no binding, or no active pick: keep the query as saved.
|
|
3192
3239
|
* - `pending` — the pick is still resolving: show loading, send no query.
|
|
3193
|
-
* - `scoped` — query
|
|
3240
|
+
* - `scoped` — query restricted to the resolved rtIds: stream data via
|
|
3241
|
+
* `streamDataArgs.rtIds`, runtime queries via the `rtId IN` field filter
|
|
3242
|
+
* (`QueryExecutionOptions.scopeRtIds`).
|
|
3194
3243
|
* - `empty` — the pick has nothing in scope or the lookup failed: show the
|
|
3195
3244
|
* no-data state and send no query. Never fall back to the persisted scope.
|
|
3196
|
-
*
|
|
3197
|
-
* A legacy config without `queryFamily` is treated as stream data.
|
|
3198
3245
|
*/
|
|
3199
|
-
|
|
3200
|
-
if (!entitySelectorId
|
|
3246
|
+
resolveEntitySelectorScope(entitySelectorId) {
|
|
3247
|
+
if (!entitySelectorId) {
|
|
3201
3248
|
return { kind: 'unscoped' };
|
|
3202
3249
|
}
|
|
3203
3250
|
const state = this._entitySelectorScopes()[entitySelectorId];
|
|
@@ -3225,12 +3272,46 @@ class MeshBoardStateService {
|
|
|
3225
3272
|
* skip the query for `pending` / `empty` (AB#7721).
|
|
3226
3273
|
*/
|
|
3227
3274
|
resolveStreamDataRtIds(entitySelectorId) {
|
|
3228
|
-
const scope = this.
|
|
3275
|
+
const scope = this.resolveEntitySelectorScope(entitySelectorId);
|
|
3229
3276
|
return scope.kind === 'scoped' ? scope.rtIds : undefined;
|
|
3230
3277
|
}
|
|
3231
3278
|
setEntitySelectorScope(selectorId, state) {
|
|
3232
3279
|
this._entitySelectorScopes.update(map => ({ ...map, [selectorId]: state }));
|
|
3233
3280
|
}
|
|
3281
|
+
/**
|
|
3282
|
+
* Sets (or with `null` clears) the period of the page embedding the board
|
|
3283
|
+
* (AB#7582). Called by `MeshBoardViewComponent` for its `timeRange` input; a
|
|
3284
|
+
* host normally sets the input instead of calling this. Transient: not
|
|
3285
|
+
* persisted, not written into the board config, so it never causes unsaved
|
|
3286
|
+
* changes. Only takes effect while the board's time filter is enabled.
|
|
3287
|
+
*/
|
|
3288
|
+
setHostTimeRange(state, owner) {
|
|
3289
|
+
this._hostTimeRange.set(state);
|
|
3290
|
+
this.hostTimeRangeOwner = state ? owner ?? null : null;
|
|
3291
|
+
}
|
|
3292
|
+
/**
|
|
3293
|
+
* Clears the host period only if `owner` set it (AB#7582). The service is
|
|
3294
|
+
* root-provided, so a view that is destroyed (or drops its `timeRange`) must not
|
|
3295
|
+
* clear a period another view set in the meantime.
|
|
3296
|
+
*/
|
|
3297
|
+
clearHostTimeRange(owner) {
|
|
3298
|
+
if (this.hostTimeRangeOwner === owner) {
|
|
3299
|
+
this.setHostTimeRange(null);
|
|
3300
|
+
}
|
|
3301
|
+
}
|
|
3302
|
+
/** The view (or other caller) that set the current host period, if it said so. */
|
|
3303
|
+
hostTimeRangeOwner = null;
|
|
3304
|
+
/** The period set by the embedding host, or `null` when the board's own selection applies. */
|
|
3305
|
+
hostTimeRange = this._hostTimeRange.asReadonly();
|
|
3306
|
+
/**
|
|
3307
|
+
* True when an invalid host period blocks a stream-data query that is bound to
|
|
3308
|
+
* the time filter (AB#7582): the period is invalid, the board's time filter is
|
|
3309
|
+
* enabled and the caller did not opt out with `ignoreTimeFilter`. Such a query
|
|
3310
|
+
* must not be sent — without a window it would read the whole archive.
|
|
3311
|
+
*/
|
|
3312
|
+
isStreamDataBlockedByHostTimeRange(ignoreTimeFilter) {
|
|
3313
|
+
return !ignoreTimeFilter && this._hostTimeRange()?.status === 'invalid' && this.isTimeFilterEnabled();
|
|
3314
|
+
}
|
|
3234
3315
|
/**
|
|
3235
3316
|
* Updates the time filter configuration.
|
|
3236
3317
|
*/
|
|
@@ -3747,7 +3828,7 @@ class QueryExecutorService {
|
|
|
3747
3828
|
rtId: queryRtId,
|
|
3748
3829
|
first: options.first ?? undefined,
|
|
3749
3830
|
after: options.after ?? undefined,
|
|
3750
|
-
fieldFilter: options.fieldFilter
|
|
3831
|
+
fieldFilter: QueryExecutorService.withRtIdScope(options.fieldFilter, options.scopeRtIds)
|
|
3751
3832
|
},
|
|
3752
3833
|
fetchPolicy: options.forceRefresh ? 'network-only' : 'cache-first'
|
|
3753
3834
|
}).pipe(map$1(result => {
|
|
@@ -3768,7 +3849,8 @@ class QueryExecutorService {
|
|
|
3768
3849
|
}));
|
|
3769
3850
|
}
|
|
3770
3851
|
executeStreamData(queryRtId, options = {}) {
|
|
3771
|
-
const
|
|
3852
|
+
const streamDataArgs = QueryExecutorService.withStreamDataScope(options.streamDataArgs, options.scopeRtIds);
|
|
3853
|
+
const arg = streamDataArgs ? this.buildStreamDataArg(streamDataArgs) : undefined;
|
|
3772
3854
|
return this.streamDataGql.fetch({
|
|
3773
3855
|
variables: {
|
|
3774
3856
|
rtId: queryRtId,
|
|
@@ -3945,6 +4027,29 @@ class QueryExecutorService {
|
|
|
3945
4027
|
}
|
|
3946
4028
|
return groups;
|
|
3947
4029
|
}
|
|
4030
|
+
/**
|
|
4031
|
+
* Runtime entity-selector scope (AB#7725): appends `rtId IN (scopeRtIds)` to the
|
|
4032
|
+
* caller's field filters. The backend AND-combines all runtime field filters with
|
|
4033
|
+
* the persisted query's filters before the database query, so simple rows as well
|
|
4034
|
+
* as aggregation / grouping results cover the scoped entities only. Returns the
|
|
4035
|
+
* caller's filters unchanged (or `undefined`) when there is no scope.
|
|
4036
|
+
*/
|
|
4037
|
+
static withRtIdScope(fieldFilter, scopeRtIds) {
|
|
4038
|
+
if (!scopeRtIds || scopeRtIds.length === 0) {
|
|
4039
|
+
return fieldFilter ?? undefined;
|
|
4040
|
+
}
|
|
4041
|
+
return [
|
|
4042
|
+
...(fieldFilter ?? []),
|
|
4043
|
+
{ attributePath: 'rtId', operator: FieldFilterOperatorsDto.InDto, comparisonValue: scopeRtIds }
|
|
4044
|
+
];
|
|
4045
|
+
}
|
|
4046
|
+
/** Stream-data entity-selector scope: `scopeRtIds` fill `rtIds` unless the caller set them. */
|
|
4047
|
+
static withStreamDataScope(args, scopeRtIds) {
|
|
4048
|
+
if (!scopeRtIds || scopeRtIds.length === 0 || (args?.rtIds && args.rtIds.length > 0)) {
|
|
4049
|
+
return args;
|
|
4050
|
+
}
|
|
4051
|
+
return { ...args, rtIds: scopeRtIds };
|
|
4052
|
+
}
|
|
3948
4053
|
buildStreamDataArg(args) {
|
|
3949
4054
|
// Skip the entire `arg` field when the caller has nothing to override —
|
|
3950
4055
|
// the persisted query then runs with its intrinsic from/to/limit and the
|
|
@@ -4086,6 +4191,45 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
4086
4191
|
}]
|
|
4087
4192
|
}], ctorParameters: () => [{ type: i1.Apollo }] });
|
|
4088
4193
|
|
|
4194
|
+
const GetAssociationTargetRtIdsDocumentDto = gql `
|
|
4195
|
+
query getAssociationTargetRtIds($rtIds: [OctoObjectId]!, $ckTypeId: String!, $targetCkTypeId: String!, $roleId: String!, $direction: GraphDirection!, $first: Int!, $targetsFirst: Int!) {
|
|
4196
|
+
runtime {
|
|
4197
|
+
runtimeEntities(rtIds: $rtIds, ckId: $ckTypeId, first: $first) {
|
|
4198
|
+
items {
|
|
4199
|
+
rtId
|
|
4200
|
+
associations {
|
|
4201
|
+
targets(
|
|
4202
|
+
ckId: $targetCkTypeId
|
|
4203
|
+
roleId: $roleId
|
|
4204
|
+
direction: $direction
|
|
4205
|
+
first: $targetsFirst
|
|
4206
|
+
) {
|
|
4207
|
+
totalCount
|
|
4208
|
+
items {
|
|
4209
|
+
rtId
|
|
4210
|
+
}
|
|
4211
|
+
}
|
|
4212
|
+
}
|
|
4213
|
+
}
|
|
4214
|
+
}
|
|
4215
|
+
}
|
|
4216
|
+
}
|
|
4217
|
+
`;
|
|
4218
|
+
class GetAssociationTargetRtIdsDtoGQL extends i1.Query {
|
|
4219
|
+
document = GetAssociationTargetRtIdsDocumentDto;
|
|
4220
|
+
constructor(apollo) {
|
|
4221
|
+
super(apollo);
|
|
4222
|
+
}
|
|
4223
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: GetAssociationTargetRtIdsDtoGQL, deps: [{ token: i1.Apollo }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
4224
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: GetAssociationTargetRtIdsDtoGQL, providedIn: 'root' });
|
|
4225
|
+
}
|
|
4226
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: GetAssociationTargetRtIdsDtoGQL, decorators: [{
|
|
4227
|
+
type: Injectable,
|
|
4228
|
+
args: [{
|
|
4229
|
+
providedIn: 'root'
|
|
4230
|
+
}]
|
|
4231
|
+
}], ctorParameters: () => [{ type: i1.Apollo }] });
|
|
4232
|
+
|
|
4089
4233
|
const GetCkTypeAttributesForMeshboardDocumentDto = gql `
|
|
4090
4234
|
query getCkTypeAttributesForMeshboard($ckTypeId: String!, $first: Int) {
|
|
4091
4235
|
constructionKit {
|
|
@@ -4308,12 +4452,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
4308
4452
|
}]
|
|
4309
4453
|
}] });
|
|
4310
4454
|
|
|
4455
|
+
/** Source rtIds per request of one batched association hop (AB#7723). */
|
|
4456
|
+
const ASSOCIATION_HOP_SOURCE_BATCH = 100;
|
|
4457
|
+
/** Targets read per source entity in one association hop (AB#7723). */
|
|
4458
|
+
const ASSOCIATION_HOP_TARGETS_PER_SOURCE = 1000;
|
|
4311
4459
|
class MeshBoardDataService {
|
|
4312
4460
|
getDashboardEntityGQL = inject(GetDashboardEntityDtoGQL);
|
|
4313
4461
|
getCkModelsWithStateGQL = inject(GetCkModelsWithStateDtoGQL);
|
|
4314
4462
|
getEntitiesByCkTypeGQL = inject(GetEntitiesByCkTypeDtoGQL);
|
|
4315
4463
|
queryExecutor = inject(QueryExecutorService);
|
|
4316
4464
|
getAssociationTargetsGQL = inject(GetAssociationTargetsDtoGQL);
|
|
4465
|
+
getAssociationTargetRtIdsGQL = inject(GetAssociationTargetRtIdsDtoGQL);
|
|
4317
4466
|
secretSafeNames = inject(SecretSafeAttributeNamesService);
|
|
4318
4467
|
getCkTypeAttributesGQL = inject(GetCkTypeAttributesForMeshboardDtoGQL);
|
|
4319
4468
|
apollo = inject(Apollo);
|
|
@@ -4552,6 +4701,13 @@ class MeshBoardDataService {
|
|
|
4552
4701
|
*/
|
|
4553
4702
|
async fetchRepeaterFromQuery(family, queryRtId, maxItems) {
|
|
4554
4703
|
try {
|
|
4704
|
+
// AB#7582: while the embedding host's period is invalid, a stream-data
|
|
4705
|
+
// repeater query is not sent (it would read the whole archive); the group
|
|
4706
|
+
// shows no items. Runtime queries are unaffected.
|
|
4707
|
+
if (this.stateService.isStreamDataBlockedByHostTimeRange()
|
|
4708
|
+
&& (family ?? await this.queryExecutor.resolveFamily(queryRtId)) === 'streamData') {
|
|
4709
|
+
return [];
|
|
4710
|
+
}
|
|
4555
4711
|
const streamDataArgs = this.buildRepeaterStreamDataArgs();
|
|
4556
4712
|
const result = await firstValueFrom(this.queryExecutor.execute(family, queryRtId, {
|
|
4557
4713
|
first: maxItems,
|
|
@@ -4676,6 +4832,56 @@ class MeshBoardDataService {
|
|
|
4676
4832
|
}));
|
|
4677
4833
|
}));
|
|
4678
4834
|
}
|
|
4835
|
+
/**
|
|
4836
|
+
* One association hop for MANY source entities (AB#7723, multi-hop child
|
|
4837
|
+
* scope): the rtIds of the `targetCkTypeId` entities reached from each of
|
|
4838
|
+
* `sourceRtIds` (all of `sourceCkTypeId`) over `roleId` in `direction`.
|
|
4839
|
+
*
|
|
4840
|
+
* Query cost: the sources go out in batches of
|
|
4841
|
+
* {@link ASSOCIATION_HOP_SOURCE_BATCH} rtIds per request (`runtimeEntities(rtIds:)`
|
|
4842
|
+
* with the association targets nested), the batches of one hop in parallel —
|
|
4843
|
+
* never one request per entity. Only rtIds are selected. Per source at most
|
|
4844
|
+
* {@link ASSOCIATION_HOP_TARGETS_PER_SOURCE} targets are read; a truncated
|
|
4845
|
+
* source is logged as a warning. Duplicates are not removed here.
|
|
4846
|
+
* Errors propagate (the caller turns them into a failed scope).
|
|
4847
|
+
*/
|
|
4848
|
+
fetchAssociationTargetRtIds(sourceRtIds, sourceCkTypeId, targetCkTypeId, roleId, direction) {
|
|
4849
|
+
if (sourceRtIds.length === 0) {
|
|
4850
|
+
return of([]);
|
|
4851
|
+
}
|
|
4852
|
+
const graphDirection = direction === 'out' ? GraphDirectionDto.OutboundDto : GraphDirectionDto.InboundDto;
|
|
4853
|
+
const batches = [];
|
|
4854
|
+
for (let i = 0; i < sourceRtIds.length; i += ASSOCIATION_HOP_SOURCE_BATCH) {
|
|
4855
|
+
batches.push(sourceRtIds.slice(i, i + ASSOCIATION_HOP_SOURCE_BATCH));
|
|
4856
|
+
}
|
|
4857
|
+
return forkJoin(batches.map(batch => this.getAssociationTargetRtIdsGQL.fetch({
|
|
4858
|
+
variables: {
|
|
4859
|
+
rtIds: batch,
|
|
4860
|
+
ckTypeId: sourceCkTypeId,
|
|
4861
|
+
targetCkTypeId,
|
|
4862
|
+
roleId,
|
|
4863
|
+
direction: graphDirection,
|
|
4864
|
+
first: batch.length,
|
|
4865
|
+
targetsFirst: ASSOCIATION_HOP_TARGETS_PER_SOURCE
|
|
4866
|
+
}
|
|
4867
|
+
}).pipe(map(result => {
|
|
4868
|
+
const rtIds = [];
|
|
4869
|
+
for (const source of result.data?.runtime?.runtimeEntities?.items ?? []) {
|
|
4870
|
+
const targets = source?.associations?.targets;
|
|
4871
|
+
const items = targets?.items ?? [];
|
|
4872
|
+
if ((targets?.totalCount ?? 0) > items.length) {
|
|
4873
|
+
console.warn(`Child scope hop ${roleId} → ${targetCkTypeId}: source ${String(source?.rtId)} has `
|
|
4874
|
+
+ `${targets?.totalCount} targets, only the first ${items.length} are used.`);
|
|
4875
|
+
}
|
|
4876
|
+
for (const target of items) {
|
|
4877
|
+
if (target?.rtId !== null && target?.rtId !== undefined) {
|
|
4878
|
+
rtIds.push(String(target.rtId));
|
|
4879
|
+
}
|
|
4880
|
+
}
|
|
4881
|
+
}
|
|
4882
|
+
return rtIds;
|
|
4883
|
+
})))).pipe(map(perBatch => perBatch.flat()));
|
|
4884
|
+
}
|
|
4679
4885
|
/**
|
|
4680
4886
|
* Fetches CK type attribute definitions (name + type) for config dialog.
|
|
4681
4887
|
*/
|
|
@@ -6614,7 +6820,7 @@ class KpiWidgetComponent {
|
|
|
6614
6820
|
// Entity-selector scope (AB#7721): pending → stay in the loading state and
|
|
6615
6821
|
// send no query (the view refreshes once the scope is known); empty → show
|
|
6616
6822
|
// the no-data value '-' without querying the persisted (unscoped) scope.
|
|
6617
|
-
const scope = this.stateService.resolveStreamDataScope(dataSource.entitySelectorId, dataSource.queryFamily);
|
|
6823
|
+
const scope = this.stateService.resolveStreamDataScope(dataSource.entitySelectorId, dataSource.queryFamily, dataSource.ignoreTimeFilter);
|
|
6618
6824
|
if (scope.kind === 'pending') {
|
|
6619
6825
|
return;
|
|
6620
6826
|
}
|
|
@@ -6626,7 +6832,9 @@ class KpiWidgetComponent {
|
|
|
6626
6832
|
const streamDataArgs = this.buildStreamDataArgs();
|
|
6627
6833
|
const result = await firstValueFrom(this.queryExecutor.execute(dataSource.queryFamily, dataSource.queryRtId, {
|
|
6628
6834
|
fieldFilter: fieldFilter ?? undefined,
|
|
6629
|
-
streamDataArgs
|
|
6835
|
+
streamDataArgs,
|
|
6836
|
+
// Runtime queries: `rtId IN` field filter (AB#7725); stream data: arg.rtIds.
|
|
6837
|
+
scopeRtIds: scope.kind === 'scoped' ? scope.rtIds : undefined
|
|
6630
6838
|
}).pipe(catchError(err => {
|
|
6631
6839
|
console.error('Error loading KPI query data:', err);
|
|
6632
6840
|
throw err;
|
|
@@ -6866,19 +7074,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
6866
7074
|
}] } });
|
|
6867
7075
|
|
|
6868
7076
|
/**
|
|
6869
|
-
* Per-widget asset-scope binding for
|
|
7077
|
+
* Per-widget asset-scope binding for persistent-query widgets — stream-data and
|
|
7078
|
+
* runtime queries (AB#7725).
|
|
6870
7079
|
*
|
|
6871
|
-
*
|
|
6872
|
-
*
|
|
6873
|
-
* (
|
|
6874
|
-
*
|
|
6875
|
-
*
|
|
6876
|
-
*
|
|
6877
|
-
*
|
|
7080
|
+
* This control lets the designer bind a single widget to one of the MeshBoard's
|
|
7081
|
+
* entity selectors: when the selector's entity is picked, its resolved rtIds
|
|
7082
|
+
* (the selector's `childScope` hop or path, or the picked entity itself) scope
|
|
7083
|
+
* this widget's query on the server (see
|
|
7084
|
+
* `MeshBoardStateService.resolveEntitySelectorScope` and
|
|
7085
|
+
* `QueryExecutionOptions.scopeRtIds`):
|
|
7086
|
+
* - stream-data queries get the rtIds as `streamDataArgs.rtIds` (the backend
|
|
7087
|
+
* `StreamDataArguments.rtIds` override) — simple, aggregation, grouping and
|
|
7088
|
+
* downsampling queries alike;
|
|
7089
|
+
* - runtime queries (SimpleRtQuery, AggregationRtQuery,
|
|
7090
|
+
* GroupingAggregationRtQuery) get the field filter `rtId IN (rtIds)`,
|
|
7091
|
+
* AND-combined with the widget's and the saved query's own filters.
|
|
6878
7092
|
*
|
|
6879
|
-
* The control renders
|
|
6880
|
-
*
|
|
6881
|
-
* the picker stays hidden (mirrors `SdTimeFilterToggleComponent`).
|
|
7093
|
+
* The control renders once a query is selected (`family` is known); without a
|
|
7094
|
+
* query there is nothing to scope.
|
|
6882
7095
|
*
|
|
6883
7096
|
* Two-way bindable:
|
|
6884
7097
|
* ```html
|
|
@@ -6890,7 +7103,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
6890
7103
|
* ```
|
|
6891
7104
|
*/
|
|
6892
7105
|
class EntitySelectorScopePickerComponent {
|
|
6893
|
-
/** Family of the currently selected query. Picker is shown
|
|
7106
|
+
/** Family of the currently selected query. Picker is shown once a query (either family) is selected. */
|
|
6894
7107
|
family;
|
|
6895
7108
|
/** Currently bound entity selector id (`undefined` ⇒ no binding). */
|
|
6896
7109
|
entitySelectorId;
|
|
@@ -6905,7 +7118,7 @@ class EntitySelectorScopePickerComponent {
|
|
|
6905
7118
|
/** Emits on change (enables `[(entitySelectorId)]` two-way binding). */
|
|
6906
7119
|
entitySelectorIdChange = new EventEmitter();
|
|
6907
7120
|
get visible() {
|
|
6908
|
-
return this.family === 'streamData';
|
|
7121
|
+
return this.family === 'streamData' || this.family === 'runtime';
|
|
6909
7122
|
}
|
|
6910
7123
|
/** "None" row first, then one row per available selector. */
|
|
6911
7124
|
get options() {
|
|
@@ -6932,7 +7145,7 @@ class EntitySelectorScopePickerComponent {
|
|
|
6932
7145
|
(valueChange)="onSelect($event)">
|
|
6933
7146
|
</kendo-dropdownlist>
|
|
6934
7147
|
<p class="field-hint">
|
|
6935
|
-
|
|
7148
|
+
Restricts this widget's query to the entities resolved from the
|
|
6936
7149
|
selected asset. Leave unset to use the saved query's own scope.
|
|
6937
7150
|
</p>
|
|
6938
7151
|
</div>
|
|
@@ -6954,7 +7167,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
6954
7167
|
(valueChange)="onSelect($event)">
|
|
6955
7168
|
</kendo-dropdownlist>
|
|
6956
7169
|
<p class="field-hint">
|
|
6957
|
-
|
|
7170
|
+
Restricts this widget's query to the entities resolved from the
|
|
6958
7171
|
selected asset. Leave unset to use the saved query's own scope.
|
|
6959
7172
|
</p>
|
|
6960
7173
|
</div>
|
|
@@ -8871,7 +9084,7 @@ const ISO_NAIVE_DATE_TIME_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2})?(\.\d+)?$
|
|
|
8871
9084
|
* instant. A naive ISO date-time string (no `Z`/offset) is treated as UTC — the
|
|
8872
9085
|
* wire's serialization of an instant — never as browser-local time (AB#4818).
|
|
8873
9086
|
*/
|
|
8874
|
-
function toInstant(value) {
|
|
9087
|
+
function toInstant$1(value) {
|
|
8875
9088
|
if (value instanceof Date) {
|
|
8876
9089
|
return isNaN(value.getTime()) ? null : value;
|
|
8877
9090
|
}
|
|
@@ -8929,7 +9142,7 @@ function partsFormatter(mode) {
|
|
|
8929
9142
|
* non-instants so callers can skip the row.
|
|
8930
9143
|
*/
|
|
8931
9144
|
function getZonedDateParts(value, mode) {
|
|
8932
|
-
const date = toInstant(value);
|
|
9145
|
+
const date = toInstant$1(value);
|
|
8933
9146
|
if (!date) {
|
|
8934
9147
|
return null;
|
|
8935
9148
|
}
|
|
@@ -8961,7 +9174,7 @@ function zonedDateKey(parts) {
|
|
|
8961
9174
|
* instant, so callers can fall back to their own rendering.
|
|
8962
9175
|
*/
|
|
8963
9176
|
function formatInstant(value, mode, options) {
|
|
8964
|
-
const date = toInstant(value);
|
|
9177
|
+
const date = toInstant$1(value);
|
|
8965
9178
|
if (!date) {
|
|
8966
9179
|
return null;
|
|
8967
9180
|
}
|
|
@@ -10266,7 +10479,7 @@ class TableWidgetDataSourceDirective extends OctoGraphQlDataSource {
|
|
|
10266
10479
|
// state (an observable that never emits; the next refresh after the scope
|
|
10267
10480
|
// resolves cancels it) and send no query; empty → no rows, no query — never
|
|
10268
10481
|
// the persisted (unscoped) scope.
|
|
10269
|
-
const scope = this.stateService.resolveStreamDataScope(dataSource.entitySelectorId, dataSource.queryFamily);
|
|
10482
|
+
const scope = this.stateService.resolveStreamDataScope(dataSource.entitySelectorId, dataSource.queryFamily, dataSource.ignoreTimeFilter);
|
|
10270
10483
|
if (scope.kind === 'pending') {
|
|
10271
10484
|
return NEVER;
|
|
10272
10485
|
}
|
|
@@ -10285,7 +10498,9 @@ class TableWidgetDataSourceDirective extends OctoGraphQlDataSource {
|
|
|
10285
10498
|
first: queryOptions.state.take ?? this._config?.pageSize ?? 10,
|
|
10286
10499
|
after: GraphQL.offsetToCursor(queryOptions.state.skip ?? 0),
|
|
10287
10500
|
fieldFilter: fieldFilter ?? undefined,
|
|
10288
|
-
streamDataArgs
|
|
10501
|
+
streamDataArgs,
|
|
10502
|
+
// Runtime queries: `rtId IN` field filter (AB#7725); stream data: arg.rtIds.
|
|
10503
|
+
scopeRtIds: scope.kind === 'scoped' ? scope.rtIds : undefined
|
|
10289
10504
|
}).pipe(map$1(result => {
|
|
10290
10505
|
// Extract columns from query response and update signal.
|
|
10291
10506
|
// Replace dots with underscores for grid compatibility (Kendo treats dots as nested paths).
|
|
@@ -10472,6 +10687,286 @@ function resolveMeshBoardLabel(resolver, request) {
|
|
|
10472
10687
|
return typeof text === 'string' && text !== '' ? text : request.defaultText;
|
|
10473
10688
|
}
|
|
10474
10689
|
|
|
10690
|
+
/**
|
|
10691
|
+
* Optional: the host's current UI language. MeshBoard does not derive it from `LOCALE_ID` (that is
|
|
10692
|
+
* static in many apps); a host that wants per-language texts in board configurations (table column
|
|
10693
|
+
* `titles`, `valueLabels`) hands its language over once, via `provideMeshBoard({ language })`,
|
|
10694
|
+
* `provideMeshBoardLanguage(language)` or this token directly. Not provided = the configured default
|
|
10695
|
+
* texts (e.g. the column `title`).
|
|
10696
|
+
*
|
|
10697
|
+
* ```ts
|
|
10698
|
+
* providers: [provideMeshBoard({ language: languageService.currentLanguage })] // Signal<string>
|
|
10699
|
+
* ```
|
|
10700
|
+
*/
|
|
10701
|
+
const MESHBOARD_LANGUAGE = new InjectionToken('MESHBOARD_LANGUAGE');
|
|
10702
|
+
/** Reads a {@link MeshBoardLanguageSource} (fixed or signal) as a normalised culture code, or `null`. */
|
|
10703
|
+
function readMeshBoardLanguageSource(source) {
|
|
10704
|
+
if (!source) {
|
|
10705
|
+
return null;
|
|
10706
|
+
}
|
|
10707
|
+
const value = isSignal(source) ? source() : source;
|
|
10708
|
+
if (typeof value !== 'string') {
|
|
10709
|
+
return null;
|
|
10710
|
+
}
|
|
10711
|
+
const normalised = value.trim().replace(/_/g, '-');
|
|
10712
|
+
return normalised === '' ? null : normalised;
|
|
10713
|
+
}
|
|
10714
|
+
/** The host language of {@link MESHBOARD_LANGUAGE} as a signal (`null` without one). Call in an injection context. */
|
|
10715
|
+
function injectMeshBoardLanguage() {
|
|
10716
|
+
const source = inject(MESHBOARD_LANGUAGE, { optional: true });
|
|
10717
|
+
return computed(() => readMeshBoardLanguageSource(source));
|
|
10718
|
+
}
|
|
10719
|
+
/**
|
|
10720
|
+
* Well-formed culture key of a per-language text map: a 2-3 letter language, optionally followed by
|
|
10721
|
+
* subtags (`de`, `de-AT`, `en-GB`, `zh-Hant-TW`). Keys such as `german` or `de_AT` are not culture codes.
|
|
10722
|
+
*/
|
|
10723
|
+
const CULTURE_KEY = /^[A-Za-z]{2,3}(-[A-Za-z0-9]{1,8})*$/;
|
|
10724
|
+
/** Whether `key` is a well-formed culture key of a per-language text map. */
|
|
10725
|
+
function isCultureKey(key) {
|
|
10726
|
+
return CULTURE_KEY.test(key);
|
|
10727
|
+
}
|
|
10728
|
+
/**
|
|
10729
|
+
* Picks the text for `language` from a per-language map (AB#7655). Lookup order:
|
|
10730
|
+
* 1. the exact culture or language (`de-AT`, `de`; case-insensitive);
|
|
10731
|
+
* 2. a culture (`de-AT`) without an exact key: a key equal to its language part (`de`). A culture
|
|
10732
|
+
* never picks a sibling culture (`de-DE` does not take `de-AT`);
|
|
10733
|
+
* 3. a bare language (`de`) without an exact key: the first authored key of that language
|
|
10734
|
+
* (`de-AT` in `{ 'en-GB': …, 'de-AT': …, 'de-DE': … }`), in the map's key order.
|
|
10735
|
+
* Returns `undefined` when there is no language, no map or no match, so the caller applies its own
|
|
10736
|
+
* fallback (e.g. the column `title`).
|
|
10737
|
+
*/
|
|
10738
|
+
function pickLocalizedText(texts, language) {
|
|
10739
|
+
if (!texts || !language) {
|
|
10740
|
+
return undefined;
|
|
10741
|
+
}
|
|
10742
|
+
const wanted = language.toLowerCase();
|
|
10743
|
+
const isBareLanguage = !wanted.includes('-');
|
|
10744
|
+
const languagePart = wanted.split('-')[0];
|
|
10745
|
+
let languageMatch;
|
|
10746
|
+
for (const [key, text] of Object.entries(texts)) {
|
|
10747
|
+
if (typeof text !== 'string' || text === '') {
|
|
10748
|
+
continue;
|
|
10749
|
+
}
|
|
10750
|
+
const candidate = key.toLowerCase();
|
|
10751
|
+
if (candidate === wanted) {
|
|
10752
|
+
return text;
|
|
10753
|
+
}
|
|
10754
|
+
if (languageMatch !== undefined) {
|
|
10755
|
+
continue;
|
|
10756
|
+
}
|
|
10757
|
+
if (isBareLanguage ? candidate.split('-')[0] === wanted : candidate === languagePart) {
|
|
10758
|
+
languageMatch = text;
|
|
10759
|
+
}
|
|
10760
|
+
}
|
|
10761
|
+
return languageMatch;
|
|
10762
|
+
}
|
|
10763
|
+
|
|
10764
|
+
/** An empty problems record. */
|
|
10765
|
+
function emptyLabelProblems() {
|
|
10766
|
+
return { unknownCultureKeys: [], malformedValueKeys: [], malformedTitleKeys: [], statusMappingIgnored: false };
|
|
10767
|
+
}
|
|
10768
|
+
function isPlainObject(value) {
|
|
10769
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
10770
|
+
}
|
|
10771
|
+
/**
|
|
10772
|
+
* Keeps the usable entries of a per-language text map: culture keys with a non-empty string text.
|
|
10773
|
+
* Bad culture keys are reported; the keys of entries with a non-string or empty text are returned
|
|
10774
|
+
* as `malformedKeys`.
|
|
10775
|
+
*/
|
|
10776
|
+
function cleanLocalizedTexts(texts, unknownCultureKeys) {
|
|
10777
|
+
const clean = {};
|
|
10778
|
+
const malformedKeys = [];
|
|
10779
|
+
for (const [key, text] of Object.entries(texts)) {
|
|
10780
|
+
if (!isCultureKey(key)) {
|
|
10781
|
+
unknownCultureKeys.push(key);
|
|
10782
|
+
continue;
|
|
10783
|
+
}
|
|
10784
|
+
if (typeof text !== 'string' || text === '') {
|
|
10785
|
+
malformedKeys.push(key);
|
|
10786
|
+
continue;
|
|
10787
|
+
}
|
|
10788
|
+
clean[key] = text;
|
|
10789
|
+
}
|
|
10790
|
+
return { texts: clean, malformedKeys };
|
|
10791
|
+
}
|
|
10792
|
+
/**
|
|
10793
|
+
* The header text of a column: `titles` looked up with {@link pickLocalizedText} (exact culture,
|
|
10794
|
+
* then language part; a bare language takes the first key of that language), then `title`.
|
|
10795
|
+
* Without a language (no `MESHBOARD_LANGUAGE`) it is always `title`, as before AB#7655.
|
|
10796
|
+
* Unknown culture keys and malformed entries (non-string or empty text, a `titles` that is no
|
|
10797
|
+
* object) are reported in `problems` and never matched.
|
|
10798
|
+
*/
|
|
10799
|
+
function resolveColumnTitle(column, language, problems) {
|
|
10800
|
+
const titles = column.titles;
|
|
10801
|
+
if (!isPlainObject(titles)) {
|
|
10802
|
+
if (titles !== undefined && titles !== null) {
|
|
10803
|
+
problems?.malformedTitleKeys?.push('*');
|
|
10804
|
+
}
|
|
10805
|
+
return column.title;
|
|
10806
|
+
}
|
|
10807
|
+
const { texts, malformedKeys } = cleanLocalizedTexts(titles, problems?.unknownCultureKeys ?? []);
|
|
10808
|
+
problems?.malformedTitleKeys?.push(...malformedKeys);
|
|
10809
|
+
return pickLocalizedText(texts, language) ?? column.title;
|
|
10810
|
+
}
|
|
10811
|
+
/**
|
|
10812
|
+
* Resolves a column's `valueLabels` for the current language into a lookup by raw value (`"1"`,
|
|
10813
|
+
* `"2"`, ...). A label given per language follows the language like `titles`; without a match that
|
|
10814
|
+
* entry is left out, so the cell shows the value in the column's normal rendering. Malformed
|
|
10815
|
+
* entries and unknown culture keys are ignored and reported in `problems`. Returns `null` when no
|
|
10816
|
+
* label applies for the current language (no usable `valueLabels`, or only per-language labels
|
|
10817
|
+
* without a match), so the column keeps its `dataType` / `format` rendering and the label resolver.
|
|
10818
|
+
*/
|
|
10819
|
+
function resolveValueLabels(column, language, problems) {
|
|
10820
|
+
const valueLabels = column.valueLabels;
|
|
10821
|
+
if (valueLabels === undefined || valueLabels === null) {
|
|
10822
|
+
return null;
|
|
10823
|
+
}
|
|
10824
|
+
const report = problems ?? { unknownCultureKeys: [], malformedValueKeys: [] };
|
|
10825
|
+
if (!isPlainObject(valueLabels)) {
|
|
10826
|
+
report.malformedValueKeys.push('*');
|
|
10827
|
+
return null;
|
|
10828
|
+
}
|
|
10829
|
+
const resolved = new Map();
|
|
10830
|
+
for (const [key, entry] of Object.entries(valueLabels)) {
|
|
10831
|
+
if (!isPlainObject(entry)) {
|
|
10832
|
+
report.malformedValueKeys.push(key);
|
|
10833
|
+
continue;
|
|
10834
|
+
}
|
|
10835
|
+
const { label, color } = entry;
|
|
10836
|
+
if (color !== undefined && typeof color !== 'string') {
|
|
10837
|
+
report.malformedValueKeys.push(key);
|
|
10838
|
+
continue;
|
|
10839
|
+
}
|
|
10840
|
+
let text;
|
|
10841
|
+
if (typeof label === 'string' && label !== '') {
|
|
10842
|
+
text = label;
|
|
10843
|
+
}
|
|
10844
|
+
else if (isPlainObject(label)) {
|
|
10845
|
+
const cleaned = cleanLocalizedTexts(label, report.unknownCultureKeys);
|
|
10846
|
+
if (cleaned.malformedKeys.length > 0 || Object.keys(label).length === 0) {
|
|
10847
|
+
report.malformedValueKeys.push(key);
|
|
10848
|
+
continue;
|
|
10849
|
+
}
|
|
10850
|
+
text = pickLocalizedText(cleaned.texts, language);
|
|
10851
|
+
}
|
|
10852
|
+
else {
|
|
10853
|
+
report.malformedValueKeys.push(key);
|
|
10854
|
+
continue;
|
|
10855
|
+
}
|
|
10856
|
+
if (text !== undefined) {
|
|
10857
|
+
resolved.set(key, { text, ...(color ? { color } : {}) });
|
|
10858
|
+
}
|
|
10859
|
+
}
|
|
10860
|
+
return resolved.size > 0 ? resolved : null;
|
|
10861
|
+
}
|
|
10862
|
+
/**
|
|
10863
|
+
* Text of a value-label cell whose value has no label: the value as the list view renders the
|
|
10864
|
+
* column's `dataType` - `numeric` with `format` like the `number` pipe, `iso8601` with `format`
|
|
10865
|
+
* like the `date` pipe (both with the app's `locale`), plain text through `textFormatter` (the
|
|
10866
|
+
* board's date-time formatter), anything else as string. A value the pipe cannot format shows as is.
|
|
10867
|
+
*/
|
|
10868
|
+
function formatUnlabelledValue(value, column, locale, textFormatter) {
|
|
10869
|
+
const format = column.format || undefined;
|
|
10870
|
+
try {
|
|
10871
|
+
switch (column.dataType) {
|
|
10872
|
+
case 'numeric': {
|
|
10873
|
+
const number = typeof value === 'number'
|
|
10874
|
+
? value
|
|
10875
|
+
: typeof value === 'string' && value.trim() !== '' ? Number(value) : Number.NaN;
|
|
10876
|
+
return Number.isFinite(number) ? formatNumber(number, locale, format) : String(value);
|
|
10877
|
+
}
|
|
10878
|
+
case 'iso8601':
|
|
10879
|
+
return typeof value === 'string' || typeof value === 'number' || value instanceof Date
|
|
10880
|
+
? formatDate(value, format ?? 'mediumDate', locale)
|
|
10881
|
+
: String(value);
|
|
10882
|
+
case undefined:
|
|
10883
|
+
case 'text':
|
|
10884
|
+
return textFormatter(value);
|
|
10885
|
+
default:
|
|
10886
|
+
return String(value);
|
|
10887
|
+
}
|
|
10888
|
+
}
|
|
10889
|
+
catch {
|
|
10890
|
+
return String(value);
|
|
10891
|
+
}
|
|
10892
|
+
}
|
|
10893
|
+
/** Whether a problems record holds anything worth a warning. */
|
|
10894
|
+
function hasLabelProblems(problems) {
|
|
10895
|
+
return problems.unknownCultureKeys.length > 0
|
|
10896
|
+
|| problems.malformedValueKeys.length > 0
|
|
10897
|
+
|| (problems.malformedTitleKeys?.length ?? 0) > 0
|
|
10898
|
+
|| problems.statusMappingIgnored === true;
|
|
10899
|
+
}
|
|
10900
|
+
/**
|
|
10901
|
+
* Columns the table config dialog saves (AB#7655 AC 6). The dialog edits which fields are shown,
|
|
10902
|
+
* not their presentation, so it must not erase hand-authored column properties (`titles`,
|
|
10903
|
+
* `valueLabels`, `width`, `format`, `statusMapping`, ...):
|
|
10904
|
+
* - same data source (same CK type / same persistent query): a field that stays selected keeps its
|
|
10905
|
+
* existing column object as is; a newly selected field gets a fresh column with `defaultTitle(field)`;
|
|
10906
|
+
* - persistent query: see {@link mergePersistentQueryColumns}.
|
|
10907
|
+
*/
|
|
10908
|
+
function mergeRuntimeEntityColumns(selectedFields, initialColumns, sameDataSource, defaultTitle) {
|
|
10909
|
+
const existing = new Map();
|
|
10910
|
+
if (sameDataSource) {
|
|
10911
|
+
for (const column of initialColumns ?? []) {
|
|
10912
|
+
if (column && typeof column.field === 'string' && !existing.has(column.field)) {
|
|
10913
|
+
existing.set(column.field, column);
|
|
10914
|
+
}
|
|
10915
|
+
}
|
|
10916
|
+
}
|
|
10917
|
+
return selectedFields.map(field => existing.get(field) ?? { field, title: defaultTitle(field), width: undefined });
|
|
10918
|
+
}
|
|
10919
|
+
/**
|
|
10920
|
+
* Explicit columns of a persistent-query table after the dialog saved (AB#7655):
|
|
10921
|
+
* - a different query, or no explicit columns: `[]` (columns are derived from the query at run time);
|
|
10922
|
+
* - the same query with known `queryFields` (the columns the query returns now): the explicit columns
|
|
10923
|
+
* whose field the query still returns, unchanged and in their order, followed by the query's other
|
|
10924
|
+
* fields as derived columns (`defaultTitle(field)`, like the widget derives them);
|
|
10925
|
+
* - the same query, `queryFields` unknown (`null`, empty, or the lookup failed): the explicit columns unchanged.
|
|
10926
|
+
*/
|
|
10927
|
+
function mergePersistentQueryColumns(initialColumns, sameQuery, queryFields, defaultTitle = field => field) {
|
|
10928
|
+
if (!sameQuery || !initialColumns || initialColumns.length === 0) {
|
|
10929
|
+
return [];
|
|
10930
|
+
}
|
|
10931
|
+
if (!queryFields || queryFields.length === 0) {
|
|
10932
|
+
return [...initialColumns];
|
|
10933
|
+
}
|
|
10934
|
+
const returned = new Set(queryFields);
|
|
10935
|
+
const kept = initialColumns.filter(column => column && typeof column.field === 'string' && returned.has(column.field));
|
|
10936
|
+
const keptFields = new Set(kept.map(column => column.field));
|
|
10937
|
+
const added = [...returned]
|
|
10938
|
+
.filter(field => !keptFields.has(field))
|
|
10939
|
+
.map(field => ({ field, title: defaultTitle(field) }));
|
|
10940
|
+
return [...kept, ...added];
|
|
10941
|
+
}
|
|
10942
|
+
|
|
10943
|
+
/**
|
|
10944
|
+
* Cell of a table widget column with `valueLabels` (AB#7655): plain text like every other cell,
|
|
10945
|
+
* optionally in the configured colour. Without a colour it inherits the theme text colour; an
|
|
10946
|
+
* empty text renders an empty cell.
|
|
10947
|
+
*/
|
|
10948
|
+
class TableValueLabelCellComponent {
|
|
10949
|
+
/** Text of the cell: the label, or the raw value when unmapped; `''` for null / missing. */
|
|
10950
|
+
text = '';
|
|
10951
|
+
/** Optional CSS colour of the text. */
|
|
10952
|
+
color;
|
|
10953
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: TableValueLabelCellComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
10954
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: TableValueLabelCellComponent, isStandalone: true, selector: "mm-table-value-label-cell", inputs: { text: "text", color: "color" }, ngImport: i0, template: `@if (text) {<span class="mm-table-value-label" [style.color]="color || null">{{ text }}</span>}`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
10955
|
+
}
|
|
10956
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: TableValueLabelCellComponent, decorators: [{
|
|
10957
|
+
type: Component,
|
|
10958
|
+
args: [{
|
|
10959
|
+
selector: 'mm-table-value-label-cell',
|
|
10960
|
+
standalone: true,
|
|
10961
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
10962
|
+
template: `@if (text) {<span class="mm-table-value-label" [style.color]="color || null">{{ text }}</span>}`
|
|
10963
|
+
}]
|
|
10964
|
+
}], propDecorators: { text: [{
|
|
10965
|
+
type: Input
|
|
10966
|
+
}], color: [{
|
|
10967
|
+
type: Input
|
|
10968
|
+
}] } });
|
|
10969
|
+
|
|
10475
10970
|
const ICON_MAP = {
|
|
10476
10971
|
'check-circle': checkCircleIcon,
|
|
10477
10972
|
'x-circle': xCircleIcon,
|
|
@@ -10487,6 +10982,35 @@ function resolveStatusMapping(config) {
|
|
|
10487
10982
|
{ icon: ICON_MAP[val.icon] ?? questionCircleIcon, tooltip: val.tooltip, color: val.color }
|
|
10488
10983
|
]));
|
|
10489
10984
|
}
|
|
10985
|
+
/** Per-instance fallback for the warning key of widgets without an id. */
|
|
10986
|
+
let nextTableWidgetInstance = 0;
|
|
10987
|
+
/** Short, stable hash of a text (FNV-1a, base 36) for warning keys. */
|
|
10988
|
+
function shortHash(text) {
|
|
10989
|
+
let hash = 0x811c9dc5;
|
|
10990
|
+
for (let i = 0; i < text.length; i++) {
|
|
10991
|
+
hash ^= text.charCodeAt(i);
|
|
10992
|
+
hash = Math.imul(hash, 0x01000193);
|
|
10993
|
+
}
|
|
10994
|
+
return (hash >>> 0).toString(36);
|
|
10995
|
+
}
|
|
10996
|
+
/** A row's value for a column field, read like `mm-list-view` does (flat key first, then nested path). */
|
|
10997
|
+
function readCellValue(item, field) {
|
|
10998
|
+
if (item === null || typeof item !== 'object') {
|
|
10999
|
+
return undefined;
|
|
11000
|
+
}
|
|
11001
|
+
const row = item;
|
|
11002
|
+
if (!field.includes('.') || row[field] !== undefined) {
|
|
11003
|
+
return row[field];
|
|
11004
|
+
}
|
|
11005
|
+
let value = row;
|
|
11006
|
+
for (const key of field.split('.')) {
|
|
11007
|
+
if (value === null || value === undefined || typeof value !== 'object') {
|
|
11008
|
+
return undefined;
|
|
11009
|
+
}
|
|
11010
|
+
value = value[key];
|
|
11011
|
+
}
|
|
11012
|
+
return value;
|
|
11013
|
+
}
|
|
10490
11014
|
class TableWidgetComponent {
|
|
10491
11015
|
config;
|
|
10492
11016
|
dataSource;
|
|
@@ -10498,6 +11022,27 @@ class TableWidgetComponent {
|
|
|
10498
11022
|
/** CK value type per derived column field (persistent queries), for the label resolver. */
|
|
10499
11023
|
_queryColumnValueTypes = signal(new Map(), /* @ts-ignore */
|
|
10500
11024
|
...(ngDevMode ? [{ debugName: "_queryColumnValueTypes" }] : /* istanbul ignore next */ []));
|
|
11025
|
+
/** Host UI language (`MESHBOARD_LANGUAGE`, AB#7655) for column `titles` / `valueLabels`; `null` = `title` / raw texts. */
|
|
11026
|
+
language;
|
|
11027
|
+
/**
|
|
11028
|
+
* Problems already warned about (`<widget id or instance>|<field>|<problem hash>`), so a bad config
|
|
11029
|
+
* logs once, not on every language change, while a new problem after a config edit is reported again.
|
|
11030
|
+
*/
|
|
11031
|
+
warnedColumns = new Set();
|
|
11032
|
+
/** Fallback warning key of a widget without an id. */
|
|
11033
|
+
instanceKey = `#${++nextTableWidgetInstance}`;
|
|
11034
|
+
/** App locale for the `number` / `date` rendering of unlabelled values in value-label columns. */
|
|
11035
|
+
locale = inject(LOCALE_ID);
|
|
11036
|
+
/**
|
|
11037
|
+
* Bumped on every `config` change: `config` is a plain `@Input`, so the computeds that read it
|
|
11038
|
+
* depend on this signal to pick up a new config on the same widget instance (AB#7655).
|
|
11039
|
+
*/
|
|
11040
|
+
_configVersion = signal(0, /* @ts-ignore */
|
|
11041
|
+
...(ngDevMode ? [{ debugName: "_configVersion" }] : /* istanbul ignore next */ []));
|
|
11042
|
+
constructor() {
|
|
11043
|
+
// Assigned here, not as a field initialiser: relative-import initialisers break under the Vitest runner (CLAUDE.md).
|
|
11044
|
+
this.language = injectMeshBoardLanguage();
|
|
11045
|
+
}
|
|
10501
11046
|
// Widget state signals
|
|
10502
11047
|
_isLoading = signal(false, /* @ts-ignore */
|
|
10503
11048
|
...(ngDevMode ? [{ debugName: "_isLoading" }] : /* istanbul ignore next */ []));
|
|
@@ -10530,6 +11075,8 @@ class TableWidgetComponent {
|
|
|
10530
11075
|
* For persistent queries, uses dynamically derived columns from the query response.
|
|
10531
11076
|
*/
|
|
10532
11077
|
listViewColumns = computed(() => {
|
|
11078
|
+
// A new config on the same instance recomputes the columns (titles, valueLabels, columns).
|
|
11079
|
+
this._configVersion();
|
|
10533
11080
|
// Read the board's timezone mode so datetime cells (e.g. archive
|
|
10534
11081
|
// window_start/window_end, which arrive as raw ISO-8601 strings) render on
|
|
10535
11082
|
// the same basis the time filter is computed on. Reading it here makes the
|
|
@@ -10561,30 +11108,66 @@ class TableWidgetComponent {
|
|
|
10561
11108
|
...column,
|
|
10562
11109
|
formatter: (value) => {
|
|
10563
11110
|
const text = formatTableCellValue(value, mode);
|
|
10564
|
-
|
|
11111
|
+
// String cells as before; numeric cells only when the query reports an enum value
|
|
11112
|
+
// type (numeric enum codes, AB#7655) — the resolver gets the number as string.
|
|
11113
|
+
const isEnumNumber = typeof value === 'number' && Number.isFinite(value) && !!valueType && /ENUM/i.test(valueType);
|
|
11114
|
+
if (!isEnumNumber && (typeof value !== 'string' || value === '')) {
|
|
10565
11115
|
return text;
|
|
10566
11116
|
}
|
|
10567
11117
|
return resolveMeshBoardLabel(resolver, {
|
|
10568
11118
|
kind: 'tableCell',
|
|
10569
11119
|
...(ckTypeId ? { ckTypeId } : {}),
|
|
10570
11120
|
attribute: column.field,
|
|
10571
|
-
value,
|
|
11121
|
+
value: String(value),
|
|
10572
11122
|
...(valueType ? { valueType } : {}),
|
|
10573
11123
|
defaultText: text
|
|
10574
11124
|
});
|
|
10575
11125
|
}
|
|
10576
11126
|
};
|
|
10577
11127
|
};
|
|
10578
|
-
// If explicit columns are configured, use them (works for both data source types)
|
|
11128
|
+
// If explicit columns are configured, use them (works for both data source types).
|
|
11129
|
+
// Header texts and value labels follow the host language (AB#7655); reading the
|
|
11130
|
+
// language signal here re-labels the columns without refetching data.
|
|
10579
11131
|
if (this.config?.columns && this.config.columns.length > 0) {
|
|
10580
|
-
|
|
10581
|
-
|
|
10582
|
-
|
|
10583
|
-
|
|
10584
|
-
|
|
10585
|
-
|
|
10586
|
-
|
|
10587
|
-
|
|
11132
|
+
const language = this.language();
|
|
11133
|
+
const textFormatter = (value) => formatTableCellValue(value, mode);
|
|
11134
|
+
return this.config.columns.map(col => {
|
|
11135
|
+
const problems = emptyLabelProblems();
|
|
11136
|
+
const base = {
|
|
11137
|
+
field: col.field,
|
|
11138
|
+
displayName: resolveColumnTitle(col, language, problems),
|
|
11139
|
+
dataType: (col.dataType ?? 'text'),
|
|
11140
|
+
width: col.width,
|
|
11141
|
+
...(col.format ? { format: col.format } : {}),
|
|
11142
|
+
...(col.statusMapping ? { statusMapping: resolveStatusMapping(col.statusMapping) } : {})
|
|
11143
|
+
};
|
|
11144
|
+
const labels = resolveValueLabels(col, language, problems);
|
|
11145
|
+
if (labels && (col.statusMapping || col.dataType === 'statusIcons')) {
|
|
11146
|
+
// valueLabels together with statusMapping (status icons) is not supported: the labels win.
|
|
11147
|
+
problems.statusMappingIgnored = true;
|
|
11148
|
+
}
|
|
11149
|
+
this.warnOnce(col, problems);
|
|
11150
|
+
if (!labels) {
|
|
11151
|
+
return withDateFormatter(base);
|
|
11152
|
+
}
|
|
11153
|
+
// valueLabels win over the label resolver: a plain-text cell with the label (and its optional
|
|
11154
|
+
// colour); an unmapped value keeps the column's dataType / format rendering; empty for null.
|
|
11155
|
+
return {
|
|
11156
|
+
...base,
|
|
11157
|
+
dataType: 'component',
|
|
11158
|
+
cellComponent: TableValueLabelCellComponent,
|
|
11159
|
+
cellInputs: (item) => {
|
|
11160
|
+
const value = readCellValue(item, col.field);
|
|
11161
|
+
if (value === null || value === undefined || value === '') {
|
|
11162
|
+
return { text: '' };
|
|
11163
|
+
}
|
|
11164
|
+
const label = labels.get(String(value));
|
|
11165
|
+
return label
|
|
11166
|
+
? { text: label.text, color: label.color }
|
|
11167
|
+
: { text: formatUnlabelledValue(value, col, this.locale, textFormatter) };
|
|
11168
|
+
}
|
|
11169
|
+
};
|
|
11170
|
+
});
|
|
10588
11171
|
}
|
|
10589
11172
|
// For persistent query without explicit columns, use derived columns
|
|
10590
11173
|
if (this.config?.dataSource?.type === 'persistentQuery') {
|
|
@@ -10619,6 +11202,7 @@ class TableWidgetComponent {
|
|
|
10619
11202
|
* Returns true once columns are available.
|
|
10620
11203
|
*/
|
|
10621
11204
|
columnsReady = computed(() => {
|
|
11205
|
+
this._configVersion();
|
|
10622
11206
|
if (!this.config?.dataSource)
|
|
10623
11207
|
return false;
|
|
10624
11208
|
if (this.config.dataSource.type === 'persistentQuery') {
|
|
@@ -10629,6 +11213,38 @@ class TableWidgetComponent {
|
|
|
10629
11213
|
return (this.config.columns?.length ?? 0) > 0;
|
|
10630
11214
|
}, /* @ts-ignore */
|
|
10631
11215
|
...(ngDevMode ? [{ debugName: "columnsReady" }] : /* istanbul ignore next */ []));
|
|
11216
|
+
/**
|
|
11217
|
+
* Logs one console warning per widget, column and set of problems for ignored `titles` /
|
|
11218
|
+
* `valueLabels` entries (unknown culture keys, malformed titles or value labels, `valueLabels`
|
|
11219
|
+
* together with `statusMapping`, AB#7655). The table still renders. The key carries a hash of the
|
|
11220
|
+
* problem list, so a different problem after a config edit is reported again; widgets without an
|
|
11221
|
+
* id use a per-instance key.
|
|
11222
|
+
*/
|
|
11223
|
+
warnOnce(column, problems) {
|
|
11224
|
+
if (!hasLabelProblems(problems)) {
|
|
11225
|
+
return;
|
|
11226
|
+
}
|
|
11227
|
+
const parts = [];
|
|
11228
|
+
const quoted = (keys) => keys.map(k => `"${k}"`).join(', ');
|
|
11229
|
+
if (problems.unknownCultureKeys.length > 0) {
|
|
11230
|
+
parts.push(`unknown culture keys ${quoted(problems.unknownCultureKeys)}`);
|
|
11231
|
+
}
|
|
11232
|
+
if (problems.malformedTitleKeys && problems.malformedTitleKeys.length > 0) {
|
|
11233
|
+
parts.push(`malformed titles entries ${quoted(problems.malformedTitleKeys)}`);
|
|
11234
|
+
}
|
|
11235
|
+
if (problems.malformedValueKeys.length > 0) {
|
|
11236
|
+
parts.push(`malformed valueLabels entries ${quoted(problems.malformedValueKeys)}`);
|
|
11237
|
+
}
|
|
11238
|
+
if (problems.statusMappingIgnored) {
|
|
11239
|
+
parts.push('statusMapping (valueLabels together with status icons are not supported; the labels are shown)');
|
|
11240
|
+
}
|
|
11241
|
+
const key = `${this.config?.id || this.instanceKey}|${column.field}|${shortHash(parts.join(';'))}`;
|
|
11242
|
+
if (this.warnedColumns.has(key)) {
|
|
11243
|
+
return;
|
|
11244
|
+
}
|
|
11245
|
+
this.warnedColumns.add(key);
|
|
11246
|
+
console.warn(`octo-meshboard: table widget "${this.config?.title ?? ''}" (${this.config?.id ?? ''}), column "${column.field}": ignoring ${parts.join('; ')}`);
|
|
11247
|
+
}
|
|
10632
11248
|
formatColumnTitle(field) {
|
|
10633
11249
|
return field
|
|
10634
11250
|
.replace(/([A-Z])/g, ' $1')
|
|
@@ -10636,6 +11252,9 @@ class TableWidgetComponent {
|
|
|
10636
11252
|
.trim();
|
|
10637
11253
|
}
|
|
10638
11254
|
ngOnChanges(changes) {
|
|
11255
|
+
if (changes['config']) {
|
|
11256
|
+
this._configVersion.update(version => version + 1);
|
|
11257
|
+
}
|
|
10639
11258
|
if (changes['config'] && !changes['config'].firstChange) {
|
|
10640
11259
|
// Clear cached query columns when config changes (for persistent queries)
|
|
10641
11260
|
if (this.config?.dataSource?.type === 'persistentQuery') {
|
|
@@ -10713,7 +11332,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
10713
11332
|
}
|
|
10714
11333
|
</div>
|
|
10715
11334
|
`, changeDetection: ChangeDetectionStrategy.Eager, styles: [".table-widget{height:100%;width:100%;display:flex;flex-direction:column}.no-config-overlay{display:flex;align-items:center;justify-content:center;height:100%;color:var(--kendo-color-subtle, #6c757d);font-style:italic}:host ::ng-deep mm-list-view{height:100%}:host ::ng-deep .k-grid{height:100%}\n"] }]
|
|
10716
|
-
}], propDecorators: { config: [{
|
|
11335
|
+
}], ctorParameters: () => [], propDecorators: { config: [{
|
|
10717
11336
|
type: Input
|
|
10718
11337
|
}], dataSource: [{
|
|
10719
11338
|
type: ViewChild,
|
|
@@ -10732,6 +11351,10 @@ class TableConfigDialogComponent {
|
|
|
10732
11351
|
getCkTypeAvailableQueryColumnsGQL = inject(GetCkTypeAvailableQueryColumnsDtoGQL);
|
|
10733
11352
|
stateService = inject(MeshBoardStateService);
|
|
10734
11353
|
windowRef = inject(WindowRef);
|
|
11354
|
+
getRuntimeQueryColumnsGQL = inject(GetRuntimeQueryColumnsDtoGQL);
|
|
11355
|
+
queryExecutor = inject(QueryExecutorService);
|
|
11356
|
+
/** Fields the saved query returns now, loaded once per query rtId (AB#7655, kept explicit columns). */
|
|
11357
|
+
queryFieldsLoad;
|
|
10735
11358
|
ckTypeSelectorInput;
|
|
10736
11359
|
filterEditor;
|
|
10737
11360
|
querySelector;
|
|
@@ -10949,14 +11572,48 @@ class TableConfigDialogComponent {
|
|
|
10949
11572
|
return 'No sorting configured';
|
|
10950
11573
|
return this.sorting.map(s => `${s.attributePath} ${s.sortOrder === SortOrdersDto.AscendingDto ? '↑' : '↓'}`).join(', ');
|
|
10951
11574
|
}
|
|
10952
|
-
|
|
11575
|
+
/**
|
|
11576
|
+
* The fields a persistent query returns now (`null` when unknown or the lookup failed). Runtime
|
|
11577
|
+
* queries use the metadata-only column resolver; stream-data queries execute with `first: 1`
|
|
11578
|
+
* (no column-introspection endpoint), like the chart dialogs.
|
|
11579
|
+
*/
|
|
11580
|
+
loadQueryFields(rtId) {
|
|
11581
|
+
if (this.queryFieldsLoad?.rtId === rtId) {
|
|
11582
|
+
return this.queryFieldsLoad.fields;
|
|
11583
|
+
}
|
|
11584
|
+
const fields = (async () => {
|
|
11585
|
+
try {
|
|
11586
|
+
const family = queryFamily(this.selectedPersistentQuery?.ckTypeId) ?? this.initialQueryFamily
|
|
11587
|
+
?? await this.queryExecutor.resolveFamily(rtId);
|
|
11588
|
+
if (family === 'runtime') {
|
|
11589
|
+
// network-only: the query may have changed since its columns were cached.
|
|
11590
|
+
const result = await firstValueFrom(this.getRuntimeQueryColumnsGQL.fetch({
|
|
11591
|
+
variables: { rtId },
|
|
11592
|
+
fetchPolicy: 'network-only'
|
|
11593
|
+
}));
|
|
11594
|
+
const columns = result.data?.runtime?.runtimeQuery?.items?.[0]?.columns ?? [];
|
|
11595
|
+
return columns
|
|
11596
|
+
.map(c => c?.attributePath ?? '')
|
|
11597
|
+
.filter(path => path !== '');
|
|
11598
|
+
}
|
|
11599
|
+
const sdResult = await firstValueFrom(this.queryExecutor.executeStreamData(rtId, { first: 1, forceRefresh: true }));
|
|
11600
|
+
return sdResult.columns.map(c => c.attributePath).filter(path => !!path);
|
|
11601
|
+
}
|
|
11602
|
+
catch (error) {
|
|
11603
|
+
console.error('Error loading query columns:', error);
|
|
11604
|
+
return null;
|
|
11605
|
+
}
|
|
11606
|
+
})();
|
|
11607
|
+
this.queryFieldsLoad = { rtId, fields };
|
|
11608
|
+
return fields;
|
|
11609
|
+
}
|
|
11610
|
+
async onSave() {
|
|
10953
11611
|
if (this.dataSourceType === 'runtimeEntity' && this.selectedCkType) {
|
|
10954
|
-
// Convert selected columns to TableColumn format
|
|
10955
|
-
|
|
10956
|
-
|
|
10957
|
-
|
|
10958
|
-
|
|
10959
|
-
}));
|
|
11612
|
+
// Convert selected columns to TableColumn format. Columns that stay selected on the same
|
|
11613
|
+
// CK type keep their authored properties (titles, valueLabels, width, format, ... — AB#7655).
|
|
11614
|
+
const sameDataSource = this.initialDataSourceType !== 'persistentQuery'
|
|
11615
|
+
&& !!this.initialCkTypeId && this.selectedCkType.rtCkTypeId === this.initialCkTypeId;
|
|
11616
|
+
const columns = mergeRuntimeEntityColumns(this.selectedColumns, this.initialColumns, sameDataSource, field => this.formatColumnTitle(field));
|
|
10960
11617
|
// Convert filters (remove IDs)
|
|
10961
11618
|
const filterDtos = this.filters.map(f => ({
|
|
10962
11619
|
attributePath: f.attributePath,
|
|
@@ -10982,15 +11639,24 @@ class TableConfigDialogComponent {
|
|
|
10982
11639
|
}));
|
|
10983
11640
|
// Derive family from the selected query's CK type so the runtime executor
|
|
10984
11641
|
// can route correctly without an extra lookup.
|
|
10985
|
-
const
|
|
11642
|
+
const query = this.selectedPersistentQuery;
|
|
11643
|
+
const family = queryFamily(query.ckTypeId) ?? this.initialQueryFamily ?? undefined;
|
|
11644
|
+
// Explicit (hand-authored) columns survive a save on the same query (AB#7655), minus the ones
|
|
11645
|
+
// the query no longer returns, plus the query's other columns as derived columns; a new query
|
|
11646
|
+
// starts without explicit columns (derived from the query at run time).
|
|
11647
|
+
const sameQuery = this.initialDataSourceType === 'persistentQuery' && query.rtId === this.initialQueryRtId;
|
|
11648
|
+
const queryFields = sameQuery && (this.initialColumns?.length ?? 0) > 0
|
|
11649
|
+
? await this.loadQueryFields(query.rtId)
|
|
11650
|
+
: null;
|
|
11651
|
+
const columns = mergePersistentQueryColumns(this.initialColumns, sameQuery, queryFields, field => this.formatColumnTitle(field));
|
|
10986
11652
|
this.windowRef.close({
|
|
10987
11653
|
dataSourceType: 'persistentQuery',
|
|
10988
11654
|
ckTypeId: '', // Not used for persistent query
|
|
10989
|
-
columns
|
|
11655
|
+
columns,
|
|
10990
11656
|
sorting: [],
|
|
10991
11657
|
filters: queryFilterDtos,
|
|
10992
|
-
queryRtId:
|
|
10993
|
-
queryName:
|
|
11658
|
+
queryRtId: query.rtId,
|
|
11659
|
+
queryName: query.name,
|
|
10994
11660
|
queryFamily: family,
|
|
10995
11661
|
ignoreTimeFilter: this.ignoreTimeFilter,
|
|
10996
11662
|
entitySelectorId: this.entitySelectorId,
|
|
@@ -11639,7 +12305,7 @@ class GaugeWidgetComponent {
|
|
|
11639
12305
|
// Entity-selector scope (AB#7721): pending → stay in the loading state and
|
|
11640
12306
|
// send no query (the view refreshes once the scope is known); empty → show
|
|
11641
12307
|
// no value ('-') without querying the persisted (unscoped) scope.
|
|
11642
|
-
const scope = this.stateService.resolveStreamDataScope(dataSource.entitySelectorId, dataSource.queryFamily);
|
|
12308
|
+
const scope = this.stateService.resolveStreamDataScope(dataSource.entitySelectorId, dataSource.queryFamily, dataSource.ignoreTimeFilter);
|
|
11643
12309
|
if (scope.kind === 'pending') {
|
|
11644
12310
|
return;
|
|
11645
12311
|
}
|
|
@@ -11651,7 +12317,9 @@ class GaugeWidgetComponent {
|
|
|
11651
12317
|
const streamDataArgs = this.buildStreamDataArgs();
|
|
11652
12318
|
const result = await firstValueFrom(this.queryExecutor.execute(dataSource.queryFamily, dataSource.queryRtId, {
|
|
11653
12319
|
fieldFilter: fieldFilter ?? undefined,
|
|
11654
|
-
streamDataArgs
|
|
12320
|
+
streamDataArgs,
|
|
12321
|
+
// Runtime queries: `rtId IN` field filter (AB#7725); stream data: arg.rtIds.
|
|
12322
|
+
scopeRtIds: scope.kind === 'scoped' ? scope.rtIds : undefined
|
|
11655
12323
|
}).pipe(catchError(err => {
|
|
11656
12324
|
console.error('Error loading Gauge query data:', err);
|
|
11657
12325
|
throw err;
|
|
@@ -13753,7 +14421,7 @@ class PieChartWidgetComponent {
|
|
|
13753
14421
|
// Entity-selector scope (AB#7721): pending → stay in the loading state and
|
|
13754
14422
|
// send no query (the view refreshes once the scope is known); empty → render
|
|
13755
14423
|
// the empty chart without querying the persisted (unscoped) scope.
|
|
13756
|
-
const scope = this.stateService.resolveStreamDataScope(dataSource.entitySelectorId, dataSource.queryFamily);
|
|
14424
|
+
const scope = this.stateService.resolveStreamDataScope(dataSource.entitySelectorId, dataSource.queryFamily, dataSource.ignoreTimeFilter);
|
|
13757
14425
|
if (scope.kind === 'pending') {
|
|
13758
14426
|
return;
|
|
13759
14427
|
}
|
|
@@ -13769,7 +14437,9 @@ class PieChartWidgetComponent {
|
|
|
13769
14437
|
const streamDataArgs = this.buildStreamDataArgs();
|
|
13770
14438
|
const result = await firstValueFrom(this.queryExecutor.execute(dataSource.queryFamily, dataSource.queryRtId, {
|
|
13771
14439
|
fieldFilter: fieldFilter ?? undefined,
|
|
13772
|
-
streamDataArgs
|
|
14440
|
+
streamDataArgs,
|
|
14441
|
+
// Runtime queries: `rtId IN` field filter (AB#7725); stream data: arg.rtIds.
|
|
14442
|
+
scopeRtIds: scope.kind === 'scoped' ? scope.rtIds : undefined
|
|
13773
14443
|
}).pipe(catchError(err => {
|
|
13774
14444
|
console.error('Error loading Pie Chart data:', err);
|
|
13775
14445
|
throw err;
|
|
@@ -14979,7 +15649,7 @@ class BarChartWidgetComponent {
|
|
|
14979
15649
|
// Entity-selector scope (AB#7721): pending → stay in the loading state and
|
|
14980
15650
|
// send no query (the view refreshes once the scope is known); empty → render
|
|
14981
15651
|
// the empty chart without querying the persisted (unscoped) scope.
|
|
14982
|
-
const scope = this.stateService.resolveStreamDataScope(queryDataSource.entitySelectorId, queryDataSource.queryFamily);
|
|
15652
|
+
const scope = this.stateService.resolveStreamDataScope(queryDataSource.entitySelectorId, queryDataSource.queryFamily, queryDataSource.ignoreTimeFilter);
|
|
14983
15653
|
if (scope.kind === 'pending') {
|
|
14984
15654
|
return;
|
|
14985
15655
|
}
|
|
@@ -14988,7 +15658,9 @@ class BarChartWidgetComponent {
|
|
|
14988
15658
|
? emptyQueryExecutionResult(queryDataSource.queryFamily)
|
|
14989
15659
|
: await firstValueFrom(this.queryExecutor.execute(queryDataSource.queryFamily, queryDataSource.queryRtId, {
|
|
14990
15660
|
fieldFilter: fieldFilter ?? undefined,
|
|
14991
|
-
streamDataArgs
|
|
15661
|
+
streamDataArgs,
|
|
15662
|
+
// Runtime queries: `rtId IN` field filter (AB#7725); stream data: arg.rtIds.
|
|
15663
|
+
scopeRtIds: scope.kind === 'scoped' ? scope.rtIds : undefined
|
|
14992
15664
|
}).pipe(catchError(err => {
|
|
14993
15665
|
console.error('Error loading Bar Chart data:', err);
|
|
14994
15666
|
throw err;
|
|
@@ -16540,7 +17212,7 @@ class LineChartWidgetComponent {
|
|
|
16540
17212
|
// Entity-selector scope (AB#7721): pending → stay in the loading state and
|
|
16541
17213
|
// send no query (the view refreshes once the scope is known); empty → render
|
|
16542
17214
|
// the empty chart without querying the persisted (unscoped) scope.
|
|
16543
|
-
const scope = this.stateService.resolveStreamDataScope(queryDataSource.entitySelectorId, queryDataSource.queryFamily);
|
|
17215
|
+
const scope = this.stateService.resolveStreamDataScope(queryDataSource.entitySelectorId, queryDataSource.queryFamily, queryDataSource.ignoreTimeFilter);
|
|
16544
17216
|
if (scope.kind === 'pending') {
|
|
16545
17217
|
return;
|
|
16546
17218
|
}
|
|
@@ -16580,7 +17252,9 @@ class LineChartWidgetComponent {
|
|
|
16580
17252
|
this.lastLimit = streamDataArgs?.limit ?? 0;
|
|
16581
17253
|
const result = await firstValueFrom(this.queryExecutor.execute(queryDataSource.queryFamily, queryDataSource.queryRtId, {
|
|
16582
17254
|
fieldFilter: fieldFilter ?? undefined,
|
|
16583
|
-
streamDataArgs
|
|
17255
|
+
streamDataArgs,
|
|
17256
|
+
// Runtime queries: `rtId IN` field filter (AB#7725); stream data: arg.rtIds.
|
|
17257
|
+
scopeRtIds: scope.kind === 'scoped' ? scope.rtIds : undefined
|
|
16584
17258
|
}).pipe(catchError(err => {
|
|
16585
17259
|
console.error('Error loading Line Chart data:', err);
|
|
16586
17260
|
throw err;
|
|
@@ -16796,7 +17470,7 @@ class LineChartWidgetComponent {
|
|
|
16796
17470
|
// as UTC — a bare `new Date(...)` would reinterpret it as browser-local time and shift
|
|
16797
17471
|
// the axis by the UTC offset (AB#4818).
|
|
16798
17472
|
if (!allCategories.has(categoryValue)) {
|
|
16799
|
-
allCategories.set(categoryValue, toInstant(categoryValue) ?? new Date(NaN));
|
|
17473
|
+
allCategories.set(categoryValue, toInstant$1(categoryValue) ?? new Date(NaN));
|
|
16800
17474
|
}
|
|
16801
17475
|
allSeriesGroups.add(seriesGroupValue);
|
|
16802
17476
|
if (!dataMap.has(categoryValue)) {
|
|
@@ -18136,7 +18810,7 @@ class HeatmapWidgetComponent {
|
|
|
18136
18810
|
// Entity-selector scope (AB#7721): pending → stay in the loading state and
|
|
18137
18811
|
// send no query (the view refreshes once the scope is known); empty → render
|
|
18138
18812
|
// the empty grid without querying the persisted (unscoped) scope.
|
|
18139
|
-
const scope = this.stateService.resolveStreamDataScope(queryDataSource.entitySelectorId, queryDataSource.queryFamily);
|
|
18813
|
+
const scope = this.stateService.resolveStreamDataScope(queryDataSource.entitySelectorId, queryDataSource.queryFamily, queryDataSource.ignoreTimeFilter);
|
|
18140
18814
|
if (scope.kind === 'pending') {
|
|
18141
18815
|
return;
|
|
18142
18816
|
}
|
|
@@ -18145,7 +18819,9 @@ class HeatmapWidgetComponent {
|
|
|
18145
18819
|
? emptyQueryExecutionResult(queryDataSource.queryFamily)
|
|
18146
18820
|
: await firstValueFrom(this.queryExecutor.execute(queryDataSource.queryFamily, queryDataSource.queryRtId, {
|
|
18147
18821
|
fieldFilter: fieldFilter ?? undefined,
|
|
18148
|
-
streamDataArgs
|
|
18822
|
+
streamDataArgs,
|
|
18823
|
+
// Runtime queries: `rtId IN` field filter (AB#7725); stream data: arg.rtIds.
|
|
18824
|
+
scopeRtIds: scope.kind === 'scoped' ? scope.rtIds : undefined
|
|
18149
18825
|
}).pipe(catchError(err => {
|
|
18150
18826
|
console.error('Error loading Heatmap data:', err);
|
|
18151
18827
|
throw err;
|
|
@@ -18413,7 +19089,7 @@ class HeatmapWidgetComponent {
|
|
|
18413
19089
|
parseDate(value) {
|
|
18414
19090
|
if (!value)
|
|
18415
19091
|
return null;
|
|
18416
|
-
return toInstant(value);
|
|
19092
|
+
return toInstant$1(value);
|
|
18417
19093
|
}
|
|
18418
19094
|
convertFiltersToDto(filters) {
|
|
18419
19095
|
const variables = this.stateService.getVariables();
|
|
@@ -19530,15 +20206,19 @@ class StatsGridWidgetComponent {
|
|
|
19530
20206
|
return aggregationResults.get(stat.queryId) ?? null;
|
|
19531
20207
|
}
|
|
19532
20208
|
async loadPersistentQueryValue(source) {
|
|
19533
|
-
//
|
|
19534
|
-
// query — never the persisted (unscoped) scope.
|
|
19535
|
-
|
|
20209
|
+
// Pending or empty scope (still resolving, nothing in scope or lookup failed,
|
|
20210
|
+
// AB#7721/AB#7725): no value, no query — never the persisted (unscoped) scope.
|
|
20211
|
+
// The view refreshes the widget once a pending scope is resolved.
|
|
20212
|
+
const scope = this.scopeOf(source);
|
|
20213
|
+
if (scope.kind === 'pending' || scope.kind === 'empty') {
|
|
19536
20214
|
return null;
|
|
19537
20215
|
}
|
|
19538
20216
|
try {
|
|
19539
20217
|
const result = await firstValueFrom(this.queryExecutor.execute(source.queryFamily, source.queryRtId, {
|
|
19540
20218
|
fieldFilter: this.convertFiltersToDto(source.filters),
|
|
19541
|
-
streamDataArgs: this.buildStreamDataArgs(source)
|
|
20219
|
+
streamDataArgs: this.buildStreamDataArgs(source),
|
|
20220
|
+
// Runtime queries: `rtId IN` field filter (AB#7725); stream data: arg.rtIds.
|
|
20221
|
+
scopeRtIds: scope.kind === 'scoped' ? scope.rtIds : undefined
|
|
19542
20222
|
}));
|
|
19543
20223
|
return extractPersistentQueryCellValue(result, source);
|
|
19544
20224
|
}
|
|
@@ -19548,7 +20228,7 @@ class StatsGridWidgetComponent {
|
|
|
19548
20228
|
}
|
|
19549
20229
|
}
|
|
19550
20230
|
scopeOf(source) {
|
|
19551
|
-
return this.stateService.resolveStreamDataScope(source.entitySelectorId, source.queryFamily);
|
|
20231
|
+
return this.stateService.resolveStreamDataScope(source.entitySelectorId, source.queryFamily, source.ignoreTimeFilter);
|
|
19552
20232
|
}
|
|
19553
20233
|
buildStreamDataArgs(source) {
|
|
19554
20234
|
const timeArgs = this.stateService.resolveStreamDataTimeArgs(source.ignoreTimeFilter);
|
|
@@ -19721,7 +20401,7 @@ class PersistentQueryCellEditorComponent {
|
|
|
19721
20401
|
</kendo-combobox>
|
|
19722
20402
|
</div>
|
|
19723
20403
|
|
|
19724
|
-
<!--
|
|
20404
|
+
<!-- Time filter: stream data only. Scope binding: both families (AB#7725). -->
|
|
19725
20405
|
<mm-sd-time-filter-toggle
|
|
19726
20406
|
[family]="family"
|
|
19727
20407
|
[(ignoreTimeFilter)]="ignoreTimeFilter"
|
|
@@ -19786,7 +20466,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
19786
20466
|
</kendo-combobox>
|
|
19787
20467
|
</div>
|
|
19788
20468
|
|
|
19789
|
-
<!--
|
|
20469
|
+
<!-- Time filter: stream data only. Scope binding: both families (AB#7725). -->
|
|
19790
20470
|
<mm-sd-time-filter-toggle
|
|
19791
20471
|
[family]="family"
|
|
19792
20472
|
[(ignoreTimeFilter)]="ignoreTimeFilter"
|
|
@@ -23229,19 +23909,23 @@ class SummaryCardWidgetComponent {
|
|
|
23229
23909
|
return null;
|
|
23230
23910
|
}
|
|
23231
23911
|
async fetchPersistentQueryValue(source) {
|
|
23232
|
-
//
|
|
23233
|
-
// query — never the persisted (unscoped) scope.
|
|
23234
|
-
|
|
23912
|
+
// Pending or empty scope (still resolving, nothing in scope or lookup failed,
|
|
23913
|
+
// AB#7721/AB#7725): no value, no query — never the persisted (unscoped) scope.
|
|
23914
|
+
// The view refreshes the widget once a pending scope is resolved.
|
|
23915
|
+
const scope = this.scopeOf(source);
|
|
23916
|
+
if (scope.kind === 'pending' || scope.kind === 'empty') {
|
|
23235
23917
|
return null;
|
|
23236
23918
|
}
|
|
23237
23919
|
const result = await firstValueFrom(this.queryExecutor.execute(source.queryFamily, source.queryRtId, {
|
|
23238
23920
|
fieldFilter: this.convertFiltersToDto(source.filters),
|
|
23239
|
-
streamDataArgs: this.buildStreamDataArgs(source)
|
|
23921
|
+
streamDataArgs: this.buildStreamDataArgs(source),
|
|
23922
|
+
// Runtime queries: `rtId IN` field filter (AB#7725); stream data: arg.rtIds.
|
|
23923
|
+
scopeRtIds: scope.kind === 'scoped' ? scope.rtIds : undefined
|
|
23240
23924
|
}));
|
|
23241
23925
|
return extractPersistentQueryCellValue(result, source);
|
|
23242
23926
|
}
|
|
23243
23927
|
scopeOf(source) {
|
|
23244
|
-
return this.stateService.resolveStreamDataScope(source.entitySelectorId, source.queryFamily);
|
|
23928
|
+
return this.stateService.resolveStreamDataScope(source.entitySelectorId, source.queryFamily, source.ignoreTimeFilter);
|
|
23245
23929
|
}
|
|
23246
23930
|
buildStreamDataArgs(source) {
|
|
23247
23931
|
const timeArgs = this.stateService.resolveStreamDataTimeArgs(source.ignoreTimeFilter);
|
|
@@ -24753,6 +25437,13 @@ class ProcessDataService {
|
|
|
24753
25437
|
if (!queryRtId) {
|
|
24754
25438
|
throw new Error('Persistent query binding requires queryRtId');
|
|
24755
25439
|
}
|
|
25440
|
+
// AB#7582: while the embedding host's period is invalid, a stream-data query
|
|
25441
|
+
// bound to the time filter is not sent (it would read the whole archive); the
|
|
25442
|
+
// binding gets no value. Runtime queries are unaffected.
|
|
25443
|
+
if (this.stateService.isStreamDataBlockedByHostTimeRange()
|
|
25444
|
+
&& await this.queryExecutor.resolveFamily(queryRtId) === 'streamData') {
|
|
25445
|
+
return undefined;
|
|
25446
|
+
}
|
|
24756
25447
|
// Execute via the unified executor (family resolved lazily). The active time
|
|
24757
25448
|
// filter is applied by default for stream-data queries; runtime queries
|
|
24758
25449
|
// ignore it. The element binding has no asset-scope, so no rtIds override.
|
|
@@ -24953,7 +25644,7 @@ class ProcessDataService {
|
|
|
24953
25644
|
// or empty — never the persisted (unscoped) scope. Pending → a `pending` result
|
|
24954
25645
|
// (the widget stays loading; a refresh follows once the scope resolves);
|
|
24955
25646
|
// empty → no rows.
|
|
24956
|
-
const scope = this.stateService.resolveStreamDataScope(config.bindingEntitySelectorId, bindingQueryFamily);
|
|
25647
|
+
const scope = this.stateService.resolveStreamDataScope(config.bindingEntitySelectorId, bindingQueryFamily, config.bindingIgnoreTimeFilter);
|
|
24957
25648
|
if (scope.kind === 'pending') {
|
|
24958
25649
|
return { type: 'query', pending: true, loadedAt: new Date() };
|
|
24959
25650
|
}
|
|
@@ -24973,7 +25664,9 @@ class ProcessDataService {
|
|
|
24973
25664
|
const result = await firstValueFrom(this.queryExecutor.execute(bindingQueryFamily, bindingQueryRtId, {
|
|
24974
25665
|
first: 1000, // Reasonable limit for process diagram data
|
|
24975
25666
|
fieldFilter: fieldFilters,
|
|
24976
|
-
streamDataArgs: this.buildBindingStreamDataArgs(config)
|
|
25667
|
+
streamDataArgs: this.buildBindingStreamDataArgs(config),
|
|
25668
|
+
// Runtime queries: `rtId IN` field filter (AB#7725); stream data: arg.rtIds.
|
|
25669
|
+
scopeRtIds: scope.kind === 'scoped' ? scope.rtIds : undefined
|
|
24977
25670
|
}));
|
|
24978
25671
|
return {
|
|
24979
25672
|
type: 'query',
|
|
@@ -30592,7 +31285,8 @@ function registerDefaultWidgets(registry) {
|
|
|
30592
31285
|
return {
|
|
30593
31286
|
...widget,
|
|
30594
31287
|
dataSource,
|
|
30595
|
-
columns
|
|
31288
|
+
// Explicit columns the dialog kept (same query, AB#7655); [] = derived from the query at runtime
|
|
31289
|
+
columns: result.columns ?? [],
|
|
30596
31290
|
sorting: [],
|
|
30597
31291
|
filters,
|
|
30598
31292
|
pageSize: result.pageSize,
|
|
@@ -32279,6 +32973,9 @@ function provideMeshBoard(options) {
|
|
|
32279
32973
|
if (opts.messages) {
|
|
32280
32974
|
providers.push({ provide: MESHBOARD_MESSAGES, useValue: opts.messages });
|
|
32281
32975
|
}
|
|
32976
|
+
if (opts.language) {
|
|
32977
|
+
providers.push({ provide: MESHBOARD_LANGUAGE, useValue: opts.language });
|
|
32978
|
+
}
|
|
32282
32979
|
// Include default widgets unless explicitly disabled
|
|
32283
32980
|
if (opts.includeDefaultWidgets !== false) {
|
|
32284
32981
|
providers.push(provideAppInitializer(() => {
|
|
@@ -32295,6 +32992,13 @@ function provideMeshBoard(options) {
|
|
|
32295
32992
|
function provideMeshBoardMessages(messages) {
|
|
32296
32993
|
return makeEnvironmentProviders([{ provide: MESHBOARD_MESSAGES, useValue: messages }]);
|
|
32297
32994
|
}
|
|
32995
|
+
/**
|
|
32996
|
+
* Provides the host's UI language (`MESHBOARD_LANGUAGE`, AB#7655) without `provideMeshBoard()` —
|
|
32997
|
+
* for apps that register the widgets lazily. A string or a signal (the widgets follow it).
|
|
32998
|
+
*/
|
|
32999
|
+
function provideMeshBoardLanguage(language) {
|
|
33000
|
+
return makeEnvironmentProviders([{ provide: MESHBOARD_LANGUAGE, useValue: language }]);
|
|
33001
|
+
}
|
|
32298
33002
|
/**
|
|
32299
33003
|
* Provider function for registering custom widgets.
|
|
32300
33004
|
* Use this to add your own widget types to MeshBoard.
|
|
@@ -32426,6 +33130,506 @@ function labelList(selectors) {
|
|
|
32426
33130
|
return `${labels.slice(0, -1).join(', ')} and ${labels[labels.length - 1]}`;
|
|
32427
33131
|
}
|
|
32428
33132
|
|
|
33133
|
+
/** Turns a selector's child scope into its resolution plan. */
|
|
33134
|
+
function scopePlan(childScope) {
|
|
33135
|
+
if (!childScope) {
|
|
33136
|
+
return { kind: 'none' };
|
|
33137
|
+
}
|
|
33138
|
+
const path = childScope.path;
|
|
33139
|
+
if (Array.isArray(path) && path.length > 0) {
|
|
33140
|
+
if (path.length > MAX_SCOPE_HOPS) {
|
|
33141
|
+
return { kind: 'invalid', reason: `the path has ${path.length} hops (at most ${MAX_SCOPE_HOPS})` };
|
|
33142
|
+
}
|
|
33143
|
+
const hops = [];
|
|
33144
|
+
for (let i = 0; i < path.length; i++) {
|
|
33145
|
+
const hop = normalizeHop(path[i]);
|
|
33146
|
+
if (!hop) {
|
|
33147
|
+
return { kind: 'invalid', reason: `hop ${i + 1} needs an association role and a target type` };
|
|
33148
|
+
}
|
|
33149
|
+
hops.push(hop);
|
|
33150
|
+
}
|
|
33151
|
+
return { kind: 'path', hops };
|
|
33152
|
+
}
|
|
33153
|
+
// Original one-hop form; incomplete top-level fields mean "no child scope",
|
|
33154
|
+
// exactly as before AB#7723.
|
|
33155
|
+
if (!childScope.targetCkTypeId || !childScope.roleId) {
|
|
33156
|
+
return { kind: 'none' };
|
|
33157
|
+
}
|
|
33158
|
+
return {
|
|
33159
|
+
kind: 'single',
|
|
33160
|
+
hop: {
|
|
33161
|
+
roleId: childScope.roleId,
|
|
33162
|
+
direction: childScope.direction ?? 'in',
|
|
33163
|
+
targetCkTypeId: childScope.targetCkTypeId
|
|
33164
|
+
}
|
|
33165
|
+
};
|
|
33166
|
+
}
|
|
33167
|
+
/** The hops of a child scope as the editor shows them (one-hop form = one hop). */
|
|
33168
|
+
function scopeHopsOf(childScope) {
|
|
33169
|
+
if (!childScope) {
|
|
33170
|
+
return [];
|
|
33171
|
+
}
|
|
33172
|
+
if (Array.isArray(childScope.path) && childScope.path.length > 0) {
|
|
33173
|
+
return childScope.path.map(h => ({ roleId: h.roleId, direction: h.direction ?? 'in', targetCkTypeId: h.targetCkTypeId }));
|
|
33174
|
+
}
|
|
33175
|
+
// Incomplete top-level fields are "no child scope" at runtime; show them as none.
|
|
33176
|
+
if (childScope.targetCkTypeId && childScope.roleId) {
|
|
33177
|
+
return [{ roleId: childScope.roleId, direction: childScope.direction ?? 'in', targetCkTypeId: childScope.targetCkTypeId }];
|
|
33178
|
+
}
|
|
33179
|
+
return [];
|
|
33180
|
+
}
|
|
33181
|
+
/**
|
|
33182
|
+
* Editor validation of a hop list: the message for the first hop without role
|
|
33183
|
+
* or target type, or for more than {@link MAX_SCOPE_HOPS} hops; `null` when the
|
|
33184
|
+
* list can be saved (an empty list = no child scope).
|
|
33185
|
+
*/
|
|
33186
|
+
function scopeHopsValidationError(hops) {
|
|
33187
|
+
if (hops.length > MAX_SCOPE_HOPS) {
|
|
33188
|
+
return `At most ${MAX_SCOPE_HOPS} hops.`;
|
|
33189
|
+
}
|
|
33190
|
+
const index = hops.findIndex(h => !h.roleId?.trim() || !h.targetCkTypeId?.trim());
|
|
33191
|
+
return index < 0 ? null : `Hop ${index + 1} needs an association and a target type.`;
|
|
33192
|
+
}
|
|
33193
|
+
/**
|
|
33194
|
+
* The child scope the editor saves for a list of hops: none for zero hops, the
|
|
33195
|
+
* original top-level form for one hop (older library versions keep reading
|
|
33196
|
+
* it), and `path` only for two or more hops. Roles are trimmed.
|
|
33197
|
+
*/
|
|
33198
|
+
function childScopeFromHops(hops) {
|
|
33199
|
+
const clean = hops.map(h => ({
|
|
33200
|
+
roleId: h.roleId.trim(),
|
|
33201
|
+
direction: h.direction ?? 'in',
|
|
33202
|
+
targetCkTypeId: h.targetCkTypeId.trim()
|
|
33203
|
+
}));
|
|
33204
|
+
if (clean.length === 0) {
|
|
33205
|
+
return undefined;
|
|
33206
|
+
}
|
|
33207
|
+
if (clean.length === 1) {
|
|
33208
|
+
const [hop] = clean;
|
|
33209
|
+
return { targetCkTypeId: hop.targetCkTypeId, roleId: hop.roleId, direction: hop.direction };
|
|
33210
|
+
}
|
|
33211
|
+
return { path: clean };
|
|
33212
|
+
}
|
|
33213
|
+
/**
|
|
33214
|
+
* Resolves a scope path hop by hop from the picked entity: each hop is ONE
|
|
33215
|
+
* batched step over all entities the previous hop reached (the fetcher decides
|
|
33216
|
+
* how to batch), the source type of hop n is the target type of hop n-1, and
|
|
33217
|
+
* the result is the distinct rtIds of the last hop.
|
|
33218
|
+
*
|
|
33219
|
+
* - An empty hop ends the resolution: no further hop is requested and the
|
|
33220
|
+
* scope is empty (AB#7721 empty-scope rule).
|
|
33221
|
+
* - A failing hop rejects; the caller turns that into a failed scope.
|
|
33222
|
+
* - `isCurrent` is checked after every hop: when a newer pick has started, the
|
|
33223
|
+
* resolution stops and returns `null` (stale — must not be applied).
|
|
33224
|
+
*/
|
|
33225
|
+
async function resolveScopePath(pickedRtId, pickedCkTypeId, hops, fetchHop, isCurrent = () => true) {
|
|
33226
|
+
let sources = [pickedRtId];
|
|
33227
|
+
let sourceCkTypeId = pickedCkTypeId;
|
|
33228
|
+
for (const hop of hops) {
|
|
33229
|
+
const targets = await fetchHop(sources, sourceCkTypeId, hop);
|
|
33230
|
+
if (!isCurrent()) {
|
|
33231
|
+
return null;
|
|
33232
|
+
}
|
|
33233
|
+
sources = distinctRtIds(targets);
|
|
33234
|
+
if (sources.length === 0) {
|
|
33235
|
+
return [];
|
|
33236
|
+
}
|
|
33237
|
+
sourceCkTypeId = hop.targetCkTypeId;
|
|
33238
|
+
}
|
|
33239
|
+
return sources;
|
|
33240
|
+
}
|
|
33241
|
+
/** Non-empty rtIds in first-seen order, without duplicates. */
|
|
33242
|
+
function distinctRtIds(rtIds) {
|
|
33243
|
+
const seen = new Set();
|
|
33244
|
+
for (const id of rtIds) {
|
|
33245
|
+
const value = id === null || id === undefined ? '' : String(id);
|
|
33246
|
+
if (value.length > 0) {
|
|
33247
|
+
seen.add(value);
|
|
33248
|
+
}
|
|
33249
|
+
}
|
|
33250
|
+
return [...seen];
|
|
33251
|
+
}
|
|
33252
|
+
/** "Test/Site → Test/System → Test/Device" for the editor's path line. */
|
|
33253
|
+
function scopePathLabel(startCkTypeId, hops) {
|
|
33254
|
+
return [startCkTypeId, ...hops.map(h => h.targetCkTypeId || '?')].join(' → ');
|
|
33255
|
+
}
|
|
33256
|
+
function normalizeHop(hop) {
|
|
33257
|
+
const roleId = typeof hop?.roleId === 'string' ? hop.roleId.trim() : '';
|
|
33258
|
+
const targetCkTypeId = typeof hop?.targetCkTypeId === 'string' ? hop.targetCkTypeId.trim() : '';
|
|
33259
|
+
if (!roleId || !targetCkTypeId) {
|
|
33260
|
+
return null;
|
|
33261
|
+
}
|
|
33262
|
+
return { roleId, direction: hop?.direction === 'out' ? 'out' : 'in', targetCkTypeId };
|
|
33263
|
+
}
|
|
33264
|
+
|
|
33265
|
+
/**
|
|
33266
|
+
* Picker filter of an entity selector (AB#7724).
|
|
33267
|
+
*
|
|
33268
|
+
* `EntitySelectorConfig.filters` narrows what the selector's pickers list. The
|
|
33269
|
+
* rows are converted exactly like widget filters (variables substituted, AND)
|
|
33270
|
+
* and sent as GraphQL `fieldFilter` of the `runtimeEntities` query, so the
|
|
33271
|
+
* server filters — the picker never loads all entities of the type. Pure
|
|
33272
|
+
* helpers, shared by the toolbar (runtime) and the selector editor.
|
|
33273
|
+
*/
|
|
33274
|
+
/** True when the selector carries at least one filter row that applies (never for followers). */
|
|
33275
|
+
function hasPickerFilter(selector) {
|
|
33276
|
+
return !isFollowerSelector(selector) && (selector.filters?.length ?? 0) > 0;
|
|
33277
|
+
}
|
|
33278
|
+
/**
|
|
33279
|
+
* The GraphQL field filters of the selector's picker, with board variables
|
|
33280
|
+
* substituted; `undefined` when the selector has no filter or is a follower
|
|
33281
|
+
* (a follower has no picker and ignores a filter).
|
|
33282
|
+
*/
|
|
33283
|
+
function pickerFieldFilters(selector, variables, variableService) {
|
|
33284
|
+
if (!hasPickerFilter(selector)) {
|
|
33285
|
+
return undefined;
|
|
33286
|
+
}
|
|
33287
|
+
return variableService.convertToFieldFilterDto(selector.filters, variables);
|
|
33288
|
+
}
|
|
33289
|
+
/**
|
|
33290
|
+
* Wraps the picker query so every fetch carries `baseFilters` in front of the
|
|
33291
|
+
* fetch's own field filters (the text search of the dropdown and the dialog).
|
|
33292
|
+
* GraphQL combines field filters with AND, so the search works on top of the
|
|
33293
|
+
* selector filter. Without base filters the original query is returned
|
|
33294
|
+
* unchanged — an unfiltered picker sends exactly the query it sent before.
|
|
33295
|
+
*
|
|
33296
|
+
* The wrapper inherits from the original service object, so it is usable
|
|
33297
|
+
* wherever the picker data sources (octo-services) expect the query service.
|
|
33298
|
+
*/
|
|
33299
|
+
function withPickerFilters(gql, baseFilters) {
|
|
33300
|
+
if (!baseFilters?.length) {
|
|
33301
|
+
return gql;
|
|
33302
|
+
}
|
|
33303
|
+
const wrapped = Object.create(gql);
|
|
33304
|
+
wrapped.fetch = ((options) => {
|
|
33305
|
+
const variables = options?.variables;
|
|
33306
|
+
const own = variables?.fieldFilters;
|
|
33307
|
+
const ownList = own === undefined || own === null ? [] : Array.isArray(own) ? own : [own];
|
|
33308
|
+
return gql.fetch({
|
|
33309
|
+
...options,
|
|
33310
|
+
variables: { ...variables, fieldFilters: [...baseFilters, ...ownList] }
|
|
33311
|
+
});
|
|
33312
|
+
});
|
|
33313
|
+
return wrapped;
|
|
33314
|
+
}
|
|
33315
|
+
/**
|
|
33316
|
+
* Variable names a filter row still references after substitution — a board
|
|
33317
|
+
* variable without a value. Such a value is sent literally, exactly like in
|
|
33318
|
+
* widget filters (`MeshBoardVariableService.resolveVariables` keeps the
|
|
33319
|
+
* placeholder), so it usually matches nothing; the picker names the variable
|
|
33320
|
+
* in its empty-list text instead of showing an unexplained empty list.
|
|
33321
|
+
*/
|
|
33322
|
+
function unresolvedFilterVariables(filters) {
|
|
33323
|
+
const names = new Set();
|
|
33324
|
+
for (const filter of filters ?? []) {
|
|
33325
|
+
const value = typeof filter?.comparisonValue === 'string' ? filter.comparisonValue : '';
|
|
33326
|
+
for (const match of value.matchAll(/\$\{([^}]+)\}|\$([a-zA-Z_]\w*)/g)) {
|
|
33327
|
+
names.add(match[1] ?? match[2]);
|
|
33328
|
+
}
|
|
33329
|
+
}
|
|
33330
|
+
return [...names];
|
|
33331
|
+
}
|
|
33332
|
+
/**
|
|
33333
|
+
* Empty-list text of a filtered picker (wording agreed with the Studio PO):
|
|
33334
|
+
* the search alone may have found nothing, so it names the filter as a
|
|
33335
|
+
* possible reason without claiming it is the reason.
|
|
33336
|
+
*/
|
|
33337
|
+
function filteredNoEntitiesMessage(label, unresolvedVariables = []) {
|
|
33338
|
+
const name = label || 'entity';
|
|
33339
|
+
const base = `No ${name} found. This board only lists ${name} that match its filter.`;
|
|
33340
|
+
if (unresolvedVariables.length === 0) {
|
|
33341
|
+
return base;
|
|
33342
|
+
}
|
|
33343
|
+
const vars = unresolvedVariables.map(v => `$${v}`).join(', ');
|
|
33344
|
+
return `${base} The filter uses ${vars}, which has no value yet.`;
|
|
33345
|
+
}
|
|
33346
|
+
/** Title of the picker's search dialog when a filter is set. */
|
|
33347
|
+
function filteredDialogTitle(label) {
|
|
33348
|
+
return `Select ${label} (filtered)`;
|
|
33349
|
+
}
|
|
33350
|
+
|
|
33351
|
+
/** Key of the "Other role…" entry: free-text role and direction. */
|
|
33352
|
+
const OTHER_ASSOCIATION = '__other__';
|
|
33353
|
+
/**
|
|
33354
|
+
* Hop list of an entity selector's child scope (AB#7723; wireframe agreed by
|
|
33355
|
+
* Freya, AB#7723 comment 6625315). Each hop picks an association of its source
|
|
33356
|
+
* type from the CK metadata (role + direction + base target in one entry, or
|
|
33357
|
+
* "Other role…" for free text) and a target type restricted to types derived
|
|
33358
|
+
* from the association's target. The source type of hop 1 is the selector's
|
|
33359
|
+
* CK type, of every later hop the previous hop's target type. Hops can be
|
|
33360
|
+
* added (at most {@link MAX_SCOPE_HOPS}), removed and moved up/down.
|
|
33361
|
+
*
|
|
33362
|
+
* Two-way bindable; emits the hops as configured (validation is the host's,
|
|
33363
|
+
* see `scopeHopsValidationError`):
|
|
33364
|
+
* ```html
|
|
33365
|
+
* <mm-entity-selector-scope-path-editor
|
|
33366
|
+
* [sourceCkTypeId]="ckTypeId"
|
|
33367
|
+
* [(hops)]="hops">
|
|
33368
|
+
* </mm-entity-selector-scope-path-editor>
|
|
33369
|
+
* ```
|
|
33370
|
+
*/
|
|
33371
|
+
class EntitySelectorScopePathEditorComponent {
|
|
33372
|
+
rolesGQL = inject(GetCkTypeAssociationRolesDtoGQL);
|
|
33373
|
+
cdr = inject(ChangeDetectorRef);
|
|
33374
|
+
hopsChange = new EventEmitter();
|
|
33375
|
+
hopCards;
|
|
33376
|
+
rows = [];
|
|
33377
|
+
plusIcon = plusIcon;
|
|
33378
|
+
arrowUpIcon = arrowUpIcon;
|
|
33379
|
+
arrowDownIcon = arrowDownIcon;
|
|
33380
|
+
trashIcon = MM_ACTION_ICONS.delete;
|
|
33381
|
+
_sourceCkTypeId = '';
|
|
33382
|
+
lastEmitted = null;
|
|
33383
|
+
associations = new Map();
|
|
33384
|
+
/** CK type of the picked entity — the source type of hop 1. */
|
|
33385
|
+
set sourceCkTypeId(value) {
|
|
33386
|
+
this._sourceCkTypeId = value ?? '';
|
|
33387
|
+
this.syncAssociationKeys();
|
|
33388
|
+
}
|
|
33389
|
+
get sourceCkTypeId() {
|
|
33390
|
+
return this._sourceCkTypeId;
|
|
33391
|
+
}
|
|
33392
|
+
/** The configured hops (one-hop form = one hop). */
|
|
33393
|
+
set hops(value) {
|
|
33394
|
+
// Our own emission coming back through the two-way binding: keep the rows
|
|
33395
|
+
// (free-text mode) as they are.
|
|
33396
|
+
if (value && value === this.lastEmitted)
|
|
33397
|
+
return;
|
|
33398
|
+
this.rows = (value ?? []).map(h => ({
|
|
33399
|
+
roleId: h.roleId ?? '',
|
|
33400
|
+
direction: h.direction ?? 'in',
|
|
33401
|
+
targetCkTypeId: h.targetCkTypeId ?? '',
|
|
33402
|
+
targetItem: h.targetCkTypeId ? ckTypeItem(h.targetCkTypeId) : null,
|
|
33403
|
+
associationKey: ''
|
|
33404
|
+
}));
|
|
33405
|
+
this.syncAssociationKeys();
|
|
33406
|
+
}
|
|
33407
|
+
/** Maximum hop count (getter: relative imports must not initialise fields, see CLAUDE.md). */
|
|
33408
|
+
get maxHops() {
|
|
33409
|
+
return MAX_SCOPE_HOPS;
|
|
33410
|
+
}
|
|
33411
|
+
get canAddHop() {
|
|
33412
|
+
return this.rows.length < MAX_SCOPE_HOPS;
|
|
33413
|
+
}
|
|
33414
|
+
/** "Test/Site → Test/System → Test/Device". */
|
|
33415
|
+
get pathLabel() {
|
|
33416
|
+
return scopePathLabel(this.sourceCkTypeId || '?', this.rows);
|
|
33417
|
+
}
|
|
33418
|
+
/** True when the association metadata of any shown source type could not be loaded. */
|
|
33419
|
+
get metadataFailed() {
|
|
33420
|
+
return this.rows.some((_, i) => this.loadOf(i)?.state === 'failed');
|
|
33421
|
+
}
|
|
33422
|
+
/** Source type of hop `index`. */
|
|
33423
|
+
sourceTypeOf(index) {
|
|
33424
|
+
return index === 0 ? this.sourceCkTypeId : this.rows[index - 1]?.targetCkTypeId ?? '';
|
|
33425
|
+
}
|
|
33426
|
+
/** Dropdown entries of hop `index`: the source type's associations plus "Other role…". */
|
|
33427
|
+
associationOptions(index) {
|
|
33428
|
+
const load = this.loadOf(index);
|
|
33429
|
+
const options = load?.state === 'loaded' ? load.options : [];
|
|
33430
|
+
return [...options, {
|
|
33431
|
+
key: OTHER_ASSOCIATION,
|
|
33432
|
+
roleId: '',
|
|
33433
|
+
direction: 'in',
|
|
33434
|
+
baseTargetCkTypeId: '',
|
|
33435
|
+
label: 'Other role…'
|
|
33436
|
+
}];
|
|
33437
|
+
}
|
|
33438
|
+
/** True when hop `index` offers the association dropdown (metadata loaded with ≥1 association). */
|
|
33439
|
+
hasAssociationList(index) {
|
|
33440
|
+
const load = this.loadOf(index);
|
|
33441
|
+
return load?.state === 'loaded' && load.options.length > 0;
|
|
33442
|
+
}
|
|
33443
|
+
/** True when hop `index` shows the free-text role and direction fields. */
|
|
33444
|
+
isFreeText(index) {
|
|
33445
|
+
return !this.hasAssociationList(index) || this.rows[index].associationKey === OTHER_ASSOCIATION;
|
|
33446
|
+
}
|
|
33447
|
+
/** Base type the target-type picker of hop `index` is restricted to (undefined = any type). */
|
|
33448
|
+
baseTargetOf(index) {
|
|
33449
|
+
const row = this.rows[index];
|
|
33450
|
+
const option = this.associationOptions(index).find(o => o.key === row.associationKey && o.key !== OTHER_ASSOCIATION);
|
|
33451
|
+
return option?.baseTargetCkTypeId || undefined;
|
|
33452
|
+
}
|
|
33453
|
+
/**
|
|
33454
|
+
* Warning of hop `index` when its role is not an association of its (new)
|
|
33455
|
+
* source type — after a reorder, a removal or a changed earlier target. The
|
|
33456
|
+
* values are kept; it does not block saving.
|
|
33457
|
+
*/
|
|
33458
|
+
associationWarning(index) {
|
|
33459
|
+
const row = this.rows[index];
|
|
33460
|
+
if (!row.roleId.trim() || !this.hasAssociationList(index))
|
|
33461
|
+
return '';
|
|
33462
|
+
const load = this.loadOf(index);
|
|
33463
|
+
const defined = load?.state === 'loaded'
|
|
33464
|
+
&& load.options.some(o => o.roleId === row.roleId.trim() && o.direction === row.direction);
|
|
33465
|
+
return defined ? '' : `Association ${row.roleId.trim()} is not defined on ${this.sourceTypeOf(index) || 'the source type'}.`;
|
|
33466
|
+
}
|
|
33467
|
+
/** Validation message of hop `index`: an incomplete hop blocks saving (see `scopeHopsValidationError`). */
|
|
33468
|
+
hopError(index) {
|
|
33469
|
+
const row = this.rows[index];
|
|
33470
|
+
return !row.roleId.trim() || !row.targetCkTypeId.trim()
|
|
33471
|
+
? `Hop ${index + 1} needs an association and a target type.`
|
|
33472
|
+
: '';
|
|
33473
|
+
}
|
|
33474
|
+
isRoleInvalid(index) {
|
|
33475
|
+
return !this.rows[index].roleId.trim();
|
|
33476
|
+
}
|
|
33477
|
+
isTargetInvalid(index) {
|
|
33478
|
+
return !this.rows[index].targetCkTypeId.trim();
|
|
33479
|
+
}
|
|
33480
|
+
addHop() {
|
|
33481
|
+
if (!this.canAddHop)
|
|
33482
|
+
return;
|
|
33483
|
+
this.rows = [...this.rows, {
|
|
33484
|
+
roleId: 'System/ParentChild',
|
|
33485
|
+
direction: 'in',
|
|
33486
|
+
targetCkTypeId: '',
|
|
33487
|
+
targetItem: null,
|
|
33488
|
+
associationKey: ''
|
|
33489
|
+
}];
|
|
33490
|
+
this.syncAssociationKeys();
|
|
33491
|
+
this.emit();
|
|
33492
|
+
}
|
|
33493
|
+
removeHop(index) {
|
|
33494
|
+
this.rows = this.rows.filter((_, i) => i !== index);
|
|
33495
|
+
this.syncAssociationKeys();
|
|
33496
|
+
this.emit();
|
|
33497
|
+
}
|
|
33498
|
+
moveHop(index, delta) {
|
|
33499
|
+
const target = index + delta;
|
|
33500
|
+
if (target < 0 || target >= this.rows.length)
|
|
33501
|
+
return;
|
|
33502
|
+
const rows = [...this.rows];
|
|
33503
|
+
[rows[index], rows[target]] = [rows[target], rows[index]];
|
|
33504
|
+
this.rows = rows;
|
|
33505
|
+
this.syncAssociationKeys();
|
|
33506
|
+
this.emit();
|
|
33507
|
+
this.focusHop(target);
|
|
33508
|
+
}
|
|
33509
|
+
onAssociationChanged(index, key) {
|
|
33510
|
+
const row = this.rows[index];
|
|
33511
|
+
row.associationKey = key;
|
|
33512
|
+
const option = this.associationOptions(index).find(o => o.key === key);
|
|
33513
|
+
if (option && key !== OTHER_ASSOCIATION) {
|
|
33514
|
+
row.roleId = option.roleId;
|
|
33515
|
+
row.direction = option.direction;
|
|
33516
|
+
}
|
|
33517
|
+
this.emit();
|
|
33518
|
+
}
|
|
33519
|
+
onRoleChanged(index, roleId) {
|
|
33520
|
+
this.rows[index].roleId = roleId;
|
|
33521
|
+
this.emit();
|
|
33522
|
+
}
|
|
33523
|
+
onDirectionChanged(index, direction) {
|
|
33524
|
+
this.rows[index].direction = direction;
|
|
33525
|
+
this.emit();
|
|
33526
|
+
}
|
|
33527
|
+
onTargetSelected(index, ckType) {
|
|
33528
|
+
const row = this.rows[index];
|
|
33529
|
+
row.targetCkTypeId = ckType.rtCkTypeId;
|
|
33530
|
+
row.targetItem = ckType;
|
|
33531
|
+
// Later hops keep their values; their source type changed, so re-match them.
|
|
33532
|
+
this.syncAssociationKeys();
|
|
33533
|
+
this.emit();
|
|
33534
|
+
}
|
|
33535
|
+
onTargetCleared(index) {
|
|
33536
|
+
const row = this.rows[index];
|
|
33537
|
+
row.targetCkTypeId = '';
|
|
33538
|
+
row.targetItem = null;
|
|
33539
|
+
this.syncAssociationKeys();
|
|
33540
|
+
this.emit();
|
|
33541
|
+
}
|
|
33542
|
+
emit() {
|
|
33543
|
+
this.lastEmitted = this.rows.map(r => ({ roleId: r.roleId, direction: r.direction, targetCkTypeId: r.targetCkTypeId }));
|
|
33544
|
+
this.hopsChange.emit(this.lastEmitted);
|
|
33545
|
+
}
|
|
33546
|
+
loadOf(index) {
|
|
33547
|
+
const type = this.sourceTypeOf(index);
|
|
33548
|
+
return type ? this.associations.get(type) : undefined;
|
|
33549
|
+
}
|
|
33550
|
+
/**
|
|
33551
|
+
* Matches every hop's role/direction to an entry of its source type's
|
|
33552
|
+
* associations (loading missing metadata); a hop whose association is not
|
|
33553
|
+
* listed switches to "Other role…" with its values kept.
|
|
33554
|
+
*/
|
|
33555
|
+
syncAssociationKeys() {
|
|
33556
|
+
this.rows.forEach((row, index) => {
|
|
33557
|
+
const type = this.sourceTypeOf(index);
|
|
33558
|
+
if (type && !this.associations.has(type)) {
|
|
33559
|
+
void this.loadAssociations(type);
|
|
33560
|
+
}
|
|
33561
|
+
const load = this.loadOf(index);
|
|
33562
|
+
if (load?.state !== 'loaded' || load.options.length === 0) {
|
|
33563
|
+
row.associationKey = '';
|
|
33564
|
+
return;
|
|
33565
|
+
}
|
|
33566
|
+
const match = load.options.find(o => o.roleId === row.roleId.trim() && o.direction === row.direction);
|
|
33567
|
+
row.associationKey = match ? match.key : (row.roleId.trim() ? OTHER_ASSOCIATION : '');
|
|
33568
|
+
});
|
|
33569
|
+
}
|
|
33570
|
+
async loadAssociations(ckTypeId) {
|
|
33571
|
+
this.associations.set(ckTypeId, { state: 'loading' });
|
|
33572
|
+
try {
|
|
33573
|
+
const result = await firstValueFrom(this.rolesGQL.fetch({ variables: { ckTypeId } }));
|
|
33574
|
+
const ckType = result.data?.constructionKit?.types?.items?.[0];
|
|
33575
|
+
const options = [];
|
|
33576
|
+
const add = (direction, all) => {
|
|
33577
|
+
for (const assoc of all ?? []) {
|
|
33578
|
+
const roleId = assoc?.rtRoleId ? String(assoc.rtRoleId) : '';
|
|
33579
|
+
if (!roleId)
|
|
33580
|
+
continue;
|
|
33581
|
+
const base = assoc?.rtTargetCkTypeId ? String(assoc.rtTargetCkTypeId) : '';
|
|
33582
|
+
const key = `${direction}|${roleId}|${base}`;
|
|
33583
|
+
if (options.some(o => o.key === key))
|
|
33584
|
+
continue;
|
|
33585
|
+
options.push({
|
|
33586
|
+
key,
|
|
33587
|
+
roleId,
|
|
33588
|
+
direction,
|
|
33589
|
+
baseTargetCkTypeId: base,
|
|
33590
|
+
label: `${roleId} · ${direction === 'in' ? 'inbound' : 'outbound'}${base ? ` · ${base}` : ''}`
|
|
33591
|
+
});
|
|
33592
|
+
}
|
|
33593
|
+
};
|
|
33594
|
+
add('in', ckType?.associations?.in?.all);
|
|
33595
|
+
add('out', ckType?.associations?.out?.all);
|
|
33596
|
+
this.associations.set(ckTypeId, { state: 'loaded', options });
|
|
33597
|
+
}
|
|
33598
|
+
catch (error) {
|
|
33599
|
+
console.warn(`Could not load the associations of ${ckTypeId}:`, error);
|
|
33600
|
+
this.associations.set(ckTypeId, { state: 'failed' });
|
|
33601
|
+
}
|
|
33602
|
+
this.syncAssociationKeys();
|
|
33603
|
+
this.cdr.markForCheck();
|
|
33604
|
+
}
|
|
33605
|
+
/** Keeps the keyboard focus on a moved hop (wireframe change 4). */
|
|
33606
|
+
focusHop(index) {
|
|
33607
|
+
setTimeout(() => {
|
|
33608
|
+
const card = this.hopCards?.get(index)?.nativeElement;
|
|
33609
|
+
const button = card?.querySelector(`[data-hop-action="${index > 0 ? 'up' : 'down'}"]`);
|
|
33610
|
+
(button ?? card)?.focus();
|
|
33611
|
+
});
|
|
33612
|
+
}
|
|
33613
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: EntitySelectorScopePathEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
33614
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: EntitySelectorScopePathEditorComponent, isStandalone: true, selector: "mm-entity-selector-scope-path-editor", inputs: { sourceCkTypeId: "sourceCkTypeId", hops: "hops" }, outputs: { hopsChange: "hopsChange" }, viewQueries: [{ propertyName: "hopCards", predicate: ["hopCard"], descendants: true }], ngImport: i0, template: "<div class=\"scope-path-editor\">\n <span class=\"field-hint\">\n Optional. Path of associations from the picked entity to the entities whose\n rtIds scope bound stream-data widgets (up to {{ maxHops }} hops).\n </span>\n\n @if (rows.length === 0) {\n <p class=\"scope-empty\" data-test=\"scope-empty\">No child scope: widgets use the picked entity's own rtId.</p>\n } @else {\n <span class=\"scope-from\">From: <code>{{ sourceCkTypeId || '?' }}</code> (picked entity)</span>\n @if (metadataFailed) {\n <span class=\"field-hint scope-metadata-failed\" data-test=\"metadata-failed\">\n Associations could not be loaded; enter the role manually.\n </span>\n }\n\n @for (row of rows; track $index; let i = $index; let first = $first; let last = $last) {\n <div class=\"hop-card\" #hopCard tabindex=\"-1\" [attr.data-test]=\"'hop-' + (i + 1)\">\n <div class=\"hop-header\">\n <span class=\"hop-title\">\n Hop {{ i + 1 }}\n @if (i > 0) {\n <span class=\"hop-source\">(from <code>{{ sourceTypeOf(i) || '?' }}</code>)</span>\n }\n </span>\n <span class=\"hop-actions\">\n @if (!first) {\n <button kendoButton fillMode=\"flat\" size=\"small\" [svgIcon]=\"arrowUpIcon\"\n data-hop-action=\"up\"\n [attr.aria-label]=\"'Move hop ' + (i + 1) + ' up'\" [title]=\"'Move hop ' + (i + 1) + ' up'\"\n (click)=\"moveHop(i, -1)\"></button>\n }\n @if (!last) {\n <button kendoButton fillMode=\"flat\" size=\"small\" [svgIcon]=\"arrowDownIcon\"\n data-hop-action=\"down\"\n [attr.aria-label]=\"'Move hop ' + (i + 1) + ' down'\" [title]=\"'Move hop ' + (i + 1) + ' down'\"\n (click)=\"moveHop(i, 1)\"></button>\n }\n <button kendoButton fillMode=\"flat\" size=\"small\" [svgIcon]=\"trashIcon\" class=\"hop-remove\"\n data-hop-action=\"remove\"\n [attr.aria-label]=\"'Remove hop ' + (i + 1)\" [title]=\"'Remove hop ' + (i + 1)\"\n (click)=\"removeHop(i)\"></button>\n </span>\n </div>\n\n <div class=\"hop-field\">\n <label class=\"field-label\" [attr.for]=\"'hop-assoc-' + i\">Association</label>\n @if (hasAssociationList(i)) {\n <kendo-dropdownlist\n [id]=\"'hop-assoc-' + i\"\n [data]=\"associationOptions(i)\"\n textField=\"label\"\n valueField=\"key\"\n [valuePrimitive]=\"true\"\n [value]=\"row.associationKey || null\"\n [defaultItem]=\"{ key: '', label: 'Select an association\u2026' }\"\n [class.k-invalid]=\"isRoleInvalid(i)\"\n (valueChange)=\"onAssociationChanged(i, $event)\">\n <ng-template kendoDropDownListValueTemplate let-item>\n <span class=\"assoc-text\" [title]=\"item?.label ?? ''\">{{ item?.label }}</span>\n </ng-template>\n <ng-template kendoDropDownListItemTemplate let-item>\n <span class=\"assoc-text\" [title]=\"item.label\">{{ item.label }}</span>\n </ng-template>\n </kendo-dropdownlist>\n }\n @if (isFreeText(i)) {\n <div class=\"hop-free-text\">\n <kendo-textbox\n [id]=\"hasAssociationList(i) ? null : 'hop-assoc-' + i\"\n [value]=\"row.roleId\"\n placeholder=\"Association role (e.g. System/ParentChild)\"\n [class.k-invalid]=\"isRoleInvalid(i)\"\n (valueChange)=\"onRoleChanged(i, $event)\">\n </kendo-textbox>\n <select class=\"hop-direction\" [attr.aria-label]=\"'Direction of hop ' + (i + 1)\"\n [ngModel]=\"row.direction\" (ngModelChange)=\"onDirectionChanged(i, $event)\">\n <option value=\"in\">Inbound</option>\n <option value=\"out\">Outbound</option>\n </select>\n </div>\n }\n @if (associationWarning(i); as warning) {\n <span class=\"field-warning\" data-test=\"association-warning\">{{ warning }}</span>\n }\n </div>\n\n <div class=\"hop-field\">\n <label class=\"field-label\">Target type</label>\n <mm-ck-type-selector-input\n [ngModel]=\"row.targetItem\"\n [derivedFromRtCkTypeId]=\"baseTargetOf(i)\"\n [placeholder]=\"'Target CK type\u2026'\"\n [class.k-invalid]=\"isTargetInvalid(i)\"\n (ckTypeSelected)=\"onTargetSelected(i, $event)\"\n (ckTypeCleared)=\"onTargetCleared(i)\">\n </mm-ck-type-selector-input>\n </div>\n\n @if (hopError(i); as error) {\n <span class=\"field-error\" data-test=\"hop-error\">{{ error }}</span>\n }\n </div>\n }\n }\n\n <div class=\"scope-footer\">\n <button kendoButton fillMode=\"flat\" [svgIcon]=\"plusIcon\" [disabled]=\"!canAddHop\"\n data-test=\"add-hop\" (click)=\"addHop()\">Add hop</button>\n @if (!canAddHop) {\n <span class=\"field-hint\">At most {{ maxHops }} hops.</span>\n }\n </div>\n\n @if (rows.length > 0) {\n <span class=\"scope-path\" data-test=\"scope-path\">Path: {{ pathLabel }}</span>\n }\n</div>\n", styles: [".scope-path-editor{display:flex;flex-direction:column;gap:.5rem;min-width:0}.field-label{font-weight:500;font-size:.875rem;color:var(--kendo-color-on-app-surface, #424242)}.field-hint,.scope-from,.scope-empty{font-size:.75rem;color:var(--kendo-color-subtle, #757575);margin:0}.field-error{font-size:.75rem;color:var(--kendo-color-error, #f44336)}.field-warning{font-size:.75rem;color:var(--kendo-color-warning, #b26a00)}code{font-family:var(--theme-font-mono, monospace)}.hop-card{display:flex;flex-direction:column;gap:.5rem;padding:.5rem .75rem .75rem;border:1px solid var(--kendo-color-border, #e0e0e0);border-radius:4px;background:var(--kendo-color-surface-alt, #fafafa);min-width:0}.hop-header{display:flex;align-items:center;justify-content:space-between;gap:.5rem}.hop-title{font-weight:500;font-size:.875rem;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.hop-source{font-weight:400;color:var(--kendo-color-subtle, #757575)}.hop-actions{display:flex;flex-wrap:nowrap;flex-shrink:0}.hop-remove{color:var(--kendo-color-error, #f44336)}.hop-field{display:flex;flex-direction:column;gap:.25rem;min-width:0}.hop-field kendo-dropdownlist{width:100%;min-width:0}.assoc-text{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.hop-free-text{display:flex;gap:.5rem;min-width:0}.hop-free-text kendo-textbox{flex:1 1 auto;min-width:0}.hop-direction{flex:0 0 auto}.scope-footer{display:flex;align-items:center;gap:.5rem}.scope-path{font-size:.75rem;font-family:var(--theme-font-mono, monospace);color:var(--kendo-color-on-app-surface, #424242);overflow-wrap:break-word}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.SelectControlValueAccessor, selector: "select:not([multiple]):not([ngNoCva])[formControlName],select:not([multiple]):not([ngNoCva])[formControl],select:not([multiple]):not([ngNoCva])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: DropDownsModule }, { kind: "directive", type: i4.ItemTemplateDirective, selector: "[kendoDropDownListItemTemplate],[kendoComboBoxItemTemplate],[kendoAutoCompleteItemTemplate],[kendoMultiSelectItemTemplate]" }, { kind: "component", type: i4.DropDownListComponent, selector: "kendo-dropdownlist", inputs: ["customIconClass", "showStickyHeader", "icon", "svgIcon", "loading", "data", "value", "textField", "valueField", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "defaultItem", "disabled", "itemDisabled", "readonly", "filterable", "virtual", "ignoreCase", "delay", "valuePrimitive", "tabindex", "tabIndex", "size", "rounded", "fillMode", "leftRightArrowsNavigation", "id"], outputs: ["valueChange", "filterChange", "selectionChange", "open", "opened", "close", "closed", "focus", "blur"], exportAs: ["kendoDropDownList"] }, { kind: "directive", type: i4.ValueTemplateDirective, selector: "[kendoDropDownListValueTemplate],[kendoDropDownTreeValueTemplate]" }, { kind: "ngmodule", type: InputsModule }, { kind: "component", type: i3.TextBoxComponent, selector: "kendo-textbox", inputs: ["focusableId", "title", "type", "disabled", "readonly", "tabindex", "value", "selectOnFocus", "showSuccessIcon", "showErrorIcon", "clearButton", "successIcon", "successSvgIcon", "errorIcon", "errorSvgIcon", "clearButtonIcon", "clearButtonSvgIcon", "size", "rounded", "fillMode", "tabIndex", "placeholder", "maxlength", "inputAttributes"], outputs: ["valueChange", "inputFocus", "inputBlur", "focus", "blur"], exportAs: ["kendoTextBox"] }, { kind: "component", type: CkTypeSelectorInputComponent, selector: "mm-ck-type-selector-input", inputs: ["placeholder", "minSearchLength", "maxResults", "debounceMs", "ckModelIds", "allowAbstract", "dialogTitle", "advancedSearchLabel", "derivedFromRtCkTypeId", "messages", "dialogMessages", "disabled", "required"], outputs: ["ckTypeSelected", "ckTypeCleared"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
|
|
33615
|
+
}
|
|
33616
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: EntitySelectorScopePathEditorComponent, decorators: [{
|
|
33617
|
+
type: Component,
|
|
33618
|
+
args: [{ selector: 'mm-entity-selector-scope-path-editor', standalone: true, imports: [FormsModule, ButtonModule, DropDownsModule, InputsModule, CkTypeSelectorInputComponent], changeDetection: ChangeDetectionStrategy.Eager, template: "<div class=\"scope-path-editor\">\n <span class=\"field-hint\">\n Optional. Path of associations from the picked entity to the entities whose\n rtIds scope bound stream-data widgets (up to {{ maxHops }} hops).\n </span>\n\n @if (rows.length === 0) {\n <p class=\"scope-empty\" data-test=\"scope-empty\">No child scope: widgets use the picked entity's own rtId.</p>\n } @else {\n <span class=\"scope-from\">From: <code>{{ sourceCkTypeId || '?' }}</code> (picked entity)</span>\n @if (metadataFailed) {\n <span class=\"field-hint scope-metadata-failed\" data-test=\"metadata-failed\">\n Associations could not be loaded; enter the role manually.\n </span>\n }\n\n @for (row of rows; track $index; let i = $index; let first = $first; let last = $last) {\n <div class=\"hop-card\" #hopCard tabindex=\"-1\" [attr.data-test]=\"'hop-' + (i + 1)\">\n <div class=\"hop-header\">\n <span class=\"hop-title\">\n Hop {{ i + 1 }}\n @if (i > 0) {\n <span class=\"hop-source\">(from <code>{{ sourceTypeOf(i) || '?' }}</code>)</span>\n }\n </span>\n <span class=\"hop-actions\">\n @if (!first) {\n <button kendoButton fillMode=\"flat\" size=\"small\" [svgIcon]=\"arrowUpIcon\"\n data-hop-action=\"up\"\n [attr.aria-label]=\"'Move hop ' + (i + 1) + ' up'\" [title]=\"'Move hop ' + (i + 1) + ' up'\"\n (click)=\"moveHop(i, -1)\"></button>\n }\n @if (!last) {\n <button kendoButton fillMode=\"flat\" size=\"small\" [svgIcon]=\"arrowDownIcon\"\n data-hop-action=\"down\"\n [attr.aria-label]=\"'Move hop ' + (i + 1) + ' down'\" [title]=\"'Move hop ' + (i + 1) + ' down'\"\n (click)=\"moveHop(i, 1)\"></button>\n }\n <button kendoButton fillMode=\"flat\" size=\"small\" [svgIcon]=\"trashIcon\" class=\"hop-remove\"\n data-hop-action=\"remove\"\n [attr.aria-label]=\"'Remove hop ' + (i + 1)\" [title]=\"'Remove hop ' + (i + 1)\"\n (click)=\"removeHop(i)\"></button>\n </span>\n </div>\n\n <div class=\"hop-field\">\n <label class=\"field-label\" [attr.for]=\"'hop-assoc-' + i\">Association</label>\n @if (hasAssociationList(i)) {\n <kendo-dropdownlist\n [id]=\"'hop-assoc-' + i\"\n [data]=\"associationOptions(i)\"\n textField=\"label\"\n valueField=\"key\"\n [valuePrimitive]=\"true\"\n [value]=\"row.associationKey || null\"\n [defaultItem]=\"{ key: '', label: 'Select an association\u2026' }\"\n [class.k-invalid]=\"isRoleInvalid(i)\"\n (valueChange)=\"onAssociationChanged(i, $event)\">\n <ng-template kendoDropDownListValueTemplate let-item>\n <span class=\"assoc-text\" [title]=\"item?.label ?? ''\">{{ item?.label }}</span>\n </ng-template>\n <ng-template kendoDropDownListItemTemplate let-item>\n <span class=\"assoc-text\" [title]=\"item.label\">{{ item.label }}</span>\n </ng-template>\n </kendo-dropdownlist>\n }\n @if (isFreeText(i)) {\n <div class=\"hop-free-text\">\n <kendo-textbox\n [id]=\"hasAssociationList(i) ? null : 'hop-assoc-' + i\"\n [value]=\"row.roleId\"\n placeholder=\"Association role (e.g. System/ParentChild)\"\n [class.k-invalid]=\"isRoleInvalid(i)\"\n (valueChange)=\"onRoleChanged(i, $event)\">\n </kendo-textbox>\n <select class=\"hop-direction\" [attr.aria-label]=\"'Direction of hop ' + (i + 1)\"\n [ngModel]=\"row.direction\" (ngModelChange)=\"onDirectionChanged(i, $event)\">\n <option value=\"in\">Inbound</option>\n <option value=\"out\">Outbound</option>\n </select>\n </div>\n }\n @if (associationWarning(i); as warning) {\n <span class=\"field-warning\" data-test=\"association-warning\">{{ warning }}</span>\n }\n </div>\n\n <div class=\"hop-field\">\n <label class=\"field-label\">Target type</label>\n <mm-ck-type-selector-input\n [ngModel]=\"row.targetItem\"\n [derivedFromRtCkTypeId]=\"baseTargetOf(i)\"\n [placeholder]=\"'Target CK type\u2026'\"\n [class.k-invalid]=\"isTargetInvalid(i)\"\n (ckTypeSelected)=\"onTargetSelected(i, $event)\"\n (ckTypeCleared)=\"onTargetCleared(i)\">\n </mm-ck-type-selector-input>\n </div>\n\n @if (hopError(i); as error) {\n <span class=\"field-error\" data-test=\"hop-error\">{{ error }}</span>\n }\n </div>\n }\n }\n\n <div class=\"scope-footer\">\n <button kendoButton fillMode=\"flat\" [svgIcon]=\"plusIcon\" [disabled]=\"!canAddHop\"\n data-test=\"add-hop\" (click)=\"addHop()\">Add hop</button>\n @if (!canAddHop) {\n <span class=\"field-hint\">At most {{ maxHops }} hops.</span>\n }\n </div>\n\n @if (rows.length > 0) {\n <span class=\"scope-path\" data-test=\"scope-path\">Path: {{ pathLabel }}</span>\n }\n</div>\n", styles: [".scope-path-editor{display:flex;flex-direction:column;gap:.5rem;min-width:0}.field-label{font-weight:500;font-size:.875rem;color:var(--kendo-color-on-app-surface, #424242)}.field-hint,.scope-from,.scope-empty{font-size:.75rem;color:var(--kendo-color-subtle, #757575);margin:0}.field-error{font-size:.75rem;color:var(--kendo-color-error, #f44336)}.field-warning{font-size:.75rem;color:var(--kendo-color-warning, #b26a00)}code{font-family:var(--theme-font-mono, monospace)}.hop-card{display:flex;flex-direction:column;gap:.5rem;padding:.5rem .75rem .75rem;border:1px solid var(--kendo-color-border, #e0e0e0);border-radius:4px;background:var(--kendo-color-surface-alt, #fafafa);min-width:0}.hop-header{display:flex;align-items:center;justify-content:space-between;gap:.5rem}.hop-title{font-weight:500;font-size:.875rem;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.hop-source{font-weight:400;color:var(--kendo-color-subtle, #757575)}.hop-actions{display:flex;flex-wrap:nowrap;flex-shrink:0}.hop-remove{color:var(--kendo-color-error, #f44336)}.hop-field{display:flex;flex-direction:column;gap:.25rem;min-width:0}.hop-field kendo-dropdownlist{width:100%;min-width:0}.assoc-text{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.hop-free-text{display:flex;gap:.5rem;min-width:0}.hop-free-text kendo-textbox{flex:1 1 auto;min-width:0}.hop-direction{flex:0 0 auto}.scope-footer{display:flex;align-items:center;gap:.5rem}.scope-path{font-size:.75rem;font-family:var(--theme-font-mono, monospace);color:var(--kendo-color-on-app-surface, #424242);overflow-wrap:break-word}\n"] }]
|
|
33619
|
+
}], propDecorators: { hopsChange: [{
|
|
33620
|
+
type: Output
|
|
33621
|
+
}], hopCards: [{
|
|
33622
|
+
type: ViewChildren,
|
|
33623
|
+
args: ['hopCard']
|
|
33624
|
+
}], sourceCkTypeId: [{
|
|
33625
|
+
type: Input
|
|
33626
|
+
}], hops: [{
|
|
33627
|
+
type: Input
|
|
33628
|
+
}] } });
|
|
33629
|
+
function ckTypeItem(ckTypeId) {
|
|
33630
|
+
return { fullName: ckTypeId, rtCkTypeId: ckTypeId, isAbstract: false, isFinal: false };
|
|
33631
|
+
}
|
|
33632
|
+
|
|
32429
33633
|
/**
|
|
32430
33634
|
* Component for editing entity selector configurations in MeshBoard settings.
|
|
32431
33635
|
* Allows adding, editing, and removing entity selectors.
|
|
@@ -32437,9 +33641,16 @@ class EntitySelectorEditorComponent {
|
|
|
32437
33641
|
variableService = inject(MeshBoardVariableService);
|
|
32438
33642
|
getEntitiesByCkTypeGQL = inject(GetEntitiesByCkTypeDtoGQL);
|
|
32439
33643
|
confirmationService = inject(ConfirmationService, { optional: true });
|
|
33644
|
+
cdr = inject(ChangeDetectorRef);
|
|
33645
|
+
destroyRef = inject(DestroyRef);
|
|
32440
33646
|
entitySelectors = [];
|
|
32441
33647
|
/** Existing variable names from other sources (static variables, other selectors) for duplicate detection */
|
|
32442
33648
|
existingVariableNames = [];
|
|
33649
|
+
/**
|
|
33650
|
+
* The board's static variables (AB#7724): offered in the picker filter and
|
|
33651
|
+
* substituted when the editor lists default entities and checks the default.
|
|
33652
|
+
*/
|
|
33653
|
+
variables = [];
|
|
32443
33654
|
entitySelectorsChange = new EventEmitter();
|
|
32444
33655
|
/** Emits true when entering edit mode, false when leaving — allows parent to hide its own buttons */
|
|
32445
33656
|
editingStateChange = new EventEmitter();
|
|
@@ -32456,22 +33667,32 @@ class EntitySelectorEditorComponent {
|
|
|
32456
33667
|
editMappings = [];
|
|
32457
33668
|
editShowInToolbar = true;
|
|
32458
33669
|
editDefaultRtId = '';
|
|
32459
|
-
// Child scope (stream-data):
|
|
32460
|
-
//
|
|
32461
|
-
|
|
32462
|
-
|
|
32463
|
-
editChildScopeRoleId = '';
|
|
32464
|
-
// Parent → children via System/ParentChild is reached INBOUND (the child holds
|
|
32465
|
-
// the association to its parent), so 'in' is the correct default for a
|
|
32466
|
-
// childScope that resolves a picked parent to its children.
|
|
32467
|
-
editChildScopeDirection = 'in';
|
|
33670
|
+
// Child scope (stream-data, AB#7723): the hops from the picked entity to the
|
|
33671
|
+
// entities whose rtIds scope bound widgets. Empty = the picked entity itself.
|
|
33672
|
+
// One hop saves in the original top-level form, two or more as `path`.
|
|
33673
|
+
editScopeHops = [];
|
|
32468
33674
|
// Leader/follower (AB#7722): id of the selector this one follows ('' = none).
|
|
32469
33675
|
editFollowsSelectorId = '';
|
|
32470
33676
|
/** Why choosing the leader changed the form (CK type fixed, mappings/default cleared). */
|
|
32471
33677
|
followNotice = '';
|
|
33678
|
+
// Picker filter (AB#7724): attribute rows on the CK type, AND-combined.
|
|
33679
|
+
editFilters = [];
|
|
33680
|
+
/** Filter rows a follower carries from hand-editing; ignored and dropped on save. */
|
|
33681
|
+
followerFilterCount = 0;
|
|
33682
|
+
/** Result of the server check whether the default entity matches the filter. */
|
|
33683
|
+
defaultFilterCheck = { state: 'none' };
|
|
33684
|
+
defaultCheckRequests = new Subject();
|
|
32472
33685
|
// Data sources for the default entity picker (created when CK type is set)
|
|
32473
33686
|
defaultEntityDataSource = null;
|
|
32474
33687
|
defaultEntityDialogDataSource = null;
|
|
33688
|
+
constructor() {
|
|
33689
|
+
// Debounced so typing in the filter grid does not fire a request per key;
|
|
33690
|
+
// switchMap drops the answer of an outdated check.
|
|
33691
|
+
this.defaultCheckRequests.pipe(debounceTime(DEFAULT_CHECK_DEBOUNCE_MS), switchMap(request => 'state' in request ? of(request) : this.checkDefaultEntity(request)), takeUntilDestroyed(this.destroyRef)).subscribe(check => {
|
|
33692
|
+
this.defaultFilterCheck = check;
|
|
33693
|
+
this.cdr.markForCheck();
|
|
33694
|
+
});
|
|
33695
|
+
}
|
|
32475
33696
|
/**
|
|
32476
33697
|
* Starts adding a new entity selector.
|
|
32477
33698
|
*/
|
|
@@ -32484,12 +33705,12 @@ class EntitySelectorEditorComponent {
|
|
|
32484
33705
|
this.editMappings = [];
|
|
32485
33706
|
this.editShowInToolbar = true;
|
|
32486
33707
|
this.editDefaultRtId = '';
|
|
32487
|
-
this.
|
|
32488
|
-
this.editChildScopeTargetCkTypeItem = null;
|
|
32489
|
-
this.editChildScopeRoleId = 'System/ParentChild';
|
|
32490
|
-
this.editChildScopeDirection = 'in';
|
|
33708
|
+
this.editScopeHops = [];
|
|
32491
33709
|
this.editFollowsSelectorId = '';
|
|
32492
33710
|
this.followNotice = '';
|
|
33711
|
+
this.editFilters = [];
|
|
33712
|
+
this.followerFilterCount = 0;
|
|
33713
|
+
this.defaultFilterCheck = { state: 'none' };
|
|
32493
33714
|
this.defaultEntityDataSource = null;
|
|
32494
33715
|
this.defaultEntityDialogDataSource = null;
|
|
32495
33716
|
this.isEditing = true;
|
|
@@ -32514,19 +33735,17 @@ class EntitySelectorEditorComponent {
|
|
|
32514
33735
|
this.editMappings = [...selector.attributeMappings];
|
|
32515
33736
|
this.editShowInToolbar = selector.showInToolbar !== false;
|
|
32516
33737
|
this.editDefaultRtId = selector.defaultRtId ?? '';
|
|
32517
|
-
this.
|
|
32518
|
-
this.editChildScopeTargetCkTypeItem = selector.childScope?.targetCkTypeId
|
|
32519
|
-
? {
|
|
32520
|
-
fullName: selector.childScope.targetCkTypeId,
|
|
32521
|
-
rtCkTypeId: selector.childScope.targetCkTypeId,
|
|
32522
|
-
isAbstract: false,
|
|
32523
|
-
isFinal: false
|
|
32524
|
-
}
|
|
32525
|
-
: null;
|
|
32526
|
-
this.editChildScopeRoleId = selector.childScope?.roleId ?? 'System/ParentChild';
|
|
32527
|
-
this.editChildScopeDirection = selector.childScope?.direction ?? 'in';
|
|
33738
|
+
this.editScopeHops = scopeHopsOf(selector.childScope);
|
|
32528
33739
|
this.editFollowsSelectorId = selector.followsSelectorId ?? '';
|
|
32529
33740
|
this.followNotice = '';
|
|
33741
|
+
this.editFilters = (selector.filters ?? []).map((f, i) => ({
|
|
33742
|
+
id: i + 1,
|
|
33743
|
+
attributePath: f.attributePath,
|
|
33744
|
+
operator: f.operator,
|
|
33745
|
+
comparisonValue: f.comparisonValue
|
|
33746
|
+
}));
|
|
33747
|
+
this.followerFilterCount = selector.followsSelectorId ? this.editFilters.length : 0;
|
|
33748
|
+
this.defaultFilterCheck = { state: 'none' };
|
|
32530
33749
|
// A follower picks from its leader's CK type; re-sync it in case the leader's
|
|
32531
33750
|
// type changed after the follower was saved.
|
|
32532
33751
|
const leader = resolveFollowLeader(this.entitySelectors, selector);
|
|
@@ -32534,6 +33753,7 @@ class EntitySelectorEditorComponent {
|
|
|
32534
33753
|
this.applyLeaderCkType(leader.leader.ckTypeId);
|
|
32535
33754
|
}
|
|
32536
33755
|
this.updateDefaultEntityDataSources();
|
|
33756
|
+
this.requestDefaultCheck();
|
|
32537
33757
|
this.isEditing = true;
|
|
32538
33758
|
this.editingStateChange.emit(true);
|
|
32539
33759
|
}
|
|
@@ -32593,6 +33813,10 @@ class EntitySelectorEditorComponent {
|
|
|
32593
33813
|
this.editFollowsSelectorId = leaderId;
|
|
32594
33814
|
this.followNotice = '';
|
|
32595
33815
|
this.editShowInToolbar = false;
|
|
33816
|
+
// A follower has no picker: its filter rows are ignored and dropped on save (AB#7724).
|
|
33817
|
+
this.followerFilterCount = leaderId ? this.editFilters.length : 0;
|
|
33818
|
+
this.requestDefaultCheck();
|
|
33819
|
+
this.updateDefaultEntityDataSources();
|
|
32596
33820
|
if (!leaderId)
|
|
32597
33821
|
return;
|
|
32598
33822
|
const leader = this.entitySelectors.find(s => s.id === leaderId);
|
|
@@ -32615,6 +33839,8 @@ class EntitySelectorEditorComponent {
|
|
|
32615
33839
|
this.editCkTypeItem = { fullName: ckTypeId, rtCkTypeId: ckTypeId, isAbstract: false, isFinal: false };
|
|
32616
33840
|
this.editMappings = [];
|
|
32617
33841
|
this.editDefaultRtId = '';
|
|
33842
|
+
this.editFilters = [];
|
|
33843
|
+
this.followerFilterCount = 0;
|
|
32618
33844
|
}
|
|
32619
33845
|
/**
|
|
32620
33846
|
* Handles CK type selection in the editor form.
|
|
@@ -32622,10 +33848,12 @@ class EntitySelectorEditorComponent {
|
|
|
32622
33848
|
onCkTypeSelected(ckType) {
|
|
32623
33849
|
this.editCkTypeId = ckType.rtCkTypeId;
|
|
32624
33850
|
this.editCkTypeItem = ckType;
|
|
32625
|
-
// Clear mappings
|
|
33851
|
+
// Clear mappings, default entity and filter rows when CK type changes
|
|
32626
33852
|
this.editMappings = [];
|
|
32627
33853
|
this.editDefaultRtId = '';
|
|
33854
|
+
this.editFilters = [];
|
|
32628
33855
|
this.updateDefaultEntityDataSources();
|
|
33856
|
+
this.requestDefaultCheck();
|
|
32629
33857
|
}
|
|
32630
33858
|
/**
|
|
32631
33859
|
* Handles CK type cleared in the editor form.
|
|
@@ -32635,8 +33863,10 @@ class EntitySelectorEditorComponent {
|
|
|
32635
33863
|
this.editCkTypeItem = null;
|
|
32636
33864
|
this.editMappings = [];
|
|
32637
33865
|
this.editDefaultRtId = '';
|
|
33866
|
+
this.editFilters = [];
|
|
32638
33867
|
this.defaultEntityDataSource = null;
|
|
32639
33868
|
this.defaultEntityDialogDataSource = null;
|
|
33869
|
+
this.requestDefaultCheck();
|
|
32640
33870
|
}
|
|
32641
33871
|
/**
|
|
32642
33872
|
* Opens the attribute selector dialog to pick attributes for variable mappings.
|
|
@@ -32657,31 +33887,70 @@ class EntitySelectorEditorComponent {
|
|
|
32657
33887
|
};
|
|
32658
33888
|
});
|
|
32659
33889
|
}
|
|
32660
|
-
/**
|
|
32661
|
-
|
|
32662
|
-
|
|
32663
|
-
onChildScopeCkTypeSelected(ckType) {
|
|
32664
|
-
this.editChildScopeTargetCkTypeId = ckType.rtCkTypeId;
|
|
32665
|
-
this.editChildScopeTargetCkTypeItem = ckType;
|
|
33890
|
+
/** Takes the hop list of the child-scope editor (AB#7723). */
|
|
33891
|
+
onScopeHopsChanged(hops) {
|
|
33892
|
+
this.editScopeHops = hops;
|
|
32666
33893
|
}
|
|
32667
|
-
/**
|
|
32668
|
-
|
|
32669
|
-
|
|
32670
|
-
onChildScopeCkTypeCleared() {
|
|
32671
|
-
this.editChildScopeTargetCkTypeId = '';
|
|
32672
|
-
this.editChildScopeTargetCkTypeItem = null;
|
|
33894
|
+
/** Validation message of the hop list, or '' when it can be saved. */
|
|
33895
|
+
get scopeHopsError() {
|
|
33896
|
+
return scopeHopsValidationError(this.editScopeHops) ?? '';
|
|
32673
33897
|
}
|
|
32674
33898
|
/**
|
|
32675
33899
|
* Handles default entity selection from the entity picker.
|
|
32676
33900
|
*/
|
|
32677
33901
|
onDefaultEntitySelected(entity) {
|
|
32678
33902
|
this.editDefaultRtId = entity.rtId;
|
|
33903
|
+
this.requestDefaultCheck();
|
|
32679
33904
|
}
|
|
32680
33905
|
/**
|
|
32681
33906
|
* Handles default entity cleared.
|
|
32682
33907
|
*/
|
|
32683
33908
|
onDefaultEntityCleared() {
|
|
32684
33909
|
this.editDefaultRtId = '';
|
|
33910
|
+
this.requestDefaultCheck();
|
|
33911
|
+
}
|
|
33912
|
+
/** Takes the rows of the picker filter editor (AB#7724). */
|
|
33913
|
+
onFiltersChange(filters) {
|
|
33914
|
+
this.editFilters = filters;
|
|
33915
|
+
this.updateDefaultEntityDataSources();
|
|
33916
|
+
this.requestDefaultCheck();
|
|
33917
|
+
}
|
|
33918
|
+
/**
|
|
33919
|
+
* Variables offered in filter values: the board's static variables and the
|
|
33920
|
+
* variables other selectors map. The edited selector's own mappings are
|
|
33921
|
+
* left out — they are filled from its own pick.
|
|
33922
|
+
*/
|
|
33923
|
+
get filterVariables() {
|
|
33924
|
+
const result = new Map();
|
|
33925
|
+
for (const v of this.variables) {
|
|
33926
|
+
result.set(v.name, { name: v.name, label: v.label || v.name, type: v.type });
|
|
33927
|
+
}
|
|
33928
|
+
this.entitySelectors
|
|
33929
|
+
.filter((_, i) => i !== this.editingIndex)
|
|
33930
|
+
.forEach(es => es.attributeMappings.forEach(m => {
|
|
33931
|
+
if (!result.has(m.variableName)) {
|
|
33932
|
+
result.set(m.variableName, { name: m.variableName, label: `${m.variableName} (${es.label || es.id})` });
|
|
33933
|
+
}
|
|
33934
|
+
}));
|
|
33935
|
+
return [...result.values()];
|
|
33936
|
+
}
|
|
33937
|
+
/** "2 filters" chip of a selector in the list, or '' without filter. */
|
|
33938
|
+
getFilterSummary(selector) {
|
|
33939
|
+
const count = selector.followsSelectorId ? 0 : selector.filters?.length ?? 0;
|
|
33940
|
+
if (count === 0)
|
|
33941
|
+
return '';
|
|
33942
|
+
return count === 1 ? '1 filter' : `${count} filters`;
|
|
33943
|
+
}
|
|
33944
|
+
/** "$a, $b" of the variables the default-entity check cannot substitute. */
|
|
33945
|
+
get uncheckedVariablesText() {
|
|
33946
|
+
return this.defaultFilterCheck.state === 'unchecked'
|
|
33947
|
+
? this.defaultFilterCheck.variables.map(v => `$${v}`).join(', ')
|
|
33948
|
+
: '';
|
|
33949
|
+
}
|
|
33950
|
+
/** Label of the leader the edited follower takes its pick from. */
|
|
33951
|
+
get leaderLabel() {
|
|
33952
|
+
const leader = this.entitySelectors.find(s => s.id === this.editFollowsSelectorId);
|
|
33953
|
+
return leader ? (leader.label || leader.id) : this.editFollowsSelectorId;
|
|
32685
33954
|
}
|
|
32686
33955
|
/**
|
|
32687
33956
|
* Saves the current entity selector being edited.
|
|
@@ -32689,15 +33958,10 @@ class EntitySelectorEditorComponent {
|
|
|
32689
33958
|
saveEdit() {
|
|
32690
33959
|
if (!this.isEditValid())
|
|
32691
33960
|
return;
|
|
32692
|
-
//
|
|
32693
|
-
//
|
|
32694
|
-
|
|
32695
|
-
|
|
32696
|
-
targetCkTypeId: this.editChildScopeTargetCkTypeId,
|
|
32697
|
-
roleId: this.editChildScopeRoleId.trim(),
|
|
32698
|
-
direction: this.editChildScopeDirection
|
|
32699
|
-
}
|
|
32700
|
-
: undefined;
|
|
33961
|
+
// No hops = no childScope (the picked entity itself is the scope); one hop
|
|
33962
|
+
// in the original top-level form (older library versions keep reading it);
|
|
33963
|
+
// two or more hops as `path` (AB#7723).
|
|
33964
|
+
const childScope = childScopeFromHops(this.editScopeHops);
|
|
32701
33965
|
const selector = {
|
|
32702
33966
|
id: this.editId,
|
|
32703
33967
|
label: this.editLabel.trim(),
|
|
@@ -32706,6 +33970,8 @@ class EntitySelectorEditorComponent {
|
|
|
32706
33970
|
showInToolbar: this.editShowInToolbar,
|
|
32707
33971
|
defaultRtId: this.editDefaultRtId.trim() || undefined,
|
|
32708
33972
|
childScope,
|
|
33973
|
+
// Picker filter (AB#7724); omitted when empty and never written for a follower.
|
|
33974
|
+
filters: !this.editFollowsSelectorId && this.editFilters.length > 0 ? this.toFilterConfigs(this.editFilters) : undefined,
|
|
32709
33975
|
// A follower never renders a picker (AB#7722).
|
|
32710
33976
|
...(this.editFollowsSelectorId ? { showInToolbar: false, followsSelectorId: this.editFollowsSelectorId } : {})
|
|
32711
33977
|
};
|
|
@@ -32749,6 +34015,10 @@ class EntitySelectorEditorComponent {
|
|
|
32749
34015
|
if (this.followError) {
|
|
32750
34016
|
return false;
|
|
32751
34017
|
}
|
|
34018
|
+
// Every hop needs an association and a target type; at most 5 hops (AB#7723).
|
|
34019
|
+
if (this.scopeHopsError) {
|
|
34020
|
+
return false;
|
|
34021
|
+
}
|
|
32752
34022
|
return hasRequiredFields;
|
|
32753
34023
|
}
|
|
32754
34024
|
/**
|
|
@@ -32827,19 +34097,74 @@ class EntitySelectorEditorComponent {
|
|
|
32827
34097
|
}
|
|
32828
34098
|
updateDefaultEntityDataSources() {
|
|
32829
34099
|
if (this.editCkTypeId) {
|
|
32830
|
-
|
|
32831
|
-
|
|
34100
|
+
// The default-entity picker lists what the toolbar picker lists (AB#7724).
|
|
34101
|
+
const gql = withPickerFilters(this.getEntitiesByCkTypeGQL, this.resolvedEditorFilters().filters);
|
|
34102
|
+
this.defaultEntityDataSource = new RuntimeEntitySelectDataSource(gql, this.editCkTypeId);
|
|
34103
|
+
this.defaultEntityDialogDataSource = new RuntimeEntityDialogDataSource(gql, this.editCkTypeId);
|
|
32832
34104
|
}
|
|
32833
34105
|
else {
|
|
32834
34106
|
this.defaultEntityDataSource = null;
|
|
32835
34107
|
this.defaultEntityDialogDataSource = null;
|
|
32836
34108
|
}
|
|
32837
34109
|
}
|
|
34110
|
+
toFilterConfigs(rows) {
|
|
34111
|
+
return rows.map(f => ({
|
|
34112
|
+
attributePath: f.attributePath,
|
|
34113
|
+
operator: f.operator,
|
|
34114
|
+
comparisonValue: f.comparisonValue == null ? '' : String(f.comparisonValue)
|
|
34115
|
+
}));
|
|
34116
|
+
}
|
|
34117
|
+
/**
|
|
34118
|
+
* The filter rows that apply in the editor, with the board's static
|
|
34119
|
+
* variables substituted. Rows whose value still names a variable without a
|
|
34120
|
+
* value (e.g. one a selector pick fills at view time) are left out and
|
|
34121
|
+
* reported in `unresolved`; the editor cannot know their value.
|
|
34122
|
+
*/
|
|
34123
|
+
resolvedEditorFilters() {
|
|
34124
|
+
if (this.isFollower || this.editFilters.length === 0) {
|
|
34125
|
+
return { filters: undefined, unresolved: [] };
|
|
34126
|
+
}
|
|
34127
|
+
const converted = this.variableService.convertToFieldFilterDto(this.toFilterConfigs(this.editFilters), this.variables) ?? [];
|
|
34128
|
+
const unresolved = unresolvedFilterVariables(converted);
|
|
34129
|
+
const applicable = converted.filter(f => unresolvedFilterVariables([f]).length === 0);
|
|
34130
|
+
return { filters: applicable.length > 0 ? applicable : undefined, unresolved };
|
|
34131
|
+
}
|
|
34132
|
+
/** Queues the (debounced) check whether the default entity matches the filter. */
|
|
34133
|
+
requestDefaultCheck() {
|
|
34134
|
+
const rtId = this.editDefaultRtId.trim();
|
|
34135
|
+
if (!rtId || !this.editCkTypeId || this.isFollower || this.editFilters.length === 0) {
|
|
34136
|
+
this.defaultCheckRequests.next({ state: 'none' });
|
|
34137
|
+
return;
|
|
34138
|
+
}
|
|
34139
|
+
const { filters, unresolved } = this.resolvedEditorFilters();
|
|
34140
|
+
if (unresolved.length > 0) {
|
|
34141
|
+
this.defaultCheckRequests.next({ state: 'unchecked', variables: unresolved });
|
|
34142
|
+
return;
|
|
34143
|
+
}
|
|
34144
|
+
this.defaultCheckRequests.next({ ckTypeId: this.editCkTypeId, rtId, filters: filters ?? [] });
|
|
34145
|
+
}
|
|
34146
|
+
/** Asks the server whether the entity is among the filtered ones (rtId + filter, first 1). */
|
|
34147
|
+
checkDefaultEntity(request) {
|
|
34148
|
+
return this.getEntitiesByCkTypeGQL.fetch({
|
|
34149
|
+
variables: {
|
|
34150
|
+
ckTypeId: request.ckTypeId,
|
|
34151
|
+
attributeNames: [],
|
|
34152
|
+
rtId: request.rtId,
|
|
34153
|
+
first: 1,
|
|
34154
|
+
fieldFilters: request.filters
|
|
34155
|
+
},
|
|
34156
|
+
fetchPolicy: 'network-only'
|
|
34157
|
+
}).pipe(map$1(result => {
|
|
34158
|
+
const count = result.data?.runtime?.runtimeEntities?.totalCount
|
|
34159
|
+
?? result.data?.runtime?.runtimeEntities?.items?.length ?? 0;
|
|
34160
|
+
return (count > 0 ? { state: 'match' } : { state: 'mismatch' });
|
|
34161
|
+
}), catchError$1(() => of({ state: 'error' })));
|
|
34162
|
+
}
|
|
32838
34163
|
emitChange() {
|
|
32839
34164
|
this.entitySelectorsChange.emit([...this.entitySelectors]);
|
|
32840
34165
|
}
|
|
32841
34166
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: EntitySelectorEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
32842
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: EntitySelectorEditorComponent, isStandalone: true, selector: "mm-entity-selector-editor", inputs: { entitySelectors: "entitySelectors", existingVariableNames: "existingVariableNames" }, outputs: { entitySelectorsChange: "entitySelectorsChange", editingStateChange: "editingStateChange" }, ngImport: i0, template: "<div class=\"entity-selector-editor\">\n @if (!isEditing) {\n <!-- Selector List -->\n <div class=\"selector-list\">\n @for (selector of entitySelectors; track selector.id; let i = $index) {\n <div class=\"selector-item\">\n <div class=\"selector-info\">\n <span class=\"selector-label\">{{ selector.label }}</span>\n <span class=\"selector-mode\" [class.selector-mode-error]=\"isOrphanFollower(selector)\">{{ getModeLabel(selector) }}</span>\n <span class=\"selector-ck-type\">{{ selector.ckTypeId }}</span>\n <span class=\"selector-mappings\">{{ getMappingSummary(selector) }}</span>\n </div>\n <div class=\"selector-actions\">\n <button\n kendoButton\n [svgIcon]=\"pencilIcon\"\n (click)=\"editSelector(i)\"\n fillMode=\"flat\"\n size=\"small\"\n title=\"Edit selector\"\n [attr.aria-label]=\"'Edit selector ' + (selector.label || selector.ckTypeId)\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"trashIcon\"\n (click)=\"removeSelector(i)\"\n fillMode=\"flat\"\n size=\"small\"\n themeColor=\"error\"\n title=\"Remove selector\"\n [attr.aria-label]=\"'Remove selector ' + (selector.label || selector.ckTypeId)\"\n class=\"remove-btn\">\n </button>\n </div>\n </div>\n }\n\n @if (entitySelectors.length === 0) {\n <div class=\"empty-state\">\n No entity selectors configured. Add one to enable entity-based variables.\n </div>\n }\n </div>\n\n <div class=\"add-button-row\">\n <button\n kendoButton\n [svgIcon]=\"plusIcon\"\n (click)=\"addSelector()\"\n themeColor=\"primary\">\n Add Entity Selector\n </button>\n </div>\n } @else {\n <!-- Edit Form -->\n <div class=\"edit-panel\">\n <div class=\"edit-panel-header\">\n {{ editingIndex !== null ? 'Edit Entity Selector' : 'New Entity Selector' }}\n </div>\n <div class=\"edit-form\">\n <div class=\"form-field\">\n <label class=\"field-label\">ID *</label>\n <kendo-textbox\n [(ngModel)]=\"editId\"\n placeholder=\"e.g., mp\"\n [readonly]=\"editingIndex !== null\">\n </kendo-textbox>\n @if (isDuplicateId(editId, editingIndex)) {\n <span class=\"field-error\">ID must be unique</span>\n }\n <span class=\"field-hint\">Used in URL parameters (es_<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 });
|
|
34167
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: EntitySelectorEditorComponent, isStandalone: true, selector: "mm-entity-selector-editor", inputs: { entitySelectors: "entitySelectors", existingVariableNames: "existingVariableNames", variables: "variables" }, outputs: { entitySelectorsChange: "entitySelectorsChange", editingStateChange: "editingStateChange" }, ngImport: i0, template: "<div class=\"entity-selector-editor\">\n @if (!isEditing) {\n <!-- Selector List -->\n <div class=\"selector-list\">\n @for (selector of entitySelectors; track selector.id; let i = $index) {\n <div class=\"selector-item\">\n <div class=\"selector-info\">\n <span class=\"selector-label\">{{ selector.label }}</span>\n <span class=\"selector-mode\" [class.selector-mode-error]=\"isOrphanFollower(selector)\">{{ getModeLabel(selector) }}</span>\n @if (getFilterSummary(selector); as filterSummary) {\n <span class=\"selector-mode selector-filter-chip\">{{ filterSummary }}</span>\n }\n <span class=\"selector-ck-type\">{{ selector.ckTypeId }}</span>\n <span class=\"selector-mappings\">{{ getMappingSummary(selector) }}</span>\n </div>\n <div class=\"selector-actions\">\n <button\n kendoButton\n [svgIcon]=\"pencilIcon\"\n (click)=\"editSelector(i)\"\n fillMode=\"flat\"\n size=\"small\"\n title=\"Edit selector\"\n [attr.aria-label]=\"'Edit selector ' + (selector.label || selector.ckTypeId)\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"trashIcon\"\n (click)=\"removeSelector(i)\"\n fillMode=\"flat\"\n size=\"small\"\n themeColor=\"error\"\n title=\"Remove selector\"\n [attr.aria-label]=\"'Remove selector ' + (selector.label || selector.ckTypeId)\"\n class=\"remove-btn\">\n </button>\n </div>\n </div>\n }\n\n @if (entitySelectors.length === 0) {\n <div class=\"empty-state\">\n No entity selectors configured. Add one to enable entity-based variables.\n </div>\n }\n </div>\n\n <div class=\"add-button-row\">\n <button\n kendoButton\n [svgIcon]=\"plusIcon\"\n (click)=\"addSelector()\"\n themeColor=\"primary\">\n Add Entity Selector\n </button>\n </div>\n } @else {\n <!-- Edit Form -->\n <div class=\"edit-panel\">\n <div class=\"edit-panel-header\">\n {{ editingIndex !== null ? 'Edit Entity Selector' : 'New Entity Selector' }}\n </div>\n <div class=\"edit-form\">\n <div class=\"form-field\">\n <label class=\"field-label\">ID *</label>\n <kendo-textbox\n [(ngModel)]=\"editId\"\n placeholder=\"e.g., mp\"\n [readonly]=\"editingIndex !== null\">\n </kendo-textbox>\n @if (isDuplicateId(editId, editingIndex)) {\n <span class=\"field-error\">ID must be unique</span>\n }\n <span class=\"field-hint\">Used in URL parameters (es_<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 @if (editCkTypeId) {\n <div class=\"form-field picker-filter-field\">\n <label class=\"field-label\">Picker filter</label>\n @if (isFollower) {\n <span class=\"field-hint\">A follower takes the leader's pick. Set the filter on {{ leaderLabel }}.</span>\n @if (followerFilterCount > 0) {\n <span class=\"field-hint follow-notice\">Filters on a follower are ignored and are removed when you save.</span>\n }\n } @else {\n <span class=\"field-hint\">\n Only entities that match all rows are listed in the toolbar picker and in the default-entity picker below.\n </span>\n <mm-field-filter-editor\n [ckTypeId]=\"editCkTypeId\"\n [filters]=\"editFilters\"\n [enableVariables]=\"filterVariables.length > 0\"\n [availableVariables]=\"filterVariables\"\n (filtersChange)=\"onFiltersChange($event)\">\n </mm-field-filter-editor>\n }\n </div>\n }\n\n <div class=\"form-field\">\n <label class=\"field-label\">Attribute Mappings{{ isFollower ? '' : ' *' }}</label>\n <button\n kendoButton\n (click)=\"selectAttributes()\"\n [disabled]=\"!editCkTypeId\"\n fillMode=\"outline\"\n size=\"small\">\n Select Attributes...\n </button>\n\n @if (editMappings.length > 0) {\n <div class=\"mappings-list\">\n <div class=\"mappings-header\">\n <span class=\"mappings-header-attr\">Attribute</span>\n <span class=\"mappings-header-var\">Variable Name</span>\n </div>\n @for (mapping of editMappings; track mapping.attributePath; let j = $index) {\n <div class=\"mapping-item\">\n <span class=\"mapping-path\">{{ mapping.attributePath }}</span>\n <span class=\"mapping-arrow\">→</span>\n <div class=\"mapping-var-field\">\n <div class=\"mapping-var-input-wrapper\">\n <span class=\"mapping-var-prefix\">$</span>\n <kendo-textbox\n [(ngModel)]=\"mapping.variableName\"\n size=\"small\"\n class=\"mapping-var-input\"\n placeholder=\"variableName\"\n [class.invalid]=\"getVariableNameError(mapping)\">\n </kendo-textbox>\n </div>\n @if (getVariableNameError(mapping); as error) {\n <span class=\"field-error\">{{ error }}</span>\n }\n </div>\n </div>\n }\n </div>\n }\n </div>\n\n @if (!isFollower) {\n <div class=\"form-field\">\n <div class=\"checkbox-row\">\n <input\n type=\"checkbox\"\n kendoCheckBox\n [(ngModel)]=\"editShowInToolbar\"\n id=\"showInToolbar\" />\n <label class=\"field-label checkbox-label\" for=\"showInToolbar\">Show in Toolbar</label>\n </div>\n <span class=\"field-hint\">\n @if (editShowInToolbar) {\n Users can select an entity from a dropdown in the toolbar.\n } @else {\n Hidden selector \u2014 variables are resolved from the default entity below.\n }\n </span>\n </div>\n }\n\n <div class=\"form-field\">\n <label class=\"field-label\">Default Entity{{ !editShowInToolbar && !isFollower ? ' *' : '' }}</label>\n @if (defaultEntityDataSource) {\n <mm-entity-select-input\n [dataSource]=\"defaultEntityDataSource\"\n [dialogDataSource]=\"defaultEntityDialogDataSource!\"\n [placeholder]=\"'Select default entity...'\"\n [dialogTitle]=\"'Select Default ' + editLabel\"\n (entitySelected)=\"onDefaultEntitySelected($event)\"\n (entityCleared)=\"onDefaultEntityCleared()\">\n </mm-entity-select-input>\n } @else {\n <span class=\"field-hint\">Select a CK type first to pick a default entity.</span>\n }\n @if (editDefaultRtId) {\n <span class=\"field-hint\">Selected: {{ editDefaultRtId }}</span>\n }\n @switch (defaultFilterCheck.state) {\n @case ('mismatch') {\n <span class=\"field-warning default-filter-warning\">The default entity does not match the picker filter. It still loads, but users cannot pick it again.</span>\n }\n @case ('error') {\n <span class=\"field-hint default-filter-warning\">Could not check the default entity.</span>\n }\n @case ('unchecked') {\n <span class=\"field-hint default-filter-warning\">Cannot check the default entity: the filter uses {{ uncheckedVariablesText }}, which is filled at view time.</span>\n }\n }\n @if (!editShowInToolbar && !isFollower && !editDefaultRtId) {\n <span class=\"field-error\">A default entity is required when not shown in toolbar.</span>\n }\n <span class=\"field-hint\">\n @if (isFollower) {\n Optional \u2014 used only while the leader has no pick.\n } @else if (editShowInToolbar) {\n Optional \u2014 pre-selected entity when the MeshBoard loads.\n } @else {\n This entity's attributes will be used to populate the variables.\n }\n </span>\n </div>\n\n <div class=\"form-field\">\n <label class=\"field-label\">Child Scope (stream-data)</label>\n <mm-entity-selector-scope-path-editor\n [sourceCkTypeId]=\"editCkTypeId\"\n [hops]=\"editScopeHops\"\n (hopsChange)=\"onScopeHopsChanged($event)\">\n </mm-entity-selector-scope-path-editor>\n </div>\n\n <div class=\"edit-actions mm-dialog-actions\">\n <button kendoButton (click)=\"cancelEdit()\" fillMode=\"flat\">\n Cancel\n </button>\n <button\n kendoButton\n (click)=\"saveEdit()\"\n [disabled]=\"!isEditValid()\"\n themeColor=\"primary\">\n {{ editingIndex !== null ? 'Update' : 'Add' }}\n </button>\n </div>\n </div>\n </div>\n }\n</div>\n", styles: [".entity-selector-editor{display:flex;flex-direction:column;gap:1.25rem}.selector-list{display:flex;flex-direction:column;gap:.5rem}.selector-item{display:flex;justify-content:space-between;align-items:center;padding:.75rem 1rem;background:var(--kendo-color-surface-alt, #fafafa);border:1px solid var(--kendo-color-border, #e0e0e0);border-radius:4px}.selector-item .selector-info{display:flex;flex-direction:column;gap:.25rem;min-width:0;flex:1}.selector-item .selector-label{font-weight:500;color:var(--kendo-color-on-app-surface, #424242)}.selector-item .selector-mode{font-size:.75rem;font-weight:500;color:var(--kendo-color-info, #0288d1)}.selector-item .selector-mode-error{color:var(--kendo-color-error, #d32f2f)}.selector-item .selector-filter-chip{color:var(--kendo-color-subtle, #757575)}.selector-item .selector-ck-type{font-size:.8rem;color:var(--kendo-color-subtle, #757575);font-family:var(--theme-font-mono)}.selector-item .selector-mappings{font-size:.8rem;color:var(--kendo-color-primary, #3f51b5);font-family:var(--theme-font-mono)}.selector-item .selector-actions{display:flex;gap:.25rem;flex-shrink:0}.selector-item .remove-btn{color:var(--kendo-color-error, #f44336)}.empty-state{padding:1.5rem;text-align:center;color:var(--kendo-color-subtle, #757575);font-size:.875rem}.add-button-row{display:flex;justify-content:flex-start}.edit-panel{border:1px solid var(--kendo-color-border, #e0e0e0);border-radius:4px;overflow:hidden}.edit-panel-header{padding:.625rem 1rem;font-weight:600;font-size:.875rem;color:var(--kendo-color-on-primary, #ffffff);background:var(--kendo-color-primary, #3f51b5)}.edit-form{display:flex;flex-direction:column;gap:1.25rem;padding:1.25rem}.edit-form .form-field{display:flex;flex-direction:column;gap:.5rem}.edit-form .field-label{font-weight:500;font-size:.875rem;color:var(--kendo-color-on-app-surface, #424242)}.edit-form .field-hint{font-size:.75rem;color:var(--kendo-color-subtle, #757575)}.edit-form .field-error{font-size:.75rem;color:var(--kendo-color-error, #f44336)}.edit-form .field-warning{font-size:.75rem;color:var(--kendo-color-warning, #f57c00)}.edit-form .checkbox-row{display:flex;align-items:center;gap:.5rem}.edit-form .checkbox-row .checkbox-label{margin:0;cursor:pointer}.mappings-list{display:flex;flex-direction:column;gap:.5rem;margin-top:.5rem;padding:.75rem;background:var(--kendo-color-surface-alt, #fafafa);border:1px solid var(--kendo-color-border, #e0e0e0);border-radius:4px}.mappings-header{display:flex;align-items:center;gap:.5rem;font-size:.75rem;font-weight:600;color:var(--kendo-color-subtle, #757575);padding-bottom:.25rem;border-bottom:1px solid var(--kendo-color-border, #e0e0e0)}.mappings-header .mappings-header-attr{min-width:150px}.mappings-header .mappings-header-var{margin-left:1.75rem}.mapping-item{display:flex;align-items:flex-start;gap:.5rem}.mapping-item .mapping-path{font-size:.8rem;font-family:var(--theme-font-mono);color:var(--kendo-color-subtle, #757575);min-width:150px;padding-top:5px}.mapping-item .mapping-arrow{color:var(--kendo-color-subtle, #757575);flex-shrink:0;padding-top:5px}.mapping-item .mapping-var-field{display:flex;flex-direction:column;gap:2px;flex:1;min-width:100px}.mapping-item .mapping-var-input-wrapper{display:flex;align-items:center;gap:0}.mapping-item .mapping-var-prefix{font-family:var(--theme-font-mono);font-size:.875rem;font-weight:600;color:var(--kendo-color-primary, #3f51b5);padding-right:2px;flex-shrink:0}.mapping-item .mapping-var-input{flex:1}.mapping-item .mapping-var-input.invalid ::ng-deep .k-input{border-color:var(--kendo-color-error, #f44336)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox]:not([ngNoCva])[formControlName],input[type=checkbox]:not([ngNoCva])[formControl],input[type=checkbox]:not([ngNoCva])[ngModel]" }, { kind: "directive", type: i1$1.SelectControlValueAccessor, selector: "select:not([multiple]):not([ngNoCva])[formControlName],select:not([multiple]):not([ngNoCva])[formControl],select:not([multiple]):not([ngNoCva])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: InputsModule }, { kind: "component", type: i3.TextBoxComponent, selector: "kendo-textbox", inputs: ["focusableId", "title", "type", "disabled", "readonly", "tabindex", "value", "selectOnFocus", "showSuccessIcon", "showErrorIcon", "clearButton", "successIcon", "successSvgIcon", "errorIcon", "errorSvgIcon", "clearButtonIcon", "clearButtonSvgIcon", "size", "rounded", "fillMode", "tabIndex", "placeholder", "maxlength", "inputAttributes"], outputs: ["valueChange", "inputFocus", "inputBlur", "focus", "blur"], exportAs: ["kendoTextBox"] }, { kind: "directive", type: i3.CheckBoxDirective, selector: "input[kendoCheckBox]", inputs: ["size", "rounded"] }, { kind: "ngmodule", type: LabelModule }, { kind: "directive", type: i4$1.LabelDirective, selector: "label[for]", inputs: ["for", "labelClass"] }, { kind: "ngmodule", type: SVGIconModule }, { kind: "component", type: CkTypeSelectorInputComponent, selector: "mm-ck-type-selector-input", inputs: ["placeholder", "minSearchLength", "maxResults", "debounceMs", "ckModelIds", "allowAbstract", "dialogTitle", "advancedSearchLabel", "derivedFromRtCkTypeId", "messages", "dialogMessages", "disabled", "required"], outputs: ["ckTypeSelected", "ckTypeCleared"] }, { kind: "component", type: EntitySelectInputComponent, selector: "mm-entity-select-input", inputs: ["dataSource", "placeholder", "minSearchLength", "maxResults", "debounceMs", "prefix", "initialDisplayValue", "dialogDataSource", "dialogTitle", "multiSelect", "advancedSearchLabel", "dialogMessages", "messages", "disabled", "required"], outputs: ["entitySelected", "entityCleared", "entitiesSelected"] }, { kind: "component", type: EntitySelectorScopePathEditorComponent, selector: "mm-entity-selector-scope-path-editor", inputs: ["sourceCkTypeId", "hops"], outputs: ["hopsChange"] }, { kind: "component", type: FieldFilterEditorComponent, selector: "mm-field-filter-editor", inputs: ["availableAttributes", "ckTypeId", "hideNavigationProperties", "attributePaths", "enableVariables", "availableVariables", "filters"], outputs: ["filtersChange"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
|
|
32843
34168
|
}
|
|
32844
34169
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: EntitySelectorEditorComponent, decorators: [{
|
|
32845
34170
|
type: Component,
|
|
@@ -32851,38 +34176,60 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
32851
34176
|
LabelModule,
|
|
32852
34177
|
SVGIconModule,
|
|
32853
34178
|
CkTypeSelectorInputComponent,
|
|
32854
|
-
EntitySelectInputComponent
|
|
32855
|
-
], changeDetection: ChangeDetectionStrategy.Eager, template: "<div class=\"entity-selector-editor\">\n @if (!isEditing) {\n <!-- Selector List -->\n <div class=\"selector-list\">\n @for (selector of entitySelectors; track selector.id; let i = $index) {\n <div class=\"selector-item\">\n <div class=\"selector-info\">\n <span class=\"selector-label\">{{ selector.label }}</span>\n <span class=\"selector-mode\" [class.selector-mode-error]=\"isOrphanFollower(selector)\">{{ getModeLabel(selector) }}</span>\n <span class=\"selector-ck-type\">{{ selector.ckTypeId }}</span>\n <span class=\"selector-mappings\">{{ getMappingSummary(selector) }}</span>\n </div>\n <div class=\"selector-actions\">\n <button\n kendoButton\n [svgIcon]=\"pencilIcon\"\n (click)=\"editSelector(i)\"\n fillMode=\"flat\"\n size=\"small\"\n title=\"Edit selector\"\n [attr.aria-label]=\"'Edit selector ' + (selector.label || selector.ckTypeId)\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"trashIcon\"\n (click)=\"removeSelector(i)\"\n fillMode=\"flat\"\n size=\"small\"\n themeColor=\"error\"\n title=\"Remove selector\"\n [attr.aria-label]=\"'Remove selector ' + (selector.label || selector.ckTypeId)\"\n class=\"remove-btn\">\n </button>\n </div>\n </div>\n }\n\n @if (entitySelectors.length === 0) {\n <div class=\"empty-state\">\n No entity selectors configured. Add one to enable entity-based variables.\n </div>\n }\n </div>\n\n <div class=\"add-button-row\">\n <button\n kendoButton\n [svgIcon]=\"plusIcon\"\n (click)=\"addSelector()\"\n themeColor=\"primary\">\n Add Entity Selector\n </button>\n </div>\n } @else {\n <!-- Edit Form -->\n <div class=\"edit-panel\">\n <div class=\"edit-panel-header\">\n {{ editingIndex !== null ? 'Edit Entity Selector' : 'New Entity Selector' }}\n </div>\n <div class=\"edit-form\">\n <div class=\"form-field\">\n <label class=\"field-label\">ID *</label>\n <kendo-textbox\n [(ngModel)]=\"editId\"\n placeholder=\"e.g., mp\"\n [readonly]=\"editingIndex !== null\">\n </kendo-textbox>\n @if (isDuplicateId(editId, editingIndex)) {\n <span class=\"field-error\">ID must be unique</span>\n }\n <span class=\"field-hint\">Used in URL parameters (es_<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"] }]
|
|
32856
|
-
|
|
34179
|
+
EntitySelectInputComponent,
|
|
34180
|
+
EntitySelectorScopePathEditorComponent,
|
|
34181
|
+
FieldFilterEditorComponent
|
|
34182
|
+
], changeDetection: ChangeDetectionStrategy.Eager, template: "<div class=\"entity-selector-editor\">\n @if (!isEditing) {\n <!-- Selector List -->\n <div class=\"selector-list\">\n @for (selector of entitySelectors; track selector.id; let i = $index) {\n <div class=\"selector-item\">\n <div class=\"selector-info\">\n <span class=\"selector-label\">{{ selector.label }}</span>\n <span class=\"selector-mode\" [class.selector-mode-error]=\"isOrphanFollower(selector)\">{{ getModeLabel(selector) }}</span>\n @if (getFilterSummary(selector); as filterSummary) {\n <span class=\"selector-mode selector-filter-chip\">{{ filterSummary }}</span>\n }\n <span class=\"selector-ck-type\">{{ selector.ckTypeId }}</span>\n <span class=\"selector-mappings\">{{ getMappingSummary(selector) }}</span>\n </div>\n <div class=\"selector-actions\">\n <button\n kendoButton\n [svgIcon]=\"pencilIcon\"\n (click)=\"editSelector(i)\"\n fillMode=\"flat\"\n size=\"small\"\n title=\"Edit selector\"\n [attr.aria-label]=\"'Edit selector ' + (selector.label || selector.ckTypeId)\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"trashIcon\"\n (click)=\"removeSelector(i)\"\n fillMode=\"flat\"\n size=\"small\"\n themeColor=\"error\"\n title=\"Remove selector\"\n [attr.aria-label]=\"'Remove selector ' + (selector.label || selector.ckTypeId)\"\n class=\"remove-btn\">\n </button>\n </div>\n </div>\n }\n\n @if (entitySelectors.length === 0) {\n <div class=\"empty-state\">\n No entity selectors configured. Add one to enable entity-based variables.\n </div>\n }\n </div>\n\n <div class=\"add-button-row\">\n <button\n kendoButton\n [svgIcon]=\"plusIcon\"\n (click)=\"addSelector()\"\n themeColor=\"primary\">\n Add Entity Selector\n </button>\n </div>\n } @else {\n <!-- Edit Form -->\n <div class=\"edit-panel\">\n <div class=\"edit-panel-header\">\n {{ editingIndex !== null ? 'Edit Entity Selector' : 'New Entity Selector' }}\n </div>\n <div class=\"edit-form\">\n <div class=\"form-field\">\n <label class=\"field-label\">ID *</label>\n <kendo-textbox\n [(ngModel)]=\"editId\"\n placeholder=\"e.g., mp\"\n [readonly]=\"editingIndex !== null\">\n </kendo-textbox>\n @if (isDuplicateId(editId, editingIndex)) {\n <span class=\"field-error\">ID must be unique</span>\n }\n <span class=\"field-hint\">Used in URL parameters (es_<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 @if (editCkTypeId) {\n <div class=\"form-field picker-filter-field\">\n <label class=\"field-label\">Picker filter</label>\n @if (isFollower) {\n <span class=\"field-hint\">A follower takes the leader's pick. Set the filter on {{ leaderLabel }}.</span>\n @if (followerFilterCount > 0) {\n <span class=\"field-hint follow-notice\">Filters on a follower are ignored and are removed when you save.</span>\n }\n } @else {\n <span class=\"field-hint\">\n Only entities that match all rows are listed in the toolbar picker and in the default-entity picker below.\n </span>\n <mm-field-filter-editor\n [ckTypeId]=\"editCkTypeId\"\n [filters]=\"editFilters\"\n [enableVariables]=\"filterVariables.length > 0\"\n [availableVariables]=\"filterVariables\"\n (filtersChange)=\"onFiltersChange($event)\">\n </mm-field-filter-editor>\n }\n </div>\n }\n\n <div class=\"form-field\">\n <label class=\"field-label\">Attribute Mappings{{ isFollower ? '' : ' *' }}</label>\n <button\n kendoButton\n (click)=\"selectAttributes()\"\n [disabled]=\"!editCkTypeId\"\n fillMode=\"outline\"\n size=\"small\">\n Select Attributes...\n </button>\n\n @if (editMappings.length > 0) {\n <div class=\"mappings-list\">\n <div class=\"mappings-header\">\n <span class=\"mappings-header-attr\">Attribute</span>\n <span class=\"mappings-header-var\">Variable Name</span>\n </div>\n @for (mapping of editMappings; track mapping.attributePath; let j = $index) {\n <div class=\"mapping-item\">\n <span class=\"mapping-path\">{{ mapping.attributePath }}</span>\n <span class=\"mapping-arrow\">→</span>\n <div class=\"mapping-var-field\">\n <div class=\"mapping-var-input-wrapper\">\n <span class=\"mapping-var-prefix\">$</span>\n <kendo-textbox\n [(ngModel)]=\"mapping.variableName\"\n size=\"small\"\n class=\"mapping-var-input\"\n placeholder=\"variableName\"\n [class.invalid]=\"getVariableNameError(mapping)\">\n </kendo-textbox>\n </div>\n @if (getVariableNameError(mapping); as error) {\n <span class=\"field-error\">{{ error }}</span>\n }\n </div>\n </div>\n }\n </div>\n }\n </div>\n\n @if (!isFollower) {\n <div class=\"form-field\">\n <div class=\"checkbox-row\">\n <input\n type=\"checkbox\"\n kendoCheckBox\n [(ngModel)]=\"editShowInToolbar\"\n id=\"showInToolbar\" />\n <label class=\"field-label checkbox-label\" for=\"showInToolbar\">Show in Toolbar</label>\n </div>\n <span class=\"field-hint\">\n @if (editShowInToolbar) {\n Users can select an entity from a dropdown in the toolbar.\n } @else {\n Hidden selector \u2014 variables are resolved from the default entity below.\n }\n </span>\n </div>\n }\n\n <div class=\"form-field\">\n <label class=\"field-label\">Default Entity{{ !editShowInToolbar && !isFollower ? ' *' : '' }}</label>\n @if (defaultEntityDataSource) {\n <mm-entity-select-input\n [dataSource]=\"defaultEntityDataSource\"\n [dialogDataSource]=\"defaultEntityDialogDataSource!\"\n [placeholder]=\"'Select default entity...'\"\n [dialogTitle]=\"'Select Default ' + editLabel\"\n (entitySelected)=\"onDefaultEntitySelected($event)\"\n (entityCleared)=\"onDefaultEntityCleared()\">\n </mm-entity-select-input>\n } @else {\n <span class=\"field-hint\">Select a CK type first to pick a default entity.</span>\n }\n @if (editDefaultRtId) {\n <span class=\"field-hint\">Selected: {{ editDefaultRtId }}</span>\n }\n @switch (defaultFilterCheck.state) {\n @case ('mismatch') {\n <span class=\"field-warning default-filter-warning\">The default entity does not match the picker filter. It still loads, but users cannot pick it again.</span>\n }\n @case ('error') {\n <span class=\"field-hint default-filter-warning\">Could not check the default entity.</span>\n }\n @case ('unchecked') {\n <span class=\"field-hint default-filter-warning\">Cannot check the default entity: the filter uses {{ uncheckedVariablesText }}, which is filled at view time.</span>\n }\n }\n @if (!editShowInToolbar && !isFollower && !editDefaultRtId) {\n <span class=\"field-error\">A default entity is required when not shown in toolbar.</span>\n }\n <span class=\"field-hint\">\n @if (isFollower) {\n Optional \u2014 used only while the leader has no pick.\n } @else if (editShowInToolbar) {\n Optional \u2014 pre-selected entity when the MeshBoard loads.\n } @else {\n This entity's attributes will be used to populate the variables.\n }\n </span>\n </div>\n\n <div class=\"form-field\">\n <label class=\"field-label\">Child Scope (stream-data)</label>\n <mm-entity-selector-scope-path-editor\n [sourceCkTypeId]=\"editCkTypeId\"\n [hops]=\"editScopeHops\"\n (hopsChange)=\"onScopeHopsChanged($event)\">\n </mm-entity-selector-scope-path-editor>\n </div>\n\n <div class=\"edit-actions mm-dialog-actions\">\n <button kendoButton (click)=\"cancelEdit()\" fillMode=\"flat\">\n Cancel\n </button>\n <button\n kendoButton\n (click)=\"saveEdit()\"\n [disabled]=\"!isEditValid()\"\n themeColor=\"primary\">\n {{ editingIndex !== null ? 'Update' : 'Add' }}\n </button>\n </div>\n </div>\n </div>\n }\n</div>\n", styles: [".entity-selector-editor{display:flex;flex-direction:column;gap:1.25rem}.selector-list{display:flex;flex-direction:column;gap:.5rem}.selector-item{display:flex;justify-content:space-between;align-items:center;padding:.75rem 1rem;background:var(--kendo-color-surface-alt, #fafafa);border:1px solid var(--kendo-color-border, #e0e0e0);border-radius:4px}.selector-item .selector-info{display:flex;flex-direction:column;gap:.25rem;min-width:0;flex:1}.selector-item .selector-label{font-weight:500;color:var(--kendo-color-on-app-surface, #424242)}.selector-item .selector-mode{font-size:.75rem;font-weight:500;color:var(--kendo-color-info, #0288d1)}.selector-item .selector-mode-error{color:var(--kendo-color-error, #d32f2f)}.selector-item .selector-filter-chip{color:var(--kendo-color-subtle, #757575)}.selector-item .selector-ck-type{font-size:.8rem;color:var(--kendo-color-subtle, #757575);font-family:var(--theme-font-mono)}.selector-item .selector-mappings{font-size:.8rem;color:var(--kendo-color-primary, #3f51b5);font-family:var(--theme-font-mono)}.selector-item .selector-actions{display:flex;gap:.25rem;flex-shrink:0}.selector-item .remove-btn{color:var(--kendo-color-error, #f44336)}.empty-state{padding:1.5rem;text-align:center;color:var(--kendo-color-subtle, #757575);font-size:.875rem}.add-button-row{display:flex;justify-content:flex-start}.edit-panel{border:1px solid var(--kendo-color-border, #e0e0e0);border-radius:4px;overflow:hidden}.edit-panel-header{padding:.625rem 1rem;font-weight:600;font-size:.875rem;color:var(--kendo-color-on-primary, #ffffff);background:var(--kendo-color-primary, #3f51b5)}.edit-form{display:flex;flex-direction:column;gap:1.25rem;padding:1.25rem}.edit-form .form-field{display:flex;flex-direction:column;gap:.5rem}.edit-form .field-label{font-weight:500;font-size:.875rem;color:var(--kendo-color-on-app-surface, #424242)}.edit-form .field-hint{font-size:.75rem;color:var(--kendo-color-subtle, #757575)}.edit-form .field-error{font-size:.75rem;color:var(--kendo-color-error, #f44336)}.edit-form .field-warning{font-size:.75rem;color:var(--kendo-color-warning, #f57c00)}.edit-form .checkbox-row{display:flex;align-items:center;gap:.5rem}.edit-form .checkbox-row .checkbox-label{margin:0;cursor:pointer}.mappings-list{display:flex;flex-direction:column;gap:.5rem;margin-top:.5rem;padding:.75rem;background:var(--kendo-color-surface-alt, #fafafa);border:1px solid var(--kendo-color-border, #e0e0e0);border-radius:4px}.mappings-header{display:flex;align-items:center;gap:.5rem;font-size:.75rem;font-weight:600;color:var(--kendo-color-subtle, #757575);padding-bottom:.25rem;border-bottom:1px solid var(--kendo-color-border, #e0e0e0)}.mappings-header .mappings-header-attr{min-width:150px}.mappings-header .mappings-header-var{margin-left:1.75rem}.mapping-item{display:flex;align-items:flex-start;gap:.5rem}.mapping-item .mapping-path{font-size:.8rem;font-family:var(--theme-font-mono);color:var(--kendo-color-subtle, #757575);min-width:150px;padding-top:5px}.mapping-item .mapping-arrow{color:var(--kendo-color-subtle, #757575);flex-shrink:0;padding-top:5px}.mapping-item .mapping-var-field{display:flex;flex-direction:column;gap:2px;flex:1;min-width:100px}.mapping-item .mapping-var-input-wrapper{display:flex;align-items:center;gap:0}.mapping-item .mapping-var-prefix{font-family:var(--theme-font-mono);font-size:.875rem;font-weight:600;color:var(--kendo-color-primary, #3f51b5);padding-right:2px;flex-shrink:0}.mapping-item .mapping-var-input{flex:1}.mapping-item .mapping-var-input.invalid ::ng-deep .k-input{border-color:var(--kendo-color-error, #f44336)}\n"] }]
|
|
34183
|
+
}], ctorParameters: () => [], propDecorators: { entitySelectors: [{
|
|
32857
34184
|
type: Input
|
|
32858
34185
|
}], existingVariableNames: [{
|
|
32859
34186
|
type: Input
|
|
34187
|
+
}], variables: [{
|
|
34188
|
+
type: Input
|
|
32860
34189
|
}], entitySelectorsChange: [{
|
|
32861
34190
|
type: Output
|
|
32862
34191
|
}], editingStateChange: [{
|
|
32863
34192
|
type: Output
|
|
32864
34193
|
}] } });
|
|
34194
|
+
/** Debounce of the default-entity filter check (ms). */
|
|
34195
|
+
const DEFAULT_CHECK_DEBOUNCE_MS = 400;
|
|
32865
34196
|
|
|
32866
34197
|
/**
|
|
32867
34198
|
* Toolbar component that renders entity select inputs for each configured entity selector.
|
|
32868
|
-
* Creates data sources per CK type and emits selection/clear events.
|
|
34199
|
+
* Creates data sources per CK type and picker filter (AB#7724) and emits selection/clear events.
|
|
32869
34200
|
*/
|
|
32870
34201
|
class EntitySelectorToolbarComponent {
|
|
32871
34202
|
getEntitiesByCkTypeGQL = inject(GetEntitiesByCkTypeDtoGQL);
|
|
34203
|
+
variableService = inject(MeshBoardVariableService);
|
|
32872
34204
|
entitySelectors = [];
|
|
34205
|
+
/**
|
|
34206
|
+
* Board variables substituted into the selectors' picker filters (AB#7724),
|
|
34207
|
+
* the same substitution widget filters get.
|
|
34208
|
+
*/
|
|
34209
|
+
variables = [];
|
|
32873
34210
|
entitySelected = new EventEmitter();
|
|
32874
34211
|
entityCleared = new EventEmitter();
|
|
32875
|
-
// Cached data sources per CK type
|
|
34212
|
+
// Cached data sources per CK type and resolved picker filter
|
|
32876
34213
|
dataSourceCache = new Map();
|
|
32877
34214
|
dialogDataSourceCache = new Map();
|
|
32878
34215
|
// Map of selector ID -> data sources for template
|
|
32879
34216
|
selectorDataSources = new Map();
|
|
32880
34217
|
selectorDialogDataSources = new Map();
|
|
34218
|
+
// Picker texts of filtered selectors (absent ⇒ the input's defaults)
|
|
34219
|
+
selectorMessages = new Map();
|
|
32881
34220
|
ngOnChanges(changes) {
|
|
32882
|
-
if (changes['entitySelectors']) {
|
|
34221
|
+
if (changes['entitySelectors'] || changes['variables']) {
|
|
32883
34222
|
this.updateDataSources();
|
|
32884
34223
|
}
|
|
32885
34224
|
}
|
|
34225
|
+
/** Texts of the selector's picker: a filtered picker explains its empty list (AB#7724). */
|
|
34226
|
+
getMessages(selector) {
|
|
34227
|
+
return this.selectorMessages.get(selector.id) ?? NO_MESSAGES;
|
|
34228
|
+
}
|
|
34229
|
+
/** Title of the selector's search dialog; marks a filtered picker (AB#7724). */
|
|
34230
|
+
getDialogTitle(selector) {
|
|
34231
|
+
return this.selectorMessages.has(selector.id) ? filteredDialogTitle(selector.label) : 'Select ' + selector.label;
|
|
34232
|
+
}
|
|
32886
34233
|
/**
|
|
32887
34234
|
* True when the selector renders a picker. Hidden selectors and followers
|
|
32888
34235
|
* (AB#7722) never do; a follower's pick is derived from its leader.
|
|
@@ -32915,23 +34262,45 @@ class EntitySelectorToolbarComponent {
|
|
|
32915
34262
|
this.entityCleared.emit({ selectorId });
|
|
32916
34263
|
}
|
|
32917
34264
|
/**
|
|
32918
|
-
* Creates or retrieves cached data sources for each selector.
|
|
34265
|
+
* Creates or retrieves cached data sources for each selector. Selectors of
|
|
34266
|
+
* the same CK type and the same resolved filter share their data sources; a
|
|
34267
|
+
* selector without filter uses the plain query, exactly as before AB#7724.
|
|
34268
|
+
* Entries no selector uses any more (e.g. after a filter variable changed)
|
|
34269
|
+
* are dropped, so the caches never outgrow the current selectors.
|
|
32919
34270
|
*/
|
|
32920
34271
|
updateDataSources() {
|
|
34272
|
+
this.selectorMessages = new Map();
|
|
34273
|
+
const usedKeys = new Set();
|
|
32921
34274
|
for (const selector of this.entitySelectors) {
|
|
32922
|
-
|
|
32923
|
-
|
|
32924
|
-
|
|
34275
|
+
const filters = pickerFieldFilters(selector, this.variables, this.variableService);
|
|
34276
|
+
const key = filters ? `${selector.ckTypeId}|${JSON.stringify(filters)}` : selector.ckTypeId;
|
|
34277
|
+
usedKeys.add(key);
|
|
34278
|
+
if (!this.dataSourceCache.has(key) || !this.dialogDataSourceCache.has(key)) {
|
|
34279
|
+
const gql = withPickerFilters(this.getEntitiesByCkTypeGQL, filters);
|
|
34280
|
+
this.dataSourceCache.set(key, new RuntimeEntitySelectDataSource(gql, selector.ckTypeId));
|
|
34281
|
+
this.dialogDataSourceCache.set(key, new RuntimeEntityDialogDataSource(gql, selector.ckTypeId));
|
|
34282
|
+
}
|
|
34283
|
+
this.selectorDataSources.set(selector.id, this.dataSourceCache.get(key));
|
|
34284
|
+
this.selectorDialogDataSources.set(selector.id, this.dialogDataSourceCache.get(key));
|
|
34285
|
+
if (filters) {
|
|
34286
|
+
this.selectorMessages.set(selector.id, {
|
|
34287
|
+
noEntitiesFound: filteredNoEntitiesMessage(selector.label, unresolvedFilterVariables(filters))
|
|
34288
|
+
});
|
|
32925
34289
|
}
|
|
32926
|
-
|
|
32927
|
-
|
|
34290
|
+
}
|
|
34291
|
+
for (const key of [...this.dataSourceCache.keys()]) {
|
|
34292
|
+
if (!usedKeys.has(key)) {
|
|
34293
|
+
this.dataSourceCache.delete(key);
|
|
34294
|
+
this.dialogDataSourceCache.delete(key);
|
|
32928
34295
|
}
|
|
32929
|
-
this.selectorDataSources.set(selector.id, this.dataSourceCache.get(selector.ckTypeId));
|
|
32930
|
-
this.selectorDialogDataSources.set(selector.id, this.dialogDataSourceCache.get(selector.ckTypeId));
|
|
32931
34296
|
}
|
|
32932
34297
|
}
|
|
34298
|
+
/** Number of cached data source pairs (for tests). */
|
|
34299
|
+
get cachedDataSourceCount() {
|
|
34300
|
+
return this.dataSourceCache.size;
|
|
34301
|
+
}
|
|
32933
34302
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: EntitySelectorToolbarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
32934
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: EntitySelectorToolbarComponent, isStandalone: true, selector: "mm-entity-selector-toolbar", inputs: { entitySelectors: "entitySelectors" }, outputs: { entitySelected: "entitySelected", entityCleared: "entityCleared" }, usesOnChanges: true, ngImport: i0, template: "<div class=\"entity-selector-toolbar\">\n @for (selector of entitySelectors; track selector.id) {\n @if (showsInToolbar(selector)) {\n <div class=\"entity-selector-item\">\n <kendo-label [text]=\"selector.label\">\n <mm-entity-select-input\n [dataSource]=\"getDataSource(selector)\"\n [dialogDataSource]=\"getDialogDataSource(selector)\"\n [initialDisplayValue]=\"selector.selectedDisplayName\"\n [placeholder]=\"'Select ' + selector.label + '...'\"\n [dialogTitle]=\"
|
|
34303
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: EntitySelectorToolbarComponent, isStandalone: true, selector: "mm-entity-selector-toolbar", inputs: { entitySelectors: "entitySelectors", variables: "variables" }, outputs: { entitySelected: "entitySelected", entityCleared: "entityCleared" }, usesOnChanges: true, ngImport: i0, template: "<div class=\"entity-selector-toolbar\">\n @for (selector of entitySelectors; track selector.id) {\n @if (showsInToolbar(selector)) {\n <div class=\"entity-selector-item\">\n <kendo-label [text]=\"selector.label\">\n <mm-entity-select-input\n [dataSource]=\"getDataSource(selector)\"\n [dialogDataSource]=\"getDialogDataSource(selector)\"\n [initialDisplayValue]=\"selector.selectedDisplayName\"\n [placeholder]=\"'Select ' + selector.label + '...'\"\n [dialogTitle]=\"getDialogTitle(selector)\"\n [messages]=\"getMessages(selector)\"\n (entitySelected)=\"onEntitySelected(selector.id, $event)\"\n (entityCleared)=\"onEntityCleared(selector.id)\">\n </mm-entity-select-input>\n </kendo-label>\n </div>\n }\n }\n</div>\n", styles: [".entity-selector-toolbar{display:flex;align-items:flex-end;gap:1rem;flex-wrap:wrap}.entity-selector-item{display:flex;flex-direction:column}.entity-selector-item kendo-label{display:flex;flex-direction:column;gap:.25rem}.entity-selector-item mm-entity-select-input{min-width:200px;max-width:300px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: i4$1.LabelComponent, selector: "kendo-label", inputs: ["text", "for", "optional", "labelCssStyle", "labelCssClass"], exportAs: ["kendoLabel"] }, { kind: "component", type: EntitySelectInputComponent, selector: "mm-entity-select-input", inputs: ["dataSource", "placeholder", "minSearchLength", "maxResults", "debounceMs", "prefix", "initialDisplayValue", "dialogDataSource", "dialogTitle", "multiSelect", "advancedSearchLabel", "dialogMessages", "messages", "disabled", "required"], outputs: ["entitySelected", "entityCleared", "entitiesSelected"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
|
|
32935
34304
|
}
|
|
32936
34305
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: EntitySelectorToolbarComponent, decorators: [{
|
|
32937
34306
|
type: Component,
|
|
@@ -32939,14 +34308,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
32939
34308
|
CommonModule,
|
|
32940
34309
|
KENDO_LABELS,
|
|
32941
34310
|
EntitySelectInputComponent
|
|
32942
|
-
], changeDetection: ChangeDetectionStrategy.Eager, template: "<div class=\"entity-selector-toolbar\">\n @for (selector of entitySelectors; track selector.id) {\n @if (showsInToolbar(selector)) {\n <div class=\"entity-selector-item\">\n <kendo-label [text]=\"selector.label\">\n <mm-entity-select-input\n [dataSource]=\"getDataSource(selector)\"\n [dialogDataSource]=\"getDialogDataSource(selector)\"\n [initialDisplayValue]=\"selector.selectedDisplayName\"\n [placeholder]=\"'Select ' + selector.label + '...'\"\n [dialogTitle]=\"
|
|
34311
|
+
], changeDetection: ChangeDetectionStrategy.Eager, template: "<div class=\"entity-selector-toolbar\">\n @for (selector of entitySelectors; track selector.id) {\n @if (showsInToolbar(selector)) {\n <div class=\"entity-selector-item\">\n <kendo-label [text]=\"selector.label\">\n <mm-entity-select-input\n [dataSource]=\"getDataSource(selector)\"\n [dialogDataSource]=\"getDialogDataSource(selector)\"\n [initialDisplayValue]=\"selector.selectedDisplayName\"\n [placeholder]=\"'Select ' + selector.label + '...'\"\n [dialogTitle]=\"getDialogTitle(selector)\"\n [messages]=\"getMessages(selector)\"\n (entitySelected)=\"onEntitySelected(selector.id, $event)\"\n (entityCleared)=\"onEntityCleared(selector.id)\">\n </mm-entity-select-input>\n </kendo-label>\n </div>\n }\n }\n</div>\n", styles: [".entity-selector-toolbar{display:flex;align-items:flex-end;gap:1rem;flex-wrap:wrap}.entity-selector-item{display:flex;flex-direction:column}.entity-selector-item kendo-label{display:flex;flex-direction:column;gap:.25rem}.entity-selector-item mm-entity-select-input{min-width:200px;max-width:300px}\n"] }]
|
|
32943
34312
|
}], propDecorators: { entitySelectors: [{
|
|
32944
34313
|
type: Input
|
|
34314
|
+
}], variables: [{
|
|
34315
|
+
type: Input
|
|
32945
34316
|
}], entitySelected: [{
|
|
32946
34317
|
type: Output
|
|
32947
34318
|
}], entityCleared: [{
|
|
32948
34319
|
type: Output
|
|
32949
34320
|
}] } });
|
|
34321
|
+
const NO_MESSAGES = {};
|
|
32950
34322
|
|
|
32951
34323
|
/** Below this container width the board stacks in a single column. */
|
|
32952
34324
|
const PHONE_MAX_WIDTH = 700;
|
|
@@ -33294,6 +34666,90 @@ function applyTimeFilterParams(url, selection) {
|
|
|
33294
34666
|
return applyQueryParams(url, timeFilterQueryParams(selection));
|
|
33295
34667
|
}
|
|
33296
34668
|
|
|
34669
|
+
/**
|
|
34670
|
+
* RFC 3339 date-time: `YYYY-MM-DDTHH:MM:SS`, optional 1-3 fraction digits, then `Z` or `±HH:MM`.
|
|
34671
|
+
* Uppercase `T`/`Z` only, seconds required, no compact offset (`+0100`): those forms parse
|
|
34672
|
+
* differently across browsers (Safari), and a string without an offset would be read in the
|
|
34673
|
+
* browser's own zone.
|
|
34674
|
+
*/
|
|
34675
|
+
const RFC3339 = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,3}))?(?:(Z)|([+-])(\d{2}):(\d{2}))$/;
|
|
34676
|
+
function daysInMonth(year, month) {
|
|
34677
|
+
return new Date(Date.UTC(year, month, 0)).getUTCDate();
|
|
34678
|
+
}
|
|
34679
|
+
/**
|
|
34680
|
+
* The instant of an RFC 3339 string, computed from its fields (never from `Date.parse`, so every
|
|
34681
|
+
* browser gets the same result), or `null` when the string is not RFC 3339 or names a date or time
|
|
34682
|
+
* that does not exist (`2026-02-30`, `24:00`, `23:60`, leap second `:60`, offset hour > 23).
|
|
34683
|
+
*/
|
|
34684
|
+
function parseRfc3339(value) {
|
|
34685
|
+
const m = RFC3339.exec(value);
|
|
34686
|
+
if (!m) {
|
|
34687
|
+
return null;
|
|
34688
|
+
}
|
|
34689
|
+
const [year, month, day, hour, minute, second] = m.slice(1, 7).map(Number);
|
|
34690
|
+
const millis = m[7] ? Number(m[7].padEnd(3, '0')) : 0;
|
|
34691
|
+
if (month < 1 || month > 12 || day < 1 || day > daysInMonth(year, month)
|
|
34692
|
+
|| hour > 23 || minute > 59 || second > 59) {
|
|
34693
|
+
return null;
|
|
34694
|
+
}
|
|
34695
|
+
let offsetMinutes = 0;
|
|
34696
|
+
if (!m[8]) {
|
|
34697
|
+
const offsetHours = Number(m[10]);
|
|
34698
|
+
const offsetMins = Number(m[11]);
|
|
34699
|
+
if (offsetHours > 23 || offsetMins > 59) {
|
|
34700
|
+
return null;
|
|
34701
|
+
}
|
|
34702
|
+
offsetMinutes = (m[9] === '-' ? -1 : 1) * (offsetHours * 60 + offsetMins);
|
|
34703
|
+
}
|
|
34704
|
+
// setUTCFullYear (not Date.UTC, which maps years 0-99 to 1900-1999) for the wall-clock fields,
|
|
34705
|
+
// then the offset: local wall clock = UTC + offset.
|
|
34706
|
+
const date = new Date(0);
|
|
34707
|
+
date.setUTCFullYear(year, month - 1, day);
|
|
34708
|
+
date.setUTCHours(hour, minute, second, millis);
|
|
34709
|
+
date.setTime(date.getTime() - offsetMinutes * 60_000);
|
|
34710
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
34711
|
+
}
|
|
34712
|
+
function toInstant(value) {
|
|
34713
|
+
if (value instanceof Date) {
|
|
34714
|
+
return Number.isNaN(value.getTime()) ? null : new Date(value.getTime());
|
|
34715
|
+
}
|
|
34716
|
+
return typeof value === 'string' ? parseRfc3339(value) : null;
|
|
34717
|
+
}
|
|
34718
|
+
/**
|
|
34719
|
+
* Parses the host's `timeRange` input. `undefined`/`null` = the host sets no period.
|
|
34720
|
+
* Never snaps or converts: the instants are taken as given. `Date` objects are accepted as they
|
|
34721
|
+
* are; strings must be RFC 3339 with an offset, e.g. `2026-03-28T05:00:00Z` or
|
|
34722
|
+
* `2026-03-28T06:00:00+01:00` (see {@link RFC3339}).
|
|
34723
|
+
*/
|
|
34724
|
+
function parseHostTimeRange(value) {
|
|
34725
|
+
if (value === undefined || value === null) {
|
|
34726
|
+
return undefined;
|
|
34727
|
+
}
|
|
34728
|
+
const from = toInstant(value.from);
|
|
34729
|
+
const to = toInstant(value.to);
|
|
34730
|
+
if (!from || !to) {
|
|
34731
|
+
return {
|
|
34732
|
+
status: 'invalid',
|
|
34733
|
+
reason: 'from and to must be Date objects or RFC 3339 date-times with an offset '
|
|
34734
|
+
+ '(YYYY-MM-DDTHH:MM:SS[.sss] followed by Z or ±HH:MM)'
|
|
34735
|
+
};
|
|
34736
|
+
}
|
|
34737
|
+
if (from.getTime() >= to.getTime()) {
|
|
34738
|
+
return { status: 'invalid', reason: 'from must be before to' };
|
|
34739
|
+
}
|
|
34740
|
+
return { status: 'valid', from, to };
|
|
34741
|
+
}
|
|
34742
|
+
/** True when two parsed ranges mean the same period (same instants, or both invalid for the same reason). */
|
|
34743
|
+
function sameHostTimeRange(a, b) {
|
|
34744
|
+
if (!a || !b) {
|
|
34745
|
+
return a === b;
|
|
34746
|
+
}
|
|
34747
|
+
if (a.status === 'valid' && b.status === 'valid') {
|
|
34748
|
+
return a.from.getTime() === b.from.getTime() && a.to.getTime() === b.to.getTime();
|
|
34749
|
+
}
|
|
34750
|
+
return a.status === 'invalid' && b.status === 'invalid' && a.reason === b.reason;
|
|
34751
|
+
}
|
|
34752
|
+
|
|
33297
34753
|
/**
|
|
33298
34754
|
* Component for editing MeshBoard variables.
|
|
33299
34755
|
* Allows adding, editing, and removing static variables with type-specific input controls.
|
|
@@ -33630,7 +35086,7 @@ class MeshBoardSettingsDialogComponent {
|
|
|
33630
35086
|
this.windowRef.close();
|
|
33631
35087
|
}
|
|
33632
35088
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: MeshBoardSettingsDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
33633
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: MeshBoardSettingsDialogComponent, isStandalone: true, selector: "mm-meshboard-settings-dialog", ngImport: i0, template: "<div class=\"meshboard-settings-dialog\">\n <kendo-tabstrip [animate]=\"false\">\n <!-- General Tab -->\n <kendo-tabstrip-tab [title]=\"'General'\" [selected]=\"true\">\n <ng-template kendoTabContent>\n <div class=\"tab-content\">\n <form class=\"settings-form\">\n <!-- Name Field -->\n <kendo-formfield>\n <kendo-label [for]=\"nameInput\" text=\"Name *\"></kendo-label>\n <kendo-textbox\n #nameInput\n [(ngModel)]=\"name\"\n name=\"name\"\n placeholder=\"Enter MeshBoard name\"\n required>\n </kendo-textbox>\n @if (name.trim().length === 0) {\n <kendo-formerror>Name is required</kendo-formerror>\n }\n </kendo-formfield>\n\n <!-- Description Field -->\n <kendo-formfield>\n <kendo-label [for]=\"descriptionInput\" text=\"Description\"></kendo-label>\n <kendo-textarea\n #descriptionInput\n [(ngModel)]=\"description\"\n name=\"description\"\n placeholder=\"Enter MeshBoard description (optional)\"\n [rows]=\"3\">\n </kendo-textarea>\n </kendo-formfield>\n\n <!-- Well-Known Name Field -->\n <kendo-formfield>\n <kendo-label [for]=\"wellKnownNameInput\" text=\"Well-Known Name\"></kendo-label>\n <kendo-textbox\n #wellKnownNameInput\n [(ngModel)]=\"rtWellKnownName\"\n name=\"rtWellKnownName\"\n placeholder=\"e.g., cockpit, dashboard-main\">\n </kendo-textbox>\n <kendo-formhint>Unique identifier for routing. Use lowercase with hyphens (e.g., 'cockpit', 'sales-dashboard').</kendo-formhint>\n </kendo-formfield>\n\n <!-- Layout Settings -->\n <div class=\"section-title\">Layout Settings</div>\n\n <!-- Columns Field -->\n <kendo-formfield>\n <kendo-label [for]=\"columnsInput\" text=\"Columns *\"></kendo-label>\n <kendo-numerictextbox\n #columnsInput\n [(ngModel)]=\"columns\"\n name=\"columns\"\n [min]=\"1\"\n [max]=\"12\"\n [decimals]=\"0\"\n [format]=\"'n0'\"\n [spinners]=\"true\"\n required>\n </kendo-numerictextbox>\n <kendo-formhint>Number of columns in the grid (1-12)</kendo-formhint>\n @if (columns < 1 || columns > 12) {\n <kendo-formerror>Columns must be between 1 and 12</kendo-formerror>\n }\n </kendo-formfield>\n\n <!-- Row Height Field -->\n <kendo-formfield>\n <kendo-label [for]=\"rowHeightInput\" text=\"Row Height *\"></kendo-label>\n <kendo-numerictextbox\n #rowHeightInput\n [(ngModel)]=\"rowHeight\"\n name=\"rowHeight\"\n [min]=\"100\"\n [max]=\"1000\"\n [decimals]=\"0\"\n [format]=\"'n0'\"\n [spinners]=\"true\"\n required>\n </kendo-numerictextbox>\n <kendo-formhint>Height of each row in pixels (100-1000)</kendo-formhint>\n @if (rowHeight < 100 || rowHeight > 1000) {\n <kendo-formerror>Row height must be between 100 and 1000</kendo-formerror>\n }\n </kendo-formfield>\n\n <!-- Gap Field -->\n <kendo-formfield>\n <kendo-label [for]=\"gapInput\" text=\"Gap *\"></kendo-label>\n <kendo-numerictextbox\n #gapInput\n [(ngModel)]=\"gap\"\n name=\"gap\"\n [min]=\"0\"\n [max]=\"100\"\n [decimals]=\"0\"\n [format]=\"'n0'\"\n [spinners]=\"true\"\n required>\n </kendo-numerictextbox>\n <kendo-formhint>Space between widgets in pixels (0-100)</kendo-formhint>\n @if (gap < 0 || gap > 100) {\n <kendo-formerror>Gap must be between 0 and 100</kendo-formerror>\n }\n </kendo-formfield>\n\n <!-- Auto-Refresh Field -->\n <kendo-formfield>\n <kendo-label [for]=\"autoRefreshInput\" text=\"Auto-refresh\"></kendo-label>\n <kendo-numerictextbox\n #autoRefreshInput\n [(ngModel)]=\"autoRefreshSeconds\"\n name=\"autoRefreshSeconds\"\n [min]=\"0\"\n [max]=\"3600\"\n [decimals]=\"0\"\n [format]=\"'n0'\"\n [spinners]=\"true\">\n </kendo-numerictextbox>\n <kendo-formhint>Refresh interval in seconds (0 = disabled, max 3600). Pauses while the tab is hidden.</kendo-formhint>\n @if (autoRefreshSeconds < 0 || autoRefreshSeconds > 3600) {\n <kendo-formerror>Auto-refresh must be between 0 and 3600 seconds</kendo-formerror>\n }\n </kendo-formfield>\n </form>\n </div>\n </ng-template>\n </kendo-tabstrip-tab>\n\n <!-- Variables Tab -->\n <kendo-tabstrip-tab [title]=\"'Variables'\">\n <ng-template kendoTabContent>\n <div class=\"tab-content\">\n <mm-variables-editor\n [(variables)]=\"variables\">\n </mm-variables-editor>\n </div>\n </ng-template>\n </kendo-tabstrip-tab>\n\n <!-- Time Filter Tab -->\n <kendo-tabstrip-tab [title]=\"'Time Filter'\">\n <ng-template kendoTabContent>\n <div class=\"tab-content\">\n <form class=\"settings-form\">\n <kendo-formfield>\n <kendo-label text=\"Timezone\"></kendo-label>\n <div class=\"radio-group\">\n <div class=\"radio-wrapper\">\n <input\n type=\"radio\"\n kendoRadioButton\n #tzLocal\n [(ngModel)]=\"timeZoneMode\"\n name=\"timeZoneMode\"\n value=\"local\"\n id=\"timeZoneModeLocal\"/>\n <kendo-label [for]=\"tzLocal\" text=\"Local time (browser)\" class=\"radio-label\"></kendo-label>\n </div>\n <div class=\"radio-wrapper\">\n <input\n type=\"radio\"\n kendoRadioButton\n #tzUtc\n [(ngModel)]=\"timeZoneMode\"\n name=\"timeZoneMode\"\n value=\"utc\"\n id=\"timeZoneModeUtc\"/>\n <kendo-label [for]=\"tzUtc\" text=\"UTC\" class=\"radio-label\"></kendo-label>\n </div>\n <div class=\"radio-wrapper\">\n <input\n type=\"radio\"\n kendoRadioButton\n #tzZone\n [(ngModel)]=\"timeZoneMode\"\n name=\"timeZoneMode\"\n [value]=\"zoneSelection\"\n id=\"timeZoneModeZone\"/>\n <kendo-label [for]=\"tzZone\" text=\"Specific time zone\" class=\"radio-label\"></kendo-label>\n <kendo-dropdownlist\n class=\"tz-zone-select\"\n [data]=\"commonTimeZones\"\n [value]=\"zoneSelection\"\n (valueChange)=\"onZoneSelectionChange($event)\"\n [disabled]=\"!isSpecificZone\"\n [filterable]=\"true\"\n aria-label=\"IANA time zone\">\n </kendo-dropdownlist>\n </div>\n </div>\n <kendo-formhint>\n Basis for the time filter and for how all widgets display dates and times. Local time is the default.\n A specific time zone resolves civil days (today / yesterday / this month) DST-correctly in that\n zone and selects matching calendar rollups.\n </kendo-formhint>\n </kendo-formfield>\n\n <kendo-formfield>\n <div class=\"checkbox-wrapper\">\n <input\n type=\"checkbox\"\n kendoCheckBox\n #timeFilterCheckbox\n [(ngModel)]=\"timeFilterEnabled\"\n name=\"timeFilterEnabled\"\n id=\"timeFilterEnabled\"/>\n <kendo-label\n [for]=\"timeFilterCheckbox\"\n text=\"Enable Time Filter\"\n class=\"checkbox-label\">\n </kendo-label>\n </div>\n <kendo-formhint>\n Shows a time range picker in the toolbar. Sets $timeRangeFrom and $timeRangeTo variables.\n </kendo-formhint>\n </kendo-formfield>\n\n @if (timeFilterEnabled) {\n <kendo-formfield>\n <kendo-label text=\"Default Selection\"></kendo-label>\n <mm-time-range-picker\n [initialSelection]=\"initialDefaultSelection\"\n (selectionChange)=\"onDefaultSelectionChange($event)\">\n </mm-time-range-picker>\n <kendo-formhint>Initial time filter shown when no URL parameters are set. Note: User-selected filters override this default. Use the reset button in the toolbar to revert to the default.</kendo-formhint>\n </kendo-formfield>\n }\n </form>\n </div>\n </ng-template>\n </kendo-tabstrip-tab>\n\n <!-- Entity Selectors Tab -->\n <kendo-tabstrip-tab [title]=\"'Entity Selectors'\">\n <ng-template kendoTabContent>\n <div class=\"tab-content\">\n <mm-entity-selector-editor\n [(entitySelectors)]=\"entitySelectors\"\n [existingVariableNames]=\"staticVariableNames\"\n (editingStateChange)=\"entitySelectorEditing = $event\">\n </mm-entity-selector-editor>\n </div>\n </ng-template>\n </kendo-tabstrip-tab>\n </kendo-tabstrip>\n\n <!-- Dialog Actions -->\n @if (!entitySelectorEditing) {\n <div class=\"dialog-actions mm-dialog-actions\">\n <button kendoButton (click)=\"cancel()\" fillMode=\"flat\">\n Cancel\n </button>\n <button\n kendoButton\n (click)=\"save()\"\n [disabled]=\"!isValid\"\n themeColor=\"primary\">\n Save\n </button>\n </div>\n }\n</div>\n", styles: [".meshboard-settings-dialog{display:flex;flex-direction:column;height:100%;overflow:hidden}.meshboard-settings-dialog kendo-tabstrip{flex:1;min-height:0;display:flex;flex-direction:column}.meshboard-settings-dialog kendo-tabstrip ::ng-deep .k-tabstrip-content{flex:1;min-height:0;overflow:hidden}.meshboard-settings-dialog .tab-content{height:100%;overflow-y:auto;padding:1.5rem}.meshboard-settings-dialog .settings-form{display:flex;flex-direction:column;gap:1.25rem}.meshboard-settings-dialog .settings-form kendo-formfield{display:flex;flex-direction:column;gap:.5rem}.meshboard-settings-dialog .settings-form kendo-formfield kendo-label{font-weight:500;color:var(--kendo-color-on-app-surface, #424242)}.meshboard-settings-dialog .settings-form kendo-formfield kendo-textbox,.meshboard-settings-dialog .settings-form kendo-formfield kendo-textarea,.meshboard-settings-dialog .settings-form kendo-formfield kendo-numerictextbox{width:100%}.meshboard-settings-dialog .settings-form kendo-formfield kendo-formhint{font-size:.75rem;color:var(--kendo-color-subtle, #757575)}.meshboard-settings-dialog .settings-form kendo-formfield kendo-formerror{font-size:.75rem;color:var(--kendo-color-error, #f44336)}.meshboard-settings-dialog .settings-form .section-title{font-size:.875rem;font-weight:600;color:var(--kendo-color-on-app-surface, #424242);margin-top:.5rem;padding-bottom:.5rem;border-bottom:1px solid var(--kendo-color-border, #e0e0e0)}.meshboard-settings-dialog .settings-form .checkbox-wrapper,.meshboard-settings-dialog .settings-form .radio-wrapper{display:flex;align-items:center;gap:.5rem}.meshboard-settings-dialog .settings-form .checkbox-wrapper .checkbox-label,.meshboard-settings-dialog .settings-form .checkbox-wrapper .radio-label,.meshboard-settings-dialog .settings-form .radio-wrapper .checkbox-label,.meshboard-settings-dialog .settings-form .radio-wrapper .radio-label{font-weight:400;cursor:pointer}.meshboard-settings-dialog .settings-form .checkbox-wrapper .tz-zone-select,.meshboard-settings-dialog .settings-form .radio-wrapper .tz-zone-select{min-width:12rem}.meshboard-settings-dialog .settings-form .radio-group{display:flex;flex-direction:column;gap:.5rem;margin-top:.25rem}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox]:not([ngNoCva])[formControlName],input[type=checkbox]:not([ngNoCva])[formControl],input[type=checkbox]:not([ngNoCva])[ngModel]" }, { kind: "directive", type: i1$1.RadioControlValueAccessor, selector: "input[type=radio]:not([ngNoCva])[formControlName],input[type=radio]:not([ngNoCva])[formControl],input[type=radio]:not([ngNoCva])[ngModel]", inputs: ["name", "formControlName", "value"] }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1$1.NgForm, selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: InputsModule }, { kind: "component", type: i3.TextBoxComponent, selector: "kendo-textbox", inputs: ["focusableId", "title", "type", "disabled", "readonly", "tabindex", "value", "selectOnFocus", "showSuccessIcon", "showErrorIcon", "clearButton", "successIcon", "successSvgIcon", "errorIcon", "errorSvgIcon", "clearButtonIcon", "clearButtonSvgIcon", "size", "rounded", "fillMode", "tabIndex", "placeholder", "maxlength", "inputAttributes"], outputs: ["valueChange", "inputFocus", "inputBlur", "focus", "blur"], exportAs: ["kendoTextBox"] }, { kind: "component", type: i3.NumericTextBoxComponent, selector: "kendo-numerictextbox", inputs: ["focusableId", "disabled", "readonly", "title", "autoCorrect", "format", "max", "min", "decimals", "placeholder", "step", "spinners", "rangeValidation", "tabindex", "tabIndex", "changeValueOnScroll", "selectOnFocus", "value", "maxlength", "size", "rounded", "fillMode", "inputAttributes"], outputs: ["valueChange", "focus", "blur", "inputFocus", "inputBlur"], exportAs: ["kendoNumericTextBox"] }, { kind: "component", type: i3.TextAreaComponent, selector: "kendo-textarea", inputs: ["focusableId", "flow", "inputAttributes", "adornmentsOrientation", "rows", "cols", "maxlength", "maxResizableRows", "tabindex", "tabIndex", "resizable", "size", "rounded", "fillMode", "showPrefixSeparator", "showSuffixSeparator"], outputs: ["focus", "blur", "valueChange"], exportAs: ["kendoTextArea"] }, { kind: "directive", type: i3.CheckBoxDirective, selector: "input[kendoCheckBox]", inputs: ["size", "rounded"] }, { kind: "directive", type: i3.RadioButtonDirective, selector: "input[kendoRadioButton]", inputs: ["size"] }, { kind: "component", type: i3.FormFieldComponent, selector: "kendo-formfield", inputs: ["showHints", "orientation", "showErrors", "colSpan"] }, { kind: "component", type: i3.HintComponent, selector: "kendo-formhint", inputs: ["align"] }, { kind: "component", type: i3.ErrorComponent, selector: "kendo-formerror", inputs: ["align"] }, { kind: "ngmodule", type: CheckBoxModule }, { kind: "ngmodule", type: LabelModule }, { kind: "component", type: i4$1.LabelComponent, selector: "kendo-label", inputs: ["text", "for", "optional", "labelCssStyle", "labelCssClass"], exportAs: ["kendoLabel"] }, { kind: "ngmodule", type: FormFieldModule }, { kind: "ngmodule", type: TabStripModule }, { kind: "component", type: i5.TabStripComponent, selector: "kendo-tabstrip", inputs: ["height", "animate", "tabAlignment", "tabPosition", "keepTabContent", "closable", "scrollable", "size", "closeIcon", "closeIconClass", "closeSVGIcon", "showContentArea"], outputs: ["tabSelect", "tabClose", "tabScroll"], exportAs: ["kendoTabStrip"] }, { kind: "component", type: i5.TabStripTabComponent, selector: "kendo-tabstrip-tab", inputs: ["title", "disabled", "cssClass", "cssStyle", "selected", "closable", "closeIcon", "closeIconClass", "closeSVGIcon"], exportAs: ["kendoTabStripTab"] }, { kind: "directive", type: i5.TabContentDirective, selector: "[kendoTabContent]" }, { kind: "ngmodule", type: DropDownListModule }, { kind: "component", type: i4.DropDownListComponent, selector: "kendo-dropdownlist", inputs: ["customIconClass", "showStickyHeader", "icon", "svgIcon", "loading", "data", "value", "textField", "valueField", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "defaultItem", "disabled", "itemDisabled", "readonly", "filterable", "virtual", "ignoreCase", "delay", "valuePrimitive", "tabindex", "tabIndex", "size", "rounded", "fillMode", "leftRightArrowsNavigation", "id"], outputs: ["valueChange", "filterChange", "selectionChange", "open", "opened", "close", "closed", "focus", "blur"], exportAs: ["kendoDropDownList"] }, { kind: "component", type: VariablesEditorComponent, selector: "mm-variables-editor", inputs: ["variables"], outputs: ["variablesChange"] }, { kind: "component", type: EntitySelectorEditorComponent, selector: "mm-entity-selector-editor", inputs: ["entitySelectors", "existingVariableNames"], outputs: ["entitySelectorsChange", "editingStateChange"] }, { kind: "component", type: TimeRangePickerComponent, selector: "mm-time-range-picker", inputs: ["config", "labels", "initialSelection"], outputs: ["rangeChange", "rangeChangeISO", "selectionChange"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
|
|
35089
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: MeshBoardSettingsDialogComponent, isStandalone: true, selector: "mm-meshboard-settings-dialog", ngImport: i0, template: "<div class=\"meshboard-settings-dialog\">\n <kendo-tabstrip [animate]=\"false\">\n <!-- General Tab -->\n <kendo-tabstrip-tab [title]=\"'General'\" [selected]=\"true\">\n <ng-template kendoTabContent>\n <div class=\"tab-content\">\n <form class=\"settings-form\">\n <!-- Name Field -->\n <kendo-formfield>\n <kendo-label [for]=\"nameInput\" text=\"Name *\"></kendo-label>\n <kendo-textbox\n #nameInput\n [(ngModel)]=\"name\"\n name=\"name\"\n placeholder=\"Enter MeshBoard name\"\n required>\n </kendo-textbox>\n @if (name.trim().length === 0) {\n <kendo-formerror>Name is required</kendo-formerror>\n }\n </kendo-formfield>\n\n <!-- Description Field -->\n <kendo-formfield>\n <kendo-label [for]=\"descriptionInput\" text=\"Description\"></kendo-label>\n <kendo-textarea\n #descriptionInput\n [(ngModel)]=\"description\"\n name=\"description\"\n placeholder=\"Enter MeshBoard description (optional)\"\n [rows]=\"3\">\n </kendo-textarea>\n </kendo-formfield>\n\n <!-- Well-Known Name Field -->\n <kendo-formfield>\n <kendo-label [for]=\"wellKnownNameInput\" text=\"Well-Known Name\"></kendo-label>\n <kendo-textbox\n #wellKnownNameInput\n [(ngModel)]=\"rtWellKnownName\"\n name=\"rtWellKnownName\"\n placeholder=\"e.g., cockpit, dashboard-main\">\n </kendo-textbox>\n <kendo-formhint>Unique identifier for routing. Use lowercase with hyphens (e.g., 'cockpit', 'sales-dashboard').</kendo-formhint>\n </kendo-formfield>\n\n <!-- Layout Settings -->\n <div class=\"section-title\">Layout Settings</div>\n\n <!-- Columns Field -->\n <kendo-formfield>\n <kendo-label [for]=\"columnsInput\" text=\"Columns *\"></kendo-label>\n <kendo-numerictextbox\n #columnsInput\n [(ngModel)]=\"columns\"\n name=\"columns\"\n [min]=\"1\"\n [max]=\"12\"\n [decimals]=\"0\"\n [format]=\"'n0'\"\n [spinners]=\"true\"\n required>\n </kendo-numerictextbox>\n <kendo-formhint>Number of columns in the grid (1-12)</kendo-formhint>\n @if (columns < 1 || columns > 12) {\n <kendo-formerror>Columns must be between 1 and 12</kendo-formerror>\n }\n </kendo-formfield>\n\n <!-- Row Height Field -->\n <kendo-formfield>\n <kendo-label [for]=\"rowHeightInput\" text=\"Row Height *\"></kendo-label>\n <kendo-numerictextbox\n #rowHeightInput\n [(ngModel)]=\"rowHeight\"\n name=\"rowHeight\"\n [min]=\"100\"\n [max]=\"1000\"\n [decimals]=\"0\"\n [format]=\"'n0'\"\n [spinners]=\"true\"\n required>\n </kendo-numerictextbox>\n <kendo-formhint>Height of each row in pixels (100-1000)</kendo-formhint>\n @if (rowHeight < 100 || rowHeight > 1000) {\n <kendo-formerror>Row height must be between 100 and 1000</kendo-formerror>\n }\n </kendo-formfield>\n\n <!-- Gap Field -->\n <kendo-formfield>\n <kendo-label [for]=\"gapInput\" text=\"Gap *\"></kendo-label>\n <kendo-numerictextbox\n #gapInput\n [(ngModel)]=\"gap\"\n name=\"gap\"\n [min]=\"0\"\n [max]=\"100\"\n [decimals]=\"0\"\n [format]=\"'n0'\"\n [spinners]=\"true\"\n required>\n </kendo-numerictextbox>\n <kendo-formhint>Space between widgets in pixels (0-100)</kendo-formhint>\n @if (gap < 0 || gap > 100) {\n <kendo-formerror>Gap must be between 0 and 100</kendo-formerror>\n }\n </kendo-formfield>\n\n <!-- Auto-Refresh Field -->\n <kendo-formfield>\n <kendo-label [for]=\"autoRefreshInput\" text=\"Auto-refresh\"></kendo-label>\n <kendo-numerictextbox\n #autoRefreshInput\n [(ngModel)]=\"autoRefreshSeconds\"\n name=\"autoRefreshSeconds\"\n [min]=\"0\"\n [max]=\"3600\"\n [decimals]=\"0\"\n [format]=\"'n0'\"\n [spinners]=\"true\">\n </kendo-numerictextbox>\n <kendo-formhint>Refresh interval in seconds (0 = disabled, max 3600). Pauses while the tab is hidden.</kendo-formhint>\n @if (autoRefreshSeconds < 0 || autoRefreshSeconds > 3600) {\n <kendo-formerror>Auto-refresh must be between 0 and 3600 seconds</kendo-formerror>\n }\n </kendo-formfield>\n </form>\n </div>\n </ng-template>\n </kendo-tabstrip-tab>\n\n <!-- Variables Tab -->\n <kendo-tabstrip-tab [title]=\"'Variables'\">\n <ng-template kendoTabContent>\n <div class=\"tab-content\">\n <mm-variables-editor\n [(variables)]=\"variables\">\n </mm-variables-editor>\n </div>\n </ng-template>\n </kendo-tabstrip-tab>\n\n <!-- Time Filter Tab -->\n <kendo-tabstrip-tab [title]=\"'Time Filter'\">\n <ng-template kendoTabContent>\n <div class=\"tab-content\">\n <form class=\"settings-form\">\n <kendo-formfield>\n <kendo-label text=\"Timezone\"></kendo-label>\n <div class=\"radio-group\">\n <div class=\"radio-wrapper\">\n <input\n type=\"radio\"\n kendoRadioButton\n #tzLocal\n [(ngModel)]=\"timeZoneMode\"\n name=\"timeZoneMode\"\n value=\"local\"\n id=\"timeZoneModeLocal\"/>\n <kendo-label [for]=\"tzLocal\" text=\"Local time (browser)\" class=\"radio-label\"></kendo-label>\n </div>\n <div class=\"radio-wrapper\">\n <input\n type=\"radio\"\n kendoRadioButton\n #tzUtc\n [(ngModel)]=\"timeZoneMode\"\n name=\"timeZoneMode\"\n value=\"utc\"\n id=\"timeZoneModeUtc\"/>\n <kendo-label [for]=\"tzUtc\" text=\"UTC\" class=\"radio-label\"></kendo-label>\n </div>\n <div class=\"radio-wrapper\">\n <input\n type=\"radio\"\n kendoRadioButton\n #tzZone\n [(ngModel)]=\"timeZoneMode\"\n name=\"timeZoneMode\"\n [value]=\"zoneSelection\"\n id=\"timeZoneModeZone\"/>\n <kendo-label [for]=\"tzZone\" text=\"Specific time zone\" class=\"radio-label\"></kendo-label>\n <kendo-dropdownlist\n class=\"tz-zone-select\"\n [data]=\"commonTimeZones\"\n [value]=\"zoneSelection\"\n (valueChange)=\"onZoneSelectionChange($event)\"\n [disabled]=\"!isSpecificZone\"\n [filterable]=\"true\"\n aria-label=\"IANA time zone\">\n </kendo-dropdownlist>\n </div>\n </div>\n <kendo-formhint>\n Basis for the time filter and for how all widgets display dates and times. Local time is the default.\n A specific time zone resolves civil days (today / yesterday / this month) DST-correctly in that\n zone and selects matching calendar rollups.\n </kendo-formhint>\n </kendo-formfield>\n\n <kendo-formfield>\n <div class=\"checkbox-wrapper\">\n <input\n type=\"checkbox\"\n kendoCheckBox\n #timeFilterCheckbox\n [(ngModel)]=\"timeFilterEnabled\"\n name=\"timeFilterEnabled\"\n id=\"timeFilterEnabled\"/>\n <kendo-label\n [for]=\"timeFilterCheckbox\"\n text=\"Enable Time Filter\"\n class=\"checkbox-label\">\n </kendo-label>\n </div>\n <kendo-formhint>\n Shows a time range picker in the toolbar. Sets $timeRangeFrom and $timeRangeTo variables.\n </kendo-formhint>\n </kendo-formfield>\n\n @if (timeFilterEnabled) {\n <kendo-formfield>\n <kendo-label text=\"Default Selection\"></kendo-label>\n <mm-time-range-picker\n [initialSelection]=\"initialDefaultSelection\"\n (selectionChange)=\"onDefaultSelectionChange($event)\">\n </mm-time-range-picker>\n <kendo-formhint>Initial time filter shown when no URL parameters are set. Note: User-selected filters override this default. Use the reset button in the toolbar to revert to the default.</kendo-formhint>\n </kendo-formfield>\n }\n </form>\n </div>\n </ng-template>\n </kendo-tabstrip-tab>\n\n <!-- Entity Selectors Tab -->\n <kendo-tabstrip-tab [title]=\"'Entity Selectors'\">\n <ng-template kendoTabContent>\n <div class=\"tab-content\">\n <mm-entity-selector-editor\n [(entitySelectors)]=\"entitySelectors\"\n [existingVariableNames]=\"staticVariableNames\"\n [variables]=\"variables\"\n (editingStateChange)=\"entitySelectorEditing = $event\">\n </mm-entity-selector-editor>\n </div>\n </ng-template>\n </kendo-tabstrip-tab>\n </kendo-tabstrip>\n\n <!-- Dialog Actions -->\n @if (!entitySelectorEditing) {\n <div class=\"dialog-actions mm-dialog-actions\">\n <button kendoButton (click)=\"cancel()\" fillMode=\"flat\">\n Cancel\n </button>\n <button\n kendoButton\n (click)=\"save()\"\n [disabled]=\"!isValid\"\n themeColor=\"primary\">\n Save\n </button>\n </div>\n }\n</div>\n", styles: [".meshboard-settings-dialog{display:flex;flex-direction:column;height:100%;overflow:hidden}.meshboard-settings-dialog kendo-tabstrip{flex:1;min-height:0;display:flex;flex-direction:column}.meshboard-settings-dialog kendo-tabstrip ::ng-deep .k-tabstrip-content{flex:1;min-height:0;overflow:hidden}.meshboard-settings-dialog .tab-content{height:100%;overflow-y:auto;padding:1.5rem}.meshboard-settings-dialog .settings-form{display:flex;flex-direction:column;gap:1.25rem}.meshboard-settings-dialog .settings-form kendo-formfield{display:flex;flex-direction:column;gap:.5rem}.meshboard-settings-dialog .settings-form kendo-formfield kendo-label{font-weight:500;color:var(--kendo-color-on-app-surface, #424242)}.meshboard-settings-dialog .settings-form kendo-formfield kendo-textbox,.meshboard-settings-dialog .settings-form kendo-formfield kendo-textarea,.meshboard-settings-dialog .settings-form kendo-formfield kendo-numerictextbox{width:100%}.meshboard-settings-dialog .settings-form kendo-formfield kendo-formhint{font-size:.75rem;color:var(--kendo-color-subtle, #757575)}.meshboard-settings-dialog .settings-form kendo-formfield kendo-formerror{font-size:.75rem;color:var(--kendo-color-error, #f44336)}.meshboard-settings-dialog .settings-form .section-title{font-size:.875rem;font-weight:600;color:var(--kendo-color-on-app-surface, #424242);margin-top:.5rem;padding-bottom:.5rem;border-bottom:1px solid var(--kendo-color-border, #e0e0e0)}.meshboard-settings-dialog .settings-form .checkbox-wrapper,.meshboard-settings-dialog .settings-form .radio-wrapper{display:flex;align-items:center;gap:.5rem}.meshboard-settings-dialog .settings-form .checkbox-wrapper .checkbox-label,.meshboard-settings-dialog .settings-form .checkbox-wrapper .radio-label,.meshboard-settings-dialog .settings-form .radio-wrapper .checkbox-label,.meshboard-settings-dialog .settings-form .radio-wrapper .radio-label{font-weight:400;cursor:pointer}.meshboard-settings-dialog .settings-form .checkbox-wrapper .tz-zone-select,.meshboard-settings-dialog .settings-form .radio-wrapper .tz-zone-select{min-width:12rem}.meshboard-settings-dialog .settings-form .radio-group{display:flex;flex-direction:column;gap:.5rem;margin-top:.25rem}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox]:not([ngNoCva])[formControlName],input[type=checkbox]:not([ngNoCva])[formControl],input[type=checkbox]:not([ngNoCva])[ngModel]" }, { kind: "directive", type: i1$1.RadioControlValueAccessor, selector: "input[type=radio]:not([ngNoCva])[formControlName],input[type=radio]:not([ngNoCva])[formControl],input[type=radio]:not([ngNoCva])[ngModel]", inputs: ["name", "formControlName", "value"] }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1$1.NgForm, selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: InputsModule }, { kind: "component", type: i3.TextBoxComponent, selector: "kendo-textbox", inputs: ["focusableId", "title", "type", "disabled", "readonly", "tabindex", "value", "selectOnFocus", "showSuccessIcon", "showErrorIcon", "clearButton", "successIcon", "successSvgIcon", "errorIcon", "errorSvgIcon", "clearButtonIcon", "clearButtonSvgIcon", "size", "rounded", "fillMode", "tabIndex", "placeholder", "maxlength", "inputAttributes"], outputs: ["valueChange", "inputFocus", "inputBlur", "focus", "blur"], exportAs: ["kendoTextBox"] }, { kind: "component", type: i3.NumericTextBoxComponent, selector: "kendo-numerictextbox", inputs: ["focusableId", "disabled", "readonly", "title", "autoCorrect", "format", "max", "min", "decimals", "placeholder", "step", "spinners", "rangeValidation", "tabindex", "tabIndex", "changeValueOnScroll", "selectOnFocus", "value", "maxlength", "size", "rounded", "fillMode", "inputAttributes"], outputs: ["valueChange", "focus", "blur", "inputFocus", "inputBlur"], exportAs: ["kendoNumericTextBox"] }, { kind: "component", type: i3.TextAreaComponent, selector: "kendo-textarea", inputs: ["focusableId", "flow", "inputAttributes", "adornmentsOrientation", "rows", "cols", "maxlength", "maxResizableRows", "tabindex", "tabIndex", "resizable", "size", "rounded", "fillMode", "showPrefixSeparator", "showSuffixSeparator"], outputs: ["focus", "blur", "valueChange"], exportAs: ["kendoTextArea"] }, { kind: "directive", type: i3.CheckBoxDirective, selector: "input[kendoCheckBox]", inputs: ["size", "rounded"] }, { kind: "directive", type: i3.RadioButtonDirective, selector: "input[kendoRadioButton]", inputs: ["size"] }, { kind: "component", type: i3.FormFieldComponent, selector: "kendo-formfield", inputs: ["showHints", "orientation", "showErrors", "colSpan"] }, { kind: "component", type: i3.HintComponent, selector: "kendo-formhint", inputs: ["align"] }, { kind: "component", type: i3.ErrorComponent, selector: "kendo-formerror", inputs: ["align"] }, { kind: "ngmodule", type: CheckBoxModule }, { kind: "ngmodule", type: LabelModule }, { kind: "component", type: i4$1.LabelComponent, selector: "kendo-label", inputs: ["text", "for", "optional", "labelCssStyle", "labelCssClass"], exportAs: ["kendoLabel"] }, { kind: "ngmodule", type: FormFieldModule }, { kind: "ngmodule", type: TabStripModule }, { kind: "component", type: i5.TabStripComponent, selector: "kendo-tabstrip", inputs: ["height", "animate", "tabAlignment", "tabPosition", "keepTabContent", "closable", "scrollable", "size", "closeIcon", "closeIconClass", "closeSVGIcon", "showContentArea"], outputs: ["tabSelect", "tabClose", "tabScroll"], exportAs: ["kendoTabStrip"] }, { kind: "component", type: i5.TabStripTabComponent, selector: "kendo-tabstrip-tab", inputs: ["title", "disabled", "cssClass", "cssStyle", "selected", "closable", "closeIcon", "closeIconClass", "closeSVGIcon"], exportAs: ["kendoTabStripTab"] }, { kind: "directive", type: i5.TabContentDirective, selector: "[kendoTabContent]" }, { kind: "ngmodule", type: DropDownListModule }, { kind: "component", type: i4.DropDownListComponent, selector: "kendo-dropdownlist", inputs: ["customIconClass", "showStickyHeader", "icon", "svgIcon", "loading", "data", "value", "textField", "valueField", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "defaultItem", "disabled", "itemDisabled", "readonly", "filterable", "virtual", "ignoreCase", "delay", "valuePrimitive", "tabindex", "tabIndex", "size", "rounded", "fillMode", "leftRightArrowsNavigation", "id"], outputs: ["valueChange", "filterChange", "selectionChange", "open", "opened", "close", "closed", "focus", "blur"], exportAs: ["kendoDropDownList"] }, { kind: "component", type: VariablesEditorComponent, selector: "mm-variables-editor", inputs: ["variables"], outputs: ["variablesChange"] }, { kind: "component", type: EntitySelectorEditorComponent, selector: "mm-entity-selector-editor", inputs: ["entitySelectors", "existingVariableNames", "variables"], outputs: ["entitySelectorsChange", "editingStateChange"] }, { kind: "component", type: TimeRangePickerComponent, selector: "mm-time-range-picker", inputs: ["config", "labels", "initialSelection"], outputs: ["rangeChange", "rangeChangeISO", "selectionChange"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
|
|
33634
35090
|
}
|
|
33635
35091
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: MeshBoardSettingsDialogComponent, decorators: [{
|
|
33636
35092
|
type: Component,
|
|
@@ -33647,7 +35103,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
33647
35103
|
VariablesEditorComponent,
|
|
33648
35104
|
EntitySelectorEditorComponent,
|
|
33649
35105
|
TimeRangePickerComponent
|
|
33650
|
-
], changeDetection: ChangeDetectionStrategy.Eager, template: "<div class=\"meshboard-settings-dialog\">\n <kendo-tabstrip [animate]=\"false\">\n <!-- General Tab -->\n <kendo-tabstrip-tab [title]=\"'General'\" [selected]=\"true\">\n <ng-template kendoTabContent>\n <div class=\"tab-content\">\n <form class=\"settings-form\">\n <!-- Name Field -->\n <kendo-formfield>\n <kendo-label [for]=\"nameInput\" text=\"Name *\"></kendo-label>\n <kendo-textbox\n #nameInput\n [(ngModel)]=\"name\"\n name=\"name\"\n placeholder=\"Enter MeshBoard name\"\n required>\n </kendo-textbox>\n @if (name.trim().length === 0) {\n <kendo-formerror>Name is required</kendo-formerror>\n }\n </kendo-formfield>\n\n <!-- Description Field -->\n <kendo-formfield>\n <kendo-label [for]=\"descriptionInput\" text=\"Description\"></kendo-label>\n <kendo-textarea\n #descriptionInput\n [(ngModel)]=\"description\"\n name=\"description\"\n placeholder=\"Enter MeshBoard description (optional)\"\n [rows]=\"3\">\n </kendo-textarea>\n </kendo-formfield>\n\n <!-- Well-Known Name Field -->\n <kendo-formfield>\n <kendo-label [for]=\"wellKnownNameInput\" text=\"Well-Known Name\"></kendo-label>\n <kendo-textbox\n #wellKnownNameInput\n [(ngModel)]=\"rtWellKnownName\"\n name=\"rtWellKnownName\"\n placeholder=\"e.g., cockpit, dashboard-main\">\n </kendo-textbox>\n <kendo-formhint>Unique identifier for routing. Use lowercase with hyphens (e.g., 'cockpit', 'sales-dashboard').</kendo-formhint>\n </kendo-formfield>\n\n <!-- Layout Settings -->\n <div class=\"section-title\">Layout Settings</div>\n\n <!-- Columns Field -->\n <kendo-formfield>\n <kendo-label [for]=\"columnsInput\" text=\"Columns *\"></kendo-label>\n <kendo-numerictextbox\n #columnsInput\n [(ngModel)]=\"columns\"\n name=\"columns\"\n [min]=\"1\"\n [max]=\"12\"\n [decimals]=\"0\"\n [format]=\"'n0'\"\n [spinners]=\"true\"\n required>\n </kendo-numerictextbox>\n <kendo-formhint>Number of columns in the grid (1-12)</kendo-formhint>\n @if (columns < 1 || columns > 12) {\n <kendo-formerror>Columns must be between 1 and 12</kendo-formerror>\n }\n </kendo-formfield>\n\n <!-- Row Height Field -->\n <kendo-formfield>\n <kendo-label [for]=\"rowHeightInput\" text=\"Row Height *\"></kendo-label>\n <kendo-numerictextbox\n #rowHeightInput\n [(ngModel)]=\"rowHeight\"\n name=\"rowHeight\"\n [min]=\"100\"\n [max]=\"1000\"\n [decimals]=\"0\"\n [format]=\"'n0'\"\n [spinners]=\"true\"\n required>\n </kendo-numerictextbox>\n <kendo-formhint>Height of each row in pixels (100-1000)</kendo-formhint>\n @if (rowHeight < 100 || rowHeight > 1000) {\n <kendo-formerror>Row height must be between 100 and 1000</kendo-formerror>\n }\n </kendo-formfield>\n\n <!-- Gap Field -->\n <kendo-formfield>\n <kendo-label [for]=\"gapInput\" text=\"Gap *\"></kendo-label>\n <kendo-numerictextbox\n #gapInput\n [(ngModel)]=\"gap\"\n name=\"gap\"\n [min]=\"0\"\n [max]=\"100\"\n [decimals]=\"0\"\n [format]=\"'n0'\"\n [spinners]=\"true\"\n required>\n </kendo-numerictextbox>\n <kendo-formhint>Space between widgets in pixels (0-100)</kendo-formhint>\n @if (gap < 0 || gap > 100) {\n <kendo-formerror>Gap must be between 0 and 100</kendo-formerror>\n }\n </kendo-formfield>\n\n <!-- Auto-Refresh Field -->\n <kendo-formfield>\n <kendo-label [for]=\"autoRefreshInput\" text=\"Auto-refresh\"></kendo-label>\n <kendo-numerictextbox\n #autoRefreshInput\n [(ngModel)]=\"autoRefreshSeconds\"\n name=\"autoRefreshSeconds\"\n [min]=\"0\"\n [max]=\"3600\"\n [decimals]=\"0\"\n [format]=\"'n0'\"\n [spinners]=\"true\">\n </kendo-numerictextbox>\n <kendo-formhint>Refresh interval in seconds (0 = disabled, max 3600). Pauses while the tab is hidden.</kendo-formhint>\n @if (autoRefreshSeconds < 0 || autoRefreshSeconds > 3600) {\n <kendo-formerror>Auto-refresh must be between 0 and 3600 seconds</kendo-formerror>\n }\n </kendo-formfield>\n </form>\n </div>\n </ng-template>\n </kendo-tabstrip-tab>\n\n <!-- Variables Tab -->\n <kendo-tabstrip-tab [title]=\"'Variables'\">\n <ng-template kendoTabContent>\n <div class=\"tab-content\">\n <mm-variables-editor\n [(variables)]=\"variables\">\n </mm-variables-editor>\n </div>\n </ng-template>\n </kendo-tabstrip-tab>\n\n <!-- Time Filter Tab -->\n <kendo-tabstrip-tab [title]=\"'Time Filter'\">\n <ng-template kendoTabContent>\n <div class=\"tab-content\">\n <form class=\"settings-form\">\n <kendo-formfield>\n <kendo-label text=\"Timezone\"></kendo-label>\n <div class=\"radio-group\">\n <div class=\"radio-wrapper\">\n <input\n type=\"radio\"\n kendoRadioButton\n #tzLocal\n [(ngModel)]=\"timeZoneMode\"\n name=\"timeZoneMode\"\n value=\"local\"\n id=\"timeZoneModeLocal\"/>\n <kendo-label [for]=\"tzLocal\" text=\"Local time (browser)\" class=\"radio-label\"></kendo-label>\n </div>\n <div class=\"radio-wrapper\">\n <input\n type=\"radio\"\n kendoRadioButton\n #tzUtc\n [(ngModel)]=\"timeZoneMode\"\n name=\"timeZoneMode\"\n value=\"utc\"\n id=\"timeZoneModeUtc\"/>\n <kendo-label [for]=\"tzUtc\" text=\"UTC\" class=\"radio-label\"></kendo-label>\n </div>\n <div class=\"radio-wrapper\">\n <input\n type=\"radio\"\n kendoRadioButton\n #tzZone\n [(ngModel)]=\"timeZoneMode\"\n name=\"timeZoneMode\"\n [value]=\"zoneSelection\"\n id=\"timeZoneModeZone\"/>\n <kendo-label [for]=\"tzZone\" text=\"Specific time zone\" class=\"radio-label\"></kendo-label>\n <kendo-dropdownlist\n class=\"tz-zone-select\"\n [data]=\"commonTimeZones\"\n [value]=\"zoneSelection\"\n (valueChange)=\"onZoneSelectionChange($event)\"\n [disabled]=\"!isSpecificZone\"\n [filterable]=\"true\"\n aria-label=\"IANA time zone\">\n </kendo-dropdownlist>\n </div>\n </div>\n <kendo-formhint>\n Basis for the time filter and for how all widgets display dates and times. Local time is the default.\n A specific time zone resolves civil days (today / yesterday / this month) DST-correctly in that\n zone and selects matching calendar rollups.\n </kendo-formhint>\n </kendo-formfield>\n\n <kendo-formfield>\n <div class=\"checkbox-wrapper\">\n <input\n type=\"checkbox\"\n kendoCheckBox\n #timeFilterCheckbox\n [(ngModel)]=\"timeFilterEnabled\"\n name=\"timeFilterEnabled\"\n id=\"timeFilterEnabled\"/>\n <kendo-label\n [for]=\"timeFilterCheckbox\"\n text=\"Enable Time Filter\"\n class=\"checkbox-label\">\n </kendo-label>\n </div>\n <kendo-formhint>\n Shows a time range picker in the toolbar. Sets $timeRangeFrom and $timeRangeTo variables.\n </kendo-formhint>\n </kendo-formfield>\n\n @if (timeFilterEnabled) {\n <kendo-formfield>\n <kendo-label text=\"Default Selection\"></kendo-label>\n <mm-time-range-picker\n [initialSelection]=\"initialDefaultSelection\"\n (selectionChange)=\"onDefaultSelectionChange($event)\">\n </mm-time-range-picker>\n <kendo-formhint>Initial time filter shown when no URL parameters are set. Note: User-selected filters override this default. Use the reset button in the toolbar to revert to the default.</kendo-formhint>\n </kendo-formfield>\n }\n </form>\n </div>\n </ng-template>\n </kendo-tabstrip-tab>\n\n <!-- Entity Selectors Tab -->\n <kendo-tabstrip-tab [title]=\"'Entity Selectors'\">\n <ng-template kendoTabContent>\n <div class=\"tab-content\">\n <mm-entity-selector-editor\n [(entitySelectors)]=\"entitySelectors\"\n [existingVariableNames]=\"staticVariableNames\"\n (editingStateChange)=\"entitySelectorEditing = $event\">\n </mm-entity-selector-editor>\n </div>\n </ng-template>\n </kendo-tabstrip-tab>\n </kendo-tabstrip>\n\n <!-- Dialog Actions -->\n @if (!entitySelectorEditing) {\n <div class=\"dialog-actions mm-dialog-actions\">\n <button kendoButton (click)=\"cancel()\" fillMode=\"flat\">\n Cancel\n </button>\n <button\n kendoButton\n (click)=\"save()\"\n [disabled]=\"!isValid\"\n themeColor=\"primary\">\n Save\n </button>\n </div>\n }\n</div>\n", styles: [".meshboard-settings-dialog{display:flex;flex-direction:column;height:100%;overflow:hidden}.meshboard-settings-dialog kendo-tabstrip{flex:1;min-height:0;display:flex;flex-direction:column}.meshboard-settings-dialog kendo-tabstrip ::ng-deep .k-tabstrip-content{flex:1;min-height:0;overflow:hidden}.meshboard-settings-dialog .tab-content{height:100%;overflow-y:auto;padding:1.5rem}.meshboard-settings-dialog .settings-form{display:flex;flex-direction:column;gap:1.25rem}.meshboard-settings-dialog .settings-form kendo-formfield{display:flex;flex-direction:column;gap:.5rem}.meshboard-settings-dialog .settings-form kendo-formfield kendo-label{font-weight:500;color:var(--kendo-color-on-app-surface, #424242)}.meshboard-settings-dialog .settings-form kendo-formfield kendo-textbox,.meshboard-settings-dialog .settings-form kendo-formfield kendo-textarea,.meshboard-settings-dialog .settings-form kendo-formfield kendo-numerictextbox{width:100%}.meshboard-settings-dialog .settings-form kendo-formfield kendo-formhint{font-size:.75rem;color:var(--kendo-color-subtle, #757575)}.meshboard-settings-dialog .settings-form kendo-formfield kendo-formerror{font-size:.75rem;color:var(--kendo-color-error, #f44336)}.meshboard-settings-dialog .settings-form .section-title{font-size:.875rem;font-weight:600;color:var(--kendo-color-on-app-surface, #424242);margin-top:.5rem;padding-bottom:.5rem;border-bottom:1px solid var(--kendo-color-border, #e0e0e0)}.meshboard-settings-dialog .settings-form .checkbox-wrapper,.meshboard-settings-dialog .settings-form .radio-wrapper{display:flex;align-items:center;gap:.5rem}.meshboard-settings-dialog .settings-form .checkbox-wrapper .checkbox-label,.meshboard-settings-dialog .settings-form .checkbox-wrapper .radio-label,.meshboard-settings-dialog .settings-form .radio-wrapper .checkbox-label,.meshboard-settings-dialog .settings-form .radio-wrapper .radio-label{font-weight:400;cursor:pointer}.meshboard-settings-dialog .settings-form .checkbox-wrapper .tz-zone-select,.meshboard-settings-dialog .settings-form .radio-wrapper .tz-zone-select{min-width:12rem}.meshboard-settings-dialog .settings-form .radio-group{display:flex;flex-direction:column;gap:.5rem;margin-top:.25rem}\n"] }]
|
|
35106
|
+
], changeDetection: ChangeDetectionStrategy.Eager, template: "<div class=\"meshboard-settings-dialog\">\n <kendo-tabstrip [animate]=\"false\">\n <!-- General Tab -->\n <kendo-tabstrip-tab [title]=\"'General'\" [selected]=\"true\">\n <ng-template kendoTabContent>\n <div class=\"tab-content\">\n <form class=\"settings-form\">\n <!-- Name Field -->\n <kendo-formfield>\n <kendo-label [for]=\"nameInput\" text=\"Name *\"></kendo-label>\n <kendo-textbox\n #nameInput\n [(ngModel)]=\"name\"\n name=\"name\"\n placeholder=\"Enter MeshBoard name\"\n required>\n </kendo-textbox>\n @if (name.trim().length === 0) {\n <kendo-formerror>Name is required</kendo-formerror>\n }\n </kendo-formfield>\n\n <!-- Description Field -->\n <kendo-formfield>\n <kendo-label [for]=\"descriptionInput\" text=\"Description\"></kendo-label>\n <kendo-textarea\n #descriptionInput\n [(ngModel)]=\"description\"\n name=\"description\"\n placeholder=\"Enter MeshBoard description (optional)\"\n [rows]=\"3\">\n </kendo-textarea>\n </kendo-formfield>\n\n <!-- Well-Known Name Field -->\n <kendo-formfield>\n <kendo-label [for]=\"wellKnownNameInput\" text=\"Well-Known Name\"></kendo-label>\n <kendo-textbox\n #wellKnownNameInput\n [(ngModel)]=\"rtWellKnownName\"\n name=\"rtWellKnownName\"\n placeholder=\"e.g., cockpit, dashboard-main\">\n </kendo-textbox>\n <kendo-formhint>Unique identifier for routing. Use lowercase with hyphens (e.g., 'cockpit', 'sales-dashboard').</kendo-formhint>\n </kendo-formfield>\n\n <!-- Layout Settings -->\n <div class=\"section-title\">Layout Settings</div>\n\n <!-- Columns Field -->\n <kendo-formfield>\n <kendo-label [for]=\"columnsInput\" text=\"Columns *\"></kendo-label>\n <kendo-numerictextbox\n #columnsInput\n [(ngModel)]=\"columns\"\n name=\"columns\"\n [min]=\"1\"\n [max]=\"12\"\n [decimals]=\"0\"\n [format]=\"'n0'\"\n [spinners]=\"true\"\n required>\n </kendo-numerictextbox>\n <kendo-formhint>Number of columns in the grid (1-12)</kendo-formhint>\n @if (columns < 1 || columns > 12) {\n <kendo-formerror>Columns must be between 1 and 12</kendo-formerror>\n }\n </kendo-formfield>\n\n <!-- Row Height Field -->\n <kendo-formfield>\n <kendo-label [for]=\"rowHeightInput\" text=\"Row Height *\"></kendo-label>\n <kendo-numerictextbox\n #rowHeightInput\n [(ngModel)]=\"rowHeight\"\n name=\"rowHeight\"\n [min]=\"100\"\n [max]=\"1000\"\n [decimals]=\"0\"\n [format]=\"'n0'\"\n [spinners]=\"true\"\n required>\n </kendo-numerictextbox>\n <kendo-formhint>Height of each row in pixels (100-1000)</kendo-formhint>\n @if (rowHeight < 100 || rowHeight > 1000) {\n <kendo-formerror>Row height must be between 100 and 1000</kendo-formerror>\n }\n </kendo-formfield>\n\n <!-- Gap Field -->\n <kendo-formfield>\n <kendo-label [for]=\"gapInput\" text=\"Gap *\"></kendo-label>\n <kendo-numerictextbox\n #gapInput\n [(ngModel)]=\"gap\"\n name=\"gap\"\n [min]=\"0\"\n [max]=\"100\"\n [decimals]=\"0\"\n [format]=\"'n0'\"\n [spinners]=\"true\"\n required>\n </kendo-numerictextbox>\n <kendo-formhint>Space between widgets in pixels (0-100)</kendo-formhint>\n @if (gap < 0 || gap > 100) {\n <kendo-formerror>Gap must be between 0 and 100</kendo-formerror>\n }\n </kendo-formfield>\n\n <!-- Auto-Refresh Field -->\n <kendo-formfield>\n <kendo-label [for]=\"autoRefreshInput\" text=\"Auto-refresh\"></kendo-label>\n <kendo-numerictextbox\n #autoRefreshInput\n [(ngModel)]=\"autoRefreshSeconds\"\n name=\"autoRefreshSeconds\"\n [min]=\"0\"\n [max]=\"3600\"\n [decimals]=\"0\"\n [format]=\"'n0'\"\n [spinners]=\"true\">\n </kendo-numerictextbox>\n <kendo-formhint>Refresh interval in seconds (0 = disabled, max 3600). Pauses while the tab is hidden.</kendo-formhint>\n @if (autoRefreshSeconds < 0 || autoRefreshSeconds > 3600) {\n <kendo-formerror>Auto-refresh must be between 0 and 3600 seconds</kendo-formerror>\n }\n </kendo-formfield>\n </form>\n </div>\n </ng-template>\n </kendo-tabstrip-tab>\n\n <!-- Variables Tab -->\n <kendo-tabstrip-tab [title]=\"'Variables'\">\n <ng-template kendoTabContent>\n <div class=\"tab-content\">\n <mm-variables-editor\n [(variables)]=\"variables\">\n </mm-variables-editor>\n </div>\n </ng-template>\n </kendo-tabstrip-tab>\n\n <!-- Time Filter Tab -->\n <kendo-tabstrip-tab [title]=\"'Time Filter'\">\n <ng-template kendoTabContent>\n <div class=\"tab-content\">\n <form class=\"settings-form\">\n <kendo-formfield>\n <kendo-label text=\"Timezone\"></kendo-label>\n <div class=\"radio-group\">\n <div class=\"radio-wrapper\">\n <input\n type=\"radio\"\n kendoRadioButton\n #tzLocal\n [(ngModel)]=\"timeZoneMode\"\n name=\"timeZoneMode\"\n value=\"local\"\n id=\"timeZoneModeLocal\"/>\n <kendo-label [for]=\"tzLocal\" text=\"Local time (browser)\" class=\"radio-label\"></kendo-label>\n </div>\n <div class=\"radio-wrapper\">\n <input\n type=\"radio\"\n kendoRadioButton\n #tzUtc\n [(ngModel)]=\"timeZoneMode\"\n name=\"timeZoneMode\"\n value=\"utc\"\n id=\"timeZoneModeUtc\"/>\n <kendo-label [for]=\"tzUtc\" text=\"UTC\" class=\"radio-label\"></kendo-label>\n </div>\n <div class=\"radio-wrapper\">\n <input\n type=\"radio\"\n kendoRadioButton\n #tzZone\n [(ngModel)]=\"timeZoneMode\"\n name=\"timeZoneMode\"\n [value]=\"zoneSelection\"\n id=\"timeZoneModeZone\"/>\n <kendo-label [for]=\"tzZone\" text=\"Specific time zone\" class=\"radio-label\"></kendo-label>\n <kendo-dropdownlist\n class=\"tz-zone-select\"\n [data]=\"commonTimeZones\"\n [value]=\"zoneSelection\"\n (valueChange)=\"onZoneSelectionChange($event)\"\n [disabled]=\"!isSpecificZone\"\n [filterable]=\"true\"\n aria-label=\"IANA time zone\">\n </kendo-dropdownlist>\n </div>\n </div>\n <kendo-formhint>\n Basis for the time filter and for how all widgets display dates and times. Local time is the default.\n A specific time zone resolves civil days (today / yesterday / this month) DST-correctly in that\n zone and selects matching calendar rollups.\n </kendo-formhint>\n </kendo-formfield>\n\n <kendo-formfield>\n <div class=\"checkbox-wrapper\">\n <input\n type=\"checkbox\"\n kendoCheckBox\n #timeFilterCheckbox\n [(ngModel)]=\"timeFilterEnabled\"\n name=\"timeFilterEnabled\"\n id=\"timeFilterEnabled\"/>\n <kendo-label\n [for]=\"timeFilterCheckbox\"\n text=\"Enable Time Filter\"\n class=\"checkbox-label\">\n </kendo-label>\n </div>\n <kendo-formhint>\n Shows a time range picker in the toolbar. Sets $timeRangeFrom and $timeRangeTo variables.\n </kendo-formhint>\n </kendo-formfield>\n\n @if (timeFilterEnabled) {\n <kendo-formfield>\n <kendo-label text=\"Default Selection\"></kendo-label>\n <mm-time-range-picker\n [initialSelection]=\"initialDefaultSelection\"\n (selectionChange)=\"onDefaultSelectionChange($event)\">\n </mm-time-range-picker>\n <kendo-formhint>Initial time filter shown when no URL parameters are set. Note: User-selected filters override this default. Use the reset button in the toolbar to revert to the default.</kendo-formhint>\n </kendo-formfield>\n }\n </form>\n </div>\n </ng-template>\n </kendo-tabstrip-tab>\n\n <!-- Entity Selectors Tab -->\n <kendo-tabstrip-tab [title]=\"'Entity Selectors'\">\n <ng-template kendoTabContent>\n <div class=\"tab-content\">\n <mm-entity-selector-editor\n [(entitySelectors)]=\"entitySelectors\"\n [existingVariableNames]=\"staticVariableNames\"\n [variables]=\"variables\"\n (editingStateChange)=\"entitySelectorEditing = $event\">\n </mm-entity-selector-editor>\n </div>\n </ng-template>\n </kendo-tabstrip-tab>\n </kendo-tabstrip>\n\n <!-- Dialog Actions -->\n @if (!entitySelectorEditing) {\n <div class=\"dialog-actions mm-dialog-actions\">\n <button kendoButton (click)=\"cancel()\" fillMode=\"flat\">\n Cancel\n </button>\n <button\n kendoButton\n (click)=\"save()\"\n [disabled]=\"!isValid\"\n themeColor=\"primary\">\n Save\n </button>\n </div>\n }\n</div>\n", styles: [".meshboard-settings-dialog{display:flex;flex-direction:column;height:100%;overflow:hidden}.meshboard-settings-dialog kendo-tabstrip{flex:1;min-height:0;display:flex;flex-direction:column}.meshboard-settings-dialog kendo-tabstrip ::ng-deep .k-tabstrip-content{flex:1;min-height:0;overflow:hidden}.meshboard-settings-dialog .tab-content{height:100%;overflow-y:auto;padding:1.5rem}.meshboard-settings-dialog .settings-form{display:flex;flex-direction:column;gap:1.25rem}.meshboard-settings-dialog .settings-form kendo-formfield{display:flex;flex-direction:column;gap:.5rem}.meshboard-settings-dialog .settings-form kendo-formfield kendo-label{font-weight:500;color:var(--kendo-color-on-app-surface, #424242)}.meshboard-settings-dialog .settings-form kendo-formfield kendo-textbox,.meshboard-settings-dialog .settings-form kendo-formfield kendo-textarea,.meshboard-settings-dialog .settings-form kendo-formfield kendo-numerictextbox{width:100%}.meshboard-settings-dialog .settings-form kendo-formfield kendo-formhint{font-size:.75rem;color:var(--kendo-color-subtle, #757575)}.meshboard-settings-dialog .settings-form kendo-formfield kendo-formerror{font-size:.75rem;color:var(--kendo-color-error, #f44336)}.meshboard-settings-dialog .settings-form .section-title{font-size:.875rem;font-weight:600;color:var(--kendo-color-on-app-surface, #424242);margin-top:.5rem;padding-bottom:.5rem;border-bottom:1px solid var(--kendo-color-border, #e0e0e0)}.meshboard-settings-dialog .settings-form .checkbox-wrapper,.meshboard-settings-dialog .settings-form .radio-wrapper{display:flex;align-items:center;gap:.5rem}.meshboard-settings-dialog .settings-form .checkbox-wrapper .checkbox-label,.meshboard-settings-dialog .settings-form .checkbox-wrapper .radio-label,.meshboard-settings-dialog .settings-form .radio-wrapper .checkbox-label,.meshboard-settings-dialog .settings-form .radio-wrapper .radio-label{font-weight:400;cursor:pointer}.meshboard-settings-dialog .settings-form .checkbox-wrapper .tz-zone-select,.meshboard-settings-dialog .settings-form .radio-wrapper .tz-zone-select{min-width:12rem}.meshboard-settings-dialog .settings-form .radio-group{display:flex;flex-direction:column;gap:.5rem;margin-top:.25rem}\n"] }]
|
|
33651
35107
|
}], ctorParameters: () => [] });
|
|
33652
35108
|
|
|
33653
35109
|
/**
|
|
@@ -34706,6 +36162,46 @@ class MeshBoardViewComponent {
|
|
|
34706
36162
|
...(ngDevMode ? [{ debugName: "routeChrome" }] : /* istanbul ignore next */ []));
|
|
34707
36163
|
effectiveChrome = computed(() => resolveMeshBoardChrome(this.chrome(), this.routeChrome()), /* @ts-ignore */
|
|
34708
36164
|
...(ngDevMode ? [{ debugName: "effectiveChrome" }] : /* istanbul ignore next */ []));
|
|
36165
|
+
/**
|
|
36166
|
+
* The period set by the host page (AB#7582): two exact instants, half-open `[from, to)`, as
|
|
36167
|
+
* `Date` objects or ISO 8601 strings with a zone designator. While set (and the board's time
|
|
36168
|
+
* filter is enabled) the view publishes exactly these instants as `$timeRangeFrom` /
|
|
36169
|
+
* `$timeRangeTo` and as the stream-data window - no day snapping, no zone conversion - and
|
|
36170
|
+
* ignores the `tf_*` query parameters and the stored/default selection. It writes no `tf_*`
|
|
36171
|
+
* parameters and causes no unsaved changes. A change reloads the widgets once; `undefined`
|
|
36172
|
+
* returns the board to its own selection. Day rules (e.g. a 06:00 gas day) belong to the host.
|
|
36173
|
+
* An invalid range (unparseable, or `from >= to`) sends no stream-data query and logs one
|
|
36174
|
+
* console error. A board with the time filter disabled ignores the input.
|
|
36175
|
+
*/
|
|
36176
|
+
timeRange = input(undefined, /* @ts-ignore */
|
|
36177
|
+
...(ngDevMode ? [{ debugName: "timeRange" }] : /* istanbul ignore next */ []));
|
|
36178
|
+
/**
|
|
36179
|
+
* Who controls the period: `board` (default, unchanged behaviour) shows the board's time-range
|
|
36180
|
+
* picker; `host` hides the picker and its reset button - entity selectors and actions stay.
|
|
36181
|
+
* While `timeRange` is set the picker is hidden and its events are ignored either way.
|
|
36182
|
+
*/
|
|
36183
|
+
timeFilterControl = input('board', /* @ts-ignore */
|
|
36184
|
+
...(ngDevMode ? [{ debugName: "timeFilterControl" }] : /* istanbul ignore next */ []));
|
|
36185
|
+
/** The parsed `timeRange` input; `undefined` = the host sets no period. */
|
|
36186
|
+
parsedHostTimeRange = computed(() => parseHostTimeRange(this.timeRange()), /* @ts-ignore */
|
|
36187
|
+
...(ngDevMode ? [{ debugName: "parsedHostTimeRange" }] : /* istanbul ignore next */ []));
|
|
36188
|
+
/** True while the host owns the period (a valid or an invalid `timeRange` is set). */
|
|
36189
|
+
isHostTimeRangeActive = computed(() => this.parsedHostTimeRange() !== undefined, /* @ts-ignore */
|
|
36190
|
+
...(ngDevMode ? [{ debugName: "isHostTimeRangeActive" }] : /* istanbul ignore next */ []));
|
|
36191
|
+
/** Whether the board's own time-range picker (and its reset button) is rendered. */
|
|
36192
|
+
showTimeRangePicker = computed(() => this.isTimeFilterEnabled() && this.timeFilterControl() !== 'host' && !this.isHostTimeRangeActive(), /* @ts-ignore */
|
|
36193
|
+
...(ngDevMode ? [{ debugName: "showTimeRangePicker" }] : /* istanbul ignore next */ []));
|
|
36194
|
+
/** The host range last applied to the state service (to reload only on a real change). */
|
|
36195
|
+
appliedHostTimeRange;
|
|
36196
|
+
/** The invalid host range already reported, so a board switch does not log it again. */
|
|
36197
|
+
reportedInvalidHostTimeRange;
|
|
36198
|
+
/**
|
|
36199
|
+
* Set once the host has owned the period: the `tf_*` parameters of the URL this view was opened
|
|
36200
|
+
* with are stale from then on and are not applied again when `timeRange` goes back to undefined.
|
|
36201
|
+
*/
|
|
36202
|
+
hostHasOwnedPeriod = false;
|
|
36203
|
+
/** Set in ngOnDestroy; the async initial load stops touching shared state after it (AB#7582). */
|
|
36204
|
+
destroyed = false;
|
|
34709
36205
|
// Computed link to MeshBoard page with tenant
|
|
34710
36206
|
meshBoardPageLink = computed(() => {
|
|
34711
36207
|
// Get tenant from route parameters (synchronously from snapshot)
|
|
@@ -34787,6 +36283,9 @@ class MeshBoardViewComponent {
|
|
|
34787
36283
|
// Entity Selector computed signals
|
|
34788
36284
|
entitySelectorsConfig = computed(() => this.stateService.getEntitySelectors(), /* @ts-ignore */
|
|
34789
36285
|
...(ngDevMode ? [{ debugName: "entitySelectorsConfig" }] : /* istanbul ignore next */ []));
|
|
36286
|
+
/** Board variables substituted into the selectors' picker filters (AB#7724). */
|
|
36287
|
+
boardVariables = computed(() => this.stateService.getVariables(), /* @ts-ignore */
|
|
36288
|
+
...(ngDevMode ? [{ debugName: "boardVariables" }] : /* istanbul ignore next */ []));
|
|
34790
36289
|
hasEntitySelectors = computed(() => !this.hideEntitySelectors() && (this.entitySelectorsConfig()?.some(es => showsInToolbar(es)) ?? false), /* @ts-ignore */
|
|
34791
36290
|
...(ngDevMode ? [{ debugName: "hasEntitySelectors" }] : /* istanbul ignore next */ []));
|
|
34792
36291
|
unselectedToolbarSelectors = computed(() => {
|
|
@@ -34828,6 +36327,15 @@ class MeshBoardViewComponent {
|
|
|
34828
36327
|
}
|
|
34829
36328
|
}
|
|
34830
36329
|
});
|
|
36330
|
+
// Host period (AB#7582): apply every change of the timeRange input after the initial load
|
|
36331
|
+
// (ngOnInit applies the initial value) and reload the widgets once.
|
|
36332
|
+
effect(() => {
|
|
36333
|
+
const parsed = this.parsedHostTimeRange();
|
|
36334
|
+
if (!this._isInitialized()) {
|
|
36335
|
+
return;
|
|
36336
|
+
}
|
|
36337
|
+
untracked(() => this.onHostTimeRangeChanged(parsed));
|
|
36338
|
+
});
|
|
34831
36339
|
// Effect to re-evaluate the auto-refresh timer whenever the MeshBoard
|
|
34832
36340
|
// config changes (e.g. user edits autoRefreshSeconds in settings, or a
|
|
34833
36341
|
// different board is loaded). Re-reads on the same interval skip the
|
|
@@ -34881,8 +36389,16 @@ class MeshBoardViewComponent {
|
|
|
34881
36389
|
this.router.navigateByUrl(applyTimeFilterParams(targetUrl, this.activeTimeFilterSelection()), { replaceUrl: true });
|
|
34882
36390
|
}
|
|
34883
36391
|
}
|
|
34884
|
-
/**
|
|
36392
|
+
/**
|
|
36393
|
+
* The active time-filter selection to mirror into the URL, or null when the filter is off or
|
|
36394
|
+
* the host owns the period or controls the filter (AB#7582: the view then writes no tf_*
|
|
36395
|
+
* parameters; with `timeFilterControl='host'` and no `timeRange` the board selection still
|
|
36396
|
+
* applies, but the URL is the host's).
|
|
36397
|
+
*/
|
|
34885
36398
|
activeTimeFilterSelection() {
|
|
36399
|
+
if (this.isHostTimeRangeActive() || this.timeFilterControl() === 'host') {
|
|
36400
|
+
return null;
|
|
36401
|
+
}
|
|
34886
36402
|
const config = this.stateService.getTimeFilterConfig();
|
|
34887
36403
|
return config?.enabled ? config.selection ?? null : null;
|
|
34888
36404
|
}
|
|
@@ -34939,10 +36455,18 @@ class MeshBoardViewComponent {
|
|
|
34939
36455
|
this.primeEntitySelectorScopes();
|
|
34940
36456
|
await this.preloadWidgetData(widgets);
|
|
34941
36457
|
}
|
|
36458
|
+
// The view may have been destroyed while the board loaded (e.g. a fast switch on the host
|
|
36459
|
+
// page): it must not publish its host period or selection into the root-provided state.
|
|
36460
|
+
if (this.destroyed) {
|
|
36461
|
+
return;
|
|
36462
|
+
}
|
|
34942
36463
|
// Initialize time filter variables if enabled with stored selection
|
|
34943
36464
|
this.initializeTimeFilterVariables();
|
|
34944
36465
|
// Initialize entity selectors from URL params, route data, or defaults
|
|
34945
36466
|
await this.initializeEntitySelectors();
|
|
36467
|
+
if (this.destroyed) {
|
|
36468
|
+
return;
|
|
36469
|
+
}
|
|
34946
36470
|
// Mark initial load as complete so the effect can handle subsequent board switches
|
|
34947
36471
|
this.initialLoadComplete = true;
|
|
34948
36472
|
// `?edit=1` (e.g. "Edit" in a host's board list) opens the board in edit mode;
|
|
@@ -35009,6 +36533,14 @@ class MeshBoardViewComponent {
|
|
|
35009
36533
|
}
|
|
35010
36534
|
}
|
|
35011
36535
|
ngOnDestroy() {
|
|
36536
|
+
this.destroyed = true;
|
|
36537
|
+
// The state service is root-provided: a host period must not outlive the view that set it.
|
|
36538
|
+
// Owner-aware, so a period another view set in the meantime stays. Only a view that set a
|
|
36539
|
+
// period clears one (the default path never touches it); a view destroyed while its board was
|
|
36540
|
+
// still loading never set one (see the guards after the awaits in ngOnInit).
|
|
36541
|
+
if (this.hostHasOwnedPeriod) {
|
|
36542
|
+
this.stateService.clearHostTimeRange(this);
|
|
36543
|
+
}
|
|
35012
36544
|
this.resizeObserver?.disconnect();
|
|
35013
36545
|
this.closeConfigDialog();
|
|
35014
36546
|
this.navigationSubscription?.unsubscribe();
|
|
@@ -35051,14 +36583,19 @@ class MeshBoardViewComponent {
|
|
|
35051
36583
|
/**
|
|
35052
36584
|
* Initializes time filter variables based on URL query params or stored selection.
|
|
35053
36585
|
* URL params take precedence over stored selection to support page reload.
|
|
36586
|
+
* A period set by the host (`timeRange`, AB#7582) takes precedence over both.
|
|
35054
36587
|
*/
|
|
35055
36588
|
initializeTimeFilterVariables() {
|
|
36589
|
+
if (this.applyHostTimeRange(this.parsedHostTimeRange())) {
|
|
36590
|
+
return;
|
|
36591
|
+
}
|
|
35056
36592
|
const timeFilter = this.stateService.getTimeFilterConfig();
|
|
35057
36593
|
if (!timeFilter?.enabled) {
|
|
35058
36594
|
return;
|
|
35059
36595
|
}
|
|
35060
|
-
// Check URL query params first (takes precedence over stored selection)
|
|
35061
|
-
|
|
36596
|
+
// Check URL query params first (takes precedence over stored selection). Once the host has
|
|
36597
|
+
// owned the period, the tf_* of the opening URL are stale and are not applied again.
|
|
36598
|
+
const urlSelection = this.hostHasOwnedPeriod ? null : this.readTimeFilterFromUrl();
|
|
35062
36599
|
if (urlSelection) {
|
|
35063
36600
|
// Signal the URL selection so the picker picks it up
|
|
35064
36601
|
this._urlTimeSelection.set(urlSelection);
|
|
@@ -35098,6 +36635,59 @@ class MeshBoardViewComponent {
|
|
|
35098
36635
|
const rangeISO = TimeRangeUtils.toISO(range);
|
|
35099
36636
|
this.stateService.setTimeFilterVariables(rangeISO.from, rangeISO.to);
|
|
35100
36637
|
}
|
|
36638
|
+
/**
|
|
36639
|
+
* Hands the host period to the state service and publishes `$timeRangeFrom` / `$timeRangeTo`
|
|
36640
|
+
* as exactly its instants (AB#7582). Returns true while the host owns the period, so the
|
|
36641
|
+
* caller skips the board's own selection. With the time filter disabled the period is only
|
|
36642
|
+
* stored (the state service ignores it then) and nothing is logged.
|
|
36643
|
+
*/
|
|
36644
|
+
applyHostTimeRange(parsed) {
|
|
36645
|
+
if (this.destroyed) {
|
|
36646
|
+
return parsed !== undefined;
|
|
36647
|
+
}
|
|
36648
|
+
const previous = this.appliedHostTimeRange;
|
|
36649
|
+
this.appliedHostTimeRange = parsed;
|
|
36650
|
+
if (!parsed) {
|
|
36651
|
+
// Without a host period nothing changes for the board (default path untouched). When the
|
|
36652
|
+
// host drops its period, its variables go too; the board's own selection (if any) re-sets them.
|
|
36653
|
+
if (previous) {
|
|
36654
|
+
this.stateService.clearHostTimeRange(this);
|
|
36655
|
+
this.stateService.clearTimeFilterVariables();
|
|
36656
|
+
}
|
|
36657
|
+
return false;
|
|
36658
|
+
}
|
|
36659
|
+
this.hostHasOwnedPeriod = true;
|
|
36660
|
+
this.stateService.setHostTimeRange(parsed, this);
|
|
36661
|
+
if (!this.stateService.isTimeFilterEnabled()) {
|
|
36662
|
+
return true;
|
|
36663
|
+
}
|
|
36664
|
+
if (parsed.status === 'valid') {
|
|
36665
|
+
this.stateService.setTimeFilterVariables(parsed.from.toISOString(), parsed.to.toISOString());
|
|
36666
|
+
}
|
|
36667
|
+
else {
|
|
36668
|
+
this.stateService.clearTimeFilterVariables();
|
|
36669
|
+
if (this.reportedInvalidHostTimeRange !== parsed) {
|
|
36670
|
+
this.reportedInvalidHostTimeRange = parsed;
|
|
36671
|
+
console.error(`mm-meshboard-view: input "timeRange" is invalid (${parsed.reason}); `
|
|
36672
|
+
+ 'the board sends no stream-data query until the host sets a valid range.');
|
|
36673
|
+
}
|
|
36674
|
+
}
|
|
36675
|
+
return true;
|
|
36676
|
+
}
|
|
36677
|
+
/**
|
|
36678
|
+
* Applies a change of the `timeRange` input after the initial load (AB#7582): the host period,
|
|
36679
|
+
* or the board's own selection again when the input went back to `undefined`. Reloads the
|
|
36680
|
+
* widgets once; an equal period (same instants) changes nothing.
|
|
36681
|
+
*/
|
|
36682
|
+
onHostTimeRangeChanged(parsed) {
|
|
36683
|
+
if (sameHostTimeRange(parsed, this.appliedHostTimeRange)) {
|
|
36684
|
+
return;
|
|
36685
|
+
}
|
|
36686
|
+
this.initializeTimeFilterVariables();
|
|
36687
|
+
if (this.stateService.isTimeFilterEnabled()) {
|
|
36688
|
+
this.stateService.triggerRefresh();
|
|
36689
|
+
}
|
|
36690
|
+
}
|
|
35101
36691
|
/**
|
|
35102
36692
|
* Recomputes the active time-filter range from the current selection on the
|
|
35103
36693
|
* board's (possibly just-changed) timezone mode and republishes the
|
|
@@ -35105,6 +36695,11 @@ class MeshBoardViewComponent {
|
|
|
35105
36695
|
* disabled or no selection is active.
|
|
35106
36696
|
*/
|
|
35107
36697
|
reapplyTimeFilterRange() {
|
|
36698
|
+
// A host period (valid or invalid) takes the same path as on every other change.
|
|
36699
|
+
if (this.isHostTimeRangeActive()) {
|
|
36700
|
+
this.applyHostTimeRange(this.parsedHostTimeRange());
|
|
36701
|
+
return;
|
|
36702
|
+
}
|
|
35108
36703
|
const range = this.stateService.resolveCurrentTimeRange();
|
|
35109
36704
|
if (!range) {
|
|
35110
36705
|
return;
|
|
@@ -35421,6 +37016,10 @@ class MeshBoardViewComponent {
|
|
|
35421
37016
|
* Updates the time filter variables, persists the selection, syncs to URL, and triggers refresh.
|
|
35422
37017
|
*/
|
|
35423
37018
|
async onTimeSelectionChange(sharedSelection) {
|
|
37019
|
+
// The host owns the period (AB#7582): no selection, URL or refresh from the picker.
|
|
37020
|
+
if (this.isHostTimeRangeActive()) {
|
|
37021
|
+
return;
|
|
37022
|
+
}
|
|
35424
37023
|
// Convert shared-ui selection to our model
|
|
35425
37024
|
const selection = {
|
|
35426
37025
|
type: sharedSelection.type,
|
|
@@ -35463,6 +37062,9 @@ class MeshBoardViewComponent {
|
|
|
35463
37062
|
* Clears the stored selection and applies the default.
|
|
35464
37063
|
*/
|
|
35465
37064
|
resetTimeFilterToDefault() {
|
|
37065
|
+
if (this.isHostTimeRangeActive()) {
|
|
37066
|
+
return;
|
|
37067
|
+
}
|
|
35466
37068
|
const config = this.timeFilterConfig();
|
|
35467
37069
|
if (!config?.defaultSelection)
|
|
35468
37070
|
return;
|
|
@@ -35996,8 +37598,8 @@ class MeshBoardViewComponent {
|
|
|
35996
37598
|
this.stateService.updateEntitySelectorSelection(follower.id, rtId, displayName);
|
|
35997
37599
|
}
|
|
35998
37600
|
});
|
|
35999
|
-
// Resolve the stream-data scope rtIds for this selection (
|
|
36000
|
-
//
|
|
37601
|
+
// Resolve the stream-data scope rtIds for this selection (childScope
|
|
37602
|
+
// hop or path, or the picked entity itself), per selector, so
|
|
36001
37603
|
// widgets bound to the leader or any follower scope their stream-data
|
|
36002
37604
|
// query by the resolved source rtIds.
|
|
36003
37605
|
await Promise.all(targets.map((t, i) => this.resolveEntitySelectorScopeRtIds(t, rtId, seqs[i])));
|
|
@@ -36049,34 +37651,52 @@ class MeshBoardViewComponent {
|
|
|
36049
37651
|
* Resolves the set of source rtIds that a stream-data widget bound to this
|
|
36050
37652
|
* selector should be scoped to, and caches them on the state service.
|
|
36051
37653
|
*
|
|
36052
|
-
*
|
|
36053
|
-
*
|
|
36054
|
-
*
|
|
36055
|
-
*
|
|
37654
|
+
* - No {@link EntitySelectorChildScope}: the picked entity's own rtId is the
|
|
37655
|
+
* scope (direct keying).
|
|
37656
|
+
* - One-hop form (top-level fields): the picked entity's targets of
|
|
37657
|
+
* `targetCkTypeId` reached via `roleId`/`direction` (e.g. MeteringPoint →
|
|
37658
|
+
* its EnergyMeasurement rtIds) — exactly as before AB#7723.
|
|
37659
|
+
* - `path` (AB#7723): hop by hop, each hop one batched request over all
|
|
37660
|
+
* entities the previous hop reached; the scope is the distinct rtIds of the
|
|
37661
|
+
* last hop. An empty hop ends the resolution with an empty scope; a failed
|
|
37662
|
+
* hop, or an invalid path (more than 5 hops, a hop without role or target
|
|
37663
|
+
* type), is a failed scope (AB#7721).
|
|
36056
37664
|
*/
|
|
36057
37665
|
async resolveEntitySelectorScopeRtIds(selector, rtId, seq) {
|
|
36058
37666
|
const isCurrent = () => this.scopeRequestSeq.get(selector.id) === seq;
|
|
36059
37667
|
if (!isCurrent())
|
|
36060
37668
|
return;
|
|
36061
|
-
const
|
|
36062
|
-
if (
|
|
37669
|
+
const plan = scopePlan(selector.childScope);
|
|
37670
|
+
if (plan.kind === 'none') {
|
|
36063
37671
|
this.stateService.setEntitySelectorRtIds(selector.id, [rtId]);
|
|
36064
37672
|
return;
|
|
36065
37673
|
}
|
|
37674
|
+
if (plan.kind === 'invalid') {
|
|
37675
|
+
console.error(`Invalid child scope path for selector '${selector.id}': ${plan.reason}`);
|
|
37676
|
+
this.stateService.setEntitySelectorScopeFailed(selector.id);
|
|
37677
|
+
return;
|
|
37678
|
+
}
|
|
36066
37679
|
try {
|
|
36067
|
-
|
|
36068
|
-
|
|
36069
|
-
|
|
36070
|
-
|
|
36071
|
-
|
|
37680
|
+
let scopeRtIds;
|
|
37681
|
+
if (plan.kind === 'single') {
|
|
37682
|
+
// The original one-hop request, unchanged (AB#7723 keeps old boards byte-identical).
|
|
37683
|
+
const targets = await firstValueFrom(this.dataService.fetchAssociationTargets(rtId, selector.ckTypeId, plan.hop.targetCkTypeId, plan.hop.roleId,
|
|
37684
|
+
// Parent → children via System/ParentChild is an INBOUND hop (the child
|
|
37685
|
+
// owns the association to its parent), so 'in' is the default.
|
|
37686
|
+
plan.hop.direction));
|
|
37687
|
+
scopeRtIds = isCurrent()
|
|
37688
|
+
? (targets ?? []).map(t => String(t.rtId)).filter(id => id.length > 0)
|
|
37689
|
+
: null;
|
|
37690
|
+
}
|
|
37691
|
+
else {
|
|
37692
|
+
scopeRtIds = await resolveScopePath(rtId, selector.ckTypeId, plan.hops, (sources, sourceCkTypeId, hop) => firstValueFrom(this.dataService.fetchAssociationTargetRtIds(sources, sourceCkTypeId, hop.targetCkTypeId, hop.roleId, hop.direction)), isCurrent);
|
|
37693
|
+
}
|
|
37694
|
+
if (scopeRtIds === null || !isCurrent())
|
|
36072
37695
|
return;
|
|
36073
|
-
const childRtIds = (targets ?? [])
|
|
36074
|
-
.map(t => String(t.rtId))
|
|
36075
|
-
.filter(id => id.length > 0);
|
|
36076
37696
|
// An empty list is a resolved EMPTY scope: bound widgets show no data and
|
|
36077
37697
|
// send no query — never the persisted scope, which would show rows of
|
|
36078
37698
|
// other entities (AB#7721).
|
|
36079
|
-
this.stateService.setEntitySelectorRtIds(selector.id,
|
|
37699
|
+
this.stateService.setEntitySelectorRtIds(selector.id, scopeRtIds);
|
|
36080
37700
|
}
|
|
36081
37701
|
catch (error) {
|
|
36082
37702
|
if (!isCurrent())
|
|
@@ -36164,10 +37784,10 @@ class MeshBoardViewComponent {
|
|
|
36164
37784
|
}
|
|
36165
37785
|
}
|
|
36166
37786
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: MeshBoardViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
36167
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: MeshBoardViewComponent, isStandalone: true, selector: "mm-meshboard-view", inputs: { headerMode: { classPropertyName: "headerMode", publicName: "headerMode", isSignal: true, isRequired: false, transformFunction: null }, chrome: { classPropertyName: "chrome", publicName: "chrome", isSignal: true, isRequired: false, transformFunction: null } }, providers: [
|
|
37787
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: MeshBoardViewComponent, isStandalone: true, selector: "mm-meshboard-view", inputs: { headerMode: { classPropertyName: "headerMode", publicName: "headerMode", isSignal: true, isRequired: false, transformFunction: null }, chrome: { classPropertyName: "chrome", publicName: "chrome", isSignal: true, isRequired: false, transformFunction: null }, timeRange: { classPropertyName: "timeRange", publicName: "timeRange", isSignal: true, isRequired: false, transformFunction: null }, timeFilterControl: { classPropertyName: "timeFilterControl", publicName: "timeFilterControl", isSignal: true, isRequired: false, transformFunction: null } }, providers: [
|
|
36168
37788
|
{ provide: HAS_UNSAVED_CHANGES, useExisting: MeshBoardViewComponent },
|
|
36169
37789
|
AutoRefreshTimerService
|
|
36170
|
-
], viewQueries: [{ propertyName: "tileLayout", first: true, predicate: TileLayoutComponent, descendants: true }], hostDirectives: [{ directive: i1$8.UnsavedChangesDirective }], ngImport: i0, template: "<div class=\"meshboard-view\" [class.chrome-plain]=\"effectiveChrome() === 'plain'\">\n @if (isLoading()) {\n <div class=\"loading-container\">\n <div class=\"k-loading-mask\">\n <span class=\"k-loading-text\">Loading MeshBoard...</span>\n <div class=\"k-loading-image\"></div>\n <div class=\"k-loading-color\"></div>\n </div>\n </div>\n } @else if (isModelAvailable() === false) {\n <div class=\"model-unavailable\">\n <div class=\"message-card\">\n <kendo-svg-icon [icon]=\"xIcon\" size=\"xlarge\"></kendo-svg-icon>\n <h2>MeshBoard Not Available</h2>\n <p>The MeshBoard feature requires the CK model 'System.UI' version 1.0.1 or higher.</p>\n <p>Please install the required model in your tenant to use this feature.</p>\n </div>\n </div>\n } @else if (notFoundError()) {\n <div class=\"model-unavailable\">\n <div class=\"message-card\">\n <kendo-svg-icon [icon]=\"xIcon\" size=\"xlarge\"></kendo-svg-icon>\n <h2>MeshBoard Not Found</h2>\n <p>{{ notFoundError() }}</p>\n <p>To create a MeshBoard with a Well-Known Name:</p>\n <ol>\n <li>Go to the <a [routerLink]=\"meshBoardPageLink()\">MeshBoard</a> page</li>\n <li>Open Settings</li>\n <li>Set the \"Well-Known Name\" field</li>\n <li>Save the MeshBoard</li>\n </ol>\n </div>\n </div>\n } @else if (isInitialized()) {\n <!-- Toolbar (headerMode: full = name + description + controls, compact = controls only, none = no row) -->\n @if (effectiveHeaderMode() !== 'none') {\n <div class=\"meshboard-toolbar\" [class.compact]=\"effectiveHeaderMode() === 'compact'\">\n @if (effectiveHeaderMode() === 'full') {\n <div class=\"toolbar-left\">\n <h2 class=\"meshboard-title\">{{ config().name }}</h2>\n @if (config().description) {\n <span class=\"meshboard-description\">{{ config().description }}</span>\n }\n </div>\n }\n\n <!-- Time Filter (center) -->\n @if (isTimeFilterEnabled()) {\n <div class=\"toolbar-center\">\n <mm-time-range-picker\n [config]=\"timeFilterConfig()?.pickerConfig ?? {}\"\n [labels]=\"timeRangeLabels()\"\n [initialSelection]=\"initialTimeSelection()\"\n (rangeChange)=\"onTimeRangeChange($event)\"\n (selectionChange)=\"onTimeSelectionChange($event)\">\n </mm-time-range-picker>\n @if (canResetTimeFilter()) {\n <button\n kendoButton\n [svgIcon]=\"undoIcon\"\n (click)=\"resetTimeFilterToDefault()\"\n title=\"Reset to default time filter\"\n aria-label=\"Reset to default time filter\"\n fillMode=\"flat\"\n size=\"small\">\n </button>\n }\n </div>\n }\n\n <!-- Entity Selectors -->\n @if (hasEntitySelectors()) {\n @if (isTimeFilterEnabled()) {\n <div class=\"toolbar-separator\"></div>\n }\n <div class=\"toolbar-entity-selectors\">\n <mm-entity-selector-toolbar\n [entitySelectors]=\"entitySelectorsConfig()\"\n (entitySelected)=\"onEntitySelectorSelected($event)\"\n (entityCleared)=\"onEntitySelectorCleared($event)\">\n </mm-entity-selector-toolbar>\n </div>\n }\n\n @if (isTimeFilterEnabled() || hasEntitySelectors()) {\n <div class=\"toolbar-separator\"></div>\n }\n <div class=\"toolbar-right mm-toolbar-actions\">\n @if (!isReadonly()) {\n <!-- Manager Button (icon only) -->\n <button\n kendoButton\n [svgIcon]=\"gridLayoutIcon\"\n (click)=\"openManager()\"\n title=\"Manage MeshBoards\"\n aria-label=\"Manage MeshBoards\"\n fillMode=\"flat\">\n </button>\n\n <!-- Settings Button (icon only) -->\n <button\n kendoButton\n [svgIcon]=\"gearIcon\"\n (click)=\"openSettings()\"\n title=\"MeshBoard Settings\"\n aria-label=\"MeshBoard Settings\"\n fillMode=\"flat\">\n </button>\n }\n\n <!-- Refresh Button (icon only) -->\n <button\n kendoButton\n [svgIcon]=\"arrowRotateCwIcon\"\n (click)=\"refresh()\"\n title=\"Refresh All Widgets\"\n aria-label=\"Refresh All Widgets\"\n fillMode=\"flat\">\n </button>\n\n @if (!isReadonly()) {\n <!-- Edit Mode Toggle (icon only) -->\n @if (!isEditMode()) {\n <button\n kendoButton\n [svgIcon]=\"pencilIcon\"\n (click)=\"toggleEditMode()\"\n title=\"Enter Edit Mode\"\n aria-label=\"Enter Edit Mode\"\n fillMode=\"flat\">\n </button>\n } @else {\n <button\n kendoButton\n [svgIcon]=\"xIcon\"\n (click)=\"cancelEdit()\"\n title=\"Cancel Edit Mode\"\n aria-label=\"Cancel Edit Mode\"\n fillMode=\"flat\">\n </button>\n }\n\n <!-- Add Widget Button (with text) -->\n <button\n kendoButton\n [svgIcon]=\"plusIcon\"\n (click)=\"openAddWidget()\"\n title=\"Add Widget\"\n themeColor=\"primary\">\n Add Widget\n </button>\n\n <!-- Save Button (with text) -->\n @if (canSave()) {\n <button\n kendoButton\n [svgIcon]=\"saveIcon\"\n (click)=\"save()\"\n [disabled]=\"isSaving()\"\n title=\"Save MeshBoard\"\n themeColor=\"primary\">\n {{ isSaving() ? 'Saving...' : 'Save' }}\n </button>\n }\n }\n </div>\n </div>\n }\n\n <!-- Entity Selector Hint -->\n @if (unselectedToolbarSelectors().length > 0) {\n <div class=\"entity-selector-hint\">\n <kendo-svg-icon [icon]=\"infoCircleIcon\" size=\"medium\"></kendo-svg-icon>\n <span>\n Please select\n @for (selector of unselectedToolbarSelectors(); track selector.id; let last = $last) {\n <strong>{{ selector.label }}</strong>@if (!last) {, }\n }\n to display dashboard data.\n </span>\n </div>\n }\n\n <!-- Variable Resolution Errors -->\n @if (variableResolutionErrors().length > 0) {\n <div class=\"variable-resolution-errors\">\n <strong>Variable resolution errors:</strong>\n @for (error of variableResolutionErrors(); track error.variableName) {\n <span class=\"error-item\">{{ error.variableName }}: {{ error.message }}</span>\n }\n </div>\n }\n\n <!-- Entity selector scopes that could not be resolved (AB#7721) -->\n @if (scopeResolutionErrors().length > 0) {\n <div class=\"variable-resolution-errors scope-resolution-errors\" role=\"status\">\n @for (error of scopeResolutionErrors(); track error.selectorId) {\n <span class=\"error-item\">{{ error.message }}</span>\n }\n </div>\n }\n\n <!-- Banner Zone: widgets placed in the banner stack above the grid -->\n @if (isEditMode()) {\n <div class=\"zone-label banner-zone-label\">Banner Zone</div>\n }\n @for (widget of bannerWidgets(); track trackByWidgetId($index, widget); let first = $first; let last = $last) {\n <div class=\"banner-zone-item\" [class.banner-edit]=\"isEditMode()\">\n @if (isEditMode()) {\n <div class=\"banner-edit-header\">\n <span class=\"widget-title\">{{ widget.title }}</span>\n <div class=\"widget-actions\">\n <button\n kendoButton\n [svgIcon]=\"arrowUpIcon\"\n (click)=\"moveBannerUp(widget.id)\"\n fillMode=\"flat\"\n size=\"small\"\n [disabled]=\"first\"\n title=\"Move up\"\n [attr.aria-label]=\"'Move up ' + widget.title\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"arrowDownIcon\"\n (click)=\"moveBannerDown(widget.id)\"\n fillMode=\"flat\"\n size=\"small\"\n [disabled]=\"last\"\n title=\"Move down\"\n [attr.aria-label]=\"'Move down ' + widget.title\">\n </button>\n @if (supportsConfiguration(widget)) {\n <button\n kendoButton\n [svgIcon]=\"linkIcon\"\n (click)=\"openWidgetConfig(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"config-widget-btn\"\n [class.needs-config]=\"isWidgetUnconfigured(widget)\"\n [title]=\"isWidgetUnconfigured(widget) ? 'Configure data source (required)' : 'Configure data source'\"\n [attr.aria-label]=\"(isWidgetUnconfigured(widget) ? 'Configure data source (required) ' : 'Configure data source ') + widget.title\">\n </button>\n }\n <button\n kendoButton\n [svgIcon]=\"gearIcon\"\n (click)=\"openEditWidgetDialog(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"edit-widget-btn\"\n title=\"Edit widget\"\n [attr.aria-label]=\"'Edit widget ' + widget.title\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"trashIcon\"\n (click)=\"removeWidget(widget.id)\"\n fillMode=\"flat\"\n themeColor=\"error\"\n size=\"small\"\n class=\"remove-widget-btn\"\n title=\"Delete widget\"\n [attr.aria-label]=\"'Delete widget ' + widget.title\">\n </button>\n </div>\n </div>\n }\n @if (getWidgetComponentType(widget.type); as componentType) {\n <ng-container *ngComponentOutlet=\"componentType; inputs: { config: widget }\"></ng-container>\n }\n </div>\n }\n\n <!-- MeshBoard Grid -->\n @if (isEditMode()) {\n <div class=\"zone-label grid-zone-label\">Grid Zone</div>\n }\n @if (hasGridWidgets()) {\n <kendo-tilelayout\n [columns]=\"displayColumns()\"\n [autoFlow]=\"displayAutoFlow()\"\n [rowHeight]=\"config().rowHeight\"\n [gap]=\"config().gap\"\n [class.has-banners]=\"bannerWidgets().length > 0 && !isEditMode()\"\n [resizable]=\"canEditLayout()\"\n [reorderable]=\"canEditLayout()\"\n (reorder)=\"onReorder($event)\"\n (resize)=\"onResize($event)\">\n @for (placement of displayGridWidgets(); track placement.widget.id) {\n @let widget = placement.widget;\n <kendo-tilelayout-item\n [col]=\"$any(placement.col)\"\n [row]=\"$any(placement.row)\"\n [colSpan]=\"placement.colSpan\"\n [rowSpan]=\"placement.rowSpan\">\n <kendo-tilelayout-item-header>\n <div class=\"widget-header\"\n [class.unconfigured]=\"isWidgetUnconfigured(widget)\"\n [class.chromeless-header]=\"widget.chromeless && !isEditMode()\">\n @if (!(widget.chromeless && !isEditMode())) {\n <span class=\"widget-title\">{{ widget.title }}</span>\n @if (isWidgetUnconfigured(widget)) {\n <span class=\"unconfigured-badge\" title=\"Widget needs configuration\">!</span>\n }\n }\n @if (isEditMode()) {\n <div class=\"widget-actions\" (pointerdown)=\"$event.stopPropagation()\">\n @if (supportsConfiguration(widget)) {\n <button\n kendoButton\n [svgIcon]=\"linkIcon\"\n (click)=\"openWidgetConfig(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"config-widget-btn\"\n [class.needs-config]=\"isWidgetUnconfigured(widget)\"\n [title]=\"isWidgetUnconfigured(widget) ? 'Configure data source (required)' : 'Configure data source'\"\n [attr.aria-label]=\"(isWidgetUnconfigured(widget) ? 'Configure data source (required) ' : 'Configure data source ') + widget.title\">\n </button>\n }\n <button\n kendoButton\n [svgIcon]=\"gearIcon\"\n (click)=\"openEditWidgetDialog(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"edit-widget-btn\"\n title=\"Edit position\"\n [attr.aria-label]=\"'Edit position ' + widget.title\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"copyIcon\"\n (click)=\"duplicateWidget(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"duplicate-widget-btn\"\n title=\"Duplicate widget\"\n [attr.aria-label]=\"'Duplicate widget ' + widget.title\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"trashIcon\"\n (click)=\"removeWidget(widget.id)\"\n fillMode=\"flat\"\n themeColor=\"error\"\n size=\"small\"\n class=\"remove-widget-btn\"\n title=\"Delete widget\"\n [attr.aria-label]=\"'Delete widget ' + widget.title\">\n </button>\n </div>\n }\n </div>\n </kendo-tilelayout-item-header>\n <kendo-tilelayout-item-body>\n @if (getWidgetComponentType(widget.type); as componentType) {\n <ng-container *ngComponentOutlet=\"componentType; inputs: { config: widget }\"></ng-container>\n } @else {\n <div class=\"widget-error\">\n <p>Unknown widget type: {{ widget.type }}</p>\n </div>\n }\n </kendo-tilelayout-item-body>\n </kendo-tilelayout-item>\n }\n </kendo-tilelayout>\n } @else {\n <mm-meshboard-empty-state [readonly]=\"isReadonly()\" (addWidget)=\"openAddWidget()\"></mm-meshboard-empty-state>\n }\n\n <!-- Edit Widget Dialog -->\n @if (showEditWidgetDialog && editingWidget) {\n <mm-edit-widget-dialog\n [widget]=\"editingWidget\"\n [widgets]=\"config().widgets\"\n [maxColumns]=\"config().columns\"\n [gridService]=\"gridService\"\n (save)=\"onEditWidgetSave($event)\"\n (cancelled)=\"closeEditWidgetDialog()\">\n </mm-edit-widget-dialog>\n }\n }\n</div>\n", styles: [":host{display:flex;flex-direction:column;flex:1;min-height:0;height:100%;width:100%}.meshboard-view{display:flex;flex-direction:column;flex:1;min-height:0;width:100%;background-color:var(--kendo-color-surface, #f5f5f5)}.loading-container{display:flex;align-items:center;justify-content:center;height:100%;width:100%}.entity-selector-hint{display:flex;align-items:center;gap:8px;padding:10px 16px;margin-bottom:8px;background:color-mix(in srgb,var(--kendo-color-info, #2196f3) 10%,var(--kendo-color-surface-alt, #fafafa));border:1px solid color-mix(in srgb,var(--kendo-color-info, #2196f3) 40%,transparent);border-radius:4px;font-size:13px;color:var(--kendo-color-on-app-surface, #424242)}.entity-selector-hint strong{color:var(--kendo-color-info, #2196f3)}.variable-resolution-errors{display:flex;flex-wrap:wrap;align-items:center;gap:8px;padding:8px 16px;margin-bottom:8px;background:color-mix(in srgb,var(--kendo-color-error, #f44336) 10%,var(--kendo-color-surface-alt, #fafafa));border:1px solid var(--kendo-color-error, #f44336);border-radius:4px;font-size:13px;color:var(--kendo-color-on-app-surface, #424242)}.variable-resolution-errors strong{color:var(--kendo-color-error, #f44336)}.variable-resolution-errors .error-item{padding:2px 8px;background:color-mix(in srgb,var(--kendo-color-error, #f44336) 8%,transparent);border-radius:3px;font-family:var(--theme-font-mono);font-size:12px}.scope-resolution-errors .error-item{font-family:inherit}.model-unavailable{display:flex;align-items:center;justify-content:center;height:100%;width:100%;padding:2rem;overflow:hidden;box-sizing:border-box}.model-unavailable .message-card{text-align:center;max-width:500px;padding:2rem;background:var(--kendo-color-surface-alt, #fafafa);border:1px solid var(--kendo-color-border, #e0e0e0);border-radius:8px;overflow:hidden}.model-unavailable .message-card kendo-svg-icon{color:var(--kendo-color-warning, #ff9800);margin-bottom:1rem}.model-unavailable .message-card h2{margin:1rem 0;color:var(--kendo-color-on-app-surface, #424242)}.model-unavailable .message-card p{margin:.5rem 0;color:var(--kendo-color-subtle, #757575);line-height:1.5}.model-unavailable .message-card ol{text-align:left;margin:1rem 0;padding-left:1.5rem;color:var(--kendo-color-subtle, #757575);line-height:1.8}.model-unavailable .message-card ol li{margin:.25rem 0}.model-unavailable .message-card ol a{color:var(--kendo-color-primary, #3f51b5);text-decoration:none}.model-unavailable .message-card ol a:hover{text-decoration:underline}.meshboard-toolbar{display:flex;justify-content:space-between;align-items:flex-end;padding:1rem 1.5rem;background-color:var(--kendo-color-surface-alt, white);border-bottom:1px solid var(--kendo-color-border, #e0e0e0);gap:1rem;flex-shrink:0}.meshboard-toolbar .toolbar-left{display:flex;align-items:baseline;gap:1rem;flex:1;min-width:0;align-self:center}.meshboard-toolbar .toolbar-left .meshboard-title{margin:0;font-size:1.5rem;font-weight:500;color:var(--kendo-color-on-app-surface, #212121);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.meshboard-toolbar .toolbar-left .meshboard-description{color:var(--kendo-color-subtle, #757575);font-size:.875rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.meshboard-toolbar .toolbar-center{display:flex;justify-content:center;align-items:flex-end;flex-shrink:0}.meshboard-toolbar .toolbar-center mm-time-range-picker ::ng-deep .time-range-picker{gap:.5rem}.meshboard-toolbar .toolbar-separator{width:1px;align-self:stretch;background-color:var(--kendo-color-border, #e0e0e0);flex-shrink:0}.meshboard-toolbar .toolbar-entity-selectors{display:flex;align-items:flex-end;flex-shrink:0}.meshboard-toolbar .toolbar-right{display:flex;gap:.5rem;align-items:center;flex-shrink:0}.meshboard-toolbar.compact{justify-content:flex-end;align-items:center;padding:.25rem 1.5rem 0;background-color:transparent;border-bottom:none}kendo-tilelayout{padding:1.5rem;overflow:auto;flex:1;min-height:0}.widget-header{display:flex;justify-content:space-between;align-items:center;width:100%;gap:.5rem}.widget-header.unconfigured{background-color:#ff98001a}.widget-header .widget-title{flex:1;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.widget-header .unconfigured-badge{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:50%;background-color:var(--kendo-color-warning, #ff9800);color:#fff;font-size:12px;font-weight:700;flex-shrink:0}.widget-header .widget-actions{display:flex;align-items:center;gap:.25rem;flex-shrink:0}.widget-header .config-widget-btn,.widget-header .edit-widget-btn,.widget-header .remove-widget-btn{opacity:.6;transition:opacity .2s}.widget-header .config-widget-btn:hover,.widget-header .edit-widget-btn:hover,.widget-header .remove-widget-btn:hover{opacity:1}.widget-header .config-widget-btn.needs-config{color:var(--kendo-color-warning, #ff9800);opacity:1}.widget-header .remove-widget-btn{color:var(--kendo-color-error, #f44336)}.widget-error{display:flex;align-items:center;justify-content:center;height:100%;padding:1rem;color:var(--kendo-color-error, #f44336);text-align:center}.meshboard-view.edit-mode kendo-tilelayout-item:hover{box-shadow:0 0 0 2px var(--kendo-color-primary, #3f51b5)}@media(max-width:768px){.meshboard-toolbar{flex-direction:column;align-items:stretch;gap:.75rem}.meshboard-toolbar .toolbar-left{flex-direction:column;align-items:flex-start;gap:.25rem}.meshboard-toolbar .toolbar-right{flex-wrap:wrap;justify-content:flex-end}.meshboard-toolbar.compact{align-items:flex-end;padding:.25rem 1rem 0}kendo-tilelayout{padding:1rem}}:host ::ng-deep kendo-tilelayout.has-banners{padding-top:0!important}:host ::ng-deep kendo-tilelayout{padding-bottom:1.5rem!important}.widget-header.chromeless-header{display:none}:host ::ng-deep .k-tilelayout-item-header:has(.chromeless-header){display:none}:host ::ng-deep kendo-tilelayout-item:has(.chromeless-header).k-tilelayout-item,:host ::ng-deep kendo-tilelayout-item:has(.chromeless-header).k-card{border:none;box-shadow:none;background:transparent}:host ::ng-deep kendo-tilelayout-item:has(kendo-dialog),:host ::ng-deep kendo-tilelayout-item:has(kendo-window){z-index:9999}.zone-label{font-size:.65rem;opacity:.4;padding:6px 1rem 2px;font-weight:600}.banner-zone-item{margin:0;padding:8px 1rem}.banner-zone-item.banner-edit{border:1px dashed var(--kendo-color-border, rgba(255, 255, 255, .2));border-radius:4px;margin:0 1rem 12px;padding:4px 0}.banner-edit-header{display:flex;align-items:center;justify-content:space-between;padding:4px 8px;opacity:.7}.banner-edit-header .widget-title{font-size:.75rem}.banner-edit-header .widget-actions{display:flex;gap:2px}.meshboard-view.chrome-plain{background:transparent!important}.meshboard-view.chrome-plain .meshboard-toolbar{padding:0 0 var(--theme-space-3, 12px);background:transparent!important;border-bottom:none!important}.meshboard-view.chrome-plain kendo-tilelayout{padding:0!important}.meshboard-view.chrome-plain .banner-zone-item:not(.banner-edit){padding-left:0;padding-right:0}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$3.NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i3$1.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }, { kind: "ngmodule", type: TileLayoutModule }, { kind: "component", type: i5.TileLayoutComponent, selector: "kendo-tilelayout", inputs: ["columns", "columnWidth", "gap", "reorderable", "resizable", "rowHeight", "autoFlow", "navigable"], outputs: ["reorder", "resize"] }, { kind: "component", type: i5.TileLayoutItemBodyComponent, selector: "kendo-tilelayout-item-body" }, { kind: "component", type: i5.TileLayoutItemComponent, selector: "kendo-tilelayout-item", inputs: ["title", "rowSpan", "colSpan", "order", "col", "row", "reorderable", "resizable"] }, { kind: "component", type: i5.TileLayoutItemHeaderComponent, selector: "kendo-tilelayout-item-header" }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: DialogModule }, { kind: "ngmodule", type: SVGIconModule }, { kind: "component", type: i1$4.SVGIconComponent, selector: "kendo-svg-icon, kendo-svgicon", inputs: ["icon", "variant"], exportAs: ["kendoSVGIcon"] }, { kind: "component", type: EditWidgetDialogComponent, selector: "mm-edit-widget-dialog", inputs: ["widget", "widgets", "maxColumns", "gridService"], outputs: ["save", "cancelled"] }, { kind: "component", type: TimeRangePickerComponent, selector: "mm-time-range-picker", inputs: ["config", "labels", "initialSelection"], outputs: ["rangeChange", "rangeChangeISO", "selectionChange"] }, { kind: "component", type: EntitySelectorToolbarComponent, selector: "mm-entity-selector-toolbar", inputs: ["entitySelectors"], outputs: ["entitySelected", "entityCleared"] }, { kind: "component", type: MeshBoardEmptyStateComponent, selector: "mm-meshboard-empty-state", inputs: ["readonly"], outputs: ["addWidget"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
|
|
37790
|
+
], viewQueries: [{ propertyName: "tileLayout", first: true, predicate: TileLayoutComponent, descendants: true }], hostDirectives: [{ directive: i1$8.UnsavedChangesDirective }], ngImport: i0, template: "<div class=\"meshboard-view\" [class.chrome-plain]=\"effectiveChrome() === 'plain'\">\n @if (isLoading()) {\n <div class=\"loading-container\">\n <div class=\"k-loading-mask\">\n <span class=\"k-loading-text\">Loading MeshBoard...</span>\n <div class=\"k-loading-image\"></div>\n <div class=\"k-loading-color\"></div>\n </div>\n </div>\n } @else if (isModelAvailable() === false) {\n <div class=\"model-unavailable\">\n <div class=\"message-card\">\n <kendo-svg-icon [icon]=\"xIcon\" size=\"xlarge\"></kendo-svg-icon>\n <h2>MeshBoard Not Available</h2>\n <p>The MeshBoard feature requires the CK model 'System.UI' version 1.0.1 or higher.</p>\n <p>Please install the required model in your tenant to use this feature.</p>\n </div>\n </div>\n } @else if (notFoundError()) {\n <div class=\"model-unavailable\">\n <div class=\"message-card\">\n <kendo-svg-icon [icon]=\"xIcon\" size=\"xlarge\"></kendo-svg-icon>\n <h2>MeshBoard Not Found</h2>\n <p>{{ notFoundError() }}</p>\n <p>To create a MeshBoard with a Well-Known Name:</p>\n <ol>\n <li>Go to the <a [routerLink]=\"meshBoardPageLink()\">MeshBoard</a> page</li>\n <li>Open Settings</li>\n <li>Set the \"Well-Known Name\" field</li>\n <li>Save the MeshBoard</li>\n </ol>\n </div>\n </div>\n } @else if (isInitialized()) {\n <!-- Toolbar (headerMode: full = name + description + controls, compact = controls only, none = no row) -->\n @if (effectiveHeaderMode() !== 'none') {\n <div class=\"meshboard-toolbar\" [class.compact]=\"effectiveHeaderMode() === 'compact'\">\n @if (effectiveHeaderMode() === 'full') {\n <div class=\"toolbar-left\">\n <h2 class=\"meshboard-title\">{{ config().name }}</h2>\n @if (config().description) {\n <span class=\"meshboard-description\">{{ config().description }}</span>\n }\n </div>\n }\n\n <!-- Time Filter (center); hidden when the host owns the period (AB#7582) -->\n @if (showTimeRangePicker()) {\n <div class=\"toolbar-center\">\n <mm-time-range-picker\n [config]=\"timeFilterConfig()?.pickerConfig ?? {}\"\n [labels]=\"timeRangeLabels()\"\n [initialSelection]=\"initialTimeSelection()\"\n (rangeChange)=\"onTimeRangeChange($event)\"\n (selectionChange)=\"onTimeSelectionChange($event)\">\n </mm-time-range-picker>\n @if (canResetTimeFilter()) {\n <button\n kendoButton\n [svgIcon]=\"undoIcon\"\n (click)=\"resetTimeFilterToDefault()\"\n title=\"Reset to default time filter\"\n aria-label=\"Reset to default time filter\"\n fillMode=\"flat\"\n size=\"small\">\n </button>\n }\n </div>\n }\n\n <!-- Entity Selectors -->\n @if (hasEntitySelectors()) {\n @if (showTimeRangePicker()) {\n <div class=\"toolbar-separator\"></div>\n }\n <div class=\"toolbar-entity-selectors\">\n <mm-entity-selector-toolbar\n [entitySelectors]=\"entitySelectorsConfig()\"\n [variables]=\"boardVariables()\"\n (entitySelected)=\"onEntitySelectorSelected($event)\"\n (entityCleared)=\"onEntitySelectorCleared($event)\">\n </mm-entity-selector-toolbar>\n </div>\n }\n\n @if (showTimeRangePicker() || hasEntitySelectors()) {\n <div class=\"toolbar-separator\"></div>\n }\n <div class=\"toolbar-right mm-toolbar-actions\">\n @if (!isReadonly()) {\n <!-- Manager Button (icon only) -->\n <button\n kendoButton\n [svgIcon]=\"gridLayoutIcon\"\n (click)=\"openManager()\"\n title=\"Manage MeshBoards\"\n aria-label=\"Manage MeshBoards\"\n fillMode=\"flat\">\n </button>\n\n <!-- Settings Button (icon only) -->\n <button\n kendoButton\n [svgIcon]=\"gearIcon\"\n (click)=\"openSettings()\"\n title=\"MeshBoard Settings\"\n aria-label=\"MeshBoard Settings\"\n fillMode=\"flat\">\n </button>\n }\n\n <!-- Refresh Button (icon only) -->\n <button\n kendoButton\n [svgIcon]=\"arrowRotateCwIcon\"\n (click)=\"refresh()\"\n title=\"Refresh All Widgets\"\n aria-label=\"Refresh All Widgets\"\n fillMode=\"flat\">\n </button>\n\n @if (!isReadonly()) {\n <!-- Edit Mode Toggle (icon only) -->\n @if (!isEditMode()) {\n <button\n kendoButton\n [svgIcon]=\"pencilIcon\"\n (click)=\"toggleEditMode()\"\n title=\"Enter Edit Mode\"\n aria-label=\"Enter Edit Mode\"\n fillMode=\"flat\">\n </button>\n } @else {\n <button\n kendoButton\n [svgIcon]=\"xIcon\"\n (click)=\"cancelEdit()\"\n title=\"Cancel Edit Mode\"\n aria-label=\"Cancel Edit Mode\"\n fillMode=\"flat\">\n </button>\n }\n\n <!-- Add Widget Button (with text) -->\n <button\n kendoButton\n [svgIcon]=\"plusIcon\"\n (click)=\"openAddWidget()\"\n title=\"Add Widget\"\n themeColor=\"primary\">\n Add Widget\n </button>\n\n <!-- Save Button (with text) -->\n @if (canSave()) {\n <button\n kendoButton\n [svgIcon]=\"saveIcon\"\n (click)=\"save()\"\n [disabled]=\"isSaving()\"\n title=\"Save MeshBoard\"\n themeColor=\"primary\">\n {{ isSaving() ? 'Saving...' : 'Save' }}\n </button>\n }\n }\n </div>\n </div>\n }\n\n <!-- Entity Selector Hint -->\n @if (unselectedToolbarSelectors().length > 0) {\n <div class=\"entity-selector-hint\">\n <kendo-svg-icon [icon]=\"infoCircleIcon\" size=\"medium\"></kendo-svg-icon>\n <span>\n Please select\n @for (selector of unselectedToolbarSelectors(); track selector.id; let last = $last) {\n <strong>{{ selector.label }}</strong>@if (!last) {, }\n }\n to display dashboard data.\n </span>\n </div>\n }\n\n <!-- Variable Resolution Errors -->\n @if (variableResolutionErrors().length > 0) {\n <div class=\"variable-resolution-errors\">\n <strong>Variable resolution errors:</strong>\n @for (error of variableResolutionErrors(); track error.variableName) {\n <span class=\"error-item\">{{ error.variableName }}: {{ error.message }}</span>\n }\n </div>\n }\n\n <!-- Entity selector scopes that could not be resolved (AB#7721) -->\n @if (scopeResolutionErrors().length > 0) {\n <div class=\"variable-resolution-errors scope-resolution-errors\" role=\"status\">\n @for (error of scopeResolutionErrors(); track error.selectorId) {\n <span class=\"error-item\">{{ error.message }}</span>\n }\n </div>\n }\n\n <!-- Banner Zone: widgets placed in the banner stack above the grid -->\n @if (isEditMode()) {\n <div class=\"zone-label banner-zone-label\">Banner Zone</div>\n }\n @for (widget of bannerWidgets(); track trackByWidgetId($index, widget); let first = $first; let last = $last) {\n <div class=\"banner-zone-item\" [class.banner-edit]=\"isEditMode()\">\n @if (isEditMode()) {\n <div class=\"banner-edit-header\">\n <span class=\"widget-title\">{{ widget.title }}</span>\n <div class=\"widget-actions\">\n <button\n kendoButton\n [svgIcon]=\"arrowUpIcon\"\n (click)=\"moveBannerUp(widget.id)\"\n fillMode=\"flat\"\n size=\"small\"\n [disabled]=\"first\"\n title=\"Move up\"\n [attr.aria-label]=\"'Move up ' + widget.title\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"arrowDownIcon\"\n (click)=\"moveBannerDown(widget.id)\"\n fillMode=\"flat\"\n size=\"small\"\n [disabled]=\"last\"\n title=\"Move down\"\n [attr.aria-label]=\"'Move down ' + widget.title\">\n </button>\n @if (supportsConfiguration(widget)) {\n <button\n kendoButton\n [svgIcon]=\"linkIcon\"\n (click)=\"openWidgetConfig(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"config-widget-btn\"\n [class.needs-config]=\"isWidgetUnconfigured(widget)\"\n [title]=\"isWidgetUnconfigured(widget) ? 'Configure data source (required)' : 'Configure data source'\"\n [attr.aria-label]=\"(isWidgetUnconfigured(widget) ? 'Configure data source (required) ' : 'Configure data source ') + widget.title\">\n </button>\n }\n <button\n kendoButton\n [svgIcon]=\"gearIcon\"\n (click)=\"openEditWidgetDialog(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"edit-widget-btn\"\n title=\"Edit widget\"\n [attr.aria-label]=\"'Edit widget ' + widget.title\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"trashIcon\"\n (click)=\"removeWidget(widget.id)\"\n fillMode=\"flat\"\n themeColor=\"error\"\n size=\"small\"\n class=\"remove-widget-btn\"\n title=\"Delete widget\"\n [attr.aria-label]=\"'Delete widget ' + widget.title\">\n </button>\n </div>\n </div>\n }\n @if (getWidgetComponentType(widget.type); as componentType) {\n <ng-container *ngComponentOutlet=\"componentType; inputs: { config: widget }\"></ng-container>\n }\n </div>\n }\n\n <!-- MeshBoard Grid -->\n @if (isEditMode()) {\n <div class=\"zone-label grid-zone-label\">Grid Zone</div>\n }\n @if (hasGridWidgets()) {\n <kendo-tilelayout\n [columns]=\"displayColumns()\"\n [autoFlow]=\"displayAutoFlow()\"\n [rowHeight]=\"config().rowHeight\"\n [gap]=\"config().gap\"\n [class.has-banners]=\"bannerWidgets().length > 0 && !isEditMode()\"\n [resizable]=\"canEditLayout()\"\n [reorderable]=\"canEditLayout()\"\n (reorder)=\"onReorder($event)\"\n (resize)=\"onResize($event)\">\n @for (placement of displayGridWidgets(); track placement.widget.id) {\n @let widget = placement.widget;\n <kendo-tilelayout-item\n [col]=\"$any(placement.col)\"\n [row]=\"$any(placement.row)\"\n [colSpan]=\"placement.colSpan\"\n [rowSpan]=\"placement.rowSpan\">\n <kendo-tilelayout-item-header>\n <div class=\"widget-header\"\n [class.unconfigured]=\"isWidgetUnconfigured(widget)\"\n [class.chromeless-header]=\"widget.chromeless && !isEditMode()\">\n @if (!(widget.chromeless && !isEditMode())) {\n <span class=\"widget-title\">{{ widget.title }}</span>\n @if (isWidgetUnconfigured(widget)) {\n <span class=\"unconfigured-badge\" title=\"Widget needs configuration\">!</span>\n }\n }\n @if (isEditMode()) {\n <div class=\"widget-actions\" (pointerdown)=\"$event.stopPropagation()\">\n @if (supportsConfiguration(widget)) {\n <button\n kendoButton\n [svgIcon]=\"linkIcon\"\n (click)=\"openWidgetConfig(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"config-widget-btn\"\n [class.needs-config]=\"isWidgetUnconfigured(widget)\"\n [title]=\"isWidgetUnconfigured(widget) ? 'Configure data source (required)' : 'Configure data source'\"\n [attr.aria-label]=\"(isWidgetUnconfigured(widget) ? 'Configure data source (required) ' : 'Configure data source ') + widget.title\">\n </button>\n }\n <button\n kendoButton\n [svgIcon]=\"gearIcon\"\n (click)=\"openEditWidgetDialog(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"edit-widget-btn\"\n title=\"Edit position\"\n [attr.aria-label]=\"'Edit position ' + widget.title\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"copyIcon\"\n (click)=\"duplicateWidget(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"duplicate-widget-btn\"\n title=\"Duplicate widget\"\n [attr.aria-label]=\"'Duplicate widget ' + widget.title\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"trashIcon\"\n (click)=\"removeWidget(widget.id)\"\n fillMode=\"flat\"\n themeColor=\"error\"\n size=\"small\"\n class=\"remove-widget-btn\"\n title=\"Delete widget\"\n [attr.aria-label]=\"'Delete widget ' + widget.title\">\n </button>\n </div>\n }\n </div>\n </kendo-tilelayout-item-header>\n <kendo-tilelayout-item-body>\n @if (getWidgetComponentType(widget.type); as componentType) {\n <ng-container *ngComponentOutlet=\"componentType; inputs: { config: widget }\"></ng-container>\n } @else {\n <div class=\"widget-error\">\n <p>Unknown widget type: {{ widget.type }}</p>\n </div>\n }\n </kendo-tilelayout-item-body>\n </kendo-tilelayout-item>\n }\n </kendo-tilelayout>\n } @else {\n <mm-meshboard-empty-state [readonly]=\"isReadonly()\" (addWidget)=\"openAddWidget()\"></mm-meshboard-empty-state>\n }\n\n <!-- Edit Widget Dialog -->\n @if (showEditWidgetDialog && editingWidget) {\n <mm-edit-widget-dialog\n [widget]=\"editingWidget\"\n [widgets]=\"config().widgets\"\n [maxColumns]=\"config().columns\"\n [gridService]=\"gridService\"\n (save)=\"onEditWidgetSave($event)\"\n (cancelled)=\"closeEditWidgetDialog()\">\n </mm-edit-widget-dialog>\n }\n }\n</div>\n", styles: [":host{display:flex;flex-direction:column;flex:1;min-height:0;height:100%;width:100%}.meshboard-view{display:flex;flex-direction:column;flex:1;min-height:0;width:100%;background-color:var(--kendo-color-surface, #f5f5f5)}.loading-container{display:flex;align-items:center;justify-content:center;height:100%;width:100%}.entity-selector-hint{display:flex;align-items:center;gap:8px;padding:10px 16px;margin-bottom:8px;background:color-mix(in srgb,var(--kendo-color-info, #2196f3) 10%,var(--kendo-color-surface-alt, #fafafa));border:1px solid color-mix(in srgb,var(--kendo-color-info, #2196f3) 40%,transparent);border-radius:4px;font-size:13px;color:var(--kendo-color-on-app-surface, #424242)}.entity-selector-hint strong{color:var(--kendo-color-info, #2196f3)}.variable-resolution-errors{display:flex;flex-wrap:wrap;align-items:center;gap:8px;padding:8px 16px;margin-bottom:8px;background:color-mix(in srgb,var(--kendo-color-error, #f44336) 10%,var(--kendo-color-surface-alt, #fafafa));border:1px solid var(--kendo-color-error, #f44336);border-radius:4px;font-size:13px;color:var(--kendo-color-on-app-surface, #424242)}.variable-resolution-errors strong{color:var(--kendo-color-error, #f44336)}.variable-resolution-errors .error-item{padding:2px 8px;background:color-mix(in srgb,var(--kendo-color-error, #f44336) 8%,transparent);border-radius:3px;font-family:var(--theme-font-mono);font-size:12px}.scope-resolution-errors .error-item{font-family:inherit}.model-unavailable{display:flex;align-items:center;justify-content:center;height:100%;width:100%;padding:2rem;overflow:hidden;box-sizing:border-box}.model-unavailable .message-card{text-align:center;max-width:500px;padding:2rem;background:var(--kendo-color-surface-alt, #fafafa);border:1px solid var(--kendo-color-border, #e0e0e0);border-radius:8px;overflow:hidden}.model-unavailable .message-card kendo-svg-icon{color:var(--kendo-color-warning, #ff9800);margin-bottom:1rem}.model-unavailable .message-card h2{margin:1rem 0;color:var(--kendo-color-on-app-surface, #424242)}.model-unavailable .message-card p{margin:.5rem 0;color:var(--kendo-color-subtle, #757575);line-height:1.5}.model-unavailable .message-card ol{text-align:left;margin:1rem 0;padding-left:1.5rem;color:var(--kendo-color-subtle, #757575);line-height:1.8}.model-unavailable .message-card ol li{margin:.25rem 0}.model-unavailable .message-card ol a{color:var(--kendo-color-primary, #3f51b5);text-decoration:none}.model-unavailable .message-card ol a:hover{text-decoration:underline}.meshboard-toolbar{display:flex;justify-content:space-between;align-items:flex-end;padding:1rem 1.5rem;background-color:var(--kendo-color-surface-alt, white);border-bottom:1px solid var(--kendo-color-border, #e0e0e0);gap:1rem;flex-shrink:0}.meshboard-toolbar .toolbar-left{display:flex;align-items:baseline;gap:1rem;flex:1;min-width:0;align-self:center}.meshboard-toolbar .toolbar-left .meshboard-title{margin:0;font-size:1.5rem;font-weight:500;color:var(--kendo-color-on-app-surface, #212121);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.meshboard-toolbar .toolbar-left .meshboard-description{color:var(--kendo-color-subtle, #757575);font-size:.875rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.meshboard-toolbar .toolbar-center{display:flex;justify-content:center;align-items:flex-end;flex-shrink:0}.meshboard-toolbar .toolbar-center mm-time-range-picker ::ng-deep .time-range-picker{gap:.5rem}.meshboard-toolbar .toolbar-separator{width:1px;align-self:stretch;background-color:var(--kendo-color-border, #e0e0e0);flex-shrink:0}.meshboard-toolbar .toolbar-entity-selectors{display:flex;align-items:flex-end;flex-shrink:0}.meshboard-toolbar .toolbar-right{display:flex;gap:.5rem;align-items:center;flex-shrink:0}.meshboard-toolbar.compact{justify-content:flex-end;align-items:center;padding:.25rem 1.5rem 0;background-color:transparent;border-bottom:none}kendo-tilelayout{padding:1.5rem;overflow:auto;flex:1;min-height:0}.widget-header{display:flex;justify-content:space-between;align-items:center;width:100%;gap:.5rem}.widget-header.unconfigured{background-color:#ff98001a}.widget-header .widget-title{flex:1;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.widget-header .unconfigured-badge{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:50%;background-color:var(--kendo-color-warning, #ff9800);color:#fff;font-size:12px;font-weight:700;flex-shrink:0}.widget-header .widget-actions{display:flex;align-items:center;gap:.25rem;flex-shrink:0}.widget-header .config-widget-btn,.widget-header .edit-widget-btn,.widget-header .remove-widget-btn{opacity:.6;transition:opacity .2s}.widget-header .config-widget-btn:hover,.widget-header .edit-widget-btn:hover,.widget-header .remove-widget-btn:hover{opacity:1}.widget-header .config-widget-btn.needs-config{color:var(--kendo-color-warning, #ff9800);opacity:1}.widget-header .remove-widget-btn{color:var(--kendo-color-error, #f44336)}.widget-error{display:flex;align-items:center;justify-content:center;height:100%;padding:1rem;color:var(--kendo-color-error, #f44336);text-align:center}.meshboard-view.edit-mode kendo-tilelayout-item:hover{box-shadow:0 0 0 2px var(--kendo-color-primary, #3f51b5)}@media(max-width:768px){.meshboard-toolbar{flex-direction:column;align-items:stretch;gap:.75rem}.meshboard-toolbar .toolbar-left{flex-direction:column;align-items:flex-start;gap:.25rem}.meshboard-toolbar .toolbar-right{flex-wrap:wrap;justify-content:flex-end}.meshboard-toolbar.compact{align-items:flex-end;padding:.25rem 1rem 0}kendo-tilelayout{padding:1rem}}:host ::ng-deep kendo-tilelayout.has-banners{padding-top:0!important}:host ::ng-deep kendo-tilelayout{padding-bottom:1.5rem!important}.widget-header.chromeless-header{display:none}:host ::ng-deep .k-tilelayout-item-header:has(.chromeless-header){display:none}:host ::ng-deep kendo-tilelayout-item:has(.chromeless-header).k-tilelayout-item,:host ::ng-deep kendo-tilelayout-item:has(.chromeless-header).k-card{border:none;box-shadow:none;background:transparent}:host ::ng-deep kendo-tilelayout-item:has(kendo-dialog),:host ::ng-deep kendo-tilelayout-item:has(kendo-window){z-index:9999}.zone-label{font-size:.65rem;opacity:.4;padding:6px 1rem 2px;font-weight:600}.banner-zone-item{margin:0;padding:8px 1rem}.banner-zone-item.banner-edit{border:1px dashed var(--kendo-color-border, rgba(255, 255, 255, .2));border-radius:4px;margin:0 1rem 12px;padding:4px 0}.banner-edit-header{display:flex;align-items:center;justify-content:space-between;padding:4px 8px;opacity:.7}.banner-edit-header .widget-title{font-size:.75rem}.banner-edit-header .widget-actions{display:flex;gap:2px}.meshboard-view.chrome-plain{background:transparent!important}.meshboard-view.chrome-plain .meshboard-toolbar{padding:0 0 var(--theme-space-3, 12px);background:transparent!important;border-bottom:none!important}.meshboard-view.chrome-plain kendo-tilelayout{padding:0!important}.meshboard-view.chrome-plain .banner-zone-item:not(.banner-edit){padding-left:0;padding-right:0}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$3.NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i3$1.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }, { kind: "ngmodule", type: TileLayoutModule }, { kind: "component", type: i5.TileLayoutComponent, selector: "kendo-tilelayout", inputs: ["columns", "columnWidth", "gap", "reorderable", "resizable", "rowHeight", "autoFlow", "navigable"], outputs: ["reorder", "resize"] }, { kind: "component", type: i5.TileLayoutItemBodyComponent, selector: "kendo-tilelayout-item-body" }, { kind: "component", type: i5.TileLayoutItemComponent, selector: "kendo-tilelayout-item", inputs: ["title", "rowSpan", "colSpan", "order", "col", "row", "reorderable", "resizable"] }, { kind: "component", type: i5.TileLayoutItemHeaderComponent, selector: "kendo-tilelayout-item-header" }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: DialogModule }, { kind: "ngmodule", type: SVGIconModule }, { kind: "component", type: i1$4.SVGIconComponent, selector: "kendo-svg-icon, kendo-svgicon", inputs: ["icon", "variant"], exportAs: ["kendoSVGIcon"] }, { kind: "component", type: EditWidgetDialogComponent, selector: "mm-edit-widget-dialog", inputs: ["widget", "widgets", "maxColumns", "gridService"], outputs: ["save", "cancelled"] }, { kind: "component", type: TimeRangePickerComponent, selector: "mm-time-range-picker", inputs: ["config", "labels", "initialSelection"], outputs: ["rangeChange", "rangeChangeISO", "selectionChange"] }, { kind: "component", type: EntitySelectorToolbarComponent, selector: "mm-entity-selector-toolbar", inputs: ["entitySelectors", "variables"], outputs: ["entitySelected", "entityCleared"] }, { kind: "component", type: MeshBoardEmptyStateComponent, selector: "mm-meshboard-empty-state", inputs: ["readonly"], outputs: ["addWidget"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
|
|
36171
37791
|
}
|
|
36172
37792
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: MeshBoardViewComponent, decorators: [{
|
|
36173
37793
|
type: Component,
|
|
@@ -36185,11 +37805,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
36185
37805
|
], hostDirectives: [UnsavedChangesDirective], providers: [
|
|
36186
37806
|
{ provide: HAS_UNSAVED_CHANGES, useExisting: MeshBoardViewComponent },
|
|
36187
37807
|
AutoRefreshTimerService
|
|
36188
|
-
], changeDetection: ChangeDetectionStrategy.Eager, template: "<div class=\"meshboard-view\" [class.chrome-plain]=\"effectiveChrome() === 'plain'\">\n @if (isLoading()) {\n <div class=\"loading-container\">\n <div class=\"k-loading-mask\">\n <span class=\"k-loading-text\">Loading MeshBoard...</span>\n <div class=\"k-loading-image\"></div>\n <div class=\"k-loading-color\"></div>\n </div>\n </div>\n } @else if (isModelAvailable() === false) {\n <div class=\"model-unavailable\">\n <div class=\"message-card\">\n <kendo-svg-icon [icon]=\"xIcon\" size=\"xlarge\"></kendo-svg-icon>\n <h2>MeshBoard Not Available</h2>\n <p>The MeshBoard feature requires the CK model 'System.UI' version 1.0.1 or higher.</p>\n <p>Please install the required model in your tenant to use this feature.</p>\n </div>\n </div>\n } @else if (notFoundError()) {\n <div class=\"model-unavailable\">\n <div class=\"message-card\">\n <kendo-svg-icon [icon]=\"xIcon\" size=\"xlarge\"></kendo-svg-icon>\n <h2>MeshBoard Not Found</h2>\n <p>{{ notFoundError() }}</p>\n <p>To create a MeshBoard with a Well-Known Name:</p>\n <ol>\n <li>Go to the <a [routerLink]=\"meshBoardPageLink()\">MeshBoard</a> page</li>\n <li>Open Settings</li>\n <li>Set the \"Well-Known Name\" field</li>\n <li>Save the MeshBoard</li>\n </ol>\n </div>\n </div>\n } @else if (isInitialized()) {\n <!-- Toolbar (headerMode: full = name + description + controls, compact = controls only, none = no row) -->\n @if (effectiveHeaderMode() !== 'none') {\n <div class=\"meshboard-toolbar\" [class.compact]=\"effectiveHeaderMode() === 'compact'\">\n @if (effectiveHeaderMode() === 'full') {\n <div class=\"toolbar-left\">\n <h2 class=\"meshboard-title\">{{ config().name }}</h2>\n @if (config().description) {\n <span class=\"meshboard-description\">{{ config().description }}</span>\n }\n </div>\n }\n\n <!-- Time Filter (center) -->\n @if (isTimeFilterEnabled()) {\n <div class=\"toolbar-center\">\n <mm-time-range-picker\n [config]=\"timeFilterConfig()?.pickerConfig ?? {}\"\n [labels]=\"timeRangeLabels()\"\n [initialSelection]=\"initialTimeSelection()\"\n (rangeChange)=\"onTimeRangeChange($event)\"\n (selectionChange)=\"onTimeSelectionChange($event)\">\n </mm-time-range-picker>\n @if (canResetTimeFilter()) {\n <button\n kendoButton\n [svgIcon]=\"undoIcon\"\n (click)=\"resetTimeFilterToDefault()\"\n title=\"Reset to default time filter\"\n aria-label=\"Reset to default time filter\"\n fillMode=\"flat\"\n size=\"small\">\n </button>\n }\n </div>\n }\n\n <!-- Entity Selectors -->\n @if (hasEntitySelectors()) {\n @if (isTimeFilterEnabled()) {\n <div class=\"toolbar-separator\"></div>\n }\n <div class=\"toolbar-entity-selectors\">\n <mm-entity-selector-toolbar\n [entitySelectors]=\"entitySelectorsConfig()\"\n (entitySelected)=\"onEntitySelectorSelected($event)\"\n (entityCleared)=\"onEntitySelectorCleared($event)\">\n </mm-entity-selector-toolbar>\n </div>\n }\n\n @if (isTimeFilterEnabled() || hasEntitySelectors()) {\n <div class=\"toolbar-separator\"></div>\n }\n <div class=\"toolbar-right mm-toolbar-actions\">\n @if (!isReadonly()) {\n <!-- Manager Button (icon only) -->\n <button\n kendoButton\n [svgIcon]=\"gridLayoutIcon\"\n (click)=\"openManager()\"\n title=\"Manage MeshBoards\"\n aria-label=\"Manage MeshBoards\"\n fillMode=\"flat\">\n </button>\n\n <!-- Settings Button (icon only) -->\n <button\n kendoButton\n [svgIcon]=\"gearIcon\"\n (click)=\"openSettings()\"\n title=\"MeshBoard Settings\"\n aria-label=\"MeshBoard Settings\"\n fillMode=\"flat\">\n </button>\n }\n\n <!-- Refresh Button (icon only) -->\n <button\n kendoButton\n [svgIcon]=\"arrowRotateCwIcon\"\n (click)=\"refresh()\"\n title=\"Refresh All Widgets\"\n aria-label=\"Refresh All Widgets\"\n fillMode=\"flat\">\n </button>\n\n @if (!isReadonly()) {\n <!-- Edit Mode Toggle (icon only) -->\n @if (!isEditMode()) {\n <button\n kendoButton\n [svgIcon]=\"pencilIcon\"\n (click)=\"toggleEditMode()\"\n title=\"Enter Edit Mode\"\n aria-label=\"Enter Edit Mode\"\n fillMode=\"flat\">\n </button>\n } @else {\n <button\n kendoButton\n [svgIcon]=\"xIcon\"\n (click)=\"cancelEdit()\"\n title=\"Cancel Edit Mode\"\n aria-label=\"Cancel Edit Mode\"\n fillMode=\"flat\">\n </button>\n }\n\n <!-- Add Widget Button (with text) -->\n <button\n kendoButton\n [svgIcon]=\"plusIcon\"\n (click)=\"openAddWidget()\"\n title=\"Add Widget\"\n themeColor=\"primary\">\n Add Widget\n </button>\n\n <!-- Save Button (with text) -->\n @if (canSave()) {\n <button\n kendoButton\n [svgIcon]=\"saveIcon\"\n (click)=\"save()\"\n [disabled]=\"isSaving()\"\n title=\"Save MeshBoard\"\n themeColor=\"primary\">\n {{ isSaving() ? 'Saving...' : 'Save' }}\n </button>\n }\n }\n </div>\n </div>\n }\n\n <!-- Entity Selector Hint -->\n @if (unselectedToolbarSelectors().length > 0) {\n <div class=\"entity-selector-hint\">\n <kendo-svg-icon [icon]=\"infoCircleIcon\" size=\"medium\"></kendo-svg-icon>\n <span>\n Please select\n @for (selector of unselectedToolbarSelectors(); track selector.id; let last = $last) {\n <strong>{{ selector.label }}</strong>@if (!last) {, }\n }\n to display dashboard data.\n </span>\n </div>\n }\n\n <!-- Variable Resolution Errors -->\n @if (variableResolutionErrors().length > 0) {\n <div class=\"variable-resolution-errors\">\n <strong>Variable resolution errors:</strong>\n @for (error of variableResolutionErrors(); track error.variableName) {\n <span class=\"error-item\">{{ error.variableName }}: {{ error.message }}</span>\n }\n </div>\n }\n\n <!-- Entity selector scopes that could not be resolved (AB#7721) -->\n @if (scopeResolutionErrors().length > 0) {\n <div class=\"variable-resolution-errors scope-resolution-errors\" role=\"status\">\n @for (error of scopeResolutionErrors(); track error.selectorId) {\n <span class=\"error-item\">{{ error.message }}</span>\n }\n </div>\n }\n\n <!-- Banner Zone: widgets placed in the banner stack above the grid -->\n @if (isEditMode()) {\n <div class=\"zone-label banner-zone-label\">Banner Zone</div>\n }\n @for (widget of bannerWidgets(); track trackByWidgetId($index, widget); let first = $first; let last = $last) {\n <div class=\"banner-zone-item\" [class.banner-edit]=\"isEditMode()\">\n @if (isEditMode()) {\n <div class=\"banner-edit-header\">\n <span class=\"widget-title\">{{ widget.title }}</span>\n <div class=\"widget-actions\">\n <button\n kendoButton\n [svgIcon]=\"arrowUpIcon\"\n (click)=\"moveBannerUp(widget.id)\"\n fillMode=\"flat\"\n size=\"small\"\n [disabled]=\"first\"\n title=\"Move up\"\n [attr.aria-label]=\"'Move up ' + widget.title\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"arrowDownIcon\"\n (click)=\"moveBannerDown(widget.id)\"\n fillMode=\"flat\"\n size=\"small\"\n [disabled]=\"last\"\n title=\"Move down\"\n [attr.aria-label]=\"'Move down ' + widget.title\">\n </button>\n @if (supportsConfiguration(widget)) {\n <button\n kendoButton\n [svgIcon]=\"linkIcon\"\n (click)=\"openWidgetConfig(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"config-widget-btn\"\n [class.needs-config]=\"isWidgetUnconfigured(widget)\"\n [title]=\"isWidgetUnconfigured(widget) ? 'Configure data source (required)' : 'Configure data source'\"\n [attr.aria-label]=\"(isWidgetUnconfigured(widget) ? 'Configure data source (required) ' : 'Configure data source ') + widget.title\">\n </button>\n }\n <button\n kendoButton\n [svgIcon]=\"gearIcon\"\n (click)=\"openEditWidgetDialog(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"edit-widget-btn\"\n title=\"Edit widget\"\n [attr.aria-label]=\"'Edit widget ' + widget.title\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"trashIcon\"\n (click)=\"removeWidget(widget.id)\"\n fillMode=\"flat\"\n themeColor=\"error\"\n size=\"small\"\n class=\"remove-widget-btn\"\n title=\"Delete widget\"\n [attr.aria-label]=\"'Delete widget ' + widget.title\">\n </button>\n </div>\n </div>\n }\n @if (getWidgetComponentType(widget.type); as componentType) {\n <ng-container *ngComponentOutlet=\"componentType; inputs: { config: widget }\"></ng-container>\n }\n </div>\n }\n\n <!-- MeshBoard Grid -->\n @if (isEditMode()) {\n <div class=\"zone-label grid-zone-label\">Grid Zone</div>\n }\n @if (hasGridWidgets()) {\n <kendo-tilelayout\n [columns]=\"displayColumns()\"\n [autoFlow]=\"displayAutoFlow()\"\n [rowHeight]=\"config().rowHeight\"\n [gap]=\"config().gap\"\n [class.has-banners]=\"bannerWidgets().length > 0 && !isEditMode()\"\n [resizable]=\"canEditLayout()\"\n [reorderable]=\"canEditLayout()\"\n (reorder)=\"onReorder($event)\"\n (resize)=\"onResize($event)\">\n @for (placement of displayGridWidgets(); track placement.widget.id) {\n @let widget = placement.widget;\n <kendo-tilelayout-item\n [col]=\"$any(placement.col)\"\n [row]=\"$any(placement.row)\"\n [colSpan]=\"placement.colSpan\"\n [rowSpan]=\"placement.rowSpan\">\n <kendo-tilelayout-item-header>\n <div class=\"widget-header\"\n [class.unconfigured]=\"isWidgetUnconfigured(widget)\"\n [class.chromeless-header]=\"widget.chromeless && !isEditMode()\">\n @if (!(widget.chromeless && !isEditMode())) {\n <span class=\"widget-title\">{{ widget.title }}</span>\n @if (isWidgetUnconfigured(widget)) {\n <span class=\"unconfigured-badge\" title=\"Widget needs configuration\">!</span>\n }\n }\n @if (isEditMode()) {\n <div class=\"widget-actions\" (pointerdown)=\"$event.stopPropagation()\">\n @if (supportsConfiguration(widget)) {\n <button\n kendoButton\n [svgIcon]=\"linkIcon\"\n (click)=\"openWidgetConfig(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"config-widget-btn\"\n [class.needs-config]=\"isWidgetUnconfigured(widget)\"\n [title]=\"isWidgetUnconfigured(widget) ? 'Configure data source (required)' : 'Configure data source'\"\n [attr.aria-label]=\"(isWidgetUnconfigured(widget) ? 'Configure data source (required) ' : 'Configure data source ') + widget.title\">\n </button>\n }\n <button\n kendoButton\n [svgIcon]=\"gearIcon\"\n (click)=\"openEditWidgetDialog(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"edit-widget-btn\"\n title=\"Edit position\"\n [attr.aria-label]=\"'Edit position ' + widget.title\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"copyIcon\"\n (click)=\"duplicateWidget(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"duplicate-widget-btn\"\n title=\"Duplicate widget\"\n [attr.aria-label]=\"'Duplicate widget ' + widget.title\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"trashIcon\"\n (click)=\"removeWidget(widget.id)\"\n fillMode=\"flat\"\n themeColor=\"error\"\n size=\"small\"\n class=\"remove-widget-btn\"\n title=\"Delete widget\"\n [attr.aria-label]=\"'Delete widget ' + widget.title\">\n </button>\n </div>\n }\n </div>\n </kendo-tilelayout-item-header>\n <kendo-tilelayout-item-body>\n @if (getWidgetComponentType(widget.type); as componentType) {\n <ng-container *ngComponentOutlet=\"componentType; inputs: { config: widget }\"></ng-container>\n } @else {\n <div class=\"widget-error\">\n <p>Unknown widget type: {{ widget.type }}</p>\n </div>\n }\n </kendo-tilelayout-item-body>\n </kendo-tilelayout-item>\n }\n </kendo-tilelayout>\n } @else {\n <mm-meshboard-empty-state [readonly]=\"isReadonly()\" (addWidget)=\"openAddWidget()\"></mm-meshboard-empty-state>\n }\n\n <!-- Edit Widget Dialog -->\n @if (showEditWidgetDialog && editingWidget) {\n <mm-edit-widget-dialog\n [widget]=\"editingWidget\"\n [widgets]=\"config().widgets\"\n [maxColumns]=\"config().columns\"\n [gridService]=\"gridService\"\n (save)=\"onEditWidgetSave($event)\"\n (cancelled)=\"closeEditWidgetDialog()\">\n </mm-edit-widget-dialog>\n }\n }\n</div>\n", styles: [":host{display:flex;flex-direction:column;flex:1;min-height:0;height:100%;width:100%}.meshboard-view{display:flex;flex-direction:column;flex:1;min-height:0;width:100%;background-color:var(--kendo-color-surface, #f5f5f5)}.loading-container{display:flex;align-items:center;justify-content:center;height:100%;width:100%}.entity-selector-hint{display:flex;align-items:center;gap:8px;padding:10px 16px;margin-bottom:8px;background:color-mix(in srgb,var(--kendo-color-info, #2196f3) 10%,var(--kendo-color-surface-alt, #fafafa));border:1px solid color-mix(in srgb,var(--kendo-color-info, #2196f3) 40%,transparent);border-radius:4px;font-size:13px;color:var(--kendo-color-on-app-surface, #424242)}.entity-selector-hint strong{color:var(--kendo-color-info, #2196f3)}.variable-resolution-errors{display:flex;flex-wrap:wrap;align-items:center;gap:8px;padding:8px 16px;margin-bottom:8px;background:color-mix(in srgb,var(--kendo-color-error, #f44336) 10%,var(--kendo-color-surface-alt, #fafafa));border:1px solid var(--kendo-color-error, #f44336);border-radius:4px;font-size:13px;color:var(--kendo-color-on-app-surface, #424242)}.variable-resolution-errors strong{color:var(--kendo-color-error, #f44336)}.variable-resolution-errors .error-item{padding:2px 8px;background:color-mix(in srgb,var(--kendo-color-error, #f44336) 8%,transparent);border-radius:3px;font-family:var(--theme-font-mono);font-size:12px}.scope-resolution-errors .error-item{font-family:inherit}.model-unavailable{display:flex;align-items:center;justify-content:center;height:100%;width:100%;padding:2rem;overflow:hidden;box-sizing:border-box}.model-unavailable .message-card{text-align:center;max-width:500px;padding:2rem;background:var(--kendo-color-surface-alt, #fafafa);border:1px solid var(--kendo-color-border, #e0e0e0);border-radius:8px;overflow:hidden}.model-unavailable .message-card kendo-svg-icon{color:var(--kendo-color-warning, #ff9800);margin-bottom:1rem}.model-unavailable .message-card h2{margin:1rem 0;color:var(--kendo-color-on-app-surface, #424242)}.model-unavailable .message-card p{margin:.5rem 0;color:var(--kendo-color-subtle, #757575);line-height:1.5}.model-unavailable .message-card ol{text-align:left;margin:1rem 0;padding-left:1.5rem;color:var(--kendo-color-subtle, #757575);line-height:1.8}.model-unavailable .message-card ol li{margin:.25rem 0}.model-unavailable .message-card ol a{color:var(--kendo-color-primary, #3f51b5);text-decoration:none}.model-unavailable .message-card ol a:hover{text-decoration:underline}.meshboard-toolbar{display:flex;justify-content:space-between;align-items:flex-end;padding:1rem 1.5rem;background-color:var(--kendo-color-surface-alt, white);border-bottom:1px solid var(--kendo-color-border, #e0e0e0);gap:1rem;flex-shrink:0}.meshboard-toolbar .toolbar-left{display:flex;align-items:baseline;gap:1rem;flex:1;min-width:0;align-self:center}.meshboard-toolbar .toolbar-left .meshboard-title{margin:0;font-size:1.5rem;font-weight:500;color:var(--kendo-color-on-app-surface, #212121);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.meshboard-toolbar .toolbar-left .meshboard-description{color:var(--kendo-color-subtle, #757575);font-size:.875rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.meshboard-toolbar .toolbar-center{display:flex;justify-content:center;align-items:flex-end;flex-shrink:0}.meshboard-toolbar .toolbar-center mm-time-range-picker ::ng-deep .time-range-picker{gap:.5rem}.meshboard-toolbar .toolbar-separator{width:1px;align-self:stretch;background-color:var(--kendo-color-border, #e0e0e0);flex-shrink:0}.meshboard-toolbar .toolbar-entity-selectors{display:flex;align-items:flex-end;flex-shrink:0}.meshboard-toolbar .toolbar-right{display:flex;gap:.5rem;align-items:center;flex-shrink:0}.meshboard-toolbar.compact{justify-content:flex-end;align-items:center;padding:.25rem 1.5rem 0;background-color:transparent;border-bottom:none}kendo-tilelayout{padding:1.5rem;overflow:auto;flex:1;min-height:0}.widget-header{display:flex;justify-content:space-between;align-items:center;width:100%;gap:.5rem}.widget-header.unconfigured{background-color:#ff98001a}.widget-header .widget-title{flex:1;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.widget-header .unconfigured-badge{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:50%;background-color:var(--kendo-color-warning, #ff9800);color:#fff;font-size:12px;font-weight:700;flex-shrink:0}.widget-header .widget-actions{display:flex;align-items:center;gap:.25rem;flex-shrink:0}.widget-header .config-widget-btn,.widget-header .edit-widget-btn,.widget-header .remove-widget-btn{opacity:.6;transition:opacity .2s}.widget-header .config-widget-btn:hover,.widget-header .edit-widget-btn:hover,.widget-header .remove-widget-btn:hover{opacity:1}.widget-header .config-widget-btn.needs-config{color:var(--kendo-color-warning, #ff9800);opacity:1}.widget-header .remove-widget-btn{color:var(--kendo-color-error, #f44336)}.widget-error{display:flex;align-items:center;justify-content:center;height:100%;padding:1rem;color:var(--kendo-color-error, #f44336);text-align:center}.meshboard-view.edit-mode kendo-tilelayout-item:hover{box-shadow:0 0 0 2px var(--kendo-color-primary, #3f51b5)}@media(max-width:768px){.meshboard-toolbar{flex-direction:column;align-items:stretch;gap:.75rem}.meshboard-toolbar .toolbar-left{flex-direction:column;align-items:flex-start;gap:.25rem}.meshboard-toolbar .toolbar-right{flex-wrap:wrap;justify-content:flex-end}.meshboard-toolbar.compact{align-items:flex-end;padding:.25rem 1rem 0}kendo-tilelayout{padding:1rem}}:host ::ng-deep kendo-tilelayout.has-banners{padding-top:0!important}:host ::ng-deep kendo-tilelayout{padding-bottom:1.5rem!important}.widget-header.chromeless-header{display:none}:host ::ng-deep .k-tilelayout-item-header:has(.chromeless-header){display:none}:host ::ng-deep kendo-tilelayout-item:has(.chromeless-header).k-tilelayout-item,:host ::ng-deep kendo-tilelayout-item:has(.chromeless-header).k-card{border:none;box-shadow:none;background:transparent}:host ::ng-deep kendo-tilelayout-item:has(kendo-dialog),:host ::ng-deep kendo-tilelayout-item:has(kendo-window){z-index:9999}.zone-label{font-size:.65rem;opacity:.4;padding:6px 1rem 2px;font-weight:600}.banner-zone-item{margin:0;padding:8px 1rem}.banner-zone-item.banner-edit{border:1px dashed var(--kendo-color-border, rgba(255, 255, 255, .2));border-radius:4px;margin:0 1rem 12px;padding:4px 0}.banner-edit-header{display:flex;align-items:center;justify-content:space-between;padding:4px 8px;opacity:.7}.banner-edit-header .widget-title{font-size:.75rem}.banner-edit-header .widget-actions{display:flex;gap:2px}.meshboard-view.chrome-plain{background:transparent!important}.meshboard-view.chrome-plain .meshboard-toolbar{padding:0 0 var(--theme-space-3, 12px);background:transparent!important;border-bottom:none!important}.meshboard-view.chrome-plain kendo-tilelayout{padding:0!important}.meshboard-view.chrome-plain .banner-zone-item:not(.banner-edit){padding-left:0;padding-right:0}\n"] }]
|
|
37808
|
+
], changeDetection: ChangeDetectionStrategy.Eager, template: "<div class=\"meshboard-view\" [class.chrome-plain]=\"effectiveChrome() === 'plain'\">\n @if (isLoading()) {\n <div class=\"loading-container\">\n <div class=\"k-loading-mask\">\n <span class=\"k-loading-text\">Loading MeshBoard...</span>\n <div class=\"k-loading-image\"></div>\n <div class=\"k-loading-color\"></div>\n </div>\n </div>\n } @else if (isModelAvailable() === false) {\n <div class=\"model-unavailable\">\n <div class=\"message-card\">\n <kendo-svg-icon [icon]=\"xIcon\" size=\"xlarge\"></kendo-svg-icon>\n <h2>MeshBoard Not Available</h2>\n <p>The MeshBoard feature requires the CK model 'System.UI' version 1.0.1 or higher.</p>\n <p>Please install the required model in your tenant to use this feature.</p>\n </div>\n </div>\n } @else if (notFoundError()) {\n <div class=\"model-unavailable\">\n <div class=\"message-card\">\n <kendo-svg-icon [icon]=\"xIcon\" size=\"xlarge\"></kendo-svg-icon>\n <h2>MeshBoard Not Found</h2>\n <p>{{ notFoundError() }}</p>\n <p>To create a MeshBoard with a Well-Known Name:</p>\n <ol>\n <li>Go to the <a [routerLink]=\"meshBoardPageLink()\">MeshBoard</a> page</li>\n <li>Open Settings</li>\n <li>Set the \"Well-Known Name\" field</li>\n <li>Save the MeshBoard</li>\n </ol>\n </div>\n </div>\n } @else if (isInitialized()) {\n <!-- Toolbar (headerMode: full = name + description + controls, compact = controls only, none = no row) -->\n @if (effectiveHeaderMode() !== 'none') {\n <div class=\"meshboard-toolbar\" [class.compact]=\"effectiveHeaderMode() === 'compact'\">\n @if (effectiveHeaderMode() === 'full') {\n <div class=\"toolbar-left\">\n <h2 class=\"meshboard-title\">{{ config().name }}</h2>\n @if (config().description) {\n <span class=\"meshboard-description\">{{ config().description }}</span>\n }\n </div>\n }\n\n <!-- Time Filter (center); hidden when the host owns the period (AB#7582) -->\n @if (showTimeRangePicker()) {\n <div class=\"toolbar-center\">\n <mm-time-range-picker\n [config]=\"timeFilterConfig()?.pickerConfig ?? {}\"\n [labels]=\"timeRangeLabels()\"\n [initialSelection]=\"initialTimeSelection()\"\n (rangeChange)=\"onTimeRangeChange($event)\"\n (selectionChange)=\"onTimeSelectionChange($event)\">\n </mm-time-range-picker>\n @if (canResetTimeFilter()) {\n <button\n kendoButton\n [svgIcon]=\"undoIcon\"\n (click)=\"resetTimeFilterToDefault()\"\n title=\"Reset to default time filter\"\n aria-label=\"Reset to default time filter\"\n fillMode=\"flat\"\n size=\"small\">\n </button>\n }\n </div>\n }\n\n <!-- Entity Selectors -->\n @if (hasEntitySelectors()) {\n @if (showTimeRangePicker()) {\n <div class=\"toolbar-separator\"></div>\n }\n <div class=\"toolbar-entity-selectors\">\n <mm-entity-selector-toolbar\n [entitySelectors]=\"entitySelectorsConfig()\"\n [variables]=\"boardVariables()\"\n (entitySelected)=\"onEntitySelectorSelected($event)\"\n (entityCleared)=\"onEntitySelectorCleared($event)\">\n </mm-entity-selector-toolbar>\n </div>\n }\n\n @if (showTimeRangePicker() || hasEntitySelectors()) {\n <div class=\"toolbar-separator\"></div>\n }\n <div class=\"toolbar-right mm-toolbar-actions\">\n @if (!isReadonly()) {\n <!-- Manager Button (icon only) -->\n <button\n kendoButton\n [svgIcon]=\"gridLayoutIcon\"\n (click)=\"openManager()\"\n title=\"Manage MeshBoards\"\n aria-label=\"Manage MeshBoards\"\n fillMode=\"flat\">\n </button>\n\n <!-- Settings Button (icon only) -->\n <button\n kendoButton\n [svgIcon]=\"gearIcon\"\n (click)=\"openSettings()\"\n title=\"MeshBoard Settings\"\n aria-label=\"MeshBoard Settings\"\n fillMode=\"flat\">\n </button>\n }\n\n <!-- Refresh Button (icon only) -->\n <button\n kendoButton\n [svgIcon]=\"arrowRotateCwIcon\"\n (click)=\"refresh()\"\n title=\"Refresh All Widgets\"\n aria-label=\"Refresh All Widgets\"\n fillMode=\"flat\">\n </button>\n\n @if (!isReadonly()) {\n <!-- Edit Mode Toggle (icon only) -->\n @if (!isEditMode()) {\n <button\n kendoButton\n [svgIcon]=\"pencilIcon\"\n (click)=\"toggleEditMode()\"\n title=\"Enter Edit Mode\"\n aria-label=\"Enter Edit Mode\"\n fillMode=\"flat\">\n </button>\n } @else {\n <button\n kendoButton\n [svgIcon]=\"xIcon\"\n (click)=\"cancelEdit()\"\n title=\"Cancel Edit Mode\"\n aria-label=\"Cancel Edit Mode\"\n fillMode=\"flat\">\n </button>\n }\n\n <!-- Add Widget Button (with text) -->\n <button\n kendoButton\n [svgIcon]=\"plusIcon\"\n (click)=\"openAddWidget()\"\n title=\"Add Widget\"\n themeColor=\"primary\">\n Add Widget\n </button>\n\n <!-- Save Button (with text) -->\n @if (canSave()) {\n <button\n kendoButton\n [svgIcon]=\"saveIcon\"\n (click)=\"save()\"\n [disabled]=\"isSaving()\"\n title=\"Save MeshBoard\"\n themeColor=\"primary\">\n {{ isSaving() ? 'Saving...' : 'Save' }}\n </button>\n }\n }\n </div>\n </div>\n }\n\n <!-- Entity Selector Hint -->\n @if (unselectedToolbarSelectors().length > 0) {\n <div class=\"entity-selector-hint\">\n <kendo-svg-icon [icon]=\"infoCircleIcon\" size=\"medium\"></kendo-svg-icon>\n <span>\n Please select\n @for (selector of unselectedToolbarSelectors(); track selector.id; let last = $last) {\n <strong>{{ selector.label }}</strong>@if (!last) {, }\n }\n to display dashboard data.\n </span>\n </div>\n }\n\n <!-- Variable Resolution Errors -->\n @if (variableResolutionErrors().length > 0) {\n <div class=\"variable-resolution-errors\">\n <strong>Variable resolution errors:</strong>\n @for (error of variableResolutionErrors(); track error.variableName) {\n <span class=\"error-item\">{{ error.variableName }}: {{ error.message }}</span>\n }\n </div>\n }\n\n <!-- Entity selector scopes that could not be resolved (AB#7721) -->\n @if (scopeResolutionErrors().length > 0) {\n <div class=\"variable-resolution-errors scope-resolution-errors\" role=\"status\">\n @for (error of scopeResolutionErrors(); track error.selectorId) {\n <span class=\"error-item\">{{ error.message }}</span>\n }\n </div>\n }\n\n <!-- Banner Zone: widgets placed in the banner stack above the grid -->\n @if (isEditMode()) {\n <div class=\"zone-label banner-zone-label\">Banner Zone</div>\n }\n @for (widget of bannerWidgets(); track trackByWidgetId($index, widget); let first = $first; let last = $last) {\n <div class=\"banner-zone-item\" [class.banner-edit]=\"isEditMode()\">\n @if (isEditMode()) {\n <div class=\"banner-edit-header\">\n <span class=\"widget-title\">{{ widget.title }}</span>\n <div class=\"widget-actions\">\n <button\n kendoButton\n [svgIcon]=\"arrowUpIcon\"\n (click)=\"moveBannerUp(widget.id)\"\n fillMode=\"flat\"\n size=\"small\"\n [disabled]=\"first\"\n title=\"Move up\"\n [attr.aria-label]=\"'Move up ' + widget.title\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"arrowDownIcon\"\n (click)=\"moveBannerDown(widget.id)\"\n fillMode=\"flat\"\n size=\"small\"\n [disabled]=\"last\"\n title=\"Move down\"\n [attr.aria-label]=\"'Move down ' + widget.title\">\n </button>\n @if (supportsConfiguration(widget)) {\n <button\n kendoButton\n [svgIcon]=\"linkIcon\"\n (click)=\"openWidgetConfig(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"config-widget-btn\"\n [class.needs-config]=\"isWidgetUnconfigured(widget)\"\n [title]=\"isWidgetUnconfigured(widget) ? 'Configure data source (required)' : 'Configure data source'\"\n [attr.aria-label]=\"(isWidgetUnconfigured(widget) ? 'Configure data source (required) ' : 'Configure data source ') + widget.title\">\n </button>\n }\n <button\n kendoButton\n [svgIcon]=\"gearIcon\"\n (click)=\"openEditWidgetDialog(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"edit-widget-btn\"\n title=\"Edit widget\"\n [attr.aria-label]=\"'Edit widget ' + widget.title\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"trashIcon\"\n (click)=\"removeWidget(widget.id)\"\n fillMode=\"flat\"\n themeColor=\"error\"\n size=\"small\"\n class=\"remove-widget-btn\"\n title=\"Delete widget\"\n [attr.aria-label]=\"'Delete widget ' + widget.title\">\n </button>\n </div>\n </div>\n }\n @if (getWidgetComponentType(widget.type); as componentType) {\n <ng-container *ngComponentOutlet=\"componentType; inputs: { config: widget }\"></ng-container>\n }\n </div>\n }\n\n <!-- MeshBoard Grid -->\n @if (isEditMode()) {\n <div class=\"zone-label grid-zone-label\">Grid Zone</div>\n }\n @if (hasGridWidgets()) {\n <kendo-tilelayout\n [columns]=\"displayColumns()\"\n [autoFlow]=\"displayAutoFlow()\"\n [rowHeight]=\"config().rowHeight\"\n [gap]=\"config().gap\"\n [class.has-banners]=\"bannerWidgets().length > 0 && !isEditMode()\"\n [resizable]=\"canEditLayout()\"\n [reorderable]=\"canEditLayout()\"\n (reorder)=\"onReorder($event)\"\n (resize)=\"onResize($event)\">\n @for (placement of displayGridWidgets(); track placement.widget.id) {\n @let widget = placement.widget;\n <kendo-tilelayout-item\n [col]=\"$any(placement.col)\"\n [row]=\"$any(placement.row)\"\n [colSpan]=\"placement.colSpan\"\n [rowSpan]=\"placement.rowSpan\">\n <kendo-tilelayout-item-header>\n <div class=\"widget-header\"\n [class.unconfigured]=\"isWidgetUnconfigured(widget)\"\n [class.chromeless-header]=\"widget.chromeless && !isEditMode()\">\n @if (!(widget.chromeless && !isEditMode())) {\n <span class=\"widget-title\">{{ widget.title }}</span>\n @if (isWidgetUnconfigured(widget)) {\n <span class=\"unconfigured-badge\" title=\"Widget needs configuration\">!</span>\n }\n }\n @if (isEditMode()) {\n <div class=\"widget-actions\" (pointerdown)=\"$event.stopPropagation()\">\n @if (supportsConfiguration(widget)) {\n <button\n kendoButton\n [svgIcon]=\"linkIcon\"\n (click)=\"openWidgetConfig(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"config-widget-btn\"\n [class.needs-config]=\"isWidgetUnconfigured(widget)\"\n [title]=\"isWidgetUnconfigured(widget) ? 'Configure data source (required)' : 'Configure data source'\"\n [attr.aria-label]=\"(isWidgetUnconfigured(widget) ? 'Configure data source (required) ' : 'Configure data source ') + widget.title\">\n </button>\n }\n <button\n kendoButton\n [svgIcon]=\"gearIcon\"\n (click)=\"openEditWidgetDialog(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"edit-widget-btn\"\n title=\"Edit position\"\n [attr.aria-label]=\"'Edit position ' + widget.title\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"copyIcon\"\n (click)=\"duplicateWidget(widget)\"\n fillMode=\"flat\"\n size=\"small\"\n class=\"duplicate-widget-btn\"\n title=\"Duplicate widget\"\n [attr.aria-label]=\"'Duplicate widget ' + widget.title\">\n </button>\n <button\n kendoButton\n [svgIcon]=\"trashIcon\"\n (click)=\"removeWidget(widget.id)\"\n fillMode=\"flat\"\n themeColor=\"error\"\n size=\"small\"\n class=\"remove-widget-btn\"\n title=\"Delete widget\"\n [attr.aria-label]=\"'Delete widget ' + widget.title\">\n </button>\n </div>\n }\n </div>\n </kendo-tilelayout-item-header>\n <kendo-tilelayout-item-body>\n @if (getWidgetComponentType(widget.type); as componentType) {\n <ng-container *ngComponentOutlet=\"componentType; inputs: { config: widget }\"></ng-container>\n } @else {\n <div class=\"widget-error\">\n <p>Unknown widget type: {{ widget.type }}</p>\n </div>\n }\n </kendo-tilelayout-item-body>\n </kendo-tilelayout-item>\n }\n </kendo-tilelayout>\n } @else {\n <mm-meshboard-empty-state [readonly]=\"isReadonly()\" (addWidget)=\"openAddWidget()\"></mm-meshboard-empty-state>\n }\n\n <!-- Edit Widget Dialog -->\n @if (showEditWidgetDialog && editingWidget) {\n <mm-edit-widget-dialog\n [widget]=\"editingWidget\"\n [widgets]=\"config().widgets\"\n [maxColumns]=\"config().columns\"\n [gridService]=\"gridService\"\n (save)=\"onEditWidgetSave($event)\"\n (cancelled)=\"closeEditWidgetDialog()\">\n </mm-edit-widget-dialog>\n }\n }\n</div>\n", styles: [":host{display:flex;flex-direction:column;flex:1;min-height:0;height:100%;width:100%}.meshboard-view{display:flex;flex-direction:column;flex:1;min-height:0;width:100%;background-color:var(--kendo-color-surface, #f5f5f5)}.loading-container{display:flex;align-items:center;justify-content:center;height:100%;width:100%}.entity-selector-hint{display:flex;align-items:center;gap:8px;padding:10px 16px;margin-bottom:8px;background:color-mix(in srgb,var(--kendo-color-info, #2196f3) 10%,var(--kendo-color-surface-alt, #fafafa));border:1px solid color-mix(in srgb,var(--kendo-color-info, #2196f3) 40%,transparent);border-radius:4px;font-size:13px;color:var(--kendo-color-on-app-surface, #424242)}.entity-selector-hint strong{color:var(--kendo-color-info, #2196f3)}.variable-resolution-errors{display:flex;flex-wrap:wrap;align-items:center;gap:8px;padding:8px 16px;margin-bottom:8px;background:color-mix(in srgb,var(--kendo-color-error, #f44336) 10%,var(--kendo-color-surface-alt, #fafafa));border:1px solid var(--kendo-color-error, #f44336);border-radius:4px;font-size:13px;color:var(--kendo-color-on-app-surface, #424242)}.variable-resolution-errors strong{color:var(--kendo-color-error, #f44336)}.variable-resolution-errors .error-item{padding:2px 8px;background:color-mix(in srgb,var(--kendo-color-error, #f44336) 8%,transparent);border-radius:3px;font-family:var(--theme-font-mono);font-size:12px}.scope-resolution-errors .error-item{font-family:inherit}.model-unavailable{display:flex;align-items:center;justify-content:center;height:100%;width:100%;padding:2rem;overflow:hidden;box-sizing:border-box}.model-unavailable .message-card{text-align:center;max-width:500px;padding:2rem;background:var(--kendo-color-surface-alt, #fafafa);border:1px solid var(--kendo-color-border, #e0e0e0);border-radius:8px;overflow:hidden}.model-unavailable .message-card kendo-svg-icon{color:var(--kendo-color-warning, #ff9800);margin-bottom:1rem}.model-unavailable .message-card h2{margin:1rem 0;color:var(--kendo-color-on-app-surface, #424242)}.model-unavailable .message-card p{margin:.5rem 0;color:var(--kendo-color-subtle, #757575);line-height:1.5}.model-unavailable .message-card ol{text-align:left;margin:1rem 0;padding-left:1.5rem;color:var(--kendo-color-subtle, #757575);line-height:1.8}.model-unavailable .message-card ol li{margin:.25rem 0}.model-unavailable .message-card ol a{color:var(--kendo-color-primary, #3f51b5);text-decoration:none}.model-unavailable .message-card ol a:hover{text-decoration:underline}.meshboard-toolbar{display:flex;justify-content:space-between;align-items:flex-end;padding:1rem 1.5rem;background-color:var(--kendo-color-surface-alt, white);border-bottom:1px solid var(--kendo-color-border, #e0e0e0);gap:1rem;flex-shrink:0}.meshboard-toolbar .toolbar-left{display:flex;align-items:baseline;gap:1rem;flex:1;min-width:0;align-self:center}.meshboard-toolbar .toolbar-left .meshboard-title{margin:0;font-size:1.5rem;font-weight:500;color:var(--kendo-color-on-app-surface, #212121);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.meshboard-toolbar .toolbar-left .meshboard-description{color:var(--kendo-color-subtle, #757575);font-size:.875rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.meshboard-toolbar .toolbar-center{display:flex;justify-content:center;align-items:flex-end;flex-shrink:0}.meshboard-toolbar .toolbar-center mm-time-range-picker ::ng-deep .time-range-picker{gap:.5rem}.meshboard-toolbar .toolbar-separator{width:1px;align-self:stretch;background-color:var(--kendo-color-border, #e0e0e0);flex-shrink:0}.meshboard-toolbar .toolbar-entity-selectors{display:flex;align-items:flex-end;flex-shrink:0}.meshboard-toolbar .toolbar-right{display:flex;gap:.5rem;align-items:center;flex-shrink:0}.meshboard-toolbar.compact{justify-content:flex-end;align-items:center;padding:.25rem 1.5rem 0;background-color:transparent;border-bottom:none}kendo-tilelayout{padding:1.5rem;overflow:auto;flex:1;min-height:0}.widget-header{display:flex;justify-content:space-between;align-items:center;width:100%;gap:.5rem}.widget-header.unconfigured{background-color:#ff98001a}.widget-header .widget-title{flex:1;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.widget-header .unconfigured-badge{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:50%;background-color:var(--kendo-color-warning, #ff9800);color:#fff;font-size:12px;font-weight:700;flex-shrink:0}.widget-header .widget-actions{display:flex;align-items:center;gap:.25rem;flex-shrink:0}.widget-header .config-widget-btn,.widget-header .edit-widget-btn,.widget-header .remove-widget-btn{opacity:.6;transition:opacity .2s}.widget-header .config-widget-btn:hover,.widget-header .edit-widget-btn:hover,.widget-header .remove-widget-btn:hover{opacity:1}.widget-header .config-widget-btn.needs-config{color:var(--kendo-color-warning, #ff9800);opacity:1}.widget-header .remove-widget-btn{color:var(--kendo-color-error, #f44336)}.widget-error{display:flex;align-items:center;justify-content:center;height:100%;padding:1rem;color:var(--kendo-color-error, #f44336);text-align:center}.meshboard-view.edit-mode kendo-tilelayout-item:hover{box-shadow:0 0 0 2px var(--kendo-color-primary, #3f51b5)}@media(max-width:768px){.meshboard-toolbar{flex-direction:column;align-items:stretch;gap:.75rem}.meshboard-toolbar .toolbar-left{flex-direction:column;align-items:flex-start;gap:.25rem}.meshboard-toolbar .toolbar-right{flex-wrap:wrap;justify-content:flex-end}.meshboard-toolbar.compact{align-items:flex-end;padding:.25rem 1rem 0}kendo-tilelayout{padding:1rem}}:host ::ng-deep kendo-tilelayout.has-banners{padding-top:0!important}:host ::ng-deep kendo-tilelayout{padding-bottom:1.5rem!important}.widget-header.chromeless-header{display:none}:host ::ng-deep .k-tilelayout-item-header:has(.chromeless-header){display:none}:host ::ng-deep kendo-tilelayout-item:has(.chromeless-header).k-tilelayout-item,:host ::ng-deep kendo-tilelayout-item:has(.chromeless-header).k-card{border:none;box-shadow:none;background:transparent}:host ::ng-deep kendo-tilelayout-item:has(kendo-dialog),:host ::ng-deep kendo-tilelayout-item:has(kendo-window){z-index:9999}.zone-label{font-size:.65rem;opacity:.4;padding:6px 1rem 2px;font-weight:600}.banner-zone-item{margin:0;padding:8px 1rem}.banner-zone-item.banner-edit{border:1px dashed var(--kendo-color-border, rgba(255, 255, 255, .2));border-radius:4px;margin:0 1rem 12px;padding:4px 0}.banner-edit-header{display:flex;align-items:center;justify-content:space-between;padding:4px 8px;opacity:.7}.banner-edit-header .widget-title{font-size:.75rem}.banner-edit-header .widget-actions{display:flex;gap:2px}.meshboard-view.chrome-plain{background:transparent!important}.meshboard-view.chrome-plain .meshboard-toolbar{padding:0 0 var(--theme-space-3, 12px);background:transparent!important;border-bottom:none!important}.meshboard-view.chrome-plain kendo-tilelayout{padding:0!important}.meshboard-view.chrome-plain .banner-zone-item:not(.banner-edit){padding-left:0;padding-right:0}\n"] }]
|
|
36189
37809
|
}], ctorParameters: () => [], propDecorators: { tileLayout: [{
|
|
36190
37810
|
type: ViewChild,
|
|
36191
37811
|
args: [TileLayoutComponent]
|
|
36192
|
-
}], headerMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "headerMode", required: false }] }], chrome: [{ type: i0.Input, args: [{ isSignal: true, alias: "chrome", required: false }] }] } });
|
|
37812
|
+
}], headerMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "headerMode", required: false }] }], chrome: [{ type: i0.Input, args: [{ isSignal: true, alias: "chrome", required: false }] }], timeRange: [{ type: i0.Input, args: [{ isSignal: true, alias: "timeRange", required: false }] }], timeFilterControl: [{ type: i0.Input, args: [{ isSignal: true, alias: "timeFilterControl", required: false }] }] } });
|
|
36193
37813
|
|
|
36194
37814
|
/** English defaults — today's texts. Complete: every member (optional ones too) has a value. */
|
|
36195
37815
|
const DEFAULT_COCKPIT_WIDGET_MESSAGES = {
|
|
@@ -39635,5 +41255,5 @@ function provideCockpitWidgets() {
|
|
|
39635
41255
|
* Generated bundle index. Do not edit.
|
|
39636
41256
|
*/
|
|
39637
41257
|
|
|
39638
|
-
export { ADAPTER_OFFLINE_GRACE_MS, ATTENTION_SEVERITY_ORDER, AdaptersAttentionProvider, AddWidgetDialogComponent, AiInsightsConfigDialogComponent, AiInsightsService, AiInsightsWidgetComponent, AlertBannerConfigDialogComponent, AlertBannerWidgetComponent, AlertListConfigDialogComponent, AlertListWidgetComponent, AssociationsConfigDialogComponent, AttentionListConfigDialogComponent, AttentionListWidgetComponent, AutoRefreshTimerService, BUILT_IN_ATTENTION_PROVIDERS, BarChartConfigDialogComponent, BarChartWidgetComponent, BlueprintUpdatesAttentionProvider, COCKPIT_ADAPTER_LIMIT, COCKPIT_ATTENTION_PROVIDERS, COCKPIT_BLUEPRINT_CATALOG_LIMIT, COCKPIT_CHILD_TENANT_IDS, COCKPIT_DATA_FLOW_LIMIT, COCKPIT_EXPLAIN_HANDLER, COCKPIT_FAILED_EXECUTIONS_OPTIONS, COCKPIT_KPI_GATES, COCKPIT_KPI_SLOW_LOADING_MS, COCKPIT_LINK_RESOLVER, COCKPIT_RECENT_ITEMS, COCKPIT_ROLES, COCKPIT_SERVICE_NAMES, COCKPIT_VERSION_SOURCE, COCKPIT_VIEWER_ACCESS, COCKPIT_WIDGET_MESSAGES, COCKPIT_WIDGET_TYPES, CkModelsResolveFailedAttentionProvider, CockpitAdapterStatesService, CockpitAttentionService, CockpitBlueprintStatusService, CockpitChildTenantsService, CockpitCkModelStatesService, CockpitContextService, CockpitDataFlowExecutionsService, CockpitKpiConfigDialogComponent, CockpitKpiService, CockpitKpiWidgetComponent, CockpitServiceHealthService, DAY_MS, DEFAULT_ATTENTION_MAX_ITEMS, DEFAULT_COCKPIT_WIDGET_MESSAGES, DEFAULT_FAILED_EXECUTIONS_OPTIONS, DEFAULT_MESHBOARD_MESSAGES, DEFAULT_RECENT_ITEMS_MAX, DEFAULT_TIME_ZONE_MODE, DashboardDataService, DashboardGridService, EditModeStateService, EditWidgetDialogComponent, EntityAssociationsWidgetComponent, EntityCardConfigDialogComponent, EntityCardWidgetComponent, EntityDetailDialogComponent, EntitySelectorEditorComponent, EntitySelectorToolbarComponent, FALLBACK_SERIES_COLORS, FailedExecutionsAttentionProvider, FeaturesNotInstalledAttentionProvider, GaugeConfigDialogComponent, GaugeWidgetComponent, HISTOGRAM_HOURS, HOUR_MS, HeatmapConfigDialogComponent, HeatmapWidgetComponent, KPI_ERROR_TEXT, KpiConfigDialogComponent, KpiWidgetComponent, LazyMarkdownComponent, LineChartConfigDialogComponent, LineChartWidgetComponent, MAX_RECENT_ITEMS, MESHBOARD_CHROME_ROUTE_DATA, MESHBOARD_DESCRIPTION_MARKER, MESHBOARD_EDIT_QUERY_PARAM, MESHBOARD_HEADER_MODE_ROUTE_DATA, MESHBOARD_LABEL_RESOLVER, MESHBOARD_MESSAGES, MESHBOARD_OPTIONS, MESHBOARD_TENANT_ID_PROVIDER, MarkdownConfigDialogComponent, MarkdownWidgetComponent, MeshBoardDataService, MeshBoardGridService, MeshBoardManagerDialogComponent, MeshBoardPersistenceService, MeshBoardSettingsDialogComponent, MeshBoardSettingsResult, MeshBoardStateService, MeshBoardViewComponent, NOT_AVAILABLE_TEXT, PieChartConfigDialogComponent, PieChartWidgetComponent, QueryExecutorService, QuerySelectorComponent, RecentItemsConfigDialogComponent, RecentItemsWidgetComponent, RuntimeEntitySelectorComponent, SERVICE_MANAGED_BLUEPRINT_PREFIX, STATE_STATUS_BY_KEY, ServiceHealthConfigDialogComponent, ServiceHealthWidgetComponent, ServicesHealthAttentionProvider, StatsGridConfigDialogComponent, StatsGridWidgetComponent, StatusIndicatorConfigDialogComponent, StatusIndicatorWidgetComponent, StatusListConfigDialogComponent, StatusListWidgetComponent, SummaryCardConfigDialogComponent, SummaryCardWidgetComponent, TableConfigDialogComponent, TableWidgetComponent, TableWidgetDataSourceDirective, UnregisteredPoolsAttentionProvider, WidgetFactoryService, WidgetGroupComponent, WidgetGroupConfigDialogComponent, WidgetNotConfiguredComponent, WidgetRegistryService, adapterKpi, adaptersInError, adaptersOffline, blueprintUpdatesKpi, buildHourlyHistogram, categoryStatus, chartSeriesColors, ckModelKpi, classifyQuery, compareBlueprintVersions, compareMeshBoardNavigation, countFlowExecutions, countLabel, countPipelineExecutions, emptyQueryExecutionResult, enabledButNotInstalled, executionKpi, failedExecutionsSeverity, formatCockpitMessage, formatCount, humanizeCategory, isAdapterAtRest, isAdapterExpectedToRun, isAdapterOnline, isCockpitWidgetType, isMeshBoardChrome, isMeshBoardHeaderMode, isServiceConfigured, joinEncodedDescription, kpiKindOf, latestStatisticsUpdate, legacyCockpitLinkTarget, loadNgxMarkdown, nameList, newMeshBoardConfig, normalizeNavigation, observeThemeChanges, pipelineExecutionInputs, plural, provideCockpitWidgetHost, provideCockpitWidgets, provideDefaultWidgets, provideMeshBoard, provideMeshBoardMessages, provideProcessWidget, provideWidgetRegistrations, queryFamily, readMeshBoardNavigation, recentRelativeTime, registerCockpitWidgets, registerDefaultWidgets, registerProcessWidget, resolveChartColor, resolveCockpitWidgetMessages, resolveMeshBoardChrome, resolveMeshBoardHeaderMode, resolveMeshBoardLabel, resolveMeshBoardMessages, selectProviders, serviceStateText, servicesHealthKpi, sortAttentionFindings, sparklineGeometry, splitBlueprintId, splitEncodedDescription, statusColor, summarizeAdapterOnline, tenantCountKpi, toBlueprintStatus, toServiceHealthStatus, truncationNote, versionKpi, withMeshBoardNavigation };
|
|
41258
|
+
export { ADAPTER_OFFLINE_GRACE_MS, ASSOCIATION_HOP_SOURCE_BATCH, ASSOCIATION_HOP_TARGETS_PER_SOURCE, ATTENTION_SEVERITY_ORDER, AdaptersAttentionProvider, AddWidgetDialogComponent, AiInsightsConfigDialogComponent, AiInsightsService, AiInsightsWidgetComponent, AlertBannerConfigDialogComponent, AlertBannerWidgetComponent, AlertListConfigDialogComponent, AlertListWidgetComponent, AssociationsConfigDialogComponent, AttentionListConfigDialogComponent, AttentionListWidgetComponent, AutoRefreshTimerService, BUILT_IN_ATTENTION_PROVIDERS, BarChartConfigDialogComponent, BarChartWidgetComponent, BlueprintUpdatesAttentionProvider, COCKPIT_ADAPTER_LIMIT, COCKPIT_ATTENTION_PROVIDERS, COCKPIT_BLUEPRINT_CATALOG_LIMIT, COCKPIT_CHILD_TENANT_IDS, COCKPIT_DATA_FLOW_LIMIT, COCKPIT_EXPLAIN_HANDLER, COCKPIT_FAILED_EXECUTIONS_OPTIONS, COCKPIT_KPI_GATES, COCKPIT_KPI_SLOW_LOADING_MS, COCKPIT_LINK_RESOLVER, COCKPIT_RECENT_ITEMS, COCKPIT_ROLES, COCKPIT_SERVICE_NAMES, COCKPIT_VERSION_SOURCE, COCKPIT_VIEWER_ACCESS, COCKPIT_WIDGET_MESSAGES, COCKPIT_WIDGET_TYPES, CkModelsResolveFailedAttentionProvider, CockpitAdapterStatesService, CockpitAttentionService, CockpitBlueprintStatusService, CockpitChildTenantsService, CockpitCkModelStatesService, CockpitContextService, CockpitDataFlowExecutionsService, CockpitKpiConfigDialogComponent, CockpitKpiService, CockpitKpiWidgetComponent, CockpitServiceHealthService, DAY_MS, DEFAULT_ATTENTION_MAX_ITEMS, DEFAULT_COCKPIT_WIDGET_MESSAGES, DEFAULT_FAILED_EXECUTIONS_OPTIONS, DEFAULT_MESHBOARD_MESSAGES, DEFAULT_RECENT_ITEMS_MAX, DEFAULT_TIME_ZONE_MODE, DashboardDataService, DashboardGridService, EditModeStateService, EditWidgetDialogComponent, EntityAssociationsWidgetComponent, EntityCardConfigDialogComponent, EntityCardWidgetComponent, EntityDetailDialogComponent, EntitySelectorEditorComponent, EntitySelectorScopePathEditorComponent, EntitySelectorToolbarComponent, FALLBACK_SERIES_COLORS, FailedExecutionsAttentionProvider, FeaturesNotInstalledAttentionProvider, GaugeConfigDialogComponent, GaugeWidgetComponent, HISTOGRAM_HOURS, HOUR_MS, HeatmapConfigDialogComponent, HeatmapWidgetComponent, KPI_ERROR_TEXT, KpiConfigDialogComponent, KpiWidgetComponent, LazyMarkdownComponent, LineChartConfigDialogComponent, LineChartWidgetComponent, MAX_RECENT_ITEMS, MAX_SCOPE_HOPS, MESHBOARD_CHROME_ROUTE_DATA, MESHBOARD_DESCRIPTION_MARKER, MESHBOARD_EDIT_QUERY_PARAM, MESHBOARD_HEADER_MODE_ROUTE_DATA, MESHBOARD_LABEL_RESOLVER, MESHBOARD_LANGUAGE, MESHBOARD_MESSAGES, MESHBOARD_OPTIONS, MESHBOARD_TENANT_ID_PROVIDER, MarkdownConfigDialogComponent, MarkdownWidgetComponent, MeshBoardDataService, MeshBoardGridService, MeshBoardManagerDialogComponent, MeshBoardPersistenceService, MeshBoardSettingsDialogComponent, MeshBoardSettingsResult, MeshBoardStateService, MeshBoardViewComponent, NOT_AVAILABLE_TEXT, PieChartConfigDialogComponent, PieChartWidgetComponent, QueryExecutorService, QuerySelectorComponent, RecentItemsConfigDialogComponent, RecentItemsWidgetComponent, RuntimeEntitySelectorComponent, SERVICE_MANAGED_BLUEPRINT_PREFIX, STATE_STATUS_BY_KEY, ServiceHealthConfigDialogComponent, ServiceHealthWidgetComponent, ServicesHealthAttentionProvider, StatsGridConfigDialogComponent, StatsGridWidgetComponent, StatusIndicatorConfigDialogComponent, StatusIndicatorWidgetComponent, StatusListConfigDialogComponent, StatusListWidgetComponent, SummaryCardConfigDialogComponent, SummaryCardWidgetComponent, TableConfigDialogComponent, TableWidgetComponent, TableWidgetDataSourceDirective, UnregisteredPoolsAttentionProvider, WidgetFactoryService, WidgetGroupComponent, WidgetGroupConfigDialogComponent, WidgetNotConfiguredComponent, WidgetRegistryService, adapterKpi, adaptersInError, adaptersOffline, blueprintUpdatesKpi, buildHourlyHistogram, categoryStatus, chartSeriesColors, ckModelKpi, classifyQuery, compareBlueprintVersions, compareMeshBoardNavigation, countFlowExecutions, countLabel, countPipelineExecutions, emptyQueryExecutionResult, enabledButNotInstalled, executionKpi, failedExecutionsSeverity, formatCockpitMessage, formatCount, humanizeCategory, isAdapterAtRest, isAdapterExpectedToRun, isAdapterOnline, isCockpitWidgetType, isMeshBoardChrome, isMeshBoardHeaderMode, isServiceConfigured, joinEncodedDescription, kpiKindOf, latestStatisticsUpdate, legacyCockpitLinkTarget, loadNgxMarkdown, nameList, newMeshBoardConfig, normalizeNavigation, observeThemeChanges, parseHostTimeRange, pickLocalizedText, pipelineExecutionInputs, plural, provideCockpitWidgetHost, provideCockpitWidgets, provideDefaultWidgets, provideMeshBoard, provideMeshBoardLanguage, provideMeshBoardMessages, provideProcessWidget, provideWidgetRegistrations, queryFamily, readMeshBoardNavigation, recentRelativeTime, registerCockpitWidgets, registerDefaultWidgets, registerProcessWidget, resolveChartColor, resolveCockpitWidgetMessages, resolveMeshBoardChrome, resolveMeshBoardHeaderMode, resolveMeshBoardLabel, resolveMeshBoardMessages, selectProviders, serviceStateText, servicesHealthKpi, sortAttentionFindings, sparklineGeometry, splitBlueprintId, splitEncodedDescription, statusColor, summarizeAdapterOnline, tenantCountKpi, toBlueprintStatus, toServiceHealthStatus, truncationNote, versionKpi, withMeshBoardNavigation };
|
|
39639
41259
|
//# sourceMappingURL=meshmakers-octo-meshboard.mjs.map
|