datajunction-ui 0.0.26-alpha.0 → 0.0.27

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
@@ -1,65 +1,66 @@
1
- import { render, screen } from '@testing-library/react';
1
+ import { render, screen, fireEvent } from '@testing-library/react';
2
2
  import React from 'react';
3
3
 
4
- // Mock the entire MetricFlowGraph module since dagre is difficult to mock
5
- jest.mock('../MetricFlowGraph', () => ({
6
- MetricFlowGraph: ({
7
- grainGroups,
8
- metricFormulas,
9
- selectedNode,
10
- onNodeSelect,
11
- }) => {
12
- if (!grainGroups?.length || !metricFormulas?.length) {
13
- return (
14
- <div data-testid="graph-empty">
15
- Select metrics and dimensions above to visualize the data flow
16
- </div>
17
- );
18
- }
19
- return (
20
- <div data-testid="metric-flow-graph">
21
- <div data-testid="nodes-count">
22
- {grainGroups.length + metricFormulas.length}
23
- </div>
24
- {grainGroups.map((gg, i) => (
25
- <div
26
- key={`preagg-${i}`}
27
- data-testid={`preagg-node-${i}`}
28
- onClick={() =>
29
- onNodeSelect?.({ type: 'preagg', index: i, data: gg })
30
- }
31
- >
32
- {gg.parent_name?.split('.').pop()}
33
- </div>
34
- ))}
35
- {metricFormulas.map((m, i) => (
4
+ // Mock dagre module with inline class definition
5
+ jest.mock('dagre', () => {
6
+ // Define mock graph class inside the mock factory
7
+ function MockGraph() {
8
+ this.setDefaultEdgeLabel = jest.fn().mockReturnValue(this);
9
+ this.setGraph = jest.fn().mockReturnValue(this);
10
+ this.setNode = jest.fn().mockReturnValue(this);
11
+ this.setEdge = jest.fn().mockReturnValue(this);
12
+ this.node = jest.fn().mockReturnValue({ x: 100, y: 100 });
13
+ }
14
+
15
+ return {
16
+ graphlib: {
17
+ Graph: MockGraph,
18
+ },
19
+ layout: jest.fn(),
20
+ };
21
+ });
22
+
23
+ // Mock ReactFlow and related components
24
+ jest.mock('reactflow', () => {
25
+ return {
26
+ __esModule: true,
27
+ default: ({ children, nodes, edges, onNodeClick, onPaneClick }) => (
28
+ <div data-testid="react-flow" onClick={onPaneClick}>
29
+ {nodes?.map(node => (
36
30
  <div
37
- key={`metric-${i}`}
38
- data-testid={`metric-node-${i}`}
39
- onClick={() =>
40
- onNodeSelect?.({ type: 'metric', index: i, data: m })
41
- }
31
+ key={node.id}
32
+ data-testid={`node-${node.id}`}
33
+ className={`flow-node ${node.type}`}
34
+ onClick={e => {
35
+ e.stopPropagation();
36
+ onNodeClick?.(e, node);
37
+ }}
42
38
  >
43
- {m.short_name}
39
+ {/* For metrics, use shortName; for preaggs, use name */}
40
+ {node.data.shortName || node.data.name}
44
41
  </div>
45
42
  ))}
46
- <div data-testid="legend">
47
- <span>Pre-agg</span>
48
- <span>Metric</span>
49
- <span>Derived</span>
50
- </div>
43
+ {children}
51
44
  </div>
52
- );
53
- },
54
- }));
45
+ ),
46
+ Background: () => <div data-testid="background" />,
47
+ Controls: () => <div data-testid="controls" />,
48
+ MarkerType: { ArrowClosed: 'arrowclosed' },
49
+ useNodesState: nodes => [nodes, jest.fn(), jest.fn()],
50
+ useEdgesState: edges => [edges, jest.fn(), jest.fn()],
51
+ Handle: ({ type, position }) => (
52
+ <div data-testid={`handle-${type}-${position}`} />
53
+ ),
54
+ Position: { Left: 'left', Right: 'right' },
55
+ };
56
+ });
55
57
 
56
- // Import after mock
57
- const { MetricFlowGraph } = require('../MetricFlowGraph');
58
+ // Import the component after mocks are set up
59
+ import { MetricFlowGraph } from '../MetricFlowGraph';
58
60
 
59
61
  const mockGrainGroups = [
60
62
  {
61
63
  parent_name: 'default.repair_orders',
62
- aggregability: 'FULL',
63
64
  grain: ['date_id', 'customer_id'],
64
65
  components: [
65
66
  { name: 'sum_revenue', expression: 'SUM(revenue)' },
@@ -68,7 +69,6 @@ const mockGrainGroups = [
68
69
  },
69
70
  {
70
71
  parent_name: 'inventory.stock',
71
- aggregability: 'LIMITED',
72
72
  grain: ['warehouse_id'],
73
73
  components: [{ name: 'sum_quantity', expression: 'SUM(quantity)' }],
74
74
  },
@@ -76,164 +76,357 @@ const mockGrainGroups = [
76
76
 
77
77
  const mockMetricFormulas = [
78
78
  {
79
- name: 'default.total_revenue',
80
- short_name: 'total_revenue',
81
- combiner: 'SUM(sum_revenue)',
82
- is_derived: false,
83
- components: ['sum_revenue'],
84
- },
85
- {
86
- name: 'default.order_count',
87
- short_name: 'order_count',
79
+ name: 'default.num_repair_orders',
80
+ short_name: 'num_repair_orders',
88
81
  combiner: 'SUM(count_orders)',
89
82
  is_derived: false,
90
83
  components: ['count_orders'],
91
84
  },
92
85
  {
93
- name: 'default.avg_order_value',
94
- short_name: 'avg_order_value',
86
+ name: 'default.avg_repair_price',
87
+ short_name: 'avg_repair_price',
95
88
  combiner: 'SUM(sum_revenue) / SUM(count_orders)',
96
89
  is_derived: true,
97
90
  components: ['sum_revenue', 'count_orders'],
98
91
  },
92
+ {
93
+ name: 'inventory.total_stock',
94
+ short_name: 'total_stock',
95
+ combiner: 'SUM(sum_quantity)',
96
+ is_derived: false,
97
+ components: ['sum_quantity'],
98
+ },
99
99
  ];
100
100
 
101
101
  describe('MetricFlowGraph', () => {
102
- const defaultProps = {
103
- grainGroups: mockGrainGroups,
104
- metricFormulas: mockMetricFormulas,
105
- selectedNode: null,
106
- onNodeSelect: jest.fn(),
107
- };
108
-
109
- beforeEach(() => {
110
- jest.clearAllMocks();
111
- });
112
-
113
102
  describe('Empty State', () => {
114
- it('shows empty state when no grain groups', () => {
115
- render(<MetricFlowGraph {...defaultProps} grainGroups={[]} />);
116
- expect(screen.getByTestId('graph-empty')).toBeInTheDocument();
103
+ it('shows empty state when no grain groups provided', () => {
104
+ render(
105
+ <MetricFlowGraph
106
+ grainGroups={[]}
107
+ metricFormulas={mockMetricFormulas}
108
+ onNodeSelect={jest.fn()}
109
+ />,
110
+ );
111
+
117
112
  expect(
118
113
  screen.getByText(
119
- 'Select metrics and dimensions above to visualize the data flow',
114
+ /Select metrics and dimensions above to visualize the data flow/i,
120
115
  ),
121
116
  ).toBeInTheDocument();
122
117
  });
123
118
 
124
- it('shows empty state when no metric formulas', () => {
125
- render(<MetricFlowGraph {...defaultProps} metricFormulas={[]} />);
126
- expect(screen.getByTestId('graph-empty')).toBeInTheDocument();
119
+ it('shows empty state when no metric formulas provided', () => {
120
+ render(
121
+ <MetricFlowGraph
122
+ grainGroups={mockGrainGroups}
123
+ metricFormulas={[]}
124
+ onNodeSelect={jest.fn()}
125
+ />,
126
+ );
127
+
128
+ expect(
129
+ screen.getByText(
130
+ /Select metrics and dimensions above to visualize the data flow/i,
131
+ ),
132
+ ).toBeInTheDocument();
127
133
  });
128
134
 
129
135
  it('shows empty state when both are null', () => {
130
136
  render(
131
137
  <MetricFlowGraph
132
- {...defaultProps}
133
138
  grainGroups={null}
134
139
  metricFormulas={null}
140
+ onNodeSelect={jest.fn()}
135
141
  />,
136
142
  );
137
- expect(screen.getByTestId('graph-empty')).toBeInTheDocument();
143
+
144
+ expect(
145
+ screen.getByText(
146
+ /Select metrics and dimensions above to visualize the data flow/i,
147
+ ),
148
+ ).toBeInTheDocument();
149
+ });
150
+
151
+ it('shows empty icon in empty state', () => {
152
+ render(
153
+ <MetricFlowGraph
154
+ grainGroups={[]}
155
+ metricFormulas={[]}
156
+ onNodeSelect={jest.fn()}
157
+ />,
158
+ );
159
+
160
+ expect(screen.getByText('◎')).toBeInTheDocument();
138
161
  });
139
162
  });
140
163
 
141
164
  describe('Graph Rendering', () => {
142
- it('renders graph container when data is provided', () => {
143
- render(<MetricFlowGraph {...defaultProps} />);
144
- expect(screen.getByTestId('metric-flow-graph')).toBeInTheDocument();
165
+ it('renders ReactFlow when data is provided', () => {
166
+ render(
167
+ <MetricFlowGraph
168
+ grainGroups={mockGrainGroups}
169
+ metricFormulas={mockMetricFormulas}
170
+ onNodeSelect={jest.fn()}
171
+ />,
172
+ );
173
+
174
+ expect(screen.getByTestId('react-flow')).toBeInTheDocument();
145
175
  });
146
176
 
147
- it('renders correct number of nodes', () => {
148
- render(<MetricFlowGraph {...defaultProps} />);
149
- // 2 pre-agg nodes + 3 metric nodes = 5 total
150
- expect(screen.getByTestId('nodes-count')).toHaveTextContent('5');
177
+ it('renders background and controls', () => {
178
+ render(
179
+ <MetricFlowGraph
180
+ grainGroups={mockGrainGroups}
181
+ metricFormulas={mockMetricFormulas}
182
+ onNodeSelect={jest.fn()}
183
+ />,
184
+ );
185
+
186
+ expect(screen.getByTestId('background')).toBeInTheDocument();
187
+ expect(screen.getByTestId('controls')).toBeInTheDocument();
151
188
  });
152
189
 
153
- it('displays pre-aggregation short names', () => {
154
- render(<MetricFlowGraph {...defaultProps} />);
190
+ it('renders pre-aggregation nodes', () => {
191
+ render(
192
+ <MetricFlowGraph
193
+ grainGroups={mockGrainGroups}
194
+ metricFormulas={mockMetricFormulas}
195
+ onNodeSelect={jest.fn()}
196
+ />,
197
+ );
198
+
155
199
  expect(screen.getByText('repair_orders')).toBeInTheDocument();
156
200
  expect(screen.getByText('stock')).toBeInTheDocument();
157
201
  });
158
202
 
159
- it('displays metric short names', () => {
160
- render(<MetricFlowGraph {...defaultProps} />);
161
- expect(screen.getByText('total_revenue')).toBeInTheDocument();
162
- expect(screen.getByText('order_count')).toBeInTheDocument();
163
- expect(screen.getByText('avg_order_value')).toBeInTheDocument();
203
+ it('renders metric nodes', () => {
204
+ render(
205
+ <MetricFlowGraph
206
+ grainGroups={mockGrainGroups}
207
+ metricFormulas={mockMetricFormulas}
208
+ onNodeSelect={jest.fn()}
209
+ />,
210
+ );
211
+
212
+ expect(screen.getByText('num_repair_orders')).toBeInTheDocument();
213
+ expect(screen.getByText('avg_repair_price')).toBeInTheDocument();
214
+ expect(screen.getByText('total_stock')).toBeInTheDocument();
164
215
  });
165
216
  });
166
217
 
167
218
  describe('Node Selection', () => {
168
- it('calls onNodeSelect when preagg node is clicked', () => {
219
+ it('calls onNodeSelect with preagg data when preagg node is clicked', () => {
169
220
  const onNodeSelect = jest.fn();
170
- render(<MetricFlowGraph {...defaultProps} onNodeSelect={onNodeSelect} />);
221
+ render(
222
+ <MetricFlowGraph
223
+ grainGroups={mockGrainGroups}
224
+ metricFormulas={mockMetricFormulas}
225
+ onNodeSelect={onNodeSelect}
226
+ />,
227
+ );
171
228
 
172
- const preaggNode = screen.getByTestId('preagg-node-0');
173
- preaggNode.click();
229
+ const preaggNode = screen.getByText('repair_orders');
230
+ fireEvent.click(preaggNode);
174
231
 
175
232
  expect(onNodeSelect).toHaveBeenCalledWith(
176
233
  expect.objectContaining({
177
234
  type: 'preagg',
178
235
  index: 0,
236
+ data: mockGrainGroups[0],
179
237
  }),
180
238
  );
181
239
  });
182
240
 
183
- it('calls onNodeSelect when metric node is clicked', () => {
241
+ it('calls onNodeSelect with metric data when metric node is clicked', () => {
184
242
  const onNodeSelect = jest.fn();
185
- render(<MetricFlowGraph {...defaultProps} onNodeSelect={onNodeSelect} />);
243
+ render(
244
+ <MetricFlowGraph
245
+ grainGroups={mockGrainGroups}
246
+ metricFormulas={mockMetricFormulas}
247
+ onNodeSelect={onNodeSelect}
248
+ />,
249
+ );
186
250
 
187
- const metricNode = screen.getByTestId('metric-node-0');
188
- metricNode.click();
251
+ const metricNode = screen.getByText('num_repair_orders');
252
+ fireEvent.click(metricNode);
189
253
 
190
254
  expect(onNodeSelect).toHaveBeenCalledWith(
191
255
  expect.objectContaining({
192
256
  type: 'metric',
193
257
  index: 0,
258
+ data: mockMetricFormulas[0],
194
259
  }),
195
260
  );
196
261
  });
197
262
 
198
- it('passes grain data when preagg is selected', () => {
263
+ it('calls onNodeSelect with null when pane is clicked', () => {
264
+ const onNodeSelect = jest.fn();
265
+ render(
266
+ <MetricFlowGraph
267
+ grainGroups={mockGrainGroups}
268
+ metricFormulas={mockMetricFormulas}
269
+ onNodeSelect={onNodeSelect}
270
+ />,
271
+ );
272
+
273
+ const flowPane = screen.getByTestId('react-flow');
274
+ fireEvent.click(flowPane);
275
+
276
+ expect(onNodeSelect).toHaveBeenCalledWith(null);
277
+ });
278
+ });
279
+
280
+ describe('Legend', () => {
281
+ it('renders legend with Pre-agg, Metric, and Derived labels', () => {
282
+ render(
283
+ <MetricFlowGraph
284
+ grainGroups={mockGrainGroups}
285
+ metricFormulas={mockMetricFormulas}
286
+ onNodeSelect={jest.fn()}
287
+ />,
288
+ );
289
+
290
+ expect(screen.getByText('Pre-agg')).toBeInTheDocument();
291
+ expect(screen.getByText('Metric')).toBeInTheDocument();
292
+ expect(screen.getByText('Derived')).toBeInTheDocument();
293
+ });
294
+ });
295
+
296
+ describe('Node Types', () => {
297
+ it('creates preagg nodes for each grain group', () => {
298
+ render(
299
+ <MetricFlowGraph
300
+ grainGroups={mockGrainGroups}
301
+ metricFormulas={mockMetricFormulas}
302
+ onNodeSelect={jest.fn()}
303
+ />,
304
+ );
305
+
306
+ // 2 grain groups = 2 preagg nodes
307
+ const preaggNodes = document.querySelectorAll('.flow-node.preagg');
308
+ expect(preaggNodes.length).toBe(2);
309
+ });
310
+
311
+ it('creates metric nodes for each metric formula', () => {
312
+ render(
313
+ <MetricFlowGraph
314
+ grainGroups={mockGrainGroups}
315
+ metricFormulas={mockMetricFormulas}
316
+ onNodeSelect={jest.fn()}
317
+ />,
318
+ );
319
+
320
+ // 3 metric formulas = 3 metric nodes
321
+ const metricNodes = document.querySelectorAll('.flow-node.metric');
322
+ expect(metricNodes.length).toBe(3);
323
+ });
324
+ });
325
+
326
+ describe('Node Selection by Index', () => {
327
+ it('selects correct preagg when second preagg is clicked', () => {
199
328
  const onNodeSelect = jest.fn();
200
- render(<MetricFlowGraph {...defaultProps} onNodeSelect={onNodeSelect} />);
329
+ render(
330
+ <MetricFlowGraph
331
+ grainGroups={mockGrainGroups}
332
+ metricFormulas={mockMetricFormulas}
333
+ onNodeSelect={onNodeSelect}
334
+ />,
335
+ );
201
336
 
202
- const preaggNode = screen.getByTestId('preagg-node-0');
203
- preaggNode.click();
337
+ const stockNode = screen.getByText('stock');
338
+ fireEvent.click(stockNode);
204
339
 
205
340
  expect(onNodeSelect).toHaveBeenCalledWith(
206
341
  expect.objectContaining({
207
- data: expect.objectContaining({
208
- grain: ['date_id', 'customer_id'],
209
- }),
342
+ type: 'preagg',
343
+ index: 1,
344
+ data: mockGrainGroups[1],
210
345
  }),
211
346
  );
212
347
  });
213
348
 
214
- it('passes combiner data when metric is selected', () => {
349
+ it('selects correct metric when derived metric is clicked', () => {
215
350
  const onNodeSelect = jest.fn();
216
- render(<MetricFlowGraph {...defaultProps} onNodeSelect={onNodeSelect} />);
351
+ render(
352
+ <MetricFlowGraph
353
+ grainGroups={mockGrainGroups}
354
+ metricFormulas={mockMetricFormulas}
355
+ onNodeSelect={onNodeSelect}
356
+ />,
357
+ );
217
358
 
218
- const metricNode = screen.getByTestId('metric-node-0');
219
- metricNode.click();
359
+ const derivedMetric = screen.getByText('avg_repair_price');
360
+ fireEvent.click(derivedMetric);
220
361
 
221
362
  expect(onNodeSelect).toHaveBeenCalledWith(
222
363
  expect.objectContaining({
364
+ type: 'metric',
365
+ index: 1,
223
366
  data: expect.objectContaining({
224
- combiner: 'SUM(sum_revenue)',
367
+ is_derived: true,
368
+ short_name: 'avg_repair_price',
225
369
  }),
226
370
  }),
227
371
  );
228
372
  });
229
373
  });
230
374
 
231
- describe('Legend', () => {
232
- it('renders graph legend', () => {
233
- render(<MetricFlowGraph {...defaultProps} />);
234
- expect(screen.getByText('Pre-agg')).toBeInTheDocument();
235
- expect(screen.getByText('Metric')).toBeInTheDocument();
236
- expect(screen.getByText('Derived')).toBeInTheDocument();
375
+ describe('Graph Layout', () => {
376
+ it('uses dagre for layout computation', () => {
377
+ const dagre = require('dagre');
378
+
379
+ render(
380
+ <MetricFlowGraph
381
+ grainGroups={mockGrainGroups}
382
+ metricFormulas={mockMetricFormulas}
383
+ onNodeSelect={jest.fn()}
384
+ />,
385
+ );
386
+
387
+ // dagre.layout should have been called
388
+ expect(dagre.layout).toHaveBeenCalled();
237
389
  });
238
390
  });
239
391
  });
392
+
393
+ describe('MetricFlowGraph Node Display', () => {
394
+ it('shows parent name without namespace prefix', () => {
395
+ render(
396
+ <MetricFlowGraph
397
+ grainGroups={[
398
+ {
399
+ parent_name: 'default.namespace.my_table',
400
+ grain: ['id'],
401
+ components: [{ name: 'comp1' }],
402
+ },
403
+ ]}
404
+ metricFormulas={[
405
+ {
406
+ name: 'default.metric',
407
+ short_name: 'metric',
408
+ components: ['comp1'],
409
+ },
410
+ ]}
411
+ onNodeSelect={jest.fn()}
412
+ />,
413
+ );
414
+
415
+ // Should show just 'my_table', not the full path
416
+ expect(screen.getByText('my_table')).toBeInTheDocument();
417
+ });
418
+
419
+ it('shows short name for metric', () => {
420
+ render(
421
+ <MetricFlowGraph
422
+ grainGroups={mockGrainGroups}
423
+ metricFormulas={mockMetricFormulas}
424
+ onNodeSelect={jest.fn()}
425
+ />,
426
+ );
427
+
428
+ // Should show short_name
429
+ expect(screen.getByText('num_repair_orders')).toBeInTheDocument();
430
+ expect(screen.getByText('avg_repair_price')).toBeInTheDocument();
431
+ });
432
+ });