mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-31 00:16:47 +01:00
feat: Use new Variants API (#518)
* feat: Use new Variants API Co-authored-by: Fredrik Strand Oseberg <fredrik.no@gmail.com>
This commit is contained in:
parent
b69606cd98
commit
83443627d9
@ -250,18 +250,18 @@ describe('feature toggle', () => {
|
|||||||
cy.visit(`/projects/default/features2/${featureToggleName}/variants`);
|
cy.visit(`/projects/default/features2/${featureToggleName}/variants`);
|
||||||
cy.intercept(
|
cy.intercept(
|
||||||
'PATCH',
|
'PATCH',
|
||||||
`/api/admin/projects/default/features/${featureToggleName}`,
|
`/api/admin/projects/default/features/${featureToggleName}/variants`,
|
||||||
req => {
|
req => {
|
||||||
if (req.body.length === 1) {
|
if (req.body.length === 1) {
|
||||||
expect(req.body[0].op).to.equal('add');
|
expect(req.body[0].op).to.equal('add');
|
||||||
expect(req.body[0].path).to.match(/variants/);
|
expect(req.body[0].path).to.match(/\//);
|
||||||
expect(req.body[0].value.name).to.equal(variantName);
|
expect(req.body[0].value.name).to.equal(variantName);
|
||||||
} else if (req.body.length === 2) {
|
} else if (req.body.length === 2) {
|
||||||
expect(req.body[0].op).to.equal('replace');
|
expect(req.body[0].op).to.equal('replace');
|
||||||
expect(req.body[0].path).to.match(/weight/);
|
expect(req.body[0].path).to.match(/weight/);
|
||||||
expect(req.body[0].value).to.equal(500);
|
expect(req.body[0].value).to.equal(500);
|
||||||
expect(req.body[1].op).to.equal('add');
|
expect(req.body[1].op).to.equal('add');
|
||||||
expect(req.body[1].path).to.match(/variants/);
|
expect(req.body[1].path).to.match(/\//);
|
||||||
expect(req.body[1].value.name).to.equal(secondVariantName);
|
expect(req.body[1].value.name).to.equal(secondVariantName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -291,7 +291,7 @@ describe('feature toggle', () => {
|
|||||||
cy.get('[data-test=VARIANT_WEIGHT_INPUT]').clear().type('15');
|
cy.get('[data-test=VARIANT_WEIGHT_INPUT]').clear().type('15');
|
||||||
cy.intercept(
|
cy.intercept(
|
||||||
'PATCH',
|
'PATCH',
|
||||||
`/api/admin/projects/default/features/${featureToggleName}`,
|
`/api/admin/projects/default/features/${featureToggleName}/variants`,
|
||||||
req => {
|
req => {
|
||||||
expect(req.body[0].op).to.equal('replace');
|
expect(req.body[0].op).to.equal('replace');
|
||||||
expect(req.body[0].path).to.match(/weight/);
|
expect(req.body[0].path).to.match(/weight/);
|
||||||
@ -320,10 +320,10 @@ describe('feature toggle', () => {
|
|||||||
cy.get('[data-test=DIALOGUE_CONFIRM_ID]').click();
|
cy.get('[data-test=DIALOGUE_CONFIRM_ID]').click();
|
||||||
cy.intercept(
|
cy.intercept(
|
||||||
'PATCH',
|
'PATCH',
|
||||||
`/api/admin/projects/default/features/${featureToggleName}`,
|
`/api/admin/projects/default/features/${featureToggleName}/variants`,
|
||||||
req => {
|
req => {
|
||||||
const e = req.body.find(e => e.op === 'remove');
|
const e = req.body.find(e => e.op === 'remove');
|
||||||
expect(e.path).to.match(/variants/);
|
expect(e.path).to.match(/\//);
|
||||||
}
|
}
|
||||||
).as('delete');
|
).as('delete');
|
||||||
cy.get(`[data-test=VARIANT_DELETE_BUTTON_${variantName}]`).click();
|
cy.get(`[data-test=VARIANT_DELETE_BUTTON_${variantName}]`).click();
|
||||||
|
@ -41,6 +41,7 @@ const AddVariant = ({
|
|||||||
save,
|
save,
|
||||||
editVariant,
|
editVariant,
|
||||||
validateName,
|
validateName,
|
||||||
|
validateWeight,
|
||||||
title,
|
title,
|
||||||
editing,
|
editing,
|
||||||
}) => {
|
}) => {
|
||||||
@ -115,9 +116,14 @@ const AddVariant = ({
|
|||||||
setError({});
|
setError({});
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const validationError = validateName(data.name);
|
const nameValidation = validateName(data.name);
|
||||||
if (validationError) {
|
if (nameValidation) {
|
||||||
setError(validationError);
|
setError(nameValidation);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const weightValidation = validateWeight(data.weight);
|
||||||
|
if (weightValidation) {
|
||||||
|
setError(weightValidation);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -240,8 +246,9 @@ const AddVariant = ({
|
|||||||
/>
|
/>
|
||||||
<br />
|
<br />
|
||||||
<Grid container>
|
<Grid container>
|
||||||
|
{/* If we're editing, we need to have at least 2 existing variants, since we require at least 1 variable. If adding, we could be adding nr 2, and as such should be allowed to set weightType to variable */}
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={variants.length > 0}
|
condition={(editing && variants.length > 1) || (!editing && variants.length > 0)}
|
||||||
show={
|
show={
|
||||||
<Grid
|
<Grid
|
||||||
item
|
item
|
||||||
@ -296,6 +303,8 @@ const AddVariant = ({
|
|||||||
onChange={e => {
|
onChange={e => {
|
||||||
setVariantValue(e);
|
setVariantValue(e);
|
||||||
}}
|
}}
|
||||||
|
aria-valuemin={0}
|
||||||
|
aria-valuemax={100}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
}
|
}
|
||||||
|
@ -2,14 +2,7 @@ import classnames from 'classnames';
|
|||||||
import * as jsonpatch from 'fast-json-patch';
|
import * as jsonpatch from 'fast-json-patch';
|
||||||
|
|
||||||
import styles from './variants.module.scss';
|
import styles from './variants.module.scss';
|
||||||
import {
|
import { Table, TableBody, TableCell, TableHead, TableRow, Typography } from '@material-ui/core';
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableRow,
|
|
||||||
Typography,
|
|
||||||
} from '@material-ui/core';
|
|
||||||
import AddVariant from './AddFeatureVariant/AddFeatureVariant';
|
import AddVariant from './AddFeatureVariant/AddFeatureVariant';
|
||||||
|
|
||||||
import { useContext, useEffect, useState } from 'react';
|
import { useContext, useEffect, useState } from 'react';
|
||||||
@ -29,16 +22,17 @@ import { updateWeight } from '../../../../common/util';
|
|||||||
import cloneDeep from 'lodash.clonedeep';
|
import cloneDeep from 'lodash.clonedeep';
|
||||||
import useDeleteVariantMarkup from './FeatureVariantsListItem/useDeleteVariantMarkup';
|
import useDeleteVariantMarkup from './FeatureVariantsListItem/useDeleteVariantMarkup';
|
||||||
import PermissionButton from '../../../../common/PermissionButton/PermissionButton';
|
import PermissionButton from '../../../../common/PermissionButton/PermissionButton';
|
||||||
|
import { mutate } from 'swr';
|
||||||
|
|
||||||
const FeatureOverviewVariants = () => {
|
const FeatureOverviewVariants = () => {
|
||||||
const { hasAccess } = useContext(AccessContext);
|
const { hasAccess } = useContext(AccessContext);
|
||||||
const { projectId, featureId } = useParams<IFeatureViewParams>();
|
const { projectId, featureId } = useParams<IFeatureViewParams>();
|
||||||
const { feature, refetch } = useFeature(projectId, featureId);
|
const { feature, FEATURE_CACHE_KEY } = useFeature(projectId, featureId);
|
||||||
const [variants, setVariants] = useState<IFeatureVariant[]>([]);
|
const [variants, setVariants] = useState<IFeatureVariant[]>([]);
|
||||||
const [editing, setEditing] = useState(false);
|
const [editing, setEditing] = useState(false);
|
||||||
const { context } = useUnleashContext();
|
const { context } = useUnleashContext();
|
||||||
const { toast, setToastData } = useToast();
|
const { toast, setToastData } = useToast();
|
||||||
const { patchFeatureToggle } = useFeatureApi();
|
const { patchFeatureVariants } = useFeatureApi();
|
||||||
const [editVariant, setEditVariant] = useState({});
|
const [editVariant, setEditVariant] = useState({});
|
||||||
const [showAddVariant, setShowAddVariant] = useState(false);
|
const [showAddVariant, setShowAddVariant] = useState(false);
|
||||||
const [stickinessOptions, setStickinessOptions] = useState([]);
|
const [stickinessOptions, setStickinessOptions] = useState([]);
|
||||||
@ -110,7 +104,7 @@ const FeatureOverviewVariants = () => {
|
|||||||
return (
|
return (
|
||||||
<section style={{ paddingTop: '16px' }}>
|
<section style={{ paddingTop: '16px' }}>
|
||||||
<GeneralSelect
|
<GeneralSelect
|
||||||
label="Stickiness"
|
label='Stickiness'
|
||||||
options={options}
|
options={options}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
@ -124,9 +118,9 @@ const FeatureOverviewVariants = () => {
|
|||||||
is used to ensure consistent traffic allocation across
|
is used to ensure consistent traffic allocation across
|
||||||
variants.{' '}
|
variants.{' '}
|
||||||
<a
|
<a
|
||||||
href="https://docs.getunleash.io/advanced/toggle_variants"
|
href='https://docs.getunleash.io/advanced/toggle_variants'
|
||||||
target="_blank"
|
target='_blank'
|
||||||
rel="noreferrer"
|
rel='noreferrer'
|
||||||
>
|
>
|
||||||
Read more
|
Read more
|
||||||
</a>
|
</a>
|
||||||
@ -145,8 +139,10 @@ const FeatureOverviewVariants = () => {
|
|||||||
if (patch.length === 0) return;
|
if (patch.length === 0) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await patchFeatureToggle(projectId, featureId, patch);
|
const res = await patchFeatureVariants(projectId, featureId, patch);
|
||||||
refetch();
|
// @ts-ignore
|
||||||
|
const { variants } = await res.json();
|
||||||
|
mutate(FEATURE_CACHE_KEY, { ...feature, variants }, false);
|
||||||
setToastData({
|
setToastData({
|
||||||
show: true,
|
show: true,
|
||||||
type: 'success',
|
type: 'success',
|
||||||
@ -166,7 +162,7 @@ const FeatureOverviewVariants = () => {
|
|||||||
try {
|
try {
|
||||||
await updateVariants(
|
await updateVariants(
|
||||||
updatedVariants,
|
updatedVariants,
|
||||||
'Successfully removed variant'
|
'Successfully removed variant',
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setToastData({
|
setToastData({
|
||||||
@ -179,7 +175,7 @@ const FeatureOverviewVariants = () => {
|
|||||||
const updateVariant = async (variant: IFeatureVariant) => {
|
const updateVariant = async (variant: IFeatureVariant) => {
|
||||||
const updatedVariants = cloneDeep(variants);
|
const updatedVariants = cloneDeep(variants);
|
||||||
const variantIdxToUpdate = updatedVariants.findIndex(
|
const variantIdxToUpdate = updatedVariants.findIndex(
|
||||||
(v: IFeatureVariant) => v.name === variant.name
|
(v: IFeatureVariant) => v.name === variant.name,
|
||||||
);
|
);
|
||||||
updatedVariants[variantIdxToUpdate] = variant;
|
updatedVariants[variantIdxToUpdate] = variant;
|
||||||
await updateVariants(updatedVariants, 'Successfully updated variant');
|
await updateVariants(updatedVariants, 'Successfully updated variant');
|
||||||
@ -194,30 +190,36 @@ const FeatureOverviewVariants = () => {
|
|||||||
|
|
||||||
await updateVariants(
|
await updateVariants(
|
||||||
[...variants, variant],
|
[...variants, variant],
|
||||||
'Successfully added a variant'
|
'Successfully added a variant',
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateVariants = async (
|
const updateVariants = async (
|
||||||
variants: IFeatureVariant[],
|
variants: IFeatureVariant[],
|
||||||
successText: string
|
successText: string,
|
||||||
) => {
|
) => {
|
||||||
const newVariants = updateWeight(variants, 1000);
|
const newVariants = updateWeight(variants, 1000);
|
||||||
const patch = createPatch(newVariants);
|
const patch = createPatch(newVariants);
|
||||||
|
|
||||||
if (patch.length === 0) return;
|
if (patch.length === 0) return;
|
||||||
await patchFeatureToggle(projectId, featureId, patch)
|
try {
|
||||||
.then(() => {
|
const res = await patchFeatureVariants(projectId, featureId, patch);
|
||||||
refetch();
|
// @ts-ignore
|
||||||
setToastData({
|
const { variants } = await res.json();
|
||||||
show: true,
|
mutate(FEATURE_CACHE_KEY, { ...feature, variants }, false);
|
||||||
type: 'success',
|
setToastData({
|
||||||
text: successText,
|
show: true,
|
||||||
});
|
type: 'success',
|
||||||
})
|
text: successText,
|
||||||
.catch(e => {
|
|
||||||
throw e;
|
|
||||||
});
|
});
|
||||||
|
} catch (e) {
|
||||||
|
setToastData({
|
||||||
|
show: true,
|
||||||
|
type: 'error',
|
||||||
|
text: e.toString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateName = (name: string) => {
|
const validateName = (name: string) => {
|
||||||
@ -248,17 +250,13 @@ const FeatureOverviewVariants = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const createPatch = (newVariants: IFeatureVariant[]) => {
|
const createPatch = (newVariants: IFeatureVariant[]) => {
|
||||||
const patch = jsonpatch
|
return jsonpatch
|
||||||
.compare(feature.variants, newVariants)
|
.compare(feature.variants, newVariants);
|
||||||
.map(patch => {
|
|
||||||
return { ...patch, path: `/variants${patch.path}` };
|
|
||||||
});
|
|
||||||
return patch;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section style={{ padding: '16px' }}>
|
<section style={{ padding: '16px' }}>
|
||||||
<Typography variant="body1">
|
<Typography variant='body1'>
|
||||||
Variants allows you to return a variant object if the feature
|
Variants allows you to return a variant object if the feature
|
||||||
toggle is considered enabled for the current request. When using
|
toggle is considered enabled for the current request. When using
|
||||||
variants you should use the{' '}
|
variants you should use the{' '}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { IFeatureToggleDTO } from '../../../../interfaces/featureToggle';
|
import { IFeatureToggleDTO } from '../../../../interfaces/featureToggle';
|
||||||
import { ITag } from '../../../../interfaces/tags';
|
import { ITag } from '../../../../interfaces/tags';
|
||||||
import useAPI from '../useApi/useApi';
|
import useAPI from '../useApi/useApi';
|
||||||
|
import { Operation } from 'fast-json-patch';
|
||||||
|
|
||||||
const useFeatureApi = () => {
|
const useFeatureApi = () => {
|
||||||
const { makeRequest, createRequest, errors, loading } = useAPI({
|
const { makeRequest, createRequest, errors, loading } = useAPI({
|
||||||
@ -182,6 +183,21 @@ const useFeatureApi = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const patchFeatureVariants = async (projectId: string, featureId: string, patchPayload: Operation[]) => {
|
||||||
|
const path = `api/admin/projects/${projectId}/features/${featureId}/variants`;
|
||||||
|
const req = createRequest(path, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: JSON.stringify(patchPayload),
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await makeRequest(req.caller, req.id);
|
||||||
|
return res;
|
||||||
|
} catch (e) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const cloneFeatureToggle = async (
|
const cloneFeatureToggle = async (
|
||||||
projectId: string,
|
projectId: string,
|
||||||
featureId: string,
|
featureId: string,
|
||||||
@ -213,6 +229,7 @@ const useFeatureApi = () => {
|
|||||||
deleteTagFromFeature,
|
deleteTagFromFeature,
|
||||||
archiveFeatureToggle,
|
archiveFeatureToggle,
|
||||||
patchFeatureToggle,
|
patchFeatureToggle,
|
||||||
|
patchFeatureVariants,
|
||||||
cloneFeatureToggle,
|
cloneFeatureToggle,
|
||||||
loading,
|
loading,
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user