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,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
|
+
});
|