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.
- package/package.json +8 -2
- 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
|
@@ -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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
190
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
});
|