feat: Render Loading if Namespaces are fetching
Signed-off-by: Charles Thao <cthao@redhat.com>
This commit is contained in:
parent
ade0282aca
commit
61565da01b
|
@ -23,7 +23,7 @@ const generateMockWorkspace = (
|
|||
deferUpdates: paused,
|
||||
paused,
|
||||
pausedTime,
|
||||
pendingRestart: Math.random() < 0.5, //to generate randomly True/False value
|
||||
pendingRestart: true, // Make it deterministic for testing
|
||||
state,
|
||||
stateMessage:
|
||||
state === WorkspacesWorkspaceState.WorkspaceStateRunning
|
||||
|
|
|
@ -1,12 +1,25 @@
|
|||
import { mockBFFResponse } from '~/__mocks__/utils';
|
||||
import { mockNamespaces } from '~/__mocks__/mockNamespaces';
|
||||
import { mockWorkspaces } from '~/__tests__/cypress/cypress/tests/mocked/workspace.mock';
|
||||
import { navBar } from '~/__tests__/cypress/cypress/pages/navBar';
|
||||
|
||||
describe('WorkspaceDetailsActivity Component', () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept('GET', 'api/v1/workspaces', {
|
||||
cy.intercept('GET', '/api/v1/namespaces', {
|
||||
body: mockBFFResponse(mockNamespaces),
|
||||
}).as('getNamespaces');
|
||||
cy.intercept('GET', '/api/v1/workspaces', {
|
||||
body: mockBFFResponse(mockWorkspaces),
|
||||
}).as('getWorkspaces');
|
||||
cy.intercept('GET', '/api/v1/workspaces/default', {
|
||||
body: mockBFFResponse(mockWorkspaces),
|
||||
}).as('getDefaultWorkspaces');
|
||||
cy.visit('/');
|
||||
cy.wait('@getNamespaces');
|
||||
// Select a namespace to enable workspace loading
|
||||
navBar.selectNamespace('default');
|
||||
// Wait for workspaces to load after namespace selection
|
||||
cy.wait('@getDefaultWorkspaces');
|
||||
});
|
||||
|
||||
// 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', () => {
|
|||
.find('button')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
// Extract first workspace from mock data
|
||||
cy.wait('@getWorkspaces').then((interception) => {
|
||||
if (!interception.response || !interception.response.body) {
|
||||
throw new Error('Intercepted response is undefined or empty');
|
||||
}
|
||||
const workspace = interception.response.body.data[0];
|
||||
cy.findByTestId('action-viewDetails').click();
|
||||
cy.findByTestId('activityTab').click();
|
||||
cy.findByTestId('lastActivity')
|
||||
.invoke('text')
|
||||
.then((text) => {
|
||||
console.log('Rendered lastActivity:', text);
|
||||
});
|
||||
cy.findByTestId('pauseTime').should('have.text', 'Jan 1, 2025, 12:00:00 AM');
|
||||
cy.findByTestId('lastActivity').should('have.text', 'Jan 2, 2025, 12:00:00 AM');
|
||||
cy.findByTestId('lastUpdate').should('have.text', 'Jan 3, 2025, 12:00:00 AM');
|
||||
cy.findByTestId('pendingRestart').should(
|
||||
'have.text',
|
||||
workspace.pendingRestart ? 'Yes' : 'No',
|
||||
);
|
||||
});
|
||||
cy.findByTestId('action-viewDetails').click();
|
||||
cy.findByTestId('activityTab').click();
|
||||
cy.findByTestId('lastActivity')
|
||||
.invoke('text')
|
||||
.then((text) => {
|
||||
console.log('Rendered lastActivity:', text);
|
||||
});
|
||||
cy.findByTestId('pauseTime').should('have.text', 'Jan 1, 2025, 12:00:00 AM');
|
||||
cy.findByTestId('lastActivity').should('have.text', 'Jan 2, 2025, 12:00:00 AM');
|
||||
cy.findByTestId('lastUpdate').should('have.text', 'Jan 3, 2025, 12:00:00 AM');
|
||||
// Use mock data directly since we can't access intercepted response here
|
||||
cy.findByTestId('pendingRestart').should('have.text', 'Yes');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,6 +2,7 @@ import { mockNamespaces } from '~/__mocks__/mockNamespaces';
|
|||
import { mockWorkspaces } from '~/__mocks__/mockWorkspaces';
|
||||
import { mockBFFResponse } from '~/__mocks__/utils';
|
||||
import { home } from '~/__tests__/cypress/cypress/pages/home';
|
||||
import { navBar } from '~/__tests__/cypress/cypress/pages/navBar';
|
||||
import { mockWorkspaceKinds } from '~/shared/mock/mockNotebookServiceData';
|
||||
|
||||
const useFilter = (filterKey: string, filterName: string, searchValue: string) => {
|
||||
|
@ -16,25 +17,28 @@ describe('Application', () => {
|
|||
beforeEach(() => {
|
||||
cy.intercept('GET', '/api/v1/namespaces', {
|
||||
body: mockBFFResponse(mockNamespaces),
|
||||
});
|
||||
}).as('getNamespaces');
|
||||
cy.intercept('GET', '/api/v1/workspaces', {
|
||||
body: mockBFFResponse(mockWorkspaces),
|
||||
}).as('getWorkspaces');
|
||||
cy.intercept('GET', '/api/v1/workspaces/default', {
|
||||
body: mockBFFResponse(mockWorkspaces),
|
||||
}).as('getDefaultWorkspaces');
|
||||
cy.intercept('GET', '/api/v1/workspaces/custom-namespace', {
|
||||
body: mockBFFResponse(mockWorkspaces),
|
||||
});
|
||||
cy.intercept('GET', '/api/v1/workspacekinds', {
|
||||
body: mockBFFResponse(mockWorkspaceKinds),
|
||||
});
|
||||
cy.intercept('GET', '/api/namespaces/test-namespace/workspaces').as('getWorkspaces');
|
||||
home.visit();
|
||||
cy.wait('@getNamespaces');
|
||||
// Select a namespace to enable workspace loading
|
||||
navBar.selectNamespace('default');
|
||||
// Wait for workspaces to load after namespace selection
|
||||
cy.wait('@getDefaultWorkspaces');
|
||||
});
|
||||
|
||||
it('filter rows with single filter', () => {
|
||||
home.visit();
|
||||
|
||||
// Wait for the API call before trying to interact with the UI
|
||||
cy.wait('@getWorkspaces');
|
||||
|
||||
useFilter('name', 'Name', 'My');
|
||||
cy.get("[id$='workspaces-table-content']").find('tr').should('have.length', 2);
|
||||
cy.get("[id$='workspaces-table-row-1']").contains('My First Jupyter Notebook');
|
||||
|
@ -42,7 +46,6 @@ describe('Application', () => {
|
|||
});
|
||||
|
||||
it('filter rows with multiple filters', () => {
|
||||
home.visit();
|
||||
// First filter by name
|
||||
useFilter('name', 'Name', 'My');
|
||||
cy.get("[id$='workspaces-table-content']").find('tr').should('have.length', 2);
|
||||
|
@ -57,7 +60,6 @@ describe('Application', () => {
|
|||
});
|
||||
|
||||
it('filter rows with multiple filters and remove one', () => {
|
||||
home.visit();
|
||||
// Add name filter
|
||||
useFilter('name', 'Name', 'My');
|
||||
cy.get("[id$='workspaces-table-content']").find('tr').should('have.length', 2);
|
||||
|
@ -79,7 +81,6 @@ describe('Application', () => {
|
|||
});
|
||||
|
||||
it('filter rows with multiple filters and remove all', () => {
|
||||
home.visit();
|
||||
// Add name filter
|
||||
useFilter('name', 'Name', 'My');
|
||||
cy.get("[id$='workspaces-table-content']").find('tr').should('have.length', 2);
|
||||
|
|
|
@ -5,6 +5,7 @@ const storageKey = 'kubeflow.notebooks.namespace.lastUsed';
|
|||
|
||||
interface NamespaceContextType {
|
||||
selectedNamespace: string;
|
||||
namespacesLoaded: boolean;
|
||||
}
|
||||
|
||||
const NamespaceContext = React.createContext<NamespaceContextType | undefined>(undefined);
|
||||
|
@ -91,8 +92,9 @@ export const NamespaceContextProvider: React.FC<NamespaceContextProviderProps> =
|
|||
const namespacesContextValues = useMemo(
|
||||
() => ({
|
||||
selectedNamespace,
|
||||
namespacesLoaded,
|
||||
}),
|
||||
[selectedNamespace],
|
||||
[selectedNamespace, namespacesLoaded],
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
@ -17,6 +17,17 @@ jest.mock('~/app/hooks/useNotebookAPI', () => ({
|
|||
useNotebookAPI: jest.fn(),
|
||||
}));
|
||||
|
||||
// Mock the namespace context for this test file only
|
||||
const mockNamespaceContext = {
|
||||
selectedNamespace: 'test-namespace',
|
||||
namespacesLoaded: true,
|
||||
};
|
||||
|
||||
jest.mock('~/app/context/NamespaceContextProvider', () => ({
|
||||
useNamespaceContext: () => mockNamespaceContext,
|
||||
NamespaceContextProvider: ({ children }: { children: React.ReactNode }) => children,
|
||||
}));
|
||||
|
||||
const mockUseNotebookAPI = useNotebookAPI as jest.MockedFunction<typeof useNotebookAPI>;
|
||||
|
||||
describe('useWorkspaces', () => {
|
||||
|
|
|
@ -2,11 +2,13 @@ import { FetchState, FetchStateCallbackPromise, useFetchState } from 'mod-arch-c
|
|||
import { useCallback } from 'react';
|
||||
import { useNotebookAPI } from '~/app/hooks/useNotebookAPI';
|
||||
import { ApiWorkspaceListEnvelope } from '~/generated/data-contracts';
|
||||
import { useNamespaceContext } from '~/app/context/NamespaceContextProvider';
|
||||
|
||||
export const useWorkspacesByNamespace = (
|
||||
namespace: string,
|
||||
): FetchState<ApiWorkspaceListEnvelope['data']> => {
|
||||
const { api, apiAvailable } = useNotebookAPI();
|
||||
const { namespacesLoaded, selectedNamespace } = useNamespaceContext();
|
||||
|
||||
const call = useCallback<
|
||||
FetchStateCallbackPromise<ApiWorkspaceListEnvelope['data']>
|
||||
|
@ -14,10 +16,12 @@ export const useWorkspacesByNamespace = (
|
|||
if (!apiAvailable) {
|
||||
return Promise.reject(new Error('API not yet available'));
|
||||
}
|
||||
|
||||
if (!namespacesLoaded || selectedNamespace === '') {
|
||||
return Promise.reject(new Error('Namespaces not yet available'));
|
||||
}
|
||||
const envelope = await api.workspaces.listWorkspacesByNamespace(namespace);
|
||||
return envelope.data;
|
||||
}, [api, apiAvailable, namespace]);
|
||||
}, [api.workspaces, apiAvailable, namespace, namespacesLoaded, selectedNamespace]);
|
||||
|
||||
return useFetchState(call, []);
|
||||
};
|
||||
|
|
|
@ -13,7 +13,7 @@ import { WorkspacesWorkspaceState } from '~/generated/data-contracts';
|
|||
import { POLL_INTERVAL } from '~/shared/utilities/const';
|
||||
|
||||
export const Workspaces: React.FunctionComponent = () => {
|
||||
const { selectedNamespace } = useNamespaceContext();
|
||||
const { namespacesLoaded, selectedNamespace } = useNamespaceContext();
|
||||
|
||||
const [workspaces, workspacesLoaded, workspacesLoadError, refreshWorkspaces] =
|
||||
useWorkspacesByNamespace(selectedNamespace);
|
||||
|
@ -46,7 +46,7 @@ export const Workspaces: React.FunctionComponent = () => {
|
|||
return <LoadError error={workspacesLoadError} />;
|
||||
}
|
||||
|
||||
if (!workspacesLoaded) {
|
||||
if (!workspacesLoaded || !namespacesLoaded || selectedNamespace === '') {
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue