feat(gateway): refactor gateway form and connection check logic

This commit is contained in:
Abhimanyu Saharan
2026-02-08 23:26:57 +05:30
parent cdda147feb
commit 03317f0baf
4 changed files with 341 additions and 378 deletions

View File

@@ -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<string | null>(null);
const [gatewayCheckStatus, setGatewayCheckStatus] = useState<
"idle" | "checking" | "success" | "error"
>("idle");
const [gatewayCheckStatus, setGatewayCheckStatus] =
useState<GatewayCheckStatus>("idle");
const [gatewayCheckMessage, setGatewayCheckMessage] = useState<string | null>(
null,
);
@@ -147,36 +131,13 @@ export default function EditGatewayPage() {
if (!isSignedIn) return;
setGatewayCheckStatus("checking");
setGatewayCheckMessage(null);
try {
const params: Record<string, string> = {
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<HTMLFormElement>) => {
@@ -253,139 +214,45 @@ export default function EditGatewayPage() {
Only organization owners and admins can edit gateways.
</div>
) : (
<form
<GatewayForm
name={resolvedName}
gatewayUrl={resolvedGatewayUrl}
gatewayToken={resolvedGatewayToken}
mainSessionKey={resolvedMainSessionKey}
workspaceRoot={resolvedWorkspaceRoot}
gatewayUrlError={gatewayUrlError}
gatewayCheckStatus={gatewayCheckStatus}
gatewayCheckMessage={gatewayCheckMessage}
errorMessage={errorMessage}
isLoading={isLoading}
canSubmit={canSubmit}
mainSessionKeyPlaceholder={DEFAULT_MAIN_SESSION_KEY}
workspaceRootPlaceholder={DEFAULT_WORKSPACE_ROOT}
cancelLabel="Back"
submitLabel="Save changes"
submitBusyLabel="Saving…"
onSubmit={handleSubmit}
className="space-y-6 rounded-xl border border-slate-200 bg-white p-6 shadow-sm"
>
<div className="space-y-2">
<label className="text-sm font-medium text-slate-900">
Gateway name <span className="text-red-500">*</span>
</label>
<Input
value={resolvedName}
onChange={(event) => setName(event.target.value)}
placeholder="Primary gateway"
disabled={isLoading}
/>
</div>
<div className="grid gap-6 md:grid-cols-2">
<div className="space-y-2">
<label className="text-sm font-medium text-slate-900">
Gateway URL <span className="text-red-500">*</span>
</label>
<div className="relative">
<Input
value={resolvedGatewayUrl}
onChange={(event) => {
setGatewayUrl(event.target.value);
setGatewayUrlError(null);
setGatewayCheckStatus("idle");
setGatewayCheckMessage(null);
}}
onBlur={runGatewayCheck}
placeholder="ws://gateway:18789"
disabled={isLoading}
className={
gatewayUrlError ? "border-red-500" : undefined
}
/>
<button
type="button"
onClick={runGatewayCheck}
className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600"
aria-label="Check gateway connection"
>
{gatewayCheckStatus === "checking" ? (
<RefreshCcw className="h-4 w-4 animate-spin" />
) : gatewayCheckStatus === "success" ? (
<CheckCircle2 className="h-4 w-4 text-emerald-500" />
) : gatewayCheckStatus === "error" ? (
<XCircle className="h-4 w-4 text-red-500" />
) : (
<RefreshCcw className="h-4 w-4" />
)}
</button>
</div>
{gatewayUrlError ? (
<p className="text-xs text-red-500">{gatewayUrlError}</p>
) : gatewayCheckMessage ? (
<p
className={
gatewayCheckStatus === "success"
? "text-xs text-emerald-600"
: "text-xs text-red-500"
}
>
{gatewayCheckMessage}
</p>
) : null}
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-slate-900">
Gateway token
</label>
<Input
value={resolvedGatewayToken}
onChange={(event) => {
setGatewayToken(event.target.value);
setGatewayCheckStatus("idle");
setGatewayCheckMessage(null);
}}
onBlur={runGatewayCheck}
placeholder="Bearer token"
disabled={isLoading}
/>
</div>
</div>
<div className="grid gap-6 md:grid-cols-2">
<div className="space-y-2">
<label className="text-sm font-medium text-slate-900">
Main session key <span className="text-red-500">*</span>
</label>
<Input
value={resolvedMainSessionKey}
onChange={(event) => {
setMainSessionKey(event.target.value);
setGatewayCheckStatus("idle");
setGatewayCheckMessage(null);
}}
placeholder={DEFAULT_MAIN_SESSION_KEY}
disabled={isLoading}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-slate-900">
Workspace root <span className="text-red-500">*</span>
</label>
<Input
value={resolvedWorkspaceRoot}
onChange={(event) => setWorkspaceRoot(event.target.value)}
placeholder={DEFAULT_WORKSPACE_ROOT}
disabled={isLoading}
/>
</div>
</div>
{errorMessage ? (
<p className="text-sm text-red-500">{errorMessage}</p>
) : null}
<div className="flex justify-end gap-3">
<Button
type="button"
variant="ghost"
onClick={() => router.push("/gateways")}
disabled={isLoading}
>
Back
</Button>
<Button type="submit" disabled={isLoading || !canSubmit}>
{isLoading ? "Saving…" : "Save changes"}
</Button>
</div>
</form>
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}
/>
)}
</div>
</main>