From fe0f71b7ef385b6588c15836956b9b17df05bfec Mon Sep 17 00:00:00 2001 From: Josiah Clumont Date: Thu, 23 Jul 2026 17:11:45 +1200 Subject: [PATCH] fix(helm): stop cosmetic not-found toast on release uninstall [C9S-192] (#3231) --- .../useUninstallHelmAppMutation.test.tsx | 82 +++++++++++++++++++ .../useUninstallHelmAppMutation.ts | 24 ++++-- 2 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 app/react/kubernetes/helm/helmReleaseQueries/useUninstallHelmAppMutation.test.tsx diff --git a/app/react/kubernetes/helm/helmReleaseQueries/useUninstallHelmAppMutation.test.tsx b/app/react/kubernetes/helm/helmReleaseQueries/useUninstallHelmAppMutation.test.tsx new file mode 100644 index 0000000000..473332877b --- /dev/null +++ b/app/react/kubernetes/helm/helmReleaseQueries/useUninstallHelmAppMutation.test.tsx @@ -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 ( +
+ {releaseQuery.data?.name ?? 'no-release'} + +
+ ); +} + +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(); + + // 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(); +}); diff --git a/app/react/kubernetes/helm/helmReleaseQueries/useUninstallHelmAppMutation.ts b/app/react/kubernetes/helm/helmReleaseQueries/useUninstallHelmAppMutation.ts index cb75ac09cf..b8cd034138 100644 --- a/app/react/kubernetes/helm/helmReleaseQueries/useUninstallHelmAppMutation.ts +++ b/app/react/kubernetes/helm/helmReleaseQueries/useUninstallHelmAppMutation.ts @@ -1,7 +1,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; 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 { EnvironmentId } from '@/react/portainer/environments/types'; @@ -17,11 +17,25 @@ export function useUninstallHelmAppMutation(environmentId: EnvironmentId) { releaseName: string; namespace?: string; }) => uninstallHelmApplication(environmentId, releaseName, namespace), - ...withInvalidate(queryClient, [ - queryKeys.releases(environmentId), - applicationsQueryKeys.applications(environmentId), - ]), ...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) + ); + }, }); }