Pipeline/frontend/src/components/git/ForgejoConnectionsTable.tsx

291 lines
7.7 KiB
TypeScript

"use client";
import { useState } from "react";
import {
type ColumnDef,
type Table as TableType,
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Loader2, CheckCircle2 } from "lucide-react";
import { DataTable } from "@/components/tables/DataTable";
import DropdownSelect from "@/components/ui/dropdown-select";
import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils";
import type { ForgejoConnection } from "@/lib/api-forgejo";
interface ConnectionsTableProps {
connections: ForgejoConnection[];
isLoading: boolean;
onEdit?: (connection: ForgejoConnection) => void;
onDelete?: (connection: ForgejoConnection) => void;
onValidate?: (connection: ForgejoConnection) => void;
}
export function ForgejoConnectionsTable({
connections,
isLoading,
onEdit,
onDelete,
onValidate,
}: ConnectionsTableProps) {
// onEdit available for future use
const _ = onEdit;
const table = useReactTable({
data: connections,
columns: columns(onValidate),
getCoreRowModel: getCoreRowModel(),
});
return (
<DataTable
table={table}
isLoading={isLoading}
emptyState={{
icon: (
<svg
className="h-16 w-16 text-slate-300"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M9 11l3 3L22 4" />
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11" />
</svg>
),
title: "No Forgejo connections yet",
description:
"Connect a Forgejo instance to start tracking issues and pull requests from your Git projects.",
actionHref: "/git-projects/connections/new",
actionLabel: "Add connection",
}}
rowActions={{
getEditHref: (row) => `/git-projects/connections/${row.id}/edit`,
onDelete: onDelete ?? undefined,
}}
/>
);
}
const columns = (
onValidate?: (connection: ForgejoConnection) => void
): ColumnDef<ForgejoConnection>[] => [
{
accessorKey: "name",
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(false)}
className="px-0 hover:bg-transparent hover:text-slate-900"
>
Name
{column.getIsSorted() === "asc" && "↑"}
{column.getIsSorted() === "desc" && "↓"}
</Button>
);
},
cell: ({ row }) => (
<div className="flex flex-col">
<span className="font-medium text-slate-900">{row.original.name}</span>
<span className="text-xs text-slate-500">{row.original.base_url}</span>
</div>
),
},
{
accessorKey: "status",
header: "Status",
cell: ({ row }) => {
const isActive = row.original.active;
return (
<Badge variant={isActive ? "default" : "outline"}>
{isActive ? "Active" : "Inactive"}
</Badge>
);
},
},
{
accessorKey: "hasToken",
header: "Auth",
cell: ({ row }) => {
const hasToken = row.original.has_token;
const tokenLastEight = row.original.token_last_eight;
return (
<div className="flex items-center gap-2">
<Badge variant={hasToken ? "outline" : "default"}>
{hasToken ? "Configured" : "Missing"}
</Badge>
{tokenLastEight && hasToken && (
<span className="text-xs text-slate-500 font-mono">{tokenLastEight}</span>
)}
</div>
);
},
},
{
id: "actions",
cell: ({ row }) => <ActionsCell connection={row.original} onValidate={onValidate} />,
},
];
function ActionsCell({
connection,
onValidate,
}: {
connection: ForgejoConnection;
onValidate?: (connection: ForgejoConnection) => void;
}) {
const [isValidateLoading, setIsValidateLoading] = useState(false);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [validateResult, setValidateResult] = useState<{
ok: boolean;
error_message?: string;
response_time_ms?: number;
} | null>(null);
const handleValidate = async () => {
if (!onValidate) return;
setIsValidateLoading(true);
try {
await onValidate(connection);
} finally {
setIsValidateLoading(false);
}
};
const options = [
{ value: "edit", label: "Edit" },
{ value: "delete", label: "Delete", icon: (props: { className?: string }) => (
<svg
className={cn("h-4 w-4", props.className)}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M3 6h18" />
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" />
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
</svg>
)},
];
const handleSelect = (value: string) => {
if (value === "edit") {
// Navigate to edit page
} else if (value === "delete") {
if (confirm(`Are you sure you want to delete "${connection.name}"?`)) {
// Trigger delete via parent
}
}
};
return (
<div className="flex items-center gap-2">
{onValidate && (
<Button
variant="ghost"
onClick={handleValidate}
disabled={isValidateLoading}
className="h-8 w-8 p-0"
title="Validate connection"
>
{isValidateLoading ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : validateResult?.ok ? (
<CheckCircle2 className="h-4 w-4 text-green-600" />
) : (
<CheckCircle2 className="h-4 w-4" />
)}
</Button>
)}
<DropdownSelect
ariaLabel="Connection actions"
options={options}
onValueChange={handleSelect}
triggerClassName="h-8 w-8 p-0"
/>
</div>
);
}
// Filter component
export function ConnectionsTableFilter({
value,
onChange,
}: {
value: string;
onChange: (value: string) => void;
}) {
return (
<Input
placeholder="Filter connections..."
value={value ?? ""}
onChange={(e) => onChange(e.target.value)}
className="h-8 w-[150px] lg:w-[250px]"
/>
);
}
// Column toggle component
export function ConnectionsTableToggle({
table,
}: {
table: TableType<ForgejoConnection>;
}) {
return (
<div className="flex items-center gap-2">
<div className="flex items-center gap-2">
<span className="text-xs text-slate-500">Show:</span>
<Button
variant="ghost"
size="sm"
onClick={() => table.toggleAllColumnsVisible()}
className="h-8 px-2 py-1 text-xs"
>
All
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => table.toggleAllColumnsVisible(false)}
className="h-8 px-2 py-1 text-xs"
>
None
</Button>
</div>
<div className="flex gap-1">
{table
.getAllColumns()
.filter((column) => column.getCanHide())
.map((column) => {
return (
<Button
key={column.id}
variant="ghost"
size="sm"
onClick={() => column.toggleVisibility(!column.getIsVisible())}
className={cn(
"h-8 px-2 py-1 text-xs",
column.getIsVisible() ? "bg-slate-100 text-slate-900" : "text-slate-500",
)}
>
{column.id}
</Button>
);
})}
</div>
</div>
);
}