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 { 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>
);

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 { 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';

View File

@ -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

View File

@ -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