@finos/legend-query-builder 4.11.4 → 4.11.6

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