datajunction-ui 0.0.1-a44.dev1 → 0.0.1-a44.dev4

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-a44.dev1",
3
+ "version": "0.0.1-a44.dev4",
4
4
  "description": "DataJunction Metrics Platform UI",
5
5
  "module": "src/index.tsx",
6
6
  "repository": {
@@ -162,7 +162,7 @@
162
162
  ],
163
163
  "coverageThreshold": {
164
164
  "global": {
165
- "statements": 90,
165
+ "statements": 89,
166
166
  "branches": 75,
167
167
  "lines": 90,
168
168
  "functions": 85
@@ -68,7 +68,7 @@ export function DJNode({ id, data }) {
68
68
  {data.type === 'source' ? data.table : data.display_name}
69
69
  </a>
70
70
  <Collapse
71
- collapsed={true}
71
+ collapsed={data.is_current && data.type != 'metric' ? false : true}
72
72
  text={data.type !== 'metric' ? 'columns' : 'dimensions'}
73
73
  data={data}
74
74
  />
@@ -101,9 +101,7 @@ describe('AddEditNodePage submission failed', () => {
101
101
  status: 404,
102
102
  });
103
103
 
104
- expect(
105
- await screen.getByText('Update failed, Some tags were not found'),
106
- ).toBeInTheDocument();
104
+ expect(await screen.getByText('Update failed')).toBeInTheDocument();
107
105
  });
108
106
  }, 60000);
109
107
  });
@@ -24,6 +24,7 @@ import { DisplayNameField } from './DisplayNameField';
24
24
  import { DescriptionField } from './DescriptionField';
25
25
  import { NodeModeField } from './NodeModeField';
26
26
  import { RequiredDimensionsSelect } from './RequiredDimensionsSelect';
27
+ import LoadingIcon from '../../icons/LoadingIcon';
27
28
 
28
29
  class Action {
29
30
  static Add = new Action('add');
@@ -63,19 +64,18 @@ export function AddEditNodePage() {
63
64
  return errors;
64
65
  };
65
66
 
66
- const handleSubmit = (values, { setSubmitting, setStatus }) => {
67
+ const handleSubmit = async (values, { setSubmitting, setStatus }) => {
67
68
  if (action === Action.Add) {
68
- setTimeout(() => {
69
- createNode(values, setStatus);
69
+ await createNode(values, setStatus).then(_ => {
70
+ window.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
70
71
  setSubmitting(false);
71
- }, 400);
72
+ });
72
73
  } else {
73
- setTimeout(() => {
74
- patchNode(values, setStatus);
74
+ await patchNode(values, setStatus).then(_ => {
75
+ window.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
75
76
  setSubmitting(false);
76
- }, 400);
77
+ });
77
78
  }
78
- window.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
79
79
  };
80
80
 
81
81
  const pageTitle =
@@ -169,7 +169,7 @@ export function AddEditNodePage() {
169
169
  });
170
170
  } else {
171
171
  setStatus({
172
- failure: `${json.message}, ${tagsResponse.json.message}`,
172
+ failure: `${json.message}`,
173
173
  });
174
174
  }
175
175
  };
@@ -410,7 +410,12 @@ export function AddEditNodePage() {
410
410
  <NodeModeField />
411
411
 
412
412
  <button type="submit" disabled={isSubmitting}>
413
- {action === Action.Add ? 'Create' : 'Save'} {nodeType}
413
+ {isSubmitting ? (
414
+ <LoadingIcon />
415
+ ) : (
416
+ (action === Action.Add ? 'Create ' : 'Save ') +
417
+ (nodeType ? nodeType : '')
418
+ )}
414
419
  </button>
415
420
  </>
416
421
  )}
@@ -3,6 +3,8 @@ import * as React from 'react';
3
3
  import DJClientContext from '../../providers/djclient';
4
4
  import { ErrorMessage, Field, Form, Formik } from 'formik';
