datajunction-ui 0.0.1-rc.24 → 0.0.1-rc.26

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 (44) hide show
  1. package/.env +1 -0
  2. package/package.json +3 -2
  3. package/src/app/components/Tab.jsx +0 -1
  4. package/src/app/constants.js +2 -2
  5. package/src/app/icons/LoadingIcon.jsx +14 -0
  6. package/src/app/index.tsx +11 -1
  7. package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormFailed.test.jsx +28 -2
  8. package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormSuccess.test.jsx +44 -9
  9. package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/AddEditNodePageFormFailed.test.jsx.snap +1 -0
  10. package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/AddEditNodePageFormSuccess.test.jsx.snap +0 -50
  11. package/src/app/pages/AddEditNodePage/__tests__/index.test.jsx +2 -0
  12. package/src/app/pages/AddEditNodePage/index.jsx +60 -6
  13. package/src/app/pages/AddEditTagPage/Loadable.jsx +16 -0
  14. package/src/app/pages/AddEditTagPage/__tests__/AddEditTagPage.test.jsx +107 -0
  15. package/src/app/pages/AddEditTagPage/index.jsx +132 -0
  16. package/src/app/pages/LoginPage/LoginForm.jsx +124 -0
  17. package/src/app/pages/LoginPage/SignupForm.jsx +156 -0
  18. package/src/app/pages/LoginPage/__tests__/index.test.jsx +34 -2
  19. package/src/app/pages/LoginPage/index.jsx +9 -82
  20. package/src/app/pages/NamespacePage/index.jsx +5 -0
  21. package/src/app/pages/NodePage/AddBackfillPopover.jsx +166 -0
  22. package/src/app/pages/NodePage/AddMaterializationPopover.jsx +161 -0
  23. package/src/app/pages/NodePage/EditColumnPopover.jsx +1 -1
  24. package/src/app/pages/NodePage/LinkDimensionPopover.jsx +0 -1
  25. package/src/app/pages/NodePage/NodeColumnTab.jsx +102 -25
  26. package/src/app/pages/NodePage/NodeInfoTab.jsx +33 -23
  27. package/src/app/pages/NodePage/NodeMaterializationTab.jsx +158 -99
  28. package/src/app/pages/NodePage/PartitionColumnPopover.jsx +153 -0
  29. package/src/app/pages/NodePage/__tests__/AddBackfillPopover.test.jsx +47 -0
  30. package/src/app/pages/NodePage/__tests__/NodePage.test.jsx +47 -17
  31. package/src/app/pages/NodePage/__tests__/__snapshots__/NodePage.test.jsx.snap +101 -100
  32. package/src/app/pages/RegisterTablePage/__tests__/__snapshots__/RegisterTablePage.test.jsx.snap +1 -0
  33. package/src/app/pages/Root/index.tsx +1 -1
  34. package/src/app/pages/TagPage/Loadable.jsx +16 -0
  35. package/src/app/pages/TagPage/__tests__/TagPage.test.jsx +70 -0
  36. package/src/app/pages/TagPage/index.jsx +79 -0
  37. package/src/app/services/DJService.js +166 -1
  38. package/src/app/services/__tests__/DJService.test.jsx +196 -1
  39. package/src/mocks/mockNodes.jsx +64 -31
  40. package/src/styles/dag.css +4 -0
  41. package/src/styles/index.css +89 -1
  42. package/src/styles/loading.css +34 -0
  43. package/src/styles/login.css +17 -3
  44. package/src/utils/form.jsx +2 -2
