From 895b42f00eb3a65c8f4ebf02ef797773d70d38f6 Mon Sep 17 00:00:00 2001 From: mx <1669547593@qq.com> Date: Tue, 27 Jan 2026 19:16:29 +0800 Subject: [PATCH] feat: build orbit api studio --- .gitignore | 6 + README.md | 11 +- index.html | 12 ++ package.json | 28 +++ postcss.config.cjs | 6 + src/App.tsx | 35 +++ src/components/commands/CommandPalette.tsx | 96 +++++++++ src/components/layout/AppLayout.tsx | 60 ++++++ src/components/layout/TopBar.tsx | 136 ++++++++++++ src/components/request/CodeSnippetPanel.tsx | 54 +++++ src/components/request/KeyValueTable.tsx | 107 ++++++++++ src/components/request/RequestBuilder.tsx | 74 +++++++ src/components/request/RequestTabs.tsx | 39 ++++ src/components/request/RequestWorkspace.tsx | 202 ++++++++++++++++++ src/components/request/auth/AuthEditor.tsx | 87 ++++++++ src/components/request/body/BodyEditor.tsx | 108 ++++++++++ .../request/scripts/ScriptEditor.tsx | 30 +++ src/components/response/ResponseBody.tsx | 71 ++++++ src/components/response/ResponseHeaders.tsx | 31 +++ src/components/response/ResponsePanel.tsx | 93 ++++++++ src/components/response/TestResults.tsx | 32 +++ src/components/sidebar/EnvironmentEditor.tsx | 180 ++++++++++++++++ src/components/sidebar/Sidebar.tsx | 154 +++++++++++++ src/components/ui/button.tsx | 38 ++++ src/components/ui/input.tsx | 12 ++ src/components/ui/select.tsx | 18 ++ src/components/ui/tabs.tsx | 26 +++ src/components/ui/textarea.tsx | 15 ++ src/components/ui/toast.tsx | 78 +++++++ src/index.css | 58 +++++ src/main.tsx | 13 ++ src/store/useAppStore.ts | 174 +++++++++++++++ src/store/useThemeStore.ts | 16 ++ src/types/index.ts | 132 ++++++++++++ src/utils/cn.ts | 2 + src/utils/id.ts | 1 + src/utils/request.ts | 145 +++++++++++++ src/utils/sandbox.ts | 115 ++++++++++ src/utils/snippets.ts | 40 ++++ src/utils/storage.ts | 84 ++++++++ src/utils/variables.ts | 36 ++++ tailwind.config.ts | 43 ++++ tsconfig.json | 24 +++ vite.config.ts | 9 + 44 files changed, 2730 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 index.html create mode 100644 package.json create mode 100644 postcss.config.cjs create mode 100644 src/App.tsx create mode 100644 src/components/commands/CommandPalette.tsx create mode 100644 src/components/layout/AppLayout.tsx create mode 100644 src/components/layout/TopBar.tsx create mode 100644 src/components/request/CodeSnippetPanel.tsx create mode 100644 src/components/request/KeyValueTable.tsx create mode 100644 src/components/request/RequestBuilder.tsx create mode 100644 src/components/request/RequestTabs.tsx create mode 100644 src/components/request/RequestWorkspace.tsx create mode 100644 src/components/request/auth/AuthEditor.tsx create mode 100644 src/components/request/body/BodyEditor.tsx create mode 100644 src/components/request/scripts/ScriptEditor.tsx create mode 100644 src/components/response/ResponseBody.tsx create mode 100644 src/components/response/ResponseHeaders.tsx create mode 100644 src/components/response/ResponsePanel.tsx create mode 100644 src/components/response/TestResults.tsx create mode 100644 src/components/sidebar/EnvironmentEditor.tsx create mode 100644 src/components/sidebar/Sidebar.tsx create mode 100644 src/components/ui/button.tsx create mode 100644 src/components/ui/input.tsx create mode 100644 src/components/ui/select.tsx create mode 100644 src/components/ui/tabs.tsx create mode 100644 src/components/ui/textarea.tsx create mode 100644 src/components/ui/toast.tsx create mode 100644 src/index.css create mode 100644 src/main.tsx create mode 100644 src/store/useAppStore.ts create mode 100644 src/store/useThemeStore.ts create mode 100644 src/types/index.ts create mode 100644 src/utils/cn.ts create mode 100644 src/utils/id.ts create mode 100644 src/utils/request.ts create mode 100644 src/utils/sandbox.ts create mode 100644 src/utils/snippets.ts create mode 100644 src/utils/storage.ts create mode 100644 src/utils/variables.ts create mode 100644 tailwind.config.ts create mode 100644 tsconfig.json create mode 100644 vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e39f0d9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules +.dist +.dist-ssr +coverage +.vite +.env diff --git a/README.md b/README.md index 9184765..ab158b2 100644 --- a/README.md +++ b/README.md @@ -1 +1,10 @@ -# note \ No newline at end of file +# Orbit API Studio + +## Run + +```bash +pnpm install +pnpm dev +``` + +Open http://localhost:5173 in your browser. diff --git a/index.html b/index.html new file mode 100644 index 0000000..319c467 --- /dev/null +++ b/index.html @@ -0,0 +1,12 @@ + + + + + + Orbit API Studio + + +
+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..df9ca90 --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "note-postman-clone", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "lucide-react": "^0.473.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.26.2", + "zustand": "^4.5.5" + }, + "devDependencies": { + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "autoprefixer": "^10.4.20", + "postcss": "^8.4.41", + "tailwindcss": "^3.4.10", + "typescript": "^5.5.4", + "vite": "^5.4.1" + } +} diff --git a/postcss.config.cjs b/postcss.config.cjs new file mode 100644 index 0000000..12a703d --- /dev/null +++ b/postcss.config.cjs @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..616f543 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,35 @@ +import { Route, Routes } from "react-router-dom"; +import { useEffect } from "react"; +import { AppLayout } from "./components/layout/AppLayout"; +import { useThemeStore } from "./store/useThemeStore"; +import { ToastProvider } from "./components/ui/toast"; + +const Workspace = () => { + return ; +}; + +const NotFound = () => ( +
+
+

404

+

Page not found.

+
+
+); + +export default function App() { + const { theme } = useThemeStore(); + + useEffect(() => { + document.documentElement.classList.toggle("dark", theme === "dark"); + }, [theme]); + + return ( + + + } /> + } /> + + + ); +} diff --git a/src/components/commands/CommandPalette.tsx b/src/components/commands/CommandPalette.tsx new file mode 100644 index 0000000..f9c7a0a --- /dev/null +++ b/src/components/commands/CommandPalette.tsx @@ -0,0 +1,96 @@ +import { useEffect, useMemo, useState } from "react"; +import { Search } from "lucide-react"; +import { useAppStore } from "../../store/useAppStore"; +import { Input } from "../ui/input"; + +export const CommandPalette = () => { + const [open, setOpen] = useState(false); + const [query, setQuery] = useState(""); + const { requests, history, environments, globals, openRequestInTab } = useAppStore( + (state) => state + ); + + useEffect(() => { + const handler = (event: KeyboardEvent) => { + if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") { + event.preventDefault(); + setOpen((prev) => !prev); + } + if (event.key === "Escape") { + setOpen(false); + } + }; + window.addEventListener("keydown", handler); + return () => window.removeEventListener("keydown", handler); + }, []); + + const results = useMemo(() => { + const lower = query.toLowerCase(); + const requestItems = requests + .filter((item) => item.name.toLowerCase().includes(lower) || item.url.includes(lower)) + .map((item) => ({ + id: item.id, + label: `Request · ${item.name}`, + action: () => openRequestInTab(item.id), + })); + const historyItems = history + .filter((item) => item.request.name.toLowerCase().includes(lower)) + .slice(0, 4) + .map((item) => ({ + id: item.id, + label: `History · ${item.request.name}`, + action: () => openRequestInTab(item.request.id), + })); + const variableItems = [ + ...globals, + ...environments.flatMap((env) => env.variables), + ] + .filter((item) => item.key.toLowerCase().includes(lower)) + .slice(0, 4) + .map((item) => ({ id: item.id, label: `Var · ${item.key}`, action: () => {} })); + + return [...requestItems, ...historyItems, ...variableItems].slice(0, 8); + }, [query, requests, history, environments, globals, openRequestInTab]); + + if (!open) return null; + + return ( +
+
+
+ + setQuery(event.target.value)} + autoFocus + /> +
+
+ {results.length === 0 && ( +
+ No results. Try searching by request name, URL, or variable. +
+ )} + {results.map((item) => ( + + ))} +
+

+ Tip: Cmd/Ctrl+Enter to send request. Esc to close. +

+
+
+ ); +}; diff --git a/src/components/layout/AppLayout.tsx b/src/components/layout/AppLayout.tsx new file mode 100644 index 0000000..9c5498c --- /dev/null +++ b/src/components/layout/AppLayout.tsx @@ -0,0 +1,60 @@ +import { useEffect, useMemo, useRef } from "react"; +import { useAppStore } from "../../store/useAppStore"; +import { TopBar } from "./TopBar"; +import { Sidebar } from "../sidebar/Sidebar"; +import { RequestWorkspace } from "../request/RequestWorkspace"; +import { ResponsePanel } from "../response/ResponsePanel"; +import { CommandPalette } from "../commands/CommandPalette"; +import { useToast } from "../ui/toast"; + +export const AppLayout = () => { + const { notify } = useToast(); + const activeTabId = useAppStore((state) => state.activeTabId); + const tabs = useAppStore((state) => state.tabs); + const setResponse = useAppStore((state) => state.setResponse); + const isSending = useAppStore((state) => state.isSending); + const setSending = useAppStore((state) => state.setSending); + const responseRef = useRef(null); + + const activeTab = useMemo( + () => tabs.find((tab) => tab.id === activeTabId) ?? tabs[0], + [tabs, activeTabId] + ); + + useEffect(() => { + if (!activeTab) return; + if (!activeTabId) { + useAppStore.setState({ activeTabId: activeTab.id }); + } + }, [activeTab, activeTabId]); + + useEffect(() => { + const handler = (event: KeyboardEvent) => { + if ((event.metaKey || event.ctrlKey) && event.key === "Enter") { + event.preventDefault(); + document.dispatchEvent(new CustomEvent("send-request")); + } + if (event.key === "Escape" && isSending) { + event.preventDefault(); + document.dispatchEvent(new CustomEvent("cancel-request")); + notify({ title: "Request cancelled", description: "Abort signal sent." }); + setSending(false); + setResponse(undefined); + } + }; + window.addEventListener("keydown", handler); + return () => window.removeEventListener("keydown", handler); + }, [isSending, notify, setResponse, setSending]); + + return ( +
+ +
+ + + +
+ +
+ ); +}; diff --git a/src/components/layout/TopBar.tsx b/src/components/layout/TopBar.tsx new file mode 100644 index 0000000..b7c9371 --- /dev/null +++ b/src/components/layout/TopBar.tsx @@ -0,0 +1,136 @@ +import { useMemo, useRef } from "react"; +import { Download, Upload, Sun, Moon, Users, User } from "lucide-react"; +import { Button } from "../ui/button"; +import { Select } from "../ui/select"; +import { useThemeStore } from "../../store/useThemeStore"; +import { useAppStore } from "../../store/useAppStore"; +import { exportState, importState, saveState } from "../../utils/storage"; +import { useToast } from "../ui/toast"; + +export const TopBar = () => { + const { toggle, theme } = useThemeStore(); + const { notify } = useToast(); + const { + workspace, + setWorkspace, + environments, + activeEnvironmentId, + setActiveEnvironment, + requests, + history, + collections, + globals, + } = useAppStore((state) => state); + const fileRef = useRef(null); + + const exportPayload = useMemo( + () => ({ + version: 1, + requests, + history, + collections, + environments, + globals, + workspace, + }), + [collections, environments, globals, history, requests, workspace] + ); + + const handleExport = async () => { + const blob = await exportState(exportPayload); + const url = URL.createObjectURL(blob); + const anchor = document.createElement("a"); + anchor.href = url; + anchor.download = "orbit-api-export.json"; + anchor.click(); + URL.revokeObjectURL(url); + notify({ title: "Export ready", description: "JSON export generated." }); + }; + + const handleImport = async (file?: File) => { + if (!file) return; + const state = await importState(file); + if (!state) { + notify({ title: "Import failed", description: "Invalid JSON bundle.", variant: "error" }); + return; + } + useAppStore.setState({ + requests: state.requests, + history: state.history, + collections: state.collections, + environments: state.environments, + globals: state.globals, + workspace: state.workspace, + }); + saveState(state); + notify({ title: "Import complete", description: "Workspace restored." }); + }; + + return ( +
+
+
+

Orbit API Studio

+

Modern API workspace

+
+
+ + +
+
+
+
+ +
+ + + { + const file = event.target.files?.[0]; + handleImport(file); + event.target.value = ""; + }} + /> + +
+
+ ); +}; diff --git a/src/components/request/CodeSnippetPanel.tsx b/src/components/request/CodeSnippetPanel.tsx new file mode 100644 index 0000000..a2105fa --- /dev/null +++ b/src/components/request/CodeSnippetPanel.tsx @@ -0,0 +1,54 @@ +import { useMemo, useState } from "react"; +import { Copy } from "lucide-react"; +import { RequestConfig } from "../../types"; +import { generateSnippets } from "../../utils/snippets"; +import { useAppStore } from "../../store/useAppStore"; +import { Button } from "../ui/button"; +import { useToast } from "../ui/toast"; + +export const CodeSnippetPanel = ({ request }: { request: RequestConfig }) => { + const { globals, environments, activeEnvironmentId } = useAppStore((state) => state); + const env = environments.find((item) => item.id === activeEnvironmentId); + const snippets = useMemo( + () => generateSnippets(request, { globals, environment: env }), + [request, globals, env] + ); + const [active, setActive] = useState(snippets[0]?.id ?? "curl"); + const { notify } = useToast(); + + const current = snippets.find((item) => item.id === active) ?? snippets[0]; + + return ( +
+
+ {snippets.map((item) => ( + + ))} + +
+ {current && ( +
+          {current.code}
+        
+ )} +
+ ); +}; diff --git a/src/components/request/KeyValueTable.tsx b/src/components/request/KeyValueTable.tsx new file mode 100644 index 0000000..7ad3a1c --- /dev/null +++ b/src/components/request/KeyValueTable.tsx @@ -0,0 +1,107 @@ +import { useEffect } from "react"; +import { KeyValueRow } from "../../types"; +import { createId } from "../../utils/id"; +import { Button } from "../ui/button"; + +const ensureTrailingRow = (rows: KeyValueRow[]) => { + const last = rows[rows.length - 1]; + if (!last || last.key || last.value) { + return [...rows, { id: createId(), key: "", value: "", enabled: true }]; + } + return rows; +}; + +export const KeyValueTable = ({ + rows, + onChange, + hint, + quickAdd = [], +}: { + rows: KeyValueRow[]; + onChange: (rows: KeyValueRow[]) => void; + hint?: string; + quickAdd?: string[]; +}) => { + useEffect(() => { + onChange(ensureTrailingRow(rows)); + }, []); + + const updateRow = (id: string, updates: Partial) => { + const updated = rows.map((row) => (row.id === id ? { ...row, ...updates } : row)); + onChange(ensureTrailingRow(updated)); + }; + + const removeRow = (id: string) => { + onChange(rows.filter((row) => row.id !== id)); + }; + + return ( +
+
+ {hint &&

{hint}

} +
+ {quickAdd.map((item) => ( + + ))} +
+
+
+
+ On + Key + Value + +
+ {rows.map((row, index) => ( +
+ updateRow(row.id, { enabled: !row.enabled })} + /> + updateRow(row.id, { key: event.target.value })} + onKeyDown={(event) => { + if (event.key === "Enter" && index === rows.length - 1) { + onChange(ensureTrailingRow([...rows])); + } + }} + placeholder="Key" + /> + updateRow(row.id, { value: event.target.value })} + placeholder="Value" + /> + +
+ ))} +
+
+ ); +}; diff --git a/src/components/request/RequestBuilder.tsx b/src/components/request/RequestBuilder.tsx new file mode 100644 index 0000000..70a1241 --- /dev/null +++ b/src/components/request/RequestBuilder.tsx @@ -0,0 +1,74 @@ +import { useState } from "react"; +import { RequestConfig } from "../../types"; +import { useAppStore } from "../../store/useAppStore"; +import { Tabs, Tab } from "../ui/tabs"; +import { KeyValueTable } from "./KeyValueTable"; +import { AuthEditor } from "./auth/AuthEditor"; +import { BodyEditor } from "./body/BodyEditor"; +import { ScriptEditor } from "./scripts/ScriptEditor"; +import { CodeSnippetPanel } from "./CodeSnippetPanel"; + +const tabs = ["Params", "Headers", "Auth", "Body", "Pre-request", "Tests", "Code"] as const; + +export const RequestBuilder = ({ request, tabId }: { request: RequestConfig; tabId: string }) => { + const [activeTab, setActiveTab] = useState<(typeof tabs)[number]>("Params"); + const updateRequest = useAppStore((state) => state.updateRequest); + const markTabDirty = useAppStore((state) => state.markTabDirty); + + const update = (next: Partial) => { + updateRequest({ ...request, ...next, updatedAt: Date.now() }); + markTabDirty(tabId, true); + }; + + return ( +
+ + {tabs.map((label) => ( + setActiveTab(label)}> + {label} + + ))} + +
+ {activeTab === "Params" && ( + update({ params: rows })} + hint="Query parameters appended to URL" + /> + )} + {activeTab === "Headers" && ( + update({ headers: rows })} + hint="Header keys are case-insensitive and merged" + quickAdd={["Content-Type", "Authorization", "Accept"]} + /> + )} + {activeTab === "Auth" && ( + update({ auth })} /> + )} + {activeTab === "Body" && ( + update({ body })} /> + )} + {activeTab === "Pre-request" && ( + update({ scripts: { ...request.scripts, preRequest: value } })} + /> + )} + {activeTab === "Tests" && ( + update({ scripts: { ...request.scripts, tests: value } })} + /> + )} + {activeTab === "Code" && } +
+
+ ); +}; diff --git a/src/components/request/RequestTabs.tsx b/src/components/request/RequestTabs.tsx new file mode 100644 index 0000000..adc74b1 --- /dev/null +++ b/src/components/request/RequestTabs.tsx @@ -0,0 +1,39 @@ +import { X } from "lucide-react"; +import { useAppStore } from "../../store/useAppStore"; +import { cn } from "../../utils/cn"; + +export const RequestTabs = () => { + const { tabs, activeTabId, openRequestInTab, closeTab, requests } = useAppStore( + (state) => state + ); + + return ( +
+ {tabs.map((tab) => { + const request = requests.find((item) => item.id === tab.requestId); + return ( + + ); + })} +
+ ); +}; diff --git a/src/components/request/RequestWorkspace.tsx b/src/components/request/RequestWorkspace.tsx new file mode 100644 index 0000000..3af8120 --- /dev/null +++ b/src/components/request/RequestWorkspace.tsx @@ -0,0 +1,202 @@ +import { RefObject, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { Save, Send, XCircle } from "lucide-react"; +import { useAppStore } from "../../store/useAppStore"; +import { Button } from "../ui/button"; +import { RequestBuilder } from "./RequestBuilder"; +import { RequestTabs } from "./RequestTabs"; +import { executeRequest, buildRequest } from "../../utils/request"; +import { useToast } from "../ui/toast"; +import { createId } from "../../utils/id"; +import { runPreRequestScript, runTests } from "../../utils/sandbox"; +import { resolveTemplate } from "../../utils/variables"; + +export const RequestWorkspace = ({ + scrollRef, +}: { + scrollRef: RefObject; +}) => { + const { + tabs, + activeTabId, + requests, + updateRequest, + markTabDirty, + openRequestInTab, + addHistory, + setResponse, + setSending, + isSending, + globals, + environments, + activeEnvironmentId, + } = useAppStore((state) => state); + const { notify } = useToast(); + const [controller, setController] = useState(null); + const urlHistory = useRef([]); + + const activeTab = useMemo( + () => tabs.find((tab) => tab.id === activeTabId) ?? tabs[0], + [tabs, activeTabId] + ); + const request = useMemo( + () => requests.find((item) => item.id === activeTab?.requestId) ?? requests[0], + [requests, activeTab] + ); + + useEffect(() => { + if (!activeTab && request) { + openRequestInTab(request.id); + } + }, [activeTab, openRequestInTab, request]); + + if (!request || !activeTab) return null; + + const env = environments.find((item) => item.id === activeEnvironmentId); + + const sendRequest = useCallback(async () => { + if (!request.url) { + notify({ title: "Missing URL", description: "Enter a valid URL.", variant: "error" }); + return; + } + const localVars = [{ id: createId(), key: "requestId", value: request.id, enabled: true }]; + const scope = { + globals, + environment: env, + local: localVars, + }; + + const current = { ...request }; + try { + setSending(true); + setResponse(undefined); + await runPreRequestScript(request.scripts.preRequest, { + globals, + environment: env?.variables ?? [], + variables: localVars, + }); + const prepared = buildRequest(request, scope); + const abortController = new AbortController(); + setController(abortController); + const response = await executeRequest(prepared, abortController); + const testResults = await runTests(request.scripts.tests, response.body); + response.testResults = testResults; + setResponse(response); + addHistory({ id: createId(), request: current, response, createdAt: Date.now() }); + urlHistory.current = Array.from(new Set([request.url, ...urlHistory.current])).slice(0, 10); + if (scrollRef.current) { + scrollRef.current.scrollTo({ top: 0, behavior: "smooth" }); + } + notify({ title: "Request completed", description: `${response.status} ${response.statusText}` }); + } catch (error) { + notify({ + title: "Request failed", + description: (error as Error).message, + variant: "error", + }); + } finally { + setSending(false); + setController(null); + } + }, [ + request, + globals, + env, + setSending, + setResponse, + addHistory, + notify, + scrollRef, + ]); + + useEffect(() => { + const handler = () => sendRequest(); + const cancelHandler = () => controller?.abort(); + document.addEventListener("send-request", handler); + document.addEventListener("cancel-request", cancelHandler); + return () => { + document.removeEventListener("send-request", handler); + document.removeEventListener("cancel-request", cancelHandler); + }; + }, [controller, sendRequest]); + + const handleUrlChange = (value: string) => { + updateRequest({ ...request, url: value }); + markTabDirty(activeTab.id, true); + }; + + const resolvedUrl = resolveTemplate(request.url, { + globals, + environment: env, + }); + + return ( +
+ +
+
+ { + updateRequest({ ...request, name: event.target.value }); + markTabDirty(activeTab.id, true); + }} + placeholder="Request name" + /> + + handleUrlChange(event.target.value)} + list="url-history" + placeholder="{{baseUrl}}/users" + /> + + {urlHistory.current.map((item) => ( + + + + +
+
+
+ Resolved URL: {resolvedUrl} + + Web requests are subject to CORS. Use desktop/agent proxy for unrestricted access. + +
+ +
+ ); +}; diff --git a/src/components/request/auth/AuthEditor.tsx b/src/components/request/auth/AuthEditor.tsx new file mode 100644 index 0000000..9e69266 --- /dev/null +++ b/src/components/request/auth/AuthEditor.tsx @@ -0,0 +1,87 @@ +import { RequestAuth } from "../../../types"; +import { Input } from "../../ui/input"; +import { Select } from "../../ui/select"; + +export const AuthEditor = ({ + auth, + onChange, +}: { + auth: RequestAuth; + onChange: (auth: RequestAuth) => void; +}) => { + return ( +
+
+ +
+ {auth.type === "bearer" && ( + onChange({ ...auth, token: event.target.value })} + /> + )} + {auth.type === "basic" && ( +
+ onChange({ ...auth, username: event.target.value })} + /> + onChange({ ...auth, password: event.target.value })} + /> +
+ )} + {auth.type === "apiKey" && ( +
+ onChange({ ...auth, key: event.target.value })} + /> + onChange({ ...auth, value: event.target.value })} + /> +
+ +
+
+ )} + {auth.type === "none" && ( +

No authentication will be attached.

+ )} +
+ ); +}; diff --git a/src/components/request/body/BodyEditor.tsx b/src/components/request/body/BodyEditor.tsx new file mode 100644 index 0000000..229d6c3 --- /dev/null +++ b/src/components/request/body/BodyEditor.tsx @@ -0,0 +1,108 @@ +import { FileText, FileUp, Brackets } from "lucide-react"; +import { RequestBody } from "../../../types"; +import { Tabs, Tab } from "../../ui/tabs"; +import { KeyValueTable } from "../KeyValueTable"; +import { Textarea } from "../../ui/textarea"; +import { Select } from "../../ui/select"; +import { Button } from "../../ui/button"; + +const bodyTabs = ["none", "form-data", "x-www-form-urlencoded", "raw"] as const; + +export const BodyEditor = ({ + body, + onChange, +}: { + body: RequestBody; + onChange: (body: RequestBody) => void; +}) => { + const currentTab = body.type; + + const handleTabChange = (next: (typeof bodyTabs)[number]) => { + if (next === "none") onChange({ type: "none" }); + if (next === "form-data") onChange({ type: "form-data", rows: [] }); + if (next === "x-www-form-urlencoded") onChange({ type: "x-www-form-urlencoded", rows: [] }); + if (next === "raw") onChange({ type: "raw", raw: { format: "json", value: "" } }); + }; + + return ( +
+ + {bodyTabs.map((tab) => ( + handleTabChange(tab)}> + {tab} + + ))} + + {body.type === "none" && ( +
+ No request body will be sent. +
+ )} + {body.type === "form-data" && ( +
+

Supports text + file placeholders.

+ onChange({ type: "form-data", rows })} + /> +
+ )} + {body.type === "x-www-form-urlencoded" && ( + onChange({ type: "x-www-form-urlencoded", rows })} + /> + )} + {body.type === "raw" && ( +
+
+ + + + Raw body + Upload (placeholder) + +
+