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:
parent
9540ed6e3d
commit
b8cdd1d004
@ -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,32 +161,13 @@ export const FeatureStrategyMenu = ({
|
||||
|
||||
return (
|
||||
<StyledStrategyMenu onClick={(event) => event.stopPropagation()}>
|
||||
<>
|
||||
{displayReleasePlanButton ? (
|
||||
<PermissionButton
|
||||
data-testid='ADD_TEMPLATE_BUTTON'
|
||||
permission={CREATE_FEATURE_STRATEGY}
|
||||
projectId={projectId}
|
||||
environmentId={environmentId}
|
||||
onClick={openReleasePlans}
|
||||
aria-labelledby={dialogId}
|
||||
variant='outlined'
|
||||
sx={{ minWidth: matchWidth ? '282px' : 'auto' }}
|
||||
disabled={Boolean(disableReason)}
|
||||
tooltipProps={{
|
||||
title: disableReason ? disableReason : undefined,
|
||||
}}
|
||||
>
|
||||
Use template
|
||||
</PermissionButton>
|
||||
) : null}
|
||||
|
||||
{newStrategyModalEnabled ? (
|
||||
<PermissionButton
|
||||
data-testid='ADD_STRATEGY_BUTTON'
|
||||
permission={CREATE_FEATURE_STRATEGY}
|
||||
projectId={projectId}
|
||||
environmentId={environmentId}
|
||||
onClick={openDefaultStrategyCreationModal}
|
||||
onClick={openMoreStrategies}
|
||||
aria-labelledby={dialogId}
|
||||
variant={variant}
|
||||
sx={{ minWidth: matchWidth ? '282px' : 'auto' }}
|
||||
@ -192,26 +176,66 @@ export const FeatureStrategyMenu = ({
|
||||
title: disableReason ? disableReason : undefined,
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
Add strategy
|
||||
</PermissionButton>
|
||||
) : (
|
||||
<>
|
||||
{displayReleasePlanButton ? (
|
||||
<PermissionButton
|
||||
data-testid='ADD_TEMPLATE_BUTTON'
|
||||
permission={CREATE_FEATURE_STRATEGY}
|
||||
projectId={projectId}
|
||||
environmentId={environmentId}
|
||||
onClick={openReleasePlans}
|
||||
aria-labelledby={dialogId}
|
||||
variant='outlined'
|
||||
sx={{ minWidth: matchWidth ? '282px' : 'auto' }}
|
||||
disabled={Boolean(disableReason)}
|
||||
tooltipProps={{
|
||||
title: disableReason
|
||||
? disableReason
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
Use template
|
||||
</PermissionButton>
|
||||
) : null}
|
||||
|
||||
<StyledAdditionalMenuButton
|
||||
permission={CREATE_FEATURE_STRATEGY}
|
||||
projectId={projectId}
|
||||
environmentId={environmentId}
|
||||
onClick={openMoreStrategies}
|
||||
variant='outlined'
|
||||
hideLockIcon
|
||||
disabled={Boolean(disableReason)}
|
||||
tooltipProps={{
|
||||
title: disableReason
|
||||
? disableReason
|
||||
: 'More strategies',
|
||||
}}
|
||||
>
|
||||
<MoreVert />
|
||||
</StyledAdditionalMenuButton>
|
||||
</>
|
||||
<PermissionButton
|
||||
data-testid='ADD_STRATEGY_BUTTON'
|
||||
permission={CREATE_FEATURE_STRATEGY}
|
||||
projectId={projectId}
|
||||
environmentId={environmentId}
|
||||
onClick={openDefaultStrategyCreationModal}
|
||||
aria-labelledby={dialogId}
|
||||
variant={variant}
|
||||
sx={{ minWidth: matchWidth ? '282px' : 'auto' }}
|
||||
disabled={Boolean(disableReason)}
|
||||
tooltipProps={{
|
||||
title: disableReason ? disableReason : undefined,
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</PermissionButton>
|
||||
|
||||
<StyledAdditionalMenuButton
|
||||
permission={CREATE_FEATURE_STRATEGY}
|
||||
projectId={projectId}
|
||||
environmentId={environmentId}
|
||||
onClick={openMoreStrategies}
|
||||
variant='outlined'
|
||||
hideLockIcon
|
||||
disabled={Boolean(disableReason)}
|
||||
tooltipProps={{
|
||||
title: disableReason
|
||||
? disableReason
|
||||
: 'More strategies',
|
||||
}}
|
||||
>
|
||||
<MoreVert />
|
||||
</StyledAdditionalMenuButton>
|
||||
</>
|
||||
)}
|
||||
<Dialog
|
||||
open={isStrategyMenuDialogOpen}
|
||||
onClose={onClose}
|
||||
@ -222,22 +246,41 @@ export const FeatureStrategyMenu = ({
|
||||
},
|
||||
}}
|
||||
>
|
||||
<FeatureStrategyMenuCards
|
||||
projectId={projectId}
|
||||
featureId={featureId}
|
||||
environmentId={environmentId}
|
||||
onlyReleasePlans={onlyReleasePlans}
|
||||
onAddReleasePlan={(template) => {
|
||||
setSelectedTemplate(template);
|
||||
addReleasePlan(template);
|
||||
}}
|
||||
onReviewReleasePlan={(template) => {
|
||||
setSelectedTemplate(template);
|
||||
setAddReleasePlanOpen(true);
|
||||
onClose();
|
||||
}}
|
||||
onClose={onClose}
|
||||
/>
|
||||
{newStrategyModalEnabled ? (
|
||||
<FeatureStrategyMenuCards
|
||||
projectId={projectId}
|
||||
featureId={featureId}
|
||||
environmentId={environmentId}
|
||||
onlyReleasePlans={onlyReleasePlans}
|
||||
onAddReleasePlan={(template) => {
|
||||
setSelectedTemplate(template);
|
||||
addReleasePlan(template);
|
||||
}}
|
||||
onReviewReleasePlan={(template) => {
|
||||
setSelectedTemplate(template);
|
||||
setAddReleasePlanOpen(true);
|
||||
onClose();
|
||||
}}
|
||||
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
|
||||
|
@ -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}
|
||||
|
@ -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 > 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>
|
||||
);
|
||||
};
|
@ -87,6 +87,7 @@ export type UiFlags = {
|
||||
customMetrics?: boolean;
|
||||
impactMetrics?: boolean;
|
||||
lifecycleGraphs?: boolean;
|
||||
newStrategyModal?: boolean;
|
||||
};
|
||||
|
||||
export interface IVersionInfo {
|
||||
|
@ -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 = {
|
||||
|
@ -53,6 +53,7 @@ process.nextTick(async () => {
|
||||
customMetrics: true,
|
||||
impactMetrics: true,
|
||||
lifecycleGraphs: true,
|
||||
newStrategyModal: true,
|
||||
},
|
||||
},
|
||||
authentication: {
|
||||
|
Loading…
Reference in New Issue
Block a user