datajunction-ui 0.0.1-a44.dev1 → 0.0.1-a44.dev4
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/package.json +2 -2
- package/src/app/components/djgraph/DJNode.jsx +1 -1
- package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormFailed.test.jsx +1 -3
- package/src/app/pages/AddEditNodePage/index.jsx +15 -10
- package/src/app/pages/NodePage/AddMaterializationPopover.jsx +57 -34
- package/src/app/pages/NodePage/MaterializationConfigField.jsx +53 -0
- package/src/app/pages/NodePage/NodeGraphTab.jsx +37 -23
- package/src/app/pages/NodePage/NodeMaterializationTab.jsx +1 -1
- package/src/app/pages/NodePage/__tests__/AddMaterializationPopover.test.jsx +3 -0
- package/src/app/pages/NodePage/__tests__/NodeGraphTab.test.jsx +4 -0
- package/src/styles/index.css +3 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "datajunction-ui",
|
|
3
|
-
"version": "0.0.1-a44.
|
|
3
|
+
"version": "0.0.1-a44.dev4",
|
|
4
4
|
"description": "DataJunction Metrics Platform UI",
|
|
5
5
|
"module": "src/index.tsx",
|
|
6
6
|
"repository": {
|
|
@@ -162,7 +162,7 @@
|
|
|
162
162
|
],
|
|
163
163
|
"coverageThreshold": {
|
|
164
164
|
"global": {
|
|
165
|
-
"statements":
|
|
165
|
+
"statements": 89,
|
|
166
166
|
"branches": 75,
|
|
167
167
|
"lines": 90,
|
|
168
168
|
"functions": 85
|
|
@@ -68,7 +68,7 @@ export function DJNode({ id, data }) {
|
|
|
68
68
|
{data.type === 'source' ? data.table : data.display_name}
|
|
69
69
|
</a>
|
|
70
70
|
<Collapse
|
|
71
|
-
collapsed={true}
|
|
71
|
+
collapsed={data.is_current && data.type != 'metric' ? false : true}
|
|
72
72
|
text={data.type !== 'metric' ? 'columns' : 'dimensions'}
|
|
73
73
|
data={data}
|
|
74
74
|
/>
|
|
@@ -101,9 +101,7 @@ describe('AddEditNodePage submission failed', () => {
|
|
|
101
101
|
status: 404,
|
|
102
102
|
});
|
|
103
103
|
|
|
104
|
-
expect(
|
|
105
|
-
await screen.getByText('Update failed, Some tags were not found'),
|
|
106
|
-
).toBeInTheDocument();
|
|
104
|
+
expect(await screen.getByText('Update failed')).toBeInTheDocument();
|
|
107
105
|
});
|
|
108
106
|
}, 60000);
|
|
109
107
|
});
|
|
@@ -24,6 +24,7 @@ import { DisplayNameField } from './DisplayNameField';
|
|
|
24
24
|
import { DescriptionField } from './DescriptionField';
|
|
25
25
|
import { NodeModeField } from './NodeModeField';
|
|
26
26
|
import { RequiredDimensionsSelect } from './RequiredDimensionsSelect';
|
|
27
|
+
import LoadingIcon from '../../icons/LoadingIcon';
|
|
27
28
|
|
|
28
29
|
class Action {
|
|
29
30
|
static Add = new Action('add');
|
|
@@ -63,19 +64,18 @@ export function AddEditNodePage() {
|
|
|
63
64
|
return errors;
|
|
64
65
|
};
|
|
65
66
|
|
|
66
|
-
const handleSubmit = (values, { setSubmitting, setStatus }) => {
|
|
67
|
+
const handleSubmit = async (values, { setSubmitting, setStatus }) => {
|
|
67
68
|
if (action === Action.Add) {
|
|
68
|
-
|
|
69
|
-
|
|
69
|
+
await createNode(values, setStatus).then(_ => {
|
|
70
|
+
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
|
|
70
71
|
setSubmitting(false);
|
|
71
|
-
}
|
|
72
|
+
});
|
|
72
73
|
} else {
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
await patchNode(values, setStatus).then(_ => {
|
|
75
|
+
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
|
|
75
76
|
setSubmitting(false);
|
|
76
|
-
}
|
|
77
|
+
});
|
|
77
78
|
}
|
|
78
|
-
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
|
|
79
79
|
};
|
|
80
80
|
|
|
81
81
|
const pageTitle =
|
|
@@ -169,7 +169,7 @@ export function AddEditNodePage() {
|
|
|
169
169
|
});
|
|
170
170
|
} else {
|
|
171
171
|
setStatus({
|
|
172
|
-
failure: `${json.message}
|
|
172
|
+
failure: `${json.message}`,
|
|
173
173
|
});
|
|
174
174
|
}
|
|
175
175
|
};
|
|
@@ -410,7 +410,12 @@ export function AddEditNodePage() {
|
|
|
410
410
|
<NodeModeField />
|
|
411
411
|
|
|
412
412
|
<button type="submit" disabled={isSubmitting}>
|
|
413
|
-
{
|
|
413
|
+
{isSubmitting ? (
|
|
414
|
+
<LoadingIcon />
|
|
415
|
+
) : (
|
|
416
|
+
(action === Action.Add ? 'Create ' : 'Save ') +
|
|
417
|
+
(nodeType ? nodeType : '')
|
|
418
|
+
)}
|
|
414
419
|
</button>
|
|
415
420
|
</>
|
|
416
421
|
)}
|
|
@@ -3,6 +3,8 @@ import * as React from 'react';
|
|
|
3
3
|
import DJClientContext from '../../providers/djclient';
|
|
4
4
|
import { ErrorMessage, Field, Form, Formik } from 'formik';
|
|
5
5
|
import { displayMessageAfterSubmit, labelize } from '../../../utils/form';
|
|
6
|
+
import { ConfigField } from './MaterializationConfigField';
|
|
7
|
+
import LoadingIcon from '../../icons/LoadingIcon';
|
|
6
8
|
|
|
7
9
|
export default function AddMaterializationPopover({ node, onSubmit }) {
|
|
8
10
|
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
@@ -33,29 +35,34 @@ export default function AddMaterializationPopover({ node, onSubmit }) {
|
|
|
33
35
|
};
|
|
34
36
|
}, [djClient, setPopoverAnchor]);
|
|
35
37
|
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
) => {
|
|
40
|
-
setSubmitting(false);
|
|
41
|
-
const config = JSON.parse(values.config);
|
|
38
|
+
const materialize = async (values, setStatus) => {
|
|
39
|
+
const config = {};
|
|
40
|
+
config.spark = values.spark_config;
|
|
42
41
|
config.lookback_window = values.lookback_window;
|
|
43
|
-
const
|
|
42
|
+
const { status, json } = await djClient.materialize(
|
|
44
43
|
values.node,
|
|
45
44
|
values.job_type,
|
|
46
45
|
values.strategy,
|
|
47
46
|
values.schedule,
|
|
48
47
|
config,
|
|
49
48
|
);
|
|
50
|
-
if (
|
|
51
|
-
setStatus({ success:
|
|
49
|
+
if (status === 200 || status === 201) {
|
|
50
|
+
setStatus({ success: json.message });
|
|
52
51
|
} else {
|
|
53
52
|
setStatus({
|
|
54
|
-
failure: `${
|
|
53
|
+
failure: `${json.message}`,
|
|
55
54
|
});
|
|
56
55
|
}
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const configureMaterialization = async (
|
|
59
|
+
values,
|
|
60
|
+
{ setSubmitting, setStatus },
|
|
61
|
+
) => {
|
|
62
|
+
await materialize(values, setStatus).then(_ => {
|
|
63
|
+
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
|
|
64
|
+
setSubmitting(false);
|
|
65
|
+
});
|
|
59
66
|
};
|
|
60
67
|
|
|
61
68
|
return (
|
|
@@ -87,11 +94,15 @@ export default function AddMaterializationPopover({ node, onSubmit }) {
|
|
|
87
94
|
<Formik
|
|
88
95
|
initialValues={{
|
|
89
96
|
node: node?.name,
|
|
90
|
-
job_type:
|
|
97
|
+
job_type:
|
|
98
|
+
node?.type === 'cube' ? 'druid_metrics_cube' : 'spark_sql',
|
|
91
99
|
strategy: 'full',
|
|
92
|
-
config: '{"spark": {"spark.executor.memory": "6g"}}',
|
|
93
100
|
schedule: '@daily',
|
|
94
101
|
lookback_window: '1 DAY',
|
|
102
|
+
spark_config: {
|
|
103
|
+
'spark.executor.memory': '16g',
|
|
104
|
+
'spark.memory.fraction': '0.3',
|
|
105
|
+
},
|
|
95
106
|
}}
|
|
96
107
|
onSubmit={configureMaterialization}
|
|
97
108
|
>
|
|
@@ -104,11 +115,21 @@ export default function AddMaterializationPopover({ node, onSubmit }) {
|
|
|
104
115
|
<label htmlFor="job_type">Job Type</label>
|
|
105
116
|
<Field as="select" name="job_type">
|
|
106
117
|
<>
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
118
|
+
<option
|
|
119
|
+
key={'druid_measures_cube'}
|
|
120
|
+
value={'druid_measures_cube'}
|
|
121
|
+
>
|
|
122
|
+
Druid Measures Cube (Pre-Agg Cube)
|
|
123
|
+
</option>
|
|
124
|
+
<option
|
|
125
|
+
key={'druid_metrics_cube'}
|
|
126
|
+
value={'druid_metrics_cube'}
|
|
127
|
+
>
|
|
128
|
+
Druid Metrics Cube (Post-Agg Cube)
|
|
129
|
+
</option>
|
|
130
|
+
<option key={'spark_sql'} value={'spark_sql'}>
|
|
131
|
+
Iceberg Table
|
|
132
|
+
</option>
|
|
112
133
|
</>
|
|
113
134
|
</Field>
|
|
114
135
|
</span>
|
|
@@ -124,9 +145,15 @@ export default function AddMaterializationPopover({ node, onSubmit }) {
|
|
|
124
145
|
<label htmlFor="strategy">Strategy</label>
|
|
125
146
|
<Field as="select" name="strategy">
|
|
126
147
|
<>
|
|
127
|
-
{
|
|
128
|
-
|
|
129
|
-
|
|
148
|
+
<option key={'full'} value={'full'}>
|
|
149
|
+
Full
|
|
150
|
+
</option>
|
|
151
|
+
<option
|
|
152
|
+
key={'incremental_time'}
|
|
153
|
+
value={'incremental_time'}
|
|
154
|
+
>
|
|
155
|
+
Incremental Time
|
|
156
|
+
</option>
|
|
130
157
|
</>
|
|
131
158
|
</Field>
|
|
132
159
|
</span>
|
|
@@ -154,24 +181,20 @@ export default function AddMaterializationPopover({ node, onSubmit }) {
|
|
|
154
181
|
/>
|
|
155
182
|
</div>
|
|
156
183
|
<br />
|
|
157
|
-
<
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
name="config"
|
|
164
|
-
id="Config"
|
|
165
|
-
placeholder="Optional engine-specific configuration (i.e., Spark conf etc)"
|
|
166
|
-
/>
|
|
167
|
-
</div>
|
|
184
|
+
<ConfigField
|
|
185
|
+
value={{
|
|
186
|
+
'spark.executor.memory': '16g',
|
|
187
|
+
'spark.memory.fraction': '0.3',
|
|
188
|
+
}}
|
|
189
|
+
/>
|
|
168
190
|
<button
|
|
169
191
|
className="add_node"
|
|
170
192
|
type="submit"
|
|
171
193
|
aria-label="SaveEditColumn"
|
|
172
194
|
aria-hidden="false"
|
|
195
|
+
disabled={isSubmitting}
|
|
173
196
|
>
|
|
174
|
-
Save
|
|
197
|
+
{isSubmitting ? <LoadingIcon /> : 'Save'}
|
|
175
198
|
</button>
|
|
176
199
|
</Form>
|
|
177
200
|
);
|
|
@@ -0,0 +1,53 @@
|
|
|
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', val);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div className="DescriptionInput">
|
|
19
|
+
<ErrorMessage name="spark_config" component="span" />
|
|
20
|
+
<label htmlFor="SparkConfig">Spark Config</label>
|
|
21
|
+
<Field
|
|
22
|
+
type="textarea"
|
|
23
|
+
style={{ display: 'none' }}
|
|
24
|
+
as="textarea"
|
|
25
|
+
name="spark_config"
|
|
26
|
+
id="SparkConfig"
|
|
27
|
+
/>
|
|
28
|
+
<div role="button" tabIndex={0} className="relative flex bg-[#282a36]">
|
|
29
|
+
<CodeMirror
|
|
30
|
+
id={'spark_config'}
|
|
31
|
+
name={'spark_config'}
|
|
32
|
+
extensions={[jsonExt]}
|
|
33
|
+
value={JSON.stringify(value, null, ' ')}
|
|
34
|
+
options={{
|
|
35
|
+
theme: 'default',
|
|
36
|
+
lineNumbers: true,
|
|
37
|
+
}}
|
|
38
|
+
width="100%"
|
|
39
|
+
height="170px"
|
|
40
|
+
style={{
|
|
41
|
+
margin: '0 0 23px 0',
|
|
42
|
+
flex: 1,
|
|
43
|
+
fontSize: '150%',
|
|
44
|
+
textAlign: 'left',
|
|
45
|
+
}}
|
|
46
|
+
onChange={(value, viewUpdate) => {
|
|
47
|
+
updateFormik(value);
|
|
48
|
+
}}
|
|
49
|
+
/>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
};
|
|
@@ -15,6 +15,11 @@ 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 dimensionLinkForeignKeys = node.dimension_links
|
|
19
|
+
? node.dimension_links.flatMap(link =>
|
|
20
|
+
Object.keys(link.foreign_keys).map(key => key.split('.').slice(-1)),
|
|
21
|
+
)
|
|
22
|
+
: [];
|
|
18
23
|
const column_names = node.columns
|
|
19
24
|
.map(col => {
|
|
20
25
|
return {
|
|
@@ -23,7 +28,7 @@ const NodeLineage = djNode => {
|
|
|
23
28
|
dimension: col.dimension !== null ? col.dimension.name : null,
|
|
24
29
|
order: primary_key.includes(col.name)
|
|
25
30
|
? -1
|
|
26
|
-
: col.
|
|
31
|
+
: dimensionLinkForeignKeys.includes(col.name)
|
|
27
32
|
? 0
|
|
28
33
|
: 1,
|
|
29
34
|
};
|
|
@@ -49,28 +54,37 @@ const NodeLineage = djNode => {
|
|
|
49
54
|
};
|
|
50
55
|
|
|
51
56
|
const dimensionEdges = node => {
|
|
52
|
-
return node.
|
|
53
|
-
|
|
54
|
-
.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
+
});
|
|
74
88
|
};
|
|
75
89
|
|
|
76
90
|
const parentEdges = node => {
|
|
@@ -134,7 +134,7 @@ export default function NodeMaterializationTab({ node, djClient }) {
|
|
|
134
134
|
<div className="table-vertical">
|
|
135
135
|
<div>
|
|
136
136
|
<h2>Materializations</h2>
|
|
137
|
-
<AddMaterializationPopover node={node} />
|
|
137
|
+
{node ? <AddMaterializationPopover node={node} /> : <></>}
|
|
138
138
|
{materializations.length > 0 ? (
|
|
139
139
|
<table
|
|
140
140
|
className="card-inner-table table"
|
|
@@ -17,6 +17,9 @@ describe('<AddMaterializationPopover />', () => {
|
|
|
17
17
|
const onSubmitMock = jest.fn();
|
|
18
18
|
mockDjClient.DataJunctionAPI.materialize.mockReturnValue({
|
|
19
19
|
status: 201,
|
|
20
|
+
json: {
|
|
21
|
+
message: 'Saved!',
|
|
22
|
+
},
|
|
20
23
|
});
|
|
21
24
|
mockDjClient.DataJunctionAPI.materializationInfo.mockReturnValue({
|
|
22
25
|
status: 200,
|
|
@@ -73,6 +73,7 @@ describe('<NodeLineage />', () => {
|
|
|
73
73
|
parents: [],
|
|
74
74
|
created_at: '2023-08-21T16:48:52.970554+00:00',
|
|
75
75
|
tags: [],
|
|
76
|
+
dimension_links: [],
|
|
76
77
|
},
|
|
77
78
|
{
|
|
78
79
|
namespace: 'default',
|
|
@@ -98,6 +99,7 @@ describe('<NodeLineage />', () => {
|
|
|
98
99
|
query:
|
|
99
100
|
'\n SELECT\n dateint,\n month,\n year,\n day\n FROM default.date\n ',
|
|
100
101
|
availability: null,
|
|
102
|
+
dimension_links: [],
|
|
101
103
|
columns: [
|
|
102
104
|
{
|
|
103
105
|
name: 'dateint',
|
|
@@ -165,6 +167,7 @@ describe('<NodeLineage />', () => {
|
|
|
165
167
|
query:
|
|
166
168
|
'\n SELECT\n hard_hat_id,\n last_name,\n first_name,\n title,\n birth_date,\n hire_date,\n address,\n city,\n state,\n postal_code,\n country,\n manager,\n contractor_id\n FROM default.hard_hats\n ',
|
|
167
169
|
availability: null,
|
|
170
|
+
dimension_links: [],
|
|
168
171
|
columns: [
|
|
169
172
|
{
|
|
170
173
|
name: 'hard_hat_id',
|
|
@@ -292,6 +295,7 @@ describe('<NodeLineage />', () => {
|
|
|
292
295
|
query:
|
|
293
296
|
'SELECT avg(price) default_DOT_avg_repair_price \n FROM default.repair_order_details\n\n',
|
|
294
297
|
availability: null,
|
|
298
|
+
dimension_links: [],
|
|
295
299
|
columns: [
|
|
296
300
|
{
|
|
297
301
|
name: 'default_DOT_avg_repair_price',
|
package/src/styles/index.css
CHANGED
|
@@ -241,7 +241,7 @@ table {
|
|
|
241
241
|
}
|
|
242
242
|
tr {
|
|
243
243
|
display: table-row;
|
|
244
|
-
vertical-align:
|
|
244
|
+
vertical-align: top;
|
|
245
245
|
border-color: inherit;
|
|
246
246
|
}
|
|
247
247
|
.card-table {
|
|
@@ -339,7 +339,7 @@ tr {
|
|
|
339
339
|
.table thead th,
|
|
340
340
|
td,
|
|
341
341
|
tbody th {
|
|
342
|
-
vertical-align:
|
|
342
|
+
vertical-align: top;
|
|
343
343
|
text-align: left;
|
|
344
344
|
}
|
|
345
345
|
.table [data-sort],
|
|
@@ -371,6 +371,7 @@ tbody th {
|
|
|
371
371
|
border-bottom: 0;
|
|
372
372
|
padding: 1rem;
|
|
373
373
|
max-width: 25rem;
|
|
374
|
+
vertical-align: top;
|
|
374
375
|
}
|
|
375
376
|
.card-inner-table td,
|
|
376
377
|
.card-inner-table th {
|