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:
parent
ea401f3ec5
commit
15bd0fbc84
@ -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();
|
||||||
|
@ -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%',
|
||||||
|
},
|
||||||
|
}));
|
@ -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;
|
|
||||||
|
@ -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';
|
||||||
|
Loading…
Reference in New Issue
Block a user