1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-22 19:07:54 +01:00

feat: selector dropdown for milestone new strategy (#8841)

This commit is contained in:
David Leek 2024-11-25 07:59:58 +01:00 committed by GitHub
parent c85c877c93
commit f985cb1deb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 229 additions and 14 deletions

View File

@ -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<Element>();
const isPopoverOpen = Boolean(anchor);
const popoverId = isPopoverOpen
? 'MilestoneStrategyMenuPopover'
: undefined;
const onClose = () => {
setAnchor(undefined);
};
const onSelectStrategy = (
milestoneId: string,
strategy: IReleasePlanMilestoneStrategy,
) => {
showAddStrategyDialog(index, strategy);
};
return (
<StyledMilestoneCard>
@ -111,10 +134,27 @@ export const MilestoneCard = ({
<Button
variant='outlined'
color='primary'
onClick={() => showAddStrategyDialog(index)}
onClick={(ev) => setAnchor(ev.currentTarget)}
>
Add strategy
</Button>
<Popover
id={popoverId}
open={isPopoverOpen}
anchorEl={anchor}
onClose={onClose}
onClick={onClose}
PaperProps={{
sx: (theme) => ({
paddingBottom: theme.spacing(1),
}),
}}
>
<MilestoneStrategyMenuCards
milestoneId={milestone.id ?? index.toString()}
openAddStrategy={onSelectStrategy}
/>
</Popover>
</Grid>
</Grid>
</StyledMilestoneCardBody>

View File

@ -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<IReleasePlanMilestonePayload[]>
>;
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}
/>

View File

@ -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 (
<StyledCard
onClick={() => {
const strat = createFeatureStrategy('', strategy);
onClick({
id: uuidv4(),
name: strat.name,
title: '',
constraints: strat.constraints,
parameters: strat.parameters,
});
}}
>
<StyledIcon>
<StrategyIcon />
</StyledIcon>
<div>
<StyledName
text={strategy.displayName || strategyName}
maxWidth='200'
maxLength={25}
/>
<StyledDescription>{strategy.description}</StyledDescription>
</div>
</StyledCard>
);
};

View File

@ -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 (
<List dense>
<>
<StyledTypography color='textSecondary'>
Predefined strategy types
</StyledTypography>
{preDefinedStrategies.map((strategy) => (
<ListItem key={strategy.name}>
<MilestoneStrategyMenuCard
strategy={strategy}
onClick={onClick}
/>
</ListItem>
))}
</>
</List>
);
};

View File

@ -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<ITemplateFormProps> = ({
children,
}) => {
const [addStrategyOpen, setAddStrategyOpen] = useState(false);
const [activeMilestoneIndex, setActiveMilestoneIndex] = useState<
number | undefined
>();
const [strategy, setStrategy] = useState<IReleasePlanMilestoneStrategy>({
name: 'flexibleRollout',
parameters: { rollout: '50' },
constraints: [],
title: '',
id: '',
});
const openAddStrategyForm = (
index: number,
strategy: IReleasePlanMilestoneStrategy,
) => {
setActiveMilestoneIndex(index);
setStrategy(strategy);
setAddStrategyOpen(true);
};
return (
<FormTemplate
@ -90,7 +111,7 @@ export const TemplateForm: React.FC<ITemplateFormProps> = ({
<MilestoneList
milestones={milestones}
setMilestones={setMilestones}
setAddStrategyOpen={setAddStrategyOpen}
openAddStrategyForm={openAddStrategyForm}
errors={errors}
clearErrors={clearErrors}
/>

View File

@ -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;