@@ -0,0 +1,166 @@
1
+ import { useContext, useEffect, useRef, useState } from 'react';
2
+ import * as React from 'react';
3
+ import DJClientContext from '../../providers/djclient';
4
+ import { ErrorMessage, Field, Form, Formik } from 'formik';
5
+ import { FormikSelect } from '../AddEditNodePage/FormikSelect';
6
+ import EditIcon from '../../icons/EditIcon';
7
+ import { displayMessageAfterSubmit, labelize } from '../../../utils/form';
8
+
9
+ export default function AddBackfillPopover({
10
+ node,
11
+ materialization,
12
+ onSubmit,
13
+ }) {
14
+ const djClient = useContext(DJClientContext).DataJunctionAPI;
15
+ const [popoverAnchor, setPopoverAnchor] = useState(false);
16
+ const ref = useRef(null);
17
+
18
+ useEffect(() => {
19
+ const handleClickOutside = event => {
20
+ if (ref.current && !ref.current.contains(event.target)) {
21
+ setPopoverAnchor(false);
22
+ }
23
+ };
24
+ document.addEventListener('click', handleClickOutside, true);
25
+ return () => {
26
+ document.removeEventListener('click', handleClickOutside, true);
27
+ };
28
+ }, [setPopoverAnchor]);
29
+
30
+ const partitionColumns = node.columns.filter(col => col.partition !== null);
31
+
32
+ const temporalPartitionColumns = partitionColumns.filter(
33
+ col => col.partition.type_ === 'temporal',
34
+ );
35
+
36
+ const initialValues = {
37
+ node: node.name,
38
+ materializationName: materialization.name,
39
+ partitionColumn:
40
+ temporalPartitionColumns.length > 0
41
+ ? temporalPartitionColumns[0].name
42
+ : '',
43
+ from: '',
44
+ to: '',
45
+ };
46
+
47
+ const savePartition = async (values, { setSubmitting, setStatus }) => {
48
+ setSubmitting(false);
49
+ const response = await djClient.runBackfill(
50
+ values.node,
51
+ values.materializationName,
52
+ values.partitionColumn,
53
+ values.from,
54
+ values.to,
55
+ );
56
+ if (response.status === 200 || response.status === 201) {
57
+ setStatus({ success: 'Saved!' });
58
+ } else {
59
+ setStatus({
60
+ failure: `${response.json.message}`,
61
+ });
62
+ }
63
+ onSubmit();
64
+ window.location.reload();
65
+ };
66
+
67
+ return (
68
+ <>
69
+ <button
70
+ className="edit_button"
71
+ aria-label="AddBackfill"
72
+ tabIndex="0"
73
+ onClick={() => {
74
+ setPopoverAnchor(!popoverAnchor);
75
+ }}
76
+ >
77
+ <span className="add_node">+ Add Backfill</span>
78
+ </button>
79
+ <div
80
+ className="fade modal-backdrop in"
81
+ style={{ display: popoverAnchor === false ? 'none' : 'block' }}
82
+ ></div>
83
+ <div
84
+ className="centerPopover"
85
+ role="dialog"
86
+ aria-label="client-code"
87
+ style={{
88
+ display: popoverAnchor === false ? 'none' : 'block',
89
+ width: '50%',
90
+ }}
91
+ ref={ref}
92
+ >
93
+ <Formik initialValues={initialValues} onSubmit={savePartition}>
94
+ {function Render({ isSubmitting, status, setFieldValue }) {
95
+ return (
96
+ <Form>
97
+ {displayMessageAfterSubmit(status)}
98
+ <h2>Run Backfill</h2>
99
+ <span data-testid="edit-partition">
100
+ <label htmlFor="engine" style={{ paddingBottom: '1rem' }}>
101
+ Engine
102
+ </label>
103
+ <Field as="select" name="engine" id="engine" disabled={true}>
104
+ <option value={materialization?.engine?.name}>
105
+ {materialization?.engine?.name}{' '}
106
+ {materialization?.engine?.version}
107
+ </option>
108
+ </Field>
109
+ </span>
110
+ <br />
111
+ <br />
112
+ <label htmlFor="partition" style={{ paddingBottom: '1rem' }}>
113
+ Partition Range
114
+ </label>
115
+ {node.columns
116
+ .filter(col => col.partition !== null)
117
+ .map(col => {
118
+ return (
119
+ <div
120
+ className="partition__full"
121
+ key={col.name}
122
+ style={{ width: '50%' }}
123
+ >
124
+ <div className="partition__header">
125
+ {col.display_name}
126
+ </div>
127
+ <div className="partition__body">
128
+ <span style={{ padding: '0.5rem' }}>From</span>{' '}
129
+ <Field
130
+ type="text"
131
+ name="from"
132
+ id={`${col.name}__from`}
133
+ placeholder="20230101"
134
+ default="20230101"
135
+ style={{ width: '7rem', paddingRight: '1rem' }}
136
+ />{' '}
137
+ <span style={{ padding: '0.5rem' }}>To</span>
138
+ <Field
139
+ type="text"
140
+ name="to"
141
+ id={`${col.name}__to`}
142
+ placeholder="20230102"
143
+ default="20230102"
144
+ style={{ width: '7rem' }}
145
+ />
146
+ </div>
147
+ </div>
148
+ );
149
+ })}
150
+ <br />
151
+ <button
152
+ className="add_node"
153
+ type="submit"
154
+ aria-label="SaveEditColumn"
155
+ aria-hidden="false"
156
+ >
157
+ Save
158
+ </button>
159
+ </Form>
160
+ );
161
+ }}
162
+ </Formik>
163
+ </div>
164
+ </>
165
+ );
166
+ }
@@ -0,0 +1,161 @@
1
+ import { useContext, useEffect, useRef, useState } from 'react';
2
+ import * as React from 'react';
3
+ import DJClientContext from '../../providers/djclient';
4
+ import { ErrorMessage, Field, Form, Formik } from 'formik';
5
+ import { FormikSelect } from '../AddEditNodePage/FormikSelect';
6
+ import EditIcon from '../../icons/EditIcon';
7
+ import { displayMessageAfterSubmit, labelize } from '../../../utils/form';
8
+
9
+ export default function AddMaterializationPopover({ node, onSubmit }) {
10
+ const djClient = useContext(DJClientContext).DataJunctionAPI;
11
+ const [popoverAnchor, setPopoverAnchor] = useState(false);
12
+ const [engines, setEngines] = useState([]);
13
+ const [defaultEngine, setDefaultEngine] = useState('');
14
+
15
+ const ref = useRef(null);
16
+
17
+ useEffect(() => {
18
+ const fetchData = async () => {
19
+ const engines = await djClient.engines();
20
+ setEngines(engines);
21
+ setDefaultEngine(
22
+ engines && engines.length > 0
23
+ ? engines[0].name + '__' + engines[0].version
24
+ : '',
25
+ );
26
+ };
27
+ fetchData().catch(console.error);
28
+ const handleClickOutside = event => {
29
+ if (ref.current && !ref.current.contains(event.target)) {
30
+ setPopoverAnchor(false);
31
+ }
32
+ };
33
+ document.addEventListener('click', handleClickOutside, true);
34
+ return () => {
35
+ document.removeEventListener('click', handleClickOutside, true);
36
+ };
37
+ }, [djClient, setPopoverAnchor]);
38
+
39
+ const configureMaterialization = async (
40
+ values,
41
+ { setSubmitting, setStatus },
42
+ ) => {
43
+ setSubmitting(false);
44
+ const engineVersion = values.engine.split('__').slice(-1).join('');
45
+ const engineName = values.engine.split('__').slice(0, -1).join('');
46
+ const response = await djClient.materialize(
47
+ values.node,
48
+ engineName,
49
+ engineVersion,
50
+ values.schedule,
51
+ values.config,
52
+ );
53
+ if (response.status === 200 || response.status === 201) {
54
+ setStatus({ success: 'Saved!' });
55
+ } else {
56
+ setStatus({
57
+ failure: `${response.json.message}`,
58
+ });
59
+ }
60
+ onSubmit();
61
+ // window.location.reload();
62
+ };
63
+
64
+ return (
65
+ <>
66
+ <button
67
+ className="edit_button"
68
+ aria-label="PartitionColumn"
69
+ tabIndex="0"
70
+ onClick={() => {
71
+ setPopoverAnchor(!popoverAnchor);
72
+ }}
73
+ >
74
+ <span className="add_node">+ Add Materialization</span>
75
+ </button>
76
+ <div
77
+ className="fade modal-backdrop in"
78
+ style={{ display: popoverAnchor === false ? 'none' : 'block' }}
79
+ ></div>
80
+ <div
81
+ className="centerPopover"
82
+ role="dialog"
83
+ aria-label="client-code"
84
+ style={{
85
+ display: popoverAnchor === false ? 'none' : 'block',
86
+ width: '50%',
87
+ }}
88
+ ref={ref}
89
+ >
90
+ <Formik
91
+ initialValues={{
92
+ node: node?.name,
93
+ engine: defaultEngine,
94
+ config: '{"spark": {"spark.executor.memory": "6g"}}',
95
+ schedule: '@daily',
96
+ }}
97
+ onSubmit={configureMaterialization}
98
+ >
99
+ {function Render({ isSubmitting, status, setFieldValue }) {
100
+ return (
101
+ <Form>
102
+ <h2>Configure Materialization</h2>
103
+ {displayMessageAfterSubmit(status)}
104
+ <span data-testid="edit-partition">
105
+ <label htmlFor="engine">Engine</label>
106
+ <Field as="select" name="engine">
107
+ <>
108
+ {engines?.map(engine => (
109
+ <option value={engine.name + '__' + engine.version}>
110
+ {engine.name} {engine.version}
111
+ </option>
112
+ ))}
113
+ <option value=""></option>
114
+ </>
115
+ </Field>
116
+ </span>
117
+ <input
118
+ hidden={true}
119
+ name="node"
120
+ value={node?.name}
121
+ readOnly={true}
122
+ />
123
+ <br />
124
+ <br />
125
+ <label htmlFor="schedule">Schedule</label>
126
+ <Field
127
+ type="text"
128
+ name="schedule"
129
+ id="schedule"
130
+ placeholder="Cron"
131
+ default="@daily"
132
+ />
133
+ <br />
134
+ <br />
135
+ <div className="DescriptionInput">
136
+ <ErrorMessage name="description" component="span" />
137
+ <label htmlFor="Config">Config</label>
138
+ <Field
139
+ type="textarea"
140
+ as="textarea"
141
+ name="config"
142
+ id="Config"
143
+ placeholder="Optional engine-specific configuration (i.e., Spark conf etc)"
144
+ />
145
+ </div>
146
+ <button
147
+ className="add_node"
148
+ type="submit"
149
+ aria-label="SaveEditColumn"
150
+ aria-hidden="false"
151
+ >
152
+ Save
153
+ </button>
154
+ </Form>
155
+ );
156
+ }}
157
+ </Formik>
158
+ </div>
159
+ </>
160
+ );
161
+ }
@@ -1,4 +1,4 @@
1
- import {useContext, useEffect, useRef, useState} from 'react';
1
+ import { useContext, useEffect, useRef, useState } from 'react';
2
2
  import * as React from 'react';
