1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-04-29 01:15:48 +02:00

Feat/new strategy configuration targeting tab (#5643)

This PR sets up the new targeting tab for strategy configuration:

<img width="1292" alt="Skjermbilde 2023-12-14 kl 11 24 11"
src="https://github.com/Unleash/unleash/assets/16081982/5c2d8f02-b3ec-49d4-b8bd-90f93ef3931c">
This commit is contained in:
Fredrik Strand Oseberg 2023-12-15 10:20:34 +01:00 committed by GitHub
parent 1338496445
commit 53b32db278
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 286 additions and 22 deletions

View File

@ -1,8 +1,10 @@
import { useStyles } from 'component/common/AutocompleteBox/AutocompleteBox.styles'; import { useStyles } from 'component/common/AutocompleteBox/AutocompleteBox.styles';
import { Search, ArrowDropDown } from '@mui/icons-material'; import { Search, ArrowDropDown, Add } from '@mui/icons-material';
import { Autocomplete, styled } from '@mui/material'; import { Autocomplete, styled, InputAdornment, useTheme } from '@mui/material';
import { AutocompleteRenderInputParams } from '@mui/material/Autocomplete'; import { AutocompleteRenderInputParams } from '@mui/material/Autocomplete';
import { TextField } from '@mui/material'; import { TextField } from '@mui/material';
import { useUiFlag } from 'hooks/useUiFlag';
import { useState } from 'react';
interface IAutocompleteBoxProps { interface IAutocompleteBoxProps {
label: string; label: string;
@ -54,12 +56,80 @@ export const AutocompleteBox = ({
onChange, onChange,
disabled, disabled,
}: IAutocompleteBoxProps) => { }: IAutocompleteBoxProps) => {
const [placeHolder, setPlaceholder] = useState('Add Segments');
const { classes: styles } = useStyles(); const { classes: styles } = useStyles();
const theme = useTheme();
const newStrategyConfiguration = useUiFlag('newStrategyConfiguration');
const renderInput = (params: AutocompleteRenderInputParams) => { const renderInput = (params: AutocompleteRenderInputParams) => {
return <TextField {...params} variant='outlined' label={label} />; return <TextField {...params} variant='outlined' label={label} />;
}; };
const renderCustomInput = (params: AutocompleteRenderInputParams) => {
const { InputProps } = params;
return (
<TextField
{...params}
InputProps={{
...InputProps,
startAdornment: (
<InputAdornment position='start'>
<Add
sx={{
height: 20,
width: 20,
color: theme.palette.primary.main,
}}
/>
</InputAdornment>
),
}}
variant='outlined'
sx={{
width: '215px',
'& .MuiOutlinedInput-root': {
'& .MuiInputBase-input': {
color: theme.palette.primary.main,
opacity: 1,
'&::placeholder': {
color: theme.palette.primary.main,
fontWeight: 'bold',
opacity: 1,
},
},
'& .MuiOutlinedInput-notchedOutline': {
borderColor: theme.palette.primary.main,
opacity: 0.5,
},
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
borderWidth: '1px',
},
},
}}
placeholder={placeHolder}
onFocus={() => setPlaceholder('')}
onBlur={() => setPlaceholder('Add Segments')}
/>
);
};
if (newStrategyConfiguration) {
return (
<StyledContainer>
<StyledAutocomplete
options={options}
value={value}
onChange={(event, value) => onChange(value || [])}
renderInput={renderCustomInput}
getOptionLabel={(value) => value.label}
disabled={disabled}
size='small'
multiple
/>
</StyledContainer>
);
}
return ( return (
<StyledContainer> <StyledContainer>
<StyledIcon $disabled={Boolean(disabled)} aria-hidden> <StyledIcon $disabled={Boolean(disabled)} aria-hidden>

View File

@ -1,6 +1,6 @@
import React, { forwardRef, Fragment, useImperativeHandle } from 'react'; import React, { forwardRef, Fragment, useImperativeHandle } from 'react';
import { Button, styled, Tooltip } from '@mui/material'; import { Box, Button, styled, Tooltip, Typography } from '@mui/material';
import { HelpOutline } from '@mui/icons-material'; import { Add, HelpOutline } from '@mui/icons-material';
import { IConstraint } from 'interfaces/strategy'; import { IConstraint } from 'interfaces/strategy';
import { ConstraintAccordion } from 'component/common/ConstraintAccordion/ConstraintAccordion'; import { ConstraintAccordion } from 'component/common/ConstraintAccordion/ConstraintAccordion';
import produce from 'immer'; import produce from 'immer';
@ -10,6 +10,8 @@ import { objectId } from 'utils/objectId';
import { createEmptyConstraint } from 'component/common/ConstraintAccordion/ConstraintAccordionList/createEmptyConstraint'; import { createEmptyConstraint } from 'component/common/ConstraintAccordion/ConstraintAccordionList/createEmptyConstraint';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator'; import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator';
import { useUiFlag } from 'hooks/useUiFlag';
import { HelpIcon } from 'component/common/HelpIcon/HelpIcon';
interface IConstraintAccordionListProps { interface IConstraintAccordionListProps {
constraints: IConstraint[]; constraints: IConstraint[];
@ -64,6 +66,13 @@ const StyledAddCustomLabel = styled('div')(({ theme }) => ({
display: 'flex', display: 'flex',
})); }));
const StyledHelpIconBox = styled(Box)(({ theme }) => ({
display: 'flex',
alignItems: 'center',
marginTop: theme.spacing(1),
marginBottom: theme.spacing(1),
}));
export const ConstraintAccordionList = forwardRef< export const ConstraintAccordionList = forwardRef<
IConstraintAccordionListRef | undefined, IConstraintAccordionListRef | undefined,
IConstraintAccordionListProps IConstraintAccordionListProps
@ -78,6 +87,8 @@ export const ConstraintAccordionList = forwardRef<
>(); >();
const { context } = useUnleashContext(); const { context } = useUnleashContext();
const newStrategyConfiguration = useUiFlag('newStrategyConfiguration');
const addConstraint = const addConstraint =
setConstraints && setConstraints &&
((contextName: string) => { ((contextName: string) => {
@ -135,6 +146,86 @@ export const ConstraintAccordionList = forwardRef<
return null; return null;
} }
if (newStrategyConfiguration) {
return (
<StyledContainer id={constraintAccordionListId}>
<ConditionallyRender
condition={Boolean(showCreateButton && onAdd)}
show={
<div>
<StyledHelpIconBox>
<Typography>Constraints</Typography>
<HelpIcon
htmlTooltip
tooltip={
<Box>
<Typography variant='body2'>
Constraints are advanced
targeting rules that you can
use to enable a feature
toggle for a subset of your
users. Read more about
constraints{' '}
<a
href='https://docs.getunleash.io/reference/strategy-constraints'
target='_blank'
rel='noopener noreferrer'
>
here
</a>
</Typography>
</Box>
}
/>
</StyledHelpIconBox>
{constraints.map((constraint, index) => (
<Fragment key={objectId(constraint)}>
<ConditionallyRender
condition={index > 0}
show={
<StrategySeparator text='AND' />
}
/>
<ConstraintAccordion
constraint={constraint}
onEdit={onEdit?.bind(
null,
constraint,
)}
onCancel={onCancel.bind(
null,
index,
)}
onDelete={onRemove?.bind(
null,
index,
)}
onSave={onSave?.bind(null, index)}
editing={Boolean(
state.get(constraint)?.editing,
)}
compact
/>
</Fragment>
))}
<Button
sx={{ marginTop: '1rem' }}
type='button'
onClick={onAdd}
startIcon={<Add />}
variant='outlined'
color='primary'
data-testid='ADD_CONSTRAINT_BUTTON'
>
Add constraint
</Button>
</div>
}
/>
</StyledContainer>
);
}
return ( return (
<StyledContainer id={constraintAccordionListId}> <StyledContainer id={constraintAccordionListId}>
<ConditionallyRender <ConditionallyRender

View File

@ -1,6 +1,15 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { Alert, Button, styled, Tabs, Tab } from '@mui/material'; import {
Alert,
Button,
styled,
Tabs,
Tab,
Typography,
Divider,
Box,
} from '@mui/material';
import { import {
IFeatureStrategy, IFeatureStrategy,
IFeatureStrategyParameters, IFeatureStrategyParameters,
@ -54,6 +63,19 @@ interface IFeatureStrategyFormProps {
setTab: React.Dispatch<React.SetStateAction<number>>; setTab: React.Dispatch<React.SetStateAction<number>>;
} }
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,
}));
const StyledForm = styled('form')(({ theme }) => ({ const StyledForm = styled('form')(({ theme }) => ({
display: 'grid', display: 'grid',
gap: theme.spacing(2), gap: theme.spacing(2),
@ -74,6 +96,21 @@ const StyledButtons = styled('div')(({ theme }) => ({
paddingBottom: theme.spacing(10), paddingBottom: theme.spacing(10),
})); }));
const StyledBox = styled(Box)(({ theme }) => ({
display: 'flex',
position: 'relative',
marginTop: theme.spacing(3.5),
}));
const StyledDivider = styled(Divider)(({ theme }) => ({
width: '100%',
}));
const StyledTargetingHeader = styled('div')(({ theme }) => ({
color: theme.palette.text.secondary,
marginTop: theme.spacing(1.5),
}));
export const NewFeatureStrategyForm = ({ export const NewFeatureStrategyForm = ({
projectId, projectId,
feature, feature,
@ -274,11 +311,22 @@ export const NewFeatureStrategyForm = ({
condition={tab === 1} condition={tab === 1}
show={ show={
<> <>
<StyledTargetingHeader>
Segmentation and constraints allow you to set
filters on your strategies, so that they will only
be evaluated for users and applications that match
the specified preconditions.
</StyledTargetingHeader>
<FeatureStrategySegment <FeatureStrategySegment
segments={segments} segments={segments}
setSegments={setSegments} setSegments={setSegments}
projectId={projectId} projectId={projectId}
/> />
<StyledBox>
<StyledDivider />
<StyledDividerContent>AND</StyledDividerContent>
</StyledBox>
<FeatureStrategyConstraints <FeatureStrategyConstraints
projectId={feature.project} projectId={feature.project}
environmentId={environmentId} environmentId={environmentId}

View File

@ -8,7 +8,9 @@ import {
import { FeatureStrategySegmentList } from 'component/feature/FeatureStrategy/FeatureStrategySegment/FeatureStrategySegmentList'; import { FeatureStrategySegmentList } from 'component/feature/FeatureStrategy/FeatureStrategySegment/FeatureStrategySegmentList';
import { SegmentDocsStrategyWarning } from 'component/segments/SegmentDocs'; import { SegmentDocsStrategyWarning } from 'component/segments/SegmentDocs';
import { useSegmentLimits } from 'hooks/api/getters/useSegmentLimits/useSegmentLimits'; import { useSegmentLimits } from 'hooks/api/getters/useSegmentLimits/useSegmentLimits';
import { Divider, styled, Typography } from '@mui/material'; import { Box, Divider, styled, Typography } from '@mui/material';
import { useUiFlag } from 'hooks/useUiFlag';
import { HelpIcon } from 'component/common/HelpIcon/HelpIcon';
interface IFeatureStrategySegmentProps { interface IFeatureStrategySegmentProps {
segments: ISegment[]; segments: ISegment[];
@ -20,6 +22,13 @@ const StyledDivider = styled(Divider)(({ theme }) => ({
fontSize: theme.fontSizes.smallBody, fontSize: theme.fontSizes.smallBody,
})); }));
const StyledHelpIconBox = styled(Box)(({ theme }) => ({
display: 'flex',
alignItems: 'center',
marginTop: theme.spacing(1),
marginBottom: theme.spacing(1),
}));
export const FeatureStrategySegment = ({ export const FeatureStrategySegment = ({
segments: selectedSegments, segments: selectedSegments,
setSegments: setSelectedSegments, setSegments: setSelectedSegments,
@ -28,6 +37,8 @@ export const FeatureStrategySegment = ({
const { segments: allSegments } = useSegments(); const { segments: allSegments } = useSegments();
const { strategySegmentsLimit } = useSegmentLimits(); const { strategySegmentsLimit } = useSegmentLimits();
const newStrategyConfiguration = useUiFlag('newStrategyConfiguration');
const atStrategySegmentsLimit: boolean = Boolean( const atStrategySegmentsLimit: boolean = Boolean(
strategySegmentsLimit && strategySegmentsLimit &&
selectedSegments.length >= strategySegmentsLimit, selectedSegments.length >= strategySegmentsLimit,
@ -59,6 +70,49 @@ export const FeatureStrategySegment = ({
} }
}; };
if (newStrategyConfiguration) {
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}
/>
<FeatureStrategySegmentList
segments={selectedSegments}
setSegments={setSelectedSegments}
/>
</>
);
}
return ( return (
<> <>
<Typography component='h3' sx={{ m: 0 }} variant='h3'> <Typography component='h3' sx={{ m: 0 }} variant='h3'>
@ -76,6 +130,7 @@ export const FeatureStrategySegment = ({
segments={selectedSegments} segments={selectedSegments}
setSegments={setSelectedSegments} setSegments={setSelectedSegments}
/> />
<StyledDivider /> <StyledDivider />
</> </>
); );

View File

@ -35,7 +35,7 @@ const StyledBox = styled(Box)(({ theme }) => ({
})); }));
const StyledOuterBox = styled(Box)(({ theme }) => ({ const StyledOuterBox = styled(Box)(({ theme }) => ({
marginTop: '1rem', marginTop: theme.spacing(1),
display: 'flex', display: 'flex',
width: '100%', width: '100%',
justifyContent: 'space-between', justifyContent: 'space-between',

View File

@ -25,6 +25,15 @@ const StyledSlider = withStyles(Slider, (theme) => ({
}, },
})); }));
const StyledHeader = styled(Typography)(({ theme }) => ({
marginBottom: theme.spacing(1),
}));
const StyledSubheader = styled(Typography)(({ theme }) => ({
marginBottom: theme.spacing(1),
marginTop: theme.spacing(1),
}));
const StyledBox = styled(Box)(({ theme }) => ({ const StyledBox = styled(Box)(({ theme }) => ({
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
@ -91,12 +100,9 @@ const RolloutSlider = ({
htmlTooltip htmlTooltip
tooltip={ tooltip={
<Box> <Box>
<Typography <StyledHeader variant='h3'>
variant='h3'
sx={{ marginBottom: '1rem' }}
>
Rollout percentage Rollout percentage
</Typography> </StyledHeader>
<Typography variant='body2'> <Typography variant='body2'>
The rollout percentage determines the proportion The rollout percentage determines the proportion
of users exposed to a feature. It's based on the of users exposed to a feature. It's based on the
@ -108,12 +114,9 @@ const RolloutSlider = ({
of the feature among users. of the feature among users.
</Typography> </Typography>
<Typography <StyledSubheader variant='h3'>
variant='h3'
sx={{ marginBottom: '1rem', marginTop: '1rem' }}
>
Stickiness Stickiness
</Typography> </StyledSubheader>
<Typography variant='body2'> <Typography variant='body2'>
Stickiness refers to the value used for hashing Stickiness refers to the value used for hashing
to ensure a consistent user experience. It to ensure a consistent user experience. It
@ -122,12 +125,9 @@ const RolloutSlider = ({
consistent across sessions. consistent across sessions.
</Typography> </Typography>
<Typography <StyledSubheader variant='h3'>
variant='h3'
sx={{ marginBottom: '1rem', marginTop: '1rem' }}
>
GroupId GroupId
</Typography> </StyledSubheader>
<Typography variant='body2'> <Typography variant='body2'>
The groupId is used as a seed for the hash The groupId is used as a seed for the hash
function, ensuring consistent feature exposure function, ensuring consistent feature exposure