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 +1 -1
- package/src/app/components/djgraph/Collapse.jsx +3 -2
- package/src/app/components/djgraph/DJNodeColumns.jsx +5 -1
- package/src/app/components/djgraph/LayoutFlow.jsx +5 -3
- 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 +5 -0
- 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/NodeGraphTab.jsx +14 -3
- 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/dag.css +4 -2
- package/src/styles/index.css +15 -0
package/package.json
CHANGED
|
@@ -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:
|
|
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 >
|
|
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={
|
|
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
|
-
|
|
36
|
-
|
|
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] /
|
|
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
|
-
[
|
|
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
|
-
[
|
|
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 {
|
|
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
|
+
}
|
|
@@ -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
|
|
19
|
-
|
|
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>
|
|
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/dag.css
CHANGED
|
@@ -208,10 +208,12 @@
|
|
|
208
208
|
}
|
|
209
209
|
.custom-node-port {
|
|
210
210
|
color: #636776;
|
|
211
|
-
font-size:
|
|
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;
|
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,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
|
+
}
|