datajunction-ui 0.0.1-a42.dev0 → 0.0.1-a43.dev0

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.
Files changed (36) hide show
  1. package/package.json +1 -1
  2. package/src/app/index.tsx +1 -0
  3. package/src/app/pages/AddEditNodePage/AlertMessage.jsx +10 -0
  4. package/src/app/pages/AddEditNodePage/DescriptionField.jsx +17 -0
  5. package/src/app/pages/AddEditNodePage/DisplayNameField.jsx +16 -0
  6. package/src/app/pages/AddEditNodePage/FormikSelect.jsx +2 -0
  7. package/src/app/pages/AddEditNodePage/FullNameField.jsx +3 -2
  8. package/src/app/pages/AddEditNodePage/MetricMetadataFields.jsx +60 -0
  9. package/src/app/pages/AddEditNodePage/MetricQueryField.jsx +71 -0
  10. package/src/app/pages/AddEditNodePage/NamespaceField.jsx +40 -0
  11. package/src/app/pages/AddEditNodePage/NodeModeField.jsx +14 -0
  12. package/src/app/pages/AddEditNodePage/NodeQueryField.jsx +5 -3
  13. package/src/app/pages/AddEditNodePage/PrimaryKeySelect.jsx +61 -0
  14. package/src/app/pages/AddEditNodePage/RequiredDimensionsSelect.jsx +54 -0
  15. package/src/app/pages/AddEditNodePage/TagsField.jsx +47 -0
  16. package/src/app/pages/AddEditNodePage/UpstreamNodeField.jsx +49 -0
  17. package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormFailed.test.jsx +2 -1
  18. package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormSuccess.test.jsx +150 -14
  19. package/src/app/pages/AddEditNodePage/__tests__/index.test.jsx +35 -8
  20. package/src/app/pages/AddEditNodePage/index.jsx +177 -232
  21. package/src/app/pages/CubeBuilderPage/MetricsSelect.jsx +0 -1
  22. package/src/app/pages/CubeBuilderPage/__tests__/index.test.jsx +1 -1
  23. package/src/app/pages/CubeBuilderPage/index.jsx +1 -1
  24. package/src/app/pages/NodePage/AddMaterializationPopover.jsx +0 -1
  25. package/src/app/pages/NodePage/NodeHistory.jsx +1 -1
  26. package/src/app/pages/NodePage/NodeInfoTab.jsx +54 -13
  27. package/src/app/pages/NodePage/__tests__/NodePage.test.jsx +34 -28
  28. package/src/app/pages/NodePage/__tests__/__snapshots__/NodePage.test.jsx.snap +2 -18
  29. package/src/app/pages/NodePage/index.jsx +30 -27
  30. package/src/app/pages/Root/index.tsx +3 -2
  31. package/src/app/services/DJService.js +37 -0
  32. package/src/app/services/__tests__/DJService.test.jsx +23 -0
  33. package/src/mocks/mockNodes.jsx +63 -0
  34. package/src/styles/index.css +6 -0
  35. package/src/styles/node-creation.scss +63 -5
  36. package/dj.internal.db +0 -0
@@ -9,12 +9,21 @@ import NamespaceHeader from '../../components/NamespaceHeader';
9
9
  import { useContext, useEffect, useState } from 'react';
10
10
  import DJClientContext from '../../providers/djclient';
11
11
  import 'styles/node-creation.scss';
12
- import AlertIcon from '../../icons/AlertIcon';
13
12
  import { useParams, useNavigate } from 'react-router-dom';
14
13
  import { FullNameField } from './FullNameField';
15
- import { FormikSelect } from './FormikSelect';
14
+ import { MetricQueryField } from './MetricQueryField';
15
+ import { displayMessageAfterSubmit } from '../../../utils/form';
16
+ import { PrimaryKeySelect } from './PrimaryKeySelect';
16
17
  import { NodeQueryField } from './NodeQueryField';
