@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,570 @@
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
+ clsx,
26
+ PauseCircleIcon,
27
+ ExclamationTriangleIcon,
28
+ PanelContent,
29
+ Button,
30
+ SQLIcon,
31
+ Dialog,
32
+ Modal,
33
+ ModalBody,
34
+ ModalFooter,
35
+ ModalFooterButton,
36
+ ModalHeader,
37
+ PanelDivider,
38
+ SquareIcon,
39
+ CheckSquareIcon,
40
+ } from '@finos/legend-art';
41
+ import { observer } from 'mobx-react-lite';
42
+ import { flowResult } from 'mobx';
43
+ import type { QueryBuilderState } from '../../stores/QueryBuilderState.js';
44
+ import {
45
+ type ExecutionResult,
46
+ extractExecutionResultValues,
47
+ TDSExecutionResult,
48
+ RawExecutionResult,
49
+ } from '@finos/legend-graph';
50
+ import {
51
+ ActionAlertActionType,
52
+ ActionAlertType,
53
+ DEFAULT_TAB_SIZE,
54
+ useApplicationStore,
55
+ } from '@finos/legend-application';
56
+ import { prettyDuration } from '@finos/legend-shared';
57
+ import { useRef, useState } from 'react';
58
+ import { QueryBuilderTDSState } from '../../stores/fetch-structure/tds/QueryBuilderTDSState.js';
59
+ import { PARAMETER_SUBMIT_ACTION } from '../../stores/shared/LambdaParameterState.js';
60
+
61
+ import { QUERY_BUILDER_TEST_ID } from '../../__lib__/QueryBuilderTesting.js';
62
+ import {
63
+ CODE_EDITOR_LANGUAGE,
64
+ CodeEditor,
65
+ } from '@finos/legend-lego/code-editor';
66
+ import { ExecutionPlanViewer } from '../execution-plan/ExecutionPlanViewer.js';
67
+ import { QueryUsageViewer } from '../QueryUsageViewer.js';
68
+ import { DocumentationLink } from '@finos/legend-lego/application';
69
+ import { QUERY_BUILDER_DOCUMENTATION_KEY } from '../../__lib__/QueryBuilderDocumentation.js';
70
+ import { QueryBuilderTDSSimpleGridResult } from './tds/QueryBuilderTDSSimpleGridResult.js';
71
+ import { isEnterpriseModeEnabled } from '@finos/legend-lego/data-grid';
72
+ import {
73
+ getExecutedSqlFromExecutionResult,
74
+ tryToFormatSql,
75
+ } from './tds/QueryBuilderTDSResultShared.js';
76
+ import { QueryBuilderTDSGridResult } from './tds/QueryBuilderTDSGridResult.js';
77
+
78
+ const QueryBuilderResultValues = observer(
79
+ (props: {
80
+ executionResult: ExecutionResult;
81
+ queryBuilderState: QueryBuilderState;
82
+ }) => {
83
+ const { executionResult, queryBuilderState } = props;
84
+ if (executionResult instanceof TDSExecutionResult) {
85
+ if (isEnterpriseModeEnabled) {
86
+ return (
87
+ <QueryBuilderTDSGridResult
88
+ queryBuilderState={queryBuilderState}
89
+ executionResult={executionResult}
90
+ />
91
+ );
92
+ } else {
93
+ return (
94
+ <QueryBuilderTDSSimpleGridResult
95
+ queryBuilderState={queryBuilderState}
96
+ executionResult={executionResult}
97
+ />
98
+ );
99
+ }
100
+ } else if (executionResult instanceof RawExecutionResult) {
101
+ const inputValue =
102
+ executionResult.value === null
103
+ ? 'null'
104
+ : executionResult.value.toString();
105
+ return (
106
+ <CodeEditor
107
+ language={CODE_EDITOR_LANGUAGE.TEXT}
108
+ inputValue={inputValue}
109
+ isReadOnly={true}
110
+ />
111
+ );
112
+ }
113
+ return (
114
+ <CodeEditor
115
+ language={CODE_EDITOR_LANGUAGE.JSON}
116
+ inputValue={JSON.stringify(
117
+ extractExecutionResultValues(executionResult),
118
+ null,
119
+ DEFAULT_TAB_SIZE,
120
+ )}
121
+ isReadOnly={true}
122
+ />
123
+ );
124
+ },
125
+ );
126
+
127
+ export const QueryBuilderResultPanel = observer(
128
+ (props: { queryBuilderState: QueryBuilderState }) => {
129
+ const { queryBuilderState } = props;
130
+ const applicationStore = useApplicationStore();
131
+ const resultState = queryBuilderState.resultState;
132
+ const queryParametersState = queryBuilderState.parametersState;
133
+ const executionResult = resultState.executionResult;
134
+ const [showSqlModal, setShowSqlModal] = useState(false);
135
+ const executedSql = executionResult
136
+ ? getExecutedSqlFromExecutionResult(executionResult)
137
+ : undefined;
138
+
139
+ const fetchStructureImplementation =
140
+ queryBuilderState.fetchStructureState.implementation;
141
+ const USER_ATTESTATION_MESSAGE =
142
+ '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.';
143
+ const exportQueryResults = async (format: string): Promise<void> => {
144
+ if (queryBuilderState.parametersState.parameterStates.length) {
145
+ queryParametersState.parameterValuesEditorState.open(
146
+ (): Promise<void> =>
147
+ flowResult(resultState.exportData(format)).catch(
148
+ applicationStore.alertUnhandledError,
149
+ ),
150
+ PARAMETER_SUBMIT_ACTION.EXPORT,
151
+ );
152
+ } else {
153
+ await flowResult(resultState.exportData(format)).catch(
154
+ applicationStore.alertUnhandledError,
155
+ );
156
+ }
157
+ };
158
+ const confirmExport = (format: string): void => {
159
+ applicationStore.alertService.setActionAlertInfo({
160
+ message: USER_ATTESTATION_MESSAGE,
161
+ type: ActionAlertType.CAUTION,
162
+ actions: [
163
+ {
164
+ label: 'Accept',
165
+ type: ActionAlertActionType.PROCEED_WITH_CAUTION,
166
+ handler: applicationStore.guardUnhandledError(() =>
167
+ exportQueryResults(format),
168
+ ),
169
+ },
170
+ {
171
+ label: 'Decline',
172
+ type: ActionAlertActionType.PROCEED,
173
+ default: true,
174
+ },
175
+ ],
176
+ });
177
+ };
178
+
179
+ const allValidationIssues = queryBuilderState.allValidationIssues;
180
+
181
+ const isSupportedQueryValid = allValidationIssues.length === 0;
182
+
183
+ const isQueryValid =
184
+ !queryBuilderState.isQuerySupported || isSupportedQueryValid;
185
+
186
+ const isQueryValidForAdvancedMode =
187
+ isQueryValid &&
188
+ queryBuilderState.fetchStructureState.implementation instanceof
189
+ QueryBuilderTDSState;
190
+
191
+ const runQuery = (): void => {
192
+ resultState.setSelectedCells([]);
193
+ resultState.pressedRunQuery.inProgress();
194
+ if (queryParametersState.parameterStates.length) {
195
+ queryParametersState.parameterValuesEditorState.open(
196
+ (): Promise<void> =>
197
+ flowResult(resultState.runQuery()).catch(
198
+ applicationStore.alertUnhandledError,
199
+ ),
200
+ PARAMETER_SUBMIT_ACTION.RUN,
201
+ );
202
+ } else {
203
+ flowResult(resultState.runQuery()).catch(
204
+ applicationStore.alertUnhandledError,
205
+ );
206
+ }
207
+ resultState.pressedRunQuery.complete();
208
+ };
209
+ const cancelQuery = applicationStore.guardUnhandledError(() =>
210
+ flowResult(resultState.cancelQuery()),
211
+ );
212
+
213
+ const generatePlan = applicationStore.guardUnhandledError(() =>
214
+ flowResult(resultState.generatePlan(false)),
215
+ );
216
+ const debugPlanGeneration = applicationStore.guardUnhandledError(() =>
217
+ flowResult(resultState.generatePlan(true)),
218
+ );
219
+
220
+ const allowSettingPreviewLimit = queryBuilderState.isQuerySupported;
221
+
222
+ const allowSettingAdvancedMode =
223
+ queryBuilderState.isQuerySupported && isEnterpriseModeEnabled;
224
+
225
+ const copyExpression = (value: string): void => {
226
+ applicationStore.clipboardService
227
+ .copyTextToClipboard(value)
228
+ .then(() =>
229
+ applicationStore.notificationService.notifySuccess(
230
+ 'SQL Query copied',
231
+ undefined,
232
+ 2500,
233
+ ),
234
+ )
235
+ .catch(applicationStore.alertUnhandledError);
236
+ };
237
+
238
+ const isRunQueryDisabled =
239
+ !isQueryValid ||
240
+ resultState.isGeneratingPlan ||
241
+ resultState.pressedRunQuery.isInProgress;
242
+
243
+ const getResultSetDescription = (
244
+ _executionResult: ExecutionResult,
245
+ ): string | undefined => {
246
+ const queryDuration = resultState.executionDuration
247
+ ? prettyDuration(resultState.executionDuration, {
248
+ ms: true,
249
+ })
250
+ : undefined;
251
+ if (_executionResult instanceof TDSExecutionResult) {
252
+ const rowLength = _executionResult.result.rows.length;
253
+ return `${rowLength} row(s)${
254
+ queryDuration ? ` in ${queryDuration}` : ''
255
+ }`;
256
+ }
257
+ if (!queryDuration) {
258
+ return undefined;
259
+ }
260
+ return `query ran in ${queryDuration}`;
261
+ };
262
+ const resultDescription = executionResult
263
+ ? getResultSetDescription(executionResult)
264
+ : undefined;
265
+
266
+ const [previewLimitValue, setPreviewLimitValue] = useState(
267
+ resultState.previewLimit,
268
+ );
269
+
270
+ const changePreviewLimit: React.ChangeEventHandler<HTMLInputElement> = (
271
+ event,
272
+ ) => {
273
+ setPreviewLimitValue(parseInt(event.target.value, 10));
274
+ };
275
+
276
+ const inputRef = useRef<HTMLInputElement>(null);
277
+
278
+ const getPreviewLimit = (): void => {
279
+ if (isNaN(previewLimitValue) || previewLimitValue === 0) {
280
+ setPreviewLimitValue(1);
281
+ queryBuilderState.resultState.setPreviewLimit(1);
282
+ } else {
283
+ queryBuilderState.resultState.setPreviewLimit(previewLimitValue);
284
+ }
285
+ };
286
+
287
+ const onKeyDown: React.KeyboardEventHandler<HTMLInputElement> = (event) => {
288
+ if (event.code === 'Enter') {
289
+ getPreviewLimit();
290
+ inputRef.current?.focus();
291
+ } else if (event.code === 'Escape') {
292
+ inputRef.current?.select();
293
+ }
294
+ };
295
+
296
+ const toggleIsAdvancedModeEnabled = (): void => {
297
+ resultState.setExecutionResult(undefined);
298
+ queryBuilderState.setIsAdvancedModeEnabled(
299
+ !queryBuilderState.isAdvancedModeEnabled,
300
+ );
301
+ };
302
+
303
+ return (
304
+ <div
305
+ data-testid={QUERY_BUILDER_TEST_ID.QUERY_BUILDER_RESULT_PANEL}
306
+ className="panel query-builder__result"
307
+ >
308
+ {showSqlModal && executedSql && (
309
+ <Dialog
310
+ open={Boolean(showSqlModal)}
311
+ onClose={() => setShowSqlModal(false)}
312
+ >
313
+ <Modal className="editor-modal" darkMode={true}>
314
+ <ModalHeader title="Executed SQL Query" />
315
+ <ModalBody className="query-builder__sql__modal">
316
+ <>
317
+ <CodeEditor
318
+ inputValue={tryToFormatSql(executedSql)}
319
+ isReadOnly={true}
320
+ language={CODE_EDITOR_LANGUAGE.SQL}
321
+ hideMinimap={true}
322
+ />
323
+
324
+ <PanelDivider />
325
+ </>
326
+ </ModalBody>
327
+ <ModalFooter>
328
+ <ModalFooterButton
329
+ formatText={false}
330
+ onClick={() => copyExpression(executedSql)}
331
+ text="Copy SQL to Clipboard"
332
+ />
333
+
334
+ <ModalFooterButton
335
+ onClick={() => setShowSqlModal(false)}
336
+ text="Close"
337
+ />
338
+ </ModalFooter>
339
+ </Modal>
340
+ </Dialog>
341
+ )}
342
+
343
+ <div className="panel__header">
344
+ <div className="panel__header__title">
345
+ <div className="panel__header__title__label">result</div>
346
+ {executedSql && (
347
+ <Button
348
+ onClick={() => setShowSqlModal(true)}
349
+ title="Executed SQL"
350
+ className="query-builder__result__sql__actions"
351
+ >
352
+ <SQLIcon />
353
+ </Button>
354
+ )}
355
+ {resultState.pressedRunQuery.isInProgress && (
356
+ <div className="panel__header__title__label__status">
357
+ Running Query...
358
+ </div>
359
+ )}
360
+
361
+ <div
362
+ data-testid={QUERY_BUILDER_TEST_ID.QUERY_BUILDER_RESULT_ANALYTICS}
363
+ className="query-builder__result__analytics"
364
+ >
365
+ {resultDescription ?? ''}
366
+ </div>
367
+ {executionResult && resultState.checkForStaleResults && (
368
+ <div className="query-builder__result__stale-status">
369
+ <div className="query-builder__result__stale-status__icon">
370
+ <ExclamationTriangleIcon />
371
+ </div>
372
+ <div className="query-builder__result__stale-status__label">
373
+ Preview data might be stale
374
+ </div>
375
+ </div>
376
+ )}
377
+ </div>
378
+ <div className="panel__header__actions query-builder__result__header__actions">
379
+ {allowSettingAdvancedMode && (
380
+ <div className="query-builder__result__advanced__mode">
381
+ <div className="query-builder__result__advanced__mode__label">
382
+ Advanced Mode
383
+ <DocumentationLink
384
+ 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"
385
+ documentationKey={
386
+ QUERY_BUILDER_DOCUMENTATION_KEY.QUESTION_HOW_TO_USE_ADVANCED_GRID_MODE
387
+ }
388
+ />
389
+ </div>
390
+ <button
391
+ className={clsx(
392
+ 'query-builder__result__advanced__mode__toggler__btn',
393
+ {
394
+ 'query-builder__result__advanced__mode__toggler__btn--toggled':
395
+ queryBuilderState.isAdvancedModeEnabled,
396
+ },
397
+ )}
398
+ disabled={!isQueryValidForAdvancedMode}
399
+ onClick={toggleIsAdvancedModeEnabled}
400
+ tabIndex={-1}
401
+ >
402
+ {queryBuilderState.isAdvancedModeEnabled ? (
403
+ <CheckSquareIcon />
404
+ ) : (
405
+ <SquareIcon />
406
+ )}
407
+ </button>
408
+ </div>
409
+ )}
410
+
411
+ {allowSettingPreviewLimit && (
412
+ <div className="query-builder__result__limit">
413
+ <div className="query-builder__result__limit__label">
414
+ preview limit
415
+ </div>
416
+ <input
417
+ ref={inputRef}
418
+ className="input--dark query-builder__result__limit__input"
419
+ spellCheck={false}
420
+ type="number"
421
+ value={previewLimitValue}
422
+ onChange={changePreviewLimit}
423
+ onBlur={getPreviewLimit}
424
+ onKeyDown={onKeyDown}
425
+ disabled={!isQueryValid}
426
+ />
427
+ </div>
428
+ )}
429
+
430
+ <div className="query-builder__result__execute-btn btn__dropdown-combo btn__dropdown-combo--primary">
431
+ {resultState.isRunningQuery ? (
432
+ <button
433
+ className="btn__dropdown-combo__canceler"
434
+ onClick={cancelQuery}
435
+ tabIndex={-1}
436
+ disabled={!isQueryValid}
437
+ >
438
+ <div className="btn--dark btn--caution btn__dropdown-combo__canceler__label">
439
+ <PauseCircleIcon className="btn__dropdown-combo__canceler__label__icon" />
440
+ <div className="btn__dropdown-combo__canceler__label__title">
441
+ Stop
442
+ </div>
443
+ </div>
444
+ </button>
445
+ ) : (
446
+ <>
447
+ <button
448
+ className="btn__dropdown-combo__label"
449
+ onClick={runQuery}
450
+ tabIndex={-1}
451
+ title={
452
+ allValidationIssues.length
453
+ ? `Query is not valid:\n${allValidationIssues
454
+ .map((issue) => `\u2022 ${issue}`)
455
+ .join('\n')}`
456
+ : undefined
457
+ }
458
+ disabled={isRunQueryDisabled}
459
+ >
460
+ <PlayIcon className="btn__dropdown-combo__label__icon" />
461
+ <div className="btn__dropdown-combo__label__title">
462
+ Run Query
463
+ </div>
464
+ </button>
465
+ <DropdownMenu
466
+ className="btn__dropdown-combo__dropdown-btn"
467
+ disabled={isRunQueryDisabled}
468
+ content={
469
+ <MenuContent>
470
+ <MenuContentItem
471
+ className="btn__dropdown-combo__option"
472
+ onClick={generatePlan}
473
+ disabled={isRunQueryDisabled}
474
+ >
475
+ Generate Plan
476
+ </MenuContentItem>
477
+ <MenuContentItem
478
+ className="btn__dropdown-combo__option"
479
+ onClick={debugPlanGeneration}
480
+ disabled={isRunQueryDisabled}
481
+ >
482
+ Debug
483
+ </MenuContentItem>
484
+ </MenuContent>
485
+ }
486
+ menuProps={{
487
+ anchorOrigin: { vertical: 'bottom', horizontal: 'right' },
488
+ transformOrigin: { vertical: 'top', horizontal: 'right' },
489
+ }}
490
+ >
491
+ <CaretDownIcon />
492
+ </DropdownMenu>
493
+ </>
494
+ )}
495
+ </div>
496
+ <DropdownMenu
497
+ className="query-builder__result__export__dropdown"
498
+ title="Export"
499
+ disabled={!isQueryValid}
500
+ content={
501
+ <MenuContent>
502
+ {Object.values(
503
+ fetchStructureImplementation.exportDataFormatOptions,
504
+ ).map((format) => (
505
+ <MenuContentItem
506
+ key={format}
507
+ className="query-builder__result__export__dropdown__menu__item"
508
+ onClick={(): void => confirmExport(format)}
509
+ >
510
+ {format}
511
+ </MenuContentItem>
512
+ ))}
513
+ <MenuContentItem
514
+ className="query-builder__result__export__dropdown__menu__item"
515
+ onClick={(): void =>
516
+ resultState.setIsQueryUsageViewerOpened(true)
517
+ }
518
+ disabled={queryBuilderState.changeDetectionState.hasChanged}
519
+ >
520
+ View Query Usage...
521
+ </MenuContentItem>
522
+ </MenuContent>
523
+ }
524
+ menuProps={{
525
+ anchorOrigin: { vertical: 'bottom', horizontal: 'right' },
526
+ transformOrigin: { vertical: 'top', horizontal: 'right' },
527
+ elevation: 7,
528
+ }}
529
+ >
530
+ <div className="query-builder__result__export__dropdown__label">
531
+ Export
532
+ </div>
533
+ <div className="query-builder__result__export__dropdown__trigger">
534
+ <CaretDownIcon />
535
+ </div>
536
+ </DropdownMenu>
537
+ {resultState.isQueryUsageViewerOpened && (
538
+ <QueryUsageViewer resultState={resultState} />
539
+ )}
540
+ </div>
541
+ </div>
542
+ <PanelContent>
543
+ <PanelLoadingIndicator
544
+ isLoading={
545
+ resultState.isRunningQuery ||
546
+ resultState.isGeneratingPlan ||
547
+ resultState.exportDataState.isInProgress
548
+ }
549
+ />
550
+ {!executionResult && (
551
+ <BlankPanelContent>
552
+ Build or load a valid query first
553
+ </BlankPanelContent>
554
+ )}
555
+ {executionResult && (
556
+ <div className="query-builder__result__values">
557
+ <QueryBuilderResultValues
558
+ executionResult={executionResult}
559
+ queryBuilderState={queryBuilderState}
560
+ />
561
+ </div>
562
+ )}
563
+ </PanelContent>
564
+ <ExecutionPlanViewer
565
+ executionPlanState={resultState.executionPlanState}
566
+ />
567
+ </div>
568
+ );
569
+ },
570
+ );