datajunction-ui 0.0.1-a1 → 0.0.1-a101
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/Makefile +7 -1
- package/package.json +18 -7
- package/public/index.html +1 -1
- package/src/app/components/AddNodeDropdown.jsx +44 -0
- package/src/app/components/ListGroupItem.jsx +2 -1
- package/src/app/components/NodeListActions.jsx +69 -0
- package/src/app/components/NodeMaterializationDelete.jsx +80 -0
- package/src/app/components/QueryInfo.jsx +96 -1
- package/src/app/components/Search.jsx +94 -0
- package/src/app/components/__tests__/NodeListActions.test.jsx +94 -0
- package/src/app/components/__tests__/Search.test.jsx +63 -0
- package/src/app/components/__tests__/__snapshots__/ListGroupItem.test.tsx.snap +5 -3
- package/src/app/components/djgraph/Collapse.jsx +3 -2
- package/src/app/components/djgraph/DJNode.jsx +1 -1
- package/src/app/components/djgraph/DJNodeColumns.jsx +5 -1
- package/src/app/components/djgraph/LayoutFlow.jsx +5 -3
- package/src/app/components/forms/Action.jsx +8 -0
- package/src/app/components/forms/NodeNameField.jsx +64 -0
- package/src/app/components/search.css +17 -0
- package/src/app/icons/AddItemIcon.jsx +16 -0
- package/src/app/icons/CommitIcon.jsx +45 -0
- package/src/app/icons/DiffIcon.jsx +63 -0
- package/src/app/icons/EyeIcon.jsx +20 -0
- package/src/app/icons/FilterIcon.jsx +7 -0
- package/src/app/icons/JupyterExportIcon.jsx +25 -0
- package/src/app/icons/LoadingIcon.jsx +10 -10
- package/src/app/icons/PythonIcon.jsx +6 -44
- package/src/app/index.tsx +24 -0
- package/src/app/pages/AddEditNodePage/AlertMessage.jsx +10 -0
- package/src/app/pages/AddEditNodePage/ColumnsSelect.jsx +84 -0
- package/src/app/pages/AddEditNodePage/DescriptionField.jsx +17 -0
- package/src/app/pages/AddEditNodePage/DisplayNameField.jsx +16 -0
- package/src/app/pages/AddEditNodePage/FormikSelect.jsx +5 -0
- package/src/app/pages/AddEditNodePage/FullNameField.jsx +3 -2
- package/src/app/pages/AddEditNodePage/Loadable.jsx +6 -2
- package/src/app/pages/AddEditNodePage/MetricMetadataFields.jsx +75 -0
- package/src/app/pages/AddEditNodePage/MetricQueryField.jsx +71 -0
- package/src/app/pages/AddEditNodePage/NamespaceField.jsx +40 -0
- package/src/app/pages/AddEditNodePage/NodeModeField.jsx +14 -0
- package/src/app/pages/AddEditNodePage/NodeQueryField.jsx +8 -3
- package/src/app/pages/AddEditNodePage/RequiredDimensionsSelect.jsx +54 -0
- package/src/app/pages/AddEditNodePage/TagsField.jsx +47 -0
- package/src/app/pages/AddEditNodePage/UpstreamNodeField.jsx +49 -0
- package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormFailed.test.jsx +15 -9
- package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormSuccess.test.jsx +167 -24
- package/src/app/pages/AddEditNodePage/__tests__/index.test.jsx +55 -25
- package/src/app/pages/AddEditNodePage/index.jsx +275 -194
- package/src/app/pages/CubeBuilderPage/DimensionsSelect.jsx +154 -0
- package/src/app/pages/CubeBuilderPage/Loadable.jsx +16 -0
- package/src/app/pages/CubeBuilderPage/MetricsSelect.jsx +77 -0
- package/src/app/pages/CubeBuilderPage/__tests__/index.test.jsx +405 -0
- package/src/app/pages/CubeBuilderPage/index.jsx +267 -0
- package/src/app/pages/NamespacePage/AddNamespacePopover.jsx +5 -5
- package/src/app/pages/NamespacePage/Explorer.jsx +6 -2
- package/src/app/pages/NamespacePage/FieldControl.jsx +21 -0
- package/src/app/pages/NamespacePage/NodeTypeSelect.jsx +30 -0
- package/src/app/pages/NamespacePage/TagSelect.jsx +44 -0
- package/src/app/pages/NamespacePage/UserSelect.jsx +47 -0
- package/src/app/pages/NamespacePage/__tests__/index.test.jsx +98 -19
- package/src/app/pages/NamespacePage/index.jsx +272 -89
- package/src/app/pages/NodePage/AddBackfillPopover.jsx +60 -61
- package/src/app/pages/NodePage/AddMaterializationPopover.jsx +104 -51
- package/src/app/pages/NodePage/ClientCodePopover.jsx +73 -25
- package/src/app/pages/NodePage/DimensionFilter.jsx +86 -0
- package/src/app/pages/NodePage/EditColumnDescriptionPopover.jsx +116 -0
- package/src/app/pages/NodePage/LinkDimensionPopover.jsx +38 -23
- package/src/app/pages/NodePage/MaterializationConfigField.jsx +60 -0
- package/src/app/pages/NodePage/NodeColumnTab.jsx +183 -113
- package/src/app/pages/NodePage/NodeDependenciesTab.jsx +153 -0
- package/src/app/pages/NodePage/NodeGraphTab.jsx +56 -29
- package/src/app/pages/NodePage/NodeHistory.jsx +165 -161
- package/src/app/pages/NodePage/NodeInfoTab.jsx +148 -14
- package/src/app/pages/NodePage/NodeMaterializationTab.jsx +201 -104
- package/src/app/pages/NodePage/NodeStatus.jsx +96 -21
- package/src/app/pages/NodePage/NodeValidateTab.jsx +367 -0
- package/src/app/pages/NodePage/NotebookDownload.jsx +36 -0
- package/src/app/pages/NodePage/PartitionColumnPopover.jsx +3 -5
- package/src/app/pages/NodePage/PartitionValueForm.jsx +60 -0
- package/src/app/pages/NodePage/RevisionDiff.jsx +209 -0
- package/src/app/pages/NodePage/WatchNodeButton.jsx +226 -0
- package/src/app/pages/NodePage/__tests__/AddBackfillPopover.test.jsx +13 -4
- package/src/app/pages/NodePage/__tests__/AddMaterializationPopover.test.jsx +87 -0
- package/src/app/pages/NodePage/__tests__/DimensionFilter.test.jsx +74 -0
- package/src/app/pages/NodePage/__tests__/EditColumnDescriptionPopover.test.jsx +149 -0
- package/src/app/pages/NodePage/__tests__/LinkDimensionPopover.test.jsx +10 -14
- package/src/app/pages/NodePage/__tests__/NodeColumnTab.test.jsx +166 -0
- package/src/app/pages/NodePage/__tests__/NodeDependenciesTab.test.jsx +151 -0
- package/src/app/pages/NodePage/__tests__/NodeGraphTab.test.jsx +6 -2
- package/src/app/pages/NodePage/__tests__/NodeLineageTab.test.jsx +3 -2
- package/src/app/pages/NodePage/__tests__/NodeMaterializationTab.test.jsx +148 -0
- package/src/app/pages/NodePage/__tests__/NodePage.test.jsx +159 -57
- package/src/app/pages/NodePage/__tests__/RevisionDiff.test.jsx +164 -0
- package/src/app/pages/NodePage/__tests__/__snapshots__/NodePage.test.jsx.snap +2 -386
- package/src/app/pages/NodePage/index.jsx +94 -57
- package/src/app/pages/Root/__tests__/index.test.jsx +3 -1
- package/src/app/pages/Root/index.tsx +62 -12
- package/src/app/services/DJService.js +587 -55
- package/src/app/services/__tests__/DJService.test.jsx +382 -45
- package/src/index.tsx +1 -0
- package/src/mocks/mockNodes.jsx +265 -227
- package/src/styles/dag.css +4 -2
- package/src/styles/index.css +474 -10
- package/src/styles/loading.css +1 -1
- package/src/styles/node-creation.scss +84 -5
- package/src/styles/node-list.css +4 -0
- package/src/styles/sorted-table.css +15 -0
- package/src/app/components/DeleteNode.jsx +0 -55
- package/src/app/components/__tests__/DeleteNode.test.jsx +0 -53
- package/src/app/pages/NodePage/NodeSQLTab.jsx +0 -82
- package/src/app/pages/NodePage/__tests__/ClientCodePopover.test.jsx +0 -49
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Materialization configuration field.
|
|
3
|
+
*/
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import { ErrorMessage, Field, useFormikContext } from 'formik';
|
|
6
|
+
import CodeMirror from '@uiw/react-codemirror';
|
|
7
|
+
import { langs } from '@uiw/codemirror-extensions-langs';
|
|
8
|
+
|
|
9
|
+
export const ConfigField = ({ djClient, value }) => {
|
|
10
|
+
const formik = useFormikContext();
|
|
11
|
+
const jsonExt = langs.json();
|
|
12
|
+
|
|
13
|
+
const updateFormik = val => {
|
|
14
|
+
formik.setFieldValue('spark_config', JSON.parse(val));
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div className="DescriptionInput">
|
|
19
|
+
<details>
|
|
20
|
+
<summary>
|
|
21
|
+
<label htmlFor="SparkConfig" style={{ display: 'inline-block' }}>
|
|
22
|
+
Spark Config
|
|
23
|
+
</label>
|
|
24
|
+
</summary>
|
|
25
|
+
<ErrorMessage name="spark_config" component="span" />
|
|
26
|
+
<Field
|
|
27
|
+
type="textarea"
|
|
28
|
+
style={{ display: 'none' }}
|
|
29
|
+
as="textarea"
|
|
30
|
+
name="spark_config"
|
|
31
|
+
id="SparkConfig"
|
|
32
|
+
/>
|
|
33
|
+
<div role="button" tabIndex={0} className="relative flex bg-[#282a36]">
|
|
34
|
+
<CodeMirror
|
|
35
|
+
id={'spark_config'}
|
|
36
|
+
name={'spark_config'}
|
|
37
|
+
extensions={[jsonExt]}
|
|
38
|
+
value={JSON.stringify(value, null, ' ')}
|
|
39
|
+
options={{
|
|
40
|
+
theme: 'default',
|
|
41
|
+
lineNumbers: true,
|
|
42
|
+
}}
|
|
43
|
+
width="100%"
|
|
44
|
+
height="170px"
|
|
45
|
+
style={{
|
|
46
|
+
margin: '0 0 23px 0',
|
|
47
|
+
flex: 1,
|
|
48
|
+
fontSize: '150%',
|
|
49
|
+
textAlign: 'left',
|
|
50
|
+
}}
|
|
51
|
+
onChange={(value, viewUpdate) => {
|
|
52
|
+
updateFormik(value);
|
|
53
|
+
}}
|
|
54
|
+
/>
|
|
55
|
+
</div>
|
|
56
|
+
</details>{' '}
|
|
57
|
+
<></>
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
};
|
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react';
|
|
2
|
-
import ClientCodePopover from './ClientCodePopover';
|
|
3
2
|
import * as React from 'react';
|
|
4
3
|
import EditColumnPopover from './EditColumnPopover';
|
|
4
|
+
import EditColumnDescriptionPopover from './EditColumnDescriptionPopover';
|
|
5
5
|
import LinkDimensionPopover from './LinkDimensionPopover';
|
|
6
6
|
import { labelize } from '../../../utils/form';
|
|
7
7
|
import PartitionColumnPopover from './PartitionColumnPopover';
|
|
8
|
+
import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
|
|
9
|
+
import { foundation } from 'react-syntax-highlighter/src/styles/hljs';
|
|
10
|
+
import { link } from 'fs';
|
|
8
11
|
|
|
9
12
|
export default function NodeColumnTab({ node, djClient }) {
|
|
10
13
|
const [attributes, setAttributes] = useState([]);
|
|
11
14
|
const [dimensions, setDimensions] = useState([]);
|
|
12
15
|
const [columns, setColumns] = useState([]);
|
|
16
|
+
const [links, setLinks] = useState([]);
|
|
17
|
+
|
|
13
18
|
useEffect(() => {
|
|
14
19
|
const fetchData = async () => {
|
|
15
|
-
|
|
20
|
+
if (node) {
|
|
21
|
+
setColumns(await djClient.columns(node));
|
|
22
|
+
}
|
|
16
23
|
};
|
|
17
24
|
fetchData().catch(console.error);
|
|
18
25
|
}, [djClient, node]);
|
|
@@ -31,8 +38,11 @@ export default function NodeColumnTab({ node, djClient }) {
|
|
|
31
38
|
useEffect(() => {
|
|
32
39
|
const fetchData = async () => {
|
|
33
40
|
const dimensions = await djClient.dimensions();
|
|
34
|
-
const options = dimensions.map(
|
|
35
|
-
return {
|
|
41
|
+
const options = dimensions.map(dim => {
|
|
42
|
+
return {
|
|
43
|
+
value: dim.name,
|
|
44
|
+
label: `${dim.name} (${dim.indegree} links)`,
|
|
45
|
+
};
|
|
36
46
|
});
|
|
37
47
|
setDimensions(options);
|
|
38
48
|
};
|
|
@@ -54,28 +64,16 @@ export default function NodeColumnTab({ node, djClient }) {
|
|
|
54
64
|
if (col.partition) {
|
|
55
65
|
return (
|
|
56
66
|
<>
|
|
57
|
-
<span
|
|
58
|
-
className="
|
|
59
|
-
key={`col-attr-partition-type`}
|
|
60
|
-
>
|
|
61
|
-
<span
|
|
62
|
-
className="partition_value badge"
|
|
63
|
-
key={`col-attr-partition-type`}
|
|
64
|
-
>
|
|
67
|
+
<span className="node_type badge node_type__blank">
|
|
68
|
+
<span className="partition_value badge">
|
|
65
69
|
<b>Type:</b> {col.partition.type_}
|
|
66
70
|
</span>
|
|
67
71
|
<br />
|
|
68
|
-
<span
|
|
69
|
-
className="partition_value badge"
|
|
70
|
-
key={`col-attr-partition-type`}
|
|
71
|
-
>
|
|
72
|
+
<span className="partition_value badge">
|
|
72
73
|
<b>Format:</b> <code>{col.partition.format}</code>
|
|
73
74
|
</span>
|
|
74
75
|
<br />
|
|
75
|
-
<span
|
|
76
|
-
className="partition_value badge"
|
|
77
|
-
key={`col-attr-partition-type`}
|
|
78
|
-
>
|
|
76
|
+
<span className="partition_value badge">
|
|
79
77
|
<b>Granularity:</b> <code>{col.partition.granularity}</code>
|
|
80
78
|
</span>
|
|
81
79
|
</span>
|
|
@@ -86,115 +84,187 @@ export default function NodeColumnTab({ node, djClient }) {
|
|
|
86
84
|
};
|
|
87
85
|
|
|
88
86
|
const columnList = columns => {
|
|
89
|
-
return columns
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
aria-hidden="false"
|
|
105
|
-
>
|
|
106
|
-
{col.display_name}
|
|
107
|
-
</span>
|
|
108
|
-
</td>
|
|
109
|
-
<td>
|
|
110
|
-
<span
|
|
111
|
-
className={`node_type__${
|
|
112
|
-
node.type === 'cube' ? col.type : 'transform'
|
|
113
|
-
} badge node_type`}
|
|
87
|
+
return columns?.map(col => {
|
|
88
|
+
const dimensionLinks = (links.length > 0 ? links : node?.dimension_links)
|
|
89
|
+
.map(link => [
|
|
90
|
+
link.dimension.name,
|
|
91
|
+
Object.entries(link.foreign_keys).filter(
|
|
92
|
+
entry => entry[0] === node.name + '.' + col.name,
|
|
93
|
+
),
|
|
94
|
+
])
|
|
95
|
+
.filter(keys => keys[1].length >= 1);
|
|
96
|
+
const referencedDimensionNode =
|
|
97
|
+
dimensionLinks.length > 0 ? dimensionLinks[0][0] : null;
|
|
98
|
+
return (
|
|
99
|
+
<tr key={col.name}>
|
|
100
|
+
<td
|
|
101
|
+
className="text-start"
|
|
114
102
|
role="columnheader"
|
|
115
|
-
aria-label="
|
|
103
|
+
aria-label="ColumnName"
|
|
116
104
|
aria-hidden="false"
|
|
117
105
|
>
|
|
118
|
-
{col.
|
|
119
|
-
</
|
|
120
|
-
</td>
|
|
121
|
-
{node.type !== 'cube' ? (
|
|
106
|
+
{col.name}
|
|
107
|
+
</td>
|
|
122
108
|
<td>
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
<
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
109
|
+
<span
|
|
110
|
+
className=""
|
|
111
|
+
role="columnheader"
|
|
112
|
+
aria-label="ColumnDisplayName"
|
|
113
|
+
aria-hidden="false"
|
|
114
|
+
>
|
|
115
|
+
{col.display_name}
|
|
116
|
+
</span>
|
|
117
|
+
</td>
|
|
118
|
+
<td>
|
|
119
|
+
<span
|
|
120
|
+
className=""
|
|
121
|
+
role="columnheader"
|
|
122
|
+
aria-label="ColumnDescription"
|
|
123
|
+
aria-hidden="false"
|
|
124
|
+
>
|
|
125
|
+
{col.description || ''}
|
|
126
|
+
<EditColumnDescriptionPopover
|
|
127
|
+
column={col}
|
|
128
|
+
node={node}
|
|
129
|
+
onSubmit={async () => {
|
|
130
|
+
const res = await djClient.node(node.name);
|
|
131
|
+
setColumns(res.columns);
|
|
132
|
+
}}
|
|
133
|
+
/>
|
|
134
|
+
</span>
|
|
142
135
|
</td>
|
|
143
|
-
) : (
|
|
144
|
-
''
|
|
145
|
-
)}
|
|
146
|
-
{node.type !== 'cube' ? (
|
|
147
136
|
<td>
|
|
148
|
-
|
|
149
|
-
|
|
137
|
+
<span
|
|
138
|
+
className={`node_type__${
|
|
139
|
+
node.type === 'cube' ? col.type : 'transform'
|
|
140
|
+
} badge node_type`}
|
|
141
|
+
role="columnheader"
|
|
142
|
+
aria-label="ColumnType"
|
|
143
|
+
aria-hidden="false"
|
|
144
|
+
>
|
|
145
|
+
{col.type}
|
|
146
|
+
</span>
|
|
147
|
+
</td>
|
|
148
|
+
{node.type !== 'cube' ? (
|
|
149
|
+
<td>
|
|
150
|
+
{dimensionLinks.length > 0
|
|
151
|
+
? dimensionLinks.map(link => (
|
|
152
|
+
<span
|
|
153
|
+
className="rounded-pill badge bg-secondary-soft"
|
|
154
|
+
style={{ fontSize: '14px' }}
|
|
155
|
+
key={link[0]}
|
|
156
|
+
>
|
|
157
|
+
<a href={`/nodes/${link[0]}`}>{link[0]}</a>
|
|
158
|
+
</span>
|
|
159
|
+
))
|
|
160
|
+
: ''}
|
|
161
|
+
<LinkDimensionPopover
|
|
162
|
+
column={col}
|
|
163
|
+
dimensionNodes={dimensionLinks.map(link => link[0])}
|
|
164
|
+
node={node}
|
|
165
|
+
options={dimensions}
|
|
166
|
+
onSubmit={async () => {
|
|
167
|
+
const res = await djClient.node(node.name);
|
|
168
|
+
setLinks(res.dimension_links);
|
|
169
|
+
}}
|
|
170
|
+
/>
|
|
171
|
+
</td>
|
|
172
|
+
) : (
|
|
173
|
+
''
|
|
174
|
+
)}
|
|
175
|
+
{node.type !== 'cube' ? (
|
|
176
|
+
<td>
|
|
177
|
+
{showColumnAttributes(col)}
|
|
178
|
+
<EditColumnPopover
|
|
179
|
+
column={col}
|
|
180
|
+
node={node}
|
|
181
|
+
options={attributes}
|
|
182
|
+
onSubmit={async () => {
|
|
183
|
+
const res = await djClient.node(node.name);
|
|
184
|
+
setColumns(res.columns);
|
|
185
|
+
}}
|
|
186
|
+
/>
|
|
187
|
+
</td>
|
|
188
|
+
) : (
|
|
189
|
+
''
|
|
190
|
+
)}
|
|
191
|
+
<td>
|
|
192
|
+
{showColumnPartition(col)}
|
|
193
|
+
<PartitionColumnPopover
|
|
150
194
|
column={col}
|
|
151
195
|
node={node}
|
|
152
|
-
options={attributes}
|
|
153
196
|
onSubmit={async () => {
|
|
154
197
|
const res = await djClient.node(node.name);
|
|
155
198
|
setColumns(res.columns);
|
|
156
199
|
}}
|
|
157
200
|
/>
|
|
158
201
|
</td>
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
<td>
|
|
163
|
-
{showColumnPartition(col)}
|
|
164
|
-
<PartitionColumnPopover
|
|
165
|
-
column={col}
|
|
166
|
-
node={node}
|
|
167
|
-
onSubmit={async () => {
|
|
168
|
-
const res = await djClient.node(node.name);
|
|
169
|
-
setColumns(res.columns);
|
|
170
|
-
}}
|
|
171
|
-
/>
|
|
172
|
-
</td>
|
|
173
|
-
</tr>
|
|
174
|
-
));
|
|
202
|
+
</tr>
|
|
203
|
+
);
|
|
204
|
+
});
|
|
175
205
|
};
|
|
176
206
|
|
|
177
207
|
return (
|
|
178
|
-
|
|
179
|
-
<
|
|
180
|
-
<
|
|
181
|
-
<
|
|
182
|
-
<
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
208
|
+
<>
|
|
209
|
+
<div className="table-responsive">
|
|
210
|
+
<table className="card-inner-table table">
|
|
211
|
+
<thead className="fs-7 fw-bold text-gray-400 border-bottom-0">
|
|
212
|
+
<tr>
|
|
213
|
+
<th className="text-start">Column</th>
|
|
214
|
+
<th>Display Name</th>
|
|
215
|
+
<th>Description</th>
|
|
216
|
+
<th>Type</th>
|
|
217
|
+
{node?.type !== 'cube' ? (
|
|
218
|
+
<>
|
|
219
|
+
<th>Linked Dimension</th>
|
|
220
|
+
<th>Attributes</th>
|
|
221
|
+
</>
|
|
222
|
+
) : (
|
|
223
|
+
''
|
|
224
|
+
)}
|
|
225
|
+
<th>Partition</th>
|
|
226
|
+
</tr>
|
|
227
|
+
</thead>
|
|
228
|
+
<tbody>{columnList(columns)}</tbody>
|
|
229
|
+
</table>
|
|
230
|
+
</div>
|
|
231
|
+
<div>
|
|
232
|
+
<h3>Linked Dimensions (Custom Join SQL)</h3>
|
|
233
|
+
<table className="card-inner-table table">
|
|
234
|
+
<thead className="fs-7 fw-bold text-gray-400 border-bottom-0">
|
|
235
|
+
<tr>
|
|
236
|
+
<th className="text-start">Dimension Node</th>
|
|
237
|
+
<th>Join Type</th>
|
|
238
|
+
<th>Join SQL</th>
|
|
239
|
+
<th>Role</th>
|
|
240
|
+
</tr>
|
|
241
|
+
</thead>
|
|
242
|
+
<tbody>
|
|
243
|
+
{node?.dimension_links.map(link => {
|
|
244
|
+
return (
|
|
245
|
+
<tr key={link.dimension.name}>
|
|
246
|
+
<td>
|
|
247
|
+
<a href={'/nodes/' + link.dimension.name}>
|
|
248
|
+
{link.dimension.name}
|
|
249
|
+
</a>
|
|
250
|
+
</td>
|
|
251
|
+
<td>{link.join_type.toUpperCase()}</td>
|
|
252
|
+
<td style={{ width: '25rem', maxWidth: 'none' }}>
|
|
253
|
+
<SyntaxHighlighter
|
|
254
|
+
language="sql"
|
|
255
|
+
style={foundation}
|
|
256
|
+
wrapLongLines={true}
|
|
257
|
+
>
|
|
258
|
+
{link.join_sql}
|
|
259
|
+
</SyntaxHighlighter>
|
|
260
|
+
</td>
|
|
261
|
+
<td>{link.role}</td>
|
|
262
|
+
</tr>
|
|
263
|
+
);
|
|
264
|
+
})}
|
|
265
|
+
</tbody>
|
|
266
|
+
</table>
|
|
267
|
+
</div>
|
|
268
|
+
</>
|
|
199
269
|
);
|
|
200
270
|
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { labelize } from '../../../utils/form';
|
|
4
|
+
import LoadingIcon from '../../icons/LoadingIcon';
|
|
5
|
+
|
|
6
|
+
export default function NodeDependenciesTab({ node, djClient }) {
|
|
7
|
+
const [nodeDAG, setNodeDAG] = useState({
|
|
8
|
+
upstreams: [],
|
|
9
|
+
downstreams: [],
|
|
10
|
+
dimensions: [],
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const [retrieved, setRetrieved] = useState(false);
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const fetchData = async () => {
|
|
17
|
+
if (node) {
|
|
18
|
+
let upstreams = await djClient.upstreams(node.name);
|
|
19
|
+
let downstreams = await djClient.downstreams(node.name);
|
|
20
|
+
let dimensions = await djClient.nodeDimensions(node.name);
|
|
21
|
+
setNodeDAG({
|
|
22
|
+
upstreams: upstreams,
|
|
23
|
+
downstreams: downstreams,
|
|
24
|
+
dimensions: dimensions,
|
|
25
|
+
});
|
|
26
|
+
setRetrieved(true);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
fetchData().catch(console.error);
|
|
30
|
+
}, [djClient, node]);
|
|
31
|
+
|
|
32
|
+
// Builds the block of dimensions selectors, grouped by node name + path
|
|
33
|
+
return (
|
|
34
|
+
<div>
|
|
35
|
+
<h2>Upstreams</h2>
|
|
36
|
+
{retrieved ? (
|
|
37
|
+
<NodeList nodes={nodeDAG.upstreams} />
|
|
38
|
+
) : (
|
|
39
|
+
<span style={{ display: 'block' }}>
|
|
40
|
+
<LoadingIcon centered={false} />
|
|
41
|
+
</span>
|
|
42
|
+
)}
|
|
43
|
+
<h2>Downstreams</h2>
|
|
44
|
+
{retrieved ? (
|
|
45
|
+
<NodeList nodes={nodeDAG.downstreams} />
|
|
46
|
+
) : (
|
|
47
|
+
<span style={{ display: 'block' }}>
|
|
48
|
+
<LoadingIcon centered={false} />
|
|
49
|
+
</span>
|
|
50
|
+
)}
|
|
51
|
+
<h2>Dimensions</h2>
|
|
52
|
+
{retrieved ? (
|
|
53
|
+
<NodeDimensionsList rawDimensions={nodeDAG.dimensions} />
|
|
54
|
+
) : (
|
|
55
|
+
<span style={{ display: 'block' }}>
|
|
56
|
+
<LoadingIcon centered={false} />
|
|
57
|
+
</span>
|
|
58
|
+
)}
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function NodeDimensionsList({ rawDimensions }) {
|
|
64
|
+
const dimensions = Object.entries(
|
|
65
|
+
rawDimensions.reduce((group, dimension) => {
|
|
66
|
+
group[dimension.node_name + dimension.path] =
|
|
67
|
+
group[dimension.node_name + dimension.path] ?? [];
|
|
68
|
+
group[dimension.node_name + dimension.path].push(dimension);
|
|
69
|
+
return group;
|
|
70
|
+
}, {}),
|
|
71
|
+
);
|
|
72
|
+
return (
|
|
73
|
+
<div style={{ padding: '0.5rem' }}>
|
|
74
|
+
{dimensions.map(grouping => {
|
|
75
|
+
const dimensionsInGroup = grouping[1];
|
|
76
|
+
const role = dimensionsInGroup[0].path
|
|
77
|
+
.map(pathItem => pathItem.split('.').slice(-1))
|
|
78
|
+
.join(' → ');
|
|
79
|
+
const fullPath = dimensionsInGroup[0].path.join(' → ');
|
|
80
|
+
const groupHeader = (
|
|
81
|
+
<span
|
|
82
|
+
style={{
|
|
83
|
+
fontWeight: 'normal',
|
|
84
|
+
marginBottom: '15px',
|
|
85
|
+
marginTop: '15px',
|
|
86
|
+
}}
|
|
87
|
+
>
|
|
88
|
+
<a href={`/nodes/${dimensionsInGroup[0].node_name}`}>
|
|
89
|
+
<b>{dimensionsInGroup[0].node_display_name}</b>
|
|
90
|
+
</a>{' '}
|
|
91
|
+
with role{' '}
|
|
92
|
+
<span className="HighlightPath">
|
|
93
|
+
<b>{role}</b>
|
|
94
|
+
</span>{' '}
|
|
95
|
+
via <span className="HighlightPath">{fullPath}</span>
|
|
96
|
+
</span>
|
|
97
|
+
);
|
|
98
|
+
const dimensionGroupOptions = dimensionsInGroup.map(dim => {
|
|
99
|
+
return {
|
|
100
|
+
value: dim.name,
|
|
101
|
+
label:
|
|
102
|
+
labelize(dim.name.split('.').slice(-1)[0]) +
|
|
103
|
+
(dim.properties.includes('primary_key') ? ' (PK)' : ''),
|
|
104
|
+
};
|
|
105
|
+
});
|
|
106
|
+
return (
|
|
107
|
+
<details key={grouping[0]}>
|
|
108
|
+
<summary style={{ marginBottom: '10px' }}>{groupHeader}</summary>
|
|
109
|
+
<div className="dimensionsList">
|
|
110
|
+
{dimensionGroupOptions.map(dimension => {
|
|
111
|
+
return (
|
|
112
|
+
<div key={dimension.value}>
|
|
113
|
+
{dimension.label.split('[').slice(0)[0]} ⇢{' '}
|
|
114
|
+
<code className="DimensionAttribute">
|
|
115
|
+
{dimension.value}
|
|
116
|
+
</code>
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
})}
|
|
120
|
+
</div>
|
|
121
|
+
</details>
|
|
122
|
+
);
|
|
123
|
+
})}
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function NodeList({ nodes }) {
|
|
129
|
+
return nodes && nodes.length > 0 ? (
|
|
130
|
+
<ul className="backfills">
|
|
131
|
+
{nodes?.map(node => (
|
|
132
|
+
<li
|
|
133
|
+
className="backfill"
|
|
134
|
+
style={{ marginBottom: '5px' }}
|
|
135
|
+
key={node.name}
|
|
136
|
+
>
|
|
137
|
+
<span
|
|
138
|
+
className={`node_type__${node.type} badge node_type`}
|
|
139
|
+
style={{ marginRight: '5px' }}
|
|
140
|
+
role="dialog"
|
|
141
|
+
aria-hidden="false"
|
|
142
|
+
aria-label="NodeType"
|
|
143
|
+
>
|
|
144
|
+
{node.type}
|
|
145
|
+
</span>
|
|
146
|
+
<a href={`/nodes/${node.name}`}>{node.name}</a>
|
|
147
|
+
</li>
|
|
148
|
+
))}
|
|
149
|
+
</ul>
|
|
150
|
+
) : (
|
|
151
|
+
<span style={{ display: 'block' }}>None</span>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
@@ -6,7 +6,7 @@ import 'reactflow/dist/style.css';
|
|
|
6
6
|
import DJClientContext from '../../providers/djclient';
|
|
7
7
|
import LayoutFlow from '../../components/djgraph/LayoutFlow';
|
|
8
8
|
|
|
9
|
-
const
|
|
9
|
+
const NodeGraphTab = djNode => {
|
|
10
10
|
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
11
11
|
|
|
12
12
|
const createNode = node => {
|
|
@@ -15,9 +15,25 @@ const NodeLineage = djNode => {
|
|
|
15
15
|
col.attributes.some(attr => attr.attribute_type.name === 'primary_key'),
|
|
16
16
|
)
|
|
17
17
|
.map(col => col.name);
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
const dimensionLinkForeignKeys = node.dimension_links
|
|
19
|
+
? node.dimension_links.flatMap(link =>
|
|
20
|
+
Object.keys(link.foreign_keys).map(key => key.split('.').slice(-1)),
|
|
21
|
+
)
|
|
22
|
+
: [];
|
|
23
|
+
const column_names = node.columns
|
|
24
|
+
.map(col => {
|
|
25
|
+
return {
|
|
26
|
+
name: col.name,
|
|
27
|
+
type: col.type,
|
|
28
|
+
dimension: col.dimension !== null ? col.dimension.name : null,
|
|
29
|
+
order: primary_key.includes(col.name)
|
|
30
|
+
? -1
|
|
31
|
+
: dimensionLinkForeignKeys.includes(col.name)
|
|
32
|
+
? 0
|
|
33
|
+
: 1,
|
|
34
|
+
};
|
|
35
|
+
})
|
|
36
|
+
.sort((a, b) => a.order - b.order);
|
|
21
37
|
return {
|
|
22
38
|
id: String(node.name),
|
|
23
39
|
type: 'DJNode',
|
|
@@ -38,28 +54,37 @@ const NodeLineage = djNode => {
|
|
|
38
54
|
};
|
|
39
55
|
|
|
40
56
|
const dimensionEdges = node => {
|
|
41
|
-
return node.
|
|
42
|
-
|
|
43
|
-
.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
57
|
+
return node.dimension_links === undefined
|
|
58
|
+
? []
|
|
59
|
+
: node.dimension_links.flatMap(link => {
|
|
60
|
+
return Object.keys(link.foreign_keys).map(fk => {
|
|
61
|
+
return {
|
|
62
|
+
id:
|
|
63
|
+
link.dimension.name +
|
|
64
|
+
'->' +
|
|
65
|
+
node.name +
|
|
66
|
+
'=' +
|
|
67
|
+
link.foreign_keys[fk] +
|
|
68
|
+
'->' +
|
|
69
|
+
fk,
|
|
70
|
+
source: link.dimension.name,
|
|
71
|
+
sourceHandle: link.foreign_keys[fk],
|
|
72
|
+
target: node.name,
|
|
73
|
+
targetHandle: fk,
|
|
74
|
+
draggable: true,
|
|
75
|
+
markerStart: {
|
|
76
|
+
type: MarkerType.Arrow,
|
|
77
|
+
width: 20,
|
|
78
|
+
height: 20,
|
|
79
|
+
color: '#b0b9c2',
|
|
80
|
+
},
|
|
81
|
+
style: {
|
|
82
|
+
strokeWidth: 3,
|
|
83
|
+
stroke: '#b0b9c2',
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
});
|
|
87
|
+
});
|
|
63
88
|
};
|
|
64
89
|
|
|
65
90
|
const parentEdges = node => {
|
|
@@ -85,8 +110,10 @@ const NodeLineage = djNode => {
|
|
|
85
110
|
};
|
|
86
111
|
|
|
87
112
|
const dagFetch = async (getLayoutedElements, setNodes, setEdges) => {
|
|
88
|
-
let related_nodes =
|
|
89
|
-
|
|
113
|
+
let related_nodes = djNode.djNode
|
|
114
|
+
? await djClient.node_dag(djNode.djNode.name)
|
|
115
|
+
: [];
|
|
116
|
+
var djNodes = djNode.djNode ? [djNode.djNode] : [];
|
|
90
117
|
for (const iterable of [related_nodes]) {
|
|
91
118
|
for (const item of iterable) {
|
|
92
119
|
if (item.type !== 'cube') {
|
|
@@ -109,4 +136,4 @@ const NodeLineage = djNode => {
|
|
|
109
136
|
};
|
|
110
137
|
return LayoutFlow(djNode, dagFetch);
|
|
111
138
|
};
|
|
112
|
-
export default
|
|
139
|
+
export default NodeGraphTab;
|