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