@lukoweb/apitogo 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/cli.js +168 -57
- package/dist/declarations/lib/ui/Command.d.ts +1 -1
- package/dist/declarations/lib/util/MdxComponents.d.ts +1 -1
- package/package.json +2 -1
- package/src/app/main.tsx +150 -150
- package/src/config/loader.ts +245 -245
- package/src/index.ts +33 -33
- package/src/lib/authentication/components/CallbackHandler.tsx +45 -45
- package/src/lib/authentication/components/SignIn.tsx +46 -46
- package/src/lib/authentication/components/SignUp.tsx +48 -48
- package/src/lib/authentication/providers/clerk.tsx +224 -224
- package/src/lib/authentication/ui/AuthCard.tsx +7 -7
- package/src/lib/authentication/ui/EmailLinkCallbackUi.tsx +129 -125
- package/src/lib/authentication/ui/EmailLinkSentUi.tsx +105 -101
- package/src/lib/authentication/ui/EmailLinkSignInUi.tsx +100 -96
- package/src/lib/authentication/ui/EmailVerificationUi.tsx +128 -124
- package/src/lib/authentication/ui/ZudokuAuthUi.tsx +620 -616
- package/src/lib/components/AiAssistantMenuItems.tsx +102 -102
- package/src/lib/components/Autocomplete.tsx +123 -123
- package/src/lib/components/DeveloperHint.tsx +39 -35
- package/src/lib/components/Header.tsx +260 -260
- package/src/lib/components/Layout.tsx +53 -53
- package/src/lib/components/Main.tsx +51 -51
- package/src/lib/components/Mermaid.tsx +74 -70
- package/src/lib/components/MobileTopNavigation.tsx +276 -276
- package/src/lib/components/MultiSelect.tsx +86 -82
- package/src/lib/components/TopNavigation.tsx +103 -103
- package/src/lib/components/index.ts +26 -26
- package/src/lib/components/navigation/NavigationCategory.tsx +157 -157
- package/src/lib/components/navigation/NavigationFilterInput.tsx +35 -35
- package/src/lib/components/navigation/NavigationItem.tsx +181 -177
- package/src/lib/core/RouteGuard.tsx +193 -193
- package/src/lib/errors/ErrorMessage.tsx +50 -46
- package/src/lib/plugins/api-catalog/Catalog.tsx +80 -80
- package/src/lib/plugins/api-keys/CreateApiKey.tsx +128 -124
- package/src/lib/plugins/api-keys/CreateApiKeyDialog.tsx +49 -49
- package/src/lib/plugins/api-keys/SettingsApiKeys.tsx +72 -72
- package/src/lib/plugins/api-keys/settings/ApiKeyItem.tsx +342 -342
- package/src/lib/plugins/api-keys/settings/RevealApiKey.tsx +127 -127
- package/src/lib/plugins/markdown/MdxPage.tsx +285 -285
- package/src/lib/plugins/openapi/ApiHeader.tsx +96 -96
- package/src/lib/plugins/openapi/CollapsibleCode.tsx +88 -88
- package/src/lib/plugins/openapi/DownloadSchemaButton.tsx +94 -94
- package/src/lib/plugins/openapi/GeneratedExampleSidecarBox.tsx +52 -52
- package/src/lib/plugins/openapi/OperationListItem.tsx +183 -183
- package/src/lib/plugins/openapi/RequestBodySidecarBox.tsx +63 -63
- package/src/lib/plugins/openapi/ResponsesSidecarBox.tsx +120 -117
- package/src/lib/plugins/openapi/SchemaInfo.tsx +344 -344
- package/src/lib/plugins/openapi/SchemaList.tsx +113 -113
- package/src/lib/plugins/openapi/Sidecar.tsx +312 -309
- package/src/lib/plugins/openapi/SidecarExamples.tsx +177 -174
- package/src/lib/plugins/openapi/components/EnumValues.tsx +58 -58
- package/src/lib/plugins/openapi/components/ResponseContent.tsx +117 -114
- package/src/lib/plugins/openapi/playground/BodyPanel.tsx +274 -271
- package/src/lib/plugins/openapi/playground/CollapsibleHeader.tsx +51 -51
- package/src/lib/plugins/openapi/playground/ExamplesDropdown.tsx +60 -60
- package/src/lib/plugins/openapi/playground/Headers.tsx +188 -181
- package/src/lib/plugins/openapi/playground/IdentityDialog.tsx +75 -75
- package/src/lib/plugins/openapi/playground/IdentitySelector.tsx +38 -38
- package/src/lib/plugins/openapi/playground/ParamsGrid.tsx +45 -45
- package/src/lib/plugins/openapi/playground/Playground.tsx +602 -599
- package/src/lib/plugins/openapi/playground/PlaygroundDialog.tsx +56 -56
- package/src/lib/plugins/openapi/playground/QueryParams.tsx +151 -148
- package/src/lib/plugins/openapi/playground/RequestLoginDialog.tsx +70 -70
- package/src/lib/plugins/openapi/playground/request-panel/MultipartField.tsx +91 -91
- package/src/lib/plugins/openapi/playground/result-panel/AudioPlayer.tsx +50 -50
- package/src/lib/plugins/openapi/playground/result-panel/ResponseTab.tsx +355 -355
- package/src/lib/plugins/openapi/playground/result-panel/ResultPanel.tsx +106 -102
- package/src/lib/plugins/openapi/schema/SchemaPropertyItem.tsx +189 -184
- package/src/lib/plugins/openapi/schema/SchemaView.tsx +299 -299
- package/src/lib/plugins/search-pagefind/IndexingDialog.tsx +162 -162
- package/src/lib/plugins/search-pagefind/PagefindSearch.tsx +202 -202
- package/src/lib/plugins/search-pagefind/ResultList.tsx +122 -118
- package/src/lib/ui/ActionButton.tsx +21 -21
- package/src/lib/ui/Command.tsx +191 -191
- package/src/lib/util/MdxComponents.tsx +150 -150
- package/src/vite/config.ts +234 -231
- package/src/vite/dev-server.ts +291 -291
- package/src/vite/plugin-api-keys.ts +50 -50
- package/src/vite/plugin-api.ts +320 -318
- package/src/vite/plugin-auth.ts +38 -38
- package/src/vite/plugin-component.ts +45 -39
- package/src/vite/plugin-config.ts +75 -75
- package/src/vite/plugin-custom-pages.ts +36 -36
- package/src/vite/plugin-docs.ts +202 -202
- package/src/vite/plugin-markdown-export.ts +214 -214
- package/src/vite/plugin-mdx.ts +149 -149
- package/src/vite/plugin-navigation.ts +106 -106
- package/src/vite/plugin-search.ts +47 -47
- package/src/vite/plugin.ts +42 -42
- package/src/vite/prerender/prerender.ts +233 -233
|
@@ -1,355 +1,355 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
} from "
|
|
27
|
-
import {
|
|
28
|
-
import { cn } from "../../../../util/cn.js";
|
|
29
|
-
import createVariantComponent from "../../../../util/createVariantComponent.js";
|
|
30
|
-
import { humanFileSize } from "../../../../util/humanFileSize.js";
|
|
31
|
-
import {
|
|
32
|
-
CollapsibleHeader,
|
|
33
|
-
CollapsibleHeaderTrigger,
|
|
34
|
-
} from "../CollapsibleHeader.js";
|
|
35
|
-
import { isAudioContentType } from "../fileUtils.js";
|
|
36
|
-
import { AudioPlayer } from "./AudioPlayer.js";
|
|
37
|
-
import { convertToTypes } from "./convertToTypes.js";
|
|
38
|
-
|
|
39
|
-
const mimeTypeToLanguage = (mimeType: string) => {
|
|
40
|
-
const mimeTypeMapping = {
|
|
41
|
-
"application/json": "json",
|
|
42
|
-
"text/json": "json",
|
|
43
|
-
"text/html": "html",
|
|
44
|
-
"text/css": "css",
|
|
45
|
-
"text/javascript": "javascript",
|
|
46
|
-
"application/xml": "xml",
|
|
47
|
-
"application/xhtml+xml": "xhtml",
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
return Object.entries(mimeTypeMapping).find(([mime]) =>
|
|
51
|
-
mimeType.includes(mime),
|
|
52
|
-
)?.[1];
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
const getContentType = (headers: Array<[string, string]>) => {
|
|
56
|
-
return (
|
|
57
|
-
headers.find(([key]) => key.toLowerCase() === "content-type")?.[1] || ""
|
|
58
|
-
);
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
const detectLanguage = (headers: Array<[string, string]>) => {
|
|
62
|
-
const contentType = getContentType(headers);
|
|
63
|
-
return mimeTypeToLanguage(contentType);
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
const tryParseJson = (body: string) => {
|
|
67
|
-
try {
|
|
68
|
-
return JSON.stringify(JSON.parse(body), null, 2);
|
|
69
|
-
} catch {
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
const sortHeadersByRelevance = (
|
|
75
|
-
headers: Array<[string, string]>,
|
|
76
|
-
): Array<[string, string]> => {
|
|
77
|
-
const priorityOrder = [
|
|
78
|
-
"Content-Type",
|
|
79
|
-
"Content-Length",
|
|
80
|
-
"Authorization",
|
|
81
|
-
"X-RateLimit-Remaining",
|
|
82
|
-
"X-RateLimit-Limit",
|
|
83
|
-
"Cache-Control",
|
|
84
|
-
"ETag",
|
|
85
|
-
].map((key) => key.toLowerCase());
|
|
86
|
-
|
|
87
|
-
return [...headers].sort(([keyA], [keyB]) => {
|
|
88
|
-
const indexA = priorityOrder.indexOf(keyA.toLowerCase());
|
|
89
|
-
const indexB = priorityOrder.indexOf(keyB.toLowerCase());
|
|
90
|
-
if (indexA === indexB) return 0;
|
|
91
|
-
if (indexA === -1) return 1;
|
|
92
|
-
if (indexB === -1) return -1;
|
|
93
|
-
return indexA - indexB;
|
|
94
|
-
});
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
const MAX_HEADERS_TO_SHOW = 3;
|
|
98
|
-
|
|
99
|
-
const Row = createVariantComponent(
|
|
100
|
-
"div",
|
|
101
|
-
"grid-cols-subgrid grid border-b col-span-full px-4 py-1.5 font-mono text-xs",
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
const RowContent = createVariantComponent("div", "py-1 break-words");
|
|
105
|
-
const RowValue = ({ value, header }: { value: string; header: string }) => {
|
|
106
|
-
const secretHeaders = ["authorization", "key", "secret", "token"];
|
|
107
|
-
const isSecret = secretHeaders.includes(header.toLowerCase());
|
|
108
|
-
const [revealed, setRevealed] = useState(!isSecret);
|
|
109
|
-
return (
|
|
110
|
-
<RowContent
|
|
111
|
-
className={cn(
|
|
112
|
-
"max-h-28 overflow-auto",
|
|
113
|
-
isSecret && "cursor-pointer flex group",
|
|
114
|
-
)}
|
|
115
|
-
onClick={() => {
|
|
116
|
-
if (isSecret) {
|
|
117
|
-
setRevealed((prev) => !prev);
|
|
118
|
-
}
|
|
119
|
-
}}
|
|
120
|
-
>
|
|
121
|
-
{!isSecret ? (
|
|
122
|
-
value
|
|
123
|
-
) : (
|
|
124
|
-
<>
|
|
125
|
-
<SecretText secret={value} previewChars={0} revealed={revealed} />
|
|
126
|
-
{revealed ? (
|
|
127
|
-
<EyeOffIcon size={14} className={cn("hidden group-hover:block")} />
|
|
128
|
-
) : (
|
|
129
|
-
<EyeIcon size={14} className={cn("hidden group-hover:block")} />
|
|
130
|
-
)}
|
|
131
|
-
</>
|
|
132
|
-
)}
|
|
133
|
-
</RowContent>
|
|
134
|
-
);
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
export const ResponseTab = ({
|
|
138
|
-
body = "",
|
|
139
|
-
headers,
|
|
140
|
-
request,
|
|
141
|
-
size,
|
|
142
|
-
isBinary = false,
|
|
143
|
-
fileName,
|
|
144
|
-
blob,
|
|
145
|
-
}: {
|
|
146
|
-
body?: string;
|
|
147
|
-
headers: Array<[string, string]>;
|
|
148
|
-
request: {
|
|
149
|
-
method: string;
|
|
150
|
-
url: string;
|
|
151
|
-
headers: Array<[string, string]>;
|
|
152
|
-
body?: string;
|
|
153
|
-
};
|
|
154
|
-
size: number;
|
|
155
|
-
isBinary?: boolean;
|
|
156
|
-
fileName?: string;
|
|
157
|
-
blob?: Blob;
|
|
158
|
-
}) => {
|
|
159
|
-
const detectedLanguage = detectLanguage(headers);
|
|
160
|
-
const jsonContent = tryParseJson(body);
|
|
161
|
-
const beautifiedBody = jsonContent || body;
|
|
162
|
-
const [view, setView] = useState<"formatted" | "raw" | "types">(
|
|
163
|
-
jsonContent ? "formatted" : "raw",
|
|
164
|
-
);
|
|
165
|
-
|
|
166
|
-
const types = useQuery({
|
|
167
|
-
queryKey: ["types", beautifiedBody],
|
|
168
|
-
queryFn: async () => {
|
|
169
|
-
return convertToTypes(JSON.parse(beautifiedBody));
|
|
170
|
-
},
|
|
171
|
-
enabled: view === "types" && !isBinary,
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
const handleDownload = () => {
|
|
175
|
-
if (blob && fileName) {
|
|
176
|
-
const url = URL.createObjectURL(blob);
|
|
177
|
-
const link = document.createElement("a");
|
|
178
|
-
link.href = url;
|
|
179
|
-
link.download = fileName;
|
|
180
|
-
document.body.appendChild(link);
|
|
181
|
-
link.click();
|
|
182
|
-
document.body.removeChild(link);
|
|
183
|
-
URL.revokeObjectURL(url);
|
|
184
|
-
}
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
const sortedHeaders = sortHeadersByRelevance([...headers]);
|
|
188
|
-
|
|
189
|
-
return (
|
|
190
|
-
<>
|
|
191
|
-
<Collapsible defaultOpen>
|
|
192
|
-
<CollapsibleHeaderTrigger>
|
|
193
|
-
<CornerDownRightIcon size={14} />
|
|
194
|
-
<CollapsibleHeader>Request Headers</CollapsibleHeader>
|
|
195
|
-
</CollapsibleHeaderTrigger>
|
|
196
|
-
<CollapsibleContent>
|
|
197
|
-
<div className="grid grid-cols-[2fr_3fr] gap-x-6 text-sm">
|
|
198
|
-
{request.headers
|
|
199
|
-
.slice(0, MAX_HEADERS_TO_SHOW)
|
|
200
|
-
.map(([key, value]) => (
|
|
201
|
-
<Row key={key}>
|
|
202
|
-
<RowContent>{key}</RowContent>
|
|
203
|
-
<RowValue value={value} header={key} />
|
|
204
|
-
</Row>
|
|
205
|
-
))}
|
|
206
|
-
{request.headers.length > MAX_HEADERS_TO_SHOW && (
|
|
207
|
-
<Collapsible className="col-span-full grid-cols-subgrid grid group">
|
|
208
|
-
<CollapsibleTrigger className="data-[state=open]:hidden justify-center col-span-2 text-xs text-muted-foreground hover:text-primary border-b h-8 flex items-center gap-2">
|
|
209
|
-
Show {request.headers.length - MAX_HEADERS_TO_SHOW} more
|
|
210
|
-
headers
|
|
211
|
-
<PlusCircleIcon size={12} className="text-muted-foreground" />
|
|
212
|
-
</CollapsibleTrigger>
|
|
213
|
-
<CollapsibleContent className="col-span-full grid grid-cols-subgrid">
|
|
214
|
-
{request.headers
|
|
215
|
-
.slice(MAX_HEADERS_TO_SHOW)
|
|
216
|
-
.map(([key, value]) => (
|
|
217
|
-
<Row key={key}>
|
|
218
|
-
<RowContent>{key}</RowContent>
|
|
219
|
-
<RowValue value={value} header={key} />
|
|
220
|
-
</Row>
|
|
221
|
-
))}
|
|
222
|
-
<CollapsibleTrigger className="justify-center col-span-2 text-xs text-muted-foreground hover:text-primary border-b h-8 flex items-center gap-2">
|
|
223
|
-
Hide {request.headers.length - MAX_HEADERS_TO_SHOW} headers
|
|
224
|
-
<MinusCircleIcon
|
|
225
|
-
size={12}
|
|
226
|
-
className="text-muted-foreground"
|
|
227
|
-
/>
|
|
228
|
-
</CollapsibleTrigger>
|
|
229
|
-
</CollapsibleContent>
|
|
230
|
-
</Collapsible>
|
|
231
|
-
)}
|
|
232
|
-
</div>
|
|
233
|
-
</CollapsibleContent>
|
|
234
|
-
</Collapsible>
|
|
235
|
-
|
|
236
|
-
<Collapsible defaultOpen>
|
|
237
|
-
<CollapsibleHeaderTrigger>
|
|
238
|
-
<CornerDownLeftIcon size={14} />
|
|
239
|
-
<CollapsibleHeader>Response Headers</CollapsibleHeader>
|
|
240
|
-
</CollapsibleHeaderTrigger>
|
|
241
|
-
<CollapsibleContent>
|
|
242
|
-
<div className="grid grid-cols-[2fr_3fr] gap-x-6 text-sm">
|
|
243
|
-
{sortedHeaders.slice(0, MAX_HEADERS_TO_SHOW).map(([key, value]) => (
|
|
244
|
-
<Row key={key}>
|
|
245
|
-
<RowContent>{key}</RowContent>
|
|
246
|
-
<RowValue value={value} header={key} />
|
|
247
|
-
</Row>
|
|
248
|
-
))}
|
|
249
|
-
{sortedHeaders.length > MAX_HEADERS_TO_SHOW && (
|
|
250
|
-
<Collapsible className="col-span-full grid-cols-subgrid grid group">
|
|
251
|
-
<CollapsibleTrigger className="data-[state=open]:hidden justify-center col-span-2 text-xs text-muted-foreground hover:text-primary border-b h-8 flex items-center gap-2">
|
|
252
|
-
Show {sortedHeaders.length - MAX_HEADERS_TO_SHOW} more headers
|
|
253
|
-
<PlusCircleIcon size={12} className="text-muted-foreground" />
|
|
254
|
-
</CollapsibleTrigger>
|
|
255
|
-
<CollapsibleContent className="col-span-full grid grid-cols-subgrid">
|
|
256
|
-
{sortedHeaders
|
|
257
|
-
.slice(MAX_HEADERS_TO_SHOW)
|
|
258
|
-
.map(([key, value]) => (
|
|
259
|
-
<Row key={key}>
|
|
260
|
-
<RowContent>{key}</RowContent>
|
|
261
|
-
<RowValue value={value} header={key} />
|
|
262
|
-
</Row>
|
|
263
|
-
))}
|
|
264
|
-
<CollapsibleTrigger className="justify-center col-span-2 text-xs text-muted-foreground hover:text-primary border-b h-8 flex items-center gap-2">
|
|
265
|
-
Hide {sortedHeaders.length - MAX_HEADERS_TO_SHOW} headers
|
|
266
|
-
<MinusCircleIcon
|
|
267
|
-
size={12}
|
|
268
|
-
className="text-muted-foreground"
|
|
269
|
-
/>
|
|
270
|
-
</CollapsibleTrigger>
|
|
271
|
-
</CollapsibleContent>
|
|
272
|
-
</Collapsible>
|
|
273
|
-
)}
|
|
274
|
-
</div>
|
|
275
|
-
</CollapsibleContent>
|
|
276
|
-
</Collapsible>
|
|
277
|
-
|
|
278
|
-
<div className="flex gap-2 justify-between items-center border-b px-2 flex-0">
|
|
279
|
-
<CollapsibleHeader className="flex items-center gap-2">
|
|
280
|
-
<SquareCodeIcon size={14} />
|
|
281
|
-
Response body
|
|
282
|
-
</CollapsibleHeader>
|
|
283
|
-
{jsonContent && !isBinary && (
|
|
284
|
-
<Select
|
|
285
|
-
value={view}
|
|
286
|
-
onValueChange={(value) =>
|
|
287
|
-
setView(value as "formatted" | "raw" | "types")
|
|
288
|
-
}
|
|
289
|
-
>
|
|
290
|
-
<SelectTrigger className="max-w-32 border-0 bg-transparent">
|
|
291
|
-
<SelectValue placeholder="View" />
|
|
292
|
-
</SelectTrigger>
|
|
293
|
-
<SelectContent>
|
|
294
|
-
<SelectItem value="formatted">Formatted</SelectItem>
|
|
295
|
-
<SelectItem value="raw">Raw</SelectItem>
|
|
296
|
-
<SelectItem value="types">Types</SelectItem>
|
|
297
|
-
</SelectContent>
|
|
298
|
-
</Select>
|
|
299
|
-
)}
|
|
300
|
-
</div>
|
|
301
|
-
<div className="flex-1">
|
|
302
|
-
{isBinary ? (
|
|
303
|
-
blob && isAudioContentType(getContentType(headers)) ? (
|
|
304
|
-
<AudioPlayer
|
|
305
|
-
blob={blob}
|
|
306
|
-
fileName={fileName ?? "audio"}
|
|
307
|
-
size={size}
|
|
308
|
-
onDownload={handleDownload}
|
|
309
|
-
/>
|
|
310
|
-
) : (
|
|
311
|
-
<div className="p-4 text-center">
|
|
312
|
-
<div className="flex flex-col items-center gap-4">
|
|
313
|
-
<div className="text-lg font-semibold">Binary Content</div>
|
|
314
|
-
<div className="text-sm text-muted-foreground">
|
|
315
|
-
This response contains binary data that cannot be displayed as
|
|
316
|
-
text.
|
|
317
|
-
</div>
|
|
318
|
-
<Button
|
|
319
|
-
onClick={handleDownload}
|
|
320
|
-
className="flex items-center gap-2"
|
|
321
|
-
disabled={!blob}
|
|
322
|
-
>
|
|
323
|
-
<DownloadIcon className="h-4 w-4" />
|
|
324
|
-
Download {fileName || "file"} ({humanFileSize(size)})
|
|
325
|
-
</Button>
|
|
326
|
-
</div>
|
|
327
|
-
</div>
|
|
328
|
-
)
|
|
329
|
-
) : (
|
|
330
|
-
<SyntaxHighlight
|
|
331
|
-
className="text-xs flex-1"
|
|
332
|
-
embedded
|
|
333
|
-
fullHeight
|
|
334
|
-
language={
|
|
335
|
-
view === "types"
|
|
336
|
-
? "typescript"
|
|
337
|
-
: view === "raw"
|
|
338
|
-
? jsonContent
|
|
339
|
-
? "text"
|
|
340
|
-
: detectedLanguage
|
|
341
|
-
: "json"
|
|
342
|
-
}
|
|
343
|
-
code={
|
|
344
|
-
(view === "raw"
|
|
345
|
-
? body
|
|
346
|
-
: view === "types"
|
|
347
|
-
? types.data?.lines.join("\n")
|
|
348
|
-
: beautifiedBody) ?? ""
|
|
349
|
-
}
|
|
350
|
-
/>
|
|
351
|
-
)}
|
|
352
|
-
</div>
|
|
353
|
-
</>
|
|
354
|
-
);
|
|
355
|
-
};
|
|
1
|
+
import { Button } from "@lukoweb/apitogo/ui/Button.js";
|
|
2
|
+
import {
|
|
3
|
+
Collapsible,
|
|
4
|
+
CollapsibleContent,
|
|
5
|
+
CollapsibleTrigger,
|
|
6
|
+
} from "@lukoweb/apitogo/ui/Collapsible.js";
|
|
7
|
+
import { SecretText } from "@lukoweb/apitogo/ui/Secret.js";
|
|
8
|
+
import {
|
|
9
|
+
Select,
|
|
10
|
+
SelectContent,
|
|
11
|
+
SelectItem,
|
|
12
|
+
SelectTrigger,
|
|
13
|
+
SelectValue,
|
|
14
|
+
} from "@lukoweb/apitogo/ui/Select.js";
|
|
15
|
+
import { SyntaxHighlight } from "@lukoweb/apitogo/ui/SyntaxHighlight.js";
|
|
16
|
+
import { useQuery } from "@tanstack/react-query";
|
|
17
|
+
import {
|
|
18
|
+
CornerDownLeftIcon,
|
|
19
|
+
CornerDownRightIcon,
|
|
20
|
+
DownloadIcon,
|
|
21
|
+
EyeIcon,
|
|
22
|
+
EyeOffIcon,
|
|
23
|
+
MinusCircleIcon,
|
|
24
|
+
PlusCircleIcon,
|
|
25
|
+
SquareCodeIcon,
|
|
26
|
+
} from "lucide-react";
|
|
27
|
+
import { useState } from "react";
|
|
28
|
+
import { cn } from "../../../../util/cn.js";
|
|
29
|
+
import createVariantComponent from "../../../../util/createVariantComponent.js";
|
|
30
|
+
import { humanFileSize } from "../../../../util/humanFileSize.js";
|
|
31
|
+
import {
|
|
32
|
+
CollapsibleHeader,
|
|
33
|
+
CollapsibleHeaderTrigger,
|
|
34
|
+
} from "../CollapsibleHeader.js";
|
|
35
|
+
import { isAudioContentType } from "../fileUtils.js";
|
|
36
|
+
import { AudioPlayer } from "./AudioPlayer.js";
|
|
37
|
+
import { convertToTypes } from "./convertToTypes.js";
|
|
38
|
+
|
|
39
|
+
const mimeTypeToLanguage = (mimeType: string) => {
|
|
40
|
+
const mimeTypeMapping = {
|
|
41
|
+
"application/json": "json",
|
|
42
|
+
"text/json": "json",
|
|
43
|
+
"text/html": "html",
|
|
44
|
+
"text/css": "css",
|
|
45
|
+
"text/javascript": "javascript",
|
|
46
|
+
"application/xml": "xml",
|
|
47
|
+
"application/xhtml+xml": "xhtml",
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return Object.entries(mimeTypeMapping).find(([mime]) =>
|
|
51
|
+
mimeType.includes(mime),
|
|
52
|
+
)?.[1];
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const getContentType = (headers: Array<[string, string]>) => {
|
|
56
|
+
return (
|
|
57
|
+
headers.find(([key]) => key.toLowerCase() === "content-type")?.[1] || ""
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const detectLanguage = (headers: Array<[string, string]>) => {
|
|
62
|
+
const contentType = getContentType(headers);
|
|
63
|
+
return mimeTypeToLanguage(contentType);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const tryParseJson = (body: string) => {
|
|
67
|
+
try {
|
|
68
|
+
return JSON.stringify(JSON.parse(body), null, 2);
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const sortHeadersByRelevance = (
|
|
75
|
+
headers: Array<[string, string]>,
|
|
76
|
+
): Array<[string, string]> => {
|
|
77
|
+
const priorityOrder = [
|
|
78
|
+
"Content-Type",
|
|
79
|
+
"Content-Length",
|
|
80
|
+
"Authorization",
|
|
81
|
+
"X-RateLimit-Remaining",
|
|
82
|
+
"X-RateLimit-Limit",
|
|
83
|
+
"Cache-Control",
|
|
84
|
+
"ETag",
|
|
85
|
+
].map((key) => key.toLowerCase());
|
|
86
|
+
|
|
87
|
+
return [...headers].sort(([keyA], [keyB]) => {
|
|
88
|
+
const indexA = priorityOrder.indexOf(keyA.toLowerCase());
|
|
89
|
+
const indexB = priorityOrder.indexOf(keyB.toLowerCase());
|
|
90
|
+
if (indexA === indexB) return 0;
|
|
91
|
+
if (indexA === -1) return 1;
|
|
92
|
+
if (indexB === -1) return -1;
|
|
93
|
+
return indexA - indexB;
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const MAX_HEADERS_TO_SHOW = 3;
|
|
98
|
+
|
|
99
|
+
const Row = createVariantComponent(
|
|
100
|
+
"div",
|
|
101
|
+
"grid-cols-subgrid grid border-b col-span-full px-4 py-1.5 font-mono text-xs",
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const RowContent = createVariantComponent("div", "py-1 break-words");
|
|
105
|
+
const RowValue = ({ value, header }: { value: string; header: string }) => {
|
|
106
|
+
const secretHeaders = ["authorization", "key", "secret", "token"];
|
|
107
|
+
const isSecret = secretHeaders.includes(header.toLowerCase());
|
|
108
|
+
const [revealed, setRevealed] = useState(!isSecret);
|
|
109
|
+
return (
|
|
110
|
+
<RowContent
|
|
111
|
+
className={cn(
|
|
112
|
+
"max-h-28 overflow-auto",
|
|
113
|
+
isSecret && "cursor-pointer flex group",
|
|
114
|
+
)}
|
|
115
|
+
onClick={() => {
|
|
116
|
+
if (isSecret) {
|
|
117
|
+
setRevealed((prev) => !prev);
|
|
118
|
+
}
|
|
119
|
+
}}
|
|
120
|
+
>
|
|
121
|
+
{!isSecret ? (
|
|
122
|
+
value
|
|
123
|
+
) : (
|
|
124
|
+
<>
|
|
125
|
+
<SecretText secret={value} previewChars={0} revealed={revealed} />
|
|
126
|
+
{revealed ? (
|
|
127
|
+
<EyeOffIcon size={14} className={cn("hidden group-hover:block")} />
|
|
128
|
+
) : (
|
|
129
|
+
<EyeIcon size={14} className={cn("hidden group-hover:block")} />
|
|
130
|
+
)}
|
|
131
|
+
</>
|
|
132
|
+
)}
|
|
133
|
+
</RowContent>
|
|
134
|
+
);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export const ResponseTab = ({
|
|
138
|
+
body = "",
|
|
139
|
+
headers,
|
|
140
|
+
request,
|
|
141
|
+
size,
|
|
142
|
+
isBinary = false,
|
|
143
|
+
fileName,
|
|
144
|
+
blob,
|
|
145
|
+
}: {
|
|
146
|
+
body?: string;
|
|
147
|
+
headers: Array<[string, string]>;
|
|
148
|
+
request: {
|
|
149
|
+
method: string;
|
|
150
|
+
url: string;
|
|
151
|
+
headers: Array<[string, string]>;
|
|
152
|
+
body?: string;
|
|
153
|
+
};
|
|
154
|
+
size: number;
|
|
155
|
+
isBinary?: boolean;
|
|
156
|
+
fileName?: string;
|
|
157
|
+
blob?: Blob;
|
|
158
|
+
}) => {
|
|
159
|
+
const detectedLanguage = detectLanguage(headers);
|
|
160
|
+
const jsonContent = tryParseJson(body);
|
|
161
|
+
const beautifiedBody = jsonContent || body;
|
|
162
|
+
const [view, setView] = useState<"formatted" | "raw" | "types">(
|
|
163
|
+
jsonContent ? "formatted" : "raw",
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
const types = useQuery({
|
|
167
|
+
queryKey: ["types", beautifiedBody],
|
|
168
|
+
queryFn: async () => {
|
|
169
|
+
return convertToTypes(JSON.parse(beautifiedBody));
|
|
170
|
+
},
|
|
171
|
+
enabled: view === "types" && !isBinary,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const handleDownload = () => {
|
|
175
|
+
if (blob && fileName) {
|
|
176
|
+
const url = URL.createObjectURL(blob);
|
|
177
|
+
const link = document.createElement("a");
|
|
178
|
+
link.href = url;
|
|
179
|
+
link.download = fileName;
|
|
180
|
+
document.body.appendChild(link);
|
|
181
|
+
link.click();
|
|
182
|
+
document.body.removeChild(link);
|
|
183
|
+
URL.revokeObjectURL(url);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const sortedHeaders = sortHeadersByRelevance([...headers]);
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
<>
|
|
191
|
+
<Collapsible defaultOpen>
|
|
192
|
+
<CollapsibleHeaderTrigger>
|
|
193
|
+
<CornerDownRightIcon size={14} />
|
|
194
|
+
<CollapsibleHeader>Request Headers</CollapsibleHeader>
|
|
195
|
+
</CollapsibleHeaderTrigger>
|
|
196
|
+
<CollapsibleContent>
|
|
197
|
+
<div className="grid grid-cols-[2fr_3fr] gap-x-6 text-sm">
|
|
198
|
+
{request.headers
|
|
199
|
+
.slice(0, MAX_HEADERS_TO_SHOW)
|
|
200
|
+
.map(([key, value]) => (
|
|
201
|
+
<Row key={key}>
|
|
202
|
+
<RowContent>{key}</RowContent>
|
|
203
|
+
<RowValue value={value} header={key} />
|
|
204
|
+
</Row>
|
|
205
|
+
))}
|
|
206
|
+
{request.headers.length > MAX_HEADERS_TO_SHOW && (
|
|
207
|
+
<Collapsible className="col-span-full grid-cols-subgrid grid group">
|
|
208
|
+
<CollapsibleTrigger className="data-[state=open]:hidden justify-center col-span-2 text-xs text-muted-foreground hover:text-primary border-b h-8 flex items-center gap-2">
|
|
209
|
+
Show {request.headers.length - MAX_HEADERS_TO_SHOW} more
|
|
210
|
+
headers
|
|
211
|
+
<PlusCircleIcon size={12} className="text-muted-foreground" />
|
|
212
|
+
</CollapsibleTrigger>
|
|
213
|
+
<CollapsibleContent className="col-span-full grid grid-cols-subgrid">
|
|
214
|
+
{request.headers
|
|
215
|
+
.slice(MAX_HEADERS_TO_SHOW)
|
|
216
|
+
.map(([key, value]) => (
|
|
217
|
+
<Row key={key}>
|
|
218
|
+
<RowContent>{key}</RowContent>
|
|
219
|
+
<RowValue value={value} header={key} />
|
|
220
|
+
</Row>
|
|
221
|
+
))}
|
|
222
|
+
<CollapsibleTrigger className="justify-center col-span-2 text-xs text-muted-foreground hover:text-primary border-b h-8 flex items-center gap-2">
|
|
223
|
+
Hide {request.headers.length - MAX_HEADERS_TO_SHOW} headers
|
|
224
|
+
<MinusCircleIcon
|
|
225
|
+
size={12}
|
|
226
|
+
className="text-muted-foreground"
|
|
227
|
+
/>
|
|
228
|
+
</CollapsibleTrigger>
|
|
229
|
+
</CollapsibleContent>
|
|
230
|
+
</Collapsible>
|
|
231
|
+
)}
|
|
232
|
+
</div>
|
|
233
|
+
</CollapsibleContent>
|
|
234
|
+
</Collapsible>
|
|
235
|
+
|
|
236
|
+
<Collapsible defaultOpen>
|
|
237
|
+
<CollapsibleHeaderTrigger>
|
|
238
|
+
<CornerDownLeftIcon size={14} />
|
|
239
|
+
<CollapsibleHeader>Response Headers</CollapsibleHeader>
|
|
240
|
+
</CollapsibleHeaderTrigger>
|
|
241
|
+
<CollapsibleContent>
|
|
242
|
+
<div className="grid grid-cols-[2fr_3fr] gap-x-6 text-sm">
|
|
243
|
+
{sortedHeaders.slice(0, MAX_HEADERS_TO_SHOW).map(([key, value]) => (
|
|
244
|
+
<Row key={key}>
|
|
245
|
+
<RowContent>{key}</RowContent>
|
|
246
|
+
<RowValue value={value} header={key} />
|
|
247
|
+
</Row>
|
|
248
|
+
))}
|
|
249
|
+
{sortedHeaders.length > MAX_HEADERS_TO_SHOW && (
|
|
250
|
+
<Collapsible className="col-span-full grid-cols-subgrid grid group">
|
|
251
|
+
<CollapsibleTrigger className="data-[state=open]:hidden justify-center col-span-2 text-xs text-muted-foreground hover:text-primary border-b h-8 flex items-center gap-2">
|
|
252
|
+
Show {sortedHeaders.length - MAX_HEADERS_TO_SHOW} more headers
|
|
253
|
+
<PlusCircleIcon size={12} className="text-muted-foreground" />
|
|
254
|
+
</CollapsibleTrigger>
|
|
255
|
+
<CollapsibleContent className="col-span-full grid grid-cols-subgrid">
|
|
256
|
+
{sortedHeaders
|
|
257
|
+
.slice(MAX_HEADERS_TO_SHOW)
|
|
258
|
+
.map(([key, value]) => (
|
|
259
|
+
<Row key={key}>
|
|
260
|
+
<RowContent>{key}</RowContent>
|
|
261
|
+
<RowValue value={value} header={key} />
|
|
262
|
+
</Row>
|
|
263
|
+
))}
|
|
264
|
+
<CollapsibleTrigger className="justify-center col-span-2 text-xs text-muted-foreground hover:text-primary border-b h-8 flex items-center gap-2">
|
|
265
|
+
Hide {sortedHeaders.length - MAX_HEADERS_TO_SHOW} headers
|
|
266
|
+
<MinusCircleIcon
|
|
267
|
+
size={12}
|
|
268
|
+
className="text-muted-foreground"
|
|
269
|
+
/>
|
|
270
|
+
</CollapsibleTrigger>
|
|
271
|
+
</CollapsibleContent>
|
|
272
|
+
</Collapsible>
|
|
273
|
+
)}
|
|
274
|
+
</div>
|
|
275
|
+
</CollapsibleContent>
|
|
276
|
+
</Collapsible>
|
|
277
|
+
|
|
278
|
+
<div className="flex gap-2 justify-between items-center border-b px-2 flex-0">
|
|
279
|
+
<CollapsibleHeader className="flex items-center gap-2">
|
|
280
|
+
<SquareCodeIcon size={14} />
|
|
281
|
+
Response body
|
|
282
|
+
</CollapsibleHeader>
|
|
283
|
+
{jsonContent && !isBinary && (
|
|
284
|
+
<Select
|
|
285
|
+
value={view}
|
|
286
|
+
onValueChange={(value) =>
|
|
287
|
+
setView(value as "formatted" | "raw" | "types")
|
|
288
|
+
}
|
|
289
|
+
>
|
|
290
|
+
<SelectTrigger className="max-w-32 border-0 bg-transparent">
|
|
291
|
+
<SelectValue placeholder="View" />
|
|
292
|
+
</SelectTrigger>
|
|
293
|
+
<SelectContent>
|
|
294
|
+
<SelectItem value="formatted">Formatted</SelectItem>
|
|
295
|
+
<SelectItem value="raw">Raw</SelectItem>
|
|
296
|
+
<SelectItem value="types">Types</SelectItem>
|
|
297
|
+
</SelectContent>
|
|
298
|
+
</Select>
|
|
299
|
+
)}
|
|
300
|
+
</div>
|
|
301
|
+
<div className="flex-1">
|
|
302
|
+
{isBinary ? (
|
|
303
|
+
blob && isAudioContentType(getContentType(headers)) ? (
|
|
304
|
+
<AudioPlayer
|
|
305
|
+
blob={blob}
|
|
306
|
+
fileName={fileName ?? "audio"}
|
|
307
|
+
size={size}
|
|
308
|
+
onDownload={handleDownload}
|
|
309
|
+
/>
|
|
310
|
+
) : (
|
|
311
|
+
<div className="p-4 text-center">
|
|
312
|
+
<div className="flex flex-col items-center gap-4">
|
|
313
|
+
<div className="text-lg font-semibold">Binary Content</div>
|
|
314
|
+
<div className="text-sm text-muted-foreground">
|
|
315
|
+
This response contains binary data that cannot be displayed as
|
|
316
|
+
text.
|
|
317
|
+
</div>
|
|
318
|
+
<Button
|
|
319
|
+
onClick={handleDownload}
|
|
320
|
+
className="flex items-center gap-2"
|
|
321
|
+
disabled={!blob}
|
|
322
|
+
>
|
|
323
|
+
<DownloadIcon className="h-4 w-4" />
|
|
324
|
+
Download {fileName || "file"} ({humanFileSize(size)})
|
|
325
|
+
</Button>
|
|
326
|
+
</div>
|
|
327
|
+
</div>
|
|
328
|
+
)
|
|
329
|
+
) : (
|
|
330
|
+
<SyntaxHighlight
|
|
331
|
+
className="text-xs flex-1"
|
|
332
|
+
embedded
|
|
333
|
+
fullHeight
|
|
334
|
+
language={
|
|
335
|
+
view === "types"
|
|
336
|
+
? "typescript"
|
|
337
|
+
: view === "raw"
|
|
338
|
+
? jsonContent
|
|
339
|
+
? "text"
|
|
340
|
+
: detectedLanguage
|
|
341
|
+
: "json"
|
|
342
|
+
}
|
|
343
|
+
code={
|
|
344
|
+
(view === "raw"
|
|
345
|
+
? body
|
|
346
|
+
: view === "types"
|
|
347
|
+
? types.data?.lines.join("\n")
|
|
348
|
+
: beautifiedBody) ?? ""
|
|
349
|
+
}
|
|
350
|
+
/>
|
|
351
|
+
)}
|
|
352
|
+
</div>
|
|
353
|
+
</>
|
|
354
|
+
);
|
|
355
|
+
};
|