Migrate Audit evidence interfaces

This commit is contained in:
2026-08-03 10:43:24 +02:00
parent 4177287b22
commit 6d3fcc1572
9 changed files with 268 additions and 25 deletions
+101 -20
View File
@@ -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));
+50
View File
@@ -0,0 +1,50 @@
import type { PlatformTranslations } from "@govoplan/core-webui";
export const generatedTranslations: PlatformTranslations = {
en: {
"i18n:govoplan-audit.action.f1a20801": "Action",
"i18n:govoplan-audit.actions.f1a20802": "Actions",
"i18n:govoplan-audit.actor.f1a20803": "Actor",
"i18n:govoplan-audit.close.f1a20804": "Close",
"i18n:govoplan-audit.details.f1a20805": "Detail",
"i18n:govoplan-audit.audit_event_details.f1a20806": "Audit event details",
"i18n:govoplan-audit.inspect_audit_event.f1a20807": "Inspect audit event",
"i18n:govoplan-audit.audit_evidence_is_loading.f1a20808": "Audit evidence is loading.",
"i18n:govoplan-audit.no_additional_details_were_recorded.f1a20809": "No additional details were recorded.",
"i18n:govoplan-audit.no_audit_records_match_the_current_scope_and_filters.f1a20810": "No audit records match the current scope and filters.",
"i18n:govoplan-audit.object.f1a20811": "Object",
"i18n:govoplan-audit.reload_audit_evidence.f1a20812": "Reload audit evidence",
"i18n:govoplan-audit.scope.f1a20813": "Scope",
"i18n:govoplan-audit.system.f1a20814": "System",
"i18n:govoplan-audit.system_audit.f1a20815": "System audit",
"i18n:govoplan-audit.system_level_administrative_history_showing_value0_value1_of_value2.f1a20816": "System-level administrative history, showing {value0}-{value1} of {value2}.",
"i18n:govoplan-audit.tenant_audit.f1a20817": "Tenant audit",
"i18n:govoplan-audit.tenant_context.f1a20818": "Tenant context",
"i18n:govoplan-audit.tenant_level_administrative_history_showing_value0_value1_of_value2.f1a20819": "Tenant-level administrative history for the active tenant, showing {value0}-{value1} of {value2}.",
"i18n:govoplan-audit.time.f1a20820": "Time",
"i18n:govoplan-audit.value.f1a20821": "Value"
},
de: {
"i18n:govoplan-audit.action.f1a20801": "Aktion",
"i18n:govoplan-audit.actions.f1a20802": "Aktionen",
"i18n:govoplan-audit.actor.f1a20803": "Akteur",
"i18n:govoplan-audit.close.f1a20804": "Schließen",
"i18n:govoplan-audit.details.f1a20805": "Detail",
"i18n:govoplan-audit.audit_event_details.f1a20806": "Details des Auditereignisses",
"i18n:govoplan-audit.inspect_audit_event.f1a20807": "Auditereignis prüfen",
"i18n:govoplan-audit.audit_evidence_is_loading.f1a20808": "Auditnachweise werden geladen.",
"i18n:govoplan-audit.no_additional_details_were_recorded.f1a20809": "Es wurden keine zusätzlichen Details aufgezeichnet.",
"i18n:govoplan-audit.no_audit_records_match_the_current_scope_and_filters.f1a20810": "Keine Auditaufzeichnungen entsprechen dem aktuellen Bereich und den Filtern.",
"i18n:govoplan-audit.object.f1a20811": "Objekt",
"i18n:govoplan-audit.reload_audit_evidence.f1a20812": "Auditnachweise neu laden",
"i18n:govoplan-audit.scope.f1a20813": "Geltungsbereich",
"i18n:govoplan-audit.system.f1a20814": "System",
"i18n:govoplan-audit.system_audit.f1a20815": "Systemaudit",
"i18n:govoplan-audit.system_level_administrative_history_showing_value0_value1_of_value2.f1a20816": "Systemweite administrative Historie, angezeigt werden {value0}-{value1} von {value2}.",
"i18n:govoplan-audit.tenant_audit.f1a20817": "Mandantenaudit",
"i18n:govoplan-audit.tenant_context.f1a20818": "Mandantenkontext",
"i18n:govoplan-audit.tenant_level_administrative_history_showing_value0_value1_of_value2.f1a20819": "Administrative Historie des aktiven Mandanten, angezeigt werden {value0}-{value1} von {value2}.",
"i18n:govoplan-audit.time.f1a20820": "Zeit",
"i18n:govoplan-audit.value.f1a20821": "Wert"
}
};
+9 -3
View File
@@ -1,7 +1,12 @@
import { createElement, lazy } from "react";
import { type AdminSectionsUiCapability, type PlatformWebModule } from "@govoplan/core-webui";
import { generatedTranslations } from "./i18n/generatedTranslations";
const AdminAuditPanel = lazy(() => import("./features/audit/AdminAuditPanel"));
const translations = {
en: generatedTranslations.en,
de: generatedTranslations.de
};
const auditAdminSections: AdminSectionsUiCapability = {
sections: [
@@ -10,7 +15,7 @@ const auditAdminSections: AdminSectionsUiCapability = {
moduleId: "audit",
kind: "management",
surfaceId: "audit.admin.system",
label: "Audit",
label: "i18n:govoplan-audit.system_audit.f1a20815",
group: "SYSTEM",
order: 90,
allOf: ["system:audit:read"],
@@ -25,7 +30,7 @@ const auditAdminSections: AdminSectionsUiCapability = {
moduleId: "audit",
kind: "management",
surfaceId: "audit.admin.tenant",
label: "Audit",
label: "i18n:govoplan-audit.tenant_audit.f1a20817",
group: "TENANT",
order: 100,
allOf: ["audit:read"],
@@ -41,12 +46,13 @@ const auditAdminSections: AdminSectionsUiCapability = {
export const auditModule: PlatformWebModule = {
id: "audit",
label: "Audit",
version: "0.1.6",
version: "0.1.8",
dependencies: ["access", "admin"],
viewSurfaces: [
{ id: "audit.admin.system", moduleId: "audit", kind: "section", label: "System audit", order: 90 },
{ id: "audit.admin.tenant", moduleId: "audit", kind: "section", label: "Tenant audit", order: 100 }
],
translations,
uiCapabilities: {
"admin.sections": auditAdminSections
}