e63d2ffe9b
Clicking "Update" on a stack member (and the native auto-update daemon updating one) redeployed the whole compose stack instead of updating just that container. Match Watchtower behaviour: always recreate the single container with a re-pull. The recreate endpoint preserves config + compose labels, so the container stays part of its project. Collapse all update surfaces to a single-container recreate and drop the now-dead stack-aware routing: - frontend: "Update now" button, list badge and bulk "Update selected" now recreate each container individually; remove standalone/stack/external routing, the external refusal, the PortainerStackUpdate gate and the stack-update confirm dialog. - daemon: route every outdated candidate through updateStandalone; remove updateStack, the stack/external grouping and the stackDeployer dependency. - add a regression test asserting a Portainer-managed compose-stack member is recreated individually, not stack-redeployed. Behavioural notes: git/external compose containers are now auto-updated too (were detect-only), and updating a stack member no longer requires PortainerStackUpdate (same auth as the normal Recreate action). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
80 lines
2.2 KiB
TypeScript
80 lines
2.2 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 { 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, which
|
|
* always recreates just this one container with a fresh image pull (the recreate
|
|
* endpoint preserves config + compose labels, so a stack member stays part of
|
|
* its project). The button is disabled only for Portainer's own container, which
|
|
* can't recreate itself.
|
|
*/
|
|
export function UpdateNowButton({
|
|
environmentId,
|
|
containerId,
|
|
nodeName,
|
|
containerImage,
|
|
containerName,
|
|
labels,
|
|
isPortainer,
|
|
}: UpdateNowButtonProps) {
|
|
const router = useRouter();
|
|
const statusQuery = useContainerImageStatus(
|
|
environmentId,
|
|
containerId,
|
|
nodeName
|
|
);
|
|
const { apply, isLoading, canApply } = useApplyContainerImageUpdate({
|
|
environmentId,
|
|
containerId,
|
|
nodeName,
|
|
containerImage,
|
|
containerName,
|
|
labels,
|
|
isPortainer,
|
|
// The details view reloads to reflect the recreated 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>
|
|
);
|
|
|
|
return <ButtonGroup>{button}</ButtonGroup>;
|
|
}
|