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

feat: improve milestone automation UI positioning and styling (#10758)

This commit is contained in:
Fredrik Strand Oseberg 2025-10-08 11:56:41 +02:00 committed by GitHub
parent 43fa239e72
commit 1d4f72cf81
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 208 additions and 176 deletions

View File

@ -20,41 +20,26 @@ const StyledFormContainer = styled('div')(({ theme }) => ({
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
gap: theme.spacing(1.5), gap: theme.spacing(1.5),
padding: theme.spacing(2), padding: theme.spacing(1.5, 2),
backgroundColor: theme.palette.background.paper, backgroundColor: theme.palette.background.elevation1,
borderRadius: theme.spacing(0.75), width: '100%',
border: `1px solid ${theme.palette.divider}`, borderRadius: `${theme.shape.borderRadiusLarge}px`,
boxShadow: theme.boxShadows.elevated,
position: 'relative', position: 'relative',
marginLeft: theme.spacing(3.25),
marginTop: theme.spacing(1.5),
marginBottom: theme.spacing(1.5),
animation: 'slideDown 0.5s ease-out',
'@keyframes slideDown': {
from: {
opacity: 0,
transform: 'translateY(-24px)',
},
to: {
opacity: 1,
transform: 'translateY(0)',
},
},
})); }));
const StyledTopRow = styled('div')(({ theme }) => ({ const StyledTopRow = styled('div')(({ theme }) => ({
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: theme.spacing(1.5), gap: theme.spacing(1),
})); }));
const StyledIcon = styled(BoltIcon)(({ theme }) => ({ const StyledIcon = styled(BoltIcon)(({ theme }) => ({
color: theme.palette.primary.main, color: theme.palette.common.white,
fontSize: 20, fontSize: 18,
flexShrink: 0, flexShrink: 0,
backgroundColor: theme.palette.background.elevation1, backgroundColor: theme.palette.primary.main,
borderRadius: '50%', borderRadius: '50%',
border: `1px solid ${theme.palette.divider}`, padding: theme.spacing(0.25),
})); }));
const StyledLabel = styled('span')(({ theme }) => ({ const StyledLabel = styled('span')(({ theme }) => ({
@ -105,8 +90,8 @@ const StyledButtonGroup = styled('div')(({ theme }) => ({
gap: theme.spacing(1), gap: theme.spacing(1),
justifyContent: 'flex-end', justifyContent: 'flex-end',
alignItems: 'center', alignItems: 'center',
paddingTop: theme.spacing(1.5), paddingTop: theme.spacing(1),
marginTop: theme.spacing(1), marginTop: theme.spacing(0.5),
borderTop: `1px solid ${theme.palette.divider}`, borderTop: `1px solid ${theme.palette.divider}`,
})); }));

View File

@ -1,6 +1,5 @@
import Delete from '@mui/icons-material/Delete'; import Delete from '@mui/icons-material/Delete';
import Add from '@mui/icons-material/Add'; import { styled } from '@mui/material';
import { styled, IconButton, Button } from '@mui/material';
import { DELETE_FEATURE_STRATEGY } from '@server/types/permissions'; import { DELETE_FEATURE_STRATEGY } from '@server/types/permissions';
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton'; import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
import { useReleasePlansApi } from 'hooks/api/actions/useReleasePlansApi/useReleasePlansApi'; import { useReleasePlansApi } from 'hooks/api/actions/useReleasePlansApi/useReleasePlansApi';
@ -72,47 +71,12 @@ const StyledBody = styled('div')(({ theme }) => ({
})); }));
const StyledConnection = styled('div')(({ theme }) => ({ const StyledConnection = styled('div')(({ theme }) => ({
width: 4,
height: theme.spacing(6),
backgroundColor: theme.palette.divider,
marginLeft: theme.spacing(3.25),
}));
const StyledConnectionSimple = styled('div')(({ theme }) => ({
width: 4, width: 4,
height: theme.spacing(2), height: theme.spacing(2),
backgroundColor: theme.palette.divider, backgroundColor: theme.palette.divider,
marginLeft: theme.spacing(3.25), marginLeft: theme.spacing(3.25),
})); }));
const StyledConnectionContainer = styled('div')(({ theme }) => ({
position: 'relative',
display: 'flex',
alignItems: 'center',
}));
const StyledAddAutomationIconButton = styled(IconButton)(({ theme }) => ({
position: 'absolute',
left: theme.spacing(2),
top: '12px',
width: 24,
height: 24,
border: `1px solid ${theme.palette.primary.main}`,
backgroundColor: theme.palette.background.elevation2,
zIndex: 1,
'& svg': {
fontSize: 16,
},
}));
const StyledAddAutomationButton = styled(Button)(({ theme }) => ({
marginLeft: theme.spacing(3),
textTransform: 'none',
fontWeight: theme.typography.fontWeightBold,
padding: 0,
minWidth: 'auto',
}));
interface IReleasePlanProps { interface IReleasePlanProps {
plan: IReleasePlan; plan: IReleasePlan;
environmentIsDisabled?: boolean; environmentIsDisabled?: boolean;
@ -332,63 +296,27 @@ export const ReleasePlan = ({
: 'not-started' : 'not-started'
} }
onStartMilestone={onStartMilestone} onStartMilestone={onStartMilestone}
showAutomation={
milestoneProgressionsEnabled &&
isNotLastMilestone
}
onAddAutomation={handleOpenProgressionForm}
automationForm={
isProgressionFormOpen ? (
<MilestoneProgressionForm
sourceMilestoneId={milestone.id}
targetMilestoneId={nextMilestoneId}
projectId={projectId}
environment={environment}
onSave={handleProgressionSave}
onCancel={handleProgressionCancel}
/>
) : undefined
}
/> />
<ConditionallyRender <ConditionallyRender
condition={isNotLastMilestone} condition={isNotLastMilestone}
show={ show={<StyledConnection />}
<ConditionallyRender
condition={milestoneProgressionsEnabled}
show={
<ConditionallyRender
condition={
isProgressionFormOpen
}
show={
<MilestoneProgressionForm
sourceMilestoneId={
milestone.id
}
targetMilestoneId={
nextMilestoneId
}
projectId={projectId}
environment={
environment
}
onSave={
handleProgressionSave
}
onCancel={
handleProgressionCancel
}
/>
}
elseShow={
<StyledConnectionContainer>
<StyledConnection />
<StyledAddAutomationIconButton
onClick={
handleOpenProgressionForm
}
color='primary'
>
<Add />
</StyledAddAutomationIconButton>
<StyledAddAutomationButton
onClick={
handleOpenProgressionForm
}
color='primary'
>
Add automation
</StyledAddAutomationButton>
</StyledConnectionContainer>
}
/>
}
elseShow={<StyledConnectionSimple />}
/>
}
/> />
</div> </div>
); );

