1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-09-24 17:51:14 +02:00

chore: new strategy modal design

This commit is contained in:
Nuno Góis 2025-09-08 16:08:14 +01:00
parent 0a7b295cc8
commit 38b1937bc6
No known key found for this signature in database
GPG Key ID: 71ECC689F1091765

View File

@ -1,68 +1,51 @@
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_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),
@ -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,70 @@ export const FeatureStrategyMenuCards = ({
return null; return null;
} }
const slicedTemplates = seeAllReleaseTemplates
? templates
: templates.slice(0, RELEASE_TEMPLATE_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_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 +236,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 +275,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>
); );
}; };