1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-11-24 20:06:55 +01:00

feat: delete safeguard button (#10974)

This commit is contained in:
Mateusz Kwasniewski 2025-11-13 12:01:53 +01:00 committed by GitHub
parent 684a0ff48c
commit 71099247e7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 76 additions and 11 deletions

View File

@ -132,7 +132,7 @@ export const ReleasePlan = ({
const { removeReleasePlanFromFeature, startReleasePlanMilestone } = const { removeReleasePlanFromFeature, startReleasePlanMilestone } =
useReleasePlansApi(); useReleasePlansApi();
const { deleteMilestoneProgression } = useMilestoneProgressionsApi(); const { deleteMilestoneProgression } = useMilestoneProgressionsApi();
const { createOrUpdateSafeguard } = useSafeguardsApi(); const { createOrUpdateSafeguard, deleteSafeguard } = useSafeguardsApi();
const { setToastData, setToastApiError } = useToast(); const { setToastData, setToastApiError } = useToast();
const { trackEvent } = usePlausibleTracker(); 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 ( return (
<StyledContainer> <StyledContainer>
<StyledHeader> <StyledHeader>
@ -436,6 +454,7 @@ export const ReleasePlan = ({
safeguard={safeguards[0]} safeguard={safeguards[0]}
onSubmit={handleSafeguardSubmit} onSubmit={handleSafeguardSubmit}
onCancel={() => setSafeguardFormOpen(false)} onCancel={() => setSafeguardFormOpen(false)}
onDelete={handleSafeguardDelete}
/> />
) : safeguardFormOpen ? ( ) : safeguardFormOpen ? (
<SafeguardForm <SafeguardForm

View File

@ -1,5 +1,6 @@
import { Button, FormControl, TextField } from '@mui/material'; import { Button, FormControl, IconButton, TextField } from '@mui/material';
import ShieldIcon from '@mui/icons-material/Shield'; import ShieldIcon from '@mui/icons-material/Shield';
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
import type { FormEvent } from 'react'; import type { FormEvent } from 'react';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { useImpactMetricsOptions } from 'hooks/api/getters/useImpactMetricsMetadata/useImpactMetricsMetadata'; import { useImpactMetricsOptions } from 'hooks/api/getters/useImpactMetricsMetadata/useImpactMetricsMetadata';
@ -27,6 +28,7 @@ const StyledIcon = createStyledIcon(ShieldIcon);
interface ISafeguardFormProps { interface ISafeguardFormProps {
onSubmit: (data: CreateSafeguardSchema) => void; onSubmit: (data: CreateSafeguardSchema) => void;
onCancel: () => void; onCancel: () => void;
onDelete?: () => void;
safeguard?: ISafeguard; safeguard?: ISafeguard;
} }
@ -63,6 +65,7 @@ const getDefaultAggregationMode = (
export const SafeguardForm = ({ export const SafeguardForm = ({
onSubmit, onSubmit,
onCancel, onCancel,
onDelete,
safeguard, safeguard,
}: ISafeguardFormProps) => { }: ISafeguardFormProps) => {
const { metricOptions, loading } = useImpactMetricsOptions(); const { metricOptions, loading } = useImpactMetricsOptions();
@ -208,11 +211,27 @@ export const SafeguardForm = ({
const showButtons = mode === 'create' || mode === 'edit'; const showButtons = mode === 'create' || mode === 'edit';
const handleDelete = () => {
if (onDelete) {
onDelete();
}
};
return ( return (
<StyledFormContainer onSubmit={handleSubmit}> <StyledFormContainer onSubmit={handleSubmit}>
<StyledTopRow> <StyledTopRow sx={{ mb: 1 }}>
<StyledIcon /> <StyledIcon />
<StyledLabel>Pause automation when</StyledLabel> <StyledLabel>Pause automation when</StyledLabel>
{mode !== 'create' && (
<IconButton
onClick={handleDelete}
size='small'
aria-label='Delete safeguard'
sx={{ padding: 0.5, marginLeft: 'auto' }}
>
<DeleteOutlineIcon fontSize='small' />
</IconButton>
)}
</StyledTopRow> </StyledTopRow>
<StyledTopRow> <StyledTopRow>
<MetricSelector <MetricSelector

View File

@ -88,7 +88,7 @@ test('filters by flag type', async () => {
await screen.findByText('Flag type'); await screen.findByText('Flag type');
await screen.findByText('Operational'); await screen.findByText('Operational');
}); }, 10000);
test('selects project features', async () => { test('selects project features', async () => {
setupApi(); setupApi();
@ -130,7 +130,7 @@ test('selects project features', async () => {
// deselect a single item // deselect a single item
fireEvent.click(selectFeatureA); fireEvent.click(selectFeatureA);
expect(screen.queryByTestId(BATCH_SELECTED_COUNT)).not.toBeInTheDocument(); expect(screen.queryByTestId(BATCH_SELECTED_COUNT)).not.toBeInTheDocument();
}); }, 10000);
test('filters by tag', async () => { test('filters by tag', async () => {
setupApi(); setupApi();
@ -155,7 +155,7 @@ test('filters by tag', async () => {
await screen.findByText('include'); await screen.findByText('include');
expect(await screen.findAllByText('backend:sdk')).toHaveLength(2); expect(await screen.findAllByText('backend:sdk')).toHaveLength(2);
}); }, 10000);
test('filters by flag author', async () => { test('filters by flag author', async () => {
setupApi(); setupApi();
@ -184,7 +184,7 @@ test('filters by flag author', async () => {
fireEvent.click(authorA); fireEvent.click(authorA);
expect(window.location.href).toContain('createdBy=IS%3A1'); expect(window.location.href).toContain('createdBy=IS%3A1');
}); }, 10000);
test('Project is onboarded', async () => { test('Project is onboarded', async () => {
const projectId = 'default'; const projectId = 'default';
@ -208,7 +208,7 @@ test('Project is onboarded', async () => {
expect( expect(
screen.queryByText('Welcome to your project'), screen.queryByText('Welcome to your project'),
).not.toBeInTheDocument(); ).not.toBeInTheDocument();
}); }, 10000);
test('Project is not onboarded', async () => { test('Project is not onboarded', async () => {
const projectId = 'default'; const projectId = 'default';
@ -230,7 +230,7 @@ test('Project is not onboarded', async () => {
}, },
); );
await screen.findByText('Welcome to your project'); await screen.findByText('Welcome to your project');
}); }, 10000);
test('renders lifecycle quick filters', async () => { test('renders lifecycle quick filters', async () => {
setupApi(); setupApi();
@ -255,7 +255,7 @@ test('renders lifecycle quick filters', async () => {
await screen.findByText(/Develop/); await screen.findByText(/Develop/);
await screen.findByText(/Rollout production/); await screen.findByText(/Rollout production/);
await screen.findByText(/Cleanup/); await screen.findByText(/Cleanup/);
}); }, 10000);
test('clears lifecycle filter when switching to archived view', async () => { test('clears lifecycle filter when switching to archived view', async () => {
setupApi(); 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).not.toContain('lifecycle=IS%3Alive');
expect(window.location.href).toContain('archived=IS%3Atrue'); expect(window.location.href).toContain('archived=IS%3Atrue');
}); }, 10000);

View File

@ -9,6 +9,13 @@ export interface CreateSafeguardParams {
body: CreateSafeguardSchema; body: CreateSafeguardSchema;
} }
export interface DeleteSafeguardParams {
projectId: string;
featureName: string;
environment: string;
planId: string;
}
export const useSafeguardsApi = () => { export const useSafeguardsApi = () => {
const { makeRequest, createRequest, errors, loading } = useAPI({ const { makeRequest, createRequest, errors, loading } = useAPI({
propagateErrors: true, propagateErrors: true,
@ -35,8 +42,28 @@ export const useSafeguardsApi = () => {
await makeRequest(req.caller, req.id); await makeRequest(req.caller, req.id);
}; };
const deleteSafeguard = async ({
projectId,
featureName,
environment,
planId,
}: DeleteSafeguardParams): Promise<void> => {
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 { return {
createOrUpdateSafeguard, createOrUpdateSafeguard,
deleteSafeguard,
errors, errors,
loading, loading,
}; };