datajunction-ui 0.0.1-a93 → 0.0.1-a95

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.1a93",
3
+ "version": "0.0.1a95",
4
4
  "description": "DataJunction Metrics Platform UI",
5
5
  "module": "src/index.tsx",
6
6
  "repository": {
@@ -24,11 +24,17 @@ export const MetricMetadataFields = () => {
24
24
  }, [djClient]);
25
25
 
26
26
  return (
27
- <>
28
- <div
29
- className="MetricDirectionInput NodeCreationInput"
30
- style={{ width: '25%' }}
31
- >
27
+ <div
28
+ style={{
29
+ borderRadius: '8px',
30
+ padding: '10px 10px 20px 10px',
31
+ margin: '32px 0',
32
+ background: '#f9f9f9',
33
+ width: 'max-content',
34
+ display: 'flex',
35
+ }}
36
+ >
37
+ <div style={{ margin: '15px 25px' }}>
32
38
  <ErrorMessage name="metric_direction" component="span" />
33
39
  <label htmlFor="MetricDirection">Metric Direction</label>
34
40
  <Field as="select" name="metric_direction" id="MetricDirection">
@@ -40,10 +46,7 @@ export const MetricMetadataFields = () => {
40
46
  ))}
41
47
  </Field>
42
48
  </div>
43
- <div
44
- className="MetricUnitInput NodeCreationInput"
45
- style={{ width: '25%' }}
46
- >
49
+ <div style={{ margin: '15px 25px' }}>
47
50
  <ErrorMessage name="metric_unit" component="span" />
48
51
  <label htmlFor="MetricUnit">Metric Unit</label>
49
52
  <Field as="select" name="metric_unit" id="MetricUnit">
@@ -55,6 +58,18 @@ export const MetricMetadataFields = () => {
55
58
  ))}
56
59
  </Field>
57
60
  </div>
58
- </>
61
+ <div style={{ margin: '15px 25px' }}>
62
+ <ErrorMessage name="significant_digits" component="span" />
63
+ <label htmlFor="SignificantDigits">Significant Digits</label>
64
+ <Field as="select" name="significant_digits" id="SignificantDigits">
65
+ <option value=""></option>
66
+ {[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(val => (
67
+ <option value={val} key={val}>
68
+ {val}
69
+ </option>
70
+ ))}
71
+ </Field>
72
+ </div>
73
+ </div>
59
74
  );
60
75
  };
