D3V-Server/frontend/src/pages/AuditLog/Table.tsx
xtcnet adf738bc33
All checks were successful
Docker Cloud Build / Build & Publish Image (push) Successful in 11m13s
fix(audit-log): remove invalid defaultMessage prop from T component in pagination
2026-03-18 23:45:47 +07:00

202 lines
5.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
createColumnHelper,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
useReactTable,
} from "@tanstack/react-table";
import { useMemo } from "react";
import type { AuditLog } from "src/api/backend";
import { EventFormatter, GravatarFormatter } from "src/components";
import { TableLayout } from "src/components/Table/TableLayout";
import { intl, T } from "src/locale";
interface Props {
data: AuditLog[];
isFetching?: boolean;
onSelectItem?: (id: number) => void;
globalFilter?: string;
}
/**
* Export filtered rows to CSV and trigger browser download.
*/
function exportCSV(rows: AuditLog[]) {
const headers = ["ID", "Date", "User", "Object Type", "Action", "Object ID"];
const escape = (v: string) => `"${String(v ?? "").replace(/"/g, '""')}"`;
const lines = [
headers.join(","),
...rows.map((r) =>
[
r.id,
r.createdOn,
r.user?.name ?? r.userId,
r.objectType,
r.action,
r.objectId,
]
.map(String)
.map(escape)
.join(","),
),
];
const blob = new Blob([lines.join("\r\n")], { type: "text/csv;charset=utf-8;" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `audit-log-${new Date().toISOString().slice(0, 10)}.csv`;
a.click();
URL.revokeObjectURL(url);
}
export default function Table({ data, isFetching, onSelectItem, globalFilter }: Props) {
const columnHelper = createColumnHelper<AuditLog>();
const columns = useMemo(
() => [
columnHelper.accessor((row: AuditLog) => row.user, {
id: "user.avatar",
cell: (info: any) => {
const value = info.getValue();
return <GravatarFormatter url={value ? value.avatar : ""} name={value ? value.name : ""} />;
},
meta: { className: "w-1" },
}),
columnHelper.accessor((row: AuditLog) => row, {
id: "objectType",
header: intl.formatMessage({ id: "column.event" }),
cell: (info: any) => <EventFormatter row={info.getValue()} />,
filterFn: (row, _columnId, filterValue: string) => {
const r = row.original;
const haystack = [
r.user?.name ?? "",
r.objectType,
r.action,
r.createdOn,
String(r.objectId),
]
.join(" ")
.toLowerCase();
return haystack.includes(filterValue.toLowerCase());
},
}),
columnHelper.display({
id: "id",
cell: (info: any) => (
<button
type="button"
className="btn btn-action btn-sm px-1"
onClick={(e) => {
e.preventDefault();
onSelectItem?.(info.row.original.id);
}}
>
<T id="action.view-details" />
</button>
),
meta: { className: "text-end w-1" },
}),
],
[columnHelper, onSelectItem],
);
const tableInstance = useReactTable<AuditLog>({
columns,
data,
state: {
globalFilter,
},
globalFilterFn: (row, _columnId, filterValue: string) => {
if (!filterValue) return true;
const r = row.original;
const haystack = [
r.user?.name ?? "",
r.objectType,
r.action,
r.createdOn,
String(r.objectId),
]
.join(" ")
.toLowerCase();
return haystack.includes(filterValue.toLowerCase());
},
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
initialState: {
pagination: { pageSize: 10 },
},
rowCount: data.length,
meta: { isFetching },
enableSortingRemoval: false,
});
const { pageIndex, pageSize } = tableInstance.getState().pagination;
const filteredRows = tableInstance.getFilteredRowModel().rows.map((r) => r.original);
const totalRows = filteredRows.length;
const pageCount = tableInstance.getPageCount();
const from = totalRows === 0 ? 0 : pageIndex * pageSize + 1;
const to = Math.min((pageIndex + 1) * pageSize, totalRows);
return (
<>
<TableLayout tableInstance={tableInstance} />
{(totalRows > pageSize || pageCount > 1) && (
<div className="card-footer d-flex align-items-center justify-content-between py-2 px-3 flex-wrap gap-2">
{/* Record count */}
<small className="text-muted">
{from}{to} / {totalRows}
</small>
{/* Prev / Page / Next */}
<div className="d-flex gap-1">
<button
type="button"
className="btn btn-sm btn-outline-secondary"
onClick={() => tableInstance.previousPage()}
disabled={!tableInstance.getCanPreviousPage()}
>
Trước
</button>
<span className="btn btn-sm disabled text-muted px-2">
{pageIndex + 1} / {pageCount}
</span>
<button
type="button"
className="btn btn-sm btn-outline-secondary"
onClick={() => tableInstance.nextPage()}
disabled={!tableInstance.getCanNextPage()}
>
Tiếp
</button>
</div>
{/* Page size */}
<div className="d-flex align-items-center gap-2">
<small className="text-muted text-nowrap">Mỗi trang:</small>
<select
className="form-select form-select-sm w-auto"
value={pageSize}
onChange={(e) => tableInstance.setPageSize(Number(e.target.value))}
>
{[10, 25, 50, 100].map((size) => (
<option key={size} value={size}>
{size}
</option>
))}
</select>
</div>
{/* Export */}
<button
type="button"
className="btn btn-sm btn-outline-success"
onClick={() => exportCSV(filteredRows)}
title="Export CSV"
>
CSV
</button>
</div>
)}
</>
);
}