datajunction-ui 0.0.1-rc.25 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "datajunction-ui",
3
- "version": "0.0.1-rc.25",
3
+ "version": "0.0.1-rc.26",
4
4
  "description": "DataJunction Metrics Platform UI",
5
5
  "module": "src/index.tsx",
6
6
  "repository": {
@@ -63,13 +63,21 @@ export default function LoginForm({ setShowSignup }) {
63
63
  <Field type="text" name="username" placeholder="Username" />
64
64
  </div>
65
65
  <div>
66
- <ErrorMessage className="form-error" name="username" component="span" />
66
+ <ErrorMessage
67
+ className="form-error"
68
+ name="username"
69
+ component="span"
70
+ />
67
71
  </div>
68
72
  <div>
69
73
  <Field type="password" name="password" placeholder="Password" />
70
74
  </div>
71
75
  <div>
72
- <ErrorMessage className="form-error" name="password" component="span" />
76
+ <ErrorMessage
77
+ className="form-error"
78
+ name="password"
79
+ component="span"
80
+ />
73
81
  </div>
74
82
  <div>
75
83
  <p>
@@ -81,13 +81,21 @@ export default function SignupForm({ setShowSignup }) {
81
81
  <Field type="text" name="email" placeholder="Email" />
82
82
  </div>
83
83
  <div>
84
- <ErrorMessage className="form-error" name="email" component="span" />
84
+ <ErrorMessage
85
+ className="form-error"
86
+ name="email"
87
+ component="span"
88
+ />
85
89
  </div>
86
90
  <div>
87
91
  <Field type="text" name="signupUsername" placeholder="Username" />
88
92
  </div>
89
93
  <div>
90
- <ErrorMessage className="form-error" name="signupUsername" component="span" />
94
+ <ErrorMessage
95
+ className="form-error"
96
+ name="signupUsername"
97
+ component="span"
98
+ />
91
99
  </div>
92
100
  <div>
93
101
  <Field
@@ -97,7 +105,11 @@ export default function SignupForm({ setShowSignup }) {
97
105
  />
98
106
  </div>
99
107
  <div>
100
- <ErrorMessage className="form-error" name="signupPassword" component="span" />
108
+ <ErrorMessage
109
+ className="form-error"
110
+ name="signupPassword"
111
+ component="span"
112
+ />
101
113
  </div>
102
114
  <div>
103
115
  <p>
@@ -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
+ }
@@ -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}>
@@ -72,7 +108,9 @@ export default function NodeColumnTab({ node, djClient }) {
72
108
  </td>
73
109
  <td>
74
110
  <span
75
- className="node_type__transform badge node_type"
111
+ className={`node_type__${
112
+ node.type === 'cube' ? col.type : 'transform'
113
+ } badge node_type`}
76
114
  role="columnheader"
77
115
  aria-label="ColumnType"
78
116
  aria-hidden="false"
@@ -80,31 +118,52 @@ export default function NodeColumnTab({ node, djClient }) {
80
118
  {col.type}
81
119
  </span>
82
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
+ )}
83
162
  <td>
84
- {col.dimension !== undefined && col.dimension !== null ? (
85
- <>
86
- <a href={`/nodes/${col.dimension.name}`}>{col.dimension.name}</a>
87
- <ClientCodePopover code={col.clientCode} />
88
- </>
89
- ) : (
90
- ''
91
- )}{' '}
92
- <LinkDimensionPopover
93
- column={col}
94
- node={node}
95
- options={dimensions}
96
- onSubmit={async () => {
97
- const res = await djClient.node(node.name);
98
- setColumns(res.columns);
99
- }}
100
- />
101
- </td>
102
- <td>
103
- {showColumnAttributes(col)}
104
- <EditColumnPopover
163
+ {showColumnPartition(col)}
164
+ <PartitionColumnPopover
105
165
  column={col}
106
166
  node={node}
107
- options={attributes}
108
167
  onSubmit={async () => {
109
168
  const res = await djClient.node(node.name);
110
169
  setColumns(res.columns);
@@ -123,8 +182,15 @@ export default function NodeColumnTab({ node, djClient }) {
123
182
  <th className="text-start">Column</th>
124
183
  <th>Display Name</th>
125
184
  <th>Type</th>
126
- <th>Linked Dimension</th>
127
- <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>
128
194
  </tr>
129
195
  </thead>
130
196
  <tbody>{columnList(columns)}</tbody>
@@ -66,6 +66,28 @@ export default function NodeInfoTab({ node }) {
66
66
  <></>
67
67
  );
68
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
+
69
91
  const cubeElementsDiv = node?.cube_elements ? (
70
92
  <div className="list-group-item d-flex">
71
93
  <div className="d-flex gap-2 w-100 justify-content-between py-3">
@@ -76,28 +98,12 @@ export default function NodeInfoTab({ node }) {
76
98
  >
77
99
  <h6 className="mb-0 w-100">Cube Elements</h6>
78
100
  <div className={`list-group-item`}>
79
- {node.cube_elements.map(cubeElem => (
80
- <div
81
- className="button-3 cube-element"
82
- key={cubeElem.name}
83
- role="cell"
84
- aria-label="CubeElement"
85
- aria-hidden="false"
86
- >
87
- <a href={`/nodes/${cubeElem.node_name}`}>
88
- {cubeElem.type === 'metric'
89
- ? cubeElem.node_name
90
- : cubeElem.name}
91
- </a>
92
- <span
93
- className={`badge node_type__${
94
- cubeElem.type === 'metric' ? cubeElem.type : 'dimension'
95
- }`}
96
- >
97
- {cubeElem.type === 'metric' ? cubeElem.type : 'dimension'}
98
- </span>
99
- </div>
100
- ))}
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
+ )}
101
107
  </div>
102
108
  </div>
103
109
  </div>