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 { useParams, useRouter } from "next/navigation";
import { SignInButton, SignedIn, SignedOut, useAuth } from "@/auth/clerk"; import { SignInButton, SignedIn, SignedOut, useAuth } from "@/auth/clerk";
import { CheckCircle2, RefreshCcw, XCircle } from "lucide-react";
import { ApiError } from "@/api/mutator"; import { ApiError } from "@/api/mutator";
import { import {
gatewaysStatusApiV1GatewaysStatusGet,
type getGatewayApiV1GatewaysGatewayIdGetResponse, type getGatewayApiV1GatewaysGatewayIdGetResponse,
useGetGatewayApiV1GatewaysGatewayIdGet, useGetGatewayApiV1GatewaysGatewayIdGet,
useUpdateGatewayApiV1GatewaysGatewayIdPatch, useUpdateGatewayApiV1GatewaysGatewayIdPatch,
@@ -20,30 +18,17 @@ import {
useGetMyMembershipApiV1OrganizationsMeMemberGet, useGetMyMembershipApiV1OrganizationsMeMemberGet,
} from "@/api/generated/organizations/organizations"; } from "@/api/generated/organizations/organizations";
import type { GatewayUpdate } from "@/api/generated/model"; import type { GatewayUpdate } from "@/api/generated/model";
import { GatewayForm } from "@/components/gateways/GatewayForm";
import { DashboardSidebar } from "@/components/organisms/DashboardSidebar"; import { DashboardSidebar } from "@/components/organisms/DashboardSidebar";
import { DashboardShell } from "@/components/templates/DashboardShell"; import { DashboardShell } from "@/components/templates/DashboardShell";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import {
DEFAULT_MAIN_SESSION_KEY,
const DEFAULT_MAIN_SESSION_KEY = "agent:main:main"; DEFAULT_WORKSPACE_ROOT,
const DEFAULT_WORKSPACE_ROOT = "~/.openclaw"; checkGatewayConnection,
type GatewayCheckStatus,
const validateGatewayUrl = (value: string) => { validateGatewayUrl,
const trimmed = value.trim(); } from "@/lib/gateway-form";
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 default function EditGatewayPage() { export default function EditGatewayPage() {
const { isSignedIn } = useAuth(); const { isSignedIn } = useAuth();
@@ -81,9 +66,8 @@ export default function EditGatewayPage() {
); );
const [gatewayUrlError, setGatewayUrlError] = useState<string | null>(null); const [gatewayUrlError, setGatewayUrlError] = useState<string | null>(null);
const [gatewayCheckStatus, setGatewayCheckStatus] = useState< const [gatewayCheckStatus, setGatewayCheckStatus] =
"idle" | "checking" | "success" | "error" useState<GatewayCheckStatus>("idle");
>("idle");
const [gatewayCheckMessage, setGatewayCheckMessage] = useState<string | null>( const [gatewayCheckMessage, setGatewayCheckMessage] = useState<string | null>(
null, null,
); );
@@ -147,36 +131,13 @@ export default function EditGatewayPage() {
if (!isSignedIn) return; if (!isSignedIn) return;
setGatewayCheckStatus("checking"); setGatewayCheckStatus("checking");
setGatewayCheckMessage(null); setGatewayCheckMessage(null);
try { const { ok, message } = await checkGatewayConnection({
const params: Record<string, string> = { gatewayUrl: resolvedGatewayUrl,
gateway_url: resolvedGatewayUrl.trim(), gatewayToken: resolvedGatewayToken,
}; mainSessionKey: resolvedMainSessionKey,
if (resolvedGatewayToken.trim()) { });
params.gateway_token = resolvedGatewayToken.trim(); setGatewayCheckStatus(ok ? "success" : "error");
} setGatewayCheckMessage(message);
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 handleSubmit = (event: React.FormEvent<HTMLFormElement>) => { const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
@@ -253,139 +214,45 @@ export default function EditGatewayPage() {
Only organization owners and admins can edit gateways. Only organization owners and admins can edit gateways.
</div> </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} onSubmit={handleSubmit}
className="space-y-6 rounded-xl border border-slate-200 bg-white p-6 shadow-sm" onCancel={() => router.push("/gateways")}
> onRunGatewayCheck={runGatewayCheck}
<div className="space-y-2"> onNameChange={setName}
<label className="text-sm font-medium text-slate-900"> onGatewayUrlChange={(next) => {
Gateway name <span className="text-red-500">*</span> setGatewayUrl(next);
</label> setGatewayUrlError(null);
<Input setGatewayCheckStatus("idle");
value={resolvedName} setGatewayCheckMessage(null);
onChange={(event) => setName(event.target.value)} }}
placeholder="Primary gateway" onGatewayTokenChange={(next) => {
disabled={isLoading} setGatewayToken(next);
/> setGatewayCheckStatus("idle");
</div> setGatewayCheckMessage(null);
}}
<div className="grid gap-6 md:grid-cols-2"> onMainSessionKeyChange={(next) => {
<div className="space-y-2"> setMainSessionKey(next);
<label className="text-sm font-medium text-slate-900"> setGatewayCheckStatus("idle");
Gateway URL <span className="text-red-500">*</span> setGatewayCheckMessage(null);
</label> }}
<div className="relative"> onWorkspaceRootChange={setWorkspaceRoot}
<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>
)} )}
</div> </div>
</main> </main>

View File

@@ -6,41 +6,24 @@ import { useState } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { SignInButton, SignedIn, SignedOut, useAuth } from "@/auth/clerk"; import { SignInButton, SignedIn, SignedOut, useAuth } from "@/auth/clerk";
import { CheckCircle2, RefreshCcw, XCircle } from "lucide-react";
import { ApiError } from "@/api/mutator"; import { ApiError } from "@/api/mutator";
import { import { useCreateGatewayApiV1GatewaysPost } from "@/api/generated/gateways/gateways";
gatewaysStatusApiV1GatewaysStatusGet,
useCreateGatewayApiV1GatewaysPost,
} from "@/api/generated/gateways/gateways";
import { import {
type getMyMembershipApiV1OrganizationsMeMemberGetResponse, type getMyMembershipApiV1OrganizationsMeMemberGetResponse,
useGetMyMembershipApiV1OrganizationsMeMemberGet, useGetMyMembershipApiV1OrganizationsMeMemberGet,
} from "@/api/generated/organizations/organizations"; } from "@/api/generated/organizations/organizations";
import { GatewayForm } from "@/components/gateways/GatewayForm";
import { DashboardSidebar } from "@/components/organisms/DashboardSidebar"; import { DashboardSidebar } from "@/components/organisms/DashboardSidebar";
import { DashboardShell } from "@/components/templates/DashboardShell"; import { DashboardShell } from "@/components/templates/DashboardShell";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import {
DEFAULT_MAIN_SESSION_KEY,
const DEFAULT_MAIN_SESSION_KEY = "agent:main:main"; DEFAULT_WORKSPACE_ROOT,
const DEFAULT_WORKSPACE_ROOT = "~/.openclaw"; checkGatewayConnection,
type GatewayCheckStatus,
const validateGatewayUrl = (value: string) => { validateGatewayUrl,
const trimmed = value.trim(); } from "@/lib/gateway-form";
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 default function NewGatewayPage() { export default function NewGatewayPage() {
const { isSignedIn } = useAuth(); const { isSignedIn } = useAuth();
@@ -69,9 +52,8 @@ export default function NewGatewayPage() {
const [workspaceRoot, setWorkspaceRoot] = useState(DEFAULT_WORKSPACE_ROOT); const [workspaceRoot, setWorkspaceRoot] = useState(DEFAULT_WORKSPACE_ROOT);
const [gatewayUrlError, setGatewayUrlError] = useState<string | null>(null); const [gatewayUrlError, setGatewayUrlError] = useState<string | null>(null);
const [gatewayCheckStatus, setGatewayCheckStatus] = useState< const [gatewayCheckStatus, setGatewayCheckStatus] =
"idle" | "checking" | "success" | "error" useState<GatewayCheckStatus>("idle");
>("idle");
const [gatewayCheckMessage, setGatewayCheckMessage] = useState<string | null>( const [gatewayCheckMessage, setGatewayCheckMessage] = useState<string | null>(
null, null,
); );
@@ -111,37 +93,13 @@ export default function NewGatewayPage() {
if (!isSignedIn) return; if (!isSignedIn) return;
setGatewayCheckStatus("checking"); setGatewayCheckStatus("checking");
setGatewayCheckMessage(null); setGatewayCheckMessage(null);
try { const { ok, message } = await checkGatewayConnection({
const params: Record<string, string> = { gatewayUrl,
gateway_url: gatewayUrl.trim(), gatewayToken,
}; mainSessionKey,
if (gatewayToken.trim()) { });
params.gateway_token = gatewayToken.trim(); setGatewayCheckStatus(ok ? "success" : "error");
} setGatewayCheckMessage(message);
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 handleSubmit = (event: React.FormEvent<HTMLFormElement>) => { const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
@@ -214,137 +172,45 @@ export default function NewGatewayPage() {
Only organization owners and admins can create gateways. Only organization owners and admins can create gateways.
</div> </div>
) : ( ) : (
<form <GatewayForm
name={name}
gatewayUrl={gatewayUrl}
gatewayToken={gatewayToken}
mainSessionKey={mainSessionKey}
workspaceRoot={workspaceRoot}
gatewayUrlError={gatewayUrlError}
gatewayCheckStatus={gatewayCheckStatus}
gatewayCheckMessage={gatewayCheckMessage}
errorMessage={error}
isLoading={isLoading}
canSubmit={canSubmit}
mainSessionKeyPlaceholder={DEFAULT_MAIN_SESSION_KEY}
workspaceRootPlaceholder={DEFAULT_WORKSPACE_ROOT}
cancelLabel="Cancel"
submitLabel="Create gateway"
submitBusyLabel="Creating…"
onSubmit={handleSubmit} onSubmit={handleSubmit}
className="space-y-6 rounded-xl border border-slate-200 bg-white p-6 shadow-sm" onCancel={() => router.push("/gateways")}
> onRunGatewayCheck={runGatewayCheck}
<div className="space-y-2"> onNameChange={setName}
<label className="text-sm font-medium text-slate-900"> onGatewayUrlChange={(next) => {
Gateway name <span className="text-red-500">*</span> setGatewayUrl(next);
</label> setGatewayUrlError(null);
<Input setGatewayCheckStatus("idle");
value={name} setGatewayCheckMessage(null);
onChange={(event) => setName(event.target.value)} }}
placeholder="Primary gateway" onGatewayTokenChange={(next) => {
disabled={isLoading} setGatewayToken(next);
/> setGatewayCheckStatus("idle");
</div> setGatewayCheckMessage(null);
}}
<div className="grid gap-6 md:grid-cols-2"> onMainSessionKeyChange={(next) => {
<div className="space-y-2"> setMainSessionKey(next);
<label className="text-sm font-medium text-slate-900"> setGatewayCheckStatus("idle");
Gateway URL <span className="text-red-500">*</span> setGatewayCheckMessage(null);
</label> }}
<div className="relative"> onWorkspaceRootChange={setWorkspaceRoot}
<Input />
value={gatewayUrl}
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={gatewayToken}
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={mainSessionKey}
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={workspaceRoot}
onChange={(event) => setWorkspaceRoot(event.target.value)}
placeholder={DEFAULT_WORKSPACE_ROOT}
disabled={isLoading}
/>
</div>
</div>
{error ? <p className="text-sm text-red-500">{error}</p> : null}
<div className="flex justify-end gap-3">
<Button
type="button"
variant="ghost"
onClick={() => router.push("/gateways")}
disabled={isLoading}
>
Cancel
</Button>
<Button type="submit" disabled={isLoading || !canSubmit}>
{isLoading ? "Creating…" : "Create gateway"}
</Button>
</div>
</form>
)} )}
</div> </div>
</main> </main>

View File

@@ -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<HTMLFormElement>) => void;
onCancel: () => void;
onRunGatewayCheck: () => Promise<void>;
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 (
<form
onSubmit={onSubmit}
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={name}
onChange={(event) => onNameChange(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={gatewayUrl}
onChange={(event) => onGatewayUrlChange(event.target.value)}
onBlur={onRunGatewayCheck}
placeholder="ws://gateway:18789"
disabled={isLoading}
className={gatewayUrlError ? "border-red-500" : undefined}
/>
<button
type="button"
onClick={() => void onRunGatewayCheck()}
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={gatewayToken}
onChange={(event) => onGatewayTokenChange(event.target.value)}
onBlur={onRunGatewayCheck}
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={mainSessionKey}
onChange={(event) => onMainSessionKeyChange(event.target.value)}
placeholder={mainSessionKeyPlaceholder}
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={workspaceRoot}
onChange={(event) => onWorkspaceRootChange(event.target.value)}
placeholder={workspaceRootPlaceholder}
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={onCancel} disabled={isLoading}>
{cancelLabel}
</Button>
<Button type="submit" disabled={isLoading || !canSubmit}>
{isLoading ? submitBusyLabel : submitLabel}
</Button>
</div>
</form>
);
}

View File

@@ -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<string, string> = {
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.",
};
}
}