datajunction-ui 0.0.1-a41 → 0.0.1-a42

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.1a41",
3
+ "version": "0.0.1a42",
4
4
  "description": "DataJunction Metrics Platform UI",
5
5
  "module": "src/index.tsx",
6
6
  "repository": {
@@ -5,6 +5,7 @@ import { DJNodeColumns } from './DJNodeColumns';
5
5
  export default function Collapse({ collapsed, text, data }) {
6
6
  const [isCollapsed, setIsCollapsed] = React.useState(collapsed);
7
7
 
8
+ const limit = 5;
8
9
  return (
9
10
  <>
10
11
  <div className="collapse">
@@ -26,11 +27,11 @@ export default function Collapse({ collapsed, text, data }) {
26
27
  >
27
28
  {data.type !== 'metric'
28
29
  ? isCollapsed
29
- ? DJNodeColumns({ data: data, limit: 10 })
30
+ ? DJNodeColumns({ data: data, limit: limit })
30
31
  : DJNodeColumns({ data: data, limit: 100 })
31
32
  : DJNodeDimensions(data)}
32
33
  </div>
33
- {data.type !== 'metric' && data.column_names.length > 10 ? (
34
+ {data.type !== 'metric' && data.column_names.length > limit ? (
34
35
  <button
35
36
  className="collapse-button"
36
37
  onClick={() => setIsCollapsed(!isCollapsed)}
@@ -33,7 +33,11 @@ export function DJNodeColumns({ data, limit }) {
33
33
  };
34
34
  return data.column_names.slice(0, limit).map(col => (
35
35
  <div
36
- className={'custom-node-subheader node_type__' + data.type}
36
+ className={
37
+ 'custom-node-subheader node_type__' +
38
+ data.type +
39
+ (col.order <= 0 ? ' custom-node-emphasis' : '')
40
+ }
37
41
  key={`${data.name}.${col.name}`}
38
42
  >
39
43
  <div style={handleWrapperStyle}>
@@ -32,8 +32,10 @@ const getLayoutedElements = (
32
32
  const nodeHeightTracker = {};
33
33
 
34
34
  nodes.forEach(node => {
35
- nodeHeightTracker[node.id] =
36
- Math.min(node.data.column_names.length, 10) * 40 + 250;
35
+ const minColumnsLength = node.data.column_names.filter(
36
+ col => col.order > 0,
37
+ ).length;
38
+ nodeHeightTracker[node.id] = Math.min(minColumnsLength, 5) * 40 + 250;
37
39
  dagreGraph.setNode(node.id, {
38
40
  width: nodeWidth,
39
41
  height: nodeHeightTracker[node.id],
@@ -52,7 +54,7 @@ const getLayoutedElements = (
52
54
  node.sourcePosition = isHorizontal ? 'right' : 'bottom';
53
55
  node.position = {
54
56
  x: nodeWithPosition.x - nodeWidth / 2,
55
- y: nodeWithPosition.y - nodeHeightTracker[node.id] / 2,
57
+ y: nodeWithPosition.y - nodeHeightTracker[node.id] / 3,
56
58
  };
57
59
  node.width = nodeWidth;
58
60
  node.height = nodeHeightTracker[node.id];
@@ -93,7 +93,7 @@ describe('AddEditNodePage submission failed', () => {
93
93
  expect(mockDjClient.DataJunctionAPI.tagsNode).toBeCalled();
94
94
  expect(mockDjClient.DataJunctionAPI.tagsNode).toBeCalledWith(
95
95
  'default.num_repair_orders',
96
- [{ display_name: 'Purpose', name: 'purpose' }],
96
+ ['purpose'],
97
97
  );
98
98
  expect(mockDjClient.DataJunctionAPI.tagsNode).toReturnWith({
99
99
  json: { message: 'Some tags were not found' },
@@ -120,7 +120,7 @@ describe('AddEditNodePage submission succeeded', () => {
120
120
  expect(mockDjClient.DataJunctionAPI.tagsNode).toBeCalledTimes(1);
121
121
  expect(mockDjClient.DataJunctionAPI.tagsNode).toBeCalledWith(
122
122
  'default.num_repair_orders',
123
- [{ display_name: 'Purpose', name: 'purpose' }],
123
+ ['purpose'],
124
124
  );
125
125
 
126
126
  expect(mockDjClient.DataJunctionAPI.listMetricMetadata).toBeCalledTimes(
@@ -214,6 +214,11 @@ export function AddEditNodePage() {
214
214
  fields.forEach(field => {
215
215
  if (field === 'primary_key') {
216
216
  setFieldValue(field, primaryKey.join(', '));
217
+ } else if (field === 'tags') {
218
+ setFieldValue(
219
+ field,
220
+ data[field].map(tag => tag.name),
221
+ );
217
222
  } else {
218
223
  setFieldValue(field, data[field] || '', false);
219
224
  }
@@ -1,10 +1,8 @@
1
1
  import { useContext, useEffect, useRef, useState } from 'react';
2
2
  import * as React from 'react';
3
3
  import DJClientContext from '../../providers/djclient';
4
- import { ErrorMessage, Field, Form, Formik } from 'formik';
5
- import { FormikSelect } from '../AddEditNodePage/FormikSelect';
6
- import EditIcon from '../../icons/EditIcon';
7
- import { displayMessageAfterSubmit, labelize } from '../../../utils/form';
4
+ import { Field, Form, Formik } from 'formik';
5
+ import { displayMessageAfterSubmit } from '../../../utils/form';
8
6
 
9
7
  export default function AddBackfillPopover({
10
8
  node,
@@ -5,6 +5,8 @@ import EditColumnPopover from './EditColumnPopover';
5
5
  import LinkDimensionPopover from './LinkDimensionPopover';
6
6
  import { labelize } from '../../../utils/form';
7
7
  import PartitionColumnPopover from './PartitionColumnPopover';
8
+ import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
9
+ import { foundation } from 'react-syntax-highlighter/src/styles/hljs';
8
10
 
9
11
  export default function NodeColumnTab({ node, djClient }) {
10
12
  const [attributes, setAttributes] = useState([]);
@@ -175,26 +177,65 @@ export default function NodeColumnTab({ node, djClient }) {
175
177
  };
176
178
 
177
179
  return (
178
- <div className="table-responsive">
179
- <table className="card-inner-table table">
180
- <thead className="fs-7 fw-bold text-gray-400 border-bottom-0">
181
- <tr>
182
- <th className="text-start">Column</th>
183
- <th>Display Name</th>
184
- <th>Type</th>
185
- {node?.type !== 'cube' ? (
186
- <>
187
- <th>Linked Dimension</th>
188
- <th>Attributes</th>
189
- </>
190
- ) : (
191
- ''
192
- )}
193
- <th>Partition</th>
194
- </tr>
195
- </thead>
196
- <tbody>{columnList(columns)}</tbody>
197
- </table>
198
- </div>
180
+ <>
181
+ <div className="table-responsive">
182
+ <table className="card-inner-table table">
183
+ <thead className="fs-7 fw-bold text-gray-400 border-bottom-0">
184
+ <tr>
185
+ <th className="text-start">Column</th>
186
+ <th>Display Name</th>
187
+ <th>Type</th>
188
+ {node?.type !== 'cube' ? (
189
+ <>
190
+ <th>Linked Dimension</th>
191
+ <th>Attributes</th>
192
+ </>
193
+ ) : (
194
+ ''
195
+ )}
196
+ <th>Partition</th>
197
+ </tr>
198
+ </thead>
199
+ <tbody>{columnList(columns)}</tbody>
200
+ </table>
201
+ </div>
202
+ <div>
203
+ <h3>Linked Dimensions (Custom Join SQL)</h3>
204
+ <table className="card-inner-table table">
205
+ <thead className="fs-7 fw-bold text-gray-400 border-bottom-0">
206
+ <tr>
207
+ <th className="text-start">Dimension Node</th>
208
+ <th>Join Type</th>
209
+ <th>Join SQL</th>
210
+ <th>Role</th>
211
+ </tr>
212
+ </thead>
213
+ <tbody>
214
+ {node?.dimension_links.map(link => {
215
+ return (
216
+ <tr>
217
+ <td>
218
+ <a href={'/nodes/' + link.dimension.name}>
219
+ {link.dimension.name}
220
+ </a>
221
+ </td>
222
+ <td>{link.join_type.toUpperCase()}</td>
223
+ <td style={{ width: '25rem', maxWidth: 'none' }}>
224
+ <SyntaxHighlighter
225
+ language="sql"
226
+ style={foundation}
227
+ wrapLongLines={true}
228
+ >
229
+ {link.join_sql}
230
+ </SyntaxHighlighter>
231
+ </td>
232
+ <td>{link.role}</td>
233
+ </tr>
234
+ );
235
+ })}
236
+ </tbody>
237
+ </table>
238
+ </div>
239
+ </>
199
240
  );
200
241
  }
@@ -0,0 +1,80 @@
1
+ import { useEffect, useState } from 'react';
2
+ import * as React from 'react';
3
+ import { labelize } from '../../../utils/form';
4
+
5
+ export default function NodeDimensionsTab({ node, djClient }) {
6
+ const [dimensions, setDimensions] = useState([]);
7
+ useEffect(() => {
8
+ const fetchData = async () => {
9
+ if (node) {
10
+ const data = await djClient.nodeDimensions(node.name);
11
+ const grouped = Object.entries(
12
+ data.reduce((group, dimension) => {
13
+ group[dimension.node_name + dimension.path] =
14
+ group[dimension.node_name + dimension.path] ?? [];
15
+ group[dimension.node_name + dimension.path].push(dimension);
16
+ return group;
17
+ }, {}),
18
+ );
19
+ setDimensions(grouped);
20
+ }
21
+ };
22
+ fetchData().catch(console.error);
23
+ }, [djClient, node]);
24
+
25
+ // Builds the block of dimensions selectors, grouped by node name + path
26
+ return (
27
+ <div style={{ padding: '1rem' }}>
28
+ {dimensions.map(grouping => {
29
+ const dimensionsInGroup = grouping[1];
30
+ const role = dimensionsInGroup[0].path
31
+ .map(pathItem => pathItem.split('.').slice(-1))
32
+ .join(' → ');
33
+ const fullPath = dimensionsInGroup[0].path.join(' → ');
34
+ const groupHeader = (
35
+ <h4
36
+ style={{
37
+ fontWeight: 'normal',
38
+ marginBottom: '5px',
39
+ marginTop: '15px',
40
+ }}
41
+ >
42
+ <a href={`/nodes/${dimensionsInGroup[0].node_name}`}>
43
+ <b>{dimensionsInGroup[0].node_display_name}</b>
44
+ </a>{' '}
45
+ with role{' '}
46
+ <span className="HighlightPath">
47
+ <b>{role}</b>
48
+ </span>{' '}
49
+ via <span className="HighlightPath">{fullPath}</span>
50
+ </h4>
51
+ );
52
+ const dimensionGroupOptions = dimensionsInGroup.map(dim => {
53
+ return {
54
+ value: dim.name,
55
+ label:
56
+ labelize(dim.name.split('.').slice(-1)[0]) +
57
+ (dim.is_primary_key ? ' (PK)' : ''),
58
+ };
59
+ });
60
+ return (
61
+ <>
62
+ {groupHeader}
63
+ <div className="dimensionsList">
64
+ {dimensionGroupOptions.map(dimension => {
65
+ return (
66
+ <div>
67
+ {dimension.label.split('[').slice(0)[0]} ⇢{' '}
68
+ <code className="DimensionAttribute">
69
+ {dimension.value}
70
+ </code>
71
+ </div>
72
+ );
73
+ })}
74
+ </div>
75
+ </>
76
+ );
77
+ })}
78
+ </div>
79
+ );
80
+ }
@@ -15,9 +15,20 @@ 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 column_names = node.columns.map(col => {
19
- return { name: col.name, type: col.type };
20
- });
18
+ const column_names = node.columns
19
+ .map(col => {
20
+ return {
21
+ name: col.name,
22
+ type: col.type,
23
+ dimension: col.dimension !== null ? col.dimension.name : null,
24
+ order: primary_key.includes(col.name)
25
+ ? -1
26
+ : col.dimension !== null
27
+ ? 0
28
+ : 1,
29
+ };
30
+ })
31
+ .sort((a, b) => a.order - b.order);
21
32
  return {
22
33
  id: String(node.name),
23
34
  type: 'DJNode',
@@ -1,4 +1,7 @@
1
1
  import { useEffect, useState } from 'react';
2
+ import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
3
+ import { foundation } from 'react-syntax-highlighter/src/styles/hljs';
4
+ import * as React from 'react';
2
5
 
3
6
  export default function NodeHistory({ node, djClient }) {
4
7
  const [history, setHistory] = useState([]);
@@ -175,7 +178,15 @@ export default function NodeHistory({ node, djClient }) {
175
178
  </td>
176
179
  <td>{revision.display_name}</td>
177
180
  <td>{revision.description}</td>
178
- <td>{revision.query}</td>
181
+ <td>
182
+ <SyntaxHighlighter
183
+ language="sql"
184
+ style={foundation}
185
+ wrapLongLines={true}
186
+ >
187
+ {revision.query}
188
+ </SyntaxHighlighter>
189
+ </td>
179
190
  <td>{revision.tags}</td>
180
191
  </tr>
181
192
  ));
@@ -0,0 +1,162 @@
1
+ import React from 'react';
2
+ import { render, waitFor, screen } from '@testing-library/react';
3
+ import NodeColumnTab from '../NodeColumnTab';
4
+
5
+ describe('<NodeColumnTab />', () => {
6
+ const mockDjClient = {
7
+ node: jest.fn(),
8
+ columns: jest.fn(),
9
+ attributes: jest.fn(),
10
+ dimensions: jest.fn(),
11
+ };
12
+
13
+ const mockNodeColumns = [
14
+ {
15
+ name: 'repair_order_id',
16
+ display_name: 'Repair Order Id',
17
+ type: 'int',
18
+ attributes: [],
19
+ dimension: null,
20
+ },
21
+ {
22
+ name: 'municipality_id',
23
+ display_name: 'Municipality Id',
24
+ type: 'string',
25
+ attributes: [],
26
+ dimension: null,
27
+ },
28
+ {
29
+ name: 'hard_hat_id',
30
+ display_name: 'Hard Hat Id',
31
+ type: 'int',
32
+ attributes: [],
33
+ dimension: null,
34
+ },
35
+ {
36
+ name: 'order_date',
37
+ display_name: 'Order Date',
38
+ type: 'date',
39
+ attributes: [],
40
+ dimension: null,
41
+ },
42
+ {
43
+ name: 'required_date',
44
+ display_name: 'Required Date',
45
+ type: 'date',
46
+ attributes: [],
47
+ dimension: null,
48
+ },
49
+ {
50
+ name: 'dispatched_date',
51
+ display_name: 'Dispatched Date',
52
+ type: 'date',
53
+ attributes: [],
54
+ dimension: null,
55
+ },
56
+ {
57
+ name: 'dispatcher_id',
58
+ display_name: 'Dispatcher Id',
59
+ type: 'int',
60
+ attributes: [],
61
+ dimension: null,
62
+ },
63
+ ];
64
+
65
+ const mockNode = {
66
+ node_revision_id: 1,
67
+ node_id: 1,
68
+ type: 'source',
69
+ name: 'default.repair_orders',
70
+ display_name: 'Default: Repair Orders',
71
+ version: 'v1.0',
72
+ status: 'valid',
73
+ mode: 'published',
74
+ catalog: {
75
+ id: 1,
76
+ uuid: '0fc18295-e1a2-4c3c-b72a-894725c12488',
77
+ created_at: '2023-08-21T16:48:51.146121+00:00',
78
+ updated_at: '2023-08-21T16:48:51.146122+00:00',
79
+ extra_params: {},
80
+ name: 'warehouse',
81
+ },
82
+ schema_: 'roads',
83
+ table: 'repair_orders',
84
+ description: 'Repair orders',
85
+ query: null,
86
+ availability: null,
87
+ columns: mockNodeColumns,
88
+ updated_at: '2023-08-21T16:48:52.880498+00:00',
89
+ materializations: [],
90
+ parents: [],
91
+ dimension_links: [
92
+ {
93
+ dimension: {
94
+ name: 'default.contractor',
95
+ },
96
+ join_type: 'left',
97
+ join_sql:
98
+ 'default.contractor.contractor_id = default.repair_orders.contractor_id',
99
+ join_cardinality: 'one_to_one',
100
+ role: 'contractor',
101
+ },
102
+ ],
103
+ };
104
+
105
+ const mockAttributes = [
106
+ {
107
+ uniqueness_scope: [],
108
+ namespace: 'system',
109
+ name: 'primary_key',
110
+ description:
111
+ 'Points to a column which is part of the primary key of the node',
112
+ allowed_node_types: ['source', 'transform', 'dimension'],
113
+ id: 1,
114
+ },
115
+ {
116
+ uniqueness_scope: [],
117
+ namespace: 'system',
118
+ name: 'dimension',
119
+ description: 'Points to a dimension attribute column',
120
+ allowed_node_types: ['source', 'transform'],
121
+ id: 2,
122
+ },
123
+ ];
124
+
125
+ const mockDimensions = ['default.contractor', 'default.hard_hat'];
126
+
127
+ beforeEach(() => {
128
+ // Reset the mocks before each test
129
+ mockDjClient.node.mockReset();
130
+ mockDjClient.columns.mockReset();
131
+ mockDjClient.attributes.mockReset();
132
+ mockDjClient.dimensions.mockReset();
133
+ });
134
+
135
+ it('renders node columns and dimension links', async () => {
136
+ mockDjClient.node.mockReturnValue(mockNode);
137
+ mockDjClient.columns.mockReturnValue(mockNodeColumns);
138
+ mockDjClient.attributes.mockReturnValue(mockAttributes);
139
+ mockDjClient.dimensions.mockReturnValue(mockDimensions);
140
+
141
+ render(<NodeColumnTab node={mockNode} djClient={mockDjClient} />);
142
+
143
+ await waitFor(() => {
144
+ // Displays the columns
145
+ for (const column of mockNode.columns) {
146
+ expect(screen.getByText(column.name)).toBeInTheDocument();
147
+ expect(screen.getByText(column.display_name)).toBeInTheDocument();
148
+ }
149
+
150
+ // Displays the dimension links
151
+ for (const dimensionLink of mockNode.dimension_links) {
152
+ const link = screen
153
+ .getByText(dimensionLink.dimension.name)
154
+ .closest('a');
155
+ expect(link).toHaveAttribute(
156
+ 'href',
157
+ `/nodes/${dimensionLink.dimension.name}`,
158
+ );
159
+ }
160
+ });
161
+ });
162
+ });
@@ -0,0 +1,145 @@
1
+ import React from 'react';
2
+ import { render, waitFor, screen } from '@testing-library/react';
3
+ import NodeDimensionsTab from '../NodeDimensionsTab';
4
+
5
+ describe('<NodeDimensionsTab />', () => {
6
+ const mockDjClient = {
7
+ node: jest.fn(),
8
+ nodeDimensions: jest.fn(),
9
+ };
10
+
11
+ const mockNode = {
12
+ node_revision_id: 1,
13
+ node_id: 1,
14
+ type: 'source',
15
+ name: 'default.repair_orders',
16
+ display_name: 'Default: Repair Orders',
17
+ version: 'v1.0',
18
+ status: 'valid',
19
+ mode: 'published',
20
+ catalog: {
21
+ id: 1,
22
+ uuid: '0fc18295-e1a2-4c3c-b72a-894725c12488',
23
+ created_at: '2023-08-21T16:48:51.146121+00:00',
24
+ updated_at: '2023-08-21T16:48:51.146122+00:00',
25
+ extra_params: {},
26
+ name: 'warehouse',
27
+ },
28
+ schema_: 'roads',
29
+ table: 'repair_orders',
30
+ description: 'Repair orders',
31
+ query: null,
32
+ availability: null,
33
+ columns: [
34
+ {
35
+ name: 'repair_order_id',
36
+ type: 'int',
37
+ attributes: [],
38
+ dimension: null,
39
+ },
40
+ {
41
+ name: 'municipality_id',
42
+ type: 'string',
43
+ attributes: [],
44
+ dimension: null,
45
+ },
46
+ {
47
+ name: 'hard_hat_id',
48
+ type: 'int',
49
+ attributes: [],
50
+ dimension: null,
51
+ },
52
+ {
53
+ name: 'order_date',
54
+ type: 'date',
55
+ attributes: [],
56
+ dimension: null,
57
+ },
58
+ {
59
+ name: 'required_date',
60
+ type: 'date',
61
+ attributes: [],
62
+ dimension: null,
63
+ },
64
+ {
65
+ name: 'dispatched_date',
66
+ type: 'date',
67
+ attributes: [],
68
+ dimension: null,
69
+ },
70
+ {
71
+ name: 'dispatcher_id',
72
+ type: 'int',
73
+ attributes: [],
74
+ dimension: null,
75
+ },
76
+ ],
77
+ updated_at: '2023-08-21T16:48:52.880498+00:00',
78
+ materializations: [],
79
+ parents: [],
80
+ dimension_links: [
81
+ {
82
+ dimension: {
83
+ name: 'default.contractor',
84
+ },
85
+ join_type: 'left',
86
+ join_sql:
87
+ 'default.contractor.contractor_id = default.repair_orders.contractor_id',
88
+ join_cardinality: 'one_to_one',
89
+ role: 'contractor',
90
+ },
91
+ ],
92
+ };
93
+
94
+ const mockDimensions = [
95
+ {
96
+ is_primary_key: false,
97
+ name: 'default.dispatcher.company_name',
98
+ node_display_name: 'Default: Dispatcher',
99
+ node_name: 'default.dispatcher',
100
+ path: ['default.repair_orders_fact.dispatcher_id'],
101
+ type: 'string',
102
+ },
103
+ {
104
+ is_primary_key: true,
105
+ name: 'default.dispatcher.dispatcher_id',
106
+ node_display_name: 'Default: Dispatcher',
107
+ node_name: 'default.dispatcher',
108
+ path: ['default.repair_orders_fact.dispatcher_id'],
109
+ type: 'int',
110
+ },
111
+ {
112
+ is_primary_key: false,
113
+ name: 'default.hard_hat.city',
114
+ node_display_name: 'Default: Hard Hat',
115
+ node_name: 'default.hard_hat',
116
+ path: ['default.repair_orders_fact.hard_hat_id'],
117
+ type: 'string',
118
+ },
119
+ {
120
+ is_primary_key: true,
121
+ name: 'default.hard_hat.hard_hat_id',
122
+ node_display_name: 'Default: Hard Hat',
123
+ node_name: 'default.hard_hat',
124
+ path: ['default.repair_orders_fact.hard_hat_id'],
125
+ type: 'int',
126
+ },
127
+ ];
128
+
129
+ beforeEach(() => {
130
+ // Reset the mocks before each test
131
+ mockDjClient.nodeDimensions.mockReset();
132
+ });
133
+
134
+ it('renders nodes with dimensions', async () => {
135
+ mockDjClient.nodeDimensions.mockReturnValue(mockDimensions);
136
+ render(<NodeDimensionsTab node={mockNode} djClient={mockDjClient} />);
137
+ await waitFor(() => {
138
+ for (const dimension of mockDimensions) {
139
+ const link = screen.getByText(dimension.node_display_name).closest('a');
140
+ expect(link).toHaveAttribute('href', `/nodes/${dimension.node_name}`);
141
+ expect(screen.getByText(dimension.name)).toBeInTheDocument();
142
+ }
143
+ });
144
+ });
145
+ });
@@ -68,6 +68,7 @@ describe('<NodePage />', () => {
68
68
  query:
69
69
  'SELECT avg(price) default_DOT_avg_repair_price \n FROM default.repair_order_details\n\n',
70
70
  availability: null,
71
+ dimension_links: [],
71
72
  columns: [
72
73
  {
73
74
  name: 'default_DOT_avg_repair_price',
@@ -46,10 +46,53 @@ exports[`<NodePage /> renders the NodeHistory tab correctly 1`] = `
46
46
  Average repair price
47
47
  </td>
48
48
  <td>
49
- SELECT avg(price) default_DOT_avg_repair_price
50
- FROM default.repair_order_details
49
+ <pre
50
+ style="display: block; overflow-x: auto; padding: 2rem; background: rgb(238, 238, 238); color: black;"
51
+ >
52
+ <code
53
+ class="language-sql"
54
+ style="white-space: pre-wrap;"
55
+ >
56
+ <span>
57
+ <span
58
+ style="color: rgb(0, 153, 153);"
59
+ >
60
+ SELECT
61
+ </span>
62
+ <span>
63
+
64
+ </span>
65
+ <span
66
+ class="hljs-built_in"
67
+ >
68
+ avg
69
+ </span>
70
+ <span>
71
+ (price) default_DOT_avg_repair_price
72
+
73
+ </span>
74
+ </span>
75
+ <span>
76
+ <span>
77
+
78
+ </span>
79
+ <span
80
+ style="color: rgb(0, 153, 153);"
81
+ >
82
+ FROM
83
+ </span>
84
+ <span>
85
+ default.repair_order_details
51
86
 
87
+ </span>
88
+ </span>
89
+ <span>
90
+
52
91
 
92
+ </span>
93
+ <span />
94
+ </code>
95
+ </pre>
53
96
  </td>
54
97
  <td />
55
98
  </tr>
@@ -15,6 +15,7 @@ import NodesWithDimension from './NodesWithDimension';
15
15
  import NodeColumnLineage from './NodeLineageTab';
16
16
  import EditIcon from '../../icons/EditIcon';
17
17
  import AlertIcon from '../../icons/AlertIcon';
18
+ import NodeDimensionsTab from './NodeDimensionsTab';
18
19
 
19
20
  export function NodePage() {
20
21
  const djClient = useContext(DJClientContext).DataJunctionAPI;
@@ -104,6 +105,11 @@ export function NodePage() {
104
105
  name: 'Lineage',
105
106
  display: node?.type === 'metric',
106
107
  },
108
+ {
109
+ id: 8,
110
+ name: 'Dimensions',
111
+ display: node?.type !== 'cube',
112
+ },
107
113
  ];
108
114
  };
109
115
 
@@ -137,6 +143,9 @@ export function NodePage() {
137
143
  case 7:
138
144
  tabToDisplay = <NodeColumnLineage djNode={node} djClient={djClient} />;
139
145
  break;
146
+ case 8:
147
+ tabToDisplay = <NodeDimensionsTab node={node} djClient={djClient} />;
148
+ break;
140
149
  default: /* istanbul ignore next */
141
150
  tabToDisplay = <NodeInfoTab node={node} />;
142
151
  }
@@ -543,6 +543,13 @@ export const DataJunctionAPI = {
543
543
  })
544
544
  ).json();
545
545
  },
546
+ nodeDimensions: async function (nodeName) {
547
+ return await (
548
+ await fetch(`${DJ_URL}/nodes/${nodeName}/dimensions`, {
549
+ credentials: 'include',
550
+ })
551
+ ).json();
552
+ },
546
553
  linkDimension: async function (nodeName, columnName, dimensionName) {
547
554
  const response = await fetch(
548
555
  `${DJ_URL}/nodes/${nodeName}/columns/${columnName}?dimension=${dimensionName}`,
@@ -43,6 +43,7 @@ export const mocks = {
43
43
  ],
44
44
  created_at: '2023-08-21T16:48:56.841631+00:00',
45
45
  tags: [{ name: 'purpose', display_name: 'Purpose' }],
46
+ dimension_links: [],
46
47
  dimensions: [
47
48
  {
48
49
  value: 'default.date_dim.dateint',
@@ -208,10 +208,12 @@
208
208
  }
209
209
  .custom-node-port {
210
210
  color: #636776;
211
- font-size: 20px;
211
+ font-size: 23px;
212
212
  text-align: center;
213
213
  }
214
-
214
+ .custom-node-emphasis {
215
+ border: 2px dashed #636776;
216
+ }
215
217
  .white_badge {
216
218
  background-color: #ffffff !important;
217
219
  display: inline-block;
@@ -1,6 +1,7 @@
1
1
  @import url('https://fonts.googleapis.com/css?family=Jost');
2
2
  @import url('https://fonts.googleapis.com/css2?family=Raleway:wght@300;600&display=swap');
3
3
  @import url('https://fonts.googleapis.com/css?family=Lato');
4
+ @import url('https://fonts.googleapis.com/css?family=Consolas');
4
5
 
5
6
  body {
6
7
  margin: 0;
@@ -1091,3 +1092,17 @@ pre {
1091
1092
  .partitionLink:hover {
1092
1093
  text-decoration: none;
1093
1094
  }
1095
+
1096
+ .dimensionsList {
1097
+ padding: 12px;
1098
+ opacity: 1;
1099
+ border-radius: 0.5rem;
1100
+ line-height: 1.55rem;
1101
+ font-size: 0.95rem;
1102
+ }
1103
+
1104
+ .DimensionAttribute {
1105
+ background: #effcff;
1106
+ padding: 5px;
1107
+ font-family: Consolas, serif;
1108
+ }