mirror of
https://github.com/Unleash/unleash.git
synced 2025-05-03 01:18:43 +02:00
chore: new add release plan dialog (#9389)
https://linear.app/unleash/issue/2-3249/adding-a-release-plan-to-a-non-cr-environment-feels-too-immediate This adds a new confirmation / preview dialog when adding a release plan. What's cool about it is that it will describe what will happen before you confirm. It also acts as the "add to CR" dialog, so we now only have 1 dialog instead of 2 separate ones. This also refactors quite a bit of our code here, hopefully simplifying it. ### Simple (env disabled)  ### CR protected (env enabled) 
This commit is contained in:
parent
2064cae20f
commit
da91ae6afe
@ -7,7 +7,7 @@ import type {
|
||||
IChangeRequestDeleteReleasePlan,
|
||||
IChangeRequestStartMilestone,
|
||||
} from 'component/changeRequest/changeRequest.types';
|
||||
import { useReleasePlanTemplate } from 'hooks/api/getters/useReleasePlanTemplates/useReleasePlanTemplate';
|
||||
import { useReleasePlanPreview } from 'hooks/useReleasePlanPreview';
|
||||
import { useReleasePlans } from 'hooks/api/getters/useReleasePlans/useReleasePlans';
|
||||
import { TooltipLink } from 'component/common/TooltipLink/TooltipLink';
|
||||
import EventDiff from 'component/events/EventDiff/EventDiff';
|
||||
@ -181,32 +181,11 @@ const AddReleasePlan: FC<{
|
||||
featureName: string;
|
||||
actions?: ReactNode;
|
||||
}> = ({ change, environmentName, featureName, actions }) => {
|
||||
const { template } = useReleasePlanTemplate(change.payload.templateId);
|
||||
|
||||
if (!template) return;
|
||||
|
||||
const tentativeReleasePlan = {
|
||||
...template,
|
||||
environment: environmentName,
|
||||
const planPreview = useReleasePlanPreview(
|
||||
change.payload.templateId,
|
||||
featureName,
|
||||
milestones: template.milestones.map((milestone) => ({
|
||||
...milestone,
|
||||
releasePlanDefinitionId: template.id,
|
||||
strategies: (milestone.strategies || []).map((strategy) => ({
|
||||
...strategy,
|
||||
parameters: {
|
||||
...strategy.parameters,
|
||||
...(strategy.parameters.groupId && {
|
||||
groupId: String(strategy.parameters.groupId).replaceAll(
|
||||
'{{featureName}}',
|
||||
featureName,
|
||||
),
|
||||
}),
|
||||
},
|
||||
milestoneId: milestone.id,
|
||||
})),
|
||||
})),
|
||||
};
|
||||
environmentName,
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -215,11 +194,11 @@ const AddReleasePlan: FC<{
|
||||
<Typography color='success.dark'>
|
||||
+ Adding release plan:
|
||||
</Typography>
|
||||
<Typography>{template.name}</Typography>
|
||||
<Typography>{planPreview.name}</Typography>
|
||||
</ChangeItemInfo>
|
||||
<div>{actions}</div>
|
||||
</ChangeItemCreateEditDeleteWrapper>
|
||||
<ReleasePlan plan={tentativeReleasePlan} readonly />
|
||||
<ReleasePlan plan={planPreview} readonly />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1,14 +1,7 @@
|
||||
import { getFeatureStrategyIcon } from 'utils/strategyNames';
|
||||
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
|
||||
import { Link, styled } from '@mui/material';
|
||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||
import { Button, styled } from '@mui/material';
|
||||
import type { IReleasePlanTemplate } from 'interfaces/releasePlans';
|
||||
import { useReleasePlansApi } from 'hooks/api/actions/useReleasePlansApi/useReleasePlansApi';
|
||||
import useToast from 'hooks/useToast';
|
||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||
import { useReleasePlans } from 'hooks/api/getters/useReleasePlans/useReleasePlans';
|
||||
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
||||
import { useUiFlag } from 'hooks/useUiFlag';
|
||||
|
||||
const StyledIcon = styled('div')(({ theme }) => ({
|
||||
width: theme.spacing(4),
|
||||
@ -25,13 +18,14 @@ const StyledIcon = styled('div')(({ theme }) => ({
|
||||
|
||||
const StyledDescription = styled('div')(({ theme }) => ({
|
||||
fontSize: theme.fontSizes.smallBody,
|
||||
fontWeight: theme.fontWeight.medium,
|
||||
}));
|
||||
|
||||
const StyledName = styled(StringTruncator)(({ theme }) => ({
|
||||
fontWeight: theme.fontWeight.bold,
|
||||
}));
|
||||
|
||||
const StyledCard = styled(Link)(({ theme }) => ({
|
||||
const StyledCard = styled(Button)(({ theme }) => ({
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '3rem 1fr',
|
||||
width: '20rem',
|
||||
@ -43,82 +37,31 @@ const StyledCard = styled(Link)(({ theme }) => ({
|
||||
borderStyle: 'solid',
|
||||
borderColor: theme.palette.divider,
|
||||
borderRadius: theme.spacing(1),
|
||||
textAlign: 'left',
|
||||
'&:hover, &:focus': {
|
||||
borderColor: theme.palette.primary.main,
|
||||
},
|
||||
}));
|
||||
|
||||
interface IFeatureReleasePlanCardProps {
|
||||
projectId: string;
|
||||
featureId: string;
|
||||
environmentId: string;
|
||||
releasePlanTemplate: IReleasePlanTemplate;
|
||||
setTemplateForChangeRequestDialog: (template: IReleasePlanTemplate) => void;
|
||||
template: IReleasePlanTemplate;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export const FeatureReleasePlanCard = ({
|
||||
projectId,
|
||||
featureId,
|
||||
environmentId,
|
||||
releasePlanTemplate,
|
||||
setTemplateForChangeRequestDialog,
|
||||
template: { name, description },
|
||||
onClick,
|
||||
}: IFeatureReleasePlanCardProps) => {
|
||||
const Icon = getFeatureStrategyIcon('releasePlanTemplate');
|
||||
const { trackEvent } = usePlausibleTracker();
|
||||
const { refetch } = useReleasePlans(projectId, featureId, environmentId);
|
||||
const { addReleasePlanToFeature } = useReleasePlansApi();
|
||||
const { setToastApiError, setToastData } = useToast();
|
||||
const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
|
||||
const releasePlanChangeRequestsEnabled = useUiFlag(
|
||||
'releasePlanChangeRequests',
|
||||
);
|
||||
|
||||
const addReleasePlan = async () => {
|
||||
try {
|
||||
if (
|
||||
releasePlanChangeRequestsEnabled &&
|
||||
isChangeRequestConfigured(environmentId)
|
||||
) {
|
||||
setTemplateForChangeRequestDialog(releasePlanTemplate);
|
||||
} else {
|
||||
await addReleasePlanToFeature(
|
||||
featureId,
|
||||
releasePlanTemplate.id,
|
||||
projectId,
|
||||
environmentId,
|
||||
);
|
||||
setToastData({
|
||||
type: 'success',
|
||||
text: 'Release plan added',
|
||||
});
|
||||
refetch();
|
||||
}
|
||||
|
||||
trackEvent('release-management', {
|
||||
props: {
|
||||
eventType: 'add-plan',
|
||||
plan: releasePlanTemplate.name,
|
||||
},
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
setToastApiError(formatUnknownError(error));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledCard onClick={addReleasePlan}>
|
||||
<StyledCard onClick={onClick}>
|
||||
<StyledIcon>
|
||||
<Icon />
|
||||
</StyledIcon>
|
||||
<div>
|
||||
<StyledName
|
||||
text={releasePlanTemplate.name}
|
||||
maxWidth='200'
|
||||
maxLength={25}
|
||||
/>
|
||||
<StyledDescription>
|
||||
{releasePlanTemplate.description}
|
||||
</StyledDescription>
|
||||
<StyledName text={name} maxWidth='200' maxLength={25} />
|
||||
<StyledDescription>{description}</StyledDescription>
|
||||
</div>
|
||||
</StyledCard>
|
||||
);
|
||||
|
@ -10,11 +10,16 @@ import { FeatureStrategyMenuCards } from './FeatureStrategyMenuCards/FeatureStra
|
||||
import { formatCreateStrategyPath } from '../FeatureStrategyCreate/FeatureStrategyCreate';
|
||||
import MoreVert from '@mui/icons-material/MoreVert';
|
||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||
import { ReleasePlanAddChangeRequestDialog } from 'component/feature/FeatureView/FeatureOverview/ReleasePlan/ChangeRequest/ReleasePlanAddChangeRequestDialog';
|
||||
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 { ReleasePlanAddDialog } from 'component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlanAddDialog';
|
||||
import { useReleasePlansApi } from 'hooks/api/actions/useReleasePlansApi/useReleasePlansApi';
|
||||
import { useReleasePlans } from 'hooks/api/getters/useReleasePlans/useReleasePlans';
|
||||
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||
import { useUiFlag } from 'hooks/useUiFlag';
|
||||
|
||||
interface IFeatureStrategyMenuProps {
|
||||
label: string;
|
||||
@ -54,14 +59,25 @@ export const FeatureStrategyMenu = ({
|
||||
const [anchor, setAnchor] = useState<Element>();
|
||||
const navigate = useNavigate();
|
||||
const { trackEvent } = usePlausibleTracker();
|
||||
const [templateForChangeRequestDialog, setTemplateForChangeRequestDialog] =
|
||||
useState<IReleasePlanTemplate | undefined>();
|
||||
const [selectedTemplate, setSelectedTemplate] =
|
||||
useState<IReleasePlanTemplate>();
|
||||
const [addReleasePlanOpen, setAddReleasePlanOpen] = useState(false);
|
||||
const isPopoverOpen = Boolean(anchor);
|
||||
const popoverId = isPopoverOpen ? 'FeatureStrategyMenuPopover' : undefined;
|
||||
const { setToastData } = useToast();
|
||||
const { setToastApiError, setToastData } = useToast();
|
||||
const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
|
||||
const { addChange } = useChangeRequestApi();
|
||||
const { refetch: refetchChangeRequests } =
|
||||
usePendingChangeRequests(projectId);
|
||||
const { refetch } = useReleasePlans(projectId, featureId, environmentId);
|
||||
const { addReleasePlanToFeature } = useReleasePlansApi();
|
||||
const releasePlanChangeRequestsEnabled = useUiFlag(
|
||||
'releasePlanChangeRequests',
|
||||
);
|
||||
|
||||
const crProtected =
|
||||
releasePlanChangeRequestsEnabled &&
|
||||
isChangeRequestConfigured(environmentId);
|
||||
|
||||
const onClose = () => {
|
||||
setAnchor(undefined);
|
||||
@ -80,23 +96,52 @@ export const FeatureStrategyMenu = ({
|
||||
setAnchor(event.currentTarget);
|
||||
};
|
||||
|
||||
const addReleasePlanToChangeRequest = async () => {
|
||||
const addReleasePlan = async () => {
|
||||
if (!selectedTemplate) return;
|
||||
|
||||
try {
|
||||
if (crProtected) {
|
||||
await addChange(projectId, environmentId, {
|
||||
feature: featureId,
|
||||
action: 'addReleasePlan',
|
||||
payload: {
|
||||
templateId: templateForChangeRequestDialog?.id,
|
||||
templateId: selectedTemplate.id,
|
||||
},
|
||||
});
|
||||
|
||||
await refetchChangeRequests();
|
||||
|
||||
setToastData({
|
||||
type: 'success',
|
||||
text: 'Added to draft',
|
||||
});
|
||||
|
||||
setTemplateForChangeRequestDialog(undefined);
|
||||
refetchChangeRequests();
|
||||
} else {
|
||||
await addReleasePlanToFeature(
|
||||
featureId,
|
||||
selectedTemplate.id,
|
||||
projectId,
|
||||
environmentId,
|
||||
);
|
||||
|
||||
setToastData({
|
||||
type: 'success',
|
||||
text: 'Release plan added',
|
||||
});
|
||||
|
||||
refetch();
|
||||
}
|
||||
trackEvent('release-management', {
|
||||
props: {
|
||||
eventType: 'add-plan',
|
||||
plan: selectedTemplate.name,
|
||||
},
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
setToastApiError(formatUnknownError(error));
|
||||
} finally {
|
||||
setAddReleasePlanOpen(false);
|
||||
setSelectedTemplate(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
const createStrategyPath = formatCreateStrategyPath(
|
||||
@ -156,19 +201,24 @@ export const FeatureStrategyMenu = ({
|
||||
projectId={projectId}
|
||||
featureId={featureId}
|
||||
environmentId={environmentId}
|
||||
setTemplateForChangeRequestDialog={
|
||||
setTemplateForChangeRequestDialog
|
||||
}
|
||||
onAddReleasePlan={(template) => {
|
||||
setSelectedTemplate(template);
|
||||
setAddReleasePlanOpen(true);
|
||||
}}
|
||||
/>
|
||||
</Popover>
|
||||
<ReleasePlanAddChangeRequestDialog
|
||||
onConfirm={addReleasePlanToChangeRequest}
|
||||
onClosing={() => setTemplateForChangeRequestDialog(undefined)}
|
||||
isOpen={Boolean(templateForChangeRequestDialog)}
|
||||
featureId={featureId}
|
||||
environmentId={environmentId}
|
||||
releaseTemplate={templateForChangeRequestDialog}
|
||||
{selectedTemplate && (
|
||||
<ReleasePlanAddDialog
|
||||
open={addReleasePlanOpen}
|
||||
setOpen={setAddReleasePlanOpen}
|
||||
onConfirm={addReleasePlan}
|
||||
template={selectedTemplate}
|
||||
projectId={projectId}
|
||||
featureName={featureId}
|
||||
environment={environmentId}
|
||||
crProtected={crProtected}
|
||||
/>
|
||||
)}
|
||||
</StyledStrategyMenu>
|
||||
);
|
||||
};
|
||||
|
@ -10,7 +10,7 @@ interface IFeatureStrategyMenuCardsProps {
|
||||
projectId: string;
|
||||
featureId: string;
|
||||
environmentId: string;
|
||||
setTemplateForChangeRequestDialog: (template: IReleasePlanTemplate) => void;
|
||||
onAddReleasePlan: (template: IReleasePlanTemplate) => void;
|
||||
}
|
||||
|
||||
const StyledTypography = styled(Typography)(({ theme }) => ({
|
||||
@ -22,7 +22,7 @@ export const FeatureStrategyMenuCards = ({
|
||||
projectId,
|
||||
featureId,
|
||||
environmentId,
|
||||
setTemplateForChangeRequestDialog,
|
||||
onAddReleasePlan,
|
||||
}: IFeatureStrategyMenuCardsProps) => {
|
||||
const { strategies } = useStrategies();
|
||||
const { templates } = useReleasePlanTemplates();
|
||||
@ -67,13 +67,8 @@ export const FeatureStrategyMenuCards = ({
|
||||
{templates.map((template) => (
|
||||
<ListItem key={template.id}>
|
||||
<FeatureReleasePlanCard
|
||||
projectId={projectId}
|
||||
featureId={featureId}
|
||||
environmentId={environmentId}
|
||||
releasePlanTemplate={template}
|
||||
setTemplateForChangeRequestDialog={
|
||||
setTemplateForChangeRequestDialog
|
||||
}
|
||||
template={template}
|
||||
onClick={() => onAddReleasePlan(template)}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
|
@ -1,51 +0,0 @@
|
||||
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
||||
import { styled, Button } from '@mui/material';
|
||||
import type { IReleasePlanTemplate } from 'interfaces/releasePlans';
|
||||
|
||||
const StyledBoldSpan = styled('span')(({ theme }) => ({
|
||||
fontWeight: theme.typography.fontWeightBold,
|
||||
}));
|
||||
|
||||
interface IReleasePlanAddChangeRequestDialogProps {
|
||||
featureId: string;
|
||||
environmentId: string;
|
||||
releaseTemplate?: IReleasePlanTemplate;
|
||||
isOpen: boolean;
|
||||
onConfirm: () => Promise<void>;
|
||||
onClosing: () => void;
|
||||
}
|
||||
|
||||
export const ReleasePlanAddChangeRequestDialog = ({
|
||||
featureId,
|
||||
environmentId,
|
||||
releaseTemplate,
|
||||
isOpen,
|
||||
onConfirm,
|
||||
onClosing,
|
||||
}: IReleasePlanAddChangeRequestDialogProps) => {
|
||||
return (
|
||||
<Dialogue
|
||||
title='Request changes'
|
||||
open={isOpen}
|
||||
secondaryButtonText='Cancel'
|
||||
onClose={onClosing}
|
||||
customButton={
|
||||
<Button
|
||||
color='primary'
|
||||
variant='contained'
|
||||
onClick={onConfirm}
|
||||
autoFocus={true}
|
||||
>
|
||||
Add suggestion to draft
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<p>
|
||||
<StyledBoldSpan>Add</StyledBoldSpan> release template{' '}
|
||||
<StyledBoldSpan>{releaseTemplate?.name}</StyledBoldSpan> to{' '}
|
||||
<StyledBoldSpan>{featureId}</StyledBoldSpan> in{' '}
|
||||
<StyledBoldSpan>{environmentId}</StyledBoldSpan>
|
||||
</p>
|
||||
</Dialogue>
|
||||
);
|
||||
};
|
@ -0,0 +1,92 @@
|
||||
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
||||
import type { IReleasePlanTemplate } from 'interfaces/releasePlans';
|
||||
import { ReleasePlan } from './ReleasePlan';
|
||||
import { useReleasePlanPreview } from 'hooks/useReleasePlanPreview';
|
||||
import { styled, Typography, Alert } from '@mui/material';
|
||||
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
||||
|
||||
const StyledReleasePlanContainer = styled('div')(({ theme }) => ({
|
||||
margin: theme.spacing(2, 0),
|
||||
}));
|
||||
interface IReleasePlanAddDialogProps {
|
||||
open: boolean;
|
||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
onConfirm: () => void;
|
||||
template: IReleasePlanTemplate;
|
||||
projectId: string;
|
||||
featureName: string;
|
||||
environment: string;
|
||||
crProtected?: boolean;
|
||||
}
|
||||
|
||||
export const ReleasePlanAddDialog = ({
|
||||
open,
|
||||
setOpen,
|
||||
onConfirm,
|
||||
template,
|
||||
projectId,
|
||||
featureName,
|
||||
environment,
|
||||
crProtected,
|
||||
}: IReleasePlanAddDialogProps) => {
|
||||
const { feature } = useFeature(projectId, featureName);
|
||||
|
||||
const environmentData = feature?.environments.find(
|
||||
({ name }) => name === environment,
|
||||
);
|
||||
const environmentEnabled = environmentData?.enabled;
|
||||
|
||||
const planPreview = useReleasePlanPreview(
|
||||
template.id,
|
||||
featureName,
|
||||
environment,
|
||||
);
|
||||
|
||||
const firstMilestone = planPreview.milestones[0];
|
||||
|
||||
return (
|
||||
<Dialogue
|
||||
title='Add release plan?'
|
||||
open={open}
|
||||
primaryButtonText={
|
||||
crProtected ? 'Add suggestion to draft' : 'Add release plan'
|
||||
}
|
||||
secondaryButtonText='Cancel'
|
||||
onClick={onConfirm}
|
||||
onClose={() => setOpen(false)}
|
||||
>
|
||||
{environmentEnabled ? (
|
||||
<Alert severity='info'>
|
||||
This environment is currently <strong>enabled</strong>.
|
||||
{firstMilestone && (
|
||||
<p>
|
||||
The first milestone will be started as soon as the
|
||||
release plan is added:{' '}
|
||||
<strong>{planPreview.milestones[0].name}</strong>
|
||||
</p>
|
||||
)}
|
||||
</Alert>
|
||||
) : (
|
||||
<Alert severity='warning'>
|
||||
This environment is currently <strong>disabled</strong>.
|
||||
<p>
|
||||
The milestones will not start automatically after adding
|
||||
the release plan. They will remain paused until the
|
||||
environment is enabled.
|
||||
</p>
|
||||
</Alert>
|
||||
)}
|
||||
<StyledReleasePlanContainer>
|
||||
<ReleasePlan plan={planPreview} readonly />
|
||||
</StyledReleasePlanContainer>
|
||||
{crProtected && (
|
||||
<Typography sx={{ mt: 4 }}>
|
||||
<strong>Adding</strong> release plan template{' '}
|
||||
<strong>{template?.name}</strong> to{' '}
|
||||
<strong>{featureName}</strong> in{' '}
|
||||
<strong>{environment}</strong>.
|
||||
</Typography>
|
||||
)}
|
||||
</Dialogue>
|
||||
);
|
||||
};
|
33
frontend/src/hooks/useReleasePlanPreview.ts
Normal file
33
frontend/src/hooks/useReleasePlanPreview.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import type { IReleasePlan } from 'interfaces/releasePlans';
|
||||
import { useReleasePlanTemplate } from './api/getters/useReleasePlanTemplates/useReleasePlanTemplate';
|
||||
|
||||
export const useReleasePlanPreview = (
|
||||
templateId: string,
|
||||
featureName: string,
|
||||
environment: string,
|
||||
): IReleasePlan => {
|
||||
const { template } = useReleasePlanTemplate(templateId);
|
||||
|
||||
return {
|
||||
...template,
|
||||
featureName,
|
||||
environment,
|
||||
milestones: template.milestones.map((milestone) => ({
|
||||
...milestone,
|
||||
releasePlanDefinitionId: template.id,
|
||||
strategies: (milestone.strategies || []).map((strategy) => ({
|
||||
...strategy,
|
||||
parameters: {
|
||||
...strategy.parameters,
|
||||
...(strategy.parameters.groupId && {
|
||||
groupId: String(strategy.parameters.groupId).replaceAll(
|
||||
'{{featureName}}',
|
||||
featureName,
|
||||
),
|
||||
}),
|
||||
},
|
||||
milestoneId: milestone.id,
|
||||
})),
|
||||
})),
|
||||
};
|
||||
};
|
Loading…
Reference in New Issue
Block a user