diff --git a/frontend/cypress/integration/feature-toggle/feature.spec.js b/frontend/cypress/integration/feature-toggle/feature.spec.js index e82a0bf968..3fc213e00a 100644 --- a/frontend/cypress/integration/feature-toggle/feature.spec.js +++ b/frontend/cypress/integration/feature-toggle/feature.spec.js @@ -250,18 +250,18 @@ describe('feature toggle', () => { cy.visit(`/projects/default/features2/${featureToggleName}/variants`); cy.intercept( 'PATCH', - `/api/admin/projects/default/features/${featureToggleName}`, + `/api/admin/projects/default/features/${featureToggleName}/variants`, req => { if (req.body.length === 1) { 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); } else if (req.body.length === 2) { expect(req.body[0].op).to.equal('replace'); expect(req.body[0].path).to.match(/weight/); expect(req.body[0].value).to.equal(500); 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); } } @@ -291,7 +291,7 @@ describe('feature toggle', () => { cy.get('[data-test=VARIANT_WEIGHT_INPUT]').clear().type('15'); cy.intercept( 'PATCH', - `/api/admin/projects/default/features/${featureToggleName}`, + `/api/admin/projects/default/features/${featureToggleName}/variants`, req => { expect(req.body[0].op).to.equal('replace'); expect(req.body[0].path).to.match(/weight/); @@ -320,10 +320,10 @@ describe('feature toggle', () => { cy.get('[data-test=DIALOGUE_CONFIRM_ID]').click(); cy.intercept( 'PATCH', - `/api/admin/projects/default/features/${featureToggleName}`, + `/api/admin/projects/default/features/${featureToggleName}/variants`, req => { const e = req.body.find(e => e.op === 'remove'); - expect(e.path).to.match(/variants/); + expect(e.path).to.match(/\//); } ).as('delete'); cy.get(`[data-test=VARIANT_DELETE_BUTTON_${variantName}]`).click(); diff --git a/frontend/src/component/feature/FeatureView2/FeatureVariants/FeatureVariantsList/AddFeatureVariant/AddFeatureVariant.tsx b/frontend/src/component/feature/FeatureView2/FeatureVariants/FeatureVariantsList/AddFeatureVariant/AddFeatureVariant.tsx index 4d9a3b7153..43cd726101 100644 --- a/frontend/src/component/feature/FeatureView2/FeatureVariants/FeatureVariantsList/AddFeatureVariant/AddFeatureVariant.tsx +++ b/frontend/src/component/feature/FeatureView2/FeatureVariants/FeatureVariantsList/AddFeatureVariant/AddFeatureVariant.tsx @@ -41,6 +41,7 @@ const AddVariant = ({ save, editVariant, validateName, + validateWeight, title, editing, }) => { @@ -115,9 +116,14 @@ const AddVariant = ({ setError({}); e.preventDefault(); - const validationError = validateName(data.name); - if (validationError) { - setError(validationError); + const nameValidation = validateName(data.name); + if (nameValidation) { + setError(nameValidation); + return; + } + const weightValidation = validateWeight(data.weight); + if (weightValidation) { + setError(weightValidation); return; } @@ -240,8 +246,9 @@ const AddVariant = ({ />
+ {/* 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 */} 0} + condition={(editing && variants.length > 1) || (!editing && variants.length > 0)} show={ { setVariantValue(e); }} + aria-valuemin={0} + aria-valuemax={100} /> } diff --git a/frontend/src/component/feature/FeatureView2/FeatureVariants/FeatureVariantsList/FeatureVariantsList.tsx b/frontend/src/component/feature/FeatureView2/FeatureVariants/FeatureVariantsList/FeatureVariantsList.tsx index ec8ea38dea..b468a19f23 100644 --- a/frontend/src/component/feature/FeatureView2/FeatureVariants/FeatureVariantsList/FeatureVariantsList.tsx +++ b/frontend/src/component/feature/FeatureView2/FeatureVariants/FeatureVariantsList/FeatureVariantsList.tsx @@ -2,14 +2,7 @@ import classnames from 'classnames'; import * as jsonpatch from 'fast-json-patch'; import styles from './variants.module.scss'; -import { - Table, - TableBody, - TableCell, - TableHead, - TableRow, - Typography, -} from '@material-ui/core'; +import { Table, TableBody, TableCell, TableHead, TableRow, Typography } from '@material-ui/core'; import AddVariant from './AddFeatureVariant/AddFeatureVariant'; import { useContext, useEffect, useState } from 'react'; @@ -29,16 +22,17 @@ import { updateWeight } from '../../../../common/util'; import cloneDeep from 'lodash.clonedeep'; import useDeleteVariantMarkup from './FeatureVariantsListItem/useDeleteVariantMarkup'; import PermissionButton from '../../../../common/PermissionButton/PermissionButton'; +import { mutate } from 'swr'; const FeatureOverviewVariants = () => { const { hasAccess } = useContext(AccessContext); const { projectId, featureId } = useParams(); - const { feature, refetch } = useFeature(projectId, featureId); + const { feature, FEATURE_CACHE_KEY } = useFeature(projectId, featureId); const [variants, setVariants] = useState([]); const [editing, setEditing] = useState(false); const { context } = useUnleashContext(); const { toast, setToastData } = useToast(); - const { patchFeatureToggle } = useFeatureApi(); + const { patchFeatureVariants } = useFeatureApi(); const [editVariant, setEditVariant] = useState({}); const [showAddVariant, setShowAddVariant] = useState(false); const [stickinessOptions, setStickinessOptions] = useState([]); @@ -110,7 +104,7 @@ const FeatureOverviewVariants = () => { return (
{ is used to ensure consistent traffic allocation across variants.{' '} Read more @@ -145,8 +139,10 @@ const FeatureOverviewVariants = () => { if (patch.length === 0) return; try { - await patchFeatureToggle(projectId, featureId, patch); - refetch(); + const res = await patchFeatureVariants(projectId, featureId, patch); + // @ts-ignore + const { variants } = await res.json(); + mutate(FEATURE_CACHE_KEY, { ...feature, variants }, false); setToastData({ show: true, type: 'success', @@ -166,7 +162,7 @@ const FeatureOverviewVariants = () => { try { await updateVariants( updatedVariants, - 'Successfully removed variant' + 'Successfully removed variant', ); } catch (e) { setToastData({ @@ -179,7 +175,7 @@ const FeatureOverviewVariants = () => { const updateVariant = async (variant: IFeatureVariant) => { const updatedVariants = cloneDeep(variants); const variantIdxToUpdate = updatedVariants.findIndex( - (v: IFeatureVariant) => v.name === variant.name + (v: IFeatureVariant) => v.name === variant.name, ); updatedVariants[variantIdxToUpdate] = variant; await updateVariants(updatedVariants, 'Successfully updated variant'); @@ -194,30 +190,36 @@ const FeatureOverviewVariants = () => { await updateVariants( [...variants, variant], - 'Successfully added a variant' + 'Successfully added a variant', ); }; const updateVariants = async ( variants: IFeatureVariant[], - successText: string + successText: string, ) => { const newVariants = updateWeight(variants, 1000); const patch = createPatch(newVariants); if (patch.length === 0) return; - await patchFeatureToggle(projectId, featureId, patch) - .then(() => { - refetch(); - setToastData({ - show: true, - type: 'success', - text: successText, - }); - }) - .catch(e => { - throw e; + try { + const res = await patchFeatureVariants(projectId, featureId, patch); + // @ts-ignore + const { variants } = await res.json(); + mutate(FEATURE_CACHE_KEY, { ...feature, variants }, false); + setToastData({ + show: true, + type: 'success', + text: successText, }); + } catch (e) { + setToastData({ + show: true, + type: 'error', + text: e.toString(), + }); + } + }; const validateName = (name: string) => { @@ -248,17 +250,13 @@ const FeatureOverviewVariants = () => { }); const createPatch = (newVariants: IFeatureVariant[]) => { - const patch = jsonpatch - .compare(feature.variants, newVariants) - .map(patch => { - return { ...patch, path: `/variants${patch.path}` }; - }); - return patch; + return jsonpatch + .compare(feature.variants, newVariants); }; return (
- + Variants allows you to return a variant object if the feature toggle is considered enabled for the current request. When using variants you should use the{' '} diff --git a/frontend/src/hooks/api/actions/useFeatureApi/useFeatureApi.ts b/frontend/src/hooks/api/actions/useFeatureApi/useFeatureApi.ts index 0921a49599..360f5c0187 100644 --- a/frontend/src/hooks/api/actions/useFeatureApi/useFeatureApi.ts +++ b/frontend/src/hooks/api/actions/useFeatureApi/useFeatureApi.ts @@ -1,6 +1,7 @@ import { IFeatureToggleDTO } from '../../../../interfaces/featureToggle'; import { ITag } from '../../../../interfaces/tags'; import useAPI from '../useApi/useApi'; +import { Operation } from 'fast-json-patch'; const useFeatureApi = () => { 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 ( projectId: string, featureId: string, @@ -213,6 +229,7 @@ const useFeatureApi = () => { deleteTagFromFeature, archiveFeatureToggle, patchFeatureToggle, + patchFeatureVariants, cloneFeatureToggle, loading, };