@finos/legend-query-builder 4.11.4 → 4.11.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. package/lib/components/QueryBuilder.js +1 -1
  2. package/lib/components/QueryBuilder.js.map +1 -1
  3. package/lib/components/execution-plan/SQLExecutionNodeViewer.js +1 -1
  4. package/lib/components/execution-plan/SQLExecutionNodeViewer.js.map +1 -1
  5. package/lib/components/{QueryBuilderResultPanel.d.ts → result/QueryBuilderResultPanel.d.ts} +1 -2
  6. package/lib/components/result/QueryBuilderResultPanel.d.ts.map +1 -0
  7. package/lib/components/result/QueryBuilderResultPanel.js +185 -0
  8. package/lib/components/result/QueryBuilderResultPanel.js.map +1 -0
  9. package/lib/components/result/tds/QueryBuilderTDSGridResult.d.ts +24 -0
  10. package/lib/components/result/tds/QueryBuilderTDSGridResult.d.ts.map +1 -0
  11. package/lib/components/result/tds/QueryBuilderTDSGridResult.js +140 -0
  12. package/lib/components/result/tds/QueryBuilderTDSGridResult.js.map +1 -0
  13. package/lib/components/result/tds/QueryBuilderTDSResultShared.d.ts +41 -0
  14. package/lib/components/result/tds/QueryBuilderTDSResultShared.d.ts.map +1 -0
  15. package/lib/components/result/tds/QueryBuilderTDSResultShared.js +465 -0
  16. package/lib/components/result/tds/QueryBuilderTDSResultShared.js.map +1 -0
  17. package/lib/components/result/tds/QueryBuilderTDSSimpleGridResult.d.ts +24 -0
  18. package/lib/components/result/tds/QueryBuilderTDSSimpleGridResult.d.ts.map +1 -0
  19. package/lib/components/result/tds/QueryBuilderTDSSimpleGridResult.js +47 -0
  20. package/lib/components/result/tds/QueryBuilderTDSSimpleGridResult.js.map +1 -0
  21. package/lib/index.css +2 -2
  22. package/lib/index.css.map +1 -1
  23. package/lib/package.json +1 -1
  24. package/lib/stores/QueryBuilderResultState.d.ts +5 -1
  25. package/lib/stores/QueryBuilderResultState.d.ts.map +1 -1
  26. package/lib/stores/QueryBuilderResultState.js.map +1 -1
  27. package/package.json +2 -2
  28. package/src/components/QueryBuilder.tsx +1 -1
  29. package/src/components/execution-plan/SQLExecutionNodeViewer.tsx +1 -1
  30. package/src/components/result/QueryBuilderResultPanel.tsx +570 -0
  31. package/src/components/result/tds/QueryBuilderTDSGridResult.tsx +239 -0
  32. package/src/components/result/tds/QueryBuilderTDSResultShared.tsx +866 -0
  33. package/src/components/result/tds/QueryBuilderTDSSimpleGridResult.tsx +80 -0
  34. package/src/stores/QueryBuilderResultState.ts +12 -1
  35. package/tsconfig.json +4 -1
  36. package/lib/components/QueryBuilderResultPanel.d.ts.map +0 -1
  37. package/lib/components/QueryBuilderResultPanel.js +0 -633
  38. package/lib/components/QueryBuilderResultPanel.js.map +0 -1
  39. package/src/components/QueryBuilderResultPanel.tsx +0 -1412
