strapi-cms-audit-log 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/CHANGELOG.md +212 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1026 -0
  4. package/dist/admin/App-BLt4lqEM.js +1284 -0
  5. package/dist/admin/App-ou5hy99f.mjs +1266 -0
  6. package/dist/admin/en-B0rPiE2W.mjs +87 -0
  7. package/dist/admin/en-BMlJxq3g.js +87 -0
  8. package/dist/admin/index-BYi8OPTw.js +80 -0
  9. package/dist/admin/index-DdmY-p3Q.mjs +81 -0
  10. package/dist/admin/index.js +4 -0
  11. package/dist/admin/index.mjs +4 -0
  12. package/dist/admin/src/components/AuditLogFilters.d.ts +18 -0
  13. package/dist/admin/src/components/AuditLogTable.d.ts +19 -0
  14. package/dist/admin/src/components/ChangeViewer.d.ts +14 -0
  15. package/dist/admin/src/components/JsonViewer.d.ts +20 -0
  16. package/dist/admin/src/components/PluginIcon.d.ts +3 -0
  17. package/dist/admin/src/components/WidgetDiff.d.ts +19 -0
  18. package/dist/admin/src/hooks/useAuditLogs.d.ts +36 -0
  19. package/dist/admin/src/index.d.ts +3 -0
  20. package/dist/admin/src/pages/App.d.ts +11 -0
  21. package/dist/admin/src/pages/AuditLogDetails.d.ts +10 -0
  22. package/dist/admin/src/pages/AuditLogs.d.ts +3 -0
  23. package/dist/admin/src/permissions.d.ts +26 -0
  24. package/dist/admin/src/pluginId.d.ts +2 -0
  25. package/dist/admin/src/types.d.ts +91 -0
  26. package/dist/admin/src/utils/format.d.ts +41 -0
  27. package/dist/admin/src/utils/getTranslation.d.ts +2 -0
  28. package/dist/admin/src/utils/widgets.d.ts +85 -0
  29. package/dist/server/index.js +1856 -0
  30. package/dist/server/index.mjs +1856 -0
  31. package/dist/server/src/bootstrap.d.ts +14 -0
  32. package/dist/server/src/config/index.d.ts +52 -0
  33. package/dist/server/src/constants.d.ts +140 -0
  34. package/dist/server/src/content-types/audit-log/index.d.ts +86 -0
  35. package/dist/server/src/content-types/audit-log/schema.d.ts +141 -0
  36. package/dist/server/src/content-types/index.d.ts +88 -0
  37. package/dist/server/src/controllers/audit-log.d.ts +31 -0
  38. package/dist/server/src/controllers/index.d.ts +42 -0
  39. package/dist/server/src/destroy.d.ts +19 -0
  40. package/dist/server/src/index.d.ts +328 -0
  41. package/dist/server/src/register.d.ts +31 -0
  42. package/dist/server/src/routes/admin.d.ts +26 -0
  43. package/dist/server/src/routes/index.d.ts +19 -0
  44. package/dist/server/src/services/access.d.ts +51 -0
  45. package/dist/server/src/services/audit.d.ts +25 -0
  46. package/dist/server/src/services/config.d.ts +30 -0
  47. package/dist/server/src/services/context.d.ts +33 -0
  48. package/dist/server/src/services/diff.d.ts +48 -0
  49. package/dist/server/src/services/immutability.d.ts +30 -0
  50. package/dist/server/src/services/index.d.ts +137 -0
  51. package/dist/server/src/services/retention.d.ts +22 -0
  52. package/dist/server/src/services/security.d.ts +54 -0
  53. package/dist/server/src/services/snapshot.d.ts +46 -0
  54. package/dist/server/src/services/tracker.d.ts +39 -0
  55. package/dist/server/src/types/index.d.ts +214 -0
  56. package/dist/server/src/utils/json.d.ts +17 -0
  57. package/dist/server/src/utils/paths.d.ts +34 -0
  58. package/dist/server/src/utils/sanitize.d.ts +16 -0
  59. package/package.json +112 -0
