datajunction-ui 0.0.1-a46.dev1 → 0.0.1-a47

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 CHANGED
@@ -2,3 +2,8 @@ dev-release:
2
2
  yarn version --prerelease --preid dev --no-git-tag-version
3
3
  npm publish
4
4
 
5
+ test:
6
+ yarn test --coverage --watchAll --runInBand
7
+
8
+ lint:
9
+ npm run lint
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "datajunction-ui",
3
- "version": "0.0.1a46.dev1",
3
+ "version": "0.0.1a47",
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": 90,
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
- <NodeTagsInput action={action} node={node} />
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;
@@ -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
- partitionColumn:
38
- temporalPartitionColumns.length > 0
39
- ? temporalPartitionColumns[0].name
40
- : '',
41
- from: '',
42
- to: '',
34
+ partitionValues: {},
43
35
  };
44
36
 
45
- const savePartition = async (values, { setSubmitting, setStatus }) => {
46
- setSubmitting(false);
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.partitionColumn,
51
- values.from,
52
- values.to,
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
- onSubmit();
62
- window.location.reload();
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="add_node">+ Add Backfill</span>
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={savePartition}>
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 Range
136
+ Partition
119
137
  </label>
120
138
  {node.columns
121
139
  .filter(col => col.partition !== null)
122
140
  .map(col => {
123
141
  return (
124
- <div
125
- className="partition__full"
142
+ <PartitionValueForm
143
+ col={col}
144
+ materialization={materialization}
126
145
  key={col.name}
127
- style={{ width: '50%' }}
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
- <span data-testid="job-type">
115
- <label htmlFor="job_type">Job Type</label>
116
- <Field as="select" name="job_type">
117
- <>
118
- <option
119
- key={'druid_measures_cube'}
120
- value={'druid_measures_cube'}
121
- >
122
- Druid Measures Cube (Pre-Agg Cube)
123
- </option>
124
- <option
125
- key={'druid_metrics_cube'}
126
- value={'druid_metrics_cube'}
127
- >
128
- Druid Metrics Cube (Post-Agg Cube)
129
- </option>
130
- <option key={'spark_sql'} value={'spark_sql'}>
131
- Iceberg Table
132
- </option>
133
- </>
134
- </Field>
135
- </span>
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
- <ErrorMessage name="spark_config" component="span" />
20
- <label htmlFor="SparkConfig">Spark Config</label>
21
- <Field
22
- type="textarea"
23
- style={{ display: 'none' }}
24
- as="textarea"
25
- name="spark_config"
26
- id="SparkConfig"
27
- />
28
- <div role="button" tabIndex={0} className="relative flex bg-[#282a36]">
29
- <CodeMirror
30
- id={'spark_config'}
31
- name={'spark_config'}
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
- </div>
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
  };