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; | 		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 | 	 * 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 | 	 * 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; | export default router; | ||||||
|   | |||||||
| @@ -1,9 +1,10 @@ | |||||||
| import * as api from "./base"; | import * as api from "./base"; | ||||||
|  | import type { AuditLogExpansion } from "./getAuditLogs"; | ||||||
| import type { AuditLog } from "./models"; | 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({ | 	return await api.get({ | ||||||
| 		url: "/audit-log", | 		url: `/audit-log/${id}`, | ||||||
| 		params: { | 		params: { | ||||||
| 			expand: expand?.join(","), | 			expand: expand?.join(","), | ||||||
| 			...params, | 			...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 "./getAccessList"; | ||||||
| export * from "./getAccessLists"; | export * from "./getAccessLists"; | ||||||
| export * from "./getAuditLog"; | export * from "./getAuditLog"; | ||||||
|  | export * from "./getAuditLogs"; | ||||||
| export * from "./getCertificate"; | export * from "./getCertificate"; | ||||||
| export * from "./getCertificates"; | export * from "./getCertificates"; | ||||||
| export * from "./getDeadHost"; | export * from "./getDeadHost"; | ||||||
|   | |||||||
| @@ -40,6 +40,8 @@ export interface AuditLog { | |||||||
| 	objectId: number; | 	objectId: number; | ||||||
| 	action: string; | 	action: string; | ||||||
| 	meta: Record<string, any>; | 	meta: Record<string, any>; | ||||||
|  | 	// Expansions: | ||||||
|  | 	user?: User; | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface AccessList { | export interface AccessList { | ||||||
|   | |||||||
| @@ -1,5 +1,4 @@ | |||||||
| import { intlFormat, parseISO } from "date-fns"; | import { DateTimeFormat, intl } from "src/locale"; | ||||||
| import { intl } from "src/locale"; |  | ||||||
|  |  | ||||||
| interface Props { | interface Props { | ||||||
| 	domains: string[]; | 	domains: string[]; | ||||||
| @@ -17,7 +16,7 @@ export function DomainsFormatter({ domains, createdOn }: Props) { | |||||||
| 			</div> | 			</div> | ||||||
| 			{createdOn ? ( | 			{createdOn ? ( | ||||||
| 				<div className="text-secondary mt-1"> | 				<div className="text-secondary mt-1"> | ||||||
| 					{intl.formatMessage({ id: "created-on" }, { date: intlFormat(parseISO(createdOn)) })} | 					{intl.formatMessage({ id: "created-on" }, { date: DateTimeFormat(createdOn) })} | ||||||
| 				</div> | 				</div> | ||||||
| 			) : null} | 			) : null} | ||||||
| 		</div> | 		</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 { DateTimeFormat, intl } from "src/locale"; | ||||||
| import { intl } from "src/locale"; |  | ||||||
|  |  | ||||||
| interface Props { | interface Props { | ||||||
| 	value: string; | 	value: string; | ||||||
| @@ -16,7 +15,7 @@ export function ValueWithDateFormatter({ value, createdOn, disabled }: Props) { | |||||||
| 				<div className={`text-secondary mt-1 ${disabled ? "text-red" : ""}`}> | 				<div className={`text-secondary mt-1 ${disabled ? "text-red" : ""}`}> | ||||||
| 					{disabled | 					{disabled | ||||||
| 						? intl.formatMessage({ id: "disabled" }) | 						? intl.formatMessage({ id: "disabled" }) | ||||||
| 						: intl.formatMessage({ id: "created-on" }, { date: intlFormat(parseISO(createdOn)) })} | 						: intl.formatMessage({ id: "created-on" }, { date: DateTimeFormat(createdOn) })} | ||||||
| 				</div> | 				</div> | ||||||
| 			) : null} | 			) : null} | ||||||
| 		</div> | 		</div> | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| export * from "./CertificateFormatter"; | export * from "./CertificateFormatter"; | ||||||
| export * from "./DomainsFormatter"; | export * from "./DomainsFormatter"; | ||||||
| export * from "./EmailFormatter"; | export * from "./EmailFormatter"; | ||||||
|  | export * from "./EventFormatter"; | ||||||
| export * from "./GravatarFormatter"; | export * from "./GravatarFormatter"; | ||||||
| export * from "./RolesFormatter"; | export * from "./RolesFormatter"; | ||||||
| export * from "./StatusFormatter"; | export * from "./StatusFormatter"; | ||||||
|   | |||||||
| @@ -1,4 +1,6 @@ | |||||||
| export * from "./useAccessLists"; | export * from "./useAccessLists"; | ||||||
|  | export * from "./useAuditLog"; | ||||||
|  | export * from "./useAuditLogs"; | ||||||
| export * from "./useDeadHosts"; | export * from "./useDeadHosts"; | ||||||
| export * from "./useHealth"; | export * from "./useHealth"; | ||||||
| export * from "./useHostReport"; | 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"; | export * from "./IntlProvider"; | ||||||
|   | |||||||
| @@ -12,13 +12,16 @@ | |||||||
|   "action.edit": "Edit", |   "action.edit": "Edit", | ||||||
|   "action.enable": "Enable", |   "action.enable": "Enable", | ||||||
|   "action.permissions": "Permissions", |   "action.permissions": "Permissions", | ||||||
|  |   "action.view-details": "View Details", | ||||||
|   "auditlog.title": "Audit Log", |   "auditlog.title": "Audit Log", | ||||||
|   "cancel": "Cancel", |   "cancel": "Cancel", | ||||||
|   "certificates.title": "SSL Certificates", |   "certificates.title": "SSL Certificates", | ||||||
|  |   "close": "Close", | ||||||
|   "column.access": "Access", |   "column.access": "Access", | ||||||
|   "column.authorization": "Authorization", |   "column.authorization": "Authorization", | ||||||
|   "column.destination": "Destination", |   "column.destination": "Destination", | ||||||
|   "column.email": "Email", |   "column.email": "Email", | ||||||
|  |   "column.event": "Event", | ||||||
|   "column.http-code": "Access", |   "column.http-code": "Access", | ||||||
|   "column.incoming-port": "Incoming Port", |   "column.incoming-port": "Incoming Port", | ||||||
|   "column.name": "Name", |   "column.name": "Name", | ||||||
| @@ -41,6 +44,9 @@ | |||||||
|   "empty-subtitle": "Why don't you create one?", |   "empty-subtitle": "Why don't you create one?", | ||||||
|   "error.invalid-auth": "Invalid email or password", |   "error.invalid-auth": "Invalid email or password", | ||||||
|   "error.passwords-must-match": "Passwords must match", |   "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", |   "footer.github-fork": "Fork me on Github", | ||||||
|   "hosts.title": "Hosts", |   "hosts.title": "Hosts", | ||||||
|   "http-only": "HTTP Only", |   "http-only": "HTTP Only", | ||||||
|   | |||||||
| @@ -41,12 +41,18 @@ | |||||||
| 	"auditlog.title": { | 	"auditlog.title": { | ||||||
| 		"defaultMessage": "Audit Log" | 		"defaultMessage": "Audit Log" | ||||||
| 	}, | 	}, | ||||||
|  | 	"action.view-details": { | ||||||
|  | 		"defaultMessage": "View Details" | ||||||
|  | 	}, | ||||||
| 	"cancel": { | 	"cancel": { | ||||||
| 		"defaultMessage": "Cancel" | 		"defaultMessage": "Cancel" | ||||||
| 	}, | 	}, | ||||||
| 	"certificates.title": { | 	"certificates.title": { | ||||||
| 		"defaultMessage": "SSL Certificates" | 		"defaultMessage": "SSL Certificates" | ||||||
| 	}, | 	}, | ||||||
|  | 	"close": { | ||||||
|  | 		"defaultMessage": "Close" | ||||||
|  | 	}, | ||||||
| 	"created-on": { | 	"created-on": { | ||||||
| 		"defaultMessage": "Created: {date}" | 		"defaultMessage": "Created: {date}" | ||||||
| 	}, | 	}, | ||||||
| @@ -62,6 +68,9 @@ | |||||||
| 	"column.email": { | 	"column.email": { | ||||||
| 		"defaultMessage": "Email" | 		"defaultMessage": "Email" | ||||||
| 	}, | 	}, | ||||||
|  | 	"column.event": { | ||||||
|  | 		"defaultMessage": "Event" | ||||||
|  | 	}, | ||||||
| 	"column.http-code": { | 	"column.http-code": { | ||||||
| 		"defaultMessage": "Access" | 		"defaultMessage": "Access" | ||||||
| 	}, | 	}, | ||||||
| @@ -122,6 +131,15 @@ | |||||||
| 	"error.invalid-auth": { | 	"error.invalid-auth": { | ||||||
| 		"defaultMessage": "Invalid email or password" | 		"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": { | 	"footer.github-fork": { | ||||||
| 		"defaultMessage": "Fork me on Github" | 		"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 ( | 	return ( | ||||||
| 		<Modal show onHide={onClose} animation={false}> | 		<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 && <Loading noLogo />} | ||||||
| 			{!isLoading && data && ( | 			{!isLoading && data && ( | ||||||
| 				<Formik | 				<Formik | ||||||
|   | |||||||
| @@ -49,7 +49,11 @@ export function UserModal({ userId, onClose }: Props) { | |||||||
|  |  | ||||||
| 	return ( | 	return ( | ||||||
| 		<Modal show onHide={onClose} animation={false}> | 		<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) && <Loading noLogo />} | ||||||
| 			{!isLoading && !currentIsLoading && data && currentUser && ( | 			{!isLoading && !currentIsLoading && data && currentUser && ( | ||||||
| 				<Formik | 				<Formik | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| export * from "./ChangePasswordModal"; | export * from "./ChangePasswordModal"; | ||||||
| export * from "./DeleteConfirmModal"; | export * from "./DeleteConfirmModal"; | ||||||
|  | export * from "./EventDetailsModal"; | ||||||
| export * from "./PermissionsModal"; | export * from "./PermissionsModal"; | ||||||
| export * from "./SetPasswordModal"; | export * from "./SetPasswordModal"; | ||||||
| export * from "./UserModal"; | 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 { HasPermission } from "src/components"; | ||||||
| import AuditTable from "./AuditTable"; | import TableWrapper from "./TableWrapper"; | ||||||
|  |  | ||||||
| const AuditLog = () => { | const AuditLog = () => { | ||||||
| 	return ( | 	return ( | ||||||
| 		<HasPermission permission="admin" type="manage" pageLoading loadingNoLogo> | 		<HasPermission permission="admin" type="manage" pageLoading loadingNoLogo> | ||||||
| 			<AuditTable /> | 			<TableWrapper /> | ||||||
| 		</HasPermission> | 		</HasPermission> | ||||||
| 	); | 	); | ||||||
| }; | }; | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user