datajunction-ui 0.0.20 → 0.0.22
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 +47 -45
- package/src/app/pages/NodePage/__tests__/NodeDependenciesTab.test.jsx +13 -7
- package/src/app/services/DJService.js +43 -0
- package/src/app/services/__tests__/DJService.test.jsx +76 -0
- package/src/styles/nav-bar.css +5 -5
- package/webpack.config.js +3 -1
package/package.json
CHANGED
|
@@ -4,53 +4,51 @@ import { labelize } from '../../../utils/form';
|
|
|
4
4
|
import LoadingIcon from '../../icons/LoadingIcon';
|
|
5
5
|
|
|
6
6
|
export default function NodeDependenciesTab({ node, djClient }) {
|
|
7
|
-
const [
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
dimensions: [],
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
const [retrieved, setRetrieved] = useState(false);
|
|
7
|
+
const [upstreams, setUpstreams] = useState(null);
|
|
8
|
+
const [downstreams, setDownstreams] = useState(null);
|
|
9
|
+
const [dimensions, setDimensions] = useState(null);
|
|
14
10
|
|
|
15
11
|
useEffect(() => {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
12
|
+
if (node) {
|
|
13
|
+
// Reset state when node changes
|
|
14
|
+
setUpstreams(null);
|
|
15
|
+
setDownstreams(null);
|
|
16
|
+
setDimensions(null);
|
|
17
|
+
|
|
18
|
+
// Load all three in parallel, display each as it loads
|
|
19
|
+
djClient.upstreamsGQL(node.name).then(setUpstreams).catch(console.error);
|
|
20
|
+
djClient
|
|
21
|
+
.downstreamsGQL(node.name)
|
|
22
|
+
.then(setDownstreams)
|
|
23
|
+
.catch(console.error);
|
|
24
|
+
djClient
|
|
25
|
+
.nodeDimensions(node.name)
|
|
26
|
+
.then(setDimensions)
|
|
27
|
+
.catch(console.error);
|
|
28
|
+
}
|
|
30
29
|
}, [djClient, node]);
|
|
31
30
|
|
|
32
|
-
// Builds the block of dimensions selectors, grouped by node name + path
|
|
33
31
|
return (
|
|
34
32
|
<div>
|
|
35
33
|
<h2>Upstreams</h2>
|
|
36
|
-
{
|
|
37
|
-
<NodeList nodes={
|
|
34
|
+
{upstreams !== null ? (
|
|
35
|
+
<NodeList nodes={upstreams} />
|
|
38
36
|
) : (
|
|
39
37
|
<span style={{ display: 'block' }}>
|
|
40
38
|
<LoadingIcon centered={false} />
|
|
41
39
|
</span>
|
|
42
40
|
)}
|
|
43
41
|
<h2>Downstreams</h2>
|
|
44
|
-
{
|
|
45
|
-
<NodeList nodes={
|
|
42
|
+
{downstreams !== null ? (
|
|
43
|
+
<NodeList nodes={downstreams} />
|
|
46
44
|
) : (
|
|
47
45
|
<span style={{ display: 'block' }}>
|
|
48
46
|
<LoadingIcon centered={false} />
|
|
49
47
|
</span>
|
|
50
48
|
)}
|
|
51
49
|
<h2>Dimensions</h2>
|
|
52
|
-
{
|
|
53
|
-
<NodeDimensionsList rawDimensions={
|
|
50
|
+
{dimensions !== null ? (
|
|
51
|
+
<NodeDimensionsList rawDimensions={dimensions} />
|
|
54
52
|
) : (
|
|
55
53
|
<span style={{ display: 'block' }}>
|
|
56
54
|
<LoadingIcon centered={false} />
|
|
@@ -128,24 +126,28 @@ export function NodeDimensionsList({ rawDimensions }) {
|
|
|
128
126
|
export function NodeList({ nodes }) {
|
|
129
127
|
return nodes && nodes.length > 0 ? (
|
|
130
128
|
<ul className="backfills">
|
|
131
|
-
{nodes?.map(node =>
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
style={{ marginRight: '5px' }}
|
|
140
|
-
role="dialog"
|
|
141
|
-
aria-hidden="false"
|
|
142
|
-
aria-label="NodeType"
|
|
129
|
+
{nodes?.map(node => {
|
|
130
|
+
// GraphQL returns uppercase types (e.g., "SOURCE"), CSS classes expect lowercase
|
|
131
|
+
const nodeType = node.type?.toLowerCase() || '';
|
|
132
|
+
return (
|
|
133
|
+
<li
|
|
134
|
+
className="backfill"
|
|
135
|
+
style={{ marginBottom: '5px' }}
|
|
136
|
+
key={node.name}
|
|
143
137
|
>
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
138
|
+
<span
|
|
139
|
+
className={`node_type__${nodeType} badge node_type`}
|
|
140
|
+
style={{ marginRight: '5px' }}
|
|
141
|
+
role="dialog"
|
|
142
|
+
aria-hidden="false"
|
|
143
|
+
aria-label="NodeType"
|
|
144
|
+
>
|
|
145
|
+
{nodeType}
|
|
146
|
+
</span>
|
|
147
|
+
<a href={`/nodes/${node.name}`}>{node.name}</a>
|
|
148
|
+
</li>
|
|
149
|
+
);
|
|
150
|
+
})}
|
|
149
151
|
</ul>
|
|
150
152
|
) : (
|
|
151
153
|
<span style={{ display: 'block' }}>None</span>
|
|
@@ -6,8 +6,8 @@ describe('<NodeDependenciesTab />', () => {
|
|
|
6
6
|
const mockDjClient = {
|
|
7
7
|
node: jest.fn(),
|
|
8
8
|
nodeDimensions: jest.fn(),
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
upstreamsGQL: jest.fn(),
|
|
10
|
+
downstreamsGQL: jest.fn(),
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
const mockNode = {
|
|
@@ -131,14 +131,20 @@ describe('<NodeDependenciesTab />', () => {
|
|
|
131
131
|
beforeEach(() => {
|
|
132
132
|
// Reset the mocks before each test
|
|
133
133
|
mockDjClient.nodeDimensions.mockReset();
|
|
134
|
-
mockDjClient.
|
|
135
|
-
mockDjClient.
|
|
134
|
+
mockDjClient.upstreamsGQL.mockReset();
|
|
135
|
+
mockDjClient.downstreamsGQL.mockReset();
|
|
136
136
|
});
|
|
137
137
|
|
|
138
138
|
it('renders nodes with dimensions', async () => {
|
|
139
|
-
|
|
140
|
-
mockDjClient.
|
|
141
|
-
|
|
139
|
+
// Use mockResolvedValue since the component now uses .then()
|
|
140
|
+
mockDjClient.nodeDimensions.mockResolvedValue(mockDimensions);
|
|
141
|
+
// GraphQL responses return uppercase types (e.g., "SOURCE")
|
|
142
|
+
mockDjClient.upstreamsGQL.mockResolvedValue([
|
|
143
|
+
{ name: mockNode.name, type: 'SOURCE' },
|
|
144
|
+
]);
|
|
145
|
+
mockDjClient.downstreamsGQL.mockResolvedValue([
|
|
146
|
+
{ name: mockNode.name, type: 'SOURCE' },
|
|
147
|
+
]);
|
|
142
148
|
render(<NodeDependenciesTab node={mockNode} djClient={mockDjClient} />);
|
|
143
149
|
await waitFor(() => {
|
|
144
150
|
for (const dimension of mockDimensions) {
|
|
@@ -690,6 +690,49 @@ export const DataJunctionAPI = {
|
|
|
690
690
|
).json();
|
|
691
691
|
},
|
|
692
692
|
|
|
693
|
+
// GraphQL-based upstream/downstream queries - more efficient as they only fetch needed fields
|
|
694
|
+
upstreamsGQL: async function (nodeNames) {
|
|
695
|
+
const names = Array.isArray(nodeNames) ? nodeNames : [nodeNames];
|
|
696
|
+
const query = `
|
|
697
|
+
query GetUpstreamNodes($nodeNames: [String!]!) {
|
|
698
|
+
upstreamNodes(nodeNames: $nodeNames) {
|
|
699
|
+
name
|
|
700
|
+
type
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
`;
|
|
704
|
+
const results = await (
|
|
705
|
+
await fetch(DJ_GQL, {
|
|
706
|
+
method: 'POST',
|
|
707
|
+
headers: { 'Content-Type': 'application/json' },
|
|
708
|
+
credentials: 'include',
|
|
709
|
+
body: JSON.stringify({ query, variables: { nodeNames: names } }),
|
|
710
|
+
})
|
|
711
|
+
).json();
|
|
712
|
+
return results.data?.upstreamNodes || [];
|
|
713
|
+
},
|
|
714
|
+
|
|
715
|
+
downstreamsGQL: async function (nodeNames) {
|
|
716
|
+
const names = Array.isArray(nodeNames) ? nodeNames : [nodeNames];
|
|
717
|
+
const query = `
|
|
718
|
+
query GetDownstreamNodes($nodeNames: [String!]!) {
|
|
719
|
+
downstreamNodes(nodeNames: $nodeNames) {
|
|
720
|
+
name
|
|
721
|
+
type
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
`;
|
|
725
|
+
const results = await (
|
|
726
|
+
await fetch(DJ_GQL, {
|
|
727
|
+
method: 'POST',
|
|
728
|
+
headers: { 'Content-Type': 'application/json' },
|
|
729
|
+
credentials: 'include',
|
|
730
|
+
body: JSON.stringify({ query, variables: { nodeNames: names } }),
|
|
731
|
+
})
|
|
732
|
+
).json();
|
|
733
|
+
return results.data?.downstreamNodes || [];
|
|
734
|
+
},
|
|
735
|
+
|
|
693
736
|
node_dag: async function (name) {
|
|
694
737
|
return await (
|
|
695
738
|
await fetch(`${DJ_URL}/nodes/${name}/dag/`, {
|
|
@@ -324,6 +324,82 @@ describe('DataJunctionAPI', () => {
|
|
|
324
324
|
);
|
|
325
325
|
});
|
|
326
326
|
|
|
327
|
+
it('calls upstreamsGQL correctly with single node', async () => {
|
|
328
|
+
const nodeName = 'sampleNode';
|
|
329
|
+
fetch.mockResponseOnce(
|
|
330
|
+
JSON.stringify({
|
|
331
|
+
data: { upstreamNodes: [{ name: 'upstream1', type: 'SOURCE' }] },
|
|
332
|
+
}),
|
|
333
|
+
);
|
|
334
|
+
const result = await DataJunctionAPI.upstreamsGQL(nodeName);
|
|
335
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
336
|
+
`${DJ_URL}/graphql`,
|
|
337
|
+
expect.objectContaining({
|
|
338
|
+
method: 'POST',
|
|
339
|
+
credentials: 'include',
|
|
340
|
+
headers: { 'Content-Type': 'application/json' },
|
|
341
|
+
}),
|
|
342
|
+
);
|
|
343
|
+
expect(result).toEqual([{ name: 'upstream1', type: 'SOURCE' }]);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('calls upstreamsGQL correctly with multiple nodes', async () => {
|
|
347
|
+
const nodeNames = ['node1', 'node2'];
|
|
348
|
+
fetch.mockResponseOnce(
|
|
349
|
+
JSON.stringify({
|
|
350
|
+
data: {
|
|
351
|
+
upstreamNodes: [
|
|
352
|
+
{ name: 'upstream1', type: 'SOURCE' },
|
|
353
|
+
{ name: 'upstream2', type: 'TRANSFORM' },
|
|
354
|
+
],
|
|
355
|
+
},
|
|
356
|
+
}),
|
|
357
|
+
);
|
|
358
|
+
const result = await DataJunctionAPI.upstreamsGQL(nodeNames);
|
|
359
|
+
expect(result).toEqual([
|
|
360
|
+
{ name: 'upstream1', type: 'SOURCE' },
|
|
361
|
+
{ name: 'upstream2', type: 'TRANSFORM' },
|
|
362
|
+
]);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('calls downstreamsGQL correctly with single node', async () => {
|
|
366
|
+
const nodeName = 'sampleNode';
|
|
367
|
+
fetch.mockResponseOnce(
|
|
368
|
+
JSON.stringify({
|
|
369
|
+
data: { downstreamNodes: [{ name: 'downstream1', type: 'METRIC' }] },
|
|
370
|
+
}),
|
|
371
|
+
);
|
|
372
|
+
const result = await DataJunctionAPI.downstreamsGQL(nodeName);
|
|
373
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
374
|
+
`${DJ_URL}/graphql`,
|
|
375
|
+
expect.objectContaining({
|
|
376
|
+
method: 'POST',
|
|
377
|
+
credentials: 'include',
|
|
378
|
+
headers: { 'Content-Type': 'application/json' },
|
|
379
|
+
}),
|
|
380
|
+
);
|
|
381
|
+
expect(result).toEqual([{ name: 'downstream1', type: 'METRIC' }]);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it('calls downstreamsGQL correctly with multiple nodes', async () => {
|
|
385
|
+
const nodeNames = ['node1', 'node2'];
|
|
386
|
+
fetch.mockResponseOnce(
|
|
387
|
+
JSON.stringify({
|
|
388
|
+
data: {
|
|
389
|
+
downstreamNodes: [
|
|
390
|
+
{ name: 'downstream1', type: 'METRIC' },
|
|
391
|
+
{ name: 'downstream2', type: 'CUBE' },
|
|
392
|
+
],
|
|
393
|
+
},
|
|
394
|
+
}),
|
|
395
|
+
);
|
|
396
|
+
const result = await DataJunctionAPI.downstreamsGQL(nodeNames);
|
|
397
|
+
expect(result).toEqual([
|
|
398
|
+
{ name: 'downstream1', type: 'METRIC' },
|
|
399
|
+
{ name: 'downstream2', type: 'CUBE' },
|
|
400
|
+
]);
|
|
401
|
+
});
|
|
402
|
+
|
|
327
403
|
it('calls node_dag correctly', async () => {
|
|
328
404
|
const nodeName = 'sampleNode';
|
|
329
405
|
fetch.mockResponseOnce(JSON.stringify({}));
|
package/src/styles/nav-bar.css
CHANGED
|
@@ -135,7 +135,7 @@
|
|
|
135
135
|
|
|
136
136
|
/* Notifications menu */
|
|
137
137
|
.notifications-menu {
|
|
138
|
-
min-width:
|
|
138
|
+
min-width: 340px;
|
|
139
139
|
max-width: 600px;
|
|
140
140
|
padding: 0;
|
|
141
141
|
}
|
|
@@ -221,7 +221,7 @@
|
|
|
221
221
|
}
|
|
222
222
|
|
|
223
223
|
.notification-title {
|
|
224
|
-
font-size:
|
|
224
|
+
font-size: 15px;
|
|
225
225
|
color: #333;
|
|
226
226
|
font-weight: 500;
|
|
227
227
|
display: flex;
|
|
@@ -230,7 +230,7 @@
|
|
|
230
230
|
}
|
|
231
231
|
|
|
232
232
|
.notification-entity {
|
|
233
|
-
font-size:
|
|
233
|
+
font-size: 13px;
|
|
234
234
|
}
|
|
235
235
|
|
|
236
236
|
.notification-title .badge.version {
|
|
@@ -243,9 +243,9 @@
|
|
|
243
243
|
}
|
|
244
244
|
|
|
245
245
|
.notification-meta {
|
|
246
|
-
font-size:
|
|
246
|
+
font-size: 13px;
|
|
247
247
|
color: #999;
|
|
248
|
-
display:
|
|
248
|
+
display: block;
|
|
249
249
|
align-items: center;
|
|
250
250
|
gap: 0.35rem;
|
|
251
251
|
}
|
package/webpack.config.js
CHANGED
|
@@ -10,6 +10,8 @@ var babelOptions = {
|
|
|
10
10
|
presets: ['@babel/preset-react'],
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
+
const isProduction = process.env.NODE_ENV === 'production';
|
|
14
|
+
|
|
13
15
|
module.exports = {
|
|
14
16
|
cache: true,
|
|
15
17
|
entry: {
|
|
@@ -17,7 +19,7 @@ module.exports = {
|
|
|
17
19
|
vendor: ['events', 'react', 'react-dom'],
|
|
18
20
|
},
|
|
19
21
|
target: 'web',
|
|
20
|
-
mode: 'development',
|
|
22
|
+
mode: isProduction ? 'production' : 'development',
|
|
21
23
|
stats: 'minimal',
|
|
22
24
|
output: {
|
|
23
25
|
path: path.resolve(__dirname, './dist'),
|