datajunction-ui 0.0.1-a49 → 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 +2 -2
- package/src/app/pages/NodePage/DimensionFilter.jsx +1 -8
- package/src/app/pages/NodePage/LinkDimensionPopover.jsx +13 -5
- package/src/app/pages/NodePage/NodeColumnTab.jsx +5 -3
- package/src/app/pages/NodePage/NodeDependenciesTab.jsx +147 -0
- package/src/app/pages/NodePage/NodeGraphTab.jsx +2 -2
- package/src/app/pages/NodePage/NodeHistory.jsx +11 -0
- package/src/app/pages/NodePage/NodeValidateTab.jsx +11 -14
- package/src/app/pages/NodePage/__tests__/LinkDimensionPopover.test.jsx +0 -4
- 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/__tests__/NodePage.test.jsx +22 -14
- 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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "datajunction-ui",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.1a50",
|
|
4
4
|
"description": "DataJunction Metrics Platform UI",
|
|
5
5
|
"module": "src/index.tsx",
|
|
6
6
|
"repository": {
|
|
@@ -164,7 +164,7 @@
|
|
|
164
164
|
],
|
|
165
165
|
"coverageThreshold": {
|
|
166
166
|
"global": {
|
|
167
|
-
"statements":
|
|
167
|
+
"statements": 87,
|
|
168
168
|
"branches": 75,
|
|
169
169
|
"lines": 80,
|
|
170
170
|
"functions": 85
|
|
@@ -33,14 +33,7 @@ export default function DimensionFilter({ dimension, onChange }) {
|
|
|
33
33
|
)
|
|
34
34
|
.map(col => col.name);
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
// data endpoints are. To re-enable, uncomment the following line:
|
|
38
|
-
// const data = await djClient.nodeData(dimension.metadata.node_name);
|
|
39
|
-
const data = { results: [] };
|
|
40
|
-
|
|
41
|
-
// TODO: when the above is enabled, this will use each dimension node's 'Label' column
|
|
42
|
-
// to build the display label for the dropdown, while continuing to pass the primary
|
|
43
|
-
// key in for filtering
|
|
36
|
+
const data = await djClient.nodeData(dimension.metadata.node_name);
|
|
44
37
|
/* istanbul ignore if */
|
|
45
38
|
if (dimensionNode && data.results && data.results.length > 0) {
|
|
46
39
|
const columnNames = data.results[0].columns.map(
|
|
@@ -1,10 +1,11 @@
|
|
|
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 { Form, Formik } from 'formik';
|
|
4
|
+
import { Field, Form, Formik } from 'formik';
|
|
5
5
|
import { FormikSelect } from '../AddEditNodePage/FormikSelect';
|
|
6
6
|
import EditIcon from '../../icons/EditIcon';
|
|
7
7
|
import { displayMessageAfterSubmit } from '../../../utils/form';
|
|
8
|
+
import LoadingIcon from '../../icons/LoadingIcon';
|
|
8
9
|
|
|
9
10
|
export default function LinkDimensionPopover({
|
|
10
11
|
column,
|
|
@@ -35,11 +36,17 @@ export default function LinkDimensionPopover({
|
|
|
35
36
|
{ node, column, dimension },
|
|
36
37
|
{ setSubmitting, setStatus },
|
|
37
38
|
) => {
|
|
38
|
-
setSubmitting(false);
|
|
39
39
|
if (referencedDimensionNode && dimension === 'Remove') {
|
|
40
|
-
await unlinkDimension(
|
|
40
|
+
await unlinkDimension(
|
|
41
|
+
node,
|
|
42
|
+
column,
|
|
43
|
+
referencedDimensionNode,
|
|
44
|
+
setStatus,
|
|
45
|
+
).then(_ => setSubmitting(false));
|
|
41
46
|
} else {
|
|
42
|
-
await linkDimension(node, column, dimension, setStatus)
|
|
47
|
+
await linkDimension(node, column, dimension, setStatus).then(_ =>
|
|
48
|
+
setSubmitting(false),
|
|
49
|
+
);
|
|
43
50
|
}
|
|
44
51
|
onSubmit();
|
|
45
52
|
};
|
|
@@ -137,8 +144,9 @@ export default function LinkDimensionPopover({
|
|
|
137
144
|
type="submit"
|
|
138
145
|
aria-label="SaveLinkDimension"
|
|
139
146
|
aria-hidden="false"
|
|
147
|
+
disabled={isSubmitting}
|
|
140
148
|
>
|
|
141
|
-
Save
|
|
149
|
+
{isSubmitting ? <LoadingIcon /> : 'Save'}
|
|
142
150
|
</button>
|
|
143
151
|
</Form>
|
|
144
152
|
);
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react';
|
|
2
|
-
import ClientCodePopover from './ClientCodePopover';
|
|
3
2
|
import * as React from 'react';
|
|
4
3
|
import EditColumnPopover from './EditColumnPopover';
|
|
5
4
|
import LinkDimensionPopover from './LinkDimensionPopover';
|
|
@@ -35,8 +34,11 @@ export default function NodeColumnTab({ node, djClient }) {
|
|
|
35
34
|
useEffect(() => {
|
|
36
35
|
const fetchData = async () => {
|
|
37
36
|
const dimensions = await djClient.dimensions();
|
|
38
|
-
const options = dimensions.map(
|
|
39
|
-
return {
|
|
37
|
+
const options = dimensions.map(dim => {
|
|
38
|
+
return {
|
|
39
|
+
value: dim.name,
|
|
40
|
+
label: `${dim.name} (${dim.indegree} links)`,
|
|
41
|
+
};
|
|
40
42
|
});
|
|
41
43
|
setDimensions(options);
|
|
42
44
|
};
|
|
@@ -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;
|
|
@@ -74,6 +74,17 @@ export default function NodeHistory({ node, djClient }) {
|
|
|
74
74
|
</div>
|
|
75
75
|
);
|
|
76
76
|
}
|
|
77
|
+
if (event.activity_type === 'refresh') {
|
|
78
|
+
return (
|
|
79
|
+
<div className="history-left">
|
|
80
|
+
<b style={{ textTransform: 'capitalize' }}>{event.activity_type}</b>{' '}
|
|
81
|
+
{event.entity_type}{' '}
|
|
82
|
+
<b>
|
|
83
|
+
<a href={'/nodes/' + event.entity_name}>{event.entity_name}</a>
|
|
84
|
+
</b>
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
77
88
|
if (event.activity_type === 'update' && event.entity_type === 'node') {
|
|
78
89
|
return (
|
|
79
90
|
<div className="history-left">
|
|
@@ -93,26 +93,31 @@ export default function NodeValidateTab({ node, djClient }) {
|
|
|
93
93
|
const sse = await djClient.streamNodeData(node?.name, selection);
|
|
94
94
|
sse.onmessage = e => {
|
|
95
95
|
const messageData = JSON.parse(JSON.parse(e.data));
|
|
96
|
+
if (
|
|
97
|
+
messageData !== null &&
|
|
98
|
+
messageData?.state !== 'FINISHED' &&
|
|
99
|
+
messageData?.state !== 'CANCELED' &&
|
|
100
|
+
messageData?.state !== 'FAILED'
|
|
101
|
+
) {
|
|
102
|
+
setRunning(false);
|
|
103
|
+
}
|
|
96
104
|
if (messageData.results && messageData.results?.length > 0) {
|
|
97
105
|
messageData.numRows = messageData.results?.length
|
|
98
106
|
? messageData.results[0].rows.length
|
|
99
107
|
: [];
|
|
100
108
|
switchTab('results');
|
|
109
|
+
setRunning(false);
|
|
101
110
|
} else {
|
|
102
111
|
switchTab('info');
|
|
103
112
|
}
|
|
104
113
|
setQueryInfo(messageData);
|
|
105
114
|
};
|
|
106
115
|
sse.onerror = () => sse.close();
|
|
107
|
-
setRunning(false);
|
|
108
116
|
};
|
|
109
117
|
|
|
110
118
|
// Handle form submission (runs the query)
|
|
111
119
|
const handleSubmit = async (values, { setSubmitting, setStatus }) => {
|
|
112
|
-
await runQuery(values, setStatus, setSubmitting)
|
|
113
|
-
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
|
|
114
|
-
setSubmitting(false);
|
|
115
|
-
});
|
|
120
|
+
await runQuery(values, setStatus, setSubmitting);
|
|
116
121
|
};
|
|
117
122
|
|
|
118
123
|
// Handle when filter values are updated. This is available for all nodes.
|
|
@@ -245,15 +250,7 @@ export default function NodeValidateTab({ node, djClient }) {
|
|
|
245
250
|
className="button-3 execute-button"
|
|
246
251
|
style={{ marginTop: '1rem' }}
|
|
247
252
|
>
|
|
248
|
-
{running
|
|
249
|
-
(queryInfo !== null &&
|
|
250
|
-
queryInfo?.state !== 'FINISHED' &&
|
|
251
|
-
queryInfo?.state !== 'CANCELED' &&
|
|
252
|
-
queryInfo?.state !== 'FAILED') ? (
|
|
253
|
-
<LoadingIcon />
|
|
254
|
-
) : (
|
|
255
|
-
'► Run'
|
|
256
|
-
)}
|
|
253
|
+
{isSubmitting || running === true ? <LoadingIcon /> : '► Run'}
|
|
257
254
|
</button>
|
|
258
255
|
</div>
|
|
259
256
|
<div
|
|
@@ -58,7 +58,6 @@ describe('<LinkDimensionPopover />', () => {
|
|
|
58
58
|
fireEvent.keyDown(linkDimension.firstChild, { key: 'ArrowDown' });
|
|
59
59
|
fireEvent.click(screen.getByText('Dimension 1'));
|
|
60
60
|
fireEvent.click(getByText('Save'));
|
|
61
|
-
getByText('Save').click();
|
|
62
61
|
|
|
63
62
|
// Expect linkDimension to be called
|
|
64
63
|
await waitFor(() => {
|
|
@@ -74,7 +73,6 @@ describe('<LinkDimensionPopover />', () => {
|
|
|
74
73
|
fireEvent.keyDown(linkDimension.firstChild, { key: 'ArrowDown' });
|
|
75
74
|
fireEvent.click(screen.getByText('[Remove dimension link]'));
|
|
76
75
|
fireEvent.click(getByText('Save'));
|
|
77
|
-
getByText('Save').click();
|
|
78
76
|
|
|
79
77
|
// Expect unlinkDimension to be called
|
|
80
78
|
await waitFor(() => {
|
|
@@ -133,7 +131,6 @@ describe('<LinkDimensionPopover />', () => {
|
|
|
133
131
|
fireEvent.keyDown(linkDimension.firstChild, { key: 'ArrowDown' });
|
|
134
132
|
fireEvent.click(screen.getByText('Dimension 1'));
|
|
135
133
|
fireEvent.click(getByText('Save'));
|
|
136
|
-
getByText('Save').click();
|
|
137
134
|
|
|
138
135
|
// Expect linkDimension to be called
|
|
139
136
|
await waitFor(() => {
|
|
@@ -151,7 +148,6 @@ describe('<LinkDimensionPopover />', () => {
|
|
|
151
148
|
fireEvent.keyDown(linkDimension.firstChild, { key: 'ArrowDown' });
|
|
152
149
|
fireEvent.click(screen.getByText('[Remove Dimension]'));
|
|
153
150
|
fireEvent.click(getByText('Save'));
|
|
154
|
-
getByText('Save').click();
|
|
155
151
|
|
|
156
152
|
// Expect unlinkDimension to be called
|
|
157
153
|
await waitFor(() => {
|
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
|
|
|
@@ -706,9 +706,11 @@ describe('<NodePage />', () => {
|
|
|
706
706
|
expect(screen.getByText('Add Filters')).toBeInTheDocument();
|
|
707
707
|
expect(screen.getByText('Generated Query')).toBeInTheDocument();
|
|
708
708
|
expect(screen.getByText('Results')).toBeInTheDocument();
|
|
709
|
+
});
|
|
710
|
+
// Click on the 'Validate' tab
|
|
711
|
+
fireEvent.click(screen.getByRole('button', { name: '► Validate' }));
|
|
709
712
|
|
|
710
|
-
|
|
711
|
-
fireEvent.click(screen.getByRole('button', { name: '► Validate' }));
|
|
713
|
+
await waitFor(() => {
|
|
712
714
|
expect(djClient.DataJunctionAPI.node).toHaveBeenCalledWith(
|
|
713
715
|
mocks.mockMetricNode.name,
|
|
714
716
|
);
|
|
@@ -719,35 +721,41 @@ describe('<NodePage />', () => {
|
|
|
719
721
|
expect(djClient.DataJunctionAPI.nodeDimensions).toHaveBeenCalledWith(
|
|
720
722
|
mocks.mockMetricNode.name,
|
|
721
723
|
);
|
|
724
|
+
});
|
|
722
725
|
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
+
// Click on 'Run' to run the node query
|
|
727
|
+
const runButton = screen.getByText('► Run');
|
|
728
|
+
fireEvent.click(runButton);
|
|
726
729
|
|
|
730
|
+
await waitFor(() => {
|
|
727
731
|
expect(djClient.DataJunctionAPI.streamNodeData).toHaveBeenCalledWith(
|
|
728
732
|
mocks.mockMetricNode.name,
|
|
729
733
|
{ dimensions: [], filters: [] },
|
|
730
734
|
);
|
|
731
735
|
expect(streamNodeData.onmessage).toBeDefined();
|
|
732
736
|
expect(streamNodeData.onerror).toBeDefined();
|
|
737
|
+
});
|
|
733
738
|
|
|
734
|
-
|
|
735
|
-
|
|
739
|
+
const infoTab = screen.getByRole('button', { name: 'QueryInfo' });
|
|
740
|
+
const resultsTab = screen.getByText('Results');
|
|
736
741
|
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
742
|
+
// Initially, the Results tab should be active
|
|
743
|
+
expect(resultsTab).toHaveClass('active');
|
|
744
|
+
expect(infoTab).not.toHaveClass('active');
|
|
740
745
|
|
|
741
|
-
|
|
742
|
-
|
|
746
|
+
// Click on the Info tab first
|
|
747
|
+
fireEvent.click(infoTab);
|
|
743
748
|
|
|
749
|
+
await waitFor(() => {
|
|
744
750
|
// Now, the Info tab should be active
|
|
745
751
|
expect(infoTab).toHaveClass('active');
|
|
746
752
|
expect(resultsTab).not.toHaveClass('active');
|
|
753
|
+
});
|
|
747
754
|
|
|
748
|
-
|
|
749
|
-
|
|
755
|
+
// Click on the Results tab
|
|
756
|
+
fireEvent.click(resultsTab);
|
|
750
757
|
|
|
758
|
+
await waitFor(() => {
|
|
751
759
|
// Now, the Results tab should be active again
|
|
752
760
|
expect(resultsTab).toHaveClass('active');
|
|
753
761
|
expect(infoTab).not.toHaveClass('active');
|
|
@@ -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
|
-
}
|