Files
portainer/app/react/docker/containers/ItemView/ContainerActionsSection/SecondaryActions/UpdateNowButton.tsx
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

105 lines
3.1 KiB
TypeScript

import { Download } from 'lucide-react';
import { useRouter } from '@uirouter/react';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { useContainerImageStatus } from '@/react/docker/containers/queries/useContainerImageStatus';
import { useApplyContainerImageUpdate } from '@/react/docker/containers/update';
import { ButtonGroup, LoadingButton } from '@@/buttons';
import { TooltipWithChildren } from '@@/Tip/TooltipWithChildren';
import { ContainerId } from '../../../types';
interface UpdateNowButtonProps {
environmentId: EnvironmentId;
containerId: ContainerId;
nodeName?: string;
containerImage: string;
containerName: string;
labels?: Record<string, string>;
isPortainer: boolean;
}
/**
* "Update now" surfaces a discoverable per-container apply action ONLY when the
* image is `outdated`. It routes through the shared update primitive:
* standalone -> recreate-with-pull, stack-managed -> stack redeploy-with-pull
* (container stays in its stack). Externally-managed compose containers are
* shown disabled with an explanatory tooltip, never recreated out-of-band.
*
* A stack redeploy is gated by `PortainerStackUpdate` (as everywhere else in the
* app): a user with container-create but without stack-update rights sees the
* button disabled with a tooltip rather than getting a 403 on click.
*/
export function UpdateNowButton({
environmentId,
containerId,
nodeName,
containerImage,
containerName,
labels,
isPortainer,
}: UpdateNowButtonProps) {
const router = useRouter();
const statusQuery = useContainerImageStatus(
environmentId,
containerId,
nodeName
);
const { apply, isLoading, isExternal, stackUpdateForbidden, canApply } =
useApplyContainerImageUpdate({
environmentId,
containerId,
nodeName,
containerImage,
containerName,
labels,
isPortainer,
// The details view reloads to reflect the recreated/redeployed container.
onSuccess: () =>
router.stateService.go('docker.containers', {}, { reload: true }),
});
// Only meaningful when a newer image is actually available.
if (statusQuery.data?.Status !== 'outdated') {
return null;
}
const button = (
<LoadingButton
color="primary"
size="small"
onClick={apply}
disabled={!canApply}
isLoading={isLoading}
loadingText="Updating..."
data-cy="update-now-button"
icon={Download}
>
Update now
</LoadingButton>
);
if (isExternal) {
return (
<ButtonGroup>
<TooltipWithChildren message="This container belongs to a compose project that is managed outside Portainer, so it can't be updated from here.">
{button}
</TooltipWithChildren>
</ButtonGroup>
);
}
if (stackUpdateForbidden) {
return (
<ButtonGroup>
<TooltipWithChildren message="Updating this container redeploys its stack, which requires stack update permission you don't have.">
{button}
</TooltipWithChildren>
</ButtonGroup>
);
}
return <ButtonGroup>{button}</ButtonGroup>;
}