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
@@ -271,7 +271,7 @@ describe('<NodePage />', () => {
271
271
  },
272
272
  };
273
273
 
274
- it('renders the NodeInfo tab correctly', async () => {
274
+ it('renders the NodeInfo tab correctly for a metric node', async () => {
275
275
  const djClient = mockDJClient();
276
276
  djClient.DataJunctionAPI.node.mockReturnValue(mocks.mockMetricNode);
277
277
  djClient.DataJunctionAPI.metric.mockReturnValue(mocks.mockMetricNode);
@@ -281,9 +281,9 @@ describe('<NodePage />', () => {
281
281
  </DJClientContext.Provider>
282
282
  );
283
283
  const { container } = render(
284
- <MemoryRouter initialEntries={['/nodes/default.num_repair_orders']}>
284
+ <MemoryRouter initialEntries={['/nodes/default.num_repair_orders/info']}>
285
285
  <Routes>
286
- <Route path="nodes/:name" element={element} />
286
+ <Route path="nodes/:name/:tab" element={element} />
287
287
  </Routes>
288
288
  </MemoryRouter>,
289
289
  );
@@ -292,7 +292,6 @@ describe('<NodePage />', () => {
292
292
  expect(djClient.DataJunctionAPI.node).toHaveBeenCalledWith(
293
293
  'default.num_repair_orders',
294
294
  );
295
- userEvent.click(screen.getByRole('button', { name: 'Info' }));
296
295
 
297
296
  expect(
298
297
  screen.getByRole('dialog', { name: 'NodeName' }),
@@ -316,8 +315,8 @@ describe('<NodePage />', () => {
316
315
  );
317
316
 
318
317
  expect(
319
- screen.getByRole('dialog', { name: 'PrimaryKey' }),
320
- ).toHaveTextContent('repair_order_id, country');
318
+ screen.getByRole('dialog', { name: 'RequiredDimensions' }),
319
+ ).toHaveTextContent('');
321
320
 
322
321
  expect(
323
322
  screen.getByRole('dialog', { name: 'DisplayName' }),
@@ -411,14 +410,15 @@ describe('<NodePage />', () => {
411
410
  </DJClientContext.Provider>
412
411
  );
413
412
  render(
414
- <MemoryRouter initialEntries={['/nodes/default.num_repair_orders']}>
413
+ <MemoryRouter
414
+ initialEntries={['/nodes/default.num_repair_orders/columns']}
415
+ >
415
416
  <Routes>
416
- <Route path="nodes/:name" element={element} />
417
+ <Route path="nodes/:name/:tab" element={element} />
417
418
  </Routes>
418
419
  </MemoryRouter>,
419
420
  );
420
421
  await waitFor(() => {
421
- fireEvent.click(screen.getByRole('button', { name: 'Columns' }));
422
422
  expect(djClient.DataJunctionAPI.columns).toHaveBeenCalledWith(
423
423
  mocks.mockMetricNode,
424
424
  );
@@ -484,9 +484,11 @@ describe('<NodePage />', () => {
484
484
  </DJClientContext.Provider>
485
485
  );
486
486
  const { container } = render(
487
- <MemoryRouter initialEntries={['/nodes/default.num_repair_orders']}>
487
+ <MemoryRouter
488
+ initialEntries={['/nodes/default.num_repair_orders/history']}
489
+ >
488
490
  <Routes>
489
- <Route path="nodes/:name" element={element} />
491
+ <Route path="nodes/:name/:tab" element={element} />
490
492
  </Routes>
491
493
  </MemoryRouter>,
492
494
  );
@@ -543,14 +545,12 @@ describe('<NodePage />', () => {
543
545
  'Status changed from valid to invalid Caused by a change in upstream default.repair_order_details',
544
546
  ),
545
547
  );
546
- screen.debug();
547
548
  });
548
549
  });
549
550
 
550
551
  it('renders compiled sql correctly', async () => {
551
552
  const djClient = mockDJClient();
552
- djClient.DataJunctionAPI.node.mockReturnValue(mocks.mockMetricNode);
553
- djClient.DataJunctionAPI.metric.mockReturnValue(mocks.mockMetricNode);
553
+ djClient.DataJunctionAPI.node.mockReturnValue(mocks.mockTransformNode);
554
554
  djClient.DataJunctionAPI.columns.mockReturnValue(mocks.metricNodeColumns);
555
555
  djClient.DataJunctionAPI.compiledSql.mockReturnValue('select 1');
556
556
 
@@ -560,7 +560,7 @@ describe('<NodePage />', () => {
560
560
  </DJClientContext.Provider>
561
561
  );
562
562
  render(
563
- <MemoryRouter initialEntries={['/nodes/default.num_repair_orders']}>
563
+ <MemoryRouter initialEntries={[`/nodes/${mocks.mockTransformNode.name}`]}>
564
564
  <Routes>
565
565
  <Route path="nodes/:name" element={element} />
566
566
  </Routes>
@@ -569,7 +569,7 @@ describe('<NodePage />', () => {
569
569
  await waitFor(() => {
570
570
  fireEvent.click(screen.getByRole('checkbox', { name: 'ToggleSwitch' }));
571
571
  expect(djClient.DataJunctionAPI.compiledSql).toHaveBeenCalledWith(
572
- mocks.mockMetricNode.name,
572
+ mocks.mockTransformNode.name,
573
573
  );
574
574
  });
575
575
  });
@@ -587,9 +587,11 @@ describe('<NodePage />', () => {
587
587
  </DJClientContext.Provider>
588
588
  );
589
589
  render(
590
- <MemoryRouter initialEntries={['/nodes/default.num_repair_orders']}>
590
+ <MemoryRouter
591
+ initialEntries={['/nodes/default.num_repair_orders/materializations']}
592
+ >
591
593
  <Routes>
592
- <Route path="nodes/:name" element={element} />
594
+ <Route path="nodes/:name/:tab" element={element} />
593
595
  </Routes>
594
596
  </MemoryRouter>,
595
597
  );
@@ -620,9 +622,11 @@ describe('<NodePage />', () => {
620
622
  </DJClientContext.Provider>
621
623
  );
622
624
  render(
623
- <MemoryRouter initialEntries={['/nodes/default.num_repair_orders']}>
625
+ <MemoryRouter
626
+ initialEntries={['/nodes/default.num_repair_orders/materializations']}
627
+ >
624
628
  <Routes>
625
- <Route path="nodes/:name" element={element} />
629
+ <Route path="nodes/:name/:tab" element={element} />
626
630
  </Routes>
627
631
  </MemoryRouter>,
628
632
  );
@@ -658,9 +662,9 @@ describe('<NodePage />', () => {
658
662
  </DJClientContext.Provider>
659
663
  );
660
664
  render(
661
- <MemoryRouter initialEntries={['/nodes/default.num_repair_orders']}>
665
+ <MemoryRouter initialEntries={['/nodes/default.num_repair_orders/sql']}>
662
666
  <Routes>
663
- <Route path="nodes/:name" element={element} />
667
+ <Route path="nodes/:name/:tab" element={element} />
664
668
  </Routes>
665
669
  </MemoryRouter>,
666
670
  );
@@ -689,9 +693,11 @@ describe('<NodePage />', () => {
689
693
  </DJClientContext.Provider>
690
694
  );
691
695
  render(
692
- <MemoryRouter initialEntries={['/nodes/default.num_repair_orders']}>
696
+ <MemoryRouter
697
+ initialEntries={['/nodes/default.num_repair_orders/lineage']}
698
+ >
693
699
  <Routes>
694
- <Route path="nodes/:name" element={element} />
700
+ <Route path="nodes/:name/:tab" element={element} />
695
701
  </Routes>
696
702
  </MemoryRouter>,
697
703
  );
@@ -715,9 +721,9 @@ describe('<NodePage />', () => {
715
721
  </DJClientContext.Provider>
716
722
  );
717
723
  render(
718
- <MemoryRouter initialEntries={['/nodes/default.num_repair_orders']}>
724
+ <MemoryRouter initialEntries={['/nodes/default.num_repair_orders/graph']}>
719
725
  <Routes>
720
- <Route path="nodes/:name" element={element} />
726
+ <Route path="nodes/:name/:tab" element={element} />
721
727
  </Routes>
722
728
  </MemoryRouter>,
723
729
  );
@@ -742,9 +748,9 @@ describe('<NodePage />', () => {
742
748
  </DJClientContext.Provider>
743
749
  );
744
750
  render(
745
- <MemoryRouter initialEntries={['/nodes/default.dispatcher']}>
751
+ <MemoryRouter initialEntries={['/nodes/default.dispatcher/linked']}>
746
752
  <Routes>
747
- <Route path="nodes/:name" element={element} />
753
+ <Route path="nodes/:name/:tab" element={element} />
748
754
  </Routes>
749
755
  </MemoryRouter>,
750
756
  );
@@ -100,35 +100,19 @@ exports[`<NodePage /> renders the NodeHistory tab correctly 1`] = `
100
100
  </table>
101
101
  `;
102
102
 
103
- exports[`<NodePage /> renders the NodeInfo tab correctly 1`] = `
103
+ exports[`<NodePage /> renders the NodeInfo tab correctly for a metric node 1`] = `
104
104
  HTMLCollection [
105
105
  <code
106
106
  class="language-sql"
107
107
  style="white-space: pre;"
108
108
  >
109
- <span
110
- style="color: rgb(0, 153, 153);"
111
- >
112
- SELECT
113
- </span>
114
- <span>
115
-
116
- </span>
117
109
  <span
118
110
  class="hljs-built_in"
119
111
  >
120
112
  count
121
113
  </span>
122
114
  <span>
123
- (repair_order_id) default_DOT_num_repair_orders
124
- </span>
125
- <span
126
- style="color: rgb(0, 153, 153);"
127
- >
128
- FROM
129
- </span>
130
- <span>
131
- default.repair_orders
115
+ (repair_order_id)
132
116
  </span>
133
117
  </code>,
134
118
  ]
@@ -16,16 +16,22 @@ import NodeColumnLineage from './NodeLineageTab';
16
16
  import EditIcon from '../../icons/EditIcon';
17
17
  import AlertIcon from '../../icons/AlertIcon';
18
18
  import NodeDimensionsTab from './NodeDimensionsTab';
19
+ import { useNavigate } from 'react-router-dom';
19
20
 
20
21
  export function NodePage() {
21
22
  const djClient = useContext(DJClientContext).DataJunctionAPI;
23
+ const navigate = useNavigate();
24
+
25
+ const { name, tab } = useParams();
26
+
22
27
  const [state, setState] = useState({
23
- selectedTab: 0,
28
+ selectedTab: tab || 'info',
24
29
  });
25
30
 
26
31
  const [node, setNode] = useState();
27
32
 
28
33
  const onClickTab = id => () => {
34
+ navigate(`/nodes/${name}/${id}`);
29
35
  setState({ selectedTab: id });
30
36
  };
31
37
 
@@ -41,24 +47,23 @@ export function NodePage() {
41
47
  ) : null;
42
48
  };
43
49
 
44
- const { name } = useParams();
45
-
46
50
  useEffect(() => {
47
51
  const fetchData = async () => {
48
52
  const data = await djClient.node(name);
49
53
  data.createNodeClientCode = await djClient.clientCode(name);
50
- setNode(data);
51
54
  if (data.type === 'metric') {
52
55
  const metric = await djClient.metric(name);
53
56
  data.dimensions = metric.dimensions;
54
57
  data.metric_metadata = metric.metric_metadata;
55
- setNode(data);
58
+ data.required_dimensions = metric.required_dimensions;
59
+ data.upstream_node = metric.upstream_node;
60
+ data.expression = metric.expression;
56
61
  }
57
62
  if (data.type === 'cube') {
58
63
  const cube = await djClient.cube(name);
59
64
  data.cube_elements = cube.cube_elements;
60
- setNode(data);
61
65
  }
66
+ setNode(data);
62
67
  };
63
68
  fetchData().catch(console.error);
64
69
  }, [djClient, name]);
@@ -66,84 +71,82 @@ export function NodePage() {
66
71
  const tabsList = node => {
67
72
  return [
68
73
  {
69
- id: 0,
74
+ id: 'info',
70
75
  name: 'Info',
71
76
  display: true,
72
77
  },
73
78
  {
74
- id: 1,
79
+ id: 'columns',
75
80
  name: 'Columns',
76
81
  display: true,
77
82
  },
78
83
  {
79
- id: 2,
84
+ id: 'graph',
80
85
  name: 'Graph',
81
86
  display: true,
82
87
  },
83
88
  {
84
- id: 3,
89
+ id: 'history',
85
90
  name: 'History',
86
91
  display: true,
87
92
  },
88
93
  {
89
- id: 4,
94
+ id: 'sql',
90
95
  name: 'SQL',
91
96
  display: node?.type !== 'dimension' && node?.type !== 'source',
92
97
  },
93
98
  {
94
- id: 5,
99
+ id: 'materializations',
95
100
  name: 'Materializations',
96
101
  display: node?.type !== 'source',
97
102
  },
98
103
  {
99
- id: 6,
104
+ id: 'linked',
100
105
  name: 'Linked Nodes',
101
106
  display: node?.type === 'dimension',
102
107
  },
103
108
  {
104
- id: 7,
109
+ id: 'lineage',
105
110
  name: 'Lineage',
106
111
  display: node?.type === 'metric',
107
112
  },
108
113
  {
109
- id: 8,
114
+ id: 'dimensions',
110
115
  name: 'Dimensions',
111
116
  display: node?.type !== 'cube',
112
117
  },
113
118
  ];
114
119
  };
115
-
116
- //
117
- //
118
120
  let tabToDisplay = null;
121
+
119
122
  switch (state.selectedTab) {
120
- case 0:
123
+ case 'info':
121
124
  tabToDisplay =
122
125
  node && node.message === undefined ? <NodeInfoTab node={node} /> : '';
123
126
  break;
124
- case 1:
127
+ case 'columns':
125
128
  tabToDisplay = <NodeColumnTab node={node} djClient={djClient} />;
126
129
  break;
127
- case 2:
130
+ case 'graph':
128
131
  tabToDisplay = <NodeLineage djNode={node} djClient={djClient} />;
129
132
  break;
130
- case 3:
133
+ case 'history':
131
134
  tabToDisplay = <NodeHistory node={node} djClient={djClient} />;
132
135
  break;
133
- case 4:
136
+ case 'sql':
134
137
  tabToDisplay =
135
138
  node?.type === 'metric' ? <NodeSQLTab djNode={node} /> : <br />;
136
139
  break;
137
- case 5:
140
+ case 'materializations':
138
141
  tabToDisplay = <NodeMaterializationTab node={node} djClient={djClient} />;
139
142
  break;
140
- case 6:
143
+ case 'linked':
141
144
  tabToDisplay = <NodesWithDimension node={node} djClient={djClient} />;
142
145
  break;
143
- case 7:
146
+ case 'lineage':
144
147
  tabToDisplay = <NodeColumnLineage djNode={node} djClient={djClient} />;
145
148
  break;
146
- case 8:
149
+ case 'dimensions':
147
150
  tabToDisplay = <NodeDimensionsTab node={node} djClient={djClient} />;
148
151
  break;
149
152
  default: /* istanbul ignore next */
@@ -24,10 +24,11 @@ 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
- <h2>
27
+ <a href={'/'} style={{textTransform: 'none', textDecoration: 'none', color: '#000'}}>
28
+ <h2>
28
29
  <DJLogo />
29
30
  Data<b>Junction</b>
30
- </h2>
31
+ </h2></a>
31
32
  </div>
32
33
  <Search />
33
34
  <div className="menu">
@@ -60,6 +60,14 @@ export const DataJunctionAPI = {
60
60
  ).json();
61
61
  },
62
62
 
63
+ nodesWithType: async function (nodeType) {
64
+ return await (
65
+ await fetch(`${DJ_URL}/nodes/?node_type=${nodeType}`, {
66
+ credentials: 'include',
67
+ })
68
+ ).json();
69
+ },
70
+
63
71
  nodeDetails: async () => {
64
72
  return await (
65
73
  await fetch(`${DJ_URL}/nodes/details/`, {
@@ -68,6 +76,31 @@ export const DataJunctionAPI = {
68
76
  ).json();
69
77
  },
70
78
 
79
+ validateNode: async function (
80
+ nodeType,
81
+ name,
82
+ display_name,
83
+ description,
84
+ query,
85
+ ) {
86
+ const response = await fetch(`${DJ_URL}/nodes/validate`, {
87
+ method: 'POST',
88
+ headers: {
89
+ 'Content-Type': 'application/json',
90
+ },
91
+ body: JSON.stringify({
92
+ name: name,
93
+ display_name: display_name,
94
+ description: description,
95
+ query: query,
96
+ type: nodeType,
97
+ mode: 'published',
98
+ }),
99
+ credentials: 'include',
100
+ });
101
+ return { status: response.status, json: await response.json() };
102
+ },
103
+
71
104
  createNode: async function (
72
105
  nodeType,
73
106
  name,
@@ -79,6 +112,7 @@ export const DataJunctionAPI = {
79
112
  primary_key,
80
113
  metric_direction,
81
114
  metric_unit,
115
+ required_dimensions,
82
116
  ) {
83
117
  const metricMetadata =
84
118
  metric_direction || metric_unit
@@ -101,6 +135,7 @@ export const DataJunctionAPI = {
101
135
  namespace: namespace,
102
136
  primary_key: primary_key,
103
137
  metric_metadata: metricMetadata,
138
+ required_dimensions: required_dimensions,
104
139
  }),
105
140
  credentials: 'include',
106
141
  });
@@ -116,6 +151,7 @@ export const DataJunctionAPI = {
116
151
  primary_key,
117
152
  metric_direction,
118
153
  metric_unit,
154
+ required_dimensions,
119
155
  ) {
120
156
  try {
121
157
  const metricMetadata =
@@ -137,6 +173,7 @@ export const DataJunctionAPI = {
137
173
  mode: mode,
138
174
  primary_key: primary_key,
139
175
  metric_metadata: metricMetadata,
176
+ required_dimensions: required_dimensions,
140
177
  }),
141
178
  credentials: 'include',
142
179
  });
@@ -65,6 +65,18 @@ describe('DataJunctionAPI', () => {
65
65
  });
66
66
  });
67
67
 
68
+ it('calls nodesWithType correctly', async () => {
69
+ const nodeType = 'transform';
70
+ fetch.mockResponseOnce(JSON.stringify({}));
71
+ await DataJunctionAPI.nodesWithType(nodeType);
72
+ expect(fetch).toHaveBeenCalledWith(
73
+ `${DJ_URL}/nodes/?node_type=${nodeType}`,
74
+ {
75
+ credentials: 'include',
76
+ },
77
+ );
78
+ });
79
+
68
80
  it('calls createNode correctly', async () => {
69
81
  const sampleArgs = [
70
82
  'type',
@@ -266,6 +278,17 @@ describe('DataJunctionAPI', () => {
266
278
  });
267
279
  });
268
280
 
281
+ it('calls listMetricMetadata correctly', async () => {
282
+ const nodeType = 'transform';
283
+ fetch.mockResponseOnce(JSON.stringify({}));
284
+ await DataJunctionAPI.listMetricMetadata();
285
+ expect(fetch).toHaveBeenCalledWith(`${DJ_URL}/metrics/metadata`, {
286
+ method: 'GET',
287
+ headers: { 'Content-Type': 'application/json' },
288
+ credentials: 'include',
289
+ });
290
+ });
291
+
269
292
  it('calls namespaces correctly', async () => {
270
293
  fetch.mockResponseOnce(JSON.stringify({}));
271
294
  await DataJunctionAPI.namespaces();
@@ -1,4 +1,63 @@
1
1
  export const mocks = {
2
+ metricMetadata: {
3
+ directions: ['higher_is_better', 'lower_is_better', 'neutral'],
4
+ units: [
5
+ { name: 'dollar', label: 'Dollar' },
6
+ { name: 'second', label: 'Second' },
7
+ ],
8
+ },
9
+ mockTransformNode: {
10
+ namespace: 'default',
11
+ node_revision_id: 15,
12
+ node_id: 15,
13
+ type: 'transform',
14
+ name: 'default.repair_order_transform',
15
+ display_name: 'Default: Repair Order Transform',
16
+ version: 'v1.0',
17
+ status: 'valid',
18
+ mode: 'published',
19
+ catalog: {
20
+ name: 'warehouse',
21
+ engines: [],
22
+ },
23
+ schema_: null,
24
+ table: null,
25
+ description: 'Repair order dimension',
26
+ query:
27
+ 'SELECT repair_order_id, municipality_id, hard_hat_id, dispatcher_id FROM default.repair_orders',
28
+ availability: null,
29
+ columns: [
30
+ {
31
+ name: 'repair_order_id',
32
+ display_name: 'Repair Order Id',
33
+ type: 'int',
34
+ attributes: [],
35
+ dimension: null,
36
+ partition: null,
37
+ },
38
+ {
39
+ name: 'municipality_id',
40
+ display_name: 'Municipality Id',
41
+ type: 'string',
42
+ attributes: [],
43
+ dimension: null,
44
+ partition: null,
45
+ },
46
+ ],
47
+ updated_at: '2024-01-24T16:39:14.029366+00:00',
48
+ materializations: [],
49
+ parents: [
50
+ {
51
+ name: 'default.repair_orders',
52
+ },
53
+ ],
54
+ metric_metadata: null,
55
+ dimension_links: [],
56
+ created_at: '2024-01-24T16:39:14.028077+00:00',
57
+ tags: [],
58
+ current_version: 'v1.0',
59
+ missing_table: false,
60
+ },
2
61
  mockMetricNode: {
3
62
  namespace: 'default',
4
63
  node_revision_id: 23,
@@ -217,6 +276,10 @@ export const mocks = {
217
276
  },
218
277
  direction: 'neutral',
219
278
  },
279
+ upstream_node: 'default.repair_orders',
280
+ expression: 'count(repair_order_id)',
281
+ aggregate_expression: 'count(repair_order_id)',
282
+ required_dimensions: [],
220
283
  },
221
284
  attributes: [
222
285
  {
@@ -1106,3 +1106,9 @@ pre {
1106
1106
  padding: 5px;
1107
1107
  font-family: Consolas, serif;
1108
1108
  }
1109
+
1110
+ .PrimaryKey {
1111
+ margin-right: 10px;
1112
+ font-size: 100%;
1113
+ margin-bottom: 5px;
1114
+ }