mirror of
https://github.com/Unleash/unleash.git
synced 2025-05-12 01:17:04 +02: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}>
|
<Link to="/features" className={themeStyles.focusable}>
|
||||||
Feature toggles
|
Feature toggles
|
||||||
</Link>
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="/playground"
|
||||||
|
className={themeStyles.focusable}
|
||||||
|
>
|
||||||
|
Playground
|
||||||
|
</Link>
|
||||||
<button
|
<button
|
||||||
className={styles.advancedNavButton}
|
className={styles.advancedNavButton}
|
||||||
onClick={e => setConfigRef(e.currentTarget)}
|
onClick={e => setConfigRef(e.currentTarget)}
|
||||||
|
@ -119,6 +119,16 @@ exports[`returns all baseRoutes 1`] = `
|
|||||||
"title": "Feature toggles",
|
"title": "Feature toggles",
|
||||||
"type": "protected",
|
"type": "protected",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"component": [Function],
|
||||||
|
"hidden": false,
|
||||||
|
"menu": {
|
||||||
|
"mobile": true,
|
||||||
|
},
|
||||||
|
"path": "/playground",
|
||||||
|
"title": "Playground",
|
||||||
|
"type": "protected",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"component": [Function],
|
"component": [Function],
|
||||||
"menu": {},
|
"menu": {},
|
||||||
|
@ -52,6 +52,7 @@ import { SegmentTable } from 'component/segments/SegmentTable/SegmentTable';
|
|||||||
import FlaggedBillingRedirect from 'component/admin/billing/FlaggedBillingRedirect/FlaggedBillingRedirect';
|
import FlaggedBillingRedirect from 'component/admin/billing/FlaggedBillingRedirect/FlaggedBillingRedirect';
|
||||||
import { FeaturesArchiveTable } from '../archive/FeaturesArchiveTable';
|
import { FeaturesArchiveTable } from '../archive/FeaturesArchiveTable';
|
||||||
import { Billing } from 'component/admin/billing/Billing';
|
import { Billing } from 'component/admin/billing/Billing';
|
||||||
|
import { Playground } from 'component/playground/Playground/Playground';
|
||||||
|
|
||||||
export const routes: IRoute[] = [
|
export const routes: IRoute[] = [
|
||||||
// Splash
|
// Splash
|
||||||
@ -173,6 +174,16 @@ export const routes: IRoute[] = [
|
|||||||
menu: { mobile: true },
|
menu: { mobile: true },
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Playground
|
||||||
|
{
|
||||||
|
path: '/playground',
|
||||||
|
title: 'Playground',
|
||||||
|
component: Playground,
|
||||||
|
hidden: false,
|
||||||
|
type: 'protected',
|
||||||
|
menu: { mobile: true },
|
||||||
|
},
|
||||||
|
|
||||||
// Applications
|
// Applications
|
||||||
{
|
{
|
||||||
path: '/applications/:name',
|
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