@raystack/chronicle 0.7.3 → 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 +2 -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 +57 -23
- 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 +2 -2
- 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/themes/paper/Page.module.css +7 -2
- package/src/themes/paper/Page.tsx +8 -6
- package/src/themes/paper/Skeleton.tsx +9 -0
- 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
package/dist/cli/index.js
CHANGED
|
@@ -337,6 +337,7 @@ async function createViteConfig(options) {
|
|
|
337
337
|
const contentMirror = path6.resolve(packageRoot, ".content");
|
|
338
338
|
return {
|
|
339
339
|
root: packageRoot,
|
|
340
|
+
publicDir: path6.resolve(projectRoot, "public"),
|
|
340
341
|
configFile: false,
|
|
341
342
|
plugins: [
|
|
342
343
|
nitro({
|
|
@@ -420,6 +421,7 @@ async function createViteConfig(options) {
|
|
|
420
421
|
},
|
|
421
422
|
nitro: {
|
|
422
423
|
logLevel: 2,
|
|
424
|
+
publicAssets: [{ dir: path6.resolve(projectRoot, "public") }],
|
|
423
425
|
output: {
|
|
424
426
|
dir: resolveOutputDir(projectRoot, preset)
|
|
425
427
|
}
|
|
@@ -514,7 +516,8 @@ var contentEntrySchema = z.object({
|
|
|
514
516
|
dir: dirNameSchema,
|
|
515
517
|
label: z.string().min(1),
|
|
516
518
|
description: z.string().optional(),
|
|
517
|
-
icon: z.string().optional()
|
|
519
|
+
icon: z.string().optional(),
|
|
520
|
+
index_page: z.string().optional()
|
|
518
521
|
});
|
|
519
522
|
var badgeVariantSchema = z.enum([
|
|
520
523
|
"accent",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@raystack/chronicle",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Config-driven documentation framework",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"@opentelemetry/resources": "^2.6.1",
|
|
47
47
|
"@opentelemetry/sdk-metrics": "^2.6.1",
|
|
48
48
|
"@opentelemetry/semantic-conventions": "^1.40.0",
|
|
49
|
+
"@radix-ui/react-icons": "^1.3.2",
|
|
49
50
|
"@raystack/apsara": "1.0.0-rc.4",
|
|
50
51
|
"@shikijs/rehype": "^4.0.2",
|
|
51
52
|
"@vitejs/plugin-react": "^6.0.1",
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
.container {
|
|
2
|
+
border: 0.5px solid var(--rs-color-border-base-primary);
|
|
3
|
+
border-radius: var(--rs-radius-2);
|
|
4
|
+
overflow: hidden;
|
|
5
|
+
width: 100%;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.header {
|
|
9
|
+
justify-content: space-between;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.title {
|
|
13
|
+
font-size: var(--rs-font-size-regular);
|
|
14
|
+
font-weight: var(--rs-font-weight-regular);
|
|
15
|
+
line-height: var(--rs-line-height-regular);
|
|
16
|
+
letter-spacing: var(--rs-letter-spacing-regular);
|
|
17
|
+
color: var(--rs-color-foreground-base-primary);
|
|
18
|
+
white-space: nowrap;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.body {
|
|
22
|
+
background: var(--rs-color-background-base-primary);
|
|
23
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useMemo, useState } from 'react'
|
|
4
|
+
import { CodeBlock, Flex } from '@raystack/apsara'
|
|
5
|
+
import {
|
|
6
|
+
generateCurl,
|
|
7
|
+
generatePython,
|
|
8
|
+
generateGo,
|
|
9
|
+
generateTypeScript,
|
|
10
|
+
} from '@/lib/snippet-generators'
|
|
11
|
+
import styles from './api-code-snippet.module.css'
|
|
12
|
+
|
|
13
|
+
interface ApiCodeSnippetProps {
|
|
14
|
+
title: string
|
|
15
|
+
method: string
|
|
16
|
+
url: string
|
|
17
|
+
headers: Record<string, string>
|
|
18
|
+
body?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const languages = [
|
|
22
|
+
{ value: 'curl', label: 'cURL', lang: 'bash', generate: generateCurl },
|
|
23
|
+
{ value: 'python', label: 'Python', lang: 'python', generate: generatePython },
|
|
24
|
+
{ value: 'go', label: 'Go', lang: 'go', generate: generateGo },
|
|
25
|
+
{ value: 'typescript', label: 'TypeScript', lang: 'typescript', generate: generateTypeScript },
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
export function ApiCodeSnippet({ title, method, url, headers, body }: ApiCodeSnippetProps) {
|
|
29
|
+
const [selected, setSelected] = useState('curl')
|
|
30
|
+
const current = languages.find((l) => l.value === selected) ?? languages[0]
|
|
31
|
+
|
|
32
|
+
const code = useMemo(
|
|
33
|
+
() => current.generate({ method, url, headers, body }),
|
|
34
|
+
[current.generate, method, url, headers, body],
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<CodeBlock
|
|
39
|
+
value={selected}
|
|
40
|
+
onValueChange={setSelected}
|
|
41
|
+
className={styles.container}
|
|
42
|
+
>
|
|
43
|
+
<CodeBlock.Header className={styles.header}>
|
|
44
|
+
<CodeBlock.Label className={styles.title}>{title}</CodeBlock.Label>
|
|
45
|
+
<Flex align='center' gap={4}>
|
|
46
|
+
<CodeBlock.LanguageSelect>
|
|
47
|
+
<CodeBlock.LanguageSelectTrigger />
|
|
48
|
+
<CodeBlock.LanguageSelectContent>
|
|
49
|
+
{languages.map((l) => (
|
|
50
|
+
<CodeBlock.LanguageSelectItem key={l.value} value={l.value}>
|
|
51
|
+
{l.label}
|
|
52
|
+
</CodeBlock.LanguageSelectItem>
|
|
53
|
+
))}
|
|
54
|
+
</CodeBlock.LanguageSelectContent>
|
|
55
|
+
</CodeBlock.LanguageSelect>
|
|
56
|
+
<CodeBlock.CopyButton />
|
|
57
|
+
</Flex>
|
|
58
|
+
</CodeBlock.Header>
|
|
59
|
+
<CodeBlock.Content className={styles.body}>
|
|
60
|
+
<CodeBlock.Code value={selected} language={current.lang}>{code}</CodeBlock.Code>
|
|
61
|
+
</CodeBlock.Content>
|
|
62
|
+
</CodeBlock>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
.sectionTitle {
|
|
2
|
+
font-size: var(--rs-font-size-large);
|
|
3
|
+
font-weight: var(--rs-font-weight-medium);
|
|
4
|
+
line-height: var(--rs-line-height-large);
|
|
5
|
+
letter-spacing: var(--rs-letter-spacing-large);
|
|
6
|
+
color: var(--rs-color-foreground-base-primary);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.fieldItem {
|
|
10
|
+
padding-bottom: var(--rs-space-5);
|
|
11
|
+
border-bottom: 0.5px solid var(--rs-color-border-base-primary);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.fieldItem:last-child {
|
|
15
|
+
border-bottom: none;
|
|
16
|
+
padding-bottom: 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.fieldType {
|
|
20
|
+
font-size: var(--rs-font-size-small);
|
|
21
|
+
line-height: var(--rs-line-height-small);
|
|
22
|
+
letter-spacing: var(--rs-letter-spacing-small);
|
|
23
|
+
color: var(--rs-color-foreground-base-secondary);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.fieldDescription {
|
|
27
|
+
font-size: var(--rs-font-size-small);
|
|
28
|
+
line-height: var(--rs-line-height-small);
|
|
29
|
+
letter-spacing: var(--rs-letter-spacing-small);
|
|
30
|
+
color: var(--rs-color-foreground-base-secondary);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.fieldExample {
|
|
34
|
+
font-size: var(--rs-font-size-small);
|
|
35
|
+
line-height: var(--rs-line-height-small);
|
|
36
|
+
color: var(--rs-color-foreground-base-tertiary);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.fieldExample code {
|
|
40
|
+
font-family: var(--rs-font-mono);
|
|
41
|
+
font-size: var(--rs-font-size-mono-small);
|
|
42
|
+
background: var(--rs-color-background-neutral-secondary);
|
|
43
|
+
padding: 1px var(--rs-space-2);
|
|
44
|
+
border-radius: 3px;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.statusDescription {
|
|
48
|
+
font-size: var(--rs-font-size-regular);
|
|
49
|
+
line-height: var(--rs-line-height-regular);
|
|
50
|
+
color: var(--rs-color-foreground-base-primary);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.expandButton {
|
|
54
|
+
padding: var(--rs-space-3) var(--rs-space-4);
|
|
55
|
+
border: 1px solid var(--rs-color-border-base-primary);
|
|
56
|
+
border-radius: var(--rs-radius-2);
|
|
57
|
+
background: var(--rs-color-background-base-secondary);
|
|
58
|
+
cursor: pointer;
|
|
59
|
+
width: 100%;
|
|
60
|
+
color: var(--rs-color-foreground-base-primary);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.expandButton:hover {
|
|
64
|
+
background: var(--rs-color-background-neutral-secondary);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.expandLabel {
|
|
68
|
+
font-size: var(--rs-font-size-small);
|
|
69
|
+
line-height: var(--rs-line-height-small);
|
|
70
|
+
color: var(--rs-color-foreground-base-primary);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.childFields {
|
|
74
|
+
padding-left: var(--rs-space-5);
|
|
75
|
+
margin-top: var(--rs-space-3);
|
|
76
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState, type ReactNode } from 'react'
|
|
4
|
+
import { Badge, Flex } from '@raystack/apsara'
|
|
5
|
+
import { ChevronRightIcon, ChevronDownIcon } from '@radix-ui/react-icons'
|
|
6
|
+
import type { SchemaField } from '@/lib/schema'
|
|
7
|
+
import styles from './api-field-list.module.css'
|
|
8
|
+
|
|
9
|
+
interface ApiFieldSectionProps {
|
|
10
|
+
title: string
|
|
11
|
+
fields: SchemaField[]
|
|
12
|
+
headerRight?: ReactNode
|
|
13
|
+
description?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function ApiFieldSection({ title, fields, headerRight, description }: ApiFieldSectionProps) {
|
|
17
|
+
if (fields.length === 0 && !description) return null
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<Flex direction="column" gap={6}>
|
|
21
|
+
<Flex align="center" justify="between">
|
|
22
|
+
<span className={styles.sectionTitle}>{title}</span>
|
|
23
|
+
{headerRight && (
|
|
24
|
+
<Flex align="center" gap={3}>
|
|
25
|
+
{headerRight}
|
|
26
|
+
</Flex>
|
|
27
|
+
)}
|
|
28
|
+
</Flex>
|
|
29
|
+
{description && <span className={styles.statusDescription}>{description}</span>}
|
|
30
|
+
<Flex direction="column" gap={5}>
|
|
31
|
+
{fields.map((field) => (
|
|
32
|
+
<FieldItem key={field.name} field={field} />
|
|
33
|
+
))}
|
|
34
|
+
</Flex>
|
|
35
|
+
</Flex>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function FieldItem({ field }: { field: SchemaField }) {
|
|
40
|
+
const hasChildren = field.children && field.children.length > 0
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<Flex direction="column" gap={4} className={styles.fieldItem}>
|
|
44
|
+
<Flex align="center" gap={3}>
|
|
45
|
+
<Badge variant="neutral" size="micro">{field.name}</Badge>
|
|
46
|
+
<span className={styles.fieldType}>{field.type}</span>
|
|
47
|
+
{field.required && <Badge variant="danger" size="micro">required</Badge>}
|
|
48
|
+
</Flex>
|
|
49
|
+
{field.description && (
|
|
50
|
+
<span className={styles.fieldDescription}>{field.description}</span>
|
|
51
|
+
)}
|
|
52
|
+
{field.example !== undefined && (
|
|
53
|
+
<span className={styles.fieldExample}>Example: <code>{JSON.stringify(field.example)}</code></span>
|
|
54
|
+
)}
|
|
55
|
+
{hasChildren && <ExpandableChildren field={field} />}
|
|
56
|
+
</Flex>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function ExpandableChildren({ field }: { field: SchemaField }) {
|
|
61
|
+
const [expanded, setExpanded] = useState(false)
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<Flex direction="column">
|
|
65
|
+
<Flex
|
|
66
|
+
align="center"
|
|
67
|
+
justify="between"
|
|
68
|
+
className={styles.expandButton}
|
|
69
|
+
onClick={() => setExpanded(!expanded)}
|
|
70
|
+
role="button"
|
|
71
|
+
tabIndex={0}
|
|
72
|
+
>
|
|
73
|
+
<span className={styles.expandLabel}>
|
|
74
|
+
{expanded ? 'Hide' : 'Show'} child attributes
|
|
75
|
+
</span>
|
|
76
|
+
{expanded ? (
|
|
77
|
+
<ChevronDownIcon width={16} height={16} />
|
|
78
|
+
) : (
|
|
79
|
+
<ChevronRightIcon width={16} height={16} />
|
|
80
|
+
)}
|
|
81
|
+
</Flex>
|
|
82
|
+
{expanded && (
|
|
83
|
+
<Flex direction="column" gap={5} className={styles.childFields}>
|
|
84
|
+
{field.children!.map((child) => (
|
|
85
|
+
<FieldItem key={child.name} field={child} />
|
|
86
|
+
))}
|
|
87
|
+
</Flex>
|
|
88
|
+
)}
|
|
89
|
+
</Flex>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
.layout {
|
|
2
|
+
align-items: flex-start;
|
|
3
|
+
justify-content: space-between;
|
|
4
|
+
padding-left: var(--rs-space-9);
|
|
5
|
+
padding-right: var(--rs-space-9);
|
|
6
|
+
width: 100%;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.left {
|
|
10
|
+
min-width: 0;
|
|
11
|
+
flex: 0 1 545px;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.right {
|
|
15
|
+
min-width: 376px;
|
|
16
|
+
max-width: 500px;
|
|
17
|
+
width: 100%;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.title {
|
|
21
|
+
font-family: var(--rs-font-title);
|
|
22
|
+
font-size: var(--rs-font-size-t3);
|
|
23
|
+
font-weight: var(--rs-font-weight-medium);
|
|
24
|
+
line-height: var(--rs-line-height-t3);
|
|
25
|
+
letter-spacing: var(--rs-letter-spacing-t3);
|
|
26
|
+
color: var(--rs-color-foreground-base-primary);
|
|
27
|
+
margin: 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.description {
|
|
31
|
+
font-size: var(--rs-font-size-regular);
|
|
32
|
+
line-height: var(--rs-line-height-regular);
|
|
33
|
+
letter-spacing: var(--rs-letter-spacing-regular);
|
|
34
|
+
color: var(--rs-color-foreground-base-secondary);
|
|
35
|
+
margin: 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.methodBar {
|
|
39
|
+
padding: var(--rs-space-3) 0;
|
|
40
|
+
border-radius: var(--rs-radius-2);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.path {
|
|
44
|
+
font-family: var(--rs-font-mono);
|
|
45
|
+
font-size: var(--rs-font-size-mono-regular);
|
|
46
|
+
line-height: var(--rs-line-height-regular);
|
|
47
|
+
color: var(--rs-color-foreground-base-primary);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.divider {
|
|
51
|
+
padding: 0;
|
|
52
|
+
margin: var(--rs-space-4) 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@media (max-width: 1100px) {
|
|
56
|
+
.layout {
|
|
57
|
+
flex-direction: column;
|
|
58
|
+
gap: var(--rs-space-9);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.left,
|
|
62
|
+
.right {
|
|
63
|
+
width: 100%;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import type { OpenAPIV3 } from 'openapi-types'
|
|
5
|
+
import { Flex, Button, Menu, CopyButton, Separator } from '@raystack/apsara'
|
|
6
|
+
import { ChevronDownIcon } from '@radix-ui/react-icons'
|
|
7
|
+
import { MethodBadge } from '@/components/api/method-badge'
|
|
8
|
+
import { ApiCodeSnippet } from './api-code-snippet'
|
|
9
|
+
import { ApiResponsePanel } from './api-response-panel'
|
|
10
|
+
import { flattenSchema, generateExampleJson, type SchemaField } from '@/lib/schema'
|
|
11
|
+
import { ApiFieldSection } from './api-field-list'
|
|
12
|
+
import { toKind } from '@/lib/schema'
|
|
13
|
+
import styles from './api-overview.module.css'
|
|
14
|
+
|
|
15
|
+
interface ApiOverviewProps {
|
|
16
|
+
method: string
|
|
17
|
+
path: string
|
|
18
|
+
operation: OpenAPIV3.OperationObject
|
|
19
|
+
serverUrl: string
|
|
20
|
+
specName: string
|
|
21
|
+
auth?: { type: string; header: string; placeholder?: string }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function ApiOverview({ method, path, operation, auth }: ApiOverviewProps) {
|
|
25
|
+
const params = (operation.parameters ?? []) as OpenAPIV3.ParameterObject[]
|
|
26
|
+
const body = getRequestBody(operation.requestBody as OpenAPIV3.RequestBodyObject | undefined)
|
|
27
|
+
|
|
28
|
+
const headerFields = paramsToFields(params.filter((p) => p.in === 'header'))
|
|
29
|
+
const pathFields = paramsToFields(params.filter((p) => p.in === 'path'))
|
|
30
|
+
const queryFields = paramsToFields(params.filter((p) => p.in === 'query'))
|
|
31
|
+
const responses = getResponseSections(operation.responses as Record<string, OpenAPIV3.ResponseObject>)
|
|
32
|
+
|
|
33
|
+
const authFields: SchemaField[] = auth
|
|
34
|
+
? [{ name: auth.header, type: 'String', kind: 'string' as const, required: false }]
|
|
35
|
+
: headerFields.length > 0
|
|
36
|
+
? headerFields
|
|
37
|
+
: []
|
|
38
|
+
|
|
39
|
+
const fullUrl = '{domain}' + path
|
|
40
|
+
const snippetHeaders: Record<string, string> = {}
|
|
41
|
+
if (auth) snippetHeaders[auth.header] = auth.placeholder ?? 'YOUR_API_KEY'
|
|
42
|
+
if (body) snippetHeaders['Content-Type'] = body.contentType ?? 'application/json'
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
const hasSections = authFields.length > 0 || pathFields.length > 0 ||
|
|
46
|
+
queryFields.length > 0 || (body && body.fields.length > 0) || responses.length > 0
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<Flex className={styles.layout}>
|
|
50
|
+
<Flex direction='column' gap={9} className={styles.left}>
|
|
51
|
+
<Flex direction='column' gap={7}>
|
|
52
|
+
<Flex direction='column' gap={4}>
|
|
53
|
+
{operation.summary && (
|
|
54
|
+
<h1 className={styles.title}>{operation.summary}</h1>
|
|
55
|
+
)}
|
|
56
|
+
{operation.description && (
|
|
57
|
+
<p className={styles.description}>{operation.description}</p>
|
|
58
|
+
)}
|
|
59
|
+
</Flex>
|
|
60
|
+
<Flex align='center' gap={3} className={styles.methodBar}>
|
|
61
|
+
<MethodBadge method={method} />
|
|
62
|
+
<span className={styles.path}>{path}</span>
|
|
63
|
+
<CopyButton text={path} size={2} />
|
|
64
|
+
</Flex>
|
|
65
|
+
</Flex>
|
|
66
|
+
|
|
67
|
+
{hasSections && (
|
|
68
|
+
<Flex direction='column' gap={6}>
|
|
69
|
+
{authFields.length > 0 && (
|
|
70
|
+
<ApiFieldSection title="Authorisations" fields={authFields} />
|
|
71
|
+
)}
|
|
72
|
+
|
|
73
|
+
{authFields.length > 0 && (queryFields.length > 0 || pathFields.length > 0 || (body && body.fields.length > 0) || responses.length > 0) && (
|
|
74
|
+
<Separator className={styles.divider} />
|
|
75
|
+
)}
|
|
76
|
+
|
|
77
|
+
{pathFields.length > 0 && (
|
|
78
|
+
<ApiFieldSection title="Path Parameters" fields={pathFields} />
|
|
79
|
+
)}
|
|
80
|
+
|
|
81
|
+
{pathFields.length > 0 && (queryFields.length > 0 || (body && body.fields.length > 0) || responses.length > 0) && (
|
|
82
|
+
<Separator className={styles.divider} />
|
|
83
|
+
)}
|
|
84
|
+
|
|
85
|
+
{queryFields.length > 0 && (
|
|
86
|
+
<ApiFieldSection title="Query Parameters" fields={queryFields} />
|
|
87
|
+
)}
|
|
88
|
+
|
|
89
|
+
{queryFields.length > 0 && ((body && body.fields.length > 0) || responses.length > 0) && (
|
|
90
|
+
<Separator className={styles.divider} />
|
|
91
|
+
)}
|
|
92
|
+
|
|
93
|
+
{body && body.fields.length > 0 && (
|
|
94
|
+
<ApiFieldSection title="Request Body" fields={body.fields} />
|
|
95
|
+
)}
|
|
96
|
+
|
|
97
|
+
{body && body.fields.length > 0 && responses.length > 0 && (
|
|
98
|
+
<Separator className={styles.divider} />
|
|
99
|
+
)}
|
|
100
|
+
|
|
101
|
+
{responses.length > 0 && (
|
|
102
|
+
<ResponseSection responses={responses} />
|
|
103
|
+
)}
|
|
104
|
+
</Flex>
|
|
105
|
+
)}
|
|
106
|
+
</Flex>
|
|
107
|
+
|
|
108
|
+
<Flex direction='column' gap={8} className={styles.right}>
|
|
109
|
+
<ApiCodeSnippet
|
|
110
|
+
title={operation.summary ?? `${method.toUpperCase()} ${path}`}
|
|
111
|
+
method={method}
|
|
112
|
+
url={fullUrl}
|
|
113
|
+
headers={snippetHeaders}
|
|
114
|
+
body={body ? body.jsonExample : undefined}
|
|
115
|
+
/>
|
|
116
|
+
<ApiResponsePanel responses={responses} />
|
|
117
|
+
</Flex>
|
|
118
|
+
</Flex>
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function ResponseSection({ responses }: { responses: ResponseSectionData[] }) {
|
|
123
|
+
const [selectedStatus, setSelectedStatus] = useState(responses[0]?.status ?? '200')
|
|
124
|
+
if (responses.length === 0) return null
|
|
125
|
+
const active = responses.find((r) => r.status === selectedStatus) ?? responses[0]
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<ApiFieldSection
|
|
129
|
+
title="Response"
|
|
130
|
+
fields={active.fields}
|
|
131
|
+
description={active.description}
|
|
132
|
+
headerRight={
|
|
133
|
+
<>
|
|
134
|
+
{active.contentType && (
|
|
135
|
+
<span className={styles.path}>{active.contentType}</span>
|
|
136
|
+
)}
|
|
137
|
+
<Menu>
|
|
138
|
+
<Menu.Trigger
|
|
139
|
+
render={
|
|
140
|
+
<Button variant="text" color="neutral" size="small" trailingIcon={<ChevronDownIcon />} />
|
|
141
|
+
}
|
|
142
|
+
>
|
|
143
|
+
{active.status}
|
|
144
|
+
</Menu.Trigger>
|
|
145
|
+
<Menu.Content>
|
|
146
|
+
{responses.map((resp) => (
|
|
147
|
+
<Menu.Item key={resp.status} onClick={() => setSelectedStatus(resp.status)}>
|
|
148
|
+
{resp.status}{resp.description ? ` — ${resp.description}` : ''}
|
|
149
|
+
</Menu.Item>
|
|
150
|
+
))}
|
|
151
|
+
</Menu.Content>
|
|
152
|
+
</Menu>
|
|
153
|
+
</>
|
|
154
|
+
}
|
|
155
|
+
/>
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function paramsToFields(params: OpenAPIV3.ParameterObject[]): SchemaField[] {
|
|
160
|
+
return params.map((p) => {
|
|
161
|
+
const schema = (p.schema ?? {}) as OpenAPIV3.SchemaObject
|
|
162
|
+
return {
|
|
163
|
+
name: p.name,
|
|
164
|
+
type: schema.type ? String(schema.type) : 'string',
|
|
165
|
+
kind: toKind(schema.type),
|
|
166
|
+
required: p.required ?? false,
|
|
167
|
+
description: p.description,
|
|
168
|
+
default: schema.default,
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
interface RequestBody {
|
|
174
|
+
contentType: string
|
|
175
|
+
fields: SchemaField[]
|
|
176
|
+
jsonExample: string
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function getRequestBody(body: OpenAPIV3.RequestBodyObject | undefined): RequestBody | null {
|
|
180
|
+
if (!body?.content) return null
|
|
181
|
+
const contentType = Object.keys(body.content)[0]
|
|
182
|
+
if (!contentType) return null
|
|
183
|
+
const schema = body.content[contentType]?.schema as OpenAPIV3.SchemaObject | undefined
|
|
184
|
+
if (!schema) return null
|
|
185
|
+
return {
|
|
186
|
+
contentType,
|
|
187
|
+
fields: flattenSchema(schema),
|
|
188
|
+
jsonExample: JSON.stringify(generateExampleJson(schema), null, 2),
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
interface ResponseSectionData {
|
|
193
|
+
status: string
|
|
194
|
+
description?: string
|
|
195
|
+
contentType?: string
|
|
196
|
+
fields: SchemaField[]
|
|
197
|
+
jsonExample?: string
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function getResponseSections(responses: Record<string, OpenAPIV3.ResponseObject>): ResponseSectionData[] {
|
|
201
|
+
return Object.entries(responses).map(([status, resp]) => {
|
|
202
|
+
const content = resp.content ?? {}
|
|
203
|
+
const contentType = Object.keys(content)[0]
|
|
204
|
+
const schema = contentType
|
|
205
|
+
? (content[contentType]?.schema as OpenAPIV3.SchemaObject | undefined)
|
|
206
|
+
: undefined
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
status,
|
|
210
|
+
description: resp.description,
|
|
211
|
+
contentType,
|
|
212
|
+
fields: schema ? flattenSchema(schema) : [],
|
|
213
|
+
jsonExample: schema ? JSON.stringify(generateExampleJson(schema), null, 2) : undefined,
|
|
214
|
+
}
|
|
215
|
+
})
|
|
216
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
.label {
|
|
2
|
+
font-size: var(--rs-font-size-small);
|
|
3
|
+
line-height: var(--rs-line-height-small);
|
|
4
|
+
letter-spacing: var(--rs-letter-spacing-small);
|
|
5
|
+
color: var(--rs-color-foreground-base-primary);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.container {
|
|
9
|
+
border: 0.5px solid var(--rs-color-border-base-primary);
|
|
10
|
+
border-radius: var(--rs-radius-2);
|
|
11
|
+
overflow: hidden;
|
|
12
|
+
height: 440px;
|
|
13
|
+
width: 100%;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.header {
|
|
17
|
+
padding: var(--rs-space-3) var(--rs-space-5);
|
|
18
|
+
background: var(--rs-color-background-base-secondary);
|
|
19
|
+
border-bottom: 0.5px solid var(--rs-color-border-base-primary);
|
|
20
|
+
flex-shrink: 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.tab {
|
|
24
|
+
display: flex;
|
|
25
|
+
align-items: center;
|
|
26
|
+
justify-content: center;
|
|
27
|
+
height: 20px;
|
|
28
|
+
padding: 0 var(--rs-space-2);
|
|
29
|
+
border: 0.5px solid var(--rs-color-border-base-primary);
|
|
30
|
+
border-radius: var(--rs-radius-2);
|
|
31
|
+
background: transparent;
|
|
32
|
+
cursor: pointer;
|
|
33
|
+
font-family: var(--rs-font-body);
|
|
34
|
+
font-size: var(--rs-font-size-mini);
|
|
35
|
+
font-weight: var(--rs-font-weight-medium);
|
|
36
|
+
line-height: var(--rs-line-height-mini);
|
|
37
|
+
letter-spacing: var(--rs-letter-spacing-mini);
|
|
38
|
+
color: var(--rs-color-foreground-base-secondary);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.tab:hover {
|
|
42
|
+
background: var(--rs-color-background-neutral-secondary);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.tabActive {
|
|
46
|
+
background: var(--rs-color-background-neutral-primary);
|
|
47
|
+
border-color: var(--rs-color-border-base-secondary);
|
|
48
|
+
color: var(--rs-color-foreground-base-primary);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.body {
|
|
52
|
+
flex: 1;
|
|
53
|
+
min-height: 0;
|
|
54
|
+
overflow-x: hidden;
|
|
55
|
+
overflow-y: auto;
|
|
56
|
+
background: var(--rs-color-background-base-primary);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.codeBlock {
|
|
60
|
+
border: none;
|
|
61
|
+
border-radius: 0;
|
|
62
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import { CodeBlock, CopyButton, Flex } from '@raystack/apsara'
|
|
5
|
+
import styles from './api-response-panel.module.css'
|
|
6
|
+
|
|
7
|
+
interface ResponseData {
|
|
8
|
+
status: string
|
|
9
|
+
description?: string
|
|
10
|
+
jsonExample?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface ApiResponsePanelProps {
|
|
14
|
+
responses: ResponseData[]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function ApiResponsePanel({ responses }: ApiResponsePanelProps) {
|
|
18
|
+
const [selected, setSelected] = useState(responses[0]?.status ?? '')
|
|
19
|
+
|
|
20
|
+
if (responses.length === 0) return null
|
|
21
|
+
|
|
22
|
+
const active = responses.find((r) => r.status === selected) ?? responses[0]
|
|
23
|
+
const displayJson = active.jsonExample ?? '{}'
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<Flex direction='column' gap={4}>
|
|
27
|
+
<span className={styles.label}>Response:</span>
|
|
28
|
+
<Flex direction='column' className={styles.container}>
|
|
29
|
+
<Flex align='center' justify='between' className={styles.header}>
|
|
30
|
+
<Flex align='center' gap={3}>
|
|
31
|
+
{responses.map((resp) => (
|
|
32
|
+
<button
|
|
33
|
+
key={resp.status}
|
|
34
|
+
type="button"
|
|
35
|
+
className={`${styles.tab} ${resp.status === active.status ? styles.tabActive : ''}`}
|
|
36
|
+
onClick={() => setSelected(resp.status)}
|
|
37
|
+
>
|
|
38
|
+
{resp.status}
|
|
39
|
+
</button>
|
|
40
|
+
))}
|
|
41
|
+
</Flex>
|
|
42
|
+
<CopyButton text={displayJson} size={3} />
|
|
43
|
+
</Flex>
|
|
44
|
+
<div className={styles.body}>
|
|
45
|
+
<CodeBlock hideLineNumbers className={styles.codeBlock}>
|
|
46
|
+
<CodeBlock.Content>
|
|
47
|
+
<CodeBlock.Code language="json">{displayJson}</CodeBlock.Code>
|
|
48
|
+
</CodeBlock.Content>
|
|
49
|
+
</CodeBlock>
|
|
50
|
+
</div>
|
|
51
|
+
</Flex>
|
|
52
|
+
</Flex>
|
|
53
|
+
)
|
|
54
|
+
}
|