202 lines
5.1 KiB
TypeScript
202 lines
5.1 KiB
TypeScript
"use client";
|
|
|
|
import {
|
|
useCallback,
|
|
useEffect,
|
|
useMemo,
|
|
useState,
|
|
useTransition,
|
|
} from "react";
|
|
import {
|
|
DataGrid,
|
|
GridColDef,
|
|
GridPaginationModel,
|
|
GridSortModel,
|
|
} from "@mui/x-data-grid";
|
|
import TextField from "@mui/material/TextField";
|
|
import { Box, debounce } from "@mui/material";
|
|
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
|
|
|
import { AuditRow } from "./auditTransforms";
|
|
import { PAGE_SIZE_OPTIONS } from "./auditConstants";
|
|
import {
|
|
buildSortParam,
|
|
deriveColumns,
|
|
parseSortModel,
|
|
toTitle,
|
|
} from "./utils";
|
|
|
|
const FALLBACK_COLUMNS: GridColDef[] = [
|
|
{
|
|
field: "placeholder",
|
|
headerName: "Audit Data",
|
|
flex: 1,
|
|
sortable: false,
|
|
filterable: false,
|
|
},
|
|
];
|
|
|
|
interface AuditTableClientProps {
|
|
rows: AuditRow[];
|
|
total: number;
|
|
pageIndex: number;
|
|
pageSize: number;
|
|
sortParam?: string;
|
|
entityQuery?: string;
|
|
}
|
|
|
|
export default function AuditTableClient({
|
|
rows,
|
|
total,
|
|
pageIndex,
|
|
pageSize,
|
|
sortParam,
|
|
entityQuery = "",
|
|
}: AuditTableClientProps) {
|
|
const router = useRouter();
|
|
const pathname = usePathname();
|
|
const searchParams = useSearchParams();
|
|
const searchParamsString = searchParams.toString();
|
|
const [isNavigating, startTransition] = useTransition();
|
|
|
|
// Derive values directly from props
|
|
const paginationModel = useMemo<GridPaginationModel>(
|
|
() => ({
|
|
page: pageIndex,
|
|
pageSize,
|
|
}),
|
|
[pageIndex, pageSize]
|
|
);
|
|
|
|
const sortModel = useMemo(() => parseSortModel(sortParam), [sortParam]);
|
|
const columns = useMemo(
|
|
() => (rows.length ? deriveColumns(rows) : FALLBACK_COLUMNS),
|
|
[rows]
|
|
);
|
|
|
|
// Only entitySearchInput needs state since it's a controlled input
|
|
const normalizedEntityQuery = entityQuery.trim();
|
|
const [entitySearchInput, setEntitySearchInput] = useState<string>(
|
|
normalizedEntityQuery
|
|
);
|
|
|
|
// Sync entity input when query prop changes (e.g., from URL navigation)
|
|
useEffect(() => {
|
|
setEntitySearchInput(normalizedEntityQuery);
|
|
}, [normalizedEntityQuery]);
|
|
|
|
const pageTitle = useMemo(
|
|
() =>
|
|
sortModel.length && sortModel[0].field
|
|
? `Audit Logs · sorted by ${toTitle(sortModel[0].field)}`
|
|
: "Audit Logs",
|
|
[sortModel]
|
|
);
|
|
|
|
const navigateWithParams = useCallback(
|
|
(updates: Record<string, string | null | undefined>) => {
|
|
const params = new URLSearchParams(searchParamsString);
|
|
|
|
Object.entries(updates).forEach(([key, value]) => {
|
|
if (!value) {
|
|
params.delete(key);
|
|
} else {
|
|
params.set(key, value);
|
|
}
|
|
});
|
|
|
|
startTransition(() => {
|
|
const queryString = params.toString();
|
|
router.push(queryString ? `${pathname}?${queryString}` : pathname);
|
|
});
|
|
},
|
|
[pathname, router, searchParamsString, startTransition]
|
|
);
|
|
|
|
const debouncedEntityNavigate = useMemo(
|
|
() =>
|
|
debounce((value: string) => {
|
|
navigateWithParams({
|
|
page: "1",
|
|
entity: value.trim() ? value.trim() : null,
|
|
});
|
|
}, 500),
|
|
[navigateWithParams]
|
|
);
|
|
|
|
useEffect(() => {
|
|
return () => {
|
|
debouncedEntityNavigate.clear();
|
|
};
|
|
}, [debouncedEntityNavigate]);
|
|
|
|
const handlePaginationChange = (model: GridPaginationModel) => {
|
|
navigateWithParams({
|
|
page: String(model.page + 1),
|
|
limit: String(model.pageSize),
|
|
});
|
|
};
|
|
|
|
const handleSortModelChange = (model: GridSortModel) => {
|
|
navigateWithParams({
|
|
page: "1",
|
|
sort: buildSortParam(model) ?? null,
|
|
});
|
|
};
|
|
|
|
const handleEntitySearchChange = (value: string) => {
|
|
setEntitySearchInput(value);
|
|
debouncedEntityNavigate(value);
|
|
};
|
|
|
|
const loading = isNavigating;
|
|
|
|
return (
|
|
<div className="audits-page">
|
|
<Box sx={{ display: "flex", gap: 2, mt: 5 }}>
|
|
<TextField
|
|
label="Search by Entity"
|
|
variant="outlined"
|
|
size="small"
|
|
value={entitySearchInput}
|
|
onChange={e => handleEntitySearchChange(e.target.value)}
|
|
sx={{ width: 300, backgroundColor: "#f0f0f0" }}
|
|
/>
|
|
</Box>
|
|
<h1 className="page-title">{pageTitle}</h1>
|
|
<div className="table-container">
|
|
<div className="scroll-wrapper">
|
|
<div
|
|
className="table-inner"
|
|
style={{ minWidth: `${columns.length * 200}px` }}
|
|
>
|
|
<DataGrid
|
|
rows={rows}
|
|
columns={columns.length ? columns : FALLBACK_COLUMNS}
|
|
loading={loading}
|
|
paginationMode="server"
|
|
sortingMode="server"
|
|
paginationModel={paginationModel}
|
|
onPaginationModelChange={handlePaginationChange}
|
|
rowCount={total}
|
|
sortModel={sortModel}
|
|
onSortModelChange={handleSortModelChange}
|
|
pageSizeOptions={[...PAGE_SIZE_OPTIONS]}
|
|
disableRowSelectionOnClick
|
|
sx={{
|
|
border: 0,
|
|
minHeight: 500,
|
|
"& .MuiDataGrid-cell": {
|
|
whiteSpace: "nowrap",
|
|
overflow: "hidden",
|
|
textOverflow: "ellipsis",
|
|
},
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|