mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	chore: release plan changes in change request view (#9225)
https://linear.app/unleash/issue/2-3169/add-release-plan-ui-representation-in-change-request-ui Adds visual representations for release plan change requests. ### Add release plan  ### Remove release plan  ### Start milestone  
This commit is contained in:
		
							parent
							
								
									9a8607b07e
								
							
						
					
					
						commit
						9fa7f5aa7b
					
				| @ -14,6 +14,7 @@ import { EnvironmentStrategyExecutionOrder } from './EnvironmentStrategyExecutio | ||||
| import { ArchiveFeatureChange } from './ArchiveFeatureChange'; | ||||
| import { DependencyChange } from './DependencyChange'; | ||||
| import { Link } from 'react-router-dom'; | ||||
| import { ReleasePlanChange } from './ReleasePlanChange'; | ||||
| 
 | ||||
| const StyledSingleChangeBox = styled(Box, { | ||||
|     shouldForwardProp: (prop: string) => !prop.startsWith('$'), | ||||
| @ -192,6 +193,18 @@ export const FeatureChange: FC<{ | ||||
|                         actions={actions} | ||||
|                     /> | ||||
|                 )} | ||||
|                 {(change.action === 'addReleasePlan' || | ||||
|                     change.action === 'deleteReleasePlan' || | ||||
|                     change.action === 'startMilestone') && ( | ||||
|                     <ReleasePlanChange | ||||
|                         actions={actions} | ||||
|                         change={change} | ||||
|                         featureName={feature.name} | ||||
|                         environmentName={changeRequest.environment} | ||||
|                         projectId={changeRequest.project} | ||||
|                         changeRequestState={changeRequest.state} | ||||
|                     /> | ||||
|                 )} | ||||
|             </ChangeInnerBox> | ||||
|         </StyledSingleChangeBox> | ||||
|     ); | ||||
|  | ||||
| @ -0,0 +1,277 @@ | ||||
| import type React from 'react'; | ||||
| import type { FC, ReactNode } from 'react'; | ||||
| import { Box, styled, Typography } from '@mui/material'; | ||||
| import type { | ||||
|     ChangeRequestState, | ||||
|     IChangeRequestAddReleasePlan, | ||||
|     IChangeRequestDeleteReleasePlan, | ||||
|     IChangeRequestStartMilestone, | ||||
| } from 'component/changeRequest/changeRequest.types'; | ||||
| import { useReleasePlanTemplate } from 'hooks/api/getters/useReleasePlanTemplates/useReleasePlanTemplate'; | ||||
| import { useReleasePlans } from 'hooks/api/getters/useReleasePlans/useReleasePlans'; | ||||
| import { TooltipLink } from 'component/common/TooltipLink/TooltipLink'; | ||||
| import EventDiff from 'component/events/EventDiff/EventDiff'; | ||||
| import { ReleasePlan } from 'component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlan'; | ||||
| import { ReleasePlanMilestone } from 'component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlanMilestone/ReleasePlanMilestone'; | ||||
| 
 | ||||
| export const ChangeItemWrapper = styled(Box)({ | ||||
|     display: 'flex', | ||||
|     justifyContent: 'space-between', | ||||
|     alignItems: 'center', | ||||
| }); | ||||
| 
 | ||||
| const ChangeItemCreateEditDeleteWrapper = styled(Box)(({ theme }) => ({ | ||||
|     display: 'grid', | ||||
|     gridTemplateColumns: 'auto auto', | ||||
|     justifyContent: 'space-between', | ||||
|     gap: theme.spacing(1), | ||||
|     alignItems: 'center', | ||||
|     marginBottom: theme.spacing(2), | ||||
|     width: '100%', | ||||
| })); | ||||
| 
 | ||||
| const ChangeItemInfo: FC<{ children?: React.ReactNode }> = styled(Box)( | ||||
|     ({ theme }) => ({ | ||||
|         display: 'flex', | ||||
|         gap: theme.spacing(1), | ||||
|     }), | ||||
| ); | ||||
| 
 | ||||
| const ViewDiff = styled('span')(({ theme }) => ({ | ||||
|     color: theme.palette.primary.main, | ||||
|     marginLeft: theme.spacing(1), | ||||
| })); | ||||
| 
 | ||||
| const StyledCodeSection = styled('div')(({ theme }) => ({ | ||||
|     overflowX: 'auto', | ||||
|     '& code': { | ||||
|         wordWrap: 'break-word', | ||||
|         whiteSpace: 'pre-wrap', | ||||
|         fontFamily: 'monospace', | ||||
|         lineHeight: 1.5, | ||||
|         fontSize: theme.fontSizes.smallBody, | ||||
|     }, | ||||
| })); | ||||
| 
 | ||||
| const DeleteReleasePlan: FC<{ | ||||
|     change: IChangeRequestDeleteReleasePlan; | ||||
|     environmentName: string; | ||||
|     featureName: string; | ||||
|     projectId: string; | ||||
|     changeRequestState: ChangeRequestState; | ||||
|     actions?: ReactNode; | ||||
| }> = ({ | ||||
|     change, | ||||
|     environmentName, | ||||
|     featureName, | ||||
|     projectId, | ||||
|     changeRequestState, | ||||
|     actions, | ||||
| }) => { | ||||
|     const { releasePlans } = useReleasePlans( | ||||
|         projectId, | ||||
|         featureName, | ||||
|         environmentName, | ||||
|     ); | ||||
|     const currentReleasePlan = releasePlans[0]; | ||||
| 
 | ||||
|     const releasePlan = | ||||
|         changeRequestState === 'Applied' && change.payload.snapshot | ||||
|             ? change.payload.snapshot | ||||
|             : currentReleasePlan; | ||||
| 
 | ||||
|     if (!releasePlan) return; | ||||
| 
 | ||||
|     return ( | ||||
|         <> | ||||
|             <ChangeItemCreateEditDeleteWrapper> | ||||
|                 <ChangeItemInfo> | ||||
|                     <Typography | ||||
|                         sx={(theme) => ({ | ||||
|                             color: theme.palette.error.main, | ||||
|                         })} | ||||
|                     > | ||||
|                         - Deleting release plan: | ||||
|                     </Typography> | ||||
|                     <Typography>{releasePlan.name}</Typography> | ||||
|                 </ChangeItemInfo> | ||||
|                 <div>{actions}</div> | ||||
|             </ChangeItemCreateEditDeleteWrapper> | ||||
|             <ReleasePlan plan={releasePlan} readonly /> | ||||
|         </> | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| const StartMilestone: FC<{ | ||||
|     change: IChangeRequestStartMilestone; | ||||
|     environmentName: string; | ||||
|     featureName: string; | ||||
|     projectId: string; | ||||
|     changeRequestState: ChangeRequestState; | ||||
|     actions?: ReactNode; | ||||
| }> = ({ | ||||
|     change, | ||||
|     environmentName, | ||||
|     featureName, | ||||
|     projectId, | ||||
|     changeRequestState, | ||||
|     actions, | ||||
| }) => { | ||||
|     const { releasePlans } = useReleasePlans( | ||||
|         projectId, | ||||
|         featureName, | ||||
|         environmentName, | ||||
|     ); | ||||
|     const currentReleasePlan = releasePlans[0]; | ||||
| 
 | ||||
|     const releasePlan = | ||||
|         changeRequestState === 'Applied' && change.payload.snapshot | ||||
|             ? change.payload.snapshot | ||||
|             : currentReleasePlan; | ||||
| 
 | ||||
|     if (!releasePlan) return; | ||||
| 
 | ||||
|     const previousMilestone = releasePlan.milestones.find( | ||||
|         (milestone) => milestone.id === releasePlan.activeMilestoneId, | ||||
|     ); | ||||
| 
 | ||||
|     const newMilestone = releasePlan.milestones.find( | ||||
|         (milestone) => milestone.id === change.payload.milestoneId, | ||||
|     ); | ||||
| 
 | ||||
|     if (!newMilestone) return; | ||||
| 
 | ||||
|     return ( | ||||
|         <> | ||||
|             <ChangeItemCreateEditDeleteWrapper> | ||||
|                 <ChangeItemInfo> | ||||
|                     <Typography color='success.dark'> | ||||
|                         + Start milestone: | ||||
|                     </Typography> | ||||
|                     <Typography>{newMilestone.name}</Typography> | ||||
|                     <TooltipLink | ||||
|                         tooltip={ | ||||
|                             <StyledCodeSection> | ||||
|                                 <EventDiff | ||||
|                                     entry={{ | ||||
|                                         preData: previousMilestone, | ||||
|                                         data: newMilestone, | ||||
|                                     }} | ||||
|                                 /> | ||||
|                             </StyledCodeSection> | ||||
|                         } | ||||
|                         tooltipProps={{ | ||||
|                             maxWidth: 500, | ||||
|                             maxHeight: 600, | ||||
|                         }} | ||||
|                     > | ||||
|                         <ViewDiff>View Diff</ViewDiff> | ||||
|                     </TooltipLink> | ||||
|                 </ChangeItemInfo> | ||||
|                 <div>{actions}</div> | ||||
|             </ChangeItemCreateEditDeleteWrapper> | ||||
|             <ReleasePlanMilestone readonly milestone={newMilestone} /> | ||||
|         </> | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| const AddReleasePlan: FC<{ | ||||
|     change: IChangeRequestAddReleasePlan; | ||||
|     environmentName: string; | ||||
|     featureName: string; | ||||
|     actions?: ReactNode; | ||||
| }> = ({ change, environmentName, featureName, actions }) => { | ||||
|     const { template } = useReleasePlanTemplate(change.payload.templateId); | ||||
| 
 | ||||
|     if (!template) return; | ||||
| 
 | ||||
|     const tentativeReleasePlan = { | ||||
|         ...template, | ||||
|         environment: environmentName, | ||||
|         featureName, | ||||
|         milestones: template.milestones.map((milestone) => ({ | ||||
|             ...milestone, | ||||
|             releasePlanDefinitionId: template.id, | ||||
|             strategies: (milestone.strategies || []).map((strategy) => ({ | ||||
|                 ...strategy, | ||||
|                 parameters: { | ||||
|                     ...strategy.parameters, | ||||
|                     ...(strategy.parameters.groupId && { | ||||
|                         groupId: String(strategy.parameters.groupId).replaceAll( | ||||
|                             '{{featureName}}', | ||||
|                             featureName, | ||||
|                         ), | ||||
|                     }), | ||||
|                 }, | ||||
|                 milestoneId: milestone.id, | ||||
|             })), | ||||
|         })), | ||||
|     }; | ||||
| 
 | ||||
|     return ( | ||||
|         <> | ||||
|             <ChangeItemCreateEditDeleteWrapper> | ||||
|                 <ChangeItemInfo> | ||||
|                     <Typography color='success.dark'> | ||||
|                         + Adding release plan: | ||||
|                     </Typography> | ||||
|                     <Typography>{template.name}</Typography> | ||||
|                 </ChangeItemInfo> | ||||
|                 <div>{actions}</div> | ||||
|             </ChangeItemCreateEditDeleteWrapper> | ||||
|             <ReleasePlan plan={tentativeReleasePlan} readonly /> | ||||
|         </> | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| export const ReleasePlanChange: FC<{ | ||||
|     actions?: ReactNode; | ||||
|     change: | ||||
|         | IChangeRequestAddReleasePlan | ||||
|         | IChangeRequestDeleteReleasePlan | ||||
|         | IChangeRequestStartMilestone; | ||||
|     environmentName: string; | ||||
|     featureName: string; | ||||
|     projectId: string; | ||||
|     changeRequestState: ChangeRequestState; | ||||
| }> = ({ | ||||
|     actions, | ||||
|     change, | ||||
|     featureName, | ||||
|     environmentName, | ||||
|     projectId, | ||||
|     changeRequestState, | ||||
| }) => { | ||||
|     return ( | ||||
|         <> | ||||
|             {change.action === 'addReleasePlan' && ( | ||||
|                 <AddReleasePlan | ||||
|                     change={change} | ||||
|                     environmentName={environmentName} | ||||
|                     featureName={featureName} | ||||
|                     actions={actions} | ||||
|                 /> | ||||
|             )} | ||||
|             {change.action === 'deleteReleasePlan' && ( | ||||
|                 <DeleteReleasePlan | ||||
|                     change={change} | ||||
|                     environmentName={environmentName} | ||||
|                     featureName={featureName} | ||||
|                     projectId={projectId} | ||||
|                     changeRequestState={changeRequestState} | ||||
|                     actions={actions} | ||||
|                 /> | ||||
|             )} | ||||
|             {change.action === 'startMilestone' && ( | ||||
|                 <StartMilestone | ||||
|                     change={change} | ||||
|                     environmentName={environmentName} | ||||
|                     featureName={featureName} | ||||
|                     projectId={projectId} | ||||
|                     changeRequestState={changeRequestState} | ||||
|                     actions={actions} | ||||
|                 /> | ||||
|             )} | ||||
|         </> | ||||
|     ); | ||||
| }; | ||||
| @ -3,6 +3,7 @@ import type { ISegment } from 'interfaces/segment'; | ||||
| import type { IFeatureStrategy } from '../../interfaces/strategy'; | ||||
| import type { IUser } from '../../interfaces/user'; | ||||
| import type { SetStrategySortOrderSchema } from '../../openapi'; | ||||
| import type { IReleasePlan } from 'interfaces/releasePlans'; | ||||
| 
 | ||||
| type BaseChangeRequest = { | ||||
|     id: number; | ||||
| @ -126,7 +127,10 @@ type ChangeRequestPayload = | ||||
|     | IChangeRequestDeleteSegment | ||||
|     | SetStrategySortOrderSchema | ||||
|     | IChangeRequestArchiveFeature | ||||
|     | ChangeRequestAddDependency; | ||||
|     | ChangeRequestAddDependency | ||||
|     | ChangeRequestAddReleasePlan | ||||
|     | ChangeRequestDeleteReleasePlan | ||||
|     | ChangeRequestStartMilestone; | ||||
| 
 | ||||
| export interface IChangeRequestAddStrategy extends IChangeRequestChangeBase { | ||||
|     action: 'addStrategy'; | ||||
| @ -167,6 +171,22 @@ export interface IChangeRequestDeleteDependency | ||||
|     action: 'deleteDependency'; | ||||
| } | ||||
| 
 | ||||
| export interface IChangeRequestAddReleasePlan extends IChangeRequestChangeBase { | ||||
|     action: 'addReleasePlan'; | ||||
|     payload: ChangeRequestAddReleasePlan; | ||||
| } | ||||
| 
 | ||||
| export interface IChangeRequestDeleteReleasePlan | ||||
|     extends IChangeRequestChangeBase { | ||||
|     action: 'deleteReleasePlan'; | ||||
|     payload: ChangeRequestDeleteReleasePlan; | ||||
| } | ||||
| 
 | ||||
| export interface IChangeRequestStartMilestone extends IChangeRequestChangeBase { | ||||
|     action: 'startMilestone'; | ||||
|     payload: ChangeRequestStartMilestone; | ||||
| } | ||||
| 
 | ||||
| export interface IChangeRequestReorderStrategy | ||||
|     extends IChangeRequestChangeBase { | ||||
|     action: 'reorderStrategy'; | ||||
| @ -211,7 +231,10 @@ export type IFeatureChange = | ||||
|     | IChangeRequestReorderStrategy | ||||
|     | IChangeRequestArchiveFeature | ||||
|     | IChangeRequestAddDependency | ||||
|     | IChangeRequestDeleteDependency; | ||||
|     | IChangeRequestDeleteDependency | ||||
|     | IChangeRequestAddReleasePlan | ||||
|     | IChangeRequestDeleteReleasePlan | ||||
|     | IChangeRequestStartMilestone; | ||||
| 
 | ||||
| export type ISegmentChange = | ||||
|     | IChangeRequestUpdateSegment | ||||
| @ -230,6 +253,20 @@ type ChangeRequestAddDependency = { | ||||
|     variants?: string[]; | ||||
| }; | ||||
| 
 | ||||
| type ChangeRequestAddReleasePlan = { | ||||
|     templateId: string; | ||||
| }; | ||||
| 
 | ||||
| type ChangeRequestDeleteReleasePlan = { | ||||
|     planId: string; | ||||
|     snapshot?: IReleasePlan; | ||||
| }; | ||||
| 
 | ||||
| type ChangeRequestStartMilestone = { | ||||
|     milestoneId: string; | ||||
|     snapshot?: IReleasePlan; | ||||
| }; | ||||
| 
 | ||||
| export type ChangeRequestAddStrategy = Pick< | ||||
|     IFeatureStrategy, | ||||
|     | 'parameters' | ||||
| @ -264,4 +301,7 @@ export type ChangeRequestAction = | ||||
|     | 'deleteSegment' | ||||
|     | 'archiveFeature' | ||||
|     | 'addDependency' | ||||
|     | 'deleteDependency'; | ||||
|     | 'deleteDependency' | ||||
|     | 'addReleasePlan' | ||||
|     | 'deleteReleasePlan' | ||||
|     | 'startMilestone'; | ||||
|  | ||||
| @ -16,13 +16,17 @@ import { ReleasePlanRemoveDialog } from './ReleasePlanRemoveDialog'; | ||||
| import { ReleasePlanMilestone } from './ReleasePlanMilestone/ReleasePlanMilestone'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| 
 | ||||
| const StyledContainer = styled('div')(({ theme }) => ({ | ||||
| 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: theme.palette.background.paper, | ||||
|     background: readonly | ||||
|         ? theme.palette.background.elevation1 | ||||
|         : theme.palette.background.paper, | ||||
| })); | ||||
| 
 | ||||
| const StyledHeader = styled('div')(({ theme }) => ({ | ||||
| @ -66,12 +70,14 @@ const StyledConnection = styled('div')(({ theme }) => ({ | ||||
| 
 | ||||
| interface IReleasePlanProps { | ||||
|     plan: IReleasePlan; | ||||
|     environmentIsDisabled: boolean; | ||||
|     environmentIsDisabled?: boolean; | ||||
|     readonly?: boolean; | ||||
| } | ||||
| 
 | ||||
| export const ReleasePlan = ({ | ||||
|     plan, | ||||
|     environmentIsDisabled, | ||||
|     readonly, | ||||
| }: IReleasePlanProps) => { | ||||
|     const { | ||||
|         id, | ||||
| @ -134,7 +140,7 @@ export const ReleasePlan = ({ | ||||
|     ); | ||||
| 
 | ||||
|     return ( | ||||
|         <StyledContainer> | ||||
|         <StyledContainer readonly={readonly}> | ||||
|             <StyledHeader> | ||||
|                 <StyledHeaderTitleContainer> | ||||
|                     <StyledHeaderTitleLabel> | ||||
| @ -145,22 +151,25 @@ export const ReleasePlan = ({ | ||||
|                         {description} | ||||
|                     </StyledHeaderDescription> | ||||
|                 </StyledHeaderTitleContainer> | ||||
|                 <PermissionIconButton | ||||
|                     onClick={() => setRemoveOpen(true)} | ||||
|                     permission={DELETE_FEATURE_STRATEGY} | ||||
|                     environmentId={environment} | ||||
|                     projectId={projectId} | ||||
|                     tooltipProps={{ | ||||
|                         title: 'Remove release plan', | ||||
|                     }} | ||||
|                 > | ||||
|                     <Delete /> | ||||
|                 </PermissionIconButton> | ||||
|                 {!readonly && ( | ||||
|                     <PermissionIconButton | ||||
|                         onClick={() => setRemoveOpen(true)} | ||||
|                         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 | ||||
|  | ||||
| @ -58,14 +58,16 @@ const StyledAccordionDetails = styled(AccordionDetails)(({ theme }) => ({ | ||||
| 
 | ||||
| interface IReleasePlanMilestoneProps { | ||||
|     milestone: IReleasePlanMilestone; | ||||
|     status: MilestoneStatus; | ||||
|     onStartMilestone: (milestone: IReleasePlanMilestone) => void; | ||||
|     status?: MilestoneStatus; | ||||
|     onStartMilestone?: (milestone: IReleasePlanMilestone) => void; | ||||
|     readonly?: boolean; | ||||
| } | ||||
| 
 | ||||
| export const ReleasePlanMilestone = ({ | ||||
|     milestone, | ||||
|     status, | ||||
|     status = 'not-started', | ||||
|     onStartMilestone, | ||||
|     readonly, | ||||
| }: IReleasePlanMilestoneProps) => { | ||||
|     const [expanded, setExpanded] = useState(false); | ||||
| 
 | ||||
| @ -75,10 +77,14 @@ export const ReleasePlanMilestone = ({ | ||||
|                 <StyledAccordionSummary> | ||||
|                     <StyledTitleContainer> | ||||
|                         <StyledTitle>{milestone.name}</StyledTitle> | ||||
|                         <ReleasePlanMilestoneStatus | ||||
|                             status={status} | ||||
|                             onStartMilestone={() => onStartMilestone(milestone)} | ||||
|                         /> | ||||
|                         {!readonly && onStartMilestone && ( | ||||
|                             <ReleasePlanMilestoneStatus | ||||
|                                 status={status} | ||||
|                                 onStartMilestone={() => | ||||
|                                     onStartMilestone(milestone) | ||||
|                                 } | ||||
|                             /> | ||||
|                         )} | ||||
|                     </StyledTitleContainer> | ||||
|                     <StyledSecondaryLabel>No strategies</StyledSecondaryLabel> | ||||
|                 </StyledAccordionSummary> | ||||
| @ -94,10 +100,12 @@ export const ReleasePlanMilestone = ({ | ||||
|             <StyledAccordionSummary expandIcon={<ExpandMore />}> | ||||
|                 <StyledTitleContainer> | ||||
|                     <StyledTitle>{milestone.name}</StyledTitle> | ||||
|                     <ReleasePlanMilestoneStatus | ||||
|                         status={status} | ||||
|                         onStartMilestone={() => onStartMilestone(milestone)} | ||||
|                     /> | ||||
|                     {!readonly && onStartMilestone && ( | ||||
|                         <ReleasePlanMilestoneStatus | ||||
|                             status={status} | ||||
|                             onStartMilestone={() => onStartMilestone(milestone)} | ||||
|                         /> | ||||
|                     )} | ||||
|                 </StyledTitleContainer> | ||||
|                 <StyledSecondaryLabel> | ||||
|                     {milestone.strategies.length === 1 | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user