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
@@ -1,6 +1,7 @@
1
1
  import { fireEvent, render, screen, waitFor } from '@testing-library/react';
2
2
  import { MemoryRouter, Route, Routes } from 'react-router-dom';
3
3
  import DJClientContext from '../../../providers/djclient';
4
+ import UserContext from '../../../providers/UserProvider';
4
5
  import { NamespacePage } from '../index';
5
6
  import React from 'react';
6
7
  import userEvent from '@testing-library/user-event';
@@ -15,6 +16,25 @@ const mockDjClient = {
15
16
  listTags: jest.fn(),
16
17
  };
17
18
 
19
+ const mockCurrentUser = { username: 'dj', email: 'dj@test.com' };
20
+
21
+ const renderWithProviders = (ui, { route = '/namespaces/default' } = {}) => {
22
+ return render(
23
+ <UserContext.Provider
24
+ value={{ currentUser: mockCurrentUser, loading: false }}
25
+ >
26
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
27
+ <MemoryRouter initialEntries={[route]}>
28
+ <Routes>
29
+ <Route path="namespaces/:namespace" element={ui} />
30
+ <Route path="/" element={ui} />
31
+ </Routes>
32
+ </MemoryRouter>
33
+ </DJClientContext.Provider>
34
+ </UserContext.Provider>,
35
+ );
36
+ };
37
+
18
38
  describe('NamespacePage', () => {
19
39
  const original = window.location;
20
40
 
@@ -164,38 +184,68 @@ describe('NamespacePage', () => {
164
184
 
165
185
  // --- Sorting ---
166
186
 
187
+ // Track current call count
188
+ const initialCallCount = mockDjClient.listNodesForLanding.mock.calls.length;
189
+
167
190
  // sort by 'name'
168
191
  fireEvent.click(screen.getByText('name'));
169
192
  await waitFor(() => {
170
- expect(mockDjClient.listNodesForLanding).toHaveBeenCalledTimes(2);
193
+ expect(
194
+ mockDjClient.listNodesForLanding.mock.calls.length,
195
+ ).toBeGreaterThan(initialCallCount);
171
196
  });
172
197
 
198
+ const afterFirstSort = mockDjClient.listNodesForLanding.mock.calls.length;
199
+
173
200
  // flip direction
174
201
  fireEvent.click(screen.getByText('name'));
175
202
  await waitFor(() => {
176
- expect(mockDjClient.listNodesForLanding).toHaveBeenCalledTimes(3);
203
+ expect(
204
+ mockDjClient.listNodesForLanding.mock.calls.length,
205
+ ).toBeGreaterThan(afterFirstSort);
177
206
  });
178
207
 
208
+ const afterSecondSort = mockDjClient.listNodesForLanding.mock.calls.length;
209
+
179
210
  // sort by 'displayName'
180
211
  fireEvent.click(screen.getByText('display Name'));
181
212
  await waitFor(() => {
182
- expect(mockDjClient.listNodesForLanding).toHaveBeenCalledTimes(4);
213
+ expect(
214
+ mockDjClient.listNodesForLanding.mock.calls.length,
215
+ ).toBeGreaterThan(afterSecondSort);
183
216
  });
184
217
 
185
218
  // --- Filters ---
186
219
 
187
- // Node type
220
+ // Node type - use react-select properly
188
221
  const selectNodeType = screen.getAllByTestId('select-node-type')[0];
189
- fireEvent.keyDown(selectNodeType.firstChild, { key: 'ArrowDown' });
190
- fireEvent.click(screen.getByText('Source'));
222
+ const typeInput = selectNodeType.querySelector('input');
223
+ if (typeInput) {
224
+ fireEvent.focus(typeInput);
225
+ fireEvent.keyDown(typeInput, { key: 'ArrowDown' });
226
+ await waitFor(() => {
227
+ const sourceOption = screen.queryByText('Source');
228
+ if (sourceOption) {
229
+ fireEvent.click(sourceOption);
230
+ }
231
+ });
232
+ }
191
233
 
192
234
  // Tag filter
193
235
  const selectTag = screen.getAllByTestId('select-tag')[0];
194
- fireEvent.keyDown(selectTag.firstChild, { key: 'ArrowDown' });
236
+ const tagInput = selectTag.querySelector('input');
237
+ if (tagInput) {
238
+ fireEvent.focus(tagInput);
239
+ fireEvent.keyDown(tagInput, { key: 'ArrowDown' });
240
+ }
195
241
 
196
242
  // User filter
197
243
  const selectUser = screen.getAllByTestId('select-user')[0];
198
- fireEvent.keyDown(selectUser.firstChild, { key: 'ArrowDown' });
244
+ const userInput = selectUser.querySelector('input');
245
+ if (userInput) {
246
+ fireEvent.focus(userInput);
247
+ fireEvent.keyDown(userInput, { key: 'ArrowDown' });
248
+ }
199
249
 
200
250
  // --- Expand/Collapse Namespace ---
201
251
  fireEvent.click(screen.getByText('common'));
@@ -328,4 +378,243 @@ describe('NamespacePage', () => {
328
378
  expect(screen.getByText('you failed')).toBeInTheDocument();
329
379
  });
330
380
  });
381
+
382
+ describe('Filter Bar', () => {
383
+ it('displays quick filter presets', async () => {
384
+ renderWithProviders(<NamespacePage />);
385
+
386
+ await waitFor(() => {
387
+ expect(screen.getByText('Quick:')).toBeInTheDocument();
388
+ });
389
+
390
+ // Check that preset buttons are rendered
391
+ expect(screen.getByText('My Nodes')).toBeInTheDocument();
392
+ expect(screen.getByText('Needs Attention')).toBeInTheDocument();
393
+ expect(screen.getByText('Drafts')).toBeInTheDocument();
394
+ });
395
+
396
+ it('applies My Nodes preset when clicked', async () => {
397
+ renderWithProviders(<NamespacePage />);
398
+
399
+ await waitFor(() => {
400
+ expect(screen.getByText('My Nodes')).toBeInTheDocument();
401
+ });
402
+
403
+ const initialCalls = mockDjClient.listNodesForLanding.mock.calls.length;
404
+ fireEvent.click(screen.getByText('My Nodes'));
405
+
406
+ await waitFor(() => {
407
+ // The API should be called again after clicking preset
408
+ expect(
409
+ mockDjClient.listNodesForLanding.mock.calls.length,
410
+ ).toBeGreaterThan(initialCalls);
411
+ });
412
+ });
413
+
414
+ it('applies Needs Attention preset when clicked', async () => {
415
+ renderWithProviders(<NamespacePage />);
416
+
417
+ await waitFor(() => {
418
+ expect(screen.getByText('Needs Attention')).toBeInTheDocument();
419
+ });
420
+
421
+ const initialCalls = mockDjClient.listNodesForLanding.mock.calls.length;
422
+ fireEvent.click(screen.getByText('Needs Attention'));
423
+
424
+ await waitFor(() => {
425
+ expect(
426
+ mockDjClient.listNodesForLanding.mock.calls.length,
427
+ ).toBeGreaterThan(initialCalls);
428
+ });
429
+ });
430
+
431
+ it('applies Drafts preset when clicked', async () => {
432
+ renderWithProviders(<NamespacePage />);
433
+
434
+ await waitFor(() => {
435
+ expect(screen.getByText('Drafts')).toBeInTheDocument();
436
+ });
437
+
438
+ const initialCalls = mockDjClient.listNodesForLanding.mock.calls.length;
439
+ fireEvent.click(screen.getByText('Drafts'));
440
+
441
+ await waitFor(() => {
442
+ expect(
443
+ mockDjClient.listNodesForLanding.mock.calls.length,
444
+ ).toBeGreaterThan(initialCalls);
445
+ });
446
+ });
447
+
448
+ it('shows Clear all button when filters are active', async () => {
449
+ renderWithProviders(<NamespacePage />);
450
+
451
+ await waitFor(() => {
452
+ expect(screen.getByText('My Nodes')).toBeInTheDocument();
453
+ });
454
+
455
+ // Apply a preset to activate filters
456
+ fireEvent.click(screen.getByText('My Nodes'));
457
+
458
+ await waitFor(() => {
459
+ expect(screen.getByText('Clear all ×')).toBeInTheDocument();
460
+ });
461
+ });
462
+
463
+ it('clears all filters when Clear all is clicked', async () => {
464
+ renderWithProviders(<NamespacePage />);
465
+
466
+ await waitFor(() => {
467
+ expect(screen.getByText('My Nodes')).toBeInTheDocument();
468
+ });
469
+
470
+ // Apply a preset
471
+ fireEvent.click(screen.getByText('My Nodes'));
472
+
473
+ await waitFor(() => {
474
+ expect(screen.getByText('Clear all ×')).toBeInTheDocument();
475
+ });
476
+
477
+ // Clear all filters
478
+ fireEvent.click(screen.getByText('Clear all ×'));
479
+
480
+ await waitFor(() => {
481
+ // Clear all button should disappear
482
+ expect(screen.queryByText('Clear all ×')).not.toBeInTheDocument();
483
+ });
484
+ });
485
+
486
+ it('displays filter dropdowns', async () => {
487
+ renderWithProviders(<NamespacePage />);
488
+
489
+ await waitFor(() => {
490
+ // Check for filter labels
491
+ expect(screen.getByText('Type')).toBeInTheDocument();
492
+ expect(screen.getByText('Tags')).toBeInTheDocument();
493
+ expect(screen.getByText('Edited By')).toBeInTheDocument();
494
+ expect(screen.getByText('Mode')).toBeInTheDocument();
495
+ expect(screen.getByText('Owner')).toBeInTheDocument();
496
+ expect(screen.getByText('Status')).toBeInTheDocument();
497
+ expect(screen.getByText('Quality')).toBeInTheDocument();
498
+ });
499
+ });
500
+
501
+ it('opens Quality dropdown when clicked', async () => {
502
+ renderWithProviders(<NamespacePage />);
503
+
504
+ await waitFor(() => {
505
+ expect(screen.getByText('Quality')).toBeInTheDocument();
506
+ });
507
+
508
+ // Find and click the Quality button
509
+ const qualityButton = screen.getByText('Issues');
510
+ fireEvent.click(qualityButton);
511
+
512
+ await waitFor(() => {
513
+ expect(screen.getByText('Missing Description')).toBeInTheDocument();
514
+ expect(screen.getByText('Orphaned Dimensions')).toBeInTheDocument();
515
+ expect(screen.getByText('Has Materialization')).toBeInTheDocument();
516
+ });
517
+ });
518
+
519
+ it('toggles quality filters in dropdown', async () => {
520
+ renderWithProviders(<NamespacePage />);
521
+
522
+ await waitFor(() => {
523
+ expect(screen.getByText('Quality')).toBeInTheDocument();
524
+ });
525
+
526
+ // Open the Quality dropdown
527
+ const qualityButton = screen.getByText('Issues');
528
+ fireEvent.click(qualityButton);
529
+
530
+ await waitFor(() => {
531
+ expect(screen.getByText('Missing Description')).toBeInTheDocument();
532
+ });
533
+
534
+ // Toggle the Missing Description checkbox
535
+ const checkbox = screen.getByLabelText('Missing Description');
536
+ const callsBefore = mockDjClient.listNodesForLanding.mock.calls.length;
537
+ fireEvent.click(checkbox);
538
+
539
+ await waitFor(() => {
540
+ expect(
541
+ mockDjClient.listNodesForLanding.mock.calls.length,
542
+ ).toBeGreaterThan(callsBefore);
543
+ });
544
+ });
545
+
546
+ it('displays no nodes message with clear filter link when no results', async () => {
547
+ mockDjClient.listNodesForLanding.mockResolvedValue({
548
+ data: {
549
+ findNodesPaginated: {
550
+ pageInfo: {
551
+ hasNextPage: false,
552
+ endCursor: null,
553
+ hasPrevPage: false,
554
+ startCursor: null,
555
+ },
556
+ edges: [],
557
+ },
558
+ },
559
+ });
560
+
561
+ renderWithProviders(<NamespacePage />);
562
+
563
+ // Apply a filter first
564
+ await waitFor(() => {
565
+ expect(screen.getByText('My Nodes')).toBeInTheDocument();
566
+ });
567
+ fireEvent.click(screen.getByText('My Nodes'));
568
+
569
+ await waitFor(() => {
570
+ expect(
571
+ screen.getByText('No nodes found with the current filters.'),
572
+ ).toBeInTheDocument();
573
+ expect(screen.getByText('Clear filters')).toBeInTheDocument();
574
+ });
575
+ });
576
+ });
577
+
578
+ describe('URL Parameter Sync', () => {
579
+ it('reads filters from URL parameters on load', async () => {
580
+ renderWithProviders(<NamespacePage />, {
581
+ route: '/namespaces/default?type=metric&ownedBy=dj',
582
+ });
583
+
584
+ await waitFor(() => {
585
+ expect(mockDjClient.listNodesForLanding).toHaveBeenCalled();
586
+ });
587
+ });
588
+
589
+ it('reads status filter from URL', async () => {
590
+ renderWithProviders(<NamespacePage />, {
591
+ route: '/namespaces/default?statuses=INVALID',
592
+ });
593
+
594
+ await waitFor(() => {
595
+ expect(mockDjClient.listNodesForLanding).toHaveBeenCalled();
596
+ });
597
+ });
598
+
599
+ it('reads mode filter from URL', async () => {
600
+ renderWithProviders(<NamespacePage />, {
601
+ route: '/namespaces/default?mode=draft',
602
+ });
603
+
604
+ await waitFor(() => {
605
+ expect(mockDjClient.listNodesForLanding).toHaveBeenCalled();
606
+ });
607
+ });
608
+
609
+ it('reads quality filters from URL', async () => {
610
+ renderWithProviders(<NamespacePage />, {
611
+ route:
612
+ '/namespaces/default?missingDescription=true&orphanedDimension=true',
613
+ });
614
+
615
+ await waitFor(() => {
616
+ expect(mockDjClient.listNodesForLanding).toHaveBeenCalled();
617
+ });
618
+ });
619
+ });
331
620
  });