@meshmakers/octo-meshboard 3.4.910 → 3.4.930
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.
package/README.md
CHANGED
|
@@ -72,10 +72,10 @@ To set a Well-Known Name for a MeshBoard:
|
|
|
72
72
|
|
|
73
73
|
The Well-Known Name should be lowercase with hyphens, similar to URL slugs.
|
|
74
74
|
|
|
75
|
-
### URL Sync
|
|
75
|
+
### URL Sync (`meshBoardSyncUrl`)
|
|
76
76
|
|
|
77
|
-
After
|
|
78
|
-
loaded board's rtId into the URL:
|
|
77
|
+
After the initial load and after every post-init board switch (e.g. via the
|
|
78
|
+
manager dialog), the view syncs the loaded board's rtId into the URL:
|
|
79
79
|
|
|
80
80
|
- If the active route has an `:rtId` param, the last URL segment is replaced —
|
|
81
81
|
always enabled, no configuration needed.
|
|
@@ -468,6 +468,8 @@ const GetEntitiesByCkTypeDocumentDto = gql `
|
|
|
468
468
|
rtId
|
|
469
469
|
ckTypeId
|
|
470
470
|
rtWellKnownName
|
|
471
|
+
rtDisplayName
|
|
472
|
+
rtDisplayDescription
|
|
471
473
|
rtCreationDateTime
|
|
472
474
|
rtChangedDateTime
|
|
473
475
|
attributes(resolveEnumValuesToNames: true) {
|
|
@@ -2910,6 +2912,14 @@ const GetStreamDataQueryArchiveDocumentDto = gql `
|
|
|
2910
2912
|
}
|
|
2911
2913
|
}
|
|
2912
2914
|
}
|
|
2915
|
+
runtime {
|
|
2916
|
+
systemSimpleSdQuery(rtId: $rtId) {
|
|
2917
|
+
items {
|
|
2918
|
+
rtId
|
|
2919
|
+
rtIds
|
|
2920
|
+
}
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2913
2923
|
}
|
|
2914
2924
|
`;
|
|
2915
2925
|
class GetStreamDataQueryArchiveDtoGQL extends i1.Query {
|
|
@@ -3158,7 +3168,11 @@ class QueryExecutorService {
|
|
|
3158
3168
|
* Resolution-aware routing (AB#4290): reads a persisted stream-data query's base archive rtId
|
|
3159
3169
|
* and target CK type WITHOUT executing the query (the `rows` sub-connection is not selected), so
|
|
3160
3170
|
* a widget can feed the archive to {@link resolveSeriesQuery} and use the CK type to resolve
|
|
3161
|
-
* per-series labels.
|
|
3171
|
+
* per-series labels. Also reads the query's persisted RtIds pin
|
|
3172
|
+
* (`System/StreamDataQuery.RtIds`) from the runtime entity — the transient downsampling path
|
|
3173
|
+
* bypasses the persisted query execution, so widgets must re-apply the pin themselves
|
|
3174
|
+
* (AB#4818). `rtIds` is null when the query has no pin (or is not a SimpleSdQuery).
|
|
3175
|
+
* Returns null when the query is missing or has no archive.
|
|
3162
3176
|
*/
|
|
3163
3177
|
async fetchQueryArchive(queryRtId) {
|
|
3164
3178
|
const result = await firstValueFrom(this.queryArchiveGql.fetch({ variables: { rtId: queryRtId }, fetchPolicy: 'cache-first' }));
|
|
@@ -3166,7 +3180,12 @@ class QueryExecutorService {
|
|
|
3166
3180
|
if (item?.archiveRtId == null) {
|
|
3167
3181
|
return null;
|
|
3168
3182
|
}
|
|
3169
|
-
|
|
3183
|
+
const pinned = result.data?.runtime?.systemSimpleSdQuery?.items?.[0]?.rtIds;
|
|
3184
|
+
return {
|
|
3185
|
+
archiveRtId: String(item.archiveRtId),
|
|
3186
|
+
ckTypeId: item.associatedCkTypeId != null ? String(item.associatedCkTypeId) : null,
|
|
3187
|
+
rtIds: pinned && pinned.length > 0 ? pinned.map(String) : null
|
|
3188
|
+
};
|
|
3170
3189
|
}
|
|
3171
3190
|
/**
|
|
3172
3191
|
* Resolution-aware routing (AB#4290): runs the AB#4233 downsampling query against an explicit
|
|
@@ -4652,7 +4671,9 @@ class EntityCardConfigDialogComponent {
|
|
|
4652
4671
|
rtId: entity.rtId,
|
|
4653
4672
|
ckTypeId: entity.ckTypeId,
|
|
4654
4673
|
rtWellKnownName: entity.rtWellKnownName ?? undefined,
|
|
4655
|
-
|
|
4674
|
+
rtDisplayName: entity.rtDisplayName,
|
|
4675
|
+
rtDisplayDescription: entity.rtDisplayDescription ?? undefined,
|
|
4676
|
+
displayName: entity.rtDisplayName
|
|
4656
4677
|
};
|
|
4657
4678
|
}
|
|
4658
4679
|
}
|
|
@@ -6181,7 +6202,9 @@ class KpiConfigDialogComponent {
|
|
|
6181
6202
|
rtId: entity.rtId,
|
|
6182
6203
|
ckTypeId: entity.ckTypeId,
|
|
6183
6204
|
rtWellKnownName: entity.rtWellKnownName ?? undefined,
|
|
6184
|
-
|
|
6205
|
+
rtDisplayName: entity.rtDisplayName,
|
|
6206
|
+
rtDisplayDescription: entity.rtDisplayDescription ?? undefined,
|
|
6207
|
+
displayName: entity.rtDisplayName
|
|
6185
6208
|
};
|
|
6186
6209
|
}
|
|
6187
6210
|
}
|
|
@@ -7545,13 +7568,26 @@ const ISO_DATE_TIME_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2})?(\.\d+)?(Z|[+-]
|
|
|
7545
7568
|
function isIsoDateTime(value) {
|
|
7546
7569
|
return typeof value === 'string' && ISO_DATE_TIME_RE.test(value);
|
|
7547
7570
|
}
|
|
7548
|
-
/**
|
|
7571
|
+
/**
|
|
7572
|
+
* ISO-8601 date-time WITHOUT a zone designator (no `Z`, no offset). The wire
|
|
7573
|
+
* serializes UTC instants in this naive form (e.g. `2026-08-16T10:50:00`), but
|
|
7574
|
+
* `new Date(...)` parses such strings as browser-LOCAL time — shifting every
|
|
7575
|
+
* timestamp by the UTC offset before the board's timezone mode is even applied
|
|
7576
|
+
* (AB#4818). {@link toInstant} pins these to UTC instead.
|
|
7577
|
+
*/
|
|
7578
|
+
const ISO_NAIVE_DATE_TIME_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2})?(\.\d+)?$/;
|
|
7579
|
+
/**
|
|
7580
|
+
* Parses a value into a valid `Date`, or returns `null` when it is not a usable
|
|
7581
|
+
* instant. A naive ISO date-time string (no `Z`/offset) is treated as UTC — the
|
|
7582
|
+
* wire's serialization of an instant — never as browser-local time (AB#4818).
|
|
7583
|
+
*/
|
|
7549
7584
|
function toInstant(value) {
|
|
7550
7585
|
if (value instanceof Date) {
|
|
7551
7586
|
return isNaN(value.getTime()) ? null : value;
|
|
7552
7587
|
}
|
|
7553
7588
|
if (typeof value === 'string' || typeof value === 'number') {
|
|
7554
|
-
const
|
|
7589
|
+
const normalized = typeof value === 'string' && ISO_NAIVE_DATE_TIME_RE.test(value) ? `${value}Z` : value;
|
|
7590
|
+
const date = new Date(normalized);
|
|
7555
7591
|
return isNaN(date.getTime()) ? null : date;
|
|
7556
7592
|
}
|
|
7557
7593
|
return null;
|
|
@@ -10959,7 +10995,9 @@ class GaugeConfigDialogComponent {
|
|
|
10959
10995
|
rtId: entity.rtId,
|
|
10960
10996
|
ckTypeId: entity.ckTypeId,
|
|
10961
10997
|
rtWellKnownName: entity.rtWellKnownName ?? undefined,
|
|
10962
|
-
|
|
10998
|
+
rtDisplayName: entity.rtDisplayName,
|
|
10999
|
+
rtDisplayDescription: entity.rtDisplayDescription ?? undefined,
|
|
11000
|
+
displayName: entity.rtDisplayName
|
|
10963
11001
|
};
|
|
10964
11002
|
}
|
|
10965
11003
|
}
|
|
@@ -14866,6 +14904,11 @@ class LineChartWidgetComponent {
|
|
|
14866
14904
|
if (!info) {
|
|
14867
14905
|
throw new Error('Resolution-aware line chart: base archive not found for the selected query.');
|
|
14868
14906
|
}
|
|
14907
|
+
// Source scope (AB#4818): an active entity-selector binding overrides; otherwise the persisted
|
|
14908
|
+
// query's RtIds pin applies — the transient downsampling bypasses the persisted query execution,
|
|
14909
|
+
// so without re-applying the pin a pinned widget would silently aggregate every entity captured
|
|
14910
|
+
// by the archive.
|
|
14911
|
+
const scopeRtIds = sourceRtIds && sourceRtIds.length > 0 ? sourceRtIds : (info.rtIds ?? undefined);
|
|
14869
14912
|
// The Y-axis value field is the column being reduced, so it doubles as the resolver's
|
|
14870
14913
|
// source path (matched case-insensitively against the rollup's aggregation spec).
|
|
14871
14914
|
const sourcePath = this.config.valueField;
|
|
@@ -14877,7 +14920,7 @@ class LineChartWidgetComponent {
|
|
|
14877
14920
|
targetPoints,
|
|
14878
14921
|
requiredAggregation: aggregation,
|
|
14879
14922
|
sourcePath,
|
|
14880
|
-
rtIds:
|
|
14923
|
+
rtIds: scopeRtIds,
|
|
14881
14924
|
// Resolve calendar rollups in the board's zone so civil-day buckets match the
|
|
14882
14925
|
// window (computed on the same zone by resolveCurrentTimeRange) — AB#4190.
|
|
14883
14926
|
timeZone: this.stateService.resolveStreamDataTimeZone()
|
|
@@ -14914,8 +14957,8 @@ class LineChartWidgetComponent {
|
|
|
14914
14957
|
fieldFilter
|
|
14915
14958
|
}).then(seriesRows => seriesRows.map(r => this.reshapeResolutionRow(r, label)));
|
|
14916
14959
|
const rows = ds.aggregateSeriesByGroup && info.ckTypeId
|
|
14917
|
-
? await this.loadGroupAggregatedRows(info.ckTypeId,
|
|
14918
|
-
: await this.loadPerRtIdRows(info.ckTypeId,
|
|
14960
|
+
? await this.loadGroupAggregatedRows(info.ckTypeId, scopeRtIds, downsample)
|
|
14961
|
+
: await this.loadPerRtIdRows(info.ckTypeId, scopeRtIds, downsample);
|
|
14919
14962
|
return { rows, signal: resolution, requestedPoints: targetPoints };
|
|
14920
14963
|
}
|
|
14921
14964
|
/**
|
|
@@ -15044,9 +15087,11 @@ class LineChartWidgetComponent {
|
|
|
15044
15087
|
}
|
|
15045
15088
|
}
|
|
15046
15089
|
if (categoryValue && seriesGroupValue) {
|
|
15047
|
-
// Parse date for sorting
|
|
15090
|
+
// Parse date for sorting. toInstant treats a naive wire timestamp (no zone designator)
|
|
15091
|
+
// as UTC — a bare `new Date(...)` would reinterpret it as browser-local time and shift
|
|
15092
|
+
// the axis by the UTC offset (AB#4818).
|
|
15048
15093
|
if (!allCategories.has(categoryValue)) {
|
|
15049
|
-
allCategories.set(categoryValue, new Date(
|
|
15094
|
+
allCategories.set(categoryValue, toInstant(categoryValue) ?? new Date(NaN));
|
|
15050
15095
|
}
|
|
15051
15096
|
allSeriesGroups.add(seriesGroupValue);
|
|
15052
15097
|
if (!dataMap.has(categoryValue)) {
|
|
@@ -16649,13 +16694,11 @@ class HeatmapWidgetComponent {
|
|
|
16649
16694
|
return values.length;
|
|
16650
16695
|
}
|
|
16651
16696
|
}
|
|
16697
|
+
/** Delegates to {@link toInstant} so naive wire timestamps are pinned to UTC (AB#4818). */
|
|
16652
16698
|
parseDate(value) {
|
|
16653
16699
|
if (!value)
|
|
16654
16700
|
return null;
|
|
16655
|
-
|
|
16656
|
-
return value;
|
|
16657
|
-
const date = new Date(String(value));
|
|
16658
|
-
return isNaN(date.getTime()) ? null : date;
|
|
16701
|
+
return toInstant(value);
|
|
16659
16702
|
}
|
|
16660
16703
|
convertFiltersToDto(filters) {
|
|
16661
16704
|
const variables = this.stateService.getVariables();
|
|
@@ -30865,6 +30908,120 @@ function buildUrlWithRtId(input) {
|
|
|
30865
30908
|
}
|
|
30866
30909
|
return null;
|
|
30867
30910
|
}
|
|
30911
|
+
/**
|
|
30912
|
+
* Returns the URL to navigate to after the *initial* board load, or `null`
|
|
30913
|
+
* when the URL must not be rewritten.
|
|
30914
|
+
*
|
|
30915
|
+
* The constructor effect skips URL sync during the initial load (it would race
|
|
30916
|
+
* with the transient board `loadInitialMeshBoard` puts up first), so the view
|
|
30917
|
+
* syncs once after loading settles. Nothing to do when no board loaded or the
|
|
30918
|
+
* URL already carried the rtId; otherwise the `buildUrlWithRtId` rules apply.
|
|
30919
|
+
*/
|
|
30920
|
+
function buildInitialUrlWithRtId(input) {
|
|
30921
|
+
const { loadedRtId, rtIdFromRoute, ...rest } = input;
|
|
30922
|
+
if (!loadedRtId || rtIdFromRoute) {
|
|
30923
|
+
return null;
|
|
30924
|
+
}
|
|
30925
|
+
return buildUrlWithRtId({ ...rest, rtId: loadedRtId });
|
|
30926
|
+
}
|
|
30927
|
+
|
|
30928
|
+
/**
|
|
30929
|
+
* Pure helpers for representing the active time-filter selection in the URL.
|
|
30930
|
+
*
|
|
30931
|
+
* The URL is written from two sides — the board-switch rtId sync and the
|
|
30932
|
+
* time-range picker — and two concurrent Router navigations supersede each
|
|
30933
|
+
* other (the later one wins, computed from the pre-navigation route, silently
|
|
30934
|
+
* dropping the rtId path change). Both writers therefore serialize the tf_*
|
|
30935
|
+
* params through these helpers onto one target URL string and navigate once.
|
|
30936
|
+
*/
|
|
30937
|
+
/** All tf_* query params; params not used by the selection type are null (= removed). */
|
|
30938
|
+
function timeFilterQueryParams(selection) {
|
|
30939
|
+
const params = {
|
|
30940
|
+
tf_type: selection.type,
|
|
30941
|
+
tf_year: null,
|
|
30942
|
+
tf_quarter: null,
|
|
30943
|
+
tf_month: null,
|
|
30944
|
+
tf_day: null,
|
|
30945
|
+
tf_hf: null,
|
|
30946
|
+
tf_ht: null,
|
|
30947
|
+
tf_rv: null,
|
|
30948
|
+
tf_ru: null,
|
|
30949
|
+
tf_from: null,
|
|
30950
|
+
tf_to: null
|
|
30951
|
+
};
|
|
30952
|
+
switch (selection.type) {
|
|
30953
|
+
case 'year':
|
|
30954
|
+
if (selection.year != null)
|
|
30955
|
+
params['tf_year'] = selection.year.toString();
|
|
30956
|
+
break;
|
|
30957
|
+
case 'quarter':
|
|
30958
|
+
if (selection.year != null)
|
|
30959
|
+
params['tf_year'] = selection.year.toString();
|
|
30960
|
+
if (selection.quarter != null)
|
|
30961
|
+
params['tf_quarter'] = selection.quarter.toString();
|
|
30962
|
+
break;
|
|
30963
|
+
case 'month':
|
|
30964
|
+
if (selection.year != null)
|
|
30965
|
+
params['tf_year'] = selection.year.toString();
|
|
30966
|
+
if (selection.month != null)
|
|
30967
|
+
params['tf_month'] = selection.month.toString();
|
|
30968
|
+
break;
|
|
30969
|
+
case 'day':
|
|
30970
|
+
if (selection.year != null)
|
|
30971
|
+
params['tf_year'] = selection.year.toString();
|
|
30972
|
+
if (selection.month != null)
|
|
30973
|
+
params['tf_month'] = selection.month.toString();
|
|
30974
|
+
if (selection.day != null)
|
|
30975
|
+
params['tf_day'] = selection.day.toString();
|
|
30976
|
+
if (selection.hourFrom != null)
|
|
30977
|
+
params['tf_hf'] = selection.hourFrom.toString();
|
|
30978
|
+
if (selection.hourTo != null)
|
|
30979
|
+
params['tf_ht'] = selection.hourTo.toString();
|
|
30980
|
+
break;
|
|
30981
|
+
case 'relative':
|
|
30982
|
+
if (selection.relativeValue != null)
|
|
30983
|
+
params['tf_rv'] = selection.relativeValue.toString();
|
|
30984
|
+
if (selection.relativeUnit)
|
|
30985
|
+
params['tf_ru'] = selection.relativeUnit;
|
|
30986
|
+
break;
|
|
30987
|
+
case 'custom':
|
|
30988
|
+
if (selection.customFrom)
|
|
30989
|
+
params['tf_from'] = selection.customFrom;
|
|
30990
|
+
if (selection.customTo)
|
|
30991
|
+
params['tf_to'] = selection.customTo;
|
|
30992
|
+
break;
|
|
30993
|
+
}
|
|
30994
|
+
return params;
|
|
30995
|
+
}
|
|
30996
|
+
/**
|
|
30997
|
+
* Returns `url` with the given query params merged in: string values are set,
|
|
30998
|
+
* null values are removed, params not mentioned are preserved.
|
|
30999
|
+
*/
|
|
31000
|
+
function applyQueryParams(url, params) {
|
|
31001
|
+
const [pathPart, queryPart] = url.split('?');
|
|
31002
|
+
const search = new URLSearchParams(queryPart ?? '');
|
|
31003
|
+
for (const [key, value] of Object.entries(params)) {
|
|
31004
|
+
if (value === null) {
|
|
31005
|
+
search.delete(key);
|
|
31006
|
+
}
|
|
31007
|
+
else {
|
|
31008
|
+
search.set(key, value);
|
|
31009
|
+
}
|
|
31010
|
+
}
|
|
31011
|
+
const query = search.toString();
|
|
31012
|
+
return query ? `${pathPart}?${query}` : pathPart;
|
|
31013
|
+
}
|
|
31014
|
+
/**
|
|
31015
|
+
* Returns `url` with its tf_* query params replaced by the given selection's.
|
|
31016
|
+
* Non-tf params (es_*, …) are preserved. `selection` null/undefined returns
|
|
31017
|
+
* the url unchanged.
|
|
31018
|
+
*/
|
|
31019
|
+
function applyTimeFilterParams(url, selection) {
|
|
31020
|
+
if (!selection) {
|
|
31021
|
+
return url;
|
|
31022
|
+
}
|
|
31023
|
+
return applyQueryParams(url, timeFilterQueryParams(selection));
|
|
31024
|
+
}
|
|
30868
31025
|
|
|
30869
31026
|
/**
|
|
30870
31027
|
* Component for editing MeshBoard variables.
|
|
@@ -32211,9 +32368,14 @@ class MeshBoardViewComponent {
|
|
|
32211
32368
|
if (currentRtId && currentRtId !== this.lastNavigatedRtId) {
|
|
32212
32369
|
this.lastNavigatedRtId = currentRtId;
|
|
32213
32370
|
if (this.initialLoadComplete) {
|
|
32214
|
-
// Only update URL and time filter for post-init board switches (e.g.
|
|
32215
|
-
|
|
32371
|
+
// Only update URL and time filter for post-init board switches (e.g.
|
|
32372
|
+
// manager dialog). Time filter first: updateUrlWithRtId serializes
|
|
32373
|
+
// the then-active selection into the SAME navigation as the rtId
|
|
32374
|
+
// path change — a second concurrent navigation (e.g. the picker's
|
|
32375
|
+
// writeTimeFilterToUrl) would supersede the path change and drop the
|
|
32376
|
+
// rtId from the URL.
|
|
32216
32377
|
this.initializeTimeFilterVariables();
|
|
32378
|
+
this.updateUrlWithRtId(currentRtId);
|
|
32217
32379
|
}
|
|
32218
32380
|
}
|
|
32219
32381
|
});
|
|
@@ -32265,9 +32427,16 @@ class MeshBoardViewComponent {
|
|
|
32265
32427
|
syncUrlOptIn: this.route.snapshot.data['meshBoardSyncUrl'] === true
|
|
32266
32428
|
});
|
|
32267
32429
|
if (targetUrl !== null) {
|
|
32268
|
-
|
|
32430
|
+
// One navigation for path + tf_* params: a separate time-filter
|
|
32431
|
+
// navigation would supersede this one and drop the rtId path change.
|
|
32432
|
+
this.router.navigateByUrl(applyTimeFilterParams(targetUrl, this.activeTimeFilterSelection()), { replaceUrl: true });
|
|
32269
32433
|
}
|
|
32270
32434
|
}
|
|
32435
|
+
/** The active time-filter selection to mirror into the URL, or null when the filter is off. */
|
|
32436
|
+
activeTimeFilterSelection() {
|
|
32437
|
+
const config = this.stateService.getTimeFilterConfig();
|
|
32438
|
+
return config?.enabled ? config.selection ?? null : null;
|
|
32439
|
+
}
|
|
32271
32440
|
async ngOnInit() {
|
|
32272
32441
|
// Track the component's own width for the compact layout tiers. The callback
|
|
32273
32442
|
// fires outside the Angular zone; re-enter it so the signal update schedules
|
|
@@ -32323,6 +32492,21 @@ class MeshBoardViewComponent {
|
|
|
32323
32492
|
await this.initializeEntitySelectors();
|
|
32324
32493
|
// Mark initial load as complete so the effect can handle subsequent board switches
|
|
32325
32494
|
this.initialLoadComplete = true;
|
|
32495
|
+
// The constructor effect skipped the URL sync during the initial load
|
|
32496
|
+
// (and already consumed the rtId via lastNavigatedRtId), so sync once
|
|
32497
|
+
// here, after loading settled — the transient board from
|
|
32498
|
+
// loadInitialMeshBoard never reaches the URL. Stays a no-op without
|
|
32499
|
+
// meshBoardSyncUrl/:rtId (AB#4457) and preserves the query params.
|
|
32500
|
+
const initialUrl = buildInitialUrlWithRtId({
|
|
32501
|
+
currentUrl: this.router.url,
|
|
32502
|
+
loadedRtId: this.stateService.persistedMeshBoardId(),
|
|
32503
|
+
rtIdFromRoute,
|
|
32504
|
+
hasRtIdParam: this.route.snapshot.paramMap.has('rtId'),
|
|
32505
|
+
syncUrlOptIn: this.route.snapshot.data['meshBoardSyncUrl'] === true
|
|
32506
|
+
});
|
|
32507
|
+
if (initialUrl !== null) {
|
|
32508
|
+
this.router.navigateByUrl(applyTimeFilterParams(initialUrl, this.activeTimeFilterSelection()), { replaceUrl: true });
|
|
32509
|
+
}
|
|
32326
32510
|
// Update breadcrumb with MeshBoard name
|
|
32327
32511
|
this.updateBreadcrumb();
|
|
32328
32512
|
// Wire visibility-aware auto-refresh. The config-watching effect already
|
|
@@ -32760,69 +32944,17 @@ class MeshBoardViewComponent {
|
|
|
32760
32944
|
* Only writes params relevant to the current type, clears all others.
|
|
32761
32945
|
*/
|
|
32762
32946
|
writeTimeFilterToUrl(selection) {
|
|
32763
|
-
//
|
|
32764
|
-
|
|
32765
|
-
|
|
32766
|
-
|
|
32767
|
-
|
|
32768
|
-
|
|
32769
|
-
|
|
32770
|
-
|
|
32771
|
-
|
|
32772
|
-
tf_rv: null,
|
|
32773
|
-
tf_ru: null,
|
|
32774
|
-
tf_from: null,
|
|
32775
|
-
tf_to: null
|
|
32776
|
-
};
|
|
32777
|
-
// Set only the params relevant to the current type
|
|
32778
|
-
switch (selection.type) {
|
|
32779
|
-
case 'year':
|
|
32780
|
-
if (selection.year != null)
|
|
32781
|
-
cleanParams['tf_year'] = selection.year.toString();
|
|
32782
|
-
break;
|
|
32783
|
-
case 'quarter':
|
|
32784
|
-
if (selection.year != null)
|
|
32785
|
-
cleanParams['tf_year'] = selection.year.toString();
|
|
32786
|
-
if (selection.quarter != null)
|
|
32787
|
-
cleanParams['tf_quarter'] = selection.quarter.toString();
|
|
32788
|
-
break;
|
|
32789
|
-
case 'month':
|
|
32790
|
-
if (selection.year != null)
|
|
32791
|
-
cleanParams['tf_year'] = selection.year.toString();
|
|
32792
|
-
if (selection.month != null)
|
|
32793
|
-
cleanParams['tf_month'] = selection.month.toString();
|
|
32794
|
-
break;
|
|
32795
|
-
case 'day':
|
|
32796
|
-
if (selection.year != null)
|
|
32797
|
-
cleanParams['tf_year'] = selection.year.toString();
|
|
32798
|
-
if (selection.month != null)
|
|
32799
|
-
cleanParams['tf_month'] = selection.month.toString();
|
|
32800
|
-
if (selection.day != null)
|
|
32801
|
-
cleanParams['tf_day'] = selection.day.toString();
|
|
32802
|
-
if (selection.hourFrom != null)
|
|
32803
|
-
cleanParams['tf_hf'] = selection.hourFrom.toString();
|
|
32804
|
-
if (selection.hourTo != null)
|
|
32805
|
-
cleanParams['tf_ht'] = selection.hourTo.toString();
|
|
32806
|
-
break;
|
|
32807
|
-
case 'relative':
|
|
32808
|
-
if (selection.relativeValue != null)
|
|
32809
|
-
cleanParams['tf_rv'] = selection.relativeValue.toString();
|
|
32810
|
-
if (selection.relativeUnit)
|
|
32811
|
-
cleanParams['tf_ru'] = selection.relativeUnit;
|
|
32812
|
-
break;
|
|
32813
|
-
case 'custom':
|
|
32814
|
-
if (selection.customFrom)
|
|
32815
|
-
cleanParams['tf_from'] = selection.customFrom;
|
|
32816
|
-
if (selection.customTo)
|
|
32817
|
-
cleanParams['tf_to'] = selection.customTo;
|
|
32818
|
-
break;
|
|
32947
|
+
// Serialize onto the current URL string instead of router.navigate([],
|
|
32948
|
+
// {relativeTo}): the relative form is computed from the pre-navigation
|
|
32949
|
+
// route and, when it supersedes an in-flight rtId navigation, restores the
|
|
32950
|
+
// stale path (old/no board rtId) into the URL.
|
|
32951
|
+
const targetUrl = applyTimeFilterParams(this.router.url, selection);
|
|
32952
|
+
if (targetUrl === this.router.url) {
|
|
32953
|
+
// No-op writes must not navigate: the router would still cancel an
|
|
32954
|
+
// in-flight rtId navigation before skipping the identical URL.
|
|
32955
|
+
return;
|
|
32819
32956
|
}
|
|
32820
|
-
this.router.
|
|
32821
|
-
relativeTo: this.route,
|
|
32822
|
-
queryParams: cleanParams,
|
|
32823
|
-
queryParamsHandling: 'merge',
|
|
32824
|
-
replaceUrl: true
|
|
32825
|
-
});
|
|
32957
|
+
this.router.navigateByUrl(targetUrl, { replaceUrl: true });
|
|
32826
32958
|
}
|
|
32827
32959
|
/**
|
|
32828
32960
|
* Reads the time filter selection from URL query parameters.
|
|
@@ -32851,17 +32983,33 @@ class MeshBoardViewComponent {
|
|
|
32851
32983
|
* Compares two TimeRangeSelection objects for equality.
|
|
32852
32984
|
*/
|
|
32853
32985
|
isSelectionEqual(a, b) {
|
|
32854
|
-
|
|
32855
|
-
|
|
32856
|
-
|
|
32857
|
-
|
|
32858
|
-
|
|
32859
|
-
|
|
32860
|
-
|
|
32861
|
-
|
|
32862
|
-
|
|
32863
|
-
|
|
32864
|
-
|
|
32986
|
+
// Compare only the fields the selection type actually uses: the picker
|
|
32987
|
+
// pre-fills the unused fields with current-date defaults (e.g. year/month
|
|
32988
|
+
// on a relative selection) while stored selections omit them. A
|
|
32989
|
+
// field-by-field comparison then reports equal selections as changed and
|
|
32990
|
+
// triggers a spurious URL write that supersedes (and thereby drops) the
|
|
32991
|
+
// board-switch rtId navigation.
|
|
32992
|
+
if (a.type !== b.type) {
|
|
32993
|
+
return false;
|
|
32994
|
+
}
|
|
32995
|
+
const eq = (x, y) => (x ?? null) === (y ?? null);
|
|
32996
|
+
switch (a.type) {
|
|
32997
|
+
case 'year':
|
|
32998
|
+
return eq(a.year, b.year);
|
|
32999
|
+
case 'quarter':
|
|
33000
|
+
return eq(a.year, b.year) && eq(a.quarter, b.quarter);
|
|
33001
|
+
case 'month':
|
|
33002
|
+
return eq(a.year, b.year) && eq(a.month, b.month);
|
|
33003
|
+
case 'day':
|
|
33004
|
+
return eq(a.year, b.year) && eq(a.month, b.month) && eq(a.day, b.day) &&
|
|
33005
|
+
eq(a.hourFrom, b.hourFrom) && eq(a.hourTo, b.hourTo);
|
|
33006
|
+
case 'relative':
|
|
33007
|
+
return eq(a.relativeValue, b.relativeValue) && eq(a.relativeUnit, b.relativeUnit);
|
|
33008
|
+
case 'custom':
|
|
33009
|
+
return eq(a.customFrom, b.customFrom) && eq(a.customTo, b.customTo);
|
|
33010
|
+
default:
|
|
33011
|
+
return false;
|
|
33012
|
+
}
|
|
32865
33013
|
}
|
|
32866
33014
|
/**
|
|
32867
33015
|
* Duplicates a widget with all its settings.
|
|
@@ -33298,12 +33446,13 @@ class MeshBoardViewComponent {
|
|
|
33298
33446
|
for (const selector of selectors) {
|
|
33299
33447
|
params[`es_${selector.id}`] = selector.selectedRtId ?? null;
|
|
33300
33448
|
}
|
|
33301
|
-
|
|
33302
|
-
|
|
33303
|
-
|
|
33304
|
-
|
|
33305
|
-
|
|
33306
|
-
|
|
33449
|
+
// Same single-URL serialization as writeTimeFilterToUrl: no relative
|
|
33450
|
+
// navigation off a possibly stale route, and no no-op navigation that
|
|
33451
|
+
// would cancel an in-flight rtId navigation.
|
|
33452
|
+
const targetUrl = applyQueryParams(this.router.url, params);
|
|
33453
|
+
if (targetUrl !== this.router.url) {
|
|
33454
|
+
this.router.navigateByUrl(targetUrl, { replaceUrl: true });
|
|
33455
|
+
}
|
|
33307
33456
|
}
|
|
33308
33457
|
/**
|
|
33309
33458
|
* Reads entity selector selections from URL query parameters.
|