@@ -0,0 +1,1266 @@
1
+ import { jsxs, jsx } from "react/jsx-runtime";
2
+ import { useFetchClient, Page, Layouts, BackButton, useNotification, useQueryParams, useRBAC, SearchInput, Pagination, ConfirmDialog } from "@strapi/strapi/admin";
3
+ import { useParams, useNavigate, Routes, Route } from "react-router-dom";
4
+ import { g as getTranslation, P as PLUGIN_ID, a as PERMISSIONS } from "./index-DdmY-p3Q.mjs";
5
+ import { Box, Flex, Typography, Button, Badge, Divider, Grid, Field, SingleSelect, SingleSelectOption, Combobox, ComboboxOption, TextInput, DatePicker, EmptyStateLayout, Table, Thead, Tr, Th, VisuallyHidden, Tbody, Td, IconButton, Dialog } from "@strapi/design-system";
6
+ import { useIntl } from "react-intl";
7
+ import * as React from "react";
8
+ import { CaretUp, CaretDown, Cross, Eye, Trash } from "@strapi/icons";
9
+ const ACTION_COLORS = {
10
+ create: { background: "success100", text: "success600" },
11
+ update: { background: "primary100", text: "primary600" },
12
+ delete: { background: "danger100", text: "danger600" },
13
+ publish: { background: "success100", text: "success600" },
14
+ unpublish: { background: "warning100", text: "warning600" },
15
+ // Security actions. Green for a granted access, red for a refused one, and
16
+ // warning amber for the changes that alter who is able to do either — a role
17
+ // edit is not a failure, but it is the row a reviewer should stop on.
18
+ "login.success": { background: "success100", text: "success600" },
19
+ "login.failed": { background: "danger100", text: "danger600" },
20
+ logout: { background: "neutral150", text: "neutral700" },
21
+ "access.denied": { background: "danger100", text: "danger600" },
22
+ "admin.user.create": { background: "warning100", text: "warning600" },
23
+ "admin.user.update": { background: "warning100", text: "warning600" },
24
+ "admin.user.delete": { background: "danger100", text: "danger600" },
25
+ "admin.role.create": { background: "warning100", text: "warning600" },
26
+ "admin.role.update": { background: "warning100", text: "warning600" },
27
+ "admin.role.delete": { background: "danger100", text: "danger600" },
28
+ "admin.permission.create": { background: "warning100", text: "warning600" },
29
+ "admin.permission.update": { background: "warning100", text: "warning600" },
30
+ "admin.permission.delete": { background: "danger100", text: "danger600" },
31
+ "media.create": { background: "success100", text: "success600" },
32
+ "media.update": { background: "primary100", text: "primary600" },
33
+ "media.delete": { background: "danger100", text: "danger600" },
34
+ "media-folder.create": { background: "success100", text: "success600" },
35
+ "media-folder.update": { background: "primary100", text: "primary600" },
36
+ "media-folder.delete": { background: "danger100", text: "danger600" }
37
+ };
38
+ const actionColor = (action) => ACTION_COLORS[action] ?? { background: "neutral150", text: "neutral700" };
39
+ const shortContentTypeName = (uid) => {
40
+ const parts = uid.split(".");
41
+ return parts.length > 1 ? parts[parts.length - 1] : uid;
42
+ };
43
+ const formatValue = (value, maxLength = 220) => {
44
+ if (value === null) return "null";
45
+ if (value === void 0) return "empty";
46
+ if (typeof value === "boolean") return value ? "true" : "false";
47
+ if (typeof value === "string") {
48
+ return value.length > maxLength ? `${value.slice(0, maxLength)}…` : value || '""';
49
+ }
50
+ if (typeof value === "number") return String(value);
51
+ try {
52
+ const json = JSON.stringify(value, null, 2);
53
+ return json.length > maxLength ? `${json.slice(0, maxLength)}…` : json;
54
+ } catch {
55
+ return String(value);
56
+ }
57
+ };
58
+ const isStructured = (value) => value !== null && typeof value === "object";
59
+ const formatDate = (value, locale = "en") => {
60
+ if (!value) return "-";
61
+ const date = new Date(value);
62
+ if (Number.isNaN(date.getTime())) return "-";
63
+ return new Intl.DateTimeFormat(locale, {
64
+ day: "2-digit",
65
+ month: "short",
66
+ year: "numeric",
67
+ hour: "2-digit",
68
+ minute: "2-digit"
69
+ }).format(date);
70
+ };
71
+ const formatUser = (log) => log.userName ?? log.userEmail ?? (log.userId ? `#${log.userId}` : log.source);
72
+ const stringify = (value) => {
73
+ if (value === null || value === void 0) return "null";
74
+ try {
75
+ return JSON.stringify(value, null, 2) ?? String(value);
76
+ } catch {
77
+ return "/* value could not be serialised */";
78
+ }
79
+ };
80
+ const JsonViewer = ({ value, label, defaultOpen = false, maxHeight = "420px" }) => {
81
+ const { formatMessage } = useIntl();
82
+ const [isOpen, setIsOpen] = React.useState(defaultOpen);
83
+ const json = React.useMemo(() => stringify(value), [value]);
84
+ const lineCount = React.useMemo(() => json.split("\n").length, [json]);
85
+ return /* @__PURE__ */ jsxs(Box, { children: [
86
+ /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", paddingBottom: 2, children: [
87
+ /* @__PURE__ */ jsx(Typography, { variant: "delta", tag: "h3", children: label }),
88
+ /* @__PURE__ */ jsx(
89
+ Button,
90
+ {
91
+ variant: "tertiary",
92
+ size: "S",
93
+ startIcon: isOpen ? /* @__PURE__ */ jsx(CaretUp, {}) : /* @__PURE__ */ jsx(CaretDown, {}),
94
+ onClick: () => setIsOpen((open) => !open),
95
+ children: isOpen ? formatMessage({ id: getTranslation("json.hide"), defaultMessage: "Hide" }) : formatMessage(
96
+ { id: getTranslation("json.show"), defaultMessage: "Show ({count} lines)" },
97
+ { count: lineCount }
98
+ )
99
+ }
100
+ )
101
+ ] }),
102
+ isOpen ? /* @__PURE__ */ jsx(
103
+ Box,
104
+ {
105
+ background: "neutral100",
106
+ hasRadius: true,
107
+ padding: 4,
108
+ style: { maxHeight, overflow: "auto" },
109
+ borderColor: "neutral200",
110
+ children: /* @__PURE__ */ jsx(
111
+ Typography,
112
+ {
113
+ tag: "pre",
114
+ variant: "pi",
115
+ style: {
116
+ fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace',
117
+ whiteSpace: "pre",
118
+ margin: 0
119
+ },
120
+ children: json
121
+ }
122
+ )
123
+ }
124
+ ) : null
125
+ ] });
126
+ };
127
+ const ChangeRow = ({ path, from, to }) => {
128
+ const { formatMessage } = useIntl();
129
+ const structured = isStructured(from) || isStructured(to);
130
+ return /* @__PURE__ */ jsxs(Box, { paddingTop: 4, paddingBottom: 4, children: [
131
+ /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "neutral600", children: path }),
132
+ /* @__PURE__ */ jsx(Box, { paddingTop: 2, children: /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 2, children: [
133
+ /* @__PURE__ */ jsxs(Box, { background: "danger100", hasRadius: true, padding: 3, children: [
134
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", fontWeight: "bold", textColor: "danger600", children: formatMessage({ id: getTranslation("changes.before"), defaultMessage: "Before" }) }),
135
+ /* @__PURE__ */ jsx(Box, { paddingTop: 1, children: /* @__PURE__ */ jsx(Typography, { tag: "p", style: { whiteSpace: "pre-wrap", wordBreak: "break-word" }, children: formatValue(from) }) })
136
+ ] }),
137
+ /* @__PURE__ */ jsxs(Box, { background: "success100", hasRadius: true, padding: 3, children: [
138
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", fontWeight: "bold", textColor: "success600", children: formatMessage({ id: getTranslation("changes.after"), defaultMessage: "After" }) }),
139
+ /* @__PURE__ */ jsx(Box, { paddingTop: 1, children: /* @__PURE__ */ jsx(Typography, { tag: "p", style: { whiteSpace: "pre-wrap", wordBreak: "break-word" }, children: formatValue(to) }) })
140
+ ] })
141
+ ] }) }),
142
+ structured ? /* @__PURE__ */ jsx(Box, { paddingTop: 3, children: /* @__PURE__ */ jsx(
143
+ JsonViewer,
144
+ {
145
+ label: formatMessage(
146
+ { id: getTranslation("changes.raw"), defaultMessage: "Raw value for {path}" },
147
+ { path }
148
+ ),
149
+ value: { from, to }
150
+ }
151
+ ) }) : null
152
+ ] });
153
+ };
154
+ const ChangeViewer = ({ changes }) => {
155
+ const { formatMessage } = useIntl();
156
+ const entries = React.useMemo(
157
+ () => Object.entries(changes ?? {}).sort(([a], [b]) => a.split(".").length - b.split(".").length || a.localeCompare(b)),
158
+ [changes]
159
+ );
160
+ if (entries.length === 0) {
161
+ return /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: formatMessage({
162
+ id: getTranslation("changes.empty"),
163
+ defaultMessage: "No field-level changes were recorded for this operation. This is expected for creates and deletes, and for updates that re-saved a document without modifying it."
164
+ }) });
165
+ }
166
+ return /* @__PURE__ */ jsxs(Box, { children: [
167
+ /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", paddingBottom: 2, children: [
168
+ /* @__PURE__ */ jsx(Typography, { variant: "delta", tag: "h2", children: formatMessage({ id: getTranslation("changes.title"), defaultMessage: "Changes" }) }),
169
+ /* @__PURE__ */ jsx(Badge, { children: entries.length })
170
+ ] }),
171
+ entries.map(([path, change], index) => /* @__PURE__ */ jsxs(React.Fragment, { children: [
172
+ index > 0 ? /* @__PURE__ */ jsx(Divider, {}) : null,
173
+ /* @__PURE__ */ jsx(ChangeRow, { path, from: change?.from, to: change?.to })
174
+ ] }, path))
175
+ ] });
176
+ };
177
+ const STRUCTURAL_KEYS = /* @__PURE__ */ new Set(["id", "__component", "__temp_key__", "createdAt", "updatedAt"]);
178
+ const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
179
+ const isWidgetArray = (value) => Array.isArray(value) && value.length > 0 && value.every((item) => isObject(item) && typeof item.__component === "string");
180
+ const isEqual = (a, b) => {
181
+ if (a === b) return true;
182
+ if (a == null && b == null) return true;
183
+ if (a == null || b == null) return false;
184
+ if (a instanceof Date || b instanceof Date) {
185
+ const left = a instanceof Date ? a.getTime() : Date.parse(String(a));
186
+ const right = b instanceof Date ? b.getTime() : Date.parse(String(b));
187
+ return Number.isFinite(left) && Number.isFinite(right) && left === right;
188
+ }
189
+ if (Array.isArray(a) || Array.isArray(b)) {
190
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
191
+ return a.every((item, index) => isEqual(item, b[index]));
192
+ }
193
+ if (isObject(a) && isObject(b)) {
194
+ const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
195
+ for (const key of keys) {
196
+ if (STRUCTURAL_KEYS.has(key)) continue;
197
+ if (!isEqual(a[key], b[key])) return false;
198
+ }
199
+ return true;
200
+ }
201
+ return false;
202
+ };
203
+ const diffFields = (before, after, prefix = "") => {
204
+ const paths = [];
205
+ const keys = /* @__PURE__ */ new Set([...Object.keys(before ?? {}), ...Object.keys(after ?? {})]);
206
+ for (const key of keys) {
207
+ if (prefix === "" && STRUCTURAL_KEYS.has(key)) continue;
208
+ const left = before?.[key];
209
+ const right = after?.[key];
210
+ if (isEqual(left, right)) continue;
211
+ const path = prefix ? `${prefix}.${key}` : key;
212
+ if (isObject(left) && isObject(right)) {
213
+ paths.push(...diffFields(left, right, path));
214
+ } else {
215
+ paths.push(path);
216
+ }
217
+ }
218
+ return paths;
219
+ };
220
+ const pairSlots = (before, after) => {
221
+ const length = Math.max(before.length, after.length);
222
+ const slots = [];
223
+ for (let index = 0; index < length; index += 1) {
224
+ const left = before[index] ?? null;
225
+ const right = after[index] ?? null;
226
+ let status;
227
+ if (!left) status = "added";
228
+ else if (!right) status = "removed";
229
+ else if (isEqual(left, right)) status = "unchanged";
230
+ else status = "changed";
231
+ const sameComponent = left && right && left.__component === right.__component;
232
+ slots.push({
233
+ index,
234
+ status,
235
+ component: right?.__component ?? left?.__component ?? "unknown",
236
+ // Only meaningful when the slot held one widget and now holds another —
237
+ // otherwise the header would repeat the same name twice.
238
+ replacedComponent: left && right && !sameComponent ? left.__component : null,
239
+ before: left,
240
+ after: right,
241
+ changedFields: status === "changed" ? diffFields(left, right) : []
242
+ });
243
+ }
244
+ return slots;
245
+ };
246
+ const extractWidgetZones = (before, after) => {
247
+ const zones = [];
248
+ const names = /* @__PURE__ */ new Set([...Object.keys(before ?? {}), ...Object.keys(after ?? {})]);
249
+ for (const name of names) {
250
+ const left = before?.[name];
251
+ const right = after?.[name];
252
+ if (!isWidgetArray(left) && !isWidgetArray(right)) continue;
253
+ const slots = pairSlots(
254
+ isWidgetArray(left) ? left : [],
255
+ isWidgetArray(right) ? right : []
256
+ );
257
+ zones.push({
258
+ name,
259
+ slots,
260
+ changedCount: slots.filter((slot) => slot.status !== "unchanged").length
261
+ });
262
+ }
263
+ return zones.sort((a, b) => a.name.localeCompare(b.name));
264
+ };
265
+ const widgetLabel = (component) => {
266
+ const name = component.includes(".") ? component.slice(component.indexOf(".") + 1) : component;
267
+ return name.split("-").filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
268
+ };
269
+ const widgetRows = (widget, prefix = "") => {
270
+ if (!widget) return [];
271
+ const rows = [];
272
+ for (const [key, value] of Object.entries(widget)) {
273
+ if (prefix === "" && STRUCTURAL_KEYS.has(key)) continue;
274
+ const path = prefix ? `${prefix}.${key}` : key;
275
+ if (isObject(value) && !(value instanceof Date)) {
276
+ const nested = widgetRows(value, path);
277
+ if (nested.length > 0) rows.push(...nested);
278
+ else rows.push({ path, value });
279
+ continue;
280
+ }
281
+ rows.push({ path, value });
282
+ }
283
+ return rows;
284
+ };
285
+ const STATUS_STYLE = {
286
+ changed: { background: "primary100", text: "primary600", border: "primary200" },
287
+ added: { background: "success100", text: "success600", border: "success200" },
288
+ removed: { background: "danger100", text: "danger600", border: "danger200" },
289
+ unchanged: { background: "neutral150", text: "neutral600", border: "neutral200" }
290
+ };
291
+ const COLLAPSE_AFTER_ROWS = 12;
292
+ const WidgetCard = ({ side, widget, changedFields, emptyLabel }) => {
293
+ const { formatMessage } = useIntl();
294
+ const [showAll, setShowAll] = React.useState(false);
295
+ const rows = React.useMemo(() => widgetRows(widget), [widget]);
296
+ const changed = React.useMemo(() => new Set(changedFields), [changedFields]);
297
+ const isBefore = side === "before";
298
+ const tint = isBefore ? { background: "danger100", text: "danger600", border: "danger200" } : { background: "success100", text: "success600", border: "success200" };
299
+ const visible = showAll ? rows : rows.slice(0, COLLAPSE_AFTER_ROWS);
300
+ const hidden = rows.length - visible.length;
301
+ return /* @__PURE__ */ jsxs(
302
+ Box,
303
+ {
304
+ background: tint.background,
305
+ hasRadius: true,
306
+ padding: 4,
307
+ borderColor: tint.border,
308
+ borderWidth: "1px",
309
+ borderStyle: "solid",
310
+ children: [
311
+ /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", paddingBottom: 2, children: [
312
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", fontWeight: "bold", textColor: tint.text, children: isBefore ? formatMessage({ id: getTranslation("widgets.before"), defaultMessage: "Before" }) : formatMessage({ id: getTranslation("widgets.after"), defaultMessage: "After" }) }),
313
+ rows.length > 0 ? /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: formatMessage(
314
+ { id: getTranslation("widgets.fieldCount"), defaultMessage: "{count} fields" },
315
+ { count: rows.length }
316
+ ) }) : null
317
+ ] }),
318
+ widget === null ? /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", style: { fontStyle: "italic" }, children: emptyLabel }) : /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 2, children: [
319
+ visible.map(({ path, value }) => {
320
+ const isChanged = changed.has(path);
321
+ return /* @__PURE__ */ jsxs(Flex, { alignItems: "flex-start", gap: 3, children: [
322
+ /* @__PURE__ */ jsx(Box, { style: { minWidth: "30%", maxWidth: "30%" }, children: /* @__PURE__ */ jsx(
323
+ Typography,
324
+ {
325
+ variant: "pi",
326
+ fontWeight: isChanged ? "bold" : "regular",
327
+ textColor: isChanged ? tint.text : "neutral600",
328
+ style: { wordBreak: "break-word" },
329
+ children: path
330
+ }
331
+ ) }),
332
+ /* @__PURE__ */ jsx(Box, { style: { flex: 1 }, children: /* @__PURE__ */ jsx(
333
+ Typography,
334
+ {
335
+ variant: "pi",
336
+ tag: "p",
337
+ fontWeight: isChanged ? "bold" : "regular",
338
+ textColor: "neutral800",
339
+ style: { whiteSpace: "pre-wrap", wordBreak: "break-word", margin: 0 },
340
+ children: formatValue(value, 320)
341
+ }
342
+ ) })
343
+ ] }, path);
344
+ }),
345
+ hidden > 0 ? /* @__PURE__ */ jsx(Box, { paddingTop: 1, children: /* @__PURE__ */ jsx(Button, { variant: "tertiary", size: "S", onClick: () => setShowAll(true), children: formatMessage(
346
+ {
347
+ id: getTranslation("widgets.showAllFields"),
348
+ defaultMessage: "Show {count} more fields"
349
+ },
350
+ { count: hidden }
351
+ ) }) }) : null
352
+ ] })
353
+ ]
354
+ }
355
+ );
356
+ };
357
+ const WidgetSlotView = ({ slot }) => {
358
+ const { formatMessage } = useIntl();
359
+ const [isOpen, setIsOpen] = React.useState(slot.status !== "unchanged");
360
+ const style = STATUS_STYLE[slot.status];
361
+ const statusLabel = formatMessage({
362
+ id: getTranslation(`widgets.status.${slot.status}`),
363
+ defaultMessage: slot.status
364
+ });
365
+ return /* @__PURE__ */ jsxs(
366
+ Box,
367
+ {
368
+ hasRadius: true,
369
+ background: "neutral0",
370
+ borderColor: style.border,
371
+ borderWidth: "1px",
372
+ borderStyle: "solid",
373
+ padding: 4,
374
+ children: [
375
+ /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", gap: 3, children: [
376
+ /* @__PURE__ */ jsxs(Flex, { alignItems: "center", gap: 2, style: { minWidth: 0 }, children: [
377
+ /* @__PURE__ */ jsx(Badge, { children: `#${slot.index}` }),
378
+ /* @__PURE__ */ jsx(Typography, { variant: "delta", tag: "h4", style: { wordBreak: "break-word" }, children: widgetLabel(slot.component) }),
379
+ /* @__PURE__ */ jsx(
380
+ Typography,
381
+ {
382
+ variant: "pi",
383
+ textColor: "neutral500",
384
+ style: {
385
+ fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace'
386
+ },
387
+ children: slot.component
388
+ }
389
+ )
390
+ ] }),
391
+ /* @__PURE__ */ jsxs(Flex, { alignItems: "center", gap: 2, children: [
392
+ /* @__PURE__ */ jsx(Badge, { backgroundColor: style.background, textColor: style.text, children: statusLabel }),
393
+ /* @__PURE__ */ jsx(
394
+ Button,
395
+ {
396
+ variant: "tertiary",
397
+ size: "S",
398
+ startIcon: isOpen ? /* @__PURE__ */ jsx(CaretUp, {}) : /* @__PURE__ */ jsx(CaretDown, {}),
399
+ onClick: () => setIsOpen((open) => !open),
400
+ children: isOpen ? formatMessage({ id: getTranslation("widgets.hide"), defaultMessage: "Hide" }) : formatMessage({ id: getTranslation("widgets.show"), defaultMessage: "Show" })
401
+ }
402
+ )
403
+ ] })
404
+ ] }),
405
+ slot.replacedComponent ? /* @__PURE__ */ jsx(Box, { paddingTop: 2, children: /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "warning600", children: formatMessage(
406
+ {
407
+ id: getTranslation("widgets.replaced"),
408
+ defaultMessage: "This slot previously held a different widget ({component})."
409
+ },
410
+ { component: slot.replacedComponent }
411
+ ) }) }) : null,
412
+ isOpen ? /* @__PURE__ */ jsx(Box, { paddingTop: 4, children: /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 3, children: [
413
+ slot.status !== "added" ? /* @__PURE__ */ jsx(
414
+ WidgetCard,
415
+ {
416
+ side: "before",
417
+ widget: slot.before,
418
+ changedFields: slot.changedFields,
419
+ emptyLabel: formatMessage({
420
+ id: getTranslation("widgets.noBefore"),
421
+ defaultMessage: "This widget did not exist before the change."
422
+ })
423
+ }
424
+ ) : null,
425
+ slot.status !== "removed" ? /* @__PURE__ */ jsx(
426
+ WidgetCard,
427
+ {
428
+ side: "after",
429
+ widget: slot.after,
430
+ changedFields: slot.changedFields,
431
+ emptyLabel: formatMessage({
432
+ id: getTranslation("widgets.noAfter"),
433
+ defaultMessage: "This widget was removed by the change."
434
+ })
435
+ }
436
+ ) : null,
437
+ /* @__PURE__ */ jsx(
438
+ JsonViewer,
439
+ {
440
+ label: formatMessage(
441
+ { id: getTranslation("widgets.raw"), defaultMessage: "Raw JSON for slot {index}" },
442
+ { index: slot.index }
443
+ ),
444
+ value: { before: slot.before, after: slot.after }
445
+ }
446
+ )
447
+ ] }) }) : null
448
+ ]
449
+ }
450
+ );
451
+ };
452
+ const WidgetDiff = ({ before, after }) => {
453
+ const { formatMessage } = useIntl();
454
+ const [showUnchanged, setShowUnchanged] = React.useState(false);
455
+ const zones = React.useMemo(() => extractWidgetZones(before, after), [before, after]);
456
+ if (zones.length === 0) return null;
457
+ return (
458
+ // The component owns its card rather than being wrapped by the page: it
459
+ // renders nothing when there is no dynamic zone, and a caller-supplied
460
+ // wrapper would leave an empty card behind on every content type without one.
461
+ /* @__PURE__ */ jsxs(Box, { background: "neutral0", hasRadius: true, shadow: "tableShadow", padding: 6, children: [
462
+ /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", paddingBottom: 2, children: [
463
+ /* @__PURE__ */ jsx(Typography, { variant: "delta", tag: "h2", children: formatMessage({ id: getTranslation("widgets.title"), defaultMessage: "Widgets" }) }),
464
+ /* @__PURE__ */ jsx(Button, { variant: "tertiary", size: "S", onClick: () => setShowUnchanged((show) => !show), children: showUnchanged ? formatMessage({
465
+ id: getTranslation("widgets.hideUnchanged"),
466
+ defaultMessage: "Hide unchanged widgets"
467
+ }) : formatMessage({
468
+ id: getTranslation("widgets.showUnchanged"),
469
+ defaultMessage: "Show unchanged widgets"
470
+ }) })
471
+ ] }),
472
+ /* @__PURE__ */ jsx(Box, { paddingBottom: 4, children: /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: formatMessage({
473
+ id: getTranslation("widgets.hint"),
474
+ defaultMessage: "Each changed widget is shown twice — once with the data it held before this operation, and once with the data it holds after. Fields that differ are highlighted in both copies."
475
+ }) }) }),
476
+ /* @__PURE__ */ jsx(Flex, { direction: "column", alignItems: "stretch", gap: 6, children: zones.map((zone) => {
477
+ const visible = showUnchanged ? zone.slots : zone.slots.filter((slot) => slot.status !== "unchanged");
478
+ const hiddenCount = zone.slots.length - visible.length;
479
+ return /* @__PURE__ */ jsxs(Box, { children: [
480
+ /* @__PURE__ */ jsxs(Flex, { alignItems: "center", gap: 2, paddingBottom: 3, children: [
481
+ /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "neutral600", children: zone.name }),
482
+ /* @__PURE__ */ jsx(Badge, { children: formatMessage(
483
+ {
484
+ id: getTranslation("widgets.zoneSummary"),
485
+ defaultMessage: "{changed} changed of {total}"
486
+ },
487
+ { changed: zone.changedCount, total: zone.slots.length }
488
+ ) })
489
+ ] }),
490
+ visible.length === 0 ? /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: formatMessage(
491
+ {
492
+ id: getTranslation("widgets.allUnchanged"),
493
+ defaultMessage: "No widget in this zone changed. {count} unchanged widgets are hidden."
494
+ },
495
+ { count: hiddenCount }
496
+ ) }) : /* @__PURE__ */ jsx(Flex, { direction: "column", alignItems: "stretch", gap: 3, children: visible.map((slot) => /* @__PURE__ */ jsx(WidgetSlotView, { slot }, `${zone.name}-${slot.index}`)) })
497
+ ] }, zone.name);
498
+ }) })
499
+ ] })
500
+ );
501
+ };
502
+ const EMPTY_PAGINATION = { page: 1, pageSize: 20, pageCount: 0, total: 0 };
503
+ const EMPTY_FILTERS = {
504
+ contentTypes: [],
505
+ users: [],
506
+ locales: [],
507
+ actions: [],
508
+ sources: [],
509
+ outcomes: []
510
+ };
511
+ const message = (error) => error?.response?.data?.error?.message ?? error?.message ?? "Something went wrong while loading audit logs.";
512
+ const useAuditLogs = (search) => {
513
+ const { get } = useFetchClient();
514
+ const [data, setData] = React.useState([]);
515
+ const [pagination, setPagination] = React.useState(EMPTY_PAGINATION);
516
+ const [isLoading, setIsLoading] = React.useState(true);
517
+ const [error, setError] = React.useState(null);
518
+ const [refreshToken, setRefreshToken] = React.useState(0);
519
+ const refresh = React.useCallback(() => setRefreshToken((token) => token + 1), []);
520
+ React.useEffect(() => {
521
+ let cancelled = false;
522
+ const load = async () => {
523
+ setIsLoading(true);
524
+ setError(null);
525
+ try {
526
+ const { data: response } = await get(
527
+ `/${PLUGIN_ID}/logs${search ? `?${search}` : ""}`
528
+ );
529
+ if (cancelled) return;
530
+ setData(response.results ?? []);
531
+ setPagination(response.pagination ?? EMPTY_PAGINATION);
532
+ } catch (err) {
533
+ if (cancelled) return;
534
+ setError(message(err));
535
+ setData([]);
536
+ setPagination(EMPTY_PAGINATION);
537
+ } finally {
538
+ if (!cancelled) setIsLoading(false);
539
+ }
540
+ };
541
+ void load();
542
+ return () => {
543
+ cancelled = true;
544
+ };
545
+ }, [get, search, refreshToken]);
546
+ return { data, pagination, isLoading, error, refresh };
547
+ };
548
+ const useAuditFilterOptions = () => {
549
+ const { get } = useFetchClient();
550
+ const [options, setOptions] = React.useState(EMPTY_FILTERS);
551
+ const [isLoading, setIsLoading] = React.useState(true);
552
+ React.useEffect(() => {
553
+ let cancelled = false;
554
+ const load = async () => {
555
+ try {
556
+ const { data } = await get(`/${PLUGIN_ID}/filters`);
557
+ if (!cancelled) setOptions(data.data ?? EMPTY_FILTERS);
558
+ } catch {
559
+ if (!cancelled) setOptions(EMPTY_FILTERS);
560
+ } finally {
561
+ if (!cancelled) setIsLoading(false);
562
+ }
563
+ };
564
+ void load();
565
+ return () => {
566
+ cancelled = true;
567
+ };
568
+ }, [get]);
569
+ return { options, isLoading };
570
+ };
571
+ const useAuditLog = (id) => {
572
+ const { get } = useFetchClient();
573
+ const [data, setData] = React.useState(null);
574
+ const [isLoading, setIsLoading] = React.useState(true);
575
+ const [error, setError] = React.useState(null);
576
+ React.useEffect(() => {
577
+ let cancelled = false;
578
+ if (!id) {
579
+ setError("Missing audit log id.");
580
+ setIsLoading(false);
581
+ return;
582
+ }
583
+ const load = async () => {
584
+ setIsLoading(true);
585
+ setError(null);
586
+ try {
587
+ const { data: response } = await get(`/${PLUGIN_ID}/logs/${id}`);
588
+ if (!cancelled) setData(response.data);
589
+ } catch (err) {
590
+ if (!cancelled) {
591
+ setError(message(err));
592
+ setData(null);
593
+ }
594
+ } finally {
595
+ if (!cancelled) setIsLoading(false);
596
+ }
597
+ };
598
+ void load();
599
+ return () => {
600
+ cancelled = true;
601
+ };
602
+ }, [get, id]);
603
+ return { data, isLoading, error };
604
+ };
605
+ const useDeleteAuditLog = () => {
606
+ const { del } = useFetchClient();
607
+ return React.useCallback(
608
+ async (id) => {
609
+ await del(`/${PLUGIN_ID}/logs/${id}`);
610
+ },
611
+ [del]
612
+ );
613
+ };
614
+ const MetaField = ({ label, value, mono = false }) => /* @__PURE__ */ jsxs(Grid.Item, { col: 4, s: 6, xs: 12, direction: "column", alignItems: "flex-start", gap: 1, children: [
615
+ /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "neutral600", children: label }),
616
+ /* @__PURE__ */ jsx(
617
+ Typography,
618
+ {
619
+ textColor: "neutral800",
620
+ style: {
621
+ wordBreak: "break-word",
622
+ ...mono ? {
623
+ fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace'
624
+ } : {}
625
+ },
626
+ children: value ?? "-"
627
+ }
628
+ )
629
+ ] });
630
+ const AuditLogDetails = () => {
631
+ const { formatMessage, locale } = useIntl();
632
+ const { id } = useParams();
633
+ const { data: log, isLoading, error } = useAuditLog(id);
634
+ if (isLoading) return /* @__PURE__ */ jsx(Page.Loading, {});
635
+ if (error || !log) {
636
+ return /* @__PURE__ */ jsx(Page.Error, { content: error ?? void 0 });
637
+ }
638
+ const colors = actionColor(log.action);
639
+ return /* @__PURE__ */ jsxs(Page.Main, { children: [
640
+ /* @__PURE__ */ jsx(Page.Title, { children: formatMessage({ id: getTranslation("detail.title"), defaultMessage: "Audit Log" }) }),
641
+ /* @__PURE__ */ jsx(
642
+ Layouts.Header,
643
+ {
644
+ navigationAction: /* @__PURE__ */ jsx(BackButton, { fallback: ".." }),
645
+ title: `${log.action} · ${log.contentTypeDisplayName || shortContentTypeName(log.contentType)}`,
646
+ subtitle: formatDate(log.createdAt, locale)
647
+ }
648
+ ),
649
+ /* @__PURE__ */ jsx(Layouts.Content, { children: /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 6, children: [
650
+ /* @__PURE__ */ jsxs(Box, { background: "neutral0", hasRadius: true, shadow: "tableShadow", padding: 6, children: [
651
+ /* @__PURE__ */ jsx(Typography, { variant: "delta", tag: "h2", children: formatMessage({ id: getTranslation("detail.overview"), defaultMessage: "Overview" }) }),
652
+ /* @__PURE__ */ jsx(Box, { paddingTop: 4, children: /* @__PURE__ */ jsxs(Grid.Root, { gap: 5, children: [
653
+ /* @__PURE__ */ jsx(
654
+ MetaField,
655
+ {
656
+ label: formatMessage({
657
+ id: getTranslation("detail.action"),
658
+ defaultMessage: "Action"
659
+ }),
660
+ value: /* @__PURE__ */ jsx(Badge, { backgroundColor: colors.background, textColor: colors.text, children: log.action })
661
+ }
662
+ ),
663
+ /* @__PURE__ */ jsx(
664
+ MetaField,
665
+ {
666
+ label: formatMessage({
667
+ id: getTranslation("detail.outcome"),
668
+ defaultMessage: "Outcome"
669
+ }),
670
+ value: /* @__PURE__ */ jsx(
671
+ Badge,
672
+ {
673
+ backgroundColor: log.outcome === "failure" ? "danger100" : "success100",
674
+ textColor: log.outcome === "failure" ? "danger600" : "success600",
675
+ children: log.outcome ?? "success"
676
+ }
677
+ )
678
+ }
679
+ ),
680
+ /* @__PURE__ */ jsx(
681
+ MetaField,
682
+ {
683
+ label: formatMessage({ id: getTranslation("detail.user"), defaultMessage: "User" }),
684
+ value: formatUser(log)
685
+ }
686
+ ),
687
+ /* @__PURE__ */ jsx(
688
+ MetaField,
689
+ {
690
+ label: formatMessage({
691
+ id: getTranslation("detail.userEmail"),
692
+ defaultMessage: "User email"
693
+ }),
694
+ value: log.userEmail
695
+ }
696
+ ),
697
+ /* @__PURE__ */ jsx(
698
+ MetaField,
699
+ {
700
+ label: formatMessage({ id: getTranslation("detail.date"), defaultMessage: "Date" }),
701
+ value: formatDate(log.createdAt, locale)
702
+ }
703
+ ),
704
+ /* @__PURE__ */ jsx(
705
+ MetaField,
706
+ {
707
+ label: formatMessage({
708
+ id: getTranslation("detail.contentType"),
709
+ defaultMessage: "Content type"
710
+ }),
711
+ value: log.contentType,
712
+ mono: true
713
+ }
714
+ ),
715
+ /* @__PURE__ */ jsx(
716
+ MetaField,
717
+ {
718
+ label: formatMessage({
719
+ id: getTranslation("detail.documentId"),
720
+ defaultMessage: "Document ID"
721
+ }),
722
+ value: log.contentDocumentId,
723
+ mono: true
724
+ }
725
+ ),
726
+ /* @__PURE__ */ jsx(
727
+ MetaField,
728
+ {
729
+ label: formatMessage({
730
+ id: getTranslation("detail.entryId"),
731
+ defaultMessage: "Entry ID"
732
+ }),
733
+ value: log.contentId,
734
+ mono: true
735
+ }
736
+ ),
737
+ /* @__PURE__ */ jsx(
738
+ MetaField,
739
+ {
740
+ label: formatMessage({
741
+ id: getTranslation("detail.locale"),
742
+ defaultMessage: "Locale"
743
+ }),
744
+ value: log.locale
745
+ }
746
+ ),
747
+ /* @__PURE__ */ jsx(
748
+ MetaField,
749
+ {
750
+ label: formatMessage({
751
+ id: getTranslation("detail.source"),
752
+ defaultMessage: "Source"
753
+ }),
754
+ value: log.source
755
+ }
756
+ ),
757
+ /* @__PURE__ */ jsx(
758
+ MetaField,
759
+ {
760
+ label: formatMessage({
761
+ id: getTranslation("detail.ipAddress"),
762
+ defaultMessage: "IP address"
763
+ }),
764
+ value: log.ipAddress,
765
+ mono: true
766
+ }
767
+ ),
768
+ /* @__PURE__ */ jsx(
769
+ MetaField,
770
+ {
771
+ label: formatMessage({
772
+ id: getTranslation("detail.requestId"),
773
+ defaultMessage: "Request ID"
774
+ }),
775
+ value: log.requestId,
776
+ mono: true
777
+ }
778
+ ),
779
+ /* @__PURE__ */ jsx(
780
+ MetaField,
781
+ {
782
+ label: formatMessage({
783
+ id: getTranslation("detail.userAgent"),
784
+ defaultMessage: "User agent"
785
+ }),
786
+ value: log.userAgent,
787
+ mono: true
788
+ }
789
+ )
790
+ ] }) })
791
+ ] }),
792
+ log.metadata && Object.keys(log.metadata).length > 0 ? /* @__PURE__ */ jsxs(Box, { background: "neutral0", hasRadius: true, shadow: "tableShadow", padding: 6, children: [
793
+ /* @__PURE__ */ jsx(Typography, { variant: "delta", tag: "h2", children: formatMessage({
794
+ id: getTranslation("detail.metadata"),
795
+ defaultMessage: "Event detail"
796
+ }) }),
797
+ /* @__PURE__ */ jsx(Box, { paddingTop: 4, children: /* @__PURE__ */ jsx(Grid.Root, { gap: 5, children: Object.entries(log.metadata).map(([key, value]) => /* @__PURE__ */ jsx(
798
+ MetaField,
799
+ {
800
+ label: key,
801
+ value: value === null || value === void 0 ? "-" : typeof value === "object" ? JSON.stringify(value) : String(value),
802
+ mono: key === "path" || key === "attemptedEmail"
803
+ },
804
+ key
805
+ )) }) })
806
+ ] }) : null,
807
+ /* @__PURE__ */ jsx(WidgetDiff, { before: log.before, after: log.after }),
808
+ /* @__PURE__ */ jsx(Box, { background: "neutral0", hasRadius: true, shadow: "tableShadow", padding: 6, children: /* @__PURE__ */ jsx(ChangeViewer, { changes: log.changes }) }),
809
+ /* @__PURE__ */ jsxs(Box, { background: "neutral0", hasRadius: true, shadow: "tableShadow", padding: 6, children: [
810
+ /* @__PURE__ */ jsx(Typography, { variant: "delta", tag: "h2", children: formatMessage({
811
+ id: getTranslation("detail.snapshots"),
812
+ defaultMessage: "Raw snapshots"
813
+ }) }),
814
+ /* @__PURE__ */ jsx(Box, { paddingTop: 2, paddingBottom: 4, children: /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: formatMessage({
815
+ id: getTranslation("detail.snapshotsHint"),
816
+ defaultMessage: "The document state either side of this operation, with configured sensitive fields removed. A missing snapshot means the state did not exist (a create has no before, a delete has no after) or storage for it is disabled in the plugin configuration."
817
+ }) }) }),
818
+ /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 4, children: [
819
+ /* @__PURE__ */ jsx(
820
+ JsonViewer,
821
+ {
822
+ label: formatMessage({
823
+ id: getTranslation("detail.before"),
824
+ defaultMessage: "Before"
825
+ }),
826
+ value: log.before
827
+ }
828
+ ),
829
+ /* @__PURE__ */ jsx(Divider, {}),
830
+ /* @__PURE__ */ jsx(
831
+ JsonViewer,
832
+ {
833
+ label: formatMessage({ id: getTranslation("detail.after"), defaultMessage: "After" }),
834
+ value: log.after
835
+ }
836
+ )
837
+ ] })
838
+ ] })
839
+ ] }) })
840
+ ] });
841
+ };
842
+ const ALL = "__all__";
843
+ const FILTER_KEYS = [
844
+ "action",
845
+ "contentType",
846
+ "userId",
847
+ "locale",
848
+ "source",
849
+ "outcome",
850
+ "contentDocumentId",
851
+ "dateFrom",
852
+ "dateTo"
853
+ ];
854
+ const toDate = (value) => {
855
+ if (!value) return void 0;
856
+ const date = new Date(value);
857
+ return Number.isNaN(date.getTime()) ? void 0 : date;
858
+ };
859
+ const AuditLogFilters = ({ options, query, onChange, onClear }) => {
860
+ const { formatMessage } = useIntl();
861
+ const hasActiveFilter = FILTER_KEYS.some((key) => Boolean(query[key]));
862
+ const select = (key) => (value) => onChange({ [key]: value === ALL ? void 0 : String(value) });
863
+ const combo = (key) => (value) => onChange({ [key]: value ? String(value) : void 0 });
864
+ const dateChange = (key) => (date) => {
865
+ if (!date) {
866
+ onChange({ [key]: void 0 });
867
+ return;
868
+ }
869
+ const bounded = new Date(date);
870
+ if (key === "dateFrom") bounded.setHours(0, 0, 0, 0);
871
+ else bounded.setHours(23, 59, 59, 999);
872
+ onChange({ [key]: bounded.toISOString() });
873
+ };
874
+ return /* @__PURE__ */ jsxs(Box, { background: "neutral0", hasRadius: true, shadow: "tableShadow", padding: 4, marginBottom: 4, children: [
875
+ /* @__PURE__ */ jsxs(Grid.Root, { gap: 4, children: [
876
+ /* @__PURE__ */ jsx(Grid.Item, { col: 3, s: 6, xs: 12, direction: "column", alignItems: "stretch", children: /* @__PURE__ */ jsxs(Field.Root, { name: "action", children: [
877
+ /* @__PURE__ */ jsx(Field.Label, { children: formatMessage({ id: getTranslation("filters.action"), defaultMessage: "Action" }) }),
878
+ /* @__PURE__ */ jsxs(SingleSelect, { value: query.action ?? ALL, onChange: select("action"), children: [
879
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: ALL, children: formatMessage({ id: getTranslation("filters.all"), defaultMessage: "All" }) }),
880
+ options.actions.map((action) => /* @__PURE__ */ jsx(SingleSelectOption, { value: action, children: action }, action))
881
+ ] })
882
+ ] }) }),
883
+ /* @__PURE__ */ jsx(Grid.Item, { col: 3, s: 6, xs: 12, direction: "column", alignItems: "stretch", children: /* @__PURE__ */ jsxs(Field.Root, { name: "contentType", children: [
884
+ /* @__PURE__ */ jsx(Field.Label, { children: formatMessage({
885
+ id: getTranslation("filters.contentType"),
886
+ defaultMessage: "Content type"
887
+ }) }),
888
+ /* @__PURE__ */ jsx(
889
+ Combobox,
890
+ {
891
+ value: query.contentType ?? "",
892
+ onChange: combo("contentType"),
893
+ onClear: () => onChange({ contentType: void 0 }),
894
+ placeholder: formatMessage({
895
+ id: getTranslation("filters.all"),
896
+ defaultMessage: "All"
897
+ }),
898
+ children: options.contentTypes.map(({ uid, displayName }) => /* @__PURE__ */ jsx(ComboboxOption, { value: uid, children: displayName || shortContentTypeName(uid) }, uid))
899
+ }
900
+ )
901
+ ] }) }),
902
+ /* @__PURE__ */ jsx(Grid.Item, { col: 3, s: 6, xs: 12, direction: "column", alignItems: "stretch", children: /* @__PURE__ */ jsxs(Field.Root, { name: "userId", children: [
903
+ /* @__PURE__ */ jsx(Field.Label, { children: formatMessage({ id: getTranslation("filters.user"), defaultMessage: "User" }) }),
904
+ /* @__PURE__ */ jsx(
905
+ Combobox,
906
+ {
907
+ value: query.userId ?? "",
908
+ onChange: combo("userId"),
909
+ onClear: () => onChange({ userId: void 0 }),
910
+ placeholder: formatMessage({
911
+ id: getTranslation("filters.all"),
912
+ defaultMessage: "All"
913
+ }),
914
+ children: options.users.map(({ userId, label }) => /* @__PURE__ */ jsx(ComboboxOption, { value: userId, children: label }, userId))
915
+ }
916
+ )
917
+ ] }) }),
918
+ /* @__PURE__ */ jsx(Grid.Item, { col: 3, s: 6, xs: 12, direction: "column", alignItems: "stretch", children: /* @__PURE__ */ jsxs(Field.Root, { name: "locale", children: [
919
+ /* @__PURE__ */ jsx(Field.Label, { children: formatMessage({ id: getTranslation("filters.locale"), defaultMessage: "Locale" }) }),
920
+ /* @__PURE__ */ jsxs(SingleSelect, { value: query.locale ?? ALL, onChange: select("locale"), children: [
921
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: ALL, children: formatMessage({ id: getTranslation("filters.all"), defaultMessage: "All" }) }),
922
+ options.locales.map((locale) => /* @__PURE__ */ jsx(SingleSelectOption, { value: locale, children: locale }, locale))
923
+ ] })
924
+ ] }) }),
925
+ /* @__PURE__ */ jsx(Grid.Item, { col: 3, s: 6, xs: 12, direction: "column", alignItems: "stretch", children: /* @__PURE__ */ jsxs(Field.Root, { name: "source", children: [
926
+ /* @__PURE__ */ jsx(Field.Label, { children: formatMessage({ id: getTranslation("filters.source"), defaultMessage: "Source" }) }),
927
+ /* @__PURE__ */ jsxs(SingleSelect, { value: query.source ?? ALL, onChange: select("source"), children: [
928
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: ALL, children: formatMessage({ id: getTranslation("filters.all"), defaultMessage: "All" }) }),
929
+ options.sources.map((source) => /* @__PURE__ */ jsx(SingleSelectOption, { value: source, children: source }, source))
930
+ ] })
931
+ ] }) }),
932
+ /* @__PURE__ */ jsx(Grid.Item, { col: 3, s: 6, xs: 12, direction: "column", alignItems: "stretch", children: /* @__PURE__ */ jsxs(Field.Root, { name: "outcome", children: [
933
+ /* @__PURE__ */ jsx(Field.Label, { children: formatMessage({ id: getTranslation("filters.outcome"), defaultMessage: "Outcome" }) }),
934
+ /* @__PURE__ */ jsxs(SingleSelect, { value: query.outcome ?? ALL, onChange: select("outcome"), children: [
935
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: ALL, children: formatMessage({ id: getTranslation("filters.all"), defaultMessage: "All" }) }),
936
+ options.outcomes.map((outcome) => /* @__PURE__ */ jsx(SingleSelectOption, { value: outcome, children: outcome }, outcome))
937
+ ] })
938
+ ] }) }),
939
+ /* @__PURE__ */ jsx(Grid.Item, { col: 3, s: 6, xs: 12, direction: "column", alignItems: "stretch", children: /* @__PURE__ */ jsxs(Field.Root, { name: "contentDocumentId", children: [
940
+ /* @__PURE__ */ jsx(Field.Label, { children: formatMessage({
941
+ id: getTranslation("filters.documentId"),
942
+ defaultMessage: "Document ID"
943
+ }) }),
944
+ /* @__PURE__ */ jsx(
945
+ TextInput,
946
+ {
947
+ placeholder: formatMessage({
948
+ id: getTranslation("filters.documentIdPlaceholder"),
949
+ defaultMessage: "Exact document id"
950
+ }),
951
+ value: query.contentDocumentId ?? "",
952
+ onChange: (event) => onChange({ contentDocumentId: event.target.value || void 0 })
953
+ }
954
+ )
955
+ ] }) }),
956
+ /* @__PURE__ */ jsx(Grid.Item, { col: 3, s: 6, xs: 12, direction: "column", alignItems: "stretch", children: /* @__PURE__ */ jsxs(Field.Root, { name: "dateFrom", children: [
957
+ /* @__PURE__ */ jsx(Field.Label, { children: formatMessage({ id: getTranslation("filters.dateFrom"), defaultMessage: "From" }) }),
958
+ /* @__PURE__ */ jsx(
959
+ DatePicker,
960
+ {
961
+ value: toDate(query.dateFrom),
962
+ onChange: dateChange("dateFrom"),
963
+ onClear: () => onChange({ dateFrom: void 0 }),
964
+ clearLabel: formatMessage({
965
+ id: getTranslation("filters.clearDate"),
966
+ defaultMessage: "Clear date"
967
+ })
968
+ }
969
+ )
970
+ ] }) }),
971
+ /* @__PURE__ */ jsx(Grid.Item, { col: 3, s: 6, xs: 12, direction: "column", alignItems: "stretch", children: /* @__PURE__ */ jsxs(Field.Root, { name: "dateTo", children: [
972
+ /* @__PURE__ */ jsx(Field.Label, { children: formatMessage({ id: getTranslation("filters.dateTo"), defaultMessage: "To" }) }),
973
+ /* @__PURE__ */ jsx(
974
+ DatePicker,
975
+ {
976
+ value: toDate(query.dateTo),
977
+ onChange: dateChange("dateTo"),
978
+ onClear: () => onChange({ dateTo: void 0 }),
979
+ clearLabel: formatMessage({
980
+ id: getTranslation("filters.clearDate"),
981
+ defaultMessage: "Clear date"
982
+ })
983
+ }
984
+ )
985
+ ] }) })
986
+ ] }),
987
+ hasActiveFilter ? /* @__PURE__ */ jsx(Flex, { paddingTop: 4, justifyContent: "flex-end", children: /* @__PURE__ */ jsx(Button, { variant: "tertiary", startIcon: /* @__PURE__ */ jsx(Cross, {}), onClick: onClear, children: formatMessage({
988
+ id: getTranslation("filters.clear"),
989
+ defaultMessage: "Clear all filters"
990
+ }) }) }) : null
991
+ ] });
992
+ };
993
+ const COLUMNS = [
994
+ { key: "createdAt", label: "Date", sortable: true },
995
+ { key: "userName", label: "User", sortable: true },
996
+ { key: "action", label: "Action", sortable: true },
997
+ { key: "contentType", label: "Content type", sortable: true },
998
+ { key: "contentDocumentId", label: "Document ID", sortable: false },
999
+ { key: "locale", label: "Locale", sortable: true },
1000
+ { key: "source", label: "Source", sortable: true },
1001
+ { key: "outcome", label: "Outcome", sortable: true }
1002
+ ];
1003
+ const SortIndicator = ({ direction }) => {
1004
+ if (!direction) return null;
1005
+ return direction === "asc" ? /* @__PURE__ */ jsx(CaretUp, {}) : /* @__PURE__ */ jsx(CaretDown, {});
1006
+ };
1007
+ const AuditLogTable = ({
1008
+ logs,
1009
+ sort,
1010
+ onSortChange,
1011
+ onView,
1012
+ onDelete,
1013
+ isLoading
1014
+ }) => {
1015
+ const { formatMessage, locale } = useIntl();
1016
+ const [sortField, sortDirection] = React.useMemo(() => {
1017
+ const [field, direction] = (sort || "createdAt:desc").split(":");
1018
+ return [field, direction === "asc" ? "asc" : "desc"];
1019
+ }, [sort]);
1020
+ const toggleSort = (key) => {
1021
+ const direction = sortField === key && sortDirection === "desc" ? "asc" : "desc";
1022
+ onSortChange(`${key}:${direction}`);
1023
+ };
1024
+ if (!isLoading && logs.length === 0) {
1025
+ return /* @__PURE__ */ jsx(Box, { background: "neutral0", hasRadius: true, shadow: "tableShadow", padding: 8, children: /* @__PURE__ */ jsx(
1026
+ EmptyStateLayout,
1027
+ {
1028
+ content: formatMessage({
1029
+ id: getTranslation("list.empty"),
1030
+ defaultMessage: "No audit logs match these filters. Records appear here as soon as content is created, updated, deleted, published or unpublished."
1031
+ })
1032
+ }
1033
+ ) });
1034
+ }
1035
+ return /* @__PURE__ */ jsxs(Table, { colCount: COLUMNS.length + 1, rowCount: logs.length + 1, children: [
1036
+ /* @__PURE__ */ jsx(Thead, { children: /* @__PURE__ */ jsxs(Tr, { children: [
1037
+ COLUMNS.map(({ key, label, sortable }) => /* @__PURE__ */ jsx(
1038
+ Th,
1039
+ {
1040
+ action: sortable ? /* @__PURE__ */ jsx(SortIndicator, { direction: sortField === key ? sortDirection : null }) : void 0,
1041
+ children: sortable ? /* @__PURE__ */ jsx(
1042
+ Typography,
1043
+ {
1044
+ variant: "sigma",
1045
+ tag: "button",
1046
+ onClick: () => toggleSort(key),
1047
+ style: { background: "none", border: 0, cursor: "pointer", padding: 0 },
1048
+ children: formatMessage({ id: getTranslation(`list.column.${key}`), defaultMessage: label })
1049
+ }
1050
+ ) : /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: formatMessage({ id: getTranslation(`list.column.${key}`), defaultMessage: label }) })
1051
+ },
1052
+ key
1053
+ )),
1054
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(VisuallyHidden, { children: formatMessage({ id: getTranslation("list.column.actions"), defaultMessage: "Actions" }) }) })
1055
+ ] }) }),
1056
+ /* @__PURE__ */ jsx(Tbody, { children: logs.map((log) => {
1057
+ const colors = actionColor(log.action);
1058
+ return /* @__PURE__ */ jsxs(Tr, { onClick: () => onView(log), style: { cursor: "pointer" }, children: [
1059
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral800", children: formatDate(log.createdAt, locale) }) }),
1060
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral800", children: formatUser(log) }) }),
1061
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Badge, { backgroundColor: colors.background, textColor: colors.text, children: log.action }) }),
1062
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral800", title: log.contentType, children: log.contentTypeDisplayName || shortContentTypeName(log.contentType) }) }),
1063
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: log.contentDocumentId ?? "-" }) }),
1064
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral800", children: log.locale ?? "-" }) }),
1065
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: log.source }) }),
1066
+ /* @__PURE__ */ jsx(Td, { children: log.outcome === "failure" ? /* @__PURE__ */ jsx(Badge, { backgroundColor: "danger100", textColor: "danger600", children: log.outcome }) : /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: log.outcome ?? "success" }) }),
1067
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsxs(Flex, { gap: 1, justifyContent: "flex-end", onClick: (event) => event.stopPropagation(), children: [
1068
+ /* @__PURE__ */ jsx(
1069
+ IconButton,
1070
+ {
1071
+ label: formatMessage({
1072
+ id: getTranslation("list.view"),
1073
+ defaultMessage: "View details"
1074
+ }),
1075
+ variant: "ghost",
1076
+ onClick: () => onView(log),
1077
+ children: /* @__PURE__ */ jsx(Eye, {})
1078
+ }
1079
+ ),
1080
+ onDelete ? /* @__PURE__ */ jsx(
1081
+ IconButton,
1082
+ {
1083
+ label: formatMessage({
1084
+ id: getTranslation("list.delete"),
1085
+ defaultMessage: "Delete this record"
1086
+ }),
1087
+ variant: "ghost",
1088
+ onClick: () => onDelete(log),
1089
+ children: /* @__PURE__ */ jsx(Trash, {})
1090
+ }
1091
+ ) : null
1092
+ ] }) })
1093
+ ] }, log.id);
1094
+ }) })
1095
+ ] });
1096
+ };
1097
+ const toSearchParams = (query) => {
1098
+ const params = new URLSearchParams();
1099
+ for (const [key, value] of Object.entries(query)) {
1100
+ if (value === void 0 || value === null || value === "") continue;
1101
+ params.set(key, String(value));
1102
+ }
1103
+ return params.toString();
1104
+ };
1105
+ const AuditLogs = () => {
1106
+ const { formatMessage } = useIntl();
1107
+ const navigate = useNavigate();
1108
+ const { toggleNotification } = useNotification();
1109
+ const [{ query }, setQuery] = useQueryParams({ page: 1, pageSize: 20 });
1110
+ const {
1111
+ allowedActions: { canDelete },
1112
+ isLoading: isLoadingPermissions
1113
+ } = useRBAC(PERMISSIONS);
1114
+ const search = React.useMemo(() => toSearchParams(query), [query]);
1115
+ const { data, pagination, isLoading, error, refresh } = useAuditLogs(search);
1116
+ const { options } = useAuditFilterOptions();
1117
+ const deleteLog = useDeleteAuditLog();
1118
+ const [pendingDelete, setPendingDelete] = React.useState(null);
1119
+ const updateQuery = React.useCallback(
1120
+ (patch) => {
1121
+ setQuery({ ...patch, page: 1 });
1122
+ },
1123
+ [setQuery]
1124
+ );
1125
+ const clearFilters = React.useCallback(() => {
1126
+ setQuery(
1127
+ {
1128
+ action: void 0,
1129
+ contentType: void 0,
1130
+ userId: void 0,
1131
+ locale: void 0,
1132
+ source: void 0,
1133
+ outcome: void 0,
1134
+ contentDocumentId: void 0,
1135
+ dateFrom: void 0,
1136
+ dateTo: void 0,
1137
+ page: 1
1138
+ },
1139
+ "remove"
1140
+ );
1141
+ }, [setQuery]);
1142
+ const confirmDelete = React.useCallback(async () => {
1143
+ if (!pendingDelete) return;
1144
+ try {
1145
+ await deleteLog(pendingDelete.id);
1146
+ toggleNotification({
1147
+ type: "success",
1148
+ message: formatMessage({
1149
+ id: getTranslation("delete.success"),
1150
+ defaultMessage: "Audit record deleted."
1151
+ })
1152
+ });
1153
+ refresh();
1154
+ } catch {
1155
+ toggleNotification({
1156
+ type: "danger",
1157
+ message: formatMessage({
1158
+ id: getTranslation("delete.error"),
1159
+ defaultMessage: "Could not delete this audit record."
1160
+ })
1161
+ });
1162
+ } finally {
1163
+ setPendingDelete(null);
1164
+ }
1165
+ }, [deleteLog, formatMessage, pendingDelete, refresh, toggleNotification]);
1166
+ if (isLoadingPermissions) {
1167
+ return /* @__PURE__ */ jsx(Page.Loading, {});
1168
+ }
1169
+ return /* @__PURE__ */ jsxs(Page.Main, { children: [
1170
+ /* @__PURE__ */ jsx(Page.Title, { children: formatMessage({ id: getTranslation("plugin.name"), defaultMessage: "Audit Logs" }) }),
1171
+ /* @__PURE__ */ jsx(
1172
+ Layouts.Header,
1173
+ {
1174
+ title: formatMessage({ id: getTranslation("plugin.name"), defaultMessage: "Audit Logs" }),
1175
+ subtitle: formatMessage(
1176
+ {
1177
+ id: getTranslation("list.subtitle"),
1178
+ defaultMessage: "{total, plural, =0 {No records} one {# record} other {# records}} of content changes"
1179
+ },
1180
+ { total: pagination.total }
1181
+ )
1182
+ }
1183
+ ),
1184
+ /* @__PURE__ */ jsx(
1185
+ Layouts.Action,
1186
+ {
1187
+ startActions: /* @__PURE__ */ jsx(
1188
+ SearchInput,
1189
+ {
1190
+ label: formatMessage({
1191
+ id: getTranslation("list.search"),
1192
+ defaultMessage: "Search by document id, user, content type or request id"
1193
+ })
1194
+ }
1195
+ )
1196
+ }
1197
+ ),
1198
+ /* @__PURE__ */ jsxs(Layouts.Content, { children: [
1199
+ /* @__PURE__ */ jsx(
1200
+ AuditLogFilters,
1201
+ {
1202
+ options,
1203
+ query,
1204
+ onChange: updateQuery,
1205
+ onClear: clearFilters
1206
+ }
1207
+ ),
1208
+ error ? /* @__PURE__ */ jsx(Box, { background: "danger100", padding: 4, hasRadius: true, marginBottom: 4, children: /* @__PURE__ */ jsx(Typography, { textColor: "danger600", children: error }) }) : null,
1209
+ isLoading ? /* @__PURE__ */ jsx(Page.Loading, {}) : /* @__PURE__ */ jsx(
1210
+ AuditLogTable,
1211
+ {
1212
+ logs: data,
1213
+ isLoading,
1214
+ sort: String(query.sort ?? "createdAt:desc"),
1215
+ onSortChange: (sort) => setQuery({ sort, page: 1 }),
1216
+ onView: (log) => navigate(`${log.id}`),
1217
+ onDelete: canDelete ? setPendingDelete : null
1218
+ }
1219
+ ),
1220
+ /* @__PURE__ */ jsx(Box, { paddingTop: 4, children: /* @__PURE__ */ jsx(
1221
+ Pagination.Root,
1222
+ {
1223
+ pageCount: pagination.pageCount,
1224
+ total: pagination.total,
1225
+ defaultPageSize: pagination.pageSize,
1226
+ children: /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", children: [
1227
+ /* @__PURE__ */ jsx(Pagination.PageSize, { options: ["10", "20", "50", "100"] }),
1228
+ /* @__PURE__ */ jsx(Pagination.Links, {})
1229
+ ] })
1230
+ }
1231
+ ) })
1232
+ ] }),
1233
+ /* @__PURE__ */ jsx(
1234
+ Dialog.Root,
1235
+ {
1236
+ open: pendingDelete !== null,
1237
+ onOpenChange: (open) => {
1238
+ if (!open) setPendingDelete(null);
1239
+ },
1240
+ children: /* @__PURE__ */ jsx(
1241
+ ConfirmDialog,
1242
+ {
1243
+ title: formatMessage({
1244
+ id: getTranslation("delete.title"),
1245
+ defaultMessage: "Delete audit record"
1246
+ }),
1247
+ onConfirm: confirmDelete,
1248
+ onCancel: () => setPendingDelete(null),
1249
+ children: formatMessage({
1250
+ id: getTranslation("delete.confirm"),
1251
+ defaultMessage: "Audit records are evidence of what happened to your content. Deleting one cannot be undone. Are you sure?"
1252
+ })
1253
+ }
1254
+ )
1255
+ }
1256
+ )
1257
+ ] });
1258
+ };
1259
+ const App = () => /* @__PURE__ */ jsx(Page.Protect, { permissions: PERMISSIONS.read, children: /* @__PURE__ */ jsxs(Routes, { children: [
1260
+ /* @__PURE__ */ jsx(Route, { index: true, element: /* @__PURE__ */ jsx(AuditLogs, {}) }),
1261
+ /* @__PURE__ */ jsx(Route, { path: ":id", element: /* @__PURE__ */ jsx(AuditLogDetails, {}) }),
1262
+ /* @__PURE__ */ jsx(Route, { path: "*", element: /* @__PURE__ */ jsx(Page.Error, {}) })
1263
+ ] }) });
1264
+ export {
1265
+ App
1266
+ };