Files
portainer/app/react/docker/containers/ItemView/ItemView.tsx
T
claude code agent e9fae32b43 test(stacks): cover orphaned branch; name the stack-container state; type link params (F1,F2,F4)
Maintainer pre-merge review follow-up:
F1: test the orphaned-stack breadcrumb branch (orphaned=true, no regular) —
    href carries stackId/orphaned, not external.
F2: extract STACK_CONTAINER_STATE_NAME so code + test share one literal.
F4: type buildStackLinkParams' return as StackLinkParams (documents the real
    shape; external stays boolean, serialized by ui-router — no runtime change).
F3 (legacy ?id= deep links) answered wontfix in the PR thread.

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

201 lines
6.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 { Crumb } from '@@/PageHeader/Breadcrumbs/Breadcrumbs';
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';
// ui-router state name for a container opened from within a stack. Kept as a
// single source of truth so a future state rename stays in sync with the
// breadcrumb logic (and its test) instead of silently falling back to the
// default crumb.
export const STACK_CONTAINER_STATE_NAME = 'docker.stacks.stack.container';
// Shape of the params handed to the docker.stacks.stack route when building the
// back-to-stack breadcrumb link. Values are strings (inherited route params)
// except `external`, which is emitted as a boolean and serialized to
// `external=true` by ui-router.
type StackLinkParams = {
name?: string;
stackId?: string;
type?: string;
regular?: string;
orphaned?: string;
orphanedRunning?: string;
external?: boolean;
tab?: string;
};
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}
/>
)}
</>
);
}
// When a container is opened from a stack (state STACK_CONTAINER_STATE_NAME),
// keep the stack trail in the breadcrumbs so the user can navigate back to the
// stack. Otherwise fall back to the global containers list.
function getContainerBreadcrumbs(
stateName: string | undefined,
params: Record<string, string | undefined>,
containerName: string
): Array<Crumb | string> {
if (stateName === STACK_CONTAINER_STATE_NAME) {
return [
{ label: 'Stacks', link: 'docker.stacks' },
{
label: params.name || '',
link: 'docker.stacks.stack',
linkParams: buildStackLinkParams(params),
},
containerName,
];
}
return [{ label: 'Containers', link: 'docker.containers' }, containerName];
}
// Rebuild the params expected by the docker.stacks.stack route from the current
// (container) route params, which are inherited from the parent stack state.
function buildStackLinkParams(
params: Record<string, string | undefined>
): StackLinkParams {
// External stacks have no DB id; they are identified by name/type only.
if (params.external === 'true') {
return {
name: params.name,
type: params.type,
external: true,
tab: params.tab,
};
}
return {
name: params.name,
stackId: params.stackId,
type: params.type,
regular: params.regular,
orphaned: params.orphaned,
orphanedRunning: params.orphanedRunning,
tab: params.tab,
};
}
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;
}