From f985cb1deb1878b58cf3d97ca89b34ee7118976a Mon Sep 17 00:00:00 2001 From: David Leek Date: Mon, 25 Nov 2024 07:59:58 +0100 Subject: [PATCH] feat: selector dropdown for milestone new strategy (#8841) --- .../ReleasePlanTemplate/MilestoneCard.tsx | 48 +++++++++- .../ReleasePlanTemplate/MilestoneList.tsx | 18 ++-- .../MilestoneStrategyMenuCard.tsx | 89 +++++++++++++++++++ .../MilestoneStrategyMenuCards.tsx | 50 +++++++++++ .../ReleasePlanTemplate/TemplateForm.tsx | 25 +++++- frontend/src/interfaces/releasePlans.ts | 13 +++ 6 files changed, 229 insertions(+), 14 deletions(-) create mode 100644 frontend/src/component/releases/ReleasePlanTemplate/MilestoneStrategyMenuCard.tsx create mode 100644 frontend/src/component/releases/ReleasePlanTemplate/MilestoneStrategyMenuCards.tsx diff --git a/frontend/src/component/releases/ReleasePlanTemplate/MilestoneCard.tsx b/frontend/src/component/releases/ReleasePlanTemplate/MilestoneCard.tsx index e185de66da..c077c0d58d 100644 --- a/frontend/src/component/releases/ReleasePlanTemplate/MilestoneCard.tsx +++ b/frontend/src/component/releases/ReleasePlanTemplate/MilestoneCard.tsx @@ -1,8 +1,12 @@ import Input from 'component/common/Input/Input'; -import { Box, Button, Card, Grid, styled } from '@mui/material'; +import { Box, Button, Card, Grid, Popover, styled } from '@mui/material'; import Edit from '@mui/icons-material/Edit'; -import type { IReleasePlanMilestonePayload } from 'interfaces/releasePlans'; +import type { + IReleasePlanMilestonePayload, + IReleasePlanMilestoneStrategy, +} from 'interfaces/releasePlans'; import { useState } from 'react'; +import { MilestoneStrategyMenuCards } from './MilestoneStrategyMenuCards'; const StyledEditIcon = styled(Edit)(({ theme }) => ({ cursor: 'pointer', @@ -53,7 +57,10 @@ interface IMilestoneCardProps { index: number; milestone: IReleasePlanMilestonePayload; milestoneNameChanged: (index: number, name: string) => void; - showAddStrategyDialog: (index: number) => void; + showAddStrategyDialog: ( + index: number, + strategy: IReleasePlanMilestoneStrategy, + ) => void; errors: { [key: string]: string }; clearErrors: () => void; } @@ -67,6 +74,22 @@ export const MilestoneCard = ({ clearErrors, }: IMilestoneCardProps) => { const [editMode, setEditMode] = useState(false); + const [anchor, setAnchor] = useState(); + const isPopoverOpen = Boolean(anchor); + const popoverId = isPopoverOpen + ? 'MilestoneStrategyMenuPopover' + : undefined; + + const onClose = () => { + setAnchor(undefined); + }; + + const onSelectStrategy = ( + milestoneId: string, + strategy: IReleasePlanMilestoneStrategy, + ) => { + showAddStrategyDialog(index, strategy); + }; return ( @@ -111,10 +134,27 @@ export const MilestoneCard = ({ + ({ + paddingBottom: theme.spacing(1), + }), + }} + > + + diff --git a/frontend/src/component/releases/ReleasePlanTemplate/MilestoneList.tsx b/frontend/src/component/releases/ReleasePlanTemplate/MilestoneList.tsx index 50d4cbe4f1..afd1ea2699 100644 --- a/frontend/src/component/releases/ReleasePlanTemplate/MilestoneList.tsx +++ b/frontend/src/component/releases/ReleasePlanTemplate/MilestoneList.tsx @@ -1,4 +1,7 @@ -import type { IReleasePlanMilestonePayload } from 'interfaces/releasePlans'; +import type { + IReleasePlanMilestonePayload, + IReleasePlanMilestoneStrategy, +} from 'interfaces/releasePlans'; import { MilestoneCard } from './MilestoneCard'; import { styled } from '@mui/material'; import { Button } from '@mui/material'; @@ -9,7 +12,10 @@ interface IMilestoneListProps { setMilestones: React.Dispatch< React.SetStateAction >; - setAddStrategyOpen: (open: boolean) => void; + openAddStrategyForm: ( + index: number, + strategy: IReleasePlanMilestoneStrategy, + ) => void; errors: { [key: string]: string }; clearErrors: () => void; } @@ -22,14 +28,10 @@ const StyledAddMilestoneButton = styled(Button)(({ theme }) => ({ export const MilestoneList = ({ milestones, setMilestones, - setAddStrategyOpen, + openAddStrategyForm, errors, clearErrors, }: IMilestoneListProps) => { - const showAddStrategyDialog = (index: number) => { - setAddStrategyOpen(true); - }; - const milestoneNameChanged = (index: number, name: string) => { setMilestones((prev) => prev.map((milestone, i) => @@ -46,7 +48,7 @@ export const MilestoneList = ({ index={index} milestone={milestone} milestoneNameChanged={milestoneNameChanged} - showAddStrategyDialog={showAddStrategyDialog} + showAddStrategyDialog={openAddStrategyForm} errors={errors} clearErrors={clearErrors} /> diff --git a/frontend/src/component/releases/ReleasePlanTemplate/MilestoneStrategyMenuCard.tsx b/frontend/src/component/releases/ReleasePlanTemplate/MilestoneStrategyMenuCard.tsx new file mode 100644 index 0000000000..dcbd2a4710 --- /dev/null +++ b/frontend/src/component/releases/ReleasePlanTemplate/MilestoneStrategyMenuCard.tsx @@ -0,0 +1,89 @@ +import { + formatStrategyName, + getFeatureStrategyIcon, +} from 'utils/strategyNames'; +import { styled } from '@mui/material'; +import type { IStrategy } from 'interfaces/strategy'; +import StringTruncator from 'component/common/StringTruncator/StringTruncator'; +import type { IReleasePlanMilestoneStrategy } from 'interfaces/releasePlans'; +import { v4 as uuidv4 } from 'uuid'; +import { createFeatureStrategy } from 'utils/createFeatureStrategy'; + +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('div')(({ 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, + }, +})); + +interface IMilestoneStrategyMenuCardProps { + strategy: IStrategy; + onClick: (strategy: IReleasePlanMilestoneStrategy) => void; +} + +export const MilestoneStrategyMenuCard = ({ + strategy, + onClick, +}: IMilestoneStrategyMenuCardProps) => { + const StrategyIcon = getFeatureStrategyIcon(strategy.name); + const strategyName = formatStrategyName(strategy.name); + return ( + { + const strat = createFeatureStrategy('', strategy); + onClick({ + id: uuidv4(), + name: strat.name, + title: '', + constraints: strat.constraints, + parameters: strat.parameters, + }); + }} + > + + + +
+ + {strategy.description} +
+
+ ); +}; diff --git a/frontend/src/component/releases/ReleasePlanTemplate/MilestoneStrategyMenuCards.tsx b/frontend/src/component/releases/ReleasePlanTemplate/MilestoneStrategyMenuCards.tsx new file mode 100644 index 0000000000..9aed878fdc --- /dev/null +++ b/frontend/src/component/releases/ReleasePlanTemplate/MilestoneStrategyMenuCards.tsx @@ -0,0 +1,50 @@ +import { List, ListItem, styled, Typography } from '@mui/material'; +import { MilestoneStrategyMenuCard } from './MilestoneStrategyMenuCard'; +import { useStrategies } from 'hooks/api/getters/useStrategies/useStrategies'; +import type { IReleasePlanMilestoneStrategy } from 'interfaces/releasePlans'; + +const StyledTypography = styled(Typography)(({ theme }) => ({ + fontSize: theme.fontSizes.smallBody, + padding: theme.spacing(1, 2), +})); + +interface IMilestoneStrategyMenuCardsProps { + milestoneId: string; + openAddStrategy: ( + milestoneId: string, + strategy: IReleasePlanMilestoneStrategy, + ) => void; +} + +export const MilestoneStrategyMenuCards = ({ + milestoneId, + openAddStrategy, +}: IMilestoneStrategyMenuCardsProps) => { + const { strategies } = useStrategies(); + + const preDefinedStrategies = strategies.filter( + (strategy) => !strategy.deprecated && !strategy.editable, + ); + + const onClick = (strategy: IReleasePlanMilestoneStrategy) => { + openAddStrategy(milestoneId, strategy); + }; + + return ( + + <> + + Predefined strategy types + + {preDefinedStrategies.map((strategy) => ( + + + + ))} + + + ); +}; diff --git a/frontend/src/component/releases/ReleasePlanTemplate/TemplateForm.tsx b/frontend/src/component/releases/ReleasePlanTemplate/TemplateForm.tsx index c7164e1911..ef2b283901 100644 --- a/frontend/src/component/releases/ReleasePlanTemplate/TemplateForm.tsx +++ b/frontend/src/component/releases/ReleasePlanTemplate/TemplateForm.tsx @@ -1,7 +1,10 @@ import Input from 'component/common/Input/Input'; import { styled } from '@mui/material'; import { MilestoneList } from './MilestoneList'; -import type { IReleasePlanMilestonePayload } from 'interfaces/releasePlans'; +import type { + IReleasePlanMilestonePayload, + IReleasePlanMilestoneStrategy, +} from 'interfaces/releasePlans'; import FormTemplate from 'component/common/FormTemplate/FormTemplate'; import ReleaseTemplateIcon from '@mui/icons-material/DashboardOutlined'; import { SidebarModal } from 'component/common/SidebarModal/SidebarModal'; @@ -56,6 +59,24 @@ export const TemplateForm: React.FC = ({ children, }) => { const [addStrategyOpen, setAddStrategyOpen] = useState(false); + const [activeMilestoneIndex, setActiveMilestoneIndex] = useState< + number | undefined + >(); + const [strategy, setStrategy] = useState({ + name: 'flexibleRollout', + parameters: { rollout: '50' }, + constraints: [], + title: '', + id: '', + }); + const openAddStrategyForm = ( + index: number, + strategy: IReleasePlanMilestoneStrategy, + ) => { + setActiveMilestoneIndex(index); + setStrategy(strategy); + setAddStrategyOpen(true); + }; return ( = ({ diff --git a/frontend/src/interfaces/releasePlans.ts b/frontend/src/interfaces/releasePlans.ts index 8ad6d74015..bd1332529e 100644 --- a/frontend/src/interfaces/releasePlans.ts +++ b/frontend/src/interfaces/releasePlans.ts @@ -1,3 +1,6 @@ +import type { IFeatureVariant } from './featureToggle'; +import type { IConstraint, IFeatureStrategyParameters } from './strategy'; + export interface IReleasePlanTemplate { id: string; name: string; @@ -15,6 +18,16 @@ export interface IReleasePlanTemplate { milestones: IReleasePlanMilestonePayload[]; } +export interface IReleasePlanMilestoneStrategy { + id: string; + name: string; + title: string; + disabled?: boolean; + constraints: IConstraint[]; + parameters: IFeatureStrategyParameters; + variants?: IFeatureVariant[]; +} + export interface IReleasePlanMilestone { id: string; name: string;