Skip to content

Commit

Permalink
chore: release plans small misc improvements (#8879)
Browse files Browse the repository at this point in the history
https://linear.app/unleash/issue/2-3038/release-plans-misc-ux-improvements

Includes various UX improvements focused on release plans:
- **New milestone status:** Introduced a "Paused" status for milestones.
A milestone is marked as "Paused" when it is active but the associated
environment is disabled.
- **Status display:** Paused milestones are labeled as "Paused (disabled
in environment)" for clarity.
- **Styling cleanup:** Removed unused disabled styling in the release
plan component.
- **Accordion stability:** Fixed visual shifting in milestone accordions
when toggling.
- **Strategy count:** Updated the "View Strategies" label to reflect the
total number of strategies in the milestone.
- **Edge case handling:** Improved rendering for milestones without
strategies.
- **Component extraction:** Refactored milestone status into a
standalone component.
- **Component organization:** Grouped milestone-specific components
under a `ReleasePlanMilestone` parent folder.
- **Template card cursor enhancement:** Set the cursor on the template
card to "pointer", so we better reflect the interactivity of the
element.
- **Template card created by enhancement:** Added an avatar for the
"Created by" field in release plan template cards, replacing the
creator's ID.
- **Navigation improvement:** After creating or editing a release plan
template, users are now redirected back to the release management page.


