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:
parent
12ffc0474b
commit
2e94cd660c
@ -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)}
|
||||
|
@ -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": {},
|
||||
|
@ -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',
|
||||
|
92
frontend/src/component/playground/Playground/Playground.tsx
Normal file
92
frontend/src/component/playground/Playground/Playground.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
Loading…
Reference in New Issue
Block a user