1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-22 19:07:54 +01:00

fix: remove empty variants when changing tabs (#5850)

This PR makes a change to how variants work in the new setup. Variants
will now:

* Be removed if you change tab or unmount the component and it has no
name
* Moved StrategyVariants into a separate component to isolate this
change
* Add error handling around onSubmit and only trigger feedback if it's
successful
This commit is contained in:
Fredrik Strand Oseberg 2024-01-11 10:43:29 +01:00 committed by GitHub
parent 3a2d4ae60b
commit 2a723ea9e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 251 additions and 116 deletions

View File

@ -25,7 +25,7 @@ import { useSegments } from 'hooks/api/getters/useSegments/useSegments';
import { useUiFlag } from 'hooks/useUiFlag'; import { useUiFlag } from 'hooks/useUiFlag';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { NewFeatureStrategyForm } from 'component/feature/FeatureStrategy/FeatureStrategyForm/NewFeatureStrategyForm'; import { NewFeatureStrategyForm } from 'component/feature/FeatureStrategy/FeatureStrategyForm/NewFeatureStrategyForm';
import { StrategyVariants } from 'component/feature/StrategyTypes/StrategyVariants'; import { NewStrategyVariants } from 'component/feature/StrategyTypes/NewStrategyVariants';
interface IEditChangeProps { interface IEditChangeProps {
change: IChangeRequestAddStrategy | IChangeRequestUpdateStrategy; change: IChangeRequestAddStrategy | IChangeRequestUpdateStrategy;
@ -174,7 +174,7 @@ export const NewEditChange = ({
tab={tab} tab={tab}
setTab={setTab} setTab={setTab}
StrategyVariants={ StrategyVariants={
<StrategyVariants <NewStrategyVariants
strategy={strategy} strategy={strategy}
setStrategy={setStrategy} setStrategy={setStrategy}
environment={environment} environment={environment}

View File

@ -47,6 +47,7 @@ import EnvironmentIcon from 'component/common/EnvironmentIcon/EnvironmentIcon';
import { useFeedback } from 'component/feedbackNew/useFeedback'; import { useFeedback } from 'component/feedbackNew/useFeedback';
import { useUserSubmittedFeedback } from 'hooks/useSubmittedFeedback'; import { useUserSubmittedFeedback } from 'hooks/useSubmittedFeedback';
import { useUiFlag } from 'hooks/useUiFlag'; import { useUiFlag } from 'hooks/useUiFlag';
import { NewStrategyVariants } from 'component/feature/StrategyTypes/NewStrategyVariants';
interface IFeatureStrategyFormProps { interface IFeatureStrategyFormProps {
feature: IFeatureToggle; feature: IFeatureToggle;
@ -330,11 +331,15 @@ export const NewFeatureStrategyForm = ({
}; };
const onSubmitWithFeedback = async () => { const onSubmitWithFeedback = async () => {
try {
await onSubmit(); await onSubmit();
if (newStrategyConfigurationFeedback && !hasSubmittedFeedback) { if (newStrategyConfigurationFeedback && !hasSubmittedFeedback) {
createFeedbackContext(); createFeedbackContext();
} }
} catch (e) {
console.error(e);
}
}; };
const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => { const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => {

View File

@ -199,6 +199,38 @@ describe('NewFeatureStrategyCreate', () => {
const addVariantEl = await screen.findByText('Add variant'); const addVariantEl = await screen.findByText('Add variant');
fireEvent.click(addVariantEl); fireEvent.click(addVariantEl);
const inputElement = screen.getAllByRole('textbox')[0];
fireEvent.change(inputElement, {
target: { value: expectedVariantName },
});
const targetingEl = await screen.findByText('Targeting');
fireEvent.click(targetingEl);
const addConstraintEl = await screen.findByText('Add constraint');
expect(addConstraintEl).toBeInTheDocument();
fireEvent.click(variantsEl);
const inputElement2 = screen.getAllByRole('textbox')[0];
expect(inputElement2).not.toBeDisabled();
});
test('should remove empty variants when changing tabs', async () => {
setupComponent();
const titleEl = await screen.findByText('Gradual rollout');
expect(titleEl).toBeInTheDocument();
const variantsEl = screen.getByText('Variants');
fireEvent.click(variantsEl);
const addVariantEl = await screen.findByText('Add variant');
fireEvent.click(addVariantEl);
const variants = screen.queryAllByTestId('VARIANT');
expect(variants.length).toBe(1);
const targetingEl = await screen.findByText('Targeting'); const targetingEl = await screen.findByText('Targeting');
fireEvent.click(targetingEl); fireEvent.click(targetingEl);
@ -207,12 +239,8 @@ describe('NewFeatureStrategyCreate', () => {
fireEvent.click(variantsEl); fireEvent.click(variantsEl);
const inputElement = screen.getAllByRole('textbox')[0]; const variants2 = screen.queryAllByTestId('VARIANT');
fireEvent.change(inputElement, { expect(variants2.length).toBe(0);
target: { value: expectedVariantName },
});
expect(screen.getByText(expectedVariantName)).toBeInTheDocument();
}); });
test('Should autosave constraint settings when navigating between tabs', async () => { test('Should autosave constraint settings when navigating between tabs', async () => {

View File

@ -34,7 +34,7 @@ import useQueryParams from 'hooks/useQueryParams';
import { useSegments } from 'hooks/api/getters/useSegments/useSegments'; import { useSegments } from 'hooks/api/getters/useSegments/useSegments';
import { useDefaultStrategy } from '../../../project/Project/ProjectSettings/ProjectDefaultStrategySettings/ProjectEnvironment/ProjectEnvironmentDefaultStrategy/EditDefaultStrategy'; import { useDefaultStrategy } from '../../../project/Project/ProjectSettings/ProjectDefaultStrategySettings/ProjectEnvironment/ProjectEnvironmentDefaultStrategy/EditDefaultStrategy';
import { NewFeatureStrategyForm } from 'component/feature/FeatureStrategy/FeatureStrategyForm/NewFeatureStrategyForm'; import { NewFeatureStrategyForm } from 'component/feature/FeatureStrategy/FeatureStrategyForm/NewFeatureStrategyForm';
import { StrategyVariants } from 'component/feature/StrategyTypes/StrategyVariants'; import { NewStrategyVariants } from 'component/feature/StrategyTypes/NewStrategyVariants';
export const NewFeatureStrategyCreate = () => { export const NewFeatureStrategyCreate = () => {
const [tab, setTab] = useState(0); const [tab, setTab] = useState(0);
@ -211,7 +211,7 @@ export const NewFeatureStrategyCreate = () => {
tab={tab} tab={tab}
setTab={setTab} setTab={setTab}
StrategyVariants={ StrategyVariants={
<StrategyVariants <NewStrategyVariants
strategy={strategy} strategy={strategy}
setStrategy={setStrategy} setStrategy={setStrategy}
environment={environmentId} environment={environmentId}

View File

@ -29,7 +29,7 @@ import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useCh
import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests'; import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
import { NewFeatureStrategyForm } from 'component/feature/FeatureStrategy/FeatureStrategyForm/NewFeatureStrategyForm'; import { NewFeatureStrategyForm } from 'component/feature/FeatureStrategy/FeatureStrategyForm/NewFeatureStrategyForm';
import { StrategyVariants } from 'component/feature/StrategyTypes/StrategyVariants'; import { NewStrategyVariants } from 'component/feature/StrategyTypes/NewStrategyVariants';
const useTitleTracking = () => { const useTitleTracking = () => {
const [previousTitle, setPreviousTitle] = useState<string>(''); const [previousTitle, setPreviousTitle] = useState<string>('');
@ -233,7 +233,7 @@ export const NewFeatureStrategyEdit = () => {
tab={tab} tab={tab}
setTab={setTab} setTab={setTab}
StrategyVariants={ StrategyVariants={
<StrategyVariants <NewStrategyVariants
strategy={strategy} strategy={strategy}
setStrategy={setStrategy} setStrategy={setStrategy}
environment={environmentId} environment={environmentId}

View File

@ -0,0 +1,202 @@
import { VariantForm } from '../FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantsModal/VariantForm/VariantForm';
import { updateWeightEdit } from '../../common/util';
import React, { FC, useEffect, useState } from 'react';
import { IFeatureVariantEdit } from '../FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantsModal/EnvironmentVariantsModal';
import PermissionButton from '../../common/PermissionButton/PermissionButton';
import { UPDATE_FEATURE_ENVIRONMENT_VARIANTS } from '../../providers/AccessProvider/permissions';
import { v4 as uuidv4 } from 'uuid';
import { WeightType } from '../../../constants/variantTypes';
import { Box, styled, Typography, useTheme } from '@mui/material';
import { IFeatureStrategy } from 'interfaces/strategy';
import SplitPreviewSlider from './SplitPreviewSlider/SplitPreviewSlider';
import { HelpIcon } from '../../common/HelpIcon/HelpIcon';
import { StrategyVariantsUpgradeAlert } from '../../common/StrategyVariantsUpgradeAlert/StrategyVariantsUpgradeAlert';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
import { useUiFlag } from 'hooks/useUiFlag';
import { Add } from '@mui/icons-material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
const StyledVariantForms = styled('div')({
display: 'flex',
flexDirection: 'column',
});
const StyledHelpIconBox = styled(Box)(({ theme }) => ({
display: 'flex',
alignItems: 'center',
marginTop: theme.spacing(1),
marginBottom: theme.spacing(1),
}));
const StyledVariantsHeader = styled('div')(({ theme }) => ({
color: theme.palette.text.secondary,
marginTop: theme.spacing(1.5),
}));
export const NewStrategyVariants: FC<{
setStrategy: React.Dispatch<
React.SetStateAction<Partial<IFeatureStrategy>>
>;
strategy: Partial<IFeatureStrategy>;
projectId: string;
environment: string;
editable?: boolean;
}> = ({ strategy, setStrategy, projectId, environment, editable }) => {
const { trackEvent } = usePlausibleTracker();
const [variantsEdit, setVariantsEdit] = useState<IFeatureVariantEdit[]>([]);
const theme = useTheme();
const stickiness =
strategy?.parameters && 'stickiness' in strategy?.parameters
? String(strategy.parameters.stickiness)
: 'default';
useEffect(() => {
return () => {
setStrategy((prev) => ({
...prev,
variants: variantsEdit.filter((variant) =>
Boolean(variant.name),
),
}));
};
}, [JSON.stringify(variantsEdit)]);
useEffect(() => {
setVariantsEdit(
(strategy.variants || []).map((variant) => ({
...variant,
new: editable || false,
isValid: true,
id: uuidv4(),
overrides: [],
})),
);
}, []);
useEffect(() => {
setStrategy((prev) => ({
...prev,
variants: variantsEdit.map((variant) => ({
stickiness,
name: variant.name,
weight: variant.weight,
payload: variant.payload,
weightType: variant.weightType,
})),
}));
}, [stickiness, JSON.stringify(variantsEdit)]);
const updateVariant = (updatedVariant: IFeatureVariantEdit, id: string) => {
setVariantsEdit((prevVariants) =>
updateWeightEdit(
prevVariants.map((prevVariant) =>
prevVariant.id === id ? updatedVariant : prevVariant,
),
1000,
),
);
};
const addVariant = () => {
const id = uuidv4();
setVariantsEdit((variantsEdit) => [
...variantsEdit,
{
name: '',
weightType: WeightType.VARIABLE,
weight: 0,
stickiness,
new: true,
isValid: false,
id,
},
]);
trackEvent('strategy-variants', {
props: {
eventType: 'variant added',
},
});
};
return (
<>
<StyledVariantsHeader>
Variants enhance a feature flag by providing a version of the
feature to be enabled
</StyledVariantsHeader>
<StyledHelpIconBox>
<Typography>Variants</Typography>
<HelpIcon
htmlTooltip
tooltip={
<Box>
<Typography variant='body2'>
Variants in feature toggling allow you to serve
different versions of a feature to different
users. This can be used for A/B testing, gradual
rollouts, and canary releases. Variants provide
a way to control the user experience at a
granular level, enabling you to test and
optimize different aspects of your features.
Read more about variants{' '}
<a
href='https://docs.getunleash.io/reference/strategy-variants'
target='_blank'
rel='noopener noreferrer'
>
here
</a>
</Typography>
</Box>
}
/>
</StyledHelpIconBox>
<StyledVariantForms>
<ConditionallyRender
condition={variantsEdit.length > 0}
show={<StrategyVariantsUpgradeAlert />}
/>
{variantsEdit.map((variant, i) => (
<VariantForm
disableOverrides={true}
key={variant.id}
variant={variant}
variants={variantsEdit}
updateVariant={(updatedVariant) =>
updateVariant(updatedVariant, variant.id)
}
removeVariant={() =>
setVariantsEdit((variantsEdit) =>
updateWeightEdit(
variantsEdit.filter(
(v) => v.id !== variant.id,
),
1000,
),
)
}
decorationColor={
theme.palette.variants[
i % theme.palette.variants.length
]
}
/>
))}
</StyledVariantForms>
<PermissionButton
onClick={addVariant}
variant='outlined'
permission={UPDATE_FEATURE_ENVIRONMENT_VARIANTS}
projectId={projectId}
environmentId={environment}
data-testid='ADD_STRATEGY_VARIANT_BUTTON'
startIcon={<Add />}
>
Add variant
</PermissionButton>
<SplitPreviewSlider variants={variantsEdit} />
</>
);
};

View File

@ -6,33 +6,18 @@ import PermissionButton from '../../common/PermissionButton/PermissionButton';
import { UPDATE_FEATURE_ENVIRONMENT_VARIANTS } from '../../providers/AccessProvider/permissions'; import { UPDATE_FEATURE_ENVIRONMENT_VARIANTS } from '../../providers/AccessProvider/permissions';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { WeightType } from '../../../constants/variantTypes'; import { WeightType } from '../../../constants/variantTypes';
import { Box, Link, styled, Typography, useTheme } from '@mui/material'; import { Link, styled, Typography, useTheme } from '@mui/material';
import { IFeatureStrategy } from 'interfaces/strategy'; import { IFeatureStrategy } from 'interfaces/strategy';
import SplitPreviewSlider from './SplitPreviewSlider/SplitPreviewSlider'; import SplitPreviewSlider from './SplitPreviewSlider/SplitPreviewSlider';
import { HelpIcon } from '../../common/HelpIcon/HelpIcon'; import { HelpIcon } from '../../common/HelpIcon/HelpIcon';
import { StrategyVariantsUpgradeAlert } from '../../common/StrategyVariantsUpgradeAlert/StrategyVariantsUpgradeAlert'; import { StrategyVariantsUpgradeAlert } from '../../common/StrategyVariantsUpgradeAlert/StrategyVariantsUpgradeAlert';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
import { useUiFlag } from 'hooks/useUiFlag';
import { Add } from '@mui/icons-material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
const StyledVariantForms = styled('div')({ const StyledVariantForms = styled('div')({
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
}); });
const StyledHelpIconBox = styled(Box)(({ theme }) => ({
display: 'flex',
alignItems: 'center',
marginTop: theme.spacing(1),
marginBottom: theme.spacing(1),
}));
const StyledVariantsHeader = styled('div')(({ theme }) => ({
color: theme.palette.text.secondary,
marginTop: theme.spacing(1.5),
}));
export const StrategyVariants: FC<{ export const StrategyVariants: FC<{
setStrategy: React.Dispatch< setStrategy: React.Dispatch<
React.SetStateAction<Partial<IFeatureStrategy>> React.SetStateAction<Partial<IFeatureStrategy>>
@ -45,7 +30,6 @@ export const StrategyVariants: FC<{
const { trackEvent } = usePlausibleTracker(); const { trackEvent } = usePlausibleTracker();
const [variantsEdit, setVariantsEdit] = useState<IFeatureVariantEdit[]>([]); const [variantsEdit, setVariantsEdit] = useState<IFeatureVariantEdit[]>([]);
const theme = useTheme(); const theme = useTheme();
const newStrategyConfiguration = useUiFlag('newStrategyConfiguration');
const stickiness = const stickiness =
strategy?.parameters && 'stickiness' in strategy?.parameters strategy?.parameters && 'stickiness' in strategy?.parameters
@ -109,90 +93,6 @@ export const StrategyVariants: FC<{
}); });
}; };
if (newStrategyConfiguration) {
return (
<>
<StyledVariantsHeader>
Variants enhance a feature flag by providing a version of
the feature to be enabled
</StyledVariantsHeader>
<StyledHelpIconBox>
<Typography>Variants</Typography>
<HelpIcon
htmlTooltip
tooltip={
<Box>
<Typography variant='body2'>
Variants in feature toggling allow you to
serve different versions of a feature to
different users. This can be used for A/B
testing, gradual rollouts, and canary
releases. Variants provide a way to control
the user experience at a granular level,
enabling you to test and optimize different
aspects of your features. Read more about
variants{' '}
<a
href='https://docs.getunleash.io/reference/strategy-variants'
target='_blank'
rel='noopener noreferrer'
>
here
</a>
</Typography>
</Box>
}
/>
</StyledHelpIconBox>
<StyledVariantForms>
<ConditionallyRender
condition={variantsEdit.length > 0}
show={<StrategyVariantsUpgradeAlert />}
/>
{variantsEdit.map((variant, i) => (
<VariantForm
disableOverrides={true}
key={variant.id}
variant={variant}
variants={variantsEdit}
updateVariant={(updatedVariant) =>
updateVariant(updatedVariant, variant.id)
}
removeVariant={() =>
setVariantsEdit((variantsEdit) =>
updateWeightEdit(
variantsEdit.filter(
(v) => v.id !== variant.id,
),
1000,
),
)
}
decorationColor={
theme.palette.variants[
i % theme.palette.variants.length
]
}
/>
))}
</StyledVariantForms>
<PermissionButton
onClick={addVariant}
variant='outlined'
permission={UPDATE_FEATURE_ENVIRONMENT_VARIANTS}
projectId={projectId}
environmentId={environment}
data-testid='ADD_STRATEGY_VARIANT_BUTTON'
startIcon={<Add />}
>
Add variant
</PermissionButton>
<SplitPreviewSlider variants={variantsEdit} />
</>
);
}
return ( return (
<> <>
<Typography <Typography