182 lines
5.6 KiB
TypeScript
182 lines
5.6 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 { GatewayAgentImportDialog } from "@/components/gateways/GatewayAgentImportDialog";
|
|
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 { getListAgentsApiV1AgentsGetQueryKey } from "@/api/generated/agents/agents";
|
|
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 [importTarget, setImportTarget] = useState<GatewayRead | null>(null);
|
|
|
|
const gatewaysKey = getListGatewaysApiV1GatewaysGetQueryKey();
|
|
const agentsKey = getListAgentsApiV1AgentsGetQueryKey();
|
|
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 });
|
|
};
|
|
|
|
const handleImported = async () => {
|
|
await queryClient.invalidateQueries({ queryKey: gatewaysKey });
|
|
await queryClient.invalidateQueries({ queryKey: agentsKey });
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<DashboardPageLayout
|
|
signedOut={{
|
|
message: "Sign in to view gateways.",
|
|
forceRedirectUrl: "/gateways",
|
|
}}
|
|
title="Gateways"
|
|
description="Manage Pipeline 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-border bg-card shadow-sm">
|
|
<GatewaysTable
|
|
gateways={gateways}
|
|
isLoading={gatewaysQuery.isLoading}
|
|
sorting={sorting}
|
|
onSortingChange={onSortingChange}
|
|
showActions
|
|
stickyHeader
|
|
onDelete={setDeleteTarget}
|
|
onImport={setImportTarget}
|
|
emptyState={{
|
|
title: "No gateways yet",
|
|
description:
|
|
"Create your first gateway to connect boards and start managing your Pipeline 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 Pipeline. Boards using it
|
|
will need a new gateway assigned.
|
|
</>
|
|
}
|
|
errorMessage={deleteMutation.error?.message}
|
|
errorStyle="text"
|
|
cancelVariant="ghost"
|
|
onConfirm={handleDelete}
|
|
isConfirming={deleteMutation.isPending}
|
|
/>
|
|
|
|
<GatewayAgentImportDialog
|
|
open={Boolean(importTarget)}
|
|
onOpenChange={(open) => {
|
|
if (!open) {
|
|
setImportTarget(null);
|
|
}
|
|
}}
|
|
gateway={importTarget}
|
|
onImported={handleImported}
|
|
/>
|
|
</>
|
|
);
|
|
}
|