mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-09 00:18:00 +01:00
feat: release plan template strategy types, constraints, segments (#8861)
This commit is contained in:
parent
ca5c03ed17
commit
219006c856
@ -0,0 +1,66 @@
|
|||||||
|
import { FeatureStrategyConstraintAccordionList } from 'component/feature/FeatureStrategy/FeatureStrategyConstraints/FeatureStrategyConstraintAccordionList/FeatureStrategyConstraintAccordionList';
|
||||||
|
import type { IReleasePlanMilestoneStrategy } from 'interfaces/releasePlans';
|
||||||
|
import type { IConstraint } from 'interfaces/strategy';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
interface IMilestoneStrategyConstraintsProps {
|
||||||
|
strategy: Omit<IReleasePlanMilestoneStrategy, 'milestoneId'>;
|
||||||
|
setStrategy: React.Dispatch<
|
||||||
|
React.SetStateAction<Omit<IReleasePlanMilestoneStrategy, 'milestoneId'>>
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filterConstraints = (constraint: any) => {
|
||||||
|
if (
|
||||||
|
constraint.hasOwnProperty('values') &&
|
||||||
|
(!constraint.hasOwnProperty('value') || constraint.value === '')
|
||||||
|
) {
|
||||||
|
return constraint.values && constraint.values.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (constraint.hasOwnProperty('value')) {
|
||||||
|
return constraint.value !== '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MilestoneStrategyConstraints = ({
|
||||||
|
strategy,
|
||||||
|
setStrategy,
|
||||||
|
}: IMilestoneStrategyConstraintsProps) => {
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (!strategy.constraints) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the component is unmounting we want to remove all constraints that do not have valid single value or
|
||||||
|
// valid multivalues
|
||||||
|
setStrategy((prev) => ({
|
||||||
|
...prev,
|
||||||
|
constraints: prev.constraints?.filter(filterConstraints),
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const constraints = strategy.constraints || [];
|
||||||
|
|
||||||
|
const setConstraints = (value: React.SetStateAction<IConstraint[]>) => {
|
||||||
|
setStrategy((prev) => {
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
constraints:
|
||||||
|
value instanceof Function
|
||||||
|
? value(prev.constraints || [])
|
||||||
|
: value,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FeatureStrategyConstraintAccordionList
|
||||||
|
constraints={constraints}
|
||||||
|
setConstraints={setConstraints}
|
||||||
|
showCreateButton={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,98 @@
|
|||||||
|
import { useSegmentLimits } from 'hooks/api/getters/useSegmentLimits/useSegmentLimits';
|
||||||
|
import { useSegments } from 'hooks/api/getters/useSegments/useSegments';
|
||||||
|
import type { ISegment } from 'interfaces/segment';
|
||||||
|
import { Box, styled, Typography } from '@mui/material';
|
||||||
|
import { SegmentDocsStrategyWarning } from 'component/segments/SegmentDocs';
|
||||||
|
import { HelpIcon } from 'component/common/HelpIcon/HelpIcon';
|
||||||
|
import {
|
||||||
|
AutocompleteBox,
|
||||||
|
type IAutocompleteBoxOption,
|
||||||
|
} from 'component/common/AutocompleteBox/AutocompleteBox';
|
||||||
|
import { MilestoneStrategySegmentList } from './MilestoneStrategySegmentList';
|
||||||
|
|
||||||
|
const StyledHelpIconBox = styled(Box)(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginTop: theme.spacing(1),
|
||||||
|
marginBottom: theme.spacing(1),
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface IMilestoneStrategySegmentProps {
|
||||||
|
segments: ISegment[];
|
||||||
|
setSegments: React.Dispatch<React.SetStateAction<ISegment[]>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MilestoneStrategySegment = ({
|
||||||
|
segments: selectedSegments,
|
||||||
|
setSegments: setSelectedSegments,
|
||||||
|
}: IMilestoneStrategySegmentProps) => {
|
||||||
|
const { segments: allSegments } = useSegments();
|
||||||
|
const { strategySegmentsLimit } = useSegmentLimits();
|
||||||
|
|
||||||
|
const atStrategySegmentsLimit: boolean = Boolean(
|
||||||
|
strategySegmentsLimit &&
|
||||||
|
selectedSegments.length >= strategySegmentsLimit,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!allSegments || allSegments.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const unusedSegments = allSegments.filter((segment) => {
|
||||||
|
return !selectedSegments.find((selected) => selected.id === segment.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
const autocompleteOptions = unusedSegments.map((segment) => ({
|
||||||
|
value: String(segment.id),
|
||||||
|
label: segment.name,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const onChange = ([option]: IAutocompleteBoxOption[]) => {
|
||||||
|
const selectedSegment = allSegments.find((segment) => {
|
||||||
|
return String(segment.id) === option.value;
|
||||||
|
});
|
||||||
|
if (selectedSegment) {
|
||||||
|
setSelectedSegments((prev) => [...prev, selectedSegment]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<StyledHelpIconBox>
|
||||||
|
<Typography>Segments</Typography>
|
||||||
|
<HelpIcon
|
||||||
|
htmlTooltip
|
||||||
|
tooltip={
|
||||||
|
<Box>
|
||||||
|
<Typography variant='body2'>
|
||||||
|
Segments are reusable sets of constraints that
|
||||||
|
can be defined once and reused across feature
|
||||||
|
toggle configurations. You can create a segment
|
||||||
|
on the global or the project level. Read more
|
||||||
|
about segments{' '}
|
||||||
|
<a
|
||||||
|
href='https://docs.getunleash.io/reference/segments'
|
||||||
|
target='_blank'
|
||||||
|
rel='noopener noreferrer'
|
||||||
|
>
|
||||||
|
here
|
||||||
|
</a>
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</StyledHelpIconBox>
|
||||||
|
{atStrategySegmentsLimit && <SegmentDocsStrategyWarning />}
|
||||||
|
<AutocompleteBox
|
||||||
|
label='Select segments'
|
||||||
|
options={autocompleteOptions}
|
||||||
|
onChange={onChange}
|
||||||
|
disabled={atStrategySegmentsLimit}
|
||||||
|
/>
|
||||||
|
<MilestoneStrategySegmentList
|
||||||
|
segments={selectedSegments}
|
||||||
|
setSegments={setSelectedSegments}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,80 @@
|
|||||||
|
import { Fragment, useState } from 'react';
|
||||||
|
import type { ISegment } from 'interfaces/segment';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { FeatureStrategySegmentChip } from 'component/feature/FeatureStrategy/FeatureStrategySegment/FeatureStrategySegmentChip';
|
||||||
|
import { SegmentItem } from 'component/common/SegmentItem/SegmentItem';
|
||||||
|
import { styled } from '@mui/material';
|
||||||
|
|
||||||
|
const StyledList = styled('div')(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: theme.spacing(1),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledSelectedSegmentsLabel = styled('p')(({ theme }) => ({
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledAnd = styled('p')(({ theme }) => ({
|
||||||
|
fontSize: theme.fontSizes.smallerBody,
|
||||||
|
padding: theme.spacing(0.75, 1),
|
||||||
|
display: 'block',
|
||||||
|
marginTop: 'auto',
|
||||||
|
marginBottom: 'auto',
|
||||||
|
alignItems: 'center',
|
||||||
|
borderRadius: theme.shape.borderRadius,
|
||||||
|
lineHeight: 1,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
backgroundColor: theme.palette.background.elevation2,
|
||||||
|
}));
|
||||||
|
|
||||||
|
type IMilestoneStrategySegmentListProps = {
|
||||||
|
segments: ISegment[];
|
||||||
|
setSegments: React.Dispatch<React.SetStateAction<ISegment[]>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MilestoneStrategySegmentList = ({
|
||||||
|
segments,
|
||||||
|
setSegments,
|
||||||
|
}: IMilestoneStrategySegmentListProps) => {
|
||||||
|
const [preview, setPreview] = useState<ISegment>();
|
||||||
|
const lastSegmentIndex = segments.length - 1;
|
||||||
|
|
||||||
|
if (segments.length === 0) {
|
||||||
|
console.log('segments.length === 0');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={segments && segments.length > 0}
|
||||||
|
show={
|
||||||
|
<StyledSelectedSegmentsLabel>
|
||||||
|
Selected Segments
|
||||||
|
</StyledSelectedSegmentsLabel>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<StyledList>
|
||||||
|
{segments.map((segment, i) => (
|
||||||
|
<Fragment key={segment.id}>
|
||||||
|
<FeatureStrategySegmentChip
|
||||||
|
segment={segment}
|
||||||
|
setSegments={setSegments}
|
||||||
|
preview={preview}
|
||||||
|
setPreview={setPreview}
|
||||||
|
/>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={i < lastSegmentIndex}
|
||||||
|
show={<StyledAnd>AND</StyledAnd>}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</StyledList>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={Boolean(preview)}
|
||||||
|
show={() => <SegmentItem segment={preview!} isExpanded />}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,59 @@
|
|||||||
|
import type { IFormErrors } from 'hooks/useFormErrors';
|
||||||
|
import type { IReleasePlanMilestoneStrategy } from 'interfaces/releasePlans';
|
||||||
|
import type { IStrategy } from 'interfaces/strategy';
|
||||||
|
import { MilestoneStrategyTypeFlexible } from './MilestoneStrategyTypeFlexible';
|
||||||
|
import GeneralStrategy from 'component/feature/StrategyTypes/GeneralStrategy/GeneralStrategy';
|
||||||
|
import UserWithIdStrategy from 'component/feature/StrategyTypes/UserWithIdStrategy/UserWithId';
|
||||||
|
import DefaultStrategy from 'component/feature/StrategyTypes/DefaultStrategy/DefaultStrategy';
|
||||||
|
|
||||||
|
interface IMilestoneStrategyTypeProps {
|
||||||
|
strategy: Omit<IReleasePlanMilestoneStrategy, 'milestoneId'>;
|
||||||
|
strategyDefinition?: IStrategy;
|
||||||
|
parameters: IReleasePlanMilestoneStrategy['parameters'];
|
||||||
|
updateParameter: (field: string, value: string) => void;
|
||||||
|
errors: IFormErrors;
|
||||||
|
}
|
||||||
|
export const MilestoneStrategyType = ({
|
||||||
|
strategy,
|
||||||
|
strategyDefinition,
|
||||||
|
parameters,
|
||||||
|
updateParameter,
|
||||||
|
errors,
|
||||||
|
}: IMilestoneStrategyTypeProps) => {
|
||||||
|
if (!strategyDefinition) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (strategy.name) {
|
||||||
|
case 'default':
|
||||||
|
return <DefaultStrategy strategyDefinition={strategyDefinition} />;
|
||||||
|
case 'flexibleRollout':
|
||||||
|
return (
|
||||||
|
<MilestoneStrategyTypeFlexible
|
||||||
|
parameters={parameters}
|
||||||
|
updateParameter={updateParameter}
|
||||||
|
errors={errors}
|
||||||
|
editable={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 'userWithId':
|
||||||
|
return (
|
||||||
|
<UserWithIdStrategy
|
||||||
|
editable={true}
|
||||||
|
parameters={strategy.parameters ?? {}}
|
||||||
|
updateParameter={updateParameter}
|
||||||
|
errors={errors}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<GeneralStrategy
|
||||||
|
strategyDefinition={strategyDefinition}
|
||||||
|
parameters={strategy.parameters ?? {}}
|
||||||
|
updateParameter={updateParameter}
|
||||||
|
editable={true}
|
||||||
|
errors={errors}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,111 @@
|
|||||||
|
import { Box, styled } from '@mui/material';
|
||||||
|
import { StickinessSelect } from 'component/feature/StrategyTypes/FlexibleStrategy/StickinessSelect/StickinessSelect';
|
||||||
|
import RolloutSlider from 'component/feature/StrategyTypes/RolloutSlider/RolloutSlider';
|
||||||
|
import type { IFormErrors } from 'hooks/useFormErrors';
|
||||||
|
import type { IFeatureStrategyParameters } from 'interfaces/strategy';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import {
|
||||||
|
parseParameterNumber,
|
||||||
|
parseParameterString,
|
||||||
|
} from 'utils/parseParameter';
|
||||||
|
import Input from 'component/common/Input/Input';
|
||||||
|
|
||||||
|
interface IMilestoneStrategyTypeFlexibleProps {
|
||||||
|
parameters: IFeatureStrategyParameters;
|
||||||
|
updateParameter: (field: string, value: string) => void;
|
||||||
|
editable: boolean;
|
||||||
|
errors?: IFormErrors;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StyledBox = styled(Box)(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
backgroundColor: theme.palette.background.elevation1,
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
borderRadius: `${theme.shape.borderRadiusMedium}px`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledOuterBox = styled(Box)(({ theme }) => ({
|
||||||
|
marginTop: theme.spacing(1),
|
||||||
|
display: 'flex',
|
||||||
|
width: '100%',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledInnerBox1 = styled(Box)(({ theme }) => ({
|
||||||
|
width: '50%',
|
||||||
|
marginRight: theme.spacing(0.5),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledInnerBox2 = styled(Box)(({ theme }) => ({
|
||||||
|
width: '50%',
|
||||||
|
marginLeft: theme.spacing(0.5),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const DEFAULT_STICKINESS = 'default';
|
||||||
|
|
||||||
|
export const MilestoneStrategyTypeFlexible = ({
|
||||||
|
parameters,
|
||||||
|
updateParameter,
|
||||||
|
editable,
|
||||||
|
errors,
|
||||||
|
}: IMilestoneStrategyTypeFlexibleProps) => {
|
||||||
|
const updateRollout = (e: Event, value: number | number[]) => {
|
||||||
|
updateParameter('rollout', value.toString());
|
||||||
|
};
|
||||||
|
|
||||||
|
const rollout =
|
||||||
|
parameters.rollout !== undefined
|
||||||
|
? parseParameterNumber(parameters.rollout)
|
||||||
|
: 100;
|
||||||
|
|
||||||
|
const stickiness = useMemo(() => {
|
||||||
|
if (!parameters.stickiness) {
|
||||||
|
updateParameter('stickiness', DEFAULT_STICKINESS);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseParameterString(parameters.stickiness);
|
||||||
|
}, [parameters.stickiness]);
|
||||||
|
|
||||||
|
const groupId = parseParameterString(parameters.groupId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledBox>
|
||||||
|
<RolloutSlider
|
||||||
|
name='Rollout'
|
||||||
|
value={rollout}
|
||||||
|
disabled={!editable}
|
||||||
|
onChange={updateRollout}
|
||||||
|
/>
|
||||||
|
<StyledOuterBox>
|
||||||
|
<StyledInnerBox1>
|
||||||
|
<StickinessSelect
|
||||||
|
label='Stickiness'
|
||||||
|
value={stickiness}
|
||||||
|
editable={editable}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateParameter('stickiness', e.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</StyledInnerBox1>
|
||||||
|
<StyledInnerBox2>
|
||||||
|
<Input
|
||||||
|
label='groupId'
|
||||||
|
sx={{ width: '100%' }}
|
||||||
|
id='groupId-input'
|
||||||
|
value={groupId}
|
||||||
|
disabled={!editable}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateParameter(
|
||||||
|
'groupId',
|
||||||
|
parseParameterString(e.target.value),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
error={Boolean(errors?.getFormError('groupId'))}
|
||||||
|
helperText={errors?.getFormError('groupId')}
|
||||||
|
/>
|
||||||
|
</StyledInnerBox2>
|
||||||
|
</StyledOuterBox>
|
||||||
|
</StyledBox>
|
||||||
|
);
|
||||||
|
};
|
@ -1,11 +1,28 @@
|
|||||||
import { Box, Button, styled, Tab, Tabs, Typography } from '@mui/material';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
styled,
|
||||||
|
Alert,
|
||||||
|
Link,
|
||||||
|
Tab,
|
||||||
|
Tabs,
|
||||||
|
Typography,
|
||||||
|
Divider,
|
||||||
|
} from '@mui/material';
|
||||||
import { Badge } from 'component/common/Badge/Badge';
|
import { Badge } from 'component/common/Badge/Badge';
|
||||||
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
|
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
|
||||||
import type { IReleasePlanMilestoneStrategy } from 'interfaces/releasePlans';
|
import type { IReleasePlanMilestoneStrategy } from 'interfaces/releasePlans';
|
||||||
|
import type { ISegment } from 'interfaces/segment';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { formatStrategyName } from 'utils/strategyNames';
|
import { BuiltInStrategies, formatStrategyName } from 'utils/strategyNames';
|
||||||
import { MilestoneStrategyTitle } from './MilestoneStrategyTitle';
|
import { MilestoneStrategyTitle } from './MilestoneStrategyTitle';
|
||||||
|
import { MilestoneStrategyType } from './MilestoneStrategyType';
|
||||||
|
import { useStrategy } from 'hooks/api/getters/useStrategy/useStrategy';
|
||||||
|
import { useFormErrors } from 'hooks/useFormErrors';
|
||||||
import produce from 'immer';
|
import produce from 'immer';
|
||||||
|
import { MilestoneStrategySegment } from './MilestoneStrategySegment';
|
||||||
|
import { MilestoneStrategyConstraints } from './MilestoneStrategyConstraints';
|
||||||
|
import { useConstraintsValidation } from 'hooks/api/getters/useConstraintsValidation/useConstraintsValidation';
|
||||||
|
|
||||||
const StyledCancelButton = styled(Button)(({ theme }) => ({
|
const StyledCancelButton = styled(Button)(({ theme }) => ({
|
||||||
marginLeft: theme.spacing(3),
|
marginLeft: theme.spacing(3),
|
||||||
@ -34,6 +51,15 @@ const StyledTitle = styled('h1')(({ theme }) => ({
|
|||||||
paddingBottom: theme.spacing(2),
|
paddingBottom: theme.spacing(2),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const StyledAlertBox = styled(Box)(({ theme }) => ({
|
||||||
|
paddingLeft: theme.spacing(6),
|
||||||
|
paddingRight: theme.spacing(6),
|
||||||
|
'& > *': {
|
||||||
|
marginTop: theme.spacing(2),
|
||||||
|
marginBottom: theme.spacing(2),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
const StyledTabs = styled(Tabs)(({ theme }) => ({
|
const StyledTabs = styled(Tabs)(({ theme }) => ({
|
||||||
borderTop: `1px solid ${theme.palette.divider}`,
|
borderTop: `1px solid ${theme.palette.divider}`,
|
||||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||||
@ -46,6 +72,10 @@ const StyledTab = styled(Tab)(({ theme }) => ({
|
|||||||
width: '100px',
|
width: '100px',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const StyledBadge = styled(Badge)(({ theme }) => ({
|
||||||
|
marginLeft: theme.spacing(1),
|
||||||
|
}));
|
||||||
|
|
||||||
const StyledContentDiv = styled('div')(({ theme }) => ({
|
const StyledContentDiv = styled('div')(({ theme }) => ({
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@ -58,11 +88,34 @@ const StyledContentDiv = styled('div')(({ theme }) => ({
|
|||||||
height: '100%',
|
height: '100%',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const StyledBox = styled(Box)(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
position: 'relative',
|
||||||
|
marginTop: theme.spacing(3.5),
|
||||||
|
}));
|
||||||
|
|
||||||
const StyledTargetingHeader = styled('div')(({ theme }) => ({
|
const StyledTargetingHeader = styled('div')(({ theme }) => ({
|
||||||
color: theme.palette.text.secondary,
|
color: theme.palette.text.secondary,
|
||||||
marginTop: theme.spacing(1.5),
|
marginTop: theme.spacing(1.5),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const StyledDivider = styled(Divider)(({ theme }) => ({
|
||||||
|
width: '100%',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledDividerContent = styled(Box)(({ theme }) => ({
|
||||||
|
padding: theme.spacing(0.75, 1),
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
fontSize: theme.fontSizes.smallerBody,
|
||||||
|
backgroundColor: theme.palette.background.elevation2,
|
||||||
|
borderRadius: theme.shape.borderRadius,
|
||||||
|
width: '45px',
|
||||||
|
position: 'absolute',
|
||||||
|
top: '-10px',
|
||||||
|
left: 'calc(50% - 45px)',
|
||||||
|
lineHeight: 1,
|
||||||
|
}));
|
||||||
|
|
||||||
interface IReleasePlanTemplateAddStrategyFormProps {
|
interface IReleasePlanTemplateAddStrategyFormProps {
|
||||||
milestoneId: string | undefined;
|
milestoneId: string | undefined;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
@ -81,10 +134,25 @@ export const ReleasePlanTemplateAddStrategyForm = ({
|
|||||||
}: IReleasePlanTemplateAddStrategyFormProps) => {
|
}: IReleasePlanTemplateAddStrategyFormProps) => {
|
||||||
const [addStrategy, setAddStrategy] = useState(strategy);
|
const [addStrategy, setAddStrategy] = useState(strategy);
|
||||||
const [activeTab, setActiveTab] = useState(0);
|
const [activeTab, setActiveTab] = useState(0);
|
||||||
|
const [segments, setSegments] = useState<ISegment[]>([]);
|
||||||
|
const { strategyDefinition } = useStrategy(strategy?.name);
|
||||||
|
const hasValidConstraints = useConstraintsValidation(strategy?.constraints);
|
||||||
|
const errors = useFormErrors();
|
||||||
|
if (!strategy || !addStrategy || !strategyDefinition) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => {
|
const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => {
|
||||||
setActiveTab(newValue);
|
setActiveTab(newValue);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getTargetingCount = () => {
|
||||||
|
const constraintCount = addStrategy?.constraints?.length || 0;
|
||||||
|
const segmentCount = segments?.length || 0;
|
||||||
|
|
||||||
|
return constraintCount + segmentCount;
|
||||||
|
};
|
||||||
|
|
||||||
const updateParameter = (name: string, value: string) => {
|
const updateParameter = (name: string, value: string) => {
|
||||||
setAddStrategy(
|
setAddStrategy(
|
||||||
produce((draft) => {
|
produce((draft) => {
|
||||||
@ -108,10 +176,6 @@ export const ReleasePlanTemplateAddStrategyForm = ({
|
|||||||
onAddStrategy(milestoneId, addStrategy);
|
onAddStrategy(milestoneId, addStrategy);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!strategy) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormTemplate
|
<FormTemplate
|
||||||
modal
|
modal
|
||||||
@ -127,9 +191,35 @@ export const ReleasePlanTemplateAddStrategyForm = ({
|
|||||||
)}
|
)}
|
||||||
</StyledTitle>
|
</StyledTitle>
|
||||||
</StyledHeaderBox>
|
</StyledHeaderBox>
|
||||||
|
{!BuiltInStrategies.includes(strategy.name || 'default') && (
|
||||||
|
<StyledAlertBox>
|
||||||
|
<Alert severity='warning'>
|
||||||
|
Custom strategies are deprecated. We recommend not
|
||||||
|
adding them to any templates going forward and using the
|
||||||
|
predefined strategies like Gradual rollout with{' '}
|
||||||
|
<Link
|
||||||
|
href={
|
||||||
|
'https://docs.getunleash.io/reference/strategy-constraints'
|
||||||
|
}
|
||||||
|
target='_blank'
|
||||||
|
variant='body2'
|
||||||
|
>
|
||||||
|
constraints
|
||||||
|
</Link>{' '}
|
||||||
|
instead.
|
||||||
|
</Alert>
|
||||||
|
</StyledAlertBox>
|
||||||
|
)}
|
||||||
<StyledTabs value={activeTab} onChange={handleChange}>
|
<StyledTabs value={activeTab} onChange={handleChange}>
|
||||||
<StyledTab label='General' />
|
<StyledTab label='General' />
|
||||||
<Tab label={<Typography>Targeting</Typography>} />
|
<Tab
|
||||||
|
label={
|
||||||
|
<Typography>
|
||||||
|
Targeting
|
||||||
|
<StyledBadge>{getTargetingCount()}</StyledBadge>
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</StyledTabs>
|
</StyledTabs>
|
||||||
<StyledContentDiv>
|
<StyledContentDiv>
|
||||||
{activeTab === 0 && (
|
{activeTab === 0 && (
|
||||||
@ -140,6 +230,14 @@ export const ReleasePlanTemplateAddStrategyForm = ({
|
|||||||
updateParameter('title', title)
|
updateParameter('title', title)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<MilestoneStrategyType
|
||||||
|
strategy={addStrategy}
|
||||||
|
strategyDefinition={strategyDefinition}
|
||||||
|
parameters={addStrategy.parameters}
|
||||||
|
updateParameter={updateParameter}
|
||||||
|
errors={errors}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{activeTab === 1 && (
|
{activeTab === 1 && (
|
||||||
@ -147,6 +245,18 @@ export const ReleasePlanTemplateAddStrategyForm = ({
|
|||||||
<StyledTargetingHeader>
|
<StyledTargetingHeader>
|
||||||
Segmentation and constraints allow you to set
|
Segmentation and constraints allow you to set
|
||||||
filters on your strategies, so that they will only
|
filters on your strategies, so that they will only
|
||||||
|
<MilestoneStrategySegment
|
||||||
|
segments={segments}
|
||||||
|
setSegments={setSegments}
|
||||||
|
/>
|
||||||
|
<StyledBox>
|
||||||
|
<StyledDivider />
|
||||||
|
<StyledDividerContent>AND</StyledDividerContent>
|
||||||
|
</StyledBox>
|
||||||
|
<MilestoneStrategyConstraints
|
||||||
|
strategy={addStrategy}
|
||||||
|
setStrategy={setAddStrategy}
|
||||||
|
/>
|
||||||
be evaluated for users and applications that match
|
be evaluated for users and applications that match
|
||||||
the specified preconditions.
|
the specified preconditions.
|
||||||
</StyledTargetingHeader>
|
</StyledTargetingHeader>
|
||||||
@ -158,6 +268,7 @@ export const ReleasePlanTemplateAddStrategyForm = ({
|
|||||||
variant='contained'
|
variant='contained'
|
||||||
color='primary'
|
color='primary'
|
||||||
type='submit'
|
type='submit'
|
||||||
|
disabled={!hasValidConstraints || errors.hasFormErrors()}
|
||||||
onClick={addStrategyToMilestone}
|
onClick={addStrategyToMilestone}
|
||||||
>
|
>
|
||||||
Save strategy
|
Save strategy
|
||||||
|
Loading…
Reference in New Issue
Block a user