1
0
mirror of https://github.com/Unleash/unleash.git synced 2026-02-04 20:10:52 +01:00
unleash.unleash/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenu.tsx
Kamala be130979ee
feat: Suggest release templates for production environments (#11279)
Adds a "Choose a release template" suggestion for production
environments without strategies (enterprise only).
When clicked, opens the "Add Strategy" dialog with the release templates
filter preselected.
Non-production environments continue to show the default strategy
suggestion.

## Notes 

This unfortunately turned out to be a big PR 🥲 as it includes some
refactoring to be able to reuse components.
- The "Add strategy" button has been broken out of
`FeatureStrategyMenu`, so the latter can be reused (since we want to
show the same dialog when clicking the button "Choose a release
template");
- The new `EnvironmentTemplateSuggestion` shares styles with
`EnvironmentStrategySuggestion` (which can be found in
`EnvironmentHeader.styles.tsx`);
- `FeatureStrategyMenu` now has a `defaultFilter` prop, so the dialog
can be opened with a preselected filter.

<img width="900" height="349" alt="Screenshot 2026-02-03 at 17 19 32
(2)"
src="https://github.com/user-attachments/assets/27f11e24-163f-4f4d-8134-a5d08ff540ac"
/>
<img width="1379001" height="557" alt="Screenshot 2026-02-03 at 17 20
14"
src="https://github.com/user-attachments/assets/0efe77f5-af3e-498a-b305-fd5c1ed98906"
/>
2026-02-04 11:34:42 +01:00

199 lines
7.3 KiB
TypeScript

import { useEffect, useState } from 'react';
import { Box, Dialog, IconButton, styled, Typography } from '@mui/material';
import CloseIcon from '@mui/icons-material/Close';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
import type { IReleasePlanTemplate } from 'interfaces/releasePlans';
import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests';
import useToast from 'hooks/useToast';
import { useReleasePlansApi } from 'hooks/api/actions/useReleasePlansApi/useReleasePlansApi';
import { useFeatureReleasePlans } from 'hooks/api/getters/useFeatureReleasePlans/useFeatureReleasePlans';
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
import { formatUnknownError } from 'utils/formatUnknownError';
import { ReleasePlanPreview } from './ReleasePlanPreview.tsx';
import {
FeatureStrategyMenuCards,
type StrategyFilterValue,
} from './FeatureStrategyMenuCards/FeatureStrategyMenuCards.tsx';
import { ReleasePlanConfirmationDialog } from './ReleasePlanConfirmationDialog.tsx';
interface IFeatureStrategyMenuProps {
projectId: string;
featureId: string;
environmentId: string;
isStrategyMenuDialogOpen: boolean;
onClose: any;
defaultFilter?: StrategyFilterValue;
}
const StyledHeader = styled(Box)(({ theme }) => ({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: theme.spacing(4, 4, 2, 4),
}));
export const FeatureStrategyMenu = ({
projectId,
featureId,
environmentId,
isStrategyMenuDialogOpen,
onClose,
defaultFilter = null,
}: IFeatureStrategyMenuProps) => {
const [filter, setFilter] = useState<StrategyFilterValue>(defaultFilter);
const { trackEvent } = usePlausibleTracker();
const [selectedTemplate, setSelectedTemplate] =
useState<IReleasePlanTemplate>();
const [releasePlanPreview, setReleasePlanPreview] = useState(false);
const [addReleasePlanConfirmationOpen, setAddReleasePlanConfirmationOpen] =
useState(false);
const { setToastApiError, setToastData } = useToast();
const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
const { addChange } = useChangeRequestApi();
const { refetch: refetchChangeRequests } =
usePendingChangeRequests(projectId);
const { refetch, releasePlans } = useFeatureReleasePlans(
projectId,
featureId,
environmentId,
);
const { addReleasePlanToFeature } = useReleasePlansApi();
const crProtected = isChangeRequestConfigured(environmentId);
const activeReleasePlan = releasePlans[0];
useEffect(() => {
if (!isStrategyMenuDialogOpen) return;
setReleasePlanPreview(false);
setFilter(defaultFilter);
}, [isStrategyMenuDialogOpen, defaultFilter]);
const addReleasePlan = async (
template: IReleasePlanTemplate,
confirmed?: boolean,
) => {
try {
if (!confirmed && activeReleasePlan) {
setAddReleasePlanConfirmationOpen(true);
return;
}
if (crProtected) {
await addChange(projectId, environmentId, {
feature: featureId,
action: 'addReleasePlan',
payload: {
templateId: template.id,
},
});
setToastData({
type: 'success',
text: 'Added to draft',
});
refetchChangeRequests();
} else {
await addReleasePlanToFeature(
featureId,
template.id,
projectId,
environmentId,
);
setToastData({
type: 'success',
text: 'Release plan added',
});
refetch();
}
trackEvent('release-management', {
props: {
eventType: 'add-plan',
plan: template.name,
},
});
setAddReleasePlanConfirmationOpen(false);
setSelectedTemplate(undefined);
onClose();
} catch (error: unknown) {
setToastApiError(formatUnknownError(error));
}
};
return (
<>
<Dialog
open={isStrategyMenuDialogOpen}
onClose={onClose}
maxWidth='md'
PaperProps={{
sx: {
borderRadius: '12px',
height: '100%',
width: '100%',
},
}}
>
<>
<StyledHeader>
<Typography variant='h2'>Add strategy</Typography>
<IconButton
size='small'
onClick={onClose}
edge='end'
aria-label='close'
>
<CloseIcon fontSize='small' />
</IconButton>
</StyledHeader>
{releasePlanPreview && selectedTemplate ? (
<ReleasePlanPreview
template={selectedTemplate}
projectId={projectId}
featureName={featureId}
environment={environmentId}
activeReleasePlan={activeReleasePlan}
crProtected={crProtected}
onBack={() => setReleasePlanPreview(false)}
onConfirm={() => {
addReleasePlan(selectedTemplate);
}}
/>
) : (
<FeatureStrategyMenuCards
projectId={projectId}
featureId={featureId}
environmentId={environmentId}
filter={filter}
setFilter={setFilter}
onAddReleasePlan={(template) => {
setSelectedTemplate(template);
addReleasePlan(template);
}}
onReviewReleasePlan={(template) => {
setSelectedTemplate(template);
setReleasePlanPreview(true);
}}
onClose={onClose}
/>
)}
</>
</Dialog>
{selectedTemplate && (
<ReleasePlanConfirmationDialog
template={selectedTemplate}
crProtected={crProtected}
open={addReleasePlanConfirmationOpen}
setOpen={setAddReleasePlanConfirmationOpen}
onConfirm={() => {
addReleasePlan(selectedTemplate, true);
}}
/>
)}
</>
);
};