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.
Files changed (40) hide show
  1. package/.env +1 -1
  2. package/dj-logo.svg +10 -0
  3. package/package.json +28 -6
  4. package/src/app/__tests__/__snapshots__/index.test.tsx.snap +5 -84
  5. package/src/app/components/djgraph/DJNode.jsx +1 -1
  6. package/src/app/components/djgraph/LayoutFlow.jsx +1 -1
  7. package/src/app/constants.js +2 -0
  8. package/src/app/icons/AlertIcon.jsx +32 -0
  9. package/src/app/icons/DJLogo.jsx +36 -0
  10. package/src/app/icons/DeleteIcon.jsx +21 -0
  11. package/src/app/icons/EditIcon.jsx +18 -0
  12. package/src/app/index.tsx +70 -36
  13. package/src/app/pages/AddEditNodePage/FormikSelect.jsx +33 -0
  14. package/src/app/pages/AddEditNodePage/FullNameField.jsx +36 -0
  15. package/src/app/pages/AddEditNodePage/Loadable.jsx +16 -0
  16. package/src/app/pages/AddEditNodePage/NodeQueryField.jsx +89 -0
  17. package/src/app/pages/AddEditNodePage/__tests__/FormikSelect.test.jsx +44 -0
  18. package/src/app/pages/AddEditNodePage/__tests__/FullNameField.test.jsx +29 -0
  19. package/src/app/pages/AddEditNodePage/__tests__/NodeQueryField.test.jsx +30 -0
  20. package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/index.test.jsx.snap +84 -0
  21. package/src/app/pages/AddEditNodePage/__tests__/index.test.jsx +353 -0
  22. package/src/app/pages/AddEditNodePage/index.jsx +349 -0
  23. package/src/app/pages/LoginPage/assets/sign-in-with-github.png +0 -0
  24. package/src/app/pages/LoginPage/assets/sign-in-with-google.png +0 -0
  25. package/src/app/pages/LoginPage/index.jsx +90 -0
  26. package/src/app/pages/NamespacePage/__tests__/__snapshots__/index.test.tsx.snap +50 -2
  27. package/src/app/pages/NamespacePage/index.jsx +38 -9
  28. package/src/app/pages/NodePage/NodeGraphTab.jsx +1 -2
  29. package/src/app/pages/NodePage/NodeHistory.jsx +0 -1
  30. package/src/app/pages/NodePage/index.jsx +43 -26
  31. package/src/app/pages/Root/index.tsx +20 -3
  32. package/src/app/pages/SQLBuilderPage/index.jsx +54 -8
  33. package/src/app/services/DJService.js +180 -32
  34. package/src/setupTests.ts +1 -1
  35. package/src/styles/index.css +82 -5
  36. package/src/styles/login.css +67 -0
  37. package/src/styles/node-creation.scss +190 -0
  38. package/src/styles/styles.scss +44 -0
  39. package/src/styles/styles.scss.d.ts +9 -0
  40. 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
+ }
@@ -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
- Tags
105
+ Last Updated
58
106
  </th>
59
107
  <th>
60
- Last Updated
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 djNodes = await djClient.namespace(namespace);
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 React, { useContext } from 'react';
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
 
@@ -15,7 +15,6 @@ export default function NodeHistory({ node, djClient }) {
15
15
  }, [djClient, node]);
16
16
 
17
17
  const eventData = event => {
18
- console.log('event', event);
19
18
  if (
20
19
  event.activity_type === 'set_attribute' &&
21
20
  event.entity_type === 'column_attribute'