Migrate Audit evidence interfaces
This commit is contained in:
@@ -6,7 +6,9 @@ import {
|
||||
Button,
|
||||
DataGrid,
|
||||
Dialog,
|
||||
DocumentationHelpLink,
|
||||
formatAdminDateTime as formatDateTime,
|
||||
i18nMessage,
|
||||
mergeDeltaRows,
|
||||
TableActionGroup,
|
||||
useDeltaWatermarks,
|
||||
@@ -22,6 +24,35 @@ type Props = {
|
||||
auth: AuthInfo;
|
||||
systemMode?: boolean;
|
||||
};
|
||||
type AuditDetailRow = {
|
||||
id: string;
|
||||
field: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
const I18N = {
|
||||
actionLabel: "i18n:govoplan-audit.action.f1a20801",
|
||||
actions: "i18n:govoplan-audit.actions.f1a20802",
|
||||
actor: "i18n:govoplan-audit.actor.f1a20803",
|
||||
close: "i18n:govoplan-audit.close.f1a20804",
|
||||
details: "i18n:govoplan-audit.details.f1a20805",
|
||||
eventDetails: "i18n:govoplan-audit.audit_event_details.f1a20806",
|
||||
inspect: "i18n:govoplan-audit.inspect_audit_event.f1a20807",
|
||||
loading: "i18n:govoplan-audit.audit_evidence_is_loading.f1a20808",
|
||||
noDetails: "i18n:govoplan-audit.no_additional_details_were_recorded.f1a20809",
|
||||
noRecords: "i18n:govoplan-audit.no_audit_records_match_the_current_scope_and_filters.f1a20810",
|
||||
object: "i18n:govoplan-audit.object.f1a20811",
|
||||
reload: "i18n:govoplan-audit.reload_audit_evidence.f1a20812",
|
||||
scopeLabel: "i18n:govoplan-audit.scope.f1a20813",
|
||||
system: "i18n:govoplan-audit.system.f1a20814",
|
||||
systemAudit: "i18n:govoplan-audit.system_audit.f1a20815",
|
||||
systemDescription: "i18n:govoplan-audit.system_level_administrative_history_showing_value0_value1_of_value2.f1a20816",
|
||||
tenantAudit: "i18n:govoplan-audit.tenant_audit.f1a20817",
|
||||
tenantContext: "i18n:govoplan-audit.tenant_context.f1a20818",
|
||||
tenantDescription: "i18n:govoplan-audit.tenant_level_administrative_history_showing_value0_value1_of_value2.f1a20819",
|
||||
time: "i18n:govoplan-audit.time.f1a20820",
|
||||
value: "i18n:govoplan-audit.value.f1a20821"
|
||||
} as const;
|
||||
|
||||
const DEFAULT_QUERY: DataGridQueryState = {
|
||||
sort: { columnId: "time", direction: "desc" },
|
||||
@@ -113,28 +144,49 @@ export default function AdminAuditPanel({ settings, auth, systemMode = false }:
|
||||
}, []);
|
||||
|
||||
const columns = useMemo<DataGridColumn<AuditAdminItem>[]>(() => [
|
||||
{ id: "time", header: "Time", width: 190, minWidth: 150, maxWidth: 260, resizable: true, sticky: "start", sortable: true, filterable: true, filterType: "date", value: (row) => row.created_at, render: (row) => formatDateTime(row.created_at) },
|
||||
{ id: "actor", header: "Actor", width: 220, minWidth: 170, maxWidth: 360, resizable: true, sortable: true, filterable: true, value: (row) => row.actor_email || "System" },
|
||||
{ id: "action", header: "Action", width: 250, minWidth: 170, maxWidth: 420, resizable: true, sortable: true, filterable: true, value: (row) => row.action },
|
||||
{ id: "object", header: "Object", width: 300, minWidth: 180, maxWidth: 640, resizable: true, fill: true, sortable: true, filterable: true, value: (row) => `${row.object_type || "-"} ${row.object_id || ""}`.trim() },
|
||||
...(systemMode ? [{ id: "tenant", header: "Tenant context", width: 190, minWidth: 150, maxWidth: 300, resizable: true, sortable: true, filterable: true, value: (row: AuditAdminItem) => row.tenant_id || "-" }] : []),
|
||||
{ id: "actions", header: "Actions", width: 70, sticky: "end", resizable: false, align: "right", render: (row) => <TableActionGroup actions={[{ id: "inspect", label: "Inspect audit event", icon: <Search />, onClick: () => setSelected(row) }]} /> }
|
||||
{ id: "time", header: I18N.time, width: 190, minWidth: 150, maxWidth: 260, resizable: true, sticky: "start", sortable: true, filterable: true, filterType: "date", value: (row) => row.created_at, render: (row) => formatDateTime(row.created_at) },
|
||||
{ id: "actor", header: I18N.actor, width: 220, minWidth: 170, maxWidth: 360, resizable: true, sortable: true, filterable: true, value: (row) => row.actor_email || "System", render: (row) => row.actor_email || I18N.system },
|
||||
{ id: "action", header: I18N.actionLabel, width: 250, minWidth: 170, maxWidth: 420, resizable: true, sortable: true, filterable: true, value: (row) => row.action },
|
||||
{ id: "object", header: I18N.object, width: 300, minWidth: 180, maxWidth: 640, resizable: true, fill: true, sortable: true, filterable: true, value: (row) => `${row.object_type || "-"} ${row.object_id || ""}`.trim() },
|
||||
...(systemMode ? [{ id: "tenant", header: I18N.tenantContext, width: 190, minWidth: 150, maxWidth: 300, resizable: true, sortable: true, filterable: true, value: (row: AuditAdminItem) => row.tenant_id || "-" }] : []),
|
||||
{ id: "actions", header: I18N.actions, width: 70, sticky: "end", resizable: false, align: "right", render: (row) => <TableActionGroup actions={[{ id: "inspect", label: I18N.inspect, icon: <Search aria-hidden="true" size={16} />, onClick: () => setSelected(row) }]} /> }
|
||||
], [systemMode]);
|
||||
|
||||
const detailColumns = useMemo<DataGridColumn<AuditDetailRow>[]>(() => [
|
||||
{ id: "field", header: I18N.details, minWidth: 180, resizable: true, value: (row) => row.field },
|
||||
{ id: "value", header: I18N.value, minWidth: 260, resizable: true, fill: true, value: (row) => row.value }
|
||||
], []);
|
||||
const detailRows = useMemo(() => auditDetailRows(selected?.details ?? {}), [selected]);
|
||||
|
||||
const firstShown = total === 0 ? 0 : (page - 1) * pageSize + 1;
|
||||
const lastShown = Math.min(total, page * pageSize);
|
||||
const pageDescription = systemMode
|
||||
? `System-level administrative history, showing ${firstShown}-${lastShown} of ${total}.`
|
||||
: `Tenant-level administrative history for the active tenant, showing ${firstShown}-${lastShown} of ${total}.`;
|
||||
const pageDescription = i18nMessage(
|
||||
systemMode ? I18N.systemDescription : I18N.tenantDescription,
|
||||
{ value0: firstShown, value1: lastShown, value2: total }
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AdminPageLayout
|
||||
title={systemMode ? "System audit" : "Tenant audit"}
|
||||
title={systemMode ? I18N.systemAudit : I18N.tenantAudit}
|
||||
description={pageDescription}
|
||||
loading={loading}
|
||||
error={error}
|
||||
actions={<Button onClick={() => setReloadToken((value) => value + 1)} disabled={loading}>Reload</Button>}>
|
||||
actions={(
|
||||
<>
|
||||
<DocumentationHelpLink
|
||||
reference={{
|
||||
topicId: "audit.read-authorized-evidence",
|
||||
documentationType: "user"
|
||||
}} />
|
||||
<Button
|
||||
onClick={() => setReloadToken((value) => value + 1)}
|
||||
disabled={loading}
|
||||
disabledReason={loading ? I18N.loading : undefined}>
|
||||
{I18N.reload}
|
||||
</Button>
|
||||
</>
|
||||
)}>
|
||||
<div className="admin-table-surface">
|
||||
<DataGrid
|
||||
id={systemMode ? "admin-system-audit-v6" : "admin-tenant-audit-v6"}
|
||||
@@ -142,7 +194,7 @@ export default function AdminAuditPanel({ settings, auth, systemMode = false }:
|
||||
columns={columns}
|
||||
initialFit="container"
|
||||
getRowKey={(row) => row.id}
|
||||
emptyText="No administrative audit records found."
|
||||
emptyText={I18N.noRecords}
|
||||
className="admin-audit-grid"
|
||||
initialSort={{ columnId: "time", direction: "desc" }}
|
||||
pagination={{
|
||||
@@ -159,18 +211,30 @@ export default function AdminAuditPanel({ settings, auth, systemMode = false }:
|
||||
/>
|
||||
</div>
|
||||
</AdminPageLayout>
|
||||
<Dialog open={Boolean(selected)} title="Audit event details" onClose={() => setSelected(null)} className="admin-dialog admin-dialog-wide" footer={<Button onClick={() => setSelected(null)}>Close</Button>}>
|
||||
<Dialog
|
||||
open={Boolean(selected)}
|
||||
title={I18N.eventDetails}
|
||||
onClose={() => setSelected(null)}
|
||||
className="admin-dialog admin-dialog-wide"
|
||||
footer={<Button onClick={() => setSelected(null)}>{I18N.close}</Button>}>
|
||||
{selected && (
|
||||
<>
|
||||
<dl className="admin-details-grid">
|
||||
<div><dt>Scope</dt><dd>{selected.scope}</dd></div>
|
||||
<div><dt>Action</dt><dd>{selected.action}</dd></div>
|
||||
<div><dt>Actor</dt><dd>{selected.actor_email || "System"}</dd></div>
|
||||
<div><dt>Object</dt><dd>{selected.object_type || "-"} {selected.object_id || ""}</dd></div>
|
||||
<div><dt>Tenant context</dt><dd>{selected.tenant_id || "-"}</dd></div>
|
||||
<div><dt>Time</dt><dd>{formatDateTime(selected.created_at)}</dd></div>
|
||||
<div><dt>{I18N.scopeLabel}</dt><dd>{selected.scope}</dd></div>
|
||||
<div><dt>{I18N.actionLabel}</dt><dd>{selected.action}</dd></div>
|
||||
<div><dt>{I18N.actor}</dt><dd>{selected.actor_email || I18N.system}</dd></div>
|
||||
<div><dt>{I18N.object}</dt><dd>{selected.object_type || "-"} {selected.object_id || ""}</dd></div>
|
||||
<div><dt>{I18N.tenantContext}</dt><dd>{selected.tenant_id || "-"}</dd></div>
|
||||
<div><dt>{I18N.time}</dt><dd>{formatDateTime(selected.created_at)}</dd></div>
|
||||
</dl>
|
||||
<pre className="admin-json-preview">{JSON.stringify(selected.details, null, 2)}</pre>
|
||||
{detailRows.length ? (
|
||||
<DataGrid
|
||||
id="admin-audit-event-details-grid"
|
||||
rows={detailRows}
|
||||
columns={detailColumns}
|
||||
getRowKey={(row) => row.id}
|
||||
initialFit="container" />
|
||||
) : <p className="muted">{I18N.noDetails}</p>}
|
||||
</>
|
||||
)}
|
||||
</Dialog>
|
||||
@@ -178,6 +242,23 @@ export default function AdminAuditPanel({ settings, auth, systemMode = false }:
|
||||
);
|
||||
}
|
||||
|
||||
function auditDetailRows(details: Record<string, unknown>): AuditDetailRow[] {
|
||||
return Object.entries(details)
|
||||
.sort(([left], [right]) => left.localeCompare(right))
|
||||
.map(([field, value]) => ({
|
||||
id: field,
|
||||
field,
|
||||
value: auditDetailValue(value)
|
||||
}));
|
||||
}
|
||||
|
||||
function auditDetailValue(value: unknown): string {
|
||||
if (value === null || value === undefined) return "-";
|
||||
if (typeof value === "string") return value;
|
||||
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
function compareAuditEvents(sortBy: AuditSortBy, sortDirection: "asc" | "desc"): (left: AuditAdminItem, right: AuditAdminItem) => number {
|
||||
return (left, right) => {
|
||||
const primary = compareAuditValues(auditSortValue(left, sortBy), auditSortValue(right, sortBy));
|
||||
|
||||
Reference in New Issue
Block a user