3
3
  import DJClientContext from '../../providers/djclient';
4
4
  import { Form, Formik } from 'formik';
@@ -35,7 +35,6 @@ export default function LinkDimensionPopover({
35
35
  { setSubmitting, setStatus },
36
36
  ) => {
37
37
  setSubmitting(false);
38
- console.log('dimension', dimension, 'columnDimension', columnDimension);
39
38
  if (columnDimension?.name && dimension === 'Remove') {
40
39
  await unlinkDimension(node, column, columnDimension?.name, setStatus);
41
40
  } else {
@@ -4,6 +4,7 @@ import * as React from 'react';
4
4
  import EditColumnPopover from './EditColumnPopover';
5
5
  import LinkDimensionPopover from './LinkDimensionPopover';
6
6
  import { labelize } from '../../../utils/form';
7
+ import PartitionColumnPopover from './PartitionColumnPopover';
7
8
 
8
9
  export default function NodeColumnTab({ node, djClient }) {
9
10
  const [attributes, setAttributes] = useState([]);
@@ -49,6 +50,41 @@ export default function NodeColumnTab({ node, djClient }) {
49
50
  ));
50
51
  };
51
52
 
53
+ const showColumnPartition = col => {
54
+ if (col.partition) {
55
+ return (
56
+ <>
57
+ <span
58
+ className="node_type badge node_type__blank"
59
+ key={`col-attr-partition-type`}
60
+ >
61
+ <span
62
+ className="partition_value badge"
63
+ key={`col-attr-partition-type`}
64
+ >
65
+ <b>Type:</b> {col.partition.type_}
66
+ </span>
67
+ <br />
68
+ <span
69
+ className="partition_value badge"
70
+ key={`col-attr-partition-type`}
71
+ >
72
+ <b>Format:</b> <code>{col.partition.format}</code>
73
+ </span>
74
+ <br />
75
+ <span
76
+ className="partition_value badge"
77
+ key={`col-attr-partition-type`}
78
+ >
79
+ <b>Granularity:</b> <code>{col.partition.granularity}</code>
80
+ </span>
81
+ </span>
82
+ </>
83
+ );
84
+ }
85
+ return '';
86
+ };
87
+
52
88
  const columnList = columns => {
53
89
  return columns.map(col => (
54
90
  <tr key={col.name}>
@@ -62,39 +98,72 @@ export default function NodeColumnTab({ node, djClient }) {
62
98
  </td>
63
99
  <td>
64
100
  <span
65
- className="node_type__transform badge node_type"
101
+ className=""
66
102
  role="columnheader"
67
- aria-label="ColumnType"
103
+ aria-label="ColumnDisplayName"
68
104
  aria-hidden="false"
69
105
  >
70
- {col.type}
106
+ {col.display_name}
71
107
  </span>
72
108
  </td>
73
109
  <td>
74
- {col.dimension !== undefined && col.dimension !== null ? (
75
- <>
76
- <a href={`/nodes/${col.dimension.name}`}>{col.dimension.name}</a>
77
- <ClientCodePopover code={col.clientCode} />
78
- </>
79
- ) : (
80
- ''
81
- )}{' '}
82
- <LinkDimensionPopover
83
- column={col}
84
- node={node}
85
- options={dimensions}
86
- onSubmit={async () => {
87
- const res = await djClient.node(node.name);
88
- setColumns(res.columns);
89
- }}
90
- />
110
+ <span
111
+ className={`node_type__${
112
+ node.type === 'cube' ? col.type : 'transform'
113
+ } badge node_type`}
114
+ role="columnheader"
115
+ aria-label="ColumnType"
116
+ aria-hidden="false"
117
+ >
118
+ {col.type}
119
+ </span>
91
120
  </td>
121
+ {node.type !== 'cube' ? (
122
+ <td>
123
+ {col.dimension !== undefined && col.dimension !== null ? (
124
+ <>
125
+ <a href={`/nodes/${col.dimension.name}`}>
126
+ {col.dimension.name}
127
+ </a>
128
+ <ClientCodePopover code={col.clientCode} />
129
+ </>
130
+ ) : (
131
+ ''
132
+ )}{' '}
133
+ <LinkDimensionPopover
134
+ column={col}
135
+ node={node}
136
+ options={dimensions}
137
+ onSubmit={async () => {
138
+ const res = await djClient.node(node.name);
139
+ setColumns(res.columns);
140
+ }}
141
+ />
142
+ </td>
143
+ ) : (
144
+ ''
145
+ )}
146
+ {node.type !== 'cube' ? (
147
+ <td>
148
+ {showColumnAttributes(col)}
149
+ <EditColumnPopover
150
+ column={col}
151
+ node={node}
152
+ options={attributes}
153
+ onSubmit={async () => {
154
+ const res = await djClient.node(node.name);
155
+ setColumns(res.columns);
156
+ }}
157
+ />
158
+ </td>
159
+ ) : (
160
+ ''
161
+ )}
92
162
  <td>
93
- {showColumnAttributes(col)}
94
- <EditColumnPopover
163
+ {showColumnPartition(col)}
164
+ <PartitionColumnPopover
95
165
  column={col}
96
166
  node={node}
97
- options={attributes}
98
167
  onSubmit={async () => {
99
168
  const res = await djClient.node(node.name);
100
169
  setColumns(res.columns);
@@ -111,9 +180,17 @@ export default function NodeColumnTab({ node, djClient }) {
111
180
  <thead className="fs-7 fw-bold text-gray-400 border-bottom-0">
112
181
  <tr>
113
182
  <th className="text-start">Column</th>
183
+ <th>Display Name</th>
114
184
  <th>Type</th>
115
- <th>Linked Dimension</th>
116
- <th>Attributes</th>
185
+ {node?.type !== 'cube' ? (
186
+ <>
187
+ <th>Linked Dimension</th>
188
+ <th>Attributes</th>
189
+ </>
190
+ ) : (
191
+ ''
192
+ )}
193
+ <th>Partition</th>
117
194
  </tr>
118
195
  </thead>
119
196
  <tbody>{columnList(columns)}</tbody>
@@ -13,7 +13,11 @@ foundation.hljs['padding'] = '2rem';
13
13
  export default function NodeInfoTab({ node }) {
14
14
  const [compiledSQL, setCompiledSQL] = useState('');
15
15
  const [checked, setChecked] = useState(false);
16
- const nodeTags = node?.tags.map(tag => <div>{tag}</div>);
16
+ const nodeTags = node?.tags.map(tag => (
17
+ <div className={'badge tag_value'}>
18
+ <a href={`/tags/${tag.name}`}>{tag.display_name}</a>
19
+ </div>
20
+ ));
17
21
  const djClient = useContext(DJClientContext).DataJunctionAPI;
18
22
  useEffect(() => {
19
23
  const fetchData = async () => {
@@ -62,6 +66,28 @@ export default function NodeInfoTab({ node }) {
62
66
  <></>
63
67
  );
64
68
 
69
+ const displayCubeElement = cubeElem => {
70
+ return (
71
+ <div
72
+ className="button-3 cube-element"
73
+ key={cubeElem.name}
74
+ role="cell"
75
+ aria-label="CubeElement"
76
+ aria-hidden="false"
77
+ >
78
+ <a href={`/nodes/${cubeElem.node_name}`}>{cubeElem.display_name}</a>
79
+ <span
80
+ className={`badge node_type__${
81
+ cubeElem.type === 'metric' ? cubeElem.type : 'dimension'
82
+ }`}
83
+ style={{ fontSize: '100%', textTransform: 'capitalize' }}
84
+ >
85
+ {cubeElem.type === 'metric' ? cubeElem.type : 'dimension'}
86
+ </span>
87
+ </div>
88
+ );
89
+ };
90
+
65
91
  const cubeElementsDiv = node?.cube_elements ? (
66
92
  <div className="list-group-item d-flex">
67
93
  <div className="d-flex gap-2 w-100 justify-content-between py-3">
@@ -72,28 +98,12 @@ export default function NodeInfoTab({ node }) {
72
98
  >
73
99
  <h6 className="mb-0 w-100">Cube Elements</h6>
74
100
  <div className={`list-group-item`}>
75
- {node.cube_elements.map(cubeElem => (
76
- <div
77
- className="button-3 cube-element"
78
- key={cubeElem.name}
79
- role="cell"
80
- aria-label="CubeElement"
81
- aria-hidden="false"
82
- >
83
- <a href={`/nodes/${cubeElem.node_name}`}>
84
- {cubeElem.type === 'metric'
85
- ? cubeElem.node_name
86
- : cubeElem.name}
87
- </a>
88
- <span
89
- className={`badge node_type__${
90
- cubeElem.type === 'metric' ? cubeElem.type : 'dimension'
91
- }`}
92
- >
93
- {cubeElem.type === 'metric' ? cubeElem.type : 'dimension'}
94
- </span>
95
- </div>
96
- ))}
101
+ {node.cube_elements.map(cubeElem =>
102
+ cubeElem.type === 'metric' ? displayCubeElement(cubeElem) : '',
103
+ )}
104
+ {node.cube_elements.map(cubeElem =>
105
+ cubeElem.type !== 'metric' ? displayCubeElement(cubeElem) : '',
106
+ )}
97
107
  </div>
98
108
  </div>
99
109
  </div>