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.
Files changed (25) hide show
  1. package/package.json +11 -4
  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,638 @@
1
+ import { render, screen, fireEvent } from '@testing-library/react';
2
+ import { MemoryRouter } from 'react-router-dom';
3
+ import {
4
+ QueryOverviewPanel,
5
+ PreAggDetailsPanel,
6
+ MetricDetailsPanel,
7
+ } from '../PreAggDetailsPanel';
8
+ import React from 'react';
9
+
10
+ // Mock the syntax highlighter to avoid issues with CSS imports
11
+ jest.mock('react-syntax-highlighter', () => ({
12
+ Light: ({ children }) => (
13
+ <pre data-testid="syntax-highlighter">{children}</pre>
14
+ ),
15
+ }));
16
+
17
+ jest.mock('react-syntax-highlighter/src/styles/hljs', () => ({
18
+ atomOneLight: {},
19
+ }));
20
+
21
+ const mockMeasuresResult = {
22
+ grain_groups: [
23
+ {
24
+ parent_name: 'default.repair_orders',
25
+ aggregability: 'FULL',
26
+ grain: ['date_id', 'customer_id'],
27
+ components: [
28
+ {
29
+ name: 'sum_revenue',
30
+ expression: 'SUM(revenue)',
31
+ aggregation: 'SUM',
32
+ merge: 'SUM',
33
+ },
34
+ {
35
+ name: 'count_orders',
36
+ expression: 'COUNT(*)',
37
+ aggregation: 'COUNT',
38
+ merge: 'SUM',
39
+ },
40
+ ],
41
+ sql: 'SELECT date_id, customer_id, SUM(revenue) FROM orders GROUP BY 1, 2',
42
+ },
43
+ {
44
+ parent_name: 'inventory.stock',
45
+ aggregability: 'LIMITED',
46
+ grain: ['warehouse_id'],
47
+ components: [
48
+ {
49
+ name: 'sum_quantity',
50
+ expression: 'SUM(quantity)',
51
+ aggregation: 'SUM',
52
+ merge: 'SUM',
53
+ },
54
+ ],
55
+ },
56
+ ],
57
+ metric_formulas: [
58
+ {
59
+ name: 'default.num_repair_orders',
60
+ short_name: 'num_repair_orders',
61
+ combiner: 'SUM(count_orders)',
62
+ is_derived: false,
63
+ components: ['count_orders'],
64
+ },
65
+ {
66
+ name: 'default.avg_repair_price',
67
+ short_name: 'avg_repair_price',
68
+ combiner: 'SUM(sum_revenue) / SUM(count_orders)',
69
+ is_derived: true,
70
+ components: ['sum_revenue', 'count_orders'],
71
+ },
72
+ ],
73
+ };
74
+
75
+ const mockMetricsResult = {
76
+ sql: 'SELECT date_id, SUM(revenue) as total_revenue FROM orders GROUP BY 1',
77
+ };
78
+
79
+ const renderWithRouter = component => {
80
+ return render(<MemoryRouter>{component}</MemoryRouter>);
81
+ };
82
+
83
+ describe('QueryOverviewPanel', () => {
84
+ const defaultProps = {
85
+ measuresResult: mockMeasuresResult,
86
+ metricsResult: mockMetricsResult,
87
+ selectedMetrics: ['default.num_repair_orders', 'default.avg_repair_price'],
88
+ selectedDimensions: [
89
+ 'default.date_dim.dateint',
90
+ 'default.customer.country',
91
+ ],
92
+ };
93
+
94
+ describe('Empty States', () => {
95
+ it('shows hint when no metrics selected', () => {
96
+ renderWithRouter(
97
+ <QueryOverviewPanel
98
+ {...defaultProps}
99
+ selectedMetrics={[]}
100
+ selectedDimensions={[]}
101
+ />,
102
+ );
103
+ expect(screen.getByText('Query Planner')).toBeInTheDocument();
104
+ expect(
105
+ screen.getByText(/Select metrics and dimensions/),
106
+ ).toBeInTheDocument();
107
+ });
108
+
109
+ it('shows loading state when results are pending', () => {
110
+ renderWithRouter(
111
+ <QueryOverviewPanel
112
+ {...defaultProps}
113
+ measuresResult={null}
114
+ metricsResult={null}
115
+ />,
116
+ );
117
+ expect(screen.getByText('Building query plan...')).toBeInTheDocument();
118
+ });
119
+ });
120
+
121
+ describe('Header', () => {
122
+ it('renders the overview header', () => {
123
+ renderWithRouter(<QueryOverviewPanel {...defaultProps} />);
124
+ expect(screen.getByText('Generated Query Overview')).toBeInTheDocument();
125
+ });
126
+
127
+ it('shows metric and dimension counts', () => {
128
+ renderWithRouter(<QueryOverviewPanel {...defaultProps} />);
129
+ expect(screen.getByText('2 metrics × 2 dimensions')).toBeInTheDocument();
130
+ });
131
+ });
132
+
133
+ describe('Pre-Aggregations Summary', () => {
134
+ it('displays pre-aggregations section', () => {
135
+ renderWithRouter(<QueryOverviewPanel {...defaultProps} />);
136
+ expect(screen.getByText(/Pre-Aggregations/)).toBeInTheDocument();
137
+ });
138
+
139
+ it('shows correct count of pre-aggregations', () => {
140
+ renderWithRouter(<QueryOverviewPanel {...defaultProps} />);
141
+ expect(screen.getByText('Pre-Aggregations (2)')).toBeInTheDocument();
142
+ });
143
+
144
+ it('displays pre-agg source names', () => {
145
+ renderWithRouter(<QueryOverviewPanel {...defaultProps} />);
146
+ expect(screen.getByText('repair_orders')).toBeInTheDocument();
147
+ expect(screen.getByText('stock')).toBeInTheDocument();
148
+ });
149
+
150
+ it('shows aggregability badge', () => {
151
+ renderWithRouter(<QueryOverviewPanel {...defaultProps} />);
152
+ expect(screen.getByText('FULL')).toBeInTheDocument();
153
+ expect(screen.getByText('LIMITED')).toBeInTheDocument();
154
+ });
155
+
156
+ it('displays grain columns', () => {
157
+ renderWithRouter(<QueryOverviewPanel {...defaultProps} />);
158
+ expect(screen.getByText('date_id, customer_id')).toBeInTheDocument();
159
+ expect(screen.getByText('warehouse_id')).toBeInTheDocument();
160
+ });
161
+
162
+ it('shows materialization status', () => {
163
+ renderWithRouter(<QueryOverviewPanel {...defaultProps} />);
164
+ expect(screen.getAllByText('Not materialized').length).toBe(2);
165
+ });
166
+ });
167
+
168
+ describe('Metrics Summary', () => {
169
+ it('displays metrics section', () => {
170
+ renderWithRouter(<QueryOverviewPanel {...defaultProps} />);
171
+ expect(screen.getByText(/Metrics \(2\)/)).toBeInTheDocument();
172
+ });
173
+
174
+ it('shows metric short names', () => {
175
+ renderWithRouter(<QueryOverviewPanel {...defaultProps} />);
176
+ expect(screen.getByText('num_repair_orders')).toBeInTheDocument();
177
+ expect(screen.getByText('avg_repair_price')).toBeInTheDocument();
178
+ });
179
+
180
+ it('shows derived badge for derived metrics', () => {
181
+ renderWithRouter(<QueryOverviewPanel {...defaultProps} />);
182
+ expect(screen.getByText('Derived')).toBeInTheDocument();
183
+ });
184
+
185
+ it('renders metric links', () => {
186
+ renderWithRouter(<QueryOverviewPanel {...defaultProps} />);
187
+ const links = screen.getAllByRole('link');
188
+ expect(
189
+ links.some(
190
+ link =>
191
+ link.getAttribute('href') === '/nodes/default.num_repair_orders',
192
+ ),
193
+ ).toBe(true);
194
+ });
195
+ });
196
+
197
+ describe('Dimensions Summary', () => {
198
+ it('displays dimensions section', () => {
199
+ renderWithRouter(<QueryOverviewPanel {...defaultProps} />);
200
+ expect(screen.getByText(/Dimensions \(2\)/)).toBeInTheDocument();
201
+ });
202
+
203
+ it('shows dimension short names', () => {
204
+ renderWithRouter(<QueryOverviewPanel {...defaultProps} />);
205
+ expect(screen.getByText('dateint')).toBeInTheDocument();
206
+ expect(screen.getByText('country')).toBeInTheDocument();
207
+ });
208
+
209
+ it('renders dimension links', () => {
210
+ renderWithRouter(<QueryOverviewPanel {...defaultProps} />);
211
+ const links = screen.getAllByRole('link');
212
+ expect(
213
+ links.some(
214
+ link => link.getAttribute('href') === '/nodes/default.date_dim',
215
+ ),
216
+ ).toBe(true);
217
+ });
218
+ });
219
+
220
+ describe('SQL Section', () => {
221
+ it('displays generated SQL section', () => {
222
+ renderWithRouter(<QueryOverviewPanel {...defaultProps} />);
223
+ expect(screen.getByText('Generated SQL')).toBeInTheDocument();
224
+ });
225
+
226
+ it('shows copy SQL button', () => {
227
+ renderWithRouter(<QueryOverviewPanel {...defaultProps} />);
228
+ expect(screen.getByText('Copy SQL')).toBeInTheDocument();
229
+ });
230
+
231
+ it('renders SQL in syntax highlighter', () => {
232
+ renderWithRouter(<QueryOverviewPanel {...defaultProps} />);
233
+ expect(screen.getByTestId('syntax-highlighter')).toBeInTheDocument();
234
+ expect(screen.getByText(mockMetricsResult.sql)).toBeInTheDocument();
235
+ });
236
+
237
+ it('copies SQL to clipboard when copy button clicked', () => {
238
+ const mockClipboard = { writeText: jest.fn() };
239
+ Object.assign(navigator, { clipboard: mockClipboard });
240
+
241
+ renderWithRouter(<QueryOverviewPanel {...defaultProps} />);
242
+
243
+ fireEvent.click(screen.getByText('Copy SQL'));
244
+ expect(mockClipboard.writeText).toHaveBeenCalledWith(
245
+ mockMetricsResult.sql,
246
+ );
247
+ });
248
+ });
249
+ });
250
+
251
+ describe('PreAggDetailsPanel', () => {
252
+ const mockPreAgg = {
253
+ parent_name: 'default.repair_orders',
254
+ aggregability: 'FULL',
255
+ grain: ['date_id', 'customer_id'],
256
+ components: [
257
+ {
258
+ name: 'sum_revenue',
259
+ expression: 'SUM(revenue)',
260
+ aggregation: 'SUM',
261
+ merge: 'SUM',
262
+ },
263
+ {
264
+ name: 'count_orders',
265
+ expression: 'COUNT(*)',
266
+ aggregation: 'COUNT',
267
+ merge: 'SUM',
268
+ },
269
+ ],
270
+ sql: 'SELECT date_id, customer_id, SUM(revenue) FROM orders GROUP BY 1, 2',
271
+ };
272
+
273
+ const mockMetricFormulas = [
274
+ {
275
+ name: 'default.total_revenue',
276
+ short_name: 'total_revenue',
277
+ combiner: 'SUM(sum_revenue)',
278
+ is_derived: false,
279
+ components: ['sum_revenue'],
280
+ },
281
+ ];
282
+
283
+ const onClose = jest.fn();
284
+
285
+ beforeEach(() => {
286
+ jest.clearAllMocks();
287
+ });
288
+
289
+ it('returns null when no preAgg provided', () => {
290
+ const { container } = render(
291
+ <PreAggDetailsPanel preAgg={null} onClose={onClose} />,
292
+ );
293
+ expect(container.firstChild).toBeNull();
294
+ });
295
+
296
+ it('renders pre-aggregation badge', () => {
297
+ render(
298
+ <PreAggDetailsPanel
299
+ preAgg={mockPreAgg}
300
+ metricFormulas={mockMetricFormulas}
301
+ onClose={onClose}
302
+ />,
303
+ );
304
+ expect(screen.getByText('Pre-aggregation')).toBeInTheDocument();
305
+ });
306
+
307
+ it('displays source name', () => {
308
+ render(
309
+ <PreAggDetailsPanel
310
+ preAgg={mockPreAgg}
311
+ metricFormulas={mockMetricFormulas}
312
+ onClose={onClose}
313
+ />,
314
+ );
315
+ expect(screen.getByText('repair_orders')).toBeInTheDocument();
316
+ expect(screen.getByText('default.repair_orders')).toBeInTheDocument();
317
+ });
318
+
319
+ it('displays close button', () => {
320
+ render(
321
+ <PreAggDetailsPanel
322
+ preAgg={mockPreAgg}
323
+ metricFormulas={mockMetricFormulas}
324
+ onClose={onClose}
325
+ />,
326
+ );
327
+ expect(screen.getByTitle('Close panel')).toBeInTheDocument();
328
+ });
329
+
330
+ it('calls onClose when close button clicked', () => {
331
+ render(
332
+ <PreAggDetailsPanel
333
+ preAgg={mockPreAgg}
334
+ metricFormulas={mockMetricFormulas}
335
+ onClose={onClose}
336
+ />,
337
+ );
338
+ fireEvent.click(screen.getByTitle('Close panel'));
339
+ expect(onClose).toHaveBeenCalled();
340
+ });
341
+
342
+ describe('Grain Section', () => {
343
+ it('displays grain section', () => {
344
+ render(
345
+ <PreAggDetailsPanel
346
+ preAgg={mockPreAgg}
347
+ metricFormulas={mockMetricFormulas}
348
+ onClose={onClose}
349
+ />,
350
+ );
351
+ expect(screen.getByText('Grain (GROUP BY)')).toBeInTheDocument();
352
+ });
353
+
354
+ it('shows grain columns as pills', () => {
355
+ render(
356
+ <PreAggDetailsPanel
357
+ preAgg={mockPreAgg}
358
+ metricFormulas={mockMetricFormulas}
359
+ onClose={onClose}
360
+ />,
361
+ );
362
+ expect(screen.getByText('date_id')).toBeInTheDocument();
363
+ expect(screen.getByText('customer_id')).toBeInTheDocument();
364
+ });
365
+
366
+ it('shows empty message when no grain', () => {
367
+ const noGrainPreAgg = { ...mockPreAgg, grain: [] };
368
+ render(
369
+ <PreAggDetailsPanel
370
+ preAgg={noGrainPreAgg}
371
+ metricFormulas={mockMetricFormulas}
372
+ onClose={onClose}
373
+ />,
374
+ );
375
+ expect(screen.getByText('No grain columns')).toBeInTheDocument();
376
+ });
377
+ });
378
+
379
+ describe('Related Metrics Section', () => {
380
+ it('displays metrics using this section', () => {
381
+ render(
382
+ <PreAggDetailsPanel
383
+ preAgg={mockPreAgg}
384
+ metricFormulas={mockMetricFormulas}
385
+ onClose={onClose}
386
+ />,
387
+ );
388
+ expect(screen.getByText('Metrics Using This')).toBeInTheDocument();
389
+ });
390
+
391
+ it('shows related metrics', () => {
392
+ render(
393
+ <PreAggDetailsPanel
394
+ preAgg={mockPreAgg}
395
+ metricFormulas={mockMetricFormulas}
396
+ onClose={onClose}
397
+ />,
398
+ );
399
+ expect(screen.getByText('total_revenue')).toBeInTheDocument();
400
+ });
401
+ });
402
+
403
+ describe('Components Table', () => {
404
+ it('displays components section', () => {
405
+ render(
406
+ <PreAggDetailsPanel
407
+ preAgg={mockPreAgg}
408
+ metricFormulas={mockMetricFormulas}
409
+ onClose={onClose}
410
+ />,
411
+ );
412
+ expect(screen.getByText('Components (2)')).toBeInTheDocument();
413
+ });
414
+
415
+ it('shows component names', () => {
416
+ render(
417
+ <PreAggDetailsPanel
418
+ preAgg={mockPreAgg}
419
+ metricFormulas={mockMetricFormulas}
420
+ onClose={onClose}
421
+ />,
422
+ );
423
+ expect(screen.getByText('sum_revenue')).toBeInTheDocument();
424
+ expect(screen.getByText('count_orders')).toBeInTheDocument();
425
+ });
426
+
427
+ it('shows component expressions', () => {
428
+ render(
429
+ <PreAggDetailsPanel
430
+ preAgg={mockPreAgg}
431
+ metricFormulas={mockMetricFormulas}
432
+ onClose={onClose}
433
+ />,
434
+ );
435
+ expect(screen.getByText('SUM(revenue)')).toBeInTheDocument();
436
+ expect(screen.getByText('COUNT(*)')).toBeInTheDocument();
437
+ });
438
+
439
+ it('shows aggregation functions', () => {
440
+ render(
441
+ <PreAggDetailsPanel
442
+ preAgg={mockPreAgg}
443
+ metricFormulas={mockMetricFormulas}
444
+ onClose={onClose}
445
+ />,
446
+ );
447
+ expect(screen.getAllByText('SUM').length).toBeGreaterThan(0);
448
+ expect(screen.getByText('COUNT')).toBeInTheDocument();
449
+ });
450
+ });
451
+
452
+ describe('SQL Section', () => {
453
+ it('displays SQL section when sql is present', () => {
454
+ render(
455
+ <PreAggDetailsPanel
456
+ preAgg={mockPreAgg}
457
+ metricFormulas={mockMetricFormulas}
458
+ onClose={onClose}
459
+ />,
460
+ );
461
+ expect(screen.getByText('Pre-Aggregation SQL')).toBeInTheDocument();
462
+ });
463
+
464
+ it('shows copy button', () => {
465
+ render(
466
+ <PreAggDetailsPanel
467
+ preAgg={mockPreAgg}
468
+ metricFormulas={mockMetricFormulas}
469
+ onClose={onClose}
470
+ />,
471
+ );
472
+ expect(screen.getByText('Copy SQL')).toBeInTheDocument();
473
+ });
474
+ });
475
+ });
476
+
477
+ describe('MetricDetailsPanel', () => {
478
+ const mockMetric = {
479
+ name: 'default.avg_repair_price',
480
+ short_name: 'avg_repair_price',
481
+ combiner: 'SUM(sum_revenue) / SUM(count_orders)',
482
+ is_derived: true,
483
+ components: ['sum_revenue', 'count_orders'],
484
+ };
485
+
486
+ const mockGrainGroups = [
487
+ {
488
+ parent_name: 'default.repair_orders',
489
+ components: [{ name: 'sum_revenue' }, { name: 'count_orders' }],
490
+ },
491
+ ];
492
+
493
+ const onClose = jest.fn();
494
+
495
+ beforeEach(() => {
496
+ jest.clearAllMocks();
497
+ });
498
+
499
+ it('returns null when no metric provided', () => {
500
+ const { container } = render(
501
+ <MetricDetailsPanel metric={null} onClose={onClose} />,
502
+ );
503
+ expect(container.firstChild).toBeNull();
504
+ });
505
+
506
+ it('renders metric badge', () => {
507
+ render(
508
+ <MetricDetailsPanel
509
+ metric={mockMetric}
510
+ grainGroups={mockGrainGroups}
511
+ onClose={onClose}
512
+ />,
513
+ );
514
+ expect(screen.getByText('Derived Metric')).toBeInTheDocument();
515
+ });
516
+
517
+ it('renders regular metric badge for non-derived', () => {
518
+ const nonDerivedMetric = { ...mockMetric, is_derived: false };
519
+ render(
520
+ <MetricDetailsPanel
521
+ metric={nonDerivedMetric}
522
+ grainGroups={mockGrainGroups}
523
+ onClose={onClose}
524
+ />,
525
+ );
526
+ expect(screen.getByText('Metric')).toBeInTheDocument();
527
+ });
528
+
529
+ it('displays metric name', () => {
530
+ render(
531
+ <MetricDetailsPanel
532
+ metric={mockMetric}
533
+ grainGroups={mockGrainGroups}
534
+ onClose={onClose}
535
+ />,
536
+ );
537
+ expect(screen.getByText('avg_repair_price')).toBeInTheDocument();
538
+ expect(screen.getByText('default.avg_repair_price')).toBeInTheDocument();
539
+ });
540
+
541
+ it('calls onClose when close button clicked', () => {
542
+ render(
543
+ <MetricDetailsPanel
544
+ metric={mockMetric}
545
+ grainGroups={mockGrainGroups}
546
+ onClose={onClose}
547
+ />,
548
+ );
549
+ fireEvent.click(screen.getByTitle('Close panel'));
550
+ expect(onClose).toHaveBeenCalled();
551
+ });
552
+
553
+ describe('Formula Section', () => {
554
+ it('displays combiner formula section', () => {
555
+ render(
556
+ <MetricDetailsPanel
557
+ metric={mockMetric}
558
+ grainGroups={mockGrainGroups}
559
+ onClose={onClose}
560
+ />,
561
+ );
562
+ expect(screen.getByText('Combiner Formula')).toBeInTheDocument();
563
+ });
564
+
565
+ it('shows the formula', () => {
566
+ render(
567
+ <MetricDetailsPanel
568
+ metric={mockMetric}
569
+ grainGroups={mockGrainGroups}
570
+ onClose={onClose}
571
+ />,
572
+ );
573
+ expect(
574
+ screen.getByText('SUM(sum_revenue) / SUM(count_orders)'),
575
+ ).toBeInTheDocument();
576
+ });
577
+ });
578
+
579
+ describe('Components Section', () => {
580
+ it('displays components used section', () => {
581
+ render(
582
+ <MetricDetailsPanel
583
+ metric={mockMetric}
584
+ grainGroups={mockGrainGroups}
585
+ onClose={onClose}
586
+ />,
587
+ );
588
+ expect(screen.getByText('Components Used')).toBeInTheDocument();
589
+ });
590
+
591
+ it('shows component tags', () => {
592
+ render(
593
+ <MetricDetailsPanel
594
+ metric={mockMetric}
595
+ grainGroups={mockGrainGroups}
596
+ onClose={onClose}
597
+ />,
598
+ );
599
+ expect(screen.getByText('sum_revenue')).toBeInTheDocument();
600
+ expect(screen.getByText('count_orders')).toBeInTheDocument();
601
+ });
602
+ });
603
+
604
+ describe('Source Pre-aggregations Section', () => {
605
+ it('displays source pre-aggregations section', () => {
606
+ render(
607
+ <MetricDetailsPanel
608
+ metric={mockMetric}
609
+ grainGroups={mockGrainGroups}
610
+ onClose={onClose}
611
+ />,
612
+ );
613
+ expect(screen.getByText('Source Pre-aggregations')).toBeInTheDocument();
614
+ });
615
+
616
+ it('shows related pre-agg sources', () => {
617
+ render(
618
+ <MetricDetailsPanel
619
+ metric={mockMetric}
620
+ grainGroups={mockGrainGroups}
621
+ onClose={onClose}
622
+ />,
623
+ );
624
+ expect(screen.getByText('repair_orders')).toBeInTheDocument();
625
+ });
626
+
627
+ it('shows empty message when no sources found', () => {
628
+ render(
629
+ <MetricDetailsPanel
630
+ metric={mockMetric}
631
+ grainGroups={[]}
632
+ onClose={onClose}
633
+ />,
634
+ );
635
+ expect(screen.getByText('No source found')).toBeInTheDocument();
636
+ });
637
+ });
638
+ });