1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-22 19:07:54 +01: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 { Search, ArrowDropDown } from '@mui/icons-material';
import { Autocomplete, styled } from '@mui/material';
import { Search, ArrowDropDown, Add } from '@mui/icons-material';
import { Autocomplete, styled, InputAdornment, useTheme } from '@mui/material';
import { AutocompleteRenderInputParams } from '@mui/material/Autocomplete';
import { TextField } from '@mui/material';
import { useUiFlag } from 'hooks/useUiFlag';
import { useState } from 'react';
interface IAutocompleteBoxProps {
label: string;
@ -54,12 +56,80 @@ export const AutocompleteBox = ({
onChange,
disabled,
}: IAutocompleteBoxProps) => {
const [placeHolder, setPlaceholder] = useState('Add Segments');
const { classes: styles } = useStyles();
const theme = useTheme();
const newStrategyConfiguration = useUiFlag('newStrategyConfiguration');
const renderInput = (params: AutocompleteRenderInputParams) => {
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 (
<StyledContainer>
<StyledIcon $disabled={Boolean(disabled)} aria-hidden>

View File

@ -1,6 +1,6 @@
import React, { forwardRef, Fragment, useImperativeHandle } from 'react';
import { Button, styled, Tooltip } from '@mui/material';
import { HelpOutline } from '@mui/icons-material';
import { Box, Button, styled, Tooltip, Typography } from '@mui/material';
import { Add, HelpOutline } from '@mui/icons-material';
import { IConstraint } from 'interfaces/strategy';
import { ConstraintAccordion } from 'component/common/ConstraintAccordion/ConstraintAccordion';
import produce from 'immer';
@ -10,6 +10,8 @@ import { objectId } from 'utils/objectId';
import { createEmptyConstraint } from 'component/common/ConstraintAccordion/ConstraintAccordionList/createEmptyConstraint';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator';
import { useUiFlag } from 'hooks/useUiFlag';
import { HelpIcon } from 'component/common/HelpIcon/HelpIcon';
interface IConstraintAccordionListProps {
constraints: IConstraint[];
@ -64,6 +66,13 @@ const StyledAddCustomLabel = styled('div')(({ theme }) => ({
display: 'flex',
}));
const StyledHelpIconBox = styled(Box)(({ theme }) => ({
display: 'flex',
alignItems: 'center',
marginTop: theme.spacing(1),
marginBottom: theme.spacing(1),
}));
export const ConstraintAccordionList = forwardRef<
IConstraintAccordionListRef | undefined,
IConstraintAccordionListProps
@ -78,6 +87,8 @@ export const ConstraintAccordionList = forwardRef<
>();
const { context } = useUnleashContext();
const newStrategyConfiguration = useUiFlag('newStrategyConfiguration');
const addConstraint =
setConstraints &&
((contextName: string) => {
@ -135,6 +146,86 @@ export const ConstraintAccordionList = forwardRef<
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 (
<StyledContainer id={constraintAccordionListId}>
<ConditionallyRender

View File

@ -1,6 +1,15 @@
import React, { useState } from 'react';
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 {
IFeatureStrategy,
IFeatureStrategyParameters,
@ -54,6 +63,19 @@ interface IFeatureStrategyFormProps {
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 }) => ({
display: 'grid',
gap: theme.spacing(2),
@ -74,6 +96,21 @@ const StyledButtons = styled('div')(({ theme }) => ({
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 = ({
projectId,
feature,
@ -274,11 +311,22 @@ export const NewFeatureStrategyForm = ({
condition={tab === 1}
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
segments={segments}
setSegments={setSegments}
projectId={projectId}
/>
<StyledBox>
<StyledDivider />
<StyledDividerContent>AND</StyledDividerContent>
</StyledBox>
<FeatureStrategyConstraints
projectId={feature.project}
environmentId={environmentId}

View File

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

View File

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