From b8cdd1d004e645585a41209d105eeadded4a21c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nuno=20G=C3=B3is?= Date: Mon, 8 Sep 2025 12:16:27 +0100 Subject: [PATCH] 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". image image --- .../FeatureStrategyMenu.tsx | 155 +++++---- .../FeatureStrategyMenuCards.tsx | 4 +- .../LegacyFeatureStrategyMenuCards.tsx | 294 ++++++++++++++++++ frontend/src/interfaces/uiConfig.ts | 1 + src/lib/types/experimental.ts | 7 +- src/server-dev.ts | 1 + 6 files changed, 402 insertions(+), 60 deletions(-) create mode 100644 frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/LegacyFeatureStrategyMenuCards/LegacyFeatureStrategyMenuCards.tsx diff --git a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenu.tsx b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenu.tsx index 7dc3234e9a..ec6a774d71 100644 --- a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenu.tsx +++ b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenu.tsx @@ -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 ( event.stopPropagation()}> - <> - {displayReleasePlanButton ? ( - - Use template - - ) : null} - + {newStrategyModalEnabled ? ( - {label} + Add strategy + ) : ( + <> + {displayReleasePlanButton ? ( + + Use template + + ) : null} - - - - + + {label} + + + + + + + )} - { - setSelectedTemplate(template); - addReleasePlan(template); - }} - onReviewReleasePlan={(template) => { - setSelectedTemplate(template); - setAddReleasePlanOpen(true); - onClose(); - }} - onClose={onClose} - /> + {newStrategyModalEnabled ? ( + { + setSelectedTemplate(template); + addReleasePlan(template); + }} + onReviewReleasePlan={(template) => { + setSelectedTemplate(template); + setAddReleasePlanOpen(true); + onClose(); + }} + onClose={onClose} + /> + ) : ( + { + setSelectedTemplate(template); + addReleasePlan(template); + }} + onReviewReleasePlan={(template) => { + setSelectedTemplate(template); + setAddReleasePlanOpen(true); + onClose(); + }} + onClose={onClose} + /> + )} {selectedTemplate && ( - - {onlyReleasePlans ? 'Select template' : 'Add configuration'} - + Add strategy 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 ( + + + + 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) + } + /> + + ))} + + )} + + ); + }; + + return ( + + + + {onlyReleasePlans ? 'Select template' : 'Add configuration'} + + + + + + + {onlyReleasePlans ? ( + renderReleasePlanTemplates() + ) : ( + <> + + + + Standard strategies + + + + + + + + {standardStrategies.map((strategy) => ( + + + + ))} + + + {renderReleasePlanTemplates()} + {advancedAndCustomStrategies.length > 0 && ( + + + + Custom and advanced strategies + + + + + {advancedAndCustomStrategies.map( + (strategy) => ( + + + + ), + )} + + + )} + + )} + + + ); +}; diff --git a/frontend/src/interfaces/uiConfig.ts b/frontend/src/interfaces/uiConfig.ts index 9ebe862f5d..beb3ef3d4f 100644 --- a/frontend/src/interfaces/uiConfig.ts +++ b/frontend/src/interfaces/uiConfig.ts @@ -87,6 +87,7 @@ export type UiFlags = { customMetrics?: boolean; impactMetrics?: boolean; lifecycleGraphs?: boolean; + newStrategyModal?: boolean; }; export interface IVersionInfo { diff --git a/src/lib/types/experimental.ts b/src/lib/types/experimental.ts index d68f04504a..1be94c3e81 100644 --- a/src/lib/types/experimental.ts +++ b/src/lib/types/experimental.ts @@ -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 = { diff --git a/src/server-dev.ts b/src/server-dev.ts index 1abf6b507d..0de0fff9e6 100644 --- a/src/server-dev.ts +++ b/src/server-dev.ts @@ -53,6 +53,7 @@ process.nextTick(async () => { customMetrics: true, impactMetrics: true, lifecycleGraphs: true, + newStrategyModal: true, }, }, authentication: {