1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-22 19:07:54 +01:00

Playground form (#1129)

* playground form

* playground context fields

* playground interactive json form

* add playground route

* remove memo from select options generation

* add toast when playground context parsing fails

* add error handling when adding a fiedl to playground context

* remove playground context options memo
This commit is contained in:
Tymoteusz Czech 2022-07-08 09:35:32 +02:00 committed by GitHub
parent 12ffc0474b
commit 2e94cd660c
6 changed files with 385 additions and 0 deletions

View File

@ -127,6 +127,12 @@ const Header: VFC = () => {
<Link to="/features" className={themeStyles.focusable}>
Feature toggles
</Link>
<Link
to="/playground"
className={themeStyles.focusable}
>
Playground
</Link>
<button
className={styles.advancedNavButton}
onClick={e => setConfigRef(e.currentTarget)}

View File

@ -119,6 +119,16 @@ exports[`returns all baseRoutes 1`] = `
"title": "Feature toggles",
"type": "protected",
},
{
"component": [Function],
"hidden": false,
"menu": {
"mobile": true,
},
"path": "/playground",
"title": "Playground",
"type": "protected",
},
{
"component": [Function],
"menu": {},

View File

@ -52,6 +52,7 @@ import { SegmentTable } from 'component/segments/SegmentTable/SegmentTable';
import FlaggedBillingRedirect from 'component/admin/billing/FlaggedBillingRedirect/FlaggedBillingRedirect';
import { FeaturesArchiveTable } from '../archive/FeaturesArchiveTable';
import { Billing } from 'component/admin/billing/Billing';
import { Playground } from 'component/playground/Playground/Playground';
export const routes: IRoute[] = [
// Splash
@ -173,6 +174,16 @@ export const routes: IRoute[] = [
menu: { mobile: true },
},
// Playground
{
path: '/playground',
title: 'Playground',
component: Playground,
hidden: false,
type: 'protected',
menu: { mobile: true },
},
// Applications
{
path: '/applications/:name',

View File

@ -0,0 +1,92 @@
import { FormEventHandler, useState, VFC } from 'react';
import {
Box,
Button,
Divider,
Paper,
Typography,
useTheme,
} from '@mui/material';
import { PageContent } from 'component/common/PageContent/PageContent';
import { PageHeader } from 'component/common/PageHeader/PageHeader';
import { PlaygroundConnectionFieldset } from './PlaygroundConnectionFieldset/PlaygroundConnectionFieldset';
import { PlaygroundCodeFieldset } from './PlaygroundCodeFieldset/PlaygroundCodeFieldset';
import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError';
interface IPlaygroundProps {}
export const Playground: VFC<IPlaygroundProps> = () => {
const theme = useTheme();
const [context, setContext] = useState<string>();
const [contextObject, setContextObject] = useState<string>();
const { setToastData } = useToast();
const onSubmit: FormEventHandler<HTMLFormElement> = event => {
event.preventDefault();
try {
setContextObject(
JSON.stringify(JSON.parse(context || '{}'), null, 2)
);
} catch (error: unknown) {
setToastData({
type: 'error',
title: `Error parsing context: ${formatUnknownError(error)}`,
});
}
};
return (
<PageContent header={<PageHeader title="Unleash playground" />}>
<Paper
elevation={0}
sx={{
px: 4,
py: 3,
background: theme.palette.grey[200],
}}
>
<Box component="form" onSubmit={onSubmit}>
<Typography
sx={{
mb: 3,
}}
>
Configure playground
</Typography>
<PlaygroundConnectionFieldset />
<Divider
variant="fullWidth"
sx={{
mb: 2,
borderColor: theme.palette.dividerAlternative,
borderStyle: 'dashed',
}}
/>
<PlaygroundCodeFieldset
value={context}
setValue={setContext}
/>
<Divider
variant="fullWidth"
sx={{
mt: 3,
mb: 2,
borderColor: theme.palette.dividerAlternative,
}}
/>
<Button variant="contained" size="large" type="submit">
Try configuration
</Button>
</Box>
</Paper>
{Boolean(contextObject) && (
<Box sx={{ p: 4 }}>
<Typography>TODO: Request</Typography>
<pre>{contextObject}</pre>
</Box>
)}
</PageContent>
);
};

View File

@ -0,0 +1,162 @@
import {
Dispatch,
SetStateAction,
useEffect,
useMemo,
useState,
VFC,
} from 'react';
import {
Box,
Button,
FormControl,
InputLabel,
MenuItem,
Select,
TextField,
Typography,
useTheme,
} from '@mui/material';
import { debounce } from 'debounce';
import useUnleashContext from 'hooks/api/getters/useUnleashContext/useUnleashContext';
import { formatUnknownError } from 'utils/formatUnknownError';
import useToast from 'hooks/useToast';
interface IPlaygroundCodeFieldsetProps {
value: string | undefined;
setValue: Dispatch<SetStateAction<string | undefined>>;
}
export const PlaygroundCodeFieldset: VFC<IPlaygroundCodeFieldsetProps> = ({
value,
setValue,
}) => {
const theme = useTheme();
const { setToastData } = useToast();
const { context } = useUnleashContext();
const contextOptions = context
.sort((a, b) => a.sortOrder - b.sortOrder)
.map(({ name }) => name);
const [error, setError] = useState<string>();
const debounceSetError = useMemo(
() =>
debounce((input?: string) => {
if (!input) {
return setError(undefined);
}
try {
JSON.parse(input);
} catch (error: unknown) {
return setError(formatUnknownError(error));
}
return setError(undefined);
}, 250),
[setError]
);
useEffect(() => {
debounceSetError(value);
}, [debounceSetError, value]);
const [contextField, setContextField] = useState<string>('');
const [contextValue, setContextValue] = useState<string>('');
const onAddField = () => {
try {
const currentValue = JSON.parse(value || '{}');
setValue(
JSON.stringify(
{
...currentValue,
[contextField]: contextValue,
},
null,
2
)
);
} catch (error) {
setToastData({
type: 'error',
title: `Error parsing context: ${formatUnknownError(error)}`,
});
}
};
return (
<Box>
<Typography
variant="body2"
sx={{ mb: 2 }}
color={theme.palette.text.secondary}
>
Unleash context
</Typography>
<TextField
error={Boolean(error)}
helperText={error}
autoCorrect="off"
spellCheck={false}
multiline
label="JSON"
placeholder={JSON.stringify(
{
currentTime: '2022-07-04T14:13:03.929Z',
appName: 'playground',
userId: 'test',
remoteAddress: '127.0.0.1',
},
null,
2
)}
fullWidth
InputLabelProps={{ shrink: true }}
InputProps={{ minRows: 5 }}
value={value}
onChange={event => setValue(event.target.value)}
/>
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', mt: 2 }}>
<FormControl>
<InputLabel id="context-field-label" size="small">
Context field
</InputLabel>
<Select
label="Context field"
labelId="context-field-label"
id="context-field"
value={contextField}
onChange={event =>
setContextField(event.target.value || '')
}
variant="outlined"
size="small"
sx={{ width: 300, maxWidth: '100%' }}
>
{contextOptions.map(option => (
<MenuItem key={option} value={option}>
{option}
</MenuItem>
))}
</Select>
</FormControl>
<TextField
label="Value"
id="context-value"
sx={{ width: 300, maxWidth: '100%' }}
size="small"
value={contextValue}
onChange={event =>
setContextValue(event.target.value || '')
}
/>
<Button
variant="outlined"
disabled={!contextField || Boolean(error)}
onClick={onAddField}
>
Add context field
</Button>
</Box>
</Box>
);
};

View File

@ -0,0 +1,104 @@
import { ComponentProps, useMemo, useState, VFC } from 'react';
import {
Autocomplete,
Box,
TextField,
Typography,
useTheme,
} from '@mui/material';
import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments';
import useProjects from 'hooks/api/getters/useProjects/useProjects';
interface IPlaygroundConnectionFieldsetProps {}
interface IOption {
label: string;
id: string;
}
const allOption: IOption = { label: 'ALL', id: '*' };
export const PlaygroundConnectionFieldset: VFC<
IPlaygroundConnectionFieldsetProps
> = () => {
const theme = useTheme();
const { environments } = useEnvironments();
const environmentOptions = environments
.filter(({ enabled }) => Boolean(enabled))
.sort((a, b) => a.sortOrder - b.sortOrder)
.map(({ name }) => name);
const { projects: availableProjects = [] } = useProjects();
const projectsOptions = [
allOption,
...availableProjects.map(({ name: label, id }) => ({
label,
id,
})),
];
const [projects, setProjects] = useState<IOption | IOption[]>(allOption);
const onProjectsChange: ComponentProps<typeof Autocomplete>['onChange'] = (
event,
value,
reason
) => {
const newProjects = value as IOption | IOption[];
if (reason === 'clear' || newProjects === null) {
return setProjects(allOption);
}
if (Array.isArray(newProjects)) {
if (newProjects.length === 0) {
return setProjects(allOption);
}
if (
newProjects.find(({ id }) => id === allOption.id) !== undefined
) {
return setProjects(allOption);
}
return setProjects(newProjects);
}
if (newProjects.id === allOption.id) {
return setProjects(allOption);
}
return setProjects([newProjects]);
};
return (
<Box sx={{ pb: 2 }}>
<Typography
variant="body2"
sx={{ mb: 2 }}
color={theme.palette.text.secondary}
>
Access configuration
</Typography>
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap' }}>
<Autocomplete
disablePortal
id="environment"
options={environmentOptions}
sx={{ width: 300, maxWidth: '100%' }}
renderInput={params => (
<TextField {...params} label="Environment" required />
)}
size="small"
/>
<Autocomplete
disablePortal
id="projects"
multiple={Array.isArray(projects)}
options={projectsOptions}
sx={{ width: 300, maxWidth: '100%' }}
renderInput={params => (
<TextField {...params} label="Projects" />
)}
size="small"
value={projects}
onChange={onProjectsChange}
/>
</Box>
</Box>
);
};