Docker images / prepare-release (push) Successful in 5s
Docker images / backend-image (push) Successful in 2m37s
Docker images / frontend-image (push) Successful in 2m22s
Docker images / updater-image (push) Successful in 38s
Docker images / notify-webhook (push) Has been skipped
Docker images / publish-release (push) Successful in 8s
116 lines
3.0 KiB
TypeScript
116 lines
3.0 KiB
TypeScript
"use client"
|
|
|
|
import { useMemo, useState } from "react"
|
|
import {
|
|
type ColumnDef,
|
|
getCoreRowModel,
|
|
getFilteredRowModel,
|
|
getPaginationRowModel,
|
|
getSortedRowModel,
|
|
useReactTable,
|
|
} from "@tanstack/react-table"
|
|
import { Card } from "@/components/ui/card"
|
|
import {
|
|
DataGrid,
|
|
DataGridContainer,
|
|
DataGridPagination,
|
|
DataGridTable,
|
|
} from "@/components/reui/data-grid"
|
|
import { DataPageToolbar } from "@/components/data-page-toolbar"
|
|
import { EmptyState } from "@/components/empty-state"
|
|
import { InboxIcon } from "lucide-react"
|
|
|
|
export interface Column<T> {
|
|
key: string
|
|
label: string
|
|
render: (row: T) => React.ReactNode
|
|
}
|
|
|
|
interface DataTableProps<T extends { id: string }> {
|
|
data: T[]
|
|
columns: Column<T>[]
|
|
searchPlaceholder?: string
|
|
searchKeys?: (keyof T)[]
|
|
isLoading?: boolean
|
|
emptyTitle?: string
|
|
emptyDescription?: string
|
|
}
|
|
|
|
export function DataTable<T extends { id: string }>({
|
|
data,
|
|
columns,
|
|
searchPlaceholder = "Поиск…",
|
|
searchKeys = [],
|
|
isLoading = false,
|
|
emptyTitle = "Нет записей",
|
|
emptyDescription,
|
|
}: DataTableProps<T>) {
|
|
const [search, setSearch] = useState("")
|
|
const [globalFilter, setGlobalFilter] = useState("")
|
|
|
|
const filteredData = useMemo(() => {
|
|
if (!search || searchKeys.length === 0) return data
|
|
const s = search.toLowerCase()
|
|
return data.filter((row) =>
|
|
searchKeys.some((k) => String(row[k]).toLowerCase().includes(s)),
|
|
)
|
|
}, [data, search, searchKeys])
|
|
|
|
const columnDefs = useMemo<ColumnDef<T>[]>(
|
|
() =>
|
|
columns.map((col) => ({
|
|
id: col.key,
|
|
accessorKey: col.key,
|
|
header: col.label,
|
|
cell: ({ row }) => col.render(row.original),
|
|
meta: { headerTitle: col.label },
|
|
})),
|
|
[columns],
|
|
)
|
|
|
|
const table = useReactTable({
|
|
data: filteredData,
|
|
columns: columnDefs,
|
|
state: { globalFilter },
|
|
onGlobalFilterChange: setGlobalFilter,
|
|
getCoreRowModel: getCoreRowModel(),
|
|
getSortedRowModel: getSortedRowModel(),
|
|
getFilteredRowModel: getFilteredRowModel(),
|
|
getPaginationRowModel: getPaginationRowModel(),
|
|
getRowId: (row) => row.id,
|
|
})
|
|
|
|
const displayCount = searchKeys.length > 0 ? filteredData.length : data.length
|
|
|
|
return (
|
|
<Card>
|
|
<DataPageToolbar
|
|
search={search}
|
|
onSearchChange={setSearch}
|
|
searchPlaceholder={searchPlaceholder}
|
|
countLabel={`${displayCount} записей`}
|
|
/>
|
|
<DataGrid
|
|
table={table}
|
|
recordCount={filteredData.length}
|
|
isLoading={isLoading}
|
|
loadingMode="skeleton"
|
|
emptyMessage={
|
|
<EmptyState
|
|
icon={<InboxIcon className="size-4" />}
|
|
title={emptyTitle}
|
|
description={emptyDescription}
|
|
className="py-12"
|
|
/>
|
|
}
|
|
tableLayout={{ rowBorder: true, headerBackground: true }}
|
|
>
|
|
<DataGridContainer border={false}>
|
|
<DataGridTable />
|
|
</DataGridContainer>
|
|
{filteredData.length > 0 && <DataGridPagination className="px-5 pb-3" />}
|
|
</DataGrid>
|
|
</Card>
|
|
)
|
|
}
|