fix(helm): stop cosmetic not-found toast on release uninstall [C9S-192] (#3231)

This commit is contained in:
Josiah Clumont
2026-07-23 17:11:45 +12:00
committed by GitHub
parent dede61ee05
commit fe0f71b7ef
2 changed files with 101 additions and 5 deletions
@@ -0,0 +1,82 @@
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { http, HttpResponse } from 'msw';
import { vi } from 'vitest';
import { server } from '@/setup-tests/server';
import { withTestQueryProvider } from '@/react/test-utils/withTestQuery';
import { useHelmRelease } from './useHelmRelease';
import { useUninstallHelmAppMutation } from './useUninstallHelmAppMutation';
const ENV_ID = 3;
const RELEASE = 'my-release';
const NAMESPACE = 'default';
// Mounts an active release-detail observer next to the uninstall mutation, both
// sharing one query client — the exact situation of the Helm details view.
function Harness() {
const releaseQuery = useHelmRelease(ENV_ID, RELEASE, NAMESPACE, {
enabled: true,
});
const uninstall = useUninstallHelmAppMutation(ENV_ID);
return (
<div>
<span>{releaseQuery.data?.name ?? 'no-release'}</span>
<button
type="button"
onClick={() =>
uninstall.mutate({ releaseName: RELEASE, namespace: NAMESPACE })
}
>
uninstall
</button>
</div>
);
}
test('uninstall does not refetch the deleted release or raise an error toast', async () => {
const user = userEvent.setup();
let detailGetCount = 0;
let released = false;
const onQueryError = vi.fn();
server.use(
http.get('/api/endpoints/:id/kubernetes/helm/:name', () => {
detailGetCount += 1;
// After deletion the backend responds 404 "Release: not found" — the
// response that used to surface as a cosmetic error toast (C9S-192).
if (released) {
return HttpResponse.json(
{ message: 'Release: not found' },
{ status: 404 }
);
}
return HttpResponse.json({ name: RELEASE, version: 1 });
}),
http.delete('/api/endpoints/:id/kubernetes/helm/:name', () => {
released = true;
return new HttpResponse(null, { status: 204 });
})
);
const Wrapped = withTestQueryProvider(Harness, { onQueryError });
render(<Wrapped />);
// initial detail load
await waitFor(() => expect(screen.getByText(RELEASE)).toBeVisible());
expect(detailGetCount).toBe(1);
await user.click(screen.getByRole('button', { name: 'uninstall' }));
// let any (unwanted) invalidation-triggered refetch settle
await waitFor(() => expect(released).toBe(true));
await new Promise((resolve) => {
setTimeout(resolve, 100);
});
// the deleted release must not be refetched, so no 404 and no error toast
expect(detailGetCount).toBe(1);
expect(onQueryError).not.toHaveBeenCalled();
});
@@ -1,7 +1,7 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import axios, { parseAxiosError } from '@/portainer/services/axios/axios'; import axios, { parseAxiosError } from '@/portainer/services/axios/axios';
import { withError, withInvalidate } from '@/react-tools/react-query'; import { withError } from '@/react-tools/react-query';
import { queryKeys as applicationsQueryKeys } from '@/react/kubernetes/applications/queries/query-keys'; import { queryKeys as applicationsQueryKeys } from '@/react/kubernetes/applications/queries/query-keys';
import { EnvironmentId } from '@/react/portainer/environments/types'; import { EnvironmentId } from '@/react/portainer/environments/types';
@@ -17,11 +17,25 @@ export function useUninstallHelmAppMutation(environmentId: EnvironmentId) {
releaseName: string; releaseName: string;
namespace?: string; namespace?: string;
}) => uninstallHelmApplication(environmentId, releaseName, namespace), }) => uninstallHelmApplication(environmentId, releaseName, namespace),
...withInvalidate(queryClient, [
queryKeys.releases(environmentId),
applicationsQueryKeys.applications(environmentId),
]),
...withError('Unable to uninstall helm application'), ...withError('Unable to uninstall helm application'),
onSuccess: (_data, { releaseName, namespace }) => {
// The release is gone. Cancel any in-flight detail/history request for it
// (e.g. an auto-refresh poll) so it can't resolve into a cosmetic
// "Release: not found" error toast. We intentionally do NOT invalidate
// queryKeys.releases here: it is a prefix of the detail and history keys,
// so invalidating it would refetch the just-deleted release and 404 (C9S-192).
if (namespace) {
queryClient.cancelQueries(
queryKeys.release(environmentId, namespace, releaseName)
);
queryClient.cancelQueries(
queryKeys.releaseHistory(environmentId, namespace, releaseName)
);
}
return queryClient.invalidateQueries(
applicationsQueryKeys.applications(environmentId)
);
},
}); });
} }