Website changes docs playground (#10413)
From #10376 (extracting website related changes to deploy them separately, later) --------- Co-authored-by: oliver <8559757+oliverqx@users.noreply.github.com>
This commit is contained in:
@ -1,13 +0,0 @@
|
||||
'use client';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
const GraphQlPlayground = dynamic(
|
||||
() => import('@/app/_components/playground/graphql-playground'),
|
||||
{ ssr: false },
|
||||
);
|
||||
|
||||
const CoreGraphql = () => {
|
||||
return <GraphQlPlayground subDoc={'core'} />;
|
||||
};
|
||||
|
||||
export default CoreGraphql;
|
||||
@ -1,13 +0,0 @@
|
||||
'use client';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
const GraphQlPlayground = dynamic(
|
||||
() => import('@/app/_components/playground/graphql-playground'),
|
||||
{ ssr: false },
|
||||
);
|
||||
|
||||
const MetadataGraphql = () => {
|
||||
return <GraphQlPlayground subDoc={'metadata'} />;
|
||||
};
|
||||
|
||||
export default MetadataGraphql;
|
||||
@ -1,33 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import Playground from '@/app/_components/playground/playground';
|
||||
import { RestApiWrapper } from '@/app/_components/playground/rest-api-wrapper';
|
||||
|
||||
const RestApi = () => {
|
||||
const [openApiJson, setOpenApiJson] = useState({});
|
||||
const [isClient, setIsClient] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsClient(true);
|
||||
}, []);
|
||||
|
||||
if (!isClient) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const children = <RestApiWrapper openApiJson={openApiJson} />;
|
||||
|
||||
return (
|
||||
<div style={{ width: '100vw' }}>
|
||||
<Playground
|
||||
children={children}
|
||||
setOpenApiJson={setOpenApiJson}
|
||||
subDoc="core"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RestApi;
|
||||
@ -1,30 +0,0 @@
|
||||
'use client';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import Playground from '@/app/_components/playground/playground';
|
||||
import { RestApiWrapper } from '@/app/_components/playground/rest-api-wrapper';
|
||||
|
||||
const restApi = () => {
|
||||
const [openApiJson, setOpenApiJson] = useState({});
|
||||
const [isClient, setIsClient] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsClient(true);
|
||||
}, []);
|
||||
|
||||
if (!isClient) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const children = <RestApiWrapper openApiJson={openApiJson} />;
|
||||
|
||||
return (
|
||||
<Playground
|
||||
children={children}
|
||||
setOpenApiJson={setOpenApiJson}
|
||||
subDoc="metadata"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default restApi;
|
||||
@ -1,71 +0,0 @@
|
||||
'use client';
|
||||
import React, { useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { explorerPlugin } from '@graphiql/plugin-explorer';
|
||||
import { createGraphiQLFetcher } from '@graphiql/toolkit';
|
||||
import { GraphiQL } from 'graphiql';
|
||||
|
||||
import { SubDoc } from '@/app/_components/playground/token-form';
|
||||
|
||||
import Playground from './playground';
|
||||
|
||||
import 'graphiql/graphiql.css';
|
||||
import '@graphiql/plugin-explorer/dist/style.css';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
const SubDocToPath = {
|
||||
core: 'graphql',
|
||||
metadata: 'metadata',
|
||||
};
|
||||
|
||||
const GraphQlComponent = ({ token, baseUrl, path }: any) => {
|
||||
const explorer = explorerPlugin({
|
||||
showAttribution: true,
|
||||
});
|
||||
|
||||
const fetcher = createGraphiQLFetcher({
|
||||
url: baseUrl + '/' + path,
|
||||
});
|
||||
|
||||
if (!baseUrl || !token) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<GraphiQL
|
||||
plugins={[explorer]}
|
||||
fetcher={fetcher}
|
||||
defaultHeaders={JSON.stringify({ Authorization: `Bearer ${token}` })}
|
||||
/>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const GraphQlPlayground = ({ subDoc }: { subDoc: SubDoc }) => {
|
||||
const [token, setToken] = useState<string>();
|
||||
const [baseUrl, setBaseUrl] = useState<string>();
|
||||
|
||||
const children = (
|
||||
<GraphQlComponent
|
||||
token={token}
|
||||
baseUrl={baseUrl}
|
||||
path={SubDocToPath[subDoc]}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{ height: '100vh', width: '100vw' }}>
|
||||
<Playground
|
||||
children={children}
|
||||
setToken={setToken}
|
||||
setBaseUrl={setBaseUrl}
|
||||
subDoc={subDoc}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default GraphQlPlayground;
|
||||
@ -1,80 +0,0 @@
|
||||
'use client';
|
||||
import React, { useState } from 'react';
|
||||
import { TbLoader2 } from 'react-icons/tb';
|
||||
|
||||
import TokenForm, { TokenFormProps } from './token-form';
|
||||
|
||||
const Playground = ({
|
||||
children,
|
||||
setOpenApiJson,
|
||||
setToken,
|
||||
setBaseUrl,
|
||||
subDoc,
|
||||
}: Partial<React.PropsWithChildren> &
|
||||
Omit<
|
||||
TokenFormProps,
|
||||
'isTokenValid' | 'setIsTokenValid' | 'setLoadingState'
|
||||
>) => {
|
||||
const [isTokenValid, setIsTokenValid] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
paddingTop: 15,
|
||||
}}
|
||||
>
|
||||
<TokenForm
|
||||
setOpenApiJson={setOpenApiJson}
|
||||
setToken={setToken}
|
||||
setBaseUrl={setBaseUrl}
|
||||
isTokenValid={isTokenValid}
|
||||
setIsTokenValid={setIsTokenValid}
|
||||
subDoc={subDoc}
|
||||
setLoadingState={setIsLoading}
|
||||
/>
|
||||
{!isTokenValid && (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
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/apis">
|
||||
Settings > APIs
|
||||
</a>
|
||||
</div>
|
||||
{isLoading && (
|
||||
<div className="loader-container">
|
||||
<TbLoader2 className="loader" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Playground;
|
||||
@ -1,33 +0,0 @@
|
||||
import React, { useEffect } from 'react';
|
||||
// @ts-expect-error Migration loader as text not passing warnings
|
||||
import { API } from '@stoplight/elements';
|
||||
|
||||
// @ts-expect-error Migration loader as text not passing warnings
|
||||
import spotlightTheme from '!css-loader!@stoplight/elements/styles.min.css';
|
||||
|
||||
export const RestApiWrapper = ({ openApiJson }: { openApiJson: any }) => {
|
||||
// We load spotlightTheme style using useEffect as it breaks remaining docs style
|
||||
useEffect(() => {
|
||||
const styleElement = document.createElement('style');
|
||||
styleElement.innerHTML = spotlightTheme.toString();
|
||||
document.head.append(styleElement);
|
||||
|
||||
return () => styleElement.remove();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
height: 'calc(100vh - var(--ifm-navbar-height) - 45px)',
|
||||
width: '100%',
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
<API
|
||||
apiDescriptionDocument={JSON.stringify(openApiJson)}
|
||||
hideSchemas={true}
|
||||
router="hash"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -1,146 +0,0 @@
|
||||
.form-container {
|
||||
height: 45px;
|
||||
overflow: hidden;
|
||||
border-bottom: 1px solid var(--ifm-color-secondary-light);
|
||||
position: sticky;
|
||||
top: var(--ifm-navbar-height) + 10;
|
||||
padding: 0px 8px;
|
||||
background: var(--ifm-color-secondary-contrast-background);
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.form {
|
||||
display: flex;
|
||||
height: 45px;
|
||||
gap: 10px;
|
||||
width: 50%;
|
||||
margin-left: auto;
|
||||
flex: 0.7;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: white;
|
||||
text-decoration: underline;
|
||||
position: relative;
|
||||
font-weight: bold;
|
||||
transition: color 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
color: #ddd;
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
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;
|
||||
padding-left:30px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.input[disabled] {
|
||||
color: rgb(153, 153, 153)
|
||||
}
|
||||
|
||||
[data-theme='dark'] .input {
|
||||
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 {
|
||||
border: 1px solid #f83e3e;
|
||||
}
|
||||
|
||||
.token-invalid {
|
||||
color: #f83e3e;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.not-visible {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.loader {
|
||||
color: #16233f;
|
||||
font-size: 2rem;
|
||||
animation: animate 2s infinite;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .loader {
|
||||
color: #a3c0f8;
|
||||
}
|
||||
|
||||
@keyframes animate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(720deg);
|
||||
}
|
||||
}
|
||||
|
||||
.loader-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
|
||||
.backButton {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
left: 8px;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
color: #999999;
|
||||
|
||||
&:hover {
|
||||
color: #16233f;
|
||||
}
|
||||
}
|
||||
@ -1,212 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { TbApi, TbChevronLeft, TbLink } from 'react-icons/tb';
|
||||
|
||||
// @ts-expect-error Migration loader as text not passing warnings
|
||||
import tokenForm from '!css-loader!./token-form.css';
|
||||
|
||||
export type SubDoc = 'core' | 'metadata';
|
||||
export type TokenFormProps = {
|
||||
setOpenApiJson?: (json: object) => void;
|
||||
setToken?: (token: string) => void;
|
||||
setBaseUrl?: (baseUrl: string) => void;
|
||||
isTokenValid?: boolean;
|
||||
setIsTokenValid?: (arg: boolean) => void;
|
||||
setLoadingState?: (arg: boolean) => void;
|
||||
subDoc?: SubDoc;
|
||||
};
|
||||
|
||||
const TokenForm = ({
|
||||
setOpenApiJson,
|
||||
setToken,
|
||||
setBaseUrl: submitBaseUrl,
|
||||
isTokenValid,
|
||||
setIsTokenValid,
|
||||
subDoc,
|
||||
setLoadingState,
|
||||
}: TokenFormProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
const pathname = usePathname();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [locationSetting, setLocationSetting] = useState(
|
||||
(typeof window !== 'undefined' &&
|
||||
window.localStorage.getItem('baseUrl') &&
|
||||
JSON.parse(window.localStorage.getItem('baseUrl') ?? '')
|
||||
?.locationSetting) ??
|
||||
'production',
|
||||
);
|
||||
const [baseUrl, setBaseUrl] = useState(
|
||||
(typeof window !== 'undefined' &&
|
||||
window.localStorage.getItem('baseUrl') &&
|
||||
JSON.parse(window.localStorage.getItem('baseUrl') ?? '')?.baseUrl) ??
|
||||
'https://api.twenty.com',
|
||||
);
|
||||
|
||||
const tokenLocal = (
|
||||
typeof window !== 'undefined'
|
||||
? window?.localStorage?.getItem?.('TryIt_securitySchemeValues')
|
||||
: '{}'
|
||||
) as string;
|
||||
|
||||
const token = JSON.parse(tokenLocal)?.bearerAuth ?? '';
|
||||
|
||||
const updateLoading = (loading = false) => {
|
||||
setIsLoading(loading);
|
||||
setLoadingState?.(!!loading);
|
||||
};
|
||||
|
||||
const updateToken = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
window.localStorage.setItem(
|
||||
'TryIt_securitySchemeValues',
|
||||
JSON.stringify({ bearerAuth: event.target.value }),
|
||||
);
|
||||
await submitToken(event.target.value);
|
||||
};
|
||||
|
||||
const updateBaseUrl = (baseUrl: string, locationSetting: string) => {
|
||||
let url: string;
|
||||
if (locationSetting === 'production') {
|
||||
url = 'https://api.twenty.com';
|
||||
} else if (locationSetting === 'next') {
|
||||
url = 'https://api.twenty-next.com';
|
||||
} else if (locationSetting === 'localhost') {
|
||||
url = 'http://localhost:3000';
|
||||
} else {
|
||||
url = baseUrl;
|
||||
}
|
||||
|
||||
setBaseUrl(url);
|
||||
setLocationSetting(locationSetting);
|
||||
submitBaseUrl?.(url);
|
||||
window.localStorage.setItem(
|
||||
'baseUrl',
|
||||
JSON.stringify({ baseUrl: url, locationSetting }),
|
||||
);
|
||||
};
|
||||
|
||||
const validateToken = (openApiJson: any) => {
|
||||
setIsTokenValid?.(!!openApiJson.tags);
|
||||
};
|
||||
|
||||
const getJson = async (token: string) => {
|
||||
updateLoading(true);
|
||||
|
||||
return await fetch(baseUrl + '/open-api/' + (subDoc ?? 'core'), {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((result) => {
|
||||
validateToken(result);
|
||||
updateLoading(false);
|
||||
|
||||
return result;
|
||||
})
|
||||
.catch(() => {
|
||||
updateLoading(false);
|
||||
setIsTokenValid?.(false);
|
||||
});
|
||||
};
|
||||
|
||||
const submitToken = async (token: any) => {
|
||||
if (isLoading) return;
|
||||
|
||||
const json = await getJson(token);
|
||||
|
||||
setToken && setToken(token);
|
||||
|
||||
setOpenApiJson && setOpenApiJson(json);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
updateBaseUrl(baseUrl, locationSetting);
|
||||
await submitToken(token);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
// We load playground style using useEffect as it breaks remaining docs style
|
||||
useEffect(() => {
|
||||
const styleElement = document.createElement('style');
|
||||
styleElement.innerHTML = tokenForm.toString();
|
||||
document.head.append(styleElement);
|
||||
|
||||
return () => styleElement.remove();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="form-container">
|
||||
<form className="form">
|
||||
<div className="backButton" onClick={() => router.back()}>
|
||||
<TbChevronLeft size={18} />
|
||||
<span>Back</span>
|
||||
</div>
|
||||
<div className="inputWrapper">
|
||||
<select
|
||||
className="select"
|
||||
onChange={(event) => {
|
||||
updateBaseUrl(baseUrl, event.target.value);
|
||||
}}
|
||||
value={locationSetting}
|
||||
>
|
||||
<option value="production">Production API</option>
|
||||
<option value="demo">Demo API</option>
|
||||
<option value="localhost">Localhost</option>
|
||||
<option value="other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="inputWrapper">
|
||||
<div className="inputIcon" title="Base URL">
|
||||
<TbLink size={20} />
|
||||
</div>
|
||||
<input
|
||||
className={'input'}
|
||||
type="text"
|
||||
readOnly={isLoading}
|
||||
disabled={locationSetting !== 'other'}
|
||||
placeholder="Base URL"
|
||||
value={baseUrl}
|
||||
onChange={(event) =>
|
||||
updateBaseUrl(event.target.value, locationSetting)
|
||||
}
|
||||
onBlur={() => submitToken(token)}
|
||||
/>
|
||||
</div>
|
||||
<div className="inputWrapper">
|
||||
<div className="inputIcon" title="Api Key">
|
||||
<TbApi size={20} />
|
||||
</div>
|
||||
<input
|
||||
className={!isTokenValid && !isLoading ? 'input invalid' : 'input'}
|
||||
type="text"
|
||||
readOnly={isLoading}
|
||||
placeholder="API Key"
|
||||
defaultValue={token}
|
||||
onChange={updateToken}
|
||||
/>
|
||||
</div>
|
||||
<div className="inputWrapper" style={{ maxWidth: '100px' }}>
|
||||
<select
|
||||
className="select"
|
||||
onChange={(event) =>
|
||||
router.replace(
|
||||
pathname.split('/').slice(0, -1).join('/') +
|
||||
'/' +
|
||||
event.target.value,
|
||||
)
|
||||
}
|
||||
value={pathname.split('/').at(-1)}
|
||||
>
|
||||
<option value="core">Core</option>
|
||||
<option value="metadata">Metadata</option>
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TokenForm;
|
||||
Reference in New Issue
Block a user