datajunction-ui 0.0.23 → 0.0.26
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 +11 -4
- package/src/app/index.tsx +6 -0
- package/src/app/pages/NamespacePage/CompactSelect.jsx +100 -0
- package/src/app/pages/NamespacePage/NodeModeSelect.jsx +8 -5
- package/src/app/pages/NamespacePage/__tests__/CompactSelect.test.jsx +190 -0
- package/src/app/pages/NamespacePage/__tests__/index.test.jsx +297 -8
- package/src/app/pages/NamespacePage/index.jsx +489 -62
- package/src/app/pages/QueryPlannerPage/Loadable.jsx +6 -0
- package/src/app/pages/QueryPlannerPage/MetricFlowGraph.jsx +311 -0
- package/src/app/pages/QueryPlannerPage/PreAggDetailsPanel.jsx +470 -0
- package/src/app/pages/QueryPlannerPage/SelectionPanel.jsx +384 -0
- package/src/app/pages/QueryPlannerPage/__tests__/MetricFlowGraph.test.jsx +239 -0
- package/src/app/pages/QueryPlannerPage/__tests__/PreAggDetailsPanel.test.jsx +638 -0
- package/src/app/pages/QueryPlannerPage/__tests__/SelectionPanel.test.jsx +429 -0
- package/src/app/pages/QueryPlannerPage/__tests__/index.test.jsx +317 -0
- package/src/app/pages/QueryPlannerPage/index.jsx +209 -0
- package/src/app/pages/QueryPlannerPage/styles.css +1251 -0
- package/src/app/pages/Root/index.tsx +5 -0
- package/src/app/services/DJService.js +61 -2
- package/src/styles/index.css +2 -2
- package/src/app/icons/FilterIcon.jsx +0 -7
- package/src/app/pages/NamespacePage/FieldControl.jsx +0 -21
- package/src/app/pages/NamespacePage/NodeTypeSelect.jsx +0 -30
- package/src/app/pages/NamespacePage/TagSelect.jsx +0 -44
- package/src/app/pages/NamespacePage/UserSelect.jsx +0 -47
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { useMemo, useEffect, useCallback } from 'react';
|
|
2
|
+
import ReactFlow, {
|
|
3
|
+
Background,
|
|
4
|
+
Controls,
|
|
5
|
+
MarkerType,
|
|
6
|
+
useNodesState,
|
|
7
|
+
useEdgesState,
|
|
8
|
+
Handle,
|
|
9
|
+
Position,
|
|
10
|
+
} from 'reactflow';
|
|
11
|
+
import dagre from 'dagre';
|
|
12
|
+
import 'reactflow/dist/style.css';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Compact Pre-aggregation node - clickable, shows minimal info
|
|
16
|
+
*/
|
|
17
|
+
function PreAggNode({ data, selected }) {
|
|
18
|
+
const componentCount = data.components?.length || 0;
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div
|
|
22
|
+
className={`compact-node compact-node-preagg ${
|
|
23
|
+
selected ? 'selected' : ''
|
|
24
|
+
}`}
|
|
25
|
+
>
|
|
26
|
+
<div className="compact-node-icon">◫</div>
|
|
27
|
+
<div className="compact-node-content">
|
|
28
|
+
<div className="compact-node-name">{data.name}</div>
|
|
29
|
+
<div className="compact-node-meta">
|
|
30
|
+
<span className="meta-item">{componentCount} components</span>
|
|
31
|
+
{data.grain?.length > 0 && (
|
|
32
|
+
<span className="meta-item grain-count">
|
|
33
|
+
{data.grain.length} grain cols
|
|
34
|
+
</span>
|
|
35
|
+
)}
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
<Handle type="source" position={Position.Right} />
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Compact Metric node - clickable, shows minimal info
|
|
45
|
+
*/
|
|
46
|
+
function MetricNode({ data, selected }) {
|
|
47
|
+
return (
|
|
48
|
+
<div
|
|
49
|
+
className={`compact-node compact-node-metric ${
|
|
50
|
+
data.isDerived ? 'compact-node-derived' : ''
|
|
51
|
+
} ${selected ? 'selected' : ''}`}
|
|
52
|
+
>
|
|
53
|
+
<Handle type="target" position={Position.Left} />
|
|
54
|
+
<div className="compact-node-icon">{data.isDerived ? '◇' : '◈'}</div>
|
|
55
|
+
<div className="compact-node-content">
|
|
56
|
+
<div className="compact-node-name">{data.shortName}</div>
|
|
57
|
+
{data.isDerived && <div className="compact-node-badge">Derived</div>}
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const nodeTypes = {
|
|
64
|
+
preagg: PreAggNode,
|
|
65
|
+
metric: MetricNode,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// Node dimensions for dagre layout
|
|
69
|
+
const NODE_WIDTH = 200;
|
|
70
|
+
const NODE_HEIGHT = 50;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Use dagre to automatically layout nodes
|
|
74
|
+
*/
|
|
75
|
+
function getLayoutedElements(nodes, edges) {
|
|
76
|
+
const dagreGraph = new dagre.graphlib.Graph();
|
|
77
|
+
dagreGraph.setDefaultEdgeLabel(() => ({}));
|
|
78
|
+
|
|
79
|
+
// Configure the layout
|
|
80
|
+
dagreGraph.setGraph({
|
|
81
|
+
rankdir: 'LR', // Left to right
|
|
82
|
+
nodesep: 60, // Vertical spacing between nodes
|
|
83
|
+
ranksep: 150, // Horizontal spacing between columns
|
|
84
|
+
marginx: 40,
|
|
85
|
+
marginy: 40,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Add nodes to dagre
|
|
89
|
+
nodes.forEach(node => {
|
|
90
|
+
dagreGraph.setNode(node.id, { width: NODE_WIDTH, height: NODE_HEIGHT });
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Add edges to dagre
|
|
94
|
+
edges.forEach(edge => {
|
|
95
|
+
dagreGraph.setEdge(edge.source, edge.target);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Run the layout
|
|
99
|
+
dagre.layout(dagreGraph);
|
|
100
|
+
|
|
101
|
+
// Apply the calculated positions back to nodes
|
|
102
|
+
const layoutedNodes = nodes.map(node => {
|
|
103
|
+
const nodeWithPosition = dagreGraph.node(node.id);
|
|
104
|
+
return {
|
|
105
|
+
...node,
|
|
106
|
+
position: {
|
|
107
|
+
x: nodeWithPosition.x - NODE_WIDTH / 2,
|
|
108
|
+
y: nodeWithPosition.y - NODE_HEIGHT / 2,
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return { nodes: layoutedNodes, edges };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* MetricFlowGraph - Uses dagre for automatic layout
|
|
118
|
+
*/
|
|
119
|
+
export function MetricFlowGraph({
|
|
120
|
+
grainGroups,
|
|
121
|
+
metricFormulas,
|
|
122
|
+
selectedNode,
|
|
123
|
+
onNodeSelect,
|
|
124
|
+
}) {
|
|
125
|
+
const { nodes, edges } = useMemo(() => {
|
|
126
|
+
if (!grainGroups?.length || !metricFormulas?.length) {
|
|
127
|
+
return { nodes: [], edges: [] };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const rawNodes = [];
|
|
131
|
+
const rawEdges = [];
|
|
132
|
+
|
|
133
|
+
// Track mappings
|
|
134
|
+
const preAggNodesMap = new Map();
|
|
135
|
+
const componentToPreAgg = new Map();
|
|
136
|
+
|
|
137
|
+
let nodeId = 0;
|
|
138
|
+
const getNextId = () => `node-${nodeId++}`;
|
|
139
|
+
|
|
140
|
+
// Build component -> preAgg mapping
|
|
141
|
+
grainGroups.forEach((gg, idx) => {
|
|
142
|
+
gg.components?.forEach(comp => {
|
|
143
|
+
componentToPreAgg.set(comp.name, idx);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Create pre-aggregation nodes
|
|
148
|
+
grainGroups.forEach((gg, idx) => {
|
|
149
|
+
const id = getNextId();
|
|
150
|
+
preAggNodesMap.set(idx, id);
|
|
151
|
+
|
|
152
|
+
const shortName = gg.parent_name?.split('.').pop() || `preagg_${idx}`;
|
|
153
|
+
|
|
154
|
+
rawNodes.push({
|
|
155
|
+
id,
|
|
156
|
+
type: 'preagg',
|
|
157
|
+
position: { x: 0, y: 0 }, // Will be set by dagre
|
|
158
|
+
data: {
|
|
159
|
+
name: shortName,
|
|
160
|
+
fullName: gg.parent_name,
|
|
161
|
+
grain: gg.grain || [],
|
|
162
|
+
components: gg.components || [],
|
|
163
|
+
grainGroupIndex: idx,
|
|
164
|
+
},
|
|
165
|
+
selected:
|
|
166
|
+
selectedNode?.type === 'preagg' && selectedNode?.index === idx,
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Create metric nodes
|
|
171
|
+
const metricNodeIds = new Map();
|
|
172
|
+
|
|
173
|
+
metricFormulas.forEach((metric, idx) => {
|
|
174
|
+
const id = getNextId();
|
|
175
|
+
metricNodeIds.set(metric.name, id);
|
|
176
|
+
|
|
177
|
+
rawNodes.push({
|
|
178
|
+
id,
|
|
179
|
+
type: 'metric',
|
|
180
|
+
position: { x: 0, y: 0 }, // Will be set by dagre
|
|
181
|
+
data: {
|
|
182
|
+
name: metric.name,
|
|
183
|
+
shortName: metric.short_name,
|
|
184
|
+
combiner: metric.combiner,
|
|
185
|
+
isDerived: metric.is_derived,
|
|
186
|
+
components: metric.components,
|
|
187
|
+
metricIndex: idx,
|
|
188
|
+
},
|
|
189
|
+
selected:
|
|
190
|
+
selectedNode?.type === 'metric' && selectedNode?.index === idx,
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Create edges
|
|
195
|
+
metricFormulas.forEach(metric => {
|
|
196
|
+
const metricId = metricNodeIds.get(metric.name);
|
|
197
|
+
const connectedPreAggs = new Set();
|
|
198
|
+
|
|
199
|
+
metric.components?.forEach(compName => {
|
|
200
|
+
const preAggIdx = componentToPreAgg.get(compName);
|
|
201
|
+
if (preAggIdx !== undefined) {
|
|
202
|
+
connectedPreAggs.add(preAggIdx);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
connectedPreAggs.forEach(preAggIdx => {
|
|
207
|
+
const preAggId = preAggNodesMap.get(preAggIdx);
|
|
208
|
+
if (preAggId && metricId) {
|
|
209
|
+
rawEdges.push({
|
|
210
|
+
id: `edge-${preAggId}-${metricId}`,
|
|
211
|
+
source: preAggId,
|
|
212
|
+
target: metricId,
|
|
213
|
+
type: 'default', // Straight/bezier edges
|
|
214
|
+
style: { stroke: '#64748b', strokeWidth: 2 },
|
|
215
|
+
markerEnd: {
|
|
216
|
+
type: MarkerType.ArrowClosed,
|
|
217
|
+
color: '#64748b',
|
|
218
|
+
width: 16,
|
|
219
|
+
height: 16,
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// Apply dagre layout
|
|
227
|
+
return getLayoutedElements(rawNodes, rawEdges);
|
|
228
|
+
}, [grainGroups, metricFormulas, selectedNode]);
|
|
229
|
+
|
|
230
|
+
const [flowNodes, setNodes, onNodesChange] = useNodesState(nodes);
|
|
231
|
+
const [flowEdges, setEdges, onEdgesChange] = useEdgesState(edges);
|
|
232
|
+
|
|
233
|
+
// Update nodes/edges when data changes
|
|
234
|
+
useEffect(() => {
|
|
235
|
+
setNodes(nodes);
|
|
236
|
+
setEdges(edges);
|
|
237
|
+
}, [nodes, edges, setNodes, setEdges]);
|
|
238
|
+
|
|
239
|
+
const handleNodeClick = useCallback(
|
|
240
|
+
(event, node) => {
|
|
241
|
+
if (node.type === 'preagg') {
|
|
242
|
+
onNodeSelect?.({
|
|
243
|
+
type: 'preagg',
|
|
244
|
+
index: node.data.grainGroupIndex,
|
|
245
|
+
data: grainGroups[node.data.grainGroupIndex],
|
|
246
|
+
});
|
|
247
|
+
} else if (node.type === 'metric') {
|
|
248
|
+
onNodeSelect?.({
|
|
249
|
+
type: 'metric',
|
|
250
|
+
index: node.data.metricIndex,
|
|
251
|
+
data: metricFormulas[node.data.metricIndex],
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
[onNodeSelect, grainGroups, metricFormulas],
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
const handlePaneClick = useCallback(() => {
|
|
259
|
+
onNodeSelect?.(null);
|
|
260
|
+
}, [onNodeSelect]);
|
|
261
|
+
|
|
262
|
+
if (!grainGroups?.length || !metricFormulas?.length) {
|
|
263
|
+
return (
|
|
264
|
+
<div className="graph-empty-state">
|
|
265
|
+
<div className="empty-icon">◎</div>
|
|
266
|
+
<p>Select metrics and dimensions above to visualize the data flow</p>
|
|
267
|
+
</div>
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return (
|
|
272
|
+
<div className="compact-flow-container">
|
|
273
|
+
<ReactFlow
|
|
274
|
+
nodes={flowNodes}
|
|
275
|
+
edges={flowEdges}
|
|
276
|
+
onNodesChange={onNodesChange}
|
|
277
|
+
onEdgesChange={onEdgesChange}
|
|
278
|
+
nodeTypes={nodeTypes}
|
|
279
|
+
onNodeClick={handleNodeClick}
|
|
280
|
+
onPaneClick={handlePaneClick}
|
|
281
|
+
fitView
|
|
282
|
+
fitViewOptions={{ padding: 0.2 }}
|
|
283
|
+
minZoom={0.5}
|
|
284
|
+
maxZoom={1.5}
|
|
285
|
+
attributionPosition="bottom-left"
|
|
286
|
+
proOptions={{ hideAttribution: true }}
|
|
287
|
+
>
|
|
288
|
+
<Background color="#cbd5e1" gap={20} size={1} />
|
|
289
|
+
<Controls showInteractive={false} />
|
|
290
|
+
</ReactFlow>
|
|
291
|
+
|
|
292
|
+
{/* Legend */}
|
|
293
|
+
<div className="graph-legend">
|
|
294
|
+
<div className="legend-item">
|
|
295
|
+
<span className="legend-dot preagg"></span>
|
|
296
|
+
<span>Pre-agg</span>
|
|
297
|
+
</div>
|
|
298
|
+
<div className="legend-item">
|
|
299
|
+
<span className="legend-dot metric"></span>
|
|
300
|
+
<span>Metric</span>
|
|
301
|
+
</div>
|
|
302
|
+
<div className="legend-item">
|
|
303
|
+
<span className="legend-dot derived"></span>
|
|
304
|
+
<span>Derived</span>
|
|
305
|
+
</div>
|
|
306
|
+
</div>
|
|
307
|
+
</div>
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export default MetricFlowGraph;
|