datajunction-ui 0.0.1-a39.dev0 → 0.0.1-a40.dev
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 +1 -1
- package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormFailed.test.jsx +1 -1
- package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormSuccess.test.jsx +1 -1
- package/src/app/pages/AddEditNodePage/index.jsx +1 -2
- package/src/app/pages/NodePage/AddBackfillPopover.jsx +2 -4
- package/src/app/pages/NodePage/NodeColumnTab.jsx +62 -21
- package/src/app/pages/NodePage/NodeDimensionsTab.jsx +80 -0
- package/src/app/pages/NodePage/NodeHistory.jsx +12 -1
- package/src/app/pages/NodePage/__tests__/NodeColumnTab.test.jsx +162 -0
- package/src/app/pages/NodePage/__tests__/NodeDimensionsTab.test.jsx +145 -0
- package/src/app/pages/NodePage/__tests__/NodePage.test.jsx +1 -0
- package/src/app/pages/NodePage/__tests__/__snapshots__/NodePage.test.jsx.snap +45 -2
- package/src/app/pages/NodePage/index.jsx +9 -0
- package/src/app/services/DJService.js +7 -0
- package/src/mocks/mockNodes.jsx +1 -0
- package/src/styles/index.css +16 -0
package/package.json
CHANGED
|
@@ -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
|
-
['purpose'],
|
|
96
|
+
[{ display_name: 'Purpose', name: '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
|
-
['purpose'],
|
|
123
|
+
[{ display_name: 'Purpose', name: 'purpose' }],
|
|
124
124
|
);
|
|
125
125
|
|
|
126
126
|
expect(mockDjClient.DataJunctionAPI.listMetricMetadata).toBeCalledTimes(
|
|
@@ -144,10 +144,9 @@ export function AddEditNodePage() {
|
|
|
144
144
|
values.metric_direction,
|
|
145
145
|
values.metric_unit,
|
|
146
146
|
);
|
|
147
|
-
|
|
148
147
|
const tagsResponse = await djClient.tagsNode(
|
|
149
148
|
values.name,
|
|
150
|
-
values.tags.map(tag => tag
|
|
149
|
+
values.tags.map(tag => tag),
|
|
151
150
|
);
|
|
152
151
|
if ((status === 200 || status === 201) && tagsResponse.status === 200) {
|
|
153
152
|
setStatus({
|
|
@@ -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 {
|
|
5
|
-
import {
|
|
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
|
-
|
|
179
|
-
<
|
|
180
|
-
<
|
|
181
|
-
<
|
|
182
|
-
<
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
+
}
|
|
@@ -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>
|
|
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
|
+
});
|
|
@@ -46,10 +46,53 @@ exports[`<NodePage /> renders the NodeHistory tab correctly 1`] = `
|
|
|
46
46
|
Average repair price
|
|
47
47
|
</td>
|
|
48
48
|
<td>
|
|
49
|
-
|
|
50
|
-
|
|
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}`,
|
package/src/mocks/mockNodes.jsx
CHANGED
package/src/styles/index.css
CHANGED
|
@@ -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,18 @@ pre {
|
|
|
1091
1092
|
.partitionLink:hover {
|
|
1092
1093
|
text-decoration: none;
|
|
1093
1094
|
}
|
|
1095
|
+
|
|
1096
|
+
|
|
1097
|
+
.dimensionsList {
|
|
1098
|
+
padding: 12px;
|
|
1099
|
+
opacity: 1;
|
|
1100
|
+
border-radius: 0.5rem;
|
|
1101
|
+
line-height: 1.55rem;
|
|
1102
|
+
font-size: 0.95rem;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
.DimensionAttribute {
|
|
1106
|
+
background: #effcff;
|
|
1107
|
+
padding: 5px;
|
|
1108
|
+
font-family: Consolas, serif;
|
|
1109
|
+
}
|