mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-28 00:17:12 +01:00
chore: handle release plans in new strategy list (#9380)
Splits the release plan component into a Legacy component and a new one with the initial changes for the new strategy list view. Here's what it looks like:  Notice that the background color stops a little early (before the OR token). I'll handle that in a follow-up because the changes also impact how the rest of the env accordion body is rendered.
This commit is contained in:
parent
e29eb51f3c
commit
359b7cc4c0
@ -22,10 +22,10 @@ import type { IFeatureStrategy } from 'interfaces/strategy';
|
||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||
import { useUiFlag } from 'hooks/useUiFlag';
|
||||
import { useReleasePlans } from 'hooks/api/getters/useReleasePlans/useReleasePlans';
|
||||
import { ReleasePlan } from '../../../ReleasePlan/ReleasePlan';
|
||||
import { Badge } from 'component/common/Badge/Badge';
|
||||
import { SectionSeparator } from '../SectionSeparator/SectionSeparator';
|
||||
import { StrategyDraggableItem as NewStrategyDraggableItem } from './StrategyDraggableItem/StrategyDraggableItem';
|
||||
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator';
|
||||
import { ReleasePlan } from '../../../ReleasePlan/ReleasePlan';
|
||||
|
||||
interface IEnvironmentAccordionBodyProps {
|
||||
isDisabled: boolean;
|
||||
@ -66,6 +66,10 @@ const StyledStrategyList = styled('ol')({
|
||||
margin: 0,
|
||||
});
|
||||
|
||||
const StyledReleasePlanList = styled(StyledStrategyList)(({ theme }) => ({
|
||||
background: theme.palette.background.elevation2,
|
||||
}));
|
||||
|
||||
export const EnvironmentAccordionBody = ({
|
||||
featureEnvironment,
|
||||
isDisabled,
|
||||
@ -234,29 +238,20 @@ export const EnvironmentAccordionBody = ({
|
||||
condition={releasePlans.length > 0 || strategies.length > 0}
|
||||
show={
|
||||
<>
|
||||
{releasePlans.map((plan) => (
|
||||
<ReleasePlan
|
||||
key={plan.id}
|
||||
plan={plan}
|
||||
environmentIsDisabled={isDisabled}
|
||||
/>
|
||||
))}
|
||||
<ConditionallyRender
|
||||
condition={
|
||||
releasePlans.length > 0 &&
|
||||
strategies.length > 0
|
||||
}
|
||||
show={
|
||||
<>
|
||||
<SectionSeparator>
|
||||
<StyledBadge>OR</StyledBadge>
|
||||
</SectionSeparator>
|
||||
<AdditionalStrategiesDiv>
|
||||
Additional strategies
|
||||
</AdditionalStrategiesDiv>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<StyledReleasePlanList>
|
||||
{releasePlans.map((plan) => (
|
||||
<li key={plan.id}>
|
||||
<ReleasePlan
|
||||
plan={plan}
|
||||
environmentIsDisabled={isDisabled}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</StyledReleasePlanList>
|
||||
{releasePlans.length > 0 &&
|
||||
strategies.length > 0 ? (
|
||||
<StrategySeparator text='OR' />
|
||||
) : null}
|
||||
<ConditionallyRender
|
||||
condition={
|
||||
strategies.length < 50 ||
|
||||
|
@ -23,7 +23,7 @@ import type { IFeatureStrategy } from 'interfaces/strategy';
|
||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||
import { useUiFlag } from 'hooks/useUiFlag';
|
||||
import { useReleasePlans } from 'hooks/api/getters/useReleasePlans/useReleasePlans';
|
||||
import { ReleasePlan } from '../../../ReleasePlan/ReleasePlan';
|
||||
import { ReleasePlan } from '../../../ReleasePlan/LegacyReleasePlan';
|
||||
import { Badge } from 'component/common/Badge/Badge';
|
||||
import { SectionSeparator } from '../SectionSeparator/SectionSeparator';
|
||||
|
||||
|
@ -0,0 +1,324 @@
|
||||
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/ReleasePlanMilestone';
|
||||
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 releasePlanChangeRequestsEnabled = useUiFlag(
|
||||
'releasePlanChangeRequests',
|
||||
);
|
||||
|
||||
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 (
|
||||
releasePlanChangeRequestsEnabled &&
|
||||
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 (
|
||||
releasePlanChangeRequestsEnabled &&
|
||||
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>
|
||||
);
|
||||
};
|
@ -28,13 +28,10 @@ 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,
|
||||
background: 'inherit',
|
||||
}));
|
||||
|
||||
const StyledHeader = styled('div')(({ theme }) => ({
|
||||
@ -43,22 +40,28 @@ const StyledHeader = styled('div')(({ theme }) => ({
|
||||
color: theme.palette.text.primary,
|
||||
}));
|
||||
|
||||
const StyledHeaderTitleContainer = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
gap: theme.spacing(1),
|
||||
const StyledHeaderGroup = styled('hgroup')(({ theme }) => ({
|
||||
paddingTop: theme.spacing(1.5),
|
||||
}));
|
||||
|
||||
const StyledHeaderTitleLabel = styled('span')(({ theme }) => ({
|
||||
fontSize: theme.fontSizes.smallerBody,
|
||||
const StyledHeaderTitleLabel = styled('p')(({ theme }) => ({
|
||||
fontWeight: 'bold',
|
||||
fontSize: theme.typography.body1.fontSize,
|
||||
lineHeight: 0.5,
|
||||
color: theme.palette.text.secondary,
|
||||
marginBottom: theme.spacing(0.5),
|
||||
display: 'inline',
|
||||
}));
|
||||
|
||||
const StyledHeaderDescription = styled('span')(({ theme }) => ({
|
||||
fontSize: theme.fontSizes.smallBody,
|
||||
const StyledHeaderTitle = styled('h3')(({ theme }) => ({
|
||||
display: 'inline',
|
||||
margin: 0,
|
||||
fontWeight: 'normal',
|
||||
fontSize: theme.typography.body1.fontSize,
|
||||
}));
|
||||
|
||||
const StyledHeaderDescription = styled('p')(({ theme }) => ({
|
||||
marginTop: theme.spacing(1),
|
||||
fontSize: theme.typography.body2.fontSize,
|
||||
color: theme.palette.text.secondary,
|
||||
}));
|
||||
|
||||
@ -242,17 +245,17 @@ export const ReleasePlan = ({
|
||||
return (
|
||||
<StyledContainer readonly={readonly}>
|
||||
<StyledHeader>
|
||||
<StyledHeaderTitleContainer>
|
||||
<StyledHeaderGroup>
|
||||
<StyledHeaderTitleLabel>
|
||||
Release plan
|
||||
Release plan:{' '}
|
||||
</StyledHeaderTitleLabel>
|
||||
<span>{name}</span>
|
||||
<StyledHeaderTitle>{name}</StyledHeaderTitle>
|
||||
<StyledHeaderDescription>
|
||||
<Truncator lines={2} title={description}>
|
||||
{description}
|
||||
</Truncator>
|
||||
</StyledHeaderDescription>
|
||||
</StyledHeaderTitleContainer>
|
||||
</StyledHeaderGroup>
|
||||
{!readonly && (
|
||||
<PermissionIconButton
|
||||
onClick={confirmRemoveReleasePlan}
|
||||
|
Loading…
Reference in New Issue
Block a user