datajunction-ui 0.0.27 → 0.0.30
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/__tests__/reportWebVitals.test.ts +27 -0
- package/src/app/components/NamespaceHeader.jsx +85 -26
- package/src/app/components/__tests__/NamespaceHeader.test.jsx +144 -0
- package/src/app/components/__tests__/NotificationBell.test.tsx +23 -0
- package/src/app/components/djgraph/__tests__/DJNode.test.tsx +36 -0
- package/src/app/icons/__tests__/Icons.test.jsx +24 -0
- package/src/app/pages/NamespacePage/Explorer.jsx +68 -10
- package/src/app/pages/NamespacePage/__tests__/index.test.jsx +21 -11
- package/src/app/pages/NamespacePage/index.jsx +625 -48
- package/src/app/pages/NotificationsPage/__tests__/index.test.jsx +28 -0
- package/src/app/pages/OverviewPage/OverviewPanel.jsx +1 -3
- package/src/app/pages/QueryPlannerPage/PreAggDetailsPanel.jsx +20 -20
- package/src/app/pages/QueryPlannerPage/index.jsx +1 -1
- package/src/app/pages/Root/__tests__/index.test.jsx +99 -4
- package/src/app/pages/SQLBuilderPage/__tests__/index.test.jsx +177 -0
- package/src/app/pages/SettingsPage/__tests__/CreateServiceAccountModal.test.jsx +50 -0
- package/src/app/pages/SettingsPage/__tests__/NotificationSubscriptionsSection.test.jsx +95 -0
- package/src/app/pages/SettingsPage/__tests__/index.test.jsx +315 -28
- package/src/app/providers/UserProvider.tsx +1 -5
- package/src/app/services/DJService.js +48 -2
- package/src/app/utils/__tests__/date.test.js +60 -140
- package/src/styles/index.css +51 -10
package/package.json
CHANGED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import reportWebVitals from '../reportWebVitals';
|
|
2
|
+
|
|
3
|
+
describe('reportWebVitals', () => {
|
|
4
|
+
it('calls web vitals functions when handler is provided', async () => {
|
|
5
|
+
const mockHandler = jest.fn();
|
|
6
|
+
|
|
7
|
+
// Call reportWebVitals with a handler
|
|
8
|
+
reportWebVitals(mockHandler);
|
|
9
|
+
|
|
10
|
+
// Wait for dynamic import to resolve
|
|
11
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
12
|
+
|
|
13
|
+
// The handler should have been called by web vitals
|
|
14
|
+
// (we just verify it doesn't throw)
|
|
15
|
+
expect(mockHandler).toBeDefined();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('does nothing when no handler is provided', () => {
|
|
19
|
+
// Should not throw
|
|
20
|
+
expect(() => reportWebVitals()).not.toThrow();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('does nothing when handler is not a function', () => {
|
|
24
|
+
// Should not throw
|
|
25
|
+
expect(() => reportWebVitals(undefined)).not.toThrow();
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -1,31 +1,90 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useContext, useEffect, useState } from 'react';
|
|
2
2
|
import HorizontalHierarchyIcon from '../icons/HorizontalHierarchyIcon';
|
|
3
|
+
import DJClientContext from '../providers/djclient';
|
|
4
|
+
|
|
5
|
+
export default function NamespaceHeader({ namespace }) {
|
|
6
|
+
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
7
|
+
const [sources, setSources] = useState(null);
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
const fetchSources = async () => {
|
|
11
|
+
if (namespace) {
|
|
12
|
+
try {
|
|
13
|
+
const data = await djClient.namespaceSources(namespace);
|
|
14
|
+
setSources(data);
|
|
15
|
+
} catch (e) {
|
|
16
|
+
// Silently fail - badge just won't show
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
fetchSources();
|
|
21
|
+
}, [djClient, namespace]);
|
|
22
|
+
|
|
23
|
+
const namespaceParts = namespace ? namespace.split('.') : [];
|
|
24
|
+
const namespaceList = namespaceParts.map((piece, index) => {
|
|
25
|
+
return (
|
|
26
|
+
<li className="breadcrumb-item" key={index}>
|
|
27
|
+
<a
|
|
28
|
+
className="link-body-emphasis"
|
|
29
|
+
href={'/namespaces/' + namespaceParts.slice(0, index + 1).join('.')}
|
|
30
|
+
>
|
|
31
|
+
{piece}
|
|
32
|
+
</a>
|
|
33
|
+
</li>
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Render source badge
|
|
38
|
+
const renderSourceBadge = () => {
|
|
39
|
+
if (!sources || sources.total_deployments === 0) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const isGit = sources.primary_source?.type === 'git';
|
|
3
44
|
|
|
4
|
-
export default class NamespaceHeader extends Component {
|
|
5
|
-
render() {
|
|
6
|
-
const { namespace } = this.props;
|
|
7
|
-
const namespaceParts = namespace.split('.');
|
|
8
|
-
const namespaceList = namespaceParts.map((piece, index) => {
|
|
9
|
-
return (
|
|
10
|
-
<li className="breadcrumb-item" key={index}>
|
|
11
|
-
<a
|
|
12
|
-
className="link-body-emphasis"
|
|
13
|
-
href={'/namespaces/' + namespaceParts.slice(0, index + 1).join('.')}
|
|
14
|
-
>
|
|
15
|
-
{piece}
|
|
16
|
-
</a>
|
|
17
|
-
</li>
|
|
18
|
-
);
|
|
19
|
-
});
|
|
20
45
|
return (
|
|
21
|
-
<
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
46
|
+
<li
|
|
47
|
+
className="breadcrumb-item"
|
|
48
|
+
style={{ display: 'flex', alignItems: 'center' }}
|
|
49
|
+
>
|
|
50
|
+
<span
|
|
51
|
+
title={
|
|
52
|
+
isGit
|
|
53
|
+
? `CI-managed: ${sources.primary_source.repository}${
|
|
54
|
+
sources.primary_source.branch
|
|
55
|
+
? ` (${sources.primary_source.branch})`
|
|
56
|
+
: ''
|
|
57
|
+
}`
|
|
58
|
+
: 'Local/adhoc deployment'
|
|
59
|
+
}
|
|
60
|
+
style={{
|
|
61
|
+
display: 'inline-flex',
|
|
62
|
+
alignItems: 'center',
|
|
63
|
+
gap: '4px',
|
|
64
|
+
padding: '2px 8px',
|
|
65
|
+
fontSize: '11px',
|
|
66
|
+
borderRadius: '12px',
|
|
67
|
+
backgroundColor: isGit ? '#d4edda' : '#e2e3e5',
|
|
68
|
+
color: isGit ? '#155724' : '#383d41',
|
|
69
|
+
cursor: 'help',
|
|
70
|
+
}}
|
|
71
|
+
>
|
|
72
|
+
{isGit ? '🔗' : '📁'}
|
|
73
|
+
{isGit ? 'CI' : 'Local'}
|
|
74
|
+
</span>
|
|
75
|
+
</li>
|
|
29
76
|
);
|
|
30
|
-
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<ol className="breadcrumb breadcrumb-chevron p-3 bg-body-tertiary rounded-3">
|
|
81
|
+
<li className="breadcrumb-item">
|
|
82
|
+
<a href="/">
|
|
83
|
+
<HorizontalHierarchyIcon />
|
|
84
|
+
</a>
|
|
85
|
+
</li>
|
|
86
|
+
{namespaceList}
|
|
87
|
+
{renderSourceBadge()}
|
|
88
|
+
</ol>
|
|
89
|
+
);
|
|
31
90
|
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
2
3
|
import { createRenderer } from 'react-test-renderer/shallow';
|
|
4
|
+
import { MemoryRouter } from 'react-router-dom';
|
|
3
5
|
|
|
4
6
|
import NamespaceHeader from '../NamespaceHeader';
|
|
7
|
+
import DJClientContext from '../../providers/djclient';
|
|
5
8
|
|
|
6
9
|
const renderer = createRenderer();
|
|
7
10
|
|
|
@@ -11,4 +14,145 @@ describe('<NamespaceHeader />', () => {
|
|
|
11
14
|
const renderedOutput = renderer.getRenderOutput();
|
|
12
15
|
expect(renderedOutput).toMatchSnapshot();
|
|
13
16
|
});
|
|
17
|
+
|
|
18
|
+
it('should render git source badge when source type is git with branch', async () => {
|
|
19
|
+
const mockDjClient = {
|
|
20
|
+
namespaceSources: jest.fn().mockResolvedValue({
|
|
21
|
+
total_deployments: 5,
|
|
22
|
+
primary_source: {
|
|
23
|
+
type: 'git',
|
|
24
|
+
repository: 'github.com/test/repo',
|
|
25
|
+
branch: 'main',
|
|
26
|
+
},
|
|
27
|
+
}),
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
render(
|
|
31
|
+
<MemoryRouter>
|
|
32
|
+
<DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
|
|
33
|
+
<NamespaceHeader namespace="test.namespace" />
|
|
34
|
+
</DJClientContext.Provider>
|
|
35
|
+
</MemoryRouter>,
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
await waitFor(() => {
|
|
39
|
+
expect(mockDjClient.namespaceSources).toHaveBeenCalledWith(
|
|
40
|
+
'test.namespace',
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Should render CI badge for git source
|
|
45
|
+
expect(screen.getByText(/CI/)).toBeInTheDocument();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should render git source badge when source type is git without branch', async () => {
|
|
49
|
+
const mockDjClient = {
|
|
50
|
+
namespaceSources: jest.fn().mockResolvedValue({
|
|
51
|
+
total_deployments: 3,
|
|
52
|
+
primary_source: {
|
|
53
|
+
type: 'git',
|
|
54
|
+
repository: 'github.com/test/repo',
|
|
55
|
+
branch: null,
|
|
56
|
+
},
|
|
57
|
+
}),
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
render(
|
|
61
|
+
<MemoryRouter>
|
|
62
|
+
<DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
|
|
63
|
+
<NamespaceHeader namespace="test.namespace" />
|
|
64
|
+
</DJClientContext.Provider>
|
|
65
|
+
</MemoryRouter>,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
await waitFor(() => {
|
|
69
|
+
expect(mockDjClient.namespaceSources).toHaveBeenCalledWith(
|
|
70
|
+
'test.namespace',
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Should render CI badge for git source even without branch
|
|
75
|
+
expect(screen.getByText(/CI/)).toBeInTheDocument();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should render local source badge when source type is local', async () => {
|
|
79
|
+
const mockDjClient = {
|
|
80
|
+
namespaceSources: jest.fn().mockResolvedValue({
|
|
81
|
+
total_deployments: 2,
|
|
82
|
+
primary_source: {
|
|
83
|
+
type: 'local',
|
|
84
|
+
hostname: 'localhost',
|
|
85
|
+
},
|
|
86
|
+
}),
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
render(
|
|
90
|
+
<MemoryRouter>
|
|
91
|
+
<DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
|
|
92
|
+
<NamespaceHeader namespace="test.namespace" />
|
|
93
|
+
</DJClientContext.Provider>
|
|
94
|
+
</MemoryRouter>,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
await waitFor(() => {
|
|
98
|
+
expect(mockDjClient.namespaceSources).toHaveBeenCalledWith(
|
|
99
|
+
'test.namespace',
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Should render Local badge for local source
|
|
104
|
+
expect(screen.getByText(/Local/)).toBeInTheDocument();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should not render badge when no deployments', async () => {
|
|
108
|
+
const mockDjClient = {
|
|
109
|
+
namespaceSources: jest.fn().mockResolvedValue({
|
|
110
|
+
total_deployments: 0,
|
|
111
|
+
primary_source: null,
|
|
112
|
+
}),
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
render(
|
|
116
|
+
<MemoryRouter>
|
|
117
|
+
<DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
|
|
118
|
+
<NamespaceHeader namespace="test.namespace" />
|
|
119
|
+
</DJClientContext.Provider>
|
|
120
|
+
</MemoryRouter>,
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
await waitFor(() => {
|
|
124
|
+
expect(mockDjClient.namespaceSources).toHaveBeenCalledWith(
|
|
125
|
+
'test.namespace',
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Should not render any source badge
|
|
130
|
+
expect(screen.queryByText(/CI/)).not.toBeInTheDocument();
|
|
131
|
+
expect(screen.queryByText(/Local/)).not.toBeInTheDocument();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should handle API error gracefully', async () => {
|
|
135
|
+
const mockDjClient = {
|
|
136
|
+
namespaceSources: jest.fn().mockRejectedValue(new Error('API Error')),
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
render(
|
|
140
|
+
<MemoryRouter>
|
|
141
|
+
<DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
|
|
142
|
+
<NamespaceHeader namespace="test.namespace" />
|
|
143
|
+
</DJClientContext.Provider>
|
|
144
|
+
</MemoryRouter>,
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
await waitFor(() => {
|
|
148
|
+
expect(mockDjClient.namespaceSources).toHaveBeenCalledWith(
|
|
149
|
+
'test.namespace',
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Should still render breadcrumb without badge
|
|
154
|
+
expect(screen.getByText('test')).toBeInTheDocument();
|
|
155
|
+
expect(screen.getByText('namespace')).toBeInTheDocument();
|
|
156
|
+
expect(screen.queryByText(/CI/)).not.toBeInTheDocument();
|
|
157
|
+
});
|
|
14
158
|
});
|
|
@@ -310,4 +310,27 @@ describe('<NotificationBell />', () => {
|
|
|
310
310
|
// onDropdownToggle should have been called with false
|
|
311
311
|
expect(onDropdownToggle).toHaveBeenCalledWith(false);
|
|
312
312
|
});
|
|
313
|
+
|
|
314
|
+
it('handles error when fetching notifications fails', async () => {
|
|
315
|
+
const consoleErrorSpy = jest
|
|
316
|
+
.spyOn(console, 'error')
|
|
317
|
+
.mockImplementation(() => {});
|
|
318
|
+
|
|
319
|
+
const mockDjClient = createMockDjClient({
|
|
320
|
+
getSubscribedHistory: jest
|
|
321
|
+
.fn()
|
|
322
|
+
.mockRejectedValue(new Error('Network error')),
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
renderWithContext(mockDjClient);
|
|
326
|
+
|
|
327
|
+
await waitFor(() => {
|
|
328
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
329
|
+
'Error fetching notifications:',
|
|
330
|
+
expect.any(Error),
|
|
331
|
+
);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
consoleErrorSpy.mockRestore();
|
|
335
|
+
});
|
|
313
336
|
});
|
|
@@ -21,4 +21,40 @@ describe('<DJNode />', () => {
|
|
|
21
21
|
const renderedOutput = renderer.getRenderOutput();
|
|
22
22
|
expect(renderedOutput).toMatchSnapshot();
|
|
23
23
|
});
|
|
24
|
+
|
|
25
|
+
it('should render with is_current true and non-metric type (collapsed=false)', () => {
|
|
26
|
+
renderer.render(
|
|
27
|
+
<DJNode
|
|
28
|
+
id="2"
|
|
29
|
+
data={{
|
|
30
|
+
name: 'shared.dimensions.accounts',
|
|
31
|
+
column_names: ['a', 'b'],
|
|
32
|
+
type: 'dimension',
|
|
33
|
+
primary_key: ['id'],
|
|
34
|
+
is_current: true,
|
|
35
|
+
display_name: 'Accounts',
|
|
36
|
+
}}
|
|
37
|
+
/>,
|
|
38
|
+
);
|
|
39
|
+
const renderedOutput = renderer.getRenderOutput();
|
|
40
|
+
expect(renderedOutput).toBeTruthy();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should render with metric type (collapsed=true)', () => {
|
|
44
|
+
renderer.render(
|
|
45
|
+
<DJNode
|
|
46
|
+
id="3"
|
|
47
|
+
data={{
|
|
48
|
+
name: 'default.revenue',
|
|
49
|
+
column_names: [],
|
|
50
|
+
type: 'metric',
|
|
51
|
+
primary_key: [],
|
|
52
|
+
is_current: true,
|
|
53
|
+
display_name: 'Revenue',
|
|
54
|
+
}}
|
|
55
|
+
/>,
|
|
56
|
+
);
|
|
57
|
+
const renderedOutput = renderer.getRenderOutput();
|
|
58
|
+
expect(renderedOutput).toBeTruthy();
|
|
59
|
+
});
|
|
24
60
|
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
|
|
4
|
+
import CommitIcon from '../CommitIcon';
|
|
5
|
+
import InvalidIcon from '../InvalidIcon';
|
|
6
|
+
|
|
7
|
+
describe('Icon components', () => {
|
|
8
|
+
it('should render CommitIcon with default props', () => {
|
|
9
|
+
render(<CommitIcon />);
|
|
10
|
+
expect(document.querySelector('svg')).toBeInTheDocument();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('should render InvalidIcon with default props', () => {
|
|
14
|
+
render(<InvalidIcon />);
|
|
15
|
+
expect(screen.getByTestId('invalid-icon')).toBeInTheDocument();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should render InvalidIcon with custom props', () => {
|
|
19
|
+
render(<InvalidIcon width="50px" height="50px" style={{ color: 'red' }} />);
|
|
20
|
+
const icon = screen.getByTestId('invalid-icon');
|
|
21
|
+
expect(icon).toHaveAttribute('width', '50px');
|
|
22
|
+
expect(icon).toHaveAttribute('height', '50px');
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -4,7 +4,12 @@ import ExpandedIcon from '../../icons/ExpandedIcon';
|
|
|
4
4
|
import AddItemIcon from '../../icons/AddItemIcon';
|
|
5
5
|
import DJClientContext from '../../providers/djclient';
|
|
6
6
|
|
|
7
|
-
const Explorer = ({
|
|
7
|
+
const Explorer = ({
|
|
8
|
+
item = [],
|
|
9
|
+
current,
|
|
10
|
+
isTopLevel = false,
|
|
11
|
+
namespaceSources = {},
|
|
12
|
+
}) => {
|
|
8
13
|
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
9
14
|
const [items, setItems] = useState([]);
|
|
10
15
|
const [expand, setExpand] = useState(false);
|
|
@@ -106,13 +111,65 @@ const Explorer = ({ item = [], current, isTopLevel = false }) => {
|
|
|
106
111
|
}}
|
|
107
112
|
>
|
|
108
113
|
{items.children && items.children.length > 0 ? (
|
|
109
|
-
<span
|
|
114
|
+
<span
|
|
115
|
+
style={{
|
|
116
|
+
fontSize: '10px',
|
|
117
|
+
color: '#94a3b8',
|
|
118
|
+
width: '12px',
|
|
119
|
+
display: 'flex',
|
|
120
|
+
alignItems: 'center',
|
|
121
|
+
}}
|
|
122
|
+
>
|
|
110
123
|
{!expand ? <CollapsedIcon /> : <ExpandedIcon />}
|
|
111
124
|
</span>
|
|
112
125
|
) : (
|
|
113
|
-
<span style={{
|
|
126
|
+
<span style={{ width: '12px' }} />
|
|
114
127
|
)}
|
|
115
128
|
<a href={`/namespaces/${items.path}`}>{items.namespace}</a>
|
|
129
|
+
{/* Deployment source badge */}
|
|
130
|
+
{namespaceSources[items.path] &&
|
|
131
|
+
namespaceSources[items.path].total_deployments > 0 &&
|
|
132
|
+
namespaceSources[items.path].primary_source?.type === 'git' && (
|
|
133
|
+
<span
|
|
134
|
+
title={`Git: ${
|
|
135
|
+
namespaceSources[items.path].primary_source.repository ||
|
|
136
|
+
'unknown'
|
|
137
|
+
}${
|
|
138
|
+
namespaceSources[items.path].primary_source.branch
|
|
139
|
+
? ` (${namespaceSources[items.path].primary_source.branch})`
|
|
140
|
+
: ''
|
|
141
|
+
}`}
|
|
142
|
+
style={{
|
|
143
|
+
marginLeft: '6px',
|
|
144
|
+
fontSize: '9px',
|
|
145
|
+
padding: '1px 4px',
|
|
146
|
+
borderRadius: '3px',
|
|
147
|
+
backgroundColor: '#d4edda',
|
|
148
|
+
color: '#155724',
|
|
149
|
+
display: 'inline-flex',
|
|
150
|
+
alignItems: 'center',
|
|
151
|
+
gap: '2px',
|
|
152
|
+
}}
|
|
153
|
+
>
|
|
154
|
+
<svg
|
|
155
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
156
|
+
width="10"
|
|
157
|
+
height="10"
|
|
158
|
+
viewBox="0 0 24 24"
|
|
159
|
+
fill="none"
|
|
160
|
+
stroke="currentColor"
|
|
161
|
+
strokeWidth="2"
|
|
162
|
+
strokeLinecap="round"
|
|
163
|
+
strokeLinejoin="round"
|
|
164
|
+
>
|
|
165
|
+
<line x1="6" y1="3" x2="6" y2="15"></line>
|
|
166
|
+
<circle cx="18" cy="6" r="3"></circle>
|
|
167
|
+
<circle cx="6" cy="18" r="3"></circle>
|
|
168
|
+
<path d="M18 9a9 9 0 0 1-9 9"></path>
|
|
169
|
+
</svg>
|
|
170
|
+
Git
|
|
171
|
+
</span>
|
|
172
|
+
)}
|
|
116
173
|
<button
|
|
117
174
|
className="namespace-add-button"
|
|
118
175
|
onClick={e => {
|
|
@@ -144,10 +201,10 @@ const Explorer = ({ item = [], current, isTopLevel = false }) => {
|
|
|
144
201
|
{isCreatingChild && (
|
|
145
202
|
<div
|
|
146
203
|
style={{
|
|
147
|
-
paddingLeft: '
|
|
148
|
-
marginLeft: '
|
|
149
|
-
borderLeft: '1px solid
|
|
150
|
-
marginTop: '
|
|
204
|
+
paddingLeft: '0.55rem',
|
|
205
|
+
marginLeft: '0.25rem',
|
|
206
|
+
borderLeft: '1px solid #e2e8f0',
|
|
207
|
+
marginTop: '2px',
|
|
151
208
|
}}
|
|
152
209
|
>
|
|
153
210
|
<form
|
|
@@ -205,9 +262,9 @@ const Explorer = ({ item = [], current, isTopLevel = false }) => {
|
|
|
205
262
|
items.children.map((item, index) => (
|
|
206
263
|
<div
|
|
207
264
|
style={{
|
|
208
|
-
paddingLeft: '
|
|
209
|
-
marginLeft: '
|
|
210
|
-
borderLeft: '1px solid
|
|
265
|
+
paddingLeft: '0.55rem',
|
|
266
|
+
marginLeft: '0.25rem',
|
|
267
|
+
borderLeft: '1px solid #e2e8f0',
|
|
211
268
|
}}
|
|
212
269
|
key={index}
|
|
213
270
|
>
|
|
@@ -219,6 +276,7 @@ const Explorer = ({ item = [], current, isTopLevel = false }) => {
|
|
|
219
276
|
item={item}
|
|
220
277
|
current={highlight}
|
|
221
278
|
isTopLevel={false}
|
|
279
|
+
namespaceSources={namespaceSources}
|
|
222
280
|
/>
|
|
223
281
|
</div>
|
|
224
282
|
</div>
|
|
@@ -14,6 +14,8 @@ const mockDjClient = {
|
|
|
14
14
|
whoami: jest.fn(),
|
|
15
15
|
users: jest.fn(),
|
|
16
16
|
listTags: jest.fn(),
|
|
17
|
+
namespaceSources: jest.fn(),
|
|
18
|
+
namespaceSourcesBulk: jest.fn(),
|
|
17
19
|
};
|
|
18
20
|
|
|
19
21
|
const mockCurrentUser = { username: 'dj', email: 'dj@test.com' };
|
|
@@ -67,6 +69,10 @@ describe('NamespacePage', () => {
|
|
|
67
69
|
{ name: 'tag1' },
|
|
68
70
|
{ name: 'tag2' },
|
|
69
71
|
]);
|
|
72
|
+
mockDjClient.namespaceSources.mockResolvedValue({ sources: [] });
|
|
73
|
+
mockDjClient.namespaceSourcesBulk.mockResolvedValue({
|
|
74
|
+
namespace_sources: {},
|
|
75
|
+
});
|
|
70
76
|
mockDjClient.namespaces.mockResolvedValue([
|
|
71
77
|
{
|
|
72
78
|
namespace: 'common.one',
|
|
@@ -280,13 +286,15 @@ describe('NamespacePage', () => {
|
|
|
280
286
|
|
|
281
287
|
// Wait for namespaces to load
|
|
282
288
|
await waitFor(() => {
|
|
283
|
-
expect(screen.
|
|
289
|
+
expect(screen.getAllByText('default').length).toBeGreaterThan(0);
|
|
284
290
|
});
|
|
285
291
|
|
|
286
|
-
// Find the namespace
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
.
|
|
292
|
+
// Find the namespace in the sidebar by looking for the link with specific href
|
|
293
|
+
const allLinks = screen.getAllByRole('link');
|
|
294
|
+
const defaultNamespaceLink = allLinks.find(
|
|
295
|
+
link => link.getAttribute('href') === '/namespaces/default',
|
|
296
|
+
);
|
|
297
|
+
const defaultNamespace = defaultNamespaceLink.closest('.select-name');
|
|
290
298
|
fireEvent.mouseEnter(defaultNamespace);
|
|
291
299
|
|
|
292
300
|
// Find the add namespace button (it exists but is hidden, so use getAllByTitle)
|
|
@@ -340,13 +348,15 @@ describe('NamespacePage', () => {
|
|
|
340
348
|
|
|
341
349
|
// Wait for namespaces to load
|
|
342
350
|
await waitFor(() => {
|
|
343
|
-
expect(screen.
|
|
351
|
+
expect(screen.getAllByText('default').length).toBeGreaterThan(0);
|
|
344
352
|
});
|
|
345
353
|
|
|
346
|
-
// Find the namespace
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
.
|
|
354
|
+
// Find the namespace in the sidebar by looking for the link with specific href
|
|
355
|
+
const allLinks = screen.getAllByRole('link');
|
|
356
|
+
const defaultNamespaceLink = allLinks.find(
|
|
357
|
+
link => link.getAttribute('href') === '/namespaces/default',
|
|
358
|
+
);
|
|
359
|
+
const defaultNamespace = defaultNamespaceLink.closest('.select-name');
|
|
350
360
|
fireEvent.mouseEnter(defaultNamespace);
|
|
351
361
|
|
|
352
362
|
// Find the add namespace button (it exists but is hidden, so use getAllByTitle)
|
|
@@ -384,7 +394,7 @@ describe('NamespacePage', () => {
|
|
|
384
394
|
renderWithProviders(<NamespacePage />);
|
|
385
395
|
|
|
386
396
|
await waitFor(() => {
|
|
387
|
-
expect(screen.getByText('Quick
|
|
397
|
+
expect(screen.getByText('Quick')).toBeInTheDocument();
|
|
388
398
|
});
|
|
389
399
|
|
|
390
400
|
// Check that preset buttons are rendered
|