@clidey/dory 0.30.0 → 0.33.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/bin/dist/dory.js +44 -20
- package/bin/dist/dory.js.map +1 -1
- package/index.html +54 -0
- package/package.json +9 -4
- package/src/app.css +5 -0
- package/src/app.tsx +13 -0
- package/src/components/dropdown.tsx +53 -0
- package/src/components/hooks.tsx +36 -0
- package/src/components/loading.tsx +21 -0
- package/src/components/mdx-provider.tsx +13 -0
- package/src/components/navigation.tsx +58 -0
- package/src/components/notification.tsx +13 -0
- package/src/components/routes.tsx +92 -0
- package/src/components/search-index.ts +43 -0
- package/src/components/search.tsx +448 -0
- package/src/components/store.ts +216 -0
- package/src/config/mdx.js +12 -0
- package/src/config/mdx.ts +12 -0
- package/src/embed/loader.ts +200 -0
- package/src/embed/types.ts +54 -0
- package/src/embed/widget-iframe.ts +302 -0
- package/src/main.tsx +22 -0
- package/src/mdx/accordion.tsx +41 -0
- package/src/mdx/api-playground.tsx +659 -0
- package/src/mdx/api.tsx +354 -0
- package/src/mdx/async-api.tsx +353 -0
- package/src/mdx/callout.tsx +64 -0
- package/src/mdx/card.tsx +54 -0
- package/src/mdx/code.tsx +289 -0
- package/src/mdx/expandable.tsx +36 -0
- package/src/mdx/fence.tsx +74 -0
- package/src/mdx/icon.tsx +73 -0
- package/src/mdx/latex.tsx +26 -0
- package/src/mdx/list.tsx +7 -0
- package/src/mdx/mdx.tsx +47 -0
- package/src/mdx/mermaid.tsx +147 -0
- package/src/mdx/open-api.tsx +311 -0
- package/src/mdx/processor.ts +174 -0
- package/src/mdx/source.tsx +58 -0
- package/src/mdx/step.tsx +51 -0
- package/src/mdx/table.tsx +20 -0
- package/src/mdx/tag.tsx +53 -0
- package/src/mdx/text.tsx +99 -0
- package/src/mdx/websocket-playground.tsx +824 -0
- package/src/plugins/docs-assets.ts +79 -0
- package/src/plugins/frontmatter-dev-server.ts +158 -0
- package/src/plugins/frontmatter-generator.ts +157 -0
- package/src/plugins/html-files-middleware.ts +108 -0
- package/src/plugins/html-metadata-injector.ts +76 -0
- package/src/plugins/llm-txt-dev-server.ts +183 -0
- package/src/plugins/llm-txt-generator.ts +186 -0
- package/src/plugins/sanitize.ts +163 -0
- package/src/plugins/unified-mdx-plugin.ts +35 -0
- package/src/sdk/index.ts +89 -0
- package/src/sdk/react.tsx +139 -0
- package/src/sdk/vue.ts +107 -0
- package/src/theme.scss +72 -0
- package/src/types/config.ts +89 -0
- package/src/ui/header.tsx +96 -0
- package/src/ui/layout.tsx +237 -0
- package/src/ui/mobile-navigation.tsx +72 -0
- package/src/ui/prev-next-link.tsx +74 -0
- package/src/ui/table-of-content.tsx +167 -0
- package/src/utils/colors.ts +54 -0
- package/src/utils/fonts.ts +23 -0
- package/src/utils/functions.ts +12 -0
- package/src/vite-env.d.ts +1 -0
- package/vite.config.embed-loader.ts +28 -0
- package/vite.config.embed-widget.ts +31 -0
- package/vite.config.ts +45 -0
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
import { Button, TextArea, useTheme, Input } from '@clidey/ux'
|
|
2
|
+
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from '@headlessui/react'
|
|
3
|
+
import { CheckIcon } from '@heroicons/react/24/outline'
|
|
4
|
+
import classNames from 'classnames'
|
|
5
|
+
import { PencilIcon, PlusIcon, TrashIcon, XIcon } from 'lucide-react'
|
|
6
|
+
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
|
|
7
|
+
import { Highlight, themes } from 'prism-react-renderer'
|
|
8
|
+
import Dropdown from '../components/dropdown'
|
|
9
|
+
import { Fence } from './fence'
|
|
10
|
+
import { Tag } from './tag'
|
|
11
|
+
|
|
12
|
+
export interface Server {
|
|
13
|
+
url: string;
|
|
14
|
+
description?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface APIPlaygroundProps {
|
|
18
|
+
method: string
|
|
19
|
+
url: string
|
|
20
|
+
title?: string
|
|
21
|
+
description?: string
|
|
22
|
+
parameters?: Array<{
|
|
23
|
+
name: string
|
|
24
|
+
type: string
|
|
25
|
+
required?: boolean
|
|
26
|
+
description?: string
|
|
27
|
+
defaultValue?: string
|
|
28
|
+
}>
|
|
29
|
+
headers?: Record<string, string>
|
|
30
|
+
body?: object
|
|
31
|
+
authType?: 'bearer' | 'apiKey' | 'basic' | 'none'
|
|
32
|
+
servers?: Server[]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface RequestConfig {
|
|
36
|
+
method: string
|
|
37
|
+
url: string
|
|
38
|
+
headers: Record<string, string>
|
|
39
|
+
body?: string
|
|
40
|
+
queryParams: Record<string, string>
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface ResponseData {
|
|
44
|
+
status: number
|
|
45
|
+
statusText: string
|
|
46
|
+
headers: Record<string, string>
|
|
47
|
+
data: any
|
|
48
|
+
duration: number
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function PlayButton(props: React.ComponentPropsWithoutRef<'svg'>) {
|
|
52
|
+
return (
|
|
53
|
+
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
|
|
54
|
+
<path
|
|
55
|
+
fill="currentColor"
|
|
56
|
+
d="M6.3 2.841A1.5 1.5 0 0 0 4 4.11v11.78a1.5 1.5 0 0 0 2.3 1.269l9.344-5.89a1.5 1.5 0 0 0 0-2.538L6.3 2.841Z"
|
|
57
|
+
/>
|
|
58
|
+
</svg>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function LoadingSpinner(props: React.ComponentPropsWithoutRef<'svg'>) {
|
|
63
|
+
return (
|
|
64
|
+
<svg viewBox="0 0 20 20" aria-hidden="true" className="animate-spin" {...props}>
|
|
65
|
+
<path
|
|
66
|
+
fill="currentColor"
|
|
67
|
+
d="M10 2a8 8 0 0 1 8 8c0 .3-.02.59-.06.88l-1.99-.22A6 6 0 1 0 4 10c0-.34.03-.67.09-1L2.1 8.78A8 8 0 0 1 10 2Z"
|
|
68
|
+
/>
|
|
69
|
+
</svg>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface RequestConfig {
|
|
74
|
+
headers: Record<string, string>
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface HeadersTabProps {
|
|
78
|
+
requestConfig: RequestConfig
|
|
79
|
+
setRequestConfig: React.Dispatch<React.SetStateAction<RequestConfig>>
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
interface HeaderItem {
|
|
83
|
+
key: string
|
|
84
|
+
value: string
|
|
85
|
+
isEditing: boolean
|
|
86
|
+
isNew?: boolean
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const HeadersTab = ({ requestConfig, setRequestConfig }: HeadersTabProps) => {
|
|
90
|
+
const [headersList, setHeadersList] = useState<HeaderItem[]>([])
|
|
91
|
+
const hasInitialized = useRef(false)
|
|
92
|
+
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
if (!hasInitialized.current) {
|
|
95
|
+
const initialHeaders = Object.entries(requestConfig.headers ?? {}).map(([key, value]) => ({
|
|
96
|
+
key,
|
|
97
|
+
value,
|
|
98
|
+
isEditing: false
|
|
99
|
+
}))
|
|
100
|
+
setHeadersList(initialHeaders)
|
|
101
|
+
hasInitialized.current = true
|
|
102
|
+
}
|
|
103
|
+
}, [requestConfig.headers])
|
|
104
|
+
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
const newHeaders: Record<string, string> = {}
|
|
107
|
+
headersList.forEach(h => {
|
|
108
|
+
if (h.key.trim()) newHeaders[h.key] = h.value
|
|
109
|
+
})
|
|
110
|
+
setRequestConfig(prev => ({ ...prev, headers: newHeaders }))
|
|
111
|
+
}, [headersList, setRequestConfig])
|
|
112
|
+
|
|
113
|
+
const updateHeader = (index: number, field: 'key' | 'value', value: string) => {
|
|
114
|
+
setHeadersList(list =>
|
|
115
|
+
list.map((h, i) => (i === index ? { ...h, [field]: value } : h))
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const saveHeader = (index: number) => {
|
|
120
|
+
setHeadersList(list =>
|
|
121
|
+
list.map((h, i) => (i === index ? { ...h, isEditing: false, isNew: false } : h))
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const cancelEdit = (index: number) => {
|
|
126
|
+
setHeadersList(list =>
|
|
127
|
+
list.reduce<HeaderItem[]>((acc, h, i) => {
|
|
128
|
+
if (i === index) {
|
|
129
|
+
if (h.isNew) return acc // remove new row if cancelled
|
|
130
|
+
return [...acc, { ...h, isEditing: false }]
|
|
131
|
+
}
|
|
132
|
+
return [...acc, h]
|
|
133
|
+
}, [])
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const editHeader = (index: number) => {
|
|
138
|
+
setHeadersList(list =>
|
|
139
|
+
list.map((h, i) => (i === index ? { ...h, isEditing: true } : h))
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const removeHeader = (index: number) => {
|
|
144
|
+
setHeadersList(list => list.filter((_, i) => i !== index))
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const addEmptyHeaderRow = () => {
|
|
148
|
+
setHeadersList(list => [
|
|
149
|
+
...list,
|
|
150
|
+
{ key: '', value: '', isEditing: true, isNew: true }
|
|
151
|
+
])
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<div className="flex flex-col gap-2 px-2 py-4">
|
|
156
|
+
<div className="flex items-center gap-2 px-2">
|
|
157
|
+
<span className="text-sm font-medium text-zinc-400 w-32">Key</span>
|
|
158
|
+
<span className="text-sm font-medium text-zinc-400 flex-1">Value</span>
|
|
159
|
+
</div>
|
|
160
|
+
{headersList.map((hdr, idx) => (
|
|
161
|
+
<div key={idx} className="flex items-center gap-2">
|
|
162
|
+
{hdr.isEditing ? (
|
|
163
|
+
<>
|
|
164
|
+
<Input
|
|
165
|
+
type="text"
|
|
166
|
+
value={hdr.key}
|
|
167
|
+
onChange={e => updateHeader(idx, 'key', (e.target as HTMLInputElement).value)}
|
|
168
|
+
placeholder="Header name"
|
|
169
|
+
className="w-32 text-sm"
|
|
170
|
+
/>
|
|
171
|
+
<Input
|
|
172
|
+
type="text"
|
|
173
|
+
value={hdr.value}
|
|
174
|
+
onChange={e => updateHeader(idx, 'value', (e.target as HTMLInputElement).value)}
|
|
175
|
+
placeholder="Header value"
|
|
176
|
+
className="flex-1 text-sm"
|
|
177
|
+
/>
|
|
178
|
+
<Button onClick={() => saveHeader(idx)}>
|
|
179
|
+
<CheckIcon className="w-4 h-4" />
|
|
180
|
+
</Button>
|
|
181
|
+
<Button onClick={() => cancelEdit(idx)}>
|
|
182
|
+
<XIcon className="w-4 h-4" />
|
|
183
|
+
</Button>
|
|
184
|
+
</>
|
|
185
|
+
) : (
|
|
186
|
+
<>
|
|
187
|
+
<span className="text-sm w-32 truncate">{hdr.key}</span>
|
|
188
|
+
<span className="text-sm flex-1 truncate">{hdr.value}</span>
|
|
189
|
+
<Button onClick={() => editHeader(idx)}>
|
|
190
|
+
<PencilIcon className="w-4 h-4" />
|
|
191
|
+
</Button>
|
|
192
|
+
<Button onClick={() => removeHeader(idx)} className="text-red-400">
|
|
193
|
+
<TrashIcon className="w-4 h-4" />
|
|
194
|
+
</Button>
|
|
195
|
+
</>
|
|
196
|
+
)}
|
|
197
|
+
</div>
|
|
198
|
+
))}
|
|
199
|
+
<Button onClick={addEmptyHeaderRow} className="text-sm mt-2 w-fit">
|
|
200
|
+
<PlusIcon className="w-4 h-4" /> Add Header
|
|
201
|
+
</Button>
|
|
202
|
+
</div>
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
interface ParamItem {
|
|
207
|
+
name: string;
|
|
208
|
+
value: string;
|
|
209
|
+
isEditing: boolean;
|
|
210
|
+
isNew?: boolean;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
interface ParamsTabProps {
|
|
214
|
+
queryParams: Record<string, string>;
|
|
215
|
+
setQueryParams: React.Dispatch<React.SetStateAction<RequestConfig>>;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export const ParamsTab = ({ queryParams, setQueryParams }: ParamsTabProps) => {
|
|
219
|
+
const [paramsList, setParamsList] = useState<ParamItem[]>([]);
|
|
220
|
+
const hasInitialized = useRef(false);
|
|
221
|
+
|
|
222
|
+
useEffect(() => {
|
|
223
|
+
if (!hasInitialized.current) {
|
|
224
|
+
const initial = Object.entries(queryParams ?? {}).map(([key, value]) => ({
|
|
225
|
+
name: key, value, isEditing: false
|
|
226
|
+
}));
|
|
227
|
+
setParamsList(initial);
|
|
228
|
+
hasInitialized.current = true;
|
|
229
|
+
}
|
|
230
|
+
}, [queryParams]);
|
|
231
|
+
|
|
232
|
+
useEffect(() => {
|
|
233
|
+
const newParams: Record<string, string> = {};
|
|
234
|
+
paramsList.forEach(p => {
|
|
235
|
+
if (p.name.trim()) newParams[p.name] = p.value;
|
|
236
|
+
});
|
|
237
|
+
setQueryParams(prev => ({ ...prev, queryParams: newParams }));
|
|
238
|
+
}, [paramsList, setQueryParams]);
|
|
239
|
+
|
|
240
|
+
const updateParam = (idx: number, field: 'name' | 'value', val: string) =>
|
|
241
|
+
setParamsList(list => list.map((p, i) =>
|
|
242
|
+
i === idx ? { ...p, [field]: val } : p
|
|
243
|
+
));
|
|
244
|
+
|
|
245
|
+
const saveParam = (idx: number) =>
|
|
246
|
+
setParamsList(list => list.map((p, i) =>
|
|
247
|
+
i === idx ? { ...p, isEditing: false, isNew: false } : p
|
|
248
|
+
));
|
|
249
|
+
|
|
250
|
+
const cancelEdit = (idx: number) =>
|
|
251
|
+
setParamsList(list =>
|
|
252
|
+
list.reduce<ParamItem[]>((acc, p, i) => {
|
|
253
|
+
if (i === idx) {
|
|
254
|
+
if (p.isNew) return acc;
|
|
255
|
+
return [...acc, { ...p, isEditing: false }];
|
|
256
|
+
}
|
|
257
|
+
return [...acc, p];
|
|
258
|
+
}, [])
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
const editParam = (idx: number) =>
|
|
262
|
+
setParamsList(list =>
|
|
263
|
+
list.map((p, i) => (i === idx ? { ...p, isEditing: true } : p))
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
const removeParam = (idx: number) =>
|
|
267
|
+
setParamsList(list => list.filter((_, i) => i !== idx));
|
|
268
|
+
|
|
269
|
+
const addParamRow = () =>
|
|
270
|
+
setParamsList(list => [
|
|
271
|
+
...list, { name: '', value: '', isEditing: true, isNew: true }
|
|
272
|
+
]);
|
|
273
|
+
|
|
274
|
+
return (
|
|
275
|
+
<div className="flex flex-col gap-2 px-2 py-4">
|
|
276
|
+
<div className="flex items-center gap-2">
|
|
277
|
+
<span className="text-sm font-medium text-zinc-400 w-32">Name</span>
|
|
278
|
+
<span className="text-sm font-medium text-zinc-400 flex-1">Value</span>
|
|
279
|
+
</div>
|
|
280
|
+
{paramsList.map((p, i) => (
|
|
281
|
+
<div key={i} className="flex items-center gap-2">
|
|
282
|
+
{p.isEditing ? (
|
|
283
|
+
<>
|
|
284
|
+
<Input value={p.name} onChange={e => updateParam(i, 'name', (e.target as HTMLInputElement).value)} placeholder="Param name" className="w-32 text-sm" />
|
|
285
|
+
<Input value={p.value} onChange={e => updateParam(i, 'value', (e.target as HTMLInputElement).value)} placeholder="Param value" className="flex-1 text-sm" />
|
|
286
|
+
<Button onClick={() => saveParam(i)}>
|
|
287
|
+
<CheckIcon className="w-4 h-4" />
|
|
288
|
+
</Button>
|
|
289
|
+
<Button onClick={() => cancelEdit(i)}>
|
|
290
|
+
<XIcon className="w-4 h-4" />
|
|
291
|
+
</Button>
|
|
292
|
+
</>
|
|
293
|
+
) : (
|
|
294
|
+
<>
|
|
295
|
+
<span className="w-32 truncate text-sm">{p.name}</span>
|
|
296
|
+
<span className="flex-1 truncate text-sm">{p.value}</span>
|
|
297
|
+
<Button onClick={() => editParam(i)}>
|
|
298
|
+
<PencilIcon className="w-4 h-4" />
|
|
299
|
+
</Button>
|
|
300
|
+
<Button onClick={() => removeParam(i)} className="text-red-400">
|
|
301
|
+
<TrashIcon className="w-4 h-4" />
|
|
302
|
+
</Button>
|
|
303
|
+
</>
|
|
304
|
+
)}
|
|
305
|
+
</div>
|
|
306
|
+
))}
|
|
307
|
+
<Button onClick={addParamRow} className="mt-2 text-sm self-end">
|
|
308
|
+
<PlusIcon className="w-4 h-4" /> Add Parameter
|
|
309
|
+
</Button>
|
|
310
|
+
</div>
|
|
311
|
+
);
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
export function APIPlayground({
|
|
316
|
+
method,
|
|
317
|
+
url,
|
|
318
|
+
title,
|
|
319
|
+
description,
|
|
320
|
+
parameters = [],
|
|
321
|
+
headers: defaultHeaders = {},
|
|
322
|
+
body: defaultBody,
|
|
323
|
+
authType = 'none',
|
|
324
|
+
servers = []
|
|
325
|
+
}: APIPlaygroundProps) {
|
|
326
|
+
const [selectedTab, setSelectedTab] = useState(0)
|
|
327
|
+
const [isLoading, setIsLoading] = useState(false)
|
|
328
|
+
const [response, setResponse] = useState<ResponseData | null>(null)
|
|
329
|
+
const [error, setError] = useState<string | null>(null)
|
|
330
|
+
const isDark = useTheme().theme === 'dark';
|
|
331
|
+
const [selectedServer, setSelectedServer] = useState<Server | null>(servers?.[0] || null)
|
|
332
|
+
|
|
333
|
+
// Request configuration state
|
|
334
|
+
const [requestConfig, setRequestConfig] = useState<RequestConfig>({
|
|
335
|
+
method: method.toUpperCase(),
|
|
336
|
+
url: selectedServer?.url + url,
|
|
337
|
+
headers: { 'Content-Type': 'application/json', ...defaultHeaders },
|
|
338
|
+
queryParams: parameters.reduce((acc, param) => {
|
|
339
|
+
acc[param.name] = param.defaultValue || ''
|
|
340
|
+
return acc
|
|
341
|
+
}, {} as Record<string, string>),
|
|
342
|
+
body: defaultBody ? JSON.stringify(defaultBody, null, 2) : ''
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
// Auth state
|
|
346
|
+
const [auth, setAuth] = useState({
|
|
347
|
+
type: authType,
|
|
348
|
+
token: '',
|
|
349
|
+
apiKey: '',
|
|
350
|
+
username: '',
|
|
351
|
+
password: ''
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
const executeRequest = useCallback(async () => {
|
|
355
|
+
setIsLoading(true)
|
|
356
|
+
setError(null)
|
|
357
|
+
setResponse(null)
|
|
358
|
+
|
|
359
|
+
const startTime = Date.now()
|
|
360
|
+
|
|
361
|
+
try {
|
|
362
|
+
// Prepare headers based on auth type
|
|
363
|
+
const headers = { ...requestConfig.headers }
|
|
364
|
+
|
|
365
|
+
switch (auth.type) {
|
|
366
|
+
case 'bearer':
|
|
367
|
+
if (auth.token) {
|
|
368
|
+
headers['Authorization'] = `Bearer ${auth.token}`
|
|
369
|
+
}
|
|
370
|
+
break
|
|
371
|
+
case 'apiKey':
|
|
372
|
+
if (auth.apiKey) {
|
|
373
|
+
headers['X-API-Key'] = auth.apiKey
|
|
374
|
+
}
|
|
375
|
+
break
|
|
376
|
+
case 'basic':
|
|
377
|
+
if (auth.username && auth.password) {
|
|
378
|
+
headers['Authorization'] = `Basic ${btoa(`${auth.username}:${auth.password}`)}`
|
|
379
|
+
}
|
|
380
|
+
break
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const fetchOptions: RequestInit = {
|
|
384
|
+
method: requestConfig.method,
|
|
385
|
+
headers
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (requestConfig.method !== 'GET' && requestConfig.body) {
|
|
389
|
+
fetchOptions.body = JSON.stringify(JSON.parse(requestConfig.body))
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const response = await fetch(requestConfig.url, fetchOptions)
|
|
393
|
+
const endTime = Date.now()
|
|
394
|
+
|
|
395
|
+
let responseData
|
|
396
|
+
const contentType = response.headers.get('content-type')
|
|
397
|
+
|
|
398
|
+
if (contentType?.includes('application/json')) {
|
|
399
|
+
responseData = await response.json()
|
|
400
|
+
} else {
|
|
401
|
+
responseData = await response.text()
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const responseHeaders: Record<string, string> = {}
|
|
405
|
+
response.headers.forEach((value, key) => {
|
|
406
|
+
responseHeaders[key] = value
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
setResponse({
|
|
410
|
+
status: response.status,
|
|
411
|
+
statusText: response.statusText,
|
|
412
|
+
headers: responseHeaders,
|
|
413
|
+
data: responseData,
|
|
414
|
+
duration: endTime - startTime
|
|
415
|
+
})
|
|
416
|
+
} catch (err) {
|
|
417
|
+
setError(err instanceof Error ? err.message : 'Request failed')
|
|
418
|
+
} finally {
|
|
419
|
+
setIsLoading(false)
|
|
420
|
+
}
|
|
421
|
+
}, [requestConfig, auth])
|
|
422
|
+
|
|
423
|
+
const tabs = [
|
|
424
|
+
{ name: 'Parameters', key: 'params' },
|
|
425
|
+
{ name: 'Headers', key: 'headers' },
|
|
426
|
+
{ name: 'Body', key: 'body' },
|
|
427
|
+
{ name: 'Auth', key: 'auth' }
|
|
428
|
+
]
|
|
429
|
+
|
|
430
|
+
return (
|
|
431
|
+
<div className="my-6 overflow-hidden rounded-2xl">
|
|
432
|
+
{/* Header */}
|
|
433
|
+
<div className="flex min-h-[calc(--spacing(12)+1px)] flex-wrap items-start gap-x-4 px-4 pt-2 pb-4 justify-between">
|
|
434
|
+
<div className="flex items-center gap-3 pt-3">
|
|
435
|
+
<Tag variant="small">{method.toUpperCase()}</Tag>
|
|
436
|
+
<span className="font-mono text-sm">{title || url}</span>
|
|
437
|
+
</div>
|
|
438
|
+
<Button
|
|
439
|
+
onClick={executeRequest}
|
|
440
|
+
disabled={isLoading}>
|
|
441
|
+
{isLoading ? 'Sending...' : 'Send Request'}
|
|
442
|
+
{isLoading ? <LoadingSpinner className="h-4 w-4" /> : <PlayButton className="h-4 w-4" />}
|
|
443
|
+
</Button>
|
|
444
|
+
</div>
|
|
445
|
+
|
|
446
|
+
{description && (
|
|
447
|
+
<p className="text-sm px-4 py-3">{description}</p>
|
|
448
|
+
)}
|
|
449
|
+
|
|
450
|
+
<div className="flex flex-col gap-4">
|
|
451
|
+
<div className="flex flex-col gap-2">
|
|
452
|
+
<label className="text-sm font-medium">Server</label>
|
|
453
|
+
<Dropdown
|
|
454
|
+
buttonLabel={selectedServer?.description || 'Select Server'}
|
|
455
|
+
items={servers.map(server => ({ label: server.description || server.url, onClick: () => setSelectedServer(server) }))}
|
|
456
|
+
className="w-full"
|
|
457
|
+
/>
|
|
458
|
+
</div>
|
|
459
|
+
|
|
460
|
+
{/* Request Configuration */}
|
|
461
|
+
<div className="flex-1">
|
|
462
|
+
<TabGroup selectedIndex={selectedTab} onChange={setSelectedTab}>
|
|
463
|
+
<TabList className="flex">
|
|
464
|
+
{tabs.map((tab) => (
|
|
465
|
+
<Tab
|
|
466
|
+
key={tab.key}
|
|
467
|
+
className={({ selected }) =>
|
|
468
|
+
classNames(
|
|
469
|
+
'px-4 py-3 text-sm font-medium transition border-b-2',
|
|
470
|
+
selected
|
|
471
|
+
? 'border-brand-foreground text-brand-foreground'
|
|
472
|
+
: 'border-transparent text-zinc-400 hover:text-zinc-300'
|
|
473
|
+
)
|
|
474
|
+
}
|
|
475
|
+
>
|
|
476
|
+
{tab.name}
|
|
477
|
+
</Tab>
|
|
478
|
+
))}
|
|
479
|
+
</TabList>
|
|
480
|
+
|
|
481
|
+
<TabPanels className="min-h-[250px]">
|
|
482
|
+
<TabPanel className="p-4">
|
|
483
|
+
<ParamsTab
|
|
484
|
+
queryParams={requestConfig.queryParams}
|
|
485
|
+
setQueryParams={setRequestConfig}
|
|
486
|
+
/>
|
|
487
|
+
</TabPanel>
|
|
488
|
+
|
|
489
|
+
<TabPanel className="p-4">
|
|
490
|
+
<HeadersTab
|
|
491
|
+
requestConfig={requestConfig}
|
|
492
|
+
setRequestConfig={setRequestConfig}
|
|
493
|
+
/>
|
|
494
|
+
</TabPanel>
|
|
495
|
+
|
|
496
|
+
{/* Body Tab */}
|
|
497
|
+
<TabPanel className="p-4">
|
|
498
|
+
{['POST', 'PUT', 'PATCH'].includes(method.toUpperCase()) ? (
|
|
499
|
+
<div className="flex flex-col space-y-2">
|
|
500
|
+
<label className="text-sm font-medium">Request Body (JSON)</label>
|
|
501
|
+
<Highlight
|
|
502
|
+
code={requestConfig.body || ''}
|
|
503
|
+
language="json"
|
|
504
|
+
theme={isDark ? themes.vsDark : themes.vsLight}>
|
|
505
|
+
{({ className, style }) => (
|
|
506
|
+
<TextArea
|
|
507
|
+
value={requestConfig.body}
|
|
508
|
+
onChange={(e) => setRequestConfig(prev => ({ ...prev, body: (e.target as HTMLTextAreaElement).value }))}
|
|
509
|
+
placeholder="Enter JSON body..."
|
|
510
|
+
rows={8}
|
|
511
|
+
className={`w-full text-sm font-mono ${className}`}
|
|
512
|
+
style={style}
|
|
513
|
+
/>
|
|
514
|
+
)}
|
|
515
|
+
</Highlight>
|
|
516
|
+
</div>
|
|
517
|
+
) : (
|
|
518
|
+
<p className="text-sm text-zinc-400">Request body not applicable for {method.toUpperCase()} requests.</p>
|
|
519
|
+
)}
|
|
520
|
+
</TabPanel>
|
|
521
|
+
|
|
522
|
+
{/* Auth Tab */}
|
|
523
|
+
<TabPanel className="p-4 space-y-4">
|
|
524
|
+
<div className="flex flex-col space-y-2">
|
|
525
|
+
<label className="text-sm font-medium">Authentication Type</label>
|
|
526
|
+
<Dropdown
|
|
527
|
+
buttonLabel={auth.type === 'none' ? 'Select Auth Type' : auth.type.charAt(0).toUpperCase() + auth.type.slice(1)}
|
|
528
|
+
items={[
|
|
529
|
+
{ label: 'None', onClick: () => setAuth(prev => ({ ...prev, type: 'none' })) },
|
|
530
|
+
{ label: 'Bearer Token', onClick: () => setAuth(prev => ({ ...prev, type: 'bearer' })) },
|
|
531
|
+
{ label: 'API Key', onClick: () => setAuth(prev => ({ ...prev, type: 'apiKey' })) },
|
|
532
|
+
{ label: 'Basic Auth', onClick: () => setAuth(prev => ({ ...prev, type: 'basic' })) }
|
|
533
|
+
]}
|
|
534
|
+
className="w-full"
|
|
535
|
+
/>
|
|
536
|
+
</div>
|
|
537
|
+
|
|
538
|
+
{auth.type === 'bearer' && (
|
|
539
|
+
<div className="space-y-2">
|
|
540
|
+
<label className="text-sm font-medium">Bearer Token</label>
|
|
541
|
+
<Input
|
|
542
|
+
type="password"
|
|
543
|
+
value={auth.token}
|
|
544
|
+
onChange={(e) => setAuth(prev => ({ ...prev, token: (e.target as HTMLInputElement).value }))}
|
|
545
|
+
placeholder="Enter bearer token..."
|
|
546
|
+
className="w-full text-sm"
|
|
547
|
+
/>
|
|
548
|
+
</div>
|
|
549
|
+
)}
|
|
550
|
+
|
|
551
|
+
{auth.type === 'apiKey' && (
|
|
552
|
+
<div className="space-y-2">
|
|
553
|
+
<label className="text-sm font-medium">API Key</label>
|
|
554
|
+
<Input
|
|
555
|
+
type="password"
|
|
556
|
+
value={auth.apiKey}
|
|
557
|
+
onChange={(e) => setAuth(prev => ({ ...prev, apiKey: (e.target as HTMLInputElement).value }))}
|
|
558
|
+
placeholder="Enter API key..."
|
|
559
|
+
className="w-full text-sm"
|
|
560
|
+
/>
|
|
561
|
+
</div>
|
|
562
|
+
)}
|
|
563
|
+
|
|
564
|
+
{auth.type === 'basic' && (
|
|
565
|
+
<div className="space-y-4">
|
|
566
|
+
<div className="space-y-2">
|
|
567
|
+
<label className="text-sm font-medium">Username</label>
|
|
568
|
+
<Input
|
|
569
|
+
type="text"
|
|
570
|
+
value={auth.username}
|
|
571
|
+
onChange={(e) => setAuth(prev => ({ ...prev, username: (e.target as HTMLInputElement).value }))}
|
|
572
|
+
placeholder="Enter username..."
|
|
573
|
+
className="w-full text-sm"
|
|
574
|
+
/>
|
|
575
|
+
</div>
|
|
576
|
+
<div className="space-y-2">
|
|
577
|
+
<label className="text-sm font-medium">Password</label>
|
|
578
|
+
<Input
|
|
579
|
+
type="password"
|
|
580
|
+
value={auth.password}
|
|
581
|
+
onChange={(e) => setAuth(prev => ({ ...prev, password: (e.target as HTMLInputElement).value }))}
|
|
582
|
+
placeholder="Enter password..."
|
|
583
|
+
className="w-full text-sm"
|
|
584
|
+
/>
|
|
585
|
+
</div>
|
|
586
|
+
</div>
|
|
587
|
+
)}
|
|
588
|
+
</TabPanel>
|
|
589
|
+
</TabPanels>
|
|
590
|
+
</TabGroup>
|
|
591
|
+
</div>
|
|
592
|
+
|
|
593
|
+
{/* Response */}
|
|
594
|
+
<div className="flex-1">
|
|
595
|
+
<div className="px-4 py-3">
|
|
596
|
+
<h3 className="text-sm font-semibold">Response</h3>
|
|
597
|
+
</div>
|
|
598
|
+
|
|
599
|
+
<div className="p-4">
|
|
600
|
+
{error ? (
|
|
601
|
+
<div className="space-y-4">
|
|
602
|
+
<div className="flex items-center gap-2">
|
|
603
|
+
<span className="inline-flex items-center rounded-full bg-rose-500/10 px-2 py-1 text-xs font-medium text-rose-400 ring-1 ring-rose-500/20">
|
|
604
|
+
Error
|
|
605
|
+
</span>
|
|
606
|
+
</div>
|
|
607
|
+
<pre className="text-sm text-rose-400 bg-rose-500/5 p-3 rounded-lg border border-rose-500/20">
|
|
608
|
+
{error}
|
|
609
|
+
</pre>
|
|
610
|
+
</div>
|
|
611
|
+
) : response ? (
|
|
612
|
+
<div className="space-y-4">
|
|
613
|
+
<div className="flex items-center gap-4">
|
|
614
|
+
<span className={classNames(
|
|
615
|
+
'inline-flex items-center rounded-full px-2 py-1 text-xs font-medium ring-1 ring-inset',
|
|
616
|
+
response.status >= 200 && response.status < 300
|
|
617
|
+
? 'bg-sky-500/10 text-sky-400 ring-sky-500/20'
|
|
618
|
+
: response.status >= 400
|
|
619
|
+
? 'bg-rose-500/10 text-rose-400 ring-rose-500/20'
|
|
620
|
+
: 'bg-yellow-500/10 text-yellow-400 ring-yellow-500/20'
|
|
621
|
+
)}>
|
|
622
|
+
{response.status} {response.statusText}
|
|
623
|
+
</span>
|
|
624
|
+
<span className="text-xs text-zinc-400">{response.duration}ms</span>
|
|
625
|
+
</div>
|
|
626
|
+
|
|
627
|
+
<div className="space-y-2">
|
|
628
|
+
<h4 className="text-sm font-medium">Response Body</h4>
|
|
629
|
+
<Fence language="json">
|
|
630
|
+
{typeof response.data === 'string'
|
|
631
|
+
? response.data
|
|
632
|
+
: JSON.stringify(response.data, null, 2)
|
|
633
|
+
}
|
|
634
|
+
</Fence>
|
|
635
|
+
</div>
|
|
636
|
+
|
|
637
|
+
<div className="space-y-2">
|
|
638
|
+
<h4 className="text-sm font-medium">Response Headers</h4>
|
|
639
|
+
<div className="space-y-1">
|
|
640
|
+
{Object.entries(response.headers).map(([key, value]) => (
|
|
641
|
+
<div key={key} className="flex text-xs">
|
|
642
|
+
<span className="text-zinc-400 w-32">{key}:</span>
|
|
643
|
+
<span className="text-zinc-300">{value}</span>
|
|
644
|
+
</div>
|
|
645
|
+
))}
|
|
646
|
+
</div>
|
|
647
|
+
</div>
|
|
648
|
+
</div>
|
|
649
|
+
) : (
|
|
650
|
+
<p className="text-sm text-center py-8 text-muted-foreground">
|
|
651
|
+
Click"Send Request" to see the response
|
|
652
|
+
</p>
|
|
653
|
+
)}
|
|
654
|
+
</div>
|
|
655
|
+
</div>
|
|
656
|
+
</div>
|
|
657
|
+
</div>
|
|
658
|
+
)
|
|
659
|
+
}
|