From 71099247e76b708f312681a9ba9c0e312238d638 Mon Sep 17 00:00:00 2001 From: Mateusz Kwasniewski Date: Thu, 13 Nov 2025 12:01:53 +0100 Subject: [PATCH] feat: delete safeguard button (#10974) --- .../ReleasePlan/ReleasePlan.tsx | 21 ++++++++++++++- .../SafeguardForm/SafeguardForm.tsx | 23 ++++++++++++++-- .../ProjectFeatureToggles.test.tsx | 16 +++++------ .../useSafeguardsApi/useSafeguardsApi.ts | 27 +++++++++++++++++++ 4 files changed, 76 insertions(+), 11 deletions(-) diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlan.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlan.tsx index ea9f5614ed..3d1a771e23 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlan.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlan.tsx @@ -132,7 +132,7 @@ export const ReleasePlan = ({ const { removeReleasePlanFromFeature, startReleasePlanMilestone } = useReleasePlansApi(); const { deleteMilestoneProgression } = useMilestoneProgressionsApi(); - const { createOrUpdateSafeguard } = useSafeguardsApi(); + const { createOrUpdateSafeguard, deleteSafeguard } = useSafeguardsApi(); const { setToastData, setToastApiError } = useToast(); const { trackEvent } = usePlausibleTracker(); @@ -400,6 +400,24 @@ export const ReleasePlan = ({ } }; + const handleSafeguardDelete = async () => { + try { + await deleteSafeguard({ + projectId, + featureName, + environment, + planId: id, + }); + setToastData({ + type: 'success', + text: 'Safeguard deleted successfully', + }); + refetch(); + } catch (error: unknown) { + setToastApiError(formatUnknownError(error)); + } + }; + return ( @@ -436,6 +454,7 @@ export const ReleasePlan = ({ safeguard={safeguards[0]} onSubmit={handleSafeguardSubmit} onCancel={() => setSafeguardFormOpen(false)} + onDelete={handleSafeguardDelete} /> ) : safeguardFormOpen ? ( void; onCancel: () => void; + onDelete?: () => void; safeguard?: ISafeguard; } @@ -63,6 +65,7 @@ const getDefaultAggregationMode = ( export const SafeguardForm = ({ onSubmit, onCancel, + onDelete, safeguard, }: ISafeguardFormProps) => { const { metricOptions, loading } = useImpactMetricsOptions(); @@ -208,11 +211,27 @@ export const SafeguardForm = ({ const showButtons = mode === 'create' || mode === 'edit'; + const handleDelete = () => { + if (onDelete) { + onDelete(); + } + }; + return ( - + Pause automation when + {mode !== 'create' && ( + + + + )} { await screen.findByText('Flag type'); await screen.findByText('Operational'); -}); +}, 10000); test('selects project features', async () => { setupApi(); @@ -130,7 +130,7 @@ test('selects project features', async () => { // deselect a single item fireEvent.click(selectFeatureA); expect(screen.queryByTestId(BATCH_SELECTED_COUNT)).not.toBeInTheDocument(); -}); +}, 10000); test('filters by tag', async () => { setupApi(); @@ -155,7 +155,7 @@ test('filters by tag', async () => { await screen.findByText('include'); expect(await screen.findAllByText('backend:sdk')).toHaveLength(2); -}); +}, 10000); test('filters by flag author', async () => { setupApi(); @@ -184,7 +184,7 @@ test('filters by flag author', async () => { fireEvent.click(authorA); expect(window.location.href).toContain('createdBy=IS%3A1'); -}); +}, 10000); test('Project is onboarded', async () => { const projectId = 'default'; @@ -208,7 +208,7 @@ test('Project is onboarded', async () => { expect( screen.queryByText('Welcome to your project'), ).not.toBeInTheDocument(); -}); +}, 10000); test('Project is not onboarded', async () => { const projectId = 'default'; @@ -230,7 +230,7 @@ test('Project is not onboarded', async () => { }, ); await screen.findByText('Welcome to your project'); -}); +}, 10000); test('renders lifecycle quick filters', async () => { setupApi(); @@ -255,7 +255,7 @@ test('renders lifecycle quick filters', async () => { await screen.findByText(/Develop/); await screen.findByText(/Rollout production/); await screen.findByText(/Cleanup/); -}); +}, 10000); test('clears lifecycle filter when switching to archived view', async () => { setupApi(); @@ -291,4 +291,4 @@ test('clears lifecycle filter when switching to archived view', async () => { expect(window.location.href).not.toContain('lifecycle=IS%3Alive'); expect(window.location.href).toContain('archived=IS%3Atrue'); -}); +}, 10000); diff --git a/frontend/src/hooks/api/actions/useSafeguardsApi/useSafeguardsApi.ts b/frontend/src/hooks/api/actions/useSafeguardsApi/useSafeguardsApi.ts index 688a0d5ee3..bd1b862e88 100644 --- a/frontend/src/hooks/api/actions/useSafeguardsApi/useSafeguardsApi.ts +++ b/frontend/src/hooks/api/actions/useSafeguardsApi/useSafeguardsApi.ts @@ -9,6 +9,13 @@ export interface CreateSafeguardParams { body: CreateSafeguardSchema; } +export interface DeleteSafeguardParams { + projectId: string; + featureName: string; + environment: string; + planId: string; +} + export const useSafeguardsApi = () => { const { makeRequest, createRequest, errors, loading } = useAPI({ propagateErrors: true, @@ -35,8 +42,28 @@ export const useSafeguardsApi = () => { await makeRequest(req.caller, req.id); }; + const deleteSafeguard = async ({ + projectId, + featureName, + environment, + planId, + }: DeleteSafeguardParams): Promise => { + const requestId = 'deleteSafeguard'; + const path = `api/admin/projects/${projectId}/features/${featureName}/environments/${environment}/release_plans/${planId}/safeguards`; + const req = createRequest( + path, + { + method: 'DELETE', + }, + requestId, + ); + + await makeRequest(req.caller, req.id); + }; + return { createOrUpdateSafeguard, + deleteSafeguard, errors, loading, };