@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.
- package/lib/components/QueryBuilder.js +1 -1
- package/lib/components/QueryBuilder.js.map +1 -1
- package/lib/components/execution-plan/SQLExecutionNodeViewer.js +1 -1
- package/lib/components/execution-plan/SQLExecutionNodeViewer.js.map +1 -1
- package/lib/components/{QueryBuilderResultPanel.d.ts → result/QueryBuilderResultPanel.d.ts} +1 -2
- package/lib/components/result/QueryBuilderResultPanel.d.ts.map +1 -0
- package/lib/components/result/QueryBuilderResultPanel.js +185 -0
- package/lib/components/result/QueryBuilderResultPanel.js.map +1 -0
- package/lib/components/result/tds/QueryBuilderTDSGridResult.d.ts +24 -0
- package/lib/components/result/tds/QueryBuilderTDSGridResult.d.ts.map +1 -0
- package/lib/components/result/tds/QueryBuilderTDSGridResult.js +140 -0
- package/lib/components/result/tds/QueryBuilderTDSGridResult.js.map +1 -0
- package/lib/components/result/tds/QueryBuilderTDSResultShared.d.ts +41 -0
- package/lib/components/result/tds/QueryBuilderTDSResultShared.d.ts.map +1 -0
- package/lib/components/result/tds/QueryBuilderTDSResultShared.js +465 -0
- package/lib/components/result/tds/QueryBuilderTDSResultShared.js.map +1 -0
- package/lib/components/result/tds/QueryBuilderTDSSimpleGridResult.d.ts +24 -0
- package/lib/components/result/tds/QueryBuilderTDSSimpleGridResult.d.ts.map +1 -0
- package/lib/components/result/tds/QueryBuilderTDSSimpleGridResult.js +47 -0
- package/lib/components/result/tds/QueryBuilderTDSSimpleGridResult.js.map +1 -0
- package/lib/index.css +2 -2
- package/lib/index.css.map +1 -1
- package/lib/package.json +1 -1
- package/lib/stores/QueryBuilderResultState.d.ts +5 -1
- package/lib/stores/QueryBuilderResultState.d.ts.map +1 -1
- package/lib/stores/QueryBuilderResultState.js.map +1 -1
- package/package.json +2 -2
- package/src/components/QueryBuilder.tsx +1 -1
- package/src/components/execution-plan/SQLExecutionNodeViewer.tsx +1 -1
- package/src/components/result/QueryBuilderResultPanel.tsx +570 -0
- package/src/components/result/tds/QueryBuilderTDSGridResult.tsx +239 -0
- package/src/components/result/tds/QueryBuilderTDSResultShared.tsx +866 -0
- package/src/components/result/tds/QueryBuilderTDSSimpleGridResult.tsx +80 -0
- package/src/stores/QueryBuilderResultState.ts +12 -1
- package/tsconfig.json +4 -1
- package/lib/components/QueryBuilderResultPanel.d.ts.map +0 -1
- package/lib/components/QueryBuilderResultPanel.js +0 -633
- package/lib/components/QueryBuilderResultPanel.js.map +0 -1
- 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
|
-
);
|