mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-12 12:25:22 +00:00
295 lines
12 KiB
TypeScript
295 lines
12 KiB
TypeScript
"use client"
|
|
|
|
import { DisplayDate } from "@/app/[domain]/components/DisplayDate"
|
|
import { NotificationDot } from "@/app/[domain]/components/notificationDot"
|
|
import { useToast } from "@/components/hooks/use-toast"
|
|
import { Badge } from "@/components/ui/badge"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Input } from "@/components/ui/input"
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
|
|
import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants"
|
|
import { getCodeHostIcon } from "@/lib/utils"
|
|
import { ConnectionType } from "@sourcebot/db"
|
|
import {
|
|
type ColumnDef,
|
|
type ColumnFiltersState,
|
|
type SortingState,
|
|
type VisibilityState,
|
|
flexRender,
|
|
getCoreRowModel,
|
|
getFilteredRowModel,
|
|
getPaginationRowModel,
|
|
getSortedRowModel,
|
|
useReactTable,
|
|
} from "@tanstack/react-table"
|
|
import { cva } from "class-variance-authority"
|
|
import { ArrowUpDown, RefreshCwIcon } from "lucide-react"
|
|
import Image from "next/image"
|
|
import Link from "next/link"
|
|
import { useRouter } from "next/navigation"
|
|
import { useMemo, useState } from "react"
|
|
|
|
|
|
export type Connection = {
|
|
id: number
|
|
name: string
|
|
syncedAt: Date | null
|
|
connectionType: ConnectionType
|
|
latestJobStatus: "PENDING" | "IN_PROGRESS" | "COMPLETED" | "FAILED" | null
|
|
isFirstTimeSync: boolean
|
|
}
|
|
|
|
const statusBadgeVariants = cva("", {
|
|
variants: {
|
|
status: {
|
|
PENDING: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
IN_PROGRESS: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
COMPLETED: "bg-green-600 text-white hover:bg-green-700",
|
|
FAILED: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
|
},
|
|
},
|
|
})
|
|
|
|
const getStatusBadge = (status: Connection["latestJobStatus"]) => {
|
|
if (!status) {
|
|
return "-";
|
|
}
|
|
|
|
const labels = {
|
|
PENDING: "Pending",
|
|
IN_PROGRESS: "In Progress",
|
|
COMPLETED: "Completed",
|
|
FAILED: "Failed",
|
|
}
|
|
|
|
return <Badge className={statusBadgeVariants({ status })}>{labels[status]}</Badge>
|
|
}
|
|
|
|
export const columns: ColumnDef<Connection>[] = [
|
|
{
|
|
accessorKey: "name",
|
|
size: 400,
|
|
header: ({ column }) => {
|
|
return (
|
|
<Button variant="ghost" onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}>
|
|
Name
|
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
|
</Button>
|
|
)
|
|
},
|
|
cell: ({ row }) => {
|
|
const connection = row.original;
|
|
const codeHostIcon = getCodeHostIcon(connection.connectionType);
|
|
|
|
return (
|
|
<div className="flex flex-row gap-2 items-center">
|
|
<Image
|
|
src={codeHostIcon.src}
|
|
alt={`${connection.connectionType} logo`}
|
|
className={codeHostIcon.className}
|
|
width={20}
|
|
height={20}
|
|
/>
|
|
<Link href={`/${SINGLE_TENANT_ORG_DOMAIN}/settings/connections/${connection.id}`} className="font-medium hover:underline">
|
|
{connection.name}
|
|
</Link>
|
|
{connection.isFirstTimeSync && (
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<span>
|
|
<NotificationDot className="ml-1.5" />
|
|
</span>
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
<span>This is the first time Sourcebot is syncing this connection. It may take a few minutes to complete.</span>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
)}
|
|
</div>
|
|
)
|
|
},
|
|
},
|
|
{
|
|
accessorKey: "latestJobStatus",
|
|
size: 150,
|
|
header: "Lastest status",
|
|
cell: ({ row }) => getStatusBadge(row.getValue("latestJobStatus")),
|
|
},
|
|
{
|
|
accessorKey: "syncedAt",
|
|
size: 200,
|
|
header: ({ column }) => {
|
|
return (
|
|
<Button
|
|
variant="ghost"
|
|
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
|
>
|
|
Last synced
|
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
|
</Button>
|
|
)
|
|
},
|
|
cell: ({ row }) => {
|
|
const syncedAt = row.getValue("syncedAt") as Date | null;
|
|
if (!syncedAt) {
|
|
return "-";
|
|
}
|
|
|
|
return (
|
|
<DisplayDate date={syncedAt} className="ml-3" />
|
|
)
|
|
}
|
|
},
|
|
]
|
|
|
|
export const ConnectionsTable = ({ data }: { data: Connection[] }) => {
|
|
const [sorting, setSorting] = useState<SortingState>([])
|
|
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
|
|
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({})
|
|
const [rowSelection, setRowSelection] = useState({})
|
|
const router = useRouter();
|
|
const { toast } = useToast();
|
|
|
|
const {
|
|
numCompleted,
|
|
numInProgress,
|
|
numPending,
|
|
numFailed,
|
|
numNoJobs,
|
|
} = useMemo(() => {
|
|
return {
|
|
numCompleted: data.filter((connection) => connection.latestJobStatus === "COMPLETED").length,
|
|
numInProgress: data.filter((connection) => connection.latestJobStatus === "IN_PROGRESS").length,
|
|
numPending: data.filter((connection) => connection.latestJobStatus === "PENDING").length,
|
|
numFailed: data.filter((connection) => connection.latestJobStatus === "FAILED").length,
|
|
numNoJobs: data.filter((connection) => connection.latestJobStatus === null).length,
|
|
}
|
|
}, [data]);
|
|
|
|
const table = useReactTable({
|
|
data,
|
|
columns,
|
|
onSortingChange: setSorting,
|
|
onColumnFiltersChange: setColumnFilters,
|
|
getCoreRowModel: getCoreRowModel(),
|
|
getPaginationRowModel: getPaginationRowModel(),
|
|
getSortedRowModel: getSortedRowModel(),
|
|
getFilteredRowModel: getFilteredRowModel(),
|
|
onColumnVisibilityChange: setColumnVisibility,
|
|
onRowSelectionChange: setRowSelection,
|
|
columnResizeMode: 'onChange',
|
|
enableColumnResizing: false,
|
|
state: {
|
|
sorting,
|
|
columnFilters,
|
|
columnVisibility,
|
|
rowSelection,
|
|
},
|
|
})
|
|
|
|
return (
|
|
<div className="w-full">
|
|
<div className="flex items-center gap-4 py-4">
|
|
<Input
|
|
placeholder="Filter connections..."
|
|
value={(table.getColumn("name")?.getFilterValue() as string) ?? ""}
|
|
onChange={(event) => table.getColumn("name")?.setFilterValue(event.target.value)}
|
|
className="max-w-sm"
|
|
/>
|
|
<Select
|
|
value={(table.getColumn("latestJobStatus")?.getFilterValue() as string) ?? "all"}
|
|
onValueChange={(value) => {
|
|
table.getColumn("latestJobStatus")?.setFilterValue(value === "all" ? "" : value)
|
|
}}
|
|
>
|
|
<SelectTrigger className="w-[180px]">
|
|
<SelectValue placeholder="Filter by status" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">Filter by status</SelectItem>
|
|
<SelectItem value="COMPLETED">Completed ({numCompleted})</SelectItem>
|
|
<SelectItem value="IN_PROGRESS">In progress ({numInProgress})</SelectItem>
|
|
<SelectItem value="PENDING">Pending ({numPending})</SelectItem>
|
|
<SelectItem value="FAILED">Failed ({numFailed})</SelectItem>
|
|
<SelectItem value="null">No status ({numNoJobs})</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<Button
|
|
variant="outline"
|
|
className="ml-auto"
|
|
onClick={() => {
|
|
router.refresh();
|
|
toast({
|
|
description: "Page refreshed",
|
|
});
|
|
}}
|
|
>
|
|
<RefreshCwIcon className="w-3 h-3" />
|
|
Refresh
|
|
</Button>
|
|
</div>
|
|
<div className="rounded-md border">
|
|
<Table style={{ width: '100%' }}>
|
|
<TableHeader>
|
|
{table.getHeaderGroups().map((headerGroup) => (
|
|
<TableRow key={headerGroup.id}>
|
|
{headerGroup.headers.map((header) => {
|
|
return (
|
|
<TableHead
|
|
key={header.id}
|
|
style={{ width: `${header.getSize()}px` }}
|
|
>
|
|
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
|
</TableHead>
|
|
)
|
|
})}
|
|
</TableRow>
|
|
))}
|
|
</TableHeader>
|
|
<TableBody>
|
|
{table.getRowModel().rows?.length ? (
|
|
table.getRowModel().rows.map((row) => (
|
|
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
|
|
{row.getVisibleCells().map((cell) => (
|
|
<TableCell
|
|
key={cell.id}
|
|
style={{ width: `${cell.column.getSize()}px` }}
|
|
>
|
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
</TableCell>
|
|
))}
|
|
</TableRow>
|
|
))
|
|
) : (
|
|
<TableRow>
|
|
<TableCell colSpan={columns.length} className="h-24 text-center">
|
|
No results.
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
<div className="flex items-center justify-end space-x-2 py-4">
|
|
<div className="flex-1 text-sm text-muted-foreground">
|
|
{table.getFilteredRowModel().rows.length} {data.length > 1 ? 'connections' : 'connection'} total
|
|
</div>
|
|
<div className="space-x-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => table.previousPage()}
|
|
disabled={!table.getCanPreviousPage()}
|
|
>
|
|
Previous
|
|
</Button>
|
|
<Button variant="outline" size="sm" onClick={() => table.nextPage()} disabled={!table.getCanNextPage()}>
|
|
Next
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|