Skip to content

Commit d71a3f5

Browse files
authored
feat(ws): use workspace counts from API response (#508)
Signed-off-by: Guilherme Caponetto <[email protected]>
1 parent 3fb42da commit d71a3f5

File tree

7 files changed

+461
-32
lines changed

7 files changed

+461
-32
lines changed

workspaces/frontend/src/app/context/NamespaceContextProvider.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export const NamespaceContextProvider: React.FC<NamespaceContextProviderProps> =
3838
const namespaceNames = namespacesData.map((ns) => ns.name);
3939
setNamespaces(namespaceNames);
4040
setSelectedNamespace(lastUsedNamespace.length ? lastUsedNamespace : namespaceNames[0]);
41-
if (!lastUsedNamespace.length) {
41+
if (!lastUsedNamespace.length || !namespaceNames.includes(lastUsedNamespace)) {
4242
setLastUsedNamespace(storageKey, namespaceNames[0]);
4343
}
4444
} else {
Lines changed: 341 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,341 @@
1+
import { waitFor } from '@testing-library/react';
2+
import { renderHook } from '~/__tests__/unit/testUtils/hooks';
3+
import { useNotebookAPI } from '~/app/hooks/useNotebookAPI';
4+
import { useWorkspaceCountPerKind } from '~/app/hooks/useWorkspaceCountPerKind';
5+
import {
6+
Workspace,
7+
WorkspaceImageConfigValue,
8+
WorkspaceKind,
9+
WorkspaceKindInfo,
10+
WorkspacePodConfigValue,
11+
} from '~/shared/api/backendApiTypes';
12+
import { NotebookAPIs } from '~/shared/api/notebookApi';
13+
import { buildMockWorkspace, buildMockWorkspaceKind } from '~/shared/mock/mockBuilder';
14+
15+
jest.mock('~/app/hooks/useNotebookAPI', () => ({
16+
useNotebookAPI: jest.fn(),
17+
}));
18+
19+
const mockUseNotebookAPI = useNotebookAPI as jest.MockedFunction<typeof useNotebookAPI>;
20+
21+
const baseWorkspaceKindInfoTest: WorkspaceKindInfo = {
22+
name: 'jupyter',
23+
missing: false,
24+
icon: { url: '' },
25+
logo: { url: '' },
26+
};
27+
28+
const baseWorkspaceTest = buildMockWorkspace({
29+
name: 'workspace',
30+
namespace: 'namespace',
31+
workspaceKind: baseWorkspaceKindInfoTest,
32+
});
33+
34+
const baseImageConfigTest: WorkspaceImageConfigValue = {
35+
id: 'image',
36+
displayName: 'Image',
37+
description: 'Test image',
38+
labels: [],
39+
hidden: false,
40+
clusterMetrics: undefined,
41+
};
42+
43+
const basePodConfigTest: WorkspacePodConfigValue = {
44+
id: 'podConfig',
45+
displayName: 'Pod Config',
46+
description: 'Test pod config',
47+
labels: [],
48+
hidden: false,
49+
clusterMetrics: undefined,
50+
};
51+
52+
describe('useWorkspaceCountPerKind', () => {
53+
const mockListAllWorkspaces = jest.fn();
54+
const mockListWorkspaceKinds = jest.fn();
55+
56+
const mockApi: Partial<NotebookAPIs> = {
57+
listAllWorkspaces: mockListAllWorkspaces,
58+
listWorkspaceKinds: mockListWorkspaceKinds,
59+
};
60+
61+
beforeEach(() => {
62+
jest.clearAllMocks();
63+
mockUseNotebookAPI.mockReturnValue({
64+
api: mockApi as NotebookAPIs,
65+
apiAvailable: true,
66+
refreshAllAPI: jest.fn(),
67+
});
68+
});
69+
70+
it('should return empty object initially', () => {
71+
mockListAllWorkspaces.mockResolvedValue([]);
72+
mockListWorkspaceKinds.mockResolvedValue([]);
73+
74+
const { result } = renderHook(() => useWorkspaceCountPerKind());
75+
76+
waitFor(() => {
77+
expect(result.current).toEqual({});
78+
});
79+
});
80+
81+
it('should fetch and calculate workspace counts on mount', async () => {
82+
const mockWorkspaces: Workspace[] = [
83+
{
84+
...baseWorkspaceTest,
85+
name: 'workspace1',
86+
namespace: 'namespace1',
87+
workspaceKind: { ...baseWorkspaceKindInfoTest, name: 'jupyter1' },
88+
},
89+
{
90+
...baseWorkspaceTest,
91+
name: 'workspace2',
92+
namespace: 'namespace1',
93+
workspaceKind: { ...baseWorkspaceKindInfoTest, name: 'jupyter1' },
94+
},
95+
{
96+
...baseWorkspaceTest,
97+
name: 'workspace3',
98+
namespace: 'namespace2',
99+
workspaceKind: { ...baseWorkspaceKindInfoTest, name: 'jupyter2' },
100+
},
101+
];
102+
103+
const mockWorkspaceKinds: WorkspaceKind[] = [
104+
buildMockWorkspaceKind({
105+
name: 'jupyter1',
106+
clusterMetrics: { workspacesCount: 10 },
107+
podTemplate: {
108+
podMetadata: { labels: {}, annotations: {} },
109+
volumeMounts: { home: '/home' },
110+
options: {
111+
imageConfig: {
112+
default: 'image1',
113+
values: [
114+
{
115+
...baseImageConfigTest,
116+
id: 'image1',
117+
clusterMetrics: { workspacesCount: 1 },
118+
},
119+
{
120+
...baseImageConfigTest,
121+
id: 'image2',
122+
clusterMetrics: { workspacesCount: 2 },
123+
},
124+
],
125+
},
126+
podConfig: {
127+
default: 'podConfig1',
128+
values: [
129+
{
130+
...basePodConfigTest,
131+
id: 'podConfig1',
132+
clusterMetrics: { workspacesCount: 3 },
133+
},
134+
{
135+
...basePodConfigTest,
136+
id: 'podConfig2',
137+
clusterMetrics: { workspacesCount: 4 },
138+
},
139+
],
140+
},
141+
},
142+
},
143+
}),
144+
buildMockWorkspaceKind({
145+
name: 'jupyter2',
146+
clusterMetrics: { workspacesCount: 20 },
147+
podTemplate: {
148+
podMetadata: { labels: {}, annotations: {} },
149+
volumeMounts: { home: '/home' },
150+
options: {
151+
imageConfig: {
152+
default: 'image1',
153+
values: [
154+
{
155+
...baseImageConfigTest,
156+
id: 'image1',
157+
clusterMetrics: { workspacesCount: 11 },
158+
},
159+
],
160+
},
161+
podConfig: {
162+
default: 'podConfig1',
163+
values: [
164+
{
165+
...basePodConfigTest,
166+
id: 'podConfig1',
167+
clusterMetrics: { workspacesCount: 12 },
168+
},
169+
],
170+
},
171+
},
172+
},
173+
}),
174+
];
175+
176+
mockListAllWorkspaces.mockResolvedValue(mockWorkspaces);
177+
mockListWorkspaceKinds.mockResolvedValue(mockWorkspaceKinds);
178+
179+
const { result } = renderHook(() => useWorkspaceCountPerKind());
180+
181+
await waitFor(() => {
182+
expect(result.current).toEqual({
183+
jupyter1: {
184+
count: 10,
185+
countByImage: {
186+
image1: 1,
187+
image2: 2,
188+
},
189+
countByPodConfig: {
190+
podConfig1: 3,
191+
podConfig2: 4,
192+
},
193+
countByNamespace: {
194+
namespace1: 2,
195+
},
196+
},
197+
jupyter2: {
198+
count: 20,
199+
countByImage: {
200+
image1: 11,
201+
},
202+
countByPodConfig: {
203+
podConfig1: 12,
204+
},
205+
countByNamespace: {
206+
namespace2: 1,
207+
},
208+
},
209+
});
210+
});
211+
});
212+
213+
it('should handle missing cluster metrics gracefully', async () => {
214+
const mockEmptyWorkspaces: Workspace[] = [];
215+
const mockWorkspaceKinds: WorkspaceKind[] = [
216+
buildMockWorkspaceKind({
217+
name: 'no-metrics',
218+
clusterMetrics: undefined,
219+
podTemplate: {
220+
podMetadata: { labels: {}, annotations: {} },
221+
volumeMounts: { home: '/home' },
222+
options: {
223+
imageConfig: {
224+
default: baseImageConfigTest.id,
225+
values: [{ ...baseImageConfigTest }],
226+
},
227+
podConfig: {
228+
default: basePodConfigTest.id,
229+
values: [{ ...basePodConfigTest }],
230+
},
231+
},
232+
},
233+
}),
234+
buildMockWorkspaceKind({
235+
name: 'no-metrics-2',
236+
clusterMetrics: undefined,
237+
podTemplate: {
238+
podMetadata: { labels: {}, annotations: {} },
239+
volumeMounts: { home: '/home' },
240+
options: {
241+
imageConfig: {
242+
default: 'empty',
243+
values: [],
244+
},
245+
podConfig: {
246+
default: 'empty',
247+
values: [],
248+
},
249+
},
250+
},
251+
}),
252+
];
253+
254+
mockListAllWorkspaces.mockResolvedValue(mockEmptyWorkspaces);
255+
mockListWorkspaceKinds.mockResolvedValue(mockWorkspaceKinds);
256+
257+
const { result } = renderHook(() => useWorkspaceCountPerKind());
258+
259+
await waitFor(() => {
260+
expect(result.current).toEqual({
261+
'no-metrics': {
262+
count: 0,
263+
countByImage: {
264+
image: 0,
265+
},
266+
countByPodConfig: {
267+
podConfig: 0,
268+
},
269+
countByNamespace: {},
270+
},
271+
'no-metrics-2': {
272+
count: 0,
273+
countByImage: {},
274+
countByPodConfig: {},
275+
countByNamespace: {},
276+
},
277+
});
278+
});
279+
});
280+
281+
it('should return empty object in case of API errors rather than propagating them', async () => {
282+
mockListAllWorkspaces.mockRejectedValue(new Error('API Error'));
283+
mockListWorkspaceKinds.mockRejectedValue(new Error('API Error'));
284+
285+
const { result } = renderHook(() => useWorkspaceCountPerKind());
286+
287+
await waitFor(() => {
288+
expect(result.current).toEqual({});
289+
});
290+
});
291+
292+
it('should handle empty workspace kinds array', async () => {
293+
mockListWorkspaceKinds.mockResolvedValue([]);
294+
295+
const { result } = renderHook(() => useWorkspaceCountPerKind());
296+
297+
await waitFor(() => {
298+
expect(result.current).toEqual({});
299+
});
300+
});
301+
302+
it('should handle workspaces with no matching kinds', async () => {
303+
const mockWorkspaces: Workspace[] = [baseWorkspaceTest];
304+
const workspaceKind = buildMockWorkspaceKind({
305+
name: 'nomatch',
306+
clusterMetrics: { workspacesCount: 0 },
307+
podTemplate: {
308+
podMetadata: { labels: {}, annotations: {} },
309+
volumeMounts: { home: '/home' },
310+
options: {
311+
imageConfig: {
312+
default: baseImageConfigTest.id,
313+
values: [{ ...baseImageConfigTest }],
314+
},
315+
podConfig: {
316+
default: basePodConfigTest.id,
317+
values: [{ ...basePodConfigTest }],
318+
},
319+
},
320+
},
321+
});
322+
323+
const mockWorkspaceKinds: WorkspaceKind[] = [workspaceKind];
324+
325+
mockListAllWorkspaces.mockResolvedValue(mockWorkspaces);
326+
mockListWorkspaceKinds.mockResolvedValue(mockWorkspaceKinds);
327+
328+
const { result } = renderHook(() => useWorkspaceCountPerKind());
329+
330+
await waitFor(() => {
331+
expect(result.current).toEqual({
332+
[workspaceKind.name]: {
333+
count: 0,
334+
countByImage: { [baseImageConfigTest.id]: 0 },
335+
countByPodConfig: { [basePodConfigTest.id]: 0 },
336+
countByNamespace: {},
337+
},
338+
});
339+
});
340+
});
341+
});

0 commit comments

Comments
 (0)