datajunction-ui 0.0.1-a49.dev2 → 0.0.1-a50
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/NodePage/NodeDependenciesTab.jsx +147 -0
- package/src/app/pages/NodePage/NodeGraphTab.jsx +2 -2
- package/src/app/pages/NodePage/__tests__/{NodeDimensionsTab.test.jsx → NodeDependenciesTab.test.jsx} +9 -3
- package/src/app/pages/NodePage/__tests__/NodeGraphTab.test.jsx +2 -2
- package/src/app/pages/NodePage/index.jsx +7 -7
- package/src/styles/loading.css +1 -1
- package/src/app/pages/NodePage/NodeDimensionsTab.jsx +0 -80
package/package.json
CHANGED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { labelize } from '../../../utils/form';
|
|
4
|
+
import LoadingIcon from '../../icons/LoadingIcon';
|
|
5
|
+
|
|
6
|
+
export default function NodeDependenciesTab({ node, djClient }) {
|
|
7
|
+
const [nodeDAG, setNodeDAG] = useState({
|
|
8
|
+
upstreams: [],
|
|
9
|
+
downstreams: [],
|
|
10
|
+
dimensions: [],
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const [retrieved, setRetrieved] = useState(false);
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const fetchData = async () => {
|
|
17
|
+
let upstreams = await djClient.upstreams(node.name);
|
|
18
|
+
let downstreams = await djClient.downstreams(node.name);
|
|
19
|
+
let dimensions = await djClient.nodeDimensions(node.name);
|
|
20
|
+
setNodeDAG({
|
|
21
|
+
upstreams: upstreams,
|
|
22
|
+
downstreams: downstreams,
|
|
23
|
+
dimensions: dimensions,
|
|
24
|
+
});
|
|
25
|
+
setRetrieved(true);
|
|
26
|
+
};
|
|
27
|
+
fetchData().catch(console.error);
|
|
28
|
+
}, [djClient, node]);
|
|
29
|
+
|
|
30
|
+
// Builds the block of dimensions selectors, grouped by node name + path
|
|
31
|
+
return (
|
|
32
|
+
<div>
|
|
33
|
+
<h2>Upstreams</h2>
|
|
34
|
+
{retrieved ? (
|
|
35
|
+
<NodeList nodes={nodeDAG.upstreams} />
|
|
36
|
+
) : (
|
|
37
|
+
<span style={{ display: 'inline-block' }}>
|
|
38
|
+
<LoadingIcon />
|
|
39
|
+
</span>
|
|
40
|
+
)}
|
|
41
|
+
<h2>Downstreams</h2>
|
|
42
|
+
{retrieved ? (
|
|
43
|
+
<NodeList nodes={nodeDAG.downstreams} />
|
|
44
|
+
) : (
|
|
45
|
+
<span style={{ display: 'inline-block' }}>
|
|
46
|
+
<LoadingIcon />
|
|
47
|
+
</span>
|
|
48
|
+
)}
|
|
49
|
+
<h2>Dimensions</h2>
|
|
50
|
+
{retrieved ? (
|
|
51
|
+
<NodeDimensionsList rawDimensions={nodeDAG.dimensions} />
|
|
52
|
+
) : (
|
|
53
|
+
<span style={{ display: 'inline-block' }}>
|
|
54
|
+
<LoadingIcon />
|
|
55
|
+
</span>
|
|
56
|
+
)}
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function NodeDimensionsList({ rawDimensions }) {
|
|
62
|
+
const dimensions = Object.entries(
|
|
63
|
+
rawDimensions.reduce((group, dimension) => {
|
|
64
|
+
group[dimension.node_name + dimension.path] =
|
|
65
|
+
group[dimension.node_name + dimension.path] ?? [];
|
|
66
|
+
group[dimension.node_name + dimension.path].push(dimension);
|
|
67
|
+
return group;
|
|
68
|
+
}, {}),
|
|
69
|
+
);
|
|
70
|
+
return (
|
|
71
|
+
<div style={{ padding: '0.5rem' }}>
|
|
72
|
+
{dimensions.map(grouping => {
|
|
73
|
+
const dimensionsInGroup = grouping[1];
|
|
74
|
+
const role = dimensionsInGroup[0].path
|
|
75
|
+
.map(pathItem => pathItem.split('.').slice(-1))
|
|
76
|
+
.join(' → ');
|
|
77
|
+
const fullPath = dimensionsInGroup[0].path.join(' → ');
|
|
78
|
+
const groupHeader = (
|
|
79
|
+
<span
|
|
80
|
+
style={{
|
|
81
|
+
fontWeight: 'normal',
|
|
82
|
+
marginBottom: '15px',
|
|
83
|
+
marginTop: '15px',
|
|
84
|
+
}}
|
|
85
|
+
>
|
|
86
|
+
<a href={`/nodes/${dimensionsInGroup[0].node_name}`}>
|
|
87
|
+
<b>{dimensionsInGroup[0].node_display_name}</b>
|
|
88
|
+
</a>{' '}
|
|
89
|
+
with role{' '}
|
|
90
|
+
<span className="HighlightPath">
|
|
91
|
+
<b>{role}</b>
|
|
92
|
+
</span>{' '}
|
|
93
|
+
via <span className="HighlightPath">{fullPath}</span>
|
|
94
|
+
</span>
|
|
95
|
+
);
|
|
96
|
+
const dimensionGroupOptions = dimensionsInGroup.map(dim => {
|
|
97
|
+
return {
|
|
98
|
+
value: dim.name,
|
|
99
|
+
label:
|
|
100
|
+
labelize(dim.name.split('.').slice(-1)[0]) +
|
|
101
|
+
(dim.is_primary_key ? ' (PK)' : ''),
|
|
102
|
+
};
|
|
103
|
+
});
|
|
104
|
+
return (
|
|
105
|
+
<details>
|
|
106
|
+
<summary style={{ marginBottom: '10px' }}>{groupHeader}</summary>
|
|
107
|
+
<div className="dimensionsList">
|
|
108
|
+
{dimensionGroupOptions.map(dimension => {
|
|
109
|
+
return (
|
|
110
|
+
<div>
|
|
111
|
+
{dimension.label.split('[').slice(0)[0]} ⇢{' '}
|
|
112
|
+
<code className="DimensionAttribute">
|
|
113
|
+
{dimension.value}
|
|
114
|
+
</code>
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
})}
|
|
118
|
+
</div>
|
|
119
|
+
</details>
|
|
120
|
+
);
|
|
121
|
+
})}
|
|
122
|
+
</div>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function NodeList({ nodes }) {
|
|
127
|
+
return nodes && nodes.length > 0 ? (
|
|
128
|
+
<ul className="backfills">
|
|
129
|
+
{nodes?.map(node => (
|
|
130
|
+
<li className="backfill" style={{ marginBottom: '5px' }}>
|
|
131
|
+
<span
|
|
132
|
+
className={`node_type__${node.type} badge node_type`}
|
|
133
|
+
style={{ marginRight: '5px' }}
|
|
134
|
+
role="dialog"
|
|
135
|
+
aria-hidden="false"
|
|
136
|
+
aria-label="NodeType"
|
|
137
|
+
>
|
|
138
|
+
{node.type}
|
|
139
|
+
</span>
|
|
140
|
+
<a href={`/nodes/${node.name}`}>{node.name}</a>
|
|
141
|
+
</li>
|
|
142
|
+
))}
|
|
143
|
+
</ul>
|
|
144
|
+
) : (
|
|
145
|
+
<span style={{ display: 'inline-block' }}>None</span>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
@@ -6,7 +6,7 @@ import 'reactflow/dist/style.css';
|
|
|
6
6
|
import DJClientContext from '../../providers/djclient';
|
|
7
7
|
import LayoutFlow from '../../components/djgraph/LayoutFlow';
|
|
8
8
|
|
|
9
|
-
const
|
|
9
|
+
const NodeGraphTab = djNode => {
|
|
10
10
|
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
11
11
|
|
|
12
12
|
const createNode = node => {
|
|
@@ -134,4 +134,4 @@ const NodeLineage = djNode => {
|
|
|
134
134
|
};
|
|
135
135
|
return LayoutFlow(djNode, dagFetch);
|
|
136
136
|
};
|
|
137
|
-
export default
|
|
137
|
+
export default NodeGraphTab;
|
package/src/app/pages/NodePage/__tests__/{NodeDimensionsTab.test.jsx → NodeDependenciesTab.test.jsx}
RENAMED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { render, waitFor, screen } from '@testing-library/react';
|
|
3
|
-
import
|
|
3
|
+
import NodeDependenciesTab from '../NodeDependenciesTab';
|
|
4
4
|
|
|
5
|
-
describe('<
|
|
5
|
+
describe('<NodeDependenciesTab />', () => {
|
|
6
6
|
const mockDjClient = {
|
|
7
7
|
node: jest.fn(),
|
|
8
8
|
nodeDimensions: jest.fn(),
|
|
9
|
+
upstreams: jest.fn(),
|
|
10
|
+
downstreams: jest.fn(),
|
|
9
11
|
};
|
|
10
12
|
|
|
11
13
|
const mockNode = {
|
|
@@ -129,11 +131,15 @@ describe('<NodeDimensionsTab />', () => {
|
|
|
129
131
|
beforeEach(() => {
|
|
130
132
|
// Reset the mocks before each test
|
|
131
133
|
mockDjClient.nodeDimensions.mockReset();
|
|
134
|
+
mockDjClient.upstreams.mockReset();
|
|
135
|
+
mockDjClient.downstreams.mockReset();
|
|
132
136
|
});
|
|
133
137
|
|
|
134
138
|
it('renders nodes with dimensions', async () => {
|
|
135
139
|
mockDjClient.nodeDimensions.mockReturnValue(mockDimensions);
|
|
136
|
-
|
|
140
|
+
mockDjClient.upstreams.mockReturnValue([mockNode]);
|
|
141
|
+
mockDjClient.downstreams.mockReturnValue([mockNode]);
|
|
142
|
+
render(<NodeDependenciesTab node={mockNode} djClient={mockDjClient} />);
|
|
137
143
|
await waitFor(() => {
|
|
138
144
|
for (const dimension of mockDimensions) {
|
|
139
145
|
const link = screen.getByText(dimension.node_display_name).closest('a');
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { render, screen, waitFor } from '@testing-library/react';
|
|
3
|
-
import
|
|
3
|
+
import NodeGraphTab from '../NodeGraphTab';
|
|
4
4
|
import DJClientContext from '../../../providers/djclient';
|
|
5
5
|
|
|
6
6
|
describe('<NodeLineage />', () => {
|
|
@@ -564,7 +564,7 @@ describe('<NodeLineage />', () => {
|
|
|
564
564
|
// const layoutFlowMock = jest.spyOn(LayoutFlow);
|
|
565
565
|
const { container } = render(
|
|
566
566
|
<DJClientContext.Provider value={djClient}>
|
|
567
|
-
<
|
|
567
|
+
<NodeGraphTab {...defaultProps} />
|
|
568
568
|
</DJClientContext.Provider>,
|
|
569
569
|
);
|
|
570
570
|
|
|
@@ -5,7 +5,7 @@ import Tab from '../../components/Tab';
|
|
|
5
5
|
import NamespaceHeader from '../../components/NamespaceHeader';
|
|
6
6
|
import NodeInfoTab from './NodeInfoTab';
|
|
7
7
|
import NodeColumnTab from './NodeColumnTab';
|
|
8
|
-
import
|
|
8
|
+
import NodeGraphTab from './NodeGraphTab';
|
|
9
9
|
import NodeHistory from './NodeHistory';
|
|
10
10
|
import DJClientContext from '../../providers/djclient';
|
|
11
11
|
import NodeValidateTab from './NodeValidateTab';
|
|
@@ -15,7 +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
|
|
18
|
+
import NodeDependenciesTab from './NodeDependenciesTab';
|
|
19
19
|
import { useNavigate } from 'react-router-dom';
|
|
20
20
|
|
|
21
21
|
export function NodePage() {
|
|
@@ -112,8 +112,8 @@ export function NodePage() {
|
|
|
112
112
|
display: node?.type === 'metric',
|
|
113
113
|
},
|
|
114
114
|
{
|
|
115
|
-
id: '
|
|
116
|
-
name: '
|
|
115
|
+
id: 'dependencies',
|
|
116
|
+
name: 'Dependencies',
|
|
117
117
|
display: node?.type !== 'cube',
|
|
118
118
|
},
|
|
119
119
|
];
|
|
@@ -129,7 +129,7 @@ export function NodePage() {
|
|
|
129
129
|
tabToDisplay = <NodeColumnTab node={node} djClient={djClient} />;
|
|
130
130
|
break;
|
|
131
131
|
case 'graph':
|
|
132
|
-
tabToDisplay = <
|
|
132
|
+
tabToDisplay = <NodeGraphTab djNode={node} djClient={djClient} />;
|
|
133
133
|
break;
|
|
134
134
|
case 'history':
|
|
135
135
|
tabToDisplay = <NodeHistory node={node} djClient={djClient} />;
|
|
@@ -146,8 +146,8 @@ export function NodePage() {
|
|
|
146
146
|
case 'lineage':
|
|
147
147
|
tabToDisplay = <NodeColumnLineage djNode={node} djClient={djClient} />;
|
|
148
148
|
break;
|
|
149
|
-
case '
|
|
150
|
-
tabToDisplay = <
|
|
149
|
+
case 'dependencies':
|
|
150
|
+
tabToDisplay = <NodeDependenciesTab node={node} djClient={djClient} />;
|
|
151
151
|
break;
|
|
152
152
|
default: /* istanbul ignore next */
|
|
153
153
|
tabToDisplay = <NodeInfoTab node={node} />;
|
package/src/styles/loading.css
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
border: 8px solid #fff;
|
|
14
14
|
border-radius: 50%;
|
|
15
15
|
animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
|
|
16
|
-
border-color: #
|
|
16
|
+
border-color: #bfbfbf transparent transparent transparent;
|
|
17
17
|
}
|
|
18
18
|
.lds-ring div:nth-child(1) {
|
|
19
19
|
animation-delay: -0.45s;
|
|
@@ -1,80 +0,0 @@
|
|
|
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
|
-
}
|