@finos/legend-cube-builder 0.0.5 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/adding-an-operation.md +4 -2
- package/docs/demo/README.md +1 -1
- package/docs/hosting.md +40 -9
- package/lib/__lib__/LegendCubeHelpText.d.ts.map +1 -1
- package/lib/__lib__/LegendCubeHelpText.js +3 -1
- package/lib/__lib__/LegendCubeHelpText.js.map +1 -1
- package/lib/__lib__/LegendCubeLabels.d.ts +34 -5
- package/lib/__lib__/LegendCubeLabels.d.ts.map +1 -1
- package/lib/__lib__/LegendCubeLabels.js +45 -8
- package/lib/__lib__/LegendCubeLabels.js.map +1 -1
- package/lib/__lib__/LegendCubeNavigation.d.ts +23 -0
- package/lib/__lib__/LegendCubeNavigation.d.ts.map +1 -0
- package/lib/__lib__/LegendCubeNavigation.js +33 -0
- package/lib/__lib__/LegendCubeNavigation.js.map +1 -0
- package/lib/__lib__/LegendCubeTesting.d.ts +3 -1
- package/lib/__lib__/LegendCubeTesting.d.ts.map +1 -1
- package/lib/__lib__/LegendCubeTesting.js +2 -0
- package/lib/__lib__/LegendCubeTesting.js.map +1 -1
- package/lib/__lib__/LegendCubeTour.d.ts +62 -0
- package/lib/__lib__/LegendCubeTour.d.ts.map +1 -0
- package/lib/__lib__/LegendCubeTour.js +149 -0
- package/lib/__lib__/LegendCubeTour.js.map +1 -0
- package/lib/__test-utils__/CubeExampleTestUtils.d.ts +34 -0
- package/lib/__test-utils__/CubeExampleTestUtils.d.ts.map +1 -0
- package/lib/__test-utils__/CubeExampleTestUtils.js +72 -0
- package/lib/__test-utils__/CubeExampleTestUtils.js.map +1 -0
- package/lib/__test-utils__/CubeVariantTestUtils.d.ts +16 -0
- package/lib/__test-utils__/CubeVariantTestUtils.d.ts.map +1 -0
- package/lib/__test-utils__/CubeVariantTestUtils.js +66 -0
- package/lib/__test-utils__/CubeVariantTestUtils.js.map +1 -0
- package/lib/components/CubeAddItems.d.ts.map +1 -1
- package/lib/components/CubeAddItems.js +2 -1
- package/lib/components/CubeAddItems.js.map +1 -1
- package/lib/components/CubeEditor.d.ts.map +1 -1
- package/lib/components/CubeEditor.js +11 -5
- package/lib/components/CubeEditor.js.map +1 -1
- package/lib/components/CubeFileMenu.d.ts.map +1 -1
- package/lib/components/CubeFileMenu.js +2 -1
- package/lib/components/CubeFileMenu.js.map +1 -1
- package/lib/components/CubeNodeIcon.d.ts.map +1 -1
- package/lib/components/CubeNodeIcon.js +2 -1
- package/lib/components/CubeNodeIcon.js.map +1 -1
- package/lib/components/canvas/CubeCanvas.d.ts.map +1 -1
- package/lib/components/canvas/CubeCanvas.js +3 -2
- package/lib/components/canvas/CubeCanvas.js.map +1 -1
- package/lib/components/canvas/CubeCanvasNode.d.ts.map +1 -1
- package/lib/components/canvas/CubeCanvasNode.js +4 -1
- package/lib/components/canvas/CubeCanvasNode.js.map +1 -1
- package/lib/components/dev-settings/CubeDevSettings.d.ts +2 -2
- package/lib/components/dev-settings/CubeDevSettings.d.ts.map +1 -1
- package/lib/components/dev-settings/CubeDevSettings.js +12 -8
- package/lib/components/dev-settings/CubeDevSettings.js.map +1 -1
- package/lib/components/editors/CubeColumnPicker.d.ts.map +1 -1
- package/lib/components/editors/CubeColumnPicker.js +3 -1
- package/lib/components/editors/CubeColumnPicker.js.map +1 -1
- package/lib/components/editors/CubeExplodeEditor.d.ts +26 -0
- package/lib/components/editors/CubeExplodeEditor.d.ts.map +1 -0
- package/lib/components/editors/CubeExplodeEditor.js +61 -0
- package/lib/components/editors/CubeExplodeEditor.js.map +1 -0
- package/lib/components/editors/CubeExpressionFilterEditor.d.ts.map +1 -1
- package/lib/components/editors/CubeExpressionFilterEditor.js +8 -5
- package/lib/components/editors/CubeExpressionFilterEditor.js.map +1 -1
- package/lib/components/editors/CubeExtendEditor.d.ts +0 -2
- package/lib/components/editors/CubeExtendEditor.d.ts.map +1 -1
- package/lib/components/editors/CubeExtendEditor.js +12 -11
- package/lib/components/editors/CubeExtendEditor.js.map +1 -1
- package/lib/components/editors/CubeJsonFieldsTree.d.ts +46 -0
- package/lib/components/editors/CubeJsonFieldsTree.d.ts.map +1 -0
- package/lib/components/editors/CubeJsonFieldsTree.js +93 -0
- package/lib/components/editors/CubeJsonFieldsTree.js.map +1 -0
- package/lib/components/editors/CubeNodeEditorRegistry.d.ts.map +1 -1
- package/lib/components/editors/CubeNodeEditorRegistry.js +3 -1
- package/lib/components/editors/CubeNodeEditorRegistry.js.map +1 -1
- package/lib/components/grid/CubeGridRegion.d.ts.map +1 -1
- package/lib/components/grid/CubeGridRegion.js +5 -4
- package/lib/components/grid/CubeGridRegion.js.map +1 -1
- package/lib/components/grid/CubeGridValues.d.ts +5 -3
- package/lib/components/grid/CubeGridValues.d.ts.map +1 -1
- package/lib/components/grid/CubeGridValues.js +8 -3
- package/lib/components/grid/CubeGridValues.js.map +1 -1
- package/lib/components/grid/CubeJsonExplorerDialog.d.ts +27 -0
- package/lib/components/grid/CubeJsonExplorerDialog.d.ts.map +1 -0
- package/lib/components/grid/CubeJsonExplorerDialog.js +102 -0
- package/lib/components/grid/CubeJsonExplorerDialog.js.map +1 -0
- package/lib/components/grid/CubeJsonViewerDialog.d.ts +27 -0
- package/lib/components/grid/CubeJsonViewerDialog.d.ts.map +1 -0
- package/lib/components/grid/CubeJsonViewerDialog.js +68 -0
- package/lib/components/grid/CubeJsonViewerDialog.js.map +1 -0
- package/lib/components/grid/CubeResultGrid.d.ts +15 -3
- package/lib/components/grid/CubeResultGrid.d.ts.map +1 -1
- package/lib/components/grid/CubeResultGrid.js +95 -3
- package/lib/components/grid/CubeResultGrid.js.map +1 -1
- package/lib/components/grid/CubeServerSideGrid.d.ts.map +1 -1
- package/lib/components/grid/CubeServerSideGrid.js +4 -2
- package/lib/components/grid/CubeServerSideGrid.js.map +1 -1
- package/lib/components/palette/CubePalette.d.ts.map +1 -1
- package/lib/components/palette/CubePalette.js +4 -1
- package/lib/components/palette/CubePalette.js.map +1 -1
- package/lib/components/source-picker/CubeIngestTab.d.ts +2 -2
- package/lib/components/source-picker/CubeIngestTab.d.ts.map +1 -1
- package/lib/components/source-picker/CubeIngestTab.js +19 -13
- package/lib/components/source-picker/CubeIngestTab.js.map +1 -1
- package/lib/components/tour/CubeTour.d.ts +36 -0
- package/lib/components/tour/CubeTour.d.ts.map +1 -0
- package/lib/components/tour/CubeTour.js +183 -0
- package/lib/components/tour/CubeTour.js.map +1 -0
- package/lib/index.css +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/stores/CubeEditorState.d.ts +17 -1
- package/lib/stores/CubeEditorState.d.ts.map +1 -1
- package/lib/stores/CubeEditorState.js +35 -5
- package/lib/stores/CubeEditorState.js.map +1 -1
- package/lib/stores/CubeExamples.d.ts.map +1 -1
- package/lib/stores/CubeExamples.js +101 -4
- package/lib/stores/CubeExamples.js.map +1 -1
- package/lib/stores/CubeExamplesState.d.ts.map +1 -1
- package/lib/stores/CubeExamplesState.js +4 -1
- package/lib/stores/CubeExamplesState.js.map +1 -1
- package/lib/stores/CubeGridQuickActions.d.ts +7 -0
- package/lib/stores/CubeGridQuickActions.d.ts.map +1 -1
- package/lib/stores/CubeGridQuickActions.js +20 -11
- package/lib/stores/CubeGridQuickActions.js.map +1 -1
- package/lib/stores/CubePureText.d.ts +20 -0
- package/lib/stores/CubePureText.d.ts.map +1 -0
- package/lib/stores/CubePureText.js +24 -0
- package/lib/stores/CubePureText.js.map +1 -0
- package/lib/stores/CubeTourState.d.ts +49 -0
- package/lib/stores/CubeTourState.d.ts.map +1 -0
- package/lib/stores/CubeTourState.js +92 -0
- package/lib/stores/CubeTourState.js.map +1 -0
- package/lib/stores/LegendCubeHost.d.ts +88 -0
- package/lib/stores/LegendCubeHost.d.ts.map +1 -0
- package/lib/stores/LegendCubeHost.js +149 -0
- package/lib/stores/LegendCubeHost.js.map +1 -0
- package/lib/stores/editors/CubeExplodeDraft.d.ts +87 -0
- package/lib/stores/editors/CubeExplodeDraft.d.ts.map +1 -0
- package/lib/stores/editors/CubeExplodeDraft.js +221 -0
- package/lib/stores/editors/CubeExplodeDraft.js.map +1 -0
- package/lib/stores/editors/CubeExpressionFilterDraft.d.ts +3 -0
- package/lib/stores/editors/CubeExpressionFilterDraft.d.ts.map +1 -1
- package/lib/stores/editors/CubeExpressionFilterDraft.js +4 -0
- package/lib/stores/editors/CubeExpressionFilterDraft.js.map +1 -1
- package/lib/stores/editors/CubeExtendDraft.d.ts +9 -0
- package/lib/stores/editors/CubeExtendDraft.d.ts.map +1 -1
- package/lib/stores/editors/CubeExtendDraft.js +26 -0
- package/lib/stores/editors/CubeExtendDraft.js.map +1 -1
- package/lib/stores/editors/CubeNodeDraftRegistry.d.ts.map +1 -1
- package/lib/stores/editors/CubeNodeDraftRegistry.js +6 -1
- package/lib/stores/editors/CubeNodeDraftRegistry.js.map +1 -1
- package/lib/stores/fixtures/CubeSalesDuckDbModel.d.ts +1 -0
- package/lib/stores/fixtures/CubeSalesDuckDbModel.d.ts.map +1 -1
- package/lib/stores/fixtures/CubeSalesDuckDbModel.js +8 -2
- package/lib/stores/fixtures/CubeSalesDuckDbModel.js.map +1 -1
- package/lib/stores/source-picker/CubeIngestTabState.d.ts +48 -14
- package/lib/stores/source-picker/CubeIngestTabState.d.ts.map +1 -1
- package/lib/stores/source-picker/CubeIngestTabState.js +159 -62
- package/lib/stores/source-picker/CubeIngestTabState.js.map +1 -1
- package/lib/stores/variant/CubeJson.d.ts +48 -0
- package/lib/stores/variant/CubeJson.d.ts.map +1 -0
- package/lib/stores/variant/CubeJson.js +207 -0
- package/lib/stores/variant/CubeJson.js.map +1 -0
- package/lib/stores/variant/CubeJsonExplorerState.d.ts +88 -0
- package/lib/stores/variant/CubeJsonExplorerState.d.ts.map +1 -0
- package/lib/stores/variant/CubeJsonExplorerState.js +297 -0
- package/lib/stores/variant/CubeJsonExplorerState.js.map +1 -0
- package/lib/stores/variant/CubeJsonFieldsState.d.ts +83 -0
- package/lib/stores/variant/CubeJsonFieldsState.d.ts.map +1 -0
- package/lib/stores/variant/CubeJsonFieldsState.js +207 -0
- package/lib/stores/variant/CubeJsonFieldsState.js.map +1 -0
- package/lib/stores/variant/CubeJsonOffers.d.ts +76 -0
- package/lib/stores/variant/CubeJsonOffers.d.ts.map +1 -0
- package/lib/stores/variant/CubeJsonOffers.js +250 -0
- package/lib/stores/variant/CubeJsonOffers.js.map +1 -0
- package/lib/stores/variant/CubeJsonParse.d.ts +28 -0
- package/lib/stores/variant/CubeJsonParse.d.ts.map +1 -0
- package/lib/stores/variant/CubeJsonParse.js +53 -0
- package/lib/stores/variant/CubeJsonParse.js.map +1 -0
- package/lib/stores/variant/CubeJsonSampling.d.ts +41 -0
- package/lib/stores/variant/CubeJsonSampling.d.ts.map +1 -0
- package/lib/stores/variant/CubeJsonSampling.js +59 -0
- package/lib/stores/variant/CubeJsonSampling.js.map +1 -0
- package/lib/stores/variant/CubeJsonShape.d.ts +87 -0
- package/lib/stores/variant/CubeJsonShape.d.ts.map +1 -0
- package/lib/stores/variant/CubeJsonShape.js +209 -0
- package/lib/stores/variant/CubeJsonShape.js.map +1 -0
- package/lib/stores/variant/CubeJsonViewerState.d.ts +33 -0
- package/lib/stores/variant/CubeJsonViewerState.d.ts.map +1 -0
- package/lib/stores/variant/CubeJsonViewerState.js +53 -0
- package/lib/stores/variant/CubeJsonViewerState.js.map +1 -0
- package/package.json +2 -2
- package/src/__lib__/LegendCubeHelpText.ts +4 -0
- package/src/__lib__/LegendCubeLabels.ts +60 -8
- package/src/__lib__/LegendCubeNavigation.ts +48 -0
- package/src/__lib__/LegendCubeTesting.ts +2 -0
- package/src/__lib__/LegendCubeTour.ts +173 -0
- package/src/__test-utils__/CubeExampleTestUtils.ts +119 -0
- package/src/__test-utils__/CubeVariantTestUtils.ts +103 -0
- package/src/components/CubeAddItems.tsx +2 -0
- package/src/components/CubeEditor.tsx +15 -0
- package/src/components/CubeFileMenu.tsx +2 -0
- package/src/components/CubeNodeIcon.tsx +2 -0
- package/src/components/canvas/CubeCanvas.tsx +17 -1
- package/src/components/canvas/CubeCanvasNode.tsx +7 -0
- package/src/components/dev-settings/CubeDevSettings.tsx +17 -2
- package/src/components/editors/CubeColumnPicker.tsx +3 -0
- package/src/components/editors/CubeExplodeEditor.tsx +207 -0
- package/src/components/editors/CubeExpressionFilterEditor.tsx +25 -8
- package/src/components/editors/CubeExtendEditor.tsx +32 -19
- package/src/components/editors/CubeJsonFieldsTree.tsx +316 -0
- package/src/components/editors/CubeNodeEditorRegistry.ts +3 -0
- package/src/components/grid/CubeGridRegion.tsx +21 -3
- package/src/components/grid/CubeGridValues.ts +8 -3
- package/src/components/grid/CubeJsonExplorerDialog.tsx +304 -0
- package/src/components/grid/CubeJsonViewerDialog.tsx +190 -0
- package/src/components/grid/CubeResultGrid.tsx +144 -2
- package/src/components/grid/CubeServerSideGrid.tsx +13 -0
- package/src/components/palette/CubePalette.tsx +9 -0
- package/src/components/source-picker/CubeIngestTab.tsx +62 -30
- package/src/components/tour/CubeTour.tsx +317 -0
- package/src/index.ts +7 -0
- package/src/stores/CubeEditorState.ts +36 -5
- package/src/stores/CubeExamples.ts +120 -2
- package/src/stores/CubeExamplesState.ts +9 -1
- package/src/stores/CubeGridQuickActions.ts +23 -12
- package/src/stores/CubePureText.ts +28 -0
- package/src/stores/CubeTourState.ts +116 -0
- package/src/stores/LegendCubeHost.ts +243 -0
- package/src/stores/editors/CubeExplodeDraft.ts +272 -0
- package/src/stores/editors/CubeExpressionFilterDraft.ts +4 -0
- package/src/stores/editors/CubeExtendDraft.ts +29 -0
- package/src/stores/editors/CubeNodeDraftRegistry.ts +7 -0
- package/src/stores/fixtures/CubeSalesDuckDbModel.ts +8 -2
- package/src/stores/source-picker/CubeIngestTabState.ts +253 -102
- package/src/stores/variant/CubeJson.ts +229 -0
- package/src/stores/variant/CubeJsonExplorerState.ts +378 -0
- package/src/stores/variant/CubeJsonFieldsState.ts +252 -0
- package/src/stores/variant/CubeJsonOffers.ts +359 -0
- package/src/stores/variant/CubeJsonParse.ts +75 -0
- package/src/stores/variant/CubeJsonSampling.ts +90 -0
- package/src/stores/variant/CubeJsonShape.ts +279 -0
- package/src/stores/variant/CubeJsonViewerState.ts +59 -0
- package/tsconfig.json +22 -1
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026-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
|
+
clsx,
|
|
19
|
+
Dialog,
|
|
20
|
+
Modal,
|
|
21
|
+
ModalBody,
|
|
22
|
+
ModalFooter,
|
|
23
|
+
ModalFooterButton,
|
|
24
|
+
ModalHeader,
|
|
25
|
+
} from '@finos/legend-art';
|
|
26
|
+
import { flowResult } from 'mobx';
|
|
27
|
+
import { observer } from 'mobx-react-lite';
|
|
28
|
+
import { CUBE_JSON_EXPLODE_OFFER_TITLE } from '../../__lib__/LegendCubeLabels.js';
|
|
29
|
+
import { LEGEND_CUBE_TEST_ID } from '../../__lib__/LegendCubeTesting.js';
|
|
30
|
+
import type { CubeEditorState } from '../../stores/CubeEditorState.js';
|
|
31
|
+
import type { CubeJsonExplorerState } from '../../stores/variant/CubeJsonExplorerState.js';
|
|
32
|
+
import {
|
|
33
|
+
type CubeJsonField,
|
|
34
|
+
filterCubeJsonFields,
|
|
35
|
+
} from '../../stores/variant/CubeJsonOffers.js';
|
|
36
|
+
import { CubeButton } from '../CubeButton.js';
|
|
37
|
+
import { CUBE_PRIMARY_BUTTON_DARK_MODE } from '../CubeDialogTheme.js';
|
|
38
|
+
|
|
39
|
+
const format = (count: number): string => count.toLocaleString('en-US');
|
|
40
|
+
|
|
41
|
+
/** A share, 0 to 1, as a whole percentage */
|
|
42
|
+
const percentOf = (share: number): number => Math.round(share * 100);
|
|
43
|
+
|
|
44
|
+
/** A field's name: its last key, or the column's for the column itself */
|
|
45
|
+
const nameOf = (
|
|
46
|
+
explorer: CubeJsonExplorerState,
|
|
47
|
+
field: CubeJsonField,
|
|
48
|
+
): string => field.path.at(-1) ?? explorer.column?.name ?? '';
|
|
49
|
+
|
|
50
|
+
/** What was read, on one line: the first values or every row, and why it stopped */
|
|
51
|
+
const CubeJsonExplorerStatus = observer(
|
|
52
|
+
(props: { explorer: CubeJsonExplorerState }) => {
|
|
53
|
+
const { explorer } = props;
|
|
54
|
+
const { sample, column } = explorer;
|
|
55
|
+
let text: string;
|
|
56
|
+
if (explorer.isReading) {
|
|
57
|
+
text = `Reading ${column?.name ?? ''}…`;
|
|
58
|
+
} else if (!sample) {
|
|
59
|
+
text = '';
|
|
60
|
+
} else {
|
|
61
|
+
const values = sample.rows - sample.unreadable;
|
|
62
|
+
const unreadable = sample.unreadable
|
|
63
|
+
? ` (${format(sample.unreadable)} not JSON)`
|
|
64
|
+
: '';
|
|
65
|
+
text = sample.complete
|
|
66
|
+
? `All ${format(sample.rows)} values read${unreadable}: the fields, their types and shares cover the whole column.`
|
|
67
|
+
: `The first ${format(sample.rows)} values read${unreadable}: the types and shares are from this sample.`;
|
|
68
|
+
if (values === 0) {
|
|
69
|
+
text = `No JSON in the ${format(sample.rows)} values read.`;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return (
|
|
73
|
+
<div className="mb-2 flex min-h-6 items-center gap-2 text-sm text-[var(--color-text-secondary)]">
|
|
74
|
+
<span role="status">{text}</span>
|
|
75
|
+
{explorer.isReading ? (
|
|
76
|
+
<CubeButton onClick={() => explorer.stop()}>Stop</CubeButton>
|
|
77
|
+
) : (
|
|
78
|
+
sample &&
|
|
79
|
+
!sample.complete && (
|
|
80
|
+
<CubeButton
|
|
81
|
+
title="Read every value of the column, for the shape of the whole column: listed in Runs, and it can be stopped"
|
|
82
|
+
onClick={() => {
|
|
83
|
+
// a failed read shows its message in the dialog
|
|
84
|
+
flowResult(explorer.read(true)).catch(() => undefined);
|
|
85
|
+
}}
|
|
86
|
+
>
|
|
87
|
+
Read every row
|
|
88
|
+
</CubeButton>
|
|
89
|
+
)
|
|
90
|
+
)}
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
},
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
/** A field of the tree, and its fields, indented */
|
|
97
|
+
const CubeJsonFieldRow = observer(
|
|
98
|
+
(props: {
|
|
99
|
+
explorer: CubeJsonExplorerState;
|
|
100
|
+
field: CubeJsonField;
|
|
101
|
+
depth: number;
|
|
102
|
+
}) => {
|
|
103
|
+
const { explorer, field, depth } = props;
|
|
104
|
+
const selected =
|
|
105
|
+
JSON.stringify(field.path) === JSON.stringify(explorer.selectedPath);
|
|
106
|
+
const share = percentOf(field.presence);
|
|
107
|
+
return (
|
|
108
|
+
<>
|
|
109
|
+
<button
|
|
110
|
+
type="button"
|
|
111
|
+
className={clsx(
|
|
112
|
+
'flex w-full items-baseline gap-2 py-0.5 pr-2 text-left hover:bg-[var(--color-bg-hover)]',
|
|
113
|
+
{ 'bg-[var(--color-bg-selected)]': selected },
|
|
114
|
+
)}
|
|
115
|
+
style={{ paddingLeft: `${8 + depth * 16}px` }}
|
|
116
|
+
aria-pressed={selected}
|
|
117
|
+
aria-label={`Field ${[explorer.column?.name ?? '', ...field.path].join('.')}`}
|
|
118
|
+
onClick={() => explorer.select(field.path)}
|
|
119
|
+
>
|
|
120
|
+
<span className="break-all font-mono text-sm text-[var(--color-text-primary)]">
|
|
121
|
+
{nameOf(explorer, field)}
|
|
122
|
+
</span>
|
|
123
|
+
<span className="text-xs text-[var(--color-text-secondary)]">
|
|
124
|
+
{field.description}
|
|
125
|
+
</span>
|
|
126
|
+
{share < 100 && (
|
|
127
|
+
<span
|
|
128
|
+
className="ml-auto text-xs text-[var(--color-text-muted)]"
|
|
129
|
+
title={
|
|
130
|
+
field.path.length
|
|
131
|
+
? `In ${share}% of the objects it could be in`
|
|
132
|
+
: `In ${share}% of the rows read`
|
|
133
|
+
}
|
|
134
|
+
>
|
|
135
|
+
{share}%
|
|
136
|
+
</span>
|
|
137
|
+
)}
|
|
138
|
+
</button>
|
|
139
|
+
{field.children.map((child) => (
|
|
140
|
+
<CubeJsonFieldRow
|
|
141
|
+
key={JSON.stringify(child.path)}
|
|
142
|
+
explorer={explorer}
|
|
143
|
+
field={child}
|
|
144
|
+
depth={depth + 1}
|
|
145
|
+
/>
|
|
146
|
+
))}
|
|
147
|
+
</>
|
|
148
|
+
);
|
|
149
|
+
},
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
/** The selected field: where it is, what it held, and the columns it can become, to tick */
|
|
153
|
+
const CubeJsonFieldOffers = observer(
|
|
154
|
+
(props: { explorer: CubeJsonExplorerState }) => {
|
|
155
|
+
const { explorer } = props;
|
|
156
|
+
const field = explorer.selectedField;
|
|
157
|
+
if (!field) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
const { offers } = field;
|
|
161
|
+
const share = percentOf(field.presence);
|
|
162
|
+
return (
|
|
163
|
+
<div className="flex flex-col gap-2 p-2">
|
|
164
|
+
<div className="break-all font-mono text-sm text-[var(--color-text-primary)]">
|
|
165
|
+
{[explorer.column?.name ?? '', ...field.path].join(' › ')}
|
|
166
|
+
</div>
|
|
167
|
+
<div className="text-sm text-[var(--color-text-secondary)]">
|
|
168
|
+
{field.description}
|
|
169
|
+
{share < 100
|
|
170
|
+
? ` · in ${share}% of ${field.path.length ? 'the objects it could be in' : 'the rows read'}`
|
|
171
|
+
: ''}
|
|
172
|
+
</div>
|
|
173
|
+
{field.topValues.length > 0 && (
|
|
174
|
+
<div className="break-all text-xs text-[var(--color-text-secondary)]">
|
|
175
|
+
Values seen: {field.topValues.join(', ')}
|
|
176
|
+
</div>
|
|
177
|
+
)}
|
|
178
|
+
{offers.length ? (
|
|
179
|
+
<div role="group" aria-label="Columns it can become">
|
|
180
|
+
{offers.map((offer) => (
|
|
181
|
+
<label
|
|
182
|
+
key={offer.key}
|
|
183
|
+
className="flex cursor-pointer items-baseline gap-2 py-0.5 text-sm"
|
|
184
|
+
title={offer.code ?? CUBE_JSON_EXPLODE_OFFER_TITLE}
|
|
185
|
+
>
|
|
186
|
+
<input
|
|
187
|
+
type="checkbox"
|
|
188
|
+
checked={explorer.isTicked(field.path, offer)}
|
|
189
|
+
onChange={() => explorer.toggle(field.path, offer)}
|
|
190
|
+
/>
|
|
191
|
+
<span className="text-[var(--color-text-primary)]">
|
|
192
|
+
{offer.label}
|
|
193
|
+
</span>
|
|
194
|
+
<span className="text-xs text-[var(--color-text-muted)]">
|
|
195
|
+
{offer.explode ? 'Explode Array' : offer.type} · {offer.name}
|
|
196
|
+
</span>
|
|
197
|
+
</label>
|
|
198
|
+
))}
|
|
199
|
+
</div>
|
|
200
|
+
) : (
|
|
201
|
+
<div className="text-sm text-[var(--color-text-muted)]">
|
|
202
|
+
{field.children.length
|
|
203
|
+
? 'Pick one of its fields.'
|
|
204
|
+
: 'Nothing to make of it.'}
|
|
205
|
+
</div>
|
|
206
|
+
)}
|
|
207
|
+
</div>
|
|
208
|
+
);
|
|
209
|
+
},
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Explore JSON (PLAN §11.20): a Variant column's fields, read from its first
|
|
214
|
+
* values or every row, each with what it held and how often; the selected
|
|
215
|
+
* field's offers, ticked and added as one Extend after the node that ran
|
|
216
|
+
*/
|
|
217
|
+
export const CubeJsonExplorerDialog = observer(
|
|
218
|
+
(props: { editorState: CubeEditorState }) => {
|
|
219
|
+
const { editorState } = props;
|
|
220
|
+
const explorer = editorState.jsonExplorer;
|
|
221
|
+
const { applicationStore } = editorState.host;
|
|
222
|
+
const darkMode =
|
|
223
|
+
!applicationStore.layoutService.TEMPORARY__isLightColorThemeEnabled;
|
|
224
|
+
const fields = explorer.fields
|
|
225
|
+
? filterCubeJsonFields(explorer.fields, explorer.search)
|
|
226
|
+
: undefined;
|
|
227
|
+
const count = explorer.tickedOffers.length;
|
|
228
|
+
const add = (): void => {
|
|
229
|
+
flowResult(explorer.add()).catch(applicationStore.alertUnhandledError);
|
|
230
|
+
};
|
|
231
|
+
return (
|
|
232
|
+
<Dialog open={explorer.isOpen} onClose={() => explorer.close()}>
|
|
233
|
+
<Modal darkMode={darkMode} className="w-[960px] max-w-full">
|
|
234
|
+
<ModalHeader>
|
|
235
|
+
<div className="modal__title">{`Explore JSON: ${explorer.column?.name ?? ''}`}</div>
|
|
236
|
+
</ModalHeader>
|
|
237
|
+
<ModalBody>
|
|
238
|
+
<div data-testid={LEGEND_CUBE_TEST_ID.JSON_EXPLORER}>
|
|
239
|
+
<CubeJsonExplorerStatus explorer={explorer} />
|
|
240
|
+
{explorer.error && (
|
|
241
|
+
<div
|
|
242
|
+
role="alert"
|
|
243
|
+
className="mb-2 break-words text-sm text-[var(--color-status-error)]"
|
|
244
|
+
>
|
|
245
|
+
{explorer.error}
|
|
246
|
+
</div>
|
|
247
|
+
)}
|
|
248
|
+
{explorer.fields && (
|
|
249
|
+
<div className="grid h-[50vh] grid-cols-2 gap-2">
|
|
250
|
+
<div className="flex min-h-0 flex-col rounded-sm border border-[var(--color-border-default)] bg-[var(--color-bg-panel)]">
|
|
251
|
+
<input
|
|
252
|
+
className="m-1 rounded-sm border border-[var(--color-border-default)] px-1 text-sm"
|
|
253
|
+
placeholder="Find a field"
|
|
254
|
+
aria-label="Find a field"
|
|
255
|
+
value={explorer.search}
|
|
256
|
+
onChange={(event) =>
|
|
257
|
+
explorer.setSearch(event.target.value)
|
|
258
|
+
}
|
|
259
|
+
/>
|
|
260
|
+
<div
|
|
261
|
+
className="min-h-0 flex-1 overflow-auto"
|
|
262
|
+
aria-label="Fields"
|
|
263
|
+
>
|
|
264
|
+
{fields ? (
|
|
265
|
+
<CubeJsonFieldRow
|
|
266
|
+
explorer={explorer}
|
|
267
|
+
field={fields}
|
|
268
|
+
depth={0}
|
|
269
|
+
/>
|
|
270
|
+
) : (
|
|
271
|
+
<div className="p-2 text-sm text-[var(--color-text-muted)]">
|
|
272
|
+
No field matches.
|
|
273
|
+
</div>
|
|
274
|
+
)}
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
<div className="min-h-0 overflow-auto rounded-sm border border-[var(--color-border-default)] bg-[var(--color-bg-panel)]">
|
|
278
|
+
<CubeJsonFieldOffers explorer={explorer} />
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
)}
|
|
282
|
+
</div>
|
|
283
|
+
</ModalBody>
|
|
284
|
+
<ModalFooter>
|
|
285
|
+
<ModalFooterButton
|
|
286
|
+
darkMode={CUBE_PRIMARY_BUTTON_DARK_MODE}
|
|
287
|
+
formatText={false}
|
|
288
|
+
text={count === 1 ? 'Add 1 column' : `Add ${count} columns`}
|
|
289
|
+
disabled={!count || explorer.isAdding || explorer.isReading}
|
|
290
|
+
onClick={add}
|
|
291
|
+
/>
|
|
292
|
+
<ModalFooterButton
|
|
293
|
+
darkMode={darkMode}
|
|
294
|
+
formatText={false}
|
|
295
|
+
type="secondary"
|
|
296
|
+
text="Cancel"
|
|
297
|
+
onClick={() => explorer.close()}
|
|
298
|
+
/>
|
|
299
|
+
</ModalFooter>
|
|
300
|
+
</Modal>
|
|
301
|
+
</Dialog>
|
|
302
|
+
);
|
|
303
|
+
},
|
|
304
|
+
);
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026-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
|
+
CaretDownIcon,
|
|
19
|
+
CaretRightIcon,
|
|
20
|
+
Dialog,
|
|
21
|
+
Modal,
|
|
22
|
+
ModalBody,
|
|
23
|
+
ModalFooter,
|
|
24
|
+
ModalFooterButton,
|
|
25
|
+
ModalHeader,
|
|
26
|
+
} from '@finos/legend-art';
|
|
27
|
+
import { observer } from 'mobx-react-lite';
|
|
28
|
+
import { useState } from 'react';
|
|
29
|
+
import { LEGEND_CUBE_TEST_ID } from '../../__lib__/LegendCubeTesting.js';
|
|
30
|
+
import type { CubeEditorState } from '../../stores/CubeEditorState.js';
|
|
31
|
+
import {
|
|
32
|
+
type CubeJsonNode,
|
|
33
|
+
formatCubeJsonPretty,
|
|
34
|
+
} from '../../stores/variant/CubeJson.js';
|
|
35
|
+
import { CUBE_PRIMARY_BUTTON_DARK_MODE } from '../CubeDialogTheme.js';
|
|
36
|
+
|
|
37
|
+
/** Levels open when the dialog opens: the document and its first fields */
|
|
38
|
+
const OPEN_DEPTH = 2;
|
|
39
|
+
|
|
40
|
+
/** What a closed object or array holds, e.g. `{2 keys}`, `[3 items]` */
|
|
41
|
+
const summarize = (node: CubeJsonNode): string =>
|
|
42
|
+
node.kind === 'object'
|
|
43
|
+
? `{${node.entries.length} ${node.entries.length === 1 ? 'key' : 'keys'}}`
|
|
44
|
+
: node.kind === 'array'
|
|
45
|
+
? `[${node.items.length} ${node.items.length === 1 ? 'item' : 'items'}]`
|
|
46
|
+
: '';
|
|
47
|
+
|
|
48
|
+
/** A scalar, coloured by its kind: strings quoted, numbers as written */
|
|
49
|
+
const CubeJsonScalar: React.FC<{ node: CubeJsonNode }> = ({ node }) =>
|
|
50
|
+
node.kind === 'string' ? (
|
|
51
|
+
<span className="break-all text-[var(--color-category-enumeration)]">
|
|
52
|
+
{JSON.stringify(node.value)}
|
|
53
|
+
</span>
|
|
54
|
+
) : node.kind === 'number' ? (
|
|
55
|
+
<span className="text-[var(--color-category-primitive)]">{node.text}</span>
|
|
56
|
+
) : node.kind === 'boolean' ? (
|
|
57
|
+
<span className="text-[var(--color-category-class)]">
|
|
58
|
+
{String(node.value)}
|
|
59
|
+
</span>
|
|
60
|
+
) : (
|
|
61
|
+
<span className="text-[var(--color-text-muted)]">null</span>
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
/** One entry of the tree: its key or index, and its value, open or closed */
|
|
65
|
+
const CubeJsonTreeNode: React.FC<{
|
|
66
|
+
label: string | undefined;
|
|
67
|
+
node: CubeJsonNode;
|
|
68
|
+
depth: number;
|
|
69
|
+
}> = ({ label, node, depth }) => {
|
|
70
|
+
const [open, setOpen] = useState(depth < OPEN_DEPTH);
|
|
71
|
+
const children =
|
|
72
|
+
node.kind === 'object'
|
|
73
|
+
? node.entries.map(([key, value]) => [key, value] as const)
|
|
74
|
+
: node.kind === 'array'
|
|
75
|
+
? node.items.map((item, index) => [`[${index}]`, item] as const)
|
|
76
|
+
: undefined;
|
|
77
|
+
const name =
|
|
78
|
+
label === undefined ? undefined : (
|
|
79
|
+
<span className="font-medium text-[var(--color-text-primary)]">
|
|
80
|
+
{label}:{' '}
|
|
81
|
+
</span>
|
|
82
|
+
);
|
|
83
|
+
if (!children) {
|
|
84
|
+
return (
|
|
85
|
+
<div className="py-px pl-5 font-mono text-sm">
|
|
86
|
+
{name}
|
|
87
|
+
<CubeJsonScalar node={node} />
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
return (
|
|
92
|
+
<div className="font-mono text-sm">
|
|
93
|
+
<button
|
|
94
|
+
type="button"
|
|
95
|
+
className="flex items-center gap-1 py-px text-left hover:bg-[var(--color-bg-hover)]"
|
|
96
|
+
aria-expanded={open}
|
|
97
|
+
aria-label={`${open ? 'Close' : 'Open'} ${label ?? 'the document'}`}
|
|
98
|
+
onClick={() => setOpen(!open)}
|
|
99
|
+
>
|
|
100
|
+
{open ? (
|
|
101
|
+
<CaretDownIcon aria-hidden={true} />
|
|
102
|
+
) : (
|
|
103
|
+
<CaretRightIcon aria-hidden={true} />
|
|
104
|
+
)}
|
|
105
|
+
{name}
|
|
106
|
+
<span className="text-[var(--color-text-secondary)]">
|
|
107
|
+
{summarize(node)}
|
|
108
|
+
</span>
|
|
109
|
+
</button>
|
|
110
|
+
{open && (
|
|
111
|
+
<div className="ml-2 border-l border-[var(--color-border-subtle)] pl-2">
|
|
112
|
+
{children.map(([key, value]) => (
|
|
113
|
+
<CubeJsonTreeNode
|
|
114
|
+
key={key}
|
|
115
|
+
label={key}
|
|
116
|
+
node={value}
|
|
117
|
+
depth={depth + 1}
|
|
118
|
+
/>
|
|
119
|
+
))}
|
|
120
|
+
</div>
|
|
121
|
+
)}
|
|
122
|
+
</div>
|
|
123
|
+
);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Open JSON (PLAN §11.20): a Variant cell's document as a tree, each object
|
|
128
|
+
* and array open to `OPEN_DEPTH` and opened or closed by a click, with Copy
|
|
129
|
+
* JSON (indented, numbers as written); text that isn't JSON is shown as it is
|
|
130
|
+
*/
|
|
131
|
+
export const CubeJsonViewerDialog = observer(
|
|
132
|
+
(props: { editorState: CubeEditorState }) => {
|
|
133
|
+
const { editorState } = props;
|
|
134
|
+
const viewer = editorState.jsonViewer;
|
|
135
|
+
const { applicationStore } = editorState.host;
|
|
136
|
+
const darkMode =
|
|
137
|
+
!applicationStore.layoutService.TEMPORARY__isLightColorThemeEnabled;
|
|
138
|
+
const document = viewer.document;
|
|
139
|
+
const copy = (): void => {
|
|
140
|
+
const text = document
|
|
141
|
+
? formatCubeJsonPretty(document)
|
|
142
|
+
: (viewer.text ?? '');
|
|
143
|
+
applicationStore.clipboardService
|
|
144
|
+
.copyTextToClipboard(text)
|
|
145
|
+
.catch(applicationStore.alertUnhandledError);
|
|
146
|
+
};
|
|
147
|
+
return (
|
|
148
|
+
<Dialog open={viewer.isOpen} onClose={() => viewer.close()}>
|
|
149
|
+
<Modal darkMode={darkMode} className="w-[720px] max-w-full">
|
|
150
|
+
<ModalHeader>
|
|
151
|
+
<div className="modal__title">{`JSON of ${viewer.column ?? ''}`}</div>
|
|
152
|
+
</ModalHeader>
|
|
153
|
+
<ModalBody>
|
|
154
|
+
<div
|
|
155
|
+
className="max-h-[60vh] overflow-auto rounded-sm border border-[var(--color-border-default)] bg-[var(--color-bg-panel)] p-2"
|
|
156
|
+
aria-label="JSON document"
|
|
157
|
+
data-testid={LEGEND_CUBE_TEST_ID.JSON_VIEWER}
|
|
158
|
+
>
|
|
159
|
+
{document ? (
|
|
160
|
+
<CubeJsonTreeNode
|
|
161
|
+
key={viewer.text}
|
|
162
|
+
label={undefined}
|
|
163
|
+
node={document}
|
|
164
|
+
depth={0}
|
|
165
|
+
/>
|
|
166
|
+
) : (
|
|
167
|
+
<div className="break-all font-mono text-sm">{viewer.text}</div>
|
|
168
|
+
)}
|
|
169
|
+
</div>
|
|
170
|
+
</ModalBody>
|
|
171
|
+
<ModalFooter>
|
|
172
|
+
<ModalFooterButton
|
|
173
|
+
darkMode={CUBE_PRIMARY_BUTTON_DARK_MODE}
|
|
174
|
+
formatText={false}
|
|
175
|
+
text="Copy JSON"
|
|
176
|
+
onClick={copy}
|
|
177
|
+
/>
|
|
178
|
+
<ModalFooterButton
|
|
179
|
+
darkMode={darkMode}
|
|
180
|
+
formatText={false}
|
|
181
|
+
type="secondary"
|
|
182
|
+
text="Close"
|
|
183
|
+
onClick={() => viewer.close()}
|
|
184
|
+
/>
|
|
185
|
+
</ModalFooter>
|
|
186
|
+
</Modal>
|
|
187
|
+
</Dialog>
|
|
188
|
+
);
|
|
189
|
+
},
|
|
190
|
+
);
|
|
@@ -19,11 +19,12 @@ import {
|
|
|
19
19
|
AggregationFunction,
|
|
20
20
|
type CheckedDrilldown,
|
|
21
21
|
type ColumnFormat,
|
|
22
|
+
type CubeType,
|
|
22
23
|
DEFAULT_PRESENTATION,
|
|
23
24
|
foldColumnName,
|
|
24
25
|
getColumnFormat,
|
|
25
26
|
type Presentation,
|
|
26
|
-
|
|
27
|
+
TypeFamily,
|
|
27
28
|
} from '@finos/legend-cube';
|
|
28
29
|
import {
|
|
29
30
|
DataGrid,
|
|
@@ -34,17 +35,30 @@ import {
|
|
|
34
35
|
type DataGridGetContextMenuItemsParams,
|
|
35
36
|
type DataGridIAggFuncParams,
|
|
36
37
|
type DataGridMenuItemDef,
|
|
38
|
+
type DataGridOptions,
|
|
37
39
|
} from '@finos/legend-lego/data-grid';
|
|
38
40
|
import { flowResult } from 'mobx';
|
|
39
41
|
import { memo, useCallback, useMemo } from 'react';
|
|
40
42
|
import {
|
|
41
43
|
CUBE_FORMAT_NOT_A_NUMBER,
|
|
44
|
+
CUBE_JSON_DISABLED_REASON,
|
|
42
45
|
NULL_CELL_TEXT,
|
|
43
46
|
READ_ONLY_CUBE_TITLE,
|
|
44
47
|
} from '../../__lib__/LegendCubeLabels.js';
|
|
45
48
|
import { LEGEND_CUBE_TEST_ID } from '../../__lib__/LegendCubeTesting.js';
|
|
46
|
-
import
|
|
49
|
+
import {
|
|
50
|
+
CUBE_TOUR_ANCHOR,
|
|
51
|
+
cubeTourAnchor,
|
|
52
|
+
} from '../../__lib__/LegendCubeTour.js';
|
|
53
|
+
import {
|
|
54
|
+
CubeEngineError,
|
|
55
|
+
type CubeResultValue,
|
|
56
|
+
} from '../../graph-manager/CubeEngine.js';
|
|
47
57
|
import type { CubeEditorState } from '../../stores/CubeEditorState.js';
|
|
58
|
+
import {
|
|
59
|
+
addCubeJsonParseColumn,
|
|
60
|
+
isCubeJsonDocumentText,
|
|
61
|
+
} from '../../stores/variant/CubeJsonParse.js';
|
|
48
62
|
import type { CubeExecutionResult } from '../../stores/CubeExecutionState.js';
|
|
49
63
|
import { getCubeExportFileName } from '../../stores/CubeExportState.js';
|
|
50
64
|
import {
|
|
@@ -170,6 +184,126 @@ const CUBE_GRID_MENU_ITEMS: readonly DataGridDefaultMenuItem[] = [
|
|
|
170
184
|
'copyWithHeaders',
|
|
171
185
|
];
|
|
172
186
|
|
|
187
|
+
/**
|
|
188
|
+
* Explore JSON… on a Variant column (PLAN §11.20), in its cells' menu and its
|
|
189
|
+
* header's: the column's fields, and the columns they can become
|
|
190
|
+
*/
|
|
191
|
+
export const getCubeExploreJsonItem = <Row extends CubeAnyRow>(
|
|
192
|
+
editorState: CubeEditorState,
|
|
193
|
+
columnName: string,
|
|
194
|
+
): DataGridMenuItemDef<Row> => {
|
|
195
|
+
const column = editorState.execution.result?.schema.columns.find(
|
|
196
|
+
({ name }) => name === columnName,
|
|
197
|
+
);
|
|
198
|
+
const reason = editorState.jsonExplorer.getDisabledReason();
|
|
199
|
+
return {
|
|
200
|
+
name: 'Explore JSON…',
|
|
201
|
+
disabled: reason !== undefined || !column,
|
|
202
|
+
...(reason === undefined ? {} : { tooltip: reason }),
|
|
203
|
+
action: () => {
|
|
204
|
+
if (column && reason === undefined) {
|
|
205
|
+
flowResult(editorState.jsonExplorer.open(column)).catch(
|
|
206
|
+
editorState.host.applicationStore.alertUnhandledError,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* A JSON cell's items (PLAN §11.20): on a Variant cell, Open JSON shows a run
|
|
215
|
+
* row's document as a tree (a group row's cell, or an empty one, has none to
|
|
216
|
+
* show), and Explore JSON…; on a text cell holding a JSON object or array,
|
|
217
|
+
* Parse as JSON
|
|
218
|
+
*/
|
|
219
|
+
const getCubeVariantMenuItems = <Row extends CubeAnyRow>(
|
|
220
|
+
editorState: CubeEditorState,
|
|
221
|
+
column: { readonly name: string; readonly type: CubeType },
|
|
222
|
+
cell: CubeResultValue,
|
|
223
|
+
kind: CubeGridCellKind,
|
|
224
|
+
): DataGridMenuItemDef<Row>[] => {
|
|
225
|
+
// a text cell holding a JSON object or array: Parse as JSON
|
|
226
|
+
if (
|
|
227
|
+
column.type.family === TypeFamily.STRING &&
|
|
228
|
+
kind === CubeGridCellKind.VALUE &&
|
|
229
|
+
typeof cell === 'string' &&
|
|
230
|
+
isCubeJsonDocumentText(cell)
|
|
231
|
+
) {
|
|
232
|
+
const reason = editorState.jsonExplorer.getDisabledReason();
|
|
233
|
+
const { applicationStore } = editorState.host;
|
|
234
|
+
return [
|
|
235
|
+
{
|
|
236
|
+
name: 'Parse as JSON',
|
|
237
|
+
disabled: reason !== undefined,
|
|
238
|
+
...(reason === undefined ? {} : { tooltip: reason }),
|
|
239
|
+
action: () => {
|
|
240
|
+
if (reason === undefined) {
|
|
241
|
+
addCubeJsonParseColumn(editorState, column.name).catch(
|
|
242
|
+
(error: unknown) =>
|
|
243
|
+
applicationStore.notificationService.notifyError(
|
|
244
|
+
error instanceof CubeEngineError
|
|
245
|
+
? error.firstLine
|
|
246
|
+
: String(error),
|
|
247
|
+
),
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
];
|
|
253
|
+
}
|
|
254
|
+
if (column.type.family !== TypeFamily.VARIANT) {
|
|
255
|
+
return [];
|
|
256
|
+
}
|
|
257
|
+
const reason =
|
|
258
|
+
kind !== CubeGridCellKind.VALUE
|
|
259
|
+
? CUBE_JSON_DISABLED_REASON.NOT_A_ROW
|
|
260
|
+
: typeof cell !== 'string'
|
|
261
|
+
? CUBE_JSON_DISABLED_REASON.EMPTY
|
|
262
|
+
: undefined;
|
|
263
|
+
return [
|
|
264
|
+
{
|
|
265
|
+
name: 'Open JSON',
|
|
266
|
+
disabled: reason !== undefined,
|
|
267
|
+
...(reason === undefined ? {} : { tooltip: reason }),
|
|
268
|
+
action: () => {
|
|
269
|
+
if (typeof cell === 'string') {
|
|
270
|
+
editorState.jsonViewer.open(column.name, cell);
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
getCubeExploreJsonItem<Row>(editorState, column.name),
|
|
275
|
+
];
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
/** What ag-grid gives a column header's menu */
|
|
279
|
+
export type CubeGridHeaderMenuParams<Row extends CubeAnyRow> = Parameters<
|
|
280
|
+
NonNullable<DataGridOptions<Row>['getMainMenuItems']>
|
|
281
|
+
>[0];
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* A column header's menu: ag-grid's own items, after Explore JSON… on a
|
|
285
|
+
* Variant column (PLAN §11.20)
|
|
286
|
+
*/
|
|
287
|
+
export const getCubeGridHeaderMenuItems = <Row extends CubeAnyRow>(
|
|
288
|
+
editorState: CubeEditorState,
|
|
289
|
+
params: CubeGridHeaderMenuParams<Row>,
|
|
290
|
+
): (DataGridDefaultMenuItem | DataGridMenuItemDef<Row>)[] => {
|
|
291
|
+
const defaults = params.defaultItems;
|
|
292
|
+
const colId = params.column?.getColId();
|
|
293
|
+
const column = colId?.startsWith(COLUMN_ID_PREFIX)
|
|
294
|
+
? editorState.execution.result?.schema.columns[
|
|
295
|
+
Number(colId.slice(COLUMN_ID_PREFIX.length))
|
|
296
|
+
]
|
|
297
|
+
: undefined;
|
|
298
|
+
return column?.type.family === TypeFamily.VARIANT
|
|
299
|
+
? [
|
|
300
|
+
getCubeExploreJsonItem<Row>(editorState, column.name),
|
|
301
|
+
'separator',
|
|
302
|
+
...defaults,
|
|
303
|
+
]
|
|
304
|
+
: defaults;
|
|
305
|
+
};
|
|
306
|
+
|
|
173
307
|
/**
|
|
174
308
|
* The grid's context menu (spec §12.4, PLAN §11.10), curated: on a cell,
|
|
175
309
|
* 'Drilldown by' its column and the quick actions, then a separator; then,
|
|
@@ -224,6 +358,7 @@ export const getCubeGridContextMenuItems = <Row extends CubeAnyRow>(
|
|
|
224
358
|
action: () => quickAction.apply(),
|
|
225
359
|
};
|
|
226
360
|
}),
|
|
361
|
+
...getCubeVariantMenuItems<Row>(editorState, column, cell, kind),
|
|
227
362
|
'separator',
|
|
228
363
|
{
|
|
229
364
|
name: 'Format Column…',
|
|
@@ -497,6 +632,11 @@ export const CubeResultGrid = memo(
|
|
|
497
632
|
getCubeGridContextMenuItems(editorState, params),
|
|
498
633
|
[editorState],
|
|
499
634
|
);
|
|
635
|
+
const getMainMenuItems = useCallback(
|
|
636
|
+
(params: CubeGridHeaderMenuParams<CubeRow>) =>
|
|
637
|
+
getCubeGridHeaderMenuItems(editorState, params),
|
|
638
|
+
[editorState],
|
|
639
|
+
);
|
|
500
640
|
const columnDefs = useMemo(
|
|
501
641
|
() =>
|
|
502
642
|
buildCubeColumnDefs<CubeRow>(
|
|
@@ -529,6 +669,7 @@ export const CubeResultGrid = memo(
|
|
|
529
669
|
'ag-theme-balham-dark': darkMode,
|
|
530
670
|
})}
|
|
531
671
|
data-testid={LEGEND_CUBE_TEST_ID.RESULT_GRID}
|
|
672
|
+
{...cubeTourAnchor(CUBE_TOUR_ANCHOR.RESULTS)}
|
|
532
673
|
>
|
|
533
674
|
<DataGrid<CubeRow>
|
|
534
675
|
rowData={result.rows as CubeRow[]}
|
|
@@ -536,6 +677,7 @@ export const CubeResultGrid = memo(
|
|
|
536
677
|
suppressFieldDotNotation={true}
|
|
537
678
|
tooltipShowDelay={500}
|
|
538
679
|
getContextMenuItems={getContextMenuItems}
|
|
680
|
+
getMainMenuItems={getMainMenuItems}
|
|
539
681
|
// local mode (spec §12.1): ag-grid's own grouping, pivot and
|
|
540
682
|
// filters on the run's rows, with their panels and totals
|
|
541
683
|
defaultColDef={LOCAL_COLUMN_DEFAULTS}
|