From 03317f0bafff5a53af487813d6ac195d59f1e5ab Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Sun, 8 Feb 2026 23:26:57 +0530 Subject: [PATCH] feat(gateway): refactor gateway form and connection check logic --- .../app/gateways/[gatewayId]/edit/page.tsx | 243 ++++------------- frontend/src/app/gateways/new/page.tsx | 246 ++++-------------- .../src/components/gateways/GatewayForm.tsx | 174 +++++++++++++ frontend/src/lib/gateway-form.ts | 56 ++++ 4 files changed, 341 insertions(+), 378 deletions(-) create mode 100644 frontend/src/components/gateways/GatewayForm.tsx create mode 100644 frontend/src/lib/gateway-form.ts diff --git a/frontend/src/app/gateways/[gatewayId]/edit/page.tsx b/frontend/src/app/gateways/[gatewayId]/edit/page.tsx index 86a68a5..8fe9925 100644 --- a/frontend/src/app/gateways/[gatewayId]/edit/page.tsx +++ b/frontend/src/app/gateways/[gatewayId]/edit/page.tsx @@ -6,11 +6,9 @@ import { useState } from "react"; import { useParams, useRouter } from "next/navigation"; import { SignInButton, SignedIn, SignedOut, useAuth } from "@/auth/clerk"; -import { CheckCircle2, RefreshCcw, XCircle } from "lucide-react"; import { ApiError } from "@/api/mutator"; import { - gatewaysStatusApiV1GatewaysStatusGet, type getGatewayApiV1GatewaysGatewayIdGetResponse, useGetGatewayApiV1GatewaysGatewayIdGet, useUpdateGatewayApiV1GatewaysGatewayIdPatch, @@ -20,30 +18,17 @@ import { useGetMyMembershipApiV1OrganizationsMeMemberGet, } from "@/api/generated/organizations/organizations"; import type { GatewayUpdate } from "@/api/generated/model"; +import { GatewayForm } from "@/components/gateways/GatewayForm"; import { DashboardSidebar } from "@/components/organisms/DashboardSidebar"; import { DashboardShell } from "@/components/templates/DashboardShell"; import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; - -const DEFAULT_MAIN_SESSION_KEY = "agent:main:main"; -const DEFAULT_WORKSPACE_ROOT = "~/.openclaw"; - -const validateGatewayUrl = (value: string) => { - const trimmed = value.trim(); - if (!trimmed) return "Gateway URL is required."; - try { - const url = new URL(trimmed); - if (url.protocol !== "ws:" && url.protocol !== "wss:") { - return "Gateway URL must start with ws:// or wss://."; - } - if (!url.port) { - return "Gateway URL must include an explicit port."; - } - return null; - } catch { - return "Enter a valid gateway URL including port."; - } -}; +import { + DEFAULT_MAIN_SESSION_KEY, + DEFAULT_WORKSPACE_ROOT, + checkGatewayConnection, + type GatewayCheckStatus, + validateGatewayUrl, +} from "@/lib/gateway-form"; export default function EditGatewayPage() { const { isSignedIn } = useAuth(); @@ -81,9 +66,8 @@ export default function EditGatewayPage() { ); const [gatewayUrlError, setGatewayUrlError] = useState(null); - const [gatewayCheckStatus, setGatewayCheckStatus] = useState< - "idle" | "checking" | "success" | "error" - >("idle"); + const [gatewayCheckStatus, setGatewayCheckStatus] = + useState("idle"); const [gatewayCheckMessage, setGatewayCheckMessage] = useState( null, ); @@ -147,36 +131,13 @@ export default function EditGatewayPage() { if (!isSignedIn) return; setGatewayCheckStatus("checking"); setGatewayCheckMessage(null); - try { - const params: Record = { - gateway_url: resolvedGatewayUrl.trim(), - }; - if (resolvedGatewayToken.trim()) { - params.gateway_token = resolvedGatewayToken.trim(); - } - if (resolvedMainSessionKey.trim()) { - params.gateway_main_session_key = resolvedMainSessionKey.trim(); - } - const response = await gatewaysStatusApiV1GatewaysStatusGet(params); - if (response.status !== 200) { - setGatewayCheckStatus("error"); - setGatewayCheckMessage("Unable to reach gateway."); - return; - } - const data = response.data; - if (!data.connected) { - setGatewayCheckStatus("error"); - setGatewayCheckMessage(data.error ?? "Unable to reach gateway."); - return; - } - setGatewayCheckStatus("success"); - setGatewayCheckMessage("Gateway reachable."); - } catch (err) { - setGatewayCheckStatus("error"); - setGatewayCheckMessage( - err instanceof Error ? err.message : "Unable to reach gateway.", - ); - } + const { ok, message } = await checkGatewayConnection({ + gatewayUrl: resolvedGatewayUrl, + gatewayToken: resolvedGatewayToken, + mainSessionKey: resolvedMainSessionKey, + }); + setGatewayCheckStatus(ok ? "success" : "error"); + setGatewayCheckMessage(message); }; const handleSubmit = (event: React.FormEvent) => { @@ -253,139 +214,45 @@ export default function EditGatewayPage() { Only organization owners and admins can edit gateways. ) : ( -
-
- - setName(event.target.value)} - placeholder="Primary gateway" - disabled={isLoading} - /> -
- -
-
- -
- { - setGatewayUrl(event.target.value); - setGatewayUrlError(null); - setGatewayCheckStatus("idle"); - setGatewayCheckMessage(null); - }} - onBlur={runGatewayCheck} - placeholder="ws://gateway:18789" - disabled={isLoading} - className={ - gatewayUrlError ? "border-red-500" : undefined - } - /> - -
- {gatewayUrlError ? ( -

{gatewayUrlError}

- ) : gatewayCheckMessage ? ( -

- {gatewayCheckMessage} -

- ) : null} -
-
- - { - setGatewayToken(event.target.value); - setGatewayCheckStatus("idle"); - setGatewayCheckMessage(null); - }} - onBlur={runGatewayCheck} - placeholder="Bearer token" - disabled={isLoading} - /> -
-
- -
-
- - { - setMainSessionKey(event.target.value); - setGatewayCheckStatus("idle"); - setGatewayCheckMessage(null); - }} - placeholder={DEFAULT_MAIN_SESSION_KEY} - disabled={isLoading} - /> -
-
- - setWorkspaceRoot(event.target.value)} - placeholder={DEFAULT_WORKSPACE_ROOT} - disabled={isLoading} - /> -
-
- - {errorMessage ? ( -

{errorMessage}

- ) : null} - -
- - -
-
+ onCancel={() => router.push("/gateways")} + onRunGatewayCheck={runGatewayCheck} + onNameChange={setName} + onGatewayUrlChange={(next) => { + setGatewayUrl(next); + setGatewayUrlError(null); + setGatewayCheckStatus("idle"); + setGatewayCheckMessage(null); + }} + onGatewayTokenChange={(next) => { + setGatewayToken(next); + setGatewayCheckStatus("idle"); + setGatewayCheckMessage(null); + }} + onMainSessionKeyChange={(next) => { + setMainSessionKey(next); + setGatewayCheckStatus("idle"); + setGatewayCheckMessage(null); + }} + onWorkspaceRootChange={setWorkspaceRoot} + /> )} diff --git a/frontend/src/app/gateways/new/page.tsx b/frontend/src/app/gateways/new/page.tsx index 25949b6..5d339ec 100644 --- a/frontend/src/app/gateways/new/page.tsx +++ b/frontend/src/app/gateways/new/page.tsx @@ -6,41 +6,24 @@ import { useState } from "react"; import { useRouter } from "next/navigation"; import { SignInButton, SignedIn, SignedOut, useAuth } from "@/auth/clerk"; -import { CheckCircle2, RefreshCcw, XCircle } from "lucide-react"; import { ApiError } from "@/api/mutator"; -import { - gatewaysStatusApiV1GatewaysStatusGet, - useCreateGatewayApiV1GatewaysPost, -} from "@/api/generated/gateways/gateways"; +import { useCreateGatewayApiV1GatewaysPost } from "@/api/generated/gateways/gateways"; import { type getMyMembershipApiV1OrganizationsMeMemberGetResponse, useGetMyMembershipApiV1OrganizationsMeMemberGet, } from "@/api/generated/organizations/organizations"; +import { GatewayForm } from "@/components/gateways/GatewayForm"; import { DashboardSidebar } from "@/components/organisms/DashboardSidebar"; import { DashboardShell } from "@/components/templates/DashboardShell"; import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; - -const DEFAULT_MAIN_SESSION_KEY = "agent:main:main"; -const DEFAULT_WORKSPACE_ROOT = "~/.openclaw"; - -const validateGatewayUrl = (value: string) => { - const trimmed = value.trim(); - if (!trimmed) return "Gateway URL is required."; - try { - const url = new URL(trimmed); - if (url.protocol !== "ws:" && url.protocol !== "wss:") { - return "Gateway URL must start with ws:// or wss://."; - } - if (!url.port) { - return "Gateway URL must include an explicit port."; - } - return null; - } catch { - return "Enter a valid gateway URL including port."; - } -}; +import { + DEFAULT_MAIN_SESSION_KEY, + DEFAULT_WORKSPACE_ROOT, + checkGatewayConnection, + type GatewayCheckStatus, + validateGatewayUrl, +} from "@/lib/gateway-form"; export default function NewGatewayPage() { const { isSignedIn } = useAuth(); @@ -69,9 +52,8 @@ export default function NewGatewayPage() { const [workspaceRoot, setWorkspaceRoot] = useState(DEFAULT_WORKSPACE_ROOT); const [gatewayUrlError, setGatewayUrlError] = useState(null); - const [gatewayCheckStatus, setGatewayCheckStatus] = useState< - "idle" | "checking" | "success" | "error" - >("idle"); + const [gatewayCheckStatus, setGatewayCheckStatus] = + useState("idle"); const [gatewayCheckMessage, setGatewayCheckMessage] = useState( null, ); @@ -111,37 +93,13 @@ export default function NewGatewayPage() { if (!isSignedIn) return; setGatewayCheckStatus("checking"); setGatewayCheckMessage(null); - try { - const params: Record = { - gateway_url: gatewayUrl.trim(), - }; - if (gatewayToken.trim()) { - params.gateway_token = gatewayToken.trim(); - } - if (mainSessionKey.trim()) { - params.gateway_main_session_key = mainSessionKey.trim(); - } - - const response = await gatewaysStatusApiV1GatewaysStatusGet(params); - if (response.status !== 200) { - setGatewayCheckStatus("error"); - setGatewayCheckMessage("Unable to reach gateway."); - return; - } - const data = response.data; - if (!data.connected) { - setGatewayCheckStatus("error"); - setGatewayCheckMessage(data.error ?? "Unable to reach gateway."); - return; - } - setGatewayCheckStatus("success"); - setGatewayCheckMessage("Gateway reachable."); - } catch (err) { - setGatewayCheckStatus("error"); - setGatewayCheckMessage( - err instanceof Error ? err.message : "Unable to reach gateway.", - ); - } + const { ok, message } = await checkGatewayConnection({ + gatewayUrl, + gatewayToken, + mainSessionKey, + }); + setGatewayCheckStatus(ok ? "success" : "error"); + setGatewayCheckMessage(message); }; const handleSubmit = (event: React.FormEvent) => { @@ -214,137 +172,45 @@ export default function NewGatewayPage() { Only organization owners and admins can create gateways. ) : ( -
-
- - setName(event.target.value)} - placeholder="Primary gateway" - disabled={isLoading} - /> -
- -
-
- -
- { - setGatewayUrl(event.target.value); - setGatewayUrlError(null); - setGatewayCheckStatus("idle"); - setGatewayCheckMessage(null); - }} - onBlur={runGatewayCheck} - placeholder="ws://gateway:18789" - disabled={isLoading} - className={ - gatewayUrlError ? "border-red-500" : undefined - } - /> - -
- {gatewayUrlError ? ( -

{gatewayUrlError}

- ) : gatewayCheckMessage ? ( -

- {gatewayCheckMessage} -

- ) : null} -
-
- - { - setGatewayToken(event.target.value); - setGatewayCheckStatus("idle"); - setGatewayCheckMessage(null); - }} - onBlur={runGatewayCheck} - placeholder="Bearer token" - disabled={isLoading} - /> -
-
- -
-
- - { - setMainSessionKey(event.target.value); - setGatewayCheckStatus("idle"); - setGatewayCheckMessage(null); - }} - placeholder={DEFAULT_MAIN_SESSION_KEY} - disabled={isLoading} - /> -
-
- - setWorkspaceRoot(event.target.value)} - placeholder={DEFAULT_WORKSPACE_ROOT} - disabled={isLoading} - /> -
-
- - {error ?

{error}

: null} - -
- - -
-
+ onCancel={() => router.push("/gateways")} + onRunGatewayCheck={runGatewayCheck} + onNameChange={setName} + onGatewayUrlChange={(next) => { + setGatewayUrl(next); + setGatewayUrlError(null); + setGatewayCheckStatus("idle"); + setGatewayCheckMessage(null); + }} + onGatewayTokenChange={(next) => { + setGatewayToken(next); + setGatewayCheckStatus("idle"); + setGatewayCheckMessage(null); + }} + onMainSessionKeyChange={(next) => { + setMainSessionKey(next); + setGatewayCheckStatus("idle"); + setGatewayCheckMessage(null); + }} + onWorkspaceRootChange={setWorkspaceRoot} + /> )} diff --git a/frontend/src/components/gateways/GatewayForm.tsx b/frontend/src/components/gateways/GatewayForm.tsx new file mode 100644 index 0000000..bba3f82 --- /dev/null +++ b/frontend/src/components/gateways/GatewayForm.tsx @@ -0,0 +1,174 @@ +import type { FormEvent } from "react"; +import { CheckCircle2, RefreshCcw, XCircle } from "lucide-react"; + +import type { GatewayCheckStatus } from "@/lib/gateway-form"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; + +type GatewayFormProps = { + name: string; + gatewayUrl: string; + gatewayToken: string; + mainSessionKey: string; + workspaceRoot: string; + gatewayUrlError: string | null; + gatewayCheckStatus: GatewayCheckStatus; + gatewayCheckMessage: string | null; + errorMessage: string | null; + isLoading: boolean; + canSubmit: boolean; + mainSessionKeyPlaceholder: string; + workspaceRootPlaceholder: string; + cancelLabel: string; + submitLabel: string; + submitBusyLabel: string; + onSubmit: (event: FormEvent) => void; + onCancel: () => void; + onRunGatewayCheck: () => Promise; + onNameChange: (next: string) => void; + onGatewayUrlChange: (next: string) => void; + onGatewayTokenChange: (next: string) => void; + onMainSessionKeyChange: (next: string) => void; + onWorkspaceRootChange: (next: string) => void; +}; + +export function GatewayForm({ + name, + gatewayUrl, + gatewayToken, + mainSessionKey, + workspaceRoot, + gatewayUrlError, + gatewayCheckStatus, + gatewayCheckMessage, + errorMessage, + isLoading, + canSubmit, + mainSessionKeyPlaceholder, + workspaceRootPlaceholder, + cancelLabel, + submitLabel, + submitBusyLabel, + onSubmit, + onCancel, + onRunGatewayCheck, + onNameChange, + onGatewayUrlChange, + onGatewayTokenChange, + onMainSessionKeyChange, + onWorkspaceRootChange, +}: GatewayFormProps) { + return ( +
+
+ + onNameChange(event.target.value)} + placeholder="Primary gateway" + disabled={isLoading} + /> +
+ +
+
+ +
+ onGatewayUrlChange(event.target.value)} + onBlur={onRunGatewayCheck} + placeholder="ws://gateway:18789" + disabled={isLoading} + className={gatewayUrlError ? "border-red-500" : undefined} + /> + +
+ {gatewayUrlError ? ( +

{gatewayUrlError}

+ ) : gatewayCheckMessage ? ( +

+ {gatewayCheckMessage} +

+ ) : null} +
+
+ + onGatewayTokenChange(event.target.value)} + onBlur={onRunGatewayCheck} + placeholder="Bearer token" + disabled={isLoading} + /> +
+
+ +
+
+ + onMainSessionKeyChange(event.target.value)} + placeholder={mainSessionKeyPlaceholder} + disabled={isLoading} + /> +
+
+ + onWorkspaceRootChange(event.target.value)} + placeholder={workspaceRootPlaceholder} + disabled={isLoading} + /> +
+
+ + {errorMessage ?

{errorMessage}

: null} + +
+ + +
+
+ ); +} diff --git a/frontend/src/lib/gateway-form.ts b/frontend/src/lib/gateway-form.ts new file mode 100644 index 0000000..3fc26ee --- /dev/null +++ b/frontend/src/lib/gateway-form.ts @@ -0,0 +1,56 @@ +import { gatewaysStatusApiV1GatewaysStatusGet } from "@/api/generated/gateways/gateways"; + +export const DEFAULT_MAIN_SESSION_KEY = "agent:main:main"; +export const DEFAULT_WORKSPACE_ROOT = "~/.openclaw"; + +export type GatewayCheckStatus = "idle" | "checking" | "success" | "error"; + +export const validateGatewayUrl = (value: string) => { + const trimmed = value.trim(); + if (!trimmed) return "Gateway URL is required."; + try { + const url = new URL(trimmed); + if (url.protocol !== "ws:" && url.protocol !== "wss:") { + return "Gateway URL must start with ws:// or wss://."; + } + if (!url.port) { + return "Gateway URL must include an explicit port."; + } + return null; + } catch { + return "Enter a valid gateway URL including port."; + } +}; + +export async function checkGatewayConnection(params: { + gatewayUrl: string; + gatewayToken: string; + mainSessionKey: string; +}): Promise<{ ok: boolean; message: string }> { + try { + const requestParams: Record = { + gateway_url: params.gatewayUrl.trim(), + }; + if (params.gatewayToken.trim()) { + requestParams.gateway_token = params.gatewayToken.trim(); + } + if (params.mainSessionKey.trim()) { + requestParams.gateway_main_session_key = params.mainSessionKey.trim(); + } + + const response = await gatewaysStatusApiV1GatewaysStatusGet(requestParams); + if (response.status !== 200) { + return { ok: false, message: "Unable to reach gateway." }; + } + const data = response.data; + if (!data.connected) { + return { ok: false, message: data.error ?? "Unable to reach gateway." }; + } + return { ok: true, message: "Gateway reachable." }; + } catch (error) { + return { + ok: false, + message: error instanceof Error ? error.message : "Unable to reach gateway.", + }; + } +}