diff --git a/src/components/ApplicationDetails/WorkspaceSwitcher.tsx b/src/components/ApplicationDetails/WorkspaceSwitcher.tsx index d6a53a871..c86a01346 100644 --- a/src/components/ApplicationDetails/WorkspaceSwitcher.tsx +++ b/src/components/ApplicationDetails/WorkspaceSwitcher.tsx @@ -5,7 +5,7 @@ import { ContextMenuItem, ContextSwitcher } from '../ContextSwitcher'; export const WorkspaceSwitcher: React.FC<{ selectedWorkspace?: string }> = () => { const navigate = useNavigate(); - const { workspace, setWorkspace, workspaces } = React.useContext(WorkspaceContext); + const { workspace, workspaces } = React.useContext(WorkspaceContext); const menuItems = React.useMemo( () => workspaces?.map((app) => ({ key: app.metadata.name, name: app.metadata.name })) || [], @@ -15,7 +15,6 @@ export const WorkspaceSwitcher: React.FC<{ selectedWorkspace?: string }> = () => const onSelect = (item: ContextMenuItem) => { navigate(`/application-pipeline/workspaces/${item.name}/applications`); - setWorkspace(item.name); }; return workspaces.length > 0 ? ( diff --git a/src/components/Releases/ReleaseDetailsView.tsx b/src/components/Releases/ReleaseDetailsView.tsx index dcd28f101..6b48fe426 100644 --- a/src/components/Releases/ReleaseDetailsView.tsx +++ b/src/components/Releases/ReleaseDetailsView.tsx @@ -73,7 +73,7 @@ const ReleaseDetailsView: React.FC = ({ key: 'overview', label: 'Overview', isFilled: true, - component: , + component: , }, ]} /> diff --git a/src/components/Releases/ReleaseOverviewTab.tsx b/src/components/Releases/ReleaseOverviewTab.tsx index d15fb6aef..b95286637 100644 --- a/src/components/Releases/ReleaseOverviewTab.tsx +++ b/src/components/Releases/ReleaseOverviewTab.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import { Link } from 'react-router-dom'; +import { useK8sWatchResource } from '@openshift/dynamic-plugin-sdk-utils'; import { DescriptionList, DescriptionListDescription, @@ -10,21 +11,28 @@ import { Title, } from '@patternfly/react-core'; import { useReleaseStatus } from '../../hooks/useReleaseStatus'; +import { useWorkspaceResource } from '../../hooks/useWorkspaceResource'; +import { ReleasePlanGroupVersionKind } from '../../models'; import { Timestamp } from '../../shared/components/timestamp/Timestamp'; import { ReleaseKind } from '../../types'; +import { ReleasePlanKind } from '../../types/coreBuildService'; import { calculateDuration } from '../../utils/pipeline-utils'; import { useWorkspaceInfo } from '../../utils/workspace-context-utils'; import MetadataList from '../PipelineRunDetailsView/MetadataList'; import { StatusIconWithText } from '../topology/StatusIcon'; type ReleaseOverviewTabProps = { - applicationName: string; release: ReleaseKind; }; -const ReleaseOverviewTab: React.FC = ({ applicationName, release }) => { - const { workspace } = useWorkspaceInfo(); - const pipelineRun = release.status?.processing?.pipelineRun?.split('/')[1]; +const ReleaseOverviewTab: React.FC = ({ release }) => { + const { namespace } = useWorkspaceInfo(); + const [pipelineRun, prWorkspace] = useWorkspaceResource(release.status?.processing?.pipelineRun); + const [releasePlan, releasePlanLoaded] = useK8sWatchResource({ + name: release.spec.releasePlan, + groupVersionKind: ReleasePlanGroupVersionKind, + namespace, + }); const duration = calculateDuration( typeof release.status?.startTime === 'string' ? release.status?.startTime : '', typeof release.status?.completionTime === 'string' ? release.status?.completionTime : '', @@ -114,9 +122,9 @@ const ReleaseOverviewTab: React.FC = ({ applicationName Pipeline Run - {pipelineRun ? ( + {pipelineRun && prWorkspace && releasePlanLoaded ? ( {pipelineRun} diff --git a/src/components/Releases/__tests__/ReleaseOverviewTab.spec.tsx b/src/components/Releases/__tests__/ReleaseOverviewTab.spec.tsx index 3d69615a8..90fbf717a 100644 --- a/src/components/Releases/__tests__/ReleaseOverviewTab.spec.tsx +++ b/src/components/Releases/__tests__/ReleaseOverviewTab.spec.tsx @@ -12,9 +12,17 @@ jest.mock('../../../utils/workspace-context-utils', () => ({ useWorkspaceInfo: jest.fn(() => ({ namespace: 'test-ns', workspace: 'test-ws' })), })); +jest.mock('@openshift/dynamic-plugin-sdk-utils', () => ({ + useK8sWatchResource: jest.fn(() => [{ spec: { application: 'test-app' } }, true]), +})); + +jest.mock('../../../hooks/useWorkspaceResource', () => ({ + useWorkspaceResource: jest.fn(() => ['test-pipelinerun', 'target-ws']), +})); + describe('ReleaseOverviewTab', () => { it('should render correct details', () => { - render(); + render(); expect(screen.getByText('Duration')).toBeVisible(); expect(screen.getByText('10 seconds')).toBeVisible(); @@ -39,7 +47,7 @@ describe('ReleaseOverviewTab', () => { expect(screen.getByText('Pipeline Run')).toBeVisible(); expect(screen.getByText('test-strategy')).toBeVisible(); expect(screen.getByRole('link', { name: 'test-pipelinerun' }).getAttribute('href')).toBe( - '/application-pipeline/workspaces/test-ws/applications/test-app/pipelineruns/test-pipelinerun', + '/application-pipeline/workspaces/target-ws/applications/test-app/pipelineruns/test-pipelinerun', ); }); }); diff --git a/src/hooks/__tests__/useWorkspaceForNamespace.spec.tsx b/src/hooks/__tests__/useWorkspaceForNamespace.spec.tsx new file mode 100644 index 000000000..9ab852a6d --- /dev/null +++ b/src/hooks/__tests__/useWorkspaceForNamespace.spec.tsx @@ -0,0 +1,39 @@ +import * as React from 'react'; +import { renderHook } from '@testing-library/react-hooks'; +import { WorkspaceProvider } from '../../utils/workspace-context-utils'; +import { useWorkspaceForNamespace } from '../useWorkspaceForNamespace'; + +describe('useWorkspaceForNamespace', () => { + it('should find correct workspace', () => { + const wrapper = ({ children }) => ( + {}, + workspacesLoaded: false, + }} + > + {children} + + ); + const { result } = renderHook(() => useWorkspaceForNamespace('my-ns'), { wrapper }); + expect(result.current.metadata.name).toBe('ws1'); + }); + + it('should return undefined if workspace is not found', () => { + const { result } = renderHook(() => useWorkspaceForNamespace('my-ns-1')); + expect(result.current).toBe(undefined); + }); + + it('should return undefined if namespace is not provided', () => { + const { result } = renderHook(() => useWorkspaceForNamespace('')); + expect(result.current).toBe(undefined); + }); +}); diff --git a/src/hooks/__tests__/useWorkspaceResource.spec.ts b/src/hooks/__tests__/useWorkspaceResource.spec.ts new file mode 100644 index 000000000..14c0383fd --- /dev/null +++ b/src/hooks/__tests__/useWorkspaceResource.spec.ts @@ -0,0 +1,24 @@ +import { renderHook } from '@testing-library/react-hooks'; +import { useWorkspaceForNamespace } from '../useWorkspaceForNamespace'; +import { useWorkspaceResource } from '../useWorkspaceResource'; + +jest.mock('../useWorkspaceForNamespace', () => ({ + useWorkspaceForNamespace: jest.fn(), +})); + +const useWorkspaceMock = useWorkspaceForNamespace as jest.Mock; + +describe('useWorkspaceResource', () => { + it('should return correct resource name and workspace', () => { + useWorkspaceMock.mockReturnValue({ metadata: { name: 'ws1' } }); + const { result } = renderHook(() => useWorkspaceResource('my-ns/my-resource')); + expect(useWorkspaceMock).toHaveBeenCalledWith('my-ns'); + expect(result.current).toEqual(['my-resource', 'ws1']); + }); + + it('should return undefined if invalid string is provided', () => { + useWorkspaceMock.mockReturnValue(undefined); + const { result } = renderHook(() => useWorkspaceResource('')); + expect(result.current).toEqual([undefined, undefined]); + }); +}); diff --git a/src/hooks/useWorkspaceForNamespace.ts b/src/hooks/useWorkspaceForNamespace.ts new file mode 100644 index 000000000..6bd7d4e7d --- /dev/null +++ b/src/hooks/useWorkspaceForNamespace.ts @@ -0,0 +1,14 @@ +import * as React from 'react'; +import { WorkspaceContext } from '../utils/workspace-context-utils'; + +export const useWorkspaceForNamespace = (namespace?: string) => { + const { workspaces } = React.useContext(WorkspaceContext); + + return React.useMemo(() => { + if (!namespace) { + return undefined; + } + + return workspaces.find((w) => w.status?.namespaces?.some((ns) => ns.name === namespace)); + }, [namespace, workspaces]); +}; diff --git a/src/hooks/useWorkspaceResource.ts b/src/hooks/useWorkspaceResource.ts new file mode 100644 index 000000000..63ce4e609 --- /dev/null +++ b/src/hooks/useWorkspaceResource.ts @@ -0,0 +1,21 @@ +import * as React from 'react'; +import { useWorkspaceForNamespace } from './useWorkspaceForNamespace'; + +/** + * @param namespacedObj string with the format `/` + * @returns resource name and workspace if accessible + */ +export const useWorkspaceResource = ( + namespacedObj?: string, +): [resource?: string, workspace?: string] => { + const [namespace, resource] = React.useMemo(() => { + if (!namespacedObj) { + return [undefined, undefined]; + } + return namespacedObj.split('/'); + }, [namespacedObj]); + + const workspace = useWorkspaceForNamespace(namespace); + + return [resource, workspace?.metadata.name]; +}; diff --git a/src/types/workspace.ts b/src/types/workspace.ts index 6d7a1c425..e5f7f2395 100644 --- a/src/types/workspace.ts +++ b/src/types/workspace.ts @@ -2,7 +2,7 @@ import { K8sResourceCommon } from '@openshift/dynamic-plugin-sdk-utils'; export interface Workspace extends K8sResourceCommon { status: { - type: string; + type?: string; namespaces: Namespace[]; owner: string; role: string; diff --git a/src/utils/__tests__/workspace-context-utils.spec.ts b/src/utils/__tests__/workspace-context-utils.spec.ts index c00710ea2..61d3744ae 100644 --- a/src/utils/__tests__/workspace-context-utils.spec.ts +++ b/src/utils/__tests__/workspace-context-utils.spec.ts @@ -208,4 +208,18 @@ describe('useActiveWorkspace', () => { ); }); }); + + it('should update workspace if url segment changes', async () => { + k8sListResourceItemsMock.mockReturnValue(mockWorkspaces); + window.location.pathname = '/application-pipeline/workspaces/workspace-one/applications'; + const { result, waitForNextUpdate, rerender } = renderHook(() => useActiveWorkspace()); + await waitForNextUpdate(); + + expect(result.current.workspace).toBe('workspace-one'); + + window.location.pathname = '/application-pipeline/workspaces/workspace-two/applications'; + rerender(); + + expect(result.current.workspace).toBe('workspace-two'); + }); }); diff --git a/src/utils/workspace-context-utils.ts b/src/utils/workspace-context-utils.ts index 287aaae1b..1e8689ce8 100644 --- a/src/utils/workspace-context-utils.ts +++ b/src/utils/workspace-context-utils.ts @@ -35,7 +35,8 @@ export const useWorkspaceInfo = () => { const { namespace, workspace } = React.useContext(WorkspaceContext); return { namespace, workspace }; }; -export const getHomeWorkspace = (workspaces) => workspaces?.find((w) => w?.status?.type === 'home'); +export const getHomeWorkspace = (workspaces: Workspace[]) => + workspaces?.find((w) => w?.status?.type === 'home'); export const useActiveWorkspace = (): WorkspaceContextData => { const lastUsedWorkspace = useLastUsedWorkspace(); @@ -43,13 +44,16 @@ export const useActiveWorkspace = (): WorkspaceContextData => { const [, workspaceFromUrl = ''] = window.location.pathname.match(workspacePathMatcher) || []; const [workspace, setWorkspace] = React.useState(getActiveWorkspace); const [namespace, setNamespace] = React.useState(''); - const [workspaces, setWorkspaces] = React.useState([]); + const [workspaces, setWorkspaces] = React.useState([]); const [workspacesLoaded, setWorkspacesLoaded] = React.useState(false); - const getDefaultNsForWorkspace = React.useCallback((allWorkspaces, currentWorkspace) => { - const obj = allWorkspaces?.find((w) => w.metadata.name === currentWorkspace); - return obj?.status?.namespaces.find((n) => n.type === 'default'); - }, []); + const getDefaultNsForWorkspace = React.useCallback( + (allWorkspaces: Workspace[], currentWorkspace: string) => { + const obj = allWorkspaces?.find((w) => w.metadata.name === currentWorkspace); + return obj?.status?.namespaces.find((n) => n.type === 'default'); + }, + [], + ); React.useEffect(() => { if (workspace && workspaces?.length > 0) { @@ -61,13 +65,24 @@ export const useActiveWorkspace = (): WorkspaceContextData => { } }, [getDefaultNsForWorkspace, setNamespace, workspace, workspaces]); + // switch workspace if URL segment has changed + React.useEffect(() => { + if ( + workspace && + workspaceFromUrl && + workspaceFromUrl !== workspace && + workspaces.some((w) => w.metadata.name === workspaceFromUrl) + ) { + setWorkspace(workspaceFromUrl); + } + }, [workspace, workspaceFromUrl, workspaces]); + React.useEffect(() => { let unmounted = false; const fetchWorkspaces = async () => { - let allWorkspaces = []; + let allWorkspaces: Workspace[] = []; try { - setActiveWorkspace(''); // to fetch root level workspaces - allWorkspaces = await k8sListResourceItems({ + allWorkspaces = await k8sListResourceItems({ model: WorkspaceModel, }); } catch (e) { @@ -79,6 +94,8 @@ export const useActiveWorkspace = (): WorkspaceContextData => { return; } + setActiveWorkspace(''); // to fetch root level workspaces + let ws: string; if (Array.isArray(allWorkspaces)) { const workspaceNames = allWorkspaces.map((dataResource) => dataResource.metadata.name);