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 {
|
|
2
|
+
useCallback,
|
|
3
|
+
useContext,
|
|
4
|
+
useEffect,
|
|
5
|
+
useMemo,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
} from 'react'; // useRef kept for hasFitRef
|
|
9
|
+
import * as React from 'react';
|
|
10
|
+
import ReactFlow, {
|
|
11
|
+
addEdge,
|
|
12
|
+
useEdgesState,
|
|
13
|
+
useNodesState,
|
|
14
|
+
Handle,
|
|
15
|
+
MarkerType,
|
|
16
|
+
Position,
|
|
17
|
+
} from 'reactflow';
|
|
18
|
+
import { useNavigate } from 'react-router-dom';
|
|
19
|
+
import dagre from 'dagre';
|
|
20
|
+
import 'reactflow/dist/style.css';
|
|
21
|
+
import DJClientContext from '../../providers/djclient';
|
|
22
|
+
|
|
23
|
+
const TYPE_COLORS = {
|
|
24
|
+
source: { border: '#00b368', text: '#00b368', bg: '#ccf7e5' },
|
|
25
|
+
transform: { border: '#0063b4', text: '#0063b4', bg: '#ccefff' },
|
|
26
|
+
metric: { border: '#a2283e', text: '#a2283e', bg: '#fad7dd' },
|
|
27
|
+
dimension: { border: '#a96621', text: '#a96621', bg: '#ffefd0' },
|
|
28
|
+
cube: { border: '#580076', text: '#580076', bg: '#dbafff' },
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const NODE_WIDTH = 220;
|
|
32
|
+
const NODE_HEIGHT = 72;
|
|
33
|
+
const HANDLE_BASE = {
|
|
34
|
+
width: 1,
|
|
35
|
+
height: 1,
|
|
36
|
+
opacity: 0,
|
|
37
|
+
border: 'none',
|
|
38
|
+
background: 'transparent',
|
|
39
|
+
pointerEvents: 'none',
|
|
40
|
+
};
|
|
41
|
+
// ReactFlow connects Position.Right edges to handle.x + handle.width (right edge of handle).
|
|
42
|
+
// Setting right:0 puts the right edge flush with the node's right border → edge is flush.
|
|
43
|
+
// Similarly, left:0 puts the left edge flush with the node's left border.
|
|
44
|
+
const TARGET_HANDLE = { ...HANDLE_BASE, left: 0 };
|
|
45
|
+
const SOURCE_HANDLE = { ...HANDLE_BASE, right: 0 };
|
|
46
|
+
|
|
47
|
+
const BASE_EDGE = { strokeWidth: 1, stroke: '#dde3ec' };
|
|
48
|
+
const BASE_MARKER = {
|
|
49
|
+
type: MarkerType.ArrowClosed,
|
|
50
|
+
color: '#dde3ec',
|
|
51
|
+
width: 12,
|
|
52
|
+
height: 12,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
function DimNode({ data }) {
|
|
56
|
+
const navigate = useNavigate();
|
|
57
|
+
const c = TYPE_COLORS[data.type] ?? {
|
|
58
|
+
border: '#cbd5e1',
|
|
59
|
+
text: '#64748b',
|
|
60
|
+
bg: '#f8fafc',
|
|
61
|
+
};
|
|
62
|
+
const label = (data.display_name || data.name || '').split('.').pop();
|
|
63
|
+
const namespace = (data.name || '').split('.').slice(0, -1).join('.');
|
|
64
|
+
|
|
65
|
+
const borderStyle = data.isCurrent
|
|
66
|
+
? {
|
|
67
|
+
border: `3px solid ${c.border}`,
|
|
68
|
+
boxShadow: `0 0 0 1px ${c.border}33, 0 4px 12px rgba(0,0,0,0.12)`,
|
|
69
|
+
}
|
|
70
|
+
: {
|
|
71
|
+
border: `1px solid ${c.border}66`,
|
|
72
|
+
opacity: data.dimmed ? 0.3 : 1,
|
|
73
|
+
boxShadow: '0 1px 4px rgba(0,0,0,0.06)',
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<>
|
|
78
|
+
<Handle type="target" position={Position.Left} style={TARGET_HANDLE} />
|
|
79
|
+
<div
|
|
80
|
+
onClick={() => navigate('/nodes/' + data.name)}
|
|
81
|
+
style={{
|
|
82
|
+
width: NODE_WIDTH,
|
|
83
|
+
padding: '12px 16px',
|
|
84
|
+
background: '#fff',
|
|
85
|
+
borderRadius: 12,
|
|
86
|
+
cursor: 'pointer',
|
|
87
|
+
transition: 'opacity 0.15s',
|
|
88
|
+
...borderStyle,
|
|
89
|
+
}}
|
|
90
|
+
>
|
|
91
|
+
<div
|
|
92
|
+
style={{
|
|
93
|
+
display: 'flex',
|
|
94
|
+
alignItems: 'flex-start',
|
|
95
|
+
justifyContent: 'space-between',
|
|
96
|
+
gap: 8,
|
|
97
|
+
}}
|
|
98
|
+
>
|
|
99
|
+
<div
|
|
100
|
+
style={{
|
|
101
|
+
fontSize: 13,
|
|
102
|
+
fontWeight: 700,
|
|
103
|
+
color: '#0f172a',
|
|
104
|
+
lineHeight: 1.3,
|
|
105
|
+
flex: 1,
|
|
106
|
+
minWidth: 0,
|
|
107
|
+
overflow: 'hidden',
|
|
108
|
+
textOverflow: 'ellipsis',
|
|
109
|
+
whiteSpace: 'nowrap',
|
|
110
|
+
}}
|
|
111
|
+
>
|
|
112
|
+
{label}
|
|
113
|
+
</div>
|
|
114
|
+
<span
|
|
115
|
+
style={{
|
|
116
|
+
fontSize: 10,
|
|
117
|
+
fontWeight: 600,
|
|
118
|
+
color: c.text,
|
|
119
|
+
background: c.bg,
|
|
120
|
+
borderRadius: 5,
|
|
121
|
+
padding: '2px 6px',
|
|
122
|
+
textTransform: 'uppercase',
|
|
123
|
+
letterSpacing: '0.05em',
|
|
124
|
+
whiteSpace: 'nowrap',
|
|
125
|
+
flexShrink: 0,
|
|
126
|
+
}}
|
|
127
|
+
>
|
|
128
|
+
{data.type}
|
|
129
|
+
</span>
|
|
130
|
+
</div>
|
|
131
|
+
{namespace && (
|
|
132
|
+
<div
|
|
133
|
+
style={{
|
|
134
|
+
fontSize: 11,
|
|
135
|
+
color: '#94a3b8',
|
|
136
|
+
marginTop: 5,
|
|
137
|
+
overflow: 'hidden',
|
|
138
|
+
textOverflow: 'ellipsis',
|
|
139
|
+
whiteSpace: 'nowrap',
|
|
140
|
+
}}
|
|
141
|
+
>
|
|
142
|
+
{namespace}
|
|
143
|
+
</div>
|
|
144
|
+
)}
|
|
145
|
+
</div>
|
|
146
|
+
<Handle type="source" position={Position.Right} style={SOURCE_HANDLE} />
|
|
147
|
+
</>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const getLayoutedElements = (nodes, edges, currentName) => {
|
|
152
|
+
const g = new dagre.graphlib.Graph();
|
|
153
|
+
g.setDefaultEdgeLabel(() => ({}));
|
|
154
|
+
g.setGraph({
|
|
155
|
+
rankdir: 'LR',
|
|
156
|
+
nodesep: 24,
|
|
157
|
+
ranksep: 80,
|
|
158
|
+
ranker: 'longest-path',
|
|
159
|
+
});
|
|
160
|
+
nodes.forEach(n =>
|
|
161
|
+
g.setNode(n.id, { width: NODE_WIDTH, height: NODE_HEIGHT }),
|
|
162
|
+
);
|
|
163
|
+
edges.forEach(e => g.setEdge(e.source, e.target));
|
|
164
|
+
dagre.layout(g);
|
|
165
|
+
nodes.forEach(n => {
|
|
166
|
+
const pos = g.node(n.id);
|
|
167
|
+
n.position = { x: pos.x - NODE_WIDTH / 2, y: pos.y - NODE_HEIGHT / 2 };
|
|
168
|
+
n.targetPosition = 'left';
|
|
169
|
+
n.sourcePosition = 'right';
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Translate so the current node's center sits at the origin (0, 0).
|
|
173
|
+
// fitView then centers on it regardless of how asymmetric the rest of the graph is.
|
|
174
|
+
const current = nodes.find(n => n.id === currentName);
|
|
175
|
+
if (current) {
|
|
176
|
+
const dx = -(current.position.x + NODE_WIDTH / 2);
|
|
177
|
+
const dy = -(current.position.y + NODE_HEIGHT / 2);
|
|
178
|
+
nodes.forEach(n => {
|
|
179
|
+
n.position = { x: n.position.x + dx, y: n.position.y + dy };
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return { nodes, edges };
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const buildRFNode = (n, currentName) => ({
|
|
187
|
+
id: n.name,
|
|
188
|
+
type: 'DimNode',
|
|
189
|
+
zIndex: 10,
|
|
190
|
+
data: {
|
|
191
|
+
name: n.name,
|
|
192
|
+
display_name: n.display_name,
|
|
193
|
+
type: n.type,
|
|
194
|
+
isCurrent: n.name === currentName,
|
|
195
|
+
dimmed: false,
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
export default function NodeDimensionsTab({ djNode }) {
|
|
200
|
+
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
201
|
+
const nodeTypes = useMemo(() => ({ DimNode }), []);
|
|
202
|
+
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
|
203
|
+
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
|
204
|
+
const [loaded, setLoaded] = useState(false);
|
|
205
|
+
const [rfInstance, setRfInstance] = useState(null);
|
|
206
|
+
const [currentNodeName, setCurrentNodeName] = useState(null);
|
|
207
|
+
const hasFitRef = useRef(false);
|
|
208
|
+
|
|
209
|
+
// Reset fit-view state whenever the node changes so a fresh fitView fires on load.
|
|
210
|
+
useEffect(() => {
|
|
211
|
+
hasFitRef.current = false;
|
|
212
|
+
setCurrentNodeName(null);
|
|
213
|
+
}, [djNode?.name]);
|
|
214
|
+
|
|
215
|
+
// Center viewport on the current node (which is placed at the origin by getLayoutedElements).
|
|
216
|
+
// rfInstance is state (not ref) so this re-runs if onInit fires after nodes are set.
|
|
217
|
+
useEffect(() => {
|
|
218
|
+
if (
|
|
219
|
+
nodes.length > 0 &&
|
|
220
|
+
rfInstance &&
|
|
221
|
+
currentNodeName &&
|
|
222
|
+
!hasFitRef.current
|
|
223
|
+
) {
|
|
224
|
+
hasFitRef.current = true;
|
|
225
|
+
rfInstance.setCenter(0, 0, { zoom: 0.9 });
|
|
226
|
+
}
|
|
227
|
+
}, [nodes, rfInstance, currentNodeName]);
|
|
228
|
+
|
|
229
|
+
useEffect(() => {
|
|
230
|
+
if (!djNode?.name) return;
|
|
231
|
+
|
|
232
|
+
const makeEdge = (() => {
|
|
233
|
+
const seen = new Set();
|
|
234
|
+
return (source, target) => {
|
|
235
|
+
const id = `${source}->${target}`;
|
|
236
|
+
if (seen.has(id)) return null;
|
|
237
|
+
seen.add(id);
|
|
238
|
+
return { id, source, target, style: BASE_EDGE, markerEnd: BASE_MARKER };
|
|
239
|
+
};
|
|
240
|
+
})();
|
|
241
|
+
|
|
242
|
+
const applyLayout = (rfNodes, rfEdges) => {
|
|
243
|
+
const { nodes: ln, edges: le } = getLayoutedElements(
|
|
244
|
+
rfNodes,
|
|
245
|
+
rfEdges,
|
|
246
|
+
djNode.name,
|
|
247
|
+
);
|
|
248
|
+
setCurrentNodeName(djNode.name);
|
|
249
|
+
setNodes(ln);
|
|
250
|
+
setEdges(le);
|
|
251
|
+
setLoaded(true);
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
djClient
|
|
255
|
+
.dimensionDag(djNode.name)
|
|
256
|
+
.then(
|
|
257
|
+
({
|
|
258
|
+
inbound = [],
|
|
259
|
+
inbound_edges = [],
|
|
260
|
+
outbound = [],
|
|
261
|
+
outbound_edges = [],
|
|
262
|
+
}) => {
|
|
263
|
+
if (inbound.length === 0 && outbound.length === 0) {
|
|
264
|
+
setLoaded(true);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const allRelated = [...inbound, ...outbound];
|
|
268
|
+
const rfNodes = [
|
|
269
|
+
buildRFNode(djNode, djNode.name),
|
|
270
|
+
...allRelated.map(n => buildRFNode(n, djNode.name)),
|
|
271
|
+
];
|
|
272
|
+
const rfEdges = [
|
|
273
|
+
...inbound_edges.map(e => makeEdge(e.source, e.target)),
|
|
274
|
+
...outbound_edges.map(e => makeEdge(e.source, e.target)),
|
|
275
|
+
].filter(Boolean);
|
|
276
|
+
applyLayout(rfNodes, rfEdges);
|
|
277
|
+
},
|
|
278
|
+
)
|
|
279
|
+
.catch(err => {
|
|
280
|
+
console.error(err);
|
|
281
|
+
setLoaded(true);
|
|
282
|
+
});
|
|
283
|
+
}, [djNode, djClient]);
|
|
284
|
+
|
|
285
|
+
const onNodeMouseEnter = useCallback(
|
|
286
|
+
(_, node) => {
|
|
287
|
+
const connectedIds = new Set([node.id]);
|
|
288
|
+
setEdges(eds => {
|
|
289
|
+
eds.forEach(e => {
|
|
290
|
+
if (e.source === node.id) connectedIds.add(e.target);
|
|
291
|
+
if (e.target === node.id) connectedIds.add(e.source);
|
|
292
|
+
});
|
|
293
|
+
return eds.map(e => {
|
|
294
|
+
const isConn = e.source === node.id || e.target === node.id;
|
|
295
|
+
const hoverColor = isConn ? '#475569' : '#cbd5e1';
|
|
296
|
+
return {
|
|
297
|
+
...e,
|
|
298
|
+
style: { strokeWidth: isConn ? 2 : 1, stroke: hoverColor },
|
|
299
|
+
markerEnd: {
|
|
300
|
+
type: MarkerType.ArrowClosed,
|
|
301
|
+
color: hoverColor,
|
|
302
|
+
width: 12,
|
|
303
|
+
height: 12,
|
|
304
|
+
},
|
|
305
|
+
};
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
setNodes(ns =>
|
|
309
|
+
ns.map(n => ({
|
|
310
|
+
...n,
|
|
311
|
+
data: { ...n.data, dimmed: !connectedIds.has(n.id) },
|
|
312
|
+
})),
|
|
313
|
+
);
|
|
314
|
+
},
|
|
315
|
+
[setEdges, setNodes],
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
const onNodeMouseLeave = useCallback(() => {
|
|
319
|
+
setEdges(eds =>
|
|
320
|
+
eds.map(e => ({ ...e, style: BASE_EDGE, markerEnd: BASE_MARKER })),
|
|
321
|
+
);
|
|
322
|
+
setNodes(ns => ns.map(n => ({ ...n, data: { ...n.data, dimmed: false } })));
|
|
323
|
+
}, [setEdges, setNodes]);
|
|
324
|
+
|
|
325
|
+
const onConnect = useCallback(
|
|
326
|
+
params => setEdges(eds => addEdge(params, eds)),
|
|
327
|
+
[setEdges],
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
if (loaded && nodes.length === 0) {
|
|
331
|
+
return (
|
|
332
|
+
<div style={{ padding: '2rem', color: '#64748b', fontSize: 14 }}>
|
|
333
|
+
No dimension links found for this node.
|
|
334
|
+
</div>
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return (
|
|
339
|
+
<div
|
|
340
|
+
style={{
|
|
341
|
+
height: 'calc(100vh - 280px)',
|
|
342
|
+
minHeight: 400,
|
|
343
|
+
background: '#fff',
|
|
344
|
+
}}
|
|
345
|
+
>
|
|
346
|
+
{/* Ensure node HTML layer always renders above the edge SVG layer */}
|
|
347
|
+
<style>{`.react-flow__nodes { z-index: 10 !important; }`}</style>
|
|
348
|
+
<ReactFlow
|
|
349
|
+
nodes={nodes}
|
|
350
|
+
edges={edges}
|
|
351
|
+
nodeTypes={nodeTypes}
|
|
352
|
+
onNodesChange={onNodesChange}
|
|
353
|
+
onEdgesChange={onEdgesChange}
|
|
354
|
+
onConnect={onConnect}
|
|
355
|
+
onNodeMouseEnter={onNodeMouseEnter}
|
|
356
|
+
onNodeMouseLeave={onNodeMouseLeave}
|
|
357
|
+
onInit={setRfInstance}
|
|
358
|
+
proOptions={{ hideAttribution: true }}
|
|
359
|
+
/>
|
|
360
|
+
</div>
|
|
361
|
+
);
|
|
362
|
+
}
|
|
@@ -26,6 +26,7 @@ const createDJNode = node => {
|
|
|
26
26
|
const NodeColumnLineage = djNode => {
|
|
27
27
|
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
28
28
|
const dagFetch = async (getLayoutedElements, setNodes, setEdges) => {
|
|
29
|
+
if (!djNode?.djNode) return;
|
|
29
30
|
let relatedNodes = await djClient.node_lineage(djNode.djNode.name);
|
|
30
31
|
let nodesMapping = {};
|
|
31
32
|
let edgesMapping = {};
|
|
@@ -24,15 +24,15 @@ export default function NodesWithDimension({ node, djClient }) {
|
|
|
24
24
|
{availableNodes.map(node => (
|
|
25
25
|
<tr>
|
|
26
26
|
<td>
|
|
27
|
-
<a href={`/nodes/${node.name}`}>{node.
|
|
27
|
+
<a href={`/nodes/${node.name}`}>{node.name}</a>
|
|
28
28
|
</td>
|
|
29
|
-
<td>
|
|
29
|
+
{/* <td>
|
|
30
30
|
<span
|
|
31
31
|
className={'node_type__' + node.type + ' badge node_type'}
|
|
32
32
|
>
|
|
33
33
|
{node.type}
|
|
34
34
|
</span>
|
|
35
|
-
</td>
|
|
35
|
+
</td> */}
|
|
36
36
|
</tr>
|
|
37
37
|
))}
|
|
38
38
|
</tbody>
|