From 2cd81359881b00be1448ab52db91cee9ddae17a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nuno=20G=C3=B3is?= Date: Tue, 9 Sep 2025 10:57:06 +0100 Subject: [PATCH] chore: new add strategy modal design (#10633) https://linear.app/unleash/issue/2-3870/new-modal-design New "add strategy" modal base design. We're still missing a few details, like the new card design and the filters at the top, but those will come in follow-up PRs. ### New layout image ### Default strategy tooltip, with a link to the default strategy settings image ### Default strategy with a custom title image --- .../FeatureStrategyMenuCards.tsx | 343 ++++++++++-------- 1 file changed, 194 insertions(+), 149 deletions(-) diff --git a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenuCards/FeatureStrategyMenuCards.tsx b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenuCards/FeatureStrategyMenuCards.tsx index 81535adc13..4f50059a1c 100644 --- a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenuCards/FeatureStrategyMenuCards.tsx +++ b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenuCards/FeatureStrategyMenuCards.tsx @@ -1,72 +1,55 @@ -import { Link, styled, Typography, Box, IconButton } from '@mui/material'; +import { styled, Typography, Box, IconButton, Button } from '@mui/material'; import { useStrategies } from 'hooks/api/getters/useStrategies/useStrategies'; import { FeatureStrategyMenuCard } from '../FeatureStrategyMenuCard/FeatureStrategyMenuCard.tsx'; import { useReleasePlanTemplates } from 'hooks/api/getters/useReleasePlanTemplates/useReleasePlanTemplates'; import { FeatureReleasePlanCard } from '../FeatureReleasePlanCard/FeatureReleasePlanCard.tsx'; import type { IReleasePlanTemplate } from 'interfaces/releasePlans'; -import { useNavigate } from 'react-router-dom'; +import { Link as RouterLink } from 'react-router-dom'; import CloseIcon from '@mui/icons-material/Close'; import FactCheckOutlinedIcon from '@mui/icons-material/FactCheckOutlined'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig.ts'; import { HelpIcon } from 'component/common/HelpIcon/HelpIcon.tsx'; +import useProjectOverview from 'hooks/api/getters/useProjectOverview/useProjectOverview.ts'; +import { useState } from 'react'; -interface IFeatureStrategyMenuCardsProps { - projectId: string; - featureId: string; - environmentId: string; - onlyReleasePlans: boolean; - onAddReleasePlan: (template: IReleasePlanTemplate) => void; - onReviewReleasePlan: (template: IReleasePlanTemplate) => void; - onClose: () => void; -} +const RELEASE_TEMPLATE_DISPLAY_LIMIT = 5; -const GridContainer = styled(Box)(() => ({ +const StyledContainer = styled(Box)(() => ({ width: '100%', display: 'flex', flexDirection: 'column', })); -const ScrollableContent = styled(Box)(({ theme }) => ({ +const StyledScrollableContent = styled(Box)(({ theme }) => ({ width: '100%', - maxHeight: '70vh', + maxHeight: theme.spacing(62), overflowY: 'auto', padding: theme.spacing(4), - paddingTop: 0, + paddingTop: theme.spacing(1), display: 'flex', flexDirection: 'column', - gap: theme.spacing(3), + gap: theme.spacing(5), })); -const GridSection = styled(Box)(({ theme }) => ({ +const StyledGrid = styled(Box)(({ theme }) => ({ display: 'grid', - gridTemplateColumns: 'repeat(2, 1fr)', - gap: theme.spacing(1.5), + gridTemplateColumns: 'repeat(3, 1fr)', + gap: theme.spacing(2), width: '100%', })); -const CardWrapper = styled(Box)(() => ({ - width: '100%', - minWidth: 0, -})); - -const TitleRow = styled(Box)(({ theme }) => ({ +const StyledHeader = styled(Box)(({ theme }) => ({ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: theme.spacing(4, 4, 2, 4), })); -const TitleText = styled(Typography)(({ theme }) => ({ - fontSize: theme.typography.body1.fontSize, - fontWeight: theme.typography.fontWeightBold, - margin: 0, -})); - -const SectionTitle = styled(Box)(({ theme }) => ({ +const StyledSectionHeader = styled(Box)(({ theme }) => ({ display: 'flex', alignItems: 'center', gap: theme.spacing(0.5), - marginBottom: theme.spacing(1), + marginBottom: theme.spacing(0.5), width: '100%', })); @@ -81,7 +64,7 @@ const StyledIcon = styled('span')(({ theme }) => ({ alignItems: 'center', })); -const EmptyStateContainer = styled(Box)(({ theme }) => ({ +const StyledNoTemplatesContainer = styled(Box)(({ theme }) => ({ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', @@ -92,7 +75,7 @@ const EmptyStateContainer = styled(Box)(({ theme }) => ({ width: 'auto', })); -const EmptyStateTitle = styled(Typography)(({ theme }) => ({ +const StyledNoTemplatesTitle = styled(Typography)(({ theme }) => ({ fontSize: theme.typography.caption.fontSize, fontWeight: theme.typography.fontWeightBold, marginBottom: theme.spacing(1), @@ -100,24 +83,39 @@ const EmptyStateTitle = styled(Typography)(({ theme }) => ({ alignItems: 'center', })); -const EmptyStateDescription = styled(Typography)(({ theme }) => ({ +const StyledNoTemplatesDescription = styled(Typography)(({ theme }) => ({ fontSize: theme.typography.caption.fontSize, color: theme.palette.text.secondary, })); -const ClickableBoldText = styled(Link)(({ theme }) => ({ - fontWeight: theme.typography.fontWeightBold, - cursor: 'pointer', +const StyledLink = styled(RouterLink)({ + textDecoration: 'none', '&:hover': { textDecoration: 'underline', }, -})); +}); + +const StyledViewAllTemplates = styled(Box)({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + height: '100%', +}); + +interface IFeatureStrategyMenuCardsProps { + projectId: string; + featureId: string; + environmentId: string; + onlyReleasePlans: boolean; + onAddReleasePlan: (template: IReleasePlanTemplate) => void; + onReviewReleasePlan: (template: IReleasePlanTemplate) => void; + onClose: () => void; +} export const FeatureStrategyMenuCards = ({ projectId, featureId, environmentId, - onlyReleasePlans, onAddReleasePlan, onReviewReleasePlan, onClose, @@ -126,7 +124,9 @@ export const FeatureStrategyMenuCards = ({ const { strategies } = useStrategies(); const { templates } = useReleasePlanTemplates(); - const navigate = useNavigate(); + const { project } = useProjectOverview(projectId); + + const [seeAllReleaseTemplates, setSeeAllReleaseTemplates] = useState(false); const activeStrategies = strategies.filter( (strategy) => !strategy.deprecated, @@ -136,14 +136,26 @@ export const FeatureStrategyMenuCards = ({ (strategy) => !strategy.advanced && !strategy.editable, ); - const advancedAndCustomStrategies = activeStrategies.filter( - (strategy) => strategy.editable || strategy.advanced, + const advancedStrategies = activeStrategies.filter( + (strategy) => strategy.advanced && !strategy.editable, ); - const defaultStrategy = { + const customStrategies = activeStrategies.filter( + (strategy) => strategy.editable, + ); + + const projectDefaultStrategy = project?.environments?.find( + (env) => env.environment === environmentId, + )?.defaultStrategy || { name: 'flexibleRollout', + title: '100% of all users', + }; + + const defaultStrategy = { + name: projectDefaultStrategy.name || 'flexibleRollout', displayName: 'Default strategy', description: + projectDefaultStrategy.title || 'This is the default strategy defined for this environment in the project', }; @@ -152,59 +164,71 @@ export const FeatureStrategyMenuCards = ({ return null; } + const slicedTemplates = seeAllReleaseTemplates + ? templates + : templates.slice(0, RELEASE_TEMPLATE_DISPLAY_LIMIT); + return ( - + Release templates - - + {!templates.length ? ( - - + + Create your own release templates - - + + Standardize your rollouts and save time by reusing predefined strategies. Find release templates in the side menu under{' '} - navigate('/release-templates')} - > + Configure > Release templates - - - + + + ) : ( - - {templates.map((template) => ( - - onAddReleasePlan(template)} - onPreviewClick={() => - onReviewReleasePlan(template) - } - /> - + + {slicedTemplates.map((template) => ( + onAddReleasePlan(template)} + onPreviewClick={() => + onReviewReleasePlan(template) + } + /> ))} - + {slicedTemplates.length < templates.length && + templates.length > + RELEASE_TEMPLATE_DISPLAY_LIMIT && ( + + + + )} + )} ); }; return ( - - - Add strategy + + + Add strategy - - - {onlyReleasePlans ? ( - renderReleasePlanTemplates() - ) : ( - <> - - - - Standard strategies - - + + + + + + Project default + + + This is set per project, per + environment, and can be configured{' '} + + here + + + } + size='16px' + /> + + + + Standard strategies + + + + + + {standardStrategies.map((strategy) => ( + + ))} + + + {renderReleasePlanTemplates()} + {advancedStrategies.length > 0 && ( + + + + Advanced strategies + + + + {advancedStrategies.map((strategy) => ( + - - - - - - {standardStrategies.map((strategy) => ( - - - - ))} - - - {renderReleasePlanTemplates()} - {advancedAndCustomStrategies.length > 0 && ( - - - - Custom and advanced strategies - - - - - {advancedAndCustomStrategies.map( - (strategy) => ( - - - - ), - )} - - - )} - + ))} + + )} - - + {customStrategies.length > 0 && ( + + + + Custom strategies + + + + {customStrategies.map((strategy) => ( + + ))} + + + )} + + ); };