datajunction-ui 0.0.103 → 0.0.105
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 +1 -1
- package/src/app/components/NamespaceHeader.jsx +224 -77
- package/src/app/components/NodeComponents.jsx +3 -2
- package/src/app/components/__tests__/NodeComponents.test.jsx +2 -2
- package/src/app/pages/NamespacePage/__tests__/index.test.jsx +212 -0
- package/src/app/pages/NamespacePage/index.jsx +950 -321
- package/src/app/services/DJService.js +16 -0
package/package.json
CHANGED
|
@@ -26,6 +26,11 @@ export default function NamespaceHeader({
|
|
|
26
26
|
const [existingPR, setExistingPR] = useState(null);
|
|
27
27
|
const [prLoading, setPrLoading] = useState(false);
|
|
28
28
|
|
|
29
|
+
// Branch switcher state
|
|
30
|
+
const [branches, setBranches] = useState([]);
|
|
31
|
+
const [branchDropdownOpen, setBranchDropdownOpen] = useState(false);
|
|
32
|
+
const branchDropdownRef = useRef(null);
|
|
33
|
+
|
|
29
34
|
// Modal states
|
|
30
35
|
const [showGitSettings, setShowGitSettings] = useState(false);
|
|
31
36
|
const [showCreateBranch, setShowCreateBranch] = useState(false);
|
|
@@ -65,7 +70,7 @@ export default function NamespaceHeader({
|
|
|
65
70
|
onGitConfigLoaded(config);
|
|
66
71
|
}
|
|
67
72
|
|
|
68
|
-
// If this is a branch namespace, fetch parent's git config and check for existing PR
|
|
73
|
+
// If this is a branch namespace, fetch parent's git config, branches, and check for existing PR
|
|
69
74
|
if (config?.parent_namespace) {
|
|
70
75
|
try {
|
|
71
76
|
const parentConfig = await djClient.getNamespaceGitConfig(
|
|
@@ -76,6 +81,15 @@ export default function NamespaceHeader({
|
|
|
76
81
|
console.error('Failed to fetch parent git config:', e);
|
|
77
82
|
}
|
|
78
83
|
|
|
84
|
+
try {
|
|
85
|
+
const branchList = await djClient.getNamespaceBranches(
|
|
86
|
+
config.parent_namespace,
|
|
87
|
+
);
|
|
88
|
+
setBranches(branchList || []);
|
|
89
|
+
} catch (e) {
|
|
90
|
+
console.error('Failed to fetch branches:', e);
|
|
91
|
+
}
|
|
92
|
+
|
|
79
93
|
// Check for existing PR
|
|
80
94
|
setPrLoading(true);
|
|
81
95
|
try {
|
|
@@ -102,12 +116,18 @@ export default function NamespaceHeader({
|
|
|
102
116
|
fetchData();
|
|
103
117
|
}, [djClient, namespace]);
|
|
104
118
|
|
|
105
|
-
// Close
|
|
119
|
+
// Close dropdowns when clicking outside
|
|
106
120
|
useEffect(() => {
|
|
107
121
|
const handleClickOutside = event => {
|
|
108
122
|
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
|
109
123
|
setDeploymentsDropdownOpen(false);
|
|
110
124
|
}
|
|
125
|
+
if (
|
|
126
|
+
branchDropdownRef.current &&
|
|
127
|
+
!branchDropdownRef.current.contains(event.target)
|
|
128
|
+
) {
|
|
129
|
+
setBranchDropdownOpen(false);
|
|
130
|
+
}
|
|
111
131
|
};
|
|
112
132
|
document.addEventListener('mousedown', handleClickOutside);
|
|
113
133
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
@@ -243,88 +263,215 @@ export default function NamespaceHeader({
|
|
|
243
263
|
/>
|
|
244
264
|
</svg>
|
|
245
265
|
{namespace ? (
|
|
246
|
-
namespaceParts.map((part, index, arr) =>
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
gap: '8px'
|
|
253
|
-
}}
|
|
254
|
-
>
|
|
255
|
-
<a
|
|
256
|
-
href={`/namespaces/${arr.slice(0, index + 1).join('.')}`}
|
|
257
|
-
style={{
|
|
258
|
-
fontWeight: '400',
|
|
259
|
-
color: '#1e293b',
|
|
260
|
-
textDecoration: 'none',
|
|
261
|
-
}}
|
|
266
|
+
namespaceParts.map((part, index, arr) => {
|
|
267
|
+
const isLast = index === arr.length - 1;
|
|
268
|
+
const href = `/namespaces/${arr.slice(0, index + 1).join('.')}`;
|
|
269
|
+
return (
|
|
270
|
+
<span
|
|
271
|
+
key={index}
|
|
272
|
+
style={{ display: 'flex', alignItems: 'center', gap: '8px' }}
|
|
262
273
|
>
|
|
263
|
-
{
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
274
|
+
{/* Last segment of a branch namespace becomes the branch switcher */}
|
|
275
|
+
{isLast && isBranchNamespace ? (
|
|
276
|
+
<div
|
|
277
|
+
ref={branchDropdownRef}
|
|
278
|
+
style={{ position: 'relative' }}
|
|
279
|
+
>
|
|
280
|
+
<button
|
|
281
|
+
onClick={() => setBranchDropdownOpen(o => !o)}
|
|
282
|
+
style={{
|
|
283
|
+
display: 'flex',
|
|
284
|
+
alignItems: 'center',
|
|
285
|
+
gap: '4px',
|
|
286
|
+
padding: '0',
|
|
287
|
+
background: 'none',
|
|
288
|
+
border: 'none',
|
|
289
|
+
fontWeight: '400',
|
|
290
|
+
fontSize: 'inherit',
|
|
291
|
+
color: '#1e293b',
|
|
292
|
+
cursor: 'pointer',
|
|
293
|
+
}}
|
|
294
|
+
>
|
|
295
|
+
<svg
|
|
296
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
297
|
+
width="12"
|
|
298
|
+
height="12"
|
|
299
|
+
viewBox="0 0 24 24"
|
|
300
|
+
fill="none"
|
|
301
|
+
stroke="#64748b"
|
|
302
|
+
strokeWidth="2"
|
|
303
|
+
strokeLinecap="round"
|
|
304
|
+
strokeLinejoin="round"
|
|
305
|
+
>
|
|
306
|
+
<line x1="6" y1="3" x2="6" y2="15" />
|
|
307
|
+
<circle cx="18" cy="6" r="3" />
|
|
308
|
+
<circle cx="6" cy="18" r="3" />
|
|
309
|
+
<path d="M18 9a9 9 0 0 1-9 9" />
|
|
310
|
+
</svg>
|
|
311
|
+
{part}
|
|
312
|
+
<span style={{ fontSize: '8px', color: '#94a3b8' }}>
|
|
313
|
+
{branchDropdownOpen ? '▲' : '▼'}
|
|
314
|
+
</span>
|
|
315
|
+
</button>
|
|
316
|
+
|
|
317
|
+
{branchDropdownOpen && (
|
|
318
|
+
<div
|
|
319
|
+
style={{
|
|
320
|
+
position: 'absolute',
|
|
321
|
+
top: '100%',
|
|
322
|
+
left: 0,
|
|
323
|
+
marginTop: '4px',
|
|
324
|
+
backgroundColor: 'white',
|
|
325
|
+
border: '1px solid #e2e8f0',
|
|
326
|
+
borderRadius: '8px',
|
|
327
|
+
boxShadow: '0 4px 12px rgba(0,0,0,0.12)',
|
|
328
|
+
zIndex: 1000,
|
|
329
|
+
minWidth: '180px',
|
|
330
|
+
overflow: 'hidden',
|
|
331
|
+
}}
|
|
332
|
+
>
|
|
333
|
+
<div
|
|
334
|
+
style={{
|
|
335
|
+
padding: '8px 12px 6px',
|
|
336
|
+
fontSize: '10px',
|
|
337
|
+
fontWeight: 600,
|
|
338
|
+
textTransform: 'uppercase',
|
|
339
|
+
letterSpacing: '0.05em',
|
|
340
|
+
color: '#94a3b8',
|
|
341
|
+
borderBottom: '1px solid #f1f5f9',
|
|
342
|
+
}}
|
|
343
|
+
>
|
|
344
|
+
<a
|
|
345
|
+
href={`/namespaces/${gitConfig.parent_namespace}`}
|
|
346
|
+
style={{
|
|
347
|
+
color: '#94a3b8',
|
|
348
|
+
textDecoration: 'none',
|
|
349
|
+
}}
|
|
350
|
+
onClick={() => setBranchDropdownOpen(false)}
|
|
351
|
+
>
|
|
352
|
+
{gitConfig.parent_namespace}
|
|
353
|
+
</a>
|
|
354
|
+
</div>
|
|
355
|
+
{branches.length === 0 ? (
|
|
356
|
+
<div
|
|
357
|
+
style={{
|
|
358
|
+
padding: '10px 12px',
|
|
359
|
+
fontSize: '12px',
|
|
360
|
+
color: '#94a3b8',
|
|
361
|
+
}}
|
|
362
|
+
>
|
|
363
|
+
No branches found
|
|
364
|
+
</div>
|
|
365
|
+
) : (
|
|
366
|
+
branches.map(b => {
|
|
367
|
+
const isCurrent = b.namespace === namespace;
|
|
368
|
+
return (
|
|
369
|
+
<a
|
|
370
|
+
key={b.namespace}
|
|
371
|
+
href={`/namespaces/${b.namespace}`}
|
|
372
|
+
onClick={() => setBranchDropdownOpen(false)}
|
|
373
|
+
style={{
|
|
374
|
+
display: 'flex',
|
|
375
|
+
alignItems: 'center',
|
|
376
|
+
justifyContent: 'space-between',
|
|
377
|
+
padding: '8px 12px',
|
|
378
|
+
fontSize: '13px',
|
|
379
|
+
color: isCurrent ? '#1e40af' : '#1e293b',
|
|
380
|
+
backgroundColor: isCurrent
|
|
381
|
+
? '#eff6ff'
|
|
382
|
+
: 'white',
|
|
383
|
+
textDecoration: 'none',
|
|
384
|
+
borderBottom: '1px solid #f8fafc',
|
|
385
|
+
}}
|
|
386
|
+
>
|
|
387
|
+
<span
|
|
388
|
+
style={{
|
|
389
|
+
display: 'flex',
|
|
390
|
+
alignItems: 'center',
|
|
391
|
+
gap: '6px',
|
|
392
|
+
minWidth: 0,
|
|
393
|
+
}}
|
|
394
|
+
>
|
|
395
|
+
{isCurrent && (
|
|
396
|
+
<svg
|
|
397
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
398
|
+
width="10"
|
|
399
|
+
height="10"
|
|
400
|
+
viewBox="0 0 24 24"
|
|
401
|
+
fill="none"
|
|
402
|
+
stroke="currentColor"
|
|
403
|
+
strokeWidth="3"
|
|
404
|
+
strokeLinecap="round"
|
|
405
|
+
strokeLinejoin="round"
|
|
406
|
+
style={{ flexShrink: 0 }}
|
|
407
|
+
>
|
|
408
|
+
<polyline points="20 6 9 17 4 12" />
|
|
409
|
+
</svg>
|
|
410
|
+
)}
|
|
411
|
+
<span
|
|
412
|
+
style={{
|
|
413
|
+
overflow: 'hidden',
|
|
414
|
+
textOverflow: 'ellipsis',
|
|
415
|
+
whiteSpace: 'nowrap',
|
|
416
|
+
maxWidth: '180px',
|
|
417
|
+
}}
|
|
418
|
+
title={b.git_branch || b.namespace}
|
|
419
|
+
>
|
|
420
|
+
{b.git_branch || b.namespace}
|
|
421
|
+
</span>
|
|
422
|
+
</span>
|
|
423
|
+
<span
|
|
424
|
+
style={{
|
|
425
|
+
fontSize: '11px',
|
|
426
|
+
color: '#94a3b8',
|
|
427
|
+
flexShrink: 0,
|
|
428
|
+
marginLeft: '8px',
|
|
429
|
+
}}
|
|
430
|
+
>
|
|
431
|
+
{b.num_nodes} nodes
|
|
432
|
+
</span>
|
|
433
|
+
</a>
|
|
434
|
+
);
|
|
435
|
+
})
|
|
436
|
+
)}
|
|
437
|
+
</div>
|
|
438
|
+
)}
|
|
439
|
+
</div>
|
|
440
|
+
) : (
|
|
441
|
+
<a
|
|
442
|
+
href={href}
|
|
443
|
+
style={{
|
|
444
|
+
fontWeight: '400',
|
|
445
|
+
color: '#1e293b',
|
|
446
|
+
textDecoration: 'none',
|
|
447
|
+
}}
|
|
448
|
+
>
|
|
449
|
+
{part}
|
|
450
|
+
</a>
|
|
451
|
+
)}
|
|
452
|
+
{!isLast && (
|
|
453
|
+
<svg
|
|
454
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
455
|
+
width="12"
|
|
456
|
+
height="12"
|
|
457
|
+
fill="#94a3b8"
|
|
458
|
+
viewBox="0 0 16 16"
|
|
459
|
+
>
|
|
460
|
+
<path
|
|
461
|
+
fillRule="evenodd"
|
|
462
|
+
d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z"
|
|
463
|
+
/>
|
|
464
|
+
</svg>
|
|
465
|
+
)}
|
|
466
|
+
</span>
|
|
467
|
+
);
|
|
468
|
+
})
|
|
281
469
|
) : (
|
|
282
470
|
<span style={{ fontWeight: '600', color: '#1e293b' }}>
|
|
283
471
|
All Namespaces
|
|
284
472
|
</span>
|
|
285
473
|
)}
|
|
286
474
|
|
|
287
|
-
{/* Branch indicator */}
|
|
288
|
-
{isBranchNamespace && (
|
|
289
|
-
<span
|
|
290
|
-
style={{
|
|
291
|
-
display: 'flex',
|
|
292
|
-
alignItems: 'center',
|
|
293
|
-
gap: '4px',
|
|
294
|
-
padding: '2px 8px',
|
|
295
|
-
backgroundColor: '#dbeafe',
|
|
296
|
-
borderRadius: '12px',
|
|
297
|
-
fontSize: '11px',
|
|
298
|
-
color: '#1e40af',
|
|
299
|
-
marginLeft: '4px',
|
|
300
|
-
}}
|
|
301
|
-
>
|
|
302
|
-
<svg
|
|
303
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
304
|
-
width="12"
|
|
305
|
-
height="12"
|
|
306
|
-
viewBox="0 0 24 24"
|
|
307
|
-
fill="none"
|
|
308
|
-
stroke="currentColor"
|
|
309
|
-
strokeWidth="2"
|
|
310
|
-
strokeLinecap="round"
|
|
311
|
-
strokeLinejoin="round"
|
|
312
|
-
>
|
|
313
|
-
<line x1="6" y1="3" x2="6" y2="15" />
|
|
314
|
-
<circle cx="18" cy="6" r="3" />
|
|
315
|
-
<circle cx="6" cy="18" r="3" />
|
|
316
|
-
<path d="M18 9a9 9 0 0 1-9 9" />
|
|
317
|
-
</svg>
|
|
318
|
-
Branch of{' '}
|
|
319
|
-
<a
|
|
320
|
-
href={`/namespaces/${gitConfig.parent_namespace}`}
|
|
321
|
-
style={{ color: '#1e40af', textDecoration: 'underline' }}
|
|
322
|
-
>
|
|
323
|
-
{gitConfig.parent_namespace}
|
|
324
|
-
</a>
|
|
325
|
-
</span>
|
|
326
|
-
)}
|
|
327
|
-
|
|
328
475
|
{/* Git-only (read-only) indicator */}
|
|
329
476
|
{gitConfig?.git_only && (
|
|
330
477
|
<span
|
|
@@ -33,6 +33,7 @@ export function NodeBadge({
|
|
|
33
33
|
...sizeStyles,
|
|
34
34
|
flexShrink: 0,
|
|
35
35
|
...style,
|
|
36
|
+
marginRight: 0,
|
|
36
37
|
}}
|
|
37
38
|
>
|
|
38
39
|
{displayText}
|
|
@@ -62,7 +63,7 @@ export function NodeLink({
|
|
|
62
63
|
const sizeMap = {
|
|
63
64
|
small: { fontSize: '10px', fontWeight: '500' },
|
|
64
65
|
medium: { fontSize: '12px', fontWeight: '500' },
|
|
65
|
-
large: { fontSize: '
|
|
66
|
+
large: { fontSize: '14px', fontWeight: '500' },
|
|
66
67
|
};
|
|
67
68
|
|
|
68
69
|
const sizeStyles = sizeMap[size] || sizeMap.medium;
|
|
@@ -160,7 +161,7 @@ export function NodeChip({ node }) {
|
|
|
160
161
|
alignItems: 'center',
|
|
161
162
|
gap: '3px',
|
|
162
163
|
padding: '2px 6px',
|
|
163
|
-
fontSize: '
|
|
164
|
+
fontSize: '12px',
|
|
164
165
|
border: '1px solid var(--border-color, #ddd)',
|
|
165
166
|
borderRadius: '3px',
|
|
166
167
|
textDecoration: 'none',
|
|
@@ -127,7 +127,7 @@ describe('NodeComponents', () => {
|
|
|
127
127
|
it('should apply large size styles', () => {
|
|
128
128
|
render(<NodeLink node={mockNode} size="large" />);
|
|
129
129
|
const link = screen.getByText('My Metric');
|
|
130
|
-
expect(link).toHaveStyle({ fontSize: '
|
|
130
|
+
expect(link).toHaveStyle({ fontSize: '14px', fontWeight: '500' });
|
|
131
131
|
});
|
|
132
132
|
|
|
133
133
|
it('should use medium as default when invalid size provided', () => {
|
|
@@ -253,7 +253,7 @@ describe('NodeComponents', () => {
|
|
|
253
253
|
const { container } = render(<NodeChip node={mockNode} />);
|
|
254
254
|
const link = container.querySelector('a');
|
|
255
255
|
expect(link).toHaveStyle({
|
|
256
|
-
fontSize: '
|
|
256
|
+
fontSize: '12px',
|
|
257
257
|
padding: '2px 6px',
|
|
258
258
|
whiteSpace: 'nowrap',
|
|
259
259
|
});
|
|
@@ -16,6 +16,10 @@ const mockDjClient = {
|
|
|
16
16
|
listTags: jest.fn(),
|
|
17
17
|
namespaceSources: jest.fn(),
|
|
18
18
|
namespaceSourcesBulk: jest.fn(),
|
|
19
|
+
getNamespaceGitConfig: jest.fn(),
|
|
20
|
+
getNamespaceBranches: jest.fn(),
|
|
21
|
+
listDeployments: jest.fn(),
|
|
22
|
+
getPullRequest: jest.fn(),
|
|
19
23
|
};
|
|
20
24
|
|
|
21
25
|
const mockCurrentUser = { username: 'dj', email: 'dj@test.com' };
|
|
@@ -73,6 +77,10 @@ describe('NamespacePage', () => {
|
|
|
73
77
|
mockDjClient.namespaceSourcesBulk.mockResolvedValue({
|
|
74
78
|
namespace_sources: {},
|
|
75
79
|
});
|
|
80
|
+
mockDjClient.getNamespaceGitConfig.mockResolvedValue(null);
|
|
81
|
+
mockDjClient.getNamespaceBranches.mockResolvedValue([]);
|
|
82
|
+
mockDjClient.listDeployments.mockResolvedValue([]);
|
|
83
|
+
mockDjClient.getPullRequest.mockResolvedValue(null);
|
|
76
84
|
mockDjClient.namespaces.mockResolvedValue([
|
|
77
85
|
{
|
|
78
86
|
namespace: 'common.one',
|
|
@@ -626,5 +634,209 @@ describe('NamespacePage', () => {
|
|
|
626
634
|
expect(mockDjClient.listNodesForLanding).toHaveBeenCalled();
|
|
627
635
|
});
|
|
628
636
|
});
|
|
637
|
+
|
|
638
|
+
it('reads hasMaterialization filter from URL', async () => {
|
|
639
|
+
renderWithProviders(<NamespacePage />, {
|
|
640
|
+
route: '/namespaces/default?hasMaterialization=true',
|
|
641
|
+
});
|
|
642
|
+
await waitFor(() => {
|
|
643
|
+
expect(mockDjClient.listNodesForLanding).toHaveBeenCalled();
|
|
644
|
+
});
|
|
645
|
+
});
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
describe('Git-root namespace (branch landing page)', () => {
|
|
649
|
+
const gitRootConfig = {
|
|
650
|
+
github_repo_path: 'org/repo',
|
|
651
|
+
git_branch: 'main',
|
|
652
|
+
default_branch: 'main',
|
|
653
|
+
parent_namespace: null,
|
|
654
|
+
git_only: false,
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
const mockBranches = [
|
|
658
|
+
{
|
|
659
|
+
namespace: 'default.main',
|
|
660
|
+
git_branch: 'main',
|
|
661
|
+
num_nodes: 10,
|
|
662
|
+
invalid_node_count: 1,
|
|
663
|
+
last_deployed_at: '2024-10-18T12:00:00+00:00',
|
|
664
|
+
},
|
|
665
|
+
{
|
|
666
|
+
namespace: 'default.feature-xyz',
|
|
667
|
+
git_branch: 'feature-xyz',
|
|
668
|
+
num_nodes: 5,
|
|
669
|
+
invalid_node_count: 0,
|
|
670
|
+
last_deployed_at: null,
|
|
671
|
+
},
|
|
672
|
+
];
|
|
673
|
+
|
|
674
|
+
beforeEach(() => {
|
|
675
|
+
mockDjClient.getNamespaceGitConfig.mockResolvedValue(gitRootConfig);
|
|
676
|
+
mockDjClient.getNamespaceBranches.mockResolvedValue(mockBranches);
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
it('shows Branches section for a git-root namespace', async () => {
|
|
680
|
+
renderWithProviders(<NamespacePage />);
|
|
681
|
+
|
|
682
|
+
await waitFor(() => {
|
|
683
|
+
expect(screen.getByText('Branches')).toBeInTheDocument();
|
|
684
|
+
// cards show git_branch value; 'main' appears in both the card title
|
|
685
|
+
// and the default branch section header, so use getAllByText
|
|
686
|
+
expect(screen.getAllByText('main').length).toBeGreaterThan(0);
|
|
687
|
+
expect(screen.getByText('feature-xyz')).toBeInTheDocument();
|
|
688
|
+
});
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
it('shows branch count next to Branches header', async () => {
|
|
692
|
+
renderWithProviders(<NamespacePage />);
|
|
693
|
+
|
|
694
|
+
await waitFor(() => {
|
|
695
|
+
expect(screen.getByText('Branches')).toBeInTheDocument();
|
|
696
|
+
// branch count (2)
|
|
697
|
+
expect(screen.getByText('2')).toBeInTheDocument();
|
|
698
|
+
});
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
it('shows node counts and invalid counts on branch cards', async () => {
|
|
702
|
+
renderWithProviders(<NamespacePage />);
|
|
703
|
+
|
|
704
|
+
await waitFor(() => {
|
|
705
|
+
expect(screen.getByText('10 nodes')).toBeInTheDocument();
|
|
706
|
+
expect(screen.getByText('1 invalid')).toBeInTheDocument();
|
|
707
|
+
expect(screen.getByText('5 nodes')).toBeInTheDocument();
|
|
708
|
+
});
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
it('shows default branch section header and View all link', async () => {
|
|
712
|
+
renderWithProviders(<NamespacePage />);
|
|
713
|
+
|
|
714
|
+
// The default branch header (name + "default" badge) and "View all" link
|
|
715
|
+
// appear as soon as gitConfig.default_branch is set and isGitRoot is true
|
|
716
|
+
await waitFor(
|
|
717
|
+
() => {
|
|
718
|
+
expect(screen.getByText('View all →')).toBeInTheDocument();
|
|
719
|
+
// default branch name shown as the section title
|
|
720
|
+
expect(screen.getAllByText('main').length).toBeGreaterThan(0);
|
|
721
|
+
},
|
|
722
|
+
{ timeout: 3000 },
|
|
723
|
+
);
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
it('calls listNodesForLanding for the default branch namespace', async () => {
|
|
727
|
+
renderWithProviders(<NamespacePage />);
|
|
728
|
+
|
|
729
|
+
await waitFor(
|
|
730
|
+
() => {
|
|
731
|
+
// After isGitRoot is set, a second listNodesForLanding call for
|
|
732
|
+
// 'default.main' should be made
|
|
733
|
+
const calls = mockDjClient.listNodesForLanding.mock.calls;
|
|
734
|
+
const defaultBranchCall = calls.find(
|
|
735
|
+
args => args[0] === 'default.main',
|
|
736
|
+
);
|
|
737
|
+
expect(defaultBranchCall).toBeDefined();
|
|
738
|
+
},
|
|
739
|
+
{ timeout: 3000 },
|
|
740
|
+
);
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
it('shows loading state while branches are loading', async () => {
|
|
744
|
+
let resolveBranches;
|
|
745
|
+
mockDjClient.getNamespaceBranches.mockReturnValue(
|
|
746
|
+
new Promise(resolve => {
|
|
747
|
+
resolveBranches = resolve;
|
|
748
|
+
}),
|
|
749
|
+
);
|
|
750
|
+
|
|
751
|
+
renderWithProviders(<NamespacePage />);
|
|
752
|
+
|
|
753
|
+
// While loading, Branches header should still show (branchesLoading=true triggers the section)
|
|
754
|
+
await waitFor(() => {
|
|
755
|
+
expect(screen.getByText('Branches')).toBeInTheDocument();
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
// Resolve to avoid act() warnings
|
|
759
|
+
resolveBranches([]);
|
|
760
|
+
});
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
describe('Quality filter checkboxes', () => {
|
|
764
|
+
it('toggles orphanedDimension filter', async () => {
|
|
765
|
+
renderWithProviders(<NamespacePage />);
|
|
766
|
+
|
|
767
|
+
await waitFor(() => {
|
|
768
|
+
expect(screen.getByText('Quality')).toBeInTheDocument();
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
fireEvent.click(screen.getByText('Issues'));
|
|
772
|
+
await waitFor(() => {
|
|
773
|
+
expect(screen.getByText('Orphaned Dimensions')).toBeInTheDocument();
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
const checkbox = screen.getByLabelText('Orphaned Dimensions');
|
|
777
|
+
const callsBefore = mockDjClient.listNodesForLanding.mock.calls.length;
|
|
778
|
+
fireEvent.click(checkbox);
|
|
779
|
+
|
|
780
|
+
await waitFor(() => {
|
|
781
|
+
expect(
|
|
782
|
+
mockDjClient.listNodesForLanding.mock.calls.length,
|
|
783
|
+
).toBeGreaterThan(callsBefore);
|
|
784
|
+
});
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
it('toggles hasMaterialization filter', async () => {
|
|
788
|
+
renderWithProviders(<NamespacePage />);
|
|
789
|
+
|
|
790
|
+
await waitFor(() => {
|
|
791
|
+
expect(screen.getByText('Quality')).toBeInTheDocument();
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
fireEvent.click(screen.getByText('Issues'));
|
|
795
|
+
await waitFor(() => {
|
|
796
|
+
expect(screen.getByText('Has Materialization')).toBeInTheDocument();
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
const checkbox = screen.getByLabelText('Has Materialization');
|
|
800
|
+
const callsBefore = mockDjClient.listNodesForLanding.mock.calls.length;
|
|
801
|
+
fireEvent.click(checkbox);
|
|
802
|
+
|
|
803
|
+
await waitFor(() => {
|
|
804
|
+
expect(
|
|
805
|
+
mockDjClient.listNodesForLanding.mock.calls.length,
|
|
806
|
+
).toBeGreaterThan(callsBefore);
|
|
807
|
+
});
|
|
808
|
+
});
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
describe('formatRelativeTime', () => {
|
|
812
|
+
it('shows last_deployed_at timestamp on branch cards', async () => {
|
|
813
|
+
mockDjClient.getNamespaceGitConfig.mockResolvedValue({
|
|
814
|
+
github_repo_path: 'org/repo',
|
|
815
|
+
git_branch: 'main',
|
|
816
|
+
default_branch: 'main',
|
|
817
|
+
parent_namespace: null,
|
|
818
|
+
git_only: false,
|
|
819
|
+
});
|
|
820
|
+
mockDjClient.getNamespaceBranches.mockResolvedValue([
|
|
821
|
+
{
|
|
822
|
+
namespace: 'default.main',
|
|
823
|
+
git_branch: 'main',
|
|
824
|
+
num_nodes: 3,
|
|
825
|
+
invalid_node_count: 0,
|
|
826
|
+
last_deployed_at: new Date(
|
|
827
|
+
Date.now() - 2 * 24 * 60 * 60 * 1000,
|
|
828
|
+
).toISOString(),
|
|
829
|
+
},
|
|
830
|
+
]);
|
|
831
|
+
|
|
832
|
+
renderWithProviders(<NamespacePage />);
|
|
833
|
+
|
|
834
|
+
await waitFor(() => {
|
|
835
|
+
// 'main' appears in both the card title and default branch section header
|
|
836
|
+
expect(screen.getAllByText('main').length).toBeGreaterThan(0);
|
|
837
|
+
// Should show relative time like "2d ago" (may appear on multiple elements)
|
|
838
|
+
expect(screen.getAllByText(/ago/).length).toBeGreaterThan(0);
|
|
839
|
+
});
|
|
840
|
+
});
|
|
629
841
|
});
|
|
630
842
|
});
|