![image](https://github.com/user-attachments/assets/b0717dc6-3049-4612-9b46-f37a4fa887a3)


![image](https://github.com/user-attachments/assets/a17daafa-f961-4269-9522-39769912752c)
  • Loading branch information
nunogois authored Nov 28, 2024
1 parent 8d1ebf6 commit f75cf1d
Show file tree
Hide file tree
Showing 9 changed files with 147 additions and 81 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,11 @@ const EnvironmentAccordionBody = ({
show={
<>
{releasePlans.map((plan) => (
<ReleasePlan key={plan.id} plan={plan} />
<ReleasePlan
key={plan.id}
plan={plan}
environmentIsDisabled={isDisabled}
/>
))}
<ConditionallyRender
condition={
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,11 @@ export const FeatureOverviewEnvironmentBody = ({
show={
<>
{releasePlans.map((plan) => (
<ReleasePlan key={plan.id} plan={plan} />
<ReleasePlan
key={plan.id}
plan={plan}
environmentIsDisabled={isDisabled}
/>
))}
<ConditionallyRender
condition={
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,29 +13,23 @@ import type {
import { useState } from 'react';
import { formatUnknownError } from 'utils/formatUnknownError';
import { ReleasePlanRemoveDialog } from './ReleasePlanRemoveDialog';
import { ReleasePlanMilestone } from './ReleasePlanMilestone';
import { ReleasePlanMilestone } from './ReleasePlanMilestone/ReleasePlanMilestone';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';

const StyledContainer = styled('div', {
shouldForwardProp: (prop) => prop !== 'disabled',
})<{ disabled?: boolean }>(({ theme, disabled }) => ({
const StyledContainer = styled('div')(({ theme }) => ({
padding: theme.spacing(2),
borderRadius: theme.shape.borderRadiusMedium,
border: `1px solid ${theme.palette.divider}`,
'& + &': {
marginTop: theme.spacing(2),
},
background: disabled
? theme.palette.envAccordion.disabled
: theme.palette.background.paper,
background: theme.palette.background.paper,
}));

const StyledHeader = styled('div', {
shouldForwardProp: (prop) => prop !== 'disabled',
})<{ disabled?: boolean }>(({ theme, disabled }) => ({
const StyledHeader = styled('div')(({ theme }) => ({
display: 'flex',
justifyContent: 'space-between',
color: disabled ? theme.palette.text.secondary : theme.palette.text.primary,
color: theme.palette.text.primary,
}));

const StyledHeaderTitleContainer = styled('div')(({ theme }) => ({
Expand Down Expand Up @@ -73,9 +67,13 @@ const StyledConnection = styled('div')(({ theme }) => ({

interface IReleasePlanProps {
plan: IReleasePlan;
environmentIsDisabled: boolean;
}

export const ReleasePlan = ({ plan }: IReleasePlanProps) => {
export const ReleasePlan = ({
plan,
environmentIsDisabled,
}: IReleasePlanProps) => {
const {
id,
name,
Expand Down Expand Up @@ -132,14 +130,13 @@ export const ReleasePlan = ({ plan }: IReleasePlanProps) => {
}
};

const disabled = !activeMilestoneId;
const activeIndex = milestones.findIndex(
(milestone) => milestone.id === activeMilestoneId,
);

return (
<StyledContainer disabled={disabled}>
<StyledHeader disabled={disabled}>
<StyledContainer>
<StyledHeader>
<StyledHeaderTitleContainer>
<StyledHeaderTitleLabel>
Release plan
Expand Down Expand Up @@ -168,7 +165,9 @@ export const ReleasePlan = ({ plan }: IReleasePlanProps) => {
milestone={milestone}
status={
milestone.id === activeMilestoneId
? 'active'
? environmentIsDisabled
? 'paused'
: 'active'
: index < activeIndex
? 'completed'
: 'not-started'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,16 @@ import {
Accordion,
AccordionDetails,
AccordionSummary,
Link,
styled,
} from '@mui/material';
import PlayCircleIcon from '@mui/icons-material/PlayCircle';
import TripOriginIcon from '@mui/icons-material/TripOrigin';
import type { IReleasePlanMilestone } from 'interfaces/releasePlans';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { ReleasePlanMilestoneStrategy } from './ReleasePlanMilestoneStrategy';
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator';

type MilestoneStatus = 'not-started' | 'active' | 'completed';
import {
ReleasePlanMilestoneStatus,
type MilestoneStatus,
} from './ReleasePlanMilestoneStatus';

const StyledAccordion = styled(Accordion, {
shouldForwardProp: (prop) => prop !== 'status',
Expand All @@ -31,6 +30,7 @@ const StyledAccordionSummary = styled(AccordionSummary)({
'& .MuiAccordionSummary-content': {
justifyContent: 'space-between',
alignItems: 'center',
minHeight: '30px',
},
});

Expand All @@ -45,33 +45,6 @@ const StyledTitle = styled('span')(({ theme }) => ({
fontWeight: theme.fontWeight.bold,
}));

const StyledStatus = styled('div', {
shouldForwardProp: (prop) => prop !== 'status',
})<{ status: MilestoneStatus }>(({ theme, status }) => ({
display: 'flex',
alignItems: 'center',
gap: theme.spacing(1),
paddingRight: theme.spacing(1),
fontSize: theme.fontSizes.smallerBody,
borderRadius: theme.shape.borderRadiusMedium,
backgroundColor:
status === 'active' ? theme.palette.success.light : 'transparent',
color:
status === 'active'
? theme.palette.success.contrastText
: status === 'completed'
? theme.palette.text.secondary
: theme.palette.text.primary,
'& svg': {
color:
status === 'active'
? theme.palette.success.main
: status === 'completed'
? theme.palette.neutral.border
: theme.palette.primary.main,
},
}));

const StyledSecondaryLabel = styled('span')(({ theme }) => ({
color: theme.palette.text.secondary,
fontSize: theme.fontSizes.smallBody,
Expand All @@ -94,41 +67,38 @@ export const ReleasePlanMilestone = ({
status,
onStartMilestone,
}: IReleasePlanMilestoneProps) => {
const statusText =
status === 'active'
? 'Running'
: status === 'completed'
? 'Restart'
: 'Start';
if (!milestone.strategies.length) {
return (
<StyledAccordion status={status}>
<StyledAccordionSummary>
<StyledTitleContainer>
<StyledTitle>{milestone.name}</StyledTitle>
<ReleasePlanMilestoneStatus
status={status}
onStartMilestone={() => onStartMilestone(milestone)}
/>
</StyledTitleContainer>
<StyledSecondaryLabel>No strategies</StyledSecondaryLabel>
</StyledAccordionSummary>
</StyledAccordion>
);
}

return (
<StyledAccordion status={status}>
<StyledAccordionSummary expandIcon={<ExpandMore />}>
<StyledTitleContainer>
<StyledTitle>{milestone.name}</StyledTitle>
<StyledStatus status={status}>
<ConditionallyRender
condition={status === 'active'}
show={<TripOriginIcon />}
elseShow={<PlayCircleIcon />}
/>
<ConditionallyRender
condition={status === 'active'}
show={<span>{statusText}</span>}
elseShow={
<Link
onClick={(e) => {
e.stopPropagation();
onStartMilestone(milestone);
}}
>
{statusText}
</Link>
}
/>
</StyledStatus>
<ReleasePlanMilestoneStatus
status={status}
onStartMilestone={() => onStartMilestone(milestone)}
/>
</StyledTitleContainer>
<StyledSecondaryLabel>View strategies</StyledSecondaryLabel>
<StyledSecondaryLabel>
{milestone.strategies.length === 1
? 'View strategy'
: `View ${milestone.strategies.length} strategies`}
</StyledSecondaryLabel>
</StyledAccordionSummary>
<StyledAccordionDetails>
{milestone.strategies.map((strategy, index) => (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { Link, styled } from '@mui/material';
import PlayCircleIcon from '@mui/icons-material/PlayCircle';
import PauseCircleIcon from '@mui/icons-material/PauseCircle';
import TripOriginIcon from '@mui/icons-material/TripOrigin';

export type MilestoneStatus = 'not-started' | 'active' | 'paused' | 'completed';

const StyledStatus = styled('div', {
shouldForwardProp: (prop) => prop !== 'status',
})<{ status: MilestoneStatus }>(({ theme, status }) => ({
display: 'flex',
alignItems: 'center',
gap: theme.spacing(1),
paddingRight: theme.spacing(1),
fontSize: theme.fontSizes.smallerBody,
borderRadius: theme.shape.borderRadiusMedium,
backgroundColor:
status === 'active' ? theme.palette.success.light : 'transparent',
color:
status === 'active'
? theme.palette.success.contrastText
: status === 'completed'
? theme.palette.text.secondary
: theme.palette.text.primary,
'& svg': {
color:
status === 'active'
? theme.palette.success.main
: status === 'paused'
? theme.palette.text.disabled
: status === 'completed'
? theme.palette.neutral.border
: theme.palette.primary.main,
},
}));

interface IReleasePlanMilestoneStatusProps {
status: MilestoneStatus;
onStartMilestone: () => void;
}

export const ReleasePlanMilestoneStatus = ({
status,
onStartMilestone,
}: IReleasePlanMilestoneStatusProps) => {
const statusText =
status === 'active'
? 'Running'
: status === 'paused'
? 'Paused (disabled in environment)'
: status === 'completed'
? 'Restart'
: 'Start';

return (
<StyledStatus status={status}>
{status === 'active' ? (
<TripOriginIcon />
) : status === 'paused' ? (
<PauseCircleIcon />
) : (
<PlayCircleIcon />
)}
{status === 'not-started' || status === 'completed' ? (
<Link
onClick={(e) => {
e.stopPropagation();
onStartMilestone();
}}
>
{statusText}
</Link>
) : (
<span>{statusText}</span>
)}
</StyledStatus>
);
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Box, styled } from '@mui/material';
import { StrategyExecution } from '../FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyExecution';
import { StrategyExecution } from '../../FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyExecution';
import SplitPreviewSlider from 'component/feature/StrategyTypes/SplitPreviewSlider/SplitPreviewSlider';
import {
formatStrategyName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ import { ReactComponent as ReleaseTemplateIcon } from 'assets/img/releaseTemplat
import { styled, Typography } from '@mui/material';
import { ReleasePlanTemplateCardMenu } from './ReleasePlanTemplateCardMenu';
import { useNavigate } from 'react-router-dom';
import { UserAvatar } from 'component/common/UserAvatar/UserAvatar';
import useUserInfo from 'hooks/api/getters/useUserInfo/useUserInfo';

const StyledTemplateCard = styled('aside')(({ theme }) => ({
height: '100%',
cursor: 'pointer',
'&:hover': {
transition: 'background-color 0.2s ease-in-out',
backgroundColor: theme.palette.neutral.light,
Expand Down Expand Up @@ -45,6 +48,12 @@ const StyledCreatedBy = styled(Typography)(({ theme }) => ({
display: 'flex',
alignItems: 'center',
marginRight: 'auto',
gap: theme.spacing(1),
}));

const StyledCreatedByAvatar = styled(UserAvatar)(({ theme }) => ({
width: theme.spacing(3),
height: theme.spacing(3),
}));

const StyledMenu = styled('div')(({ theme }) => ({
Expand All @@ -63,6 +72,7 @@ export const ReleasePlanTemplateCard = ({
const onClick = () => {
navigate(`/release-management/edit/${template.id}`);
};
const { user: createdBy } = useUserInfo(`${template.createdByUserId}`);

return (
<StyledTemplateCard onClick={onClick}>
Expand All @@ -75,7 +85,7 @@ export const ReleasePlanTemplateCard = ({
<div>{template.name}</div>
<StyledDiv>
<StyledCreatedBy>
Created by {template.createdByUserId}
Created by <StyledCreatedByAvatar user={createdBy} />
</StyledCreatedBy>
<StyledMenu
onClick={(e) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export const CreateReleasePlanTemplate = () => {
type: 'success',
title: 'Release plan template created',
});
navigate(`/release-management/edit/${template.id}`);
navigate('/release-management');
} catch (error: unknown) {
setToastApiError(formatUnknownError(error));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export const EditReleasePlanTemplate = () => {
type: 'success',
title: 'Release plan template updated',
});
navigate('/release-management');
} catch (error: unknown) {
setToastApiError(formatUnknownError(error));
}
Expand Down

0 comments on commit f75cf1d

Please sign in to comment.