@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
@@ -1,1412 +0,0 @@
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
- BlankPanelContent,
19
- PanelLoadingIndicator,
20
- PlayIcon,
21
- DropdownMenu,
22
- MenuContent,
23
- MenuContentItem,
24
- CaretDownIcon,
25
- ContextMenu,
26
- clsx,
27
- PauseCircleIcon,
28
- ExclamationTriangleIcon,
29
- PanelContent,
30
- MenuContentDivider,
31
- Button,
32
- SQLIcon,
33
- Dialog,
34
- Modal,
35
- ModalBody,
36
- ModalFooter,
37
- ModalFooterButton,
38
- ModalHeader,
39
- PanelDivider,
40
- SquareIcon,
41
- CheckSquareIcon,
42
- } from '@finos/legend-art';
43
- import { format as formatSQL } from 'sql-formatter';
44
- import { observer } from 'mobx-react-lite';
45
- import { flowResult } from 'mobx';
46
- import type { QueryBuilderState } from '../stores/QueryBuilderState.js';
47
- import {
48
- type ExecutionResult,
49
- type Enumeration,
50
- InstanceValue,
51
- extractExecutionResultValues,
52
- TDSExecutionResult,
53
- RawExecutionResult,
54
- EnumValueInstanceValue,
55
- EnumValueExplicitReference,
56
- RelationalExecutionActivities,
57
- getTDSRowRankByColumnInAsc,
58
- PRIMITIVE_TYPE,
59
- } from '@finos/legend-graph';
60
- import {
61
- ActionAlertActionType,
62
- ActionAlertType,
63
- DEFAULT_TAB_SIZE,
64
- useApplicationStore,
65
- } from '@finos/legend-application';
66
- import {
67
- assertErrorThrown,
68
- guaranteeNonNullable,
69
- isBoolean,
70
- type PlainObject,
71
- prettyDuration,
72
- filterByType,
73
- isValidURL,
74
- isString,
75
- isNumber,
76
- } from '@finos/legend-shared';
77
- import { forwardRef, useRef, useState } from 'react';
78
- import {
79
- QueryBuilderDerivationProjectionColumnState,
80
- QueryBuilderProjectionColumnState,
81
- } from '../stores/fetch-structure/tds/projection/QueryBuilderProjectionColumnState.js';
82
- import {
83
- type QueryBuilderPostFilterTreeNodeData,
84
- PostFilterConditionState,
85
- QueryBuilderPostFilterTreeConditionNodeData,
86
- PostFilterValueSpecConditionValueState,
87
- } from '../stores/fetch-structure/tds/post-filter/QueryBuilderPostFilterState.js';
88
- import {
89
- QueryBuilderPostFilterOperator_Equal,
90
- QueryBuilderPostFilterOperator_NotEqual,
91
- } from '../stores/fetch-structure/tds/post-filter/operators/QueryBuilderPostFilterOperator_Equal.js';
92
- import {
93
- QueryBuilderPostFilterOperator_In,
94
- QueryBuilderPostFilterOperator_NotIn,
95
- } from '../stores/fetch-structure/tds/post-filter/operators/QueryBuilderPostFilterOperator_In.js';
96
- import type { QueryBuilderPostFilterOperator } from '../stores/fetch-structure/tds/post-filter/QueryBuilderPostFilterOperator.js';
97
- import { QueryBuilderTDSState } from '../stores/fetch-structure/tds/QueryBuilderTDSState.js';
98
- import {
99
- instanceValue_setValue,
100
- instanceValue_setValues,
101
- } from '../stores/shared/ValueSpecificationModifierHelper.js';
102
- import { PARAMETER_SUBMIT_ACTION } from '../stores/shared/LambdaParameterState.js';
103
- import { QUERY_BUILDER_TEST_ID } from '../__lib__/QueryBuilderTesting.js';
104
- import {
105
- DataGrid,
106
- type DataGridColumnApi,
107
- type DataGridCellRendererParams,
108
- type DataGridColumnDefinition,
109
- } from '@finos/legend-lego/data-grid';
110
- import {
111
- CODE_EDITOR_LANGUAGE,
112
- CodeEditor,
113
- } from '@finos/legend-lego/code-editor';
114
- import { ExecutionPlanViewer } from './execution-plan/ExecutionPlanViewer.js';
115
- import type {
116
- QueryBuilderTDSResultCellCoordinate,
117
- QueryBuilderResultState,
118
- QueryBuilderTDSResultCellData,
119
- } from '../stores/QueryBuilderResultState.js';
120
- import {
121
- QueryBuilderPostFilterOperator_IsEmpty,
122
- QueryBuilderPostFilterOperator_IsNotEmpty,
123
- } from '../stores/fetch-structure/tds/post-filter/operators/QueryBuilderPostFilterOperator_IsEmpty.js';
124
- import { QueryUsageViewer } from './QueryUsageViewer.js';
125
- import { DEFAULT_LOCALE } from '../graph-manager/QueryBuilderConst.js';
126
- import { DocumentationLink } from '@finos/legend-lego/application';
127
- import { QUERY_BUILDER_DOCUMENTATION_KEY } from '../__lib__/QueryBuilderDocumentation.js';
128
-
129
- export const tryToFormatSql = (sql: string): string => {
130
- try {
131
- const formattedSql = formatSQL(sql, { language: 'mysql' });
132
- return formattedSql;
133
- } catch {
134
- try {
135
- const formattedSql = formatSQL(sql);
136
- return formattedSql;
137
- } catch {
138
- return sql;
139
- }
140
- }
141
- };
142
-
143
- const QueryBuilderGridResultContextMenu = observer(
144
- forwardRef<
145
- HTMLDivElement,
146
- {
147
- data: QueryBuilderTDSResultCellData | null;
148
- tdsState: QueryBuilderTDSState;
149
- }
150
- >(function QueryBuilderResultContextMenu(props, ref) {
151
- const { data, tdsState } = props;
152
-
153
- const applicationStore = useApplicationStore();
154
- const postFilterEqualOperator = new QueryBuilderPostFilterOperator_Equal();
155
- const postFilterInOperator = new QueryBuilderPostFilterOperator_In();
156
- const postFilterEmptyOperator =
157
- new QueryBuilderPostFilterOperator_IsEmpty();
158
- const postFilterNotEmptyOperator =
159
- new QueryBuilderPostFilterOperator_IsNotEmpty();
160
-
161
- const postFilterNotEqualOperator =
162
- new QueryBuilderPostFilterOperator_NotEqual();
163
- const postFilterNotInOperator = new QueryBuilderPostFilterOperator_NotIn();
164
- const postFilterState = tdsState.postFilterState;
165
-
166
- const projectionColumnState = tdsState.projectionColumns
167
- .filter((c) => c.columnName === data?.columnName)
168
- .concat(
169
- tdsState.aggregationState.columns
170
- .filter((c) => c.columnName === data?.columnName)
171
- .map((ag) => ag.projectionColumnState),
172
- )[0];
173
- const getExistingPostFilterNode = (
174
- operators: QueryBuilderPostFilterOperator[],
175
- projectionColumnName: string | undefined,
176
- ): QueryBuilderPostFilterTreeNodeData | undefined =>
177
- Array.from(postFilterState.nodes.values())
178
- .filter(
179
- (v) =>
180
- v instanceof QueryBuilderPostFilterTreeConditionNodeData &&
181
- v.condition.leftConditionValue instanceof
182
- QueryBuilderProjectionColumnState,
183
- )
184
- .filter(
185
- (n) =>
186
- (n as QueryBuilderPostFilterTreeConditionNodeData).condition
187
- .leftConditionValue.columnName ===
188
- (projectionColumnName ?? projectionColumnState?.columnName) &&
189
- operators
190
- .map((op) => op.getLabel())
191
- .includes(
192
- (
193
- n as QueryBuilderPostFilterTreeConditionNodeData
194
- ).condition.operator.getLabel(),
195
- ),
196
- )[0];
197
-
198
- const updateFilterConditionValue = (
199
- conditionValue: InstanceValue,
200
- cellData: QueryBuilderTDSResultCellData,
201
- ): void => {
202
- if (cellData.value) {
203
- instanceValue_setValue(
204
- conditionValue,
205
- conditionValue instanceof EnumValueInstanceValue
206
- ? EnumValueExplicitReference.create(
207
- guaranteeNonNullable(
208
- (
209
- conditionValue.genericType?.ownerReference
210
- .value as Enumeration
211
- ).values.filter((v) => v.name === cellData.value)[0],
212
- ),
213
- )
214
- : cellData.value,
215
- 0,
216
- tdsState.queryBuilderState.observerContext,
217
- );
218
- }
219
- };
220
-
221
- const generateNewPostFilterConditionNodeData = async (
222
- operator: QueryBuilderPostFilterOperator,
223
- cellData: QueryBuilderTDSResultCellData,
224
- ): Promise<void> => {
225
- let postFilterConditionState: PostFilterConditionState;
226
- try {
227
- const possibleProjectionColumnState = cellData.columnName
228
- ? tdsState.projectionColumns
229
- .filter((c) => c.columnName === cellData.columnName)
230
- .concat(
231
- tdsState.aggregationState.columns
232
- .filter((c) => c.columnName === cellData.columnName)
233
- .map((ag) => ag.projectionColumnState),
234
- )[0]
235
- : projectionColumnState;
236
-
237
- if (possibleProjectionColumnState) {
238
- postFilterConditionState = new PostFilterConditionState(
239
- postFilterState,
240
- possibleProjectionColumnState,
241
- operator,
242
- );
243
-
244
- if (
245
- projectionColumnState instanceof
246
- QueryBuilderDerivationProjectionColumnState
247
- ) {
248
- await flowResult(
249
- projectionColumnState.fetchDerivationLambdaReturnType(),
250
- );
251
- }
252
-
253
- const defaultFilterConditionValue =
254
- postFilterConditionState.operator.getDefaultFilterConditionValue(
255
- postFilterConditionState,
256
- );
257
-
258
- postFilterConditionState.buildFromValueSpec(
259
- defaultFilterConditionValue,
260
- );
261
- updateFilterConditionValue(
262
- defaultFilterConditionValue as InstanceValue,
263
- cellData,
264
- );
265
- postFilterState.addNodeFromNode(
266
- new QueryBuilderPostFilterTreeConditionNodeData(
267
- undefined,
268
- postFilterConditionState,
269
- ),
270
- undefined,
271
- );
272
- }
273
- } catch (error) {
274
- assertErrorThrown(error);
275
- applicationStore.notificationService.notifyWarning(error.message);
276
- return;
277
- }
278
- };
279
-
280
- const updateExistingPostFilterConditionNodeData = (
281
- existingPostFilterNode: QueryBuilderPostFilterTreeNodeData,
282
- isFilterBy: boolean,
283
- cellData: QueryBuilderTDSResultCellData,
284
- operator: QueryBuilderPostFilterOperator,
285
- ): void => {
286
- if (
287
- operator === postFilterEmptyOperator ||
288
- operator === postFilterNotEmptyOperator
289
- ) {
290
- const conditionState = (
291
- existingPostFilterNode as QueryBuilderPostFilterTreeConditionNodeData
292
- ).condition;
293
- if (conditionState.operator.getLabel() !== operator.getLabel()) {
294
- conditionState.changeOperator(
295
- isFilterBy ? postFilterEmptyOperator : postFilterNotEmptyOperator,
296
- );
297
- }
298
- return;
299
- }
300
- const conditionState = (
301
- existingPostFilterNode as QueryBuilderPostFilterTreeConditionNodeData
302
- ).condition;
303
-
304
- const rightSide = conditionState.rightConditionValue;
305
- if (rightSide instanceof PostFilterValueSpecConditionValueState) {
306
- if (conditionState.operator.getLabel() === operator.getLabel()) {
307
- const doesValueAlreadyExist =
308
- rightSide.value instanceof InstanceValue &&
309
- (rightSide.value instanceof EnumValueInstanceValue
310
- ? rightSide.value.values.map((ef) => ef.value.name)
311
- : rightSide.value.values
312
- ).includes(cellData.value);
313
-
314
- if (!doesValueAlreadyExist) {
315
- const currentValueSpecificaton = rightSide.value;
316
- const newValueSpecification =
317
- conditionState.operator.getDefaultFilterConditionValue(
318
- conditionState,
319
- );
320
- updateFilterConditionValue(
321
- newValueSpecification as InstanceValue,
322
- cellData,
323
- );
324
- conditionState.changeOperator(
325
- isFilterBy ? postFilterInOperator : postFilterNotInOperator,
326
- );
327
- instanceValue_setValues(
328
- rightSide.value as InstanceValue,
329
- [currentValueSpecificaton, newValueSpecification],
330
- tdsState.queryBuilderState.observerContext,
331
- );
332
- }
333
- } else {
334
- const doesValueAlreadyExist =
335
- rightSide.value instanceof InstanceValue &&
336
- rightSide.value.values
337
- .filter((v) => v instanceof InstanceValue)
338
- .map((v) =>
339
- v instanceof EnumValueInstanceValue
340
- ? v.values.map((ef) => ef.value.name)
341
- : (v as InstanceValue).values,
342
- )
343
- .flat()
344
- .includes(cellData.value ?? data?.value);
345
-
346
- if (!doesValueAlreadyExist) {
347
- const newValueSpecification = (
348
- isFilterBy ? postFilterEqualOperator : postFilterNotEqualOperator
349
- ).getDefaultFilterConditionValue(conditionState);
350
- updateFilterConditionValue(
351
- newValueSpecification as InstanceValue,
352
- cellData,
353
- );
354
- instanceValue_setValues(
355
- rightSide.value as InstanceValue,
356
- [
357
- ...(rightSide.value as InstanceValue).values,
358
- newValueSpecification,
359
- ],
360
- tdsState.queryBuilderState.observerContext,
361
- );
362
- }
363
- }
364
- }
365
- };
366
-
367
- const getFilterOperator = (
368
- isFilterBy: boolean,
369
- cellData: QueryBuilderTDSResultCellData,
370
- ): QueryBuilderPostFilterOperator => {
371
- if (isFilterBy === true) {
372
- if (cellData.value === null) {
373
- return postFilterEmptyOperator;
374
- } else {
375
- return postFilterEqualOperator;
376
- }
377
- } else {
378
- if (cellData.value === null) {
379
- return postFilterNotEmptyOperator;
380
- } else {
381
- return postFilterNotEqualOperator;
382
- }
383
- }
384
- };
385
-
386
- const filterByOrOutValue = (
387
- isFilterBy: boolean,
388
- cellData: QueryBuilderTDSResultCellData,
389
- ): void => {
390
- tdsState.setShowPostFilterPanel(true);
391
-
392
- const operator = getFilterOperator(isFilterBy, cellData);
393
-
394
- const existingPostFilterNode = getExistingPostFilterNode(
395
- cellData.value === null
396
- ? [postFilterEmptyOperator, postFilterNotEmptyOperator]
397
- : isFilterBy
398
- ? [postFilterEqualOperator, postFilterInOperator]
399
- : [postFilterNotEqualOperator, postFilterNotInOperator],
400
- cellData.columnName,
401
- );
402
-
403
- existingPostFilterNode === undefined
404
- ? generateNewPostFilterConditionNodeData(operator, cellData).catch(
405
- applicationStore.alertUnhandledError,
406
- )
407
- : updateExistingPostFilterConditionNodeData(
408
- existingPostFilterNode,
409
- isFilterBy,
410
- cellData,
411
- operator,
412
- );
413
- };
414
-
415
- const filterByOrOutValues = (isFilterBy: boolean): void => {
416
- tdsState.queryBuilderState.resultState.selectedCells.forEach(
417
- (cellData) => {
418
- filterByOrOutValue(isFilterBy, cellData);
419
- },
420
- );
421
- };
422
-
423
- const handleCopyCellValue = applicationStore.guardUnhandledError(() =>
424
- applicationStore.clipboardService.copyTextToClipboard(
425
- data?.value?.toString() ?? '',
426
- ),
427
- );
428
-
429
- const findRowFromRowIndex = (
430
- rowIndex: number,
431
- ): (string | number | boolean | null)[] => {
432
- if (
433
- !tdsState.queryBuilderState.resultState.executionResult ||
434
- !(
435
- tdsState.queryBuilderState.resultState.executionResult instanceof
436
- TDSExecutionResult
437
- )
438
- ) {
439
- return [''];
440
- }
441
- return (
442
- tdsState.queryBuilderState.resultState.executionResult.result.rows[
443
- rowIndex
444
- ]?.values ?? ['']
445
- );
446
- };
447
-
448
- const handleCopyRowValue = applicationStore.guardUnhandledError(() =>
449
- applicationStore.clipboardService.copyTextToClipboard(
450
- findRowFromRowIndex(
451
- tdsState.queryBuilderState.resultState.selectedCells[0]?.coordinates
452
- .rowIndex ?? 0,
453
- ).toString(),
454
- ),
455
- );
456
-
457
- return (
458
- <MenuContent ref={ref}>
459
- <MenuContentItem
460
- disabled={!projectionColumnState}
461
- onClick={(): void => {
462
- filterByOrOutValues(true);
463
- }}
464
- >
465
- Filter By
466
- </MenuContentItem>
467
- <MenuContentItem
468
- disabled={!projectionColumnState}
469
- onClick={(): void => {
470
- filterByOrOutValues(false);
471
- }}
472
- >
473
- Filter Out
474
- </MenuContentItem>
475
- <MenuContentDivider />
476
- <MenuContentItem onClick={handleCopyCellValue}>
477
- Copy Cell Value
478
- </MenuContentItem>
479
- <MenuContentItem onClick={handleCopyRowValue}>
480
- Copy Row Value
481
- </MenuContentItem>
482
- </MenuContent>
483
- );
484
- }),
485
- );
486
-
487
- type IQueryRendererParamsWithGridType = DataGridCellRendererParams & {
488
- resultState: QueryBuilderResultState;
489
- tdsExecutionResult: TDSExecutionResult;
490
- };
491
-
492
- const QueryResultCellRenderer = observer(
493
- (params: IQueryRendererParamsWithGridType) => {
494
- const resultState = params.resultState;
495
- const tdsExecutionResult = params.tdsExecutionResult;
496
- const fetchStructureImplementation =
497
- resultState.queryBuilderState.fetchStructureState.implementation;
498
- const cellValue = params.value as string | null | number | undefined;
499
- const formattedCellValue = (): string | null | number | undefined => {
500
- if (isNumber(cellValue)) {
501
- return Intl.NumberFormat(DEFAULT_LOCALE, {
502
- maximumFractionDigits: 4,
503
- }).format(Number(cellValue));
504
- }
505
- return cellValue;
506
- };
507
- const cellValueUrlLink =
508
- isString(cellValue) && isValidURL(cellValue) ? cellValue : undefined;
509
- const columnName = params.column?.getColId() ?? '';
510
- const findCoordinatesFromResultValue = (
511
- colId: string,
512
- rowNumber: number,
513
- ): QueryBuilderTDSResultCellCoordinate => {
514
- const colIndex = tdsExecutionResult.result.columns.findIndex(
515
- (col) => col === colId,
516
- );
517
- return { rowIndex: rowNumber, colIndex: colIndex };
518
- };
519
-
520
- const currentCellCoordinates = findCoordinatesFromResultValue(
521
- columnName,
522
- params.rowIndex,
523
- );
524
- const cellInFilteredResults = resultState.selectedCells.some(
525
- (result) =>
526
- result.coordinates.colIndex === currentCellCoordinates.colIndex &&
527
- result.coordinates.rowIndex === currentCellCoordinates.rowIndex,
528
- );
529
-
530
- const findColumnFromCoordinates = (
531
- colIndex: number,
532
- ): string | number | boolean | null | undefined => {
533
- if (
534
- !resultState.executionResult ||
535
- !(resultState.executionResult instanceof TDSExecutionResult)
536
- ) {
537
- return undefined;
538
- }
539
- return resultState.executionResult.result.columns[colIndex];
540
- };
541
-
542
- const findResultValueFromCoordinates = (
543
- resultCoordinate: [number, number],
544
- ): string | number | boolean | null | undefined => {
545
- const rowIndex = resultCoordinate[0];
546
- const colIndex = resultCoordinate[1];
547
-
548
- if (
549
- !resultState.executionResult ||
550
- !(resultState.executionResult instanceof TDSExecutionResult)
551
- ) {
552
- return undefined;
553
- }
554
- if (params.columnApi.getColumnState()[colIndex]?.sort === 'asc') {
555
- resultState.executionResult.result.rows.sort((a, b) =>
556
- getTDSRowRankByColumnInAsc(a, b, colIndex),
557
- );
558
- } else if (params.columnApi.getColumnState()[colIndex]?.sort === 'desc') {
559
- resultState.executionResult.result.rows.sort((a, b) =>
560
- getTDSRowRankByColumnInAsc(b, a, colIndex),
561
- );
562
- }
563
- return resultState.executionResult.result.rows[rowIndex]?.values[
564
- colIndex
565
- ];
566
- };
567
-
568
- const isCoordinatesSelected = (
569
- resultCoordinate: QueryBuilderTDSResultCellCoordinate,
570
- ): boolean =>
571
- resultState.selectedCells.some(
572
- (cell) =>
573
- cell.coordinates.rowIndex === resultCoordinate.rowIndex &&
574
- cell.coordinates.colIndex === resultCoordinate.colIndex,
575
- );
576
-
577
- const mouseDown: React.MouseEventHandler = (event) => {
578
- event.preventDefault();
579
-
580
- if (event.shiftKey) {
581
- const coordinates = findCoordinatesFromResultValue(
582
- columnName,
583
- params.rowIndex,
584
- );
585
- const actualValue = findResultValueFromCoordinates([
586
- coordinates.rowIndex,
587
- coordinates.colIndex,
588
- ]);
589
- resultState.addSelectedCell({
590
- value: actualValue,
591
- columnName: columnName,
592
- coordinates: coordinates,
593
- });
594
- return;
595
- }
596
-
597
- if (event.button === 0) {
598
- resultState.setIsSelectingCells(true);
599
- resultState.setSelectedCells([]);
600
- const coordinates = findCoordinatesFromResultValue(
601
- columnName,
602
- params.rowIndex,
603
- );
604
- const actualValue = findResultValueFromCoordinates([
605
- coordinates.rowIndex,
606
- coordinates.colIndex,
607
- ]);
608
- resultState.setSelectedCells([
609
- {
610
- value: actualValue,
611
- columnName: columnName,
612
- coordinates: coordinates,
613
- },
614
- ]);
615
- resultState.setMouseOverCell(resultState.selectedCells[0] ?? null);
616
- }
617
-
618
- if (event.button === 2) {
619
- const coordinates = findCoordinatesFromResultValue(
620
- columnName,
621
- params.rowIndex,
622
- );
623
- const isInSelected = isCoordinatesSelected(coordinates);
624
- if (!isInSelected) {
625
- const actualValue = findResultValueFromCoordinates([
626
- coordinates.rowIndex,
627
- coordinates.colIndex,
628
- ]);
629
- resultState.setSelectedCells([
630
- {
631
- value: actualValue,
632
- columnName: columnName,
633
- coordinates: coordinates,
634
- },
635
- ]);
636
- resultState.setMouseOverCell(resultState.selectedCells[0] ?? null);
637
- }
638
- }
639
- };
640
-
641
- const mouseUp: React.MouseEventHandler = (event) => {
642
- resultState.setIsSelectingCells(false);
643
- };
644
-
645
- const mouseOver: React.MouseEventHandler = (event) => {
646
- if (resultState.isSelectingCells) {
647
- if (resultState.selectedCells.length < 1) {
648
- return;
649
- }
650
- const results = resultState.selectedCells[0];
651
- if (!results) {
652
- return;
653
- }
654
-
655
- const firstCorner = results.coordinates;
656
- const secondCorner = findCoordinatesFromResultValue(
657
- columnName,
658
- params.rowIndex,
659
- );
660
-
661
- resultState.setSelectedCells([results]);
662
-
663
- const minRow = Math.min(firstCorner.rowIndex, secondCorner.rowIndex);
664
- const minCol = Math.min(firstCorner.colIndex, secondCorner.colIndex);
665
- const maxRow = Math.max(firstCorner.rowIndex, secondCorner.rowIndex);
666
- const maxCol = Math.max(firstCorner.colIndex, secondCorner.colIndex);
667
-
668
- for (let x = minRow; x <= maxRow; x++) {
669
- for (let y = minCol; y <= maxCol; y++) {
670
- const actualValue = findResultValueFromCoordinates([x, y]);
671
-
672
- const valueAndColumnId = {
673
- value: actualValue,
674
- columnName: findColumnFromCoordinates(y),
675
- coordinates: {
676
- rowIndex: x,
677
- colIndex: y,
678
- },
679
- } as QueryBuilderTDSResultCellData;
680
-
681
- if (
682
- !resultState.selectedCells.find(
683
- (result) =>
684
- result.coordinates.colIndex === y &&
685
- result.coordinates.rowIndex === x,
686
- )
687
- ) {
688
- resultState.addSelectedCell(valueAndColumnId);
689
- }
690
- }
691
- }
692
- }
693
-
694
- resultState.setMouseOverCell(resultState.selectedCells[0] ?? null);
695
- };
696
-
697
- return (
698
- <ContextMenu
699
- content={
700
- // NOTE: we only support this functionality for grid result with a projection fetch-structure
701
- fetchStructureImplementation instanceof QueryBuilderTDSState ? (
702
- <QueryBuilderGridResultContextMenu
703
- data={resultState.mousedOverCell}
704
- tdsState={fetchStructureImplementation}
705
- />
706
- ) : null
707
- }
708
- disabled={
709
- !(
710
- resultState.queryBuilderState.fetchStructureState
711
- .implementation instanceof QueryBuilderTDSState
712
- ) ||
713
- !resultState.queryBuilderState.isQuerySupported ||
714
- !resultState.mousedOverCell
715
- }
716
- menuProps={{ elevation: 7 }}
717
- className={clsx('ag-theme-balham-dark query-builder__result__tds-grid')}
718
- >
719
- <div
720
- className={clsx('query-builder__result__values__table__cell', {
721
- 'query-builder__result__values__table__cell--active':
722
- cellInFilteredResults,
723
- })}
724
- onMouseDown={(event) => mouseDown(event)}
725
- onMouseUp={(event) => mouseUp(event)}
726
- onMouseOver={(event) => mouseOver(event)}
727
- >
728
- {cellValueUrlLink ? (
729
- <a href={cellValueUrlLink} target="_blank" rel="noreferrer">
730
- {cellValueUrlLink}
731
- </a>
732
- ) : (
733
- <span>{formattedCellValue()}</span>
734
- )}
735
- </div>
736
- </ContextMenu>
737
- );
738
- },
739
- );
740
-
741
- const getColumnCustomizations = (
742
- result: TDSExecutionResult,
743
- columnName: string,
744
- ): object | undefined => {
745
- const columnType = result.builder.columns.find(
746
- (col) => col.name === columnName,
747
- )?.type;
748
- switch (columnType) {
749
- case PRIMITIVE_TYPE.STRING:
750
- return {
751
- filter: 'agTextColumnFilter',
752
- allowedAggFuncs: ['count'],
753
- };
754
- case PRIMITIVE_TYPE.DATE:
755
- case PRIMITIVE_TYPE.DATETIME:
756
- case PRIMITIVE_TYPE.STRICTDATE:
757
- return {
758
- filter: 'agDateColumnFilter',
759
- allowedAggFuncs: ['count'],
760
- };
761
- case PRIMITIVE_TYPE.DECIMAL:
762
- case PRIMITIVE_TYPE.INTEGER:
763
- case PRIMITIVE_TYPE.FLOAT:
764
- return {
765
- filter: 'agNumberColumnFilter',
766
- allowedAggFuncs: ['count', 'sum', 'max', 'min', 'avg'],
767
- };
768
- default:
769
- return {
770
- allowedAggFuncs: ['count'],
771
- };
772
- }
773
- };
774
-
775
- const QueryBuilderGridResult = observer(
776
- (props: {
777
- executionResult: TDSExecutionResult;
778
- queryBuilderState: QueryBuilderState;
779
- }) => {
780
- const { executionResult, queryBuilderState } = props;
781
-
782
- const [columnAPi, setColumnApi] = useState<DataGridColumnApi | undefined>(
783
- undefined,
784
- );
785
- const resultState = queryBuilderState.resultState;
786
- const isAdvancedModeEnabled = queryBuilderState.isAdvancedModeEnabled;
787
- const colDefs = isAdvancedModeEnabled
788
- ? executionResult.result.columns.map((colName) => {
789
- const col = {
790
- minWidth: 50,
791
- sortable: true,
792
- resizable: true,
793
- field: colName,
794
- flex: 1,
795
- enablePivot: true,
796
- enableRowGroup: true,
797
- enableValue: true,
798
- ...getColumnCustomizations(executionResult, colName),
799
- } as DataGridColumnDefinition;
800
- const persistedColumn = resultState.gridConfig.columns.find(
801
- (c) => c.colId === colName,
802
- );
803
- if (persistedColumn) {
804
- if (persistedColumn.width) {
805
- col.width = persistedColumn.width;
806
- }
807
- col.pinned = persistedColumn.pinned ?? null;
808
- col.rowGroup = persistedColumn.rowGroup ?? false;
809
- col.rowGroupIndex = persistedColumn.rowGroupIndex ?? null;
810
- col.aggFunc = persistedColumn.aggFunc ?? null;
811
- col.pivot = persistedColumn.pivot ?? false;
812
- col.hide = persistedColumn.hide ?? false;
813
- }
814
- return col;
815
- })
816
- : executionResult.result.columns.map(
817
- (colName) =>
818
- ({
819
- minWidth: 50,
820
- sortable: true,
821
- resizable: true,
822
- field: colName,
823
- flex: 1,
824
- cellRenderer: QueryResultCellRenderer,
825
- cellRendererParams: {
826
- resultState: resultState,
827
- tdsExecutionResult: executionResult,
828
- },
829
- }) as DataGridColumnDefinition,
830
- );
831
- const sideBar = isAdvancedModeEnabled ? ['columns', 'filters'] : null;
832
-
833
- const rowData = executionResult.result.rows.map((_row, rowIdx) => {
834
- const row: PlainObject = {};
835
- const cols = executionResult.result.columns;
836
- _row.values.forEach((value, colIdx) => {
837
- // `ag-grid` shows `false` value as empty string so we have
838
- // call `.toString()` to avoid this behavior.
839
- // See https://github.com/finos/legend-studio/issues/1008
840
- row[cols[colIdx] as string] = isBoolean(value) ? String(value) : value;
841
- });
842
-
843
- row.rowNumber = rowIdx;
844
- return row;
845
- });
846
- const onSaveGridColumnState = (): void => {
847
- if (!columnAPi) {
848
- return;
849
- }
850
- resultState.setGridConfig({
851
- columns: columnAPi.getColumnState(),
852
- isPivotModeEnabled: columnAPi.isPivotMode(),
853
- });
854
- };
855
-
856
- return (
857
- <div className="query-builder__result__values__table">
858
- <div
859
- className={clsx(
860
- 'ag-theme-balham-dark query-builder__result__tds-grid',
861
- )}
862
- >
863
- {isAdvancedModeEnabled ? (
864
- <DataGrid
865
- rowData={rowData}
866
- onGridReady={(params): void => {
867
- setColumnApi(params.columnApi);
868
- params.columnApi.setPivotMode(
869
- resultState.gridConfig.isPivotModeEnabled,
870
- );
871
- }}
872
- gridOptions={{
873
- suppressScrollOnNewData: true,
874
- getRowId: (data) => data.data.rowNumber,
875
- rowSelection: 'multiple',
876
- pivotPanelShow: 'always',
877
- rowGroupPanelShow: 'always',
878
- }}
879
- // NOTE: when column definition changed, we need to force refresh the cell to make sure the cell renderer is updated
880
- // See https://stackoverflow.com/questions/56341073/how-to-refresh-an-ag-grid-when-a-change-occurs-inside-a-custom-cell-renderer-com
881
- onRowDataUpdated={(params) => {
882
- params.api.refreshCells({ force: true });
883
- }}
884
- suppressFieldDotNotation={true}
885
- suppressContextMenu={!isAdvancedModeEnabled}
886
- columnDefs={colDefs}
887
- sideBar={sideBar}
888
- onColumnVisible={onSaveGridColumnState}
889
- onColumnPinned={onSaveGridColumnState}
890
- onColumnResized={onSaveGridColumnState}
891
- onColumnRowGroupChanged={onSaveGridColumnState}
892
- onColumnValueChanged={onSaveGridColumnState}
893
- onColumnPivotChanged={onSaveGridColumnState}
894
- onColumnPivotModeChanged={onSaveGridColumnState}
895
- />
896
- ) : (
897
- <DataGrid
898
- rowData={rowData}
899
- gridOptions={{
900
- suppressScrollOnNewData: true,
901
- getRowId: (data) => data.data.rowNumber,
902
- rowSelection: 'multiple',
903
- }}
904
- // NOTE: when column definition changed, we need to force refresh the cell to make sure the cell renderer is updated
905
- // See https://stackoverflow.com/questions/56341073/how-to-refresh-an-ag-grid-when-a-change-occurs-inside-a-custom-cell-renderer-com
906
- onRowDataUpdated={(params) => {
907
- params.api.refreshCells({ force: true });
908
- }}
909
- suppressFieldDotNotation={true}
910
- suppressContextMenu={!isAdvancedModeEnabled}
911
- columnDefs={colDefs}
912
- />
913
- )}
914
- </div>
915
- </div>
916
- );
917
- },
918
- );
919
-
920
- const QueryBuilderResultValues = observer(
921
- (props: {
922
- executionResult: ExecutionResult;
923
- queryBuilderState: QueryBuilderState;
924
- }) => {
925
- const { executionResult, queryBuilderState } = props;
926
- if (executionResult instanceof TDSExecutionResult) {
927
- return (
928
- <QueryBuilderGridResult
929
- queryBuilderState={queryBuilderState}
930
- executionResult={executionResult}
931
- />
932
- );
933
- } else if (executionResult instanceof RawExecutionResult) {
934
- const inputValue =
935
- executionResult.value === null
936
- ? 'null'
937
- : executionResult.value.toString();
938
- return (
939
- <CodeEditor
940
- language={CODE_EDITOR_LANGUAGE.TEXT}
941
- inputValue={inputValue}
942
- isReadOnly={true}
943
- />
944
- );
945
- }
946
- return (
947
- <CodeEditor
948
- language={CODE_EDITOR_LANGUAGE.JSON}
949
- inputValue={JSON.stringify(
950
- extractExecutionResultValues(executionResult),
951
- null,
952
- DEFAULT_TAB_SIZE,
953
- )}
954
- isReadOnly={true}
955
- />
956
- );
957
- },
958
- );
959
-
960
- export const QueryBuilderResultPanel = observer(
961
- (props: { queryBuilderState: QueryBuilderState }) => {
962
- const { queryBuilderState } = props;
963
- const applicationStore = useApplicationStore();
964
- const resultState = queryBuilderState.resultState;
965
- const queryParametersState = queryBuilderState.parametersState;
966
- const executionResult = resultState.executionResult;
967
- const [showSqlModal, setShowSqlModal] = useState(false);
968
- const relationalActivities = executionResult?.activities;
969
- const executedSqls = relationalActivities
970
- ?.filter(filterByType(RelationalExecutionActivities))
971
- .map((relationalActivity) => relationalActivity.sql);
972
-
973
- let executedSql = '';
974
- if (executedSqls?.length && executedSqls.length > 1) {
975
- for (let i = 0; i < executedSqls.length; i++) {
976
- executedSql += `\n--QUERY #${i + 1}\n`;
977
- executedSql += `${executedSqls[i]}\n`;
978
- }
979
- } else {
980
- executedSql += executedSqls?.[0];
981
- }
982
-
983
- const fetchStructureImplementation =
984
- queryBuilderState.fetchStructureState.implementation;
985
- const USER_ATTESTATION_MESSAGE =
986
- 'I attest that I am aware of the sensitive data leakage risk when exporting queried data. The data I export will only be used by me.';
987
- const exportQueryResults = async (format: string): Promise<void> => {
988
- if (queryBuilderState.parametersState.parameterStates.length) {
989
- queryParametersState.parameterValuesEditorState.open(
990
- (): Promise<void> =>
991
- flowResult(resultState.exportData(format)).catch(
992
- applicationStore.alertUnhandledError,
993
- ),
994
- PARAMETER_SUBMIT_ACTION.EXPORT,
995
- );
996
- } else {
997
- await flowResult(resultState.exportData(format)).catch(
998
- applicationStore.alertUnhandledError,
999
- );
1000
- }
1001
- };
1002
- const confirmExport = (format: string): void => {
1003
- applicationStore.alertService.setActionAlertInfo({
1004
- message: USER_ATTESTATION_MESSAGE,
1005
- type: ActionAlertType.CAUTION,
1006
- actions: [
1007
- {
1008
- label: 'Accept',
1009
- type: ActionAlertActionType.PROCEED_WITH_CAUTION,
1010
- handler: applicationStore.guardUnhandledError(() =>
1011
- exportQueryResults(format),
1012
- ),
1013
- },
1014
- {
1015
- label: 'Decline',
1016
- type: ActionAlertActionType.PROCEED,
1017
- default: true,
1018
- },
1019
- ],
1020
- });
1021
- };
1022
-
1023
- const allValidationIssues = queryBuilderState.allValidationIssues;
1024
-
1025
- const isSupportedQueryValid = allValidationIssues.length === 0;
1026
-
1027
- const isQueryValid =
1028
- !queryBuilderState.isQuerySupported || isSupportedQueryValid;
1029
-
1030
- const isQueryValidForAdvancedMode =
1031
- isQueryValid &&
1032
- queryBuilderState.fetchStructureState.implementation instanceof
1033
- QueryBuilderTDSState;
1034
-
1035
- const runQuery = (): void => {
1036
- resultState.setSelectedCells([]);
1037
- resultState.pressedRunQuery.inProgress();
1038
- if (queryParametersState.parameterStates.length) {
1039
- queryParametersState.parameterValuesEditorState.open(
1040
- (): Promise<void> =>
1041
- flowResult(resultState.runQuery()).catch(
1042
- applicationStore.alertUnhandledError,
1043
- ),
1044
- PARAMETER_SUBMIT_ACTION.RUN,
1045
- );
1046
- } else {
1047
- flowResult(resultState.runQuery()).catch(
1048
- applicationStore.alertUnhandledError,
1049
- );
1050
- }
1051
- resultState.pressedRunQuery.complete();
1052
- };
1053
- const cancelQuery = applicationStore.guardUnhandledError(() =>
1054
- flowResult(resultState.cancelQuery()),
1055
- );
1056
-
1057
- const generatePlan = applicationStore.guardUnhandledError(() =>
1058
- flowResult(resultState.generatePlan(false)),
1059
- );
1060
- const debugPlanGeneration = applicationStore.guardUnhandledError(() =>
1061
- flowResult(resultState.generatePlan(true)),
1062
- );
1063
-
1064
- const allowSettingPreviewLimit = queryBuilderState.isQuerySupported;
1065
- const allowSettingAdvancedMode = queryBuilderState.isQuerySupported;
1066
-
1067
- const copyExpression = (value: string): void => {
1068
- applicationStore.clipboardService
1069
- .copyTextToClipboard(value)
1070
- .then(() =>
1071
- applicationStore.notificationService.notifySuccess(
1072
- 'SQL Query copied',
1073
- undefined,
1074
- 2500,
1075
- ),
1076
- )
1077
- .catch(applicationStore.alertUnhandledError);
1078
- };
1079
-
1080
- const isRunQueryDisabled =
1081
- !isQueryValid ||
1082
- resultState.isGeneratingPlan ||
1083
- resultState.pressedRunQuery.isInProgress;
1084
-
1085
- const getResultSetDescription = (
1086
- _executionResult: ExecutionResult,
1087
- ): string | undefined => {
1088
- const queryDuration = resultState.executionDuration
1089
- ? prettyDuration(resultState.executionDuration, {
1090
- ms: true,
1091
- })
1092
- : undefined;
1093
- if (_executionResult instanceof TDSExecutionResult) {
1094
- const rowLength = _executionResult.result.rows.length;
1095
- return `${rowLength} row(s)${
1096
- queryDuration ? ` in ${queryDuration}` : ''
1097
- }`;
1098
- }
1099
- if (!queryDuration) {
1100
- return undefined;
1101
- }
1102
- return `query ran in ${queryDuration}`;
1103
- };
1104
- const resultDescription = executionResult
1105
- ? getResultSetDescription(executionResult)
1106
- : undefined;
1107
-
1108
- const [previewLimitValue, setPreviewLimitValue] = useState(
1109
- resultState.previewLimit,
1110
- );
1111
-
1112
- const changePreviewLimit: React.ChangeEventHandler<HTMLInputElement> = (
1113
- event,
1114
- ) => {
1115
- setPreviewLimitValue(parseInt(event.target.value, 10));
1116
- };
1117
-
1118
- const inputRef = useRef<HTMLInputElement>(null);
1119
-
1120
- const getPreviewLimit = (): void => {
1121
- if (isNaN(previewLimitValue) || previewLimitValue === 0) {
1122
- setPreviewLimitValue(1);
1123
- queryBuilderState.resultState.setPreviewLimit(1);
1124
- } else {
1125
- queryBuilderState.resultState.setPreviewLimit(previewLimitValue);
1126
- }
1127
- };
1128
-
1129
- const onKeyDown: React.KeyboardEventHandler<HTMLInputElement> = (event) => {
1130
- if (event.code === 'Enter') {
1131
- getPreviewLimit();
1132
- inputRef.current?.focus();
1133
- } else if (event.code === 'Escape') {
1134
- inputRef.current?.select();
1135
- }
1136
- };
1137
-
1138
- const toggleIsAdvancedModeEnabled = (): void => {
1139
- resultState.setExecutionResult(undefined);
1140
- queryBuilderState.setIsAdvancedModeEnabled(
1141
- !queryBuilderState.isAdvancedModeEnabled,
1142
- );
1143
- };
1144
-
1145
- return (
1146
- <div
1147
- data-testid={QUERY_BUILDER_TEST_ID.QUERY_BUILDER_RESULT_PANEL}
1148
- className="panel query-builder__result"
1149
- >
1150
- {showSqlModal && executedSqls && (
1151
- <Dialog
1152
- open={Boolean(showSqlModal)}
1153
- onClose={() => setShowSqlModal(false)}
1154
- >
1155
- <Modal className="editor-modal" darkMode={true}>
1156
- <ModalHeader title="Executed SQL Query" />
1157
- <ModalBody className="query-builder__sql__modal">
1158
- <>
1159
- <CodeEditor
1160
- inputValue={tryToFormatSql(executedSql)}
1161
- isReadOnly={true}
1162
- language={CODE_EDITOR_LANGUAGE.SQL}
1163
- hideMinimap={true}
1164
- />
1165
-
1166
- <PanelDivider />
1167
- </>
1168
- </ModalBody>
1169
- <ModalFooter>
1170
- <ModalFooterButton
1171
- formatText={false}
1172
- onClick={() => copyExpression(executedSql)}
1173
- text="Copy SQL to Clipboard"
1174
- />
1175
-
1176
- <ModalFooterButton
1177
- onClick={() => setShowSqlModal(false)}
1178
- text="Close"
1179
- />
1180
- </ModalFooter>
1181
- </Modal>
1182
- </Dialog>
1183
- )}
1184
-
1185
- <div className="panel__header">
1186
- <div className="panel__header__title">
1187
- <div className="panel__header__title__label">result</div>
1188
- {executedSqls && (
1189
- <Button
1190
- onClick={() => setShowSqlModal(true)}
1191
- title="Executed SQL"
1192
- className="query-builder__result__sql__actions"
1193
- >
1194
- <SQLIcon />
1195
- </Button>
1196
- )}
1197
- {resultState.pressedRunQuery.isInProgress && (
1198
- <div className="panel__header__title__label__status">
1199
- Running Query...
1200
- </div>
1201
- )}
1202
-
1203
- <div
1204
- data-testid={QUERY_BUILDER_TEST_ID.QUERY_BUILDER_RESULT_ANALYTICS}
1205
- className="query-builder__result__analytics"
1206
- >
1207
- {resultDescription ?? ''}
1208
- </div>
1209
- {executionResult && resultState.checkForStaleResults && (
1210
- <div className="query-builder__result__stale-status">
1211
- <div className="query-builder__result__stale-status__icon">
1212
- <ExclamationTriangleIcon />
1213
- </div>
1214
- <div className="query-builder__result__stale-status__label">
1215
- Preview data might be stale
1216
- </div>
1217
- </div>
1218
- )}
1219
- </div>
1220
- <div className="panel__header__actions query-builder__result__header__actions">
1221
- {allowSettingAdvancedMode && (
1222
- <div className="query-builder__result__advanced__mode">
1223
- <div className="query-builder__result__advanced__mode__label">
1224
- Advanced Mode
1225
- <DocumentationLink
1226
- title="The grid in advanced mode performs all operations like grouping, sorting, filtering, etc after initial query execution locally withought reaching out to server. This limits the number of rows to smaller number so they can fit in memory"
1227
- documentationKey={
1228
- QUERY_BUILDER_DOCUMENTATION_KEY.QUESTION_HOW_TO_USE_ADVANCED_GRID_MODE
1229
- }
1230
- />
1231
- </div>
1232
- <button
1233
- className={clsx(
1234
- 'query-builder__result__advanced__mode__toggler__btn',
1235
- {
1236
- 'query-builder__result__advanced__mode__toggler__btn--toggled':
1237
- queryBuilderState.isAdvancedModeEnabled,
1238
- },
1239
- )}
1240
- disabled={!isQueryValidForAdvancedMode}
1241
- onClick={toggleIsAdvancedModeEnabled}
1242
- tabIndex={-1}
1243
- >
1244
- {queryBuilderState.isAdvancedModeEnabled ? (
1245
- <CheckSquareIcon />
1246
- ) : (
1247
- <SquareIcon />
1248
- )}
1249
- </button>
1250
- </div>
1251
- )}
1252
-
1253
- {allowSettingPreviewLimit && (
1254
- <div className="query-builder__result__limit">
1255
- <div className="query-builder__result__limit__label">
1256
- preview limit
1257
- </div>
1258
- <input
1259
- ref={inputRef}
1260
- className="input--dark query-builder__result__limit__input"
1261
- spellCheck={false}
1262
- type="number"
1263
- value={previewLimitValue}
1264
- onChange={changePreviewLimit}
1265
- onBlur={getPreviewLimit}
1266
- onKeyDown={onKeyDown}
1267
- disabled={!isQueryValid}
1268
- />
1269
- </div>
1270
- )}
1271
-
1272
- <div className="query-builder__result__execute-btn btn__dropdown-combo btn__dropdown-combo--primary">
1273
- {resultState.isRunningQuery ? (
1274
- <button
1275
- className="btn__dropdown-combo__canceler"
1276
- onClick={cancelQuery}
1277
- tabIndex={-1}
1278
- disabled={!isQueryValid}
1279
- >
1280
- <div className="btn--dark btn--caution btn__dropdown-combo__canceler__label">
1281
- <PauseCircleIcon className="btn__dropdown-combo__canceler__label__icon" />
1282
- <div className="btn__dropdown-combo__canceler__label__title">
1283
- Stop
1284
- </div>
1285
- </div>
1286
- </button>
1287
- ) : (
1288
- <>
1289
- <button
1290
- className="btn__dropdown-combo__label"
1291
- onClick={runQuery}
1292
- tabIndex={-1}
1293
- title={
1294
- allValidationIssues.length
1295
- ? `Query is not valid:\n${allValidationIssues
1296
- .map((issue) => `\u2022 ${issue}`)
1297
- .join('\n')}`
1298
- : undefined
1299
- }
1300
- disabled={isRunQueryDisabled}
1301
- >
1302
- <PlayIcon className="btn__dropdown-combo__label__icon" />
1303
- <div className="btn__dropdown-combo__label__title">
1304
- Run Query
1305
- </div>
1306
- </button>
1307
- <DropdownMenu
1308
- className="btn__dropdown-combo__dropdown-btn"
1309
- disabled={isRunQueryDisabled}
1310
- content={
1311
- <MenuContent>
1312
- <MenuContentItem
1313
- className="btn__dropdown-combo__option"
1314
- onClick={generatePlan}
1315
- disabled={isRunQueryDisabled}
1316
- >
1317
- Generate Plan
1318
- </MenuContentItem>
1319
- <MenuContentItem
1320
- className="btn__dropdown-combo__option"
1321
- onClick={debugPlanGeneration}
1322
- disabled={isRunQueryDisabled}
1323
- >
1324
- Debug
1325
- </MenuContentItem>
1326
- </MenuContent>
1327
- }
1328
- menuProps={{
1329
- anchorOrigin: { vertical: 'bottom', horizontal: 'right' },
1330
- transformOrigin: { vertical: 'top', horizontal: 'right' },
1331
- }}
1332
- >
1333
- <CaretDownIcon />
1334
- </DropdownMenu>
1335
- </>
1336
- )}
1337
- </div>
1338
- <DropdownMenu
1339
- className="query-builder__result__export__dropdown"
1340
- title="Export"
1341
- disabled={!isQueryValid}
1342
- content={
1343
- <MenuContent>
1344
- {Object.values(
1345
- fetchStructureImplementation.exportDataFormatOptions,
1346
- ).map((format) => (
1347
- <MenuContentItem
1348
- key={format}
1349
- className="query-builder__result__export__dropdown__menu__item"
1350
- onClick={(): void => confirmExport(format)}
1351
- >
1352
- {format}
1353
- </MenuContentItem>
1354
- ))}
1355
- <MenuContentItem
1356
- className="query-builder__result__export__dropdown__menu__item"
1357
- onClick={(): void =>
1358
- resultState.setIsQueryUsageViewerOpened(true)
1359
- }
1360
- disabled={queryBuilderState.changeDetectionState.hasChanged}
1361
- >
1362
- View Query Usage...
1363
- </MenuContentItem>
1364
- </MenuContent>
1365
- }
1366
- menuProps={{
1367
- anchorOrigin: { vertical: 'bottom', horizontal: 'right' },
1368
- transformOrigin: { vertical: 'top', horizontal: 'right' },
1369
- elevation: 7,
1370
- }}
1371
- >
1372
- <div className="query-builder__result__export__dropdown__label">
1373
- Export
1374
- </div>
1375
- <div className="query-builder__result__export__dropdown__trigger">
1376
- <CaretDownIcon />
1377
- </div>
1378
- </DropdownMenu>
1379
- {resultState.isQueryUsageViewerOpened && (
1380
- <QueryUsageViewer resultState={resultState} />
1381
- )}
1382
- </div>
1383
- </div>
1384
- <PanelContent>
1385
- <PanelLoadingIndicator
1386
- isLoading={
1387
- resultState.isRunningQuery ||
1388
- resultState.isGeneratingPlan ||
1389
- resultState.exportDataState.isInProgress
1390
- }
1391
- />
1392
- {!executionResult && (
1393
- <BlankPanelContent>
1394
- Build or load a valid query first
1395
- </BlankPanelContent>
1396
- )}
1397
- {executionResult && (
1398
- <div className="query-builder__result__values">
1399
- <QueryBuilderResultValues
1400
- executionResult={executionResult}
1401
- queryBuilderState={queryBuilderState}
1402
- />
1403
- </div>
1404
- )}
1405
- </PanelContent>
1406
- <ExecutionPlanViewer
1407
- executionPlanState={resultState.executionPlanState}
1408
- />
1409
- </div>
1410
- );
1411
- },
1412
- );