You've already forked nginx-proxy-manager
							
							
				mirror of
				https://github.com/NginxProxyManager/nginx-proxy-manager.git
				synced 2025-10-30 18:05:34 +03:00 
			
		
		
		
	Audit log table and modal
This commit is contained in:
		| @@ -36,6 +36,35 @@ const internalAuditLog = { | ||||
| 		return await query; | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * @param  {Access}   access | ||||
| 	 * @param  {Object}   [data] | ||||
| 	 * @param  {Integer}  [data.id]          Defaults to the token user | ||||
| 	 * @param  {Array}    [data.expand] | ||||
| 	 * @return {Promise} | ||||
| 	 */ | ||||
| 	get: async (access, data) => { | ||||
| 		await access.can("auditlog:list"); | ||||
|  | ||||
| 		const query = auditLogModel | ||||
| 			.query() | ||||
| 			.andWhere("id", data.id) | ||||
| 			.allowGraph("[user]") | ||||
| 			.first(); | ||||
|  | ||||
| 		if (typeof data.expand !== "undefined" && data.expand !== null) { | ||||
| 			query.withGraphFetched(`[${data.expand.join(", ")}]`); | ||||
| 		} | ||||
|  | ||||
| 		const row = await query; | ||||
|  | ||||
| 		if (!row?.id) { | ||||
| 			throw new errs.ItemNotFoundError(data.id); | ||||
| 		} | ||||
|  | ||||
| 		return row; | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * This method should not be publicly used, it doesn't check certain things. It will be assumed | ||||
| 	 * that permission to add to audit log is already considered, however the access token is used for | ||||
|   | ||||
| @@ -52,4 +52,56 @@ router | ||||
| 		} | ||||
| 	}); | ||||
|  | ||||
| /** | ||||
|  * Specific audit log entry | ||||
|  * | ||||
|  * /api/audit-log/123 | ||||
|  */ | ||||
| router | ||||
| 	.route("/:event_id") | ||||
| 	.options((_, res) => { | ||||
| 		res.sendStatus(204); | ||||
| 	}) | ||||
| 	.all(jwtdecode()) | ||||
|  | ||||
| 	/** | ||||
| 	 * GET /api/audit-log/123 | ||||
| 	 * | ||||
| 	 * Retrieve a specific entry | ||||
| 	 */ | ||||
| 	.get(async (req, res, next) => { | ||||
| 		try { | ||||
| 			const data = await validator( | ||||
| 				{ | ||||
| 					required: ["event_id"], | ||||
| 					additionalProperties: false, | ||||
| 					properties: { | ||||
| 						event_id: { | ||||
| 							$ref: "common#/properties/id", | ||||
| 						}, | ||||
| 						expand: { | ||||
| 							$ref: "common#/properties/expand", | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 				{ | ||||
| 					event_id: req.params.event_id, | ||||
| 					expand: | ||||
| 						typeof req.query.expand === "string" | ||||
| 							? req.query.expand.split(",") | ||||
| 							: null, | ||||
| 				}, | ||||
| 			); | ||||
|  | ||||
| 			const item = await internalAuditLog.get(res.locals.access, { | ||||
| 				id: data.event_id, | ||||
| 				expand: data.expand, | ||||
| 			}); | ||||
| 			res.status(200).send(item); | ||||
| 		} catch (err) { | ||||
| 			logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`); | ||||
| 			next(err); | ||||
| 		} | ||||
| 	}); | ||||
|  | ||||
| export default router; | ||||
|   | ||||
| @@ -1,9 +1,10 @@ | ||||
| import * as api from "./base"; | ||||
| import type { AuditLogExpansion } from "./getAuditLogs"; | ||||
| import type { AuditLog } from "./models"; | ||||
|  | ||||
| export async function getAuditLog(expand?: string[], params = {}): Promise<AuditLog[]> { | ||||
| export async function getAuditLog(id: number, expand?: AuditLogExpansion[], params = {}): Promise<AuditLog> { | ||||
| 	return await api.get({ | ||||
| 		url: "/audit-log", | ||||
| 		url: `/audit-log/${id}`, | ||||
| 		params: { | ||||
| 			expand: expand?.join(","), | ||||
| 			...params, | ||||
|   | ||||
							
								
								
									
										14
									
								
								frontend/src/api/backend/getAuditLogs.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								frontend/src/api/backend/getAuditLogs.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| import * as api from "./base"; | ||||
| import type { AuditLog } from "./models"; | ||||
|  | ||||
| export type AuditLogExpansion = "user"; | ||||
|  | ||||
| export async function getAuditLogs(expand?: AuditLogExpansion[], params = {}): Promise<AuditLog[]> { | ||||
| 	return await api.get({ | ||||
| 		url: "/audit-log", | ||||
| 		params: { | ||||
| 			expand: expand?.join(","), | ||||
| 			...params, | ||||
| 		}, | ||||
| 	}); | ||||
| } | ||||
| @@ -16,6 +16,7 @@ export * from "./downloadCertificate"; | ||||
| export * from "./getAccessList"; | ||||
| export * from "./getAccessLists"; | ||||
| export * from "./getAuditLog"; | ||||
| export * from "./getAuditLogs"; | ||||
| export * from "./getCertificate"; | ||||
| export * from "./getCertificates"; | ||||
| export * from "./getDeadHost"; | ||||
|   | ||||
| @@ -40,6 +40,8 @@ export interface AuditLog { | ||||
| 	objectId: number; | ||||
| 	action: string; | ||||
| 	meta: Record<string, any>; | ||||
| 	// Expansions: | ||||
| 	user?: User; | ||||
| } | ||||
|  | ||||
| export interface AccessList { | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| import { intlFormat, parseISO } from "date-fns"; | ||||
| import { intl } from "src/locale"; | ||||
| import { DateTimeFormat, intl } from "src/locale"; | ||||
|  | ||||
| interface Props { | ||||
| 	domains: string[]; | ||||
| @@ -17,7 +16,7 @@ export function DomainsFormatter({ domains, createdOn }: Props) { | ||||
| 			</div> | ||||
| 			{createdOn ? ( | ||||
| 				<div className="text-secondary mt-1"> | ||||
| 					{intl.formatMessage({ id: "created-on" }, { date: intlFormat(parseISO(createdOn)) })} | ||||
| 					{intl.formatMessage({ id: "created-on" }, { date: DateTimeFormat(createdOn) })} | ||||
| 				</div> | ||||
| 			) : null} | ||||
| 		</div> | ||||
|   | ||||
							
								
								
									
										53
									
								
								frontend/src/components/Table/Formatter/EventFormatter.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								frontend/src/components/Table/Formatter/EventFormatter.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | ||||
| import { IconUser } from "@tabler/icons-react"; | ||||
| import type { AuditLog } from "src/api/backend"; | ||||
| import { DateTimeFormat, intl } from "src/locale"; | ||||
|  | ||||
| const getEventTitle = (event: AuditLog) => ( | ||||
| 	<span>{intl.formatMessage({ id: `event.${event.action}-${event.objectType}` })}</span> | ||||
| ); | ||||
|  | ||||
| const getEventValue = (event: AuditLog) => { | ||||
| 	switch (event.objectType) { | ||||
| 		case "user": | ||||
| 			return event.meta?.name; | ||||
| 		default: | ||||
| 			return `UNKNOWN EVENT TYPE: ${event.objectType}`; | ||||
| 	} | ||||
| }; | ||||
|  | ||||
| const getColorForAction = (action: string) => { | ||||
| 	switch (action) { | ||||
| 		case "created": | ||||
| 			return "text-lime"; | ||||
| 		case "deleted": | ||||
| 			return "text-red"; | ||||
| 		default: | ||||
| 			return "text-blue"; | ||||
| 	} | ||||
| }; | ||||
|  | ||||
| const getIcon = (row: AuditLog) => { | ||||
| 	const c = getColorForAction(row.action); | ||||
| 	let ico = null; | ||||
| 	switch (row.objectType) { | ||||
| 		case "user": | ||||
| 			ico = <IconUser size={16} className={c} />; | ||||
| 			break; | ||||
| 	} | ||||
|  | ||||
| 	return ico; | ||||
| }; | ||||
|  | ||||
| interface Props { | ||||
| 	row: AuditLog; | ||||
| } | ||||
| export function EventFormatter({ row }: Props) { | ||||
| 	return ( | ||||
| 		<div className="flex-fill"> | ||||
| 			<div className="font-weight-medium"> | ||||
| 				{getIcon(row)} {getEventTitle(row)} — <span className="badge">{getEventValue(row)}</span> | ||||
| 			</div> | ||||
| 			<div className="text-secondary mt-1">{DateTimeFormat(row.createdOn)}</div> | ||||
| 		</div> | ||||
| 	); | ||||
| } | ||||
| @@ -1,5 +1,4 @@ | ||||
| import { intlFormat, parseISO } from "date-fns"; | ||||
| import { intl } from "src/locale"; | ||||
| import { DateTimeFormat, intl } from "src/locale"; | ||||
|  | ||||
| interface Props { | ||||
| 	value: string; | ||||
| @@ -16,7 +15,7 @@ export function ValueWithDateFormatter({ value, createdOn, disabled }: Props) { | ||||
| 				<div className={`text-secondary mt-1 ${disabled ? "text-red" : ""}`}> | ||||
| 					{disabled | ||||
| 						? intl.formatMessage({ id: "disabled" }) | ||||
| 						: intl.formatMessage({ id: "created-on" }, { date: intlFormat(parseISO(createdOn)) })} | ||||
| 						: intl.formatMessage({ id: "created-on" }, { date: DateTimeFormat(createdOn) })} | ||||
| 				</div> | ||||
| 			) : null} | ||||
| 		</div> | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| export * from "./CertificateFormatter"; | ||||
| export * from "./DomainsFormatter"; | ||||
| export * from "./EmailFormatter"; | ||||
| export * from "./EventFormatter"; | ||||
| export * from "./GravatarFormatter"; | ||||
| export * from "./RolesFormatter"; | ||||
| export * from "./StatusFormatter"; | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| export * from "./useAccessLists"; | ||||
| export * from "./useAuditLog"; | ||||
| export * from "./useAuditLogs"; | ||||
| export * from "./useDeadHosts"; | ||||
| export * from "./useHealth"; | ||||
| export * from "./useHostReport"; | ||||
|   | ||||
							
								
								
									
										17
									
								
								frontend/src/hooks/useAuditLog.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								frontend/src/hooks/useAuditLog.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| import { useQuery } from "@tanstack/react-query"; | ||||
| import { type AuditLog, getAuditLog } from "src/api/backend"; | ||||
|  | ||||
| const fetchAuditLog = (id: number) => { | ||||
| 	return getAuditLog(id, ["user"]); | ||||
| }; | ||||
|  | ||||
| const useAuditLog = (id: number, options = {}) => { | ||||
| 	return useQuery<AuditLog, Error>({ | ||||
| 		queryKey: ["audit-log", id], | ||||
| 		queryFn: () => fetchAuditLog(id), | ||||
| 		staleTime: 5 * 60 * 1000, // 5 minutes | ||||
| 		...options, | ||||
| 	}); | ||||
| }; | ||||
|  | ||||
| export { useAuditLog }; | ||||
							
								
								
									
										17
									
								
								frontend/src/hooks/useAuditLogs.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								frontend/src/hooks/useAuditLogs.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| import { useQuery } from "@tanstack/react-query"; | ||||
| import { type AuditLog, type AuditLogExpansion, getAuditLogs } from "src/api/backend"; | ||||
|  | ||||
| const fetchAuditLogs = (expand?: AuditLogExpansion[]) => { | ||||
| 	return getAuditLogs(expand); | ||||
| }; | ||||
|  | ||||
| const useAuditLogs = (expand?: AuditLogExpansion[], options = {}) => { | ||||
| 	return useQuery<AuditLog[], Error>({ | ||||
| 		queryKey: ["audit-logs", { expand }], | ||||
| 		queryFn: () => fetchAuditLogs(expand), | ||||
| 		staleTime: 10 * 1000, | ||||
| 		...options, | ||||
| 	}); | ||||
| }; | ||||
|  | ||||
| export { fetchAuditLogs, useAuditLogs }; | ||||
							
								
								
									
										15
									
								
								frontend/src/locale/DateTimeFormat.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								frontend/src/locale/DateTimeFormat.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| import { intlFormat, parseISO } from "date-fns"; | ||||
|  | ||||
| const DateTimeFormat = (isoDate: string) => | ||||
| 	intlFormat(parseISO(isoDate), { | ||||
| 		weekday: "long", | ||||
| 		year: "numeric", | ||||
| 		month: "numeric", | ||||
| 		day: "numeric", | ||||
| 		hour: "numeric", | ||||
| 		minute: "numeric", | ||||
| 		second: "numeric", | ||||
| 		hour12: true, | ||||
| 	}); | ||||
|  | ||||
| export { DateTimeFormat }; | ||||
| @@ -1 +1,2 @@ | ||||
| export * from "./DateTimeFormat"; | ||||
| export * from "./IntlProvider"; | ||||
|   | ||||
| @@ -12,13 +12,16 @@ | ||||
|   "action.edit": "Edit", | ||||
|   "action.enable": "Enable", | ||||
|   "action.permissions": "Permissions", | ||||
|   "action.view-details": "View Details", | ||||
|   "auditlog.title": "Audit Log", | ||||
|   "cancel": "Cancel", | ||||
|   "certificates.title": "SSL Certificates", | ||||
|   "close": "Close", | ||||
|   "column.access": "Access", | ||||
|   "column.authorization": "Authorization", | ||||
|   "column.destination": "Destination", | ||||
|   "column.email": "Email", | ||||
|   "column.event": "Event", | ||||
|   "column.http-code": "Access", | ||||
|   "column.incoming-port": "Incoming Port", | ||||
|   "column.name": "Name", | ||||
| @@ -41,6 +44,9 @@ | ||||
|   "empty-subtitle": "Why don't you create one?", | ||||
|   "error.invalid-auth": "Invalid email or password", | ||||
|   "error.passwords-must-match": "Passwords must match", | ||||
|   "event.created-user": "Created User", | ||||
|   "event.deleted-user": "Deleted User", | ||||
|   "event.updated-user": "Updated User", | ||||
|   "footer.github-fork": "Fork me on Github", | ||||
|   "hosts.title": "Hosts", | ||||
|   "http-only": "HTTP Only", | ||||
|   | ||||
| @@ -41,12 +41,18 @@ | ||||
| 	"auditlog.title": { | ||||
| 		"defaultMessage": "Audit Log" | ||||
| 	}, | ||||
| 	"action.view-details": { | ||||
| 		"defaultMessage": "View Details" | ||||
| 	}, | ||||
| 	"cancel": { | ||||
| 		"defaultMessage": "Cancel" | ||||
| 	}, | ||||
| 	"certificates.title": { | ||||
| 		"defaultMessage": "SSL Certificates" | ||||
| 	}, | ||||
| 	"close": { | ||||
| 		"defaultMessage": "Close" | ||||
| 	}, | ||||
| 	"created-on": { | ||||
| 		"defaultMessage": "Created: {date}" | ||||
| 	}, | ||||
| @@ -62,6 +68,9 @@ | ||||
| 	"column.email": { | ||||
| 		"defaultMessage": "Email" | ||||
| 	}, | ||||
| 	"column.event": { | ||||
| 		"defaultMessage": "Event" | ||||
| 	}, | ||||
| 	"column.http-code": { | ||||
| 		"defaultMessage": "Access" | ||||
| 	}, | ||||
| @@ -122,6 +131,15 @@ | ||||
| 	"error.invalid-auth": { | ||||
| 		"defaultMessage": "Invalid email or password" | ||||
| 	}, | ||||
| 	"event.created-user": { | ||||
| 		"defaultMessage": "Created User" | ||||
| 	}, | ||||
| 	"event.deleted-user": { | ||||
| 		"defaultMessage": "Deleted User" | ||||
| 	}, | ||||
| 	"event.updated-user": { | ||||
| 		"defaultMessage": "Updated User" | ||||
| 	}, | ||||
| 	"footer.github-fork": { | ||||
| 		"defaultMessage": "Fork me on Github" | ||||
| 	}, | ||||
|   | ||||
							
								
								
									
										50
									
								
								frontend/src/modals/EventDetailsModal.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								frontend/src/modals/EventDetailsModal.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | ||||
| import { Alert } from "react-bootstrap"; | ||||
| import Modal from "react-bootstrap/Modal"; | ||||
| import { Button, EventFormatter, GravatarFormatter, Loading } from "src/components"; | ||||
| import { useAuditLog } from "src/hooks"; | ||||
| import { intl } from "src/locale"; | ||||
|  | ||||
| interface Props { | ||||
| 	id: number; | ||||
| 	onClose: () => void; | ||||
| } | ||||
| export function EventDetailsModal({ id, onClose }: Props) { | ||||
| 	const { data, isLoading, error } = useAuditLog(id); | ||||
|  | ||||
| 	return ( | ||||
| 		<Modal show onHide={onClose} animation={false}> | ||||
| 			{!isLoading && error && ( | ||||
| 				<Alert variant="danger" className="m-3"> | ||||
| 					{error?.message || "Unknown error"} | ||||
| 				</Alert> | ||||
| 			)} | ||||
| 			{isLoading && <Loading noLogo />} | ||||
| 			{!isLoading && data && ( | ||||
| 				<> | ||||
| 					<Modal.Header closeButton> | ||||
| 						<Modal.Title>{intl.formatMessage({ id: "action.view-details" })}</Modal.Title> | ||||
| 					</Modal.Header> | ||||
| 					<Modal.Body> | ||||
| 						<div className="row"> | ||||
| 							<div className="col-md-2"> | ||||
| 								<GravatarFormatter url={data.user?.avatar || ""} /> | ||||
| 							</div> | ||||
| 							<div className="col-md-10"> | ||||
| 								<EventFormatter row={data} /> | ||||
| 							</div> | ||||
| 							<hr className="mt-4 mb-3" /> | ||||
| 							<pre> | ||||
| 								<code>{JSON.stringify(data.meta, null, 2)}</code> | ||||
| 							</pre> | ||||
| 						</div> | ||||
| 					</Modal.Body> | ||||
| 					<Modal.Footer> | ||||
| 						<Button data-bs-dismiss="modal" onClick={onClose}> | ||||
| 							{intl.formatMessage({ id: "close" })} | ||||
| 						</Button> | ||||
| 					</Modal.Footer> | ||||
| 				</> | ||||
| 			)} | ||||
| 		</Modal> | ||||
| 	); | ||||
| } | ||||
| @@ -83,7 +83,11 @@ export function PermissionsModal({ userId, onClose }: Props) { | ||||
|  | ||||
| 	return ( | ||||
| 		<Modal show onHide={onClose} animation={false}> | ||||
| 			{!isLoading && error && <Alert variant="danger">{error?.message || "Unknown error"}</Alert>} | ||||
| 			{!isLoading && error && ( | ||||
| 				<Alert variant="danger" className="m-3"> | ||||
| 					{error?.message || "Unknown error"} | ||||
| 				</Alert> | ||||
| 			)} | ||||
| 			{isLoading && <Loading noLogo />} | ||||
| 			{!isLoading && data && ( | ||||
| 				<Formik | ||||
|   | ||||
| @@ -49,7 +49,11 @@ export function UserModal({ userId, onClose }: Props) { | ||||
|  | ||||
| 	return ( | ||||
| 		<Modal show onHide={onClose} animation={false}> | ||||
| 			{!isLoading && error && <Alert variant="danger">{error?.message || "Unknown error"}</Alert>} | ||||
| 			{!isLoading && error && ( | ||||
| 				<Alert variant="danger" className="m-3"> | ||||
| 					{error?.message || "Unknown error"} | ||||
| 				</Alert> | ||||
| 			)} | ||||
| 			{(isLoading || currentIsLoading) && <Loading noLogo />} | ||||
| 			{!isLoading && !currentIsLoading && data && currentUser && ( | ||||
| 				<Formik | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| export * from "./ChangePasswordModal"; | ||||
| export * from "./DeleteConfirmModal"; | ||||
| export * from "./EventDetailsModal"; | ||||
| export * from "./PermissionsModal"; | ||||
| export * from "./SetPasswordModal"; | ||||
| export * from "./UserModal"; | ||||
|   | ||||
							
								
								
									
										74
									
								
								frontend/src/pages/AuditLog/Table.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								frontend/src/pages/AuditLog/Table.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,74 @@ | ||||
| import { createColumnHelper, getCoreRowModel, 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 } from "src/locale"; | ||||
|  | ||||
| interface Props { | ||||
| 	data: AuditLog[]; | ||||
| 	isFetching?: boolean; | ||||
| 	onSelectItem?: (id: number) => void; | ||||
| } | ||||
| export default function Table({ data, isFetching, onSelectItem }: 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.avatar} name={value.name} />; | ||||
| 				}, | ||||
| 				meta: { | ||||
| 					className: "w-1", | ||||
| 				}, | ||||
| 			}), | ||||
| 			columnHelper.accessor((row: AuditLog) => row.user?.name, { | ||||
| 				id: "user.name", | ||||
| 				header: intl.formatMessage({ id: "column.name" }), | ||||
| 			}), | ||||
| 			columnHelper.accessor((row: AuditLog) => row, { | ||||
| 				id: "objectType", | ||||
| 				header: intl.formatMessage({ id: "column.event" }), | ||||
| 				cell: (info: any) => { | ||||
| 					return <EventFormatter row={info.getValue()} />; | ||||
| 				}, | ||||
| 			}), | ||||
| 			columnHelper.display({ | ||||
| 				id: "id", | ||||
| 				cell: (info: any) => { | ||||
| 					return ( | ||||
| 						<button | ||||
| 							type="button" | ||||
| 							className="btn btn-action btn-sm px-1" | ||||
| 							onClick={(e) => { | ||||
| 								e.preventDefault(); | ||||
| 								onSelectItem?.(info.row.original.id); | ||||
| 							}} | ||||
| 						> | ||||
| 							{intl.formatMessage({ id: "action.view-details" })} | ||||
| 						</button> | ||||
| 					); | ||||
| 				}, | ||||
| 				meta: { | ||||
| 					className: "text-end w-1", | ||||
| 				}, | ||||
| 			}), | ||||
| 		], | ||||
| 		[columnHelper, onSelectItem], | ||||
| 	); | ||||
|  | ||||
| 	const tableInstance = useReactTable<AuditLog>({ | ||||
| 		columns, | ||||
| 		data, | ||||
| 		getCoreRowModel: getCoreRowModel(), | ||||
| 		rowCount: data.length, | ||||
| 		meta: { | ||||
| 			isFetching, | ||||
| 		}, | ||||
| 		enableSortingRemoval: false, | ||||
| 	}); | ||||
|  | ||||
| 	return <TableLayout tableInstance={tableInstance} />; | ||||
| } | ||||
							
								
								
									
										53
									
								
								frontend/src/pages/AuditLog/TableWrapper.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								frontend/src/pages/AuditLog/TableWrapper.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | ||||
| import { IconSearch } from "@tabler/icons-react"; | ||||
| import { useState } from "react"; | ||||
| import Alert from "react-bootstrap/Alert"; | ||||
| import { LoadingPage } from "src/components"; | ||||
| import { useAuditLogs } from "src/hooks"; | ||||
| import { intl } from "src/locale"; | ||||
| import { EventDetailsModal } from "src/modals"; | ||||
| import Table from "./Table"; | ||||
|  | ||||
| export default function TableWrapper() { | ||||
| 	const [eventId, setEventId] = useState(0); | ||||
| 	const { isFetching, isLoading, isError, error, data } = useAuditLogs(["user"]); | ||||
|  | ||||
| 	if (isLoading) { | ||||
| 		return <LoadingPage />; | ||||
| 	} | ||||
|  | ||||
| 	if (isError) { | ||||
| 		return <Alert variant="danger">{error?.message || "Unknown error"}</Alert>; | ||||
| 	} | ||||
|  | ||||
| 	return ( | ||||
| 		<div className="card mt-4"> | ||||
| 			<div className="card-status-top bg-purple" /> | ||||
| 			<div className="card-table"> | ||||
| 				<div className="card-header"> | ||||
| 					<div className="row w-full"> | ||||
| 						<div className="col"> | ||||
| 							<h2 className="mt-1 mb-0">{intl.formatMessage({ id: "auditlog.title" })}</h2> | ||||
| 						</div> | ||||
| 						<div className="col-md-auto col-sm-12"> | ||||
| 							<div className="ms-auto d-flex flex-wrap btn-list"> | ||||
| 								<div className="input-group input-group-flat w-auto"> | ||||
| 									<span className="input-group-text input-group-text-sm"> | ||||
| 										<IconSearch size={16} /> | ||||
| 									</span> | ||||
| 									<input | ||||
| 										id="advanced-table-search" | ||||
| 										type="text" | ||||
| 										className="form-control form-control-sm" | ||||
| 										autoComplete="off" | ||||
| 									/> | ||||
| 								</div> | ||||
| 							</div> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 				<Table data={data ?? []} isFetching={isFetching} onSelectItem={setEventId} /> | ||||
| 				{eventId ? <EventDetailsModal id={eventId} onClose={() => setEventId(0)} /> : null} | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	); | ||||
| } | ||||
| @@ -1,10 +1,10 @@ | ||||
| import { HasPermission } from "src/components"; | ||||
| import AuditTable from "./AuditTable"; | ||||
| import TableWrapper from "./TableWrapper"; | ||||
|  | ||||
| const AuditLog = () => { | ||||
| 	return ( | ||||
| 		<HasPermission permission="admin" type="manage" pageLoading loadingNoLogo> | ||||
| 			<AuditTable /> | ||||
| 			<TableWrapper /> | ||||
| 		</HasPermission> | ||||
| 	); | ||||
| }; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user