Files
claude code agent cb11b0fca4 fix(stacks): keep stack breadcrumb trail when opening a container from a stack (#4)
Opening a container from a stack's Containers table showed
"Home > Containers > <container>" instead of keeping the stack trail,
so the user could not navigate back to the stack.

Two root causes are addressed:

1. Route param collision: docker.stacks.stack used the query param `id`
   for the numeric stack DB id, while its child docker.stacks.stack.container
   uses the path param `id` for the container id. Navigating into a container
   overwrote the stack id. The stack id param is renamed `id` -> `stackId`
   everywhere it is read or written (route url, stacks datatable link,
   create-stack redirect, gitops workflow card link, stack ItemView reader).

2. Hardcoded breadcrumbs: the container details ItemView always rendered the
   global "Containers" crumb. Breadcrumbs are now state-aware: when reached
   via docker.stacks.stack.container the stack trail
   (Stacks > <stack> > <container>) is rebuilt from the inherited stack params,
   honoring external/orphaned stacks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 07:44:18 +03:00

147 lines
4.2 KiB
TypeScript

import { useCurrentStateAndParams } from '@uirouter/react';
import { useQueryClient } from '@tanstack/react-query';
import { useEffect } from 'react';
import { StackContainersDatatable } from '@/react/docker/stacks/ItemView/StackContainersDatatable';
import { AccessControlPanel } from '@/react/portainer/access-control';
import { useStack } from '@/react/common/stacks/queries/useStack';
import { Stack, StackStatus, StackType } from '@/react/common/stacks/types';
import { ResourceControlViewModel } from '@/react/portainer/access-control/models/ResourceControlViewModel';
import { ResourceControlType } from '@/react/portainer/access-control/types';
import { queryKeys } from '@/react/common/stacks/queries/query-keys';
import { notifyError } from '@/portainer/services/notifications';
import { useUpdateStackResourcesOnDeployment } from '@/react/docker/stacks/ItemView/useUpdateStackResourcesOnDeployment';
import { PageHeader } from '@@/PageHeader';
import { StackDetails } from './StackDetails';
import { StackServicesDatatable } from './StackServicesDatatable';
export function ItemView() {
const {
isExternal,
isOrphaned,
isOrphanedRunning,
isRegular,
stackName,
stackId,
stackType,
} = useParams();
const queryClient = useQueryClient();
const stackQuery = useStack(stackId, {
enabled: isRegular || isOrphaned,
refetchInterval: (data) =>
data?.Status === StackStatus.Deploying ? 3000 : false,
});
const stack = stackQuery.data;
useUpdateStackResourcesOnDeployment(stack);
const resourceControl = stack?.ResourceControl
? new ResourceControlViewModel(stack.ResourceControl)
: undefined;
useEffect(() => {
if (
isInvalidStackType({
isExternal,
isOrphaned,
isOrphanedRunning,
stackType,
})
) {
notifyError('Failure', undefined, 'Invalid type URL parameter.');
}
}, [isExternal, isOrphaned, isOrphanedRunning, stackType]);
return (
<>
<PageHeader
title="Stack details"
breadcrumbs={[{ label: 'Stacks', link: '^' }, stackName]}
/>
<StackDetails
isExternal={isExternal}
isOrphaned={isOrphaned}
isOrphanedRunning={isOrphanedRunning}
isRegular={isRegular}
stackName={stackName}
stack={stack}
/>
{(!isOrphaned || isOrphanedRunning) && (
<>
{stackType === StackType.DockerCompose && (
<StackContainersDatatable stackName={stackName} />
)}
{stackType === StackType.DockerSwarm && (
<StackServicesDatatable name={stackName} />
)}
</>
)}
{stack && !isOrphaned && (
<AccessControlPanel
environmentId={stack.EndpointId}
resourceId={`${stack.EndpointId}_${stack.Name}`}
resourceControl={resourceControl}
resourceType={ResourceControlType.Stack}
onUpdateSuccess={() =>
queryClient.invalidateQueries(queryKeys.stack(stackId))
}
/>
)}
</>
);
}
function isInvalidStackType({
isExternal,
isOrphaned,
isOrphanedRunning,
stackType,
}: {
isExternal: boolean;
isOrphaned: boolean;
isOrphanedRunning: boolean;
stackType: StackType | undefined;
}) {
return (
(isExternal || (isOrphaned && isOrphanedRunning)) &&
(!stackType ||
(stackType !== StackType.DockerSwarm &&
stackType !== StackType.DockerCompose))
);
}
function useParams() {
/*
TODO Check:
why use stack.Name or params.stackName?
why use booleans from params instead of figuring out by name/id
why stack.EndpointID and not params.envId?
*/
const { params } = useCurrentStateAndParams();
const isRegular = params.regular === 'true';
const isExternal = params.external === 'true';
const isOrphaned = params.orphaned === 'true';
const isOrphanedRunning = params.orphanedRunning === 'true';
const stackName = params.name || ('' as string);
const id = params.stackId
? (parseInt(params.stackId, 10) as Stack['Id'])
: undefined;
const type = ['1', '2', '3'].includes(params.type)
? (parseInt(params.type, 10) as StackType)
: undefined;
return {
isExternal,
isRegular,
isOrphaned,
isOrphanedRunning,
stackName,
stackId: id,
stackType: type,
};
}