@clidey/dory 0.1.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.
Files changed (104) hide show
  1. package/.dockerignore +4 -0
  2. package/.github/ISSUE_TEMPLATE/anything-else.md +10 -0
  3. package/.github/ISSUE_TEMPLATE/bug_report.md +39 -0
  4. package/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  5. package/.github/dependabot.yml +11 -0
  6. package/.github/workflows/claude.yml +37 -0
  7. package/CODE_OF_CONDUCT.md +128 -0
  8. package/CONTRIBUTING.md +91 -0
  9. package/LICENSE +201 -0
  10. package/README.md +185 -0
  11. package/SECURITY.md +11 -0
  12. package/assets/dory_logo.png +0 -0
  13. package/bin/dory.js +131 -0
  14. package/docs/README.md +2 -0
  15. package/docs/configuration/app-settings/feature-flags.mdx +157 -0
  16. package/docs/configuration/app-settings/user-settings.mdx +201 -0
  17. package/docs/configuration/installation-setup/env-vars.mdx +198 -0
  18. package/docs/configuration/installation-setup/installation.mdx +127 -0
  19. package/docs/dory.json +162 -0
  20. package/docs/integrations/analytics-monitoring/error-tracking.mdx +199 -0
  21. package/docs/integrations/analytics-monitoring/notifications.mdx +189 -0
  22. package/docs/integrations/analytics-monitoring/user-analytics.mdx +182 -0
  23. package/docs/integrations/shopify-integration/shopify-setup.mdx +226 -0
  24. package/docs/integrations/shopify-integration/shopify-webhooks.mdx +207 -0
  25. package/docs/overview/developer-overview/dev-architecture.mdx +292 -0
  26. package/docs/overview/product-intro/app-overview.mdx +167 -0
  27. package/docs/overview/product-intro/architecture.mdx +203 -0
  28. package/docs/overview/product-intro/core-concepts.mdx +267 -0
  29. package/docs/platform-admin/billing-subscriptions/billing.mdx +176 -0
  30. package/docs/platform-admin/system-maintenance/db-migrations.mdx +184 -0
  31. package/docs/platform-admin/system-maintenance/health-monitoring.mdx +222 -0
  32. package/docs/platform-admin/system-maintenance/queue-management.mdx +234 -0
  33. package/docs/platform-admin/user-team-management/teams.mdx +170 -0
  34. package/docs/platform-admin/user-team-management/users.mdx +185 -0
  35. package/docs/support/legal/dependency-analysis.mdx +132 -0
  36. package/docs/support/legal/privacy-terms.mdx +73 -0
  37. package/docs/support/troubleshooting/common-issues.mdx +159 -0
  38. package/docs/usage/api-usage/api-inventory.mdx +213 -0
  39. package/docs/usage/api-usage/api-products.mdx +249 -0
  40. package/docs/usage/api-usage/api-purchase-orders.mdx +289 -0
  41. package/docs/usage/api-usage/api-reports.mdx +227 -0
  42. package/docs/usage/api-usage/api-suppliers.mdx +171 -0
  43. package/docs/usage/api-usage/api-user-management.mdx +171 -0
  44. package/docs/usage/ui-workflows/bundles.mdx +157 -0
  45. package/docs/usage/ui-workflows/demand-planning.mdx +210 -0
  46. package/docs/usage/ui-workflows/favorites.mdx +167 -0
  47. package/docs/usage/ui-workflows/inventory-management.mdx +179 -0
  48. package/docs/usage/ui-workflows/onboarding.mdx +215 -0
  49. package/docs/usage/ui-workflows/purchase-orders.mdx +200 -0
  50. package/docs/usage/ui-workflows/reports.mdx +257 -0
  51. package/index.html +54 -0
  52. package/k8s/Dockerfile +15 -0
  53. package/k8s/config/nginx-default.conf +22 -0
  54. package/k8s/dev.Dockerfile +16 -0
  55. package/package.json +71 -0
  56. package/src/app.css +3 -0
  57. package/src/app.tsx +13 -0
  58. package/src/components/dark-mode-toggle.tsx +25 -0
  59. package/src/components/dropdown.tsx +49 -0
  60. package/src/components/hooks.tsx +28 -0
  61. package/src/components/loading.tsx +23 -0
  62. package/src/components/mdx-provider.tsx +13 -0
  63. package/src/components/navigation.tsx +53 -0
  64. package/src/components/notification.tsx +97 -0
  65. package/src/components/routes.tsx +89 -0
  66. package/src/components/search-index.ts +43 -0
  67. package/src/components/search.tsx +508 -0
  68. package/src/components/store.ts +144 -0
  69. package/src/index.css +24 -0
  70. package/src/main.tsx +18 -0
  71. package/src/mdx/accordion.tsx +48 -0
  72. package/src/mdx/api-playground.tsx +674 -0
  73. package/src/mdx/api.tsx +354 -0
  74. package/src/mdx/callout.tsx +80 -0
  75. package/src/mdx/card.tsx +48 -0
  76. package/src/mdx/code.tsx +289 -0
  77. package/src/mdx/expandable.tsx +40 -0
  78. package/src/mdx/fence.tsx +73 -0
  79. package/src/mdx/icon.tsx +73 -0
  80. package/src/mdx/latex.tsx +26 -0
  81. package/src/mdx/list.tsx +7 -0
  82. package/src/mdx/mdx.tsx +43 -0
  83. package/src/mdx/mermaid.tsx +102 -0
  84. package/src/mdx/open-api.tsx +325 -0
  85. package/src/mdx/source.tsx +47 -0
  86. package/src/mdx/step.tsx +49 -0
  87. package/src/mdx/table.tsx +15 -0
  88. package/src/mdx/tag.tsx +66 -0
  89. package/src/mdx/text.tsx +98 -0
  90. package/src/plugins/sanitize.ts +15 -0
  91. package/src/theme.scss +124 -0
  92. package/src/ui/header.tsx +88 -0
  93. package/src/ui/layout.tsx +167 -0
  94. package/src/ui/mobile-navigation.tsx +91 -0
  95. package/src/ui/prev-next-link.tsx +74 -0
  96. package/src/ui/table-of-content.tsx +166 -0
  97. package/src/utils/fonts.ts +20 -0
  98. package/src/utils/functions.ts +12 -0
  99. package/src/utils/hooks.ts +46 -0
  100. package/src/vite-env.d.ts +1 -0
  101. package/tsconfig.app.json +32 -0
  102. package/tsconfig.json +7 -0
  103. package/tsconfig.node.json +25 -0
  104. package/vite.config.ts +27 -0
