mirror of
https://github.com/Unleash/unleash.git
synced 2024-12-22 19:07:54 +01:00
Feat/new strategy configuration general tab (#5628)
* Adds the new general tab settings behind a feature flag * Adds a test for the FlexibleStrategy component
This commit is contained in:
parent
54316cace3
commit
c552f3ae72
@ -24,6 +24,7 @@ export interface ISelectMenuProps {
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
classes?: any;
|
||||
formControlStyles?: React.CSSProperties;
|
||||
}
|
||||
|
||||
const SelectMenu: React.FC<ISelectMenuProps> = ({
|
||||
@ -36,6 +37,7 @@ const SelectMenu: React.FC<ISelectMenuProps> = ({
|
||||
disabled = false,
|
||||
className,
|
||||
classes,
|
||||
formControlStyles = {},
|
||||
...rest
|
||||
}) => {
|
||||
const renderSelectItems = () =>
|
||||
@ -51,7 +53,12 @@ const SelectMenu: React.FC<ISelectMenuProps> = ({
|
||||
));
|
||||
|
||||
return (
|
||||
<FormControl variant='outlined' size='small' classes={classes}>
|
||||
<FormControl
|
||||
variant='outlined'
|
||||
size='small'
|
||||
classes={classes}
|
||||
style={formControlStyles}
|
||||
>
|
||||
<InputLabel htmlFor={id}>{label}</InputLabel>
|
||||
<Select
|
||||
name={name}
|
||||
|
@ -1,4 +1,11 @@
|
||||
import { FormControlLabel, Switch } from '@mui/material';
|
||||
import {
|
||||
Box,
|
||||
FormControlLabel,
|
||||
Switch,
|
||||
Typography,
|
||||
styled,
|
||||
} from '@mui/material';
|
||||
import { useUiFlag } from 'hooks/useUiFlag';
|
||||
import { VFC } from 'react';
|
||||
|
||||
interface IFeatureStrategyEnabledDisabledProps {
|
||||
@ -6,9 +13,38 @@ interface IFeatureStrategyEnabledDisabledProps {
|
||||
onToggleEnabled: () => void;
|
||||
}
|
||||
|
||||
const StyledBox = styled(Box)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
backgroundColor: theme.palette.background.elevation1,
|
||||
padding: theme.spacing(2),
|
||||
borderRadius: `${theme.shape.borderRadiusMedium}px`,
|
||||
}));
|
||||
|
||||
export const FeatureStrategyEnabledDisabled: VFC<
|
||||
IFeatureStrategyEnabledDisabledProps
|
||||
> = ({ enabled, onToggleEnabled }) => {
|
||||
const strategyConfigurationEnabled = useUiFlag('newStrategyConfiguration');
|
||||
|
||||
if (strategyConfigurationEnabled) {
|
||||
return (
|
||||
<StyledBox>
|
||||
<Typography>Strategy Status</Typography>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
name='enabled'
|
||||
onChange={onToggleEnabled}
|
||||
checked={enabled}
|
||||
/>
|
||||
}
|
||||
label='Enabled'
|
||||
/>
|
||||
</StyledBox>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FormControlLabel
|
||||
control={
|
||||
|
@ -249,14 +249,6 @@ export const NewFeatureStrategyForm = ({
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
<FeatureStrategyType
|
||||
strategy={strategy}
|
||||
strategyDefinition={strategyDefinition}
|
||||
setStrategy={setStrategy}
|
||||
validateParameter={validateParameter}
|
||||
errors={errors}
|
||||
hasAccess={access}
|
||||
/>
|
||||
<FeatureStrategyEnabledDisabled
|
||||
enabled={!strategy?.disabled}
|
||||
onToggleEnabled={() =>
|
||||
@ -266,6 +258,14 @@ export const NewFeatureStrategyForm = ({
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<FeatureStrategyType
|
||||
strategy={strategy}
|
||||
strategyDefinition={strategyDefinition}
|
||||
setStrategy={setStrategy}
|
||||
validateParameter={validateParameter}
|
||||
errors={errors}
|
||||
hasAccess={access}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
@ -0,0 +1,63 @@
|
||||
import { useState } from 'react';
|
||||
import { screen, fireEvent, within } from '@testing-library/react';
|
||||
import FlexibleStrategy from './FlexibleStrategy';
|
||||
import { render } from 'utils/testRenderer';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
import { testServerSetup, testServerRoute } from 'utils/testServer';
|
||||
|
||||
const server = testServerSetup();
|
||||
|
||||
const setupApi = () => {
|
||||
testServerRoute(server, '/api/admin/projects/default', {});
|
||||
};
|
||||
|
||||
test('manipulates the rollout slider', async () => {
|
||||
const Wrapper = () => {
|
||||
const [parameters, setParameters] = useState({
|
||||
groupId: 'testid',
|
||||
rollout: '0',
|
||||
stickiness: 'default',
|
||||
});
|
||||
|
||||
const updateParameter = (parameter: string, value: string) => {
|
||||
setParameters((prevParameters) => ({
|
||||
...prevParameters,
|
||||
[parameter]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<Routes>
|
||||
<Route
|
||||
path='/projects/:projectId/features/:featureName'
|
||||
element={
|
||||
<FlexibleStrategy
|
||||
parameters={parameters}
|
||||
updateParameter={updateParameter}
|
||||
context={{}}
|
||||
editable={true}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
);
|
||||
};
|
||||
|
||||
setupApi();
|
||||
|
||||
render(<Wrapper />, {
|
||||
route: '/projects/default/features/test',
|
||||
});
|
||||
|
||||
const slider = await screen.findByRole('slider', { name: /rollout/i });
|
||||
const groupIdInput = await screen.getByLabelText('groupId');
|
||||
|
||||
expect(slider).toHaveValue('0');
|
||||
expect(groupIdInput).toHaveValue('testid');
|
||||
|
||||
fireEvent.change(slider, { target: { value: '50' } });
|
||||
fireEvent.change(groupIdInput, { target: { value: 'newGroupId' } });
|
||||
|
||||
expect(slider).toHaveValue('50');
|
||||
expect(groupIdInput).toHaveValue('newGroupId');
|
||||
});
|
@ -1,4 +1,4 @@
|
||||
import { Typography } from '@mui/material';
|
||||
import { Box, Typography, styled } from '@mui/material';
|
||||
import { IFeatureStrategyParameters } from 'interfaces/strategy';
|
||||
import RolloutSlider from '../RolloutSlider/RolloutSlider';
|
||||
import Input from 'component/common/Input/Input';
|
||||
@ -17,6 +17,7 @@ import Loader from '../../../common/Loader/Loader';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
import { useLocation } from 'react-router';
|
||||
import { useUiFlag } from 'hooks/useUiFlag';
|
||||
|
||||
interface IFlexibleStrategyProps {
|
||||
parameters: IFeatureStrategyParameters;
|
||||
@ -25,6 +26,31 @@ interface IFlexibleStrategyProps {
|
||||
editable: boolean;
|
||||
}
|
||||
|
||||
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: '1rem',
|
||||
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 FlexibleStrategy = ({
|
||||
updateParameter,
|
||||
parameters,
|
||||
@ -34,6 +60,8 @@ const FlexibleStrategy = ({
|
||||
const { defaultStickiness, loading } = useDefaultProjectSettings(projectId);
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const newStrategyConfiguration = useUiFlag('newStrategyConfiguration');
|
||||
|
||||
const isDefaultStrategyEdit = pathname.includes('default-strategy');
|
||||
const onUpdate = (field: string) => (newValue: string) => {
|
||||
updateParameter(field, newValue);
|
||||
@ -70,6 +98,45 @@ const FlexibleStrategy = ({
|
||||
return <Loader />;
|
||||
}
|
||||
|
||||
if (newStrategyConfiguration) {
|
||||
return (
|
||||
<StyledBox>
|
||||
<RolloutSlider
|
||||
name='Rollout'
|
||||
value={rollout}
|
||||
disabled={!editable}
|
||||
onChange={updateRollout}
|
||||
/>
|
||||
<StyledOuterBox>
|
||||
<StyledInnerBox1>
|
||||
<StickinessSelect
|
||||
label='Stickiness'
|
||||
value={stickiness}
|
||||
editable={editable}
|
||||
dataTestId={FLEXIBLE_STRATEGY_STICKINESS_ID}
|
||||
onChange={(e) =>
|
||||
onUpdate('stickiness')(e.target.value)
|
||||
}
|
||||
/>
|
||||
</StyledInnerBox1>
|
||||
<StyledInnerBox2>
|
||||
<Input
|
||||
label='groupId'
|
||||
sx={{ width: '100%' }}
|
||||
id='groupId-input'
|
||||
value={parseParameterString(parameters.groupId)}
|
||||
disabled={!editable}
|
||||
onChange={(e) =>
|
||||
onUpdate('groupId')(e.target.value)
|
||||
}
|
||||
data-testid={FLEXIBLE_STRATEGY_GROUP_ID}
|
||||
/>
|
||||
</StyledInnerBox2>
|
||||
</StyledOuterBox>
|
||||
</StyledBox>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<RolloutSlider
|
||||
|
@ -1,6 +1,7 @@
|
||||
import Select from 'component/common/select';
|
||||
import { SelectChangeEvent, useTheme } from '@mui/material';
|
||||
import useUnleashContext from 'hooks/api/getters/useUnleashContext/useUnleashContext';
|
||||
import { useUiFlag } from 'hooks/useUiFlag';
|
||||
|
||||
type OptionType = { key: string; label: string };
|
||||
|
||||
@ -22,6 +23,7 @@ export const StickinessSelect = ({
|
||||
dataTestId,
|
||||
}: IStickinessSelectProps) => {
|
||||
const { context } = useUnleashContext();
|
||||
const newStrategyConfiguration = useUiFlag('newStrategyConfiguration');
|
||||
const theme = useTheme();
|
||||
|
||||
const resolveStickinessOptions = () => {
|
||||
@ -51,6 +53,9 @@ export const StickinessSelect = ({
|
||||
return options;
|
||||
};
|
||||
|
||||
// newStrategyConfiguration - Temporary check for backwards compatibility
|
||||
const formControlStyles = newStrategyConfiguration ? { width: '100%' } : {};
|
||||
|
||||
const stickinessOptions = resolveStickinessOptions();
|
||||
return (
|
||||
<Select
|
||||
@ -63,10 +68,10 @@ export const StickinessSelect = ({
|
||||
data-testid={dataTestId}
|
||||
onChange={onChange}
|
||||
style={{
|
||||
width: 'inherit',
|
||||
minWidth: '100%',
|
||||
marginBottom: theme.spacing(2),
|
||||
}}
|
||||
formControlStyles={formControlStyles}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { makeStyles, withStyles } from 'tss-react/mui';
|
||||
import { Slider, Typography } from '@mui/material';
|
||||
import { Slider, Typography, Box, styled } from '@mui/material';
|
||||
import { ROLLOUT_SLIDER_ID } from 'utils/testIds';
|
||||
import React from 'react';
|
||||
import { HelpIcon } from 'component/common/HelpIcon/HelpIcon';
|
||||
|
||||
const StyledSlider = withStyles(Slider, (theme) => ({
|
||||
root: {
|
||||
@ -25,6 +25,12 @@ const StyledSlider = withStyles(Slider, (theme) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledBox = styled(Box)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginBottom: theme.spacing(1),
|
||||
}));
|
||||
|
||||
const useStyles = makeStyles()((theme) => ({
|
||||
slider: {
|
||||
width: '100%',
|
||||
@ -79,14 +85,59 @@ const RolloutSlider = ({
|
||||
|
||||
return (
|
||||
<div className={classes.slider}>
|
||||
<Typography
|
||||
id='discrete-slider-always'
|
||||
variant='h3'
|
||||
gutterBottom
|
||||
component='h3'
|
||||
>
|
||||
{name}
|
||||
</Typography>
|
||||
<StyledBox>
|
||||
<Typography id='discrete-slider-always'>{name}</Typography>
|
||||
<HelpIcon
|
||||
htmlTooltip
|
||||
tooltip={
|
||||
<Box>
|
||||
<Typography
|
||||
variant='h3'
|
||||
sx={{ marginBottom: '1rem' }}
|
||||
>
|
||||
Rollout percentage
|
||||
</Typography>
|
||||
<Typography variant='body2'>
|
||||
The rollout percentage determines the proportion
|
||||
of users exposed to a feature. It's based on the
|
||||
MurmurHash of a user's unique identifier,
|
||||
normalized to a number between 1 and 100. If the
|
||||
normalized hash is less than or equal to the
|
||||
rollout percentage, the user sees the feature.
|
||||
This ensures a consistent, random distribution
|
||||
of the feature among users.
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
variant='h3'
|
||||
sx={{ marginBottom: '1rem', marginTop: '1rem' }}
|
||||
>
|
||||
Stickiness
|
||||
</Typography>
|
||||
<Typography variant='body2'>
|
||||
Stickiness refers to the value used for hashing
|
||||
to ensure a consistent user experience. It
|
||||
determines the input for the MurmurHash,
|
||||
ensuring that a user's feature exposure remains
|
||||
consistent across sessions.
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
variant='h3'
|
||||
sx={{ marginBottom: '1rem', marginTop: '1rem' }}
|
||||
>
|
||||
GroupId
|
||||
</Typography>
|
||||
<Typography variant='body2'>
|
||||
The groupId is used as a seed for the hash
|
||||
function, ensuring consistent feature exposure
|
||||
across different feature toggles for a uniform
|
||||
user experience.
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</StyledBox>
|
||||
<StyledSlider
|
||||
min={0}
|
||||
max={100}
|
||||
|
Loading…
Reference in New Issue
Block a user