@rebasepro/studio 0.17.3 → 0.18.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/LICENSE +0 -1
- package/README.md +12 -4
- package/dist/{ApiKeysView-DiCurTEU.js → ApiKeysView-EIvegrUk.js} +54 -39
- package/dist/ApiKeysView-EIvegrUk.js.map +1 -0
- package/dist/{BackupsView-DNS6LdVg.js → BackupsView-DVkKzaPR.js} +29 -31
- package/dist/BackupsView-DVkKzaPR.js.map +1 -0
- package/dist/{BranchesView-CM3B-ER3.js → BranchesView-Df8hQ8wE.js} +85 -24
- package/dist/BranchesView-Df8hQ8wE.js.map +1 -0
- package/dist/{CronJobsView-BHtJZeJZ.js → CronJobsView-CLOGjb-L.js} +32 -27
- package/dist/CronJobsView-CLOGjb-L.js.map +1 -0
- package/dist/{RLSEditor-CBX4uD66.js → RLSEditor-Sa1D34ny.js} +724 -615
- package/dist/RLSEditor-Sa1D34ny.js.map +1 -0
- package/dist/{SQLEditor-ByecwX8B.js → SQLEditor-BfNA8xM3.js} +9 -4
- package/dist/{SQLEditor-ByecwX8B.js.map → SQLEditor-BfNA8xM3.js.map} +1 -1
- package/dist/{SchemaVisualizer-D6tMZgeC.js → SchemaVisualizer-DTDMYL6o.js} +84 -110
- package/dist/SchemaVisualizer-DTDMYL6o.js.map +1 -0
- package/dist/{StorageView-BGDeC5GI.js → StorageView-Ed9Q_VlB.js} +5 -32
- package/dist/StorageView-Ed9Q_VlB.js.map +1 -0
- package/dist/components/RLSEditor/saveRules.d.ts +38 -0
- package/dist/components/RebaseStudio.d.ts +1 -1
- package/dist/components/SchemaVisualizer/schema-visualizer.utils.d.ts +5 -2
- package/dist/components/SchemaVisualizer/useSchemaGraph.d.ts +0 -4
- package/dist/components/load-failure-view.d.ts +17 -0
- package/dist/components/load-failure.d.ts +47 -0
- package/dist/index.es.js +56 -36
- package/dist/index.es.js.map +1 -1
- package/dist/load-failure-C6OMJSGC.js +31 -0
- package/dist/load-failure-C6OMJSGC.js.map +1 -0
- package/dist/load-failure-view-CBTB_h-h.js +48 -0
- package/dist/load-failure-view-CBTB_h-h.js.map +1 -0
- package/package.json +36 -21
- package/dist/ApiKeysView-DiCurTEU.js.map +0 -1
- package/dist/BackupsView-DNS6LdVg.js.map +0 -1
- package/dist/BranchesView-CM3B-ER3.js.map +0 -1
- package/dist/CronJobsView-BHtJZeJZ.js.map +0 -1
- package/dist/RLSEditor-CBX4uD66.js.map +0 -1
- package/dist/SchemaVisualizer-D6tMZgeC.js.map +0 -1
- package/dist/StorageView-BGDeC5GI.js.map +0 -1
- package/dist/components/StorageView/storage-failure.d.ts +0 -34
- package/src/components/ApiExplorer/ApiExplorer.tsx +0 -299
- package/src/components/ApiExplorer/EndpointDetail.tsx +0 -279
- package/src/components/ApiExplorer/TryItPanel.tsx +0 -499
- package/src/components/ApiExplorer/parseSpec.ts +0 -104
- package/src/components/ApiExplorer/types.ts +0 -84
- package/src/components/ApiKeys/ApiKeysView.tsx +0 -479
- package/src/components/ApiKeys/CreateApiKeyDialog.tsx +0 -711
- package/src/components/ApiKeys/permissions.ts +0 -145
- package/src/components/AuthSimulationSelector.tsx +0 -72
- package/src/components/Backups/BackupsView.tsx +0 -185
- package/src/components/Branches/BranchesView.tsx +0 -386
- package/src/components/CronJobs/CronJobsView.tsx +0 -413
- package/src/components/JSEditor/JSEditor.tsx +0 -1100
- package/src/components/JSEditor/JSEditorSidebar.tsx +0 -350
- package/src/components/JSEditor/JSMonacoEditor.tsx +0 -410
- package/src/components/LogsExplorer/LogsExplorer.tsx +0 -273
- package/src/components/LogsExplorer/useLogTail.ts +0 -356
- package/src/components/RLSEditor/PolicyEditor.tsx +0 -399
- package/src/components/RLSEditor/RLSEditor.tsx +0 -1128
- package/src/components/RLSEditor/index.ts +0 -1
- package/src/components/RLSEditor/policy-presets.ts +0 -176
- package/src/components/RLSEditor/saveSecurityRules.ts +0 -48
- package/src/components/RebaseStudio.tsx +0 -174
- package/src/components/SQLEditor/ExplainVisualizer.tsx +0 -128
- package/src/components/SQLEditor/MonacoEditor.tsx +0 -203
- package/src/components/SQLEditor/SQLEditor.tsx +0 -1518
- package/src/components/SQLEditor/SQLEditorSidebar.tsx +0 -173
- package/src/components/SQLEditor/SchemaBrowser.tsx +0 -168
- package/src/components/SQLEditor/sql_editor_types.ts +0 -18
- package/src/components/SchemaVisualizer/RelationEdge.tsx +0 -102
- package/src/components/SchemaVisualizer/SchemaVisualizer.tsx +0 -710
- package/src/components/SchemaVisualizer/TableNode.tsx +0 -257
- package/src/components/SchemaVisualizer/schema-visualizer.utils.ts +0 -140
- package/src/components/SchemaVisualizer/useLiveRls.ts +0 -86
- package/src/components/SchemaVisualizer/useSchemaGraph.ts +0 -426
- package/src/components/StorageView/StorageView.tsx +0 -1588
- package/src/components/StorageView/storage-failure.ts +0 -63
- package/src/components/StudioHomePage.tsx +0 -430
- package/src/index.ts +0 -31
- package/src/utils/parseSpec.test.ts +0 -309
- package/src/utils/sql_utils.test.ts +0 -265
- package/src/utils/sql_utils.ts +0 -291
|
@@ -1,499 +0,0 @@
|
|
|
1
|
-
import React, { useState, useCallback, useMemo, useEffect } from "react";
|
|
2
|
-
import {
|
|
3
|
-
Button,
|
|
4
|
-
cls,
|
|
5
|
-
defaultBorderMixin,
|
|
6
|
-
IconButton,
|
|
7
|
-
iconSize,
|
|
8
|
-
LoaderIcon,
|
|
9
|
-
PlusIcon,
|
|
10
|
-
SendIcon,
|
|
11
|
-
TextField,
|
|
12
|
-
Typography,
|
|
13
|
-
XIcon
|
|
14
|
-
} from "@rebasepro/ui";
|
|
15
|
-
import { useRebaseContext, UserSelectPopover, SelectableUser } from "@rebasepro/app";
|
|
16
|
-
import { AuthSimulationSelector } from "../AuthSimulationSelector";
|
|
17
|
-
import type { ParsedEndpoint } from "./types";
|
|
18
|
-
import type { User } from "@rebasepro/types";
|
|
19
|
-
|
|
20
|
-
interface TryItPanelProps {
|
|
21
|
-
endpoint: ParsedEndpoint;
|
|
22
|
-
apiUrl: string;
|
|
23
|
-
getAuthToken: () => Promise<string | null | undefined>;
|
|
24
|
-
user: User | null;
|
|
25
|
-
basePath?: string;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Interactive "Try It" panel that lets the user execute API requests
|
|
30
|
-
* using their current JWT token directly from the Studio.
|
|
31
|
-
*/
|
|
32
|
-
export function TryItPanel({ endpoint, apiUrl, getAuthToken, user, basePath = "" }: TryItPanelProps) {
|
|
33
|
-
const storageKey = `rebase_apiexplorer_${endpoint.method}_${endpoint.path}`;
|
|
34
|
-
|
|
35
|
-
const [pathParams, setPathParams] = useState<Record<string, string>>(() => {
|
|
36
|
-
try { const v = localStorage.getItem(`${storageKey}_path`); if (v) return JSON.parse(v); } catch { /* ignore */ }
|
|
37
|
-
return {};
|
|
38
|
-
});
|
|
39
|
-
const [queryParams, setQueryParams] = useState<Record<string, string>>(() => {
|
|
40
|
-
try { const v = localStorage.getItem(`${storageKey}_query`); if (v) return JSON.parse(v); } catch { /* ignore */ }
|
|
41
|
-
return {};
|
|
42
|
-
});
|
|
43
|
-
const [customHeaders, setCustomHeaders] = useState<Array<{ key: string; value: string }>>(() => {
|
|
44
|
-
try { const v = localStorage.getItem(`${storageKey}_headers`); if (v) return JSON.parse(v); } catch { /* ignore */ }
|
|
45
|
-
return [{ key: "rebase-branch",
|
|
46
|
-
value: "" }];
|
|
47
|
-
});
|
|
48
|
-
const [body, setBody] = useState(() => {
|
|
49
|
-
try { const v = localStorage.getItem(`${storageKey}_body`); if (v) return JSON.parse(v); } catch { /* ignore */ }
|
|
50
|
-
return buildBodyTemplate(endpoint);
|
|
51
|
-
});
|
|
52
|
-
const [response, setResponse] = useState<{ status: number; statusText: string; body: string; time: number } | null>(
|
|
53
|
-
null
|
|
54
|
-
);
|
|
55
|
-
const [loading, setLoading] = useState(false);
|
|
56
|
-
const [authMode, setAuthMode] = useState<"jwt" | "none">("jwt");
|
|
57
|
-
const [validationError, setValidationError] = useState<string | null>(null);
|
|
58
|
-
|
|
59
|
-
const rebaseContext = useRebaseContext();
|
|
60
|
-
const currentUser = rebaseContext.authController?.user;
|
|
61
|
-
|
|
62
|
-
const users = useMemo((): SelectableUser[] => {
|
|
63
|
-
const managed: SelectableUser[] = [];
|
|
64
|
-
if (currentUser) {
|
|
65
|
-
managed.push({
|
|
66
|
-
uid: currentUser.uid,
|
|
67
|
-
displayName: currentUser.displayName,
|
|
68
|
-
email: currentUser.email,
|
|
69
|
-
photoURL: currentUser.photoURL,
|
|
70
|
-
roles: currentUser.roles
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
return managed;
|
|
74
|
-
}, [currentUser]);
|
|
75
|
-
|
|
76
|
-
const currentSelectableUser = useMemo((): SelectableUser | null => {
|
|
77
|
-
if (!currentUser) return null;
|
|
78
|
-
return {
|
|
79
|
-
uid: currentUser.uid,
|
|
80
|
-
displayName: currentUser.displayName,
|
|
81
|
-
email: currentUser.email,
|
|
82
|
-
photoURL: currentUser.photoURL,
|
|
83
|
-
roles: currentUser.roles
|
|
84
|
-
};
|
|
85
|
-
}, [currentUser]);
|
|
86
|
-
|
|
87
|
-
const [selectedUser, setSelectedUser] = useState<SelectableUser | null>(null);
|
|
88
|
-
|
|
89
|
-
useEffect(() => {
|
|
90
|
-
localStorage.setItem(`${storageKey}_path`, JSON.stringify(pathParams));
|
|
91
|
-
localStorage.setItem(`${storageKey}_query`, JSON.stringify(queryParams));
|
|
92
|
-
localStorage.setItem(`${storageKey}_headers`, JSON.stringify(customHeaders));
|
|
93
|
-
localStorage.setItem(`${storageKey}_body`, JSON.stringify(body));
|
|
94
|
-
}, [storageKey, pathParams, queryParams, customHeaders, body]);
|
|
95
|
-
|
|
96
|
-
// Build the final URL
|
|
97
|
-
const resolvedUrl = useMemo(() => {
|
|
98
|
-
const base = basePath.startsWith("/") ? basePath : `/${basePath}`;
|
|
99
|
-
const cleanBase = base === "/" ? "" : base;
|
|
100
|
-
let url = `${apiUrl.replace(/\/+$/, "")}${cleanBase}${endpoint.path}`;
|
|
101
|
-
// Replace path params
|
|
102
|
-
for (const [key, val] of Object.entries(pathParams)) {
|
|
103
|
-
url = url.replace(`{${key}}`, encodeURIComponent(val));
|
|
104
|
-
}
|
|
105
|
-
// Append query params
|
|
106
|
-
const qp = Object.entries(queryParams).filter(([, v]) => v.trim() !== "");
|
|
107
|
-
if (qp.length > 0) {
|
|
108
|
-
url += "?" + qp.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
|
|
109
|
-
}
|
|
110
|
-
return url;
|
|
111
|
-
}, [apiUrl, basePath, endpoint.path, pathParams, queryParams]);
|
|
112
|
-
|
|
113
|
-
const pathParamDefs = endpoint.parameters.filter((p) => p.in === "path");
|
|
114
|
-
const queryParamDefs = endpoint.parameters.filter((p) => p.in === "query");
|
|
115
|
-
const hasBody = ["post", "put", "patch"].includes(endpoint.method);
|
|
116
|
-
|
|
117
|
-
const execute = useCallback(async () => {
|
|
118
|
-
setValidationError(null);
|
|
119
|
-
if (hasBody && body.trim()) {
|
|
120
|
-
try {
|
|
121
|
-
JSON.parse(body);
|
|
122
|
-
} catch (err: unknown) {
|
|
123
|
-
setValidationError(`Invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
setLoading(true);
|
|
129
|
-
setResponse(null);
|
|
130
|
-
const start = performance.now();
|
|
131
|
-
|
|
132
|
-
try {
|
|
133
|
-
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
134
|
-
|
|
135
|
-
for (const h of customHeaders) {
|
|
136
|
-
if (h.key.trim()) headers[h.key.trim()] = h.value;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
if (authMode === "jwt") {
|
|
140
|
-
const token = await getAuthToken();
|
|
141
|
-
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
142
|
-
if (selectedUser && selectedUser.uid !== currentUser?.uid) {
|
|
143
|
-
headers["x-rebase-impersonate"] = selectedUser.uid;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const res = await fetch(resolvedUrl, {
|
|
148
|
-
method: endpoint.method.toUpperCase(),
|
|
149
|
-
headers,
|
|
150
|
-
body: hasBody && body.trim() ? body : undefined
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
const elapsed = Math.round(performance.now() - start);
|
|
154
|
-
let text: string;
|
|
155
|
-
|
|
156
|
-
const rawText = await res.text();
|
|
157
|
-
try {
|
|
158
|
-
const json = JSON.parse(rawText);
|
|
159
|
-
text = JSON.stringify(json, null, 2);
|
|
160
|
-
} catch {
|
|
161
|
-
text = rawText;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
setResponse({ status: res.status,
|
|
165
|
-
statusText: res.statusText,
|
|
166
|
-
body: text,
|
|
167
|
-
time: elapsed });
|
|
168
|
-
} catch (err: unknown) {
|
|
169
|
-
setResponse({
|
|
170
|
-
status: 0,
|
|
171
|
-
statusText: "Network Error",
|
|
172
|
-
body: err instanceof Error ? err.message : "Request failed",
|
|
173
|
-
time: Math.round(performance.now() - start)
|
|
174
|
-
});
|
|
175
|
-
} finally {
|
|
176
|
-
setLoading(false);
|
|
177
|
-
}
|
|
178
|
-
}, [resolvedUrl, endpoint.method, hasBody, body, authMode, getAuthToken, customHeaders, selectedUser, currentUser?.uid]);
|
|
179
|
-
|
|
180
|
-
return (
|
|
181
|
-
<div className="flex flex-col h-full">
|
|
182
|
-
<div className="p-5 space-y-5 overflow-y-auto flex-1">
|
|
183
|
-
{/* Auth Mode */}
|
|
184
|
-
<AuthSimulationSelector
|
|
185
|
-
authMode={authMode}
|
|
186
|
-
setAuthMode={setAuthMode}
|
|
187
|
-
selectedUser={selectedUser}
|
|
188
|
-
setSelectedUser={setSelectedUser}
|
|
189
|
-
users={users}
|
|
190
|
-
loading={false}
|
|
191
|
-
currentUser={currentSelectableUser}
|
|
192
|
-
/>
|
|
193
|
-
|
|
194
|
-
{/* Path Params */}
|
|
195
|
-
{pathParamDefs.length > 0 && (
|
|
196
|
-
<ParamSection
|
|
197
|
-
title="Path Parameters"
|
|
198
|
-
params={pathParamDefs}
|
|
199
|
-
values={pathParams}
|
|
200
|
-
onChange={(k, v) => setPathParams((prev) => ({ ...prev,
|
|
201
|
-
[k]: v }))}
|
|
202
|
-
/>
|
|
203
|
-
)}
|
|
204
|
-
|
|
205
|
-
{/* Query Params */}
|
|
206
|
-
{queryParamDefs.length > 0 && (
|
|
207
|
-
<ParamSection
|
|
208
|
-
title="Query Parameters"
|
|
209
|
-
params={queryParamDefs}
|
|
210
|
-
values={queryParams}
|
|
211
|
-
onChange={(k, v) => setQueryParams((prev) => ({ ...prev,
|
|
212
|
-
[k]: v }))}
|
|
213
|
-
/>
|
|
214
|
-
)}
|
|
215
|
-
|
|
216
|
-
{/* Custom Headers */}
|
|
217
|
-
<CustomKeyValueSection
|
|
218
|
-
title="Custom Headers"
|
|
219
|
-
values={customHeaders}
|
|
220
|
-
onChange={(i, k, v) => {
|
|
221
|
-
const next = [...customHeaders];
|
|
222
|
-
next[i] = { key: k,
|
|
223
|
-
value: v };
|
|
224
|
-
setCustomHeaders(next);
|
|
225
|
-
}}
|
|
226
|
-
onAdd={() => setCustomHeaders((prev) => [...prev, { key: "",
|
|
227
|
-
value: "" }])}
|
|
228
|
-
onRemove={(i) => {
|
|
229
|
-
const next = [...customHeaders];
|
|
230
|
-
next.splice(i, 1);
|
|
231
|
-
setCustomHeaders(next);
|
|
232
|
-
}}
|
|
233
|
-
/>
|
|
234
|
-
|
|
235
|
-
{/* Request Body */}
|
|
236
|
-
{hasBody && (
|
|
237
|
-
<div>
|
|
238
|
-
<Typography
|
|
239
|
-
variant="caption"
|
|
240
|
-
className="text-text-secondary dark:text-text-secondary-dark text-xs font-semibold uppercase tracking-wider mb-2 block"
|
|
241
|
-
>
|
|
242
|
-
Request Body
|
|
243
|
-
</Typography>
|
|
244
|
-
<TextField
|
|
245
|
-
multiline
|
|
246
|
-
minRows={10}
|
|
247
|
-
aria-label="Request Body"
|
|
248
|
-
value={body}
|
|
249
|
-
onChange={(e) => { setBody(e.target.value); setValidationError(null); }}
|
|
250
|
-
spellCheck={false}
|
|
251
|
-
error={!!validationError}
|
|
252
|
-
className="w-full"
|
|
253
|
-
inputClassName="font-mono text-xs p-3 resize-y"
|
|
254
|
-
/>
|
|
255
|
-
{validationError && (
|
|
256
|
-
<Typography variant="caption" className="text-red-500 mt-1 block text-xs">
|
|
257
|
-
{validationError}
|
|
258
|
-
</Typography>
|
|
259
|
-
)}
|
|
260
|
-
</div>
|
|
261
|
-
)}
|
|
262
|
-
|
|
263
|
-
{/* URL Preview */}
|
|
264
|
-
<div className="rounded-lg bg-surface-100 dark:bg-surface-900 p-3">
|
|
265
|
-
<Typography
|
|
266
|
-
variant="caption"
|
|
267
|
-
className="text-text-secondary dark:text-text-secondary-dark text-[10px] uppercase tracking-wider block mb-1"
|
|
268
|
-
>
|
|
269
|
-
Request URL
|
|
270
|
-
</Typography>
|
|
271
|
-
<code className="text-xs font-mono text-text-primary dark:text-text-primary-dark break-all">
|
|
272
|
-
{resolvedUrl}
|
|
273
|
-
</code>
|
|
274
|
-
</div>
|
|
275
|
-
|
|
276
|
-
{/* Execute Button */}
|
|
277
|
-
<Button variant="filled" onClick={execute} disabled={loading} className="w-full">
|
|
278
|
-
{loading ? (
|
|
279
|
-
<span className="flex items-center gap-2">
|
|
280
|
-
<LoaderIcon size={iconSize.small} className="animate-spin" />
|
|
281
|
-
Sending…
|
|
282
|
-
</span>
|
|
283
|
-
) : (
|
|
284
|
-
<span className="flex items-center gap-2">
|
|
285
|
-
<SendIcon size={iconSize.small} />
|
|
286
|
-
Send Request
|
|
287
|
-
</span>
|
|
288
|
-
)}
|
|
289
|
-
</Button>
|
|
290
|
-
|
|
291
|
-
{/* Response */}
|
|
292
|
-
{response && (
|
|
293
|
-
<div className={cls("rounded-lg border overflow-hidden", defaultBorderMixin)}>
|
|
294
|
-
<div
|
|
295
|
-
className={cls(
|
|
296
|
-
"flex items-center justify-between px-4 py-2.5",
|
|
297
|
-
"bg-surface-50 dark:bg-surface-900/50"
|
|
298
|
-
)}
|
|
299
|
-
>
|
|
300
|
-
<div className="flex items-center gap-3">
|
|
301
|
-
<ResponseBadge status={response.status}/>
|
|
302
|
-
<Typography
|
|
303
|
-
variant="caption"
|
|
304
|
-
className="text-text-secondary dark:text-text-secondary-dark text-xs"
|
|
305
|
-
>
|
|
306
|
-
{response.statusText}
|
|
307
|
-
</Typography>
|
|
308
|
-
</div>
|
|
309
|
-
<Typography
|
|
310
|
-
variant="caption"
|
|
311
|
-
className="text-text-secondary dark:text-text-secondary-dark text-xs font-mono"
|
|
312
|
-
>
|
|
313
|
-
{response.time}ms
|
|
314
|
-
</Typography>
|
|
315
|
-
</div>
|
|
316
|
-
<pre
|
|
317
|
-
className={cls(
|
|
318
|
-
"p-4 text-xs font-mono overflow-auto max-h-96",
|
|
319
|
-
"bg-surface-950 text-emerald-400",
|
|
320
|
-
"dark:bg-surface-900 dark:text-emerald-400"
|
|
321
|
-
)}
|
|
322
|
-
>
|
|
323
|
-
{response.body}
|
|
324
|
-
</pre>
|
|
325
|
-
</div>
|
|
326
|
-
)}
|
|
327
|
-
</div>
|
|
328
|
-
</div>
|
|
329
|
-
);
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
/* ── Param Section ────────────────────────────────────────────────── */
|
|
333
|
-
|
|
334
|
-
function ParamSection({
|
|
335
|
-
title,
|
|
336
|
-
params,
|
|
337
|
-
values,
|
|
338
|
-
onChange
|
|
339
|
-
}: {
|
|
340
|
-
title: string;
|
|
341
|
-
params: ParsedEndpoint["parameters"];
|
|
342
|
-
values: Record<string, string>;
|
|
343
|
-
onChange: (_key: string, _value: string) => void;
|
|
344
|
-
}) {
|
|
345
|
-
const paramIdBase = React.useId();
|
|
346
|
-
return (
|
|
347
|
-
<div>
|
|
348
|
-
<Typography
|
|
349
|
-
variant="caption"
|
|
350
|
-
className="text-text-secondary dark:text-text-secondary-dark text-xs font-semibold uppercase tracking-wider mb-2 block"
|
|
351
|
-
>
|
|
352
|
-
{title}
|
|
353
|
-
</Typography>
|
|
354
|
-
<div className="space-y-2">
|
|
355
|
-
{params.map((p, index) => (
|
|
356
|
-
<div key={p.name} className="flex items-center gap-3">
|
|
357
|
-
<label className="w-32 shrink-0" htmlFor={`${paramIdBase}-${index}-param`}>
|
|
358
|
-
<code className="text-xs font-mono font-semibold">{p.name}</code>
|
|
359
|
-
{p.required && <span className="text-red-500 ml-0.5 text-xs">*</span>}
|
|
360
|
-
</label>
|
|
361
|
-
<TextField
|
|
362
|
-
id={`${paramIdBase}-${index}-param`}
|
|
363
|
-
size="small"
|
|
364
|
-
placeholder={p.description ?? p.name}
|
|
365
|
-
value={values[p.name] ?? ""}
|
|
366
|
-
onChange={(e) => onChange(p.name, e.target.value)}
|
|
367
|
-
className="flex-1"
|
|
368
|
-
inputClassName="font-mono text-xs"
|
|
369
|
-
/>
|
|
370
|
-
</div>
|
|
371
|
-
))}
|
|
372
|
-
</div>
|
|
373
|
-
</div>
|
|
374
|
-
);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
function CustomKeyValueSection({
|
|
378
|
-
title,
|
|
379
|
-
values,
|
|
380
|
-
onChange,
|
|
381
|
-
onAdd,
|
|
382
|
-
onRemove
|
|
383
|
-
}: {
|
|
384
|
-
title: string;
|
|
385
|
-
values: Array<{ key: string; value: string }>;
|
|
386
|
-
onChange: (index: number, key: string, value: string) => void;
|
|
387
|
-
onAdd: () => void;
|
|
388
|
-
onRemove: (index: number) => void;
|
|
389
|
-
}) {
|
|
390
|
-
return (
|
|
391
|
-
<div>
|
|
392
|
-
<div className="flex items-center justify-between mb-2">
|
|
393
|
-
<Typography
|
|
394
|
-
variant="caption"
|
|
395
|
-
className="text-text-secondary dark:text-text-secondary-dark text-xs font-semibold uppercase tracking-wider block"
|
|
396
|
-
>
|
|
397
|
-
{title}
|
|
398
|
-
</Typography>
|
|
399
|
-
<Button
|
|
400
|
-
variant="text"
|
|
401
|
-
size="small"
|
|
402
|
-
color="primary"
|
|
403
|
-
onClick={onAdd}
|
|
404
|
-
className="text-xs p-0 min-h-0"
|
|
405
|
-
>
|
|
406
|
-
<PlusIcon size={iconSize.small} className="mr-1" /> Add Header
|
|
407
|
-
</Button>
|
|
408
|
-
</div>
|
|
409
|
-
<div className="space-y-2">
|
|
410
|
-
{values.map((v, i) => (
|
|
411
|
-
<div key={i} className="flex items-center gap-2">
|
|
412
|
-
<TextField
|
|
413
|
-
size="small"
|
|
414
|
-
aria-label={`Header ${i + 1} name`}
|
|
415
|
-
placeholder="Header name"
|
|
416
|
-
value={v.key}
|
|
417
|
-
onChange={(e) => onChange(i, e.target.value, v.value)}
|
|
418
|
-
className="w-1/3"
|
|
419
|
-
inputClassName="font-mono text-xs"
|
|
420
|
-
/>
|
|
421
|
-
<TextField
|
|
422
|
-
size="small"
|
|
423
|
-
aria-label={`Header ${i + 1} value`}
|
|
424
|
-
placeholder="Value"
|
|
425
|
-
value={v.value}
|
|
426
|
-
onChange={(e) => onChange(i, v.key, e.target.value)}
|
|
427
|
-
className="flex-1"
|
|
428
|
-
inputClassName="font-mono text-xs"
|
|
429
|
-
/>
|
|
430
|
-
<IconButton
|
|
431
|
-
size="small"
|
|
432
|
-
onClick={() => onRemove(i)}
|
|
433
|
-
className="text-text-secondary hover:text-red-500 shrink-0"
|
|
434
|
-
title="Remove"
|
|
435
|
-
>
|
|
436
|
-
<XIcon size={iconSize.small} />
|
|
437
|
-
</IconButton>
|
|
438
|
-
</div>
|
|
439
|
-
))}
|
|
440
|
-
{values.length === 0 && (
|
|
441
|
-
<Typography variant="caption" className="text-text-secondary/50 italic text-xs">
|
|
442
|
-
No custom headers added.
|
|
443
|
-
</Typography>
|
|
444
|
-
)}
|
|
445
|
-
</div>
|
|
446
|
-
</div>
|
|
447
|
-
);
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
/* ── Helpers ──────────────────────────────────────────────────────── */
|
|
451
|
-
|
|
452
|
-
function ResponseBadge({ status }: { status: number }) {
|
|
453
|
-
let colorClass = "text-red-500";
|
|
454
|
-
if (status >= 200 && status < 300) colorClass = "text-emerald-500";
|
|
455
|
-
else if (status >= 300 && status < 400) colorClass = "text-blue-500";
|
|
456
|
-
else if (status >= 400 && status < 500) colorClass = "text-amber-500";
|
|
457
|
-
|
|
458
|
-
return (
|
|
459
|
-
<span className={cls("text-xs font-semibold font-mono", colorClass)}>
|
|
460
|
-
{status || "ERR"}
|
|
461
|
-
</span>
|
|
462
|
-
);
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
function buildBodyTemplate(endpoint: ParsedEndpoint): string {
|
|
466
|
-
if (!endpoint.requestBody?.content) return "{\n \n}";
|
|
467
|
-
const json = endpoint.requestBody.content["application/json"];
|
|
468
|
-
if (!json?.schema?.properties) return "{\n \n}";
|
|
469
|
-
|
|
470
|
-
const props = json.schema.properties;
|
|
471
|
-
const lines: string[] = ["{"];
|
|
472
|
-
const keys = Object.keys(props);
|
|
473
|
-
keys.forEach((key, i) => {
|
|
474
|
-
const prop = props[key];
|
|
475
|
-
if (prop.readOnly) return;
|
|
476
|
-
const comma = i < keys.length - 1 ? "," : "";
|
|
477
|
-
const val = defaultValue(prop);
|
|
478
|
-
lines.push(` "${key}": ${val}${comma}`);
|
|
479
|
-
});
|
|
480
|
-
lines.push("}");
|
|
481
|
-
return lines.join("\n");
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
function defaultValue(schema: { type?: string; format?: string; enum?: (string | number)[] }): string {
|
|
485
|
-
if (schema.enum) return JSON.stringify(schema.enum[0]);
|
|
486
|
-
switch (schema.type) {
|
|
487
|
-
case "string":
|
|
488
|
-
return schema.format === "date-time" ? `"${new Date().toISOString()}"` : '""';
|
|
489
|
-
case "number":
|
|
490
|
-
case "integer":
|
|
491
|
-
return "0";
|
|
492
|
-
case "boolean":
|
|
493
|
-
return "false";
|
|
494
|
-
case "array":
|
|
495
|
-
return "[]";
|
|
496
|
-
default:
|
|
497
|
-
return "null";
|
|
498
|
-
}
|
|
499
|
-
}
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
import type { OpenApiSpec, ParsedEndpoint, EndpointGroup } from "./types";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Parse an OpenAPI 3.x spec into grouped, sorted endpoints for the sidebar.
|
|
5
|
-
*/
|
|
6
|
-
export function parseOpenApiSpec(spec: OpenApiSpec): {
|
|
7
|
-
groups: EndpointGroup[];
|
|
8
|
-
allEndpoints: ParsedEndpoint[];
|
|
9
|
-
} {
|
|
10
|
-
const allEndpoints: ParsedEndpoint[] = [];
|
|
11
|
-
const tagMap = new Map<string, ParsedEndpoint[]>();
|
|
12
|
-
|
|
13
|
-
// Build tag description lookup
|
|
14
|
-
const tagDescriptions = new Map<string, string>();
|
|
15
|
-
for (const t of spec.tags ?? []) {
|
|
16
|
-
tagDescriptions.set(t.name, t.description ?? "");
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
for (const [path, methods] of Object.entries(spec.paths ?? {})) {
|
|
20
|
-
for (const [method, op] of Object.entries(methods)) {
|
|
21
|
-
if (["get", "post", "put", "patch", "delete"].indexOf(method) === -1) continue;
|
|
22
|
-
|
|
23
|
-
const tags = op.tags?.length ? op.tags : ["Other"];
|
|
24
|
-
const shortPath = path.replace(/^\/api\/data/, "");
|
|
25
|
-
|
|
26
|
-
const endpoint: ParsedEndpoint = {
|
|
27
|
-
id: `${method}:${path}`,
|
|
28
|
-
method,
|
|
29
|
-
path,
|
|
30
|
-
shortPath: shortPath || "/",
|
|
31
|
-
summary: op.summary ?? "",
|
|
32
|
-
description: op.description ?? "",
|
|
33
|
-
tags,
|
|
34
|
-
parameters: op.parameters ?? [],
|
|
35
|
-
requestBody: op.requestBody,
|
|
36
|
-
responses: op.responses ?? {},
|
|
37
|
-
security: op.security,
|
|
38
|
-
operationId: op.operationId
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
allEndpoints.push(endpoint);
|
|
42
|
-
for (const tag of tags) {
|
|
43
|
-
if (!tagMap.has(tag)) tagMap.set(tag, []);
|
|
44
|
-
tagMap.get(tag)!.push(endpoint);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Method sort order
|
|
50
|
-
const ORDER: Record<string, number> = { get: 0,
|
|
51
|
-
post: 1,
|
|
52
|
-
put: 2,
|
|
53
|
-
patch: 3,
|
|
54
|
-
delete: 4 };
|
|
55
|
-
|
|
56
|
-
const groups: EndpointGroup[] = [];
|
|
57
|
-
// Sort tags: use spec.tags order if available, else alphabetical
|
|
58
|
-
const tagOrder = (spec.tags ?? []).map((t) => t.name);
|
|
59
|
-
const sortedTags = [...tagMap.keys()].sort((a, b) => {
|
|
60
|
-
const ai = tagOrder.indexOf(a);
|
|
61
|
-
const bi = tagOrder.indexOf(b);
|
|
62
|
-
if (ai !== -1 && bi !== -1) return ai - bi;
|
|
63
|
-
if (ai !== -1) return -1;
|
|
64
|
-
if (bi !== -1) return 1;
|
|
65
|
-
return a.localeCompare(b);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
for (const tag of sortedTags) {
|
|
69
|
-
const endpoints = tagMap.get(tag)!;
|
|
70
|
-
endpoints.sort((a, b) => {
|
|
71
|
-
const pa = a.path.localeCompare(b.path);
|
|
72
|
-
if (pa !== 0) return pa;
|
|
73
|
-
return (ORDER[a.method] ?? 99) - (ORDER[b.method] ?? 99);
|
|
74
|
-
});
|
|
75
|
-
groups.push({
|
|
76
|
-
tag,
|
|
77
|
-
description: tagDescriptions.get(tag),
|
|
78
|
-
endpoints
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return { groups,
|
|
83
|
-
allEndpoints };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Resolve a $ref string (e.g. "#/components/schemas/Author") to a schema name.
|
|
88
|
-
*/
|
|
89
|
-
export function resolveRefName(ref: string): string {
|
|
90
|
-
const parts = ref.split("/");
|
|
91
|
-
return parts[parts.length - 1];
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Resolve a $ref to its actual schema from the spec.
|
|
96
|
-
*/
|
|
97
|
-
export function resolveRef(spec: OpenApiSpec, ref: string): unknown {
|
|
98
|
-
const parts = ref.replace("#/", "").split("/");
|
|
99
|
-
let current: unknown = spec;
|
|
100
|
-
for (const part of parts) {
|
|
101
|
-
current = (current as Record<string, unknown>)?.[part];
|
|
102
|
-
}
|
|
103
|
-
return current ?? {};
|
|
104
|
-
}
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
/** Minimal OpenAPI 3.x types we care about */
|
|
2
|
-
export interface OpenApiSpec {
|
|
3
|
-
openapi: string;
|
|
4
|
-
info: { title: string; version: string; description?: string };
|
|
5
|
-
servers?: { url: string; description?: string }[];
|
|
6
|
-
paths: Record<string, Record<string, OpenApiOperation>>;
|
|
7
|
-
components?: {
|
|
8
|
-
schemas?: Record<string, OpenApiSchema>;
|
|
9
|
-
securitySchemes?: Record<string, unknown>;
|
|
10
|
-
};
|
|
11
|
-
tags?: { name: string; description?: string }[];
|
|
12
|
-
security?: Record<string, string[]>[];
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface OpenApiOperation {
|
|
16
|
-
operationId?: string;
|
|
17
|
-
summary?: string;
|
|
18
|
-
description?: string;
|
|
19
|
-
tags?: string[];
|
|
20
|
-
parameters?: OpenApiParameter[];
|
|
21
|
-
requestBody?: {
|
|
22
|
-
required?: boolean;
|
|
23
|
-
content?: Record<string, { schema?: OpenApiSchema }>;
|
|
24
|
-
};
|
|
25
|
-
responses?: Record<
|
|
26
|
-
string,
|
|
27
|
-
{
|
|
28
|
-
description?: string;
|
|
29
|
-
content?: Record<string, { schema?: OpenApiSchema }>;
|
|
30
|
-
}
|
|
31
|
-
>;
|
|
32
|
-
security?: Record<string, string[]>[];
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export interface OpenApiParameter {
|
|
36
|
-
name: string;
|
|
37
|
-
in: "query" | "path" | "header" | "cookie";
|
|
38
|
-
description?: string;
|
|
39
|
-
required?: boolean;
|
|
40
|
-
schema?: OpenApiSchema;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export interface OpenApiSchema {
|
|
44
|
-
type?: string;
|
|
45
|
-
format?: string;
|
|
46
|
-
description?: string;
|
|
47
|
-
properties?: Record<string, OpenApiSchema>;
|
|
48
|
-
required?: string[];
|
|
49
|
-
items?: OpenApiSchema;
|
|
50
|
-
enum?: (string | number)[];
|
|
51
|
-
oneOf?: OpenApiSchema[];
|
|
52
|
-
anyOf?: OpenApiSchema[];
|
|
53
|
-
allOf?: OpenApiSchema[];
|
|
54
|
-
$ref?: string;
|
|
55
|
-
readOnly?: boolean;
|
|
56
|
-
minimum?: number;
|
|
57
|
-
maximum?: number;
|
|
58
|
-
maxLength?: number;
|
|
59
|
-
minLength?: number;
|
|
60
|
-
default?: unknown;
|
|
61
|
-
example?: unknown;
|
|
62
|
-
additionalProperties?: boolean | OpenApiSchema;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export interface ParsedEndpoint {
|
|
66
|
-
id: string;
|
|
67
|
-
method: string;
|
|
68
|
-
path: string;
|
|
69
|
-
shortPath: string;
|
|
70
|
-
summary: string;
|
|
71
|
-
description: string;
|
|
72
|
-
tags: string[];
|
|
73
|
-
parameters: OpenApiParameter[];
|
|
74
|
-
requestBody?: OpenApiOperation["requestBody"];
|
|
75
|
-
responses: NonNullable<OpenApiOperation["responses"]>;
|
|
76
|
-
security?: OpenApiOperation["security"];
|
|
77
|
-
operationId?: string;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export interface EndpointGroup {
|
|
81
|
-
tag: string;
|
|
82
|
-
description?: string;
|
|
83
|
-
endpoints: ParsedEndpoint[];
|
|
84
|
-
}
|