mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2025-09-15 19:32:35 +00: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