From f75cf1dc606f59e2308a11c5da9a743b15b2756a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nuno=20G=C3=B3is?= Date: Thu, 28 Nov 2024 15:18:27 +0000 Subject: [PATCH] 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. ![image](https://github.com/user-attachments/assets/b0717dc6-3049-4612-9b46-f37a4fa887a3) ![image](https://github.com/user-attachments/assets/a17daafa-f961-4269-9522-39769912752c) --- .../EnvironmentAccordionBody.tsx | 6 +- .../FeatureOverviewEnvironmentBody.tsx | 6 +- .../ReleasePlan/ReleasePlan.tsx | 31 ++++--- .../ReleasePlanMilestone.tsx | 90 +++++++------------ .../ReleasePlanMilestoneStatus.tsx | 78 ++++++++++++++++ .../ReleasePlanMilestoneStrategy.tsx | 2 +- .../ReleasePlanTemplateCard.tsx | 12 ++- .../CreateReleasePlanTemplate.tsx | 2 +- .../EditReleasePlanTemplate.tsx | 1 + 9 files changed, 147 insertions(+), 81 deletions(-) rename frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/{ => ReleasePlanMilestone}/ReleasePlanMilestone.tsx (55%) create mode 100644 frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlanMilestone/ReleasePlanMilestoneStatus.tsx rename frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/{ => ReleasePlanMilestone}/ReleasePlanMilestoneStrategy.tsx (90%) diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/EnvironmentAccordionBody.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/EnvironmentAccordionBody.tsx index 4c3a94ce86..26b630d5df 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/EnvironmentAccordionBody.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/EnvironmentAccordionBody.tsx @@ -234,7 +234,11 @@ const EnvironmentAccordionBody = ({ show={ <> {releasePlans.map((plan) => ( - + ))} {releasePlans.map((plan) => ( - + ))} 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 ( - - + + 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' diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlanMilestone.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlanMilestone/ReleasePlanMilestone.tsx similarity index 55% rename from frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlanMilestone.tsx rename to frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlanMilestone/ReleasePlanMilestone.tsx index 3213ce37f9..4013ee9776 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlanMilestone.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlanMilestone/ReleasePlanMilestone.tsx @@ -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 ( + + + + {milestone.name} + onStartMilestone(milestone)} + /> + + No strategies + + + ); + } return ( }> {milestone.name} - - } - elseShow={} - /> - {statusText}} - elseShow={ - { - e.stopPropagation(); - onStartMilestone(milestone); - }} - > - {statusText} - - } - /> - + onStartMilestone(milestone)} + /> - View strategies + + {milestone.strategies.length === 1 + ? 'View strategy' + : `View ${milestone.strategies.length} strategies`} + {milestone.strategies.map((strategy, index) => ( diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlanMilestone/ReleasePlanMilestoneStatus.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlanMilestone/ReleasePlanMilestoneStatus.tsx new file mode 100644 index 0000000000..ccfc191258 --- /dev/null +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlanMilestone/ReleasePlanMilestoneStatus.tsx @@ -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 ( + + {status === 'active' ? ( + + ) : status === 'paused' ? ( + + ) : ( + + )} + {status === 'not-started' || status === 'completed' ? ( + { + e.stopPropagation(); + onStartMilestone(); + }} + > + {statusText} + + ) : ( + {statusText} + )} + + ); +}; diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlanMilestoneStrategy.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlanMilestone/ReleasePlanMilestoneStrategy.tsx similarity index 90% rename from frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlanMilestoneStrategy.tsx rename to frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlanMilestone/ReleasePlanMilestoneStrategy.tsx index 7ce6d6421a..0f421af5ec 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlanMilestoneStrategy.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlanMilestone/ReleasePlanMilestoneStrategy.tsx @@ -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, diff --git a/frontend/src/component/releases/ReleaseManagement/ReleasePlanTemplateCard.tsx b/frontend/src/component/releases/ReleaseManagement/ReleasePlanTemplateCard.tsx index a0ab28934c..00a658b5a1 100644 --- a/frontend/src/component/releases/ReleaseManagement/ReleasePlanTemplateCard.tsx +++ b/frontend/src/component/releases/ReleaseManagement/ReleasePlanTemplateCard.tsx @@ -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 ( @@ -75,7 +85,7 @@ export const ReleasePlanTemplateCard = ({
{template.name}
- Created by {template.createdByUserId} + Created by { diff --git a/frontend/src/component/releases/ReleasePlanTemplate/CreateReleasePlanTemplate.tsx b/frontend/src/component/releases/ReleasePlanTemplate/CreateReleasePlanTemplate.tsx index 621bc11630..5b89933805 100644 --- a/frontend/src/component/releases/ReleasePlanTemplate/CreateReleasePlanTemplate.tsx +++ b/frontend/src/component/releases/ReleasePlanTemplate/CreateReleasePlanTemplate.tsx @@ -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)); } diff --git a/frontend/src/component/releases/ReleasePlanTemplate/EditReleasePlanTemplate.tsx b/frontend/src/component/releases/ReleasePlanTemplate/EditReleasePlanTemplate.tsx index 87113926d7..622fd7961d 100644 --- a/frontend/src/component/releases/ReleasePlanTemplate/EditReleasePlanTemplate.tsx +++ b/frontend/src/component/releases/ReleasePlanTemplate/EditReleasePlanTemplate.tsx @@ -68,6 +68,7 @@ export const EditReleasePlanTemplate = () => { type: 'success', title: 'Release plan template updated', }); + navigate('/release-management'); } catch (error: unknown) { setToastApiError(formatUnknownError(error)); }