Skip to content

Commit fa5fb5a

Browse files
authored
feat: Render Loading if Namespaces are fetching (#604)
Signed-off-by: Charles Thao <[email protected]>
1 parent 426c6ad commit fa5fb5a

File tree

7 files changed

+60
-38
lines changed

7 files changed

+60
-38
lines changed

workspaces/frontend/src/__tests__/cypress/cypress/tests/mocked/workspace.mock.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const generateMockWorkspace = (
2323
deferUpdates: paused,
2424
paused,
2525
pausedTime,
26-
pendingRestart: Math.random() < 0.5, //to generate randomly True/False value
26+
pendingRestart: true, // Make it deterministic for testing
2727
state,
2828
stateMessage:
2929
state === WorkspacesWorkspaceState.WorkspaceStateRunning
Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,25 @@
11
import { mockBFFResponse } from '~/__mocks__/utils';
2+
import { mockNamespaces } from '~/__mocks__/mockNamespaces';
23
import { mockWorkspaces } from '~/__tests__/cypress/cypress/tests/mocked/workspace.mock';
4+
import { navBar } from '~/__tests__/cypress/cypress/pages/navBar';
35

46
describe('WorkspaceDetailsActivity Component', () => {
57
beforeEach(() => {
6-
cy.intercept('GET', 'api/v1/workspaces', {
8+
cy.intercept('GET', '/api/v1/namespaces', {
9+
body: mockBFFResponse(mockNamespaces),
10+
}).as('getNamespaces');
11+
cy.intercept('GET', '/api/v1/workspaces', {
712
body: mockBFFResponse(mockWorkspaces),
813
}).as('getWorkspaces');
14+
cy.intercept('GET', '/api/v1/workspaces/default', {
15+
body: mockBFFResponse(mockWorkspaces),
16+
}).as('getDefaultWorkspaces');
917
cy.visit('/');
18+
cy.wait('@getNamespaces');
19+
// Select a namespace to enable workspace loading
20+
navBar.selectNamespace('default');
21+
// Wait for workspaces to load after namespace selection
22+
cy.wait('@getDefaultWorkspaces');
1023
});
1124

1225
// This tests depends on the mocked workspaces data at home page, needs revisit once workspace data fetched from BE
@@ -17,26 +30,17 @@ describe('WorkspaceDetailsActivity Component', () => {
1730
.find('button')
1831
.should('be.visible')
1932
.click();
20-
// Extract first workspace from mock data
21-
cy.wait('@getWorkspaces').then((interception) => {
22-
if (!interception.response || !interception.response.body) {
23-
throw new Error('Intercepted response is undefined or empty');
24-
}
25-
const workspace = interception.response.body.data[0];
26-
cy.findByTestId('action-viewDetails').click();
27-
cy.findByTestId('activityTab').click();
28-
cy.findByTestId('lastActivity')
29-
.invoke('text')
30-
.then((text) => {
31-
console.log('Rendered lastActivity:', text);
32-
});
33-
cy.findByTestId('pauseTime').should('have.text', 'Jan 1, 2025, 12:00:00 AM');
34-
cy.findByTestId('lastActivity').should('have.text', 'Jan 2, 2025, 12:00:00 AM');
35-
cy.findByTestId('lastUpdate').should('have.text', 'Jan 3, 2025, 12:00:00 AM');
36-
cy.findByTestId('pendingRestart').should(
37-
'have.text',
38-
workspace.pendingRestart ? 'Yes' : 'No',
39-
);
40-
});
33+
cy.findByTestId('action-viewDetails').click();
34+
cy.findByTestId('activityTab').click();
35+
cy.findByTestId('lastActivity')
36+
.invoke('text')
37+
.then((text) => {
38+
console.log('Rendered lastActivity:', text);
39+
});
40+
cy.findByTestId('pauseTime').should('have.text', 'Jan 1, 2025, 12:00:00 AM');
41+
cy.findByTestId('lastActivity').should('have.text', 'Jan 2, 2025, 12:00:00 AM');
42+
cy.findByTestId('lastUpdate').should('have.text', 'Jan 3, 2025, 12:00:00 AM');
43+
// Use mock data directly since we can't access intercepted response here
44+
cy.findByTestId('pendingRestart').should('have.text', 'Yes');
4145
});
4246
});

workspaces/frontend/src/__tests__/cypress/cypress/tests/mocked/workspaces/filterWorkspacesTest.cy.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { mockNamespaces } from '~/__mocks__/mockNamespaces';
22
import { mockWorkspaces } from '~/__mocks__/mockWorkspaces';
33
import { mockBFFResponse } from '~/__mocks__/utils';
44
import { home } from '~/__tests__/cypress/cypress/pages/home';
5+
import { navBar } from '~/__tests__/cypress/cypress/pages/navBar';
56
import { mockWorkspaceKinds } from '~/shared/mock/mockNotebookServiceData';
67

78
const useFilter = (filterKey: string, filterName: string, searchValue: string) => {
@@ -16,33 +17,35 @@ describe('Application', () => {
1617
beforeEach(() => {
1718
cy.intercept('GET', '/api/v1/namespaces', {
1819
body: mockBFFResponse(mockNamespaces),
19-
});
20+
}).as('getNamespaces');
2021
cy.intercept('GET', '/api/v1/workspaces', {
2122
body: mockBFFResponse(mockWorkspaces),
2223
}).as('getWorkspaces');
24+
cy.intercept('GET', '/api/v1/workspaces/default', {
25+
body: mockBFFResponse(mockWorkspaces),
26+
}).as('getDefaultWorkspaces');
2327
cy.intercept('GET', '/api/v1/workspaces/custom-namespace', {
2428
body: mockBFFResponse(mockWorkspaces),
2529
});
2630
cy.intercept('GET', '/api/v1/workspacekinds', {
2731
body: mockBFFResponse(mockWorkspaceKinds),
2832
});
29-
cy.intercept('GET', '/api/namespaces/test-namespace/workspaces').as('getWorkspaces');
33+
home.visit();
34+
cy.wait('@getNamespaces');
35+
// Select a namespace to enable workspace loading
36+
navBar.selectNamespace('default');
37+
// Wait for workspaces to load after namespace selection
38+
cy.wait('@getDefaultWorkspaces');
3039
});
3140

3241
it('filter rows with single filter', () => {
33-
home.visit();
34-
35-
// Wait for the API call before trying to interact with the UI
36-
cy.wait('@getWorkspaces');
37-
3842
useFilter('name', 'Name', 'My');
3943
cy.get("[id$='workspaces-table-content']").find('tr').should('have.length', 2);
4044
cy.get("[id$='workspaces-table-row-1']").contains('My First Jupyter Notebook');
4145
cy.get("[id$='workspaces-table-row-2']").contains('My Second Jupyter Notebook');
4246
});
4347

4448
it('filter rows with multiple filters', () => {
45-
home.visit();
4649
// First filter by name
4750
useFilter('name', 'Name', 'My');
4851
cy.get("[id$='workspaces-table-content']").find('tr').should('have.length', 2);
@@ -57,7 +60,6 @@ describe('Application', () => {
5760
});
5861

5962
it('filter rows with multiple filters and remove one', () => {
60-
home.visit();
6163
// Add name filter
6264
useFilter('name', 'Name', 'My');
6365
cy.get("[id$='workspaces-table-content']").find('tr').should('have.length', 2);
@@ -79,7 +81,6 @@ describe('Application', () => {
7981
});
8082

8183
it('filter rows with multiple filters and remove all', () => {
82-
home.visit();
8384
// Add name filter
8485
useFilter('name', 'Name', 'My');
8586
cy.get("[id$='workspaces-table-content']").find('tr').should('have.length', 2);

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const storageKey = 'kubeflow.notebooks.namespace.lastUsed';
55

66
interface NamespaceContextType {
77
selectedNamespace: string;
8+
namespacesLoaded: boolean;
89
}
910

1011
const NamespaceContext = React.createContext<NamespaceContextType | undefined>(undefined);
@@ -91,8 +92,9 @@ export const NamespaceContextProvider: React.FC<NamespaceContextProviderProps> =
9192
const namespacesContextValues = useMemo(
9293
() => ({
9394
selectedNamespace,
95+
namespacesLoaded,
9496
}),
95-
[selectedNamespace],
97+
[selectedNamespace, namespacesLoaded],
9698
);
9799

98100
return (

workspaces/frontend/src/app/hooks/__tests__/useWorkspaces.spec.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,17 @@ jest.mock('~/app/hooks/useNotebookAPI', () => ({
1717
useNotebookAPI: jest.fn(),
1818
}));
1919

20+
// Mock the namespace context for this test file only
21+
const mockNamespaceContext = {
22+
selectedNamespace: 'test-namespace',
23+
namespacesLoaded: true,
24+
};
25+
26+
jest.mock('~/app/context/NamespaceContextProvider', () => ({
27+
useNamespaceContext: () => mockNamespaceContext,
28+
NamespaceContextProvider: ({ children }: { children: React.ReactNode }) => children,
29+
}));
30+
2031
const mockUseNotebookAPI = useNotebookAPI as jest.MockedFunction<typeof useNotebookAPI>;
2132

2233
describe('useWorkspaces', () => {

workspaces/frontend/src/app/hooks/useWorkspaces.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,26 @@ import { FetchState, FetchStateCallbackPromise, useFetchState } from 'mod-arch-c
22
import { useCallback } from 'react';
33
import { useNotebookAPI } from '~/app/hooks/useNotebookAPI';
44
import { ApiWorkspaceListEnvelope } from '~/generated/data-contracts';
5+
import { useNamespaceContext } from '~/app/context/NamespaceContextProvider';
56

67
export const useWorkspacesByNamespace = (
78
namespace: string,
89
): FetchState<ApiWorkspaceListEnvelope['data']> => {
910
const { api, apiAvailable } = useNotebookAPI();
11+
const { namespacesLoaded, selectedNamespace } = useNamespaceContext();
1012

1113
const call = useCallback<
1214
FetchStateCallbackPromise<ApiWorkspaceListEnvelope['data']>
1315
>(async () => {
1416
if (!apiAvailable) {
1517
return Promise.reject(new Error('API not yet available'));
1618
}
17-
19+
if (!namespacesLoaded || selectedNamespace === '') {
20+
return Promise.reject(new Error('Namespaces not yet available'));
21+
}
1822
const envelope = await api.workspaces.listWorkspacesByNamespace(namespace);
1923
return envelope.data;
20-
}, [api, apiAvailable, namespace]);
24+
}, [api.workspaces, apiAvailable, namespace, namespacesLoaded, selectedNamespace]);
2125

2226
return useFetchState(call, []);
2327
};

workspaces/frontend/src/app/pages/Workspaces/Workspaces.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { WorkspacesWorkspaceState } from '~/generated/data-contracts';
1313
import { POLL_INTERVAL } from '~/shared/utilities/const';
1414

1515
export const Workspaces: React.FunctionComponent = () => {
16-
const { selectedNamespace } = useNamespaceContext();
16+
const { namespacesLoaded, selectedNamespace } = useNamespaceContext();
1717

1818
const [workspaces, workspacesLoaded, workspacesLoadError, refreshWorkspaces] =
1919
useWorkspacesByNamespace(selectedNamespace);
@@ -46,7 +46,7 @@ export const Workspaces: React.FunctionComponent = () => {
4646
return <LoadError error={workspacesLoadError} />;
4747
}
4848

49-
if (!workspacesLoaded) {
49+
if (!workspacesLoaded || !namespacesLoaded || selectedNamespace === '') {
5050
return <LoadingSpinner />;
5151
}
5252

0 commit comments

Comments
 (0)