View File

@ -0,0 +1,79 @@
import Add from '@mui/icons-material/Add';
import { Button, styled } from '@mui/material';
import type { MilestoneStatus } from './ReleasePlanMilestoneStatus.tsx';
const StyledAutomationContainer = styled('div', {
shouldForwardProp: (prop) => prop !== 'status',
})<{ status?: MilestoneStatus }>(({ theme, status }) => ({
border: `1px solid ${status === 'active' ? theme.palette.success.border : theme.palette.divider}`,
borderTop: `1px solid ${theme.palette.divider}`,
borderRadius: `0 0 ${theme.shape.borderRadiusLarge}px ${theme.shape.borderRadiusLarge}px`,
padding: theme.spacing(1.5, 2),
backgroundColor: theme.palette.background.paper,
display: 'flex',
flexDirection: 'column',
alignItems: 'stretch',
gap: theme.spacing(1),
'& > *': {
alignSelf: 'flex-start',
},
}));
const StyledAddAutomationButton = styled(Button)(({ theme }) => ({
textTransform: 'none',
fontWeight: theme.typography.fontWeightBold,
fontSize: theme.typography.body2.fontSize,
padding: 0,
minWidth: 'auto',
gap: theme.spacing(1),
'&:hover': {
backgroundColor: 'transparent',
},
'& .MuiButton-startIcon': {
margin: 0,
width: 20,
height: 20,
border: `1px solid ${theme.palette.primary.main}`,
backgroundColor: theme.palette.background.elevation2,
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
'& svg': {
fontSize: 14,
color: theme.palette.primary.main,
},
},
}));
interface IMilestoneAutomationSectionProps {
showAutomation?: boolean;
status?: MilestoneStatus;
onAddAutomation?: () => void;
automationForm?: React.ReactNode;
}
export const MilestoneAutomationSection = ({
showAutomation,
status,
onAddAutomation,
automationForm,
}: IMilestoneAutomationSectionProps) => {
if (!showAutomation) return null;
return (
<StyledAutomationContainer status={status}>
{automationForm ? (
automationForm
) : (
<StyledAddAutomationButton
onClick={onAddAutomation}
color='primary'
startIcon={<Add />}
>
Add automation
</StyledAddAutomationButton>
)}
</StyledAutomationContainer>
);
};

