mirror of
https://github.com/Unleash/unleash.git
synced 2025-05-26 01:17:00 +02: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;
|
disabled?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
classes?: any;
|
classes?: any;
|
||||||
|
formControlStyles?: React.CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SelectMenu: React.FC<ISelectMenuProps> = ({
|
const SelectMenu: React.FC<ISelectMenuProps> = ({
|
||||||
@ -36,6 +37,7 @@ const SelectMenu: React.FC<ISelectMenuProps> = ({
|
|||||||
disabled = false,
|
disabled = false,
|
||||||
className,
|
className,
|
||||||
classes,
|
classes,
|
||||||
|
formControlStyles = {},
|
||||||
...rest
|
...rest
|
||||||
}) => {
|
}) => {
|
||||||
const renderSelectItems = () =>
|
const renderSelectItems = () =>
|
||||||
@ -51,7 +53,12 @@ const SelectMenu: React.FC<ISelectMenuProps> = ({
|
|||||||
));
|
));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormControl variant='outlined' size='small' classes={classes}>
|
<FormControl
|
||||||
|
variant='outlined'
|
||||||
|
size='small'
|
||||||
|
classes={classes}
|
||||||
|
style={formControlStyles}
|
||||||
|
>
|
||||||
<InputLabel htmlFor={id}>{label}</InputLabel>
|
<InputLabel htmlFor={id}>{label}</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
name={name}
|
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';
|
import { VFC } from 'react';
|
||||||
|
|
||||||
interface IFeatureStrategyEnabledDisabledProps {
|
interface IFeatureStrategyEnabledDisabledProps {
|
||||||
@ -6,9 +13,38 @@ interface IFeatureStrategyEnabledDisabledProps {
|
|||||||
onToggleEnabled: () => void;
|
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<
|
export const FeatureStrategyEnabledDisabled: VFC<
|
||||||
IFeatureStrategyEnabledDisabledProps
|
IFeatureStrategyEnabledDisabledProps
|
||||||
> = ({ enabled, onToggleEnabled }) => {
|
> = ({ 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 (
|
return (
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={
|
control={
|
||||||
|
@ -249,14 +249,6 @@ export const NewFeatureStrategyForm = ({
|
|||||||
}));
|
}));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<FeatureStrategyType
|
|
||||||
strategy={strategy}
|
|
||||||
strategyDefinition={strategyDefinition}
|
|
||||||
setStrategy={setStrategy}
|
|
||||||
validateParameter={validateParameter}
|
|
||||||
errors={errors}
|
|
||||||
hasAccess={access}
|
|
||||||
/>
|
|
||||||
<FeatureStrategyEnabledDisabled
|
<FeatureStrategyEnabledDisabled
|
||||||
enabled={!strategy?.disabled}
|
enabled={!strategy?.disabled}
|
||||||
onToggleEnabled={() =>
|
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 { IFeatureStrategyParameters } from 'interfaces/strategy';
|
||||||
import RolloutSlider from '../RolloutSlider/RolloutSlider';
|
import RolloutSlider from '../RolloutSlider/RolloutSlider';
|
||||||
import Input from 'component/common/Input/Input';
|
import Input from 'component/common/Input/Input';
|
||||||
@ -17,6 +17,7 @@ import Loader from '../../../common/Loader/Loader';
|
|||||||
import { useEffect, useMemo } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
import { useLocation } from 'react-router';
|
import { useLocation } from 'react-router';
|
||||||
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
|
|
||||||
interface IFlexibleStrategyProps {
|
interface IFlexibleStrategyProps {
|
||||||
parameters: IFeatureStrategyParameters;
|
parameters: IFeatureStrategyParameters;
|
||||||
@ -25,6 +26,31 @@ interface IFlexibleStrategyProps {
|
|||||||
editable: boolean;
|
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 = ({
|
const FlexibleStrategy = ({
|
||||||
updateParameter,
|
updateParameter,
|
||||||
parameters,
|
parameters,
|
||||||
@ -34,6 +60,8 @@ const FlexibleStrategy = ({
|
|||||||
const { defaultStickiness, loading } = useDefaultProjectSettings(projectId);
|
const { defaultStickiness, loading } = useDefaultProjectSettings(projectId);
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
|
const newStrategyConfiguration = useUiFlag('newStrategyConfiguration');
|
||||||
|
|
||||||
const isDefaultStrategyEdit = pathname.includes('default-strategy');
|
const isDefaultStrategyEdit = pathname.includes('default-strategy');
|
||||||
const onUpdate = (field: string) => (newValue: string) => {
|
const onUpdate = (field: string) => (newValue: string) => {
|
||||||
updateParameter(field, newValue);
|
updateParameter(field, newValue);
|
||||||
@ -70,6 +98,45 @@ const FlexibleStrategy = ({
|
|||||||
return <Loader />;
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<RolloutSlider
|
<RolloutSlider
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import Select from 'component/common/select';
|
import Select from 'component/common/select';
|
||||||
import { SelectChangeEvent, useTheme } from '@mui/material';
|
import { SelectChangeEvent, useTheme } from '@mui/material';
|
||||||
import useUnleashContext from 'hooks/api/getters/useUnleashContext/useUnleashContext';
|
import useUnleashContext from 'hooks/api/getters/useUnleashContext/useUnleashContext';
|
||||||
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
|
|
||||||
type OptionType = { key: string; label: string };
|
type OptionType = { key: string; label: string };
|
||||||
|
|
||||||
@ -22,6 +23,7 @@ export const StickinessSelect = ({
|
|||||||
dataTestId,
|
dataTestId,
|
||||||
}: IStickinessSelectProps) => {
|
}: IStickinessSelectProps) => {
|
||||||
const { context } = useUnleashContext();
|
const { context } = useUnleashContext();
|
||||||
|
const newStrategyConfiguration = useUiFlag('newStrategyConfiguration');
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
const resolveStickinessOptions = () => {
|
const resolveStickinessOptions = () => {
|
||||||
@ -51,6 +53,9 @@ export const StickinessSelect = ({
|
|||||||
return options;
|
return options;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// newStrategyConfiguration - Temporary check for backwards compatibility
|
||||||
|
const formControlStyles = newStrategyConfiguration ? { width: '100%' } : {};
|
||||||
|
|
||||||
const stickinessOptions = resolveStickinessOptions();
|
const stickinessOptions = resolveStickinessOptions();
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
@ -63,10 +68,10 @@ export const StickinessSelect = ({
|
|||||||
data-testid={dataTestId}
|
data-testid={dataTestId}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
style={{
|
style={{
|
||||||
width: 'inherit',
|
|
||||||
minWidth: '100%',
|
minWidth: '100%',
|
||||||
marginBottom: theme.spacing(2),
|
marginBottom: theme.spacing(2),
|
||||||
}}
|
}}
|
||||||
|
formControlStyles={formControlStyles}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { makeStyles, withStyles } from 'tss-react/mui';
|
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 { ROLLOUT_SLIDER_ID } from 'utils/testIds';
|
||||||
import React from 'react';
|
import { HelpIcon } from 'component/common/HelpIcon/HelpIcon';
|
||||||
|
|
||||||
const StyledSlider = withStyles(Slider, (theme) => ({
|
const StyledSlider = withStyles(Slider, (theme) => ({
|
||||||
root: {
|
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) => ({
|
const useStyles = makeStyles()((theme) => ({
|
||||||
slider: {
|
slider: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@ -79,14 +85,59 @@ const RolloutSlider = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes.slider}>
|
<div className={classes.slider}>
|
||||||
<Typography
|
<StyledBox>
|
||||||
id='discrete-slider-always'
|
<Typography id='discrete-slider-always'>{name}</Typography>
|
||||||
variant='h3'
|
<HelpIcon
|
||||||
gutterBottom
|
htmlTooltip
|
||||||
component='h3'
|
tooltip={
|
||||||
>
|
<Box>
|
||||||
{name}
|
<Typography
|
||||||
</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
|
<StyledSlider
|
||||||
min={0}
|
min={0}
|
||||||
max={100}
|
max={100}
|
||||||
|
Loading…
Reference in New Issue
Block a user