291 lines
7.7 KiB
TypeScript
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>
|
|
);
|
|
}
|