1
0
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:
Fredrik Strand Oseberg 2023-12-13 12:34:43 +01:00 committed by GitHub
parent 54316cace3
commit c552f3ae72
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 251 additions and 22 deletions

View File

@ -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}

View File

@ -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={

View File

@ -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}
/>
</>
}
/>

View File

@ -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');
});

View File

@ -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

View File

@ -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}
/>
);
};

View File

@ -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}