datajunction-ui 0.0.1-a44.dev0 → 0.0.1-a44.dev2

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.dev0",
3
+ "version": "0.0.1-a44.dev2",
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
  />
@@ -3,6 +3,7 @@ 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";
6
7
 
7
8
  export default function AddMaterializationPopover({ node, onSubmit }) {
8
9
  const djClient = useContext(DJClientContext).DataJunctionAPI;
@@ -38,7 +39,8 @@ export default function AddMaterializationPopover({ node, onSubmit }) {
38
39
  { setSubmitting, setStatus },
39
40
  ) => {
40
41
  setSubmitting(false);
41
- const config = JSON.parse(values.config);
42
+ const config = {};
43
+ config.spark = values.spark_config;
42
44
  config.lookback_window = values.lookback_window;
43
45
  const response = await djClient.materialize(
44
46
  values.node,
@@ -48,13 +50,12 @@ export default function AddMaterializationPopover({ node, onSubmit }) {
48
50
  config,
49
51
  );
50
52
  if (response.status === 200 || response.status === 201) {
51
- setStatus({ success: 'Saved!' });
53
+ setStatus({ success: response.json.message });
52
54
  } else {
53
55
  setStatus({
54
56
  failure: `${response.json.message}`,
55
57
  });
56
58
  }
57
- onSubmit();
58
59
  // window.location.reload();
59
60
  };
60
61
 
@@ -87,11 +88,14 @@ export default function AddMaterializationPopover({ node, onSubmit }) {
87
88
  <Formik
88
89
  initialValues={{
89
90
  node: node?.name,
90
- job_type: 'spark_sql',
91
+ job_type: node?.type === 'cube' ? 'druid_cube' : 'spark_sql',
91
92
  strategy: 'full',
92
- config: '{"spark": {"spark.executor.memory": "6g"}}',
93
93
  schedule: '@daily',
94
94
  lookback_window: '1 DAY',
95
+ spark_config: {
96
+ "spark.executor.memory": "16g",
97
+ "spark.memory.fraction": "0.3"
98
+ },
95
99
  }}
96
100
  onSubmit={configureMaterialization}
97
101
  >
@@ -104,11 +108,9 @@ export default function AddMaterializationPopover({ node, onSubmit }) {
104
108
  <label htmlFor="job_type">Job Type</label>
105
109
  <Field as="select" name="job_type">
106
110
  <>
107
- {jobs?.map(job => (
108
- <option key={job.name} value={job.name}>
109
- {job.label}
110
- </option>
111
- ))}
111
+ <option key={'druid_measures_cube'} value={'druid_measures_cube'}>Druid Measures Cube (Pre-Agg Cube)</option>
112
+ <option key={'druid_metrics_cube'} value={'druid_metrics_cube'}>Druid Metrics Cube (Post-Agg Cube)</option>
113
+ <option key={'spark_sql'} value={'spark_sql'}>Iceberg Table</option>
112
114
  </>
113
115
  </Field>
114
116
  </span>
@@ -124,9 +126,12 @@ export default function AddMaterializationPopover({ node, onSubmit }) {
124
126
  <label htmlFor="strategy">Strategy</label>
125
127
  <Field as="select" name="strategy">
126
128
  <>
127
- {options.strategies?.map(strategy => (
128
- <option value={strategy.name}>{strategy.label}</option>
129
- ))}
129
+ <option key={'full'} value={'full'}>
130
+ Full
131
+ </option>
132
+ <option key={'incremental_time'} value={'incremental_time'}>
133
+ Incremental Time
134
+ </option>
130
135
  </>
131
136
  </Field>
132
137
  </span>
@@ -154,17 +159,10 @@ export default function AddMaterializationPopover({ node, onSubmit }) {
154
159
  />
155
160
  </div>
156
161
  <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>
162
+ <ConfigField value={{
163
+ "spark.executor.memory": "16g",
164
+ "spark.memory.fraction": "0.3"
165
+ }}/>
168
166
  <button
169
167
  className="add_node"
170
168
  type="submit"
@@ -8,6 +8,7 @@ import { displayMessageAfterSubmit } from '../../../utils/form';
8
8
 
9
9
  export default function LinkDimensionPopover({
10
10
  column,
11
+ referencedDimensionNode,
11
12
  node,
12
13
  options,
13
14
  onSubmit,
@@ -28,15 +29,15 @@ export default function LinkDimensionPopover({
28
29
  };
29
30
  }, [setPopoverAnchor]);
30
31
 
31
- const columnDimension = column.dimension;
32
+ const columnDimension = referencedDimensionNode;
32
33
 
33
34
  const handleSubmit = async (
34
35
  { node, column, dimension },
35
36
  { setSubmitting, setStatus },
36
37
  ) => {
37
38
  setSubmitting(false);
38
- if (columnDimension?.name && dimension === 'Remove') {
39
- await unlinkDimension(node, column, columnDimension?.name, setStatus);
39
+ if (referencedDimensionNode && dimension === 'Remove') {
40
+ await unlinkDimension(node, column, referencedDimensionNode, setStatus);
40
41
  } else {
41
42
  await linkDimension(node, column, dimension, setStatus);
42
43
  }
@@ -93,7 +94,7 @@ export default function LinkDimensionPopover({
93
94
  column: column.name,
94
95
  node: node.name,
95
96
  dimension: '',
96
- currentDimension: column.dimension?.name,
97
+ currentDimension: referencedDimensionNode,
97
98
  }}
98
99
  onSubmit={handleSubmit}
99
100
  >
@@ -110,10 +111,10 @@ export default function LinkDimensionPopover({
110
111
  placeholder="Select dimension to link"
111
112
  className=""
112
113
  defaultValue={
113
- column.dimension
114
+ referencedDimensionNode
114
115
  ? {
115
- value: column.dimension.name,
116
- label: column.dimension.name,
116
+ value: referencedDimensionNode,
117
+ label: referencedDimensionNode,
117
118
  }
118
119
  : ''
119
120
  }
@@ -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
+ };
@@ -12,6 +12,8 @@ export default function NodeColumnTab({ node, djClient }) {
12
12
  const [attributes, setAttributes] = useState([]);
13
13
  const [dimensions, setDimensions] = useState([]);
14
14
  const [columns, setColumns] = useState([]);
15
+ const [links, setLinks] = useState([]);
16
+
15
17
  useEffect(() => {
16
18
  const fetchData = async () => {
17
19
  setColumns(await djClient.columns(node));
@@ -88,92 +90,103 @@ export default function NodeColumnTab({ node, djClient }) {
88
90
  };
89
91
 
90
92
  const columnList = columns => {
91
- return columns.map(col => (
92
- <tr key={col.name}>
93
- <td
94
- className="text-start"
95
- role="columnheader"
96
- aria-label="ColumnName"
97
- aria-hidden="false"
98
- >
99
- {col.name}
100
- </td>
101
- <td>
102
- <span
103
- className=""
104
- role="columnheader"
105
- aria-label="ColumnDisplayName"
106
- aria-hidden="false"
107
- >
108
- {col.display_name}
109
- </span>
110
- </td>
111
- <td>
112
- <span
113
- className={`node_type__${
114
- node.type === 'cube' ? col.type : 'transform'
115
- } badge node_type`}
93
+ return columns.map(col => {
94
+ const dimensionLinks = (links.length > 0 ? links : node?.dimension_links)
95
+ .map(link => [
96
+ link.dimension.name,
97
+ Object.entries(link.foreign_keys).filter(
98
+ entry => entry[0] === node.name + '.' + col.name,
99
+ ),
100
+ ])
101
+ .filter(keys => keys[1].length >= 1);
102
+ const referencedDimensionNode =
103
+ dimensionLinks.length > 0 ? dimensionLinks[0][0] : null;
104
+ return (
105
+ <tr key={col.name}>
106
+ <td
107
+ className="text-start"
116
108
  role="columnheader"
117
- aria-label="ColumnType"
109
+ aria-label="ColumnName"
118
110
  aria-hidden="false"
119
111
  >
120
- {col.type}
121
- </span>
122
- </td>
123
- {node.type !== 'cube' ? (
112
+ {col.name}
113
+ </td>
124
114
  <td>
125
- {col.dimension !== undefined && col.dimension !== null ? (
126
- <>
127
- <a href={`/nodes/${col.dimension.name}`}>
128
- {col.dimension.name}
129
- </a>
130
- <ClientCodePopover code={col.clientCode} />
131
- </>
132
- ) : (
133
- ''
134
- )}{' '}
135
- <LinkDimensionPopover
136
- column={col}
137
- node={node}
138
- options={dimensions}
139
- onSubmit={async () => {
140
- const res = await djClient.node(node.name);
141
- setColumns(res.columns);
142
- }}
143
- />
115
+ <span
116
+ className=""
117
+ role="columnheader"
118
+ aria-label="ColumnDisplayName"
119
+ aria-hidden="false"
120
+ >
121
+ {col.display_name}
122
+ </span>
123
+ </td>
124
+ <td>
125
+ <span
126
+ className={`node_type__${
127
+ node.type === 'cube' ? col.type : 'transform'
128
+ } badge node_type`}
129
+ role="columnheader"
130
+ aria-label="ColumnType"
131
+ aria-hidden="false"
132
+ >
133
+ {col.type}
134
+ </span>
144
135
  </td>
145
- ) : (
146
- ''
147
- )}
148
- {node.type !== 'cube' ? (
136
+ {node.type !== 'cube' ? (
137
+ <td>
138
+ {referencedDimensionNode !== null ? (
139
+ <a href={`/nodes/${referencedDimensionNode}`}>
140
+ {referencedDimensionNode}
141
+ </a>
142
+ ) : (
143
+ ''
144
+ )}
145
+ <LinkDimensionPopover
146
+ column={col}
147
+ referencedDimensionNode={referencedDimensionNode}
148
+ node={node}
149
+ options={dimensions}
150
+ onSubmit={async () => {
151
+ const res = await djClient.node(node.name);
152
+ setColumns(res.columns);
153
+ setLinks(res.dimension_links);
154
+ }}
155
+ />
156
+ </td>
157
+ ) : (
158
+ ''
159
+ )}
160
+ {node.type !== 'cube' ? (
161
+ <td>
162
+ {showColumnAttributes(col)}
163
+ <EditColumnPopover
164
+ column={col}
165
+ node={node}
166
+ options={attributes}
167
+ onSubmit={async () => {
168
+ const res = await djClient.node(node.name);
169
+ setColumns(res.columns);
170
+ }}
171
+ />
172
+ </td>
173
+ ) : (
174
+ ''
175
+ )}
149
176
  <td>
150
- {showColumnAttributes(col)}
151
- <EditColumnPopover
177
+ {showColumnPartition(col)}
178
+ <PartitionColumnPopover
152
179
  column={col}
153
180
  node={node}
154
- options={attributes}
155
181
  onSubmit={async () => {
156
182
  const res = await djClient.node(node.name);
157
183
  setColumns(res.columns);
158
184
  }}
159
185
  />
160
186
  </td>
161
- ) : (
162
- ''
163
- )}
164
- <td>
165
- {showColumnPartition(col)}
166
- <PartitionColumnPopover
167
- column={col}
168
- node={node}
169
- onSubmit={async () => {
170
- const res = await djClient.node(node.name);
171
- setColumns(res.columns);
172
- }}
173
- />
174
- </td>
175
- </tr>
176
- ));
187
+ </tr>
188
+ );
189
+ });
177
190
  };
178
191
 
179
192
  return (
@@ -15,6 +15,8 @@ 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 ? node.dimension_links
19
+ .flatMap(link => Object.keys(link.foreign_keys).map(key => key.split('.').slice(-1))) : [];
18
20
  const column_names = node.columns
19
21
  .map(col => {
20
22
  return {
@@ -23,7 +25,7 @@ const NodeLineage = djNode => {
23
25
  dimension: col.dimension !== null ? col.dimension.name : null,
24
26
  order: primary_key.includes(col.name)
25
27
  ? -1
26
- : col.dimension !== null
28
+ : dimensionLinkForeignKeys.includes(col.name)
27
29
  ? 0
28
30
  : 1,
29
31
  };
@@ -49,27 +51,29 @@ const NodeLineage = djNode => {
49
51
  };
50
52
 
51
53
  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
- };
54
+ return node.dimension_links === undefined ? [] : node.dimension_links
55
+ .flatMap(link => {
56
+ return Object.keys(link.foreign_keys).map(fk => {
57
+ return {
58
+ id: link.dimension.name + '->' + node.name + '=' + link.foreign_keys[fk] + '->' + fk,
59
+ source: link.dimension.name,
60
+ sourceHandle: link.foreign_keys[fk],
61
+ target: node.name,
62
+ targetHandle: fk,
63
+ draggable: true,
64
+ markerStart: {
65
+ type: MarkerType.Arrow,
66
+ width: 20,
67
+ height: 20,
68
+ color: '#b0b9c2',
69
+ },
70
+ style: {
71
+ strokeWidth: 3,
72
+ stroke: '#b0b9c2',
73
+ },
74
+ };
75
+ }
76
+ )
73
77
  });
74
78
  };
75
79
 
@@ -134,7 +134,8 @@ 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 ?
138
+ <AddMaterializationPopover node={node} /> : <></>}
138
139
  {materializations.length > 0 ? (
139
140
  <table
140
141
  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,
@@ -42,6 +42,7 @@ describe('<LinkDimensionPopover />', () => {
42
42
  <DJClientContext.Provider value={mockDjClient}>
43
43
  <LinkDimensionPopover
44
44
  column={column}
45
+ referencedDimensionNode={'default.dimension1'}
45
46
  node={node}
46
47
  options={options}
47
48
  onSubmit={onSubmitMock}
@@ -94,7 +95,6 @@ describe('<LinkDimensionPopover />', () => {
94
95
  };
95
96
  const node = { name: 'default.node1' };
96
97
  const options = [
97
- { value: 'Remove', label: '[Remove dimension link]' },
98
98
  { value: 'default.dimension1', label: 'Dimension 1' },
99
99
  { value: 'default.dimension2', label: 'Dimension 2' },
100
100
  ];
@@ -117,6 +117,7 @@ describe('<LinkDimensionPopover />', () => {
117
117
  <DJClientContext.Provider value={mockDjClient}>
118
118
  <LinkDimensionPopover
119
119
  column={column}
120
+ referencedDimensionNode={'default.dimension1'}
120
121
  node={node}
121
122
  options={options}
122
123
  onSubmit={onSubmitMock}
@@ -148,7 +149,7 @@ describe('<LinkDimensionPopover />', () => {
148
149
 
149
150
  // Click on the 'Remove' option and save
150
151
  fireEvent.keyDown(linkDimension.firstChild, { key: 'ArrowDown' });
151
- fireEvent.click(screen.getByText('[Remove dimension link]'));
152
+ fireEvent.click(screen.getByText('[Remove Dimension]'));
152
153
  fireEvent.click(getByText('Save'));
153
154
  getByText('Save').click();
154
155
 
@@ -98,6 +98,10 @@ describe('<NodeColumnTab />', () => {
98
98
  'default.contractor.contractor_id = default.repair_orders.contractor_id',
99
99
  join_cardinality: 'one_to_one',
100
100
  role: 'contractor',
101
+ foreign_keys: {
102
+ 'default.repair_orders.contractor_id':
103
+ 'default.contractor.contractor_id',
104
+ },
101
105
  },
102
106
  ],
103
107
  };
@@ -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',
@@ -24,11 +24,19 @@ export function Root() {
24
24
  <div className="container d-flex align-items-center justify-content-between">
25
25
  <div className="header">
26
26
  <div className="logo">
27
- <a href={'/'} style={{textTransform: 'none', textDecoration: 'none', color: '#000'}}>
27
+ <a
28
+ href={'/'}
29
+ style={{
30
+ textTransform: 'none',
31
+ textDecoration: 'none',
32
+ color: '#000',
33
+ }}
34
+ >
28
35
  <h2>
29
- <DJLogo />
30
- Data<b>Junction</b>
31
- </h2></a>
36
+ <DJLogo />
37
+ Data<b>Junction</b>
38
+ </h2>
39
+ </a>
32
40
  </div>
33
41
  <Search />
34
42
  <div className="menu">
@@ -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 {