1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-09-19 17:52:45 +02:00

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

<img width="986" height="597" alt="image"
src="https://github.com/user-attachments/assets/2bc83117-97a8-4b59-a187-73ef9a7d0469"
/>

### Default strategy tooltip, with a link to the default strategy
settings

<img width="326" height="119" alt="image"
src="https://github.com/user-attachments/assets/45da60cb-088b-4b6e-bead-f9ef13a2540b"
/>

### Default strategy with a custom title

<img width="318" height="163" alt="image"
src="https://github.com/user-attachments/assets/9c9862db-f21d-409f-8dc1-63673f00ba2e"
/>
This commit is contained in:
Nuno Góis 2025-09-09 10:57:06 +01:00 committed by GitHub
parent edaea80f0c
commit 2cd8135988
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -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 { useStrategies } from 'hooks/api/getters/useStrategies/useStrategies';
import { FeatureStrategyMenuCard } from '../FeatureStrategyMenuCard/FeatureStrategyMenuCard.tsx'; import { FeatureStrategyMenuCard } from '../FeatureStrategyMenuCard/FeatureStrategyMenuCard.tsx';
import { useReleasePlanTemplates } from 'hooks/api/getters/useReleasePlanTemplates/useReleasePlanTemplates'; import { useReleasePlanTemplates } from 'hooks/api/getters/useReleasePlanTemplates/useReleasePlanTemplates';
import { FeatureReleasePlanCard } from '../FeatureReleasePlanCard/FeatureReleasePlanCard.tsx'; import { FeatureReleasePlanCard } from '../FeatureReleasePlanCard/FeatureReleasePlanCard.tsx';
import type { IReleasePlanTemplate } from 'interfaces/releasePlans'; 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 CloseIcon from '@mui/icons-material/Close';
import FactCheckOutlinedIcon from '@mui/icons-material/FactCheckOutlined'; import FactCheckOutlinedIcon from '@mui/icons-material/FactCheckOutlined';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig.ts'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig.ts';
import { HelpIcon } from 'component/common/HelpIcon/HelpIcon.tsx'; import { HelpIcon } from 'component/common/HelpIcon/HelpIcon.tsx';
import useProjectOverview from 'hooks/api/getters/useProjectOverview/useProjectOverview.ts';
import { useState } from 'react';
interface IFeatureStrategyMenuCardsProps { const RELEASE_TEMPLATE_DISPLAY_LIMIT = 5;
projectId: string;
featureId: string;
environmentId: string;
onlyReleasePlans: boolean;
onAddReleasePlan: (template: IReleasePlanTemplate) => void;
onReviewReleasePlan: (template: IReleasePlanTemplate) => void;
onClose: () => void;
}
const GridContainer = styled(Box)(() => ({ const StyledContainer = styled(Box)(() => ({
width: '100%', width: '100%',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
})); }));
const ScrollableContent = styled(Box)(({ theme }) => ({ const StyledScrollableContent = styled(Box)(({ theme }) => ({
width: '100%', width: '100%',
maxHeight: '70vh', maxHeight: theme.spacing(62),
overflowY: 'auto', overflowY: 'auto',
padding: theme.spacing(4), padding: theme.spacing(4),
paddingTop: 0, paddingTop: theme.spacing(1),
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
gap: theme.spacing(3), gap: theme.spacing(5),
})); }));
const GridSection = styled(Box)(({ theme }) => ({ const StyledGrid = styled(Box)(({ theme }) => ({
display: 'grid', display: 'grid',
gridTemplateColumns: 'repeat(2, 1fr)', gridTemplateColumns: 'repeat(3, 1fr)',
gap: theme.spacing(1.5), gap: theme.spacing(2),
width: '100%', width: '100%',
})); }));
const CardWrapper = styled(Box)(() => ({ const StyledHeader = styled(Box)(({ theme }) => ({
width: '100%',
minWidth: 0,
}));
const TitleRow = styled(Box)(({ theme }) => ({
display: 'flex', display: 'flex',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
padding: theme.spacing(4, 4, 2, 4), padding: theme.spacing(4, 4, 2, 4),
})); }));
const TitleText = styled(Typography)(({ theme }) => ({ const StyledSectionHeader = styled(Box)(({ theme }) => ({
fontSize: theme.typography.body1.fontSize,
fontWeight: theme.typography.fontWeightBold,
margin: 0,
}));
const SectionTitle = styled(Box)(({ theme }) => ({
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: theme.spacing(0.5), gap: theme.spacing(0.5),
marginBottom: theme.spacing(1), marginBottom: theme.spacing(0.5),
width: '100%', width: '100%',
})); }));
@ -81,7 +64,7 @@ const StyledIcon = styled('span')(({ theme }) => ({
alignItems: 'center', alignItems: 'center',
})); }));
const EmptyStateContainer = styled(Box)(({ theme }) => ({ const StyledNoTemplatesContainer = styled(Box)(({ theme }) => ({
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
alignItems: 'flex-start', alignItems: 'flex-start',
@ -92,7 +75,7 @@ const EmptyStateContainer = styled(Box)(({ theme }) => ({
width: 'auto', width: 'auto',
})); }));
const EmptyStateTitle = styled(Typography)(({ theme }) => ({ const StyledNoTemplatesTitle = styled(Typography)(({ theme }) => ({
fontSize: theme.typography.caption.fontSize, fontSize: theme.typography.caption.fontSize,
fontWeight: theme.typography.fontWeightBold, fontWeight: theme.typography.fontWeightBold,
marginBottom: theme.spacing(1), marginBottom: theme.spacing(1),
@ -100,24 +83,39 @@ const EmptyStateTitle = styled(Typography)(({ theme }) => ({
alignItems: 'center', alignItems: 'center',
})); }));
const EmptyStateDescription = styled(Typography)(({ theme }) => ({ const StyledNoTemplatesDescription = styled(Typography)(({ theme }) => ({
fontSize: theme.typography.caption.fontSize, fontSize: theme.typography.caption.fontSize,
color: theme.palette.text.secondary, color: theme.palette.text.secondary,
})); }));
const ClickableBoldText = styled(Link)(({ theme }) => ({ const StyledLink = styled(RouterLink)({
fontWeight: theme.typography.fontWeightBold, textDecoration: 'none',
cursor: 'pointer',
'&:hover': { '&:hover': {
textDecoration: 'underline', 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 = ({ export const FeatureStrategyMenuCards = ({
projectId, projectId,
featureId, featureId,
environmentId, environmentId,
onlyReleasePlans,
onAddReleasePlan, onAddReleasePlan,
onReviewReleasePlan, onReviewReleasePlan,
onClose, onClose,
@ -126,7 +124,9 @@ export const FeatureStrategyMenuCards = ({
const { strategies } = useStrategies(); const { strategies } = useStrategies();
const { templates } = useReleasePlanTemplates(); const { templates } = useReleasePlanTemplates();
const navigate = useNavigate(); const { project } = useProjectOverview(projectId);
const [seeAllReleaseTemplates, setSeeAllReleaseTemplates] = useState(false);
const activeStrategies = strategies.filter( const activeStrategies = strategies.filter(
(strategy) => !strategy.deprecated, (strategy) => !strategy.deprecated,
@ -136,14 +136,26 @@ export const FeatureStrategyMenuCards = ({
(strategy) => !strategy.advanced && !strategy.editable, (strategy) => !strategy.advanced && !strategy.editable,
); );
const advancedAndCustomStrategies = activeStrategies.filter( const advancedStrategies = activeStrategies.filter(
(strategy) => strategy.editable || strategy.advanced, (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', name: 'flexibleRollout',
title: '100% of all users',
};
const defaultStrategy = {
name: projectDefaultStrategy.name || 'flexibleRollout',
displayName: 'Default strategy', displayName: 'Default strategy',
description: description:
projectDefaultStrategy.title ||
'This is the default strategy defined for this environment in the project', 'This is the default strategy defined for this environment in the project',
}; };
@ -152,59 +164,71 @@ export const FeatureStrategyMenuCards = ({
return null; return null;
} }
const slicedTemplates = seeAllReleaseTemplates
? templates
: templates.slice(0, RELEASE_TEMPLATE_DISPLAY_LIMIT);
return ( return (
<Box> <Box>
<SectionTitle> <StyledSectionHeader>
<Typography color='inherit' variant='body2'> <Typography color='inherit' variant='body2'>
Release templates Release templates
</Typography> </Typography>
<HelpIcon </StyledSectionHeader>
tooltip='Use a predefined template to roll out features to users'
size='16px'
/>
</SectionTitle>
{!templates.length ? ( {!templates.length ? (
<EmptyStateContainer> <StyledNoTemplatesContainer>
<EmptyStateTitle> <StyledNoTemplatesTitle>
<StyledIcon> <StyledIcon>
<FactCheckOutlinedIcon /> <FactCheckOutlinedIcon />
</StyledIcon> </StyledIcon>
Create your own release templates Create your own release templates
</EmptyStateTitle> </StyledNoTemplatesTitle>
<EmptyStateDescription> <StyledNoTemplatesDescription>
Standardize your rollouts and save time by reusing Standardize your rollouts and save time by reusing
predefined strategies. Find release templates in the predefined strategies. Find release templates in the
side menu under{' '} side menu under{' '}
<ClickableBoldText <StyledLink to='/release-templates'>
onClick={() => navigate('/release-templates')}
>
Configure &gt; Release templates Configure &gt; Release templates
</ClickableBoldText> </StyledLink>
</EmptyStateDescription> </StyledNoTemplatesDescription>
</EmptyStateContainer> </StyledNoTemplatesContainer>
) : ( ) : (
<GridSection> <StyledGrid>
{templates.map((template) => ( {slicedTemplates.map((template) => (
<CardWrapper key={template.id}>
<FeatureReleasePlanCard <FeatureReleasePlanCard
key={template.id}
template={template} template={template}
onClick={() => onAddReleasePlan(template)} onClick={() => onAddReleasePlan(template)}
onPreviewClick={() => onPreviewClick={() =>
onReviewReleasePlan(template) onReviewReleasePlan(template)
} }
/> />
</CardWrapper>
))} ))}
</GridSection> {slicedTemplates.length < templates.length &&
templates.length >
RELEASE_TEMPLATE_DISPLAY_LIMIT && (
<StyledViewAllTemplates>
<Button
variant='text'
size='small'
onClick={() =>
setSeeAllReleaseTemplates(true)
}
>
View all available templates
</Button>
</StyledViewAllTemplates>
)}
</StyledGrid>
)} )}
</Box> </Box>
); );
}; };
return ( return (
<GridContainer> <StyledContainer>
<TitleRow> <StyledHeader>
<TitleText variant='h2'>Add strategy</TitleText> <Typography variant='h2'>Add strategy</Typography>
<IconButton <IconButton
size='small' size='small'
onClick={onClose} onClick={onClose}
@ -213,24 +237,37 @@ export const FeatureStrategyMenuCards = ({
> >
<CloseIcon fontSize='small' /> <CloseIcon fontSize='small' />
</IconButton> </IconButton>
</TitleRow> </StyledHeader>
<ScrollableContent> <StyledScrollableContent>
{onlyReleasePlans ? (
renderReleasePlanTemplates()
) : (
<>
<Box> <Box>
<SectionTitle> <StyledGrid>
<StyledSectionHeader>
<Typography color='inherit' variant='body2'>
Project default
</Typography>
<HelpIcon
htmlTooltip
tooltip={
<>
This is set per project, per
environment, and can be configured{' '}
<StyledLink
to={`/projects/${projectId}/settings/default-strategy`}
>
here
</StyledLink>
</>
}
size='16px'
/>
</StyledSectionHeader>
<StyledSectionHeader>
<Typography color='inherit' variant='body2'> <Typography color='inherit' variant='body2'>
Standard strategies Standard strategies
</Typography> </Typography>
<HelpIcon </StyledSectionHeader>
tooltip='Standard strategies let you enable a feature only for a specified audience. Select a starting setup, then customize your strategy with targeting and variants.' </StyledGrid>
size='16px' <StyledGrid>
/>
</SectionTitle>
<GridSection>
<CardWrapper key={defaultStrategy.name}>
<FeatureStrategyMenuCard <FeatureStrategyMenuCard
projectId={projectId} projectId={projectId}
featureId={featureId} featureId={featureId}
@ -239,54 +276,62 @@ export const FeatureStrategyMenuCards = ({
defaultStrategy defaultStrategy
onClose={onClose} onClose={onClose}
/> />
</CardWrapper>
{standardStrategies.map((strategy) => ( {standardStrategies.map((strategy) => (
<CardWrapper key={strategy.name}>
<FeatureStrategyMenuCard <FeatureStrategyMenuCard
key={strategy.name}
projectId={projectId} projectId={projectId}
featureId={featureId} featureId={featureId}
environmentId={environmentId} environmentId={environmentId}
strategy={strategy} strategy={strategy}
onClose={onClose} onClose={onClose}
/> />
</CardWrapper>
))} ))}
</GridSection> </StyledGrid>
</Box> </Box>
{renderReleasePlanTemplates()} {renderReleasePlanTemplates()}
{advancedAndCustomStrategies.length > 0 && ( {advancedStrategies.length > 0 && (
<Box> <Box>
<SectionTitle> <StyledSectionHeader>
<Typography color='inherit' variant='body2'> <Typography color='inherit' variant='body2'>
Custom and advanced strategies Advanced strategies
</Typography> </Typography>
<HelpIcon </StyledSectionHeader>
tooltip='Advanced strategies let you target based on specific properties. Custom activation strategies let you define your own activation strategies to use with Unleash.' <StyledGrid>
size='16px' {advancedStrategies.map((strategy) => (
/>
</SectionTitle>
<GridSection>
{advancedAndCustomStrategies.map(
(strategy) => (
<CardWrapper key={strategy.name}>
<FeatureStrategyMenuCard <FeatureStrategyMenuCard
key={strategy.name}
projectId={projectId} projectId={projectId}
featureId={featureId} featureId={featureId}
environmentId={ environmentId={environmentId}
environmentId
}
strategy={strategy} strategy={strategy}
onClose={onClose} onClose={onClose}
/> />
</CardWrapper> ))}
), </StyledGrid>
)}
</GridSection>
</Box> </Box>
)} )}
</> {customStrategies.length > 0 && (
<Box>
<StyledSectionHeader>
<Typography color='inherit' variant='body2'>
Custom strategies
</Typography>
</StyledSectionHeader>
<StyledGrid>
{customStrategies.map((strategy) => (
<FeatureStrategyMenuCard
key={strategy.name}
projectId={projectId}
featureId={featureId}
environmentId={environmentId}
strategy={strategy}
onClose={onClose}
/>
))}
</StyledGrid>
</Box>
)} )}
</ScrollableContent> </StyledScrollableContent>
</GridContainer> </StyledContainer>
); );
}; };