datajunction-ui 0.0.26-alpha.0 → 0.0.27-alpha.0

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 (28) hide show
  1. package/package.json +2 -2
  2. package/src/app/components/Search.jsx +41 -33
  3. package/src/app/components/__tests__/Search.test.jsx +46 -11
  4. package/src/app/index.tsx +1 -1
  5. package/src/app/pages/AddEditNodePage/MetricQueryField.jsx +57 -8
  6. package/src/app/pages/AddEditNodePage/UpstreamNodeField.jsx +17 -5
  7. package/src/app/pages/AddEditNodePage/__tests__/index.test.jsx +97 -1
  8. package/src/app/pages/AddEditNodePage/index.jsx +61 -17
  9. package/src/app/pages/NodePage/WatchNodeButton.jsx +12 -5
  10. package/src/app/pages/QueryPlannerPage/MetricFlowGraph.jsx +93 -15
  11. package/src/app/pages/QueryPlannerPage/PreAggDetailsPanel.jsx +2320 -65
  12. package/src/app/pages/QueryPlannerPage/SelectionPanel.jsx +234 -25
  13. package/src/app/pages/QueryPlannerPage/__tests__/MetricFlowGraph.test.jsx +315 -122
  14. package/src/app/pages/QueryPlannerPage/__tests__/PreAggDetailsPanel.test.jsx +2672 -314
  15. package/src/app/pages/QueryPlannerPage/__tests__/SelectionPanel.test.jsx +567 -0
  16. package/src/app/pages/QueryPlannerPage/__tests__/index.test.jsx +480 -55
  17. package/src/app/pages/QueryPlannerPage/index.jsx +1021 -14
  18. package/src/app/pages/QueryPlannerPage/styles.css +1990 -62
  19. package/src/app/pages/Root/__tests__/index.test.jsx +79 -8
  20. package/src/app/pages/Root/index.tsx +1 -1
  21. package/src/app/pages/SQLBuilderPage/__tests__/index.test.jsx +82 -0
  22. package/src/app/pages/SettingsPage/__tests__/CreateServiceAccountModal.test.jsx +37 -0
  23. package/src/app/pages/SettingsPage/__tests__/ServiceAccountsSection.test.jsx +48 -0
  24. package/src/app/pages/SettingsPage/__tests__/index.test.jsx +169 -1
  25. package/src/app/services/DJService.js +492 -3
  26. package/src/app/services/__tests__/DJService.test.jsx +582 -0
  27. package/src/mocks/mockNodes.jsx +36 -0
  28. package/webpack.config.js +27 -0
@@ -7,10 +7,7 @@ import CollapsedIcon from '../../icons/CollapsedIcon';
7
7
  const EVENT_TYPES = ['delete', 'update'];
8
8
 
