@raystack/chronicle 0.7.4 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +4 -1
- package/package.json +1 -1
- package/src/components/api/api-code-snippet.module.css +23 -0
- package/src/components/api/api-code-snippet.tsx +64 -0
- package/src/components/api/api-field-list.module.css +76 -0
- package/src/components/api/api-field-list.tsx +91 -0
- package/src/components/api/api-overview.module.css +65 -0
- package/src/components/api/api-overview.tsx +216 -0
- package/src/components/api/api-response-panel.module.css +62 -0
- package/src/components/api/api-response-panel.tsx +54 -0
- package/src/components/api/index.ts +5 -6
- package/src/components/api/json-editor.tsx +8 -8
- package/src/components/api/method-badge.tsx +2 -2
- package/src/components/api/playground-dialog.module.css +342 -0
- package/src/components/api/playground-dialog.tsx +583 -0
- package/src/lib/api-routes.ts +37 -8
- package/src/lib/openapi.ts +26 -0
- package/src/lib/schema.ts +45 -3
- package/src/lib/source.ts +32 -13
- package/src/lib/use-api-operation.ts +15 -0
- package/src/pages/ApiLayout.module.css +1 -0
- package/src/pages/ApiPage.tsx +7 -38
- package/src/pages/DocsPage.tsx +40 -1
- package/src/server/api/apis-proxy.ts +8 -1
- package/src/server/entry-server.tsx +1 -1
- package/src/server/routes/[...slug].md.ts +1 -0
- package/src/server/routes/apis/[...slug].md.ts +181 -0
- package/src/server/vite-config.ts +2 -0
- package/src/themes/default/Layout.module.css +53 -0
- package/src/themes/default/Layout.tsx +162 -11
- package/src/types/config.ts +1 -0
- package/src/components/api/code-snippets.module.css +0 -7
- package/src/components/api/code-snippets.tsx +0 -76
- package/src/components/api/endpoint-page.module.css +0 -58
- package/src/components/api/endpoint-page.tsx +0 -283
- package/src/components/api/field-row.module.css +0 -126
- package/src/components/api/field-row.tsx +0 -204
- package/src/components/api/field-section.module.css +0 -24
- package/src/components/api/field-section.tsx +0 -100
- package/src/components/api/key-value-editor.module.css +0 -13
- package/src/components/api/key-value-editor.tsx +0 -62
- package/src/components/api/response-panel.module.css +0 -8
- package/src/components/api/response-panel.tsx +0 -44
|
@@ -6,18 +6,22 @@ import {
|
|
|
6
6
|
DocumentTextIcon,
|
|
7
7
|
Squares2X2Icon
|
|
8
8
|
} from '@heroicons/react/24/outline';
|
|
9
|
-
import { Flex, IconButton, Sidebar } from '@raystack/apsara';
|
|
9
|
+
import { Flex, IconButton, Button, Sidebar } from '@raystack/apsara';
|
|
10
|
+
import { PlayIcon } from '@radix-ui/react-icons';
|
|
10
11
|
import { cx } from 'class-variance-authority';
|
|
11
|
-
import { useEffect, useMemo, useRef } from 'react';
|
|
12
|
+
import { useState, useEffect, useMemo, useRef } from 'react';
|
|
12
13
|
import { Link as RouterLink, useLocation, useNavigate } from 'react-router';
|
|
14
|
+
import type { OpenAPIV3 } from 'openapi-types';
|
|
13
15
|
import { MethodBadge } from '@/components/api/method-badge';
|
|
16
|
+
import { useApiOperation } from '@/lib/use-api-operation';
|
|
17
|
+
import { PlaygroundDialog } from '@/components/api/playground-dialog';
|
|
14
18
|
import { ClientThemeSwitcher } from '@/components/ui/client-theme-switcher';
|
|
15
19
|
import { Search } from '@/components/ui/search';
|
|
16
20
|
import { Breadcrumbs } from '@/components/ui/breadcrumbs';
|
|
17
21
|
import { getLandingEntries } from '@/lib/config';
|
|
18
22
|
import { getActiveContentDir } from '@/lib/navigation';
|
|
19
23
|
import { usePageContext } from '@/lib/page-context';
|
|
20
|
-
import type { Node } from 'fumadocs-core/page-tree';
|
|
24
|
+
import type { Node, Root } from 'fumadocs-core/page-tree';
|
|
21
25
|
import type { ThemeLayoutProps } from '@/types';
|
|
22
26
|
import styles from './Layout.module.css';
|
|
23
27
|
import { OpenInAI } from './OpenInAI';
|
|
@@ -67,7 +71,12 @@ export function Layout({
|
|
|
67
71
|
const isApiRoute = pathname === '/apis' || pathname.startsWith('/apis/');
|
|
68
72
|
const isApiBase = (basePath: string) =>
|
|
69
73
|
pathname === basePath || pathname.startsWith(`${basePath}/`);
|
|
70
|
-
const
|
|
74
|
+
const docNav = page ?? { prev: null, next: null };
|
|
75
|
+
const apiNav = useMemo(() => {
|
|
76
|
+
if (!isApiRoute) return { prev: null, next: null };
|
|
77
|
+
return getApiPrevNext(pathname, tree);
|
|
78
|
+
}, [isApiRoute, pathname, tree]);
|
|
79
|
+
const { prev, next } = isApiRoute ? apiNav : docNav;
|
|
71
80
|
|
|
72
81
|
const contentEntries = getLandingEntries(config, version.dir);
|
|
73
82
|
const activeContentDir = getActiveContentDir(pathname, config);
|
|
@@ -151,11 +160,19 @@ export function Layout({
|
|
|
151
160
|
</div>
|
|
152
161
|
) : null}
|
|
153
162
|
{tree.children.map((item, i) => (
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
163
|
+
isApiRoute ? (
|
|
164
|
+
<ApiSidebarNode
|
|
165
|
+
key={item.type === 'page' ? item.url : (item.name?.toString() ?? i)}
|
|
166
|
+
item={item}
|
|
167
|
+
pathname={pathname}
|
|
168
|
+
/>
|
|
169
|
+
) : (
|
|
170
|
+
<SidebarNode
|
|
171
|
+
key={item.type === 'page' ? item.url : (item.name?.toString() ?? i)}
|
|
172
|
+
item={item}
|
|
173
|
+
pathname={pathname}
|
|
174
|
+
/>
|
|
175
|
+
)
|
|
159
176
|
))}
|
|
160
177
|
</Sidebar.Main>
|
|
161
178
|
{config.versions?.length ? (
|
|
@@ -188,9 +205,13 @@ export function Layout({
|
|
|
188
205
|
<ArrowRightIcon width={14} height={14} />
|
|
189
206
|
</IconButton>
|
|
190
207
|
</Flex>
|
|
191
|
-
|
|
208
|
+
<Breadcrumbs slug={slug} tree={tree} />
|
|
209
|
+
</Flex>
|
|
210
|
+
<Flex align='center' gap='small'>
|
|
211
|
+
{isApiRoute && <TestRequestButton />}
|
|
212
|
+
{isApiRoute && <ViewDocsButton />}
|
|
213
|
+
<OpenInAI />
|
|
192
214
|
</Flex>
|
|
193
|
-
<OpenInAI />
|
|
194
215
|
</nav>
|
|
195
216
|
<main className={cx(styles.content, classNames?.content)}>
|
|
196
217
|
{children}
|
|
@@ -262,3 +283,133 @@ function SidebarNode({
|
|
|
262
283
|
</Sidebar.Item>
|
|
263
284
|
);
|
|
264
285
|
}
|
|
286
|
+
|
|
287
|
+
const methodColorMap: Record<string, string> = {
|
|
288
|
+
'method-get': 'var(--rs-color-foreground-success-primary)',
|
|
289
|
+
'method-post': 'var(--rs-color-foreground-accent-primary)',
|
|
290
|
+
'method-put': 'var(--rs-color-foreground-attention-primary)',
|
|
291
|
+
'method-delete': 'var(--rs-color-foreground-danger-primary)',
|
|
292
|
+
'method-patch': 'var(--rs-color-foreground-base-secondary)',
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const methodLabelMap: Record<string, string> = {
|
|
296
|
+
'method-get': 'GET',
|
|
297
|
+
'method-post': 'POST',
|
|
298
|
+
'method-put': 'PUT',
|
|
299
|
+
'method-delete': 'DEL',
|
|
300
|
+
'method-patch': 'PATCH',
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
function ApiSidebarNode({ item, pathname }: { item: Node; pathname: string }) {
|
|
304
|
+
if (item.type === 'separator') return null;
|
|
305
|
+
|
|
306
|
+
if (item.type === 'folder') {
|
|
307
|
+
return (
|
|
308
|
+
<Flex direction='column' gap='small' className={styles.apiGroup}>
|
|
309
|
+
<span className={styles.apiGroupLabel}>{item.name?.toString()}</span>
|
|
310
|
+
<Flex direction='column'>
|
|
311
|
+
{item.children.map((child, i) => (
|
|
312
|
+
<ApiSidebarNode
|
|
313
|
+
key={child.type === 'page' ? child.url : (child.name?.toString() ?? i)}
|
|
314
|
+
item={child}
|
|
315
|
+
pathname={pathname}
|
|
316
|
+
/>
|
|
317
|
+
))}
|
|
318
|
+
</Flex>
|
|
319
|
+
</Flex>
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const isActive = pathname === item.url;
|
|
324
|
+
const href = item.url ?? '#';
|
|
325
|
+
const iconKey = typeof item.icon === 'string' ? item.icon : '';
|
|
326
|
+
const methodLabel = methodLabelMap[iconKey];
|
|
327
|
+
const methodColor = methodColorMap[iconKey];
|
|
328
|
+
|
|
329
|
+
return (
|
|
330
|
+
<Flex
|
|
331
|
+
align='center'
|
|
332
|
+
gap='small'
|
|
333
|
+
className={`${styles.apiItem} ${isActive ? styles.apiItemActive : ''}`}
|
|
334
|
+
render={<RouterLink to={href} />}
|
|
335
|
+
>
|
|
336
|
+
<span className={styles.apiItemName}>{item.name}</span>
|
|
337
|
+
{methodLabel && (
|
|
338
|
+
<span className={styles.apiMethodText} style={{ color: methodColor }}>
|
|
339
|
+
{methodLabel}
|
|
340
|
+
</span>
|
|
341
|
+
)}
|
|
342
|
+
</Flex>
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function TestRequestButton() {
|
|
347
|
+
const match = useApiOperation();
|
|
348
|
+
const [open, setOpen] = useState(false);
|
|
349
|
+
if (!match) return null;
|
|
350
|
+
|
|
351
|
+
return (
|
|
352
|
+
<>
|
|
353
|
+
<Button
|
|
354
|
+
variant='outline'
|
|
355
|
+
color='neutral'
|
|
356
|
+
size='small'
|
|
357
|
+
leadingIcon={<PlayIcon width={12} height={12} />}
|
|
358
|
+
onClick={() => setOpen(true)}
|
|
359
|
+
>
|
|
360
|
+
Test request
|
|
361
|
+
</Button>
|
|
362
|
+
<PlaygroundDialog
|
|
363
|
+
key={`${match.spec.name}-${match.path}-${match.method}`}
|
|
364
|
+
open={open}
|
|
365
|
+
onOpenChange={setOpen}
|
|
366
|
+
method={match.method}
|
|
367
|
+
path={match.path}
|
|
368
|
+
operation={match.operation}
|
|
369
|
+
serverUrl={match.spec.server.url}
|
|
370
|
+
specName={match.spec.name}
|
|
371
|
+
auth={match.spec.auth}
|
|
372
|
+
document={match.spec.document}
|
|
373
|
+
/>
|
|
374
|
+
</>
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function ViewDocsButton() {
|
|
379
|
+
const match = useApiOperation();
|
|
380
|
+
if (!match) return null;
|
|
381
|
+
|
|
382
|
+
const operation = match.operation as OpenAPIV3.OperationObject;
|
|
383
|
+
const docsUrl = operation.externalDocs?.url ?? match.spec.document.externalDocs?.url;
|
|
384
|
+
if (!docsUrl) return null;
|
|
385
|
+
|
|
386
|
+
return (
|
|
387
|
+
<Button
|
|
388
|
+
variant='outline'
|
|
389
|
+
color='neutral'
|
|
390
|
+
size='small'
|
|
391
|
+
leadingIcon={<DocumentTextIcon width={12} height={12} />}
|
|
392
|
+
onClick={() => window.open(docsUrl, '_blank', 'noopener,noreferrer')}
|
|
393
|
+
>
|
|
394
|
+
View documentation
|
|
395
|
+
</Button>
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function getApiPrevNext(pathname: string, tree: Root): { prev: { url: string; title: string } | null; next: { url: string; title: string } | null } {
|
|
400
|
+
const pages: { url: string; title: string }[] = [];
|
|
401
|
+
function collect(node: Node) {
|
|
402
|
+
if (node.type === 'page') {
|
|
403
|
+
pages.push({ url: node.url, title: node.name?.toString() ?? '' });
|
|
404
|
+
} else if (node.type === 'folder') {
|
|
405
|
+
for (const child of node.children) collect(child);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
for (const child of tree.children) collect(child);
|
|
409
|
+
|
|
410
|
+
const idx = pages.findIndex(p => p.url === pathname);
|
|
411
|
+
return {
|
|
412
|
+
prev: idx > 0 ? pages[idx - 1] : null,
|
|
413
|
+
next: idx >= 0 && idx < pages.length - 1 ? pages[idx + 1] : null,
|
|
414
|
+
};
|
|
415
|
+
}
|
package/src/types/config.ts
CHANGED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useMemo, useState } from "react";
|
|
4
|
-
import { CodeBlock } from "@raystack/apsara";
|
|
5
|
-
import {
|
|
6
|
-
generateCurl,
|
|
7
|
-
generatePython,
|
|
8
|
-
generateGo,
|
|
9
|
-
generateTypeScript,
|
|
10
|
-
} from "@/lib/snippet-generators";
|
|
11
|
-
import styles from "./code-snippets.module.css";
|
|
12
|
-
|
|
13
|
-
interface CodeSnippetsProps {
|
|
14
|
-
method: string;
|
|
15
|
-
url: string;
|
|
16
|
-
headers: Record<string, string>;
|
|
17
|
-
body?: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const languages = [
|
|
21
|
-
{ value: "curl", label: "cURL", lang: "curl", generate: generateCurl },
|
|
22
|
-
{
|
|
23
|
-
value: "python",
|
|
24
|
-
label: "Python",
|
|
25
|
-
lang: "python",
|
|
26
|
-
generate: generatePython,
|
|
27
|
-
},
|
|
28
|
-
{ value: "go", label: "Go", lang: "go", generate: generateGo },
|
|
29
|
-
{
|
|
30
|
-
value: "typescript",
|
|
31
|
-
label: "TypeScript",
|
|
32
|
-
lang: "typescript",
|
|
33
|
-
generate: generateTypeScript,
|
|
34
|
-
},
|
|
35
|
-
];
|
|
36
|
-
|
|
37
|
-
export function CodeSnippets({
|
|
38
|
-
method,
|
|
39
|
-
url,
|
|
40
|
-
headers,
|
|
41
|
-
body,
|
|
42
|
-
}: CodeSnippetsProps) {
|
|
43
|
-
const opts = { method, url, headers, body };
|
|
44
|
-
const [selected, setSelected] = useState("curl");
|
|
45
|
-
const current = languages.find((l) => l.value === selected) ?? languages[0];
|
|
46
|
-
|
|
47
|
-
const code = useMemo(
|
|
48
|
-
() => current.generate(opts),
|
|
49
|
-
[selected, method, url, headers, body],
|
|
50
|
-
);
|
|
51
|
-
|
|
52
|
-
return (
|
|
53
|
-
<CodeBlock
|
|
54
|
-
value={selected}
|
|
55
|
-
onValueChange={setSelected}
|
|
56
|
-
className={styles.snippets}
|
|
57
|
-
>
|
|
58
|
-
<CodeBlock.Header>
|
|
59
|
-
<CodeBlock.LanguageSelect>
|
|
60
|
-
<CodeBlock.LanguageSelectTrigger />
|
|
61
|
-
<CodeBlock.LanguageSelectContent>
|
|
62
|
-
{languages.map((l) => (
|
|
63
|
-
<CodeBlock.LanguageSelectItem key={l.value} value={l.value}>
|
|
64
|
-
{l.label}
|
|
65
|
-
</CodeBlock.LanguageSelectItem>
|
|
66
|
-
))}
|
|
67
|
-
</CodeBlock.LanguageSelectContent>
|
|
68
|
-
</CodeBlock.LanguageSelect>
|
|
69
|
-
<CodeBlock.CopyButton />
|
|
70
|
-
</CodeBlock.Header>
|
|
71
|
-
<CodeBlock.Content>
|
|
72
|
-
<CodeBlock.Code language={current.lang}>{code}</CodeBlock.Code>
|
|
73
|
-
</CodeBlock.Content>
|
|
74
|
-
</CodeBlock>
|
|
75
|
-
);
|
|
76
|
-
}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
.layout {
|
|
2
|
-
display: grid;
|
|
3
|
-
grid-template-columns: 1fr 1fr;
|
|
4
|
-
gap: var(--rs-space-9);
|
|
5
|
-
padding: var(--rs-space-7);
|
|
6
|
-
max-width: 1400px;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
.left {
|
|
10
|
-
gap: var(--rs-space-7);
|
|
11
|
-
min-width: 0;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
.right {
|
|
15
|
-
gap: var(--rs-space-5);
|
|
16
|
-
min-width: 0;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
.title {
|
|
20
|
-
margin: 0;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
.description {
|
|
24
|
-
color: var(--rs-color-foreground-base-secondary);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
.methodPath {
|
|
28
|
-
gap: var(--rs-space-3);
|
|
29
|
-
padding: var(--rs-space-4) var(--rs-space-5);
|
|
30
|
-
border: 1px solid var(--rs-color-border-base-primary);
|
|
31
|
-
border-radius: 8px;
|
|
32
|
-
background: var(--rs-color-background-base-secondary);
|
|
33
|
-
overflow: hidden;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
.path {
|
|
37
|
-
font-family: monospace;
|
|
38
|
-
flex: 1;
|
|
39
|
-
min-width: 0;
|
|
40
|
-
overflow: hidden;
|
|
41
|
-
text-overflow: ellipsis;
|
|
42
|
-
white-space: nowrap;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
.tryButton {
|
|
46
|
-
margin-left: auto;
|
|
47
|
-
flex-shrink: 0;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
@media (max-width: 900px) {
|
|
51
|
-
.layout {
|
|
52
|
-
grid-template-columns: 1fr;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
.right {
|
|
56
|
-
position: static;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
@@ -1,283 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { useState, useCallback } from 'react'
|
|
4
|
-
import type { OpenAPIV3 } from 'openapi-types'
|
|
5
|
-
import { Flex, Text, Headline, Button, CodeBlock } from '@raystack/apsara'
|
|
6
|
-
import { MethodBadge } from './method-badge'
|
|
7
|
-
import { FieldSection } from './field-section'
|
|
8
|
-
import { KeyValueEditor, type KeyValueEntry } from './key-value-editor'
|
|
9
|
-
import { CodeSnippets } from './code-snippets'
|
|
10
|
-
import { ResponsePanel } from './response-panel'
|
|
11
|
-
import { flattenSchema, generateExampleJson, type SchemaField } from '@/lib/schema'
|
|
12
|
-
import styles from './endpoint-page.module.css'
|
|
13
|
-
|
|
14
|
-
interface EndpointPageProps {
|
|
15
|
-
method: string
|
|
16
|
-
path: string
|
|
17
|
-
operation: OpenAPIV3.OperationObject
|
|
18
|
-
serverUrl: string
|
|
19
|
-
specName: string
|
|
20
|
-
auth?: { type: string; header: string; placeholder?: string }
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function EndpointPage({ method, path, operation, serverUrl, specName, auth }: EndpointPageProps) {
|
|
24
|
-
const params = (operation.parameters ?? []) as OpenAPIV3.ParameterObject[]
|
|
25
|
-
const body = getRequestBody(operation.requestBody as OpenAPIV3.RequestBodyObject | undefined)
|
|
26
|
-
|
|
27
|
-
const headerFields = paramsToFields(params.filter((p) => p.in === 'header'))
|
|
28
|
-
const headerLocations = Object.fromEntries(headerFields.map((f) => [f.name, 'header']))
|
|
29
|
-
const pathFields = paramsToFields(params.filter((p) => p.in === 'path'))
|
|
30
|
-
const pathLocations = Object.fromEntries(pathFields.map((f) => [f.name, 'path']))
|
|
31
|
-
const queryFields = paramsToFields(params.filter((p) => p.in === 'query'))
|
|
32
|
-
const queryLocations = Object.fromEntries(queryFields.map((f) => [f.name, 'query']))
|
|
33
|
-
const responses = getResponseSections(operation.responses as Record<string, OpenAPIV3.ResponseObject>)
|
|
34
|
-
|
|
35
|
-
// State for editable fields
|
|
36
|
-
const [customHeaders, setCustomHeaders] = useState<KeyValueEntry[]>(() => {
|
|
37
|
-
const initial: KeyValueEntry[] = []
|
|
38
|
-
if (auth) initial.push({ key: auth.header, value: '' })
|
|
39
|
-
return initial
|
|
40
|
-
})
|
|
41
|
-
const [headerValues, setHeaderValues] = useState<Record<string, unknown>>({})
|
|
42
|
-
const [pathValues, setPathValues] = useState<Record<string, unknown>>({})
|
|
43
|
-
const [queryValues, setQueryValues] = useState<Record<string, unknown>>({})
|
|
44
|
-
const [bodyValues, setBodyValues] = useState<Record<string, unknown>>(() => {
|
|
45
|
-
try { return body?.jsonExample ? JSON.parse(body.jsonExample) : {} }
|
|
46
|
-
catch { return {} }
|
|
47
|
-
})
|
|
48
|
-
const [bodyJsonStr, setBodyJsonStr] = useState(body?.jsonExample ?? '{}')
|
|
49
|
-
const [responseBody, setResponseBody] = useState<{ status: number; statusText: string; body: unknown } | null>(null)
|
|
50
|
-
const [loading, setLoading] = useState(false)
|
|
51
|
-
|
|
52
|
-
// Two-way sync: fields → JSON
|
|
53
|
-
const handleBodyValuesChange = useCallback((values: Record<string, unknown>) => {
|
|
54
|
-
setBodyValues(values)
|
|
55
|
-
setBodyJsonStr(JSON.stringify(values, null, 2))
|
|
56
|
-
}, [])
|
|
57
|
-
|
|
58
|
-
// Two-way sync: JSON → fields
|
|
59
|
-
const handleBodyJsonChange = useCallback((jsonStr: string) => {
|
|
60
|
-
setBodyJsonStr(jsonStr)
|
|
61
|
-
try {
|
|
62
|
-
setBodyValues(JSON.parse(jsonStr))
|
|
63
|
-
} catch { /* ignore invalid JSON while typing */ }
|
|
64
|
-
}, [])
|
|
65
|
-
|
|
66
|
-
// Try it handler
|
|
67
|
-
const handleTryIt = useCallback(async () => {
|
|
68
|
-
setLoading(true)
|
|
69
|
-
setResponseBody(null)
|
|
70
|
-
|
|
71
|
-
let resolvedPath = path
|
|
72
|
-
for (const [key, value] of Object.entries(pathValues)) {
|
|
73
|
-
resolvedPath = resolvedPath.replace(`{${key}}`, encodeURIComponent(String(value)))
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const queryEntries = Object.entries(queryValues).filter(([, v]) => v !== undefined && v !== '')
|
|
77
|
-
const queryString = queryEntries
|
|
78
|
-
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`)
|
|
79
|
-
.join('&')
|
|
80
|
-
const fullPath = queryString ? `${resolvedPath}?${queryString}` : resolvedPath
|
|
81
|
-
|
|
82
|
-
const reqHeaders: Record<string, string> = {}
|
|
83
|
-
for (const [key, value] of Object.entries(headerValues)) {
|
|
84
|
-
if (value !== undefined && value !== null && value !== '') reqHeaders[key] = String(value)
|
|
85
|
-
}
|
|
86
|
-
for (const entry of customHeaders) {
|
|
87
|
-
if (entry.key && entry.value) reqHeaders[entry.key] = entry.value
|
|
88
|
-
}
|
|
89
|
-
if (body && bodyJsonStr) {
|
|
90
|
-
reqHeaders['Content-Type'] = body.contentType ?? 'application/json'
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
try {
|
|
94
|
-
const res = await fetch('/api/apis-proxy', {
|
|
95
|
-
method: 'POST',
|
|
96
|
-
headers: { 'Content-Type': 'application/json' },
|
|
97
|
-
body: JSON.stringify({
|
|
98
|
-
specName,
|
|
99
|
-
method,
|
|
100
|
-
path: fullPath,
|
|
101
|
-
headers: reqHeaders,
|
|
102
|
-
body: body ? bodyValues : undefined,
|
|
103
|
-
}),
|
|
104
|
-
})
|
|
105
|
-
const data = await res.json()
|
|
106
|
-
if (data.status !== undefined) {
|
|
107
|
-
setResponseBody(data)
|
|
108
|
-
} else {
|
|
109
|
-
setResponseBody({ status: res.status, statusText: res.statusText, body: data.error ?? data })
|
|
110
|
-
}
|
|
111
|
-
} catch (err) {
|
|
112
|
-
console.error('API request failed:', err)
|
|
113
|
-
setResponseBody({ status: 0, statusText: 'Error', body: 'Failed to send request' })
|
|
114
|
-
} finally {
|
|
115
|
-
setLoading(false)
|
|
116
|
-
}
|
|
117
|
-
}, [specName, method, path, pathValues, queryValues, headerValues, customHeaders, bodyValues, bodyJsonStr, body])
|
|
118
|
-
|
|
119
|
-
// Snippet display values
|
|
120
|
-
const fullUrl = '{domain}' + path
|
|
121
|
-
const snippetHeaders: Record<string, string> = {}
|
|
122
|
-
if (auth) {
|
|
123
|
-
snippetHeaders[auth.header] = auth.placeholder ?? 'YOUR_API_KEY'
|
|
124
|
-
}
|
|
125
|
-
if (body) {
|
|
126
|
-
snippetHeaders['Content-Type'] = body.contentType ?? 'application/json'
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return (
|
|
130
|
-
<div className={styles.layout}>
|
|
131
|
-
<Flex direction="column" className={styles.left}>
|
|
132
|
-
{operation.summary && (
|
|
133
|
-
<Headline size="small" as="h1" className={styles.title}>{operation.summary}</Headline>
|
|
134
|
-
)}
|
|
135
|
-
{operation.description && (
|
|
136
|
-
<Text size={3} className={styles.description}>{operation.description}</Text>
|
|
137
|
-
)}
|
|
138
|
-
|
|
139
|
-
<Flex align="center" className={styles.methodPath}>
|
|
140
|
-
<MethodBadge method={method} />
|
|
141
|
-
<Text size={3} className={styles.path}>{path}</Text>
|
|
142
|
-
<Button variant="solid" size="small" className={styles.tryButton} onClick={handleTryIt} disabled={loading}>
|
|
143
|
-
{loading ? 'Sending...' : 'Send'}
|
|
144
|
-
</Button>
|
|
145
|
-
</Flex>
|
|
146
|
-
|
|
147
|
-
<FieldSection
|
|
148
|
-
title="Headers"
|
|
149
|
-
fields={headerFields}
|
|
150
|
-
locations={headerLocations}
|
|
151
|
-
editable
|
|
152
|
-
values={headerValues}
|
|
153
|
-
onValuesChange={setHeaderValues}
|
|
154
|
-
>
|
|
155
|
-
<KeyValueEditor entries={customHeaders} onChange={setCustomHeaders} />
|
|
156
|
-
</FieldSection>
|
|
157
|
-
<FieldSection
|
|
158
|
-
title="Path"
|
|
159
|
-
fields={pathFields}
|
|
160
|
-
locations={pathLocations}
|
|
161
|
-
editable
|
|
162
|
-
values={pathValues}
|
|
163
|
-
onValuesChange={setPathValues}
|
|
164
|
-
/>
|
|
165
|
-
<FieldSection
|
|
166
|
-
title="Query Parameters"
|
|
167
|
-
fields={queryFields}
|
|
168
|
-
locations={queryLocations}
|
|
169
|
-
editable
|
|
170
|
-
values={queryValues}
|
|
171
|
-
onValuesChange={setQueryValues}
|
|
172
|
-
/>
|
|
173
|
-
{body && (
|
|
174
|
-
<FieldSection
|
|
175
|
-
title="Body"
|
|
176
|
-
label={body?.contentType}
|
|
177
|
-
fields={body?.fields ?? []}
|
|
178
|
-
jsonExample={bodyJsonStr}
|
|
179
|
-
editableJson
|
|
180
|
-
onJsonChange={handleBodyJsonChange}
|
|
181
|
-
alwaysShow
|
|
182
|
-
editable
|
|
183
|
-
values={bodyValues}
|
|
184
|
-
onValuesChange={handleBodyValuesChange}
|
|
185
|
-
/>
|
|
186
|
-
)}
|
|
187
|
-
|
|
188
|
-
{responses.map((resp) => (
|
|
189
|
-
<FieldSection
|
|
190
|
-
key={resp.status}
|
|
191
|
-
title={`${resp.status}${resp.description ? ` — ${resp.description}` : ''}`}
|
|
192
|
-
fields={resp.fields}
|
|
193
|
-
jsonExample={resp.jsonExample}
|
|
194
|
-
/>
|
|
195
|
-
))}
|
|
196
|
-
</Flex>
|
|
197
|
-
<Flex direction="column" className={styles.right}>
|
|
198
|
-
<CodeSnippets
|
|
199
|
-
method={method}
|
|
200
|
-
url={fullUrl}
|
|
201
|
-
headers={snippetHeaders}
|
|
202
|
-
body={body ? bodyJsonStr : undefined}
|
|
203
|
-
/>
|
|
204
|
-
<ResponsePanel responses={responses} />
|
|
205
|
-
{responseBody && (
|
|
206
|
-
<Flex direction="column" gap="small">
|
|
207
|
-
<Text size={3} weight="medium">
|
|
208
|
-
Response — {responseBody.status} {responseBody.statusText}
|
|
209
|
-
</Text>
|
|
210
|
-
<CodeBlock>
|
|
211
|
-
<CodeBlock.Header>
|
|
212
|
-
<CodeBlock.CopyButton />
|
|
213
|
-
</CodeBlock.Header>
|
|
214
|
-
<CodeBlock.Content>
|
|
215
|
-
<CodeBlock.Code language="json">
|
|
216
|
-
{typeof responseBody.body === 'string'
|
|
217
|
-
? (responseBody.body || 'No response body')
|
|
218
|
-
: (JSON.stringify(responseBody.body, null, 2) ?? 'No response body')}
|
|
219
|
-
</CodeBlock.Code>
|
|
220
|
-
</CodeBlock.Content>
|
|
221
|
-
</CodeBlock>
|
|
222
|
-
</Flex>
|
|
223
|
-
)}
|
|
224
|
-
</Flex>
|
|
225
|
-
</div>
|
|
226
|
-
)
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function paramsToFields(params: OpenAPIV3.ParameterObject[]): SchemaField[] {
|
|
230
|
-
return params.map((p) => {
|
|
231
|
-
const schema = (p.schema ?? {}) as OpenAPIV3.SchemaObject
|
|
232
|
-
return {
|
|
233
|
-
name: p.name,
|
|
234
|
-
type: schema.type ? String(schema.type) : 'string',
|
|
235
|
-
required: p.required ?? false,
|
|
236
|
-
description: p.description,
|
|
237
|
-
default: schema.default,
|
|
238
|
-
}
|
|
239
|
-
})
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
interface RequestBody {
|
|
243
|
-
contentType: string
|
|
244
|
-
fields: SchemaField[]
|
|
245
|
-
jsonExample: string
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function getRequestBody(body: OpenAPIV3.RequestBodyObject | undefined): RequestBody | null {
|
|
249
|
-
if (!body?.content) return null
|
|
250
|
-
const contentType = Object.keys(body.content)[0]
|
|
251
|
-
if (!contentType) return null
|
|
252
|
-
const schema = body.content[contentType]?.schema as OpenAPIV3.SchemaObject | undefined
|
|
253
|
-
if (!schema) return null
|
|
254
|
-
return {
|
|
255
|
-
contentType,
|
|
256
|
-
fields: flattenSchema(schema),
|
|
257
|
-
jsonExample: JSON.stringify(generateExampleJson(schema), null, 2),
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
interface ResponseSection {
|
|
262
|
-
status: string
|
|
263
|
-
description?: string
|
|
264
|
-
fields: SchemaField[]
|
|
265
|
-
jsonExample?: string
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
function getResponseSections(responses: Record<string, OpenAPIV3.ResponseObject>): ResponseSection[] {
|
|
269
|
-
return Object.entries(responses).map(([status, resp]) => {
|
|
270
|
-
const content = resp.content ?? {}
|
|
271
|
-
const contentType = Object.keys(content)[0]
|
|
272
|
-
const schema = contentType
|
|
273
|
-
? (content[contentType]?.schema as OpenAPIV3.SchemaObject | undefined)
|
|
274
|
-
: undefined
|
|
275
|
-
|
|
276
|
-
return {
|
|
277
|
-
status,
|
|
278
|
-
description: resp.description,
|
|
279
|
-
fields: schema ? flattenSchema(schema) : [],
|
|
280
|
-
jsonExample: schema ? JSON.stringify(generateExampleJson(schema), null, 2) : undefined,
|
|
281
|
-
}
|
|
282
|
-
})
|
|
283
|
-
}
|