@meshmakers/octo-meshboard 3.3.1140 → 3.3.1160
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.
|
@@ -2545,6 +2545,7 @@ const ExecuteRuntimeQueryDocumentDto = gql `
|
|
|
2545
2545
|
columns {
|
|
2546
2546
|
attributePath
|
|
2547
2547
|
attributeValueType
|
|
2548
|
+
aggregationType
|
|
2548
2549
|
}
|
|
2549
2550
|
rows(after: $after, first: $first, fieldFilter: $fieldFilter) {
|
|
2550
2551
|
totalCount
|
|
@@ -3941,6 +3942,314 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
3941
3942
|
type: Input
|
|
3942
3943
|
}] } });
|
|
3943
3944
|
|
|
3945
|
+
/**
|
|
3946
|
+
* Utility functions for widget data transformation.
|
|
3947
|
+
* These are pure functions extracted for testability.
|
|
3948
|
+
*/
|
|
3949
|
+
/**
|
|
3950
|
+
* Supported row types for query processing.
|
|
3951
|
+
*/
|
|
3952
|
+
const SUPPORTED_ROW_TYPES = [
|
|
3953
|
+
'RtSimpleQueryRow',
|
|
3954
|
+
'RtAggregationQueryRow',
|
|
3955
|
+
'RtGroupingAggregationQueryRow'
|
|
3956
|
+
];
|
|
3957
|
+
/**
|
|
3958
|
+
* Sanitizes field names for use as object keys.
|
|
3959
|
+
* Replaces dots with underscores so the value can be stored under a single key
|
|
3960
|
+
* without Kendo's dot-path navigation interpreting the dots as nested access.
|
|
3961
|
+
* Used when generating record keys, NOT for path comparison — use
|
|
3962
|
+
* {@link matchesAttributePath} for matching cell paths against widget configs.
|
|
3963
|
+
* @param fieldName The field name to sanitize
|
|
3964
|
+
* @returns Sanitized field name
|
|
3965
|
+
*/
|
|
3966
|
+
function sanitizeFieldName(fieldName) {
|
|
3967
|
+
return fieldName.replace(/\./g, '_');
|
|
3968
|
+
}
|
|
3969
|
+
/**
|
|
3970
|
+
* Strips a trailing aggregation function suffix from a path so the base path can
|
|
3971
|
+
* be compared independently of which aggregation produced it. The engine emits
|
|
3972
|
+
* cell paths like `meterreading_count` / `amountvalue_sum` for RT aggregation,
|
|
3973
|
+
* grouped aggregation, and stream-data variants.
|
|
3974
|
+
*/
|
|
3975
|
+
function stripAggregationFunctionSuffix(path) {
|
|
3976
|
+
return path.replace(/_(?:count|sum|avg|min|max)$/i, '');
|
|
3977
|
+
}
|
|
3978
|
+
/**
|
|
3979
|
+
* Canonical key form: drop dot and underscore separators and lowercase the rest.
|
|
3980
|
+
* Lets us compare a widget config's stored field name (which may be
|
|
3981
|
+
* `meterReading`, `meter_reading`, or `amount_value`) against a cell path
|
|
3982
|
+
* (which may be wire-form `meterreading_count` or `amountvalue_sum`) on a
|
|
3983
|
+
* single common form.
|
|
3984
|
+
*/
|
|
3985
|
+
function toCanonicalAttributeKey(s) {
|
|
3986
|
+
return s.replace(/[._]/g, '').toLowerCase();
|
|
3987
|
+
}
|
|
3988
|
+
/**
|
|
3989
|
+
* Detects whether a path ends with the engine's aggregation function suffix
|
|
3990
|
+
* (`_count`, `_sum`, `_avg`, `_min`, `_max`). Used to decide whether the loose
|
|
3991
|
+
* back-compat fallback in {@link matchesAttributePath} should fire.
|
|
3992
|
+
*/
|
|
3993
|
+
function hasAggregationFunctionSuffix(path) {
|
|
3994
|
+
return /_(?:count|sum|avg|min|max)$/i.test(path);
|
|
3995
|
+
}
|
|
3996
|
+
/**
|
|
3997
|
+
* Returns true when a cell's `attributePath` refers to the same source attribute
|
|
3998
|
+
* as a widget config's stored field name. Handles three input shapes:
|
|
3999
|
+
*
|
|
4000
|
+
* - Simple-query cells use the original CK attribute path (e.g. `meterReading`
|
|
4001
|
+
* or `amount.value`).
|
|
4002
|
+
* - RT aggregation cells use the engine's wire-form key with a function suffix
|
|
4003
|
+
* (e.g. `meterreading_count`, `amountvalue_sum`).
|
|
4004
|
+
* - RT grouping cells and stream-data cells use the same wire-form without a
|
|
4005
|
+
* function suffix (e.g. `operatingstatus`).
|
|
4006
|
+
*
|
|
4007
|
+
* Widget configs typically store `sanitizeFieldName(originalPath)`, e.g.
|
|
4008
|
+
* `meterReading` or `amount_value`. This helper returns true when both refer
|
|
4009
|
+
* to the same source attribute, regardless of which form they're in.
|
|
4010
|
+
*
|
|
4011
|
+
* Exact `sanitizeFieldName` match wins (preserves behavior for simple queries
|
|
4012
|
+
* and stream-data widget configs that were saved with wire-form keys); the
|
|
4013
|
+
* canonical-form fallback unbreaks RT-aggregation / grouping widgets whose
|
|
4014
|
+
* configs were saved before the engine emitted wire-form keys.
|
|
4015
|
+
*/
|
|
4016
|
+
function matchesAttributePath(cellPath, configField) {
|
|
4017
|
+
if (!cellPath || !configField)
|
|
4018
|
+
return false;
|
|
4019
|
+
if (sanitizeFieldName(cellPath) === configField)
|
|
4020
|
+
return true;
|
|
4021
|
+
// Loose fallback for legacy configs saved with the original CK path before the engine
|
|
4022
|
+
// switched to wire-form column emission. Only fires when configField itself does NOT
|
|
4023
|
+
// carry an aggregation suffix — otherwise we would cross-match MIN against MAX cells
|
|
4024
|
+
// (both would canonicalise to the same base path).
|
|
4025
|
+
if (hasAggregationFunctionSuffix(configField))
|
|
4026
|
+
return false;
|
|
4027
|
+
const cellBase = stripAggregationFunctionSuffix(cellPath);
|
|
4028
|
+
return toCanonicalAttributeKey(cellBase) === toCanonicalAttributeKey(configField);
|
|
4029
|
+
}
|
|
4030
|
+
/**
|
|
4031
|
+
* Parses a value to a number.
|
|
4032
|
+
* Returns 0 for NaN or non-numeric values.
|
|
4033
|
+
* @param value The value to parse
|
|
4034
|
+
* @returns Parsed numeric value or 0
|
|
4035
|
+
*/
|
|
4036
|
+
function parseNumericValue(value) {
|
|
4037
|
+
if (typeof value === 'number') {
|
|
4038
|
+
return isNaN(value) ? 0 : value;
|
|
4039
|
+
}
|
|
4040
|
+
const parsed = parseFloat(String(value));
|
|
4041
|
+
return isNaN(parsed) ? 0 : parsed;
|
|
4042
|
+
}
|
|
4043
|
+
/**
|
|
4044
|
+
* Extracts a single value from an aggregation query result.
|
|
4045
|
+
* Used for KPI widgets with 'aggregation' queryMode.
|
|
4046
|
+
* @param queryResult The query result containing rows
|
|
4047
|
+
* @param valueField Optional specific field to extract (uses first cell if not specified)
|
|
4048
|
+
* @returns The extracted numeric value
|
|
4049
|
+
*/
|
|
4050
|
+
function extractAggregationValue(queryResult, valueField) {
|
|
4051
|
+
const rows = queryResult.rows?.items ?? [];
|
|
4052
|
+
// Get the first supported row
|
|
4053
|
+
const firstRow = rows.find(row => row && SUPPORTED_ROW_TYPES.includes(row.__typename ?? ''));
|
|
4054
|
+
if (!firstRow)
|
|
4055
|
+
return 0;
|
|
4056
|
+
const cells = firstRow.cells?.items ?? [];
|
|
4057
|
+
// Find the value field if specified
|
|
4058
|
+
if (valueField) {
|
|
4059
|
+
for (const cell of cells) {
|
|
4060
|
+
if (!cell?.attributePath)
|
|
4061
|
+
continue;
|
|
4062
|
+
if (matchesAttributePath(cell.attributePath, valueField)) {
|
|
4063
|
+
return parseNumericValue(cell.value);
|
|
4064
|
+
}
|
|
4065
|
+
}
|
|
4066
|
+
}
|
|
4067
|
+
// Fallback: return first cell value
|
|
4068
|
+
const firstCell = cells.find(c => c !== null);
|
|
4069
|
+
return firstCell ? parseNumericValue(firstCell.value) : 0;
|
|
4070
|
+
}
|
|
4071
|
+
/**
|
|
4072
|
+
* Extracts a value from a grouped aggregation query result.
|
|
4073
|
+
* Used for KPI widgets with 'groupedAggregation' queryMode.
|
|
4074
|
+
* Finds the row where categoryField matches categoryValue and extracts valueField.
|
|
4075
|
+
* @param queryResult The query result containing rows
|
|
4076
|
+
* @param categoryField The field name to match against
|
|
4077
|
+
* @param categoryValue The value to match
|
|
4078
|
+
* @param valueField The field to extract the value from
|
|
4079
|
+
* @returns The extracted numeric value
|
|
4080
|
+
*/
|
|
4081
|
+
function extractGroupedAggregationValue(queryResult, categoryField, categoryValue, valueField) {
|
|
4082
|
+
if (!categoryField || !categoryValue || !valueField) {
|
|
4083
|
+
return 0;
|
|
4084
|
+
}
|
|
4085
|
+
const rows = queryResult.rows?.items ?? [];
|
|
4086
|
+
// Find the row where category matches
|
|
4087
|
+
for (const row of rows) {
|
|
4088
|
+
if (!row || !SUPPORTED_ROW_TYPES.includes(row.__typename ?? ''))
|
|
4089
|
+
continue;
|
|
4090
|
+
const cells = row.cells?.items ?? [];
|
|
4091
|
+
let categoryMatch = false;
|
|
4092
|
+
let value = 0;
|
|
4093
|
+
for (const cell of cells) {
|
|
4094
|
+
if (!cell?.attributePath)
|
|
4095
|
+
continue;
|
|
4096
|
+
if (matchesAttributePath(cell.attributePath, categoryField) && String(cell.value) === categoryValue) {
|
|
4097
|
+
categoryMatch = true;
|
|
4098
|
+
}
|
|
4099
|
+
if (matchesAttributePath(cell.attributePath, valueField)) {
|
|
4100
|
+
value = parseNumericValue(cell.value);
|
|
4101
|
+
}
|
|
4102
|
+
}
|
|
4103
|
+
if (categoryMatch) {
|
|
4104
|
+
return value;
|
|
4105
|
+
}
|
|
4106
|
+
}
|
|
4107
|
+
return 0;
|
|
4108
|
+
}
|
|
4109
|
+
/**
|
|
4110
|
+
* Processes bar chart data in Static Series Mode.
|
|
4111
|
+
* Each series in config corresponds to a separate numeric field.
|
|
4112
|
+
* @param rows The query rows to process
|
|
4113
|
+
* @param categoryField The field to use for categories
|
|
4114
|
+
* @param seriesConfigs The series configurations
|
|
4115
|
+
* @returns Processed bar chart data
|
|
4116
|
+
*/
|
|
4117
|
+
function processStaticSeriesData(rows, categoryField, seriesConfigs) {
|
|
4118
|
+
const categories = [];
|
|
4119
|
+
const seriesMap = new Map();
|
|
4120
|
+
// Initialize series map from config
|
|
4121
|
+
for (const seriesConfig of seriesConfigs) {
|
|
4122
|
+
seriesMap.set(seriesConfig.field, []);
|
|
4123
|
+
}
|
|
4124
|
+
for (const row of rows) {
|
|
4125
|
+
if (!SUPPORTED_ROW_TYPES.includes(row.__typename ?? ''))
|
|
4126
|
+
continue;
|
|
4127
|
+
const cells = row.cells?.items ?? [];
|
|
4128
|
+
let categoryValue = '';
|
|
4129
|
+
const rowValues = new Map();
|
|
4130
|
+
for (const cell of cells) {
|
|
4131
|
+
if (!cell?.attributePath)
|
|
4132
|
+
continue;
|
|
4133
|
+
if (matchesAttributePath(cell.attributePath, categoryField)) {
|
|
4134
|
+
categoryValue = String(cell.value ?? '');
|
|
4135
|
+
}
|
|
4136
|
+
// Check if this cell is one of our series fields
|
|
4137
|
+
for (const seriesConfig of seriesConfigs) {
|
|
4138
|
+
if (matchesAttributePath(cell.attributePath, seriesConfig.field)) {
|
|
4139
|
+
rowValues.set(seriesConfig.field, parseNumericValue(cell.value));
|
|
4140
|
+
}
|
|
4141
|
+
}
|
|
4142
|
+
}
|
|
4143
|
+
if (categoryValue !== '') {
|
|
4144
|
+
categories.push(categoryValue);
|
|
4145
|
+
// Add values for each series
|
|
4146
|
+
for (const seriesConfig of seriesConfigs) {
|
|
4147
|
+
const value = rowValues.get(seriesConfig.field) ?? 0;
|
|
4148
|
+
seriesMap.get(seriesConfig.field)?.push(value);
|
|
4149
|
+
}
|
|
4150
|
+
}
|
|
4151
|
+
}
|
|
4152
|
+
// Convert to series data array
|
|
4153
|
+
const seriesData = seriesConfigs.map(seriesConfig => ({
|
|
4154
|
+
name: seriesConfig.name ?? seriesConfig.field,
|
|
4155
|
+
data: seriesMap.get(seriesConfig.field) ?? [],
|
|
4156
|
+
color: seriesConfig.color
|
|
4157
|
+
}));
|
|
4158
|
+
return { categories, seriesData };
|
|
4159
|
+
}
|
|
4160
|
+
/**
|
|
4161
|
+
* Processes bar chart data in Dynamic Series Mode.
|
|
4162
|
+
* Series are created dynamically from unique values of seriesGroupField.
|
|
4163
|
+
* @param rows The query rows to process
|
|
4164
|
+
* @param categoryField The field to use for categories
|
|
4165
|
+
* @param seriesGroupField The field to use for series grouping
|
|
4166
|
+
* @param valueField The field to use for values
|
|
4167
|
+
* @returns Processed bar chart data
|
|
4168
|
+
*/
|
|
4169
|
+
function processDynamicSeriesData(rows, categoryField, seriesGroupField, valueField) {
|
|
4170
|
+
// Build a map: category -> seriesGroup -> value
|
|
4171
|
+
const dataMap = new Map();
|
|
4172
|
+
const allCategories = new Set();
|
|
4173
|
+
const allSeriesGroups = new Set();
|
|
4174
|
+
for (const row of rows) {
|
|
4175
|
+
if (!SUPPORTED_ROW_TYPES.includes(row.__typename ?? ''))
|
|
4176
|
+
continue;
|
|
4177
|
+
const cells = row.cells?.items ?? [];
|
|
4178
|
+
let categoryValue = '';
|
|
4179
|
+
let seriesGroupValue = '';
|
|
4180
|
+
let numericValue = 0;
|
|
4181
|
+
for (const cell of cells) {
|
|
4182
|
+
if (!cell?.attributePath)
|
|
4183
|
+
continue;
|
|
4184
|
+
if (matchesAttributePath(cell.attributePath, categoryField)) {
|
|
4185
|
+
categoryValue = String(cell.value ?? '');
|
|
4186
|
+
}
|
|
4187
|
+
else if (matchesAttributePath(cell.attributePath, seriesGroupField)) {
|
|
4188
|
+
seriesGroupValue = String(cell.value ?? '');
|
|
4189
|
+
}
|
|
4190
|
+
else if (matchesAttributePath(cell.attributePath, valueField)) {
|
|
4191
|
+
numericValue = parseNumericValue(cell.value);
|
|
4192
|
+
}
|
|
4193
|
+
}
|
|
4194
|
+
if (categoryValue && seriesGroupValue) {
|
|
4195
|
+
allCategories.add(categoryValue);
|
|
4196
|
+
allSeriesGroups.add(seriesGroupValue);
|
|
4197
|
+
if (!dataMap.has(categoryValue)) {
|
|
4198
|
+
dataMap.set(categoryValue, new Map());
|
|
4199
|
+
}
|
|
4200
|
+
dataMap.get(categoryValue).set(seriesGroupValue, numericValue);
|
|
4201
|
+
}
|
|
4202
|
+
}
|
|
4203
|
+
// Convert to arrays (maintain insertion order)
|
|
4204
|
+
const categories = Array.from(allCategories);
|
|
4205
|
+
const seriesGroups = Array.from(allSeriesGroups);
|
|
4206
|
+
// Build series data
|
|
4207
|
+
const seriesData = seriesGroups.map(seriesGroup => {
|
|
4208
|
+
const data = categories.map(category => {
|
|
4209
|
+
return dataMap.get(category)?.get(seriesGroup) ?? 0;
|
|
4210
|
+
});
|
|
4211
|
+
return {
|
|
4212
|
+
name: seriesGroup,
|
|
4213
|
+
data
|
|
4214
|
+
};
|
|
4215
|
+
});
|
|
4216
|
+
return { categories, seriesData };
|
|
4217
|
+
}
|
|
4218
|
+
/**
|
|
4219
|
+
* Processes pie chart data from query rows.
|
|
4220
|
+
* @param rows The query rows to process
|
|
4221
|
+
* @param categoryField The field to use for categories
|
|
4222
|
+
* @param valueField The field to use for values
|
|
4223
|
+
* @returns Array of pie chart data items
|
|
4224
|
+
*/
|
|
4225
|
+
function processPieChartData(rows, categoryField, valueField) {
|
|
4226
|
+
const result = [];
|
|
4227
|
+
for (const row of rows) {
|
|
4228
|
+
if (!SUPPORTED_ROW_TYPES.includes(row.__typename ?? ''))
|
|
4229
|
+
continue;
|
|
4230
|
+
const cells = row.cells?.items ?? [];
|
|
4231
|
+
let categoryValue = '';
|
|
4232
|
+
let numericValue = 0;
|
|
4233
|
+
for (const cell of cells) {
|
|
4234
|
+
if (!cell?.attributePath)
|
|
4235
|
+
continue;
|
|
4236
|
+
if (matchesAttributePath(cell.attributePath, categoryField)) {
|
|
4237
|
+
categoryValue = String(cell.value ?? '');
|
|
4238
|
+
}
|
|
4239
|
+
else if (matchesAttributePath(cell.attributePath, valueField)) {
|
|
4240
|
+
numericValue = parseNumericValue(cell.value);
|
|
4241
|
+
}
|
|
4242
|
+
}
|
|
4243
|
+
if (categoryValue) {
|
|
4244
|
+
result.push({
|
|
4245
|
+
category: categoryValue,
|
|
4246
|
+
value: numericValue
|
|
4247
|
+
});
|
|
4248
|
+
}
|
|
4249
|
+
}
|
|
4250
|
+
return result;
|
|
4251
|
+
}
|
|
4252
|
+
|
|
3944
4253
|
class KpiWidgetComponent {
|
|
3945
4254
|
dataService = inject(DashboardDataService);
|
|
3946
4255
|
executeRuntimeQueryGQL = inject(ExecuteRuntimeQueryDtoGQL);
|
|
@@ -4266,8 +4575,7 @@ class KpiWidgetComponent {
|
|
|
4266
4575
|
for (const cell of cells) {
|
|
4267
4576
|
if (!cell?.attributePath)
|
|
4268
4577
|
continue;
|
|
4269
|
-
|
|
4270
|
-
if (valueField && sanitizedPath === valueField) {
|
|
4578
|
+
if (valueField && matchesAttributePath(cell.attributePath, valueField)) {
|
|
4271
4579
|
return this.extractCellValue(cell.value);
|
|
4272
4580
|
}
|
|
4273
4581
|
}
|
|
@@ -4295,11 +4603,10 @@ class KpiWidgetComponent {
|
|
|
4295
4603
|
for (const cell of cells) {
|
|
4296
4604
|
if (!cell?.attributePath)
|
|
4297
4605
|
continue;
|
|
4298
|
-
|
|
4299
|
-
if (sanitizedPath === categoryField && String(cell.value) === categoryValue) {
|
|
4606
|
+
if (matchesAttributePath(cell.attributePath, categoryField) && String(cell.value) === categoryValue) {
|
|
4300
4607
|
categoryMatch = true;
|
|
4301
4608
|
}
|
|
4302
|
-
if (
|
|
4609
|
+
if (matchesAttributePath(cell.attributePath, valueField)) {
|
|
4303
4610
|
value = this.extractCellValue(cell.value);
|
|
4304
4611
|
}
|
|
4305
4612
|
}
|
|
@@ -4309,12 +4616,6 @@ class KpiWidgetComponent {
|
|
|
4309
4616
|
}
|
|
4310
4617
|
return 0;
|
|
4311
4618
|
}
|
|
4312
|
-
parseNumericValue(value) {
|
|
4313
|
-
if (typeof value === 'number')
|
|
4314
|
-
return value;
|
|
4315
|
-
const parsed = parseFloat(String(value));
|
|
4316
|
-
return isNaN(parsed) ? 0 : parsed;
|
|
4317
|
-
}
|
|
4318
4619
|
/**
|
|
4319
4620
|
* Extracts a cell value, preserving strings and converting numbers.
|
|
4320
4621
|
* Returns the value as-is if it's a string, or parses it as a number.
|
|
@@ -4333,9 +4634,6 @@ class KpiWidgetComponent {
|
|
|
4333
4634
|
// For other types (boolean, object), convert to string
|
|
4334
4635
|
return String(value);
|
|
4335
4636
|
}
|
|
4336
|
-
sanitizeFieldName(fieldName) {
|
|
4337
|
-
return fieldName.replace(/\./g, '_');
|
|
4338
|
-
}
|
|
4339
4637
|
/**
|
|
4340
4638
|
* Converts widget filter configuration to GraphQL FieldFilterDto format.
|
|
4341
4639
|
* Resolves MeshBoard variables in filter values before conversion.
|
|
@@ -4354,6 +4652,38 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
4354
4652
|
type: Input
|
|
4355
4653
|
}] } });
|
|
4356
4654
|
|
|
4655
|
+
const GetRuntimeQueryColumnsDocumentDto = gql `
|
|
4656
|
+
query getRuntimeQueryColumns($rtId: OctoObjectId!) {
|
|
4657
|
+
runtime {
|
|
4658
|
+
runtimeQuery(rtId: $rtId) {
|
|
4659
|
+
items {
|
|
4660
|
+
queryRtId
|
|
4661
|
+
associatedCkTypeId
|
|
4662
|
+
columns {
|
|
4663
|
+
attributePath
|
|
4664
|
+
attributeValueType
|
|
4665
|
+
aggregationType
|
|
4666
|
+
}
|
|
4667
|
+
}
|
|
4668
|
+
}
|
|
4669
|
+
}
|
|
4670
|
+
}
|
|
4671
|
+
`;
|
|
4672
|
+
class GetRuntimeQueryColumnsDtoGQL extends i1.Query {
|
|
4673
|
+
document = GetRuntimeQueryColumnsDocumentDto;
|
|
4674
|
+
constructor(apollo) {
|
|
4675
|
+
super(apollo);
|
|
4676
|
+
}
|
|
4677
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: GetRuntimeQueryColumnsDtoGQL, deps: [{ token: i1.Apollo }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
4678
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: GetRuntimeQueryColumnsDtoGQL, providedIn: 'root' });
|
|
4679
|
+
}
|
|
4680
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: GetRuntimeQueryColumnsDtoGQL, decorators: [{
|
|
4681
|
+
type: Injectable,
|
|
4682
|
+
args: [{
|
|
4683
|
+
providedIn: 'root'
|
|
4684
|
+
}]
|
|
4685
|
+
}], ctorParameters: () => [{ type: i1.Apollo }] });
|
|
4686
|
+
|
|
4357
4687
|
/**
|
|
4358
4688
|
* Configuration dialog for KPI widgets.
|
|
4359
4689
|
* Allows selecting data source, value attribute, and display options.
|
|
@@ -4363,6 +4693,7 @@ class KpiConfigDialogComponent {
|
|
|
4363
4693
|
ckTypeSelectorService = inject(CkTypeSelectorService);
|
|
4364
4694
|
attributeSelectorService = inject(AttributeSelectorService);
|
|
4365
4695
|
executeRuntimeQueryGQL = inject(ExecuteRuntimeQueryDtoGQL);
|
|
4696
|
+
getRuntimeQueryColumnsGQL = inject(GetRuntimeQueryColumnsDtoGQL);
|
|
4366
4697
|
meshBoardStateService = inject(MeshBoardStateService);
|
|
4367
4698
|
windowRef = inject(WindowRef);
|
|
4368
4699
|
ckTypeSelectorInput;
|
|
@@ -4729,27 +5060,32 @@ class KpiConfigDialogComponent {
|
|
|
4729
5060
|
return;
|
|
4730
5061
|
this.isLoadingQueryColumns = true;
|
|
4731
5062
|
try {
|
|
4732
|
-
|
|
5063
|
+
// Metadata-only fetch — column resolver runs off the cached query definition
|
|
5064
|
+
// without executing the underlying aggregation, so the dialog opens fast even
|
|
5065
|
+
// for queries that aggregate over large data sets.
|
|
5066
|
+
const result = await firstValueFrom(this.getRuntimeQueryColumnsGQL.fetch({
|
|
4733
5067
|
variables: {
|
|
4734
|
-
rtId: rtId
|
|
4735
|
-
first: 100 // Fetch rows for category values
|
|
5068
|
+
rtId: rtId
|
|
4736
5069
|
}
|
|
4737
5070
|
}));
|
|
4738
5071
|
const queryItems = result.data?.runtime?.runtimeQuery?.items ?? [];
|
|
4739
5072
|
if (queryItems.length > 0 && queryItems[0]) {
|
|
4740
|
-
const
|
|
4741
|
-
// Extract columns
|
|
4742
|
-
const columns = queryResult.columns ?? [];
|
|
5073
|
+
const columns = queryItems[0].columns ?? [];
|
|
4743
5074
|
const filteredColumns = columns
|
|
4744
5075
|
.filter((c) => c !== null);
|
|
4745
|
-
//
|
|
5076
|
+
// Column AttributePath is already in the engine's wire form for aggregation /
|
|
5077
|
+
// grouping columns (e.g. `quantity_sum`, `operatingstatus`) so picker entries
|
|
5078
|
+
// double as both the visible label and the stored config value, and MIN + MAX
|
|
5079
|
+
// of the same source path show up as two distinct entries.
|
|
4746
5080
|
this.queryColumns = filteredColumns.map(c => ({
|
|
4747
|
-
attributePath:
|
|
4748
|
-
attributeValueType: c.attributeValueType ?? ''
|
|
5081
|
+
attributePath: c.attributePath ?? '',
|
|
5082
|
+
attributeValueType: c.attributeValueType ?? '',
|
|
5083
|
+
aggregationType: c.aggregationType ?? null
|
|
4749
5084
|
}));
|
|
4750
|
-
//
|
|
4751
|
-
|
|
4752
|
-
|
|
5085
|
+
// Category values for grouped aggregation are loaded on-demand by
|
|
5086
|
+
// loadCategoryValuesForField — only when a categoryField is actually selected.
|
|
5087
|
+
if (this.queryMode === 'groupedAggregation' && this.form.queryCategoryField) {
|
|
5088
|
+
await this.loadCategoryValuesForField(rtId, this.form.queryCategoryField);
|
|
4753
5089
|
}
|
|
4754
5090
|
}
|
|
4755
5091
|
}
|
|
@@ -4792,8 +5128,7 @@ class KpiConfigDialogComponent {
|
|
|
4792
5128
|
for (const cell of cells) {
|
|
4793
5129
|
if (!cell?.attributePath)
|
|
4794
5130
|
continue;
|
|
4795
|
-
|
|
4796
|
-
if (sanitizedPath === categoryField && cell.value !== null && cell.value !== undefined) {
|
|
5131
|
+
if (matchesAttributePath(cell.attributePath, categoryField) && cell.value !== null && cell.value !== undefined) {
|
|
4797
5132
|
values.add(String(cell.value));
|
|
4798
5133
|
}
|
|
4799
5134
|
}
|
|
@@ -4812,15 +5147,6 @@ class KpiConfigDialogComponent {
|
|
|
4812
5147
|
this.isLoadingCategoryValues = false;
|
|
4813
5148
|
}
|
|
4814
5149
|
}
|
|
4815
|
-
async extractCategoryValues(_queryResult) {
|
|
4816
|
-
// Load category values if a category field is already selected
|
|
4817
|
-
if (this.queryColumns.length > 0 && this.form.queryCategoryField && this.selectedPersistentQuery) {
|
|
4818
|
-
await this.loadCategoryValuesForField(this.selectedPersistentQuery.rtId, this.form.queryCategoryField);
|
|
4819
|
-
}
|
|
4820
|
-
}
|
|
4821
|
-
sanitizeFieldName(fieldName) {
|
|
4822
|
-
return fieldName.replace(/\./g, '_');
|
|
4823
|
-
}
|
|
4824
5150
|
// ============================================================================
|
|
4825
5151
|
// Filter Methods
|
|
4826
5152
|
// ============================================================================
|
|
@@ -7224,15 +7550,19 @@ class TableWidgetDataSourceDirective extends OctoGraphQlDataSource {
|
|
|
7224
7550
|
if (hasCkTypeIdColumn) {
|
|
7225
7551
|
record['ckTypeId'] = queryRow.ckTypeId ?? '';
|
|
7226
7552
|
}
|
|
7227
|
-
// Flatten cells into the record
|
|
7228
|
-
//
|
|
7553
|
+
// Flatten cells into the record. Each cell is stored under the COLUMN's
|
|
7554
|
+
// attributePath (which is what the Kendo grid uses as `field`) rather than
|
|
7555
|
+
// the cell's own attributePath — the two may differ now that the engine
|
|
7556
|
+
// emits cell paths in wire-form with a function suffix
|
|
7557
|
+
// (e.g. cell path `meterreading_count` for column path `meterReading`).
|
|
7558
|
+
// `matchesAttributePath` reconciles both forms.
|
|
7229
7559
|
const cells = queryRow.cells?.items ?? [];
|
|
7230
7560
|
for (const cell of cells) {
|
|
7231
|
-
if (cell?.attributePath)
|
|
7232
|
-
|
|
7233
|
-
|
|
7234
|
-
|
|
7235
|
-
|
|
7561
|
+
if (!cell?.attributePath)
|
|
7562
|
+
continue;
|
|
7563
|
+
const matchingColumn = columns.find(col => matchesAttributePath(cell.attributePath, col.attributePath));
|
|
7564
|
+
if (matchingColumn) {
|
|
7565
|
+
record[matchingColumn.attributePath] = cell.value;
|
|
7236
7566
|
}
|
|
7237
7567
|
}
|
|
7238
7568
|
return record;
|
|
@@ -8412,8 +8742,7 @@ class GaugeWidgetComponent {
|
|
|
8412
8742
|
for (const cell of cells) {
|
|
8413
8743
|
if (!cell?.attributePath)
|
|
8414
8744
|
continue;
|
|
8415
|
-
|
|
8416
|
-
if (valueField && sanitizedPath === valueField) {
|
|
8745
|
+
if (valueField && matchesAttributePath(cell.attributePath, valueField)) {
|
|
8417
8746
|
return this.parseNumericValue(cell.value);
|
|
8418
8747
|
}
|
|
8419
8748
|
}
|
|
@@ -8441,11 +8770,10 @@ class GaugeWidgetComponent {
|
|
|
8441
8770
|
for (const cell of cells) {
|
|
8442
8771
|
if (!cell?.attributePath)
|
|
8443
8772
|
continue;
|
|
8444
|
-
|
|
8445
|
-
if (sanitizedPath === categoryField && String(cell.value) === categoryValue) {
|
|
8773
|
+
if (matchesAttributePath(cell.attributePath, categoryField) && String(cell.value) === categoryValue) {
|
|
8446
8774
|
categoryMatch = true;
|
|
8447
8775
|
}
|
|
8448
|
-
if (
|
|
8776
|
+
if (matchesAttributePath(cell.attributePath, valueField)) {
|
|
8449
8777
|
value = this.parseNumericValue(cell.value);
|
|
8450
8778
|
}
|
|
8451
8779
|
}
|
|
@@ -8461,9 +8789,6 @@ class GaugeWidgetComponent {
|
|
|
8461
8789
|
const parsed = parseFloat(String(value));
|
|
8462
8790
|
return isNaN(parsed) ? 0 : parsed;
|
|
8463
8791
|
}
|
|
8464
|
-
sanitizeFieldName(fieldName) {
|
|
8465
|
-
return fieldName.replace(/\./g, '_');
|
|
8466
|
-
}
|
|
8467
8792
|
/**
|
|
8468
8793
|
* Converts widget filter configuration to GraphQL FieldFilterDto format.
|
|
8469
8794
|
* Resolves MeshBoard variables in filter values before conversion.
|
|
@@ -8833,6 +9158,7 @@ class GaugeConfigDialogComponent {
|
|
|
8833
9158
|
ckTypeSelectorService = inject(CkTypeSelectorService);
|
|
8834
9159
|
attributeSelectorService = inject(AttributeSelectorService);
|
|
8835
9160
|
executeRuntimeQueryGQL = inject(ExecuteRuntimeQueryDtoGQL);
|
|
9161
|
+
getRuntimeQueryColumnsGQL = inject(GetRuntimeQueryColumnsDtoGQL);
|
|
8836
9162
|
meshBoardStateService = inject(MeshBoardStateService);
|
|
8837
9163
|
windowRef = inject(WindowRef);
|
|
8838
9164
|
ckTypeSelectorInput;
|
|
@@ -9182,27 +9508,29 @@ class GaugeConfigDialogComponent {
|
|
|
9182
9508
|
async loadQueryColumnsAndValues(queryRtId) {
|
|
9183
9509
|
this.isLoadingQueryColumns = true;
|
|
9184
9510
|
try {
|
|
9185
|
-
|
|
9511
|
+
// Metadata-only fetch — skips backend aggregation execution so the dialog opens
|
|
9512
|
+
// fast even for large data sets.
|
|
9513
|
+
const result = await firstValueFrom(this.getRuntimeQueryColumnsGQL.fetch({
|
|
9186
9514
|
variables: {
|
|
9187
|
-
rtId: queryRtId
|
|
9188
|
-
first: 100 // Fetch rows for category values
|
|
9515
|
+
rtId: queryRtId
|
|
9189
9516
|
}
|
|
9190
9517
|
}));
|
|
9191
9518
|
const queryItems = result.data?.runtime?.runtimeQuery?.items ?? [];
|
|
9192
9519
|
if (queryItems.length > 0 && queryItems[0]) {
|
|
9193
|
-
const
|
|
9194
|
-
// Extract columns
|
|
9195
|
-
const columns = queryResult.columns ?? [];
|
|
9520
|
+
const columns = queryItems[0].columns ?? [];
|
|
9196
9521
|
const filteredColumns = columns
|
|
9197
9522
|
.filter((c) => c !== null);
|
|
9198
|
-
//
|
|
9523
|
+
// Engine emits column attributePath in wire form for aggregation / grouping
|
|
9524
|
+
// columns, so the picker can use it as both label and stored value verbatim.
|
|
9199
9525
|
this.queryColumns = filteredColumns.map(c => ({
|
|
9200
|
-
attributePath:
|
|
9201
|
-
attributeValueType: c.attributeValueType ?? ''
|
|
9526
|
+
attributePath: c.attributePath ?? '',
|
|
9527
|
+
attributeValueType: c.attributeValueType ?? '',
|
|
9528
|
+
aggregationType: c.aggregationType ?? null
|
|
9202
9529
|
}));
|
|
9203
|
-
//
|
|
9204
|
-
|
|
9205
|
-
|
|
9530
|
+
// Category values for grouped aggregation are loaded on-demand by
|
|
9531
|
+
// loadCategoryValuesForField — only when a categoryField is actually selected.
|
|
9532
|
+
if (this.queryMode === 'groupedAggregation' && this.form.queryCategoryField) {
|
|
9533
|
+
await this.loadCategoryValuesForField(queryRtId, this.form.queryCategoryField);
|
|
9206
9534
|
}
|
|
9207
9535
|
}
|
|
9208
9536
|
}
|
|
@@ -9245,8 +9573,7 @@ class GaugeConfigDialogComponent {
|
|
|
9245
9573
|
for (const cell of cells) {
|
|
9246
9574
|
if (!cell?.attributePath)
|
|
9247
9575
|
continue;
|
|
9248
|
-
|
|
9249
|
-
if (sanitizedPath === categoryField && cell.value !== null && cell.value !== undefined) {
|
|
9576
|
+
if (matchesAttributePath(cell.attributePath, categoryField) && cell.value !== null && cell.value !== undefined) {
|
|
9250
9577
|
values.add(String(cell.value));
|
|
9251
9578
|
}
|
|
9252
9579
|
}
|
|
@@ -9265,15 +9592,6 @@ class GaugeConfigDialogComponent {
|
|
|
9265
9592
|
this.isLoadingCategoryValues = false;
|
|
9266
9593
|
}
|
|
9267
9594
|
}
|
|
9268
|
-
async extractCategoryValues(_queryResult) {
|
|
9269
|
-
// Load category values if a category field is already selected
|
|
9270
|
-
if (this.queryColumns.length > 0 && this.form.queryCategoryField && this.selectedPersistentQuery) {
|
|
9271
|
-
await this.loadCategoryValuesForField(this.selectedPersistentQuery.rtId, this.form.queryCategoryField);
|
|
9272
|
-
}
|
|
9273
|
-
}
|
|
9274
|
-
sanitizeFieldName(fieldName) {
|
|
9275
|
-
return fieldName.replace(/\./g, '_');
|
|
9276
|
-
}
|
|
9277
9595
|
// ============================================================================
|
|
9278
9596
|
// Filter Methods
|
|
9279
9597
|
// ============================================================================
|
|
@@ -10293,13 +10611,15 @@ class PieChartWidgetComponent {
|
|
|
10293
10611
|
this._isLoading.set(false);
|
|
10294
10612
|
return;
|
|
10295
10613
|
}
|
|
10296
|
-
// Extract columns to
|
|
10297
|
-
|
|
10614
|
+
// Extract columns to verify configured fields are present. Both forms (original CK
|
|
10615
|
+
// path and engine wire-form) are accepted so saved configs survive the engine's
|
|
10616
|
+
// switch to wire-form keys without a migration.
|
|
10617
|
+
const columnPaths = (queryResult.columns ?? [])
|
|
10298
10618
|
.filter((c) => c !== null)
|
|
10299
|
-
.map(c =>
|
|
10300
|
-
const
|
|
10301
|
-
const
|
|
10302
|
-
if (
|
|
10619
|
+
.map(c => c.attributePath ?? '');
|
|
10620
|
+
const categoryFieldPresent = columnPaths.some(p => matchesAttributePath(p, this.config.categoryField));
|
|
10621
|
+
const valueFieldPresent = columnPaths.some(p => matchesAttributePath(p, this.config.valueField));
|
|
10622
|
+
if (!categoryFieldPresent || !valueFieldPresent) {
|
|
10303
10623
|
this._error.set('Configured fields not found in query result');
|
|
10304
10624
|
this._isLoading.set(false);
|
|
10305
10625
|
return;
|
|
@@ -10318,11 +10638,10 @@ class PieChartWidgetComponent {
|
|
|
10318
10638
|
for (const cell of cells) {
|
|
10319
10639
|
if (!cell?.attributePath)
|
|
10320
10640
|
continue;
|
|
10321
|
-
|
|
10322
|
-
if (sanitizedPath === this.sanitizeFieldName(this.config.categoryField)) {
|
|
10641
|
+
if (matchesAttributePath(cell.attributePath, this.config.categoryField)) {
|
|
10323
10642
|
category = String(cell.value ?? '');
|
|
10324
10643
|
}
|
|
10325
|
-
if (
|
|
10644
|
+
if (matchesAttributePath(cell.attributePath, this.config.valueField)) {
|
|
10326
10645
|
const numValue = typeof cell.value === 'number' ? cell.value : parseFloat(String(cell.value));
|
|
10327
10646
|
value = isNaN(numValue) ? 0 : numValue;
|
|
10328
10647
|
}
|
|
@@ -10333,13 +10652,6 @@ class PieChartWidgetComponent {
|
|
|
10333
10652
|
this._chartData.set(chartData);
|
|
10334
10653
|
this._isLoading.set(false);
|
|
10335
10654
|
}
|
|
10336
|
-
/**
|
|
10337
|
-
* Sanitizes field names for comparison.
|
|
10338
|
-
* Replaces dots with underscores (same as table widget).
|
|
10339
|
-
*/
|
|
10340
|
-
sanitizeFieldName(fieldName) {
|
|
10341
|
-
return fieldName.replace(/\./g, '_');
|
|
10342
|
-
}
|
|
10343
10655
|
/**
|
|
10344
10656
|
* Converts widget filter configuration to GraphQL FieldFilterDto format.
|
|
10345
10657
|
* Resolves MeshBoard variables in filter values before conversion.
|
|
@@ -10442,7 +10754,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
10442
10754
|
* Configuration dialog for Pie Chart widgets.
|
|
10443
10755
|
*/
|
|
10444
10756
|
class PieChartConfigDialogComponent {
|
|
10445
|
-
|
|
10757
|
+
getRuntimeQueryColumnsGQL = inject(GetRuntimeQueryColumnsDtoGQL);
|
|
10446
10758
|
stateService = inject(MeshBoardStateService);
|
|
10447
10759
|
windowRef = inject(WindowRef);
|
|
10448
10760
|
querySelector;
|
|
@@ -10611,10 +10923,12 @@ class PieChartConfigDialogComponent {
|
|
|
10611
10923
|
async loadQueryColumns(queryRtId) {
|
|
10612
10924
|
this.isLoadingColumns = true;
|
|
10613
10925
|
try {
|
|
10614
|
-
|
|
10926
|
+
// Metadata-only query: column resolver runs off the cached query definition without
|
|
10927
|
+
// touching the row execution path, so the dialog opens fast even when the underlying
|
|
10928
|
+
// persistent query aggregates over a large data set.
|
|
10929
|
+
const result = await firstValueFrom(this.getRuntimeQueryColumnsGQL.fetch({
|
|
10615
10930
|
variables: {
|
|
10616
|
-
rtId: queryRtId
|
|
10617
|
-
first: 1 // We only need columns, not data
|
|
10931
|
+
rtId: queryRtId
|
|
10618
10932
|
}
|
|
10619
10933
|
}));
|
|
10620
10934
|
const queryItems = result.data?.runtime?.runtimeQuery?.items ?? [];
|
|
@@ -10622,10 +10936,12 @@ class PieChartConfigDialogComponent {
|
|
|
10622
10936
|
const columns = queryItems[0].columns ?? [];
|
|
10623
10937
|
const filteredColumns = columns
|
|
10624
10938
|
.filter((c) => c !== null);
|
|
10625
|
-
//
|
|
10939
|
+
// Engine emits column attributePath in wire form for aggregation / grouping
|
|
10940
|
+
// columns (e.g. `quantity_sum`, `operatingstatus`); picker uses it verbatim.
|
|
10626
10941
|
this.queryColumns = filteredColumns.map(c => ({
|
|
10627
|
-
attributePath:
|
|
10628
|
-
attributeValueType: c.attributeValueType ?? ''
|
|
10942
|
+
attributePath: c.attributePath ?? '',
|
|
10943
|
+
attributeValueType: c.attributeValueType ?? '',
|
|
10944
|
+
aggregationType: c.aggregationType ?? null
|
|
10629
10945
|
}));
|
|
10630
10946
|
// Auto-select fields if only 2 columns (typical for grouped aggregations)
|
|
10631
10947
|
if (this.queryColumns.length === 2 && !this.form.categoryField && !this.form.valueField) {
|
|
@@ -10648,9 +10964,6 @@ class PieChartConfigDialogComponent {
|
|
|
10648
10964
|
this.isLoadingColumns = false;
|
|
10649
10965
|
}
|
|
10650
10966
|
}
|
|
10651
|
-
sanitizeFieldName(fieldName) {
|
|
10652
|
-
return fieldName.replace(/\./g, '_');
|
|
10653
|
-
}
|
|
10654
10967
|
onFiltersChange(updatedFilters) {
|
|
10655
10968
|
this.filters = updatedFilters;
|
|
10656
10969
|
}
|
|
@@ -11314,13 +11627,12 @@ class BarChartWidgetComponent {
|
|
|
11314
11627
|
for (const cell of cells) {
|
|
11315
11628
|
if (!cell?.attributePath)
|
|
11316
11629
|
continue;
|
|
11317
|
-
|
|
11318
|
-
if (sanitizedPath === this.sanitizeFieldName(this.config.categoryField)) {
|
|
11630
|
+
if (matchesAttributePath(cell.attributePath, this.config.categoryField)) {
|
|
11319
11631
|
categoryValue = this.formatCategoryValue(cell.value);
|
|
11320
11632
|
}
|
|
11321
11633
|
// Check if this cell is one of our series fields
|
|
11322
11634
|
for (const seriesConfig of (this.config.series ?? [])) {
|
|
11323
|
-
if (
|
|
11635
|
+
if (matchesAttributePath(cell.attributePath, seriesConfig.field)) {
|
|
11324
11636
|
const numValue = typeof cell.value === 'number' ? cell.value : parseFloat(String(cell.value));
|
|
11325
11637
|
rowValues.set(seriesConfig.field, isNaN(numValue) ? 0 : numValue);
|
|
11326
11638
|
}
|
|
@@ -11379,14 +11691,13 @@ class BarChartWidgetComponent {
|
|
|
11379
11691
|
for (const cell of cells) {
|
|
11380
11692
|
if (!cell?.attributePath)
|
|
11381
11693
|
continue;
|
|
11382
|
-
|
|
11383
|
-
if (sanitizedPath === this.sanitizeFieldName(categoryField)) {
|
|
11694
|
+
if (matchesAttributePath(cell.attributePath, categoryField)) {
|
|
11384
11695
|
categoryValue = this.formatCategoryValue(cell.value);
|
|
11385
11696
|
}
|
|
11386
|
-
else if (
|
|
11697
|
+
else if (matchesAttributePath(cell.attributePath, seriesGroupField)) {
|
|
11387
11698
|
seriesGroupValue = String(cell.value ?? '');
|
|
11388
11699
|
}
|
|
11389
|
-
else if (
|
|
11700
|
+
else if (matchesAttributePath(cell.attributePath, valueField)) {
|
|
11390
11701
|
const val = cell.value;
|
|
11391
11702
|
numericValue = typeof val === 'number' ? val : parseFloat(String(val));
|
|
11392
11703
|
if (isNaN(numericValue))
|
|
@@ -11458,9 +11769,6 @@ class BarChartWidgetComponent {
|
|
|
11458
11769
|
}
|
|
11459
11770
|
return str;
|
|
11460
11771
|
}
|
|
11461
|
-
sanitizeFieldName(fieldName) {
|
|
11462
|
-
return fieldName.replace(/\./g, '_');
|
|
11463
|
-
}
|
|
11464
11772
|
/**
|
|
11465
11773
|
* Converts widget filter configuration to GraphQL FieldFilterDto format.
|
|
11466
11774
|
* Resolves MeshBoard variables in filter values before conversion.
|
|
@@ -11632,7 +11940,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
11632
11940
|
* Supports column, bar, stacked, and 100% stacked chart types.
|
|
11633
11941
|
*/
|
|
11634
11942
|
class BarChartConfigDialogComponent {
|
|
11635
|
-
|
|
11943
|
+
getRuntimeQueryColumnsGQL = inject(GetRuntimeQueryColumnsDtoGQL);
|
|
11636
11944
|
stateService = inject(MeshBoardStateService);
|
|
11637
11945
|
windowRef = inject(WindowRef);
|
|
11638
11946
|
querySelector;
|
|
@@ -11771,10 +12079,11 @@ class BarChartConfigDialogComponent {
|
|
|
11771
12079
|
async loadQueryColumns(queryRtId) {
|
|
11772
12080
|
this.isLoadingColumns = true;
|
|
11773
12081
|
try {
|
|
11774
|
-
|
|
12082
|
+
// Metadata-only — column resolver runs off the cached query definition and skips
|
|
12083
|
+
// the row execution path, so the dialog opens fast even on large aggregations.
|
|
12084
|
+
const result = await firstValueFrom(this.getRuntimeQueryColumnsGQL.fetch({
|
|
11775
12085
|
variables: {
|
|
11776
|
-
rtId: queryRtId
|
|
11777
|
-
first: 1 // We only need columns, not data
|
|
12086
|
+
rtId: queryRtId
|
|
11778
12087
|
}
|
|
11779
12088
|
}));
|
|
11780
12089
|
const queryItems = result.data?.runtime?.runtimeQuery?.items ?? [];
|
|
@@ -11782,10 +12091,12 @@ class BarChartConfigDialogComponent {
|
|
|
11782
12091
|
const columns = queryItems[0].columns ?? [];
|
|
11783
12092
|
const filteredColumns = columns
|
|
11784
12093
|
.filter((c) => c !== null);
|
|
11785
|
-
//
|
|
12094
|
+
// Engine emits column attributePath in wire form for aggregation / grouping
|
|
12095
|
+
// columns (e.g. `quantity_sum`, `operatingstatus`); picker uses it verbatim.
|
|
11786
12096
|
this.queryColumns = filteredColumns.map(c => ({
|
|
11787
|
-
attributePath:
|
|
11788
|
-
attributeValueType: c.attributeValueType ?? ''
|
|
12097
|
+
attributePath: c.attributePath ?? '',
|
|
12098
|
+
attributeValueType: c.attributeValueType ?? '',
|
|
12099
|
+
aggregationType: c.aggregationType ?? null
|
|
11789
12100
|
}));
|
|
11790
12101
|
// Filter numeric and non-numeric columns
|
|
11791
12102
|
const numericTypes = ['INTEGER', 'FLOAT', 'DOUBLE', 'DECIMAL', 'LONG'];
|
|
@@ -11815,9 +12126,6 @@ class BarChartConfigDialogComponent {
|
|
|
11815
12126
|
this.isLoadingColumns = false;
|
|
11816
12127
|
}
|
|
11817
12128
|
}
|
|
11818
|
-
sanitizeFieldName(fieldName) {
|
|
11819
|
-
return fieldName.replace(/\./g, '_');
|
|
11820
|
-
}
|
|
11821
12129
|
onSeriesFieldsChange(fields) {
|
|
11822
12130
|
this.selectedSeriesFields = fields;
|
|
11823
12131
|
}
|
|
@@ -12623,20 +12931,19 @@ class LineChartWidgetComponent {
|
|
|
12623
12931
|
for (const cell of cells) {
|
|
12624
12932
|
if (!cell?.attributePath)
|
|
12625
12933
|
continue;
|
|
12626
|
-
|
|
12627
|
-
if (sanitizedPath === this.sanitizeFieldName(categoryField)) {
|
|
12934
|
+
if (matchesAttributePath(cell.attributePath, categoryField)) {
|
|
12628
12935
|
categoryValue = String(cell.value ?? '');
|
|
12629
12936
|
}
|
|
12630
|
-
else if (
|
|
12937
|
+
else if (matchesAttributePath(cell.attributePath, seriesGroupField)) {
|
|
12631
12938
|
seriesGroupValue = String(cell.value ?? '');
|
|
12632
12939
|
}
|
|
12633
|
-
else if (
|
|
12940
|
+
else if (matchesAttributePath(cell.attributePath, valueField)) {
|
|
12634
12941
|
const val = cell.value;
|
|
12635
12942
|
numericValue = typeof val === 'number' ? val : parseFloat(String(val));
|
|
12636
12943
|
if (isNaN(numericValue))
|
|
12637
12944
|
numericValue = 0;
|
|
12638
12945
|
}
|
|
12639
|
-
else if (unitField &&
|
|
12946
|
+
else if (unitField && matchesAttributePath(cell.attributePath, unitField)) {
|
|
12640
12947
|
unitValue = String(cell.value ?? '');
|
|
12641
12948
|
}
|
|
12642
12949
|
}
|
|
@@ -12722,9 +13029,6 @@ class LineChartWidgetComponent {
|
|
|
12722
13029
|
minute: '2-digit'
|
|
12723
13030
|
});
|
|
12724
13031
|
}
|
|
12725
|
-
sanitizeFieldName(fieldName) {
|
|
12726
|
-
return fieldName.replace(/\./g, '_');
|
|
12727
|
-
}
|
|
12728
13032
|
sanitizeAxisName(name) {
|
|
12729
13033
|
return name.replace(/[^a-zA-Z0-9]/g, '_');
|
|
12730
13034
|
}
|
|
@@ -12921,7 +13225,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
12921
13225
|
* Supports line and area chart types with multi-series and multi-axis configuration.
|
|
12922
13226
|
*/
|
|
12923
13227
|
class LineChartConfigDialogComponent {
|
|
12924
|
-
|
|
13228
|
+
getRuntimeQueryColumnsGQL = inject(GetRuntimeQueryColumnsDtoGQL);
|
|
12925
13229
|
stateService = inject(MeshBoardStateService);
|
|
12926
13230
|
windowRef = inject(WindowRef);
|
|
12927
13231
|
querySelector;
|
|
@@ -13032,10 +13336,10 @@ class LineChartConfigDialogComponent {
|
|
|
13032
13336
|
async loadQueryColumns(queryRtId) {
|
|
13033
13337
|
this.isLoadingColumns = true;
|
|
13034
13338
|
try {
|
|
13035
|
-
|
|
13339
|
+
// Metadata-only — skips the row execution path on the backend.
|
|
13340
|
+
const result = await firstValueFrom(this.getRuntimeQueryColumnsGQL.fetch({
|
|
13036
13341
|
variables: {
|
|
13037
|
-
rtId: queryRtId
|
|
13038
|
-
first: 1
|
|
13342
|
+
rtId: queryRtId
|
|
13039
13343
|
}
|
|
13040
13344
|
}));
|
|
13041
13345
|
const queryItems = result.data?.runtime?.runtimeQuery?.items ?? [];
|
|
@@ -13044,8 +13348,9 @@ class LineChartConfigDialogComponent {
|
|
|
13044
13348
|
const filteredColumns = columns
|
|
13045
13349
|
.filter((c) => c !== null);
|
|
13046
13350
|
this.queryColumns = filteredColumns.map(c => ({
|
|
13047
|
-
attributePath:
|
|
13048
|
-
attributeValueType: c.attributeValueType ?? ''
|
|
13351
|
+
attributePath: c.attributePath ?? '',
|
|
13352
|
+
attributeValueType: c.attributeValueType ?? '',
|
|
13353
|
+
aggregationType: c.aggregationType ?? null
|
|
13049
13354
|
}));
|
|
13050
13355
|
const numericTypes = ['INTEGER', 'FLOAT', 'DOUBLE', 'DECIMAL', 'LONG'];
|
|
13051
13356
|
this.numericColumns = this.queryColumns.filter(c => numericTypes.includes(c.attributeValueType));
|
|
@@ -13062,9 +13367,6 @@ class LineChartConfigDialogComponent {
|
|
|
13062
13367
|
this.isLoadingColumns = false;
|
|
13063
13368
|
}
|
|
13064
13369
|
}
|
|
13065
|
-
sanitizeFieldName(fieldName) {
|
|
13066
|
-
return fieldName.replace(/\./g, '_');
|
|
13067
|
-
}
|
|
13068
13370
|
onFiltersChange(updatedFilters) {
|
|
13069
13371
|
this.filters = updatedFilters;
|
|
13070
13372
|
}
|
|
@@ -13739,9 +14041,9 @@ class HeatmapWidgetComponent {
|
|
|
13739
14041
|
* Otherwise aggregates into hourly buckets.
|
|
13740
14042
|
*/
|
|
13741
14043
|
processHeatmapData(filteredRows) {
|
|
13742
|
-
const dateField = this.
|
|
13743
|
-
const dateEndField = this.config.dateEndField
|
|
13744
|
-
const valueField = this.config.valueField
|
|
14044
|
+
const dateField = this.config.dateField;
|
|
14045
|
+
const dateEndField = this.config.dateEndField ?? null;
|
|
14046
|
+
const valueField = this.config.valueField ?? null;
|
|
13745
14047
|
const aggregation = this.config.aggregation ?? 'count';
|
|
13746
14048
|
const parsedRows = [];
|
|
13747
14049
|
for (const row of filteredRows) {
|
|
@@ -13753,14 +14055,13 @@ class HeatmapWidgetComponent {
|
|
|
13753
14055
|
for (const cell of cells) {
|
|
13754
14056
|
if (!cell?.attributePath)
|
|
13755
14057
|
continue;
|
|
13756
|
-
|
|
13757
|
-
if (sanitizedPath === dateField) {
|
|
14058
|
+
if (matchesAttributePath(cell.attributePath, dateField)) {
|
|
13758
14059
|
dateFrom = this.parseDate(cell.value);
|
|
13759
14060
|
}
|
|
13760
|
-
else if (dateEndField &&
|
|
14061
|
+
else if (dateEndField && matchesAttributePath(cell.attributePath, dateEndField)) {
|
|
13761
14062
|
dateTo = this.parseDate(cell.value);
|
|
13762
14063
|
}
|
|
13763
|
-
else if (valueField &&
|
|
14064
|
+
else if (valueField && matchesAttributePath(cell.attributePath, valueField)) {
|
|
13764
14065
|
const val = cell.value;
|
|
13765
14066
|
numericValue = typeof val === 'number' ? val : parseFloat(String(val));
|
|
13766
14067
|
if (isNaN(numericValue))
|
|
@@ -13946,9 +14247,6 @@ class HeatmapWidgetComponent {
|
|
|
13946
14247
|
const day = date.getUTCDate().toString().padStart(2, '0');
|
|
13947
14248
|
return `${year}-${month}-${day}`;
|
|
13948
14249
|
}
|
|
13949
|
-
sanitizeFieldName(fieldName) {
|
|
13950
|
-
return fieldName.replace(/\./g, '_');
|
|
13951
|
-
}
|
|
13952
14250
|
convertFiltersToDto(filters) {
|
|
13953
14251
|
const variables = this.stateService.getVariables();
|
|
13954
14252
|
return this.variableService.convertToFieldFilterDto(filters, variables);
|
|
@@ -14082,7 +14380,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
14082
14380
|
}] } });
|
|
14083
14381
|
|
|
14084
14382
|
class HeatmapConfigDialogComponent {
|
|
14085
|
-
|
|
14383
|
+
getRuntimeQueryColumnsGQL = inject(GetRuntimeQueryColumnsDtoGQL);
|
|
14086
14384
|
stateService = inject(MeshBoardStateService);
|
|
14087
14385
|
windowRef = inject(WindowRef);
|
|
14088
14386
|
querySelector;
|
|
@@ -14211,10 +14509,10 @@ class HeatmapConfigDialogComponent {
|
|
|
14211
14509
|
async loadQueryColumns(queryRtId) {
|
|
14212
14510
|
this.isLoadingColumns = true;
|
|
14213
14511
|
try {
|
|
14214
|
-
|
|
14512
|
+
// Metadata-only — skips the row execution path on the backend.
|
|
14513
|
+
const result = await firstValueFrom(this.getRuntimeQueryColumnsGQL.fetch({
|
|
14215
14514
|
variables: {
|
|
14216
|
-
rtId: queryRtId
|
|
14217
|
-
first: 1
|
|
14515
|
+
rtId: queryRtId
|
|
14218
14516
|
}
|
|
14219
14517
|
}));
|
|
14220
14518
|
const queryItems = result.data?.runtime?.runtimeQuery?.items ?? [];
|
|
@@ -14223,8 +14521,9 @@ class HeatmapConfigDialogComponent {
|
|
|
14223
14521
|
const filteredColumns = columns
|
|
14224
14522
|
.filter((c) => c !== null);
|
|
14225
14523
|
this.queryColumns = filteredColumns.map(c => ({
|
|
14226
|
-
attributePath:
|
|
14227
|
-
attributeValueType: c.attributeValueType ?? ''
|
|
14524
|
+
attributePath: c.attributePath ?? '',
|
|
14525
|
+
attributeValueType: c.attributeValueType ?? '',
|
|
14526
|
+
aggregationType: c.aggregationType ?? null
|
|
14228
14527
|
}));
|
|
14229
14528
|
const numericTypes = ['INTEGER', 'FLOAT', 'DOUBLE', 'DECIMAL', 'LONG'];
|
|
14230
14529
|
const dateTimeTypes = ['DATE_TIME', 'DATETIME', 'DATE'];
|
|
@@ -14251,9 +14550,6 @@ class HeatmapConfigDialogComponent {
|
|
|
14251
14550
|
this.isLoadingColumns = false;
|
|
14252
14551
|
}
|
|
14253
14552
|
}
|
|
14254
|
-
sanitizeFieldName(fieldName) {
|
|
14255
|
-
return fieldName.replace(/\./g, '_');
|
|
14256
|
-
}
|
|
14257
14553
|
onFiltersChange(updatedFilters) {
|
|
14258
14554
|
this.filters = updatedFilters;
|
|
14259
14555
|
}
|
|
@@ -16306,7 +16602,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
16306
16602
|
class WidgetGroupConfigDialogComponent {
|
|
16307
16603
|
ckTypeSelectorService = inject(CkTypeSelectorService);
|
|
16308
16604
|
attributeSelectorService = inject(AttributeSelectorService);
|
|
16309
|
-
|
|
16605
|
+
getRuntimeQueryColumnsGQL = inject(GetRuntimeQueryColumnsDtoGQL);
|
|
16310
16606
|
getCkTypeAvailableQueryColumnsGQL = inject(GetCkTypeAvailableQueryColumnsDtoGQL);
|
|
16311
16607
|
meshBoardStateService = inject(MeshBoardStateService);
|
|
16312
16608
|
windowRef = inject(WindowRef);
|
|
@@ -16472,10 +16768,10 @@ class WidgetGroupConfigDialogComponent {
|
|
|
16472
16768
|
async loadQueryColumns(queryRtId) {
|
|
16473
16769
|
this.isLoadingColumns = true;
|
|
16474
16770
|
try {
|
|
16475
|
-
|
|
16771
|
+
// Metadata-only — skips the row execution path on the backend.
|
|
16772
|
+
const result = await firstValueFrom(this.getRuntimeQueryColumnsGQL.fetch({
|
|
16476
16773
|
variables: {
|
|
16477
|
-
rtId: queryRtId
|
|
16478
|
-
first: 1
|
|
16774
|
+
rtId: queryRtId
|
|
16479
16775
|
}
|
|
16480
16776
|
}));
|
|
16481
16777
|
const queryItems = result.data?.runtime?.runtimeQuery?.items ?? [];
|
|
@@ -16485,8 +16781,9 @@ class WidgetGroupConfigDialogComponent {
|
|
|
16485
16781
|
this.availableColumns = columns
|
|
16486
16782
|
.filter((c) => c !== null)
|
|
16487
16783
|
.map(c => ({
|
|
16488
|
-
attributePath: c.attributePath
|
|
16489
|
-
attributeValueType: c.attributeValueType ?? ''
|
|
16784
|
+
attributePath: c.attributePath ?? '',
|
|
16785
|
+
attributeValueType: c.attributeValueType ?? '',
|
|
16786
|
+
aggregationType: c.aggregationType ?? null
|
|
16490
16787
|
}));
|
|
16491
16788
|
this.filteredColumns.set(this.availableColumns);
|
|
16492
16789
|
}
|
|
@@ -16541,7 +16838,8 @@ class WidgetGroupConfigDialogComponent {
|
|
|
16541
16838
|
.filter((c) => c !== null)
|
|
16542
16839
|
.map(c => ({
|
|
16543
16840
|
attributePath: c.attributePath || '',
|
|
16544
|
-
attributeValueType: c.attributeValueType
|
|
16841
|
+
attributeValueType: c.attributeValueType,
|
|
16842
|
+
aggregationType: null
|
|
16545
16843
|
}));
|
|
16546
16844
|
this.availableColumns = mappedColumns;
|
|
16547
16845
|
this.filteredColumns.set(mappedColumns);
|