9
9
  export default function WatchButton({ node }) {
10
- if (!node || !node.name || !node.type) {
11
- return null;
12
- }
13
-
10
+ // All hooks must be called before any early returns
14
11
  const djClient = useContext(DJClientContext).DataJunctionAPI;
15
12
  const [selectedEvents, setSelectedEvents] = useState([]);
16
13
  const [loading, setLoading] = useState(false);
@@ -19,6 +16,10 @@ export default function WatchButton({ node }) {
19
16
 
20
17
  // Load existing preferences
21
18
  useEffect(() => {
19
+ if (!node || !node.name || !node.type) {
20
+ return;
21
+ }
22
+
22
23
  const loadPreferences = async () => {
23
24
  try {
24
25
  const preferences = await djClient.getNotificationPreferences({
@@ -38,7 +39,7 @@ export default function WatchButton({ node }) {
38
39
  };
39
40
 
40
41
  loadPreferences();
41
- }, [djClient, node.name, node.type]);
42
+ }, [djClient, node?.name, node?.type]);
42
43
 
43
44
  // Close dropdown on outside click
44
45
  useEffect(() => {
@@ -51,6 +52,12 @@ export default function WatchButton({ node }) {
51
52
  document.addEventListener('mousedown', handleClickOutside);
52
53
  return () => document.removeEventListener('mousedown', handleClickOutside);
53
54
  }, []);
55
+
56
+ // Early return after all hooks are called
57
+ if (!node || !node.name || !node.type) {
58
+ return null;
59
+ }
60
+
54
61
  const toggleEvent = async event => {
55
62
  const isSelected = selectedEvents.includes(event);
56
63
 
@@ -60,8 +60,32 @@ function MetricNode({ data, selected }) {
60
60
  );
61
61
  }
62
62
 
63
+ /**
64
+ * Component node - shows metric building blocks (e.g., SUM, COUNT)
65
+ */
66
+ function ComponentNode({ data, selected }) {
67
+ return (
68
+ <div
69
+ className={`compact-node compact-node-component ${
70
+ selected ? 'selected' : ''
71
+ }`}
72
+ >
73
+ <Handle type="target" position={Position.Left} />
74
+ <div className="compact-node-icon">●</div>
75
+ <div className="compact-node-content">
76
+ <div className="compact-node-name">{data.shortName}</div>
77
+ <div className="compact-node-meta">
78
+ <span className="meta-item">{data.aggregation || 'RAW'}</span>
79
+ </div>
80
+ </div>
81
+ <Handle type="source" position={Position.Right} />
82
+ </div>
83
+ );
84
+ }
85
+
63
86
  const nodeTypes = {
64
87
  preagg: PreAggNode,
88
+ component: ComponentNode,
65
89
  metric: MetricNode,
66
90
  };
67
91
 
@@ -132,6 +156,7 @@ export function MetricFlowGraph({
132
156
 
133
157
  // Track mappings
134
158
  const preAggNodesMap = new Map();
159
+ const componentNodeIds = new Map();
135
160
  const componentToPreAgg = new Map();
136
161
 
137
162
  let nodeId = 0;
@@ -167,6 +192,37 @@ export function MetricFlowGraph({
167
192
  });
168
193
  });
169
194
 
195
+ // Create component nodes (building blocks for metrics)
196
+ grainGroups.forEach((gg, ggIdx) => {
197
+ gg.components?.forEach(comp => {
198
+ if (!componentNodeIds.has(comp.name)) {
199
+ const id = getNextId();
200
+ componentNodeIds.set(comp.name, id);
201
+ // Shorten name for display (e.g., "unit_price_sum" -> "price_sum")
202
+ const shortName =
203
+ comp.name.length > 40
204
+ ? '...' + comp.name.split('_').slice(-2).join('_')
205
+ : comp.name;
206
+
207
+ rawNodes.push({
208
+ id,
209
+ type: 'component',
210
+ position: { x: 0, y: 0 },
211
+ data: {
212
+ name: comp.name,
213
+ shortName,
214
+ aggregation: comp.aggregation,
215
+ merge: comp.merge,
216
+ grainGroupIndex: ggIdx,
217
+ },
218
+ selected:
219
+ selectedNode?.type === 'component' &&
220
+ selectedNode?.name === comp.name,
221
+ });
222
+ }
223
+ });
224
+ });
225
+
170
226
  // Create metric nodes
171
227
  const metricNodeIds = new Map();
172
228
 
@@ -191,26 +247,38 @@ export function MetricFlowGraph({
191
247
  });
192
248
  });
193
249
 
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);
250
+ // Create edges: PreAgg -> Component
251
+ grainGroups.forEach((gg, ggIdx) => {
252
+ const preAggId = preAggNodesMap.get(ggIdx);
253
+ gg.components?.forEach(comp => {
254
+ const compId = componentNodeIds.get(comp.name);
255
+ if (preAggId && compId) {
256
+ rawEdges.push({
257
+ id: `edge-preagg-${preAggId}-${compId}`,
258
+ source: preAggId,
259
+ target: compId,
260
+ style: { stroke: '#64748b', strokeWidth: 2 },
261
+ markerEnd: {
262
+ type: MarkerType.ArrowClosed,
263
+ color: '#64748b',
264
+ width: 16,
265
+ height: 16,
266
+ },
267
+ });
203
268
  }
204
269
  });
270
+ });
205
271
 
206
- connectedPreAggs.forEach(preAggIdx => {
207
- const preAggId = preAggNodesMap.get(preAggIdx);
208
- if (preAggId && metricId) {
272
+ // Create edges: Component -> Metric
273
+ metricFormulas.forEach(metric => {
274
+ const metricId = metricNodeIds.get(metric.name);
275
+ metric.components?.forEach(compName => {
276
+ const compId = componentNodeIds.get(compName);
277
+ if (compId && metricId) {
209
278
  rawEdges.push({
210
- id: `edge-${preAggId}-${metricId}`,
211
- source: preAggId,
279
+ id: `edge-comp-${compId}-${metricId}`,
280
+ source: compId,
212
281
  target: metricId,
213
- type: 'default', // Straight/bezier edges
214
282
  style: { stroke: '#64748b', strokeWidth: 2 },
215
283
  markerEnd: {
216
284
  type: MarkerType.ArrowClosed,
@@ -244,6 +312,12 @@ export function MetricFlowGraph({
244
312
  index: node.data.grainGroupIndex,
245
313
  data: grainGroups[node.data.grainGroupIndex],
246
314
  });
315
+ } else if (node.type === 'component') {
316
+ onNodeSelect?.({
317
+ type: 'component',
318
+ name: node.data.name,
319
+ data: node.data,
320
+ });
247
321
  } else if (node.type === 'metric') {
248
322
  onNodeSelect?.({
249
323
  type: 'metric',
@@ -295,6 +369,10 @@ export function MetricFlowGraph({
295
369
  <span className="legend-dot preagg"></span>
296
370
  <span>Pre-agg</span>
297
371
  </div>
372
+ <div className="legend-item">
373
+ <span className="legend-dot component"></span>
374
+ <span>Component</span>
375
+ </div>
298
376
  <div className="legend-item">
299
377
  <span className="legend-dot metric"></span>
300
378
  <span>Metric</span>