diff --git a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureReleasePlanCard/FeatureReleasePlanCard.tsx b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureReleasePlanCard/FeatureReleasePlanCard.tsx index 18bacfaade..e2abeca4b9 100644 --- a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureReleasePlanCard/FeatureReleasePlanCard.tsx +++ b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureReleasePlanCard/FeatureReleasePlanCard.tsx @@ -2,6 +2,7 @@ import { getFeatureStrategyIcon } from 'utils/strategyNames'; import StringTruncator from 'component/common/StringTruncator/StringTruncator'; import { Button, styled } from '@mui/material'; import type { IReleasePlanTemplate } from 'interfaces/releasePlans'; +import { Truncator } from 'component/common/Truncator/Truncator'; const StyledIcon = styled('div')(({ theme }) => ({ width: theme.spacing(4), @@ -16,19 +17,22 @@ const StyledIcon = styled('div')(({ theme }) => ({ }, })); -const StyledDescription = styled('div')(({ theme }) => ({ - fontSize: theme.fontSizes.smallBody, - fontWeight: theme.fontWeight.medium, +const StyledContentContainer = styled('div')(() => ({ + overflow: 'hidden', + width: '100%', })); const StyledName = styled(StringTruncator)(({ theme }) => ({ - fontWeight: theme.fontWeight.bold, + fontWeight: theme.typography.fontWeightBold, + display: 'block', + marginBottom: theme.spacing(0.5), })); const StyledCard = styled(Button)(({ theme }) => ({ display: 'grid', - gridTemplateColumns: '3rem 1fr', - width: '20rem', + gridTemplateColumns: '2.5rem 1fr', + width: '100%', + maxWidth: '30rem', padding: theme.spacing(2), color: 'inherit', textDecoration: 'inherit', @@ -38,6 +42,7 @@ const StyledCard = styled(Button)(({ theme }) => ({ borderColor: theme.palette.divider, borderRadius: theme.spacing(1), textAlign: 'left', + overflow: 'hidden', '&:hover, &:focus': { borderColor: theme.palette.primary.main, }, @@ -59,10 +64,22 @@ export const FeatureReleasePlanCard = ({ -
+ - {description} -
+ theme.typography.body2.fontSize, + fontWeight: (theme) => + theme.typography.fontWeightRegular, + width: '100%', + }} + > + {description} + + ); }; diff --git a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureReleasePlanCard/OldFeatureReleasePlanCard.tsx b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureReleasePlanCard/OldFeatureReleasePlanCard.tsx new file mode 100644 index 0000000000..0395ca5b86 --- /dev/null +++ b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureReleasePlanCard/OldFeatureReleasePlanCard.tsx @@ -0,0 +1,68 @@ +import { getFeatureStrategyIcon } from 'utils/strategyNames'; +import StringTruncator from 'component/common/StringTruncator/StringTruncator'; +import { Button, styled } from '@mui/material'; +import type { IReleasePlanTemplate } from 'interfaces/releasePlans'; + +const StyledIcon = styled('div')(({ theme }) => ({ + width: theme.spacing(4), + height: 'auto', + '& > svg': { + fill: theme.palette.primary.main, + }, + '& > div': { + height: theme.spacing(2), + marginLeft: '-.75rem', + color: theme.palette.primary.main, + }, +})); + +const StyledDescription = styled('div')(({ theme }) => ({ + fontSize: theme.fontSizes.smallBody, + fontWeight: theme.fontWeight.medium, +})); + +const StyledName = styled(StringTruncator)(({ theme }) => ({ + fontWeight: theme.fontWeight.bold, +})); + +const StyledCard = styled(Button)(({ theme }) => ({ + display: 'grid', + gridTemplateColumns: '3rem 1fr', + width: '20rem', + padding: theme.spacing(2), + color: 'inherit', + textDecoration: 'inherit', + lineHeight: 1.25, + borderWidth: '1px', + borderStyle: 'solid', + borderColor: theme.palette.divider, + borderRadius: theme.spacing(1), + textAlign: 'left', + '&:hover, &:focus': { + borderColor: theme.palette.primary.main, + }, +})); + +interface IFeatureReleasePlanCardProps { + template: IReleasePlanTemplate; + onClick: () => void; +} + +export const OldFeatureReleasePlanCard = ({ + template: { name, description }, + onClick, +}: IFeatureReleasePlanCardProps) => { + const Icon = getFeatureStrategyIcon('releasePlanTemplate'); + + return ( + + + + +
+ + {description} +
+
+ ); +}; diff --git a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenu.tsx b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenu.tsx index 1ff5c49f6d..8157b4b19a 100644 --- a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenu.tsx +++ b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenu.tsx @@ -21,6 +21,7 @@ import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled'; import { formatUnknownError } from 'utils/formatUnknownError'; import { useUiFlag } from 'hooks/useUiFlag'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; +import { OldFeatureStrategyMenuCards } from './FeatureStrategyMenuCards/OldFeatureStrategyMenuCards'; interface IFeatureStrategyMenuProps { label: string; @@ -75,6 +76,7 @@ export const FeatureStrategyMenu = ({ const { addReleasePlanToFeature } = useReleasePlansApi(); const { isOss } = useUiConfig(); const releasePlansEnabled = useUiFlag('releasePlans'); + const newStrategyDropdownEnabled = useUiFlag('newStrategyDropdown'); const displayReleasePlanButton = !isOss() && releasePlansEnabled; const crProtected = releasePlansEnabled && isChangeRequestConfigured(environmentId); @@ -223,19 +225,36 @@ export const FeatureStrategyMenu = ({ PaperProps={{ sx: (theme) => ({ paddingBottom: theme.spacing(1), + width: 'auto', + maxWidth: '95vw', + overflow: 'hidden', }), }} > - { - setSelectedTemplate(template); - setAddReleasePlanOpen(true); - }} - /> + {newStrategyDropdownEnabled ? ( + { + setSelectedTemplate(template); + setAddReleasePlanOpen(true); + }} + onClose={onClose} + /> + ) : ( + { + setSelectedTemplate(template); + setAddReleasePlanOpen(true); + }} + /> + )} {selectedTemplate && ( ({ }, })); -const StyledDescription = styled('div')(({ theme }) => ({ - fontSize: theme.fontSizes.smallBody, +const StyledContentContainer = styled('div')(() => ({ + overflow: 'hidden', + width: '100%', })); const StyledName = styled(StringTruncator)(({ theme }) => ({ - fontWeight: theme.fontWeight.bold, + fontWeight: theme.typography.fontWeightBold, + display: 'block', + marginBottom: theme.spacing(0.5), })); const StyledCard = styled(Link)(({ theme }) => ({ display: 'grid', - gridTemplateColumns: '3rem 1fr', - width: '20rem', + gridTemplateColumns: '2.5rem 1fr', + width: '100%', + maxWidth: '30rem', padding: theme.spacing(2), color: 'inherit', textDecoration: 'inherit', @@ -53,6 +58,7 @@ const StyledCard = styled(Link)(({ theme }) => ({ borderStyle: 'solid', borderColor: theme.palette.divider, borderRadius: theme.spacing(1), + overflow: 'hidden', '&:hover, &:focus': { borderColor: theme.palette.primary.main, }, @@ -90,14 +96,24 @@ export const FeatureStrategyMenuCard = ({ -
+ - {strategy.description} -
+ theme.typography.body2.fontSize, + width: '100%', + }} + > + {strategy.description} + + ); }; diff --git a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenuCard/OldFeatureStrategyMenuCard.tsx b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenuCard/OldFeatureStrategyMenuCard.tsx new file mode 100644 index 0000000000..f92d00b07f --- /dev/null +++ b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenuCard/OldFeatureStrategyMenuCard.tsx @@ -0,0 +1,103 @@ +import type { IStrategy } from 'interfaces/strategy'; +import { Link } from 'react-router-dom'; +import { + getFeatureStrategyIcon, + formatStrategyName, +} from 'utils/strategyNames'; +import { formatCreateStrategyPath } from 'component/feature/FeatureStrategy/FeatureStrategyCreate/FeatureStrategyCreate'; +import StringTruncator from 'component/common/StringTruncator/StringTruncator'; +import { styled } from '@mui/material'; +import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; + +interface IFeatureStrategyMenuCardProps { + projectId: string; + featureId: string; + environmentId: string; + strategy: Pick & + Partial; + defaultStrategy?: boolean; +} + +const StyledIcon = styled('div')(({ theme }) => ({ + width: theme.spacing(4), + height: 'auto', + '& > svg': { + // Styling for SVG icons. + fill: theme.palette.primary.main, + }, + '& > div': { + // Styling for the Rollout icon. + height: theme.spacing(2), + marginLeft: '-.75rem', + color: theme.palette.primary.main, + }, +})); + +const StyledDescription = styled('div')(({ theme }) => ({ + fontSize: theme.fontSizes.smallBody, +})); + +const StyledName = styled(StringTruncator)(({ theme }) => ({ + fontWeight: theme.fontWeight.bold, +})); + +const StyledCard = styled(Link)(({ theme }) => ({ + display: 'grid', + gridTemplateColumns: '3rem 1fr', + width: '20rem', + padding: theme.spacing(2), + color: 'inherit', + textDecoration: 'inherit', + lineHeight: 1.25, + borderWidth: '1px', + borderStyle: 'solid', + borderColor: theme.palette.divider, + borderRadius: theme.spacing(1), + '&:hover, &:focus': { + borderColor: theme.palette.primary.main, + }, +})); + +export const OldFeatureStrategyMenuCard = ({ + projectId, + featureId, + environmentId, + strategy, + defaultStrategy, +}: IFeatureStrategyMenuCardProps) => { + const StrategyIcon = getFeatureStrategyIcon(strategy.name); + const strategyName = formatStrategyName(strategy.name); + const { trackEvent } = usePlausibleTracker(); + + const createStrategyPath = formatCreateStrategyPath( + projectId, + featureId, + environmentId, + strategy.name, + defaultStrategy, + ); + + const openStrategyCreationModal = () => { + trackEvent('strategy-add', { + props: { + buttonTitle: strategy.displayName || strategyName, + }, + }); + }; + + return ( + + + + +
+ + {strategy.description} +
+
+ ); +}; diff --git a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenuCards/FeatureStrategyMenuCards.tsx b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenuCards/FeatureStrategyMenuCards.tsx index 6b21a6cb10..2b3bd2e401 100644 --- a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenuCards/FeatureStrategyMenuCards.tsx +++ b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenuCards/FeatureStrategyMenuCards.tsx @@ -1,4 +1,4 @@ -import { Link, List, ListItem, styled, Typography } from '@mui/material'; +import { Link, styled, Typography, Box, IconButton } from '@mui/material'; import { useStrategies } from 'hooks/api/getters/useStrategies/useStrategies'; import { FeatureStrategyMenuCard } from '../FeatureStrategyMenuCard/FeatureStrategyMenuCard'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; @@ -6,6 +6,7 @@ import { useReleasePlanTemplates } from 'hooks/api/getters/useReleasePlanTemplat import { FeatureReleasePlanCard } from '../FeatureReleasePlanCard/FeatureReleasePlanCard'; import type { IReleasePlanTemplate } from 'interfaces/releasePlans'; import { useNavigate } from 'react-router-dom'; +import CloseIcon from '@mui/icons-material/Close'; interface IFeatureStrategyMenuCardsProps { projectId: string; @@ -13,11 +14,13 @@ interface IFeatureStrategyMenuCardsProps { environmentId: string; onlyReleasePlans: boolean; onAddReleasePlan: (template: IReleasePlanTemplate) => void; + onClose?: () => void; } const StyledTypography = styled(Typography)(({ theme }) => ({ fontSize: theme.fontSizes.smallBody, padding: theme.spacing(1, 2), + width: '100%', })); const StyledLink = styled(Link)(({ theme }) => ({ @@ -25,12 +28,43 @@ const StyledLink = styled(Link)(({ theme }) => ({ cursor: 'pointer', })) as typeof Link; +const GridContainer = styled(Box)(() => ({ + width: '100%', +})); + +const GridSection = styled(Box)(({ theme }) => ({ + display: 'grid', + gridTemplateColumns: 'repeat(2, 1fr)', + gap: theme.spacing(1.5), + padding: theme.spacing(0, 2), + 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(1, 2), +})); + +const TitleText = styled(Typography)(({ theme }) => ({ + fontSize: theme.typography.body1.fontSize, + fontWeight: theme.typography.fontWeightBold, + margin: 0, +})); + export const FeatureStrategyMenuCards = ({ projectId, featureId, environmentId, onlyReleasePlans, onAddReleasePlan, + onClose, }: IFeatureStrategyMenuCardsProps) => { const { strategies } = useStrategies(); const { templates } = useReleasePlanTemplates(); @@ -52,21 +86,38 @@ export const FeatureStrategyMenuCards = ({ 'This is the default strategy defined for this environment in the project', }; return ( - + + + + {onlyReleasePlans ? 'Select template' : 'Select strategy'} + + {onClose && ( + + + + )} + {allStrategies ? ( <> Default strategy for {environmentId} environment - - - + + + + + ) : null} Release templates - {templates.map((template) => ( - - onAddReleasePlan(template)} - /> - - ))} + + {templates.map((template) => ( + + + onAddReleasePlan(template) + } + /> + + ))} + } /> @@ -118,16 +173,18 @@ export const FeatureStrategyMenuCards = ({ Predefined strategy types - {preDefinedStrategies.map((strategy) => ( - - - - ))} + + {preDefinedStrategies.map((strategy) => ( + + + + ))} + 0} show={ @@ -135,21 +192,23 @@ export const FeatureStrategyMenuCards = ({ Custom strategies - {customStrategies.map((strategy) => ( - - - - ))} + + {customStrategies.map((strategy) => ( + + + + ))} + } /> ) : null} - + ); }; diff --git a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenuCards/OldFeatureStrategyMenuCards.tsx b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenuCards/OldFeatureStrategyMenuCards.tsx new file mode 100644 index 0000000000..95640668c2 --- /dev/null +++ b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenuCards/OldFeatureStrategyMenuCards.tsx @@ -0,0 +1,155 @@ +import { Link, List, ListItem, styled, Typography } from '@mui/material'; +import { useStrategies } from 'hooks/api/getters/useStrategies/useStrategies'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import { useReleasePlanTemplates } from 'hooks/api/getters/useReleasePlanTemplates/useReleasePlanTemplates'; +import type { IReleasePlanTemplate } from 'interfaces/releasePlans'; +import { useNavigate } from 'react-router-dom'; +import { OldFeatureStrategyMenuCard } from '../FeatureStrategyMenuCard/OldFeatureStrategyMenuCard'; +import { OldFeatureReleasePlanCard } from '../FeatureReleasePlanCard/OldFeatureReleasePlanCard'; + +interface IFeatureStrategyMenuCardsProps { + projectId: string; + featureId: string; + environmentId: string; + onlyReleasePlans: boolean; + onAddReleasePlan: (template: IReleasePlanTemplate) => void; +} + +const StyledTypography = styled(Typography)(({ theme }) => ({ + fontSize: theme.fontSizes.smallBody, + padding: theme.spacing(1, 2), +})); + +const StyledLink = styled(Link)(({ theme }) => ({ + fontSize: theme.fontSizes.smallBody, + cursor: 'pointer', +})) as typeof Link; + +export const OldFeatureStrategyMenuCards = ({ + projectId, + featureId, + environmentId, + onlyReleasePlans, + onAddReleasePlan, +}: IFeatureStrategyMenuCardsProps) => { + const { strategies } = useStrategies(); + const { templates } = useReleasePlanTemplates(); + const navigate = useNavigate(); + const allStrategies = !onlyReleasePlans; + + const preDefinedStrategies = strategies.filter( + (strategy) => !strategy.deprecated && !strategy.editable, + ); + + const customStrategies = strategies.filter( + (strategy) => !strategy.deprecated && strategy.editable, + ); + + const defaultStrategy = { + name: 'flexibleRollout', + displayName: 'Default strategy', + description: + 'This is the default strategy defined for this environment in the project', + }; + return ( + + {allStrategies ? ( + <> + + Default strategy for {environmentId} environment + + + + + + ) : null} + 0} + show={ + <> + + Release templates + + {templates.map((template) => ( + + onAddReleasePlan(template)} + /> + + ))} + + } + /> + + theme.spacing(1, 2, 0, 2), + }} + > +

No templates created.

+

+ Go to  + + navigate('/release-templates') + } + > + Release templates + +  to get started +

+
+ + } + /> + {allStrategies ? ( + <> + + Predefined strategy types + + {preDefinedStrategies.map((strategy) => ( + + + + ))} + 0} + show={ + <> + + Custom strategies + + {customStrategies.map((strategy) => ( + + + + ))} + + } + /> + + ) : null} +
+ ); +}; diff --git a/frontend/src/interfaces/uiConfig.ts b/frontend/src/interfaces/uiConfig.ts index 6402467315..fbe92589b0 100644 --- a/frontend/src/interfaces/uiConfig.ts +++ b/frontend/src/interfaces/uiConfig.ts @@ -91,6 +91,7 @@ export type UiFlags = { adminNavUI?: boolean; tagTypeColor?: boolean; globalChangeRequestConfig?: boolean; + newStrategyDropdown?: boolean; }; export interface IVersionInfo { diff --git a/src/lib/types/experimental.ts b/src/lib/types/experimental.ts index c38934fd81..a3bf59b5b9 100644 --- a/src/lib/types/experimental.ts +++ b/src/lib/types/experimental.ts @@ -64,7 +64,8 @@ export type IFlagKey = | 'simplifyDisableFeature' | 'adminNavUI' | 'tagTypeColor' - | 'globalChangeRequestConfig'; + | 'globalChangeRequestConfig' + | 'newStrategyDropdown'; export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>; @@ -309,6 +310,10 @@ const flags: IFlags = { process.env.UNLEASH_EXPERIMENTAL_GLOBAL_CHANGE_REQUEST_CONFIG, false, ), + newStrategyDropdown: parseEnvVarBoolean( + process.env.UNLEASH_EXPERIMENTAL_NEW_STRATEGY_DROPDOWN, + false, + ), }; export const defaultExperimentalOptions: IExperimentalOptions = { diff --git a/src/server-dev.ts b/src/server-dev.ts index a877fbc321..c0ae094087 100644 --- a/src/server-dev.ts +++ b/src/server-dev.ts @@ -58,6 +58,7 @@ process.nextTick(async () => { simplifyDisableFeature: true, adminNavUI: true, tagTypeColor: true, + newStrategyDropdown: true, }, }, authentication: {