datajunction-ui 0.0.1-rc.17 → 0.0.1-rc.19
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/.env +1 -1
- package/dj-logo.svg +10 -0
- package/package.json +28 -6
- package/src/app/__tests__/__snapshots__/index.test.tsx.snap +5 -84
- package/src/app/components/djgraph/DJNode.jsx +1 -1
- package/src/app/components/djgraph/LayoutFlow.jsx +1 -1
- package/src/app/constants.js +2 -0
- package/src/app/icons/AlertIcon.jsx +32 -0
- package/src/app/icons/DJLogo.jsx +36 -0
- package/src/app/icons/DeleteIcon.jsx +21 -0
- package/src/app/icons/EditIcon.jsx +18 -0
- package/src/app/index.tsx +70 -36
- package/src/app/pages/AddEditNodePage/FormikSelect.jsx +33 -0
- package/src/app/pages/AddEditNodePage/FullNameField.jsx +36 -0
- package/src/app/pages/AddEditNodePage/Loadable.jsx +16 -0
- package/src/app/pages/AddEditNodePage/NodeQueryField.jsx +89 -0
- package/src/app/pages/AddEditNodePage/__tests__/FormikSelect.test.jsx +44 -0
- package/src/app/pages/AddEditNodePage/__tests__/FullNameField.test.jsx +29 -0
- package/src/app/pages/AddEditNodePage/__tests__/NodeQueryField.test.jsx +30 -0
- package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/index.test.jsx.snap +84 -0
- package/src/app/pages/AddEditNodePage/__tests__/index.test.jsx +353 -0
- package/src/app/pages/AddEditNodePage/index.jsx +349 -0
- package/src/app/pages/LoginPage/assets/sign-in-with-github.png +0 -0
- package/src/app/pages/LoginPage/assets/sign-in-with-google.png +0 -0
- package/src/app/pages/LoginPage/index.jsx +90 -0
- package/src/app/pages/NamespacePage/__tests__/__snapshots__/index.test.tsx.snap +50 -2
- package/src/app/pages/NamespacePage/index.jsx +38 -9
- package/src/app/pages/NodePage/NodeGraphTab.jsx +1 -2
- package/src/app/pages/NodePage/NodeHistory.jsx +0 -1
- package/src/app/pages/NodePage/index.jsx +43 -26
- package/src/app/pages/Root/index.tsx +20 -3
- package/src/app/pages/SQLBuilderPage/index.jsx +54 -8
- package/src/app/services/DJService.js +180 -32
- package/src/setupTests.ts +1 -1
- package/src/styles/index.css +82 -5
- package/src/styles/login.css +67 -0
- package/src/styles/node-creation.scss +190 -0
- package/src/styles/styles.scss +44 -0
- package/src/styles/styles.scss.d.ts +9 -0
- package/webpack.config.js +11 -1
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node add + edit page for transforms, metrics, and dimensions. The creation and edit flow for these
|
|
3
|
+
* node types is largely the same, with minor differences handled server-side. For the `query`
|
|
4
|
+
* field, this page will render a CodeMirror SQL editor with autocompletion and syntax highlighting.
|
|
5
|
+
*/
|
|
6
|
+
import { ErrorMessage, Field, Form, Formik } from 'formik';
|
|
7
|
+
|
|
8
|
+
import NamespaceHeader from '../../components/NamespaceHeader';
|
|
9
|
+
import { useContext, useEffect, useState } from 'react';
|
|
10
|
+
import DJClientContext from '../../providers/djclient';
|
|
11
|
+
import 'styles/node-creation.scss';
|
|
12
|
+
import AlertIcon from '../../icons/AlertIcon';
|
|
13
|
+
import ValidIcon from '../../icons/ValidIcon';
|
|
14
|
+
import { useParams } from 'react-router-dom';
|
|
15
|
+
import { FullNameField } from './FullNameField';
|
|
16
|
+
import { FormikSelect } from './FormikSelect';
|
|
17
|
+
import { NodeQueryField } from './NodeQueryField';
|
|
18
|
+
|
|
19
|
+
class Action {
|
|
20
|
+
static Add = new Action('add');
|
|
21
|
+
static Edit = new Action('edit');
|
|
22
|
+
|
|
23
|
+
constructor(name) {
|
|
24
|
+
this.name = name;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function AddEditNodePage() {
|
|
29
|
+
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
30
|
+
|
|
31
|
+
let { nodeType, initialNamespace, name } = useParams();
|
|
32
|
+
const action = name !== undefined ? Action.Edit : Action.Add;
|
|
33
|
+
|
|
34
|
+
const [namespaces, setNamespaces] = useState([]);
|
|
35
|
+
|
|
36
|
+
const initialValues = {
|
|
37
|
+
name: action === Action.Edit ? name : '',
|
|
38
|
+
namespace: action === Action.Add ? initialNamespace : '',
|
|
39
|
+
display_name: '',
|
|
40
|
+
query: '',
|
|
41
|
+
node_type: '',
|
|
42
|
+
description: '',
|
|
43
|
+
primary_key: '',
|
|
44
|
+
mode: 'draft',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const validator = values => {
|
|
48
|
+
const errors = {};
|
|
49
|
+
if (!values.name) {
|
|
50
|
+
errors.name = 'Required';
|
|
51
|
+
}
|
|
52
|
+
if (!values.query) {
|
|
53
|
+
errors.query = 'Required';
|
|
54
|
+
}
|
|
55
|
+
return errors;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const handleSubmit = (values, { setSubmitting, setStatus }) => {
|
|
59
|
+
if (action === Action.Add) {
|
|
60
|
+
setTimeout(() => {
|
|
61
|
+
createNode(values, setStatus);
|
|
62
|
+
setSubmitting(false);
|
|
63
|
+
}, 400);
|
|
64
|
+
} else {
|
|
65
|
+
setTimeout(() => {
|
|
66
|
+
patchNode(values, setStatus);
|
|
67
|
+
setSubmitting(false);
|
|
68
|
+
}, 400);
|
|
69
|
+
}
|
|
70
|
+
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const displayMessageAfterSubmit = status => {
|
|
74
|
+
return status?.success !== undefined ? (
|
|
75
|
+
<div className="message success">
|
|
76
|
+
<ValidIcon />
|
|
77
|
+
{status?.success}
|
|
78
|
+
</div>
|
|
79
|
+
) : status?.failure !== undefined ? (
|
|
80
|
+
<div className="message alert">
|
|
81
|
+
<AlertIcon />
|
|
82
|
+
{status?.failure}
|
|
83
|
+
</div>
|
|
84
|
+
) : (
|
|
85
|
+
''
|
|
86
|
+
);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const pageTitle =
|
|
90
|
+
action === Action.Add ? (
|
|
91
|
+
<h2>
|
|
92
|
+
Create{' '}
|
|
93
|
+
<span className={`node_type__${nodeType} node_type_creation_heading`}>
|
|
94
|
+
{nodeType}
|
|
95
|
+
</span>
|
|
96
|
+
</h2>
|
|
97
|
+
) : (
|
|
98
|
+
<h2>Edit</h2>
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const staticFieldsInEdit = node => (
|
|
102
|
+
<>
|
|
103
|
+
<div className="NodeNameInput NodeCreationInput">
|
|
104
|
+
<label htmlFor="name">Name</label> {name}
|
|
105
|
+
</div>
|
|
106
|
+
<div className="NodeNameInput NodeCreationInput">
|
|
107
|
+
<label htmlFor="name">Type</label> {node.type}
|
|
108
|
+
</div>
|
|
109
|
+
</>
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const createNode = async (values, setStatus) => {
|
|
113
|
+
const { status, json } = await djClient.createNode(
|
|
114
|
+
nodeType,
|
|
115
|
+
values.name,
|
|
116
|
+
values.display_name,
|
|
117
|
+
values.description,
|
|
118
|
+
values.query,
|
|
119
|
+
values.mode,
|
|
120
|
+
values.namespace,
|
|
121
|
+
values.primary_key ? values.primary_key.split(',') : null,
|
|
122
|
+
);
|
|
123
|
+
if (status === 200 || status === 201) {
|
|
124
|
+
setStatus({
|
|
125
|
+
success: (
|
|
126
|
+
<>
|
|
127
|
+
Successfully created {json.type} node{' '}
|
|
128
|
+
<a href={`/nodes/${json.name}`}>{json.name}</a>!
|
|
129
|
+
</>
|
|
130
|
+
),
|
|
131
|
+
});
|
|
132
|
+
} else {
|
|
133
|
+
setStatus({
|
|
134
|
+
failure: `${json.message}`,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const patchNode = async (values, setStatus) => {
|
|
140
|
+
const { status, json } = await djClient.patchNode(
|
|
141
|
+
values.name,
|
|
142
|
+
values.display_name,
|
|
143
|
+
values.description,
|
|
144
|
+
values.query,
|
|
145
|
+
values.mode,
|
|
146
|
+
values.primary_key ? values.primary_key.split(',') : null,
|
|
147
|
+
);
|
|
148
|
+
if (status === 200 || status === 201) {
|
|
149
|
+
setStatus({
|
|
150
|
+
success: (
|
|
151
|
+
<>
|
|
152
|
+
Successfully updated {json.type} node{' '}
|
|
153
|
+
<a href={`/nodes/${json.name}`}>{json.name}</a>!
|
|
154
|
+
</>
|
|
155
|
+
),
|
|
156
|
+
});
|
|
157
|
+
} else {
|
|
158
|
+
setStatus({
|
|
159
|
+
failure: `${json.message}`,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const namespaceInput = (
|
|
165
|
+
<div className="NamespaceInput">
|
|
166
|
+
<ErrorMessage name="namespace" component="span" />
|
|
167
|
+
<label htmlFor="react-select-3-input">Namespace</label>
|
|
168
|
+
<FormikSelect
|
|
169
|
+
selectOptions={namespaces}
|
|
170
|
+
formikFieldName="namespace"
|
|
171
|
+
placeholder="Choose Namespace"
|
|
172
|
+
defaultValue={{
|
|
173
|
+
value: initialNamespace,
|
|
174
|
+
label: initialNamespace,
|
|
175
|
+
}}
|
|
176
|
+
/>
|
|
177
|
+
</div>
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
const fullNameInput = (
|
|
181
|
+
<div className="FullNameInput NodeCreationInput">
|
|
182
|
+
<ErrorMessage name="name" component="span" />
|
|
183
|
+
<label htmlFor="FullName">Full Name</label>
|
|
184
|
+
<FullNameField type="text" name="name" />
|
|
185
|
+
</div>
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
const nodeCanBeEdited = nodeType => {
|
|
189
|
+
return new Set(['transform', 'metric', 'dimension']).has(nodeType);
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const updateFieldsWithNodeData = (data, setFieldValue) => {
|
|
193
|
+
const fields = [
|
|
194
|
+
'display_name',
|
|
195
|
+
'query',
|
|
196
|
+
'type',
|
|
197
|
+
'description',
|
|
198
|
+
'primary_key',
|
|
199
|
+
'mode',
|
|
200
|
+
];
|
|
201
|
+
fields.forEach(field => {
|
|
202
|
+
if (field === 'primary_key' && data[field] !== undefined) {
|
|
203
|
+
data[field] = data[field].join(',');
|
|
204
|
+
}
|
|
205
|
+
setFieldValue(field, data[field], false);
|
|
206
|
+
});
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const alertMessage = message => {
|
|
210
|
+
return (
|
|
211
|
+
<div className="message alert">
|
|
212
|
+
<AlertIcon />
|
|
213
|
+
{message}
|
|
214
|
+
</div>
|
|
215
|
+
);
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
// Get namespaces, only necessary when creating a node
|
|
219
|
+
useEffect(() => {
|
|
220
|
+
if (action === Action.Add) {
|
|
221
|
+
const fetchData = async () => {
|
|
222
|
+
const namespaces = await djClient.namespaces();
|
|
223
|
+
setNamespaces(
|
|
224
|
+
namespaces.map(m => ({
|
|
225
|
+
value: m['namespace'],
|
|
226
|
+
label: m['namespace'],
|
|
227
|
+
})),
|
|
228
|
+
);
|
|
229
|
+
};
|
|
230
|
+
fetchData().catch(console.error);
|
|
231
|
+
}
|
|
232
|
+
}, [action, djClient, djClient.metrics]);
|
|
233
|
+
|
|
234
|
+
return (
|
|
235
|
+
<div className="mid">
|
|
236
|
+
<NamespaceHeader namespace="" />
|
|
237
|
+
<div className="card">
|
|
238
|
+
<div className="card-header">
|
|
239
|
+
{pageTitle}
|
|
240
|
+
<center>
|
|
241
|
+
<Formik
|
|
242
|
+
initialValues={initialValues}
|
|
243
|
+
validate={validator}
|
|
244
|
+
onSubmit={handleSubmit}
|
|
245
|
+
>
|
|
246
|
+
{function Render({ isSubmitting, status, setFieldValue }) {
|
|
247
|
+
const [node, setNode] = useState([]);
|
|
248
|
+
const [message, setMessage] = useState('');
|
|
249
|
+
useEffect(() => {
|
|
250
|
+
const fetchData = async () => {
|
|
251
|
+
if (action === Action.Edit) {
|
|
252
|
+
const data = await djClient.node(name);
|
|
253
|
+
|
|
254
|
+
// Check if node exists
|
|
255
|
+
if (data.message !== undefined) {
|
|
256
|
+
setNode(null);
|
|
257
|
+
setMessage(`Node ${name} does not exist!`);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Check if node type can be edited
|
|
262
|
+
if (!nodeCanBeEdited(data.type)) {
|
|
263
|
+
setNode(null);
|
|
264
|
+
setMessage(
|
|
265
|
+
`Node ${name} is of type ${data.type} and cannot be edited`,
|
|
266
|
+
);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Update fields with existing data to prepare for edit
|
|
271
|
+
updateFieldsWithNodeData(data, setFieldValue);
|
|
272
|
+
setNode(data);
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
fetchData().catch(console.error);
|
|
276
|
+
}, [setFieldValue]);
|
|
277
|
+
return (
|
|
278
|
+
<Form>
|
|
279
|
+
{displayMessageAfterSubmit(status)}
|
|
280
|
+
{action === Action.Edit && message ? (
|
|
281
|
+
alertMessage(message)
|
|
282
|
+
) : (
|
|
283
|
+
<>
|
|
284
|
+
{action === Action.Add
|
|
285
|
+
? namespaceInput
|
|
286
|
+
: staticFieldsInEdit(node)}
|
|
287
|
+
<div className="DisplayNameInput NodeCreationInput">
|
|
288
|
+
<ErrorMessage name="display_name" component="span" />
|
|
289
|
+
<label htmlFor="displayName">Display Name</label>
|
|
290
|
+
<Field
|
|
291
|
+
type="text"
|
|
292
|
+
name="display_name"
|
|
293
|
+
id="displayName"
|
|
294
|
+
placeholder="Human readable display name"
|
|
295
|
+
/>
|
|
296
|
+
</div>
|
|
297
|
+
{action === Action.Add ? fullNameInput : ''}
|
|
298
|
+
<div className="DescriptionInput NodeCreationInput">
|
|
299
|
+
<ErrorMessage name="description" component="span" />
|
|
300
|
+
<label htmlFor="Description">Description</label>
|
|
301
|
+
<Field
|
|
302
|
+
type="textarea"
|
|
303
|
+
as="textarea"
|
|
304
|
+
name="description"
|
|
305
|
+
id="Description"
|
|
306
|
+
placeholder="Describe your node"
|
|
307
|
+
/>
|
|
308
|
+
</div>
|
|
309
|
+
<div className="QueryInput NodeCreationInput">
|
|
310
|
+
<ErrorMessage name="query" component="span" />
|
|
311
|
+
<label htmlFor="Query">Query</label>
|
|
312
|
+
<NodeQueryField
|
|
313
|
+
djClient={djClient}
|
|
314
|
+
value={node.query ? node.query : ''}
|
|
315
|
+
/>
|
|
316
|
+
</div>
|
|
317
|
+
<div className="PrimaryKeyInput NodeCreationInput">
|
|
318
|
+
<ErrorMessage name="primary_key" component="span" />
|
|
319
|
+
<label htmlFor="primaryKey">Primary Key</label>
|
|
320
|
+
<Field
|
|
321
|
+
type="text"
|
|
322
|
+
name="primary_key"
|
|
323
|
+
id="primaryKey"
|
|
324
|
+
placeholder="Comma-separated list of PKs"
|
|
325
|
+
/>
|
|
326
|
+
</div>
|
|
327
|
+
<div className="NodeModeInput NodeCreationInput">
|
|
328
|
+
<ErrorMessage name="mode" component="span" />
|
|
329
|
+
<label htmlFor="Mode">Mode</label>
|
|
330
|
+
<Field as="select" name="mode" id="Mode">
|
|
331
|
+
<option value="draft">Draft</option>
|
|
332
|
+
<option value="published">Published</option>
|
|
333
|
+
</Field>
|
|
334
|
+
</div>
|
|
335
|
+
<button type="submit" disabled={isSubmitting}>
|
|
336
|
+
{action === Action.Add ? 'Create' : 'Save'} {nodeType}
|
|
337
|
+
</button>
|
|
338
|
+
</>
|
|
339
|
+
)}
|
|
340
|
+
</Form>
|
|
341
|
+
);
|
|
342
|
+
}}
|
|
343
|
+
</Formik>
|
|
344
|
+
</center>
|
|
345
|
+
</div>
|
|
346
|
+
</div>
|
|
347
|
+
</div>
|
|
348
|
+
);
|
|
349
|
+
}
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { Formik, Form, Field, ErrorMessage } from 'formik';
|
|
3
|
+
import '../../../styles/login.css';
|
|
4
|
+
import logo from '../Root/assets/dj-logo.png';
|
|
5
|
+
import GitHubLoginButton from './assets/sign-in-with-github.png';
|
|
6
|
+
|
|
7
|
+
export function LoginPage() {
|
|
8
|
+
const [, setError] = useState('');
|
|
9
|
+
const githubLoginURL = new URL('/github/login/', process.env.REACT_APP_DJ_URL)
|
|
10
|
+
.href;
|
|
11
|
+
|
|
12
|
+
const handleBasicLogin = async ({ username, password }) => {
|
|
13
|
+
const data = new FormData();
|
|
14
|
+
data.append('username', username);
|
|
15
|
+
data.append('password', password);
|
|
16
|
+
await fetch(`${process.env.REACT_APP_DJ_URL}/basic/login/`, {
|
|
17
|
+
method: 'POST',
|
|
18
|
+
body: data,
|
|
19
|
+
credentials: 'include',
|
|
20
|
+
}).catch(error => {
|
|
21
|
+
setError(error ? JSON.stringify(error) : '');
|
|
22
|
+
});
|
|
23
|
+
window.location.reload();
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className="container">
|
|
28
|
+
<div className="login">
|
|
29
|
+
<center>
|
|
30
|
+
<Formik
|
|
31
|
+
initialValues={{ username: '', password: '' }}
|
|
32
|
+
validate={values => {
|
|
33
|
+
const errors = {};
|
|
34
|
+
if (!values.username) {
|
|
35
|
+
errors.username = 'Required';
|
|
36
|
+
}
|
|
37
|
+
if (!values.password) {
|
|
38
|
+
errors.password = 'Required';
|
|
39
|
+
}
|
|
40
|
+
return errors;
|
|
41
|
+
}}
|
|
42
|
+
onSubmit={(values, { setSubmitting }) => {
|
|
43
|
+
setTimeout(() => {
|
|
44
|
+
handleBasicLogin(values);
|
|
45
|
+
setSubmitting(false);
|
|
46
|
+
}, 400);
|
|
47
|
+
}}
|
|
48
|
+
>
|
|
49
|
+
{({ isSubmitting }) => (
|
|
50
|
+
<Form>
|
|
51
|
+
<div className="logo-title">
|
|
52
|
+
<img src={logo} alt="DJ Logo" width="75px" height="75px" />
|
|
53
|
+
<h2>DataJunction</h2>
|
|
54
|
+
</div>
|
|
55
|
+
<div className="inputContainer">
|
|
56
|
+
<ErrorMessage name="username" component="span" />
|
|
57
|
+
<Field type="text" name="username" placeholder="Username" />
|
|
58
|
+
</div>
|
|
59
|
+
<div>
|
|
60
|
+
<ErrorMessage name="password" component="span" />
|
|
61
|
+
<Field
|
|
62
|
+
type="password"
|
|
63
|
+
name="password"
|
|
64
|
+
placeholder="Password"
|
|
65
|
+
/>
|
|
66
|
+
</div>
|
|
67
|
+
<button type="submit" disabled={isSubmitting}>
|
|
68
|
+
Login
|
|
69
|
+
</button>
|
|
70
|
+
{process.env.REACT_ENABLE_GITHUB_OAUTH === 'true' ? (
|
|
71
|
+
<div>
|
|
72
|
+
<a href={githubLoginURL}>
|
|
73
|
+
<img
|
|
74
|
+
src={GitHubLoginButton}
|
|
75
|
+
alt="Sign in with GitHub"
|
|
76
|
+
width="200px"
|
|
77
|
+
/>
|
|
78
|
+
</a>
|
|
79
|
+
</div>
|
|
80
|
+
) : (
|
|
81
|
+
''
|
|
82
|
+
)}
|
|
83
|
+
</Form>
|
|
84
|
+
)}
|
|
85
|
+
</Formik>
|
|
86
|
+
</center>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
@@ -13,6 +13,54 @@ exports[`<NamespacePage /> should render and match the snapshot 1`] = `
|
|
|
13
13
|
<h2>
|
|
14
14
|
Explore
|
|
15
15
|
</h2>
|
|
16
|
+
<span
|
|
17
|
+
className="menu-link"
|
|
18
|
+
>
|
|
19
|
+
<span
|
|
20
|
+
className="menu-title"
|
|
21
|
+
>
|
|
22
|
+
<div
|
|
23
|
+
className="dropdown"
|
|
24
|
+
>
|
|
25
|
+
<span
|
|
26
|
+
className="add_node"
|
|
27
|
+
>
|
|
28
|
+
+ Add Node
|
|
29
|
+
</span>
|
|
30
|
+
<div
|
|
31
|
+
className="dropdown-content"
|
|
32
|
+
>
|
|
33
|
+
<a
|
|
34
|
+
href="/create/transform/undefined"
|
|
35
|
+
>
|
|
36
|
+
<div
|
|
37
|
+
className="node_type__transform node_type_creation_heading"
|
|
38
|
+
>
|
|
39
|
+
Transform
|
|
40
|
+
</div>
|
|
41
|
+
</a>
|
|
42
|
+
<a
|
|
43
|
+
href="/create/metric/undefined"
|
|
44
|
+
>
|
|
45
|
+
<div
|
|
46
|
+
className="node_type__metric node_type_creation_heading"
|
|
47
|
+
>
|
|
48
|
+
Metric
|
|
49
|
+
</div>
|
|
50
|
+
</a>
|
|
51
|
+
<a
|
|
52
|
+
href="/create/dimension/undefined"
|
|
53
|
+
>
|
|
54
|
+
<div
|
|
55
|
+
className="node_type__dimension node_type_creation_heading"
|
|
56
|
+
>
|
|
57
|
+
Dimension
|
|
58
|
+
</div>
|
|
59
|
+
</a>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
</span>
|
|
63
|
+
</span>
|
|
16
64
|
<div
|
|
17
65
|
className="table-responsive"
|
|
18
66
|
>
|
|
@@ -54,10 +102,10 @@ exports[`<NamespacePage /> should render and match the snapshot 1`] = `
|
|
|
54
102
|
Mode
|
|
55
103
|
</th>
|
|
56
104
|
<th>
|
|
57
|
-
|
|
105
|
+
Last Updated
|
|
58
106
|
</th>
|
|
59
107
|
<th>
|
|
60
|
-
|
|
108
|
+
Actions
|
|
61
109
|
</th>
|
|
62
110
|
</tr>
|
|
63
111
|
</thead>
|
|
@@ -4,6 +4,8 @@ import { useContext, useEffect, useState } from 'react';
|
|
|
4
4
|
import NodeStatus from '../NodePage/NodeStatus';
|
|
5
5
|
import DJClientContext from '../../providers/djclient';
|
|
6
6
|
import Explorer from '../NamespacePage/Explorer';
|
|
7
|
+
import EditIcon from '../../icons/EditIcon';
|
|
8
|
+
import DeleteIcon from '../../icons/DeleteIcon';
|
|
7
9
|
|
|
8
10
|
export function NamespacePage() {
|
|
9
11
|
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
@@ -20,7 +22,7 @@ export function NamespacePage() {
|
|
|
20
22
|
const hierarchy = [];
|
|
21
23
|
|
|
22
24
|
for (const item of namespaceList) {
|
|
23
|
-
const namespaces = item.split('.');
|
|
25
|
+
const namespaces = item.namespace.split('.');
|
|
24
26
|
let currentLevel = hierarchy;
|
|
25
27
|
|
|
26
28
|
let path = '';
|
|
@@ -58,10 +60,7 @@ export function NamespacePage() {
|
|
|
58
60
|
if (namespace === undefined && namespaceHierarchy !== undefined) {
|
|
59
61
|
namespace = namespaceHierarchy.children[0].path;
|
|
60
62
|
}
|
|
61
|
-
const
|
|
62
|
-
const nodes = djNodes.map(node => {
|
|
63
|
-
return djClient.node(node);
|
|
64
|
-
});
|
|
63
|
+
const nodes = await djClient.namespace(namespace);
|
|
65
64
|
const foundNodes = await Promise.all(nodes);
|
|
66
65
|
setState({
|
|
67
66
|
namespace: namespace,
|
|
@@ -100,14 +99,19 @@ export function NamespacePage() {
|
|
|
100
99
|
<td>
|
|
101
100
|
<span className="status">{node.mode}</span>
|
|
102
101
|
</td>
|
|
103
|
-
<td>
|
|
104
|
-
<span className="status">{node.tags}</span>
|
|
105
|
-
</td>
|
|
106
102
|
<td>
|
|
107
103
|
<span className="status">
|
|
108
104
|
{new Date(node.updated_at).toLocaleString('en-us')}
|
|
109
105
|
</span>
|
|
110
106
|
</td>
|
|
107
|
+
<td>
|
|
108
|
+
<a href={`/nodes/${node?.name}/edit`} style={{ marginLeft: '0.5rem' }}>
|
|
109
|
+
<EditIcon />
|
|
110
|
+
</a>
|
|
111
|
+
<a href="#" style={{ marginLeft: '0.5rem' }}>
|
|
112
|
+
<DeleteIcon />
|
|
113
|
+
</a>
|
|
114
|
+
</td>
|
|
111
115
|
</tr>
|
|
112
116
|
));
|
|
113
117
|
|
|
@@ -116,6 +120,31 @@ export function NamespacePage() {
|
|
|
116
120
|
<div className="card">
|
|
117
121
|
<div className="card-header">
|
|
118
122
|
<h2>Explore</h2>
|
|
123
|
+
|
|
124
|
+
<span className="menu-link">
|
|
125
|
+
<span className="menu-title">
|
|
126
|
+
<div className="dropdown">
|
|
127
|
+
<span className="add_node">+ Add Node</span>
|
|
128
|
+
<div className="dropdown-content">
|
|
129
|
+
<a href={`/create/transform/${namespace}`}>
|
|
130
|
+
<div className="node_type__transform node_type_creation_heading">
|
|
131
|
+
Transform
|
|
132
|
+
</div>
|
|
133
|
+
</a>
|
|
134
|
+
<a href={`/create/metric/${namespace}`}>
|
|
135
|
+
<div className="node_type__metric node_type_creation_heading">
|
|
136
|
+
Metric
|
|
137
|
+
</div>
|
|
138
|
+
</a>
|
|
139
|
+
<a href={`/create/dimension/${namespace}`}>
|
|
140
|
+
<div className="node_type__dimension node_type_creation_heading">
|
|
141
|
+
Dimension
|
|
142
|
+
</div>
|
|
143
|
+
</a>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
</span>
|
|
147
|
+
</span>
|
|
119
148
|
<div className="table-responsive">
|
|
120
149
|
<div className={`sidebar`}>
|
|
121
150
|
<span
|
|
@@ -147,8 +176,8 @@ export function NamespacePage() {
|
|
|
147
176
|
<th>Type</th>
|
|
148
177
|
<th>Status</th>
|
|
149
178
|
<th>Mode</th>
|
|
150
|
-
<th>Tags</th>
|
|
151
179
|
<th>Last Updated</th>
|
|
180
|
+
<th>Actions</th>
|
|
152
181
|
</tr>
|
|
153
182
|
</thead>
|
|
154
183
|
<tbody>{nodesList}</tbody>
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { useContext } from 'react';
|
|
2
2
|
import { MarkerType } from 'reactflow';
|
|
3
3
|
|
|
4
4
|
import '../../../styles/dag.css';
|
|
5
5
|
import 'reactflow/dist/style.css';
|
|
6
|
-
import DJNode from '../../components/djgraph/DJNode';
|
|
7
6
|
import DJClientContext from '../../providers/djclient';
|
|
8
7
|
import LayoutFlow from '../../components/djgraph/LayoutFlow';
|
|
9
8
|
|