Files
claude code agent f7cb0f3241 feat(automation): "Update now" action (stack-aware) + bulk update (#10, epic #3 M3)
Add a discoverable per-container "Update now" action, shown only when the
image status is `outdated`, plus a bulk "Update selected" action in the
containers list.

Both manual paths share ONE apply primitive (applyContainerUpdate /
useUpdateContainerImage) that also backs the future M4 auto-update job:

- standalone container  -> recreate-with-pull (existing recreate endpoint)
- stack-managed         -> stack redeploy-with-pull (existing git/file stack
                           update mutations), so the container stays in its
                           stack and is never recreated out-of-band
- externally-managed    -> refused; the details button is disabled with an
  compose                  explanatory tooltip and the bulk action skips it

Decision logic lives in the pure, unit-tested resolveContainerUpdatePath /
groupContainersForUpdate helpers. The bulk action filters to outdated
containers and redeploys each owning stack exactly once even when several of
its containers are selected, reporting per-item success/failure.

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

73 lines
2.2 KiB
TypeScript

import { EnvironmentId } from '@/react/portainer/environments/types';
import { useAuthorizations } from '@/react/hooks/useUser';
import { ContainerDetailsViewModel } from '@/docker/models/containerDetails';
import { isPartOfSwarmService } from '@/docker/helpers/containers';
import { trimContainerName } from '@/docker/filters/utils';
import { Widget, WidgetBody } from '@@/Widget';
import { PrimaryActions } from './PrimaryActions';
import { SecondaryActions } from './SecondaryActions';
interface Props {
environmentId: EnvironmentId;
nodeName?: string;
container: ContainerDetailsViewModel;
onSuccess?(): void;
}
export function ContainerActionsSection({
environmentId,
nodeName,
container,
onSuccess,
}: Props) {
const authorizedQuery = useAuthorizations([
'DockerContainerStart',
'DockerContainerStop',
'DockerContainerKill',
'DockerContainerRestart',
'DockerContainerPause',
'DockerContainerUnpause',
'DockerContainerDelete',
'DockerContainerCreate',
]);
if (!authorizedQuery.authorized || !container.Id) {
return null;
}
const isRunning = container.State?.Running || false;
const isPaused = container.State?.Paused || false;
const isPortainer = container.IsPortainer || false;
return (
<Widget>
<Widget.Title icon="settings" title="Actions" />
<WidgetBody>
<div className="flex gap-2">
<PrimaryActions
environmentId={environmentId}
containerId={container.Id}
nodeName={nodeName}
isRunning={isRunning}
isPaused={isPaused}
isPortainer={isPortainer}
onSuccess={onSuccess}
/>
<SecondaryActions
environmentId={environmentId}
containerId={container.Id}
containerImage={container.Config?.Image || ''}
containerName={trimContainerName(container.Name) || container.Id}
containerLabels={container.Config?.Labels ?? undefined}
containerAutoRemove={container.HostConfig?.AutoRemove}
nodeName={nodeName}
partOfSwarmService={isPartOfSwarmService(container)}
isPortainer={isPortainer}
/>
</div>
</WidgetBody>
</Widget>
);
}