diff --git a/src/components/Workspace/__tests__/workspace-context.spec.tsx b/src/components/Workspace/__tests__/workspace-context.spec.tsx
new file mode 100644
index 0000000..60bbc17
--- /dev/null
+++ b/src/components/Workspace/__tests__/workspace-context.spec.tsx
@@ -0,0 +1,207 @@
+import { useContext } from 'react';
+import { useQuery } from '@tanstack/react-query';
+import { render, screen, waitFor } from '@testing-library/react';
+import { userEvent } from '@testing-library/user-event';
+import { createReactRouterMock } from '../../../utils/test-utils';
+import { getLastUsedWorkspace, setLastUsedWorkspace } from '../utils';
+import { WorkspaceProvider, WorkspaceContext } from '../workspace-context';
+
+jest.mock('@tanstack/react-query', () => ({
+ ...jest.requireActual('@tanstack/react-query'),
+ useQuery: jest.fn(),
+}));
+
+jest.mock('../utils', () => ({
+ ...jest.requireActual('../utils'),
+ createWorkspaceQueryOptions: jest.fn(),
+ getLastUsedWorkspace: jest.fn(),
+ setLastUsedWorkspace: jest.fn(),
+}));
+
+// Test data
+const mockWorkspaces = [
+ {
+ metadata: {
+ name: 'workspace-1',
+ },
+ status: {
+ namespaces: [{ type: 'default', name: 'test-namespace' }],
+ type: 'home',
+ },
+ },
+ {
+ metadata: {
+ name: 'workspace-2',
+ },
+ status: {
+ namespaces: [{ type: 'default', name: 'test-namespace-2' }],
+ },
+ },
+];
+
+const mockNamespace = 'test-namespace';
+
+const mockUseNavigate = createReactRouterMock('useNavigate');
+const mockUseParams = createReactRouterMock('useParams');
+const mockUseQuery = useQuery as jest.Mock;
+const mockGetLastUsedWorkspace = getLastUsedWorkspace as jest.Mock;
+
+describe('WorkspaceProvider', () => {
+ const mockNavigate = jest.fn();
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockUseNavigate.mockReturnValue(mockNavigate);
+ mockUseParams.mockReturnValue({});
+ mockGetLastUsedWorkspace.mockReturnValue('workspace-1');
+ });
+
+ it('should renders loading spinner when data is being fetched', () => {
+ mockUseQuery
+ .mockReturnValueOnce({
+ data: undefined,
+ isLoading: true,
+ })
+ .mockReturnValueOnce({
+ data: undefined,
+ isLoading: true,
+ });
+
+ render(
+
+ Child content
+ ,
+ );
+
+ expect(screen.getByRole('progressbar')).toBeInTheDocument();
+ expect(screen.queryByText('Child content')).not.toBeInTheDocument();
+ });
+
+ it('should renders children when data is loaded', async () => {
+ mockUseQuery
+ .mockReturnValueOnce({
+ data: mockWorkspaces,
+ isLoading: false,
+ })
+ .mockReturnValue({
+ data: mockWorkspaces[0],
+ isLoading: false,
+ });
+
+ render(
+
+ Child content
+ ,
+ );
+
+ await waitFor(() => {
+ expect(screen.getByText('Child content')).toBeInTheDocument();
+ });
+ });
+
+ it('handles error state correctly', () => {
+ const errorMessage = 'Failed to load workspace';
+ mockUseQuery
+ .mockReturnValueOnce({
+ data: mockWorkspaces,
+ isLoading: false,
+ })
+ .mockReturnValueOnce({
+ error: new Error(errorMessage),
+ isLoading: false,
+ });
+
+ render(
+
+ Child content
+ ,
+ );
+
+ expect(screen.getByText(`Unable to access workspace workspace-1`)).toBeInTheDocument();
+ expect(screen.getByText(errorMessage)).toBeInTheDocument();
+ });
+
+ it('provides correct context values', async () => {
+ mockUseQuery
+ .mockReturnValueOnce({
+ data: mockWorkspaces,
+ isLoading: false,
+ })
+ .mockReturnValueOnce({
+ data: mockWorkspaces[0],
+ isLoading: false,
+ });
+
+ const TestConsumer = () => {
+ const context = useContext(WorkspaceContext);
+ return (
+
+
{context.namespace}
+
{context.workspace}
+
{String(context.workspacesLoaded)}
+
+ );
+ };
+
+ render(
+
+
+ ,
+ );
+
+ await waitFor(() => {
+ expect(screen.getByTestId('namespace')).toHaveTextContent(mockNamespace);
+ expect(screen.getByTestId('workspace')).toHaveTextContent('workspace-1');
+ expect(screen.getByTestId('workspaces-loaded')).toHaveTextContent('true');
+ });
+ });
+
+ it('updates last used workspace when active workspace changes', async () => {
+ mockUseQuery
+ .mockReturnValueOnce({
+ data: mockWorkspaces,
+ isLoading: false,
+ })
+ .mockReturnValueOnce({
+ data: mockWorkspaces[0],
+ isLoading: false,
+ });
+
+ mockUseParams.mockReturnValue({ workspaceName: 'workspace-2' });
+
+ render(
+
+ Child content
+ ,
+ );
+
+ await waitFor(() => {
+ expect(setLastUsedWorkspace).toHaveBeenCalledWith('workspace-2');
+ });
+ });
+
+ it('navigates to home workspace when error occurs and home button is clicked', async () => {
+ const errorMessage = 'Failed to load workspace';
+ mockUseQuery
+ .mockReturnValueOnce({
+ data: mockWorkspaces,
+ isLoading: false,
+ })
+ .mockReturnValueOnce({
+ error: new Error(errorMessage),
+ isLoading: false,
+ });
+
+ render(
+
+ Child content
+ ,
+ );
+
+ const homeButton = screen.getByText('Go to workspace-1 workspace');
+ await userEvent.click(homeButton);
+
+ expect(setLastUsedWorkspace).toHaveBeenCalledWith('workspace-1');
+ expect(mockNavigate).toHaveBeenCalledWith('/workspaces/workspace-1/applications');
+ });
+});
diff --git a/src/components/Workspace/workspace-context.tsx b/src/components/Workspace/workspace-context.tsx
index 1909ed9..bab2045 100644
--- a/src/components/Workspace/workspace-context.tsx
+++ b/src/components/Workspace/workspace-context.tsx
@@ -1,8 +1,9 @@
import * as React from 'react';
-import { useParams } from 'react-router-dom';
-import { Bullseye, Spinner } from '@patternfly/react-core';
+import { useNavigate, useParams } from 'react-router-dom';
+import { Bullseye, Button, Spinner } from '@patternfly/react-core';
import { useQuery } from '@tanstack/react-query';
import { RouterParams } from '../../routes/utils';
+import ErrorEmptyState from '../../shared/components/empty-state/ErrorEmptyState';
import { Workspace } from '../../types';
import {
createWorkspaceQueryOptions,
@@ -33,26 +34,52 @@ export const WorkspaceContext = React.createContext({
export const WorkspaceProvider: React.FC = ({ children }) => {
const { data: workspaces, isLoading: workspaceLoading } = useQuery(createWorkspaceQueryOptions());
const params = useParams();
+ const navigate = useNavigate();
+
+ const homeWorkspace = React.useMemo(
+ () => (!workspaceLoading ? getHomeWorkspace(workspaces) ?? workspaces[0] : null),
+ [workspaces, workspaceLoading],
+ );
const activeWorkspaceName =
- params.workspaceName ??
- getLastUsedWorkspace() ??
- getHomeWorkspace(workspaces)?.metadata?.name ??
- workspaces[0]?.metadata?.name;
+ params.workspaceName ?? getLastUsedWorkspace() ?? homeWorkspace?.metadata?.name;
- const { data: workspaceResource, isLoading: activeWorkspaceLoading } = useQuery(
- createWorkspaceQueryOptions(activeWorkspaceName),
- );
+ const {
+ data: workspaceResource,
+ isLoading: activeWorkspaceLoading,
+ error,
+ } = useQuery({ ...createWorkspaceQueryOptions(activeWorkspaceName), retry: false });
const namespace = !activeWorkspaceLoading
? getDefaultNsForWorkspace(workspaceResource)?.name
: undefined;
React.useEffect(() => {
- if (getLastUsedWorkspace() !== activeWorkspaceName) {
+ if (!error && getLastUsedWorkspace() !== activeWorkspaceName) {
setLastUsedWorkspace(activeWorkspaceName);
}
- }, [activeWorkspaceName]);
+ }, [activeWorkspaceName, error]);
+
+ if (error) {
+ return (
+
+ {homeWorkspace ? (
+
+ ) : null}
+
+ );
+ }
return (
= ({ children
lastUsedWorkspace: getLastUsedWorkspace(),
}}
>
- {!(workspaceLoading && activeWorkspaceLoading) ? (
+ {!(workspaceLoading || activeWorkspaceLoading) ? (
children
) : (
diff --git a/src/routes/index.tsx b/src/routes/index.tsx
index ccb295c..a4088d0 100644
--- a/src/routes/index.tsx
+++ b/src/routes/index.tsx
@@ -113,7 +113,7 @@ export const router = createBrowserRouter([
element: ,
},
{
- path: `/workspaces/:${RouterParams.workspaceName}/applications`,
+ path: `workspaces/:${RouterParams.workspaceName}/applications`,
loader: applicationPageLoader,
element: ,
errorElement: ,