mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	Removes the `releasePlanChangeRequests` flag and swaps existing references to it to the `releasePlans` flag instead
		
			
				
	
	
		
			317 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			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>
 | 
						|
    );
 | 
						|
};
 |