mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-09 00:18:00 +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 ![image](https://github.com/user-attachments/assets/8511c6a3-c83e-4eee-aa18-9affe4a9ac1d) ### Remove release plan ![image](https://github.com/user-attachments/assets/ed13f9ac-140c-40c9-a1a2-3c066c89c09a) ### Start milestone ![image](https://github.com/user-attachments/assets/ac8e5408-e877-470c-a98b-295b41444bfa) ![image](https://github.com/user-attachments/assets/abf19a55-89df-4dd8-8738-9dfcd63949b7)
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,6 +151,7 @@ export const ReleasePlan = ({
|
||||
{description}
|
||||
</StyledHeaderDescription>
|
||||
</StyledHeaderTitleContainer>
|
||||
{!readonly && (
|
||||
<PermissionIconButton
|
||||
onClick={() => setRemoveOpen(true)}
|
||||
permission={DELETE_FEATURE_STRATEGY}
|
||||
@ -156,11 +163,13 @@ export const ReleasePlan = ({
|
||||
>
|
||||
<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>
|
||||
{!readonly && onStartMilestone && (
|
||||
<ReleasePlanMilestoneStatus
|
||||
status={status}
|
||||
onStartMilestone={() => onStartMilestone(milestone)}
|
||||
onStartMilestone={() =>
|
||||
onStartMilestone(milestone)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</StyledTitleContainer>
|
||||
<StyledSecondaryLabel>No strategies</StyledSecondaryLabel>
|
||||
</StyledAccordionSummary>
|
||||
@ -94,10 +100,12 @@ export const ReleasePlanMilestone = ({
|
||||
<StyledAccordionSummary expandIcon={<ExpandMore />}>
|
||||
<StyledTitleContainer>
|
||||
<StyledTitle>{milestone.name}</StyledTitle>
|
||||
{!readonly && onStartMilestone && (
|
||||
<ReleasePlanMilestoneStatus
|
||||
status={status}
|
||||
onStartMilestone={() => onStartMilestone(milestone)}
|
||||
/>
|
||||
)}
|
||||
</StyledTitleContainer>
|
||||
<StyledSecondaryLabel>
|
||||
{milestone.strategies.length === 1
|
||||
|
Loading…
Reference in New Issue
Block a user