mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-23 00:22:19 +01:00
chore: release plans small misc improvements (#8879)
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.  
This commit is contained in:
parent
8d1ebf6527
commit
f75cf1dc60
@ -234,7 +234,11 @@ const EnvironmentAccordionBody = ({
|
|||||||
show={
|
show={
|
||||||
<>
|
<>
|
||||||
{releasePlans.map((plan) => (
|
{releasePlans.map((plan) => (
|
||||||
<ReleasePlan key={plan.id} plan={plan} />
|
<ReleasePlan
|
||||||
|
key={plan.id}
|
||||||
|
plan={plan}
|
||||||
|
environmentIsDisabled={isDisabled}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={
|
condition={
|
||||||
|
@ -252,7 +252,11 @@ export const FeatureOverviewEnvironmentBody = ({
|
|||||||
show={
|
show={
|
||||||
<>
|
<>
|
||||||
{releasePlans.map((plan) => (
|
{releasePlans.map((plan) => (
|
||||||
<ReleasePlan key={plan.id} plan={plan} />
|
<ReleasePlan
|
||||||
|
key={plan.id}
|
||||||
|
plan={plan}
|
||||||
|
environmentIsDisabled={isDisabled}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={
|
condition={
|
||||||
|
@ -13,29 +13,23 @@ import type {
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
import { ReleasePlanRemoveDialog } from './ReleasePlanRemoveDialog';
|
import { ReleasePlanRemoveDialog } from './ReleasePlanRemoveDialog';
|
||||||
import { ReleasePlanMilestone } from './ReleasePlanMilestone';
|
import { ReleasePlanMilestone } from './ReleasePlanMilestone/ReleasePlanMilestone';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
|
||||||
const StyledContainer = styled('div', {
|
const StyledContainer = styled('div')(({ theme }) => ({
|
||||||
shouldForwardProp: (prop) => prop !== 'disabled',
|
|
||||||
})<{ disabled?: boolean }>(({ theme, disabled }) => ({
|
|
||||||
padding: theme.spacing(2),
|
padding: theme.spacing(2),
|
||||||
borderRadius: theme.shape.borderRadiusMedium,
|
borderRadius: theme.shape.borderRadiusMedium,
|
||||||
border: `1px solid ${theme.palette.divider}`,
|
border: `1px solid ${theme.palette.divider}`,
|
||||||
'& + &': {
|
'& + &': {
|
||||||
marginTop: theme.spacing(2),
|
marginTop: theme.spacing(2),
|
||||||
},
|
},
|
||||||
background: disabled
|
background: theme.palette.background.paper,
|
||||||
? theme.palette.envAccordion.disabled
|
|
||||||
: theme.palette.background.paper,
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledHeader = styled('div', {
|
const StyledHeader = styled('div')(({ theme }) => ({
|
||||||
shouldForwardProp: (prop) => prop !== 'disabled',
|
|
||||||
})<{ disabled?: boolean }>(({ theme, disabled }) => ({
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
color: disabled ? theme.palette.text.secondary : theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledHeaderTitleContainer = styled('div')(({ theme }) => ({
|
const StyledHeaderTitleContainer = styled('div')(({ theme }) => ({
|
||||||
@ -73,9 +67,13 @@ const StyledConnection = styled('div')(({ theme }) => ({
|
|||||||
|
|
||||||
interface IReleasePlanProps {
|
interface IReleasePlanProps {
|
||||||
plan: IReleasePlan;
|
plan: IReleasePlan;
|
||||||
|
environmentIsDisabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ReleasePlan = ({ plan }: IReleasePlanProps) => {
|
export const ReleasePlan = ({
|
||||||
|
plan,
|
||||||
|
environmentIsDisabled,
|
||||||
|
}: IReleasePlanProps) => {
|
||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
@ -132,14 +130,13 @@ export const ReleasePlan = ({ plan }: IReleasePlanProps) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const disabled = !activeMilestoneId;
|
|
||||||
const activeIndex = milestones.findIndex(
|
const activeIndex = milestones.findIndex(
|
||||||
(milestone) => milestone.id === activeMilestoneId,
|
(milestone) => milestone.id === activeMilestoneId,
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer disabled={disabled}>
|
<StyledContainer>
|
||||||
<StyledHeader disabled={disabled}>
|
<StyledHeader>
|
||||||
<StyledHeaderTitleContainer>
|
<StyledHeaderTitleContainer>
|
||||||
<StyledHeaderTitleLabel>
|
<StyledHeaderTitleLabel>
|
||||||
Release plan
|
Release plan
|
||||||
@ -168,7 +165,9 @@ export const ReleasePlan = ({ plan }: IReleasePlanProps) => {
|
|||||||
milestone={milestone}
|
milestone={milestone}
|
||||||
status={
|
status={
|
||||||
milestone.id === activeMilestoneId
|
milestone.id === activeMilestoneId
|
||||||
? 'active'
|
? environmentIsDisabled
|
||||||
|
? 'paused'
|
||||||
|
: 'active'
|
||||||
: index < activeIndex
|
: index < activeIndex
|
||||||
? 'completed'
|
? 'completed'
|
||||||
: 'not-started'
|
: 'not-started'
|
||||||
|
@ -3,17 +3,16 @@ import {
|
|||||||
Accordion,
|
Accordion,
|
||||||
AccordionDetails,
|
AccordionDetails,
|
||||||
AccordionSummary,
|
AccordionSummary,
|
||||||
Link,
|
|
||||||
styled,
|
styled,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import PlayCircleIcon from '@mui/icons-material/PlayCircle';
|
|
||||||
import TripOriginIcon from '@mui/icons-material/TripOrigin';
|
|
||||||
import type { IReleasePlanMilestone } from 'interfaces/releasePlans';
|
import type { IReleasePlanMilestone } from 'interfaces/releasePlans';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { ReleasePlanMilestoneStrategy } from './ReleasePlanMilestoneStrategy';
|
import { ReleasePlanMilestoneStrategy } from './ReleasePlanMilestoneStrategy';
|
||||||
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator';
|
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator';
|
||||||
|
import {
|
||||||
type MilestoneStatus = 'not-started' | 'active' | 'completed';
|
ReleasePlanMilestoneStatus,
|
||||||
|
type MilestoneStatus,
|
||||||
|
} from './ReleasePlanMilestoneStatus';
|
||||||
|
|
||||||
const StyledAccordion = styled(Accordion, {
|
const StyledAccordion = styled(Accordion, {
|
||||||
shouldForwardProp: (prop) => prop !== 'status',
|
shouldForwardProp: (prop) => prop !== 'status',
|
||||||
@ -31,6 +30,7 @@ const StyledAccordionSummary = styled(AccordionSummary)({
|
|||||||
'& .MuiAccordionSummary-content': {
|
'& .MuiAccordionSummary-content': {
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
minHeight: '30px',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -45,33 +45,6 @@ const StyledTitle = styled('span')(({ theme }) => ({
|
|||||||
fontWeight: theme.fontWeight.bold,
|
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 }) => ({
|
const StyledSecondaryLabel = styled('span')(({ theme }) => ({
|
||||||
color: theme.palette.text.secondary,
|
color: theme.palette.text.secondary,
|
||||||
fontSize: theme.fontSizes.smallBody,
|
fontSize: theme.fontSizes.smallBody,
|
||||||
@ -94,41 +67,38 @@ export const ReleasePlanMilestone = ({
|
|||||||
status,
|
status,
|
||||||
onStartMilestone,
|
onStartMilestone,
|
||||||
}: IReleasePlanMilestoneProps) => {
|
}: IReleasePlanMilestoneProps) => {
|
||||||
const statusText =
|
if (!milestone.strategies.length) {
|
||||||
status === 'active'
|
return (
|
||||||
? 'Running'
|
<StyledAccordion status={status}>
|
||||||
: status === 'completed'
|
<StyledAccordionSummary>
|
||||||
? 'Restart'
|
<StyledTitleContainer>
|
||||||
: 'Start';
|
<StyledTitle>{milestone.name}</StyledTitle>
|
||||||
|
<ReleasePlanMilestoneStatus
|
||||||
|
status={status}
|
||||||
|
onStartMilestone={() => onStartMilestone(milestone)}
|
||||||
|
/>
|
||||||
|
</StyledTitleContainer>
|
||||||
|
<StyledSecondaryLabel>No strategies</StyledSecondaryLabel>
|
||||||
|
</StyledAccordionSummary>
|
||||||
|
</StyledAccordion>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledAccordion status={status}>
|
<StyledAccordion status={status}>
|
||||||
<StyledAccordionSummary expandIcon={<ExpandMore />}>
|
<StyledAccordionSummary expandIcon={<ExpandMore />}>
|
||||||
<StyledTitleContainer>
|
<StyledTitleContainer>
|
||||||
<StyledTitle>{milestone.name}</StyledTitle>
|
<StyledTitle>{milestone.name}</StyledTitle>
|
||||||
<StyledStatus status={status}>
|
<ReleasePlanMilestoneStatus
|
||||||
<ConditionallyRender
|
status={status}
|
||||||
condition={status === 'active'}
|
onStartMilestone={() => onStartMilestone(milestone)}
|
||||||
show={<TripOriginIcon />}
|
/>
|
||||||
elseShow={<PlayCircleIcon />}
|
|
||||||
/>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={status === 'active'}
|
|
||||||
show={<span>{statusText}</span>}
|
|
||||||
elseShow={
|
|
||||||
<Link
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onStartMilestone(milestone);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{statusText}
|
|
||||||
</Link>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</StyledStatus>
|
|
||||||
</StyledTitleContainer>
|
</StyledTitleContainer>
|
||||||
<StyledSecondaryLabel>View strategies</StyledSecondaryLabel>
|
<StyledSecondaryLabel>
|
||||||
|
{milestone.strategies.length === 1
|
||||||
|
? 'View strategy'
|
||||||
|
: `View ${milestone.strategies.length} strategies`}
|
||||||
|
</StyledSecondaryLabel>
|
||||||
</StyledAccordionSummary>
|
</StyledAccordionSummary>
|
||||||
<StyledAccordionDetails>
|
<StyledAccordionDetails>
|
||||||
{milestone.strategies.map((strategy, index) => (
|
{milestone.strategies.map((strategy, index) => (
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -1,5 +1,5 @@
|
|||||||
import { Box, styled } from '@mui/material';
|
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 SplitPreviewSlider from 'component/feature/StrategyTypes/SplitPreviewSlider/SplitPreviewSlider';
|
||||||
import {
|
import {
|
||||||
formatStrategyName,
|
formatStrategyName,
|
@ -3,9 +3,12 @@ import { ReactComponent as ReleaseTemplateIcon } from 'assets/img/releaseTemplat
|
|||||||
import { styled, Typography } from '@mui/material';
|
import { styled, Typography } from '@mui/material';
|
||||||
import { ReleasePlanTemplateCardMenu } from './ReleasePlanTemplateCardMenu';
|
import { ReleasePlanTemplateCardMenu } from './ReleasePlanTemplateCardMenu';
|
||||||
import { useNavigate } from 'react-router-dom';
|
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 }) => ({
|
const StyledTemplateCard = styled('aside')(({ theme }) => ({
|
||||||
height: '100%',
|
height: '100%',
|
||||||
|
cursor: 'pointer',
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
transition: 'background-color 0.2s ease-in-out',
|
transition: 'background-color 0.2s ease-in-out',
|
||||||
backgroundColor: theme.palette.neutral.light,
|
backgroundColor: theme.palette.neutral.light,
|
||||||
@ -45,6 +48,12 @@ const StyledCreatedBy = styled(Typography)(({ theme }) => ({
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginRight: 'auto',
|
marginRight: 'auto',
|
||||||
|
gap: theme.spacing(1),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledCreatedByAvatar = styled(UserAvatar)(({ theme }) => ({
|
||||||
|
width: theme.spacing(3),
|
||||||
|
height: theme.spacing(3),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledMenu = styled('div')(({ theme }) => ({
|
const StyledMenu = styled('div')(({ theme }) => ({
|
||||||
@ -63,6 +72,7 @@ export const ReleasePlanTemplateCard = ({
|
|||||||
const onClick = () => {
|
const onClick = () => {
|
||||||
navigate(`/release-management/edit/${template.id}`);
|
navigate(`/release-management/edit/${template.id}`);
|
||||||
};
|
};
|
||||||
|
const { user: createdBy } = useUserInfo(`${template.createdByUserId}`);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledTemplateCard onClick={onClick}>
|
<StyledTemplateCard onClick={onClick}>
|
||||||
@ -75,7 +85,7 @@ export const ReleasePlanTemplateCard = ({
|
|||||||
<div>{template.name}</div>
|
<div>{template.name}</div>
|
||||||
<StyledDiv>
|
<StyledDiv>
|
||||||
<StyledCreatedBy>
|
<StyledCreatedBy>
|
||||||
Created by {template.createdByUserId}
|
Created by <StyledCreatedByAvatar user={createdBy} />
|
||||||
</StyledCreatedBy>
|
</StyledCreatedBy>
|
||||||
<StyledMenu
|
<StyledMenu
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
@ -61,7 +61,7 @@ export const CreateReleasePlanTemplate = () => {
|
|||||||
type: 'success',
|
type: 'success',
|
||||||
title: 'Release plan template created',
|
title: 'Release plan template created',
|
||||||
});
|
});
|
||||||
navigate(`/release-management/edit/${template.id}`);
|
navigate('/release-management');
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
setToastApiError(formatUnknownError(error));
|
setToastApiError(formatUnknownError(error));
|
||||||
}
|
}
|
||||||
|
@ -68,6 +68,7 @@ export const EditReleasePlanTemplate = () => {
|
|||||||
type: 'success',
|
type: 'success',
|
||||||
title: 'Release plan template updated',
|
title: 'Release plan template updated',
|
||||||
});
|
});
|
||||||
|
navigate('/release-management');
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
setToastApiError(formatUnknownError(error));
|
setToastApiError(formatUnknownError(error));
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user