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