6aecdfbe46
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>
122 lines
3.8 KiB
TypeScript
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?.();
|
|
},
|
|
}
|
|
);
|
|
}
|
|
}
|