1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-09-01 13:47:27 +02:00

feat: validate json (#764)

* feat: add isJSON function

* feat: validate JSON input

* feat: add JSON code editor

* feat: add error message for JSON payload

* feat: validate JSON input

* fix: merge conflict

* fix: conflict in AddFeatureVariant

* refactor: remove code editor for JSON input

* fix: update PR based on feedback

* fix: revert yarn.lock

* fix: revert yarn.lock

* fix: update PR based on feedback

* fix: styles

* fix: json input error message

* fix: remove ts-expect-error

* refactor: change inputProps type

* fix: import InputProps
This commit is contained in:
Youssef Khedher 2022-03-07 13:44:46 +01:00 committed by GitHub
parent ea401f3ec5
commit 15bd0fbc84
4 changed files with 88 additions and 63 deletions

View File

@ -1,4 +1,4 @@
import { InputLabelProps, TextField } from '@material-ui/core'; import { InputLabelProps, InputProps, TextField } from '@material-ui/core';
import { INPUT_ERROR_TEXT } from '../../../testIds'; import { INPUT_ERROR_TEXT } from '../../../testIds';
import { useStyles } from './Input.styles'; import { useStyles } from './Input.styles';
import React from 'react'; import React from 'react';
@ -15,6 +15,7 @@ interface IInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
onBlur?: (e: any) => any; onBlur?: (e: any) => any;
multiline?: boolean; multiline?: boolean;
rows?: number; rows?: number;
InputProps?: Partial<InputProps>;
InputLabelProps?: Partial<InputLabelProps>; InputLabelProps?: Partial<InputLabelProps>;
} }
@ -27,6 +28,7 @@ const Input = ({
className, className,
value, value,
onChange, onChange,
InputProps,
...rest ...rest
}: IInputProps) => { }: IInputProps) => {
const styles = useStyles(); const styles = useStyles();

View File

@ -0,0 +1,29 @@
import { makeStyles } from '@material-ui/core/styles';
export const useStyles = makeStyles(theme => ({
error: {
color: theme.palette.error.main,
fontSize: theme.fontSizes.smallBody,
position: 'relative',
},
input: {
maxWidth: 350,
width: '100%',
},
grid: {
marginBottom: '0.5rem',
},
weightInput: {
marginRight: '0.8rem',
},
label: { marginBottom: '1rem' },
info: {
width: '18.5px',
height: '18.5px',
color: 'grey',
},
select: {
minWidth: '100px',
width: '100%',
},
}));

View File

@ -5,24 +5,26 @@ import {
FormControlLabel, FormControlLabel,
Grid, Grid,
InputAdornment, InputAdornment,
TextField,
Tooltip, Tooltip,
} from '@material-ui/core'; } from '@material-ui/core';
import { Info } from '@material-ui/icons'; import { Info } from '@material-ui/icons';
import { weightTypes } from './enums'; import { weightTypes } from './enums';
import { OverrideConfig } from './OverrideConfig/OverrideConfig'; import { OverrideConfig } from './OverrideConfig/OverrideConfig';
import ConditionallyRender from '../../../../../common/ConditionallyRender'; import ConditionallyRender from 'component/common/ConditionallyRender';
import { useCommonStyles } from 'common.styles'; import { useCommonStyles } from 'common.styles';
import Dialogue from '../../../../../common/Dialogue'; import Dialogue from 'component/common/Dialogue';
import { modalStyles, trim } from 'component/common/util'; import { modalStyles, trim } from 'component/common/util';
import PermissionSwitch from '../../../../../common/PermissionSwitch/PermissionSwitch'; import PermissionSwitch from 'component/common/PermissionSwitch/PermissionSwitch';
import { UPDATE_FEATURE_VARIANTS } from 'component/providers/AccessProvider/permissions'; import { UPDATE_FEATURE_VARIANTS } from 'component/providers/AccessProvider/permissions';
import useFeature from '../../../../../../hooks/api/getters/useFeature/useFeature'; import useFeature from 'hooks/api/getters/useFeature/useFeature';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { IFeatureViewParams } from 'interfaces/params'; import { IFeatureViewParams } from 'interfaces/params';
import { IFeatureVariant, IOverride } from 'interfaces/featureToggle'; import { IFeatureVariant, IOverride } from 'interfaces/featureToggle';
import cloneDeep from 'lodash.clonedeep'; import cloneDeep from 'lodash.clonedeep';
import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect'; import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect';
import { useStyles } from './AddFeatureVariant.styles';
import Input from 'component/common/Input/Input';
import { formatUnknownError } from 'utils/format-unknown-error';
const payloadOptions = [ const payloadOptions = [
{ key: 'string', label: 'string' }, { key: 'string', label: 'string' },
@ -43,7 +45,7 @@ interface IAddVariantProps {
editing: boolean; editing: boolean;
} }
const AddVariant = ({ export const AddVariant = ({
showDialog, showDialog,
closeDialog, closeDialog,
save, save,
@ -53,6 +55,7 @@ const AddVariant = ({
title, title,
editing, editing,
}: IAddVariantProps) => { }: IAddVariantProps) => {
const styles = useStyles();
const [data, setData] = useState<Record<string, string>>({}); const [data, setData] = useState<Record<string, string>>({});
const [payload, setPayload] = useState(EMPTY_PAYLOAD); const [payload, setPayload] = useState(EMPTY_PAYLOAD);
const [overrides, setOverrides] = useState<IOverride[]>([]); const [overrides, setOverrides] = useState<IOverride[]>([]);
@ -62,6 +65,18 @@ const AddVariant = ({
const { feature } = useFeature(projectId, featureId); const { feature } = useFeature(projectId, featureId);
const [variants, setVariants] = useState<IFeatureVariant[]>([]); const [variants, setVariants] = useState<IFeatureVariant[]>([]);
const isValidJSON = (input: string): boolean => {
try {
JSON.parse(input);
return true;
} catch (e: unknown) {
setError({
payload: 'Invalid JSON',
});
return false;
}
};
const clear = () => { const clear = () => {
if (editVariant) { if (editVariant) {
setData({ setData({
@ -136,6 +151,11 @@ const AddVariant = ({
setError(weightValidation); setError(weightValidation);
return; return;
} }
const validJSON =
payload.type === 'json' && !isValidJSON(payload.value);
if (validJSON) {
return;
}
try { try {
const variant: IFeatureVariant = { const variant: IFeatureVariant = {
@ -154,19 +174,14 @@ const AddVariant = ({
await save(variant); await save(variant);
clear(); clear();
closeDialog(); closeDialog();
} catch (error) { } catch (e: unknown) {
// @ts-expect-error const error = formatUnknownError(e);
if (error?.body?.details[0]?.message?.includes('duplicate value')) { if (error.includes('duplicate value')) {
setError({ name: 'A variant with that name already exists.' }); setError({ name: 'A variant with that name already exists.' });
} else if ( } else if (error.includes('must be a number')) {
// @ts-expect-error
error?.body?.details[0]?.message?.includes('must be a number')
) {
setError({ weight: 'Weight must be a number' }); setError({ weight: 'Weight must be a number' });
} else { } else {
const msg = const msg = error || 'Could not add variant';
// @ts-expect-error
error?.body?.details[0]?.message || 'Could not add variant';
setError({ general: msg }); setError({ general: msg });
} }
} }
@ -174,6 +189,7 @@ const AddVariant = ({
const onPayload = (e: ChangeEvent<{ name?: string; value: unknown }>) => { const onPayload = (e: ChangeEvent<{ name?: string; value: unknown }>) => {
e.preventDefault(); e.preventDefault();
setError({ payload: '' });
setPayload({ setPayload({
...payload, ...payload,
// @ts-expect-error // @ts-expect-error
@ -248,20 +264,16 @@ const AddVariant = ({
onSubmit={submit} onSubmit={submit}
className={commonStyles.contentSpacingY} className={commonStyles.contentSpacingY}
> >
<p style={{ color: 'red' }}>{error.general}</p> <p className={styles.error}>{error.general}</p>
<TextField <Input
label="Variant name" label="Variant name"
autoFocus autoFocus
name="name" name="name"
placeholder="" className={styles.input}
className={commonStyles.fullWidth} errorText={error.name}
style={{ maxWidth: '350px' }}
helperText={error.name}
value={data.name || ''} value={data.name || ''}
error={Boolean(error.name)} error={Boolean(error.name)}
variant="outlined"
required required
size="small"
type="name" type="name"
disabled={editing} disabled={editing}
onChange={setVariantValue} onChange={setVariantValue}
@ -276,11 +288,7 @@ const AddVariant = ({
(!editing && variants.length > 0) (!editing && variants.length > 0)
} }
show={ show={
<Grid <Grid item md={12} className={styles.grid}>
item
md={12}
style={{ marginBottom: '0.5rem' }}
>
<FormControl> <FormControl>
<FormControlLabel <FormControlLabel
control={ control={
@ -308,13 +316,10 @@ const AddVariant = ({
condition={data.weightType === weightTypes.FIX} condition={data.weightType === weightTypes.FIX}
show={ show={
<Grid item md={4}> <Grid item md={4}>
<TextField <Input
id="weight" id="weight"
label="Weight" label="Weight"
name="weight" name="weight"
variant="outlined"
size="small"
placeholder=""
data-test={'VARIANT_WEIGHT_INPUT'} data-test={'VARIANT_WEIGHT_INPUT'}
InputProps={{ InputProps={{
endAdornment: ( endAdornment: (
@ -323,10 +328,10 @@ const AddVariant = ({
</InputAdornment> </InputAdornment>
), ),
}} }}
style={{ marginRight: '0.8rem' }} className={styles.weightInput}
value={data.weight} value={data.weight}
error={Boolean(error.weight)} error={Boolean(error.weight)}
helperText={error.weight} errorText={error.weight}
type="number" type="number"
disabled={!isFixWeight} disabled={!isFixWeight}
onChange={e => { onChange={e => {
@ -339,19 +344,13 @@ const AddVariant = ({
} }
/> />
</Grid> </Grid>
<p style={{ marginBottom: '1rem' }}> <p className={styles.label}>
<strong>Payload </strong> <strong>Payload </strong>
<Tooltip <Tooltip
title="Passed to the variant object. Can be anything title="Passed to the variant object. Can be anything
(json, value, csv)" (json, value, csv)"
> >
<Info <Info className={styles.info} />
style={{
width: '18.5px',
height: '18.5px',
color: 'grey',
}}
/>
</Tooltip> </Tooltip>
</p> </p>
<Grid container> <Grid container>
@ -360,40 +359,37 @@ const AddVariant = ({
id="variant-payload-type" id="variant-payload-type"
name="type" name="type"
label="Type" label="Type"
className={commonStyles.fullWidth} className={styles.select}
value={payload.type} value={payload.type}
options={payloadOptions} options={payloadOptions}
onChange={onPayload} onChange={onPayload}
style={{ minWidth: '100px', width: '100%' }}
/> />
</Grid> </Grid>
<Grid item md={8} sm={8} xs={6}> <Grid item md={8} sm={8} xs={6}>
<TextField <Input
rows={1} error={Boolean(error.payload)}
label="Value" errorText={error.payload}
name="value" name="value"
className={commonStyles.fullWidth} className={commonStyles.fullWidth}
value={payload.value} value={payload.value}
onChange={onPayload} onChange={onPayload}
variant="outlined"
size="small"
data-test={'VARIANT_PAYLOAD_VALUE'} data-test={'VARIANT_PAYLOAD_VALUE'}
placeholder={
payload.type === 'json'
? '{ "hello": "world" }'
: 'value'
}
label="value"
/> />
</Grid> </Grid>
</Grid> </Grid>
<ConditionallyRender <ConditionallyRender
condition={overrides.length > 0} condition={overrides.length > 0}
show={ show={
<p style={{ marginBottom: '1rem' }}> <p className={styles.label}>
<strong>Overrides </strong> <strong>Overrides </strong>
<Tooltip title="Here you can specify which users should get this variant."> <Tooltip title="Here you can specify which users should get this variant.">
<Info <Info className={styles.info} />
style={{
width: '18.5px',
height: '18.5px',
color: 'grey',
}}
/>
</Tooltip> </Tooltip>
</p> </p>
} }
@ -410,10 +406,8 @@ const AddVariant = ({
color="primary" color="primary"
> >
Add override Add override
</Button>{' '} </Button>
</form> </form>
</Dialogue> </Dialogue>
); );
}; };
export default AddVariant;

View File

@ -10,7 +10,7 @@ import {
TableRow, TableRow,
Typography, Typography,
} from '@material-ui/core'; } 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';
import useFeature from '../../../../../hooks/api/getters/useFeature/useFeature'; import useFeature from '../../../../../hooks/api/getters/useFeature/useFeature';