1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-08-13 13:48:59 +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 { ArchiveFeatureChange } from './ArchiveFeatureChange.tsx';
import { DependencyChange } from './DependencyChange.tsx'; import { DependencyChange } from './DependencyChange.tsx';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { LegacyReleasePlanChange } from './LegacyReleasePlanChange.tsx';
import { ReleasePlanChange } from './ReleasePlanChange.tsx'; import { ReleasePlanChange } from './ReleasePlanChange.tsx';
import { StrategyChange } from './StrategyChange.tsx'; import { StrategyChange } from './StrategyChange.tsx';
import { useUiFlag } from 'hooks/useUiFlag.ts'; import { useUiFlag } from 'hooks/useUiFlag.ts';
@ -94,6 +95,10 @@ export const FeatureChange: FC<{
? StrategyChange ? StrategyChange
: LegacyStrategyChange; : LegacyStrategyChange;
const ReleasePlanChangeComponent = useDiffableChangeComponent
? ReleasePlanChange
: LegacyReleasePlanChange;
return ( return (
<StyledSingleChangeBox <StyledSingleChangeBox
key={objectId(change)} key={objectId(change)}
@ -204,7 +209,7 @@ export const FeatureChange: FC<{
{(change.action === 'addReleasePlan' || {(change.action === 'addReleasePlan' ||
change.action === 'deleteReleasePlan' || change.action === 'deleteReleasePlan' ||
change.action === 'startMilestone') && ( change.action === 'startMilestone') && (
<ReleasePlanChange <ReleasePlanChangeComponent
actions={actions} actions={actions}
change={change} change={change}
featureName={feature.name} 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 { useReleasePlanPreview } from 'hooks/useReleasePlanPreview';
import { useReleasePlans } from 'hooks/api/getters/useReleasePlans/useReleasePlans'; import { useReleasePlans } from 'hooks/api/getters/useReleasePlans/useReleasePlans';
import { TooltipLink } from 'component/common/TooltipLink/TooltipLink'; 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 { ReleasePlan } from 'component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlan';
import { ReleasePlanMilestone } from 'component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlanMilestone/ReleasePlanMilestone'; import { ReleasePlanMilestone } from 'component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlanMilestone/ReleasePlanMilestone';
import type { IReleasePlan } from 'interfaces/releasePlans'; import type { IReleasePlan } from 'interfaces/releasePlans';
import { Tab, TabList, TabPanel, Tabs } from './ChangeTabComponents.tsx';
export const ChangeItemWrapper = styled(Box)({ export const ChangeItemWrapper = styled(Box)({
display: 'flex', display: 'flex',
@ -111,36 +112,34 @@ const StartMilestone: FC<{
if (!newMilestone) return; if (!newMilestone) return;
return ( return (
<> <Tabs>
<ChangeItemCreateEditDeleteWrapper> <ChangeItemCreateEditDeleteWrapper>
<ChangeItemInfo> <ChangeItemInfo>
<Typography color='success.dark'> <Typography color='success.dark'>
+ Start milestone: + Start milestone:
</Typography> </Typography>
<Typography>{newMilestone.name}</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> </ChangeItemInfo>
<div>{actions}</div> <div>
<TabList>
<Tab>Change</Tab>
<Tab>View diff</Tab>
</TabList>
{actions}
</div>
</ChangeItemCreateEditDeleteWrapper> </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, releasePlanTemplateId: change.payload.templateId,
}; };
return ( if (!currentReleasePlan) {
<> return (
<ChangeItemCreateEditDeleteWrapper> <>
<ChangeItemInfo> <ChangeItemCreateEditDeleteWrapper>
{currentReleasePlan ? ( <ChangeItemInfo>
<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'> <Typography color='success.dark'>
+ Adding release plan: + Adding release plan:
</Typography> </Typography>
)} <Typography>{planPreview.name}</Typography>
<Typography>{planPreview.name}</Typography> </ChangeItemInfo>
{currentReleasePlan && ( <div>{actions}</div>
</ChangeItemCreateEditDeleteWrapper>
<ReleasePlan plan={planPreview} readonly />
</>
);
}
return (
<Tabs>
<ChangeItemCreateEditDeleteWrapper>
<ChangeItemInfo>
<Typography>
Replacing{' '}
<TooltipLink <TooltipLink
tooltip={ tooltip={
<StyledCodeSection> <div
<EventDiff onMouseEnter={() => openCurrentTooltip()}
entry={{ onMouseLeave={() => closeCurrentTooltip()}
preData: currentReleasePlan, >
data: planPreviewDiff, <ReleasePlan
}} plan={currentReleasePlan}
readonly
/> />
</StyledCodeSection> </div>
} }
tooltipProps={{ tooltipProps={{
open: currentTooltipOpen,
maxWidth: 500, maxWidth: 500,
maxHeight: 600, maxHeight: 600,
}} }}
> >
<ViewDiff>View Diff</ViewDiff> <span
</TooltipLink> onMouseEnter={() => openCurrentTooltip()}
)} onMouseLeave={() => closeCurrentTooltip()}
>
current
</span>
</TooltipLink>{' '}
release plan with:
</Typography>
<Typography>{planPreview.name}</Typography>
</ChangeItemInfo> </ChangeItemInfo>
<div>{actions}</div> <div>
<TabList>
<Tab>Changes</Tab>
<Tab>View diff</Tab>
</TabList>
{actions}
</div>
</ChangeItemCreateEditDeleteWrapper> </ChangeItemCreateEditDeleteWrapper>
<ReleasePlan plan={planPreview} readonly /> <TabPanel>
</> <ReleasePlan plan={planPreview} readonly />
</TabPanel>
<TabPanel>
<EventDiff
entry={{
preData: currentReleasePlan,
data: planPreviewDiff,
}}
/>
</TabPanel>
</Tabs>
); );
}; };