datajunction-ui 0.0.23-rc.0 → 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.
Files changed (25) hide show
  1. package/package.json +8 -2
  2. package/src/app/index.tsx +6 -0
  3. package/src/app/pages/NamespacePage/CompactSelect.jsx +100 -0
  4. package/src/app/pages/NamespacePage/NodeModeSelect.jsx +8 -5
  5. package/src/app/pages/NamespacePage/__tests__/CompactSelect.test.jsx +190 -0
  6. package/src/app/pages/NamespacePage/__tests__/index.test.jsx +297 -8
  7. package/src/app/pages/NamespacePage/index.jsx +489 -62
  8. package/src/app/pages/QueryPlannerPage/Loadable.jsx +6 -0
  9. package/src/app/pages/QueryPlannerPage/MetricFlowGraph.jsx +311 -0
  10. package/src/app/pages/QueryPlannerPage/PreAggDetailsPanel.jsx +470 -0
  11. package/src/app/pages/QueryPlannerPage/SelectionPanel.jsx +384 -0
  12. package/src/app/pages/QueryPlannerPage/__tests__/MetricFlowGraph.test.jsx +239 -0
  13. package/src/app/pages/QueryPlannerPage/__tests__/PreAggDetailsPanel.test.jsx +638 -0
  14. package/src/app/pages/QueryPlannerPage/__tests__/SelectionPanel.test.jsx +429 -0
  15. package/src/app/pages/QueryPlannerPage/__tests__/index.test.jsx +317 -0
  16. package/src/app/pages/QueryPlannerPage/index.jsx +209 -0
  17. package/src/app/pages/QueryPlannerPage/styles.css +1251 -0
  18. package/src/app/pages/Root/index.tsx +5 -0
  19. package/src/app/services/DJService.js +61 -2
  20. package/src/styles/index.css +2 -2
  21. package/src/app/icons/FilterIcon.jsx +0 -7
  22. package/src/app/pages/NamespacePage/FieldControl.jsx +0 -21
  23. package/src/app/pages/NamespacePage/NodeTypeSelect.jsx +0 -30
  24. package/src/app/pages/NamespacePage/TagSelect.jsx +0 -44
  25. 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;