"use client"; import { useMemo, useState } from "react"; import Link from "next/link"; import { SignInButton, SignedIn, SignedOut, useAuth } from "@clerk/nextjs"; import { type ColumnDef, type SortingState, flexRender, getCoreRowModel, getSortedRowModel, useReactTable, } from "@tanstack/react-table"; import { useQueryClient } from "@tanstack/react-query"; import { DashboardSidebar } from "@/components/organisms/DashboardSidebar"; import { DashboardShell } from "@/components/templates/DashboardShell"; import { Button, buttonVariants } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { ApiError } from "@/api/mutator"; import { type listGatewaysApiV1GatewaysGetResponse, getListGatewaysApiV1GatewaysGetQueryKey, useDeleteGatewayApiV1GatewaysGatewayIdDelete, useListGatewaysApiV1GatewaysGet, } from "@/api/generated/gateways/gateways"; import type { GatewayRead } from "@/api/generated/model"; const truncate = (value?: string | null, max = 24) => { if (!value) return "—"; if (value.length <= max) return value; return `${value.slice(0, max)}…`; }; const formatTimestamp = (value?: string | null) => { if (!value) return "—"; const date = new Date(`${value}${value.endsWith("Z") ? "" : "Z"}`); if (Number.isNaN(date.getTime())) return "—"; return date.toLocaleString(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", }); }; export default function GatewaysPage() { const { isSignedIn } = useAuth(); const queryClient = useQueryClient(); const [sorting, setSorting] = useState([ { id: "name", desc: false }, ]); const [deleteTarget, setDeleteTarget] = useState(null); const gatewaysKey = getListGatewaysApiV1GatewaysGetQueryKey(); const gatewaysQuery = useListGatewaysApiV1GatewaysGet< listGatewaysApiV1GatewaysGetResponse, ApiError >(undefined, { query: { enabled: Boolean(isSignedIn), refetchInterval: 30_000, refetchOnMount: "always", }, }); const gateways = useMemo( () => gatewaysQuery.data?.status === 200 ? gatewaysQuery.data.data.items ?? [] : [], [gatewaysQuery.data] ); const sortedGateways = useMemo(() => [...gateways], [gateways]); const deleteMutation = useDeleteGatewayApiV1GatewaysGatewayIdDelete< ApiError, { previous?: listGatewaysApiV1GatewaysGetResponse } >( { mutation: { onMutate: async ({ gatewayId }) => { await queryClient.cancelQueries({ queryKey: gatewaysKey }); const previous = queryClient.getQueryData(gatewaysKey); if (previous && previous.status === 200) { const nextItems = previous.data.items.filter( (gateway) => gateway.id !== gatewayId ); const removedCount = previous.data.items.length - nextItems.length; queryClient.setQueryData(gatewaysKey, { ...previous, data: { ...previous.data, items: nextItems, total: Math.max(0, previous.data.total - removedCount), }, }); } return { previous }; }, onError: (_error, _gateway, context) => { if (context?.previous) { queryClient.setQueryData(gatewaysKey, context.previous); } }, onSuccess: () => { setDeleteTarget(null); }, onSettled: () => { queryClient.invalidateQueries({ queryKey: gatewaysKey }); }, }, }, queryClient ); const handleDelete = () => { if (!deleteTarget) return; deleteMutation.mutate({ gatewayId: deleteTarget.id }); }; const columns = useMemo[]>( () => [ { accessorKey: "name", header: "Gateway", cell: ({ row }) => (

{row.original.name}

{truncate(row.original.url, 36)}

), }, { accessorKey: "main_session_key", header: "Main session", cell: ({ row }) => ( {truncate(row.original.main_session_key, 24)} ), }, { accessorKey: "workspace_root", header: "Workspace root", cell: ({ row }) => ( {truncate(row.original.workspace_root, 28)} ), }, { accessorKey: "updated_at", header: "Updated", cell: ({ row }) => ( {formatTimestamp(row.original.updated_at)} ), }, { id: "actions", header: "", cell: ({ row }) => (
Edit
), }, ], [] ); // eslint-disable-next-line react-hooks/incompatible-library const table = useReactTable({ data: sortedGateways, columns, state: { sorting }, onSortingChange: setSorting, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), }); return (

Sign in to view gateways.

Gateways

Manage OpenClaw gateway connections used by boards

{gateways.length > 0 ? ( Create gateway ) : null}
{table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => ( ))} ))} {gatewaysQuery.isLoading ? ( ) : table.getRowModel().rows.length ? ( table.getRowModel().rows.map((row) => ( {row.getVisibleCells().map((cell) => ( ))} )) ) : ( )}
{header.isPlaceholder ? null : flexRender( header.column.columnDef.header, header.getContext() )}
Loading…
{flexRender( cell.column.columnDef.cell, cell.getContext() )}

No gateways yet

Create your first gateway to connect boards and start managing your OpenClaw connections.

Create your first gateway
{gatewaysQuery.error ? (

{gatewaysQuery.error.message}

) : null}
setDeleteTarget(null)}> Delete gateway? This removes the gateway connection from Mission Control. Boards using it will need a new gateway assigned. {deleteMutation.error ? (

{deleteMutation.error.message}

) : null}
); }