From e4a87a2d07dc26c55eb65debbe2a07638a7ded5e Mon Sep 17 00:00:00 2001 From: Sahil Budhwar Date: Tue, 11 Feb 2025 13:37:28 +0530 Subject: [PATCH 1/2] chore: mark workspace hooks and utils deprecated --- config/jest.setup.js | 4 +- .../Applications/ApplicationListView.tsx | 2 +- .../switcher/ApplicationSwitcher.tsx | 4 +- src/components/ImportForm/GitImportForm.tsx | 2 +- .../SecretSection/SecretSection.tsx | 2 +- src/components/Overview/IntroBanner.tsx | 2 +- .../SecretsForm/ApplicationDropdown.tsx | 2 +- .../SecretsListView/SecretsListView.tsx | 2 +- .../Workspace/WorkspaceSwitcher.tsx | 6 +- .../__tests__/workspace-context.spec.tsx | 207 ------------------ src/components/Workspace/index.ts | 6 + src/components/Workspace/useWorkspaceInfo.ts | 8 + src/components/Workspace/utils.ts | 43 ++++ .../Workspace/workspace-context.tsx | 9 + src/k8s/index.ts | 1 + src/routes/index.tsx | 2 +- src/routes/utils.ts | 7 + .../ContextSwitcher/ContextSwitcher.scss | 0 .../ContextSwitcher/ContextSwitcher.spec.tsx | 4 +- .../ContextSwitcher/ContextSwitcher.tsx | 2 +- .../context-switcher-utils.tsx | 0 .../components/ContextSwitcher/index.ts | 0 src/shared/components/index.ts | 1 + .../__tests__/namespace-context.spec.tsx | 2 +- .../providers}/Namespace/index.ts | 1 + .../Namespace/namespace-context.tsx | 10 +- .../providers}/Namespace/useNamespaceInfo.ts | 4 + .../providers}/Namespace/utils.ts | 7 +- src/unit-test-utils/mock-namespace.ts | 2 +- src/utils/rbac.ts | 2 +- src/utils/test-utils.tsx | 10 +- 31 files changed, 118 insertions(+), 236 deletions(-) delete mode 100644 src/components/Workspace/__tests__/workspace-context.spec.tsx rename src/{ => shared}/components/ContextSwitcher/ContextSwitcher.scss (100%) rename src/{ => shared}/components/ContextSwitcher/ContextSwitcher.spec.tsx (96%) rename src/{ => shared}/components/ContextSwitcher/ContextSwitcher.tsx (99%) rename src/{ => shared}/components/ContextSwitcher/context-switcher-utils.tsx (100%) rename src/{ => shared}/components/ContextSwitcher/index.ts (100%) rename src/{components => shared/providers}/Namespace/__tests__/namespace-context.spec.tsx (99%) rename src/{components => shared/providers}/Namespace/index.ts (89%) rename src/{components => shared/providers}/Namespace/namespace-context.tsx (89%) rename src/{components => shared/providers}/Namespace/useNamespaceInfo.ts (54%) rename src/{components => shared/providers}/Namespace/utils.ts (90%) diff --git a/config/jest.setup.js b/config/jest.setup.js index c29c3305..e80d8579 100644 --- a/config/jest.setup.js +++ b/config/jest.setup.js @@ -24,9 +24,9 @@ jest.mock('../src/components/Workspace/useWorkspaceInfo', () => ({ ...jest.requireActual('../src/components/Workspace/useWorkspaceInfo'), })); -jest.mock('../src/components/Namespace/useNamespaceInfo', () => ({ +jest.mock('../src/shared/providers/Namespace/useNamespaceInfo', () => ({ __esModule: true, - ...jest.requireActual('../src/components/Namespace/useNamespaceInfo'), + ...jest.requireActual('../src/shared/providers/Namespace/useNamespaceInfo'), })); jest.mock('../src/utils/rbac', () => ({ diff --git a/src/components/Applications/ApplicationListView.tsx b/src/components/Applications/ApplicationListView.tsx index 3c51deeb..33e282ab 100644 --- a/src/components/Applications/ApplicationListView.tsx +++ b/src/components/Applications/ApplicationListView.tsx @@ -20,11 +20,11 @@ import { IMPORT_PATH } from '../../routes/paths'; import { Table } from '../../shared'; import AppEmptyState from '../../shared/components/empty-state/AppEmptyState'; import FilteredEmptyState from '../../shared/components/empty-state/FilteredEmptyState'; +import { useNamespace } from '../../shared/providers/Namespace'; import { ApplicationKind } from '../../types'; import { useApplicationBreadcrumbs } from '../../utils/breadcrumb-utils'; import { useAccessReviewForModel } from '../../utils/rbac'; import { ButtonWithAccessTooltip } from '../ButtonWithAccessTooltip'; -import { useNamespace } from '../Namespace/useNamespaceInfo'; import PageLayout from '../PageLayout/PageLayout'; import { ApplicationListHeader } from './ApplicationListHeader'; import ApplicationListRow from './ApplicationListRow'; diff --git a/src/components/Applications/switcher/ApplicationSwitcher.tsx b/src/components/Applications/switcher/ApplicationSwitcher.tsx index a1124485..086e7f46 100644 --- a/src/components/Applications/switcher/ApplicationSwitcher.tsx +++ b/src/components/Applications/switcher/ApplicationSwitcher.tsx @@ -8,10 +8,10 @@ import { APPLICATION_LIST_PATH, IMPORT_PATH, } from '../../../routes/paths'; +import { ContextMenuItem, ContextSwitcher } from '../../../shared/components'; +import { useNamespace } from '../../../shared/providers/Namespace'; import { useAccessReviewForModel } from '../../../utils/rbac'; import { ButtonWithAccessTooltip } from '../../ButtonWithAccessTooltip'; -import { ContextMenuItem, ContextSwitcher } from '../../ContextSwitcher'; -import { useNamespace } from '../../Namespace/useNamespaceInfo'; export const ApplicationSwitcher: React.FC< React.PropsWithChildren<{ selectedApplication?: string }> diff --git a/src/components/ImportForm/GitImportForm.tsx b/src/components/ImportForm/GitImportForm.tsx index cdab0f9e..04b839ef 100644 --- a/src/components/ImportForm/GitImportForm.tsx +++ b/src/components/ImportForm/GitImportForm.tsx @@ -4,8 +4,8 @@ import { Form, PageSection } from '@patternfly/react-core'; import { Formik, FormikHelpers } from 'formik'; import { useBombinoUrl } from '../../hooks/useUIInstance'; import { APPLICATION_DETAILS_PATH } from '../../routes/paths'; +import { useNamespace } from '../../shared/providers/Namespace'; import { AnalyticsProperties, TrackEvents, useTrackEvent } from '../../utils/analytics'; -import { useNamespace } from '../Namespace/useNamespaceInfo'; import ApplicationSection from './ApplicationSection/ApplicationSection'; import { ComponentSection } from './ComponentSection/ComponentSection'; import GitImportActions from './GitImportActions'; diff --git a/src/components/ImportForm/SecretSection/SecretSection.tsx b/src/components/ImportForm/SecretSection/SecretSection.tsx index 8530898a..8eb0aec9 100644 --- a/src/components/ImportForm/SecretSection/SecretSection.tsx +++ b/src/components/ImportForm/SecretSection/SecretSection.tsx @@ -7,12 +7,12 @@ import { Base64 } from 'js-base64'; import { useSecrets } from '../../../hooks/useSecrets'; import { SecretModel } from '../../../models'; import TextColumnField from '../../../shared/components/formik-fields/text-column-field/TextColumnField'; +import { useNamespace } from '../../../shared/providers/Namespace'; import { BuildTimeSecret, SecretType } from '../../../types'; import { AccessReviewResources } from '../../../types/rbac'; import { useAccessReviewForModels } from '../../../utils/rbac'; import { ButtonWithAccessTooltip } from '../../ButtonWithAccessTooltip'; import { useModalLauncher } from '../../modal/ModalProvider'; -import { useNamespace } from '../../Namespace/useNamespaceInfo'; import { SecretModalLauncher } from '../../Secrets/SecretModalLauncher'; import { ImportFormValues } from '../type'; diff --git a/src/components/Overview/IntroBanner.tsx b/src/components/Overview/IntroBanner.tsx index 2873aae9..21d4db30 100644 --- a/src/components/Overview/IntroBanner.tsx +++ b/src/components/Overview/IntroBanner.tsx @@ -15,10 +15,10 @@ import { useApplications } from '../../hooks/useApplications'; import { ApplicationModel, ComponentModel } from '../../models'; import { APPLICATION_LIST_PATH, IMPORT_PATH } from '../../routes/paths'; import ExternalLink from '../../shared/components/links/ExternalLink'; +import { useNamespace } from '../../shared/providers/Namespace'; import { AccessReviewResources } from '../../types'; import { useAccessReviewForModels } from '../../utils/rbac'; import { ButtonWithAccessTooltip } from '../ButtonWithAccessTooltip'; -import { useNamespace } from '../Namespace/useNamespaceInfo'; import { SignupStatus } from '../SignUp/signup-utils'; import SignupButton from '../SignUp/SignupButton'; import { useSignupStatus } from '../SignUp/useSignupStatus'; diff --git a/src/components/Secrets/SecretsForm/ApplicationDropdown.tsx b/src/components/Secrets/SecretsForm/ApplicationDropdown.tsx index bdcc9cb6..93bef7ae 100644 --- a/src/components/Secrets/SecretsForm/ApplicationDropdown.tsx +++ b/src/components/Secrets/SecretsForm/ApplicationDropdown.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { useField } from 'formik'; import { useApplications } from '../../../hooks/useApplications'; import DropdownField from '../../../shared/components/formik-fields/DropdownField'; -import { useNamespace } from '../../Namespace/useNamespaceInfo'; +import { useNamespace } from '../../../shared/providers/Namespace'; type ApplicationDropdownProps = Omit< React.ComponentProps, diff --git a/src/components/Secrets/SecretsListView/SecretsListView.tsx b/src/components/Secrets/SecretsListView/SecretsListView.tsx index 44c0c8f5..5c26f48c 100644 --- a/src/components/Secrets/SecretsListView/SecretsListView.tsx +++ b/src/components/Secrets/SecretsListView/SecretsListView.tsx @@ -15,9 +15,9 @@ import { useSecrets } from '../../../hooks/useSecrets'; import { SecretModel } from '../../../models'; import AppEmptyState from '../../../shared/components/empty-state/AppEmptyState'; import FilteredEmptyState from '../../../shared/components/empty-state/FilteredEmptyState'; +import { useNamespace } from '../../../shared/providers/Namespace'; import { useAccessReviewForModel } from '../../../utils/rbac'; import { ButtonWithAccessTooltip } from '../../ButtonWithAccessTooltip'; -import { useNamespace } from '../../Namespace/useNamespaceInfo'; import SecretsList from './SecretsList'; const SecretsListView: React.FC = () => { diff --git a/src/components/Workspace/WorkspaceSwitcher.tsx b/src/components/Workspace/WorkspaceSwitcher.tsx index c27897ad..8bf9c9a4 100644 --- a/src/components/Workspace/WorkspaceSwitcher.tsx +++ b/src/components/Workspace/WorkspaceSwitcher.tsx @@ -1,10 +1,14 @@ import React from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { useQuery } from '@tanstack/react-query'; -import { ContextMenuItem, ContextSwitcher } from '../ContextSwitcher'; +import { ContextMenuItem, ContextSwitcher } from '../../shared/components'; import { useWorkspaceInfo } from './useWorkspaceInfo'; import { createWorkspaceQueryOptions } from './utils'; +/** + * + * @deprecated + */ export const WorkspaceSwitcher: React.FC< React.PropsWithChildren<{ selectedWorkspace?: string }> > = () => { diff --git a/src/components/Workspace/__tests__/workspace-context.spec.tsx b/src/components/Workspace/__tests__/workspace-context.spec.tsx deleted file mode 100644 index 60bbc176..00000000 --- a/src/components/Workspace/__tests__/workspace-context.spec.tsx +++ /dev/null @@ -1,207 +0,0 @@ -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/index.ts b/src/components/Workspace/index.ts index a46398b7..8533abc5 100644 --- a/src/components/Workspace/index.ts +++ b/src/components/Workspace/index.ts @@ -1,9 +1,15 @@ import { LoaderFunction } from 'react-router-dom'; import { queryWorkspaces } from './utils'; +/** + * @deprecated in favor of Namespaces + */ export const workspaceLoader: LoaderFunction = async () => { const workspaces = await queryWorkspaces(); return { data: workspaces }; }; +/** + * @deprecated in favor of Namespaces + */ export { WorkspaceProvider } from './workspace-context'; diff --git a/src/components/Workspace/useWorkspaceInfo.ts b/src/components/Workspace/useWorkspaceInfo.ts index 63ba8fa7..aaedb6ef 100644 --- a/src/components/Workspace/useWorkspaceInfo.ts +++ b/src/components/Workspace/useWorkspaceInfo.ts @@ -1,4 +1,12 @@ import { useContext } from 'react'; +// remove eslint disable once migration to namespace is done +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import type { useNamespace } from '../../shared/providers/Namespace'; import { WorkspaceContext } from './workspace-context'; +/** + * @deprecated Migrate to Namespaces + * use {@link useNamespace} + * + */ export const useWorkspaceInfo = () => useContext(WorkspaceContext); diff --git a/src/components/Workspace/utils.ts b/src/components/Workspace/utils.ts index 7c4e6a98..ddfcd0ca 100644 --- a/src/components/Workspace/utils.ts +++ b/src/components/Workspace/utils.ts @@ -4,33 +4,60 @@ import { queryClient } from '../../k8s/query/core'; import { WorkspaceModel } from '../../models'; import { Workspace } from '../../types'; +/** + * @deprecated in favor of [Namespace](../../shared/providers/Namespace/utils.ts) + */ const LOCAL_STORAGE_WORKSPACE_KEY = 'lastUsedWorkspace'; +/** + * @deprecated in favor of [Namespace](../../shared/providers/Namespace/utils.ts) + */ export const WORKSPACE_QUERY_KEY = 'workspaces'; +/** + * @deprecated in favor of [Namespace](../../shared/providers/Namespace/utils.ts) + */ export const getLastUsedWorkspace = (): string => { return localStorage.getItem(LOCAL_STORAGE_WORKSPACE_KEY); }; +/** + * @deprecated in favor of [Namespace](../../shared/providers/Namespace/utils.ts) + */ export const setLastUsedWorkspace = (workspace: string) => { localStorage.setItem(LOCAL_STORAGE_WORKSPACE_KEY, workspace); }; +/** + * @deprecated in favor of [Namespace](../../shared/providers/Namespace/utils.ts) + */ const createWorkspaceQueryKey = (name?: string) => { return [WORKSPACE_QUERY_KEY, ...(name ? [name] : [])]; }; +/** + * @deprecated in favor of [Namespace](../../shared/providers/Namespace/utils.ts) + */ export const getHomeWorkspace = (workspaces: Workspace[]) => workspaces?.find((w) => w?.status?.type === 'home'); +/** + * @deprecated in favor of [Namespace](../../shared/providers/Namespace/utils.ts) + */ export const getDefaultNsForWorkspace = (obj: Workspace) => { return obj?.status?.namespaces.find((n) => n.type === 'default'); }; +/** + * @deprecated in favor of [Namespace](../../shared/providers/Namespace/utils.ts) + */ export const getWorkspaceForNamespace = (workspaces: Workspace[], namespace) => { return workspaces?.find((w) => w.status?.namespaces?.some((ns) => ns.name === namespace)); }; +/** + * @deprecated in favor of [Namespace](../../shared/providers/Namespace/utils.ts) + */ function fetchWorkspaces(): Promise; function fetchWorkspaces(name: string): Promise; function fetchWorkspaces(name?: string): Promise { @@ -39,6 +66,9 @@ function fetchWorkspaces(name?: string): Promise { : K8sGetResource({ model: WorkspaceModel, queryOptions: { name } }); } +/** + * @deprecated in favor of [Namespace](../../shared/providers/Namespace/utils.ts) + */ export function createWorkspaceQueryOptions(): UseQueryOptions; export function createWorkspaceQueryOptions(name: string): UseQueryOptions; export function createWorkspaceQueryOptions( @@ -58,12 +88,18 @@ export function createWorkspaceQueryOptions( }); } +/** + * @deprecated in favor of [Namespace](../../shared/providers/Namespace/utils.ts) + */ export function invalidateWorkspaceQuery(): Promise; export function invalidateWorkspaceQuery(name: string): Promise; export async function invalidateWorkspaceQuery(name?: string): Promise { return await queryClient.invalidateQueries({ queryKey: createWorkspaceQueryKey(name) }); } +/** + * @deprecated in favor of [Namespace](../../shared/providers/Namespace/utils.ts) + */ export const getNamespaceUsingWorspaceFromQueryCache = async ( workspace: string, ): Promise => { @@ -74,6 +110,10 @@ export const getNamespaceUsingWorspaceFromQueryCache = async ( }), )?.name; }; + +/** + * @deprecated in favor of [Namespace](../../shared/providers/Namespace/utils.ts) + */ export const getWorkspaceUsingNamespaceFromQueryCache = async ( namespace: string, ): Promise => { @@ -86,6 +126,9 @@ export const getWorkspaceUsingNamespaceFromQueryCache = async ( )?.metadata?.name; }; +/** + * @deprecated in favor of [Namespace](../../shared/providers/Namespace/utils.ts) + */ export const queryWorkspaces = () => { return queryClient.ensureQueryData(createWorkspaceQueryOptions()); }; diff --git a/src/components/Workspace/workspace-context.tsx b/src/components/Workspace/workspace-context.tsx index bab20454..71938dcf 100644 --- a/src/components/Workspace/workspace-context.tsx +++ b/src/components/Workspace/workspace-context.tsx @@ -13,6 +13,9 @@ import { setLastUsedWorkspace, } from './utils'; +/** + * @deprecated in favor of [Namespace](../../shared/providers/Namespace/namespace-context.ts) + */ export type WorkspaceContextData = { namespace: string; workspace: string; @@ -22,6 +25,9 @@ export type WorkspaceContextData = { workspaces: Workspace[]; }; +/** + * @deprecated in favor of [Namespace](../../shared/providers/Namespace/namespace-context.ts) + */ export const WorkspaceContext = React.createContext({ namespace: '', workspace: '', @@ -31,6 +37,9 @@ export const WorkspaceContext = React.createContext({ lastUsedWorkspace: getLastUsedWorkspace(), }); +/** + * @deprecated in favor of [Namespace](../../shared/providers/Namespace/namespace-context.ts) + */ export const WorkspaceProvider: React.FC = ({ children }) => { const { data: workspaces, isLoading: workspaceLoading } = useQuery(createWorkspaceQueryOptions()); const params = useParams(); diff --git a/src/k8s/index.ts b/src/k8s/index.ts index 555bc154..259a7890 100644 --- a/src/k8s/index.ts +++ b/src/k8s/index.ts @@ -1,5 +1,6 @@ export * from './hooks'; export * from './query'; +export * from './query/core'; export * from './fetch'; export * from './k8s-fetch'; export * from './k8s-utils'; diff --git a/src/routes/index.tsx b/src/routes/index.tsx index efd9a03c..a7610eb8 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -32,7 +32,6 @@ import { IntegrationTestsListView, } from '../components/IntegrationTests/IntegrationTestsListView'; import { ModalProvider } from '../components/modal/ModalProvider'; -import { namespaceLoader, NamespaceProvider } from '../components/Namespace'; import { Overview } from '../components/Overview/Overview'; import { PipelineRunDetailsLayout, @@ -85,6 +84,7 @@ import { import { workspaceLoader, WorkspaceProvider } from '../components/Workspace'; import { HttpError } from '../k8s/error'; import ErrorEmptyState from '../shared/components/empty-state/ErrorEmptyState'; +import { namespaceLoader, NamespaceProvider } from '../shared/providers/Namespace'; import applicationRoutes from './page-routes/application'; import workspaceRoutes from './page-routes/workspace'; import { RouteErrorBoundry } from './RouteErrorBoundary'; diff --git a/src/routes/utils.ts b/src/routes/utils.ts index 71ace976..603813b6 100644 --- a/src/routes/utils.ts +++ b/src/routes/utils.ts @@ -1,4 +1,11 @@ export const RouterParams = { + /** + * @deprecated + * Existing usages are permitted for backward compatibility; + * however, please avoid adding new references + * + * use [useNamespace](../shared/providers/Namespace) + */ workspaceName: 'workspaceName', applicationName: 'applicationName', integrationTestName: 'integrationTestName', diff --git a/src/components/ContextSwitcher/ContextSwitcher.scss b/src/shared/components/ContextSwitcher/ContextSwitcher.scss similarity index 100% rename from src/components/ContextSwitcher/ContextSwitcher.scss rename to src/shared/components/ContextSwitcher/ContextSwitcher.scss diff --git a/src/components/ContextSwitcher/ContextSwitcher.spec.tsx b/src/shared/components/ContextSwitcher/ContextSwitcher.spec.tsx similarity index 96% rename from src/components/ContextSwitcher/ContextSwitcher.spec.tsx rename to src/shared/components/ContextSwitcher/ContextSwitcher.spec.tsx index 6b3c89a4..33bbf4fc 100644 --- a/src/components/ContextSwitcher/ContextSwitcher.spec.tsx +++ b/src/shared/components/ContextSwitcher/ContextSwitcher.spec.tsx @@ -1,9 +1,9 @@ import { act, fireEvent, render, screen } from '@testing-library/react'; import '@testing-library/jest-dom'; -import { useLocalStorage } from '../../hooks/useLocalStorage'; +import { useLocalStorage } from '../../../hooks/useLocalStorage'; import { ContextSwitcher } from './ContextSwitcher'; -jest.mock('../../hooks/useLocalStorage', () => ({ +jest.mock('../../../hooks/useLocalStorage', () => ({ useLocalStorage: jest.fn(() => [{}, jest.fn()]), })); diff --git a/src/components/ContextSwitcher/ContextSwitcher.tsx b/src/shared/components/ContextSwitcher/ContextSwitcher.tsx similarity index 99% rename from src/components/ContextSwitcher/ContextSwitcher.tsx rename to src/shared/components/ContextSwitcher/ContextSwitcher.tsx index b7222b77..e97eeb90 100644 --- a/src/components/ContextSwitcher/ContextSwitcher.tsx +++ b/src/shared/components/ContextSwitcher/ContextSwitcher.tsx @@ -14,7 +14,7 @@ import { import { Dropdown, DropdownToggle } from '@patternfly/react-core/deprecated'; import { EllipsisHIcon } from '@patternfly/react-icons/dist/esm/icons/ellipsis-h-icon'; import '././ContextSwitcher.scss'; -import { useLocalStorage } from '../../hooks/useLocalStorage'; +import { useLocalStorage } from '../../../hooks/useLocalStorage'; import { ContextMenuListItem, filteredItems, findItemByKey } from './context-switcher-utils'; const LOCAL_STORAGE_KEY = 'context-switcher'; diff --git a/src/components/ContextSwitcher/context-switcher-utils.tsx b/src/shared/components/ContextSwitcher/context-switcher-utils.tsx similarity index 100% rename from src/components/ContextSwitcher/context-switcher-utils.tsx rename to src/shared/components/ContextSwitcher/context-switcher-utils.tsx diff --git a/src/components/ContextSwitcher/index.ts b/src/shared/components/ContextSwitcher/index.ts similarity index 100% rename from src/components/ContextSwitcher/index.ts rename to src/shared/components/ContextSwitcher/index.ts diff --git a/src/shared/components/index.ts b/src/shared/components/index.ts index 603ede95..6885726e 100644 --- a/src/shared/components/index.ts +++ b/src/shared/components/index.ts @@ -7,3 +7,4 @@ export * from './timestamp'; export * from './markdown-view'; export * from './help-tooltip'; export * from './pipeline-run-logs'; +export * from './ContextSwitcher'; diff --git a/src/components/Namespace/__tests__/namespace-context.spec.tsx b/src/shared/providers/Namespace/__tests__/namespace-context.spec.tsx similarity index 99% rename from src/components/Namespace/__tests__/namespace-context.spec.tsx rename to src/shared/providers/Namespace/__tests__/namespace-context.spec.tsx index 15fef70b..7f5f0619 100644 --- a/src/components/Namespace/__tests__/namespace-context.spec.tsx +++ b/src/shared/providers/Namespace/__tests__/namespace-context.spec.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { useQuery } from '@tanstack/react-query'; import { screen, waitFor, render } from '@testing-library/react'; -import { createReactRouterMock, createUseWorkspaceInfoMock } from '../../../utils/test-utils'; +import { createReactRouterMock, createUseWorkspaceInfoMock } from '../../../../utils/test-utils'; import { NamespaceProvider, NamespaceContext } from '../namespace-context'; jest.mock('react-router-dom', () => ({ diff --git a/src/components/Namespace/index.ts b/src/shared/providers/Namespace/index.ts similarity index 89% rename from src/components/Namespace/index.ts rename to src/shared/providers/Namespace/index.ts index 1f6b2588..90eba1c5 100644 --- a/src/components/Namespace/index.ts +++ b/src/shared/providers/Namespace/index.ts @@ -7,3 +7,4 @@ export const namespaceLoader: LoaderFunction = async () => { }; export { NamespaceProvider } from './namespace-context'; +export * from './useNamespaceInfo'; diff --git a/src/components/Namespace/namespace-context.tsx b/src/shared/providers/Namespace/namespace-context.tsx similarity index 89% rename from src/components/Namespace/namespace-context.tsx rename to src/shared/providers/Namespace/namespace-context.tsx index 1c86fd91..56bf65d3 100644 --- a/src/components/Namespace/namespace-context.tsx +++ b/src/shared/providers/Namespace/namespace-context.tsx @@ -2,11 +2,11 @@ import * as React from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { Bullseye, Button, Spinner } from '@patternfly/react-core'; import { useQuery } from '@tanstack/react-query'; -import { APPLICATION_LIST_PATH } from '../../routes/paths'; -import { RouterParams } from '../../routes/utils'; -import ErrorEmptyState from '../../shared/components/empty-state/ErrorEmptyState'; -import { NamespaceKind } from '../../types'; -import { useWorkspaceInfo } from '../Workspace/useWorkspaceInfo'; +import { useWorkspaceInfo } from '../../../components/Workspace/useWorkspaceInfo'; +import { APPLICATION_LIST_PATH } from '../../../routes/paths'; +import { RouterParams } from '../../../routes/utils'; +import { NamespaceKind } from '../../../types'; +import ErrorEmptyState from '../../components/empty-state/ErrorEmptyState'; import { createNamespaceQueryOptions, getLastUsedNamespace, setLastUsedNamespace } from './utils'; export type NamespaceContextData = { diff --git a/src/components/Namespace/useNamespaceInfo.ts b/src/shared/providers/Namespace/useNamespaceInfo.ts similarity index 54% rename from src/components/Namespace/useNamespaceInfo.ts rename to src/shared/providers/Namespace/useNamespaceInfo.ts index 623bd13d..dbc8ee3d 100644 --- a/src/components/Namespace/useNamespaceInfo.ts +++ b/src/shared/providers/Namespace/useNamespaceInfo.ts @@ -1,6 +1,10 @@ import { useContext } from 'react'; +import { useQuery } from '@tanstack/react-query'; import { NamespaceContext } from './namespace-context'; +import { createNamespaceQueryOptions } from './utils'; export const useNamespaceInfo = () => useContext(NamespaceContext); export const useNamespace = () => useNamespaceInfo().namespace; + +export const useNamespacesQuery = () => useQuery(createNamespaceQueryOptions()); diff --git a/src/components/Namespace/utils.ts b/src/shared/providers/Namespace/utils.ts similarity index 90% rename from src/components/Namespace/utils.ts rename to src/shared/providers/Namespace/utils.ts index 9559b320..15c617c3 100644 --- a/src/components/Namespace/utils.ts +++ b/src/shared/providers/Namespace/utils.ts @@ -1,8 +1,7 @@ import { queryOptions as _createQueryOptions, UseQueryOptions } from '@tanstack/react-query'; -import { K8sGetResource, K8sListResourceItems } from '../../k8s/k8s-fetch'; -import { queryClient } from '../../k8s/query/core'; -import { NamespaceModel } from '../../models'; -import { NamespaceKind } from '../../types'; +import { K8sGetResource, K8sListResourceItems, queryClient } from '../../../k8s'; +import { NamespaceModel } from '../../../models'; +import { NamespaceKind } from '../../../types'; const LOCAL_STORAGE_NAMESPACE_KEY = 'lastUsedNamespace'; diff --git a/src/unit-test-utils/mock-namespace.ts b/src/unit-test-utils/mock-namespace.ts index feac7cc6..78bda6cb 100644 --- a/src/unit-test-utils/mock-namespace.ts +++ b/src/unit-test-utils/mock-namespace.ts @@ -1,4 +1,4 @@ -import * as NamespaceHook from '../components/Namespace/useNamespaceInfo'; +import * as NamespaceHook from '../shared/providers/Namespace/useNamespaceInfo'; import { createJestMockFunction } from './common'; import type { JestMockedFunction, MockFunctionKeys } from './type'; diff --git a/src/utils/rbac.ts b/src/utils/rbac.ts index c3aef652..bd447333 100644 --- a/src/utils/rbac.ts +++ b/src/utils/rbac.ts @@ -1,10 +1,10 @@ import React from 'react'; import { LoaderFunction, LoaderFunctionArgs } from 'react-router-dom'; import { memoize } from 'lodash-es'; -import { useNamespace } from '../components/Namespace/useNamespaceInfo'; import { getNamespaceUsingWorspaceFromQueryCache } from '../components/Workspace/utils'; import { k8sCreateResource } from '../k8s/k8s-fetch'; import { SelfSubjectAccessReviewModel } from '../models/rbac'; +import { useNamespace } from '../shared/providers/Namespace'; import { K8sModelCommon, K8sVerb } from '../types/k8s'; import { AccessReviewResource, diff --git a/src/utils/test-utils.tsx b/src/utils/test-utils.tsx index ce38227e..b2f75e77 100644 --- a/src/utils/test-utils.tsx +++ b/src/utils/test-utils.tsx @@ -11,11 +11,11 @@ import { act, } from '@testing-library/react'; import { FormikValues, Formik } from 'formik'; -import * as NamespaceUtils from '../components/Namespace/namespace-context'; import * as WorkspaceHook from '../components/Workspace/useWorkspaceInfo'; import * as WorkspaceUtils from '../components/Workspace/workspace-context'; import * as ApplicationHook from '../hooks/useApplications'; import * as k8s from '../k8s'; +import * as NamespaceUtils from '../shared/providers/Namespace/namespace-context'; export function createTestQueryClient() { return new QueryClient({ @@ -223,6 +223,9 @@ export const createReactRouterMock = (name): jest.Mock => { return mockFn; }; +/** + * @deprecated use [namespace-mock](../unit-test-utils/mock-namespace.ts) + */ export const createUseWorkspaceInfoMock = ( initialValue: Record = {}, ): jest.Mock => { @@ -251,6 +254,9 @@ export const createUseApplicationMock = ( return mockFn; }; +/** + * @deprecated use {@link WithTestNamespaceContext} + */ export const WithTestWorkspaceContext = (children, data?: WorkspaceUtils.WorkspaceContextData) => () => ( () => ( + (children, data?: NamespaceUtils.NamespaceContextData) => () => ( Date: Tue, 11 Feb 2025 17:42:39 +0530 Subject: [PATCH 2/2] fix: reverse unit tests for the workspace-context --- .../__tests__/workspace-context.spec.tsx | 207 ++++++++++++++++++ 1 file changed, 207 insertions(+) create mode 100644 src/components/Workspace/__tests__/workspace-context.spec.tsx 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 00000000..60bbc176 --- /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'); + }); +});