@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.
- package/.dockerignore +4 -0
- package/.github/ISSUE_TEMPLATE/anything-else.md +10 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +39 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- package/.github/dependabot.yml +11 -0
- package/.github/workflows/claude.yml +37 -0
- package/CODE_OF_CONDUCT.md +128 -0
- package/CONTRIBUTING.md +91 -0
- package/LICENSE +201 -0
- package/README.md +185 -0
- package/SECURITY.md +11 -0
- package/assets/dory_logo.png +0 -0
- package/bin/dory.js +131 -0
- package/docs/README.md +2 -0
- package/docs/configuration/app-settings/feature-flags.mdx +157 -0
- package/docs/configuration/app-settings/user-settings.mdx +201 -0
- package/docs/configuration/installation-setup/env-vars.mdx +198 -0
- package/docs/configuration/installation-setup/installation.mdx +127 -0
- package/docs/dory.json +162 -0
- package/docs/integrations/analytics-monitoring/error-tracking.mdx +199 -0
- package/docs/integrations/analytics-monitoring/notifications.mdx +189 -0
- package/docs/integrations/analytics-monitoring/user-analytics.mdx +182 -0
- package/docs/integrations/shopify-integration/shopify-setup.mdx +226 -0
- package/docs/integrations/shopify-integration/shopify-webhooks.mdx +207 -0
- package/docs/overview/developer-overview/dev-architecture.mdx +292 -0
- package/docs/overview/product-intro/app-overview.mdx +167 -0
- package/docs/overview/product-intro/architecture.mdx +203 -0
- package/docs/overview/product-intro/core-concepts.mdx +267 -0
- package/docs/platform-admin/billing-subscriptions/billing.mdx +176 -0
- package/docs/platform-admin/system-maintenance/db-migrations.mdx +184 -0
- package/docs/platform-admin/system-maintenance/health-monitoring.mdx +222 -0
- package/docs/platform-admin/system-maintenance/queue-management.mdx +234 -0
- package/docs/platform-admin/user-team-management/teams.mdx +170 -0
- package/docs/platform-admin/user-team-management/users.mdx +185 -0
- package/docs/support/legal/dependency-analysis.mdx +132 -0
- package/docs/support/legal/privacy-terms.mdx +73 -0
- package/docs/support/troubleshooting/common-issues.mdx +159 -0
- package/docs/usage/api-usage/api-inventory.mdx +213 -0
- package/docs/usage/api-usage/api-products.mdx +249 -0
- package/docs/usage/api-usage/api-purchase-orders.mdx +289 -0
- package/docs/usage/api-usage/api-reports.mdx +227 -0
- package/docs/usage/api-usage/api-suppliers.mdx +171 -0
- package/docs/usage/api-usage/api-user-management.mdx +171 -0
- package/docs/usage/ui-workflows/bundles.mdx +157 -0
- package/docs/usage/ui-workflows/demand-planning.mdx +210 -0
- package/docs/usage/ui-workflows/favorites.mdx +167 -0
- package/docs/usage/ui-workflows/inventory-management.mdx +179 -0
- package/docs/usage/ui-workflows/onboarding.mdx +215 -0
- package/docs/usage/ui-workflows/purchase-orders.mdx +200 -0
- package/docs/usage/ui-workflows/reports.mdx +257 -0
- package/index.html +54 -0
- package/k8s/Dockerfile +15 -0
- package/k8s/config/nginx-default.conf +22 -0
- package/k8s/dev.Dockerfile +16 -0
- package/package.json +71 -0
- package/src/app.css +3 -0
- package/src/app.tsx +13 -0
- package/src/components/dark-mode-toggle.tsx +25 -0
- package/src/components/dropdown.tsx +49 -0
- package/src/components/hooks.tsx +28 -0
- package/src/components/loading.tsx +23 -0
- package/src/components/mdx-provider.tsx +13 -0
- package/src/components/navigation.tsx +53 -0
- package/src/components/notification.tsx +97 -0
- package/src/components/routes.tsx +89 -0
- package/src/components/search-index.ts +43 -0
- package/src/components/search.tsx +508 -0
- package/src/components/store.ts +144 -0
- package/src/index.css +24 -0
- package/src/main.tsx +18 -0
- package/src/mdx/accordion.tsx +48 -0
- package/src/mdx/api-playground.tsx +674 -0
- package/src/mdx/api.tsx +354 -0
- package/src/mdx/callout.tsx +80 -0
- package/src/mdx/card.tsx +48 -0
- package/src/mdx/code.tsx +289 -0
- package/src/mdx/expandable.tsx +40 -0
- package/src/mdx/fence.tsx +73 -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 +43 -0
- package/src/mdx/mermaid.tsx +102 -0
- package/src/mdx/open-api.tsx +325 -0
- package/src/mdx/source.tsx +47 -0
- package/src/mdx/step.tsx +49 -0
- package/src/mdx/table.tsx +15 -0
- package/src/mdx/tag.tsx +66 -0
- package/src/mdx/text.tsx +98 -0
- package/src/plugins/sanitize.ts +15 -0
- package/src/theme.scss +124 -0
- package/src/ui/header.tsx +88 -0
- package/src/ui/layout.tsx +167 -0
- package/src/ui/mobile-navigation.tsx +91 -0
- package/src/ui/prev-next-link.tsx +74 -0
- package/src/ui/table-of-content.tsx +166 -0
- package/src/utils/fonts.ts +20 -0
- package/src/utils/functions.ts +12 -0
- package/src/utils/hooks.ts +46 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.app.json +32 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +25 -0
- 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
|
+
}
|