Files
portainer/app/react/docker/containers/update/useApplyContainerImageUpdate.ts
T
agent_coder 6aecdfbe46 feat(containers): interactive image-status badge (click to update / re-check)
Make the container image-status badge actionable, matching native Portainer:
- Clicking "Update available" opens the update confirm dialog and runs the
  existing update flow (standalone recreate-with-pull / stack redeploy), gated
  and disabled while in flight to avoid a double submit. The confirm+apply logic
  is extracted from UpdateNowButton into a shared useApplyContainerImageUpdate
  hook so the details button and the list badge share one implementation.
- Clicking "Up to date" re-queries the registry. Because the server caches image
  status (statusCache 5m + remoteDigestCache 5s), a plain refetch was a no-op, so
  the endpoint gains an optional ?force=true that bypasses BOTH caches for a
  manual re-check while still repopulating them; the default (auto badges + the
  auto-update daemon) keeps using the caches unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 19:04:49 +03:00

122 lines
3.8 KiB
TypeScript

import { EnvironmentId } from '@/react/portainer/environments/types';
import { confirmContainerRecreation } from '@/react/docker/containers/ItemView/ConfirmRecreationModal';
import { confirmStackUpdate } from '@/react/common/stacks/common/confirm-stack-update';
import { useStacks } from '@/react/common/stacks/queries/useStacks';
import { notifySuccess } from '@/portainer/services/notifications';
import { useAuthorizations } from '@/react/hooks/useUser';
import { ContainerId } from '../types';
import { resolveContainerUpdatePath } from './resolveContainerUpdatePath';
import { useUpdateContainerImage } from './useUpdateContainerImage';
import { ContainerUpdateContext } from './types';
interface Params {
environmentId: EnvironmentId;
containerId: ContainerId;
nodeName?: string;
containerImage: string;
containerName: string;
labels?: Record<string, string>;
/** Portainer's own container can't update itself, so the action is disabled. */
isPortainer: boolean;
/**
* Post-success side effect. The details-view button navigates/reloads here;
* the list badge leaves it empty and lets react-query invalidation refresh the
* row.
*/
onSuccess?: () => void;
}
/**
* Shared "apply an image update to a single container" action, used by both the
* details-view "Update now" button and the interactive list badge so the confirm
* dialogs, standalone/stack/external routing and permission gating live in ONE
* place.
*
* Routing mirrors the backend: standalone -> recreate-with-pull, stack-managed ->
* stack redeploy-with-pull (container stays in its stack), externally-managed
* compose -> not touched. A stack redeploy is gated by `PortainerStackUpdate`, so
* a user with container-create but without stack-update rights can't apply it
* (surfaced via `stackUpdateForbidden` rather than a 403 on click).
*/
export function useApplyContainerImageUpdate({
environmentId,
containerId,
nodeName,
containerImage,
containerName,
labels,
isPortainer,
onSuccess,
}: Params) {
const stacksQuery = useStacks();
const updateMutation = useUpdateContainerImage();
// A stack redeploy needs stack-update rights, not just container-create.
const { authorized: canUpdateStack } = useAuthorizations(
'PortainerStackUpdate',
environmentId
);
const stacks = stacksQuery.data ?? [];
const path = resolveContainerUpdatePath({ labels, environmentId }, stacks);
const isExternal = path.kind === 'external';
// Stack-managed container the user isn't allowed to redeploy.
const stackUpdateForbidden = path.kind === 'stack' && !canUpdateStack;
const canApply =
!isPortainer &&
!isExternal &&
!stackUpdateForbidden &&
!stacksQuery.isLoading;
return {
apply,
isLoading: updateMutation.isLoading,
isExternal,
stackUpdateForbidden,
canApply,
};
async function apply() {
const context: ContainerUpdateContext = {
id: containerId,
name: containerName,
image: containerImage,
labels,
environmentId,
nodeName,
};
let pullImage: boolean;
if (path.kind === 'stack') {
const result = await confirmStackUpdate(
'This will redeploy the stack pulling the latest images and may cause a service interruption. Do you wish to continue?',
true
);
if (!result) {
return;
}
pullImage = result.repullImageAndRedeploy;
} else {
const cannotPullImage =
!containerImage || containerImage.toLowerCase().startsWith('sha256');
const result = await confirmContainerRecreation(cannotPullImage);
if (!result) {
return;
}
pullImage = result.pullLatest;
}
updateMutation.mutate(
{ context, stacks, pullImage },
{
onSuccess: () => {
notifySuccess('Success', 'Container image update applied');
onSuccess?.();
},
}
);
}
}