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:
parent
1338496445
commit
53b32db278
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
|
@ -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 />
|
||||
</>
|
||||
);
|
||||
|
@ -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',
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user