215 lines
5.7 KiB
TypeScript
215 lines
5.7 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useMemo, useState } 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 useSWR from "swr";
|
|
import { getAudits } from "@/app/services/audits";
|
|
import {
|
|
AuditQueryResult,
|
|
AuditRow,
|
|
DEFAULT_PAGE_SIZE,
|
|
} from "./auditTransforms";
|
|
|
|
const FALLBACK_COLUMNS: GridColDef[] = [
|
|
{
|
|
field: "placeholder",
|
|
headerName: "Audit Data",
|
|
flex: 1,
|
|
sortable: false,
|
|
filterable: false,
|
|
},
|
|
];
|
|
|
|
const toTitle = (field: string) =>
|
|
field
|
|
.replace(/_/g, " ")
|
|
.replace(/-/g, " ")
|
|
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
.replace(/\s+/g, " ")
|
|
.trim()
|
|
.replace(/^\w/g, char => char.toUpperCase());
|
|
|
|
const deriveColumns = (rows: AuditRow[]): GridColDef[] => {
|
|
if (!rows.length) return [];
|
|
|
|
return Object.keys(rows[0]).map(field => ({
|
|
field,
|
|
headerName: toTitle(field),
|
|
flex: field === "id" ? 0 : 1,
|
|
minWidth: field === "id" ? 140 : 200,
|
|
sortable: true,
|
|
}));
|
|
};
|
|
|
|
interface AuditTableClientProps {
|
|
initialData: AuditQueryResult;
|
|
}
|
|
|
|
const ENTITY_PREFIX = "LIKE/";
|
|
|
|
const buildSortParam = (sortModel: GridSortModel) =>
|
|
sortModel.length && sortModel[0].field && sortModel[0].sort
|
|
? `${sortModel[0].field}:${sortModel[0].sort}`
|
|
: undefined;
|
|
|
|
const buildEntityParam = (entitySearch: string) =>
|
|
entitySearch.trim() ? `${ENTITY_PREFIX}${entitySearch.trim()}` : undefined;
|
|
|
|
export default function AuditTableClient({
|
|
initialData,
|
|
}: AuditTableClientProps) {
|
|
const [paginationModel, setPaginationModel] = useState<GridPaginationModel>({
|
|
page: initialData.pageIndex ?? 0,
|
|
pageSize: DEFAULT_PAGE_SIZE,
|
|
});
|
|
const [sortModel, setSortModel] = useState<GridSortModel>([]);
|
|
const [entitySearch, setEntitySearch] = useState<string>("");
|
|
const [entitySearchInput, setEntitySearchInput] = useState<string>("");
|
|
|
|
const [columns, setColumns] = useState<GridColDef[]>(
|
|
initialData.rows.length ? deriveColumns(initialData.rows) : FALLBACK_COLUMNS
|
|
);
|
|
|
|
const debouncedSetEntitySearch = useMemo(
|
|
() =>
|
|
debounce((value: string) => {
|
|
setEntitySearch(value);
|
|
setPaginationModel(prev => ({ ...prev, page: 0 }));
|
|
}, 500),
|
|
[]
|
|
);
|
|
|
|
useEffect(() => {
|
|
return () => {
|
|
debouncedSetEntitySearch.clear();
|
|
};
|
|
}, [debouncedSetEntitySearch]);
|
|
|
|
const sortParam = useMemo(() => buildSortParam(sortModel), [sortModel]);
|
|
const entityParam = useMemo(
|
|
() => buildEntityParam(entitySearch),
|
|
[entitySearch]
|
|
);
|
|
|
|
const { data, error, isLoading, isValidating } = useSWR(
|
|
[
|
|
"audits",
|
|
paginationModel.page,
|
|
paginationModel.pageSize,
|
|
sortParam ?? "",
|
|
entityParam ?? "",
|
|
],
|
|
() =>
|
|
getAudits({
|
|
limit: paginationModel.pageSize,
|
|
page: paginationModel.page + 1,
|
|
sort: sortParam,
|
|
entity: entityParam,
|
|
}),
|
|
{
|
|
keepPreviousData: true,
|
|
revalidateOnFocus: true,
|
|
revalidateOnReconnect: true,
|
|
fallbackData: initialData,
|
|
}
|
|
);
|
|
|
|
const rows = data?.rows ?? initialData.rows;
|
|
const rowCount = data?.total ?? initialData.total ?? rows.length;
|
|
const loading = isLoading || (isValidating && !rows.length);
|
|
const pageTitle = useMemo(
|
|
() =>
|
|
sortModel.length && sortModel[0].field
|
|
? `Audit Logs · sorted by ${toTitle(sortModel[0].field)}`
|
|
: "Audit Logs",
|
|
[sortModel]
|
|
);
|
|
|
|
useEffect(() => {
|
|
setColumns(prev =>
|
|
rows.length ? deriveColumns(rows) : prev.length ? prev : FALLBACK_COLUMNS
|
|
);
|
|
}, [rows]);
|
|
|
|
const handlePaginationChange = (model: GridPaginationModel) => {
|
|
setPaginationModel(model);
|
|
};
|
|
|
|
const handleSortModelChange = (model: GridSortModel) => {
|
|
setSortModel(model);
|
|
setPaginationModel(prev => ({ ...prev, page: 0 }));
|
|
};
|
|
|
|
const handleEntitySearchChange = (value: string) => {
|
|
setEntitySearchInput(value);
|
|
debouncedSetEntitySearch(value);
|
|
};
|
|
|
|
const errorMessage =
|
|
error instanceof Error
|
|
? error.message
|
|
: error
|
|
? "Failed to load audits"
|
|
: null;
|
|
|
|
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>
|
|
{errorMessage && (
|
|
<div className="error-alert" role="alert">
|
|
{errorMessage}
|
|
</div>
|
|
)}
|
|
<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={rowCount}
|
|
sortModel={sortModel}
|
|
onSortModelChange={handleSortModelChange}
|
|
pageSizeOptions={[10, 25, 50, 100]}
|
|
disableRowSelectionOnClick
|
|
sx={{
|
|
border: 0,
|
|
minHeight: 500,
|
|
"& .MuiDataGrid-cell": {
|
|
whiteSpace: "nowrap",
|
|
overflow: "hidden",
|
|
textOverflow: "ellipsis",
|
|
},
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|