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={
|
||||
<>
|
||||
{releasePlans.map((plan) => (
|
||||
<ReleasePlan key={plan.id} plan={plan} />
|
||||
<ReleasePlan
|
||||
key={plan.id}
|
||||
plan={plan}
|
||||
environmentIsDisabled={isDisabled}
|
||||
/>
|
||||
))}
|
||||
<ConditionallyRender
|
||||
condition={
|
||||
|
@ -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={
|
||||
|
@ -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 }) => ({
|
||||
@ -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,
|
||||
@ -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
|
||||
@ -168,7 +165,9 @@ export const ReleasePlan = ({ plan }: IReleasePlanProps) => {
|
||||
milestone={milestone}
|
||||
status={
|
||||
milestone.id === activeMilestoneId
|
||||
? 'active'
|
||||
? environmentIsDisabled
|
||||
? 'paused'
|
||||
: 'active'
|
||||
: index < activeIndex
|
||||
? 'completed'
|
||||
: 'not-started'
|
||||
|
@ -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',
|
||||
@ -31,6 +30,7 @@ const StyledAccordionSummary = styled(AccordionSummary)({
|
||||
'& .MuiAccordionSummary-content': {
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
minHeight: '30px',
|
||||
},
|
||||
});
|
||||
|
||||
@ -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,
|
||||
@ -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) => (
|
@ -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 { 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,
|
@ -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,
|
||||
@ -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 }) => ({
|
||||
@ -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}>
|
||||
@ -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) => {
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -68,6 +68,7 @@ export const EditReleasePlanTemplate = () => {
|
||||
type: 'success',
|
||||
title: 'Release plan template updated',
|
||||
});
|
||||
navigate('/release-management');
|
||||
} catch (error: unknown) {
|
||||
setToastApiError(formatUnknownError(error));
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user