f7cb0f3241
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>
66 lines
1.8 KiB
TypeScript
66 lines
1.8 KiB
TypeScript
import { Stack } from '@/react/common/stacks/types';
|
|
import { COMPOSE_STACK_NAME_LABEL } from '@/react/constants';
|
|
|
|
import { groupContainersForUpdate } from './groupContainersForUpdate';
|
|
import { ContainerUpdateContext } from './types';
|
|
|
|
function buildContext(
|
|
overrides: Partial<ContainerUpdateContext>
|
|
): ContainerUpdateContext {
|
|
return {
|
|
id: 'c1',
|
|
name: 'container',
|
|
image: 'nginx:latest',
|
|
environmentId: 3,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
const stack = {
|
|
Id: 7,
|
|
Name: 'my-stack',
|
|
EndpointId: 3,
|
|
} as Stack;
|
|
|
|
describe('groupContainersForUpdate', () => {
|
|
it('redeploys a stack only once when several of its containers are selected', () => {
|
|
const contexts = [
|
|
buildContext({
|
|
id: 'a',
|
|
labels: { [COMPOSE_STACK_NAME_LABEL]: 'my-stack' },
|
|
}),
|
|
buildContext({
|
|
id: 'b',
|
|
labels: { [COMPOSE_STACK_NAME_LABEL]: 'my-stack' },
|
|
}),
|
|
];
|
|
|
|
const result = groupContainersForUpdate(contexts, [stack]);
|
|
|
|
expect(result.stacks).toHaveLength(1);
|
|
expect(result.stacks[0].stackId).toBe(7);
|
|
expect(result.standalone).toHaveLength(0);
|
|
expect(result.external).toHaveLength(0);
|
|
});
|
|
|
|
it('partitions standalone, stack and external containers', () => {
|
|
const contexts = [
|
|
buildContext({ id: 'standalone', labels: {} }),
|
|
buildContext({
|
|
id: 'stack',
|
|
labels: { [COMPOSE_STACK_NAME_LABEL]: 'my-stack' },
|
|
}),
|
|
buildContext({
|
|
id: 'external',
|
|
labels: { [COMPOSE_STACK_NAME_LABEL]: 'not-in-portainer' },
|
|
}),
|
|
];
|
|
|
|
const result = groupContainersForUpdate(contexts, [stack]);
|
|
|
|
expect(result.standalone.map((c) => c.id)).toEqual(['standalone']);
|
|
expect(result.stacks.map((s) => s.stackId)).toEqual([7]);
|
|
expect(result.external.map((c) => c.id)).toEqual(['external']);
|
|
});
|
|
});
|