1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-09 00:18:00 +01:00
Nuno Góis 2025-02-05 15:27:36 +00:00 committed by GitHub
parent 9a8607b07e
commit 9fa7f5aa7b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 376 additions and 29 deletions

View File

@ -14,6 +14,7 @@ import { EnvironmentStrategyExecutionOrder } from './EnvironmentStrategyExecutio
import { ArchiveFeatureChange } from './ArchiveFeatureChange'; import { ArchiveFeatureChange } from './ArchiveFeatureChange';
import { DependencyChange } from './DependencyChange'; import { DependencyChange } from './DependencyChange';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { ReleasePlanChange } from './ReleasePlanChange';
const StyledSingleChangeBox = styled(Box, { const StyledSingleChangeBox = styled(Box, {
shouldForwardProp: (prop: string) => !prop.startsWith('$'), shouldForwardProp: (prop: string) => !prop.startsWith('$'),
@ -192,6 +193,18 @@ export const FeatureChange: FC<{
actions={actions} 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> </ChangeInnerBox>
</StyledSingleChangeBox> </StyledSingleChangeBox>
); );

View File

@ -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}
/>
)}
</>
);
};

View File

@ -3,6 +3,7 @@ import type { ISegment } from 'interfaces/segment';
import type { IFeatureStrategy } from '../../interfaces/strategy'; import type { IFeatureStrategy } from '../../interfaces/strategy';
import type { IUser } from '../../interfaces/user'; import type { IUser } from '../../interfaces/user';
import type { SetStrategySortOrderSchema } from '../../openapi'; import type { SetStrategySortOrderSchema } from '../../openapi';
import type { IReleasePlan } from 'interfaces/releasePlans';
type BaseChangeRequest = { type BaseChangeRequest = {
id: number; id: number;
@ -126,7 +127,10 @@ type ChangeRequestPayload =
| IChangeRequestDeleteSegment | IChangeRequestDeleteSegment
| SetStrategySortOrderSchema | SetStrategySortOrderSchema
| IChangeRequestArchiveFeature | IChangeRequestArchiveFeature
| ChangeRequestAddDependency; | ChangeRequestAddDependency
| ChangeRequestAddReleasePlan
| ChangeRequestDeleteReleasePlan
| ChangeRequestStartMilestone;
export interface IChangeRequestAddStrategy extends IChangeRequestChangeBase { export interface IChangeRequestAddStrategy extends IChangeRequestChangeBase {
action: 'addStrategy'; action: 'addStrategy';
@ -167,6 +171,22 @@ export interface IChangeRequestDeleteDependency
action: 'deleteDependency'; 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 export interface IChangeRequestReorderStrategy
extends IChangeRequestChangeBase { extends IChangeRequestChangeBase {
action: 'reorderStrategy'; action: 'reorderStrategy';
@ -211,7 +231,10 @@ export type IFeatureChange =
| IChangeRequestReorderStrategy | IChangeRequestReorderStrategy
| IChangeRequestArchiveFeature | IChangeRequestArchiveFeature
| IChangeRequestAddDependency | IChangeRequestAddDependency
| IChangeRequestDeleteDependency; | IChangeRequestDeleteDependency
| IChangeRequestAddReleasePlan
| IChangeRequestDeleteReleasePlan
| IChangeRequestStartMilestone;
export type ISegmentChange = export type ISegmentChange =
| IChangeRequestUpdateSegment | IChangeRequestUpdateSegment
@ -230,6 +253,20 @@ type ChangeRequestAddDependency = {
variants?: string[]; variants?: string[];
}; };
type ChangeRequestAddReleasePlan = {
templateId: string;
};
type ChangeRequestDeleteReleasePlan = {
planId: string;
snapshot?: IReleasePlan;
};
type ChangeRequestStartMilestone = {
milestoneId: string;
snapshot?: IReleasePlan;
};
export type ChangeRequestAddStrategy = Pick< export type ChangeRequestAddStrategy = Pick<
IFeatureStrategy, IFeatureStrategy,
| 'parameters' | 'parameters'
@ -264,4 +301,7 @@ export type ChangeRequestAction =
| 'deleteSegment' | 'deleteSegment'
| 'archiveFeature' | 'archiveFeature'
| 'addDependency' | 'addDependency'
| 'deleteDependency'; | 'deleteDependency'
| 'addReleasePlan'
| 'deleteReleasePlan'
| 'startMilestone';

View File

@ -16,13 +16,17 @@ import { ReleasePlanRemoveDialog } from './ReleasePlanRemoveDialog';
import { ReleasePlanMilestone } from './ReleasePlanMilestone/ReleasePlanMilestone'; import { ReleasePlanMilestone } from './ReleasePlanMilestone/ReleasePlanMilestone';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; 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), padding: theme.spacing(2),
borderRadius: theme.shape.borderRadiusMedium, borderRadius: theme.shape.borderRadiusMedium,
'& + &': { '& + &': {
marginTop: theme.spacing(2), marginTop: theme.spacing(2),
}, },
background: theme.palette.background.paper, background: readonly
? theme.palette.background.elevation1
: theme.palette.background.paper,
})); }));
const StyledHeader = styled('div')(({ theme }) => ({ const StyledHeader = styled('div')(({ theme }) => ({
@ -66,12 +70,14 @@ const StyledConnection = styled('div')(({ theme }) => ({
interface IReleasePlanProps { interface IReleasePlanProps {
plan: IReleasePlan; plan: IReleasePlan;
environmentIsDisabled: boolean; environmentIsDisabled?: boolean;
readonly?: boolean;
} }
export const ReleasePlan = ({ export const ReleasePlan = ({
plan, plan,
environmentIsDisabled, environmentIsDisabled,
readonly,
}: IReleasePlanProps) => { }: IReleasePlanProps) => {
const { const {
id, id,
@ -134,7 +140,7 @@ export const ReleasePlan = ({
); );
return ( return (
<StyledContainer> <StyledContainer readonly={readonly}>
<StyledHeader> <StyledHeader>
<StyledHeaderTitleContainer> <StyledHeaderTitleContainer>
<StyledHeaderTitleLabel> <StyledHeaderTitleLabel>
@ -145,22 +151,25 @@ export const ReleasePlan = ({
{description} {description}
</StyledHeaderDescription> </StyledHeaderDescription>
</StyledHeaderTitleContainer> </StyledHeaderTitleContainer>
<PermissionIconButton {!readonly && (
onClick={() => setRemoveOpen(true)} <PermissionIconButton
permission={DELETE_FEATURE_STRATEGY} onClick={() => setRemoveOpen(true)}
environmentId={environment} permission={DELETE_FEATURE_STRATEGY}
projectId={projectId} environmentId={environment}
tooltipProps={{ projectId={projectId}
title: 'Remove release plan', tooltipProps={{
}} title: 'Remove release plan',
> }}
<Delete /> >
</PermissionIconButton> <Delete />
</PermissionIconButton>
)}
</StyledHeader> </StyledHeader>
<StyledBody> <StyledBody>
{milestones.map((milestone, index) => ( {milestones.map((milestone, index) => (
<div key={milestone.id}> <div key={milestone.id}>
<ReleasePlanMilestone <ReleasePlanMilestone
readonly={readonly}
milestone={milestone} milestone={milestone}
status={ status={
milestone.id === activeMilestoneId milestone.id === activeMilestoneId

View File

@ -58,14 +58,16 @@ const StyledAccordionDetails = styled(AccordionDetails)(({ theme }) => ({
interface IReleasePlanMilestoneProps { interface IReleasePlanMilestoneProps {
milestone: IReleasePlanMilestone; milestone: IReleasePlanMilestone;
status: MilestoneStatus; status?: MilestoneStatus;
onStartMilestone: (milestone: IReleasePlanMilestone) => void; onStartMilestone?: (milestone: IReleasePlanMilestone) => void;
readonly?: boolean;
} }
export const ReleasePlanMilestone = ({ export const ReleasePlanMilestone = ({
milestone, milestone,
status, status = 'not-started',
onStartMilestone, onStartMilestone,
readonly,
}: IReleasePlanMilestoneProps) => { }: IReleasePlanMilestoneProps) => {
const [expanded, setExpanded] = useState(false); const [expanded, setExpanded] = useState(false);
@ -75,10 +77,14 @@ export const ReleasePlanMilestone = ({
<StyledAccordionSummary> <StyledAccordionSummary>
<StyledTitleContainer> <StyledTitleContainer>
<StyledTitle>{milestone.name}</StyledTitle> <StyledTitle>{milestone.name}</StyledTitle>
<ReleasePlanMilestoneStatus {!readonly && onStartMilestone && (
status={status} <ReleasePlanMilestoneStatus
onStartMilestone={() => onStartMilestone(milestone)} status={status}
/> onStartMilestone={() =>
onStartMilestone(milestone)
}
/>
)}
</StyledTitleContainer> </StyledTitleContainer>
<StyledSecondaryLabel>No strategies</StyledSecondaryLabel> <StyledSecondaryLabel>No strategies</StyledSecondaryLabel>
</StyledAccordionSummary> </StyledAccordionSummary>
@ -94,10 +100,12 @@ export const ReleasePlanMilestone = ({
<StyledAccordionSummary expandIcon={<ExpandMore />}> <StyledAccordionSummary expandIcon={<ExpandMore />}>
<StyledTitleContainer> <StyledTitleContainer>
<StyledTitle>{milestone.name}</StyledTitle> <StyledTitle>{milestone.name}</StyledTitle>
<ReleasePlanMilestoneStatus {!readonly && onStartMilestone && (
status={status} <ReleasePlanMilestoneStatus
onStartMilestone={() => onStartMilestone(milestone)} status={status}
/> onStartMilestone={() => onStartMilestone(milestone)}
/>
)}
</StyledTitleContainer> </StyledTitleContainer>
<StyledSecondaryLabel> <StyledSecondaryLabel>
{milestone.strategies.length === 1 {milestone.strategies.length === 1