Custom swagger endpoint for docs (#3869)
* custom swagger endpoint metadata graphql remove /rest from endpoint * fixed pseudo scheme creation * move graphql playground creation to own file, added navbar to change baseurl and token * add schema switcher, fix changing url not applied, add invalid overlay * fix link color * removed path on Graphql Playground, naming fixes subdoc * - fixed overflow issue Rest docs * history replace & goBack * Small fix GraphQL playground broken --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
@ -51,13 +51,14 @@ const sidebars = {
|
|||||||
{
|
{
|
||||||
type: 'link',
|
type: 'link',
|
||||||
label: 'Core API',
|
label: 'Core API',
|
||||||
href: '/rest-api/',
|
href: '/rest-api/core',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'link',
|
type: 'link',
|
||||||
label: 'Metadata API',
|
label: 'Metadata API',
|
||||||
href: '#',
|
href: '#',
|
||||||
className: 'coming-soon',
|
className: 'coming-soon',
|
||||||
|
//href: '/rest-api/metadata',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -73,13 +74,12 @@ const sidebars = {
|
|||||||
{
|
{
|
||||||
type: 'link',
|
type: 'link',
|
||||||
label: 'Core API',
|
label: 'Core API',
|
||||||
href: '/graphql/',
|
href: '/graphql/core',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'link',
|
type: 'link',
|
||||||
label: 'Metadata API',
|
label: 'Metadata API',
|
||||||
href: '#',
|
href: '/graphql/metadata',
|
||||||
className: 'coming-soon',
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@ -6,19 +6,27 @@ import { createGraphiQLFetcher } from '@graphiql/toolkit';
|
|||||||
import Layout from '@theme/Layout';
|
import Layout from '@theme/Layout';
|
||||||
import { GraphiQL } from 'graphiql';
|
import { GraphiQL } from 'graphiql';
|
||||||
|
|
||||||
import Playground from '../components/playground';
|
import Playground from './playground';
|
||||||
|
|
||||||
import explorerCss from '!css-loader!@graphiql/plugin-explorer/dist/style.css';
|
import explorerCss from '!css-loader!@graphiql/plugin-explorer/dist/style.css';
|
||||||
import graphiqlCss from '!css-loader!graphiql/graphiql.css';
|
import graphiqlCss from '!css-loader!graphiql/graphiql.css';
|
||||||
|
|
||||||
|
const SubDocToPath = {
|
||||||
|
core: 'graphql',
|
||||||
|
metadata: 'metadata',
|
||||||
|
};
|
||||||
|
|
||||||
// Docusaurus does SSR for custom pages, but we need to load GraphiQL in the browser
|
// Docusaurus does SSR for custom pages, but we need to load GraphiQL in the browser
|
||||||
const GraphQlComponent = ({ token }) => {
|
const GraphQlComponent = ({ token, baseUrl, path }) => {
|
||||||
const explorer = explorerPlugin({
|
const explorer = explorerPlugin({
|
||||||
showAttribution: true,
|
showAttribution: true,
|
||||||
});
|
});
|
||||||
|
if (!baseUrl || !token) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
const fetcher = createGraphiQLFetcher({
|
const fetcher = createGraphiQLFetcher({
|
||||||
url: 'https://api.twenty.com/graphql',
|
url: baseUrl + '/' + path,
|
||||||
});
|
});
|
||||||
|
|
||||||
// We load graphiql style using useEffect as it breaks remaining docs style
|
// We load graphiql style using useEffect as it breaks remaining docs style
|
||||||
@ -50,8 +58,9 @@ const GraphQlComponent = ({ token }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const graphQL = () => {
|
const GraphQlPlayground = ({ subDoc }: { subDoc: 'core' | 'metadata' }) => {
|
||||||
const [token, setToken] = useState();
|
const [token, setToken] = useState();
|
||||||
|
const [baseUrl, setBaseUrl] = useState();
|
||||||
const { setTheme } = useTheme();
|
const { setTheme } = useTheme();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -71,7 +80,13 @@ const graphQL = () => {
|
|||||||
return () => window.removeEventListener('storage', handleThemeChange);
|
return () => window.removeEventListener('storage', handleThemeChange);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const children = <GraphQlComponent token={token} />;
|
const children = (
|
||||||
|
<GraphQlComponent
|
||||||
|
token={token}
|
||||||
|
baseUrl={baseUrl}
|
||||||
|
path={SubDocToPath[subDoc]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout
|
<Layout
|
||||||
@ -79,9 +94,16 @@ const graphQL = () => {
|
|||||||
description="GraphQL Playground for Twenty"
|
description="GraphQL Playground for Twenty"
|
||||||
>
|
>
|
||||||
<BrowserOnly>
|
<BrowserOnly>
|
||||||
{() => <Playground children={children} setToken={setToken} />}
|
{() => (
|
||||||
|
<Playground
|
||||||
|
children={children}
|
||||||
|
setToken={setToken}
|
||||||
|
setBaseUrl={setBaseUrl}
|
||||||
|
subdocName={subDoc}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</BrowserOnly>
|
</BrowserOnly>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export default graphQL;
|
export default GraphQlPlayground;
|
||||||
@ -1,27 +1,74 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
import { TbLoader2 } from 'react-icons/tb';
|
||||||
|
|
||||||
import TokenForm, { TokenFormProps } from '../components/token-form';
|
import TokenForm, { TokenFormProps } from '../components/token-form';
|
||||||
|
|
||||||
const Playground = (
|
const Playground = ({
|
||||||
{
|
|
||||||
children,
|
children,
|
||||||
setOpenApiJson,
|
setOpenApiJson,
|
||||||
setToken
|
setToken,
|
||||||
}: Partial<React.PropsWithChildren | TokenFormProps>
|
setBaseUrl,
|
||||||
) => {
|
subDoc,
|
||||||
const [isTokenValid, setIsTokenValid] = useState(false)
|
}: Partial<React.PropsWithChildren | TokenFormProps> & {
|
||||||
|
subDoc: string;
|
||||||
|
}) => {
|
||||||
|
const [isTokenValid, setIsTokenValid] = useState(false);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
return (
|
return (
|
||||||
<>
|
<div style={{ position: 'relative' }}>
|
||||||
<TokenForm
|
<TokenForm
|
||||||
setOpenApiJson={setOpenApiJson}
|
setOpenApiJson={setOpenApiJson}
|
||||||
setToken={setToken}
|
setToken={setToken}
|
||||||
|
setBaseUrl={setBaseUrl}
|
||||||
isTokenValid={isTokenValid}
|
isTokenValid={isTokenValid}
|
||||||
setIsTokenValid={setIsTokenValid}
|
setIsTokenValid={setIsTokenValid}
|
||||||
|
subDoc={subDoc}
|
||||||
|
setLoadingState={setIsLoading}
|
||||||
/>
|
/>
|
||||||
{
|
{!isTokenValid && (
|
||||||
isTokenValid && children
|
<div
|
||||||
}
|
style={{
|
||||||
</>
|
position: 'absolute',
|
||||||
)
|
width: '100%',
|
||||||
}
|
height: 'calc(100vh - var(--ifm-navbar-height) - 45px)',
|
||||||
|
top: '45px',
|
||||||
|
display: 'flex',
|
||||||
|
flexFlow: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
zIndex: 2,
|
||||||
|
background: 'rgba(23,23,23, 0.2)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '50%',
|
||||||
|
background: 'rgba(23,23,23, 0.8)',
|
||||||
|
color: 'white',
|
||||||
|
padding: '16px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
A token is required as APIs are dynamically generated for each
|
||||||
|
workspace based on their unique metadata. <br /> Generate your token
|
||||||
|
under{' '}
|
||||||
|
<a
|
||||||
|
className="link"
|
||||||
|
href="https://app.twenty.com/settings/developers"
|
||||||
|
>
|
||||||
|
Settings > Developers
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{isLoading && (
|
||||||
|
<div className="loader-container">
|
||||||
|
<TbLoader2 className="loader" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default Playground;
|
export default Playground;
|
||||||
|
|||||||
@ -1,42 +1,90 @@
|
|||||||
.container {
|
.form-container {
|
||||||
display: flex;
|
height: 45px;
|
||||||
justify-content: center;
|
overflow: hidden;
|
||||||
align-items: center;
|
border-bottom: 1px solid var(--ifm-color-secondary-light);
|
||||||
height: 90vh;
|
position: sticky;
|
||||||
|
top: var(--ifm-navbar-height);
|
||||||
|
padding: 0px 8px;
|
||||||
|
background: var(--ifm-color-secondary-contrast-background);
|
||||||
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form {
|
.form {
|
||||||
text-align: center;
|
display: flex;
|
||||||
padding: 50px;
|
height: 45px;
|
||||||
|
gap: 10px;
|
||||||
|
width: 50%;
|
||||||
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.link {
|
.link {
|
||||||
color: #16233f;
|
color: white;
|
||||||
text-decoration: none;
|
text-decoration: underline;
|
||||||
position: relative;
|
position: relative;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
transition: color 0.3s ease;
|
transition: color 0.3s ease;
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme='dark'] .link {
|
&:hover {
|
||||||
color: #a3c0f8;
|
color: #ddd;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.input {
|
.input {
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
margin: 20px 0 5px 0;
|
margin: 5px 0 5px 0;
|
||||||
max-width: 460px;
|
max-width: 460px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background-color: #f3f3f3;
|
background-color: #f3f3f3;
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
padding-left:30px;
|
||||||
|
height: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme='dark'] .input {
|
[data-theme='dark'] .input {
|
||||||
background-color: #16233f;
|
background-color: #16233f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.inputWrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputIcon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
height: 100%;
|
||||||
|
padding: 5px;
|
||||||
|
color: #B3B3B3;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme='dark'] .inputIcon {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select {
|
||||||
|
padding: 6px;
|
||||||
|
margin: 5px 0 5px 0;
|
||||||
|
max-width: 460px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background-color: #f3f3f3;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
height: 32px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme='dark'] .select {
|
||||||
|
background-color: #16233f;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.invalid {
|
.invalid {
|
||||||
border: 1px solid #f83e3e;
|
border: 1px solid #f83e3e;
|
||||||
}
|
}
|
||||||
@ -75,3 +123,18 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.backButton {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
left: 8px;
|
||||||
|
height: 100%;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #999999;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #16233f;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { TbLoader2 } from 'react-icons/tb';
|
import { useHistory, useLocation } from '@docusaurus/router';
|
||||||
|
import { TbApi, TbChevronLeft, TbLink } from '@theme/icons';
|
||||||
import { parseJson } from 'nx/src/utils/json';
|
import { parseJson } from 'nx/src/utils/json';
|
||||||
|
|
||||||
import tokenForm from '!css-loader!./token-form.css';
|
import tokenForm from '!css-loader!./token-form.css';
|
||||||
@ -7,21 +8,38 @@ import tokenForm from '!css-loader!./token-form.css';
|
|||||||
export type TokenFormProps = {
|
export type TokenFormProps = {
|
||||||
setOpenApiJson?: (json: object) => void;
|
setOpenApiJson?: (json: object) => void;
|
||||||
setToken?: (token: string) => void;
|
setToken?: (token: string) => void;
|
||||||
|
setBaseUrl?: (baseUrl: string) => void;
|
||||||
isTokenValid: boolean;
|
isTokenValid: boolean;
|
||||||
setIsTokenValid: (boolean) => void;
|
setIsTokenValid: (boolean) => void;
|
||||||
|
setLoadingState: (boolean) => void;
|
||||||
|
subDoc?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const TokenForm = ({
|
const TokenForm = ({
|
||||||
setOpenApiJson,
|
setOpenApiJson,
|
||||||
setToken,
|
setToken,
|
||||||
|
setBaseUrl: submitBaseUrl,
|
||||||
isTokenValid,
|
isTokenValid,
|
||||||
setIsTokenValid,
|
setIsTokenValid,
|
||||||
|
subDoc,
|
||||||
|
setLoadingState,
|
||||||
}: TokenFormProps) => {
|
}: TokenFormProps) => {
|
||||||
|
const history = useHistory();
|
||||||
|
const location = useLocation();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [baseUrl, setBaseUrl] = useState(
|
||||||
|
parseJson(localStorage.getItem('baseUrl'))?.baseUrl ??
|
||||||
|
'https://api.twenty.com',
|
||||||
|
);
|
||||||
const token =
|
const token =
|
||||||
parseJson(localStorage.getItem('TryIt_securitySchemeValues'))?.bearerAuth ??
|
parseJson(localStorage.getItem('TryIt_securitySchemeValues'))?.bearerAuth ??
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
const updateLoading = (loading: boolean) => {
|
||||||
|
setIsLoading(loading);
|
||||||
|
setLoadingState(loading);
|
||||||
|
};
|
||||||
|
|
||||||
const updateToken = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
const updateToken = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
'TryIt_securitySchemeValues',
|
'TryIt_securitySchemeValues',
|
||||||
@ -30,22 +48,33 @@ const TokenForm = ({
|
|||||||
await submitToken(event.target.value);
|
await submitToken(event.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateToken = (openApiJson) => setIsTokenValid(!!openApiJson.tags);
|
const updateBaseUrl = (baseUrl) => {
|
||||||
|
setBaseUrl(baseUrl);
|
||||||
|
submitBaseUrl?.(baseUrl);
|
||||||
|
localStorage.setItem('baseUrl', JSON.stringify({ baseUrl: baseUrl }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateToken = (openApiJson) => {
|
||||||
|
setIsTokenValid(!!openApiJson.tags);
|
||||||
|
};
|
||||||
|
|
||||||
const getJson = async (token: string) => {
|
const getJson = async (token: string) => {
|
||||||
setIsLoading(true);
|
updateLoading(true);
|
||||||
|
|
||||||
return await fetch('https://api.twenty.com/open-api', {
|
return await fetch(baseUrl + '/open-api/' + (subDoc ?? 'core'), {
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
})
|
})
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
validateToken(result);
|
validateToken(result);
|
||||||
setIsLoading(false);
|
updateLoading(false);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
})
|
})
|
||||||
.catch(() => setIsLoading(false));
|
.catch(() => {
|
||||||
|
updateLoading(false);
|
||||||
|
setIsTokenValid(false);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const submitToken = async (token) => {
|
const submitToken = async (token) => {
|
||||||
@ -60,6 +89,7 @@ const TokenForm = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
|
updateBaseUrl(baseUrl);
|
||||||
await submitToken(token);
|
await submitToken(token);
|
||||||
})();
|
})();
|
||||||
}, []);
|
}, []);
|
||||||
@ -74,46 +104,61 @@ const TokenForm = ({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
!isTokenValid && (
|
<div className="form-container">
|
||||||
<div>
|
|
||||||
<div className="container">
|
|
||||||
<form className="form">
|
<form className="form">
|
||||||
<label>
|
<div className="backButton" onClick={() => history.goBack()}>
|
||||||
To load your playground schema,{' '}
|
<TbChevronLeft size={18} />
|
||||||
<a
|
<span>Back</span>
|
||||||
className="link"
|
</div>
|
||||||
href="https://app.twenty.com/settings/developers"
|
|
||||||
>
|
<div className="inputWrapper">
|
||||||
generate an API key
|
<div className="inputIcon" title="Api Key">
|
||||||
</a>{' '}
|
<TbApi size={20} />
|
||||||
and paste it here:
|
</div>
|
||||||
</label>
|
|
||||||
<p>
|
|
||||||
<input
|
<input
|
||||||
className={token && !isLoading ? 'input invalid' : 'input'}
|
className={!isTokenValid && !isLoading ? 'input invalid' : 'input'}
|
||||||
type="text"
|
type="text"
|
||||||
readOnly={isLoading}
|
readOnly={isLoading}
|
||||||
placeholder="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMD..."
|
placeholder="API Key"
|
||||||
defaultValue={token}
|
defaultValue={token}
|
||||||
onChange={updateToken}
|
onChange={updateToken}
|
||||||
/>
|
/>
|
||||||
<span
|
</div>
|
||||||
className={`token-invalid ${
|
<div className="inputWrapper">
|
||||||
(!token || isLoading) && 'not-visible'
|
<div className="inputIcon" title="Base URL">
|
||||||
}`}
|
<TbLink size={20} />
|
||||||
>
|
</div>
|
||||||
Token invalid
|
<input
|
||||||
</span>
|
className={'input'}
|
||||||
<div className="loader-container">
|
type="text"
|
||||||
<TbLoader2
|
readOnly={isLoading}
|
||||||
className={`loader ${!isLoading && 'not-visible'}`}
|
placeholder="Base URL"
|
||||||
|
defaultValue={baseUrl}
|
||||||
|
onChange={(event) => updateBaseUrl(event.target.value)}
|
||||||
|
onBlur={() => submitToken(token)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</p>
|
{!location.pathname.includes('rest-api') && (
|
||||||
|
<div className="inputWrapper" style={{ maxWidth: '100px' }}>
|
||||||
|
<select
|
||||||
|
className="select"
|
||||||
|
onChange={(event) =>
|
||||||
|
history.replace(
|
||||||
|
'/' +
|
||||||
|
location.pathname.split('/').at(-2) +
|
||||||
|
'/' +
|
||||||
|
event.target.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
value={location.pathname.split('/').at(-1)}
|
||||||
|
>
|
||||||
|
<option value="core">Core</option>
|
||||||
|
<option value="metadata">Metadata</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -250,7 +250,7 @@ li.coming-soon a::after {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.fullHeightPlayground {
|
.fullHeightPlayground {
|
||||||
height: calc(100vh - var(--ifm-navbar-height));
|
height: calc(100vh - var(--ifm-navbar-height) - 45px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.display-none {
|
.display-none {
|
||||||
|
|||||||
9
packages/twenty-docs/src/pages/graphql/core.tsx
Normal file
9
packages/twenty-docs/src/pages/graphql/core.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import GraphQlPlayground from '../../components/graphql-playground';
|
||||||
|
|
||||||
|
const CoreGraphql = () => {
|
||||||
|
return <GraphQlPlayground subDoc={'core'} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CoreGraphql;
|
||||||
9
packages/twenty-docs/src/pages/graphql/metadata.tsx
Normal file
9
packages/twenty-docs/src/pages/graphql/metadata.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import GraphQlPlayground from '../../components/graphql-playground';
|
||||||
|
|
||||||
|
const CoreGraphql = () => {
|
||||||
|
return <GraphQlPlayground subDoc={'metadata'} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CoreGraphql;
|
||||||
@ -1,13 +1,13 @@
|
|||||||
import Layout from '@theme/Layout';
|
|
||||||
import BrowserOnly from '@docusaurus/BrowserOnly';
|
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import BrowserOnly from '@docusaurus/BrowserOnly';
|
||||||
import { API } from '@stoplight/elements';
|
import { API } from '@stoplight/elements';
|
||||||
import Playground from '../components/playground';
|
import Layout from '@theme/Layout';
|
||||||
|
|
||||||
|
import Playground from '../../components/playground';
|
||||||
|
|
||||||
import spotlightTheme from '!css-loader!@stoplight/elements/styles.min.css';
|
import spotlightTheme from '!css-loader!@stoplight/elements/styles.min.css';
|
||||||
|
|
||||||
|
const RestApiComponent = ({ openApiJson }) => {
|
||||||
const RestApiComponent = ({openApiJson}) => {
|
|
||||||
|
|
||||||
// We load spotlightTheme style using useEffect as it breaks remaining docs style
|
// We load spotlightTheme style using useEffect as it breaks remaining docs style
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const styleElement = document.createElement('style');
|
const styleElement = document.createElement('style');
|
||||||
@ -18,31 +18,38 @@ const RestApiComponent = ({openApiJson}) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<API
|
<div
|
||||||
apiDescriptionDocument={JSON.stringify(openApiJson)}
|
style={{
|
||||||
router="hash"
|
height: 'calc(100vh - var(--ifm-navbar-height) - 45px)',
|
||||||
/>
|
overflow: 'auto',
|
||||||
)
|
}}
|
||||||
}
|
>
|
||||||
|
<API apiDescriptionDocument={JSON.stringify(openApiJson)} router="hash" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const restApi = () => {
|
const restApi = () => {
|
||||||
const [openApiJson, setOpenApiJson] = useState({})
|
const [openApiJson, setOpenApiJson] = useState({});
|
||||||
|
|
||||||
const children = <RestApiComponent openApiJson={openApiJson} />
|
const children = <RestApiComponent openApiJson={openApiJson} />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout
|
<Layout
|
||||||
title="REST API Playground"
|
title="REST API Playground"
|
||||||
description="REST API Playground for Twenty"
|
description="REST API Playground for Twenty"
|
||||||
>
|
>
|
||||||
<BrowserOnly>{
|
<BrowserOnly>
|
||||||
() => <Playground
|
{() => (
|
||||||
|
<Playground
|
||||||
children={children}
|
children={children}
|
||||||
setOpenApiJson={setOpenApiJson}
|
setOpenApiJson={setOpenApiJson}
|
||||||
|
subDoc="core"
|
||||||
/>
|
/>
|
||||||
}</BrowserOnly>
|
)}
|
||||||
|
</BrowserOnly>
|
||||||
</Layout>
|
</Layout>
|
||||||
)
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default restApi;
|
export default restApi;
|
||||||
@ -21,6 +21,7 @@ export {
|
|||||||
TbCheck,
|
TbCheck,
|
||||||
TbCheckbox,
|
TbCheckbox,
|
||||||
TbChecklist,
|
TbChecklist,
|
||||||
|
TbChevronLeft,
|
||||||
TbCircleCheckFilled,
|
TbCircleCheckFilled,
|
||||||
TbCircleDot,
|
TbCircleDot,
|
||||||
TbCloud,
|
TbCloud,
|
||||||
|
|||||||
@ -8,9 +8,22 @@ import { OpenApiService } from 'src/core/open-api/open-api.service';
|
|||||||
export class OpenApiController {
|
export class OpenApiController {
|
||||||
constructor(private readonly openApiService: OpenApiService) {}
|
constructor(private readonly openApiService: OpenApiService) {}
|
||||||
|
|
||||||
@Get()
|
@Get('core')
|
||||||
async generateOpenApiSchema(@Req() request: Request, @Res() res: Response) {
|
async generateOpenApiSchemaCore(
|
||||||
const data = await this.openApiService.generateSchema(request);
|
@Req() request: Request,
|
||||||
|
@Res() res: Response,
|
||||||
|
) {
|
||||||
|
const data = await this.openApiService.generateCoreSchema(request);
|
||||||
|
|
||||||
|
res.send(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('metadata')
|
||||||
|
async generateOpenApiSchemaMetaData(
|
||||||
|
@Req() request: Request,
|
||||||
|
@Res() res: Response,
|
||||||
|
) {
|
||||||
|
const data = await this.openApiService.generateMetaDataSchema();
|
||||||
|
|
||||||
res.send(data);
|
res.send(data);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,7 +24,7 @@ export class OpenApiService {
|
|||||||
private readonly objectMetadataService: ObjectMetadataService,
|
private readonly objectMetadataService: ObjectMetadataService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async generateSchema(request: Request): Promise<OpenAPIV3.Document> {
|
async generateCoreSchema(request: Request): Promise<OpenAPIV3.Document> {
|
||||||
const schema = baseSchema();
|
const schema = baseSchema();
|
||||||
|
|
||||||
let objectMetadataItems;
|
let objectMetadataItems;
|
||||||
@ -42,8 +42,8 @@ export class OpenApiService {
|
|||||||
return schema;
|
return schema;
|
||||||
}
|
}
|
||||||
schema.paths = objectMetadataItems.reduce((paths, item) => {
|
schema.paths = objectMetadataItems.reduce((paths, item) => {
|
||||||
paths[`/rest/${item.namePlural}`] = computeManyResultPath(item);
|
paths[`/${item.namePlural}`] = computeManyResultPath(item);
|
||||||
paths[`/rest/${item.namePlural}/{id}`] = computeSingleResultPath(item);
|
paths[`/${item.namePlural}/{id}`] = computeSingleResultPath(item);
|
||||||
|
|
||||||
return paths;
|
return paths;
|
||||||
}, schema.paths as OpenAPIV3.PathsObject);
|
}, schema.paths as OpenAPIV3.PathsObject);
|
||||||
@ -62,4 +62,13 @@ export class OpenApiService {
|
|||||||
|
|
||||||
return schema;
|
return schema;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async generateMetaDataSchema(): Promise<OpenAPIV3.Document> {
|
||||||
|
//TODO Add once Rest MetaData api is ready
|
||||||
|
const schema = baseSchema();
|
||||||
|
|
||||||
|
schema.tags = [{ name: 'placeholder' }];
|
||||||
|
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,7 +21,7 @@ export const baseSchema = (): OpenAPIV3.Document => {
|
|||||||
// Testing purposes
|
// Testing purposes
|
||||||
servers: [
|
servers: [
|
||||||
{
|
{
|
||||||
url: 'https://api.twenty.com/',
|
url: 'https://api.twenty.com/rest',
|
||||||
description: 'Production Development',
|
description: 'Production Development',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user