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: {