View File

@ -16,19 +16,28 @@ import { StrategySeparator } from 'component/common/StrategySeparator/StrategySe
import { StrategyItem } from '../../FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyItem.tsx'; import { StrategyItem } from '../../FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyItem.tsx';
import { StrategyList } from 'component/common/StrategyList/StrategyList'; import { StrategyList } from 'component/common/StrategyList/StrategyList';
import { StrategyListItem } from 'component/common/StrategyList/StrategyListItem'; import { StrategyListItem } from 'component/common/StrategyList/StrategyListItem';
import { MilestoneAutomationSection } from './MilestoneAutomationSection.tsx';
const StyledAccordion = styled(Accordion, { const StyledAccordion = styled(Accordion, {
shouldForwardProp: (prop) => prop !== 'status', shouldForwardProp: (prop) => prop !== 'status' && prop !== 'hasAutomation',
})<{ status: MilestoneStatus }>(({ theme, status }) => ({ })<{ status: MilestoneStatus; hasAutomation?: boolean }>(
border: `1px solid ${status === 'active' ? theme.palette.success.border : theme.palette.divider}`, ({ theme, status, hasAutomation }) => ({
overflow: 'hidden', border: `1px solid ${status === 'active' ? theme.palette.success.border : theme.palette.divider}`,
boxShadow: 'none', borderBottom: hasAutomation
margin: 0, ? 'none'
backgroundColor: theme.palette.background.paper, : `1px solid ${status === 'active' ? theme.palette.success.border : theme.palette.divider}`,
'&:before': { overflow: 'hidden',
display: 'none', boxShadow: 'none',
}, margin: 0,
})); backgroundColor: theme.palette.background.paper,
borderRadius: hasAutomation
? `${theme.shape.borderRadiusLarge}px ${theme.shape.borderRadiusLarge}px 0 0 !important`
: `${theme.shape.borderRadiusLarge}px`,
'&:before': {
display: 'none',
},
}),
);
const StyledAccordionSummary = styled(AccordionSummary)({ const StyledAccordionSummary = styled(AccordionSummary)({
'& .MuiAccordionSummary-content': { '& .MuiAccordionSummary-content': {
@ -58,11 +67,18 @@ const StyledAccordionDetails = styled(AccordionDetails)(({ theme }) => ({
padding: 0, padding: 0,
})); }));
const StyledMilestoneContainer = styled('div')({
position: 'relative',
});
interface IReleasePlanMilestoneProps { interface IReleasePlanMilestoneProps {
milestone: IReleasePlanMilestone; milestone: IReleasePlanMilestone;
status?: MilestoneStatus; status?: MilestoneStatus;
onStartMilestone?: (milestone: IReleasePlanMilestone) => void; onStartMilestone?: (milestone: IReleasePlanMilestone) => void;
readonly?: boolean; readonly?: boolean;
showAutomation?: boolean;
onAddAutomation?: () => void;
automationForm?: React.ReactNode;
} }
export const ReleasePlanMilestone = ({ export const ReleasePlanMilestone = ({
@ -70,13 +86,51 @@ export const ReleasePlanMilestone = ({
status = 'not-started', status = 'not-started',
onStartMilestone, onStartMilestone,
readonly, readonly,
showAutomation,
onAddAutomation,
automationForm,
}: IReleasePlanMilestoneProps) => { }: IReleasePlanMilestoneProps) => {
const [expanded, setExpanded] = useState(false); const [expanded, setExpanded] = useState(false);
if (!milestone.strategies.length) { if (!milestone.strategies.length) {
return ( return (
<StyledAccordion status={status}> <StyledMilestoneContainer>
<StyledAccordionSummary> <StyledAccordion status={status} hasAutomation={showAutomation}>
<StyledAccordionSummary>
<StyledTitleContainer>
<StyledTitle>{milestone.name}</StyledTitle>
{!readonly && onStartMilestone && (
<ReleasePlanMilestoneStatus
status={status}
onStartMilestone={() =>
onStartMilestone(milestone)
}
/>
)}
</StyledTitleContainer>
<StyledSecondaryLabel>
No strategies
</StyledSecondaryLabel>
</StyledAccordionSummary>
</StyledAccordion>
<MilestoneAutomationSection
showAutomation={showAutomation}
status={status}
onAddAutomation={onAddAutomation}
automationForm={automationForm}
/>
</StyledMilestoneContainer>
);
}
return (
<StyledMilestoneContainer>
<StyledAccordion
status={status}
hasAutomation={showAutomation}
onChange={(evt, expanded) => setExpanded(expanded)}
>
<StyledAccordionSummary expandIcon={<ExpandMore />}>
<StyledTitleContainer> <StyledTitleContainer>
<StyledTitle>{milestone.name}</StyledTitle> <StyledTitle>{milestone.name}</StyledTitle>
{!readonly && onStartMilestone && ( {!readonly && onStartMilestone && (
@ -88,53 +142,39 @@ export const ReleasePlanMilestone = ({
/> />
)} )}
</StyledTitleContainer> </StyledTitleContainer>
<StyledSecondaryLabel>No strategies</StyledSecondaryLabel> <StyledSecondaryLabel>
{milestone.strategies.length === 1
? `${expanded ? 'Hide' : 'View'} strategy`
: `${expanded ? 'Hide' : 'View'} ${milestone.strategies.length} strategies`}
</StyledSecondaryLabel>
</StyledAccordionSummary> </StyledAccordionSummary>
<StyledAccordionDetails>
<StrategyList>
{milestone.strategies.map((strategy, index) => (
<StrategyListItem key={strategy.id}>
{index > 0 ? <StrategySeparator /> : null}
<StrategyItem
strategyHeaderLevel={4}
strategy={{
...strategy,
name:
strategy.name ||
strategy.strategyName ||
'',
}}
/>
</StrategyListItem>
))}
</StrategyList>
</StyledAccordionDetails>
</StyledAccordion> </StyledAccordion>
); <MilestoneAutomationSection
} showAutomation={showAutomation}
status={status}
return ( onAddAutomation={onAddAutomation}
<StyledAccordion automationForm={automationForm}
status={status} />
onChange={(evt, expanded) => setExpanded(expanded)} </StyledMilestoneContainer>
>
<StyledAccordionSummary expandIcon={<ExpandMore />}>
<StyledTitleContainer>
<StyledTitle>{milestone.name}</StyledTitle>
{!readonly && onStartMilestone && (
<ReleasePlanMilestoneStatus
status={status}
onStartMilestone={() => onStartMilestone(milestone)}
/>
)}
</StyledTitleContainer>
<StyledSecondaryLabel>
{milestone.strategies.length === 1
? `${expanded ? 'Hide' : 'View'} strategy`
: `${expanded ? 'Hide' : 'View'} ${milestone.strategies.length} strategies`}
</StyledSecondaryLabel>
</StyledAccordionSummary>
<StyledAccordionDetails>
<StrategyList>
{milestone.strategies.map((strategy, index) => (
<StrategyListItem key={strategy.id}>
{index > 0 ? <StrategySeparator /> : null}
<StrategyItem
strategyHeaderLevel={4}
strategy={{
...strategy,
name:
strategy.name ||
strategy.strategyName ||
'',
}}
/>
</StrategyListItem>
))}
</StrategyList>
</StyledAccordionDetails>
</StyledAccordion>
); );
}; };