@@ -67,7 +67,9 @@ describe('AddEditNodePage submission failed', () => {
67
67
 
68
68
  it('for editing a node', async () => {
69
69
  const mockDjClient = initializeMockDJClient();
70
- mockDjClient.DataJunctionAPI.node.mockReturnValue(mocks.mockMetricNode);
70
+ mockDjClient.DataJunctionAPI.getNodeForEditing.mockReturnValue(
71
+ mocks.mockGetMetricNode,
72
+ );
71
73
  mockDjClient.DataJunctionAPI.patchNode.mockReturnValue({
72
74
  status: 500,
73
75
  json: { message: 'Update failed' },
@@ -152,7 +152,9 @@ describe('AddEditNodePage submission succeeded', () => {
152
152
  it('for editing a transform or dimension node', async () => {
153
153
  const mockDjClient = initializeMockDJClient();
154
154
 
155
- mockDjClient.DataJunctionAPI.node.mockReturnValue(mocks.mockTransformNode);
155
+ mockDjClient.DataJunctionAPI.getNodeForEditing.mockReturnValue(
156
+ mocks.mockGetTransformNode,
157
+ );
156
158
  mockDjClient.DataJunctionAPI.patchNode = jest.fn();
157
159
  mockDjClient.DataJunctionAPI.patchNode.mockReturnValue({
158
160
  status: 201,
@@ -189,8 +191,9 @@ describe('AddEditNodePage submission succeeded', () => {
189
191
  'SELECT repair_order_id, municipality_id, hard_hat_id, dispatcher_id FROM default.repair_orders',
190
192
  'published',
191
193
  [],
192
- undefined,
193
- undefined,
194
+ '',
195
+ '',
196
+ '',
194
197
  undefined,
195
198
  );
196
199
  expect(mockDjClient.DataJunctionAPI.tagsNode).toBeCalledTimes(1);
@@ -211,8 +214,9 @@ describe('AddEditNodePage submission succeeded', () => {
211
214
  it('for editing a metric node', async () => {
212
215
  const mockDjClient = initializeMockDJClient();
213
216
 
214
- mockDjClient.DataJunctionAPI.node.mockReturnValue(mocks.mockMetricNode);
215
- mockDjClient.DataJunctionAPI.metric.mockReturnValue(mocks.mockMetricNode);
217
+ mockDjClient.DataJunctionAPI.getNodeForEditing.mockReturnValue(
218
+ mocks.mockGetMetricNode,
219
+ );
216
220
  mockDjClient.DataJunctionAPI.patchNode = jest.fn();
217
221
  mockDjClient.DataJunctionAPI.patchNode.mockReturnValue({
218
222
  status: 201,
@@ -248,9 +252,10 @@ describe('AddEditNodePage submission succeeded', () => {
248
252
  'Number of repair orders!!!',
249
253
  'SELECT count(repair_order_id) FROM default.repair_orders',
250
254
  'published',
251
- [],
255
+ ['repair_order_id', 'country'],
252
256
  'neutral',
253
257
  'unitless',
258
+ 5,
254
259
  undefined,
255
260
  );
256
261
  expect(mockDjClient.DataJunctionAPI.tagsNode).toBeCalledTimes(1);
@@ -35,6 +35,7 @@ export const initializeMockDJClient = () => {
35
35
  ];
36
36
  },
37
37
  metrics: {},
38
+ getNodeForEditing: jest.fn(),
38
39
  namespaces: () => {
39
40
  return [
40
41
  {
@@ -146,7 +147,9 @@ describe('AddEditNodePage', () => {
146
147
 
147
148
  it('Edit node page renders with the selected node', async () => {
148
149
  const mockDjClient = initializeMockDJClient();
149
- mockDjClient.DataJunctionAPI.node.mockReturnValue(mocks.mockMetricNode);
150
+ mockDjClient.DataJunctionAPI.getNodeForEditing.mockReturnValue(
151
+ mocks.mockGetMetricNode,
152
+ );
150
153
 
151
154
  const element = testElement(mockDjClient);
152
155
  renderEditNode(element);
@@ -175,17 +178,12 @@ describe('AddEditNodePage', () => {
175
178
 
176
179
  it('Verify edit page node not found', async () => {
177
180
  const mockDjClient = initializeMockDJClient();
178
- mockDjClient.DataJunctionAPI.node = jest.fn();
179
- mockDjClient.DataJunctionAPI.node.mockReturnValue({
180
- message: 'A node with name `default.num_repair_orders` does not exist.',
181
- errors: [],
182
- warnings: [],
183
- });
181
+ mockDjClient.DataJunctionAPI.getNodeForEditing.mockReturnValue(null);
184
182
  const element = testElement(mockDjClient);
185
183
  renderEditNode(element);
186
184
 
187
185
  await waitFor(() => {
188
- expect(mockDjClient.DataJunctionAPI.node).toBeCalledTimes(1);
186
+ expect(mockDjClient.DataJunctionAPI.getNodeForEditing).toBeCalledTimes(1);
189
187
  expect(
190
188
  screen.getByText('Node default.num_repair_orders does not exist!'),
191
189
  ).toBeInTheDocument();
@@ -194,18 +192,14 @@ describe('AddEditNodePage', () => {
194
192
 
195
193
  it('Verify only transforms, metrics, and dimensions can be edited', async () => {
196
194
  const mockDjClient = initializeMockDJClient();
197
- mockDjClient.DataJunctionAPI.node = jest.fn();
198
- mockDjClient.DataJunctionAPI.node.mockReturnValue({
199
- namespace: 'default',
200
- type: 'source',
201
- name: 'default.repair_orders',
202
- display_name: 'Default: Repair Orders',
203
- });
195
+ mockDjClient.DataJunctionAPI.getNodeForEditing.mockReturnValue(
196
+ mocks.mockGetSourceNode,
197
+ );
204
198
  const element = testElement(mockDjClient);
205
199
  renderEditNode(element);
206
200
 
207
201
  await waitFor(() => {
208
- expect(mockDjClient.DataJunctionAPI.node).toBeCalledTimes(1);
202
+ expect(mockDjClient.DataJunctionAPI.getNodeForEditing).toBeCalledTimes(1);
209
203
  expect(
210
204
  screen.getByText(
211
205
  'Node default.num_repair_orders is of type source and cannot be edited',
@@ -152,6 +152,7 @@ export function AddEditNodePage({ extensions = {} }) {
152
152
  values.primary_key ? primaryKeyToList(values.primary_key) : null,
153
153
  values.metric_direction,
154
154
  values.metric_unit,
155
+ values.significant_digits,
155
156
  values.required_dimensions,
156
157
  );
157
158
  const tagsResponse = await djClient.tagsNode(
@@ -187,26 +188,35 @@ export function AddEditNodePage({ extensions = {} }) {
187
188
  };
188
189
 
189
190
  const getExistingNodeData = async name => {
190
- const data = await djClient.node(name);
191
- if (data.type === 'metric') {
192
- const metric = await djClient.metric(name);
193
- data.upstream_node = metric.upstream_node;
194
- data.expression = metric.expression;
195
- data.required_dimensions = metric.required_dimensions;
191
+ const node = await djClient.getNodeForEditing(name);
192
+ if (node === null) {
193
+ return { message: `Node ${name} does not exist` };
196
194
  }
197
- return data;
198
- };
195
+ const baseData = {
196
+ name: node.name,
197
+ type: node.type.toLowerCase(),
198
+ display_name: node.current.displayName,
199
+ description: node.current.description,
200
+ primary_key: node.current.primaryKey,
201
+ query: node.current.query,
202
+ tags: node.tags,
203
+ mode: node.current.mode.toLowerCase(),
204
+ };
199
205
 
200
- const primaryKeyFromNode = node => {
201
- return node.columns
202
- .filter(
203
- col =>
204
- col.attributes &&
205
- col.attributes.filter(
206
- attr => attr.attribute_type.name === 'primary_key',
207
- ).length > 0,
208
- )
209
- .map(col => col.name);
206
+ if (node.type === 'METRIC') {
207
+ return {
208
+ ...baseData,
209
+ metric_direction: node.current.metricMetadata?.direction?.toLowerCase(),
210
+ metric_unit: node.current.metricMetadata?.unit?.name?.toLowerCase(),
211
+ significant_digits: node.current.metricMetadata?.significantDigits,
212
+ required_dimensions: node.current.requiredDimensions.map(
213
+ dim => dim.name,
214
+ ),
215
+ upstream_node: node.current.parents[0]?.name,
216
+ aggregate_expression: node.current.metricMetadata?.expression,
217
+ };
218
+ }
219
+ return baseData;
210
220
  };
211
221
 
212
222
  const runValidityChecks = (data, setNode, setMessage) => {
@@ -216,7 +226,6 @@ export function AddEditNodePage({ extensions = {} }) {
216
226
  setMessage(`Node ${name} does not exist!`);
217
227
  return;
218
228
  }
219
-
220
229
  // Check if node type can be edited
221
230
  if (!nodeCanBeEdited(data.type)) {
222
231
  setNode(null);
@@ -245,14 +254,14 @@ export function AddEditNodePage({ extensions = {} }) {
245
254
  'primary_key',
246
255
  'mode',
247
256
  'tags',
248
- 'expression',
257
+ 'aggregate_expression',
249
258
  'upstream_node',
259
+ 'metric_unit',
260
+ 'metric_direction',
261
+ 'significant_digits',
250
262
  ];
251
- const primaryKey = primaryKeyFromNode(data);
252
263
  fields.forEach(field => {
253
- if (field === 'primary_key') {
254
- setFieldValue(field, primaryKey);
255
- } else if (field === 'tags') {
264
+ if (field === 'tags') {
256
265
  setFieldValue(
257
266
  field,
258
267
  data[field].map(tag => tag.name),
@@ -261,21 +270,6 @@ export function AddEditNodePage({ extensions = {} }) {
261
270
  setFieldValue(field, data[field] || '', false);
262
271
  }
263
272
  });
264
- if (data.metric_metadata?.direction) {
265
- setFieldValue('metric_direction', data.metric_metadata.direction);
266
- }
267
- if (data.metric_metadata?.unit) {
268
- setFieldValue(
269
- 'metric_unit',
270
- data.metric_metadata.unit.name.toLowerCase(),
271
- );
272
- }
273
- if (data.expression) {
274
- setFieldValue('aggregate_expression', data.expression);
275
- }
276
- if (data.upstream_node) {
277
- setFieldValue('upstream_node', data.upstream_node);
278
- }
279
273
  setNode(data);
280
274
 
281
275
  // For react-select fields, we have to explicitly set the entire
@@ -283,13 +277,13 @@ export function AddEditNodePage({ extensions = {} }) {
283
277
  setSelectTags(
284
278
  <TagsField
285
279
  defaultValue={data.tags.map(t => {
286
- return { value: t.name, label: t.display_name };
280
+ return { value: t.name, label: t.displayName };
287
281
  })}
288
282
  />,
289
283
  );
290
284
  setSelectPrimaryKey(
291
285
  <ColumnsSelect
292
- defaultValue={primaryKey}
286
+ defaultValue={data.primary_key}
293
287
  fieldName="primary_key"
294
288
  label="Primary Key"
295
289
  isMulti={true}
@@ -402,7 +396,11 @@ export function AddEditNodePage({ extensions = {} }) {
402
396
  {nodeType === 'metric' || node.type === 'metric' ? (
403
397
  <MetricQueryField
404
398
  djClient={djClient}
405
- value={node.expression ? node.expression : ''}
399
+ value={
400
+ node.aggregate_expression
401
+ ? node.aggregate_expression
402
+ : ''
403
+ }
406
404
  />
407
405
  ) : (
408
406
  <NodeQueryField
@@ -181,6 +181,17 @@ export default function NodeInfoTab({ node }) {
181
181
  : 'None'}
182
182
  </p>
183
183
  </div>
184
+ <div style={{ marginRight: '2rem' }}>
185
+ <h6 className="mb-0 w-100">Significant Digits</h6>
186
+ <p
187
+ className="mb-0 opacity-75"
188
+ role="dialog"
189
+ aria-hidden="false"
190
+ aria-label="SignificantDigits"
191
+ >
192
+ {node?.metric_metadata?.significantDigits || 'None'}
193
+ </p>
194
+ </div>
184
195
  </div>
185
196
  </div>
186
197
  ) : (
@@ -128,6 +128,59 @@ export const DataJunctionAPI = {
128
128
  return data;
129
129
  },
130
130
 
131
+ getNodeForEditing: async function (name) {
132
+ const query = `
133
+ query GetNodeForEditing($name: String!) {
134
+ findNodes (names: [$name]) {
135
+ name
136
+ type
137
+ current {
138
+ displayName
139
+ description
140
+ primaryKey
141
+ query
142
+ parents { name }
143
+ metricMetadata {
144
+ direction
145
+ unit { name }
146
+ expression
147
+ significantDigits
148
+ incompatibleDruidFunctions
149
+ }
150
+ requiredDimensions {
151
+ name
152
+ }
153
+ mode
154
+ }
155
+ tags {
156
+ name
157
+ displayName
158
+ }
159
+ }
160
+ }
161
+ `;
162
+
163
+ const results = await (
164
+ await fetch(DJ_GQL, {
165
+ method: 'POST',
166
+ headers: {
167
+ 'Content-Type': 'application/json',
168
+ },
169
+ credentials: 'include',
170
+ body: JSON.stringify({
171
+ query,
172
+ variables: {
173
+ name: name,
174
+ },
175
+ }),
176
+ })
177
+ ).json();
178
+ if (results.data.findNodes.length === 0) {
179
+ return null;
180
+ }
181
+ return results.data.findNodes[0];
182
+ },
183
+
131
184
  getMetric: async function (name) {
132
185
  const query = `
133
186
  query GetMetric($name: String!) {
@@ -139,6 +192,7 @@ export const DataJunctionAPI = {
139
192
  direction
140
193
  unit { name }
141
194
  expression
195
+ significantDigits
142
196
  incompatibleDruidFunctions
143
197
  }
144
198
  requiredDimensions {
@@ -267,6 +321,7 @@ export const DataJunctionAPI = {
267
321
  primary_key,
268
322
  metric_direction,
269
323
  metric_unit,
324
+ significant_digits,
270
325
  required_dimensions,
271
326
  ) {
272
327
  try {
@@ -275,6 +330,7 @@ export const DataJunctionAPI = {
275
330
  ? {
276
331
  direction: metric_direction,
277
332
  unit: metric_unit,
333
+ significant_digits: significant_digits || null,
278
334
  }
279
335
  : null;
280
336
  const response = await fetch(`${DJ_URL}/nodes/${name}`, {
@@ -203,6 +203,7 @@ describe('DataJunctionAPI', () => {
203
203
  metric_metadata: {
204
204
  direction: 'neutral',
205
205
  unit: '',
206
+ significant_digits: null,
206
207
  },
207
208
  }),
208
209
  credentials: 'include',
@@ -278,12 +278,82 @@ export const mocks = {
278
278
  label: 'Unitless',
279
279
  },
280
280
  direction: 'neutral',
281
+ max_decimal_exponent: null,
282
+ min_decimal_exponent: null,
283
+ significant_digits: 4,
281
284
  },
282
285
  upstream_node: 'default.repair_orders',
283
286
  expression: 'count(repair_order_id)',
284
287
  aggregate_expression: 'count(repair_order_id)',
285
288
  required_dimensions: [],
286
289
  },
290
+
291
+ mockGetSourceNode: {
292
+ name: 'default.num_repair_orders',
293
+ type: 'SOURCE',
294
+ current: {
295
+ displayName: 'source.prodhive.dse.playback_f',
296
+ description:
297
+ 'This source node was automatically created as a registered table.',
298
+ primaryKey: [],
299
+ parents: [],
300
+ metricMetadata: null,
301
+ requiredDimensions: [],
302
+ mode: 'PUBLISHED',
303
+ },
304
+ tags: [],
305
+ },
306
+
307
+ mockGetMetricNode: {
308
+ name: 'default.num_repair_orders',
309
+ type: 'METRIC',
310
+ current: {
311
+ displayName: 'Default: Num Repair Orders',
312
+ description: 'Number of repair orders',
313
+ primaryKey: ['repair_order_id', 'country'],
314
+ query:
315
+ 'SELECT count(repair_order_id) default_DOT_num_repair_orders FROM default.repair_orders',
316
+ parents: [
317
+ {
318
+ name: 'default.repair_orders',
319
+ },
320
+ ],
321
+ metricMetadata: {
322
+ direction: 'NEUTRAL',
323
+ unit: {
324
+ name: 'UNITLESS',
325
+ },
326
+ expression: 'count(repair_order_id)',
327
+ significantDigits: 5,
328
+ incompatibleDruidFunctions: ['IF'],
329
+ },
330
+ requiredDimensions: [],
331
+ mode: 'PUBLISHED',
332
+ },
333
+ tags: [{ name: 'purpose', displayName: 'Purpose' }],
334
+ },
335
+
336
+ mockGetTransformNode: {
337
+ name: 'default.repair_order_transform',
338
+ type: 'TRANSFORM',
339
+ current: {
340
+ displayName: 'Default: Repair Order Transform',
341
+ description: 'Repair order dimension',
342
+ primaryKey: [],
343
+ query:
344
+ 'SELECT repair_order_id, municipality_id, hard_hat_id, dispatcher_id FROM default.repair_orders',
345
+ parents: [
346
+ {
347
+ name: 'default.repair_orders',
348
+ },
349
+ ],
350
+ metricMetadata: null,
351
+ requiredDimensions: [],
352
+ mode: 'PUBLISHED',
353
+ },
354
+ tags: [],
355
+ },
356
+
287
357
  attributes: [
288
358
  {
289
359
  uniqueness_scope: [],