5
5
  import { displayMessageAfterSubmit, labelize } from '../../../utils/form';
6
+ import { ConfigField } from './MaterializationConfigField';
7
+ import LoadingIcon from '../../icons/LoadingIcon';
6
8
 
7
9
  export default function AddMaterializationPopover({ node, onSubmit }) {
8
10
  const djClient = useContext(DJClientContext).DataJunctionAPI;
@@ -33,29 +35,34 @@ export default function AddMaterializationPopover({ node, onSubmit }) {
33
35
  };
34
36
  }, [djClient, setPopoverAnchor]);
35
37
 
36
- const configureMaterialization = async (
37
- values,
38
- { setSubmitting, setStatus },
39
- ) => {
40
- setSubmitting(false);
41
- const config = JSON.parse(values.config);
38
+ const materialize = async (values, setStatus) => {
39
+ const config = {};
40
+ config.spark = values.spark_config;
42
41
  config.lookback_window = values.lookback_window;
43
- const response = await djClient.materialize(
42
+ const { status, json } = await djClient.materialize(
44
43
  values.node,
45
44
  values.job_type,
46
45
  values.strategy,
47
46
  values.schedule,
48
47
  config,
49
48
  );
50
- if (response.status === 200 || response.status === 201) {
51
- setStatus({ success: 'Saved!' });
49
+ if (status === 200 || status === 201) {
50
+ setStatus({ success: json.message });
52
51
  } else {
53
52
  setStatus({
54
- failure: `${response.json.message}`,
53
+ failure: `${json.message}`,
55
54
  });
56
55
  }
57
- onSubmit();
58
- // window.location.reload();
56
+ };
57
+
58
+ const configureMaterialization = async (
59
+ values,
60
+ { setSubmitting, setStatus },
61
+ ) => {
62
+ await materialize(values, setStatus).then(_ => {
63
+ window.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
64
+ setSubmitting(false);
65
+ });
59
66
  };
60
67
 
61
68
  return (
@@ -87,11 +94,15 @@ export default function AddMaterializationPopover({ node, onSubmit }) {
87
94
  <Formik
88
95
  initialValues={{
89
96
  node: node?.name,
90
- job_type: 'spark_sql',
97
+ job_type:
98
+ node?.type === 'cube' ? 'druid_metrics_cube' : 'spark_sql',
91
99
  strategy: 'full',
92
- config: '{"spark": {"spark.executor.memory": "6g"}}',
93
100
  schedule: '@daily',
94
101
  lookback_window: '1 DAY',
102
+ spark_config: {
103
+ 'spark.executor.memory': '16g',
104
+ 'spark.memory.fraction': '0.3',
105
+ },
95
106
  }}
96
107
  onSubmit={configureMaterialization}
97
108
  >
@@ -104,11 +115,21 @@ export default function AddMaterializationPopover({ node, onSubmit }) {
104
115
  <label htmlFor="job_type">Job Type</label>
105
116
  <Field as="select" name="job_type">
106
117
  <>
107
- {jobs?.map(job => (
108
- <option key={job.name} value={job.name}>
109
- {job.label}
110
- </option>
111
- ))}
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>
112
133
  </>
113
134
  </Field>
114
135
  </span>
@@ -124,9 +145,15 @@ export default function AddMaterializationPopover({ node, onSubmit }) {
124
145
  <label htmlFor="strategy">Strategy</label>
125
146
  <Field as="select" name="strategy">
126
147
  <>
127
- {options.strategies?.map(strategy => (
128
- <option value={strategy.name}>{strategy.label}</option>
129
- ))}
148
+ <option key={'full'} value={'full'}>
149
+ Full
150
+ </option>
151
+ <option
152
+ key={'incremental_time'}
153
+ value={'incremental_time'}
154
+ >
155
+ Incremental Time
156
+ </option>
130
157
  </>
131
158
  </Field>
132
159
  </span>
@@ -154,24 +181,20 @@ export default function AddMaterializationPopover({ node, onSubmit }) {
154
181
  />
155
182
  </div>
156
183
  <br />
157
- <div className="DescriptionInput">
158
- <ErrorMessage name="description" component="span" />
159
- <label htmlFor="Config">Config</label>
160
- <Field
161
- type="textarea"
162
- as="textarea"
163
- name="config"
164
- id="Config"
165
- placeholder="Optional engine-specific configuration (i.e., Spark conf etc)"
166
- />
167
- </div>
184
+ <ConfigField
185
+ value={{
186
+ 'spark.executor.memory': '16g',
187
+ 'spark.memory.fraction': '0.3',
188
+ }}
189
+ />
168
190
  <button
169
191
  className="add_node"
170
192
  type="submit"
171
193
  aria-label="SaveEditColumn"
172
194
  aria-hidden="false"
195
+ disabled={isSubmitting}
173
196
  >
174
- Save
197
+ {isSubmitting ? <LoadingIcon /> : 'Save'}
175
198
  </button>
176
199
  </Form>
177
200
  );
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Materialization configuration field.
3
+ */
4
+ import React from 'react';
5
+ import { ErrorMessage, Field, useFormikContext } from 'formik';
6
+ import CodeMirror from '@uiw/react-codemirror';
7
+ import { langs } from '@uiw/codemirror-extensions-langs';
8
+
9
+ export const ConfigField = ({ djClient, value }) => {
10
+ const formik = useFormikContext();
11
+ const jsonExt = langs.json();
12
+
13
+ const updateFormik = val => {
14
+ formik.setFieldValue('spark_config', val);
15
+ };
16
+
17
+ return (
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
+ }}
49
+ />
50
+ </div>
51
+ </div>
52
+ );
53
+ };
@@ -15,6 +15,11 @@ const NodeLineage = djNode => {
15
15
  col.attributes.some(attr => attr.attribute_type.name === 'primary_key'),
16
16
  )
17
17
  .map(col => col.name);
18
+ const dimensionLinkForeignKeys = node.dimension_links
19
+ ? node.dimension_links.flatMap(link =>
20
+ Object.keys(link.foreign_keys).map(key => key.split('.').slice(-1)),
21
+ )
22
+ : [];
18
23
  const column_names = node.columns
19
24
  .map(col => {
20
25
  return {
@@ -23,7 +28,7 @@ const NodeLineage = djNode => {
23
28
  dimension: col.dimension !== null ? col.dimension.name : null,
24
29
  order: primary_key.includes(col.name)
25
30
  ? -1
26
- : col.dimension !== null
31
+ : dimensionLinkForeignKeys.includes(col.name)
27
32
  ? 0
28
33
  : 1,
29
34
  };
@@ -49,28 +54,37 @@ const NodeLineage = djNode => {
49
54
  };
50
55
 
51
56
  const dimensionEdges = node => {
52
- return node.columns
53
- .filter(col => col.dimension)
54
- .map(col => {
55
- return {
56
- id: col.dimension.name + '->' + node.name + '.' + col.name,
57
- source: col.dimension.name,
58
- sourceHandle: col.dimension.name,
59
- target: node.name,
60
- targetHandle: node.name + '.' + col.name,
61
- draggable: true,
62
- markerStart: {
63
- type: MarkerType.Arrow,
64
- width: 20,
65
- height: 20,
66
- color: '#b0b9c2',
67
- },
68
- style: {
69
- strokeWidth: 3,
70
- stroke: '#b0b9c2',
71
- },
72
- };
73
- });
57
+ return node.dimension_links === undefined
58
+ ? []
59
+ : node.dimension_links.flatMap(link => {
60
+ return Object.keys(link.foreign_keys).map(fk => {
61
+ return {
62
+ id:
63
+ link.dimension.name +
64
+ '->' +
65
+ node.name +
66
+ '=' +
67
+ link.foreign_keys[fk] +
68
+ '->' +
69
+ fk,
70
+ source: link.dimension.name,
71
+ sourceHandle: link.foreign_keys[fk],
72
+ target: node.name,
73
+ targetHandle: fk,
74
+ draggable: true,
75
+ markerStart: {
76
+ type: MarkerType.Arrow,
77
+ width: 20,
78
+ height: 20,
79
+ color: '#b0b9c2',
80
+ },
81
+ style: {
82
+ strokeWidth: 3,
83
+ stroke: '#b0b9c2',
84
+ },
85
+ };
86
+ });
87
+ });
74
88
  };
75
89
 
76
90
  const parentEdges = node => {
@@ -134,7 +134,7 @@ export default function NodeMaterializationTab({ node, djClient }) {
134
134
  <div className="table-vertical">
135
135
  <div>
136
136
  <h2>Materializations</h2>
137
- <AddMaterializationPopover node={node} />
137
+ {node ? <AddMaterializationPopover node={node} /> : <></>}
138
138
  {materializations.length > 0 ? (
139
139
  <table
140
140
  className="card-inner-table table"
@@ -17,6 +17,9 @@ describe('<AddMaterializationPopover />', () => {
17
17
  const onSubmitMock = jest.fn();
18
18
  mockDjClient.DataJunctionAPI.materialize.mockReturnValue({
19
19
  status: 201,
20
+ json: {
21
+ message: 'Saved!',
22
+ },
20
23
  });
21
24
  mockDjClient.DataJunctionAPI.materializationInfo.mockReturnValue({
22
25
  status: 200,
@@ -73,6 +73,7 @@ describe('<NodeLineage />', () => {
73
73
  parents: [],
74
74
  created_at: '2023-08-21T16:48:52.970554+00:00',
75
75
  tags: [],
76
+ dimension_links: [],
76
77
  },
77
78
  {
78
79
  namespace: 'default',
@@ -98,6 +99,7 @@ describe('<NodeLineage />', () => {
98
99
  query:
99
100
  '\n SELECT\n dateint,\n month,\n year,\n day\n FROM default.date\n ',
100
101
  availability: null,
102
+ dimension_links: [],
101
103
  columns: [
102
104
  {
103
105
  name: 'dateint',
@@ -165,6 +167,7 @@ describe('<NodeLineage />', () => {
165
167
  query:
166
168
  '\n SELECT\n hard_hat_id,\n last_name,\n first_name,\n title,\n birth_date,\n hire_date,\n address,\n city,\n state,\n postal_code,\n country,\n manager,\n contractor_id\n FROM default.hard_hats\n ',
167
169
  availability: null,
170
+ dimension_links: [],
168
171
  columns: [
169
172
  {
170
173
  name: 'hard_hat_id',
@@ -292,6 +295,7 @@ describe('<NodeLineage />', () => {
292
295
  query:
293
296
  'SELECT avg(price) default_DOT_avg_repair_price \n FROM default.repair_order_details\n\n',
294
297
  availability: null,
298
+ dimension_links: [],
295
299
  columns: [
296
300
  {
297
301
  name: 'default_DOT_avg_repair_price',
@@ -241,7 +241,7 @@ table {
241
241
  }
242
242
  tr {
243
243
  display: table-row;
244
- vertical-align: inherit;
244
+ vertical-align: top;
245
245
  border-color: inherit;
246
246
  }
247
247
  .card-table {
@@ -339,7 +339,7 @@ tr {
339
339
  .table thead th,
340
340
  td,
341
341
  tbody th {
342
- vertical-align: middle;
342
+ vertical-align: top;
343
343
  text-align: left;
344
344
  }
345
345
  .table [data-sort],
@@ -371,6 +371,7 @@ tbody th {
371
371
  border-bottom: 0;
372
372
  padding: 1rem;
373
373
  max-width: 25rem;
374
+ vertical-align: top;
374
375
  }
375
376
  .card-inner-table td,
376
377
  .card-inner-table th {