datajunction-ui 0.0.93 → 0.0.94
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/NodeComponents.jsx +4 -0
- package/src/app/components/Tab.jsx +11 -16
- package/src/app/components/__tests__/Tab.test.jsx +4 -2
- package/src/app/hooks/useWorkspaceData.js +226 -0
- package/src/app/index.tsx +17 -1
- package/src/app/pages/MyWorkspacePage/ActiveBranchesSection.jsx +38 -107
- package/src/app/pages/MyWorkspacePage/MyNodesSection.jsx +31 -6
- package/src/app/pages/MyWorkspacePage/MyWorkspacePage.css +5 -0
- package/src/app/pages/MyWorkspacePage/NeedsAttentionSection.jsx +86 -100
- package/src/app/pages/MyWorkspacePage/TypeGroupGrid.jsx +7 -11
- package/src/app/pages/MyWorkspacePage/__tests__/ActiveBranchesSection.test.jsx +79 -11
- package/src/app/pages/MyWorkspacePage/__tests__/CollectionsSection.test.jsx +22 -0
- package/src/app/pages/MyWorkspacePage/__tests__/MaterializationsSection.test.jsx +57 -0
- package/src/app/pages/MyWorkspacePage/__tests__/MyNodesSection.test.jsx +60 -18
- package/src/app/pages/MyWorkspacePage/__tests__/MyWorkspacePage.test.jsx +156 -162
- package/src/app/pages/MyWorkspacePage/__tests__/NeedsAttentionSection.test.jsx +17 -18
- package/src/app/pages/MyWorkspacePage/__tests__/NotificationsSection.test.jsx +179 -0
- package/src/app/pages/MyWorkspacePage/__tests__/TypeGroupGrid.test.jsx +169 -49
- package/src/app/pages/MyWorkspacePage/index.jsx +41 -73
- package/src/app/pages/NodePage/NodeDataFlowTab.jsx +464 -0
- package/src/app/pages/NodePage/NodeDependenciesTab.jsx +1 -1
- package/src/app/pages/NodePage/NodeDimensionsTab.jsx +362 -0
- package/src/app/pages/NodePage/NodeLineageTab.jsx +1 -0
- package/src/app/pages/NodePage/NodesWithDimension.jsx +3 -3
- package/src/app/pages/NodePage/__tests__/NodeDataFlowTab.test.jsx +428 -0
- package/src/app/pages/NodePage/__tests__/NodeDependenciesTab.test.jsx +18 -1
- package/src/app/pages/NodePage/__tests__/NodeDimensionsTab.test.jsx +362 -0
- package/src/app/pages/NodePage/__tests__/NodePage.test.jsx +28 -3
- package/src/app/pages/NodePage/__tests__/NodeWithDimension.test.jsx +2 -2
- package/src/app/pages/NodePage/index.jsx +15 -8
- package/src/app/services/DJService.js +73 -6
- package/src/app/services/__tests__/DJService.test.jsx +591 -0
- package/src/styles/index.css +32 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
3
|
+
import { MemoryRouter } from 'react-router-dom';
|
|
4
|
+
import DJClientContext from '../../../providers/djclient';
|
|
5
|
+
import NodeDimensionsTab from '../NodeDimensionsTab';
|
|
6
|
+
|
|
7
|
+
// Mock reactflow — renders DimNode components directly to cover lines 42-136
|
|
8
|
+
jest.mock('reactflow', () => {
|
|
9
|
+
const { useState } = require('react');
|
|
10
|
+
const React = require('react');
|
|
11
|
+
return {
|
|
12
|
+
__esModule: true,
|
|
13
|
+
default: ({ nodes, nodeTypes }) => {
|
|
14
|
+
return (
|
|
15
|
+
<div data-testid="reactflow">
|
|
16
|
+
{(nodes || []).map(n => {
|
|
17
|
+
const NodeComp = nodeTypes?.[n.type];
|
|
18
|
+
return NodeComp ? (
|
|
19
|
+
<NodeComp key={n.id} data={n.data} />
|
|
20
|
+
) : (
|
|
21
|
+
<div key={n.id}>{n.id}</div>
|
|
22
|
+
);
|
|
23
|
+
})}
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
},
|
|
27
|
+
useNodesState: initial => {
|
|
28
|
+
const [n, s] = useState(initial || []);
|
|
29
|
+
return [n, s, () => {}];
|
|
30
|
+
},
|
|
31
|
+
useEdgesState: initial => {
|
|
32
|
+
const [e, s] = useState(initial || []);
|
|
33
|
+
return [e, s, () => {}];
|
|
34
|
+
},
|
|
35
|
+
Handle: () => null,
|
|
36
|
+
MarkerType: { ArrowClosed: 'arrowclosed' },
|
|
37
|
+
Position: { Left: 'left', Right: 'right' },
|
|
38
|
+
addEdge: (params, eds) => [...(eds || []), params],
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
jest.mock('reactflow/dist/style.css', () => ({}));
|
|
43
|
+
|
|
44
|
+
jest.mock('dagre', () => {
|
|
45
|
+
function MockGraph() {
|
|
46
|
+
this.setDefaultEdgeLabel = function () {};
|
|
47
|
+
this.setGraph = function () {};
|
|
48
|
+
this.setNode = function () {};
|
|
49
|
+
this.setEdge = function () {};
|
|
50
|
+
this.node = function () {
|
|
51
|
+
return { x: 100, y: 100 };
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
graphlib: { Graph: MockGraph },
|
|
56
|
+
layout: function () {},
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('<NodeDimensionsTab />', () => {
|
|
61
|
+
const mockDjClient = {
|
|
62
|
+
dimensionDag: jest.fn(),
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const renderWithContext = djNode =>
|
|
66
|
+
render(
|
|
67
|
+
<DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
|
|
68
|
+
<MemoryRouter>
|
|
69
|
+
<NodeDimensionsTab djNode={djNode} />
|
|
70
|
+
</MemoryRouter>
|
|
71
|
+
</DJClientContext.Provider>,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
beforeEach(() => {
|
|
75
|
+
jest.clearAllMocks();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('shows "No dimension links found" when dimensionDag returns empty', async () => {
|
|
79
|
+
const djNode = {
|
|
80
|
+
name: 'default.metric1',
|
|
81
|
+
type: 'metric',
|
|
82
|
+
parents: [],
|
|
83
|
+
dimension_links: [],
|
|
84
|
+
};
|
|
85
|
+
mockDjClient.dimensionDag.mockResolvedValue({
|
|
86
|
+
inbound: [],
|
|
87
|
+
inbound_edges: [],
|
|
88
|
+
outbound: [],
|
|
89
|
+
outbound_edges: [],
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
renderWithContext(djNode);
|
|
93
|
+
|
|
94
|
+
await waitFor(() => {
|
|
95
|
+
expect(
|
|
96
|
+
screen.getByText('No dimension links found for this node.'),
|
|
97
|
+
).toBeInTheDocument();
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('does not fetch when djNode has no name (early return)', () => {
|
|
102
|
+
renderWithContext({ type: 'metric', parents: [], dimension_links: [] });
|
|
103
|
+
expect(mockDjClient.dimensionDag).not.toHaveBeenCalled();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('renders ReactFlow with DimNode components when outbound dimensions exist', async () => {
|
|
107
|
+
const djNode = {
|
|
108
|
+
name: 'default.metric1',
|
|
109
|
+
type: 'metric',
|
|
110
|
+
parents: [],
|
|
111
|
+
dimension_links: [],
|
|
112
|
+
};
|
|
113
|
+
const dimNode = {
|
|
114
|
+
name: 'default.dim1',
|
|
115
|
+
type: 'dimension',
|
|
116
|
+
display_name: 'Default: Dim 1',
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
mockDjClient.dimensionDag.mockResolvedValue({
|
|
120
|
+
inbound: [],
|
|
121
|
+
inbound_edges: [],
|
|
122
|
+
outbound: [dimNode],
|
|
123
|
+
outbound_edges: [{ source: 'default.metric1', target: 'default.dim1' }],
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
renderWithContext(djNode);
|
|
127
|
+
|
|
128
|
+
await waitFor(() => {
|
|
129
|
+
expect(screen.getByTestId('reactflow')).toBeInTheDocument();
|
|
130
|
+
});
|
|
131
|
+
expect(screen.getByText('dimension')).toBeInTheDocument();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('handles error from dimensionDag gracefully', async () => {
|
|
135
|
+
const consoleSpy = jest
|
|
136
|
+
.spyOn(console, 'error')
|
|
137
|
+
.mockImplementation(() => {});
|
|
138
|
+
const djNode = {
|
|
139
|
+
name: 'default.metric1',
|
|
140
|
+
type: 'metric',
|
|
141
|
+
parents: [],
|
|
142
|
+
dimension_links: [],
|
|
143
|
+
};
|
|
144
|
+
mockDjClient.dimensionDag.mockRejectedValue(new Error('fetch failed'));
|
|
145
|
+
|
|
146
|
+
renderWithContext(djNode);
|
|
147
|
+
|
|
148
|
+
await waitFor(() => {
|
|
149
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.any(Error));
|
|
150
|
+
});
|
|
151
|
+
await waitFor(() => {
|
|
152
|
+
expect(
|
|
153
|
+
screen.getByText('No dimension links found for this node.'),
|
|
154
|
+
).toBeInTheDocument();
|
|
155
|
+
});
|
|
156
|
+
consoleSpy.mockRestore();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('covers DimNode isCurrent border style (current node renders correctly)', async () => {
|
|
160
|
+
const djNode = {
|
|
161
|
+
name: 'default.metric1',
|
|
162
|
+
type: 'metric',
|
|
163
|
+
parents: [],
|
|
164
|
+
dimension_links: [],
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
mockDjClient.dimensionDag.mockResolvedValue({
|
|
168
|
+
inbound: [],
|
|
169
|
+
inbound_edges: [],
|
|
170
|
+
outbound: [
|
|
171
|
+
{ name: 'default.dim1', type: 'dimension', display_name: 'Dim 1' },
|
|
172
|
+
],
|
|
173
|
+
outbound_edges: [{ source: 'default.metric1', target: 'default.dim1' }],
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
renderWithContext(djNode);
|
|
177
|
+
|
|
178
|
+
await waitFor(() => {
|
|
179
|
+
expect(screen.getByTestId('reactflow')).toBeInTheDocument();
|
|
180
|
+
});
|
|
181
|
+
// metric1 is the current node (isCurrent=true)
|
|
182
|
+
expect(screen.getByText('metric')).toBeInTheDocument();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('covers DimNode with no namespace (node name has no dots)', async () => {
|
|
186
|
+
const djNode = {
|
|
187
|
+
name: 'metric1',
|
|
188
|
+
type: 'metric',
|
|
189
|
+
parents: [],
|
|
190
|
+
dimension_links: [],
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
mockDjClient.dimensionDag.mockResolvedValue({
|
|
194
|
+
inbound: [],
|
|
195
|
+
inbound_edges: [],
|
|
196
|
+
outbound: [{ name: 'dim1', type: 'dimension', display_name: 'Dim 1' }],
|
|
197
|
+
outbound_edges: [{ source: 'metric1', target: 'dim1' }],
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
renderWithContext(djNode);
|
|
201
|
+
|
|
202
|
+
await waitFor(() => {
|
|
203
|
+
expect(screen.getByTestId('reactflow')).toBeInTheDocument();
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('covers DimNode with unknown type (fallback color)', async () => {
|
|
208
|
+
const djNode = {
|
|
209
|
+
name: 'default.metric1',
|
|
210
|
+
type: 'metric',
|
|
211
|
+
parents: [],
|
|
212
|
+
dimension_links: [],
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
mockDjClient.dimensionDag.mockResolvedValue({
|
|
216
|
+
inbound: [],
|
|
217
|
+
inbound_edges: [],
|
|
218
|
+
outbound: [
|
|
219
|
+
{
|
|
220
|
+
name: 'default.custom_node',
|
|
221
|
+
type: 'unknown_type',
|
|
222
|
+
display_name: 'Custom Node',
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
outbound_edges: [
|
|
226
|
+
{ source: 'default.metric1', target: 'default.custom_node' },
|
|
227
|
+
],
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
renderWithContext(djNode);
|
|
231
|
+
|
|
232
|
+
await waitFor(() => {
|
|
233
|
+
expect(screen.getByTestId('reactflow')).toBeInTheDocument();
|
|
234
|
+
});
|
|
235
|
+
expect(screen.getByText('unknown_type')).toBeInTheDocument();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('renders dimension DAG for a dimension node (inbound + outbound)', async () => {
|
|
239
|
+
const djNode = {
|
|
240
|
+
name: 'default.hard_hat',
|
|
241
|
+
type: 'dimension',
|
|
242
|
+
display_name: 'Hard Hat',
|
|
243
|
+
parents: [],
|
|
244
|
+
dimension_links: [],
|
|
245
|
+
};
|
|
246
|
+
const inboundNode = {
|
|
247
|
+
name: 'default.repair_orders',
|
|
248
|
+
display_name: 'Repair Orders',
|
|
249
|
+
type: 'source',
|
|
250
|
+
};
|
|
251
|
+
const outboundNode = {
|
|
252
|
+
name: 'default.us_state',
|
|
253
|
+
display_name: 'US State',
|
|
254
|
+
type: 'dimension',
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
mockDjClient.dimensionDag.mockResolvedValue({
|
|
258
|
+
inbound: [inboundNode],
|
|
259
|
+
inbound_edges: [{ source: inboundNode.name, target: djNode.name }],
|
|
260
|
+
outbound: [outboundNode],
|
|
261
|
+
outbound_edges: [{ source: djNode.name, target: outboundNode.name }],
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
renderWithContext(djNode);
|
|
265
|
+
|
|
266
|
+
await waitFor(() => {
|
|
267
|
+
expect(mockDjClient.dimensionDag).toHaveBeenCalledWith(
|
|
268
|
+
'default.hard_hat',
|
|
269
|
+
);
|
|
270
|
+
});
|
|
271
|
+
await waitFor(() => {
|
|
272
|
+
expect(screen.getByTestId('reactflow')).toBeInTheDocument();
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('shows "No dimension links found" when dimension DAG returns empty inbound + outbound', async () => {
|
|
277
|
+
const djNode = {
|
|
278
|
+
name: 'default.payment_type',
|
|
279
|
+
type: 'dimension',
|
|
280
|
+
display_name: 'Payment Type',
|
|
281
|
+
parents: [],
|
|
282
|
+
dimension_links: [],
|
|
283
|
+
};
|
|
284
|
+
mockDjClient.dimensionDag.mockResolvedValue({
|
|
285
|
+
inbound: [],
|
|
286
|
+
inbound_edges: [],
|
|
287
|
+
outbound: [],
|
|
288
|
+
outbound_edges: [],
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
renderWithContext(djNode);
|
|
292
|
+
|
|
293
|
+
await waitFor(() => {
|
|
294
|
+
expect(
|
|
295
|
+
screen.getByText('No dimension links found for this node.'),
|
|
296
|
+
).toBeInTheDocument();
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('handles dimensionDag error gracefully', async () => {
|
|
301
|
+
const consoleSpy = jest
|
|
302
|
+
.spyOn(console, 'error')
|
|
303
|
+
.mockImplementation(() => {});
|
|
304
|
+
const djNode = {
|
|
305
|
+
name: 'default.hard_hat',
|
|
306
|
+
type: 'dimension',
|
|
307
|
+
parents: [],
|
|
308
|
+
dimension_links: [],
|
|
309
|
+
};
|
|
310
|
+
mockDjClient.dimensionDag.mockRejectedValue(new Error('network error'));
|
|
311
|
+
|
|
312
|
+
renderWithContext(djNode);
|
|
313
|
+
|
|
314
|
+
await waitFor(() => {
|
|
315
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.any(Error));
|
|
316
|
+
});
|
|
317
|
+
await waitFor(() => {
|
|
318
|
+
expect(
|
|
319
|
+
screen.getByText('No dimension links found for this node.'),
|
|
320
|
+
).toBeInTheDocument();
|
|
321
|
+
});
|
|
322
|
+
consoleSpy.mockRestore();
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('renders multi-level inbound chain (non-flat graph)', async () => {
|
|
326
|
+
const djNode = {
|
|
327
|
+
name: 'default.us_state',
|
|
328
|
+
type: 'dimension',
|
|
329
|
+
display_name: 'US State',
|
|
330
|
+
parents: [],
|
|
331
|
+
dimension_links: [],
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
mockDjClient.dimensionDag.mockResolvedValue({
|
|
335
|
+
inbound: [
|
|
336
|
+
{
|
|
337
|
+
name: 'default.hard_hat',
|
|
338
|
+
type: 'dimension',
|
|
339
|
+
display_name: 'Hard Hat',
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
name: 'default.repair_order',
|
|
343
|
+
type: 'source',
|
|
344
|
+
display_name: 'Repair Order',
|
|
345
|
+
},
|
|
346
|
+
],
|
|
347
|
+
inbound_edges: [
|
|
348
|
+
{ source: 'default.hard_hat', target: 'default.us_state' },
|
|
349
|
+
{ source: 'default.repair_order', target: 'default.hard_hat' },
|
|
350
|
+
],
|
|
351
|
+
outbound: [],
|
|
352
|
+
outbound_edges: [],
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
renderWithContext(djNode);
|
|
356
|
+
|
|
357
|
+
await waitFor(() => {
|
|
358
|
+
expect(screen.getByTestId('reactflow')).toBeInTheDocument();
|
|
359
|
+
});
|
|
360
|
+
expect(screen.getAllByText('dimension').length).toBeGreaterThan(0);
|
|
361
|
+
});
|
|
362
|
+
});
|
|
@@ -14,6 +14,23 @@ jest.mock('cronstrue', () => ({
|
|
|
14
14
|
// Mock CSS imports
|
|
15
15
|
jest.mock('../../../../styles/preaggregations.css', () => ({}));
|
|
16
16
|
|
|
17
|
+
// Mock recharts for NodeDataFlowTab (Sankey doesn't work in jsdom)
|
|
18
|
+
jest.mock('recharts', () => ({
|
|
19
|
+
Sankey: () => <div data-testid="sankey-chart" />,
|
|
20
|
+
Tooltip: () => null,
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
// ResizeObserver is not defined in jsdom
|
|
24
|
+
global.ResizeObserver = function (callback) {
|
|
25
|
+
return { observe: () => {}, disconnect: () => {}, unobserve: () => {} };
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// jsdom doesn't implement canvas — stub getContext so text-measurement code doesn't crash
|
|
29
|
+
HTMLCanvasElement.prototype.getContext = () => ({
|
|
30
|
+
font: '',
|
|
31
|
+
measureText: () => ({ width: 0 }),
|
|
32
|
+
});
|
|
33
|
+
|
|
17
34
|
describe('<NodePage />', () => {
|
|
18
35
|
const domTestingLib = require('@testing-library/dom');
|
|
19
36
|
const { queryHelpers } = domTestingLib;
|
|
@@ -71,6 +88,12 @@ describe('<NodePage />', () => {
|
|
|
71
88
|
.mockResolvedValue({ status: 200 }),
|
|
72
89
|
listPreaggs: jest.fn().mockResolvedValue({ items: [] }),
|
|
73
90
|
deactivatePreaggWorkflow: jest.fn().mockResolvedValue({ status: 200 }),
|
|
91
|
+
namespaceSources: jest.fn().mockResolvedValue(null),
|
|
92
|
+
listDeployments: jest.fn().mockResolvedValue([]),
|
|
93
|
+
getNamespaceGitConfig: jest.fn().mockResolvedValue(null),
|
|
94
|
+
upstreamsGQL: jest.fn().mockResolvedValue([]),
|
|
95
|
+
downstreamsGQL: jest.fn().mockResolvedValue([]),
|
|
96
|
+
findCubesWithMetrics: jest.fn().mockResolvedValue([]),
|
|
74
97
|
},
|
|
75
98
|
};
|
|
76
99
|
};
|
|
@@ -729,15 +752,17 @@ describe('<NodePage />', () => {
|
|
|
729
752
|
</DJClientContext.Provider>
|
|
730
753
|
);
|
|
731
754
|
render(
|
|
732
|
-
<MemoryRouter
|
|
755
|
+
<MemoryRouter
|
|
756
|
+
initialEntries={['/nodes/default.num_repair_orders/data-flow']}
|
|
757
|
+
>
|
|
733
758
|
<Routes>
|
|
734
759
|
<Route path="nodes/:name/:tab" element={element} />
|
|
735
760
|
</Routes>
|
|
736
761
|
</MemoryRouter>,
|
|
737
762
|
);
|
|
738
763
|
await waitFor(() => {
|
|
739
|
-
fireEvent.click(screen.getByRole('button', { name: '
|
|
740
|
-
expect(djClient.DataJunctionAPI.
|
|
764
|
+
fireEvent.click(screen.getByRole('button', { name: 'Data Flow' }));
|
|
765
|
+
expect(djClient.DataJunctionAPI.upstreamsGQL).toHaveBeenCalledWith(
|
|
741
766
|
mocks.mockMetricNode.name,
|
|
742
767
|
);
|
|
743
768
|
});
|
|
@@ -164,10 +164,10 @@ describe('<NodesWithDimension />', () => {
|
|
|
164
164
|
);
|
|
165
165
|
for (const node of mockNodesWithDimension) {
|
|
166
166
|
// renders nodes based on nodesWithDimension data
|
|
167
|
-
expect(screen.getByText(node.
|
|
167
|
+
expect(screen.getByText(node.name)).toBeInTheDocument();
|
|
168
168
|
|
|
169
169
|
// renders links to the correct URLs
|
|
170
|
-
const link = screen.getByText(node.
|
|
170
|
+
const link = screen.getByText(node.name).closest('a');
|
|
171
171
|
expect(link).toHaveAttribute('href', `/nodes/${node.name}`);
|
|
172
172
|
}
|
|
173
173
|
});
|
|
@@ -5,7 +5,8 @@ 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 NodeDataFlowTab from './NodeDataFlowTab';
|
|
9
|
+
import NodeDimensionsTab from './NodeDimensionsTab';
|
|
9
10
|
import NodeHistory from './NodeHistory';
|
|
10
11
|
import NotebookDownload from './NotebookDownload';
|
|
11
12
|
import DJClientContext from '../../providers/djclient';
|
|
@@ -83,10 +84,15 @@ export function NodePage() {
|
|
|
83
84
|
display: node?.type !== 'metric',
|
|
84
85
|
},
|
|
85
86
|
{
|
|
86
|
-
id: '
|
|
87
|
-
name: '
|
|
87
|
+
id: 'data-flow',
|
|
88
|
+
name: 'Data Flow',
|
|
88
89
|
display: true,
|
|
89
90
|
},
|
|
91
|
+
{
|
|
92
|
+
id: 'dimensions',
|
|
93
|
+
name: 'Dimensions',
|
|
94
|
+
display: node?.type !== 'source',
|
|
95
|
+
},
|
|
90
96
|
{
|
|
91
97
|
id: 'history',
|
|
92
98
|
name: 'History',
|
|
@@ -128,8 +134,11 @@ export function NodePage() {
|
|
|
128
134
|
case 'columns':
|
|
129
135
|
tabToDisplay = <NodeColumnTab node={node} djClient={djClient} />;
|
|
130
136
|
break;
|
|
131
|
-
case '
|
|
132
|
-
tabToDisplay = <
|
|
137
|
+
case 'data-flow':
|
|
138
|
+
tabToDisplay = <NodeDataFlowTab djNode={node} />;
|
|
139
|
+
break;
|
|
140
|
+
case 'dimensions':
|
|
141
|
+
tabToDisplay = <NodeDimensionsTab djNode={node} />;
|
|
133
142
|
break;
|
|
134
143
|
case 'history':
|
|
135
144
|
tabToDisplay = <NodeHistory node={node} djClient={djClient} />;
|
|
@@ -261,9 +270,7 @@ export function NodePage() {
|
|
|
261
270
|
{node?.version}
|
|
262
271
|
</span>
|
|
263
272
|
</div>
|
|
264
|
-
<div className="
|
|
265
|
-
{tabsList(node).map(buildTabs)}
|
|
266
|
-
</div>
|
|
273
|
+
<div className="dj-tabs-bar">{tabsList(node).map(buildTabs)}</div>
|
|
267
274
|
{tabToDisplay}
|
|
268
275
|
</div>
|
|
269
276
|
) : node?.message !== undefined ? (
|
|
@@ -865,6 +865,11 @@ export const DataJunctionAPI = {
|
|
|
865
865
|
upstreamNodes(nodeNames: $nodeNames) {
|
|
866
866
|
name
|
|
867
867
|
type
|
|
868
|
+
current {
|
|
869
|
+
parents {
|
|
870
|
+
name
|
|
871
|
+
}
|
|
872
|
+
}
|
|
868
873
|
}
|
|
869
874
|
}
|
|
870
875
|
`;
|
|
@@ -886,6 +891,11 @@ export const DataJunctionAPI = {
|
|
|
886
891
|
downstreamNodes(nodeNames: $nodeNames) {
|
|
887
892
|
name
|
|
888
893
|
type
|
|
894
|
+
current {
|
|
895
|
+
parents {
|
|
896
|
+
name
|
|
897
|
+
}
|
|
898
|
+
}
|
|
889
899
|
}
|
|
890
900
|
}
|
|
891
901
|
`;
|
|
@@ -900,6 +910,39 @@ export const DataJunctionAPI = {
|
|
|
900
910
|
return results.data?.downstreamNodes || [];
|
|
901
911
|
},
|
|
902
912
|
|
|
913
|
+
// Batch-fetch cubes by name, returning each cube's name and its metric node names.
|
|
914
|
+
// Used to build the Sankey data flow graph without N individual cube fetches.
|
|
915
|
+
findCubesWithMetrics: async function (cubeNames) {
|
|
916
|
+
if (!cubeNames || cubeNames.length === 0) return [];
|
|
917
|
+
const query = `
|
|
918
|
+
query FindCubesWithMetrics($names: [String!]) {
|
|
919
|
+
findNodes(names: $names, nodeTypes: [CUBE]) {
|
|
920
|
+
name
|
|
921
|
+
current {
|
|
922
|
+
displayName
|
|
923
|
+
cubeMetrics {
|
|
924
|
+
name
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
`;
|
|
930
|
+
const results = await (
|
|
931
|
+
await fetch(DJ_GQL, {
|
|
932
|
+
method: 'POST',
|
|
933
|
+
headers: { 'Content-Type': 'application/json' },
|
|
934
|
+
credentials: 'include',
|
|
935
|
+
body: JSON.stringify({ query, variables: { names: cubeNames } }),
|
|
936
|
+
})
|
|
937
|
+
).json();
|
|
938
|
+
return (results.data?.findNodes || []).map(n => ({
|
|
939
|
+
name: n.name,
|
|
940
|
+
display_name: n.current?.displayName || n.name,
|
|
941
|
+
type: 'cube',
|
|
942
|
+
parents: (n.current?.cubeMetrics || []).map(m => ({ name: m.name })),
|
|
943
|
+
}));
|
|
944
|
+
},
|
|
945
|
+
|
|
903
946
|
node_dag: async function (name) {
|
|
904
947
|
return await (
|
|
905
948
|
await fetch(`${DJ_URL}/nodes/${name}/dag/`, {
|
|
@@ -1097,9 +1140,10 @@ export const DataJunctionAPI = {
|
|
|
1097
1140
|
).json();
|
|
1098
1141
|
},
|
|
1099
1142
|
|
|
1100
|
-
nodesWithDimension: async function (name) {
|
|
1143
|
+
nodesWithDimension: async function (name, nodeType = null) {
|
|
1144
|
+
const params = nodeType ? `?node_type=${nodeType}` : '';
|
|
1101
1145
|
return await (
|
|
1102
|
-
await fetch(`${DJ_URL}/dimensions/${name}/nodes
|
|
1146
|
+
await fetch(`${DJ_URL}/dimensions/${name}/nodes/${params}`, {
|
|
1103
1147
|
credentials: 'include',
|
|
1104
1148
|
})
|
|
1105
1149
|
).json();
|
|
@@ -1444,6 +1488,13 @@ export const DataJunctionAPI = {
|
|
|
1444
1488
|
})
|
|
1445
1489
|
).json();
|
|
1446
1490
|
},
|
|
1491
|
+
dimensionDag: async function (nodeName) {
|
|
1492
|
+
return await (
|
|
1493
|
+
await fetch(`${DJ_URL}/nodes/${nodeName}/dimension-dag/`, {
|
|
1494
|
+
credentials: 'include',
|
|
1495
|
+
})
|
|
1496
|
+
).json();
|
|
1497
|
+
},
|
|
1447
1498
|
linkDimension: async function (nodeName, columnName, dimensionName) {
|
|
1448
1499
|
const response = await fetch(
|
|
1449
1500
|
`${DJ_URL}/nodes/${nodeName}/columns/${columnName}?dimension=${dimensionName}`,
|
|
@@ -1928,11 +1979,18 @@ export const DataJunctionAPI = {
|
|
|
1928
1979
|
|
|
1929
1980
|
// ===== My Workspace GraphQL Queries =====
|
|
1930
1981
|
|
|
1931
|
-
getWorkspaceRecentlyEdited: async function (
|
|
1982
|
+
getWorkspaceRecentlyEdited: async function (
|
|
1983
|
+
username,
|
|
1984
|
+
limit = 10,
|
|
1985
|
+
nodeType = null,
|
|
1986
|
+
) {
|
|
1932
1987
|
// Nodes the user has edited, ordered by last updated (excluding source nodes)
|
|
1933
1988
|
const query = `
|
|
1934
1989
|
query RecentlyEdited($editedBy: String!, $limit: Int!, $nodeTypes: [NodeType!]) {
|
|
1935
1990
|
findNodesPaginated(editedBy: $editedBy, limit: $limit, nodeTypes: $nodeTypes, orderBy: UPDATED_AT, ascending: false) {
|
|
1991
|
+
pageInfo {
|
|
1992
|
+
hasNextPage
|
|
1993
|
+
}
|
|
1936
1994
|
edges {
|
|
1937
1995
|
node {
|
|
1938
1996
|
name
|
|
@@ -1957,6 +2015,7 @@ export const DataJunctionAPI = {
|
|
|
1957
2015
|
}
|
|
1958
2016
|
}
|
|
1959
2017
|
`;
|
|
2018
|
+
const allTypes = ['TRANSFORM', 'METRIC', 'DIMENSION', 'CUBE'];
|
|
1960
2019
|
return await (
|
|
1961
2020
|
await fetch(DJ_GQL, {
|
|
1962
2021
|
method: 'POST',
|
|
@@ -1967,18 +2026,25 @@ export const DataJunctionAPI = {
|
|
|
1967
2026
|
variables: {
|
|
1968
2027
|
editedBy: username,
|
|
1969
2028
|
limit,
|
|
1970
|
-
nodeTypes: [
|
|
2029
|
+
nodeTypes: nodeType ? [nodeType] : allTypes,
|
|
1971
2030
|
},
|
|
1972
2031
|
}),
|
|
1973
2032
|
})
|
|
1974
2033
|
).json();
|
|
1975
2034
|
},
|
|
1976
2035
|
|
|
1977
|
-
getWorkspaceOwnedNodes: async function (
|
|
2036
|
+
getWorkspaceOwnedNodes: async function (
|
|
2037
|
+
username,
|
|
2038
|
+
limit = 10,
|
|
2039
|
+
nodeType = null,
|
|
2040
|
+
) {
|
|
1978
2041
|
// Owned nodes ordered by UPDATED_AT (excluding source nodes)
|
|
1979
2042
|
const query = `
|
|
1980
2043
|
query OwnedNodes($ownedBy: String!, $limit: Int!, $nodeTypes: [NodeType!]) {
|
|
1981
2044
|
findNodesPaginated(ownedBy: $ownedBy, limit: $limit, nodeTypes: $nodeTypes, orderBy: UPDATED_AT, ascending: false) {
|
|
2045
|
+
pageInfo {
|
|
2046
|
+
hasNextPage
|
|
2047
|
+
}
|
|
1982
2048
|
edges {
|
|
1983
2049
|
node {
|
|
1984
2050
|
name
|
|
@@ -2003,6 +2069,7 @@ export const DataJunctionAPI = {
|
|
|
2003
2069
|
}
|
|
2004
2070
|
}
|
|
2005
2071
|
`;
|
|
2072
|
+
const allTypes = ['TRANSFORM', 'METRIC', 'DIMENSION', 'CUBE'];
|
|
2006
2073
|
return await (
|
|
2007
2074
|
await fetch(DJ_GQL, {
|
|
2008
2075
|
method: 'POST',
|
|
@@ -2013,7 +2080,7 @@ export const DataJunctionAPI = {
|
|
|
2013
2080
|
variables: {
|
|
2014
2081
|
ownedBy: username,
|
|
2015
2082
|
limit,
|
|
2016
|
-
nodeTypes: [
|
|
2083
|
+
nodeTypes: nodeType ? [nodeType] : allTypes,
|
|
2017
2084
|
},
|
|
2018
2085
|
}),
|
|
2019
2086
|
})
|