@finos/legend-query-builder 4.14.26 → 4.14.28

Sign up to get free protection for your applications and to get access to all the features.
@@ -14,11 +14,10 @@
14
14
  * limitations under the License.
15
15
  */
16
16
 
17
- import { clsx } from '@finos/legend-art';
18
17
  import { observer } from 'mobx-react-lite';
19
18
  import type { QueryBuilderState } from '../../../stores/QueryBuilderState.js';
20
19
  import { PRIMITIVE_TYPE, type TDSExecutionResult } from '@finos/legend-graph';
21
- import { useState, useCallback } from 'react';
20
+ import { useState, useCallback, useEffect } from 'react';
22
21
  import {
23
22
  DataGrid,
24
23
  type DataGridApi,
@@ -28,23 +27,45 @@ import {
28
27
  type DataGridGetContextMenuItemsParams,
29
28
  type DataGridIRowNode,
30
29
  type DataGridMenuItemDef,
30
+ type DataGridIAggFuncParams,
31
31
  } from '@finos/legend-lego/data-grid';
32
32
  import {
33
33
  getRowDataFromExecutionResult,
34
34
  type IQueryRendererParamsWithGridType,
35
35
  filterByOrOutValues,
36
36
  } from './QueryBuilderTDSResultShared.js';
37
- import type {
38
- QueryBuilderResultState,
39
- QueryBuilderTDSResultCellData,
40
- QueryBuilderTDSResultCellDataType,
41
- QueryBuilderTDSRowDataType,
37
+ import {
38
+ type QueryBuilderResultState,
39
+ QueryBuilderResultWavgAggregationState,
40
+ type QueryBuilderTDSResultCellData,
41
+ type QueryBuilderTDSResultCellDataType,
42
+ type QueryBuilderTDSRowDataType,
42
43
  } from '../../../stores/QueryBuilderResultState.js';
43
44
  import { QueryBuilderTDSState } from '../../../stores/fetch-structure/tds/QueryBuilderTDSState.js';
44
45
  import { DEFAULT_LOCALE } from '../../../graph-manager/QueryBuilderConst.js';
45
- import { isNumber, isString, isValidURL } from '@finos/legend-shared';
46
+ import {
47
+ assertErrorThrown,
48
+ isNumber,
49
+ isString,
50
+ isValidURL,
51
+ } from '@finos/legend-shared';
46
52
  import { useApplicationStore } from '@finos/legend-application';
47
53
  import { QUERY_BUILDER_TEST_ID } from '../../../__lib__/QueryBuilderTesting.js';
54
+ import {
55
+ clsx,
56
+ Modal,
57
+ ModalBody,
58
+ ModalHeader,
59
+ ModalFooter,
60
+ ModalFooterButton,
61
+ Dialog,
62
+ CustomSelectorInput,
63
+ } from '@finos/legend-art';
64
+
65
+ export const enum QueryBuilderDataGridCustomAggregationFunction {
66
+ wavg = 'wavg',
67
+ WAVG = 'WAVG',
68
+ }
48
69
 
49
70
  const getAggregationTDSColumnCustomizations = (
50
71
  result: TDSExecutionResult,
@@ -71,7 +92,7 @@ const getAggregationTDSColumnCustomizations = (
71
92
  case PRIMITIVE_TYPE.FLOAT:
72
93
  return {
73
94
  filter: 'agNumberColumnFilter',
74
- allowedAggFuncs: ['count', 'sum', 'max', 'min', 'avg'],
95
+ allowedAggFuncs: ['count', 'sum', 'max', 'min', 'avg', 'wavg'],
75
96
  };
76
97
  default:
77
98
  return {
@@ -222,16 +243,19 @@ export const QueryBuilderTDSGridResult = observer(
222
243
  }) => {
223
244
  const { executionResult, queryBuilderState } = props;
224
245
  const applicationStore = useApplicationStore();
246
+ const darkMode =
247
+ !applicationStore.layoutService.TEMPORARY__isLightColorThemeEnabled;
225
248
  const [columnAPi, setColumnApi] = useState<DataGridColumnApi | undefined>(
226
249
  undefined,
227
250
  );
251
+ const [aggFuncParams, setAggFuncParams] = useState<
252
+ DataGridIAggFuncParams | undefined
253
+ >(undefined);
228
254
  const resultState = queryBuilderState.resultState;
229
255
  const isLocalModeEnabled = queryBuilderState.isLocalModeEnabled;
230
256
  const colDefs = isLocalModeEnabled
231
257
  ? getLocalColDefs(executionResult, resultState)
232
258
  : getColDefs(executionResult, resultState);
233
- const darkMode =
234
- !applicationStore.layoutService.TEMPORARY__isLightColorThemeEnabled;
235
259
 
236
260
  const onSaveGridColumnState = (): void => {
237
261
  if (!columnAPi) {
@@ -242,6 +266,10 @@ export const QueryBuilderTDSGridResult = observer(
242
266
  isPivotModeEnabled: columnAPi.isPivotMode(),
243
267
  isLocalModeEnabled: true,
244
268
  previewLimit: resultState.previewLimit,
269
+ ...(resultState.wavgAggregationState?.weightedColumnIdPairs && {
270
+ weightedColumnPairs:
271
+ resultState.wavgAggregationState.weightedColumnIdPairs,
272
+ }),
245
273
  });
246
274
  };
247
275
 
@@ -336,6 +364,103 @@ export const QueryBuilderTDSGridResult = observer(
336
364
  ],
337
365
  );
338
366
 
367
+ const weightedColumnOptions = columnAPi
368
+ ?.getColumns()
369
+ ?.filter((c) => c.getColDef().cellDataType === 'number')
370
+ .map((col) => ({
371
+ label: col.getColId(),
372
+ value: col.getColId(),
373
+ }));
374
+
375
+ const selectedWeightedColumn =
376
+ aggFuncParams?.colDef.field &&
377
+ resultState.wavgAggregationState?.weightedColumnIdPairs.get(
378
+ aggFuncParams.colDef.field,
379
+ )
380
+ ? {
381
+ label: resultState.wavgAggregationState.weightedColumnIdPairs.get(
382
+ aggFuncParams.colDef.field,
383
+ ),
384
+ value: resultState.wavgAggregationState.weightedColumnIdPairs.get(
385
+ aggFuncParams.colDef.field,
386
+ ),
387
+ }
388
+ : null;
389
+
390
+ const onWeightedColumnOptionChange = async (
391
+ option: { label: string; value: string } | null,
392
+ ): Promise<void> => {
393
+ if (aggFuncParams?.colDef.field && option?.value) {
394
+ resultState.wavgAggregationState?.addWeightedColumnIdPair(
395
+ aggFuncParams.colDef.field,
396
+ option.value,
397
+ );
398
+ }
399
+ };
400
+
401
+ const weightedAverage = (param: DataGridIAggFuncParams): void => {
402
+ if (param.colDef.field) {
403
+ if (!resultState.wavgAggregationState) {
404
+ resultState.setWavgAggregationState(
405
+ new QueryBuilderResultWavgAggregationState(),
406
+ );
407
+ }
408
+ resultState.wavgAggregationState?.addWeightedColumnIdPair(
409
+ param.colDef.field,
410
+ param.colDef.field,
411
+ );
412
+ resultState.wavgAggregationState?.setIsApplyingWavg(true);
413
+ setAggFuncParams(param);
414
+ } else {
415
+ applicationStore.notificationService.notifyError(
416
+ 'The id of this column can`t be retrieved to perform weighted average',
417
+ );
418
+ }
419
+ };
420
+
421
+ const weightedAverageHelper = (param: DataGridIAggFuncParams): number => {
422
+ try {
423
+ const column = param.colDef.field;
424
+ if (column) {
425
+ const weightedColumnId =
426
+ resultState.wavgAggregationState?.weightedColumnIdPairs.get(column);
427
+ if (weightedColumnId) {
428
+ const weightedColumnSum = param.rowNode.allLeafChildren
429
+ .map((node) => node.data[weightedColumnId])
430
+ .reduce((a, b) => a + b) as number;
431
+ const weightedColumnMultiply = param.rowNode.allLeafChildren
432
+ .map((node) => node.data[weightedColumnId] * node.data[column])
433
+ .reduce((a, b) => a + b);
434
+ if (weightedColumnSum !== 0) {
435
+ onSaveGridColumnState();
436
+ return weightedColumnMultiply / weightedColumnSum;
437
+ } else {
438
+ applicationStore.notificationService.notifyError(
439
+ 'The weighted column sum is 0',
440
+ );
441
+ }
442
+ } else {
443
+ applicationStore.notificationService.notifyError(
444
+ 'The weighted column Id is not defined',
445
+ );
446
+ }
447
+ }
448
+ } catch (error) {
449
+ assertErrorThrown(error);
450
+ applicationStore.notificationService.notifyError(error);
451
+ }
452
+ return -1;
453
+ };
454
+
455
+ useEffect(() => {
456
+ if (aggFuncParams) {
457
+ aggFuncParams.columnApi.setColumnAggFunc(
458
+ aggFuncParams.colDef.field!,
459
+ QueryBuilderDataGridCustomAggregationFunction.WAVG,
460
+ );
461
+ }
462
+ }, [resultState.wavgAggregationState, aggFuncParams]);
463
+
339
464
  return (
340
465
  <div
341
466
  data-testid={QUERY_BUILDER_TEST_ID.QUERY_BUILDER_RESULT_VALUES_TDS}
@@ -372,6 +497,10 @@ export const QueryBuilderTDSGridResult = observer(
372
497
  suppressFieldDotNotation={true}
373
498
  suppressContextMenu={false}
374
499
  columnDefs={colDefs}
500
+ aggFuncs={{
501
+ wavg: weightedAverage,
502
+ WAVG: weightedAverageHelper,
503
+ }}
375
504
  sideBar={['columns', 'filters']}
376
505
  onColumnVisible={onSaveGridColumnState}
377
506
  onColumnPinned={onSaveGridColumnState}
@@ -411,6 +540,54 @@ export const QueryBuilderTDSGridResult = observer(
411
540
  }
412
541
  />
413
542
  )}
543
+ {resultState.wavgAggregationState?.isApplyingWavg && (
544
+ <Dialog
545
+ open={resultState.wavgAggregationState.isApplyingWavg}
546
+ onClose={() =>
547
+ resultState.wavgAggregationState?.setIsApplyingWavg(false)
548
+ }
549
+ classes={{
550
+ root: 'editor-modal__root-container',
551
+ container: 'editor-modal__container',
552
+ paper: 'editor-modal__content',
553
+ }}
554
+ >
555
+ <Modal
556
+ darkMode={
557
+ !applicationStore.layoutService
558
+ .TEMPORARY__isLightColorThemeEnabled
559
+ }
560
+ className="query-editor__blocking-alert"
561
+ >
562
+ <ModalHeader title="Applying Weighted Average" />
563
+ <ModalBody>
564
+ <div className="query-builder__result__tds-grid__text">
565
+ choose a weighted column from dropdown
566
+ </div>
567
+ <CustomSelectorInput
568
+ options={weightedColumnOptions}
569
+ onChange={onWeightedColumnOptionChange}
570
+ value={selectedWeightedColumn}
571
+ placeholder={'Choose a weighted column'}
572
+ darkMode={
573
+ !applicationStore.layoutService
574
+ .TEMPORARY__isLightColorThemeEnabled
575
+ }
576
+ />
577
+ </ModalBody>
578
+ <ModalFooter>
579
+ <ModalFooterButton
580
+ onClick={() => {
581
+ resultState.wavgAggregationState?.setIsApplyingWavg(
582
+ false,
583
+ );
584
+ }}
585
+ text="Apply"
586
+ />
587
+ </ModalFooter>
588
+ </Modal>
589
+ </Dialog>
590
+ )}
414
591
  </div>
415
592
  </div>
416
593
  );
@@ -48,6 +48,7 @@ import { QUERY_BUILDER_EVENT } from '../__lib__/QueryBuilderEvent.js';
48
48
  import { ExecutionPlanState } from './execution-plan/ExecutionPlanState.js';
49
49
  import type { DataGridColumnState } from '@finos/legend-lego/data-grid';
50
50
  import { downloadStream } from '@finos/legend-application';
51
+ import { QueryBuilderDataGridCustomAggregationFunction } from '../components/result/tds/QueryBuilderTDSGridResult.js';
51
52
 
52
53
  export const DEFAULT_LIMIT = 1000;
53
54
 
@@ -83,8 +84,37 @@ type QueryBuilderDataGridConfig = {
83
84
  isPivotModeEnabled: boolean | undefined;
84
85
  isLocalModeEnabled: boolean | undefined;
85
86
  previewLimit?: number | undefined;
87
+ weightedColumnPairs?: Map<string, string> | undefined;
86
88
  };
87
89
 
90
+ export class QueryBuilderResultWavgAggregationState {
91
+ isApplyingWavg = false;
92
+ weightedColumnIdPairs: Map<string, string>;
93
+
94
+ constructor() {
95
+ makeObservable(this, {
96
+ isApplyingWavg: observable,
97
+ weightedColumnIdPairs: observable,
98
+ setIsApplyingWavg: action,
99
+ addWeightedColumnIdPair: action,
100
+ removeWeightedColumnIdPair: action,
101
+ });
102
+ this.weightedColumnIdPairs = new Map<string, string>();
103
+ }
104
+
105
+ setIsApplyingWavg(val: boolean): void {
106
+ this.isApplyingWavg = val;
107
+ }
108
+
109
+ addWeightedColumnIdPair(col: string, weightedColumnId: string): void {
110
+ this.weightedColumnIdPairs.set(col, weightedColumnId);
111
+ }
112
+
113
+ removeWeightedColumnIdPair(col: string): void {
114
+ this.weightedColumnIdPairs.delete(col);
115
+ }
116
+ }
117
+
88
118
  export class QueryBuilderResultState {
89
119
  readonly queryBuilderState: QueryBuilderState;
90
120
  readonly executionPlanState: ExecutionPlanState;
@@ -105,6 +135,7 @@ export class QueryBuilderResultState {
105
135
  isSelectingCells: boolean;
106
136
 
107
137
  gridConfig: QueryBuilderDataGridConfig | undefined;
138
+ wavgAggregationState: QueryBuilderResultWavgAggregationState | undefined;
108
139
 
109
140
  constructor(queryBuilderState: QueryBuilderState) {
110
141
  makeObservable(this, {
@@ -120,7 +151,9 @@ export class QueryBuilderResultState {
120
151
  isSelectingCells: observable,
121
152
  isQueryUsageViewerOpened: observable,
122
153
  gridConfig: observable,
154
+ wavgAggregationState: observable,
123
155
  setGridConfig: action,
156
+ setWavgAggregationState: action,
124
157
  setIsSelectingCells: action,
125
158
  setIsRunningQuery: action,
126
159
  setExecutionResult: action,
@@ -153,6 +186,12 @@ export class QueryBuilderResultState {
153
186
  this.gridConfig = val;
154
187
  }
155
188
 
189
+ setWavgAggregationState(
190
+ val: QueryBuilderResultWavgAggregationState | undefined,
191
+ ): void {
192
+ this.wavgAggregationState = val;
193
+ }
194
+
156
195
  setIsSelectingCells(val: boolean): void {
157
196
  this.isSelectingCells = val;
158
197
  }
@@ -199,11 +238,50 @@ export class QueryBuilderResultState {
199
238
  }
200
239
  }
201
240
 
241
+ processWeightedColumnPairsMap(
242
+ config: QueryGridConfig,
243
+ ): Map<string, string> | undefined {
244
+ if (config.weightedColumnPairs) {
245
+ const wavgColumns = config.columns
246
+ .filter(
247
+ (col) =>
248
+ (col as DataGridColumnState).aggFunc ===
249
+ QueryBuilderDataGridCustomAggregationFunction.WAVG,
250
+ )
251
+ .map((col) => (col as DataGridColumnState).colId);
252
+ const weightedColumnPairsMap = new Map<string, string>();
253
+ config.weightedColumnPairs.forEach((wc) => {
254
+ if (wc[0] && wc[1]) {
255
+ weightedColumnPairsMap.set(wc[0], wc[1]);
256
+ }
257
+ });
258
+ for (const wavgCol of weightedColumnPairsMap.keys()) {
259
+ if (!wavgColumns.includes(wavgCol)) {
260
+ weightedColumnPairsMap.delete(wavgCol);
261
+ }
262
+ }
263
+ return weightedColumnPairsMap;
264
+ }
265
+ return undefined;
266
+ }
267
+
202
268
  handlePreConfiguredGridConfig(config: QueryGridConfig): void {
203
- const newConfig = {
204
- ...config,
205
- columns: config.columns as DataGridColumnState[],
206
- };
269
+ let newConfig;
270
+ const weightedColumnPairsMap = this.processWeightedColumnPairsMap(config);
271
+ if (weightedColumnPairsMap) {
272
+ this.wavgAggregationState = new QueryBuilderResultWavgAggregationState();
273
+ this.wavgAggregationState.weightedColumnIdPairs = weightedColumnPairsMap;
274
+ newConfig = {
275
+ ...config,
276
+ weightedColumnPairs: weightedColumnPairsMap,
277
+ columns: config.columns as DataGridColumnState[],
278
+ };
279
+ } else {
280
+ newConfig = {
281
+ ...config,
282
+ columns: config.columns as DataGridColumnState[],
283
+ };
284
+ }
207
285
  if (config.previewLimit) {
208
286
  this.setPreviewLimit(config.previewLimit);
209
287
  }
@@ -32,6 +32,7 @@ import {
32
32
  ActionState,
33
33
  hashArray,
34
34
  assertTrue,
35
+ assertNonNullable,
35
36
  } from '@finos/legend-shared';
36
37
  import { QueryBuilderFilterState } from './filter/QueryBuilderFilterState.js';
37
38
  import { QueryBuilderFetchStructureState } from './fetch-structure/QueryBuilderFetchStructureState.js';
@@ -54,6 +55,7 @@ import {
54
55
  type ValueSpecification,
55
56
  type Type,
56
57
  type QueryGridConfig,
58
+ type QueryExecutionContext,
57
59
  GRAPH_MANAGER_EVENT,
58
60
  CompilationError,
59
61
  extractSourceInformationCoordinates,
@@ -73,6 +75,7 @@ import {
73
75
  InstanceValue,
74
76
  Multiplicity,
75
77
  RuntimePointer,
78
+ QueryExplicitExecutionContext,
76
79
  } from '@finos/legend-graph';
77
80
  import { buildLambdaFunction } from './QueryBuilderValueSpecificationBuilder.js';
78
81
  import type {
@@ -287,6 +290,24 @@ export abstract class QueryBuilderState implements CommandRegistrar {
287
290
  return this.allVariables.map((e) => e.name);
288
291
  }
289
292
 
293
+ getQueryExecutionContext(): QueryExecutionContext {
294
+ const queryExeContext = new QueryExplicitExecutionContext();
295
+ const runtimeValue = guaranteeType(
296
+ this.executionContextState.runtimeValue,
297
+ RuntimePointer,
298
+ 'Query runtime must be of type runtime pointer',
299
+ );
300
+ assertNonNullable(
301
+ this.executionContextState.mapping,
302
+ 'Query required mapping to update',
303
+ );
304
+ queryExeContext.mapping = PackageableElementExplicitReference.create(
305
+ this.executionContextState.mapping,
306
+ );
307
+ queryExeContext.runtime = runtimeValue.packageableRuntime;
308
+ return queryExeContext;
309
+ }
310
+
290
311
  /**
291
312
  * Gets information about the current queryBuilderState.
292
313
  * This information can be used as a part of analytics