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

chore: add newStrategyModal flag (#10629)

https://linear.app/unleash/issue/2-3865/add-newstrategymodal-feature-flag

Adds `newStrategyModal` feature flag.

The approach here is to duplicate the existing
`FeatureStrategyMenuCards` into a `LegacyFeatureStrategyMenuCards`.
We'll continue working on the `FeatureStrategyMenuCards` component while
leaving `LegacyFeatureStrategyMenuCards` untouched. Once we're done with
the implementation and remove the flag we can drop the legacy file.

I think it's easier to reduce our add strategy buttons to a single one
right away (like we did with the `addConfiguration` flag in
https://github.com/Unleash/unleash/pull/10420). This allows us to focus
on the end result instead of having to implement things like "clicking
the 'add release template' button should show the modal filtered to only
release templates".

<img width="735" height="126" alt="image"
src="https://github.com/user-attachments/assets/6d10fab2-d091-40f3-9c36-05a6f28f7dda"
/>

<img width="995" height="742" alt="image"
src="https://github.com/user-attachments/assets/a0fb9366-89b5-44e1-a684-47ee30d6d36c"
/>
This commit is contained in:
Nuno Góis 2025-09-08 12:16:27 +01:00 committed by GitHub
parent 9540ed6e3d
commit b8cdd1d004
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 402 additions and 60 deletions

View File

@ -6,7 +6,7 @@ import PermissionButton, {
} from 'component/common/PermissionButton/PermissionButton';
import { CREATE_FEATURE_STRATEGY } from 'component/providers/AccessProvider/permissions';
import { Dialog, styled } from '@mui/material';
import { FeatureStrategyMenuCards } from './FeatureStrategyMenuCards/FeatureStrategyMenuCards.tsx';
import { LegacyFeatureStrategyMenuCards } from './LegacyFeatureStrategyMenuCards/LegacyFeatureStrategyMenuCards.tsx';
import { formatCreateStrategyPath } from '../FeatureStrategyCreate/FeatureStrategyCreate.tsx';
import MoreVert from '@mui/icons-material/MoreVert';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
@ -20,6 +20,8 @@ import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
import { formatUnknownError } from 'utils/formatUnknownError';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { ReleasePlanReviewDialog } from '../../FeatureView/FeatureOverview/ReleasePlan/ReleasePlanReviewDialog.tsx';
import { FeatureStrategyMenuCards } from './FeatureStrategyMenuCards/FeatureStrategyMenuCards.tsx';
import { useUiFlag } from 'hooks/useUiFlag.ts';
interface IFeatureStrategyMenuProps {
label: string;
@ -77,6 +79,7 @@ export const FeatureStrategyMenu = ({
const { isEnterprise } = useUiConfig();
const displayReleasePlanButton = isEnterprise();
const crProtected = isChangeRequestConfigured(environmentId);
const newStrategyModalEnabled = useUiFlag('newStrategyModal');
const onClose = () => {
setIsStrategyMenuDialogOpen(false);
@ -158,6 +161,24 @@ export const FeatureStrategyMenu = ({
return (
<StyledStrategyMenu onClick={(event) => event.stopPropagation()}>
{newStrategyModalEnabled ? (
<PermissionButton
data-testid='ADD_STRATEGY_BUTTON'
permission={CREATE_FEATURE_STRATEGY}
projectId={projectId}
environmentId={environmentId}
onClick={openMoreStrategies}
aria-labelledby={dialogId}
variant={variant}
sx={{ minWidth: matchWidth ? '282px' : 'auto' }}
disabled={Boolean(disableReason)}
tooltipProps={{
title: disableReason ? disableReason : undefined,
}}
>
Add strategy
</PermissionButton>
) : (
<>
{displayReleasePlanButton ? (
<PermissionButton
@ -171,7 +192,9 @@ export const FeatureStrategyMenu = ({
sx={{ minWidth: matchWidth ? '282px' : 'auto' }}
disabled={Boolean(disableReason)}
tooltipProps={{
title: disableReason ? disableReason : undefined,
title: disableReason
? disableReason
: undefined,
}}
>
Use template
@ -212,6 +235,7 @@ export const FeatureStrategyMenu = ({
<MoreVert />
</StyledAdditionalMenuButton>
</>
)}
<Dialog
open={isStrategyMenuDialogOpen}
onClose={onClose}
@ -222,6 +246,7 @@ export const FeatureStrategyMenu = ({
},
}}
>
{newStrategyModalEnabled ? (
<FeatureStrategyMenuCards
projectId={projectId}
featureId={featureId}
@ -238,6 +263,24 @@ export const FeatureStrategyMenu = ({
}}
onClose={onClose}
/>
) : (
<LegacyFeatureStrategyMenuCards
projectId={projectId}
featureId={featureId}
environmentId={environmentId}
onlyReleasePlans={onlyReleasePlans}
onAddReleasePlan={(template) => {
setSelectedTemplate(template);
addReleasePlan(template);
}}
onReviewReleasePlan={(template) => {
setSelectedTemplate(template);
setAddReleasePlanOpen(true);
onClose();
}}
onClose={onClose}
/>
)}
</Dialog>
{selectedTemplate && (
<ReleasePlanReviewDialog

View File

@ -204,9 +204,7 @@ export const FeatureStrategyMenuCards = ({
return (
<GridContainer>
<TitleRow>
<TitleText variant='h2'>
{onlyReleasePlans ? 'Select template' : 'Add configuration'}
</TitleText>
<TitleText variant='h2'>Add strategy</TitleText>
<IconButton
size='small'
onClick={onClose}

View File

@ -0,0 +1,294 @@
import { Link, styled, Typography, Box, IconButton } 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 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';
interface IFeatureStrategyMenuCardsProps {
projectId: string;
featureId: string;
environmentId: string;
onlyReleasePlans: boolean;
onAddReleasePlan: (template: IReleasePlanTemplate) => void;
onReviewReleasePlan: (template: IReleasePlanTemplate) => void;
onClose: () => void;
}
const GridContainer = styled(Box)(() => ({
width: '100%',
display: 'flex',
flexDirection: 'column',
}));
const ScrollableContent = styled(Box)(({ theme }) => ({
width: '100%',
maxHeight: '70vh',
overflowY: 'auto',
padding: theme.spacing(4),
paddingTop: 0,
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(3),
}));
const GridSection = styled(Box)(({ theme }) => ({
display: 'grid',
gridTemplateColumns: 'repeat(2, 1fr)',
gap: theme.spacing(1.5),
width: '100%',
}));
const CardWrapper = styled(Box)(() => ({
width: '100%',
minWidth: 0,
}));
const TitleRow = 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 }) => ({
display: 'flex',
alignItems: 'center',
gap: theme.spacing(0.5),
marginBottom: theme.spacing(1),
width: '100%',
}));
const StyledIcon = styled('span')(({ theme }) => ({
width: theme.spacing(3),
'& > svg': {
fill: theme.palette.primary.main,
width: theme.spacing(2.25),
height: theme.spacing(2.25),
},
display: 'flex',
alignItems: 'center',
}));
const EmptyStateContainer = styled(Box)(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
justifyContent: 'flex-start',
backgroundColor: theme.palette.neutral.light,
borderRadius: theme.shape.borderRadiusMedium,
padding: theme.spacing(3),
width: 'auto',
}));
const EmptyStateTitle = styled(Typography)(({ theme }) => ({
fontSize: theme.typography.caption.fontSize,
fontWeight: theme.typography.fontWeightBold,
marginBottom: theme.spacing(1),
display: 'flex',
alignItems: 'center',
}));
const EmptyStateDescription = styled(Typography)(({ theme }) => ({
fontSize: theme.typography.caption.fontSize,
color: theme.palette.text.secondary,
}));
const ClickableBoldText = styled(Link)(({ theme }) => ({
fontWeight: theme.typography.fontWeightBold,
cursor: 'pointer',
'&:hover': {
textDecoration: 'underline',
},
}));
export const LegacyFeatureStrategyMenuCards = ({
projectId,
featureId,
environmentId,
onlyReleasePlans,
onAddReleasePlan,
onReviewReleasePlan,
onClose,
}: IFeatureStrategyMenuCardsProps) => {
const { isEnterprise } = useUiConfig();
const { strategies } = useStrategies();
const { templates } = useReleasePlanTemplates();
const navigate = useNavigate();
const activeStrategies = strategies.filter(
(strategy) => !strategy.deprecated,
);
const standardStrategies = activeStrategies.filter(
(strategy) => !strategy.advanced && !strategy.editable,
);
const advancedAndCustomStrategies = activeStrategies.filter(
(strategy) => strategy.editable || strategy.advanced,
);
const defaultStrategy = {
name: 'flexibleRollout',
displayName: 'Default strategy',
description:
'This is the default strategy defined for this environment in the project',
};
const renderReleasePlanTemplates = () => {
if (!isEnterprise()) {
return null;
}
return (
<Box>
<SectionTitle>
<Typography color='inherit' variant='body2'>
Release templates
</Typography>
<HelpIcon
tooltip='Use a predefined template to roll out features to users'
size='16px'
/>
</SectionTitle>
{!templates.length ? (
<EmptyStateContainer>
<EmptyStateTitle>
<StyledIcon>
<FactCheckOutlinedIcon />
</StyledIcon>
Create your own release templates
</EmptyStateTitle>
<EmptyStateDescription>
Standardize your rollouts and save time by reusing
predefined strategies. Find release templates in the
side menu under{' '}
<ClickableBoldText
onClick={() => navigate('/release-templates')}
>
Configure &gt; Release templates
</ClickableBoldText>
</EmptyStateDescription>
</EmptyStateContainer>
) : (
<GridSection>
{templates.map((template) => (
<CardWrapper key={template.id}>
<FeatureReleasePlanCard
template={template}
onClick={() => onAddReleasePlan(template)}
onPreviewClick={() =>
onReviewReleasePlan(template)
}
/>
</CardWrapper>
))}
</GridSection>
)}
</Box>
);
};
return (
<GridContainer>
<TitleRow>
<TitleText variant='h2'>
{onlyReleasePlans ? 'Select template' : 'Add configuration'}
</TitleText>
<IconButton
size='small'
onClick={onClose}
edge='end'
aria-label='close'
>
<CloseIcon fontSize='small' />
</IconButton>
</TitleRow>
<ScrollableContent>
{onlyReleasePlans ? (
renderReleasePlanTemplates()
) : (
<>
<Box>
<SectionTitle>
<Typography color='inherit' variant='body2'>
Standard strategies
</Typography>
<HelpIcon
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.'
size='16px'
/>
</SectionTitle>
<GridSection>
<CardWrapper key={defaultStrategy.name}>
<FeatureStrategyMenuCard
projectId={projectId}
featureId={featureId}
environmentId={environmentId}
strategy={defaultStrategy}
defaultStrategy
onClose={onClose}
/>
</CardWrapper>
{standardStrategies.map((strategy) => (
<CardWrapper key={strategy.name}>
<FeatureStrategyMenuCard
projectId={projectId}
featureId={featureId}
environmentId={environmentId}
strategy={strategy}
onClose={onClose}
/>
</CardWrapper>
))}
</GridSection>
</Box>
{renderReleasePlanTemplates()}
{advancedAndCustomStrategies.length > 0 && (
<Box>
<SectionTitle>
<Typography color='inherit' variant='body2'>
Custom and advanced strategies
</Typography>
<HelpIcon
tooltip='Advanced strategies let you target based on specific properties. Custom activation strategies let you define your own activation strategies to use with Unleash.'
size='16px'
/>
</SectionTitle>
<GridSection>
{advancedAndCustomStrategies.map(
(strategy) => (
<CardWrapper key={strategy.name}>
<FeatureStrategyMenuCard
projectId={projectId}
featureId={featureId}
environmentId={
environmentId
}
strategy={strategy}
onClose={onClose}
/>
</CardWrapper>
),
)}
</GridSection>
</Box>
)}
</>
)}
</ScrollableContent>
</GridContainer>
);
};

View File

@ -87,6 +87,7 @@ export type UiFlags = {
customMetrics?: boolean;
impactMetrics?: boolean;
lifecycleGraphs?: boolean;
newStrategyModal?: boolean;
};
export interface IVersionInfo {

View File

@ -57,7 +57,8 @@ export type IFlagKey =
| 'lifecycleGraphs'
| 'etagByEnv'
| 'fetchMode'
| 'optimizeLifecycle';
| 'optimizeLifecycle'
| 'newStrategyModal';
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
@ -263,6 +264,10 @@ const flags: IFlags = {
false,
),
},
newStrategyModal: parseEnvVarBoolean(
process.env.UNLEASH_EXPERIMENTAL_NEW_STRATEGY_MODAL,
false,
),
};
export const defaultExperimentalOptions: IExperimentalOptions = {

View File

@ -53,6 +53,7 @@ process.nextTick(async () => {
customMetrics: true,
impactMetrics: true,
lifecycleGraphs: true,
newStrategyModal: true,
},
},
authentication: {