Files
claude code agent 0bf4e71b79 fix(stacks): keep the stack breadcrumb trail on container attribute sub-tabs
When a container is opened from a stack, the detail tab kept the stack
trail (PR #7) but the attribute sub-tabs (Logs, Stats, Inspect, Console,
Attach) dropped it: those tabs were registered only under the global
docker.containers.container.* tree, so navigating to one left the stack
state (and its inherited params) behind, and each sub-view set a hardcoded
"Containers > ..." breadcrumb.

- Register stack-scoped child states docker.stacks.stack.container.{attach,
  exec,inspect,logs,stats} mirroring the global ones, so the inherited stack
  params survive and the trail can be kept.
- Centralize the breadcrumb logic in containerBreadcrumbs.ts (moved out of
  ItemView, which re-exports it) and add isStackContainerState +
  getContainerSubTabBreadcrumbs + buildStackContainerLinkParams.
- ActionLinksRow links sub-tabs into the stack tree (with stack+container
  params) when opened from a stack, else the global states unchanged.
- InspectView + the logs/stats/console controllers render the stack-aware
  trail; set up-front (no name) so it survives the load window and errors.

Covers regular/external/orphaned stacks and the non-stack fallback,
matching the existing ItemView breadcrumb behavior. New unit tests in
containerBreadcrumbs.test.ts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 02:25:04 +03:00

135 lines
4.2 KiB
TypeScript

import { useCurrentStateAndParams } from '@uirouter/react';
import { AccessControlPanel } from '@/react/portainer/access-control/AccessControlPanel';
import { ContainerDetailsViewModel } from '@/docker/models/containerDetails';
import { ResourceControlType } from '@/react/portainer/access-control/types';
import { trimContainerName } from '@/docker/filters/utils';
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
import { useEnvironmentRegistries } from '@/react/portainer/environments/queries/useEnvironmentRegistries';
import { Registry } from '@/react/portainer/registries/types/registry';
import { PageHeader } from '@@/PageHeader';
import { findBestMatchRegistry } from '@@/ImageConfigFieldset/findRegistryMatch';
import { useContainer } from '../queries/useContainer';
import { ContainerActionsSection } from './ContainerActionsSection/ContainerActionsSection';
import { ContainerStatusSection } from './ContainerStatusSection/ContainerStatusSection';
import { CreateImageSection } from './CreateImageSection/CreateImageSection';
import { ContainerDetailsSection } from './ContainerDetailsSection/ContainerDetailsSection';
import { VolumesSection } from './VolumesSection/VolumesSection';
import { ContainerNetworksDatatable } from './ContainerNetworksDatatable';
import { HealthStatus } from './HealthStatus';
import { getContainerBreadcrumbs } from './containerBreadcrumbs';
// Re-exported for backwards compatibility: the breadcrumb source of truth now
// lives in ./containerBreadcrumbs (shared with the container sub-tab views).
export { STACK_CONTAINER_STATE_NAME } from './containerBreadcrumbs';
export function ItemView() {
const environmentId = useEnvironmentId();
const { state, params } = useCurrentStateAndParams();
const { id: containerId, nodeName } = params;
const containerQuery = useContainer(
{ environmentId, containerId, nodeName },
{ select: (c) => new ContainerDetailsViewModel(c) }
);
const registriesQuery = useEnvironmentRegistries(environmentId);
if (
containerQuery.isLoading ||
!containerQuery.data ||
!registriesQuery.data
) {
return null;
}
const container = containerQuery.data;
const registryId = getRegistryId(container, registriesQuery.data);
async function handleSuccess() {
containerQuery.refetch();
}
const containerName =
trimContainerName(container.Name) || container.Id || 'unknown';
return (
<>
<PageHeader
title="Container details"
breadcrumbs={getContainerBreadcrumbs(
state?.name,
params,
containerName
)}
/>
<div className="mx-4 mb-4 space-y-4 [&>*]:block">
<ContainerActionsSection
environmentId={environmentId}
nodeName={nodeName}
container={container}
/>
<ContainerStatusSection
environmentId={environmentId}
nodeName={nodeName}
container={container}
registryId={registryId}
/>
</div>
<AccessControlPanel
resourceId={container.Id || ''}
resourceControl={container.ResourceControl}
resourceType={ResourceControlType.Container}
onUpdateSuccess={handleSuccess}
environmentId={environmentId}
/>
{container.State?.Health && (
<HealthStatus health={container.State.Health} />
)}
<div className="mx-4 mb-4 space-y-4 [&>*]:block">
<CreateImageSection
environmentId={environmentId}
containerId={container.Id || ''}
/>
<ContainerDetailsSection
environmentId={environmentId}
container={container}
nodeName={nodeName}
/>
<VolumesSection volumes={container.Mounts} nodeName={nodeName} />
</div>
{container.NetworkSettings?.Networks && (
<ContainerNetworksDatatable
dataset={container.NetworkSettings.Networks}
containerId={containerId}
nodeName={nodeName}
/>
)}
</>
);
}
function getRegistryId(
container: ContainerDetailsViewModel,
registries?: Array<Registry>
) {
const imageName = container.Config?.Image;
if (!imageName || !registries) {
return undefined;
}
const registry = findBestMatchRegistry(imageName, registries);
return registry?.Id;
}