diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/EnvironmentAccordionBody.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/EnvironmentAccordionBody.tsx index d2f74dcfd2..319ab25780 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/EnvironmentAccordionBody.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/EnvironmentAccordionBody.tsx @@ -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) => ( - - ))} - 0 && - strategies.length > 0 - } - show={ - <> - - OR - - - Additional strategies - - - } - /> + + {releasePlans.map((plan) => ( +
  • + +
  • + ))} +
    + {releasePlans.length > 0 && + strategies.length > 0 ? ( + + ) : null} 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(); + 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 ( + + + + + Release plan + + {name} + + + {description} + + + + {!readonly && ( + + + + )} + + + {milestones.map((milestone, index) => ( +
    + + } + /> +
    + ))} +
    + + setChangeRequestDialogRemoveOpen(false)} + releasePlan={plan} + environmentActive={!environmentIsDisabled} + /> + { + setMilestoneForChangeRequestDialog(undefined); + setChangeRequestDialogStartMilestoneOpen(false); + }} + releasePlan={plan} + milestone={milestoneForChangeRequestDialog} + /> +
    + ); +}; diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlan.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlan.tsx index c3f0cdf74e..a9ba431dae 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlan.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlan.tsx @@ -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 ( - + - Release plan + Release plan:{' '} - {name} + {name} {description} - + {!readonly && (