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 && (