1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-04-01 01:18:10 +02:00

Fix/variants: Fix delete one variant + remove switch when add first variant ()

Co-authored-by: Ivar Conradi Østhus <ivarconr@gmail.com>
Co-authored-by: Fredrik Oseberg <fredrik.no@gmail.com>
This commit is contained in:
Youssef Khedher 2021-10-28 12:32:29 +01:00 committed by GitHub
parent df9c5c9b30
commit ed6efff643
11 changed files with 175 additions and 58 deletions
frontend
cypress/integration/feature-toggle
src
component
common
feature
FeatureView
FeatureView2/FeatureVariants/FeatureVariantsList
RedirectFeatureView
menu
__tests__/__snapshots__
routes.js
hooks/api/actions/useApi
store

View File

@ -276,8 +276,8 @@ describe('feature toggle', () => {
cy.wait('@variantcreation');
});
it('Can set weight to fixed value for one of the variants', () => {
const variantName = 'my-new-variant';
cy.wait(500);
cy.visit(`/projects/default/features2/${featureToggleName}/variants`);
cy.get('[data-test=VARIANT_EDIT_BUTTON]').first().click();
cy.get('[data-test=VARIANT_NAME_INPUT]')

View File

@ -50,6 +50,9 @@ export const trim = value => {
};
export function updateWeight(variants, totalWeight) {
if (variants.length === 0){
return [];
}
const variantMetadata = variants.reduce(
({ remainingPercentage, variableVariantCount }, variant) => {
if (variant.weight && variant.weightType === weightTypes.FIX) {

View File

@ -40,6 +40,8 @@ import { projectFilterGenerator } from '../../../utils/project-filter-generator'
import { getToggleCopyPath } from '../../../utils/route-path-helpers';
import useFeatureApi from '../../../hooks/api/actions/useFeatureApi/useFeatureApi';
import useToast from '../../../hooks/useToast';
import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
import { getTogglePath } from '../../../utils/route-path-helpers';
const FeatureView = ({
activeTab,
@ -68,6 +70,16 @@ const FeatureView = ({
const { changeFeatureProject } = useFeatureApi();
const { toast, setToastData } = useToast();
const archive = !Boolean(isFeatureView);
const { uiConfig } = useUiConfig();
useEffect(() => {
if(uiConfig.flags.E && featureToggle && featureToggle.project) {
const newTogglePAth = getTogglePath(featureToggle.project, featureToggleName, true);
history.push(newTogglePAth);
}
}, [featureToggleName, uiConfig, featureToggle, history])
useEffect(() => {
scrollToTop();

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import {
FormControl,
@ -21,6 +21,11 @@ import Dialogue from '../../../../../common/Dialogue';
import { trim, modalStyles } from '../../../../../common/util';
import PermissionSwitch from '../../../../../common/PermissionSwitch/PermissionSwitch';
import { UPDATE_FEATURE } from '../../../../../providers/AccessProvider/permissions';
import useFeature from '../../../../../../hooks/api/getters/useFeature/useFeature';
import { useParams } from 'react-router-dom';
import { IFeatureViewParams } from '../../../../../../interfaces/params';
import { IFeatureVariant } from '../../../../../../interfaces/featureToggle';
import cloneDeep from 'lodash.clonedeep';
const payloadOptions = [
{ key: 'string', label: 'string' },
@ -34,8 +39,8 @@ const AddVariant = ({
showDialog,
closeDialog,
save,
validateName,
editVariant,
validateName,
title,
editing,
}) => {
@ -44,6 +49,9 @@ const AddVariant = ({
const [overrides, setOverrides] = useState([]);
const [error, setError] = useState({});
const commonStyles = useCommonStyles();
const { projectId, featureId } = useParams<IFeatureViewParams>();
const { feature } = useFeature(projectId, featureId);
const [variants, setVariants] = useState<IFeatureVariant[]>([]);
const clear = () => {
if (editVariant) {
@ -55,6 +63,8 @@ const AddVariant = ({
});
if (editVariant.payload) {
setPayload(editVariant.payload);
} else {
setPayload(EMPTY_PAYLOAD);
}
if (editVariant.overrides) {
setOverrides(editVariant.overrides);
@ -69,6 +79,16 @@ const AddVariant = ({
setError({});
};
const setClonedVariants = clonedVariants =>
setVariants(cloneDeep(clonedVariants));
useEffect(() => {
if (feature) {
setClonedVariants(feature.variants);
}
/* eslint-disable-next-line react-hooks/exhaustive-deps */
}, [feature.variants]);
useEffect(() => {
clear();
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -119,10 +139,15 @@ const AddVariant = ({
clear();
closeDialog();
} catch (error) {
if (error.message.includes('duplicate value')) {
if (error?.body?.details[0]?.message?.includes('duplicate value')) {
setError({ name: 'A variant with that name already exists.' });
} else if (
error?.body?.details[0]?.message?.includes('must be a number')
) {
setError({ weight: 'Weight must be a number' });
} else {
const msg = error.message || 'Could not add variant';
const msg =
error?.body?.details[0]?.message || 'Could not add variant';
setError({ general: msg });
}
}
@ -215,46 +240,66 @@ const AddVariant = ({
/>
<br />
<Grid container>
<Grid item md={4}>
<TextField
id="weight"
label="Weight"
name="weight"
variant="outlined"
size="small"
placeholder=""
data-test={'VARIANT_WEIGHT_INPUT'}
InputProps={{
endAdornment: (
<InputAdornment position="start">
%
</InputAdornment>
),
}}
style={{ marginRight: '0.8rem' }}
value={data.weight || ''}
error={Boolean(error.weight)}
type="number"
disabled={!isFixWeight}
onChange={setVariantValue}
/>
</Grid>
<Grid item md={6}>
<FormControl>
<FormControlLabel
control={
<PermissionSwitch
permission={UPDATE_FEATURE}
name="weightType"
checked={isFixWeight}
data-test={'VARIANT_WEIGHT_TYPE'}
onChange={setVariantWeightType}
<ConditionallyRender
condition={variants.length > 0}
show={
<Grid
item
md={12}
style={{ marginBottom: '0.5rem' }}
>
<FormControl>
<FormControlLabel
control={
<PermissionSwitch
permission={UPDATE_FEATURE}
name="weightType"
checked={isFixWeight}
data-test={
'VARIANT_WEIGHT_TYPE'
}
onChange={setVariantWeightType}
/>
}
label="Custom percentage"
/>
}
label="Custom percentage"
/>
</FormControl>
</Grid>
</FormControl>
</Grid>
}
/>
<ConditionallyRender
condition={data.weightType === weightTypes.FIX}
show={
<Grid item md={4}>
<TextField
id="weight"
label="Weight"
name="weight"
variant="outlined"
size="small"
placeholder=""
data-test={'VARIANT_WEIGHT_INPUT'}
InputProps={{
endAdornment: (
<InputAdornment position="start">
%
</InputAdornment>
),
}}
style={{ marginRight: '0.8rem' }}
value={data.weight}
error={Boolean(error.weight)}
helperText={error.weight}
type="number"
disabled={!isFixWeight}
onChange={e => {
setVariantValue(e);
}}
/>
</Grid>
}
/>
</Grid>
<p style={{ marginBottom: '1rem' }}>
<strong>Payload </strong>
@ -339,6 +384,7 @@ AddVariant.propTypes = {
closeDialog: PropTypes.func.isRequired,
save: PropTypes.func.isRequired,
validateName: PropTypes.func.isRequired,
validateWeight: PropTypes.func.isRequired,
editVariant: PropTypes.object,
title: PropTypes.string,
uiConfig: PropTypes.object,

View File

@ -162,7 +162,6 @@ const FeatureOverviewVariants = () => {
};
const removeVariant = async (name: string) => {
console.log(`Removing variant ${name}`);
let updatedVariants = variants.filter(v => v.name !== name);
try {
await updateVariants(
@ -204,17 +203,21 @@ const FeatureOverviewVariants = () => {
successText: string
) => {
const newVariants = updateWeight(variants, 1000);
const patch = createPatch(newVariants);
if (patch.length === 0) return;
await patchFeatureToggle(projectId, featureId, patch);
refetch();
setToastData({
show: true,
type: 'success',
text: successText,
});
await patchFeatureToggle(projectId, featureId, patch)
.then(() => {
refetch();
setToastData({
show: true,
type: 'success',
text: successText,
});
})
.catch(e => {
throw e;
});
};
const validateName = (name: string) => {
@ -222,6 +225,14 @@ const FeatureOverviewVariants = () => {
return { name: 'Name is required' };
}
};
const validateWeight = (weight: number) => {
const weightValue = parseInt(weight);
if (weightValue > 100 || weightValue < 0) {
return { weight: 'weight must be between 0 and 100' };
}
};
const delDialogueMarkup = useDeleteVariantMarkup({
show: delDialog.show,
onClick: e => {
@ -280,7 +291,11 @@ const FeatureOverviewVariants = () => {
<PermissionButton
onClick={() => {
setEditing(false);
setEditVariant({});
if (variants.length === 0) {
setEditVariant({ weight: 1000 });
} else {
setEditVariant({ weightType: 'variable' });
}
setShowAddVariant(true);
}}
className={styles.addVariantButton}
@ -305,6 +320,7 @@ const FeatureOverviewVariants = () => {
}}
editing={editing}
validateName={validateName}
validateWeight={validateWeight}
editVariant={editVariant}
title={editing ? 'Edit variant' : 'Add variant'}
/>

View File

@ -6,11 +6,13 @@ interface IRedirectFeatureViewProps {
featureToggle: any;
features: any;
fetchFeatureToggles: () => void;
newPath: boolean;
}
const RedirectFeatureView = ({
featureToggle,
fetchFeatureToggles,
newPath = false,
}: IRedirectFeatureViewProps) => {
useEffect(() => {
if (!featureToggle) {
@ -22,7 +24,7 @@ const RedirectFeatureView = ({
if (!featureToggle) return null;
return (
<Redirect
to={getTogglePath(featureToggle?.project, featureToggle?.name)}
to={getTogglePath(featureToggle?.project, featureToggle?.name, newPath)}
/>
);
};

View File

@ -4,8 +4,11 @@ import { fetchFeatureToggles } from '../../../store/feature-toggle/actions';
import RedirectFeatureView from './RedirectFeatureView';
import { E } from '../../common/flags';
export default connect(
(state, props) => ({
newPath: !!state.uiConfig.toJS().flags[E],
featureToggle: state.features
.toJS()
.find(toggle => toggle.name === props.featureToggleName),

View File

@ -75,6 +75,15 @@ Array [
"title": "Create",
"type": "protected",
},
Object {
"component": [Function],
"layout": "main",
"menu": Object {},
"parent": "/features",
"path": "/projects/:projectId/features/:name",
"title": ":name",
"type": "protected",
},
Object {
"component": [Function],
"flag": "P",

View File

@ -117,6 +117,15 @@ export const routes = [
layout: 'main',
menu: {},
},
{
path: '/projects/:projectId/features/:name',
parent: '/features',
title: ':name',
component: RedirectFeatureViewPage,
type: 'protected',
layout: 'main',
menu: {},
},
{
path: '/projects/:id/:activeTab',
parent: '/projects',

View File

@ -8,6 +8,7 @@ import {
} from '../../../../constants/statusCodes';
import {
AuthenticationError,
BadRequestError,
ForbiddenError,
headers,
NotFoundError,
@ -107,7 +108,8 @@ const useAPI = ({
}
if (propagateErrors) {
throw new Error();
const response = await res.json();
throw new BadRequestError(res.status, response);
}
}
@ -160,7 +162,7 @@ const useAPI = ({
if (res.status > 399) {
const response = await res.json();
if (response?.details?.length > 0) {
const error = response.details[0];
if (propagateErrors) {

View File

@ -28,13 +28,28 @@ export class AuthenticationError extends Error {
export class ForbiddenError extends Error {
constructor(statusCode, body = {}) {
super(body.details?.length > 0 ? body.details[0].message : 'You cannot perform this action');
super(
body.details?.length > 0
? body.details[0].message
: 'You cannot perform this action'
);
this.name = 'ForbiddenError';
this.statusCode = statusCode;
this.body = body;
}
}
export class BadRequestError extends Error {
constructor(statusCode, body = {}) {
super(
body.details?.length > 0 ? body.details[0].message : 'Bad request'
);
this.name = 'BadRequestError';
this.statusCode = statusCode;
this.body = body;
}
}
export class NotFoundError extends Error {
constructor(statusCode) {
super(