@raystack/chronicle 0.1.0-canary.e11f924
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/bin/chronicle.js +2 -0
- package/dist/cli/index.js +9788 -0
- package/next.config.mjs +10 -0
- package/package.json +62 -0
- package/source.config.ts +50 -0
- package/src/app/[[...slug]]/layout.tsx +15 -0
- package/src/app/[[...slug]]/page.tsx +57 -0
- package/src/app/api/apis-proxy/route.ts +59 -0
- package/src/app/api/health/route.ts +3 -0
- package/src/app/api/search/route.ts +90 -0
- package/src/app/apis/[[...slug]]/layout.module.css +22 -0
- package/src/app/apis/[[...slug]]/layout.tsx +26 -0
- package/src/app/apis/[[...slug]]/page.tsx +57 -0
- package/src/app/layout.tsx +26 -0
- package/src/app/llms-full.txt/route.ts +18 -0
- package/src/app/llms.txt/route.ts +15 -0
- package/src/app/providers.tsx +8 -0
- package/src/cli/commands/build.ts +33 -0
- package/src/cli/commands/dev.ts +34 -0
- package/src/cli/commands/init.ts +58 -0
- package/src/cli/commands/serve.ts +54 -0
- package/src/cli/commands/start.ts +34 -0
- package/src/cli/index.ts +21 -0
- package/src/cli/utils/config.ts +43 -0
- package/src/cli/utils/index.ts +2 -0
- package/src/cli/utils/process.ts +7 -0
- package/src/components/api/code-snippets.module.css +7 -0
- package/src/components/api/code-snippets.tsx +76 -0
- package/src/components/api/endpoint-page.module.css +58 -0
- package/src/components/api/endpoint-page.tsx +283 -0
- package/src/components/api/field-row.module.css +126 -0
- package/src/components/api/field-row.tsx +204 -0
- package/src/components/api/field-section.module.css +24 -0
- package/src/components/api/field-section.tsx +100 -0
- package/src/components/api/index.ts +8 -0
- package/src/components/api/json-editor.module.css +9 -0
- package/src/components/api/json-editor.tsx +61 -0
- package/src/components/api/key-value-editor.module.css +13 -0
- package/src/components/api/key-value-editor.tsx +62 -0
- package/src/components/api/method-badge.module.css +4 -0
- package/src/components/api/method-badge.tsx +29 -0
- package/src/components/api/response-panel.module.css +8 -0
- package/src/components/api/response-panel.tsx +44 -0
- package/src/components/common/breadcrumb.tsx +3 -0
- package/src/components/common/button.tsx +3 -0
- package/src/components/common/callout.module.css +7 -0
- package/src/components/common/callout.tsx +27 -0
- package/src/components/common/code-block.tsx +3 -0
- package/src/components/common/dialog.tsx +3 -0
- package/src/components/common/index.ts +10 -0
- package/src/components/common/input-field.tsx +3 -0
- package/src/components/common/sidebar.tsx +3 -0
- package/src/components/common/switch.tsx +3 -0
- package/src/components/common/table.tsx +3 -0
- package/src/components/common/tabs.tsx +3 -0
- package/src/components/mdx/code.module.css +42 -0
- package/src/components/mdx/code.tsx +27 -0
- package/src/components/mdx/details.module.css +37 -0
- package/src/components/mdx/details.tsx +18 -0
- package/src/components/mdx/image.tsx +38 -0
- package/src/components/mdx/index.tsx +35 -0
- package/src/components/mdx/link.tsx +38 -0
- package/src/components/mdx/mermaid.module.css +9 -0
- package/src/components/mdx/mermaid.tsx +37 -0
- package/src/components/mdx/paragraph.module.css +8 -0
- package/src/components/mdx/paragraph.tsx +19 -0
- package/src/components/mdx/table.tsx +40 -0
- package/src/components/ui/breadcrumbs.tsx +72 -0
- package/src/components/ui/client-theme-switcher.tsx +18 -0
- package/src/components/ui/footer.module.css +27 -0
- package/src/components/ui/footer.tsx +30 -0
- package/src/components/ui/search.module.css +104 -0
- package/src/components/ui/search.tsx +202 -0
- package/src/lib/api-routes.ts +120 -0
- package/src/lib/config.ts +47 -0
- package/src/lib/get-llm-text.ts +10 -0
- package/src/lib/index.ts +2 -0
- package/src/lib/openapi.ts +188 -0
- package/src/lib/remark-unused-directives.ts +30 -0
- package/src/lib/schema.ts +99 -0
- package/src/lib/snippet-generators.ts +87 -0
- package/src/lib/source.ts +67 -0
- package/src/themes/default/Layout.module.css +81 -0
- package/src/themes/default/Layout.tsx +133 -0
- package/src/themes/default/Page.module.css +46 -0
- package/src/themes/default/Page.tsx +21 -0
- package/src/themes/default/Toc.module.css +48 -0
- package/src/themes/default/Toc.tsx +66 -0
- package/src/themes/default/font.ts +6 -0
- package/src/themes/default/index.ts +13 -0
- package/src/themes/paper/ChapterNav.module.css +71 -0
- package/src/themes/paper/ChapterNav.tsx +96 -0
- package/src/themes/paper/Layout.module.css +33 -0
- package/src/themes/paper/Layout.tsx +25 -0
- package/src/themes/paper/Page.module.css +174 -0
- package/src/themes/paper/Page.tsx +107 -0
- package/src/themes/paper/ReadingProgress.module.css +132 -0
- package/src/themes/paper/ReadingProgress.tsx +294 -0
- package/src/themes/paper/index.ts +8 -0
- package/src/themes/registry.ts +14 -0
- package/src/types/config.ts +69 -0
- package/src/types/content.ts +35 -0
- package/src/types/index.ts +3 -0
- package/src/types/theme.ts +22 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import { spawn } from 'child_process'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import { fileURLToPath } from 'url'
|
|
5
|
+
import { createRequire } from 'module'
|
|
6
|
+
import chalk from 'chalk'
|
|
7
|
+
import { resolveContentDir, loadCLIConfig, attachLifecycleHandlers } from '@/cli/utils'
|
|
8
|
+
|
|
9
|
+
const require = createRequire(import.meta.url)
|
|
10
|
+
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..')
|
|
11
|
+
const nextCli = require.resolve('next/dist/bin/next')
|
|
12
|
+
|
|
13
|
+
export const serveCommand = new Command('serve')
|
|
14
|
+
.description('Build and start production server')
|
|
15
|
+
.option('-p, --port <port>', 'Port number', '3000')
|
|
16
|
+
.option('-c, --content <path>', 'Content directory')
|
|
17
|
+
.action((options) => {
|
|
18
|
+
const contentDir = resolveContentDir(options.content)
|
|
19
|
+
loadCLIConfig(contentDir)
|
|
20
|
+
|
|
21
|
+
const env = {
|
|
22
|
+
...process.env,
|
|
23
|
+
CHRONICLE_CONTENT_DIR: contentDir,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
console.log(chalk.cyan('Building for production...'))
|
|
27
|
+
console.log(chalk.gray(`Content: ${contentDir}`))
|
|
28
|
+
|
|
29
|
+
const buildChild = spawn(process.execPath, [nextCli, 'build'], {
|
|
30
|
+
stdio: 'inherit',
|
|
31
|
+
cwd: PACKAGE_ROOT,
|
|
32
|
+
env,
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
process.once('SIGINT', () => buildChild.kill('SIGINT'))
|
|
36
|
+
process.once('SIGTERM', () => buildChild.kill('SIGTERM'))
|
|
37
|
+
|
|
38
|
+
buildChild.on('close', (code) => {
|
|
39
|
+
if (code !== 0) {
|
|
40
|
+
console.log(chalk.red('Build failed'))
|
|
41
|
+
process.exit(code ?? 1)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
console.log(chalk.cyan('Starting production server...'))
|
|
45
|
+
|
|
46
|
+
const startChild = spawn(process.execPath, [nextCli, 'start', '-p', options.port], {
|
|
47
|
+
stdio: 'inherit',
|
|
48
|
+
cwd: PACKAGE_ROOT,
|
|
49
|
+
env,
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
attachLifecycleHandlers(startChild)
|
|
53
|
+
})
|
|
54
|
+
})
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import { spawn } from 'child_process'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import { fileURLToPath } from 'url'
|
|
5
|
+
import { createRequire } from 'module'
|
|
6
|
+
import chalk from 'chalk'
|
|
7
|
+
import { resolveContentDir, loadCLIConfig, attachLifecycleHandlers } from '@/cli/utils'
|
|
8
|
+
|
|
9
|
+
const require = createRequire(import.meta.url)
|
|
10
|
+
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..')
|
|
11
|
+
const nextCli = require.resolve('next/dist/bin/next')
|
|
12
|
+
|
|
13
|
+
export const startCommand = new Command('start')
|
|
14
|
+
.description('Start production server')
|
|
15
|
+
.option('-p, --port <port>', 'Port number', '3000')
|
|
16
|
+
.option('-c, --content <path>', 'Content directory')
|
|
17
|
+
.action((options) => {
|
|
18
|
+
const contentDir = resolveContentDir(options.content)
|
|
19
|
+
loadCLIConfig(contentDir)
|
|
20
|
+
|
|
21
|
+
console.log(chalk.cyan('Starting production server...'))
|
|
22
|
+
console.log(chalk.gray(`Content: ${contentDir}`))
|
|
23
|
+
|
|
24
|
+
const child = spawn(process.execPath, [nextCli, 'start', '-p', options.port], {
|
|
25
|
+
stdio: 'inherit',
|
|
26
|
+
cwd: PACKAGE_ROOT,
|
|
27
|
+
env: {
|
|
28
|
+
...process.env,
|
|
29
|
+
CHRONICLE_CONTENT_DIR: contentDir,
|
|
30
|
+
},
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
attachLifecycleHandlers(child)
|
|
34
|
+
})
|
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import { initCommand } from './commands/init'
|
|
3
|
+
import { devCommand } from './commands/dev'
|
|
4
|
+
import { buildCommand } from './commands/build'
|
|
5
|
+
import { startCommand } from './commands/start'
|
|
6
|
+
import { serveCommand } from './commands/serve'
|
|
7
|
+
|
|
8
|
+
const program = new Command()
|
|
9
|
+
|
|
10
|
+
program
|
|
11
|
+
.name('chronicle')
|
|
12
|
+
.description('Config-driven documentation framework')
|
|
13
|
+
.version('0.1.0')
|
|
14
|
+
|
|
15
|
+
program.addCommand(initCommand)
|
|
16
|
+
program.addCommand(devCommand)
|
|
17
|
+
program.addCommand(buildCommand)
|
|
18
|
+
program.addCommand(startCommand)
|
|
19
|
+
program.addCommand(serveCommand)
|
|
20
|
+
|
|
21
|
+
program.parse()
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import { parse } from 'yaml'
|
|
4
|
+
import chalk from 'chalk'
|
|
5
|
+
import type { ChronicleConfig } from '@/types'
|
|
6
|
+
|
|
7
|
+
export interface CLIConfig {
|
|
8
|
+
config: ChronicleConfig
|
|
9
|
+
configPath: string
|
|
10
|
+
contentDir: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function resolveContentDir(contentFlag?: string): string {
|
|
14
|
+
if (contentFlag) return path.resolve(contentFlag)
|
|
15
|
+
if (process.env.CHRONICLE_CONTENT_DIR) return path.resolve(process.env.CHRONICLE_CONTENT_DIR)
|
|
16
|
+
return process.cwd()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function resolveConfigPath(contentDir: string): string | null {
|
|
20
|
+
const cwdPath = path.join(process.cwd(), 'chronicle.yaml')
|
|
21
|
+
if (fs.existsSync(cwdPath)) return cwdPath
|
|
22
|
+
const contentPath = path.join(contentDir, 'chronicle.yaml')
|
|
23
|
+
if (fs.existsSync(contentPath)) return contentPath
|
|
24
|
+
return null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function loadCLIConfig(contentDir: string): CLIConfig {
|
|
28
|
+
const configPath = resolveConfigPath(contentDir)
|
|
29
|
+
|
|
30
|
+
if (!configPath) {
|
|
31
|
+
console.log(chalk.red('Error: chronicle.yaml not found in'), process.cwd(), 'or', contentDir)
|
|
32
|
+
console.log(chalk.gray(`Run 'chronicle init' to create one`))
|
|
33
|
+
process.exit(1)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const config = parse(fs.readFileSync(configPath, 'utf-8')) as ChronicleConfig
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
config,
|
|
40
|
+
configPath,
|
|
41
|
+
contentDir,
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ChildProcess } from 'child_process'
|
|
2
|
+
|
|
3
|
+
export function attachLifecycleHandlers(child: ChildProcess) {
|
|
4
|
+
child.on('close', (code) => process.exit(code ?? 0))
|
|
5
|
+
process.on('SIGINT', () => child.kill('SIGINT'))
|
|
6
|
+
process.on('SIGTERM', () => child.kill('SIGTERM'))
|
|
7
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
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
|
+
}
|