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,429 @@
1
+ import { render, screen, fireEvent } from '@testing-library/react';
2
+ import { SelectionPanel } from '../SelectionPanel';
3
+ import React from 'react';
4
+
5
+ const mockMetrics = [
6
+ 'default.num_repair_orders',
7
+ 'default.avg_repair_price',
8
+ 'default.total_repair_cost',
9
+ 'sales.revenue',
10
+ 'sales.order_count',
11
+ 'inventory.stock_level',
12
+ ];
13
+
14
+ const mockDimensions = [
15
+ {
16
+ name: 'default.date_dim.dateint',
17
+ type: 'timestamp',
18
+ path: ['default.orders', 'default.date_dim.dateint'],
19
+ },
20
+ {
21
+ name: 'default.date_dim.month',
22
+ type: 'int',
23
+ path: ['default.orders', 'default.date_dim.month'],
24
+ },
25
+ {
26
+ name: 'default.date_dim.year',
27
+ type: 'int',
28
+ path: ['default.orders', 'default.date_dim.year'],
29
+ },
30
+ {
31
+ name: 'default.customer.country',
32
+ type: 'string',
33
+ path: ['default.orders', 'default.customer.country'],
34
+ },
35
+ ];
36
+
37
+ const defaultProps = {
38
+ metrics: mockMetrics,
39
+ selectedMetrics: [],
40
+ onMetricsChange: jest.fn(),
41
+ dimensions: mockDimensions,
42
+ selectedDimensions: [],
43
+ onDimensionsChange: jest.fn(),
44
+ loading: false,
45
+ };
46
+
47
+ describe('SelectionPanel', () => {
48
+ beforeEach(() => {
49
+ jest.clearAllMocks();
50
+ });
51
+
52
+ describe('Metrics Section', () => {
53
+ it('renders metrics section header', () => {
54
+ render(<SelectionPanel {...defaultProps} />);
55
+ expect(screen.getByText('Metrics')).toBeInTheDocument();
56
+ });
57
+
58
+ it('displays selection count', () => {
59
+ render(
60
+ <SelectionPanel
61
+ {...defaultProps}
62
+ selectedMetrics={['default.num_repair_orders']}
63
+ />,
64
+ );
65
+ expect(screen.getByText('1 selected')).toBeInTheDocument();
66
+ });
67
+
68
+ it('groups metrics by namespace', () => {
69
+ render(<SelectionPanel {...defaultProps} />);
70
+ expect(screen.getByText('default')).toBeInTheDocument();
71
+ expect(screen.getByText('sales')).toBeInTheDocument();
72
+ expect(screen.getByText('inventory')).toBeInTheDocument();
73
+ });
74
+
75
+ it('shows metric count per namespace', () => {
76
+ render(<SelectionPanel {...defaultProps} />);
77
+ // default has 3 metrics
78
+ expect(screen.getByText('3')).toBeInTheDocument();
79
+ });
80
+
81
+ it('expands namespace when clicked', () => {
82
+ render(<SelectionPanel {...defaultProps} />);
83
+
84
+ const defaultNamespace = screen.getByText('default');
85
+ fireEvent.click(defaultNamespace);
86
+
87
+ expect(screen.getByText('num_repair_orders')).toBeInTheDocument();
88
+ expect(screen.getByText('avg_repair_price')).toBeInTheDocument();
89
+ });
90
+
91
+ it('collapses namespace when clicked again', () => {
92
+ render(<SelectionPanel {...defaultProps} />);
93
+
94
+ const defaultNamespace = screen.getByText('default');
95
+ fireEvent.click(defaultNamespace);
96
+ expect(screen.getByText('num_repair_orders')).toBeInTheDocument();
97
+
98
+ fireEvent.click(defaultNamespace);
99
+ expect(screen.queryByText('num_repair_orders')).not.toBeInTheDocument();
100
+ });
101
+
102
+ it('calls onMetricsChange when metric is selected', () => {
103
+ const onMetricsChange = jest.fn();
104
+ render(
105
+ <SelectionPanel {...defaultProps} onMetricsChange={onMetricsChange} />,
106
+ );
107
+
108
+ // Expand namespace first
109
+ fireEvent.click(screen.getByText('default'));
110
+
111
+ // Click checkbox
112
+ const checkbox = screen.getByRole('checkbox', {
113
+ name: /num_repair_orders/i,
114
+ });
115
+ fireEvent.click(checkbox);
116
+
117
+ expect(onMetricsChange).toHaveBeenCalledWith([
118
+ 'default.num_repair_orders',
119
+ ]);
120
+ });
121
+
122
+ it('removes metric when unchecked', () => {
123
+ const onMetricsChange = jest.fn();
124
+ render(
125
+ <SelectionPanel
126
+ {...defaultProps}
127
+ selectedMetrics={['default.num_repair_orders']}
128
+ onMetricsChange={onMetricsChange}
129
+ />,
130
+ );
131
+
132
+ fireEvent.click(screen.getByText('default'));
133
+
134
+ const checkbox = screen.getByRole('checkbox', {
135
+ name: /num_repair_orders/i,
136
+ });
137
+ fireEvent.click(checkbox);
138
+
139
+ expect(onMetricsChange).toHaveBeenCalledWith([]);
140
+ });
141
+ });
142
+
143
+ describe('Metrics Search', () => {
144
+ it('renders search input', () => {
145
+ render(<SelectionPanel {...defaultProps} />);
146
+ expect(
147
+ screen.getByPlaceholderText('Search metrics...'),
148
+ ).toBeInTheDocument();
149
+ });
150
+
151
+ it('filters metrics by search term', () => {
152
+ render(<SelectionPanel {...defaultProps} />);
153
+
154
+ const searchInput = screen.getByPlaceholderText('Search metrics...');
155
+ fireEvent.change(searchInput, { target: { value: 'repair' } });
156
+
157
+ // Should auto-expand and show matching metrics
158
+ expect(screen.getByText('num_repair_orders')).toBeInTheDocument();
159
+ expect(screen.getByText('avg_repair_price')).toBeInTheDocument();
160
+ });
161
+
162
+ it('filters out non-matching metrics', () => {
163
+ render(<SelectionPanel {...defaultProps} />);
164
+
165
+ const searchInput = screen.getByPlaceholderText('Search metrics...');
166
+ fireEvent.change(searchInput, { target: { value: 'revenue' } });
167
+
168
+ // Only sales.revenue should match
169
+ expect(screen.getByText('revenue')).toBeInTheDocument();
170
+ expect(screen.queryByText('num_repair_orders')).not.toBeInTheDocument();
171
+ });
172
+
173
+ it('shows no results message when no metrics match', () => {
174
+ render(<SelectionPanel {...defaultProps} />);
175
+
176
+ const searchInput = screen.getByPlaceholderText('Search metrics...');
177
+ fireEvent.change(searchInput, { target: { value: 'nonexistent' } });
178
+
179
+ expect(
180
+ screen.getByText('No metrics match your search'),
181
+ ).toBeInTheDocument();
182
+ });
183
+
184
+ it('prioritizes prefix matches in search results', () => {
185
+ const metricsWithSimilarNames = [
186
+ 'default.total_orders',
187
+ 'default.orders_total',
188
+ 'default.order_count',
189
+ ];
190
+ render(
191
+ <SelectionPanel {...defaultProps} metrics={metricsWithSimilarNames} />,
192
+ );
193
+
194
+ const searchInput = screen.getByPlaceholderText('Search metrics...');
195
+ fireEvent.change(searchInput, { target: { value: 'order' } });
196
+
197
+ // order_count should appear (prefix match on short name)
198
+ // orders_total should appear (contains 'order')
199
+ const items = screen.getAllByRole('checkbox');
200
+ expect(items.length).toBeGreaterThan(0);
201
+ });
202
+
203
+ it('clears search when clear button is clicked', () => {
204
+ render(<SelectionPanel {...defaultProps} />);
205
+
206
+ const searchInput = screen.getByPlaceholderText('Search metrics...');
207
+ fireEvent.change(searchInput, { target: { value: 'test' } });
208
+
209
+ const clearButton = screen.getAllByText('×')[0];
210
+ fireEvent.click(clearButton);
211
+
212
+ expect(searchInput.value).toBe('');
213
+ });
214
+ });
215
+
216
+ describe('Select All / Clear Actions', () => {
217
+ it('shows Select all and Clear buttons when namespace is expanded', () => {
218
+ render(<SelectionPanel {...defaultProps} />);
219
+
220
+ fireEvent.click(screen.getByText('default'));
221
+
222
+ expect(screen.getByText('Select all')).toBeInTheDocument();
223
+ expect(screen.getByText('Clear')).toBeInTheDocument();
224
+ });
225
+
226
+ it('selects all metrics in namespace when Select all is clicked', () => {
227
+ const onMetricsChange = jest.fn();
228
+ render(
229
+ <SelectionPanel {...defaultProps} onMetricsChange={onMetricsChange} />,
230
+ );
231
+
232
+ fireEvent.click(screen.getByText('default'));
233
+ fireEvent.click(screen.getByText('Select all'));
234
+
235
+ expect(onMetricsChange).toHaveBeenCalledWith([
236
+ 'default.num_repair_orders',
237
+ 'default.avg_repair_price',
238
+ 'default.total_repair_cost',
239
+ ]);
240
+ });
241
+
242
+ it('clears all metrics in namespace when Clear is clicked', () => {
243
+ const onMetricsChange = jest.fn();
244
+ render(
245
+ <SelectionPanel
246
+ {...defaultProps}
247
+ selectedMetrics={[
248
+ 'default.num_repair_orders',
249
+ 'default.avg_repair_price',
250
+ ]}
251
+ onMetricsChange={onMetricsChange}
252
+ />,
253
+ );
254
+
255
+ fireEvent.click(screen.getByText('default'));
256
+ fireEvent.click(screen.getByText('Clear'));
257
+
258
+ expect(onMetricsChange).toHaveBeenCalledWith([]);
259
+ });
260
+ });
261
+
262
+ describe('Dimensions Section', () => {
263
+ it('renders dimensions section header', () => {
264
+ render(<SelectionPanel {...defaultProps} />);
265
+ expect(screen.getByText('Dimensions')).toBeInTheDocument();
266
+ });
267
+
268
+ it('shows hint when no metrics selected', () => {
269
+ render(<SelectionPanel {...defaultProps} selectedMetrics={[]} />);
270
+ expect(
271
+ screen.getByText('Select metrics to see available dimensions'),
272
+ ).toBeInTheDocument();
273
+ });
274
+
275
+ it('shows loading state while fetching dimensions', () => {
276
+ render(
277
+ <SelectionPanel
278
+ {...defaultProps}
279
+ selectedMetrics={['default.test']}
280
+ loading={true}
281
+ />,
282
+ );
283
+ expect(screen.getByText('Loading dimensions...')).toBeInTheDocument();
284
+ });
285
+
286
+ it('displays dimensions when metrics are selected', () => {
287
+ render(
288
+ <SelectionPanel
289
+ {...defaultProps}
290
+ selectedMetrics={['default.num_repair_orders']}
291
+ />,
292
+ );
293
+
294
+ expect(screen.getByText('date_dim.dateint')).toBeInTheDocument();
295
+ expect(screen.getByText('date_dim.month')).toBeInTheDocument();
296
+ });
297
+
298
+ it('calls onDimensionsChange when dimension is selected', () => {
299
+ const onDimensionsChange = jest.fn();
300
+ render(
301
+ <SelectionPanel
302
+ {...defaultProps}
303
+ selectedMetrics={['default.num_repair_orders']}
304
+ onDimensionsChange={onDimensionsChange}
305
+ />,
306
+ );
307
+
308
+ const checkbox = screen.getByRole('checkbox', { name: /dateint/i });
309
+ fireEvent.click(checkbox);
310
+
311
+ expect(onDimensionsChange).toHaveBeenCalledWith([
312
+ 'default.date_dim.dateint',
313
+ ]);
314
+ });
315
+
316
+ it('deduplicates dimensions with same name', () => {
317
+ const duplicateDimensions = [
318
+ { name: 'default.date_dim.month', path: ['path1', 'path2', 'path3'] },
319
+ { name: 'default.date_dim.month', path: ['short', 'path'] },
320
+ ];
321
+ render(
322
+ <SelectionPanel
323
+ {...defaultProps}
324
+ dimensions={duplicateDimensions}
325
+ selectedMetrics={['default.test']}
326
+ />,
327
+ );
328
+
329
+ // Should only show one checkbox for month
330
+ const monthCheckboxes = screen.getAllByRole('checkbox', {
331
+ name: /month/i,
332
+ });
333
+ expect(monthCheckboxes.length).toBe(1);
334
+ });
335
+
336
+ it('shows dimension display name (last 2 segments)', () => {
337
+ render(
338
+ <SelectionPanel
339
+ {...defaultProps}
340
+ selectedMetrics={['default.num_repair_orders']}
341
+ />,
342
+ );
343
+
344
+ // Should show 'date_dim.dateint' not full path
345
+ expect(screen.getByText('date_dim.dateint')).toBeInTheDocument();
346
+ });
347
+ });
348
+
349
+ describe('Dimensions Search', () => {
350
+ it('filters dimensions by search term', () => {
351
+ render(
352
+ <SelectionPanel
353
+ {...defaultProps}
354
+ selectedMetrics={['default.num_repair_orders']}
355
+ />,
356
+ );
357
+
358
+ const searchInput = screen.getByPlaceholderText('Search dimensions...');
359
+ fireEvent.change(searchInput, { target: { value: 'month' } });
360
+
361
+ expect(screen.getByText('date_dim.month')).toBeInTheDocument();
362
+ expect(screen.queryByText('date_dim.year')).not.toBeInTheDocument();
363
+ });
364
+
365
+ it('shows no results message when no dimensions match', () => {
366
+ render(
367
+ <SelectionPanel
368
+ {...defaultProps}
369
+ selectedMetrics={['default.num_repair_orders']}
370
+ />,
371
+ );
372
+
373
+ const searchInput = screen.getByPlaceholderText('Search dimensions...');
374
+ fireEvent.change(searchInput, { target: { value: 'nonexistent' } });
375
+
376
+ expect(
377
+ screen.getByText('No dimensions match your search'),
378
+ ).toBeInTheDocument();
379
+ });
380
+ });
381
+
382
+ describe('Selected State Display', () => {
383
+ it('shows selected badge in namespace when metrics are selected', () => {
384
+ render(
385
+ <SelectionPanel
386
+ {...defaultProps}
387
+ selectedMetrics={[
388
+ 'default.num_repair_orders',
389
+ 'default.avg_repair_price',
390
+ ]}
391
+ />,
392
+ );
393
+
394
+ // Should show '2' in the selected badge
395
+ const selectedBadge = document.querySelector('.selected-badge');
396
+ expect(selectedBadge).toBeInTheDocument();
397
+ expect(selectedBadge).toHaveTextContent('2');
398
+ });
399
+
400
+ it('shows checked state for selected metrics', () => {
401
+ render(
402
+ <SelectionPanel
403
+ {...defaultProps}
404
+ selectedMetrics={['default.num_repair_orders']}
405
+ />,
406
+ );
407
+
408
+ fireEvent.click(screen.getByText('default'));
409
+
410
+ const checkbox = screen.getByRole('checkbox', {
411
+ name: /num_repair_orders/i,
412
+ });
413
+ expect(checkbox).toBeChecked();
414
+ });
415
+
416
+ it('shows checked state for selected dimensions', () => {
417
+ render(
418
+ <SelectionPanel
419
+ {...defaultProps}
420
+ selectedMetrics={['default.test']}
421
+ selectedDimensions={['default.date_dim.dateint']}
422
+ />,
423
+ );
424
+
425
+ const checkbox = screen.getByRole('checkbox', { name: /dateint/i });
426
+ expect(checkbox).toBeChecked();
427
+ });
428
+ });
429
+ });