1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-05-17 01:17:29 +02:00
unleash.unleash/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/LegacyReleasePlan.tsx
Simon Hornby dbc953b222
chore: consolidate release plan flags (#9606)
Removes the `releasePlanChangeRequests` flag and swaps existing references to it to the `releasePlans` flag instead
2025-03-25 13:36:23 +02:00

317 lines
11 KiB
TypeScript

import Delete from '@mui/icons-material/Delete';
import { styled } from '@mui/material';
import { DELETE_FEATURE_STRATEGY } from '@server/types/permissions';
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
import { useReleasePlansApi } from 'hooks/api/actions/useReleasePlansApi/useReleasePlansApi';
import { useReleasePlans } from 'hooks/api/getters/useReleasePlans/useReleasePlans';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import useToast from 'hooks/useToast';
import type {
IReleasePlan,
IReleasePlanMilestone,
} from 'interfaces/releasePlans';
import { useState } from 'react';
import { formatUnknownError } from 'utils/formatUnknownError';
import { ReleasePlanRemoveDialog } from './ReleasePlanRemoveDialog';
import { ReleasePlanMilestone } from './ReleasePlanMilestone/LegacyReleasePlanMilestone';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
import { useUiFlag } from 'hooks/useUiFlag';
import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests';
import { RemoveReleasePlanChangeRequestDialog } from './ChangeRequest/RemoveReleasePlanChangeRequestDialog';
import { StartMilestoneChangeRequestDialog } from './ChangeRequest/StartMilestoneChangeRequestDialog';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
import { Truncator } from 'component/common/Truncator/Truncator';
const StyledContainer = styled('div', {
shouldForwardProp: (prop) => prop !== 'readonly',
})<{ readonly?: boolean }>(({ theme, readonly }) => ({
padding: theme.spacing(2),
borderRadius: theme.shape.borderRadiusMedium,
'& + &': {
marginTop: theme.spacing(2),
},
background: readonly
? theme.palette.background.elevation1
: theme.palette.background.paper,
}));
const StyledHeader = styled('div')(({ theme }) => ({
display: 'flex',
justifyContent: 'space-between',
color: theme.palette.text.primary,
}));
const StyledHeaderTitleContainer = styled('div')(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
gap: theme.spacing(1),
}));
const StyledHeaderTitleLabel = styled('span')(({ theme }) => ({
fontSize: theme.fontSizes.smallerBody,
lineHeight: 0.5,
color: theme.palette.text.secondary,
marginBottom: theme.spacing(0.5),
}));
const StyledHeaderDescription = styled('span')(({ theme }) => ({
fontSize: theme.fontSizes.smallBody,
color: theme.palette.text.secondary,
}));
const StyledBody = styled('div')(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
marginTop: theme.spacing(3),
}));
const StyledConnection = styled('div')(({ theme }) => ({
width: 4,
height: theme.spacing(2),
backgroundColor: theme.palette.divider,
marginLeft: theme.spacing(3.25),
}));
interface IReleasePlanProps {
plan: IReleasePlan;
environmentIsDisabled?: boolean;
readonly?: boolean;
}
export const ReleasePlan = ({
plan,
environmentIsDisabled,
readonly,
}: IReleasePlanProps) => {
const {
id,
name,
description,
activeMilestoneId,
featureName,
environment,
milestones,
} = plan;
const projectId = useRequiredPathParam('projectId');
const { refetch } = useReleasePlans(projectId, featureName, environment);
const { removeReleasePlanFromFeature, startReleasePlanMilestone } =
useReleasePlansApi();
const { setToastData, setToastApiError } = useToast();
const { trackEvent } = usePlausibleTracker();
const [removeOpen, setRemoveOpen] = useState(false);
const [changeRequestDialogRemoveOpen, setChangeRequestDialogRemoveOpen] =
useState(false);
const [
changeRequestDialogStartMilestoneOpen,
setChangeRequestDialogStartMilestoneOpen,
] = useState(false);
const [
milestoneForChangeRequestDialog,
setMilestoneForChangeRequestDialog,
] = useState<IReleasePlanMilestone>();
const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
const { addChange } = useChangeRequestApi();
const { refetch: refetchChangeRequests } =
usePendingChangeRequests(projectId);
const releasePlansEnabled = useUiFlag('releasePlans');
const onAddRemovePlanChangesConfirm = async () => {
await addChange(projectId, environment, {
feature: featureName,
action: 'deleteReleasePlan',
payload: {
planId: plan.id,
},
});
await refetchChangeRequests();
setToastData({
type: 'success',
text: 'Added to draft',
});
setChangeRequestDialogRemoveOpen(false);
};
const onAddStartMilestoneChangesConfirm = async () => {
await addChange(projectId, environment, {
feature: featureName,
action: 'startMilestone',
payload: {
planId: plan.id,
milestoneId: milestoneForChangeRequestDialog?.id,
},
});
await refetchChangeRequests();
setToastData({
type: 'success',
text: 'Added to draft',
});
setChangeRequestDialogStartMilestoneOpen(false);
};
const confirmRemoveReleasePlan = () => {
if (releasePlansEnabled && isChangeRequestConfigured(environment)) {
setChangeRequestDialogRemoveOpen(true);
} else {
setRemoveOpen(true);
}
trackEvent('release-management', {
props: {
eventType: 'remove-plan',
plan: name,
},
});
};
const onRemoveConfirm = async () => {
try {
await removeReleasePlanFromFeature(
projectId,
featureName,
environment,
id,
);
setToastData({
text: `Release plan "${name}" has been removed from ${featureName} in ${environment}`,
type: 'success',
});
refetch();
setRemoveOpen(false);
} catch (error: unknown) {
setToastApiError(formatUnknownError(error));
}
};
const onStartMilestone = async (milestone: IReleasePlanMilestone) => {
if (releasePlansEnabled && isChangeRequestConfigured(environment)) {
setMilestoneForChangeRequestDialog(milestone);
setChangeRequestDialogStartMilestoneOpen(true);
} else {
try {
await startReleasePlanMilestone(
projectId,
featureName,
environment,
id,
milestone.id,
);
setToastData({
text: `Milestone "${milestone.name}" has started`,
type: 'success',
});
refetch();
} catch (error: unknown) {
setToastApiError(formatUnknownError(error));
}
}
trackEvent('release-management', {
props: {
eventType: 'start-milestone',
plan: name,
milestone: milestone.name,
},
});
};
const activeIndex = milestones.findIndex(
(milestone) => milestone.id === activeMilestoneId,
);
return (
<StyledContainer readonly={readonly}>
<StyledHeader>
<StyledHeaderTitleContainer>
<StyledHeaderTitleLabel>
Release plan
</StyledHeaderTitleLabel>
<span>{name}</span>
<StyledHeaderDescription>
<Truncator lines={2} title={description}>
{description}
</Truncator>
</StyledHeaderDescription>
</StyledHeaderTitleContainer>
{!readonly && (
<PermissionIconButton
onClick={confirmRemoveReleasePlan}
permission={DELETE_FEATURE_STRATEGY}
environmentId={environment}
projectId={projectId}
tooltipProps={{
title: 'Remove release plan',
}}
>
<Delete />
</PermissionIconButton>
)}
</StyledHeader>
<StyledBody>
{milestones.map((milestone, index) => (
<div key={milestone.id}>
<ReleasePlanMilestone
readonly={readonly}
milestone={milestone}
status={
milestone.id === activeMilestoneId
? environmentIsDisabled
? 'paused'
: 'active'
: index < activeIndex
? 'completed'
: 'not-started'
}
onStartMilestone={onStartMilestone}
/>
<ConditionallyRender
condition={index < milestones.length - 1}
show={<StyledConnection />}
/>
</div>
))}
</StyledBody>
<ReleasePlanRemoveDialog
plan={plan}
open={removeOpen}
setOpen={setRemoveOpen}
onConfirm={onRemoveConfirm}
environmentActive={!environmentIsDisabled}
/>
<RemoveReleasePlanChangeRequestDialog
environmentId={environment}
featureId={featureName}
isOpen={changeRequestDialogRemoveOpen}
onConfirm={onAddRemovePlanChangesConfirm}
onClosing={() => setChangeRequestDialogRemoveOpen(false)}
releasePlan={plan}
environmentActive={!environmentIsDisabled}
/>
<StartMilestoneChangeRequestDialog
environmentId={environment}
featureId={featureName}
isOpen={changeRequestDialogStartMilestoneOpen}
onConfirm={onAddStartMilestoneChangesConfirm}
onClosing={() => {
setMilestoneForChangeRequestDialog(undefined);
setChangeRequestDialogStartMilestoneOpen(false);
}}
releasePlan={plan}
milestone={milestoneForChangeRequestDialog}
/>
</StyledContainer>
);
};