0bf4e71b79
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>
135 lines
4.2 KiB
TypeScript
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;
|
|
}
|