datajunction-ui 0.0.1-a45.dev5 → 0.0.1-a46
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 +5 -0
- package/package.json +2 -2
- package/src/app/components/NodeMaterializationDelete.jsx +80 -0
- package/src/app/pages/CubeBuilderPage/index.jsx +19 -4
- package/src/app/pages/NamespacePage/index.jsx +2 -1
- package/src/app/pages/NodePage/AddBackfillPopover.jsx +46 -51
- package/src/app/pages/NodePage/AddMaterializationPopover.jsx +33 -24
- package/src/app/pages/NodePage/MaterializationConfigField.jsx +38 -31
- package/src/app/pages/NodePage/NodeHistory.jsx +1 -1
- package/src/app/pages/NodePage/NodeInfoTab.jsx +33 -0
- package/src/app/pages/NodePage/NodeMaterializationTab.jsx +179 -110
- package/src/app/pages/NodePage/NodeStatus.jsx +94 -21
- package/src/app/pages/NodePage/PartitionValueForm.jsx +60 -0
- package/src/app/pages/NodePage/RevisionDiff.jsx +1 -1
- package/src/app/pages/NodePage/__tests__/AddBackfillPopover.test.jsx +13 -3
- package/src/app/pages/NodePage/__tests__/NodePage.test.jsx +13 -5
- package/src/app/pages/NodePage/__tests__/RevisionDiff.test.jsx +32 -47
- package/src/app/pages/NodePage/__tests__/__snapshots__/NodePage.test.jsx.snap +276 -191
- package/src/app/pages/NodePage/index.jsx +1 -0
- package/src/app/services/DJService.js +37 -25
- package/src/app/services/__tests__/DJService.test.jsx +53 -21
- package/src/mocks/mockNodes.jsx +62 -7
- package/src/styles/index.css +128 -11
- package/src/styles/node-creation.scss +9 -0
- package/src/styles/node-list.css +4 -0
- package/src/app/components/forms/NodeTagsInput.jsx +0 -61
package/Makefile
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "datajunction-ui",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.1a46",
|
|
4
4
|
"description": "DataJunction Metrics Platform UI",
|
|
5
5
|
"module": "src/index.tsx",
|
|
6
6
|
"repository": {
|
|
@@ -166,7 +166,7 @@
|
|
|
166
166
|
"global": {
|
|
167
167
|
"statements": 89,
|
|
168
168
|
"branches": 75,
|
|
169
|
-
"lines":
|
|
169
|
+
"lines": 80,
|
|
170
170
|
"functions": 85
|
|
171
171
|
}
|
|
172
172
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import DJClientContext from '../providers/djclient';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import DeleteIcon from '../icons/DeleteIcon';
|
|
4
|
+
import { Form, Formik } from 'formik';
|
|
5
|
+
import { useContext } from 'react';
|
|
6
|
+
import { displayMessageAfterSubmit } from '../../utils/form';
|
|
7
|
+
|
|
8
|
+
export default function NodeMaterializationDelete({
|
|
9
|
+
nodeName,
|
|
10
|
+
materializationName,
|
|
11
|
+
}) {
|
|
12
|
+
const [deleteButton, setDeleteButton] = React.useState(<DeleteIcon />);
|
|
13
|
+
|
|
14
|
+
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
15
|
+
const deleteNode = async (values, { setStatus }) => {
|
|
16
|
+
if (
|
|
17
|
+
!window.confirm(
|
|
18
|
+
'Deleting materialization job ' +
|
|
19
|
+
values.materializationName +
|
|
20
|
+
'. Are you sure?',
|
|
21
|
+
)
|
|
22
|
+
) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const { status, json } = await djClient.deleteMaterialization(
|
|
26
|
+
values.nodeName,
|
|
27
|
+
values.materializationName,
|
|
28
|
+
);
|
|
29
|
+
if (status === 200 || status === 201 || status === 204) {
|
|
30
|
+
window.location.reload();
|
|
31
|
+
setStatus({
|
|
32
|
+
success: (
|
|
33
|
+
<>
|
|
34
|
+
Successfully deleted materialization job:{' '}
|
|
35
|
+
{values.materializationName}
|
|
36
|
+
</>
|
|
37
|
+
),
|
|
38
|
+
});
|
|
39
|
+
setDeleteButton(''); // hide the Delete button
|
|
40
|
+
} else {
|
|
41
|
+
setStatus({
|
|
42
|
+
failure: `${json.message}`,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const initialValues = {
|
|
48
|
+
nodeName: nodeName,
|
|
49
|
+
materializationName: materializationName,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div>
|
|
54
|
+
<Formik initialValues={initialValues} onSubmit={deleteNode}>
|
|
55
|
+
{function Render({ status, setFieldValue }) {
|
|
56
|
+
return (
|
|
57
|
+
<Form className="deleteNode">
|
|
58
|
+
{displayMessageAfterSubmit(status)}
|
|
59
|
+
{
|
|
60
|
+
<>
|
|
61
|
+
<button
|
|
62
|
+
type="submit"
|
|
63
|
+
style={{
|
|
64
|
+
marginLeft: 0,
|
|
65
|
+
all: 'unset',
|
|
66
|
+
color: '#005c72',
|
|
67
|
+
cursor: 'pointer',
|
|
68
|
+
}}
|
|
69
|
+
>
|
|
70
|
+
{deleteButton}
|
|
71
|
+
</button>
|
|
72
|
+
</>
|
|
73
|
+
}
|
|
74
|
+
</Form>
|
|
75
|
+
);
|
|
76
|
+
}}
|
|
77
|
+
</Formik>
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
@@ -9,9 +9,9 @@ import { displayMessageAfterSubmit } from '../../../utils/form';
|
|
|
9
9
|
import { useParams } from 'react-router-dom';
|
|
10
10
|
import { Action } from '../../components/forms/Action';
|
|
11
11
|
import NodeNameField from '../../components/forms/NodeNameField';
|
|
12
|
-
import NodeTagsInput from '../../components/forms/NodeTagsInput';
|
|
13
12
|
import { MetricsSelect } from './MetricsSelect';
|
|
14
13
|
import { DimensionsSelect } from './DimensionsSelect';
|
|
14
|
+
import { TagsField } from '../AddEditNodePage/TagsField';
|
|
15
15
|
|
|
16
16
|
export function CubeBuilderPage() {
|
|
17
17
|
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
@@ -29,6 +29,7 @@ export function CubeBuilderPage() {
|
|
|
29
29
|
metrics: [],
|
|
30
30
|
dimensions: [],
|
|
31
31
|
filters: [],
|
|
32
|
+
tags: [],
|
|
32
33
|
};
|
|
33
34
|
|
|
34
35
|
const handleSubmit = (values, { setSubmitting, setStatus }) => {
|
|
@@ -105,10 +106,23 @@ export function CubeBuilderPage() {
|
|
|
105
106
|
}
|
|
106
107
|
};
|
|
107
108
|
|
|
108
|
-
const updateFieldsWithNodeData = (data, setFieldValue) => {
|
|
109
|
+
const updateFieldsWithNodeData = (data, setFieldValue, setSelectTags) => {
|
|
109
110
|
setFieldValue('display_name', data.display_name || '', false);
|
|
110
111
|
setFieldValue('description', data.description || '', false);
|
|
111
112
|
setFieldValue('mode', data.mode || 'draft', false);
|
|
113
|
+
setFieldValue(
|
|
114
|
+
'tags',
|
|
115
|
+
data.tags.map(tag => tag.name),
|
|
116
|
+
);
|
|
117
|
+
// For react-select fields, we have to explicitly set the entire
|
|
118
|
+
// field rather than just the values
|
|
119
|
+
setSelectTags(
|
|
120
|
+
<TagsField
|
|
121
|
+
defaultValue={data.tags.map(t => {
|
|
122
|
+
return { value: t.name, label: t.display_name };
|
|
123
|
+
})}
|
|
124
|
+
/>,
|
|
125
|
+
);
|
|
112
126
|
};
|
|
113
127
|
|
|
114
128
|
const staticFieldsInEdit = () => (
|
|
@@ -144,14 +158,15 @@ export function CubeBuilderPage() {
|
|
|
144
158
|
>
|
|
145
159
|
{function Render({ isSubmitting, status, setFieldValue, props }) {
|
|
146
160
|
const [node, setNode] = useState([]);
|
|
161
|
+
const [selectTags, setSelectTags] = useState(null);
|
|
147
162
|
|
|
148
163
|
// Get cube
|
|
149
164
|
useEffect(() => {
|
|
150
165
|
const fetchData = async () => {
|
|
151
166
|
if (name) {
|
|
152
167
|
const cube = await djClient.cube(name);
|
|
153
|
-
updateFieldsWithNodeData(cube, setFieldValue);
|
|
154
168
|
setNode(cube);
|
|
169
|
+
updateFieldsWithNodeData(cube, setFieldValue, setSelectTags);
|
|
155
170
|
}
|
|
156
171
|
};
|
|
157
172
|
fetchData().catch(console.error);
|
|
@@ -227,7 +242,7 @@ export function CubeBuilderPage() {
|
|
|
227
242
|
<option value="published">Published</option>
|
|
228
243
|
</Field>
|
|
229
244
|
</div>
|
|
230
|
-
|
|
245
|
+
{action === Action.Edit ? selectTags : <TagsField />}
|
|
231
246
|
<button
|
|
232
247
|
type="submit"
|
|
233
248
|
disabled={isSubmitting}
|
|
@@ -6,6 +6,7 @@ import DJClientContext from '../../providers/djclient';
|
|
|
6
6
|
import Explorer from '../NamespacePage/Explorer';
|
|
7
7
|
import NodeListActions from '../../components/NodeListActions';
|
|
8
8
|
import AddNamespacePopover from './AddNamespacePopover';
|
|
9
|
+
import 'styles/node-list.css';
|
|
9
10
|
|
|
10
11
|
export function NamespacePage() {
|
|
11
12
|
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
@@ -94,7 +95,7 @@ export function NamespacePage() {
|
|
|
94
95
|
</span>
|
|
95
96
|
</td>
|
|
96
97
|
<td>
|
|
97
|
-
<NodeStatus node={node} />
|
|
98
|
+
<NodeStatus node={node} revalidate={false} />
|
|
98
99
|
</td>
|
|
99
100
|
<td>
|
|
100
101
|
<span className="status">{node.mode}</span>
|
|
@@ -3,6 +3,8 @@ import * as React from 'react';
|
|
|
3
3
|
import DJClientContext from '../../providers/djclient';
|
|
4
4
|
import { Field, Form, Formik } from 'formik';
|
|
5
5
|
import { displayMessageAfterSubmit } from '../../../utils/form';
|
|
6
|
+
import PartitionValueForm from './PartitionValueForm';
|
|
7
|
+
import LoadingIcon from '../../icons/LoadingIcon';
|
|
6
8
|
|
|
7
9
|
export default function AddBackfillPopover({
|
|
8
10
|
node,
|
|
@@ -26,30 +28,39 @@ export default function AddBackfillPopover({
|
|
|
26
28
|
}, [setPopoverAnchor]);
|
|
27
29
|
|
|
28
30
|
const partitionColumns = node.columns.filter(col => col.partition !== null);
|
|
29
|
-
|
|
30
|
-
const temporalPartitionColumns = partitionColumns.filter(
|
|
31
|
-
col => col.partition.type_ === 'temporal',
|
|
32
|
-
);
|
|
33
|
-
|
|
34
31
|
const initialValues = {
|
|
35
32
|
node: node.name,
|
|
36
33
|
materializationName: materialization.name,
|
|
37
|
-
|
|
38
|
-
temporalPartitionColumns.length > 0
|
|
39
|
-
? temporalPartitionColumns[0].name
|
|
40
|
-
: '',
|
|
41
|
-
from: '',
|
|
42
|
-
to: '',
|
|
34
|
+
partitionValues: {},
|
|
43
35
|
};
|
|
44
36
|
|
|
45
|
-
|
|
46
|
-
|
|
37
|
+
for (const partitionCol of partitionColumns) {
|
|
38
|
+
if (partitionCol.partition.type_ === 'temporal') {
|
|
39
|
+
initialValues.partitionValues[partitionCol.name] = {
|
|
40
|
+
from: '',
|
|
41
|
+
to: '',
|
|
42
|
+
};
|
|
43
|
+
} else {
|
|
44
|
+
initialValues.partitionValues[partitionCol.name] = '';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const runBackfill = async (values, setStatus) => {
|
|
47
49
|
const response = await djClient.runBackfill(
|
|
48
50
|
values.node,
|
|
49
51
|
values.materializationName,
|
|
50
|
-
values.
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
Object.entries(values.partitionValues).map(entry => {
|
|
53
|
+
if (typeof entry[1] === 'object' && entry[1] !== null) {
|
|
54
|
+
return {
|
|
55
|
+
columnName: entry[0],
|
|
56
|
+
range: [entry[1].from, entry[1].to],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
columnName: entry[0],
|
|
61
|
+
values: [entry[1]],
|
|
62
|
+
};
|
|
63
|
+
}),
|
|
53
64
|
);
|
|
54
65
|
if (response.status === 200 || response.status === 201) {
|
|
55
66
|
setStatus({ success: 'Saved!' });
|
|
@@ -58,21 +69,26 @@ export default function AddBackfillPopover({
|
|
|
58
69
|
failure: `${response.json.message}`,
|
|
59
70
|
});
|
|
60
71
|
}
|
|
61
|
-
|
|
62
|
-
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const submitBackfill = async (values, { setSubmitting, setStatus }) => {
|
|
75
|
+
await runBackfill(values, setStatus).then(_ => {
|
|
76
|
+
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
|
|
77
|
+
setSubmitting(false);
|
|
78
|
+
});
|
|
63
79
|
};
|
|
64
80
|
|
|
65
81
|
return (
|
|
66
82
|
<>
|
|
67
83
|
<button
|
|
68
|
-
className="edit_button"
|
|
84
|
+
className="edit_button add_button"
|
|
69
85
|
aria-label="AddBackfill"
|
|
70
86
|
tabIndex="0"
|
|
71
87
|
onClick={() => {
|
|
72
88
|
setPopoverAnchor(!popoverAnchor);
|
|
73
89
|
}}
|
|
74
90
|
>
|
|
75
|
-
<span className="
|
|
91
|
+
<span className="add_button">+ Run</span>
|
|
76
92
|
</button>
|
|
77
93
|
<div
|
|
78
94
|
className="fade modal-backdrop in"
|
|
@@ -85,10 +101,12 @@ export default function AddBackfillPopover({
|
|
|
85
101
|
style={{
|
|
86
102
|
display: popoverAnchor === false ? 'none' : 'block',
|
|
87
103
|
width: '50%',
|
|
104
|
+
minWidth: '800px',
|
|
105
|
+
left: '-25%',
|
|
88
106
|
}}
|
|
89
107
|
ref={ref}
|
|
90
108
|
>
|
|
91
|
-
<Formik initialValues={initialValues} onSubmit={
|
|
109
|
+
<Formik initialValues={initialValues} onSubmit={submitBackfill}>
|
|
92
110
|
{function Render({ isSubmitting, status, setFieldValue }) {
|
|
93
111
|
return (
|
|
94
112
|
<Form>
|
|
@@ -115,41 +133,17 @@ export default function AddBackfillPopover({
|
|
|
115
133
|
<br />
|
|
116
134
|
<br />
|
|
117
135
|
<label htmlFor="partition" style={{ paddingBottom: '1rem' }}>
|
|
118
|
-
Partition
|
|
136
|
+
Partition
|
|
119
137
|
</label>
|
|
120
138
|
{node.columns
|
|
121
139
|
.filter(col => col.partition !== null)
|
|
122
140
|
.map(col => {
|
|
123
141
|
return (
|
|
124
|
-
<
|
|
125
|
-
|
|
142
|
+
<PartitionValueForm
|
|
143
|
+
col={col}
|
|
144
|
+
materialization={materialization}
|
|
126
145
|
key={col.name}
|
|
127
|
-
|
|
128
|
-
>
|
|
129
|
-
<div className="partition__header">
|
|
130
|
-
{col.display_name}
|
|
131
|
-
</div>
|
|
132
|
-
<div className="partition__body">
|
|
133
|
-
<span style={{ padding: '0.5rem' }}>From</span>{' '}
|
|
134
|
-
<Field
|
|
135
|
-
type="text"
|
|
136
|
-
name="from"
|
|
137
|
-
id={`${col.name}__from`}
|
|
138
|
-
placeholder="20230101"
|
|
139
|
-
default="20230101"
|
|
140
|
-
style={{ width: '7rem', paddingRight: '1rem' }}
|
|
141
|
-
/>{' '}
|
|
142
|
-
<span style={{ padding: '0.5rem' }}>To</span>
|
|
143
|
-
<Field
|
|
144
|
-
type="text"
|
|
145
|
-
name="to"
|
|
146
|
-
id={`${col.name}__to`}
|
|
147
|
-
placeholder="20230102"
|
|
148
|
-
default="20230102"
|
|
149
|
-
style={{ width: '7rem' }}
|
|
150
|
-
/>
|
|
151
|
-
</div>
|
|
152
|
-
</div>
|
|
146
|
+
/>
|
|
153
147
|
);
|
|
154
148
|
})}
|
|
155
149
|
<br />
|
|
@@ -158,8 +152,9 @@ export default function AddBackfillPopover({
|
|
|
158
152
|
type="submit"
|
|
159
153
|
aria-label="SaveEditColumn"
|
|
160
154
|
aria-hidden="false"
|
|
155
|
+
disabled={isSubmitting}
|
|
161
156
|
>
|
|
162
|
-
Save
|
|
157
|
+
{isSubmitting ? <LoadingIcon /> : 'Save'}
|
|
163
158
|
</button>
|
|
164
159
|
</Form>
|
|
165
160
|
);
|
|
@@ -39,6 +39,9 @@ export default function AddMaterializationPopover({ node, onSubmit }) {
|
|
|
39
39
|
const config = {};
|
|
40
40
|
config.spark = values.spark_config;
|
|
41
41
|
config.lookback_window = values.lookback_window;
|
|
42
|
+
if (!values.job_type) {
|
|
43
|
+
values.job_type = 'spark_sql';
|
|
44
|
+
}
|
|
42
45
|
const { status, json } = await djClient.materialize(
|
|
43
46
|
values.node,
|
|
44
47
|
values.job_type,
|
|
@@ -48,6 +51,7 @@ export default function AddMaterializationPopover({ node, onSubmit }) {
|
|
|
48
51
|
);
|
|
49
52
|
if (status === 200 || status === 201) {
|
|
50
53
|
setStatus({ success: json.message });
|
|
54
|
+
window.location.reload();
|
|
51
55
|
} else {
|
|
52
56
|
setStatus({
|
|
53
57
|
failure: `${json.message}`,
|
|
@@ -111,36 +115,41 @@ export default function AddMaterializationPopover({ node, onSubmit }) {
|
|
|
111
115
|
<Form>
|
|
112
116
|
<h2>Configure Materialization</h2>
|
|
113
117
|
{displayMessageAfterSubmit(status)}
|
|
114
|
-
|
|
115
|
-
<
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
118
|
+
{node.type === 'cube' ? (
|
|
119
|
+
<span data-testid="job-type">
|
|
120
|
+
<label htmlFor="job_type">Job Type</label>
|
|
121
|
+
|
|
122
|
+
<Field as="select" name="job_type">
|
|
123
|
+
<>
|
|
124
|
+
<option
|
|
125
|
+
key={'druid_measures_cube'}
|
|
126
|
+
value={'druid_measures_cube'}
|
|
127
|
+
>
|
|
128
|
+
Druid Measures Cube (Pre-Agg Cube)
|
|
129
|
+
</option>
|
|
130
|
+
<option
|
|
131
|
+
key={'druid_metrics_cube'}
|
|
132
|
+
value={'druid_metrics_cube'}
|
|
133
|
+
>
|
|
134
|
+
Druid Metrics Cube (Post-Agg Cube)
|
|
135
|
+
</option>
|
|
136
|
+
<option key={'spark_sql'} value={'spark_sql'}>
|
|
137
|
+
Iceberg Table
|
|
138
|
+
</option>
|
|
139
|
+
</>
|
|
140
|
+
</Field>
|
|
141
|
+
<br />
|
|
142
|
+
<br />
|
|
143
|
+
</span>
|
|
144
|
+
) : (
|
|
145
|
+
''
|
|
146
|
+
)}
|
|
136
147
|
<input
|
|
137
148
|
hidden={true}
|
|
138
149
|
name="node"
|
|
139
150
|
value={node?.name}
|
|
140
151
|
readOnly={true}
|
|
141
152
|
/>
|
|
142
|
-
<br />
|
|
143
|
-
<br />
|
|
144
153
|
<span data-testid="edit-partition">
|
|
145
154
|
<label htmlFor="strategy">Strategy</label>
|
|
146
155
|
<Field as="select" name="strategy">
|
|
@@ -16,38 +16,45 @@ export const ConfigField = ({ djClient, value }) => {
|
|
|
16
16
|
|
|
17
17
|
return (
|
|
18
18
|
<div className="DescriptionInput">
|
|
19
|
-
<
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
name="spark_config"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
}}
|
|
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"
|
|
49
32
|
/>
|
|
50
|
-
|
|
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
|
+
<></>
|
|
51
58
|
</div>
|
|
52
59
|
);
|
|
53
60
|
};
|
|
@@ -11,7 +11,7 @@ export default function NodeHistory({ node, djClient }) {
|
|
|
11
11
|
const fetchData = async () => {
|
|
12
12
|
if (node) {
|
|
13
13
|
const data = await djClient.history('node', node.name);
|
|
14
|
-
setHistory(data
|
|
14
|
+
setHistory(data);
|
|
15
15
|
}
|
|
16
16
|
};
|
|
17
17
|
fetchData().catch(console.error);
|
|
@@ -7,6 +7,9 @@ import ListGroupItem from '../../components/ListGroupItem';
|
|
|
7
7
|
import ToggleSwitch from '../../components/ToggleSwitch';
|
|
8
8
|
import DJClientContext from '../../providers/djclient';
|
|
9
9
|
import { labelize } from '../../../utils/form';
|
|
10
|
+
import { AlertMessage } from '../AddEditNodePage/AlertMessage';
|
|
11
|
+
import AlertIcon from '../../icons/AlertIcon';
|
|
12
|
+
import InvalidIcon from '../../icons/InvalidIcon';
|
|
10
13
|
|
|
11
14
|
SyntaxHighlighter.registerLanguage('sql', sql);
|
|
12
15
|
foundation.hljs['padding'] = '2rem';
|
|
@@ -39,6 +42,35 @@ export default function NodeInfoTab({ node }) {
|
|
|
39
42
|
function toggle(value) {
|
|
40
43
|
return !value;
|
|
41
44
|
}
|
|
45
|
+
const metricsWarning =
|
|
46
|
+
node?.type === 'metric' && node?.incompatible_druid_functions.length > 0 ? (
|
|
47
|
+
<div className="message warning" style={{ marginTop: '0.7rem' }}>
|
|
48
|
+
⚠{' '}
|
|
49
|
+
<small>
|
|
50
|
+
The following functions used in the metric definition may not be
|
|
51
|
+
compatible with Druid SQL:{' '}
|
|
52
|
+
{node?.incompatible_druid_functions.map(func => (
|
|
53
|
+
<li style={{ listStyleType: 'none', margin: '0.7rem 0.7rem' }}>
|
|
54
|
+
⇢{' '}
|
|
55
|
+
<span style={{ background: '#fff', padding: '0.3rem' }}>
|
|
56
|
+
{func}
|
|
57
|
+
</span>
|
|
58
|
+
</li>
|
|
59
|
+
))}
|
|
60
|
+
If you need your metrics to be compatible with Druid, please use{' '}
|
|
61
|
+
<a
|
|
62
|
+
href={
|
|
63
|
+
'https://druid.apache.org/docs/latest/querying/sql-functions/'
|
|
64
|
+
}
|
|
65
|
+
>
|
|
66
|
+
these supported functions
|
|
67
|
+
</a>
|
|
68
|
+
.
|
|
69
|
+
</small>
|
|
70
|
+
</div>
|
|
71
|
+
) : (
|
|
72
|
+
''
|
|
73
|
+
);
|
|
42
74
|
const metricQueryDiv = (
|
|
43
75
|
<div className="list-group-item d-flex">
|
|
44
76
|
<div className="gap-2 w-100 justify-content-between py-3">
|
|
@@ -205,6 +237,7 @@ export default function NodeInfoTab({ node }) {
|
|
|
205
237
|
className="list-group align-items-center justify-content-between flex-md-row gap-2"
|
|
206
238
|
style={{ minWidth: '700px' }}
|
|
207
239
|
>
|
|
240
|
+
{node?.type === 'metric' ? metricsWarning : ''}
|
|
208
241
|
<ListGroupItem label="Description" value={node?.description} />
|
|
209
242
|
<div className="list-group-item d-flex">
|
|
210
243
|
<div className="d-flex gap-2 w-100 justify-content-between py-3">
|