mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +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
 | 
				
			||||||
 | 
					            const { variants } = await res.json();
 | 
				
			||||||
 | 
					            mutate(FEATURE_CACHE_KEY, { ...feature, variants }, false);
 | 
				
			||||||
            setToastData({
 | 
					            setToastData({
 | 
				
			||||||
                show: true,
 | 
					                show: true,
 | 
				
			||||||
                type: 'success',
 | 
					                type: 'success',
 | 
				
			||||||
                text: successText,
 | 
					                text: successText,
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
            })
 | 
					        } catch (e) {
 | 
				
			||||||
            .catch(e => {
 | 
					            setToastData({
 | 
				
			||||||
                throw e;
 | 
					                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