1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-10-27 11:02:16 +01:00

feat: Add transition condition UI for release plan milestones (#10768)

This commit is contained in:
Fredrik Strand Oseberg 2025-10-09 11:41:58 +02:00 committed by GitHub
parent 247dd3af51
commit a922801690
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 82 additions and 13 deletions

View File

@ -69,7 +69,7 @@ export const EnvironmentAccordionBody = ({
const { releasePlans } = useFeatureReleasePlans( const { releasePlans } = useFeatureReleasePlans(
projectId, projectId,
featureId, featureId,
featureEnvironment, featureEnvironment?.name,
); );
const { trackEvent } = usePlausibleTracker(); const { trackEvent } = usePlausibleTracker();

View File

@ -21,7 +21,7 @@ const FeatureOverviewWithReleasePlans: FC<
const { releasePlans } = useFeatureReleasePlans( const { releasePlans } = useFeatureReleasePlans(
projectId, projectId,
featureId, featureId,
environment, environment?.name,
); );
const envAddStrategySuggestionEnabled = useUiFlag( const envAddStrategySuggestionEnabled = useUiFlag(
'envAddStrategySuggestion', 'envAddStrategySuggestion',

View File

@ -1,6 +1,7 @@
import Add from '@mui/icons-material/Add'; import Add from '@mui/icons-material/Add';
import { Button, styled } from '@mui/material'; import { Button, styled } from '@mui/material';
import type { MilestoneStatus } from './ReleasePlanMilestoneStatus.tsx'; import type { MilestoneStatus } from './ReleasePlanMilestoneStatus.tsx';
import { MilestoneTransitionDisplay } from './MilestoneTransitionDisplay.tsx';
const StyledAutomationContainer = styled('div', { const StyledAutomationContainer = styled('div', {
shouldForwardProp: (prop) => prop !== 'status', shouldForwardProp: (prop) => prop !== 'status',
@ -51,6 +52,9 @@ interface IMilestoneAutomationSectionProps {
status?: MilestoneStatus; status?: MilestoneStatus;
onAddAutomation?: () => void; onAddAutomation?: () => void;
automationForm?: React.ReactNode; automationForm?: React.ReactNode;
transitionCondition?: {
intervalMinutes: number;
} | null;
} }
export const MilestoneAutomationSection = ({ export const MilestoneAutomationSection = ({
@ -58,6 +62,7 @@ export const MilestoneAutomationSection = ({
status, status,
onAddAutomation, onAddAutomation,
automationForm, automationForm,
transitionCondition,
}: IMilestoneAutomationSectionProps) => { }: IMilestoneAutomationSectionProps) => {
if (!showAutomation) return null; if (!showAutomation) return null;
@ -65,6 +70,10 @@ export const MilestoneAutomationSection = ({
<StyledAutomationContainer status={status}> <StyledAutomationContainer status={status}>
{automationForm ? ( {automationForm ? (
automationForm automationForm
) : transitionCondition ? (
<MilestoneTransitionDisplay
intervalMinutes={transitionCondition.intervalMinutes}
/>
) : ( ) : (
<StyledAddAutomationButton <StyledAddAutomationButton
onClick={onAddAutomation} onClick={onAddAutomation}

View File

@ -0,0 +1,55 @@
import BoltIcon from '@mui/icons-material/Bolt';
import { styled } from '@mui/material';
import { formatDuration, intervalToDuration } from 'date-fns';
const StyledDisplayContainer = styled('div')(({ theme }) => ({
display: 'flex',
alignItems: 'center',
gap: theme.spacing(1),
}));
const StyledIcon = styled(BoltIcon)(({ theme }) => ({
color: theme.palette.common.white,
fontSize: 18,
flexShrink: 0,
backgroundColor: theme.palette.primary.main,
borderRadius: '50%',
padding: theme.spacing(0.25),
}));
const StyledText = styled('span')(({ theme }) => ({
color: theme.palette.text.primary,
fontSize: theme.typography.body2.fontSize,
}));
interface IMilestoneTransitionDisplayProps {
intervalMinutes: number;
}
const formatInterval = (minutes: number): string => {
if (minutes === 0) return '0 minutes';
const duration = intervalToDuration({
start: 0,
end: minutes * 60 * 1000,
});
return formatDuration(duration, {
format: ['days', 'hours', 'minutes'],
delimiter: ', ',
});
};
export const MilestoneTransitionDisplay = ({
intervalMinutes,
}: IMilestoneTransitionDisplayProps) => {
return (
<StyledDisplayContainer>
<StyledIcon />
<StyledText>
Proceed to the next milestone after{' '}
{formatInterval(intervalMinutes)}
</StyledText>
</StyledDisplayContainer>
);
};

View File

@ -118,6 +118,7 @@ export const ReleasePlanMilestone = ({
status={status} status={status}
onAddAutomation={onAddAutomation} onAddAutomation={onAddAutomation}
automationForm={automationForm} automationForm={automationForm}
transitionCondition={milestone.transitionCondition}
/> />
</StyledMilestoneContainer> </StyledMilestoneContainer>
); );
@ -174,6 +175,7 @@ export const ReleasePlanMilestone = ({
status={status} status={status}
onAddAutomation={onAddAutomation} onAddAutomation={onAddAutomation}
automationForm={automationForm} automationForm={automationForm}
transitionCondition={milestone.transitionCondition}
/> />
</StyledMilestoneContainer> </StyledMilestoneContainer>
); );

View File

@ -1,28 +1,28 @@
import { useUiFlag } from 'hooks/useUiFlag'; import { useUiFlag } from 'hooks/useUiFlag';
import { useReleasePlans } from '../useReleasePlans/useReleasePlans.js'; import { useReleasePlans } from '../useReleasePlans/useReleasePlans.js';
import { useFeature } from '../useFeature/useFeature.js'; import { useFeature } from '../useFeature/useFeature.js';
import type { IFeatureEnvironment } from 'interfaces/featureToggle';
export const useFeatureReleasePlans = ( export const useFeatureReleasePlans = (
projectId: string, projectId: string,
featureId: string, featureId: string,
environment?: IFeatureEnvironment | string, environmentName?: string,
) => { ) => {
const featureReleasePlansEnabled = useUiFlag('featureReleasePlans'); const featureReleasePlansEnabled = useUiFlag('featureReleasePlans');
const envName =
typeof environment === 'string' ? environment : environment?.name;
const { const {
releasePlans: releasePlansFromHook, releasePlans: releasePlansFromHook,
refetch: refetchReleasePlans, refetch: refetchReleasePlans,
...rest ...rest
} = useReleasePlans(projectId, featureId, envName); } = useReleasePlans(projectId, featureId, environmentName);
const { refetchFeature } = useFeature(projectId, featureId); const { feature, refetchFeature } = useFeature(projectId, featureId);
const releasePlans = featureReleasePlansEnabled let releasePlans = releasePlansFromHook;
? typeof environment === 'object'
? environment?.releasePlans || [] if (featureReleasePlansEnabled) {
: [] const matchingEnvironment = feature?.environments?.find(
: releasePlansFromHook; (env) => env.name === environmentName,
);
releasePlans = matchingEnvironment?.releasePlans || [];
}
const refetch = featureReleasePlansEnabled const refetch = featureReleasePlansEnabled
? refetchFeature ? refetchFeature

View File

@ -35,6 +35,9 @@ export interface IReleasePlanMilestone {
name: string; name: string;
releasePlanDefinitionId: string; releasePlanDefinitionId: string;
strategies: IReleasePlanMilestoneStrategy[]; strategies: IReleasePlanMilestoneStrategy[];
transitionCondition?: {
intervalMinutes: number;
} | null;
} }
export interface IReleasePlanMilestoneStrategy extends IFeatureStrategy { export interface IReleasePlanMilestoneStrategy extends IFeatureStrategy {