@easydocs/dashboard 0.2.0 → 0.4.2
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@easydocs/dashboard",
|
|
3
|
-
"version": "0.2
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "EasyDocs docs dashboard — view and export AI-generated OpenAPI specs",
|
|
5
5
|
"files": [
|
|
6
6
|
"src",
|
|
@@ -11,11 +11,15 @@
|
|
|
11
11
|
"tsconfig.json"
|
|
12
12
|
],
|
|
13
13
|
"dependencies": {
|
|
14
|
+
"@codemirror/lang-json": "^6.0.2",
|
|
15
|
+
"@codemirror/theme-one-dark": "^6.1.3",
|
|
16
|
+
"@uiw/react-codemirror": "^4.25.9",
|
|
14
17
|
"js-yaml": "^4.1.0",
|
|
15
18
|
"next": "15.1.6",
|
|
16
19
|
"react": "^19.0.0",
|
|
17
20
|
"react-dom": "^19.0.0",
|
|
18
|
-
"
|
|
21
|
+
"zod": "^3.25.76",
|
|
22
|
+
"@easydocs/core": "0.4.2"
|
|
19
23
|
},
|
|
20
24
|
"devDependencies": {
|
|
21
25
|
"@types/js-yaml": "^4.0.9",
|
|
@@ -139,7 +139,7 @@ export function Dashboard({ endpoints, projects, currentProject }: Props) {
|
|
|
139
139
|
{/* Main */}
|
|
140
140
|
<main className="flex-1 overflow-hidden">
|
|
141
141
|
{selected ? (
|
|
142
|
-
<EndpointDetail endpoint={selected} />
|
|
142
|
+
<EndpointDetail key={selected.id} endpoint={selected} />
|
|
143
143
|
) : (
|
|
144
144
|
<div className="flex items-center justify-center h-full text-zinc-500 text-sm">
|
|
145
145
|
Select an endpoint to view its documentation.
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useState } from 'react'
|
|
3
|
+
import { useState, useCallback } from 'react'
|
|
4
4
|
import { MethodBadge } from './MethodBadge'
|
|
5
|
-
import { SchemaViewer } from './SchemaViewer'
|
|
6
5
|
import { SpecEditor } from './SpecEditor'
|
|
7
6
|
import { ConflictBanner } from './ConflictBanner'
|
|
8
7
|
import type { Endpoint } from '@easydocs/core/schema'
|
|
@@ -10,6 +9,10 @@ import type { Endpoint } from '@easydocs/core/schema'
|
|
|
10
9
|
export function EndpointDetail({ endpoint: initial }: { endpoint: Endpoint }) {
|
|
11
10
|
const [endpoint, setEndpoint] = useState(initial)
|
|
12
11
|
const [editing, setEditing] = useState(false)
|
|
12
|
+
const [exampleOpen, setExampleOpen] = useState<Record<string, boolean>>({})
|
|
13
|
+
const toggleExample = useCallback((key: string) => {
|
|
14
|
+
setExampleOpen((prev) => ({ ...prev, [key]: !prev[key] }))
|
|
15
|
+
}, [])
|
|
13
16
|
|
|
14
17
|
const activeSpec = endpoint.isManuallyEdited ? endpoint.manualSpec : endpoint.spec
|
|
15
18
|
|
|
@@ -28,12 +31,17 @@ export function EndpointDetail({ endpoint: initial }: { endpoint: Endpoint }) {
|
|
|
28
31
|
)}
|
|
29
32
|
|
|
30
33
|
<div className="flex items-center justify-between px-8 pt-6 pb-2">
|
|
31
|
-
<div className="flex items-center gap-3">
|
|
34
|
+
<div className="flex items-center gap-3 flex-wrap">
|
|
32
35
|
<MethodBadge method={endpoint.method} />
|
|
33
36
|
<code className="text-lg font-mono text-zinc-100">{endpoint.path}</code>
|
|
34
37
|
{endpoint.isManuallyEdited && !endpoint.hasConflict && (
|
|
35
38
|
<span className="text-xs text-zinc-500 bg-zinc-800 px-1.5 py-0.5 rounded">edited</span>
|
|
36
39
|
)}
|
|
40
|
+
{activeSpec.tags?.map((tag) => (
|
|
41
|
+
<span key={tag} className="text-xs text-zinc-400 bg-zinc-800 border border-zinc-700 px-2 py-0.5 rounded-full">
|
|
42
|
+
{tag}
|
|
43
|
+
</span>
|
|
44
|
+
))}
|
|
37
45
|
</div>
|
|
38
46
|
<button
|
|
39
47
|
onClick={() => setEditing((e) => !e)}
|
|
@@ -45,7 +53,7 @@ export function EndpointDetail({ endpoint: initial }: { endpoint: Endpoint }) {
|
|
|
45
53
|
|
|
46
54
|
{editing ? (
|
|
47
55
|
<div className="flex-1 overflow-hidden border-t border-zinc-800 mt-4">
|
|
48
|
-
<SpecEditor endpoint={endpoint} onSaved={setEndpoint} />
|
|
56
|
+
<SpecEditor endpoint={endpoint} onSaved={setEndpoint} onCancel={() => setEditing(false)} />
|
|
49
57
|
</div>
|
|
50
58
|
) : (
|
|
51
59
|
<div className="flex-1 overflow-y-auto px-8 pb-8 space-y-8">
|
|
@@ -68,16 +76,20 @@ export function EndpointDetail({ endpoint: initial }: { endpoint: Endpoint }) {
|
|
|
68
76
|
</h3>
|
|
69
77
|
<div className="rounded-lg border border-zinc-800 divide-y divide-zinc-800">
|
|
70
78
|
{activeSpec.parameters.map((param) => (
|
|
71
|
-
<div key={`${param.in}-${param.name}`} className=
|
|
79
|
+
<div key={`${param.in}-${param.name}`} className={`px-4 py-3 flex gap-4 ${param.deprecated ? 'bg-yellow-500/5' : ''}`}>
|
|
72
80
|
<div className="min-w-0 flex-1">
|
|
73
81
|
<div className="flex items-center gap-2 flex-wrap">
|
|
74
|
-
<code className=
|
|
82
|
+
<code className={`text-sm font-mono ${param.deprecated ? 'line-through text-zinc-500' : 'text-zinc-100'}`}>
|
|
83
|
+
{param.name}
|
|
84
|
+
</code>
|
|
75
85
|
<span className="text-xs text-zinc-500 bg-zinc-800 px-1.5 py-0.5 rounded">
|
|
76
86
|
{param.in}
|
|
77
87
|
</span>
|
|
78
88
|
{param.required && <span className="text-xs text-red-400">required</span>}
|
|
79
89
|
{param.deprecated && (
|
|
80
|
-
<span className="text-xs text-yellow-400">
|
|
90
|
+
<span className="text-xs font-medium text-yellow-400 bg-yellow-500/10 border border-yellow-500/30 px-1.5 py-0.5 rounded">
|
|
91
|
+
deprecated
|
|
92
|
+
</span>
|
|
81
93
|
)}
|
|
82
94
|
</div>
|
|
83
95
|
{param.description && (
|
|
@@ -95,14 +107,27 @@ export function EndpointDetail({ endpoint: initial }: { endpoint: Endpoint }) {
|
|
|
95
107
|
</section>
|
|
96
108
|
)}
|
|
97
109
|
|
|
110
|
+
{activeSpec.security && activeSpec.security.length > 0 && (
|
|
111
|
+
<section>
|
|
112
|
+
<h3 className="text-xs font-semibold text-zinc-400 uppercase tracking-wider mb-3">
|
|
113
|
+
Security
|
|
114
|
+
</h3>
|
|
115
|
+
<div className="flex flex-wrap gap-2">
|
|
116
|
+
{activeSpec.security.flatMap((requirement) =>
|
|
117
|
+
Object.keys(requirement).map((scheme) => (
|
|
118
|
+
<SecurityBadge key={scheme} scheme={scheme} />
|
|
119
|
+
))
|
|
120
|
+
)}
|
|
121
|
+
</div>
|
|
122
|
+
</section>
|
|
123
|
+
)}
|
|
124
|
+
|
|
98
125
|
{activeSpec.requestBody && (
|
|
99
126
|
<section>
|
|
100
127
|
<h3 className="text-xs font-semibold text-zinc-400 uppercase tracking-wider mb-3">
|
|
101
128
|
Request Body
|
|
102
129
|
</h3>
|
|
103
|
-
<
|
|
104
|
-
<SchemaViewer data={activeSpec.requestBody.content} />
|
|
105
|
-
</div>
|
|
130
|
+
<ContentFields content={activeSpec.requestBody.content} />
|
|
106
131
|
</section>
|
|
107
132
|
)}
|
|
108
133
|
|
|
@@ -112,19 +137,39 @@ export function EndpointDetail({ endpoint: initial }: { endpoint: Endpoint }) {
|
|
|
112
137
|
Responses
|
|
113
138
|
</h3>
|
|
114
139
|
<div className="space-y-3">
|
|
115
|
-
{Object.entries(activeSpec.responses).map(([status, response]) =>
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
140
|
+
{Object.entries(activeSpec.responses).map(([status, response]) => {
|
|
141
|
+
const mediaType = response.content ? Object.values(response.content)[0] : null
|
|
142
|
+
const example = mediaType?.example
|
|
143
|
+
const hasExample = example !== undefined
|
|
144
|
+
const isOpen = exampleOpen[status] ?? false
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<div key={status} className="rounded-lg border border-zinc-800 overflow-hidden">
|
|
148
|
+
<div className="flex items-center gap-3 px-4 py-2 bg-zinc-900/50 border-b border-zinc-800">
|
|
149
|
+
<StatusBadge code={status} />
|
|
150
|
+
<span className="text-sm text-zinc-400 flex-1">{response.description}</span>
|
|
151
|
+
{hasExample && (
|
|
152
|
+
<button
|
|
153
|
+
onClick={() => toggleExample(status)}
|
|
154
|
+
className="text-xs text-zinc-500 hover:text-zinc-300 transition-colors"
|
|
155
|
+
>
|
|
156
|
+
{isOpen ? 'Hide example' : 'Example'}
|
|
157
|
+
</button>
|
|
158
|
+
)}
|
|
124
159
|
</div>
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
160
|
+
{response.content && !isOpen && (
|
|
161
|
+
<div className="p-4">
|
|
162
|
+
<ContentFields content={response.content} />
|
|
163
|
+
</div>
|
|
164
|
+
)}
|
|
165
|
+
{hasExample && isOpen && (
|
|
166
|
+
<pre className="p-4 text-xs font-mono text-zinc-300 leading-5 overflow-x-auto">
|
|
167
|
+
{JSON.stringify(example, null, 2)}
|
|
168
|
+
</pre>
|
|
169
|
+
)}
|
|
170
|
+
</div>
|
|
171
|
+
)
|
|
172
|
+
})}
|
|
128
173
|
</div>
|
|
129
174
|
</section>
|
|
130
175
|
)}
|
|
@@ -134,6 +179,143 @@ export function EndpointDetail({ endpoint: initial }: { endpoint: Endpoint }) {
|
|
|
134
179
|
)
|
|
135
180
|
}
|
|
136
181
|
|
|
182
|
+
type JsonSchema = {
|
|
183
|
+
type?: string
|
|
184
|
+
format?: string
|
|
185
|
+
description?: string
|
|
186
|
+
enum?: unknown[]
|
|
187
|
+
properties?: Record<string, JsonSchema>
|
|
188
|
+
required?: string[]
|
|
189
|
+
items?: JsonSchema
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function ContentFields({ content }: { content: Record<string, { schema?: JsonSchema }> }) {
|
|
193
|
+
const mediaType = Object.values(content ?? {})[0]
|
|
194
|
+
const schema = mediaType?.schema
|
|
195
|
+
if (!schema) return null
|
|
196
|
+
|
|
197
|
+
if (schema.type === 'object' && schema.properties) {
|
|
198
|
+
return (
|
|
199
|
+
<PropertiesTable properties={schema.properties} required={schema.required ?? []} depth={0} />
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (schema.type === 'array' && schema.items) {
|
|
204
|
+
return (
|
|
205
|
+
<div>
|
|
206
|
+
<p className="text-xs text-zinc-500 mb-2 font-mono">array of object</p>
|
|
207
|
+
{schema.items.properties && (
|
|
208
|
+
<PropertiesTable
|
|
209
|
+
properties={schema.items.properties}
|
|
210
|
+
required={schema.items.required ?? []}
|
|
211
|
+
depth={0}
|
|
212
|
+
/>
|
|
213
|
+
)}
|
|
214
|
+
</div>
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return (
|
|
219
|
+
<div className="rounded-lg border border-zinc-800 px-4 py-3 text-xs text-zinc-400 font-mono">
|
|
220
|
+
{schema.type ?? 'unknown'}
|
|
221
|
+
{schema.format ? <span className="text-zinc-500">({schema.format})</span> : null}
|
|
222
|
+
</div>
|
|
223
|
+
)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function PropertiesTable({
|
|
227
|
+
properties,
|
|
228
|
+
required,
|
|
229
|
+
depth,
|
|
230
|
+
}: {
|
|
231
|
+
properties: Record<string, JsonSchema>
|
|
232
|
+
required: string[]
|
|
233
|
+
depth: number
|
|
234
|
+
}) {
|
|
235
|
+
return (
|
|
236
|
+
<div className={`rounded-lg border border-zinc-800 divide-y divide-zinc-800 ${depth > 0 ? 'mt-2' : ''}`}>
|
|
237
|
+
{Object.entries(properties).map(([name, schema]) => {
|
|
238
|
+
const isRequired = required.includes(name)
|
|
239
|
+
const isArrayOfObject = schema.type === 'array' && schema.items?.properties
|
|
240
|
+
const typeLabel = isArrayOfObject
|
|
241
|
+
? 'array of object'
|
|
242
|
+
: schema.type === 'array'
|
|
243
|
+
? `array[${schema.items?.type ?? 'unknown'}]`
|
|
244
|
+
: (schema.type ?? 'unknown')
|
|
245
|
+
const formatSuffix = schema.format ? `(${schema.format})` : ''
|
|
246
|
+
const hasNestedObject = schema.type === 'object' && schema.properties
|
|
247
|
+
const hasNestedArray = isArrayOfObject
|
|
248
|
+
|
|
249
|
+
return (
|
|
250
|
+
<div key={name} className="px-4 py-3">
|
|
251
|
+
<div className="flex gap-4">
|
|
252
|
+
<div className="min-w-0 flex-1">
|
|
253
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
254
|
+
<code className="text-sm font-mono text-zinc-100">{name}</code>
|
|
255
|
+
{isRequired ? (
|
|
256
|
+
<span className="text-xs text-red-400">required</span>
|
|
257
|
+
) : (
|
|
258
|
+
<span className="text-xs text-zinc-600">optional</span>
|
|
259
|
+
)}
|
|
260
|
+
{schema.description && (
|
|
261
|
+
<p className="text-sm text-zinc-500 mt-1 w-full">{schema.description}</p>
|
|
262
|
+
)}
|
|
263
|
+
{schema.enum && (
|
|
264
|
+
<p className="text-xs text-zinc-500 mt-1 w-full">
|
|
265
|
+
One of:{' '}
|
|
266
|
+
{schema.enum.map((v, i) => (
|
|
267
|
+
<code key={i} className="text-zinc-300 bg-zinc-800 px-1 rounded mx-0.5">
|
|
268
|
+
{String(v)}
|
|
269
|
+
</code>
|
|
270
|
+
))}
|
|
271
|
+
</p>
|
|
272
|
+
)}
|
|
273
|
+
</div>
|
|
274
|
+
</div>
|
|
275
|
+
<div className="text-xs text-zinc-400 font-mono self-start whitespace-nowrap">
|
|
276
|
+
{typeLabel}
|
|
277
|
+
{formatSuffix && <span className="text-zinc-500">{formatSuffix}</span>}
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
{hasNestedObject && (
|
|
281
|
+
<PropertiesTable
|
|
282
|
+
properties={schema.properties!}
|
|
283
|
+
required={schema.required ?? []}
|
|
284
|
+
depth={depth + 1}
|
|
285
|
+
/>
|
|
286
|
+
)}
|
|
287
|
+
{hasNestedArray && (
|
|
288
|
+
<PropertiesTable
|
|
289
|
+
properties={schema.items!.properties!}
|
|
290
|
+
required={schema.items!.required ?? []}
|
|
291
|
+
depth={depth + 1}
|
|
292
|
+
/>
|
|
293
|
+
)}
|
|
294
|
+
</div>
|
|
295
|
+
)
|
|
296
|
+
})}
|
|
297
|
+
</div>
|
|
298
|
+
)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const SCHEME_META: Record<string, { label: string; detail: string }> = {
|
|
302
|
+
bearerAuth: { label: 'Bearer', detail: 'http, bearer' },
|
|
303
|
+
basicAuth: { label: 'Basic', detail: 'http, basic' },
|
|
304
|
+
apiKeyHeader: { label: 'API Key', detail: 'apiKey, header' },
|
|
305
|
+
apiKeyQuery: { label: 'API Key', detail: 'apiKey, query' },
|
|
306
|
+
cookieAuth: { label: 'Cookie', detail: 'apiKey, cookie' },
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function SecurityBadge({ scheme }: { scheme: string }) {
|
|
310
|
+
const meta = SCHEME_META[scheme] ?? { label: scheme, detail: 'unknown' }
|
|
311
|
+
return (
|
|
312
|
+
<span className="inline-flex items-center gap-1.5 rounded-md border border-zinc-700 bg-zinc-900 px-3 py-1.5 text-xs">
|
|
313
|
+
<span className="text-amber-400 font-medium">{meta.label}</span>
|
|
314
|
+
<span className="text-zinc-500">({meta.detail})</span>
|
|
315
|
+
</span>
|
|
316
|
+
)
|
|
317
|
+
}
|
|
318
|
+
|
|
137
319
|
function StatusBadge({ code }: { code: string }) {
|
|
138
320
|
const n = parseInt(code, 10)
|
|
139
321
|
const color =
|
|
@@ -1,30 +1,51 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useState } from 'react'
|
|
3
|
+
import { useState, useEffect } from 'react'
|
|
4
|
+
import CodeMirror from '@uiw/react-codemirror'
|
|
5
|
+
import { json } from '@codemirror/lang-json'
|
|
6
|
+
import { oneDark } from '@codemirror/theme-one-dark'
|
|
4
7
|
import type { Endpoint } from '@easydocs/core/schema'
|
|
5
8
|
import type { Operation } from '@easydocs/core'
|
|
9
|
+
import { OperationSchema } from '@/lib/operation-schema'
|
|
6
10
|
|
|
7
11
|
interface Props {
|
|
8
12
|
endpoint: Endpoint
|
|
9
13
|
onSaved: (updated: Endpoint) => void
|
|
14
|
+
onCancel: () => void
|
|
10
15
|
}
|
|
11
16
|
|
|
12
|
-
|
|
17
|
+
function validate(raw: string): string | null {
|
|
18
|
+
let parsed: unknown
|
|
19
|
+
try {
|
|
20
|
+
parsed = JSON.parse(raw)
|
|
21
|
+
} catch {
|
|
22
|
+
return 'Invalid JSON'
|
|
23
|
+
}
|
|
24
|
+
const result = OperationSchema.safeParse(parsed)
|
|
25
|
+
if (!result.success) {
|
|
26
|
+
const first = result.error.errors[0]
|
|
27
|
+
return `${first.path.join('.') || 'root'}: ${first.message}`
|
|
28
|
+
}
|
|
29
|
+
return null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function SpecEditor({ endpoint, onSaved, onCancel }: Props) {
|
|
13
33
|
const activeSpec = endpoint.isManuallyEdited ? endpoint.manualSpec : endpoint.spec
|
|
14
34
|
const [value, setValue] = useState(() => JSON.stringify(activeSpec, null, 2))
|
|
15
|
-
const [
|
|
35
|
+
const [validationError, setValidationError] = useState<string | null>(null)
|
|
36
|
+
const [saveError, setSaveError] = useState<string | null>(null)
|
|
16
37
|
const [saving, setSaving] = useState(false)
|
|
17
38
|
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
setValidationError(validate(value))
|
|
41
|
+
}, [value])
|
|
42
|
+
|
|
18
43
|
async function handleSave() {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
parsed = JSON.parse(value) as Operation
|
|
23
|
-
} catch {
|
|
24
|
-
setError('Invalid JSON')
|
|
25
|
-
return
|
|
26
|
-
}
|
|
44
|
+
const err = validate(value)
|
|
45
|
+
if (err) { setValidationError(err); return }
|
|
46
|
+
setSaveError(null)
|
|
27
47
|
setSaving(true)
|
|
48
|
+
const parsed = JSON.parse(value) as Operation
|
|
28
49
|
const res = await fetch(`/api/endpoints/${endpoint.id}/spec`, {
|
|
29
50
|
method: 'PUT',
|
|
30
51
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -34,31 +55,54 @@ export function SpecEditor({ endpoint, onSaved }: Props) {
|
|
|
34
55
|
if (res.ok) {
|
|
35
56
|
onSaved({ ...endpoint, manualSpec: parsed, isManuallyEdited: true, hasConflict: false })
|
|
36
57
|
} else {
|
|
37
|
-
|
|
58
|
+
setSaveError('Failed to save')
|
|
38
59
|
}
|
|
39
60
|
}
|
|
40
61
|
|
|
62
|
+
const invalid = validationError !== null
|
|
63
|
+
|
|
41
64
|
return (
|
|
42
65
|
<div className="flex flex-col h-full">
|
|
43
66
|
<div className="flex items-center justify-between px-4 py-2 border-b border-zinc-800">
|
|
44
67
|
<span className="text-xs text-zinc-400">Edit spec (JSON)</span>
|
|
45
|
-
<div className="flex gap-2">
|
|
46
|
-
{
|
|
68
|
+
<div className="flex items-center gap-2">
|
|
69
|
+
{saveError && <span className="text-xs text-red-400">{saveError}</span>}
|
|
70
|
+
<button
|
|
71
|
+
onClick={onCancel}
|
|
72
|
+
className="text-xs px-3 py-1 rounded bg-zinc-800 text-zinc-400 hover:text-zinc-200 hover:bg-zinc-700 transition-colors"
|
|
73
|
+
>
|
|
74
|
+
Cancel
|
|
75
|
+
</button>
|
|
47
76
|
<button
|
|
48
77
|
onClick={handleSave}
|
|
49
|
-
disabled={saving}
|
|
50
|
-
className="text-xs px-3 py-1 rounded bg-zinc-700 text-zinc-100 hover:bg-zinc-600 disabled:opacity-
|
|
78
|
+
disabled={saving || invalid}
|
|
79
|
+
className="text-xs px-3 py-1 rounded bg-zinc-700 text-zinc-100 hover:bg-zinc-600 disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
|
|
51
80
|
>
|
|
52
81
|
{saving ? 'Saving…' : 'Save'}
|
|
53
82
|
</button>
|
|
54
83
|
</div>
|
|
55
84
|
</div>
|
|
56
|
-
<
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
85
|
+
<div className="flex-1 overflow-auto">
|
|
86
|
+
<CodeMirror
|
|
87
|
+
value={value}
|
|
88
|
+
height="100%"
|
|
89
|
+
extensions={[json()]}
|
|
90
|
+
theme={oneDark}
|
|
91
|
+
onChange={setValue}
|
|
92
|
+
basicSetup={{
|
|
93
|
+
lineNumbers: true,
|
|
94
|
+
foldGutter: true,
|
|
95
|
+
bracketMatching: true,
|
|
96
|
+
autocompletion: true,
|
|
97
|
+
}}
|
|
98
|
+
style={{ fontSize: '12px', height: '100%' }}
|
|
99
|
+
/>
|
|
100
|
+
</div>
|
|
101
|
+
{validationError && (
|
|
102
|
+
<div className="px-4 py-2 border-t border-red-900/50 bg-red-950/30 text-xs text-red-400 font-mono">
|
|
103
|
+
{validationError}
|
|
104
|
+
</div>
|
|
105
|
+
)}
|
|
62
106
|
</div>
|
|
63
107
|
)
|
|
64
108
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
const SchemaObject = z.record(z.any()).optional()
|
|
4
|
+
|
|
5
|
+
const MediaTypeSchema = z.object({
|
|
6
|
+
schema: SchemaObject,
|
|
7
|
+
example: z.any().optional(),
|
|
8
|
+
examples: z.record(z.any()).optional(),
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
const ParameterSchema = z.object({
|
|
12
|
+
name: z.string(),
|
|
13
|
+
in: z.enum(['query', 'header', 'path', 'cookie']),
|
|
14
|
+
description: z.string().optional(),
|
|
15
|
+
required: z.boolean().default(false),
|
|
16
|
+
deprecated: z.boolean().optional(),
|
|
17
|
+
schema: SchemaObject,
|
|
18
|
+
example: z.any().optional(),
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const ResponseSchema = z.object({
|
|
22
|
+
description: z.string(),
|
|
23
|
+
headers: z.record(z.any()).optional(),
|
|
24
|
+
content: z.record(MediaTypeSchema).optional(),
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
export const OperationSchema = z.object({
|
|
28
|
+
tags: z.array(z.string()).optional(),
|
|
29
|
+
summary: z.string().optional(),
|
|
30
|
+
description: z.string().optional(),
|
|
31
|
+
operationId: z.string().optional(),
|
|
32
|
+
parameters: z.array(ParameterSchema).optional(),
|
|
33
|
+
requestBody: z
|
|
34
|
+
.object({
|
|
35
|
+
description: z.string().optional(),
|
|
36
|
+
required: z.boolean().optional(),
|
|
37
|
+
content: z.record(MediaTypeSchema),
|
|
38
|
+
})
|
|
39
|
+
.nullable()
|
|
40
|
+
.optional(),
|
|
41
|
+
responses: z.record(ResponseSchema).default({}),
|
|
42
|
+
deprecated: z.boolean().optional(),
|
|
43
|
+
security: z.array(z.record(z.array(z.string()))).optional(),
|
|
44
|
+
})
|