Files
mission-control/frontend/src/app/gateways/[gatewayId]/edit/page.tsx

263 lines
9.0 KiB
TypeScript

"use client";
export const dynamic = "force-dynamic";
import { useState } from "react";
import { useParams, useRouter } from "next/navigation";
import { SignInButton, SignedIn, SignedOut, useAuth } from "@/auth/clerk";
import { ApiError } from "@/api/mutator";
import {
type getGatewayApiV1GatewaysGatewayIdGetResponse,
useGetGatewayApiV1GatewaysGatewayIdGet,
useUpdateGatewayApiV1GatewaysGatewayIdPatch,
} from "@/api/generated/gateways/gateways";
import {
type getMyMembershipApiV1OrganizationsMeMemberGetResponse,
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 {
DEFAULT_MAIN_SESSION_KEY,
DEFAULT_WORKSPACE_ROOT,
checkGatewayConnection,
type GatewayCheckStatus,
validateGatewayUrl,
} from "@/lib/gateway-form";
export default function EditGatewayPage() {
const { isSignedIn } = useAuth();
const router = useRouter();
const params = useParams();
const gatewayIdParam = params?.gatewayId;
const gatewayId = Array.isArray(gatewayIdParam)
? gatewayIdParam[0]
: gatewayIdParam;
const membershipQuery = useGetMyMembershipApiV1OrganizationsMeMemberGet<
getMyMembershipApiV1OrganizationsMeMemberGetResponse,
ApiError
>({
query: {
enabled: Boolean(isSignedIn),
refetchOnMount: "always",
retry: false,
},
});
const member =
membershipQuery.data?.status === 200 ? membershipQuery.data.data : null;
const isAdmin = member ? ["owner", "admin"].includes(member.role) : false;
const [name, setName] = useState<string | undefined>(undefined);
const [gatewayUrl, setGatewayUrl] = useState<string | undefined>(undefined);
const [gatewayToken, setGatewayToken] = useState<string | undefined>(
undefined,
);
const [mainSessionKey, setMainSessionKey] = useState<string | undefined>(
undefined,
);
const [workspaceRoot, setWorkspaceRoot] = useState<string | undefined>(
undefined,
);
const [gatewayUrlError, setGatewayUrlError] = useState<string | null>(null);
const [gatewayCheckStatus, setGatewayCheckStatus] =
useState<GatewayCheckStatus>("idle");
const [gatewayCheckMessage, setGatewayCheckMessage] = useState<string | null>(
null,
);
const [error, setError] = useState<string | null>(null);
const gatewayQuery = useGetGatewayApiV1GatewaysGatewayIdGet<
getGatewayApiV1GatewaysGatewayIdGetResponse,
ApiError
>(gatewayId ?? "", {
query: {
enabled: Boolean(isSignedIn && isAdmin && gatewayId),
refetchOnMount: "always",
retry: false,
},
});
const updateMutation = useUpdateGatewayApiV1GatewaysGatewayIdPatch<ApiError>({
mutation: {
onSuccess: (result) => {
if (result.status === 200) {
router.push(`/gateways/${result.data.id}`);
}
},
onError: (err) => {
setError(err.message || "Something went wrong.");
},
},
});
const loadedGateway =
gatewayQuery.data?.status === 200 ? gatewayQuery.data.data : null;
const resolvedName = name ?? loadedGateway?.name ?? "";
const resolvedGatewayUrl = gatewayUrl ?? loadedGateway?.url ?? "";
const resolvedGatewayToken = gatewayToken ?? loadedGateway?.token ?? "";
const resolvedMainSessionKey =
mainSessionKey ??
loadedGateway?.main_session_key ??
DEFAULT_MAIN_SESSION_KEY;
const resolvedWorkspaceRoot =
workspaceRoot ?? loadedGateway?.workspace_root ?? DEFAULT_WORKSPACE_ROOT;
const isLoading = gatewayQuery.isLoading || updateMutation.isPending;
const errorMessage = error ?? gatewayQuery.error?.message ?? null;
const canSubmit =
Boolean(resolvedName.trim()) &&
Boolean(resolvedGatewayUrl.trim()) &&
Boolean(resolvedMainSessionKey.trim()) &&
Boolean(resolvedWorkspaceRoot.trim()) &&
gatewayCheckStatus === "success";
const runGatewayCheck = async () => {
const validationError = validateGatewayUrl(resolvedGatewayUrl);
setGatewayUrlError(validationError);
if (validationError) {
setGatewayCheckStatus("error");
setGatewayCheckMessage(validationError);
return;
}
if (!isSignedIn) return;
setGatewayCheckStatus("checking");
setGatewayCheckMessage(null);
const { ok, message } = await checkGatewayConnection({
gatewayUrl: resolvedGatewayUrl,
gatewayToken: resolvedGatewayToken,
mainSessionKey: resolvedMainSessionKey,
});
setGatewayCheckStatus(ok ? "success" : "error");
setGatewayCheckMessage(message);
};
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (!isSignedIn || !gatewayId) return;
if (!resolvedName.trim()) {
setError("Gateway name is required.");
return;
}
const gatewayValidation = validateGatewayUrl(resolvedGatewayUrl);
setGatewayUrlError(gatewayValidation);
if (gatewayValidation) {
setGatewayCheckStatus("error");
setGatewayCheckMessage(gatewayValidation);
return;
}
if (!resolvedMainSessionKey.trim()) {
setError("Main session key is required.");
return;
}
if (!resolvedWorkspaceRoot.trim()) {
setError("Workspace root is required.");
return;
}
setError(null);
const payload: GatewayUpdate = {
name: resolvedName.trim(),
url: resolvedGatewayUrl.trim(),
token: resolvedGatewayToken.trim() || null,
main_session_key: resolvedMainSessionKey.trim(),
workspace_root: resolvedWorkspaceRoot.trim(),
};
updateMutation.mutate({ gatewayId, data: payload });
};
return (
<DashboardShell>
<SignedOut>
<div className="col-span-2 flex min-h-[calc(100vh-64px)] items-center justify-center bg-slate-50 p-10 text-center">
<div className="rounded-xl border border-slate-200 bg-white px-8 py-6 shadow-sm">
<p className="text-sm text-slate-600">Sign in to edit a gateway.</p>
<SignInButton
mode="modal"
forceRedirectUrl={`/gateways/${gatewayId}/edit`}
>
<Button className="mt-4">Sign in</Button>
</SignInButton>
</div>
</div>
</SignedOut>
<SignedIn>
<DashboardSidebar />
<main className="flex-1 overflow-y-auto bg-slate-50">
<div className="border-b border-slate-200 bg-white px-8 py-6">
<div>
<h1 className="font-heading text-2xl font-semibold text-slate-900 tracking-tight">
{resolvedName.trim()
? `Edit gateway — ${resolvedName.trim()}`
: "Edit gateway"}
</h1>
<p className="mt-1 text-sm text-slate-500">
Update connection settings for this OpenClaw gateway.
</p>
</div>
</div>
<div className="p-8">
{!isAdmin ? (
<div className="rounded-xl border border-slate-200 bg-white px-6 py-5 text-sm text-slate-600 shadow-sm">
Only organization owners and admins can edit gateways.
</div>
) : (
<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}
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>
</SignedIn>
</DashboardShell>
);
}