17
- import { displayMessageAfterSubmit, labelize } from '../../../utils/form';
18
+ import { MetricMetadataFields } from './MetricMetadataFields';
19
+ import { UpstreamNodeField } from './UpstreamNodeField';
20
+ import { TagsField } from './TagsField';
21
+ import { NamespaceField } from './NamespaceField';
22
+ import { AlertMessage } from './AlertMessage';
23
+ import { DisplayNameField } from './DisplayNameField';
24
+ import { DescriptionField } from './DescriptionField';
25
+ import { NodeModeField } from './NodeModeField';
26
+ import { RequiredDimensionsSelect } from './RequiredDimensionsSelect';
18
27
 
19
28
  class Action {
20
29
  static Add = new Action('add');
@@ -32,20 +41,15 @@ export function AddEditNodePage() {
32
41
  let { nodeType, initialNamespace, name } = useParams();
33
42
  const action = name !== undefined ? Action.Edit : Action.Add;
34
43
 
35
- const [namespaces, setNamespaces] = useState([]);
36
- const [tags, setTags] = useState([]);
37
- const [metricUnits, setMetricUnits] = useState([]);
38
- const [metricDirections, setMetricDirections] = useState([]);
39
-
40
44
  const initialValues = {
41
45
  name: action === Action.Edit ? name : '',
42
46
  namespace: action === Action.Add ? initialNamespace : '',
43
47
  display_name: '',
44
48
  query: '',
45
- node_type: '',
49
+ type: nodeType,
46
50
  description: '',
47
51
  primary_key: '',
48
- mode: 'draft',
52
+ mode: 'published',
49
53
  };
50
54
 
51
55
  const validator = values => {
@@ -53,7 +57,7 @@ export function AddEditNodePage() {
53
57
  if (!values.name) {
54
58
  errors.name = 'Required';
55
59
  }
56
- if (!values.query) {
60
+ if (values.type !== 'metric' && !values.query) {
57
61
  errors.query = 'Required';
58
62
  }
59
63
  return errors;
@@ -98,7 +102,7 @@ export function AddEditNodePage() {
98
102
  );
99
103
 
100
104
  const primaryKeyToList = primaryKey => {
101
- return primaryKey.split(',').map(columnName => columnName.trim());
105
+ return primaryKey.map(columnName => columnName.trim());
102
106
  };
103
107
 
104
108
  const createNode = async (values, setStatus) => {
@@ -107,12 +111,15 @@ export function AddEditNodePage() {
107
111
  values.name,
108
112
  values.display_name,
109
113
  values.description,
110
- values.query,
114
+ values.type === 'metric'
115
+ ? `SELECT ${values.aggregate_expression} FROM ${values.upstream_node}`
116
+ : values.query,
111
117
  values.mode,
112
118
  values.namespace,
113
119
  values.primary_key ? primaryKeyToList(values.primary_key) : null,
114
120
  values.metric_direction,
115
121
  values.metric_unit,
122
+ values.required_dimensions,
116
123
  );
117
124
  if (status === 200 || status === 201) {
118
125
  if (values.tags) {
@@ -138,11 +145,14 @@ export function AddEditNodePage() {
138
145
  values.name,
139
146
  values.display_name,
140
147
  values.description,
141
- values.query,
148
+ values.type === 'metric'
149
+ ? `SELECT ${values.aggregate_expression} FROM ${values.upstream_node}`
150
+ : values.query,
142
151
  values.mode,
143
152
  values.primary_key ? primaryKeyToList(values.primary_key) : null,
144
153
  values.metric_direction,
145
154
  values.metric_unit,
155
+ values.required_dimensions,
146
156
  );
147
157
  const tagsResponse = await djClient.tagsNode(
148
158
  values.name,
@@ -164,22 +174,6 @@ export function AddEditNodePage() {
164
174
  }
165
175
  };
166
176
 
167
- const namespaceInput = (
168
- <div className="NamespaceInput">
169
- <ErrorMessage name="namespace" component="span" />
170
- <label htmlFor="react-select-3-input">Namespace *</label>
171
- <FormikSelect
172
- selectOptions={namespaces}
173
- formikFieldName="namespace"
174
- placeholder="Choose Namespace"
175
- defaultValue={{
176
- value: initialNamespace,
177
- label: initialNamespace,
178
- }}
179
- />
180
- </div>
181
- );
182
-
183
177
  const fullNameInput = (
184
178
  <div className="FullNameInput NodeCreationInput">
185
179
  <ErrorMessage name="name" component="span" />
@@ -192,7 +186,57 @@ export function AddEditNodePage() {
192
186
  return new Set(['transform', 'metric', 'dimension']).has(nodeType);
193
187
  };
194
188
 
195
- const updateFieldsWithNodeData = (data, setFieldValue) => {
189
+ 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;
196
+ }
197
+ return data;
198
+ };
199
+
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);
210
+ };
211
+
212
+ const runValidityChecks = (data, setNode, setMessage) => {
213
+ // Check if node exists
214
+ if (data.message !== undefined) {
215
+ setNode(null);
216
+ setMessage(`Node ${name} does not exist!`);
217
+ return;
218
+ }
219
+
220
+ // Check if node type can be edited
221
+ if (!nodeCanBeEdited(data.type)) {
222
+ setNode(null);
223
+ if (data.type === 'cube') {
224
+ navigate(`/nodes/${data.name}/edit-cube`);
225
+ }
226
+ setMessage(`Node ${name} is of type ${data.type} and cannot be edited`);
227
+ }
228
+ };
229
+
230
+ const updateFieldsWithNodeData = (
231
+ data,
232
+ setFieldValue,
233
+ setNode,
234
+ setSelectTags,
235
+ setSelectPrimaryKey,
236
+ setSelectUpstreamNode,
237
+ setSelectRequiredDims,
238
+ ) => {
239
+ // Update fields with existing data to prepare for edit
196
240
  const fields = [
197
241
  'display_name',
198
242
  'query',
@@ -201,19 +245,13 @@ export function AddEditNodePage() {
201
245
  'primary_key',
202
246
  'mode',
203
247
  'tags',
248
+ 'expression',
249
+ 'upstream_node',
204
250
  ];
205
- const primaryKey = data.columns
206
- .filter(
207
- col =>
208
- col.attributes &&
209
- col.attributes.filter(
210
- attr => attr.attribute_type.name === 'primary_key',
211
- ).length > 0,
212
- )
213
- .map(col => col.name);
251
+ const primaryKey = primaryKeyFromNode(data);
214
252
  fields.forEach(field => {
215
253
  if (field === 'primary_key') {
216
- setFieldValue(field, primaryKey.join(', '));
254
+ setFieldValue(field, primaryKey);
217
255
  } else if (field === 'tags') {
218
256
  setFieldValue(
219
257
  field,
@@ -232,57 +270,47 @@ export function AddEditNodePage() {
232
270
  data.metric_metadata.unit.name.toLowerCase(),
233
271
  );
234
272
  }
235
- };
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
+ setNode(data);
236
280
 
237
- const alertMessage = message => {
238
- return (
239
- <div className="message alert">
240
- <AlertIcon />
241
- {message}
242
- </div>
281
+ // For react-select fields, we have to explicitly set the entire
282
+ // field rather than just the values
283
+ setSelectTags(
284
+ <TagsField
285
+ defaultValue={data.tags.map(t => {
286
+ return { value: t.name, label: t.display_name };
287
+ })}
288
+ />,
289
+ );
290
+ setSelectPrimaryKey(
291
+ <PrimaryKeySelect
292
+ defaultValue={primaryKey.map(col => {
293
+ return { value: col, label: col };
294
+ })}
295
+ />,
296
+ );
297
+ setSelectRequiredDims(
298
+ <RequiredDimensionsSelect
299
+ defaultValue={data.required_dimensions.map(dim => {
300
+ return { value: dim, label: dim };
301
+ })}
302
+ />,
303
+ );
304
+ setSelectUpstreamNode(
305
+ <UpstreamNodeField
306
+ defaultValue={{
307
+ value: data.upstream_node,
308
+ label: data.upstream_node,
309
+ }}
310
+ />,
243
311
  );
244
312
  };
245
313
 
246
- // Get namespaces, only necessary when creating a node
247
- useEffect(() => {
248
- if (action === Action.Add) {
249
- const fetchData = async () => {
250
- const namespaces = await djClient.namespaces();
251
- setNamespaces(
252
- namespaces.map(m => ({
253
- value: m['namespace'],
254
- label: m['namespace'],
255
- })),
256
- );
257
- };
258
- fetchData().catch(console.error);
259
- }
260
- }, [action, djClient, djClient.metrics]);
261
-
262
- // Get list of tags
263
- useEffect(() => {
264
- const fetchData = async () => {
265
- const tags = await djClient.listTags();
266
- setTags(
267
- tags.map(tag => ({
268
- value: tag.name,
269
- label: tag.display_name,
270
- })),
271
- );
272
- };
273
- fetchData().catch(console.error);
274
- }, [djClient, djClient.listTags]);
275
-
276
- // Get metric metadata values
277
- useEffect(() => {
278
- const fetchData = async () => {
279
- const metadata = await djClient.listMetricMetadata();
280
- setMetricDirections(metadata.directions);
281
- setMetricUnits(metadata.units);
282
- };
283
- fetchData().catch(console.error);
284
- }, [djClient]);
285
-
286
314
  return (
287
315
  <div className="mid">
288
316
  <NamespaceHeader namespace="" />
@@ -297,172 +325,89 @@ export function AddEditNodePage() {
297
325
  >
298
326
  {function Render({ isSubmitting, status, setFieldValue }) {
299
327
  const [node, setNode] = useState([]);
328
+ const [selectPrimaryKey, setSelectPrimaryKey] = useState(null);
329
+ const [selectRequiredDims, setSelectRequiredDims] =
330
+ useState(null);
331
+ const [selectUpstreamNode, setSelectUpstreamNode] =
332
+ useState(null);
300
333
  const [selectTags, setSelectTags] = useState(null);
301
334
  const [message, setMessage] = useState('');
302
335
 
303
- const tagsInput = (
304
- <div
305
- className="TagsInput"
306
- style={{ width: '25%', margin: '1rem 0 1rem 1.2rem' }}
307
- >
308
- <ErrorMessage name="tags" component="span" />
309
- <label htmlFor="react-select-3-input">Tags</label>
310
- <span data-testid="select-tags">
311
- {action === Action.Edit ? (
312
- selectTags
313
- ) : (
314
- <FormikSelect
315
- isMulti={true}
316
- selectOptions={tags}
317
- formikFieldName="tags"
318
- placeholder="Choose Tags"
319
- />
320
- )}
321
- </span>
322
- </div>
323
- );
324
-
325
- const metricMetadataInput = (
326
- <>
327
- <div
328
- className="MetricDirectionInput NodeCreationInput"
329
- style={{ width: '25%' }}
330
- >
331
- <ErrorMessage name="metric_direction" component="span" />
332
- <label htmlFor="MetricDirection">Metric Direction</label>
333
- <Field
334
- as="select"
335
- name="metric_direction"
336
- id="MetricDirection"
337
- >
338
- <option value=""></option>
339
- {metricDirections.map(direction => (
340
- <option value={direction}>
341
- {labelize(direction)}
342
- </option>
343
- ))}
344
- </Field>
345
- </div>
346
- <div
347
- className="MetricUnitInput NodeCreationInput"
348
- style={{ width: '25%' }}
349
- >
350
- <ErrorMessage name="metric_unit" component="span" />
351
- <label htmlFor="MetricUnit">Metric Unit</label>
352
- <Field as="select" name="metric_unit" id="MetricUnit">
353
- <option value=""></option>
354
- {metricUnits.map(unit => (
355
- <option value={unit.name}>{unit.label}</option>
356
- ))}
357
- </Field>
358
- </div>
359
- </>
360
- );
361
-
362
336
  useEffect(() => {
363
337
  const fetchData = async () => {
364
338
  if (action === Action.Edit) {
365
- const data = await djClient.node(name);
366
-
367
- // Check if node exists
368
- if (data.message !== undefined) {
369
- setNode(null);
370
- setMessage(`Node ${name} does not exist!`);
371
- return;
372
- }
373
-
374
- // Check if node type can be edited
375
- if (!nodeCanBeEdited(data.type)) {
376
- setNode(null);
377
- if (data.type === 'cube') {
378
- navigate(`/nodes/${data.name}/edit-cube`);
379
- }
380
- setMessage(
381
- `Node ${name} is of type ${data.type} and cannot be edited`,
382
- );
383
- return;
384
- }
385
-
386
- // Update fields with existing data to prepare for edit
387
- updateFieldsWithNodeData(data, setFieldValue);
388
- setNode(data);
389
- setSelectTags(
390
- <FormikSelect
391
- isMulti={true}
392
- selectOptions={tags}
393
- formikFieldName="tags"
394
- placeholder="Choose Tags"
395
- defaultValue={data.tags.map(t => {
396
- return { value: t.name, label: t.display_name };
397
- })}
398
- />,
339
+ const data = await getExistingNodeData(name);
340
+ runValidityChecks(data, setNode, setMessage);
341
+ updateFieldsWithNodeData(
342
+ data,
343
+ setFieldValue,
344
+ setNode,
345
+ setSelectTags,
346
+ setSelectPrimaryKey,
347
+ setSelectUpstreamNode,
348
+ setSelectRequiredDims,
399
349
  );
400
350
  }
401
351
  };
402
352
  fetchData().catch(console.error);
403
- }, [setFieldValue, tags]);
353
+ }, [setFieldValue]);
404
354
  return (
405
355
  <Form>
406
356
  {displayMessageAfterSubmit(status)}
407
357
  {action === Action.Edit && message ? (
408
- alertMessage(message)
358
+ <AlertMessage message={message} />
409
359
  ) : (
410
360
  <>
411
- {action === Action.Add
412
- ? namespaceInput
413
- : staticFieldsInEdit(node)}
414
- <div className="DisplayNameInput NodeCreationInput">
415
- <ErrorMessage name="display_name" component="span" />
416
- <label htmlFor="displayName">Display Name *</label>
417
- <Field
418
- type="text"
419
- name="display_name"
420
- id="displayName"
421
- placeholder="Human readable display name"
422
- />
423
- </div>
361
+ {action === Action.Add ? (
362
+ <NamespaceField initialNamespace={initialNamespace} />
363
+ ) : (
364
+ staticFieldsInEdit(node)
365
+ )}
366
+ <DisplayNameField />
424
367
  {action === Action.Add ? fullNameInput : ''}
425
- <div className="DescriptionInput NodeCreationInput">
426
- <ErrorMessage name="description" component="span" />
427
- <label htmlFor="Description">Description</label>
428
- <Field
429
- type="textarea"
430
- as="textarea"
431
- name="description"
432
- id="Description"
433
- placeholder="Describe your node"
368
+ <DescriptionField />
369
+ <br />
370
+ {nodeType === 'metric' || node?.type === 'metric' ? (
371
+ action === Action.Edit ? (
372
+ selectUpstreamNode
373
+ ) : (
374
+ <UpstreamNodeField />
375
+ )
376
+ ) : (
377
+ ''
378
+ )}
379
+ <br />
380
+ <br />
381
+ {nodeType === 'metric' || node.type === 'metric' ? (
382
+ <MetricQueryField
383
+ djClient={djClient}
384
+ value={node.expression ? node.expression : ''}
434
385
  />
435
- </div>
436
- {nodeType === 'metric' || node.type === 'metric'
437
- ? metricMetadataInput
438
- : ''}
439
- <div className="QueryInput NodeCreationInput">
440
- <ErrorMessage name="query" component="span" />
441
- <label htmlFor="Query">Query *</label>
386
+ ) : (
442
387
  <NodeQueryField
443
388
  djClient={djClient}
444
389
  value={node.query ? node.query : ''}
445
390
  />
446
- </div>
447
- <div className="PrimaryKeyInput NodeCreationInput">
448
- <ErrorMessage name="primary_key" component="span" />
449
- <label htmlFor="primaryKey">Primary Key</label>
450
- <Field
451
- type="text"
452
- name="primary_key"
453
- id="primaryKey"
454
- placeholder="Comma-separated list of PKs"
455
- />
456
- </div>
457
- {tagsInput}
458
- <div className="NodeModeInput NodeCreationInput">
459
- <ErrorMessage name="mode" component="span" />
460
- <label htmlFor="Mode">Mode</label>
461
- <Field as="select" name="mode" id="Mode">
462
- <option value="draft">Draft</option>
463
- <option value="published">Published</option>
464
- </Field>
465
- </div>
391
+ )}
392
+ <br />
393
+ {nodeType === 'metric' || node.type === 'metric' ? (
394
+ <MetricMetadataFields />
395
+ ) : (
396
+ ''
397
+ )}
398
+ {nodeType !== 'metric' && node.type !== 'metric' ? (
399
+ action === Action.Edit ? (
400
+ selectPrimaryKey
401
+ ) : (
402
+ <PrimaryKeySelect />
403
+ )
404
+ ) : action === Action.Edit ? (
405
+ selectRequiredDims
406
+ ) : (
407
+ <RequiredDimensionsSelect />
408
+ )}
409
+ {action === Action.Edit ? selectTags : <TagsField />}
410
+ <NodeModeField />
466
411
 
467
412
  <button type="submit" disabled={isSubmitting}>
468
413
  {action === Action.Add ? 'Create' : 'Save'} {nodeType}
@@ -38,7 +38,6 @@ export const MetricsSelect = ({ cube }) => {
38
38
 
39
39
  const metrics = await djClient.metrics();
40
40
  setMetrics(metrics.map(m => ({ value: m, label: m })));
41
- console.log('metrics', metrics);
42
41
  };
43
42
  fetchData().catch(console.error);
44
43
  }, [djClient, djClient.metrics, cube]);
@@ -305,7 +305,7 @@ describe('CubeBuilderPage', () => {
305
305
  '',
306
306
  '',
307
307
  '',
308
- 'draft',
308
+ 'published',
309
309
  [
310
310
  'default.num_repair_orders',
311
311
  'default.avg_repair_price',
@@ -25,7 +25,7 @@ export function CubeBuilderPage() {
25
25
  namespace: action === Action.Add ? initialNamespace : '',
26
26
  display_name: '',
27
27
  description: '',
28
- mode: 'draft',
28
+ mode: 'published',
29
29
  metrics: [],
30
30
  dimensions: [],
31
31
  filters: [],
@@ -40,7 +40,6 @@ export default function AddMaterializationPopover({ node, onSubmit }) {
40
40
  setSubmitting(false);
41
41
  const config = JSON.parse(values.config);
42
42
  config.lookback_window = values.lookback_window;
43
- console.log('values', values);
44
43
  const response = await djClient.materialize(
45
44
  values.node,
46
45
  values.job_type,
@@ -11,8 +11,8 @@ export default function NodeHistory({ node, djClient }) {
11
11
  const fetchData = async () => {
12
12
  if (node) {
13
13
  const data = await djClient.history('node', node.name);
14
- setHistory(data);
15
14
  const revisions = await djClient.revisions(node.name);
15
+ setHistory(data);
16
16
  setRevisions(revisions);
17
17
  }
18
18
  };
@@ -20,6 +20,7 @@ export default function NodeInfoTab({ node }) {
20
20
  </div>
21
21
  ));
22
22
  const djClient = useContext(DJClientContext).DataJunctionAPI;
23
+
23
24
  useEffect(() => {
24
25
  const fetchData = async () => {
25
26
  if (checked === true) {
@@ -38,6 +39,24 @@ export default function NodeInfoTab({ node }) {
38
39
  function toggle(value) {
39
40
  return !value;
40
41
  }
42
+ const metricQueryDiv = (
43
+ <div className="list-group-item d-flex">
44
+ <div className="gap-2 w-100 justify-content-between py-3">
45
+ <div style={{ marginBottom: '30px' }}>
46
+ <h6 className="mb-0 w-100">Upstream Node</h6>
47
+ <p>
48
+ <a href={`/nodes/${node?.upstream_node}`}>{node?.upstream_node}</a>
49
+ </p>
50
+ </div>
51
+ <div>
52
+ <h6 className="mb-0 w-100">Aggregate Expression</h6>
53
+ <SyntaxHighlighter language="sql" style={foundation}>
54
+ {node?.expression}
55
+ </SyntaxHighlighter>
56
+ </div>
57
+ </div>
58
+ </div>
59
+ );
41
60
  const queryDiv = node?.query ? (
42
61
  <div className="list-group-item d-flex">
43
62
  <div className="d-flex gap-2 w-100 justify-content-between py-3">
@@ -153,8 +172,39 @@ export default function NodeInfoTab({ node }) {
153
172
  ) : (
154
173
  <></>
155
174
  );
175
+
176
+ const primaryKeyOrRequiredDims = (
177
+ <div style={{ maxWidth: '25%' }}>
178
+ <h6 className="mb-0 w-100">
179
+ {node?.type !== 'metric' ? 'Primary Key' : 'Required Dimensions'}
180
+ </h6>
181
+ <p
182
+ className="mb-0 opacity-75"
183
+ role="dialog"
184
+ aria-hidden="false"
185
+ aria-label={
186
+ node?.type !== 'metric' ? 'PrimaryKey' : 'RequiredDimensions'
187
+ }
188
+ >
189
+ {node?.type !== 'metric'
190
+ ? node?.primary_key?.map(dim => (
191
+ <span className="rounded-pill badge bg-secondary-soft PrimaryKey">
192
+ <a href={`/nodes/${node?.name}`}>{dim}</a>
193
+ </span>
194
+ ))
195
+ : node?.required_dimensions?.map(dim => (
196
+ <span className="rounded-pill badge bg-secondary-soft PrimaryKey">
197
+ <a href={`/nodes/${node?.upstream_node}`}>{dim}</a>
198
+ </span>
199
+ ))}
200
+ </p>
201
+ </div>
202
+ );
156
203
  return (
157
- <div className="list-group align-items-center justify-content-between flex-md-row gap-2">
204
+ <div
205
+ className="list-group align-items-center justify-content-between flex-md-row gap-2"
206
+ style={{ minWidth: '700px' }}
207
+ >
158
208
  <ListGroupItem label="Description" value={node?.description} />
159
209
  <div className="list-group-item d-flex">
160
210
  <div className="d-flex gap-2 w-100 justify-content-between py-3">
@@ -223,17 +273,7 @@ export default function NodeInfoTab({ node }) {
223
273
  {nodeTags}
224
274
  </p>
225
275
  </div>
226
- <div>
227
- <h6 className="mb-0 w-100">Primary Key</h6>
228
- <p
229
- className="mb-0 opacity-75"
230
- role="dialog"
231
- aria-hidden="false"
232
- aria-label="PrimaryKey"
233
- >
234
- {node?.primary_key?.join(', ')}
235
- </p>
236
- </div>
276
+ {primaryKeyOrRequiredDims}
237
277
  <div>
238
278
  <h6 className="mb-0 w-100">Last Updated</h6>
239
279
  <p
@@ -248,7 +288,8 @@ export default function NodeInfoTab({ node }) {
248
288
  </div>
249
289
  </div>
250
290
  {metricMetadataDiv}
251
- {node?.type !== 'cube' ? queryDiv : ''}
291
+ {node?.type !== 'cube' && node?.type !== 'metric' ? queryDiv : ''}
292
+ {node?.type === 'metric' ? metricQueryDiv : ''}
252
293
  {cubeElementsDiv}
253
294
  </div>
254
295
  );