Files
homelab-optimized/dashboard/ui/components/data-table.tsx
Gitea Mirror Bot c1a6970aa7
Some checks failed
Documentation / Build Docusaurus (push) Failing after 5m1s
Documentation / Deploy to GitHub Pages (push) Has been skipped
Sanitized mirror from private repository - 2026-04-05 05:50:13 UTC
2026-04-05 05:50:13 +00:00

127 lines
3.6 KiB
TypeScript

"use client";
import { useState, useMemo } from "react";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
export interface Column<T> {
key: string;
label: string;
render?: (row: T) => React.ReactNode;
}
interface DataTableProps<T> {
data: T[];
columns: Column<T>[];
searchKey?: string;
filterKey?: string;
filterOptions?: string[];
actions?: (row: T) => React.ReactNode;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function DataTable<T extends Record<string, any>>({
data,
columns,
searchKey,
filterKey,
filterOptions,
actions,
}: DataTableProps<T>) {
const [search, setSearch] = useState("");
const [filter, setFilter] = useState("all");
const filtered = useMemo(() => {
let rows = data;
if (search && searchKey) {
const q = search.toLowerCase();
rows = rows.filter((r) =>
String(r[searchKey] ?? "")
.toLowerCase()
.includes(q)
);
}
if (filter !== "all" && filterKey) {
rows = rows.filter((r) => String(r[filterKey]) === filter);
}
return rows;
}, [data, search, searchKey, filter, filterKey]);
return (
<div className="space-y-3">
<div className="flex items-center gap-2">
{searchKey && (
<input
type="text"
placeholder="Search..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="h-8 rounded-md border border-border bg-background px-3 text-xs text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring w-64"
/>
)}
{filterKey && filterOptions && (
<select
value={filter}
onChange={(e) => setFilter(e.target.value)}
className="h-8 rounded-md border border-border bg-background px-2 text-xs text-foreground focus:outline-none focus:ring-1 focus:ring-ring"
>
<option value="all">All</option>
{filterOptions.map((opt) => (
<option key={opt} value={opt}>
{opt}
</option>
))}
</select>
)}
</div>
<div className="rounded-md border border-border overflow-hidden">
<Table>
<TableHeader>
<TableRow>
{columns.map((col) => (
<TableHead key={col.key} className="text-xs">
{col.label}
</TableHead>
))}
{actions && <TableHead className="text-xs w-24">Actions</TableHead>}
</TableRow>
</TableHeader>
<TableBody>
{filtered.length === 0 ? (
<TableRow>
<TableCell
colSpan={columns.length + (actions ? 1 : 0)}
className="text-center text-xs text-muted-foreground py-6"
>
No results
</TableCell>
</TableRow>
) : (
filtered.map((row, i) => (
<TableRow key={i}>
{columns.map((col) => (
<TableCell key={col.key} className="text-xs">
{col.render
? col.render(row)
: String(row[col.key] ?? "")}
</TableCell>
))}
{actions && (
<TableCell className="text-xs">{actions(row)}</TableCell>
)}
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</div>
);
}