@@ -0,0 +1,866 @@
1
+ /**
2
+ * Copyright (c) 2020-present, Goldman Sachs
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import {
18
+ MenuContent,
19
+ MenuContentItem,
20
+ ContextMenu,
21
+ clsx,
22
+ MenuContentDivider,
23
+ } from '@finos/legend-art';
24
+ import { observer } from 'mobx-react-lite';
25
+ import { flowResult } from 'mobx';
26
+ import {
27
+ type Enumeration,
28
+ InstanceValue,
29
+ TDSExecutionResult,
30
+ EnumValueInstanceValue,
31
+ EnumValueExplicitReference,
32
+ getTDSRowRankByColumnInAsc,
33
+ PRIMITIVE_TYPE,
34
+ type ExecutionResult,
35
+ RelationalExecutionActivities,
36
+ } from '@finos/legend-graph';
37
+ import { format as formatSQL } from 'sql-formatter';
38
+ import { useApplicationStore } from '@finos/legend-application';
39
+ import {
40
+ assertErrorThrown,
41
+ guaranteeNonNullable,
42
+ isValidURL,
43
+ isString,
44
+ isNumber,
45
+ filterByType,
46
+ isBoolean,
47
+ type PlainObject,
48
+ } from '@finos/legend-shared';
49
+ import { forwardRef } from 'react';
50
+
51
+ import {
52
+ QueryBuilderDerivationProjectionColumnState,
53
+ QueryBuilderProjectionColumnState,
54
+ } from '../../../stores/fetch-structure/tds/projection/QueryBuilderProjectionColumnState.js';
55
+ import {
56
+ type QueryBuilderPostFilterTreeNodeData,
57
+ PostFilterConditionState,
58
+ QueryBuilderPostFilterTreeConditionNodeData,
59
+ PostFilterValueSpecConditionValueState,
60
+ } from '../../../stores/fetch-structure/tds/post-filter/QueryBuilderPostFilterState.js';
61
+ import {
62
+ QueryBuilderPostFilterOperator_Equal,
63
+ QueryBuilderPostFilterOperator_NotEqual,
64
+ } from '../../../stores/fetch-structure/tds/post-filter/operators/QueryBuilderPostFilterOperator_Equal.js';
65
+ import {
66
+ QueryBuilderPostFilterOperator_In,
67
+ QueryBuilderPostFilterOperator_NotIn,
68
+ } from '../../../stores/fetch-structure/tds/post-filter/operators/QueryBuilderPostFilterOperator_In.js';
69
+ import type { QueryBuilderPostFilterOperator } from '../../../stores/fetch-structure/tds/post-filter/QueryBuilderPostFilterOperator.js';
70
+ import { QueryBuilderTDSState } from '../../../stores/fetch-structure/tds/QueryBuilderTDSState.js';
71
+ import {
72
+ instanceValue_setValue,
73
+ instanceValue_setValues,
74
+ } from '../../../stores/shared/ValueSpecificationModifierHelper.js';
75
+ import {
76
+ isEnterpriseModeEnabled,
77
+ type DataGridApi,
78
+ type DataGridCellRendererParams,
79
+ } from '@finos/legend-lego/data-grid';
80
+ import type {
81
+ QueryBuilderTDSResultCellCoordinate,
82
+ QueryBuilderResultState,
83
+ QueryBuilderTDSResultCellData,
84
+ QueryBuilderTDSRowDataType,
85
+ QueryBuilderTDSResultCellDataType,
86
+ } from '../../../stores/QueryBuilderResultState.js';
87
+ import {
88
+ QueryBuilderPostFilterOperator_IsEmpty,
89
+ QueryBuilderPostFilterOperator_IsNotEmpty,
90
+ } from '../../../stores/fetch-structure/tds/post-filter/operators/QueryBuilderPostFilterOperator_IsEmpty.js';
91
+ import { DEFAULT_LOCALE } from '../../../graph-manager/QueryBuilderConst.js';
92
+
93
+ export const tryToFormatSql = (sql: string): string => {
94
+ try {
95
+ const formattedSql = formatSQL(sql, { language: 'mysql' });
96
+ return formattedSql;
97
+ } catch {
98
+ try {
99
+ const formattedSql = formatSQL(sql);
100
+ return formattedSql;
101
+ } catch {
102
+ return sql;
103
+ }
104
+ }
105
+ };
106
+
107
+ export const getExecutedSqlFromExecutionResult = (
108
+ execResult: ExecutionResult,
109
+ ): string | undefined => {
110
+ const executedSqls = execResult.activities
111
+ ?.filter(filterByType(RelationalExecutionActivities))
112
+ .map((relationalActivity) => relationalActivity.sql);
113
+ if (executedSqls?.length) {
114
+ let executedSql = '';
115
+ if (executedSqls.length > 1) {
116
+ for (let i = 0; i < executedSqls.length; i++) {
117
+ executedSql += `\n--QUERY #${i + 1}\n`;
118
+ executedSql += `${executedSqls[i]}\n`;
119
+ }
120
+ } else {
121
+ executedSql += executedSqls[0];
122
+ }
123
+ return executedSql;
124
+ }
125
+ return undefined;
126
+ };
127
+
128
+ export const getAggregationTDSColumnCustomizations = (
129
+ result: TDSExecutionResult,
130
+ columnName: string,
131
+ ): object | undefined => {
132
+ const columnType = result.builder.columns.find(
133
+ (col) => col.name === columnName,
134
+ )?.type;
135
+ switch (columnType) {
136
+ case PRIMITIVE_TYPE.STRING:
137
+ return {
138
+ filter: 'agTextColumnFilter',
139
+ allowedAggFuncs: ['count'],
140
+ };
141
+ case PRIMITIVE_TYPE.DATE:
142
+ case PRIMITIVE_TYPE.DATETIME:
143
+ case PRIMITIVE_TYPE.STRICTDATE:
144
+ return {
145
+ filter: 'agDateColumnFilter',
146
+ allowedAggFuncs: ['count'],
147
+ };
148
+ case PRIMITIVE_TYPE.DECIMAL:
149
+ case PRIMITIVE_TYPE.INTEGER:
150
+ case PRIMITIVE_TYPE.FLOAT:
151
+ return {
152
+ filter: 'agNumberColumnFilter',
153
+ allowedAggFuncs: ['count', 'sum', 'max', 'min', 'avg'],
154
+ };
155
+ default:
156
+ return {
157
+ allowedAggFuncs: ['count'],
158
+ };
159
+ }
160
+ };
161
+
162
+ export const getRowDataFromExecutionResult = (
163
+ executionResult: TDSExecutionResult,
164
+ ): PlainObject<QueryBuilderTDSRowDataType>[] => {
165
+ const rowData = executionResult.result.rows.map((_row, rowIdx) => {
166
+ const row: PlainObject = {};
167
+ const cols = executionResult.result.columns;
168
+ _row.values.forEach((value, colIdx) => {
169
+ // `ag-grid` shows `false` value as empty string so we have
170
+ // call `.toString()` to avoid this behavior.
171
+ // See https://github.com/finos/legend-studio/issues/1008
172
+ row[cols[colIdx] as string] = isBoolean(value) ? String(value) : value;
173
+ });
174
+
175
+ row.rowNumber = rowIdx;
176
+ return row;
177
+ });
178
+ return rowData;
179
+ };
180
+
181
+ export type IQueryRendererParamsWithGridType = DataGridCellRendererParams & {
182
+ resultState: QueryBuilderResultState;
183
+ tdsExecutionResult: TDSExecutionResult;
184
+ };
185
+
186
+ export const QueryBuilderGridResultContextMenu = observer(
187
+ forwardRef<
188
+ HTMLDivElement,
189
+ {
190
+ data: QueryBuilderTDSResultCellData | null;
191
+ tdsState: QueryBuilderTDSState;
192
+ dataGridApi: DataGridApi<QueryBuilderTDSRowDataType>;
193
+ }
194
+ >(function QueryBuilderResultContextMenu(props, ref) {
195
+ const { data, tdsState, dataGridApi } = props;
196
+
197
+ const applicationStore = useApplicationStore();
198
+ const postFilterEqualOperator = new QueryBuilderPostFilterOperator_Equal();
199
+ const postFilterInOperator = new QueryBuilderPostFilterOperator_In();
200
+ const postFilterEmptyOperator =
201
+ new QueryBuilderPostFilterOperator_IsEmpty();
202
+ const postFilterNotEmptyOperator =
203
+ new QueryBuilderPostFilterOperator_IsNotEmpty();
204
+
205
+ const postFilterNotEqualOperator =
206
+ new QueryBuilderPostFilterOperator_NotEqual();
207
+ const postFilterNotInOperator = new QueryBuilderPostFilterOperator_NotIn();
208
+ const postFilterState = tdsState.postFilterState;
209
+
210
+ const projectionColumnState = tdsState.projectionColumns
211
+ .filter((c) => c.columnName === data?.columnName)
212
+ .concat(
213
+ tdsState.aggregationState.columns
214
+ .filter((c) => c.columnName === data?.columnName)
215
+ .map((ag) => ag.projectionColumnState),
216
+ )[0];
217
+ const getExistingPostFilterNode = (
218
+ operators: QueryBuilderPostFilterOperator[],
219
+ projectionColumnName: string | undefined,
220
+ ): QueryBuilderPostFilterTreeNodeData | undefined =>
221
+ Array.from(postFilterState.nodes.values())
222
+ .filter(
223
+ (v) =>
224
+ v instanceof QueryBuilderPostFilterTreeConditionNodeData &&
225
+ v.condition.leftConditionValue instanceof
226
+ QueryBuilderProjectionColumnState,
227
+ )
228
+ .filter(
229
+ (n) =>
230
+ (n as QueryBuilderPostFilterTreeConditionNodeData).condition
231
+ .leftConditionValue.columnName ===
232
+ (projectionColumnName ?? projectionColumnState?.columnName) &&
233
+ operators
234
+ .map((op) => op.getLabel())
235
+ .includes(
236
+ (
237
+ n as QueryBuilderPostFilterTreeConditionNodeData
238
+ ).condition.operator.getLabel(),
239
+ ),
240
+ )[0];
241
+
242
+ const updateFilterConditionValue = (
243
+ conditionValue: InstanceValue,
244
+ cellData: QueryBuilderTDSResultCellData,
245
+ ): void => {
246
+ if (cellData.value) {
247
+ instanceValue_setValue(
248
+ conditionValue,
249
+ conditionValue instanceof EnumValueInstanceValue
250
+ ? EnumValueExplicitReference.create(
251
+ guaranteeNonNullable(
252
+ (
253
+ conditionValue.genericType?.ownerReference
254
+ .value as Enumeration
255
+ ).values.filter((v) => v.name === cellData.value)[0],
256
+ ),
257
+ )
258
+ : cellData.value,
259
+ 0,
260
+ tdsState.queryBuilderState.observerContext,
261
+ );
262
+ }
263
+ };
264
+
265
+ const generateNewPostFilterConditionNodeData = async (
266
+ operator: QueryBuilderPostFilterOperator,
267
+ cellData: QueryBuilderTDSResultCellData,
268
+ ): Promise<void> => {
269
+ let postFilterConditionState: PostFilterConditionState;
270
+ try {
271
+ const possibleProjectionColumnState = cellData.columnName
272
+ ? tdsState.projectionColumns
273
+ .filter((c) => c.columnName === cellData.columnName)
274
+ .concat(
275
+ tdsState.aggregationState.columns
276
+ .filter((c) => c.columnName === cellData.columnName)
277
+ .map((ag) => ag.projectionColumnState),
278
+ )[0]
279
+ : projectionColumnState;
280
+
281
+ if (possibleProjectionColumnState) {
282
+ postFilterConditionState = new PostFilterConditionState(
283
+ postFilterState,
284
+ possibleProjectionColumnState,
285
+ operator,
286
+ );
287
+
288
+ if (
289
+ projectionColumnState instanceof
290
+ QueryBuilderDerivationProjectionColumnState
291
+ ) {
292
+ await flowResult(
293
+ projectionColumnState.fetchDerivationLambdaReturnType(),
294
+ );
295
+ }
296
+
297
+ const defaultFilterConditionValue =
298
+ postFilterConditionState.operator.getDefaultFilterConditionValue(
299
+ postFilterConditionState,
300
+ );
301
+
302
+ postFilterConditionState.buildFromValueSpec(
303
+ defaultFilterConditionValue,
304
+ );
305
+ updateFilterConditionValue(
306
+ defaultFilterConditionValue as InstanceValue,
307
+ cellData,
308
+ );
309
+ postFilterState.addNodeFromNode(
310
+ new QueryBuilderPostFilterTreeConditionNodeData(
311
+ undefined,
312
+ postFilterConditionState,
313
+ ),
314
+ undefined,
315
+ );
316
+ }
317
+ } catch (error) {
318
+ assertErrorThrown(error);
319
+ applicationStore.notificationService.notifyWarning(error.message);
320
+ return;
321
+ }
322
+ };
323
+
324
+ const updateExistingPostFilterConditionNodeData = (
325
+ existingPostFilterNode: QueryBuilderPostFilterTreeNodeData,
326
+ isFilterBy: boolean,
327
+ cellData: QueryBuilderTDSResultCellData,
328
+ operator: QueryBuilderPostFilterOperator,
329
+ ): void => {
330
+ if (
331
+ operator === postFilterEmptyOperator ||
332
+ operator === postFilterNotEmptyOperator
333
+ ) {
334
+ const conditionState = (
335
+ existingPostFilterNode as QueryBuilderPostFilterTreeConditionNodeData
336
+ ).condition;
337
+ if (conditionState.operator.getLabel() !== operator.getLabel()) {
338
+ conditionState.changeOperator(
339
+ isFilterBy ? postFilterEmptyOperator : postFilterNotEmptyOperator,
340
+ );
341
+ }
342
+ return;
343
+ }
344
+ const conditionState = (
345
+ existingPostFilterNode as QueryBuilderPostFilterTreeConditionNodeData
346
+ ).condition;
347
+
348
+ const rightSide = conditionState.rightConditionValue;
349
+ if (rightSide instanceof PostFilterValueSpecConditionValueState) {
350
+ if (conditionState.operator.getLabel() === operator.getLabel()) {
351
+ const doesValueAlreadyExist =
352
+ rightSide.value instanceof InstanceValue &&
353
+ (rightSide.value instanceof EnumValueInstanceValue
354
+ ? rightSide.value.values.map((ef) => ef.value.name)
355
+ : rightSide.value.values
356
+ ).includes(cellData.value);
357
+
358
+ if (!doesValueAlreadyExist) {
359
+ const currentValueSpecificaton = rightSide.value;
360
+ const newValueSpecification =
361
+ conditionState.operator.getDefaultFilterConditionValue(
362
+ conditionState,
363
+ );
364
+ updateFilterConditionValue(
365
+ newValueSpecification as InstanceValue,
366
+ cellData,
367
+ );
368
+ conditionState.changeOperator(
369
+ isFilterBy ? postFilterInOperator : postFilterNotInOperator,
370
+ );
371
+ instanceValue_setValues(
372
+ rightSide.value as InstanceValue,
373
+ [currentValueSpecificaton, newValueSpecification],
374
+ tdsState.queryBuilderState.observerContext,
375
+ );
376
+ }
377
+ } else {
378
+ const doesValueAlreadyExist =
379
+ rightSide.value instanceof InstanceValue &&
380
+ rightSide.value.values
381
+ .filter((v) => v instanceof InstanceValue)
382
+ .map((v) =>
383
+ v instanceof EnumValueInstanceValue
384
+ ? v.values.map((ef) => ef.value.name)
385
+ : (v as InstanceValue).values,
386
+ )
387
+ .flat()
388
+ .includes(cellData.value ?? data?.value);
389
+
390
+ if (!doesValueAlreadyExist) {
391
+ const newValueSpecification = (
392
+ isFilterBy ? postFilterEqualOperator : postFilterNotEqualOperator
393
+ ).getDefaultFilterConditionValue(conditionState);
394
+ updateFilterConditionValue(
395
+ newValueSpecification as InstanceValue,
396
+ cellData,
397
+ );
398
+ instanceValue_setValues(
399
+ rightSide.value as InstanceValue,
400
+ [
401
+ ...(rightSide.value as InstanceValue).values,
402
+ newValueSpecification,
403
+ ],
404
+ tdsState.queryBuilderState.observerContext,
405
+ );
406
+ }
407
+ }
408
+ }
409
+ };
410
+
411
+ const getFilterOperator = (
412
+ isFilterBy: boolean,
413
+ cellData: QueryBuilderTDSResultCellData,
414
+ ): QueryBuilderPostFilterOperator => {
415
+ if (isFilterBy === true) {
416
+ if (cellData.value === null) {
417
+ return postFilterEmptyOperator;
418
+ } else {
419
+ return postFilterEqualOperator;
420
+ }
421
+ } else {
422
+ if (cellData.value === null) {
423
+ return postFilterNotEmptyOperator;
424
+ } else {
425
+ return postFilterNotEqualOperator;
426
+ }
427
+ }
428
+ };
429
+
430
+ const filterByOrOutValue = (
431
+ isFilterBy: boolean,
432
+ cellData: QueryBuilderTDSResultCellData,
433
+ ): void => {
434
+ tdsState.setShowPostFilterPanel(true);
435
+
436
+ const operator = getFilterOperator(isFilterBy, cellData);
437
+
438
+ const existingPostFilterNode = getExistingPostFilterNode(
439
+ cellData.value === null
440
+ ? [postFilterEmptyOperator, postFilterNotEmptyOperator]
441
+ : isFilterBy
442
+ ? [postFilterEqualOperator, postFilterInOperator]
443
+ : [postFilterNotEqualOperator, postFilterNotInOperator],
444
+ cellData.columnName,
445
+ );
446
+
447
+ existingPostFilterNode === undefined
448
+ ? generateNewPostFilterConditionNodeData(operator, cellData).catch(
449
+ applicationStore.alertUnhandledError,
450
+ )
451
+ : updateExistingPostFilterConditionNodeData(
452
+ existingPostFilterNode,
453
+ isFilterBy,
454
+ cellData,
455
+ operator,
456
+ );
457
+ };
458
+
459
+ const filterByOrOutValues = (isFilterBy: boolean): void => {
460
+ tdsState.queryBuilderState.resultState.selectedCells.forEach(
461
+ (cellData) => {
462
+ filterByOrOutValue(isFilterBy, cellData);
463
+ },
464
+ );
465
+ };
466
+
467
+ const handleCopyCellValue = isEnterpriseModeEnabled
468
+ ? (): void => {
469
+ dataGridApi.copySelectedRangeToClipboard();
470
+ }
471
+ : applicationStore.guardUnhandledError(() =>
472
+ applicationStore.clipboardService.copyTextToClipboard(
473
+ tdsState.queryBuilderState.resultState.selectedCells
474
+ .map((cellData) => cellData.value)
475
+ .join(','),
476
+ ),
477
+ );
478
+
479
+ const findRowFromRowIndex = (rowIndex: number): string => {
480
+ if (
481
+ !tdsState.queryBuilderState.resultState.executionResult ||
482
+ !(
483
+ tdsState.queryBuilderState.resultState.executionResult instanceof
484
+ TDSExecutionResult
485
+ )
486
+ ) {
487
+ return '';
488
+ }
489
+ // try to get the entire row value separated by comma
490
+ // rowData is in format of {columnName: value, columnName1: value, ...., rowNumber:value}
491
+ const valueArr: QueryBuilderTDSResultCellDataType[] = [];
492
+ Object.entries(
493
+ dataGridApi.getRenderedNodes().find((n) => n.rowIndex === rowIndex)
494
+ ?.data as QueryBuilderTDSRowDataType,
495
+ ).forEach((entry) => {
496
+ if (entry[0] !== 'rowNumber') {
497
+ valueArr.push(entry[1] as QueryBuilderTDSResultCellDataType);
498
+ }
499
+ });
500
+ return valueArr.join(',');
501
+ };
502
+
503
+ const handleCopyRowValue = isEnterpriseModeEnabled
504
+ ? (): void => {
505
+ dataGridApi.copySelectedRowsToClipboard();
506
+ }
507
+ : applicationStore.guardUnhandledError(() =>
508
+ applicationStore.clipboardService.copyTextToClipboard(
509
+ findRowFromRowIndex(
510
+ tdsState.queryBuilderState.resultState.selectedCells[0]
511
+ ?.coordinates.rowIndex ?? 0,
512
+ ),
513
+ ),
514
+ );
515
+
516
+ return (
517
+ <MenuContent ref={ref}>
518
+ <MenuContentItem
519
+ disabled={!projectionColumnState}
520
+ onClick={(): void => {
521
+ filterByOrOutValues(true);
522
+ }}
523
+ >
524
+ Filter By
525
+ </MenuContentItem>
526
+ <MenuContentItem
527
+ disabled={!projectionColumnState}
528
+ onClick={(): void => {
529
+ filterByOrOutValues(false);
530
+ }}
531
+ >
532
+ Filter Out
533
+ </MenuContentItem>
534
+ <MenuContentDivider />
535
+ <MenuContentItem onClick={handleCopyCellValue}>
536
+ Copy Cell Value
537
+ </MenuContentItem>
538
+ <MenuContentItem onClick={handleCopyRowValue}>
539
+ Copy Row Value
540
+ </MenuContentItem>
541
+ </MenuContent>
542
+ );
543
+ }),
544
+ );
545
+
546
+ export const QueryResultEnterpriseCellRenderer = observer(
547
+ (params: IQueryRendererParamsWithGridType) => {
548
+ const resultState = params.resultState;
549
+ const fetchStructureImplementation =
550
+ resultState.queryBuilderState.fetchStructureState.implementation;
551
+ const cellValue = params.value as QueryBuilderTDSResultCellDataType;
552
+ const formattedCellValue = (): QueryBuilderTDSResultCellDataType => {
553
+ if (isNumber(cellValue)) {
554
+ return Intl.NumberFormat(DEFAULT_LOCALE, {
555
+ maximumFractionDigits: 4,
556
+ }).format(Number(cellValue));
557
+ }
558
+ return cellValue;
559
+ };
560
+ const cellValueUrlLink =
561
+ isString(cellValue) && isValidURL(cellValue) ? cellValue : undefined;
562
+
563
+ const mouseDown: React.MouseEventHandler = (event) => {
564
+ event.preventDefault();
565
+ if (event.button === 0 || event.button === 2) {
566
+ resultState.setMouseOverCell(resultState.selectedCells[0] ?? null);
567
+ }
568
+ };
569
+ const mouseUp: React.MouseEventHandler = (event) => {
570
+ resultState.setIsSelectingCells(false);
571
+ };
572
+ const mouseOver: React.MouseEventHandler = (event) => {
573
+ resultState.setMouseOverCell(resultState.selectedCells[0] ?? null);
574
+ };
575
+
576
+ return (
577
+ <ContextMenu
578
+ content={
579
+ // NOTE: we only support this functionality for grid result with a projection fetch-structure
580
+ fetchStructureImplementation instanceof QueryBuilderTDSState ? (
581
+ <QueryBuilderGridResultContextMenu
582
+ data={resultState.mousedOverCell}
583
+ tdsState={fetchStructureImplementation}
584
+ dataGridApi={params.api}
585
+ />
586
+ ) : null
587
+ }
588
+ disabled={
589
+ !(
590
+ resultState.queryBuilderState.fetchStructureState
591
+ .implementation instanceof QueryBuilderTDSState
592
+ ) ||
593
+ !resultState.queryBuilderState.isQuerySupported ||
594
+ !resultState.mousedOverCell
595
+ }
596
+ menuProps={{ elevation: 7 }}
597
+ className={clsx('ag-theme-balham-dark query-builder__result__tds-grid')}
598
+ >
599
+ <div
600
+ className={clsx('query-builder__result__values__table__cell')}
601
+ onMouseDown={(event) => mouseDown(event)}
602
+ onMouseUp={(event) => mouseUp(event)}
603
+ onMouseOver={(event) => mouseOver(event)}
604
+ >
605
+ {cellValueUrlLink ? (
606
+ <a href={cellValueUrlLink} target="_blank" rel="noreferrer">
607
+ {cellValueUrlLink}
608
+ </a>
609
+ ) : (
610
+ <span>{formattedCellValue()}</span>
611
+ )}
612
+ </div>
613
+ </ContextMenu>
614
+ );
615
+ },
616
+ );
617
+
618
+ export const QueryResultCellRenderer = observer(
619
+ (params: IQueryRendererParamsWithGridType) => {
620
+ const resultState = params.resultState;
621
+ const tdsExecutionResult = params.tdsExecutionResult;
622
+ const fetchStructureImplementation =
623
+ resultState.queryBuilderState.fetchStructureState.implementation;
624
+ const cellValue = params.value as QueryBuilderTDSResultCellDataType;
625
+ const formattedCellValue = (): QueryBuilderTDSResultCellDataType => {
626
+ if (isNumber(cellValue)) {
627
+ return Intl.NumberFormat(DEFAULT_LOCALE, {
628
+ maximumFractionDigits: 4,
629
+ }).format(Number(cellValue));
630
+ }
631
+ return cellValue;
632
+ };
633
+ const cellValueUrlLink =
634
+ isString(cellValue) && isValidURL(cellValue) ? cellValue : undefined;
635
+ const columnName = params.column?.getColId() ?? '';
636
+ const findCoordinatesFromResultValue = (
637
+ colId: string,
638
+ rowNumber: number,
639
+ ): QueryBuilderTDSResultCellCoordinate => {
640
+ const colIndex = tdsExecutionResult.result.columns.findIndex(
641
+ (col) => col === colId,
642
+ );
643
+ return { rowIndex: rowNumber, colIndex: colIndex };
644
+ };
645
+
646
+ const currentCellCoordinates = findCoordinatesFromResultValue(
647
+ columnName,
648
+ params.rowIndex,
649
+ );
650
+ const cellInFilteredResults = resultState.selectedCells.some(
651
+ (result) =>
652
+ result.coordinates.colIndex === currentCellCoordinates.colIndex &&
653
+ result.coordinates.rowIndex === currentCellCoordinates.rowIndex,
654
+ );
655
+
656
+ const findColumnFromCoordinates = (
657
+ colIndex: number,
658
+ ): QueryBuilderTDSResultCellDataType => {
659
+ if (
660
+ !resultState.executionResult ||
661
+ !(resultState.executionResult instanceof TDSExecutionResult)
662
+ ) {
663
+ return undefined;
664
+ }
665
+ return resultState.executionResult.result.columns[colIndex];
666
+ };
667
+
668
+ const findResultValueFromCoordinates = (
669
+ resultCoordinate: [number, number],
670
+ ): QueryBuilderTDSResultCellDataType => {
671
+ const rowIndex = resultCoordinate[0];
672
+ const colIndex = resultCoordinate[1];
673
+
674
+ if (
675
+ !resultState.executionResult ||
676
+ !(resultState.executionResult instanceof TDSExecutionResult)
677
+ ) {
678
+ return undefined;
679
+ }
680
+ if (params.columnApi.getColumnState()[colIndex]?.sort === 'asc') {
681
+ resultState.executionResult.result.rows.sort((a, b) =>
682
+ getTDSRowRankByColumnInAsc(a, b, colIndex),
683
+ );
684
+ } else if (params.columnApi.getColumnState()[colIndex]?.sort === 'desc') {
685
+ resultState.executionResult.result.rows.sort((a, b) =>
686
+ getTDSRowRankByColumnInAsc(b, a, colIndex),
687
+ );
688
+ }
689
+ return resultState.executionResult.result.rows[rowIndex]?.values[
690
+ colIndex
691
+ ];
692
+ };
693
+
694
+ const isCoordinatesSelected = (
695
+ resultCoordinate: QueryBuilderTDSResultCellCoordinate,
696
+ ): boolean =>
697
+ resultState.selectedCells.some(
698
+ (cell) =>
699
+ cell.coordinates.rowIndex === resultCoordinate.rowIndex &&
700
+ cell.coordinates.colIndex === resultCoordinate.colIndex,
701
+ );
702
+
703
+ const mouseDown: React.MouseEventHandler = (event) => {
704
+ event.preventDefault();
705
+
706
+ if (event.shiftKey) {
707
+ const coordinates = findCoordinatesFromResultValue(
708
+ columnName,
709
+ params.rowIndex,
710
+ );
711
+ const actualValue = findResultValueFromCoordinates([
712
+ coordinates.rowIndex,
713
+ coordinates.colIndex,
714
+ ]);
715
+ resultState.addSelectedCell({
716
+ value: actualValue,
717
+ columnName: columnName,
718
+ coordinates: coordinates,
719
+ });
720
+ return;
721
+ }
722
+
723
+ if (event.button === 0) {
724
+ resultState.setIsSelectingCells(true);
725
+ resultState.setSelectedCells([]);
726
+ const coordinates = findCoordinatesFromResultValue(
727
+ columnName,
728
+ params.rowIndex,
729
+ );
730
+ const actualValue = findResultValueFromCoordinates([
731
+ coordinates.rowIndex,
732
+ coordinates.colIndex,
733
+ ]);
734
+ resultState.setSelectedCells([
735
+ {
736
+ value: actualValue,
737
+ columnName: columnName,
738
+ coordinates: coordinates,
739
+ },
740
+ ]);
741
+ resultState.setMouseOverCell(resultState.selectedCells[0] ?? null);
742
+ }
743
+
744
+ if (event.button === 2) {
745
+ const coordinates = findCoordinatesFromResultValue(
746
+ columnName,
747
+ params.rowIndex,
748
+ );
749
+ const isInSelected = isCoordinatesSelected(coordinates);
750
+ if (!isInSelected) {
751
+ const actualValue = findResultValueFromCoordinates([
752
+ coordinates.rowIndex,
753
+ coordinates.colIndex,
754
+ ]);
755
+ resultState.setSelectedCells([
756
+ {
757
+ value: actualValue,
758
+ columnName: columnName,
759
+ coordinates: coordinates,
760
+ },
761
+ ]);
762
+ resultState.setMouseOverCell(resultState.selectedCells[0] ?? null);
763
+ }
764
+ }
765
+ };
766
+
767
+ const mouseUp: React.MouseEventHandler = (event) => {
768
+ resultState.setIsSelectingCells(false);
769
+ };
770
+
771
+ const mouseOver: React.MouseEventHandler = (event) => {
772
+ if (resultState.isSelectingCells) {
773
+ if (resultState.selectedCells.length < 1) {
774
+ return;
775
+ }
776
+ const results = resultState.selectedCells[0];
777
+ if (!results) {
778
+ return;
779
+ }
780
+
781
+ const firstCorner = results.coordinates;
782
+ const secondCorner = findCoordinatesFromResultValue(
783
+ columnName,
784
+ params.rowIndex,
785
+ );
786
+
787
+ resultState.setSelectedCells([results]);
788
+
789
+ const minRow = Math.min(firstCorner.rowIndex, secondCorner.rowIndex);
790
+ const minCol = Math.min(firstCorner.colIndex, secondCorner.colIndex);
791
+ const maxRow = Math.max(firstCorner.rowIndex, secondCorner.rowIndex);
792
+ const maxCol = Math.max(firstCorner.colIndex, secondCorner.colIndex);
793
+
794
+ for (let x = minRow; x <= maxRow; x++) {
795
+ for (let y = minCol; y <= maxCol; y++) {
796
+ const actualValue = findResultValueFromCoordinates([x, y]);
797
+
798
+ const valueAndColumnId = {
799
+ value: actualValue,
800
+ columnName: findColumnFromCoordinates(y),
801
+ coordinates: {
802
+ rowIndex: x,
803
+ colIndex: y,
804
+ },
805
+ } as QueryBuilderTDSResultCellData;
806
+
807
+ if (
808
+ !resultState.selectedCells.find(
809
+ (result) =>
810
+ result.coordinates.colIndex === y &&
811
+ result.coordinates.rowIndex === x,
812
+ )
813
+ ) {
814
+ resultState.addSelectedCell(valueAndColumnId);
815
+ }
816
+ }
817
+ }
818
+ }
819
+
820
+ resultState.setMouseOverCell(resultState.selectedCells[0] ?? null);
821
+ };
822
+
823
+ return (
824
+ <ContextMenu
825
+ content={
826
+ // NOTE: we only support this functionality for grid result with a projection fetch-structure
827
+ fetchStructureImplementation instanceof QueryBuilderTDSState ? (
828
+ <QueryBuilderGridResultContextMenu
829
+ data={resultState.mousedOverCell}
830
+ tdsState={fetchStructureImplementation}
831
+ dataGridApi={params.api}
832
+ />
833
+ ) : null
834
+ }
835
+ disabled={
836
+ !(
837
+ resultState.queryBuilderState.fetchStructureState
838
+ .implementation instanceof QueryBuilderTDSState
839
+ ) ||
840
+ !resultState.queryBuilderState.isQuerySupported ||
841
+ !resultState.mousedOverCell
842
+ }
843
+ menuProps={{ elevation: 7 }}
844
+ className={clsx('ag-theme-balham-dark query-builder__result__tds-grid')}
845
+ >
846
+ <div
847
+ className={clsx('query-builder__result__values__table__cell', {
848
+ 'query-builder__result__values__table__cell--active':
849
+ cellInFilteredResults,
850
+ })}
851
+ onMouseDown={(event) => mouseDown(event)}
852
+ onMouseUp={(event) => mouseUp(event)}
853
+ onMouseOver={(event) => mouseOver(event)}
854
+ >
855
+ {cellValueUrlLink ? (
856
+ <a href={cellValueUrlLink} target="_blank" rel="noreferrer">
857
+ {cellValueUrlLink}
858
+ </a>
859
+ ) : (
860
+ <span>{formattedCellValue()}</span>
861
+ )}
862
+ </div>
863
+ </ContextMenu>
864
+ );
865
+ },
866
+ );