@burdenoff/microfe-admin 2026.624.1 → 2026.626.1
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.
- package/dist/admin/components/AccessDenied.js +2 -2
- package/dist/admin/components/AccessDenied.js.map +1 -1
- package/dist/admin/components/ActivityFeed.js +4 -4
- package/dist/admin/components/ActivityFeed.js.map +1 -1
- package/dist/admin/components/Charts.js.map +1 -1
- package/dist/admin/components/ConfirmDialog.js +1 -1
- package/dist/admin/components/ConfirmDialog.js.map +1 -1
- package/dist/admin/components/DataTable.js +5 -5
- package/dist/admin/components/DataTable.js.map +1 -1
- package/dist/admin/components/FormSheet.js +1 -1
- package/dist/admin/components/FormSheet.js.map +1 -1
- package/dist/admin/components/MetricCard.js +40 -37
- package/dist/admin/components/MetricCard.js.map +1 -1
- package/dist/admin/components/PageLayout.js +28 -22
- package/dist/admin/components/PageLayout.js.map +1 -1
- package/dist/admin/components/TenantSsoPanel.js +1 -1
- package/dist/admin/components/TenantSsoPanel.js.map +1 -1
- package/dist/admin/components/Toast.js +11 -11
- package/dist/admin/components/Toast.js.map +1 -1
- package/dist/admin/components/forms/SubscriptionForm.js +2 -2
- package/dist/admin/components/forms/SubscriptionForm.js.map +1 -1
- package/dist/admin/components/forms/TenantForm.js +1 -1
- package/dist/admin/components/forms/UserForm.js +1 -1
- package/dist/admin/components/ui.js +48 -41
- package/dist/admin/components/ui.js.map +1 -1
- package/dist/admin/constants/index.js +33 -24
- package/dist/admin/constants/index.js.map +1 -1
- package/dist/admin/pages/AuditLogsPage.js +94 -77
- package/dist/admin/pages/AuditLogsPage.js.map +1 -1
- package/dist/admin/pages/BillingPage.js +70 -18
- package/dist/admin/pages/BillingPage.js.map +1 -1
- package/dist/admin/pages/CommunicationsPage.js +98 -90
- package/dist/admin/pages/CommunicationsPage.js.map +1 -1
- package/dist/admin/pages/ContentPagesPage.js +276 -259
- package/dist/admin/pages/ContentPagesPage.js.map +1 -1
- package/dist/admin/pages/DashboardPage.js +114 -82
- package/dist/admin/pages/DashboardPage.js.map +1 -1
- package/dist/admin/pages/FeatureFlagsPage.js +94 -74
- package/dist/admin/pages/FeatureFlagsPage.js.map +1 -1
- package/dist/admin/pages/OrganizationsListPage.js +106 -86
- package/dist/admin/pages/OrganizationsListPage.js.map +1 -1
- package/dist/admin/pages/ProductsPage.js +139 -128
- package/dist/admin/pages/ProductsPage.js.map +1 -1
- package/dist/admin/pages/ReferralsPage.js +74 -52
- package/dist/admin/pages/ReferralsPage.js.map +1 -1
- package/dist/admin/pages/RolesPermissionsPage.js +137 -116
- package/dist/admin/pages/RolesPermissionsPage.js.map +1 -1
- package/dist/admin/pages/SREPage.js +38 -32
- package/dist/admin/pages/SREPage.js.map +1 -1
- package/dist/admin/pages/SandboxSettingsPage.js +361 -322
- package/dist/admin/pages/SandboxSettingsPage.js.map +1 -1
- package/dist/admin/pages/SettingsPage.js +145 -130
- package/dist/admin/pages/SettingsPage.js.map +1 -1
- package/dist/admin/pages/SyncFailuresPage.js +123 -118
- package/dist/admin/pages/SyncFailuresPage.js.map +1 -1
- package/dist/admin/pages/TenantDetailPage.js +261 -218
- package/dist/admin/pages/TenantDetailPage.js.map +1 -1
- package/dist/admin/pages/TenantFormRoutePage.js +65 -61
- package/dist/admin/pages/TenantFormRoutePage.js.map +1 -1
- package/dist/admin/pages/TenantsListPage.js +88 -71
- package/dist/admin/pages/TenantsListPage.js.map +1 -1
- package/dist/admin/pages/UsersListPage.js +129 -112
- package/dist/admin/pages/UsersListPage.js.map +1 -1
- package/dist/admin/pages/WaitlistPage.js +80 -63
- package/dist/admin/pages/WaitlistPage.js.map +1 -1
- package/dist/admin/pages/WorkflowsPage.js +108 -100
- package/dist/admin/pages/WorkflowsPage.js.map +1 -1
- package/dist/admin/pages/WorkspacesListPage.js +62 -49
- package/dist/admin/pages/WorkspacesListPage.js.map +1 -1
- package/dist/node_modules/@hookform/resolvers/dist/resolvers.js +2 -2
- package/dist/node_modules/@hookform/resolvers/dist/resolvers.js.map +1 -1
- package/dist/node_modules/@hookform/resolvers/zod/dist/zod.js +35 -26
- package/dist/node_modules/@hookform/resolvers/zod/dist/zod.js.map +1 -1
- package/dist/node_modules/react-hook-form/dist/index.esm.js +609 -516
- package/dist/node_modules/react-hook-form/dist/index.esm.js.map +1 -1
- package/package.json +2 -2
|
@@ -1,90 +1,99 @@
|
|
|
1
1
|
//#region src/admin/constants/index.ts
|
|
2
2
|
var e = {
|
|
3
|
+
success: "bg-status-success-bg text-status-success-text border-status-success-border",
|
|
4
|
+
warning: "bg-status-warning-bg text-status-warning-text border-status-warning-border",
|
|
5
|
+
error: "bg-status-error-bg text-status-error-text border-status-error-border",
|
|
6
|
+
info: "bg-status-info-bg text-status-info-text border-status-info-border",
|
|
7
|
+
neutral: "bg-status-neutral-bg text-status-neutral-text border-status-neutral-border"
|
|
8
|
+
}, t = {
|
|
3
9
|
active: {
|
|
4
10
|
label: "Active",
|
|
5
|
-
color:
|
|
11
|
+
color: e.success
|
|
6
12
|
},
|
|
7
13
|
suspended: {
|
|
8
14
|
label: "Suspended",
|
|
9
|
-
color:
|
|
15
|
+
color: e.error
|
|
10
16
|
},
|
|
11
17
|
trial: {
|
|
12
18
|
label: "Trial",
|
|
13
|
-
color:
|
|
19
|
+
color: e.warning
|
|
14
20
|
},
|
|
15
21
|
deleted: {
|
|
16
22
|
label: "Deleted",
|
|
17
|
-
color:
|
|
23
|
+
color: e.neutral
|
|
18
24
|
},
|
|
19
25
|
pending: {
|
|
20
26
|
label: "Pending",
|
|
21
|
-
color:
|
|
27
|
+
color: e.info
|
|
22
28
|
},
|
|
23
29
|
provisioning: {
|
|
24
30
|
label: "Provisioning",
|
|
25
|
-
color:
|
|
31
|
+
color: e.warning
|
|
26
32
|
}
|
|
27
|
-
},
|
|
33
|
+
}, n = {
|
|
28
34
|
active: {
|
|
29
35
|
label: "Active",
|
|
30
|
-
color:
|
|
36
|
+
color: e.success
|
|
31
37
|
},
|
|
32
38
|
inactive: {
|
|
33
39
|
label: "Inactive",
|
|
34
|
-
color:
|
|
40
|
+
color: e.neutral
|
|
35
41
|
},
|
|
36
42
|
blocked: {
|
|
37
43
|
label: "Blocked",
|
|
38
|
-
color:
|
|
44
|
+
color: e.error
|
|
39
45
|
},
|
|
40
46
|
pending: {
|
|
41
47
|
label: "Pending",
|
|
42
|
-
color:
|
|
48
|
+
color: e.warning
|
|
43
49
|
}
|
|
44
|
-
}
|
|
50
|
+
};
|
|
51
|
+
e.success, e.neutral, e.error, e.info, e.warning, e.info, e.warning, e.success, e.neutral, e.neutral, e.info, e.warning, e.error;
|
|
52
|
+
var r = {
|
|
45
53
|
healthy: {
|
|
46
54
|
label: "Healthy",
|
|
47
|
-
color:
|
|
55
|
+
color: e.success
|
|
48
56
|
},
|
|
49
57
|
degraded: {
|
|
50
58
|
label: "Degraded",
|
|
51
|
-
color:
|
|
59
|
+
color: e.warning
|
|
52
60
|
},
|
|
53
61
|
down: {
|
|
54
62
|
label: "Down",
|
|
55
|
-
color:
|
|
63
|
+
color: e.error
|
|
56
64
|
},
|
|
57
65
|
unknown: {
|
|
58
66
|
label: "Unknown",
|
|
59
|
-
color:
|
|
67
|
+
color: e.neutral
|
|
60
68
|
}
|
|
61
|
-
},
|
|
69
|
+
}, i = {
|
|
62
70
|
pending: {
|
|
63
71
|
label: "Pending",
|
|
64
|
-
color:
|
|
72
|
+
color: e.neutral
|
|
65
73
|
},
|
|
66
74
|
running: {
|
|
67
75
|
label: "Running",
|
|
68
|
-
color:
|
|
76
|
+
color: e.info
|
|
69
77
|
},
|
|
70
78
|
in_progress: {
|
|
71
79
|
label: "In Progress",
|
|
72
|
-
color:
|
|
80
|
+
color: e.info
|
|
73
81
|
},
|
|
74
82
|
success: {
|
|
75
83
|
label: "Success",
|
|
76
|
-
color:
|
|
84
|
+
color: e.success
|
|
77
85
|
},
|
|
78
86
|
failed: {
|
|
79
87
|
label: "Failed",
|
|
80
|
-
color:
|
|
88
|
+
color: e.error
|
|
81
89
|
},
|
|
82
90
|
rolled_back: {
|
|
83
91
|
label: "Rolled Back",
|
|
84
|
-
color:
|
|
92
|
+
color: e.warning
|
|
85
93
|
}
|
|
86
94
|
};
|
|
95
|
+
e.warning, e.info, e.error, e.info;
|
|
87
96
|
//#endregion
|
|
88
|
-
export {
|
|
97
|
+
export { i as DEPLOYMENT_STATUS_CONFIG, r as HEALTH_STATUS_CONFIG, t as TENANT_STATUS_CONFIG, n as USER_STATUS_CONFIG };
|
|
89
98
|
|
|
90
99
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/admin/constants/index.ts"],"sourcesContent":["import type {\n TenantStatus,\n UserStatus,\n SubscriptionStatus,\n TicketStatus,\n TicketPriority,\n HealthStatus,\n DeploymentStatus,\n CloudProvider,\n} from '../types';\n\n/**\n * Tenant status labels and colors\n */\nexport const TENANT_STATUS_CONFIG: Record<TenantStatus, { label: string; color: string }> = {\n active: { label: 'Active', color:
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/admin/constants/index.ts"],"sourcesContent":["import type {\n TenantStatus,\n UserStatus,\n SubscriptionStatus,\n TicketStatus,\n TicketPriority,\n HealthStatus,\n DeploymentStatus,\n CloudProvider,\n} from '../types';\n\n/**\n * Semantic status-badge token bundles. These map to the platform status tokens\n * (bg/text/border) which are defined for EVERY theme + dark mode, so badges and\n * pills stay legible with strong contrast everywhere. Using these instead of\n * raw `bg-success/10`/`text-info` (whose base color tokens are undefined and\n * render invisible in dark mode).\n */\nconst STATUS_TOKENS = {\n success: 'bg-status-success-bg text-status-success-text border-status-success-border',\n warning: 'bg-status-warning-bg text-status-warning-text border-status-warning-border',\n error: 'bg-status-error-bg text-status-error-text border-status-error-border',\n info: 'bg-status-info-bg text-status-info-text border-status-info-border',\n neutral: 'bg-status-neutral-bg text-status-neutral-text border-status-neutral-border',\n} as const;\n\n/**\n * Tenant status labels and colors\n */\nexport const TENANT_STATUS_CONFIG: Record<TenantStatus, { label: string; color: string }> = {\n active: { label: 'Active', color: STATUS_TOKENS.success },\n suspended: { label: 'Suspended', color: STATUS_TOKENS.error },\n trial: { label: 'Trial', color: STATUS_TOKENS.warning },\n deleted: { label: 'Deleted', color: STATUS_TOKENS.neutral },\n pending: { label: 'Pending', color: STATUS_TOKENS.info },\n provisioning: { label: 'Provisioning', color: STATUS_TOKENS.warning },\n};\n\n/**\n * User status labels and colors\n */\nexport const USER_STATUS_CONFIG: Record<UserStatus, { label: string; color: string }> = {\n active: { label: 'Active', color: STATUS_TOKENS.success },\n inactive: { label: 'Inactive', color: STATUS_TOKENS.neutral },\n blocked: { label: 'Blocked', color: STATUS_TOKENS.error },\n pending: { label: 'Pending', color: STATUS_TOKENS.warning },\n};\n\n/**\n * Subscription status labels and colors\n */\nexport const SUBSCRIPTION_STATUS_CONFIG: Record<\n SubscriptionStatus,\n { label: string; color: string }\n> = {\n active: { label: 'Active', color: STATUS_TOKENS.success },\n canceled: { label: 'Canceled', color: STATUS_TOKENS.neutral },\n past_due: { label: 'Past Due', color: STATUS_TOKENS.error },\n trialing: { label: 'Trialing', color: STATUS_TOKENS.info },\n paused: { label: 'Paused', color: STATUS_TOKENS.warning },\n};\n\n/**\n * Ticket status labels and colors\n */\nexport const TICKET_STATUS_CONFIG: Record<TicketStatus, { label: string; color: string }> = {\n open: { label: 'Open', color: STATUS_TOKENS.info },\n in_progress: { label: 'In Progress', color: STATUS_TOKENS.warning },\n resolved: { label: 'Resolved', color: STATUS_TOKENS.success },\n closed: { label: 'Closed', color: STATUS_TOKENS.neutral },\n};\n\n/**\n * Ticket priority labels and colors\n */\nexport const TICKET_PRIORITY_CONFIG: Record<TicketPriority, { label: string; color: string }> = {\n low: { label: 'Low', color: STATUS_TOKENS.neutral },\n medium: { label: 'Medium', color: STATUS_TOKENS.info },\n high: { label: 'High', color: STATUS_TOKENS.warning },\n critical: { label: 'Critical', color: STATUS_TOKENS.error },\n};\n\n/**\n * Health status labels and colors\n */\nexport const HEALTH_STATUS_CONFIG: Record<HealthStatus, { label: string; color: string }> = {\n healthy: { label: 'Healthy', color: STATUS_TOKENS.success },\n degraded: { label: 'Degraded', color: STATUS_TOKENS.warning },\n down: { label: 'Down', color: STATUS_TOKENS.error },\n unknown: { label: 'Unknown', color: STATUS_TOKENS.neutral },\n};\n\n/**\n * Deployment status labels and colors\n */\nexport const DEPLOYMENT_STATUS_CONFIG: Record<DeploymentStatus, { label: string; color: string }> =\n {\n pending: { label: 'Pending', color: STATUS_TOKENS.neutral },\n running: { label: 'Running', color: STATUS_TOKENS.info },\n in_progress: { label: 'In Progress', color: STATUS_TOKENS.info },\n success: { label: 'Success', color: STATUS_TOKENS.success },\n failed: { label: 'Failed', color: STATUS_TOKENS.error },\n rolled_back: { label: 'Rolled Back', color: STATUS_TOKENS.warning },\n };\n\n/**\n * Cloud provider labels and colors. Each provider maps to a semantic status\n * tint so the pills stay theme-aware + dark-mode legible (no raw brand hexes).\n */\nexport const CLOUD_PROVIDER_CONFIG: Record<\n CloudProvider,\n { label: string; color: string; icon: string }\n> = {\n aws: { label: 'AWS', color: STATUS_TOKENS.warning, icon: 'aws' },\n azure: { label: 'Azure', color: STATUS_TOKENS.info, icon: 'azure' },\n gcp: { label: 'GCP', color: STATUS_TOKENS.error, icon: 'gcp' },\n digitalocean: { label: 'DigitalOcean', color: STATUS_TOKENS.info, icon: 'do' },\n};\n\n/**\n * Chart colors using semantic chart tokens (theme-aware full colors — not\n * `hsl(var(...))`, which would be invalid now that the chart vars are colors).\n */\nexport const CHART_COLORS = {\n primary: 'var(--color-chart-1)',\n secondary: 'var(--color-chart-2)',\n tertiary: 'var(--color-chart-3)',\n quaternary: 'var(--color-chart-4)',\n quinary: 'var(--color-chart-5)',\n};\n\n/**\n * Default pagination\n */\nexport const DEFAULT_PAGE_SIZE = 20;\nexport const PAGE_SIZE_OPTIONS = [10, 20, 50, 100];\n"],"mappings":";AAkBA,IAAM,IAAgB;CACpB,SAAS;CACT,SAAS;CACT,OAAO;CACP,MAAM;CACN,SAAS;CACV,EAKY,IAA+E;CAC1F,QAAQ;EAAE,OAAO;EAAU,OAAO,EAAc;EAAS;CACzD,WAAW;EAAE,OAAO;EAAa,OAAO,EAAc;EAAO;CAC7D,OAAO;EAAE,OAAO;EAAS,OAAO,EAAc;EAAS;CACvD,SAAS;EAAE,OAAO;EAAW,OAAO,EAAc;EAAS;CAC3D,SAAS;EAAE,OAAO;EAAW,OAAO,EAAc;EAAM;CACxD,cAAc;EAAE,OAAO;EAAgB,OAAO,EAAc;EAAS;CACtE,EAKY,IAA2E;CACtF,QAAQ;EAAE,OAAO;EAAU,OAAO,EAAc;EAAS;CACzD,UAAU;EAAE,OAAO;EAAY,OAAO,EAAc;EAAS;CAC7D,SAAS;EAAE,OAAO;EAAW,OAAO,EAAc;EAAO;CACzD,SAAS;EAAE,OAAO;EAAW,OAAO,EAAc;EAAS;CAC5D;AASmC,EAAc,SACV,EAAc,SACd,EAAc,OACd,EAAc,MAClB,EAAc,SAOlB,EAAc,MACA,EAAc,SACpB,EAAc,SAClB,EAAc,SAOpB,EAAc,SACR,EAAc,MAClB,EAAc,SACN,EAAc;AAMtD,IAAa,IAA+E;CAC1F,SAAS;EAAE,OAAO;EAAW,OAAO,EAAc;EAAS;CAC3D,UAAU;EAAE,OAAO;EAAY,OAAO,EAAc;EAAS;CAC7D,MAAM;EAAE,OAAO;EAAQ,OAAO,EAAc;EAAO;CACnD,SAAS;EAAE,OAAO;EAAW,OAAO,EAAc;EAAS;CAC5D,EAKY,IACX;CACE,SAAS;EAAE,OAAO;EAAW,OAAO,EAAc;EAAS;CAC3D,SAAS;EAAE,OAAO;EAAW,OAAO,EAAc;EAAM;CACxD,aAAa;EAAE,OAAO;EAAe,OAAO,EAAc;EAAM;CAChE,SAAS;EAAE,OAAO;EAAW,OAAO,EAAc;EAAS;CAC3D,QAAQ;EAAE,OAAO;EAAU,OAAO,EAAc;EAAO;CACvD,aAAa;EAAE,OAAO;EAAe,OAAO,EAAc;EAAS;CACpE;AAU2B,EAAc,SACV,EAAc,MAClB,EAAc,OACI,EAAc"}
|
|
@@ -1,42 +1,43 @@
|
|
|
1
1
|
import { useAdminPermissions as e } from "../hooks/useAdminPermissions.js";
|
|
2
2
|
import { EmptyState as t, Input as n } from "../components/ui.js";
|
|
3
3
|
import { PageLayout as r } from "../components/PageLayout.js";
|
|
4
|
-
import {
|
|
5
|
-
import { DataTable as
|
|
6
|
-
import { PermissionGuard as
|
|
4
|
+
import { formatNumber as i, formatRelativeTime as a } from "../utils/formatters.js";
|
|
5
|
+
import { DataTable as o, Pagination as s } from "../components/DataTable.js";
|
|
6
|
+
import { PermissionGuard as c } from "../components/PermissionGuard.js";
|
|
7
7
|
import "../components/index.js";
|
|
8
|
-
import { useListAuditLogsQuery as
|
|
9
|
-
import { useCallback as
|
|
10
|
-
import { Database as
|
|
11
|
-
import { jsx as
|
|
12
|
-
import {
|
|
8
|
+
import { useListAuditLogsQuery as l } from "../../generated/global-operations.js";
|
|
9
|
+
import { useCallback as u, useMemo as d, useState as f } from "react";
|
|
10
|
+
import { Database as p, FileText as m, Key as h, Search as g, Settings as _, Shield as v, User as y } from "lucide-react";
|
|
11
|
+
import { jsx as b, jsxs as x } from "react/jsx-runtime";
|
|
12
|
+
import { EmphasisPanel as S, PagePurpose as C } from "@burdenoff/fe-libs/ui";
|
|
13
|
+
import { SaveToFilesButton as w } from "@burdenoff/fe-libs/storage";
|
|
13
14
|
//#region src/admin/pages/AuditLogsPage.tsx
|
|
14
|
-
var
|
|
15
|
-
auth: /* @__PURE__ */ y
|
|
16
|
-
user: /* @__PURE__ */ y
|
|
17
|
-
settings: /* @__PURE__ */
|
|
18
|
-
rbac: /* @__PURE__ */
|
|
19
|
-
data: /* @__PURE__ */
|
|
20
|
-
api: /* @__PURE__ */
|
|
15
|
+
var T = 20, E = {
|
|
16
|
+
auth: /* @__PURE__ */ b(y, { className: "h-3 w-3" }),
|
|
17
|
+
user: /* @__PURE__ */ b(y, { className: "h-3 w-3" }),
|
|
18
|
+
settings: /* @__PURE__ */ b(_, { className: "h-3 w-3" }),
|
|
19
|
+
rbac: /* @__PURE__ */ b(v, { className: "h-3 w-3" }),
|
|
20
|
+
data: /* @__PURE__ */ b(p, { className: "h-3 w-3" }),
|
|
21
|
+
api: /* @__PURE__ */ b(h, { className: "h-3 w-3" })
|
|
21
22
|
};
|
|
22
|
-
function
|
|
23
|
-
return
|
|
23
|
+
function D(e) {
|
|
24
|
+
return E[e.split(".")[0]] ?? /* @__PURE__ */ b(m, { className: "h-3 w-3" });
|
|
24
25
|
}
|
|
25
|
-
function
|
|
26
|
-
let { canViewAuditLogs:
|
|
26
|
+
function O() {
|
|
27
|
+
let { canViewAuditLogs: p, isLoading: h } = e(), [_, v] = f(1), [y, E] = f(""), [O, k] = f(""), A = h || !p, { data: j, loading: M, error: N } = l({
|
|
27
28
|
variables: {
|
|
28
|
-
resource:
|
|
29
|
-
limit:
|
|
30
|
-
offset: (
|
|
29
|
+
resource: O || void 0,
|
|
30
|
+
limit: T,
|
|
31
|
+
offset: (_ - 1) * T
|
|
31
32
|
},
|
|
32
33
|
fetchPolicy: "cache-and-network",
|
|
33
|
-
skip:
|
|
34
|
-
}),
|
|
35
|
-
if (!
|
|
36
|
-
let e =
|
|
37
|
-
return
|
|
38
|
-
}, [
|
|
39
|
-
let e =
|
|
34
|
+
skip: A
|
|
35
|
+
}), P = d(() => (j?.auditLogs?.edges ?? []).map((e) => e.node), [j]), F = d(() => {
|
|
36
|
+
if (!y) return P;
|
|
37
|
+
let e = y.toLowerCase();
|
|
38
|
+
return P.filter((t) => t.action.toLowerCase().includes(e) || t.actorId.toLowerCase().includes(e) || t.resource.toLowerCase().includes(e) || t.tenantId.toLowerCase().includes(e));
|
|
39
|
+
}, [P, y]), I = j?.auditLogs?.totalCount ?? 0, L = Math.ceil(I / T), R = u(() => {
|
|
40
|
+
let e = F;
|
|
40
41
|
if (e.length === 0) return null;
|
|
41
42
|
let t = [
|
|
42
43
|
"id",
|
|
@@ -70,23 +71,24 @@ function T() {
|
|
|
70
71
|
n(e.createdAt)
|
|
71
72
|
].join(","))];
|
|
72
73
|
return new Blob([r.join("\n")], { type: "text/csv;charset=utf-8;" });
|
|
73
|
-
}, [
|
|
74
|
-
let e =
|
|
74
|
+
}, [F]), z = u(() => {
|
|
75
|
+
let e = R();
|
|
75
76
|
if (!e) return;
|
|
76
77
|
let t = URL.createObjectURL(e), n = document.createElement("a"), r = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
77
78
|
n.href = t, n.setAttribute("download", `audit-logs-${r}.csv`), document.body.appendChild(n), n.click(), document.body.removeChild(n), URL.revokeObjectURL(t);
|
|
78
|
-
}, [
|
|
79
|
-
return /* @__PURE__ */
|
|
80
|
-
allowed:
|
|
81
|
-
isLoading:
|
|
79
|
+
}, [R]);
|
|
80
|
+
return /* @__PURE__ */ b(c, {
|
|
81
|
+
allowed: p,
|
|
82
|
+
isLoading: h,
|
|
82
83
|
pageTitle: "Audit Logs",
|
|
83
84
|
denyMessage: "You don't have permission to view audit logs.",
|
|
84
|
-
children: /* @__PURE__ */
|
|
85
|
+
children: /* @__PURE__ */ b(r, {
|
|
85
86
|
title: "Audit Logs",
|
|
86
|
-
|
|
87
|
-
|
|
87
|
+
gradientWord: "Logs",
|
|
88
|
+
description: `Security and compliance audit trail${I ? ` · ${I} total` : ""}`,
|
|
89
|
+
actions: /* @__PURE__ */ b(w, {
|
|
88
90
|
label: "Export CSV",
|
|
89
|
-
disabled:
|
|
91
|
+
disabled: M || F.length === 0,
|
|
90
92
|
testId: "audit-logs-export",
|
|
91
93
|
defaultFolderPath: "/Admin/Audit Logs",
|
|
92
94
|
source: {
|
|
@@ -94,49 +96,63 @@ function T() {
|
|
|
94
96
|
filename: `audit-logs-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.csv`,
|
|
95
97
|
mimeType: "text/csv",
|
|
96
98
|
getBlob: async () => {
|
|
97
|
-
let e =
|
|
99
|
+
let e = R();
|
|
98
100
|
return e ? { blob: e } : null;
|
|
99
101
|
}
|
|
100
102
|
},
|
|
101
|
-
onDownload:
|
|
103
|
+
onDownload: z
|
|
102
104
|
}),
|
|
103
|
-
children: /* @__PURE__ */
|
|
105
|
+
children: /* @__PURE__ */ x("div", {
|
|
104
106
|
className: "space-y-4",
|
|
105
107
|
children: [
|
|
106
|
-
/* @__PURE__ */ b("
|
|
107
|
-
|
|
108
|
+
/* @__PURE__ */ b(C, { children: "A tamper-evident record of who did what across the platform — every sign-in, permission change, and data action. Use this page to investigate a security incident, answer a compliance question, or export the trail as CSV for an auditor." }),
|
|
109
|
+
/* @__PURE__ */ x(S, {
|
|
110
|
+
className: "flex items-center gap-4 p-5",
|
|
108
111
|
children: [/* @__PURE__ */ b("div", {
|
|
112
|
+
className: "flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-action-primary-bg/15 text-action-primary-bg",
|
|
113
|
+
children: /* @__PURE__ */ b(m, { className: "h-6 w-6" })
|
|
114
|
+
}), /* @__PURE__ */ x("div", { children: [/* @__PURE__ */ b("div", {
|
|
115
|
+
className: "text-3xl font-bold tabular-nums text-text-primary",
|
|
116
|
+
children: i(I)
|
|
117
|
+
}), /* @__PURE__ */ b("p", {
|
|
118
|
+
className: "text-sm text-text-secondary",
|
|
119
|
+
children: "recorded audit events"
|
|
120
|
+
})] })]
|
|
121
|
+
}),
|
|
122
|
+
/* @__PURE__ */ x("div", {
|
|
123
|
+
className: "flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between",
|
|
124
|
+
children: [/* @__PURE__ */ x("div", {
|
|
109
125
|
className: "relative max-w-sm flex-1",
|
|
110
|
-
children: [/* @__PURE__ */
|
|
126
|
+
children: [/* @__PURE__ */ b(g, { className: "absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary" }), /* @__PURE__ */ b(n, {
|
|
111
127
|
placeholder: "Search by action, actor, or resource...",
|
|
112
|
-
value:
|
|
113
|
-
onChange: (e) =>
|
|
128
|
+
value: y,
|
|
129
|
+
onChange: (e) => E(e.target.value),
|
|
114
130
|
className: "pl-9"
|
|
115
131
|
})]
|
|
116
|
-
}), /* @__PURE__ */
|
|
132
|
+
}), /* @__PURE__ */ b(n, {
|
|
117
133
|
placeholder: "Filter by resource (e.g. tenant, user)...",
|
|
118
|
-
value:
|
|
134
|
+
value: O,
|
|
119
135
|
onChange: (e) => {
|
|
120
|
-
|
|
136
|
+
v(1), k(e.target.value);
|
|
121
137
|
},
|
|
122
138
|
className: "max-w-xs"
|
|
123
139
|
})]
|
|
124
140
|
}),
|
|
125
|
-
|
|
141
|
+
N && /* @__PURE__ */ x("div", {
|
|
126
142
|
className: "rounded-md border border-status-error-border/50 bg-status-error-bg/10 p-4 text-sm text-status-error-text",
|
|
127
|
-
children: ["Failed to load audit logs: ",
|
|
143
|
+
children: ["Failed to load audit logs: ", N.message]
|
|
128
144
|
}),
|
|
129
|
-
/* @__PURE__ */
|
|
145
|
+
/* @__PURE__ */ b(o, {
|
|
130
146
|
columns: [
|
|
131
147
|
{
|
|
132
148
|
key: "action",
|
|
133
149
|
header: "Action",
|
|
134
|
-
render: (e) => /* @__PURE__ */
|
|
150
|
+
render: (e) => /* @__PURE__ */ x("div", {
|
|
135
151
|
className: "flex items-center gap-2",
|
|
136
|
-
children: [/* @__PURE__ */
|
|
152
|
+
children: [/* @__PURE__ */ b("span", {
|
|
137
153
|
className: "text-text-secondary",
|
|
138
|
-
children:
|
|
139
|
-
}), /* @__PURE__ */
|
|
154
|
+
children: D(e.action)
|
|
155
|
+
}), /* @__PURE__ */ b("span", {
|
|
140
156
|
className: "font-mono text-sm font-medium text-text-primary",
|
|
141
157
|
children: e.action
|
|
142
158
|
})]
|
|
@@ -146,12 +162,12 @@ function T() {
|
|
|
146
162
|
key: "actorId",
|
|
147
163
|
header: "Actor",
|
|
148
164
|
width: "180px",
|
|
149
|
-
render: (e) => /* @__PURE__ */
|
|
165
|
+
render: (e) => /* @__PURE__ */ x("div", {
|
|
150
166
|
className: "min-w-0",
|
|
151
|
-
children: [/* @__PURE__ */
|
|
167
|
+
children: [/* @__PURE__ */ b("div", {
|
|
152
168
|
className: "truncate text-sm",
|
|
153
169
|
children: e.actorId
|
|
154
|
-
}), /* @__PURE__ */
|
|
170
|
+
}), /* @__PURE__ */ b("div", {
|
|
155
171
|
className: "text-xs text-text-secondary capitalize",
|
|
156
172
|
children: e.actorType
|
|
157
173
|
})]
|
|
@@ -161,12 +177,12 @@ function T() {
|
|
|
161
177
|
key: "resource",
|
|
162
178
|
header: "Resource",
|
|
163
179
|
width: "150px",
|
|
164
|
-
render: (e) => /* @__PURE__ */
|
|
180
|
+
render: (e) => /* @__PURE__ */ x("div", {
|
|
165
181
|
className: "min-w-0",
|
|
166
|
-
children: [/* @__PURE__ */
|
|
182
|
+
children: [/* @__PURE__ */ b("div", {
|
|
167
183
|
className: "truncate text-sm",
|
|
168
184
|
children: e.resource
|
|
169
|
-
}), e.resourceId && /* @__PURE__ */
|
|
185
|
+
}), e.resourceId && /* @__PURE__ */ x("div", {
|
|
170
186
|
className: "truncate text-xs text-text-secondary",
|
|
171
187
|
children: ["#", String(e.resourceId).slice(-8)]
|
|
172
188
|
})]
|
|
@@ -176,7 +192,7 @@ function T() {
|
|
|
176
192
|
key: "tenantId",
|
|
177
193
|
header: "Tenant",
|
|
178
194
|
width: "130px",
|
|
179
|
-
render: (e) => /* @__PURE__ */
|
|
195
|
+
render: (e) => /* @__PURE__ */ b("span", {
|
|
180
196
|
className: "text-xs text-text-secondary",
|
|
181
197
|
children: e.tenantId ? `#${e.tenantId.slice(-8)}` : "—"
|
|
182
198
|
})
|
|
@@ -185,7 +201,7 @@ function T() {
|
|
|
185
201
|
key: "ipAddress",
|
|
186
202
|
header: "IP",
|
|
187
203
|
width: "120px",
|
|
188
|
-
render: (e) => /* @__PURE__ */
|
|
204
|
+
render: (e) => /* @__PURE__ */ b("span", {
|
|
189
205
|
className: "font-mono text-xs text-text-secondary",
|
|
190
206
|
children: e.ipAddress ?? "—"
|
|
191
207
|
})
|
|
@@ -194,24 +210,25 @@ function T() {
|
|
|
194
210
|
key: "createdAt",
|
|
195
211
|
header: "Time",
|
|
196
212
|
width: "110px",
|
|
197
|
-
render: (e) =>
|
|
213
|
+
render: (e) => a(e.createdAt)
|
|
198
214
|
}
|
|
199
215
|
],
|
|
200
|
-
data:
|
|
216
|
+
data: F,
|
|
201
217
|
keyExtractor: (e) => e.id,
|
|
202
|
-
loading:
|
|
203
|
-
emptyState: /* @__PURE__ */
|
|
204
|
-
|
|
218
|
+
loading: M,
|
|
219
|
+
emptyState: /* @__PURE__ */ b(t, {
|
|
220
|
+
illustration: "empty-files",
|
|
221
|
+
icon: /* @__PURE__ */ b(m, { className: "h-8 w-8" }),
|
|
205
222
|
title: "No audit logs found",
|
|
206
223
|
description: "Audit events will appear here when actions are performed"
|
|
207
224
|
})
|
|
208
225
|
}),
|
|
209
|
-
|
|
210
|
-
page:
|
|
211
|
-
totalPages:
|
|
212
|
-
totalItems:
|
|
213
|
-
pageSize:
|
|
214
|
-
onPageChange:
|
|
226
|
+
I > T && /* @__PURE__ */ b(s, {
|
|
227
|
+
page: _,
|
|
228
|
+
totalPages: L,
|
|
229
|
+
totalItems: I,
|
|
230
|
+
pageSize: T,
|
|
231
|
+
onPageChange: v
|
|
215
232
|
})
|
|
216
233
|
]
|
|
217
234
|
})
|
|
@@ -219,6 +236,6 @@ function T() {
|
|
|
219
236
|
});
|
|
220
237
|
}
|
|
221
238
|
//#endregion
|
|
222
|
-
export {
|
|
239
|
+
export { O as AuditLogsPage };
|
|
223
240
|
|
|
224
241
|
//# sourceMappingURL=AuditLogsPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AuditLogsPage.js","names":[],"sources":["../../../src/admin/pages/AuditLogsPage.tsx"],"sourcesContent":["import { useState, useMemo, useCallback } from 'react';\nimport { FileText, Search, Download, User, Settings, Shield, Database, Key } from 'lucide-react';\nimport { SaveToFilesButton } from '@burdenoff/fe-libs/storage';\nimport {\n PageLayout,\n Input,\n DataTable,\n Pagination,\n EmptyState,\n Button,\n PermissionGuard,\n} from '../components';\nimport { useAdminPermissions } from '../hooks/useAdminPermissions';\nimport { formatRelativeTime } from '../utils/formatters';\nimport { useListAuditLogsQuery } from '../../generated/global-operations';\n\nconst PAGE_SIZE = 20;\n\nconst ACTION_ICONS: Record<string, React.ReactNode> = {\n auth: <User className=\"h-3 w-3\" />,\n user: <User className=\"h-3 w-3\" />,\n settings: <Settings className=\"h-3 w-3\" />,\n rbac: <Shield className=\"h-3 w-3\" />,\n data: <Database className=\"h-3 w-3\" />,\n api: <Key className=\"h-3 w-3\" />,\n};\n\nfunction getActionIcon(action: string) {\n const prefix = action.split('.')[0];\n return ACTION_ICONS[prefix] ?? <FileText className=\"h-3 w-3\" />;\n}\n\n/**\n * Audit logs page — connected to real backend\n */\nexport function AuditLogsPage() {\n const { canViewAuditLogs, isLoading: permissionsLoading } = useAdminPermissions();\n\n const [page, setPage] = useState(1);\n const [search, setSearch] = useState('');\n const [resourceFilter, setResourceFilter] = useState('');\n\n const skipQueries = permissionsLoading || !canViewAuditLogs;\n\n const { data, loading, error } = useListAuditLogsQuery({\n variables: {\n resource: resourceFilter || undefined,\n limit: PAGE_SIZE,\n offset: (page - 1) * PAGE_SIZE,\n },\n fetchPolicy: 'cache-and-network',\n skip: skipQueries,\n });\n\n const auditLogs = useMemo(() => {\n const edges = data?.auditLogs?.edges ?? [];\n return edges.map((e: (typeof edges)[number]) => e.node);\n }, [data]);\n\n type AuditLogNode = (typeof auditLogs)[number];\n\n const filteredLogs = useMemo(() => {\n if (!search) return auditLogs;\n const q = search.toLowerCase();\n return auditLogs.filter(\n (log: AuditLogNode) =>\n log.action.toLowerCase().includes(q) ||\n log.actorId.toLowerCase().includes(q) ||\n log.resource.toLowerCase().includes(q) ||\n log.tenantId.toLowerCase().includes(q)\n );\n }, [auditLogs, search]);\n\n const total = data?.auditLogs?.totalCount ?? 0;\n const totalPages = Math.ceil(total / PAGE_SIZE);\n\n const buildAuditCsvBlob = useCallback((): Blob | null => {\n const rows = filteredLogs;\n if (rows.length === 0) return null;\n\n const headers = [\n 'id',\n 'action',\n 'actorId',\n 'actorType',\n 'resource',\n 'resourceId',\n 'tenantId',\n 'ipAddress',\n 'userAgent',\n 'changes',\n 'metadata',\n 'createdAt',\n ];\n\n const escape = (value: unknown): string => {\n if (value === null || value === undefined) return '';\n const str = typeof value === 'object' ? JSON.stringify(value) : String(value);\n // RFC 4180: wrap in quotes if it contains quote, comma, or newline; double internal quotes\n if (/[\",\\n\\r]/.test(str)) {\n return `\"${str.replace(/\"/g, '\"\"')}\"`;\n }\n return str;\n };\n\n const csvLines = [\n headers.join(','),\n ...rows.map((log: AuditLogNode) =>\n [\n escape(log.id),\n escape(log.action),\n escape(log.actorId),\n escape(log.actorType),\n escape(log.resource),\n escape(log.resourceId),\n escape(log.tenantId),\n escape(log.ipAddress),\n escape((log as { userAgent?: unknown }).userAgent),\n escape((log as { changes?: unknown }).changes),\n escape((log as { metadata?: unknown }).metadata),\n escape(log.createdAt),\n ].join(',')\n ),\n ];\n\n return new Blob([csvLines.join('\\n')], { type: 'text/csv;charset=utf-8;' });\n }, [filteredLogs]);\n\n const exportToCsv = useCallback(() => {\n const blob = buildAuditCsvBlob();\n if (!blob) return;\n const url = URL.createObjectURL(blob);\n const link = document.createElement('a');\n const stamp = new Date().toISOString().replace(/[:.]/g, '-');\n link.href = url;\n link.setAttribute('download', `audit-logs-${stamp}.csv`);\n document.body.appendChild(link);\n link.click();\n document.body.removeChild(link);\n URL.revokeObjectURL(url);\n }, [buildAuditCsvBlob]);\n\n const columns = [\n {\n key: 'action',\n header: 'Action',\n render: (log: (typeof auditLogs)[number]) => (\n <div className=\"flex items-center gap-2\">\n <span className=\"text-text-secondary\">{getActionIcon(log.action)}</span>\n <span className=\"font-mono text-sm font-medium text-text-primary\">{log.action}</span>\n </div>\n ),\n },\n {\n key: 'actorId',\n header: 'Actor',\n width: '180px',\n render: (log: (typeof auditLogs)[number]) => (\n <div className=\"min-w-0\">\n <div className=\"truncate text-sm\">{log.actorId}</div>\n <div className=\"text-xs text-text-secondary capitalize\">{log.actorType}</div>\n </div>\n ),\n },\n {\n key: 'resource',\n header: 'Resource',\n width: '150px',\n render: (log: (typeof auditLogs)[number]) => (\n <div className=\"min-w-0\">\n <div className=\"truncate text-sm\">{log.resource}</div>\n {log.resourceId && (\n <div className=\"truncate text-xs text-text-secondary\">\n #{String(log.resourceId).slice(-8)}\n </div>\n )}\n </div>\n ),\n },\n {\n key: 'tenantId',\n header: 'Tenant',\n width: '130px',\n render: (log: (typeof auditLogs)[number]) => (\n <span className=\"text-xs text-text-secondary\">\n {log.tenantId ? `#${log.tenantId.slice(-8)}` : '—'}\n </span>\n ),\n },\n {\n key: 'ipAddress',\n header: 'IP',\n width: '120px',\n render: (log: (typeof auditLogs)[number]) => (\n <span className=\"font-mono text-xs text-text-secondary\">{log.ipAddress ?? '—'}</span>\n ),\n },\n {\n key: 'createdAt',\n header: 'Time',\n width: '110px',\n render: (log: (typeof auditLogs)[number]) => formatRelativeTime(log.createdAt),\n },\n ];\n\n return (\n <PermissionGuard\n allowed={canViewAuditLogs}\n isLoading={permissionsLoading}\n pageTitle=\"Audit Logs\"\n denyMessage=\"You don't have permission to view audit logs.\"\n >\n <PageLayout\n title=\"Audit Logs\"\n description={`Security and compliance audit trail${total ? ` · ${total} total` : ''}`}\n actions={\n <SaveToFilesButton\n label=\"Export CSV\"\n disabled={loading || filteredLogs.length === 0}\n testId=\"audit-logs-export\"\n defaultFolderPath=\"/Admin/Audit Logs\"\n source={{\n kind: 'blob',\n filename: `audit-logs-${new Date().toISOString().replace(/[:.]/g, '-')}.csv`,\n mimeType: 'text/csv',\n getBlob: async () => {\n const blob = buildAuditCsvBlob();\n return blob ? { blob } : null;\n },\n }}\n onDownload={exportToCsv}\n />\n }\n >\n <div className=\"space-y-4\">\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"relative max-w-sm flex-1\">\n <Search className=\"absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary\" />\n <Input\n placeholder=\"Search by action, actor, or resource...\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n className=\"pl-9\"\n />\n </div>\n <Input\n placeholder=\"Filter by resource (e.g. tenant, user)...\"\n value={resourceFilter}\n onChange={(e) => {\n setPage(1);\n setResourceFilter(e.target.value);\n }}\n className=\"max-w-xs\"\n />\n </div>\n\n {error && (\n <div className=\"rounded-md border border-status-error-border/50 bg-status-error-bg/10 p-4 text-sm text-status-error-text\">\n Failed to load audit logs: {error.message}\n </div>\n )}\n\n <DataTable\n columns={columns}\n data={filteredLogs}\n keyExtractor={(l) => l.id}\n loading={loading}\n emptyState={\n <EmptyState\n icon={<FileText className=\"h-8 w-8\" />}\n title=\"No audit logs found\"\n description=\"Audit events will appear here when actions are performed\"\n />\n }\n />\n\n {total > PAGE_SIZE && (\n <Pagination\n page={page}\n totalPages={totalPages}\n totalItems={total}\n pageSize={PAGE_SIZE}\n onPageChange={setPage}\n />\n )}\n </div>\n </PageLayout>\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AuditLogsPage.js","names":[],"sources":["../../../src/admin/pages/AuditLogsPage.tsx"],"sourcesContent":["import { useState, useMemo, useCallback } from 'react';\nimport { FileText, Search, Download, User, Settings, Shield, Database, Key } from 'lucide-react';\nimport { SaveToFilesButton } from '@burdenoff/fe-libs/storage';\nimport {\n PageLayout,\n Input,\n DataTable,\n Pagination,\n EmptyState,\n Button,\n PermissionGuard,\n} from '../components';\nimport { useAdminPermissions } from '../hooks/useAdminPermissions';\nimport { formatRelativeTime, formatNumber } from '../utils/formatters';\nimport { useListAuditLogsQuery } from '../../generated/global-operations';\nimport { PagePurpose, EmphasisPanel } from '@burdenoff/fe-libs/ui';\n\nconst PAGE_SIZE = 20;\n\nconst ACTION_ICONS: Record<string, React.ReactNode> = {\n auth: <User className=\"h-3 w-3\" />,\n user: <User className=\"h-3 w-3\" />,\n settings: <Settings className=\"h-3 w-3\" />,\n rbac: <Shield className=\"h-3 w-3\" />,\n data: <Database className=\"h-3 w-3\" />,\n api: <Key className=\"h-3 w-3\" />,\n};\n\nfunction getActionIcon(action: string) {\n const prefix = action.split('.')[0];\n return ACTION_ICONS[prefix] ?? <FileText className=\"h-3 w-3\" />;\n}\n\n/**\n * Audit logs page — connected to real backend\n */\nexport function AuditLogsPage() {\n const { canViewAuditLogs, isLoading: permissionsLoading } = useAdminPermissions();\n\n const [page, setPage] = useState(1);\n const [search, setSearch] = useState('');\n const [resourceFilter, setResourceFilter] = useState('');\n\n const skipQueries = permissionsLoading || !canViewAuditLogs;\n\n const { data, loading, error } = useListAuditLogsQuery({\n variables: {\n resource: resourceFilter || undefined,\n limit: PAGE_SIZE,\n offset: (page - 1) * PAGE_SIZE,\n },\n fetchPolicy: 'cache-and-network',\n skip: skipQueries,\n });\n\n const auditLogs = useMemo(() => {\n const edges = data?.auditLogs?.edges ?? [];\n return edges.map((e: (typeof edges)[number]) => e.node);\n }, [data]);\n\n type AuditLogNode = (typeof auditLogs)[number];\n\n const filteredLogs = useMemo(() => {\n if (!search) return auditLogs;\n const q = search.toLowerCase();\n return auditLogs.filter(\n (log: AuditLogNode) =>\n log.action.toLowerCase().includes(q) ||\n log.actorId.toLowerCase().includes(q) ||\n log.resource.toLowerCase().includes(q) ||\n log.tenantId.toLowerCase().includes(q)\n );\n }, [auditLogs, search]);\n\n const total = data?.auditLogs?.totalCount ?? 0;\n const totalPages = Math.ceil(total / PAGE_SIZE);\n\n const buildAuditCsvBlob = useCallback((): Blob | null => {\n const rows = filteredLogs;\n if (rows.length === 0) return null;\n\n const headers = [\n 'id',\n 'action',\n 'actorId',\n 'actorType',\n 'resource',\n 'resourceId',\n 'tenantId',\n 'ipAddress',\n 'userAgent',\n 'changes',\n 'metadata',\n 'createdAt',\n ];\n\n const escape = (value: unknown): string => {\n if (value === null || value === undefined) return '';\n const str = typeof value === 'object' ? JSON.stringify(value) : String(value);\n // RFC 4180: wrap in quotes if it contains quote, comma, or newline; double internal quotes\n if (/[\",\\n\\r]/.test(str)) {\n return `\"${str.replace(/\"/g, '\"\"')}\"`;\n }\n return str;\n };\n\n const csvLines = [\n headers.join(','),\n ...rows.map((log: AuditLogNode) =>\n [\n escape(log.id),\n escape(log.action),\n escape(log.actorId),\n escape(log.actorType),\n escape(log.resource),\n escape(log.resourceId),\n escape(log.tenantId),\n escape(log.ipAddress),\n escape((log as { userAgent?: unknown }).userAgent),\n escape((log as { changes?: unknown }).changes),\n escape((log as { metadata?: unknown }).metadata),\n escape(log.createdAt),\n ].join(',')\n ),\n ];\n\n return new Blob([csvLines.join('\\n')], { type: 'text/csv;charset=utf-8;' });\n }, [filteredLogs]);\n\n const exportToCsv = useCallback(() => {\n const blob = buildAuditCsvBlob();\n if (!blob) return;\n const url = URL.createObjectURL(blob);\n const link = document.createElement('a');\n const stamp = new Date().toISOString().replace(/[:.]/g, '-');\n link.href = url;\n link.setAttribute('download', `audit-logs-${stamp}.csv`);\n document.body.appendChild(link);\n link.click();\n document.body.removeChild(link);\n URL.revokeObjectURL(url);\n }, [buildAuditCsvBlob]);\n\n const columns = [\n {\n key: 'action',\n header: 'Action',\n render: (log: (typeof auditLogs)[number]) => (\n <div className=\"flex items-center gap-2\">\n <span className=\"text-text-secondary\">{getActionIcon(log.action)}</span>\n <span className=\"font-mono text-sm font-medium text-text-primary\">{log.action}</span>\n </div>\n ),\n },\n {\n key: 'actorId',\n header: 'Actor',\n width: '180px',\n render: (log: (typeof auditLogs)[number]) => (\n <div className=\"min-w-0\">\n <div className=\"truncate text-sm\">{log.actorId}</div>\n <div className=\"text-xs text-text-secondary capitalize\">{log.actorType}</div>\n </div>\n ),\n },\n {\n key: 'resource',\n header: 'Resource',\n width: '150px',\n render: (log: (typeof auditLogs)[number]) => (\n <div className=\"min-w-0\">\n <div className=\"truncate text-sm\">{log.resource}</div>\n {log.resourceId && (\n <div className=\"truncate text-xs text-text-secondary\">\n #{String(log.resourceId).slice(-8)}\n </div>\n )}\n </div>\n ),\n },\n {\n key: 'tenantId',\n header: 'Tenant',\n width: '130px',\n render: (log: (typeof auditLogs)[number]) => (\n <span className=\"text-xs text-text-secondary\">\n {log.tenantId ? `#${log.tenantId.slice(-8)}` : '—'}\n </span>\n ),\n },\n {\n key: 'ipAddress',\n header: 'IP',\n width: '120px',\n render: (log: (typeof auditLogs)[number]) => (\n <span className=\"font-mono text-xs text-text-secondary\">{log.ipAddress ?? '—'}</span>\n ),\n },\n {\n key: 'createdAt',\n header: 'Time',\n width: '110px',\n render: (log: (typeof auditLogs)[number]) => formatRelativeTime(log.createdAt),\n },\n ];\n\n return (\n <PermissionGuard\n allowed={canViewAuditLogs}\n isLoading={permissionsLoading}\n pageTitle=\"Audit Logs\"\n denyMessage=\"You don't have permission to view audit logs.\"\n >\n <PageLayout\n title=\"Audit Logs\"\n gradientWord=\"Logs\"\n description={`Security and compliance audit trail${total ? ` · ${total} total` : ''}`}\n actions={\n <SaveToFilesButton\n label=\"Export CSV\"\n disabled={loading || filteredLogs.length === 0}\n testId=\"audit-logs-export\"\n defaultFolderPath=\"/Admin/Audit Logs\"\n source={{\n kind: 'blob',\n filename: `audit-logs-${new Date().toISOString().replace(/[:.]/g, '-')}.csv`,\n mimeType: 'text/csv',\n getBlob: async () => {\n const blob = buildAuditCsvBlob();\n return blob ? { blob } : null;\n },\n }}\n onDownload={exportToCsv}\n />\n }\n >\n <div className=\"space-y-4\">\n <PagePurpose>\n A tamper-evident record of who did what across the platform — every sign-in,\n permission change, and data action. Use this page to investigate a security incident,\n answer a compliance question, or export the trail as CSV for an auditor.\n </PagePurpose>\n\n {/* Emphasis: total recorded audit events. */}\n <EmphasisPanel className=\"flex items-center gap-4 p-5\">\n <div className=\"flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-action-primary-bg/15 text-action-primary-bg\">\n <FileText className=\"h-6 w-6\" />\n </div>\n <div>\n <div className=\"text-3xl font-bold tabular-nums text-text-primary\">\n {formatNumber(total)}\n </div>\n <p className=\"text-sm text-text-secondary\">recorded audit events</p>\n </div>\n </EmphasisPanel>\n\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"relative max-w-sm flex-1\">\n <Search className=\"absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary\" />\n <Input\n placeholder=\"Search by action, actor, or resource...\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n className=\"pl-9\"\n />\n </div>\n <Input\n placeholder=\"Filter by resource (e.g. tenant, user)...\"\n value={resourceFilter}\n onChange={(e) => {\n setPage(1);\n setResourceFilter(e.target.value);\n }}\n className=\"max-w-xs\"\n />\n </div>\n\n {error && (\n <div className=\"rounded-md border border-status-error-border/50 bg-status-error-bg/10 p-4 text-sm text-status-error-text\">\n Failed to load audit logs: {error.message}\n </div>\n )}\n\n <DataTable\n columns={columns}\n data={filteredLogs}\n keyExtractor={(l) => l.id}\n loading={loading}\n emptyState={\n <EmptyState\n illustration=\"empty-files\"\n icon={<FileText className=\"h-8 w-8\" />}\n title=\"No audit logs found\"\n description=\"Audit events will appear here when actions are performed\"\n />\n }\n />\n\n {total > PAGE_SIZE && (\n <Pagination\n page={page}\n totalPages={totalPages}\n totalItems={total}\n pageSize={PAGE_SIZE}\n onPageChange={setPage}\n />\n )}\n </div>\n </PageLayout>\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
|