@@ -0,0 +1,674 @@
1
+ import { Input, Tab, TabGroup, TabList, TabPanel, TabPanels } from '@headlessui/react'
2
+ import { CheckIcon } from '@heroicons/react/24/outline'
3
+ import classNames from 'classnames'
4
+ import { PencilIcon, TrashIcon, XIcon } from 'lucide-react'
5
+ import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
6
+ import { Highlight, themes } from 'prism-react-renderer'
7
+ import Dropdown from '../components/dropdown'
8
+ import { useDarkMode } from '../utils/hooks'
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 rounded-lg bg-zinc-800 border border-zinc-600 px-2 py-1 text-sm text-white"
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 rounded-lg bg-zinc-800 border border-zinc-600 px-2 py-1 text-sm text-white"
177
+ />
178
+ <button onClick={() => saveHeader(idx)} className="text-white">
179
+ <CheckIcon className="w-4 h-4" />
180
+ </button>
181
+ <button onClick={() => cancelEdit(idx)} className="text-white">
182
+ <XIcon className="w-4 h-4" />
183
+ </button>
184
+ </>
185
+ ) : (
186
+ <>
187
+ <span className="text-sm text-white w-32 truncate">{hdr.key}</span>
188
+ <span className="text-sm text-white flex-1 truncate">{hdr.value}</span>
189
+ <button onClick={() => editHeader(idx)} className="text-white">
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 text-sky-400 hover:text-sky-300 mt-2 w-fit">
200
+ + 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 rounded-lg bg-zinc-800 border border-zinc-600 px-2 py-1 text-sm text-white" />
285
+ <Input value={p.value} onChange={e => updateParam(i, 'value', (e.target as HTMLInputElement).value)} placeholder="Param value" className="flex-1 rounded-lg bg-zinc-800 border border-zinc-600 px-2 py-1 text-sm text-white" />
286
+ <button onClick={() => saveParam(i)} className="text-white">
287
+ <CheckIcon className="w-4 h-4" />
288
+ </button>
289
+ <button onClick={() => cancelEdit(i)} className="text-white">
290
+ <XIcon className="w-4 h-4" />
291
+ </button>
292
+ </>
293
+ ) : (
294
+ <>
295
+ <span className="w-32 truncate text-sm text-white">{p.name}</span>
296
+ <span className="flex-1 truncate text-sm text-white">{p.value}</span>
297
+ <button onClick={() => editParam(i)} className="text-white">
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 text-sky-400 hover:text-sky-300 self-start">
308
+ + 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 } = useDarkMode();
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 border-b border-zinc-700 bg-zinc-800 px-4 dark:border-zinc-800 dark:bg-transparent pb-4">
434
+ <div className="flex items-center gap-3 pt-3">
435
+ <Tag variant="small">{method.toUpperCase()}</Tag>
436
+ <span className="font-mono text-sm text-white">{title || url}</span>
437
+ </div>
438
+ <button
439
+ onClick={executeRequest}
440
+ disabled={isLoading}
441
+ className={classNames(
442
+ 'ml-auto mt-3 flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition',
443
+ isLoading
444
+ ? 'bg-zinc-600 text-zinc-400 cursor-not-allowed'
445
+ : 'bg-sky-600 text-white hover:bg-sky-700'
446
+ )}
447
+ >
448
+ {isLoading ? (
449
+ <LoadingSpinner className="h-4 w-4" />
450
+ ) : (
451
+ <PlayButton className="h-4 w-4" />
452
+ )}
453
+ {isLoading ? 'Sending...' : 'Send Request'}
454
+ </button>
455
+ </div>
456
+
457
+ {description && (
458
+ <div className="px-4 py-3 border-b border-zinc-700 dark:border-zinc-800">
459
+ <p className="text-sm text-zinc-300">{description}</p>
460
+ </div>
461
+ )}
462
+
463
+ <div className="flex flex-col gap-4">
464
+ <div className="flex flex-col gap-2">
465
+ <label className="text-sm font-medium text-white">Server</label>
466
+ <Dropdown
467
+ buttonLabel={selectedServer?.description || 'Select Server'}
468
+ items={servers.map(server => ({ label: server.description || server.url, onClick: () => setSelectedServer(server) }))}
469
+ className="w-full"
470
+ />
471
+ </div>
472
+
473
+ {/* Request Configuration */}
474
+ <div className="flex-1 border-r border-zinc-700 dark:border-zinc-800">
475
+ <TabGroup selectedIndex={selectedTab} onChange={setSelectedTab}>
476
+ <TabList className="flex border-b border-zinc-700 dark:border-zinc-800">
477
+ {tabs.map((tab) => (
478
+ <Tab
479
+ key={tab.key}
480
+ className={({ selected }) =>
481
+ classNames(
482
+ 'px-4 py-3 text-sm font-medium transition border-b-2',
483
+ selected
484
+ ? 'border-sky-500 text-sky-400'
485
+ : 'border-transparent text-zinc-400 hover:text-zinc-300'
486
+ )
487
+ }
488
+ >
489
+ {tab.name}
490
+ </Tab>
491
+ ))}
492
+ </TabList>
493
+
494
+ <TabPanels className="min-h-[250px]">
495
+ <TabPanel className="p-4">
496
+ <ParamsTab
497
+ queryParams={requestConfig.queryParams}
498
+ setQueryParams={setRequestConfig}
499
+ />
500
+ </TabPanel>
501
+
502
+ <TabPanel className="p-4">
503
+ <HeadersTab
504
+ requestConfig={requestConfig}
505
+ setRequestConfig={setRequestConfig}
506
+ />
507
+ </TabPanel>
508
+
509
+ {/* Body Tab */}
510
+ <TabPanel className="p-4">
511
+ {['POST', 'PUT', 'PATCH'].includes(method.toUpperCase()) ? (
512
+ <div className="flex flex-col space-y-2">
513
+ <label className="text-sm font-medium text-white">Request Body (JSON)</label>
514
+ <Highlight
515
+ code={requestConfig.body || ''}
516
+ language="json"
517
+ theme={isDark ? themes.vsDark : themes.vsLight}>
518
+ {({ className, style }) => (
519
+ <textarea
520
+ value={requestConfig.body}
521
+ onChange={(e) => setRequestConfig(prev => ({ ...prev, body: (e.target as HTMLTextAreaElement).value }))}
522
+ placeholder="Enter JSON body..."
523
+ rows={8}
524
+ className={`w-full rounded-lg bg-zinc-800 border border-zinc-600 px-3 py-2 text-sm text-white placeholder-zinc-400 focus:border-sky-500 focus:outline-none font-mono ${className}`}
525
+ style={style}
526
+ />
527
+ )}
528
+ </Highlight>
529
+ </div>
530
+ ) : (
531
+ <p className="text-sm text-zinc-400">Request body not applicable for {method.toUpperCase()} requests.</p>
532
+ )}
533
+ </TabPanel>
534
+
535
+ {/* Auth Tab */}
536
+ <TabPanel className="p-4 space-y-4">
537
+ <div className="flex flex-col space-y-2">
538
+ <label className="text-sm font-medium text-white">Authentication Type</label>
539
+ <Dropdown
540
+ buttonLabel={auth.type === 'none' ? 'Select Auth Type' : auth.type.charAt(0).toUpperCase() + auth.type.slice(1)}
541
+ items={[
542
+ { label: 'None', onClick: () => setAuth(prev => ({ ...prev, type: 'none' })) },
543
+ { label: 'Bearer Token', onClick: () => setAuth(prev => ({ ...prev, type: 'bearer' })) },
544
+ { label: 'API Key', onClick: () => setAuth(prev => ({ ...prev, type: 'apiKey' })) },
545
+ { label: 'Basic Auth', onClick: () => setAuth(prev => ({ ...prev, type: 'basic' })) }
546
+ ]}
547
+ className="w-full"
548
+ />
549
+ </div>
550
+
551
+ {auth.type === 'bearer' && (
552
+ <div className="space-y-2">
553
+ <label className="text-sm font-medium text-white">Bearer Token</label>
554
+ <input
555
+ type="password"
556
+ value={auth.token}
557
+ onChange={(e) => setAuth(prev => ({ ...prev, token: (e.target as HTMLInputElement).value }))}
558
+ placeholder="Enter bearer token..."
559
+ className="w-full rounded-lg bg-zinc-800 border border-zinc-600 px-3 py-2 text-sm text-white placeholder-zinc-400 focus:border-sky-500 focus:outline-none"
560
+ />
561
+ </div>
562
+ )}
563
+
564
+ {auth.type === 'apiKey' && (
565
+ <div className="space-y-2">
566
+ <label className="text-sm font-medium text-white">API Key</label>
567
+ <input
568
+ type="password"
569
+ value={auth.apiKey}
570
+ onChange={(e) => setAuth(prev => ({ ...prev, apiKey: (e.target as HTMLInputElement).value }))}
571
+ placeholder="Enter API key..."
572
+ className="w-full rounded-lg bg-zinc-800 border border-zinc-600 px-3 py-2 text-sm text-white placeholder-zinc-400 focus:border-sky-500 focus:outline-none"
573
+ />
574
+ </div>
575
+ )}
576
+
577
+ {auth.type === 'basic' && (
578
+ <div className="space-y-4">
579
+ <div className="space-y-2">
580
+ <label className="text-sm font-medium text-white">Username</label>
581
+ <input
582
+ type="text"
583
+ value={auth.username}
584
+ onChange={(e) => setAuth(prev => ({ ...prev, username: (e.target as HTMLInputElement).value }))}
585
+ placeholder="Enter username..."
586
+ className="w-full rounded-lg bg-zinc-800 border border-zinc-600 px-3 py-2 text-sm text-white placeholder-zinc-400 focus:border-sky-500 focus:outline-none"
587
+ />
588
+ </div>
589
+ <div className="space-y-2">
590
+ <label className="text-sm font-medium text-white">Password</label>
591
+ <input
592
+ type="password"
593
+ value={auth.password}
594
+ onChange={(e) => setAuth(prev => ({ ...prev, password: (e.target as HTMLInputElement).value }))}
595
+ placeholder="Enter password..."
596
+ className="w-full rounded-lg bg-zinc-800 border border-zinc-600 px-3 py-2 text-sm text-white placeholder-zinc-400 focus:border-sky-500 focus:outline-none"
597
+ />
598
+ </div>
599
+ </div>
600
+ )}
601
+ </TabPanel>
602
+ </TabPanels>
603
+ </TabGroup>
604
+ </div>
605
+
606
+ {/* Response */}
607
+ <div className="flex-1">
608
+ <div className="border-b border-zinc-700 dark:border-zinc-800 px-4 py-3">
609
+ <h3 className="text-sm font-semibold text-white">Response</h3>
610
+ </div>
611
+
612
+ <div className="p-4">
613
+ {error ? (
614
+ <div className="space-y-4">
615
+ <div className="flex items-center gap-2">
616
+ <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">
617
+ Error
618
+ </span>
619
+ </div>
620
+ <pre className="text-sm text-rose-400 bg-rose-500/5 p-3 rounded-lg border border-rose-500/20">
621
+ {error}
622
+ </pre>
623
+ </div>
624
+ ) : response ? (
625
+ <div className="space-y-4">
626
+ <div className="flex items-center gap-4">
627
+ <span className={classNames(
628
+ 'inline-flex items-center rounded-full px-2 py-1 text-xs font-medium ring-1 ring-inset',
629
+ response.status >= 200 && response.status < 300
630
+ ? 'bg-sky-500/10 text-sky-400 ring-sky-500/20'
631
+ : response.status >= 400
632
+ ? 'bg-rose-500/10 text-rose-400 ring-rose-500/20'
633
+ : 'bg-yellow-500/10 text-yellow-400 ring-yellow-500/20'
634
+ )}>
635
+ {response.status} {response.statusText}
636
+ </span>
637
+ <span className="text-xs text-zinc-400">{response.duration}ms</span>
638
+ </div>
639
+
640
+ <div className="space-y-2">
641
+ <h4 className="text-sm font-medium text-white">Response Body</h4>
642
+ <Fence language="json">
643
+ {typeof response.data === 'string'
644
+ ? response.data
645
+ : JSON.stringify(response.data, null, 2)
646
+ }
647
+ </Fence>
648
+ </div>
649
+
650
+ <div className="space-y-2">
651
+ <h4 className="text-sm font-medium text-white">Response Headers</h4>
652
+ <div className="space-y-1">
653
+ {Object.entries(response.headers).map(([key, value]) => (
654
+ <div key={key} className="flex text-xs">
655
+ <span className="text-zinc-400 w-32">{key}:</span>
656
+ <span className="text-zinc-300">{value}</span>
657
+ </div>
658
+ ))}
659
+ </div>
660
+ </div>
661
+ </div>
662
+ ) : (
663
+ <div className="text-center py-8">
664
+ <div className="text-zinc-400 text-sm">
665
+ Click "Send Request" to see the response
666
+ </div>
667
+ </div>
668
+ )}
669
+ </div>
670
+ </div>
671
+ </div>
672
+ </div>
673
+ )
674
+ }