mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-14 08:02:15 +03:00
feat: replacement of the legacy rest endpoint details page with the new one
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/9821 Co-authored-by: Varun Choudhary <68095256+Varun-Choudhary@users.noreply.github.com> GitOrigin-RevId: 7b50fc7d429c865678837e2ac9391c7b4973fa56
This commit is contained in:
parent
6853688e20
commit
5897262d75
@ -198,7 +198,7 @@ class GraphiQLWrapper extends Component {
|
||||
);
|
||||
return;
|
||||
}
|
||||
dispatch(_push('/api/rest/create'));
|
||||
dispatch(_push('/api/rest/create?from=graphiql'));
|
||||
};
|
||||
|
||||
const _toggleCacheDirective = () => {
|
||||
|
@ -15,7 +15,8 @@ import globals from '../../../../../Globals';
|
||||
import { FaArrowRight, FaMagic } from 'react-icons/fa';
|
||||
import { IndicatorCard } from '../../../../../new-components/IndicatorCard';
|
||||
import clsx from 'clsx';
|
||||
import { Link } from 'react-router';
|
||||
import { openInGraphiQL } from '../../../../../features/RestEndpoints/components/RestEndpointDetails/utils';
|
||||
import { Analytics } from '../../../../../features/Analytics';
|
||||
|
||||
const editorOptions = {
|
||||
minLines: 10,
|
||||
@ -131,11 +132,19 @@ export const RestEndpointForm: React.FC<RestEndpointFormProps> = ({
|
||||
description="Support GraphQL queries and mutations."
|
||||
editorOptions={editorOptions}
|
||||
/>
|
||||
<div className="text-sm absolute top-6 right-0 mt-2 mr-2">
|
||||
<Link to="/api/api-explorer?mode=rest">
|
||||
<div className="text-sm absolute top-3 right-0 mt-2">
|
||||
<Analytics name="api-tab-rest-endpoint-form-graphiql-link">
|
||||
<Button
|
||||
icon={<FaArrowRight />}
|
||||
iconPosition="end"
|
||||
size="sm"
|
||||
onClick={e => {
|
||||
openInGraphiQL(request);
|
||||
}}
|
||||
>
|
||||
{request ? 'Test it in ' : 'Import from '} GraphiQL{' '}
|
||||
<FaArrowRight />
|
||||
</Link>
|
||||
</Button>
|
||||
</Analytics>
|
||||
</div>
|
||||
</div>
|
||||
<InputField name="name" label="Name" placeholder="Name" />
|
||||
@ -159,8 +168,8 @@ export const RestEndpointForm: React.FC<RestEndpointFormProps> = ({
|
||||
customIcon={FaMagic}
|
||||
headline="No Parameterized variable specification needed"
|
||||
>
|
||||
All parameterized variables in your GraphQL query will be
|
||||
auto-specifed in the URL
|
||||
All parameterized variables (e.g. {prependLabel}example/:id) in your
|
||||
GraphQL query will be auto-specifed in the URL
|
||||
</IndicatorCard>
|
||||
</div>
|
||||
<CheckboxesField
|
||||
@ -179,14 +188,18 @@ export const RestEndpointForm: React.FC<RestEndpointFormProps> = ({
|
||||
<Button type="button" onClick={onCancel} disabled={loading}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Analytics name={`api-tab-rest-endpoint-form-${mode}-button`}>
|
||||
<Button
|
||||
type="submit"
|
||||
mode="primary"
|
||||
isLoading={loading}
|
||||
loadingText={{ create: 'Creating...', edit: 'Modifying ..' }[mode]}
|
||||
loadingText={
|
||||
{ create: 'Creating...', edit: 'Modifying ..' }[mode]
|
||||
}
|
||||
>
|
||||
{{ create: 'Create', edit: 'Modify' }[mode]}
|
||||
</Button>
|
||||
</Analytics>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
|
@ -1,10 +1,12 @@
|
||||
import React from 'react';
|
||||
import { RouteComponentProps } from 'react-router';
|
||||
import { connect, ConnectedProps } from 'react-redux';
|
||||
import queryString from 'query-string';
|
||||
import {
|
||||
Analytics,
|
||||
REDACT_EVERYTHING,
|
||||
} from '../../../../../features/Analytics';
|
||||
import { parse, print } from 'graphql';
|
||||
import {
|
||||
AllowedRESTMethods,
|
||||
RestEndpointEntry,
|
||||
@ -84,8 +86,14 @@ const useRestEndpointFormStateForCreation: RestEndpointFormStateHook = (
|
||||
formSubmitHandler: RestEndpointFormSubmitHandler;
|
||||
} => {
|
||||
const formState: RestEndpointFormState = {};
|
||||
formState.request = getLSItem(LS_KEYS.graphiqlQuery) ?? undefined;
|
||||
|
||||
try {
|
||||
const parsedQuery = queryString.parseUrl(window.location.href);
|
||||
if (parsedQuery.query?.from === 'graphiql') {
|
||||
formState.request = print(parse(getLSItem(LS_KEYS.graphiqlQuery) ?? ''));
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
return { formState, formSubmitHandler: createEndpoint };
|
||||
};
|
||||
|
||||
|
@ -12,6 +12,12 @@ type RequestHeadersProps = {
|
||||
|
||||
export const RequestHeaders = (props: RequestHeadersProps) => {
|
||||
const { headers, setHeaders } = props;
|
||||
|
||||
const showRemove = !!(
|
||||
headers.length > 1 ||
|
||||
headers?.[0]?.name ||
|
||||
headers?.[0]?.value
|
||||
);
|
||||
return (
|
||||
<Collapsible
|
||||
defaultOpen
|
||||
@ -33,7 +39,7 @@ export const RequestHeaders = (props: RequestHeadersProps) => {
|
||||
</div>
|
||||
<div className="font-semibold text-muted mb-4">Headers List</div>
|
||||
<CardedTable
|
||||
showActionCell
|
||||
showActionCell={showRemove}
|
||||
columns={[
|
||||
<Checkbox
|
||||
checked={headers.every(header => header.selected)}
|
||||
@ -49,7 +55,8 @@ export const RequestHeaders = (props: RequestHeadersProps) => {
|
||||
'Name',
|
||||
'Value',
|
||||
]}
|
||||
data={headers.map((header, i) => [
|
||||
data={headers.map((header, i) =>
|
||||
[
|
||||
<Checkbox
|
||||
checked={header.selected}
|
||||
onCheckedChange={checked =>
|
||||
@ -78,7 +85,10 @@ export const RequestHeaders = (props: RequestHeadersProps) => {
|
||||
<input
|
||||
data-testid={`header-value-${i}`}
|
||||
placeholder="Enter value..."
|
||||
className="w-full"
|
||||
className="w-full border-0"
|
||||
type={
|
||||
header.name === 'x-hasura-admin-secret' ? 'password' : 'text'
|
||||
}
|
||||
value={header.value}
|
||||
onChange={e =>
|
||||
setHeaders(
|
||||
@ -89,10 +99,7 @@ export const RequestHeaders = (props: RequestHeadersProps) => {
|
||||
)
|
||||
}
|
||||
/>,
|
||||
|
||||
(headers.length > 1 ||
|
||||
headers?.[0]?.name ||
|
||||
headers?.[0]?.value) && (
|
||||
showRemove && (
|
||||
<Button
|
||||
mode="destructive"
|
||||
size="sm"
|
||||
@ -114,7 +121,8 @@ export const RequestHeaders = (props: RequestHeadersProps) => {
|
||||
Remove
|
||||
</Button>
|
||||
),
|
||||
])}
|
||||
].filter(Boolean)
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</Collapsible>
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { z } from 'zod';
|
||||
import AceEditor from 'react-ace';
|
||||
import {
|
||||
CheckboxesField,
|
||||
CodeEditorField,
|
||||
@ -10,13 +11,15 @@ import {
|
||||
import { Button } from '../../../../new-components/Button';
|
||||
import { FaArrowRight, FaPlay } from 'react-icons/fa';
|
||||
import { useRestEndpoint } from '../../hooks/useRestEndpoint';
|
||||
import { getSessionVarsFromLS } from '../../../../components/Common/ConfigureTransformation/utils';
|
||||
import { parseQueryVariables } from '../../../../components/Services/ApiExplorer/Rest/utils';
|
||||
import { useRestEndpointRequest } from '../../hooks/useRestEndpointRequest';
|
||||
import { IndicatorCard } from '../../../../new-components/IndicatorCard';
|
||||
import { RequestHeaders } from './RequestHeaders';
|
||||
import { Variables } from './Variables';
|
||||
import { AllowedRESTMethods } from '../../../../metadata/types';
|
||||
import { openInGraphiQL } from './utils';
|
||||
import { LS_KEYS, getLSItem } from '../../../../utils';
|
||||
import { hasuraToast } from '../../../../new-components/Toasts';
|
||||
import { Analytics } from '../../../Analytics';
|
||||
|
||||
export type Variable = Exclude<
|
||||
ReturnType<typeof parseQueryVariables>,
|
||||
@ -66,10 +69,37 @@ const validationSchema = z.object({
|
||||
request: z.string().min(1, { message: 'Please add a GraphQL query' }),
|
||||
});
|
||||
|
||||
export const getInitialHeaders = (): Header[] => {
|
||||
const headers = getLSItem(LS_KEYS.apiExplorerConsoleGraphQLHeaders);
|
||||
|
||||
if (headers) {
|
||||
return JSON.parse(headers).map(
|
||||
(header: { key: string; value: string; isDisabled: boolean }) => {
|
||||
const value =
|
||||
header.key === 'x-hasura-admin-secret'
|
||||
? window.__env.adminSecret || getLSItem(LS_KEYS.consoleAdminSecret)
|
||||
: header.value;
|
||||
return {
|
||||
name: header.key,
|
||||
value,
|
||||
selected: !header.isDisabled,
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
return [
|
||||
{
|
||||
name: '',
|
||||
value: '',
|
||||
selected: true,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const RestEndpointDetails = (props: RestEndpointDetailsProps) => {
|
||||
const endpoint = useRestEndpoint(props.name);
|
||||
|
||||
const initialHeaders = getSessionVarsFromLS();
|
||||
const initialHeaders = getInitialHeaders();
|
||||
|
||||
const [headers, setHeaders] = React.useState(
|
||||
initialHeaders.map(header => ({
|
||||
@ -80,11 +110,7 @@ export const RestEndpointDetails = (props: RestEndpointDetailsProps) => {
|
||||
|
||||
const [variables, setVariables] = React.useState<Variable[]>([]);
|
||||
|
||||
const { data, refetch, isFetching, error } = useRestEndpointRequest(
|
||||
endpoint?.endpoint,
|
||||
headers,
|
||||
variables
|
||||
);
|
||||
const { data, mutate, isLoading } = useRestEndpointRequest();
|
||||
|
||||
const {
|
||||
Form,
|
||||
@ -129,10 +155,17 @@ export const RestEndpointDetails = (props: RestEndpointDetailsProps) => {
|
||||
label="GraphQL Request"
|
||||
/>
|
||||
|
||||
<div className="text-sm absolute top-6 right-0 mt-2 mr-2">
|
||||
<a href="/api/api-explorer">
|
||||
Test it in GraphiQL <FaArrowRight />
|
||||
</a>
|
||||
<div className="text-sm absolute top-3 right-0 mt-2">
|
||||
<Button
|
||||
icon={<FaArrowRight />}
|
||||
iconPosition="end"
|
||||
size="sm"
|
||||
onClick={e => {
|
||||
openInGraphiQL(endpoint.query.query);
|
||||
}}
|
||||
>
|
||||
Test it in GraphiQL
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Textarea
|
||||
@ -163,26 +196,64 @@ export const RestEndpointDetails = (props: RestEndpointDetailsProps) => {
|
||||
)}
|
||||
orientation="horizontal"
|
||||
/>
|
||||
<RequestHeaders headers={headers} setHeaders={setHeaders} />
|
||||
<Variables variables={variables} setVariables={setVariables} />
|
||||
|
||||
<RequestHeaders headers={headers} setHeaders={setHeaders} />
|
||||
|
||||
<div className="mt-2">
|
||||
{error && (
|
||||
<IndicatorCard status="negative" headline="An error has occured">
|
||||
{JSON.stringify(error, null, 2)}
|
||||
</IndicatorCard>
|
||||
)}
|
||||
<Analytics name="api-tab-rest-endpoint-details-run-request">
|
||||
<Button
|
||||
disabled={!endpoint?.endpoint}
|
||||
isLoading={isFetching}
|
||||
isLoading={isLoading}
|
||||
icon={<FaPlay />}
|
||||
onClick={() => {
|
||||
refetch();
|
||||
mutate(
|
||||
{
|
||||
endpoint: endpoint?.endpoint,
|
||||
headers,
|
||||
variables,
|
||||
},
|
||||
{
|
||||
onSuccess: data => {
|
||||
hasuraToast({
|
||||
title: 'Success',
|
||||
message: 'Request successful',
|
||||
type: 'success',
|
||||
});
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
},
|
||||
onError: error => {
|
||||
hasuraToast({
|
||||
title: 'Error',
|
||||
message: 'Request failed',
|
||||
type: 'error',
|
||||
children: (
|
||||
<div className="overflow-hidden">
|
||||
<AceEditor
|
||||
theme="github"
|
||||
setOptions={{
|
||||
minLines: 1,
|
||||
maxLines: Infinity,
|
||||
showGutter: false,
|
||||
useWorker: false,
|
||||
}}
|
||||
value={JSON.stringify(error, null, 2)}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
}}
|
||||
mode="primary"
|
||||
>
|
||||
Run Request
|
||||
</Button>
|
||||
</Analytics>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
|
@ -0,0 +1,54 @@
|
||||
import React from 'react';
|
||||
import { RestEndpointDetails } from './RestEndpointDetails';
|
||||
import { Button } from '../../../../new-components/Button';
|
||||
import {
|
||||
BreadcrumbItem,
|
||||
Breadcrumbs,
|
||||
} from '../../../../new-components/Breadcrumbs/Breadcrumbs';
|
||||
import { RouteComponentProps, browserHistory } from 'react-router';
|
||||
|
||||
interface RestEndpointDetailsPageProps {
|
||||
params: RouteComponentProps<{ name: string }, unknown>['params'];
|
||||
}
|
||||
|
||||
export const RestEndpointDetailsPage = (
|
||||
props: RestEndpointDetailsPageProps
|
||||
) => {
|
||||
const name = props.params.name;
|
||||
|
||||
const breadcrumbs: BreadcrumbItem[] = [
|
||||
{
|
||||
title: 'Rest Endpoints',
|
||||
onClick: () => browserHistory.push('/api/rest'),
|
||||
},
|
||||
{
|
||||
title: name,
|
||||
},
|
||||
];
|
||||
|
||||
if (!name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-9">
|
||||
<div className="-ml-2 mb-2">
|
||||
<Breadcrumbs items={breadcrumbs} />
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div className="pb-2">
|
||||
<h1 className="text-xl font-semibold mb-0">{name}</h1>
|
||||
<p className="text-gray-500 mt-0">Test your REST endpoint</p>
|
||||
</div>
|
||||
<Button
|
||||
className="ml-auto"
|
||||
onClick={() => browserHistory.push(`/api/rest/edit/${name}`)}
|
||||
>
|
||||
Edit Endpoint
|
||||
</Button>
|
||||
</div>
|
||||
<hr className="mb-4 mt-2 -mx-9" />
|
||||
<RestEndpointDetails name={name} />
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,3 +1,4 @@
|
||||
import { supportedNumericTypes } from '../../../../components/Services/ApiExplorer/Rest/utils';
|
||||
import { CardedTable } from '../../../../new-components/CardedTable';
|
||||
import { Collapsible } from '../../../../new-components/Collapsible';
|
||||
import { Variable } from './RestEndpointDetails';
|
||||
@ -9,6 +10,11 @@ type VariablesProps = {
|
||||
|
||||
export const Variables = (props: VariablesProps) => {
|
||||
const { variables, setVariables } = props;
|
||||
|
||||
if (variables.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Collapsible
|
||||
defaultOpen
|
||||
@ -26,9 +32,14 @@ export const Variables = (props: VariablesProps) => {
|
||||
<span className="font-semibold text-muted">{variable.name}</span>,
|
||||
variable.type,
|
||||
<input
|
||||
type={
|
||||
supportedNumericTypes.includes(variable.type)
|
||||
? 'number'
|
||||
: 'text'
|
||||
}
|
||||
data-testid={`variable-${variable.name}`}
|
||||
placeholder="Enter value..."
|
||||
className="w-full font-normal text-muted"
|
||||
className="w-full font-normal text-muted border-0"
|
||||
value={variable.value}
|
||||
onChange={e =>
|
||||
setVariables(
|
||||
|
@ -0,0 +1,9 @@
|
||||
import { browserHistory } from 'react-router';
|
||||
import { LS_KEYS, setLSItem } from '../../../../utils';
|
||||
|
||||
export const openInGraphiQL = (query: string) => {
|
||||
if (query) {
|
||||
setLSItem(LS_KEYS.graphiqlQuery, query);
|
||||
}
|
||||
browserHistory.push('/api/api-explorer?mode=rest');
|
||||
};
|
@ -1,28 +1,43 @@
|
||||
import { RestEndpointEntry } from '../../../metadata/types';
|
||||
import { Header, Variable } from '../components/RestEndpointDetails';
|
||||
import { Api } from '../../../hooks/apiUtils';
|
||||
import {
|
||||
getCurrentPageHost,
|
||||
getValueWithType,
|
||||
} from '../../../components/Services/ApiExplorer/Rest/utils';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useMutation } from 'react-query';
|
||||
|
||||
type QueryKey = [
|
||||
string,
|
||||
{
|
||||
interface IApiArgs {
|
||||
headers: Record<string, string>;
|
||||
url: string;
|
||||
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
||||
body?: Record<any, any> | string;
|
||||
}
|
||||
|
||||
async function fetchApi<T = unknown, V = T>(args: IApiArgs): Promise<V> {
|
||||
const { headers, url, method, body } = args;
|
||||
const response = await fetch(url, {
|
||||
headers,
|
||||
method,
|
||||
body: typeof body !== 'string' ? JSON.stringify(body) : body,
|
||||
});
|
||||
const contentType = response.headers.get('Content-Type');
|
||||
const isResponseJson = `${contentType}`.includes('application/json');
|
||||
if (response.ok) {
|
||||
if (!isResponseJson) {
|
||||
return (await response.text()) as unknown as V;
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
throw response.text();
|
||||
}
|
||||
|
||||
export const useRestEndpointRequest = () => {
|
||||
const makeRequest = async (options: {
|
||||
endpoint: RestEndpointEntry | undefined;
|
||||
headers: Header[];
|
||||
variables: Variable[];
|
||||
}
|
||||
];
|
||||
|
||||
export const useRestEndpointRequest = (
|
||||
endpoint: RestEndpointEntry | undefined,
|
||||
headers: Header[],
|
||||
variables: Variable[]
|
||||
) => {
|
||||
const makeRequest = ({ queryKey }: { queryKey: QueryKey }) => {
|
||||
const [, { endpoint, headers, variables }] = queryKey;
|
||||
}) => {
|
||||
const { endpoint, headers, variables } = options;
|
||||
|
||||
const selectedHeaders = headers
|
||||
.filter(h => !!h.name && h.selected)
|
||||
@ -49,13 +64,16 @@ export const useRestEndpointRequest = (
|
||||
}
|
||||
}
|
||||
|
||||
return Api.base({
|
||||
return fetchApi({
|
||||
method: endpoint.methods?.[0],
|
||||
url: `${getCurrentPageHost()}/api/rest/${url}`,
|
||||
body: bodyVariables.reduce((acc, curr) => {
|
||||
body:
|
||||
bodyVariables.length > 0
|
||||
? bodyVariables.reduce((acc, curr) => {
|
||||
acc[curr.name] = curr.value;
|
||||
return acc;
|
||||
}, {} as Record<string, unknown>),
|
||||
}, {} as Record<string, unknown>)
|
||||
: undefined,
|
||||
headers: selectedHeaders.reduce((acc, curr) => {
|
||||
acc[curr.name] = curr.value;
|
||||
return acc;
|
||||
@ -63,13 +81,5 @@ export const useRestEndpointRequest = (
|
||||
});
|
||||
};
|
||||
|
||||
return useQuery({
|
||||
queryKey: [
|
||||
'rest-endpoint-request ',
|
||||
{ endpoint, headers, variables },
|
||||
] as QueryKey,
|
||||
queryFn: makeRequest,
|
||||
enabled: false,
|
||||
retry: 1,
|
||||
});
|
||||
return useMutation(makeRequest);
|
||||
};
|
||||
|
@ -28,13 +28,13 @@ import { SupportContainer } from './components/Services/Support/SupportContainer
|
||||
import HelpPage from './components/Services/Support/HelpPage';
|
||||
import FormRestView from './components/Services/ApiExplorer/Rest/Form';
|
||||
import RestListView from './components/Services/ApiExplorer/Rest/List';
|
||||
import DetailsView from './components/Services/ApiExplorer/Rest/Details';
|
||||
import { HerokuCallbackHandler } from './components/Services/Data/DataSources/CreateDataSource/Heroku/TempCallback';
|
||||
import { NeonCallbackHandler } from './components/Services/Data/DataSources/CreateDataSource/Neon/TempCallback';
|
||||
import InsecureDomains from './components/Services/Settings/InsercureDomains/AllowInsecureDomains';
|
||||
import AuthContainer from './components/Services/Auth/AuthContainer';
|
||||
import { FeatureFlags } from './features/FeatureFlags';
|
||||
import { AllowListDetail } from './components/Services/AllowList';
|
||||
import { RestEndpointDetailsPage } from './features/RestEndpoints/components/RestEndpointDetails/RestEndpointDetailsPage';
|
||||
|
||||
const routes = store => {
|
||||
// load hasuraCliServer migration status
|
||||
@ -121,7 +121,7 @@ const routes = store => {
|
||||
<IndexRedirect to="list" />
|
||||
<Route path="create" component={FormRestView} />
|
||||
<Route path="list" component={RestListView} />
|
||||
<Route path="details/:name" component={DetailsView} />
|
||||
<Route path="details/:name" component={RestEndpointDetailsPage} />
|
||||
<Route path="edit/:name" component={FormRestView} />
|
||||
</Route>
|
||||
<Route path="allow-list">
|
||||
|
@ -38,7 +38,6 @@ import {
|
||||
ApiContainer,
|
||||
CreateRestView,
|
||||
RestListView,
|
||||
DetailsView,
|
||||
InheritedRolesContainer,
|
||||
ApiLimits,
|
||||
IntrospectionOptions,
|
||||
@ -54,6 +53,7 @@ import {
|
||||
SingleSignOnPage,
|
||||
SchemaRegistryContainer,
|
||||
SchemaDetailsView,
|
||||
RestEndpointDetailsPage,
|
||||
} from '@hasura/console-legacy-ce';
|
||||
|
||||
import AccessDeniedComponent from './components/AccessDenied/AccessDenied';
|
||||
@ -344,7 +344,7 @@ const routes = store => {
|
||||
<IndexRedirect to="list" />
|
||||
<Route path="create" component={CreateRestView} />
|
||||
<Route path="list" component={RestListView} />
|
||||
<Route path="details/:name" component={DetailsView} />
|
||||
<Route path="details/:name" component={RestEndpointDetailsPage} />
|
||||
<Route path="edit/:name" component={CreateRestView} />
|
||||
</Route>
|
||||
<Route path="allow-list">
|
||||
|
Loading…
Reference in New Issue
Block a user