1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-08-09 13:47:13 +02:00

add tabs to milestone start (#10237)

Adds changes/view diff tabs to release plan changes that show diffs. The
only instances I found where we show JSON diffs today was starting
milestones and adding a new release plan if you already have one.

I've moved the old file into a legacy file because we're touching two
out of three internal components, so it seemed like leaving it all in
one file would be a bit of a hassle. plus, this way it's consistent with
segments and strategies.

Start milestone:
<img width="1035" alt="image"
src="https://github.com/user-attachments/assets/2b4616f6-8452-4976-8101-11a94d6d5828"
/>

<img width="1054" alt="image"
src="https://github.com/user-attachments/assets/0ba58c72-b3dc-48fa-95bf-a3980dc620fe"
/>

Plan replacement:
<img width="1006" alt="image"
src="https://github.com/user-attachments/assets/9381a48f-e23e-435e-8fa5-02fcb5050bfd"
/>

<img width="818" alt="image"
src="https://github.com/user-attachments/assets/c5ceb9db-b095-4d05-88e8-fd8a70776479"
/>
This commit is contained in:
Thomas Heartman 2025-06-30 13:18:18 +02:00 committed by GitHub
parent fdc79e624f
commit 7c0bd12a24
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 402 additions and 79 deletions

View File

@ -14,6 +14,7 @@ import { EnvironmentStrategyExecutionOrder } from './EnvironmentStrategyExecutio
import { ArchiveFeatureChange } from './ArchiveFeatureChange.tsx';
import { DependencyChange } from './DependencyChange.tsx';
import { Link } from 'react-router-dom';
import { LegacyReleasePlanChange } from './LegacyReleasePlanChange.tsx';
import { ReleasePlanChange } from './ReleasePlanChange.tsx';
import { StrategyChange } from './StrategyChange.tsx';
import { useUiFlag } from 'hooks/useUiFlag.ts';
@ -94,6 +95,10 @@ export const FeatureChange: FC<{
? StrategyChange
: LegacyStrategyChange;
const ReleasePlanChangeComponent = useDiffableChangeComponent
? ReleasePlanChange
: LegacyReleasePlanChange;
return (
<StyledSingleChangeBox
key={objectId(change)}
@ -204,7 +209,7 @@ export const FeatureChange: FC<{
{(change.action === 'addReleasePlan' ||
change.action === 'deleteReleasePlan' ||
change.action === 'startMilestone') && (
<ReleasePlanChange
<ReleasePlanChangeComponent
actions={actions}
change={change}
featureName={feature.name}

View File

@ -0,0 +1,316 @@
import type React from 'react';
import { useRef, useState, type FC, type ReactNode } from 'react';
import { Box, styled, Typography } from '@mui/material';
import type {
ChangeRequestState,
IChangeRequestAddReleasePlan,
IChangeRequestDeleteReleasePlan,
IChangeRequestStartMilestone,
} from 'component/changeRequest/changeRequest.types';
import { useReleasePlanPreview } from 'hooks/useReleasePlanPreview';
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';
import type { IReleasePlan } from 'interfaces/releasePlans';
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;
currentReleasePlan?: IReleasePlan;
changeRequestState: ChangeRequestState;
actions?: ReactNode;
}> = ({ change, currentReleasePlan, changeRequestState, actions }) => {
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;
currentReleasePlan?: IReleasePlan;
changeRequestState: ChangeRequestState;
actions?: ReactNode;
}> = ({ change, currentReleasePlan, changeRequestState, actions }) => {
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;
currentReleasePlan?: IReleasePlan;
environmentName: string;
featureName: string;
actions?: ReactNode;
}> = ({
change,
currentReleasePlan,
environmentName,
featureName,
actions,
}) => {
const [currentTooltipOpen, setCurrentTooltipOpen] = useState(false);
const currentTooltipCloseTimeoutRef = useRef<NodeJS.Timeout>();
const openCurrentTooltip = () => {
if (currentTooltipCloseTimeoutRef.current) {
clearTimeout(currentTooltipCloseTimeoutRef.current);
}
setCurrentTooltipOpen(true);
};
const closeCurrentTooltip = () => {
currentTooltipCloseTimeoutRef.current = setTimeout(() => {
setCurrentTooltipOpen(false);
}, 100);
};
const planPreview = useReleasePlanPreview(
change.payload.templateId,
featureName,
environmentName,
);
const planPreviewDiff = {
...planPreview,
discriminator: 'plan',
releasePlanTemplateId: change.payload.templateId,
};
return (
<>
<ChangeItemCreateEditDeleteWrapper>
<ChangeItemInfo>
{currentReleasePlan ? (
<Typography>
Replacing{' '}
<TooltipLink
tooltip={
<div
onMouseEnter={() =>
openCurrentTooltip()
}
onMouseLeave={() =>
closeCurrentTooltip()
}
>
<ReleasePlan
plan={currentReleasePlan}
readonly
/>
</div>
}
tooltipProps={{
open: currentTooltipOpen,
maxWidth: 500,
maxHeight: 600,
}}
>
<span
onMouseEnter={() => openCurrentTooltip()}
onMouseLeave={() => closeCurrentTooltip()}
>
current
</span>
</TooltipLink>{' '}
release plan with:
</Typography>
) : (
<Typography color='success.dark'>
+ Adding release plan:
</Typography>
)}
<Typography>{planPreview.name}</Typography>
{currentReleasePlan && (
<TooltipLink
tooltip={
<StyledCodeSection>
<EventDiff
entry={{
preData: currentReleasePlan,
data: planPreviewDiff,
}}
/>
</StyledCodeSection>
}
tooltipProps={{
maxWidth: 500,
maxHeight: 600,
}}
>
<ViewDiff>View Diff</ViewDiff>
</TooltipLink>
)}
</ChangeItemInfo>
<div>{actions}</div>
</ChangeItemCreateEditDeleteWrapper>
<ReleasePlan plan={planPreview} readonly />
</>
);
};
/**
* Deprecated: use ReleasePlanChange instead. Remove file with flag crDiffView
* @deprecated
*/
export const LegacyReleasePlanChange: FC<{
actions?: ReactNode;
change:
| IChangeRequestAddReleasePlan
| IChangeRequestDeleteReleasePlan
| IChangeRequestStartMilestone;
environmentName: string;
featureName: string;
projectId: string;
changeRequestState: ChangeRequestState;
}> = ({
actions,
change,
featureName,
environmentName,
projectId,
changeRequestState,
}) => {
const { releasePlans } = useReleasePlans(
projectId,
featureName,
environmentName,
);
const currentReleasePlan = releasePlans[0];
return (
<>
{change.action === 'addReleasePlan' && (
<AddReleasePlan
change={change}
currentReleasePlan={currentReleasePlan}
environmentName={environmentName}
featureName={featureName}
actions={actions}
/>
)}
{change.action === 'deleteReleasePlan' && (
<DeleteReleasePlan
change={change}
currentReleasePlan={currentReleasePlan}
changeRequestState={changeRequestState}
actions={actions}
/>
)}
{change.action === 'startMilestone' && (
<StartMilestone
change={change}
currentReleasePlan={currentReleasePlan}
changeRequestState={changeRequestState}
actions={actions}
/>
)}
</>
);
};

View File

@ -10,10 +10,11 @@ import type {
import { useReleasePlanPreview } from 'hooks/useReleasePlanPreview';
import { useReleasePlans } from 'hooks/api/getters/useReleasePlans/useReleasePlans';
import { TooltipLink } from 'component/common/TooltipLink/TooltipLink';
import EventDiff from 'component/events/EventDiff/EventDiff';
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';
import type { IReleasePlan } from 'interfaces/releasePlans';
import { Tab, TabList, TabPanel, Tabs } from './ChangeTabComponents.tsx';
export const ChangeItemWrapper = styled(Box)({
display: 'flex',
@ -111,36 +112,34 @@ const StartMilestone: FC<{
if (!newMilestone) return;
return (
<>
<Tabs>
<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>
<div>
<TabList>
<Tab>Change</Tab>
<Tab>View diff</Tab>
</TabList>
{actions}
</div>
</ChangeItemCreateEditDeleteWrapper>
<ReleasePlanMilestone readonly milestone={newMilestone} />
</>
<TabPanel>
<ReleasePlanMilestone readonly milestone={newMilestone} />
</TabPanel>
<TabPanel>
<EventDiff
entry={{
preData: previousMilestone,
data: newMilestone,
}}
/>
</TabPanel>
</Tabs>
);
};
@ -183,75 +182,78 @@ const AddReleasePlan: FC<{
releasePlanTemplateId: change.payload.templateId,
};
return (
<>
<ChangeItemCreateEditDeleteWrapper>
<ChangeItemInfo>
{currentReleasePlan ? (
<Typography>
Replacing{' '}
<TooltipLink
tooltip={
<div
onMouseEnter={() =>
openCurrentTooltip()
}
onMouseLeave={() =>
closeCurrentTooltip()
}
>
<ReleasePlan
plan={currentReleasePlan}
readonly
/>
</div>
}
tooltipProps={{
open: currentTooltipOpen,
maxWidth: 500,
maxHeight: 600,
}}
>
<span
onMouseEnter={() => openCurrentTooltip()}
onMouseLeave={() => closeCurrentTooltip()}
>
current
</span>
</TooltipLink>{' '}
release plan with:
</Typography>
) : (
if (!currentReleasePlan) {
return (
<>
<ChangeItemCreateEditDeleteWrapper>
<ChangeItemInfo>
<Typography color='success.dark'>
+ Adding release plan:
</Typography>
)}
<Typography>{planPreview.name}</Typography>
{currentReleasePlan && (
<Typography>{planPreview.name}</Typography>
</ChangeItemInfo>
<div>{actions}</div>
</ChangeItemCreateEditDeleteWrapper>
<ReleasePlan plan={planPreview} readonly />
</>
);
}
return (
<Tabs>
<ChangeItemCreateEditDeleteWrapper>
<ChangeItemInfo>
<Typography>
Replacing{' '}
<TooltipLink
tooltip={
<StyledCodeSection>
<EventDiff
entry={{
preData: currentReleasePlan,
data: planPreviewDiff,
}}
<div
onMouseEnter={() => openCurrentTooltip()}
onMouseLeave={() => closeCurrentTooltip()}
>
<ReleasePlan
plan={currentReleasePlan}
readonly
/>
</StyledCodeSection>
</div>
}
tooltipProps={{
open: currentTooltipOpen,
maxWidth: 500,
maxHeight: 600,
}}
>
<ViewDiff>View Diff</ViewDiff>
</TooltipLink>
)}
<span
onMouseEnter={() => openCurrentTooltip()}
onMouseLeave={() => closeCurrentTooltip()}
>
current
</span>
</TooltipLink>{' '}
release plan with:
</Typography>
<Typography>{planPreview.name}</Typography>
</ChangeItemInfo>
<div>{actions}</div>
<div>
<TabList>
<Tab>Changes</Tab>
<Tab>View diff</Tab>
</TabList>
{actions}
</div>
</ChangeItemCreateEditDeleteWrapper>
<ReleasePlan plan={planPreview} readonly />
</>
<TabPanel>
<ReleasePlan plan={planPreview} readonly />
</TabPanel>
<TabPanel>
<EventDiff
entry={{
preData: currentReleasePlan,
data: planPreviewDiff,
}}
/>
</TabPanel>
</Tabs>
);
};