@meshmakers/octo-meshboard 3.4.810 → 3.4.880
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.
|
@@ -3195,9 +3195,8 @@ class QueryExecutorService {
|
|
|
3195
3195
|
/**
|
|
3196
3196
|
* Best-effort per-series labels for resolution-aware fan-out (AB#4290): reads each source
|
|
3197
3197
|
* entity's attribute matching `labelField` (canonical, case/underscore-insensitive so an archive
|
|
3198
|
-
* column
|
|
3199
|
-
*
|
|
3200
|
-
* `rtId → label` map.
|
|
3198
|
+
* column name maps to its CK attribute). Entities that fail to load or lack the attribute are
|
|
3199
|
+
* simply omitted; the caller falls back to the rtId. Returns a `rtId → label` map.
|
|
3201
3200
|
*/
|
|
3202
3201
|
async fetchSeriesLabels(ckTypeId, rtIds, labelField) {
|
|
3203
3202
|
const map = new Map();
|
|
@@ -3218,6 +3217,49 @@ class QueryExecutorService {
|
|
|
3218
3217
|
}));
|
|
3219
3218
|
return map;
|
|
3220
3219
|
}
|
|
3220
|
+
/** Cap on the source-entity population read for group-aggregation (AB#4714). A truncated read is
|
|
3221
|
+
* logged, never silent. */
|
|
3222
|
+
static ENTITY_GROUP_FETCH_CAP = 5000;
|
|
3223
|
+
/**
|
|
3224
|
+
* Group-aggregation source resolution (AB#4714): fetches the source entities of `ckTypeId` in a
|
|
3225
|
+
* single bulk read and buckets their rtIds by the value of `groupField` (canonical match — case
|
|
3226
|
+
* and non-alphanumerics are ignored, so an archive column name maps to its CK attribute). Returns
|
|
3227
|
+
* `groupValue → rtIds`. Optionally restricted to `restrictRtIds` (an entity-selector scope) —
|
|
3228
|
+
* entities outside that set are dropped. Entities whose group attribute is missing/null are
|
|
3229
|
+
* omitted (they cannot be attributed to a line). The population is bounded to
|
|
3230
|
+
* {@link ENTITY_GROUP_FETCH_CAP}; a truncated read is logged (never silently capped).
|
|
3231
|
+
*/
|
|
3232
|
+
async fetchEntityGroups(ckTypeId, groupField, restrictRtIds) {
|
|
3233
|
+
const groups = new Map();
|
|
3234
|
+
const canon = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
3235
|
+
const wanted = canon(groupField);
|
|
3236
|
+
const restrict = restrictRtIds && restrictRtIds.length > 0 ? new Set(restrictRtIds) : null;
|
|
3237
|
+
const res = await firstValueFrom(this.entitiesGql.fetch({ variables: { ckTypeId, first: QueryExecutorService.ENTITY_GROUP_FETCH_CAP } }));
|
|
3238
|
+
const connection = res.data?.runtime?.runtimeEntities;
|
|
3239
|
+
const items = connection?.items ?? [];
|
|
3240
|
+
const total = connection?.totalCount ?? items.length;
|
|
3241
|
+
if (total > items.length) {
|
|
3242
|
+
console.warn(`Group-aggregation: ${ckTypeId} has ${total} entities but only ${items.length} were read `
|
|
3243
|
+
+ `(cap ${QueryExecutorService.ENTITY_GROUP_FETCH_CAP}); some series may be incomplete.`);
|
|
3244
|
+
}
|
|
3245
|
+
for (const item of items) {
|
|
3246
|
+
const rtId = item?.rtId != null ? String(item.rtId) : null;
|
|
3247
|
+
if (!rtId || (restrict && !restrict.has(rtId)))
|
|
3248
|
+
continue;
|
|
3249
|
+
const attr = item?.attributes?.items?.find(a => !!a?.attributeName && canon(a.attributeName) === wanted);
|
|
3250
|
+
if (attr?.value == null)
|
|
3251
|
+
continue;
|
|
3252
|
+
const groupValue = String(attr.value);
|
|
3253
|
+
const bucket = groups.get(groupValue);
|
|
3254
|
+
if (bucket) {
|
|
3255
|
+
bucket.push(rtId);
|
|
3256
|
+
}
|
|
3257
|
+
else {
|
|
3258
|
+
groups.set(groupValue, [rtId]);
|
|
3259
|
+
}
|
|
3260
|
+
}
|
|
3261
|
+
return groups;
|
|
3262
|
+
}
|
|
3221
3263
|
buildStreamDataArg(args) {
|
|
3222
3264
|
// Skip the entire `arg` field when the caller has nothing to override —
|
|
3223
3265
|
// the persisted query then runs with its intrinsic from/to/limit and the
|
|
@@ -14811,10 +14853,12 @@ class LineChartWidgetComponent {
|
|
|
14811
14853
|
}
|
|
14812
14854
|
/**
|
|
14813
14855
|
* Resolution-aware routing (AB#4290): resolve the best archive/rollup for the window + target
|
|
14814
|
-
* point count, then downsample
|
|
14815
|
-
*
|
|
14816
|
-
*
|
|
14817
|
-
*
|
|
14856
|
+
* point count, then downsample the source series against it. Two fan-out shapes, both reshaped
|
|
14857
|
+
* onto the widget's value/series fields so the shared {@link processData} pipeline renders them
|
|
14858
|
+
* unchanged:
|
|
14859
|
+
* - default: one call per source rtId → one line per register ({@link loadPerRtIdRows}).
|
|
14860
|
+
* - `aggregateSeriesByGroup` (AB#4714): one call per `seriesGroupField` value with the whole
|
|
14861
|
+
* group's rtIds → one server-side-summed line per group ({@link loadGroupAggregatedRows}).
|
|
14818
14862
|
*/
|
|
14819
14863
|
async loadResolutionAware(ds, from, to, sourceRtIds, fieldFilter) {
|
|
14820
14864
|
const targetPoints = this.computeDownsampleLimit();
|
|
@@ -14834,7 +14878,6 @@ class LineChartWidgetComponent {
|
|
|
14834
14878
|
requiredAggregation: aggregation,
|
|
14835
14879
|
sourcePath,
|
|
14836
14880
|
rtIds: sourceRtIds && sourceRtIds.length > 0 ? sourceRtIds : undefined,
|
|
14837
|
-
obisFilter: ds.obisFilter || undefined,
|
|
14838
14881
|
// Resolve calendar rollups in the board's zone so civil-day buckets match the
|
|
14839
14882
|
// window (computed on the same zone by resolveCurrentTimeRange) — AB#4190.
|
|
14840
14883
|
timeZone: this.stateService.resolveStreamDataTimeZone()
|
|
@@ -14856,31 +14899,52 @@ class LineChartWidgetComponent {
|
|
|
14856
14899
|
qFrom = new Date(alignedFrom);
|
|
14857
14900
|
qTo = new Date(alignedFrom + Math.max(1, resolution.points) * bucket);
|
|
14858
14901
|
}
|
|
14859
|
-
//
|
|
14860
|
-
//
|
|
14861
|
-
//
|
|
14902
|
+
// Group-aggregation mode (AB#4714): one downsampling call PER GROUP, each with the whole
|
|
14903
|
+
// group's rtIds, so the transient downsampling reduces over every member (per
|
|
14904
|
+
// requiredAggregation) into one aggregate line. Default mode: one call per source rtId, keeping
|
|
14905
|
+
// each source its own series. See loadGroupAggregatedRows / loadPerRtIdRows.
|
|
14906
|
+
const downsample = (rtIds, label) => this.queryExecutor.downsampleByArchive({
|
|
14907
|
+
archiveRtId: resolution.archiveRtId,
|
|
14908
|
+
from: qFrom,
|
|
14909
|
+
to: qTo,
|
|
14910
|
+
limit: Math.max(1, resolution.points),
|
|
14911
|
+
sourcePath,
|
|
14912
|
+
aggregation: resolution.reducingFunction,
|
|
14913
|
+
rtIds: rtIds && rtIds.length > 0 ? rtIds : undefined,
|
|
14914
|
+
fieldFilter
|
|
14915
|
+
}).then(seriesRows => seriesRows.map(r => this.reshapeResolutionRow(r, label)));
|
|
14916
|
+
const rows = ds.aggregateSeriesByGroup && info.ckTypeId
|
|
14917
|
+
? await this.loadGroupAggregatedRows(info.ckTypeId, sourceRtIds, downsample)
|
|
14918
|
+
: await this.loadPerRtIdRows(info.ckTypeId, sourceRtIds, downsample);
|
|
14919
|
+
return { rows, signal: resolution, requestedPoints: targetPoints };
|
|
14920
|
+
}
|
|
14921
|
+
/**
|
|
14922
|
+
* Group-aggregation fan-out (AB#4714): buckets the source entities by `seriesGroupField` and runs
|
|
14923
|
+
* one downsampling call per group with the group's rtIds. The transient downsampling reduces over
|
|
14924
|
+
* the group's members server-side, yielding one aggregate line per distinct group value. Falls
|
|
14925
|
+
* back to a single merged series when no group can be resolved.
|
|
14926
|
+
*/
|
|
14927
|
+
async loadGroupAggregatedRows(ckTypeId, sourceRtIds, downsample) {
|
|
14928
|
+
const groups = await this.queryExecutor.fetchEntityGroups(ckTypeId, this.config.seriesGroupField, sourceRtIds);
|
|
14929
|
+
if (groups.size === 0) {
|
|
14930
|
+
// No group attribute resolved → a single merged line over the whole (optionally scoped) set.
|
|
14931
|
+
return downsample(sourceRtIds, this.config.seriesGroupField);
|
|
14932
|
+
}
|
|
14933
|
+
const perGroup = await Promise.all(Array.from(groups.entries()).map(([groupValue, rtIds]) => downsample(rtIds, groupValue)));
|
|
14934
|
+
return perGroup.flat();
|
|
14935
|
+
}
|
|
14936
|
+
/**
|
|
14937
|
+
* Default resolution-aware fan-out (AB#4290): one downsampling call per source rtId so each
|
|
14938
|
+
* register stays its own line, labeled from its `seriesGroupField` attribute. With no source
|
|
14939
|
+
* scope a single merged series is produced (labeled by the series-group field name).
|
|
14940
|
+
*/
|
|
14941
|
+
async loadPerRtIdRows(ckTypeId, sourceRtIds, downsample) {
|
|
14862
14942
|
const seriesRtIds = sourceRtIds && sourceRtIds.length > 0 ? sourceRtIds : [undefined];
|
|
14863
|
-
const labels =
|
|
14864
|
-
? await this.queryExecutor.fetchSeriesLabels(
|
|
14943
|
+
const labels = ckTypeId
|
|
14944
|
+
? await this.queryExecutor.fetchSeriesLabels(ckTypeId, sourceRtIds ?? [], this.config.seriesGroupField)
|
|
14865
14945
|
: new Map();
|
|
14866
|
-
const
|
|
14867
|
-
|
|
14868
|
-
const seriesRows = await this.queryExecutor.downsampleByArchive({
|
|
14869
|
-
archiveRtId: resolution.archiveRtId,
|
|
14870
|
-
from: qFrom,
|
|
14871
|
-
to: qTo,
|
|
14872
|
-
limit: Math.max(1, resolution.points),
|
|
14873
|
-
sourcePath,
|
|
14874
|
-
aggregation: resolution.reducingFunction,
|
|
14875
|
-
rtIds: rtId ? [rtId] : undefined,
|
|
14876
|
-
fieldFilter
|
|
14877
|
-
});
|
|
14878
|
-
const label = rtId ? (labels.get(rtId) ?? rtId) : this.config.seriesGroupField;
|
|
14879
|
-
for (const r of seriesRows) {
|
|
14880
|
-
rows.push(this.reshapeResolutionRow(r, label));
|
|
14881
|
-
}
|
|
14882
|
-
}
|
|
14883
|
-
return { rows, signal: resolution, requestedPoints: targetPoints };
|
|
14946
|
+
const perSeries = await Promise.all(seriesRtIds.map(rtId => downsample(rtId ? [rtId] : undefined, rtId ? (labels.get(rtId) ?? rtId) : this.config.seriesGroupField)));
|
|
14947
|
+
return perSeries.flat();
|
|
14884
14948
|
}
|
|
14885
14949
|
/**
|
|
14886
14950
|
* Reshapes one downsampling bin onto the widget's configured value + series-group fields so the
|
|
@@ -28154,6 +28218,10 @@ function buildDataSourceFromPersisted(data, config) {
|
|
|
28154
28218
|
if (config['resolutionAware'] === true) {
|
|
28155
28219
|
persisted.resolutionAware = true;
|
|
28156
28220
|
}
|
|
28221
|
+
// Group-aggregation (AB#4714) — only meaningful with resolutionAware.
|
|
28222
|
+
if (config['aggregateSeriesByGroup'] === true) {
|
|
28223
|
+
persisted.aggregateSeriesByGroup = true;
|
|
28224
|
+
}
|
|
28157
28225
|
const persistedAggregation = config['requiredAggregation'];
|
|
28158
28226
|
if (persistedAggregation === 'AVG' || persistedAggregation === 'MIN' || persistedAggregation === 'MAX'
|
|
28159
28227
|
|| persistedAggregation === 'SUM' || persistedAggregation === 'COUNT') {
|
|
@@ -29238,6 +29306,9 @@ function registerDefaultWidgets(registry) {
|
|
|
29238
29306
|
};
|
|
29239
29307
|
},
|
|
29240
29308
|
applyConfigResult: (widget, result) => {
|
|
29309
|
+
// The config dialog does not surface aggregateSeriesByGroup; carry it over from the existing
|
|
29310
|
+
// data source so a dialog edit never silently drops the seed-level flag.
|
|
29311
|
+
const existing = widget.dataSource;
|
|
29241
29312
|
const dataSource = {
|
|
29242
29313
|
type: 'persistentQuery',
|
|
29243
29314
|
queryRtId: result.queryRtId,
|
|
@@ -29246,7 +29317,8 @@ function registerDefaultWidgets(registry) {
|
|
|
29246
29317
|
ignoreTimeFilter: result.ignoreTimeFilter,
|
|
29247
29318
|
entitySelectorId: result.entitySelectorId,
|
|
29248
29319
|
resolutionAware: result.resolutionAware,
|
|
29249
|
-
requiredAggregation: result.requiredAggregation
|
|
29320
|
+
requiredAggregation: result.requiredAggregation,
|
|
29321
|
+
aggregateSeriesByGroup: existing.aggregateSeriesByGroup
|
|
29250
29322
|
};
|
|
29251
29323
|
const filters = result.filters?.map(f => ({
|
|
29252
29324
|
attributePath: f.attributePath,
|
|
@@ -29314,6 +29386,9 @@ function registerDefaultWidgets(registry) {
|
|
|
29314
29386
|
...(widget.dataSource.requiredAggregation && {
|
|
29315
29387
|
requiredAggregation: widget.dataSource.requiredAggregation
|
|
29316
29388
|
}),
|
|
29389
|
+
...(widget.dataSource.aggregateSeriesByGroup && {
|
|
29390
|
+
aggregateSeriesByGroup: true
|
|
29391
|
+
}),
|
|
29317
29392
|
filters: widget.filters
|
|
29318
29393
|
}
|
|
29319
29394
|
}),
|