Files
mission-control/frontend/src/app/gateways/page.tsx

161 lines
4.9 KiB
TypeScript

"use client";
export const dynamic = "force-dynamic";
import { useMemo, useState } from "react";
import Link from "next/link";
import { useAuth } from "@/auth/clerk";
import { useQueryClient } from "@tanstack/react-query";
import { GatewaysTable } from "@/components/gateways/GatewaysTable";
import { DashboardPageLayout } from "@/components/templates/DashboardPageLayout";
import { buttonVariants } from "@/components/ui/button";
import { ConfirmActionDialog } from "@/components/ui/confirm-action-dialog";
import { ApiError } from "@/api/mutator";
import {
type listGatewaysApiV1GatewaysGetResponse,
getListGatewaysApiV1GatewaysGetQueryKey,
useDeleteGatewayApiV1GatewaysGatewayIdDelete,
useListGatewaysApiV1GatewaysGet,
} from "@/api/generated/gateways/gateways";
import { createOptimisticListDeleteMutation } from "@/lib/list-delete";
import { useOrganizationMembership } from "@/lib/use-organization-membership";
import type { GatewayRead } from "@/api/generated/model";
import { useUrlSorting } from "@/lib/use-url-sorting";
const GATEWAY_SORTABLE_COLUMNS = ["name", "workspace_root", "updated_at"];
export default function GatewaysPage() {
const { isSignedIn } = useAuth();
const queryClient = useQueryClient();
const { sorting, onSortingChange } = useUrlSorting({
allowedColumnIds: GATEWAY_SORTABLE_COLUMNS,
defaultSorting: [{ id: "name", desc: false }],
paramPrefix: "gateways",
});
const { isAdmin } = useOrganizationMembership(isSignedIn);
const [deleteTarget, setDeleteTarget] = useState<GatewayRead | null>(null);
const gatewaysKey = getListGatewaysApiV1GatewaysGetQueryKey();
const gatewaysQuery = useListGatewaysApiV1GatewaysGet<
listGatewaysApiV1GatewaysGetResponse,
ApiError
>(undefined, {
query: {
enabled: Boolean(isSignedIn && isAdmin),
refetchInterval: 30_000,
refetchOnMount: "always",
},
});
const gateways = useMemo(
() =>
gatewaysQuery.data?.status === 200
? (gatewaysQuery.data.data.items ?? [])
: [],
[gatewaysQuery.data],
);
const deleteMutation = useDeleteGatewayApiV1GatewaysGatewayIdDelete<
ApiError,
{ previous?: listGatewaysApiV1GatewaysGetResponse }
>(
{
mutation: createOptimisticListDeleteMutation<
GatewayRead,
listGatewaysApiV1GatewaysGetResponse,
{ gatewayId: string }
>({
queryClient,
queryKey: gatewaysKey,
getItemId: (gateway) => gateway.id,
getDeleteId: ({ gatewayId }) => gatewayId,
onSuccess: () => {
setDeleteTarget(null);
},
invalidateQueryKeys: [gatewaysKey],
}),
},
queryClient,
);
const handleDelete = () => {
if (!deleteTarget) return;
deleteMutation.mutate({ gatewayId: deleteTarget.id });
};
return (
<>
<DashboardPageLayout
signedOut={{
message: "Sign in to view gateways.",
forceRedirectUrl: "/gateways",
}}
title="Gateways"
description="Manage OpenClaw gateway connections used by boards"
headerActions={
isAdmin && gateways.length > 0 ? (
<Link
href="/gateways/new"
className={buttonVariants({
size: "md",
variant: "primary",
})}
>
Create gateway
</Link>
) : null
}
isAdmin={isAdmin}
adminOnlyMessage="Only organization owners and admins can access gateways."
stickyHeader
>
<div className="overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm">
<GatewaysTable
gateways={gateways}
isLoading={gatewaysQuery.isLoading}
sorting={sorting}
onSortingChange={onSortingChange}
showActions
stickyHeader
onDelete={setDeleteTarget}
emptyState={{
title: "No gateways yet",
description:
"Create your first gateway to connect boards and start managing your OpenClaw connections.",
actionHref: "/gateways/new",
actionLabel: "Create your first gateway",
}}
/>
</div>
{gatewaysQuery.error ? (
<p className="mt-4 text-sm text-red-500">
{gatewaysQuery.error.message}
</p>
) : null}
</DashboardPageLayout>
<ConfirmActionDialog
open={Boolean(deleteTarget)}
onOpenChange={() => setDeleteTarget(null)}
title="Delete gateway?"
description={
<>
This removes the gateway connection from Mission Control. Boards
using it will need a new gateway assigned.
</>
}
errorMessage={deleteMutation.error?.message}
errorStyle="text"
cancelVariant="ghost"
onConfirm={handleDelete}
isConfirming={deleteMutation.isPending}
/>
</>
);
}