@wakastellar/ui 2.0.0 → 2.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 (290) hide show
  1. package/dist/cli/commands/add.d.ts +7 -0
  2. package/dist/cli/commands/init.d.ts +6 -0
  3. package/dist/cli/commands/list.d.ts +5 -0
  4. package/dist/cli/commands/search.d.ts +1 -0
  5. package/dist/cli/index.cjs +4844 -0
  6. package/dist/cli/index.d.ts +1 -0
  7. package/dist/cli/utils/config.d.ts +29 -0
  8. package/dist/cli/utils/logger.d.ts +20 -0
  9. package/dist/cli/utils/registry.d.ts +23 -0
  10. package/package.json +14 -3
  11. package/src/blocks/activity-timeline/index.tsx +586 -0
  12. package/src/blocks/calendar-view/index.tsx +756 -0
  13. package/src/blocks/chat/index.tsx +1018 -0
  14. package/src/blocks/chat/widget.tsx +504 -0
  15. package/src/blocks/dashboard/index.tsx +522 -0
  16. package/src/blocks/empty-states/index.tsx +452 -0
  17. package/src/blocks/error-pages/index.tsx +426 -0
  18. package/src/blocks/faq/index.tsx +479 -0
  19. package/src/blocks/file-manager/index.tsx +890 -0
  20. package/src/blocks/footer/index.tsx +133 -0
  21. package/src/blocks/header/index.tsx +357 -0
  22. package/src/blocks/headtab/index.tsx +139 -0
  23. package/src/blocks/i18n-editor/index.tsx +1016 -0
  24. package/src/blocks/index.ts +80 -0
  25. package/src/blocks/kanban-board/index.tsx +779 -0
  26. package/src/blocks/landing/index.tsx +677 -0
  27. package/src/blocks/language-selector/index.tsx +88 -0
  28. package/src/blocks/layout/index.tsx +159 -0
  29. package/src/blocks/login/index.tsx +339 -0
  30. package/src/blocks/login/types.ts +131 -0
  31. package/src/blocks/pricing/index.tsx +564 -0
  32. package/src/blocks/profile/index.tsx +746 -0
  33. package/src/blocks/settings/index.tsx +558 -0
  34. package/src/blocks/sidebar/index.tsx +713 -0
  35. package/src/blocks/theme-creator-block/index.tsx +835 -0
  36. package/src/blocks/user-management/index.tsx +1037 -0
  37. package/src/blocks/wizard/index.tsx +719 -0
  38. package/src/components/DataTable/DataTable.tsx +406 -0
  39. package/src/components/DataTable/DataTableAdvanced.tsx +720 -0
  40. package/src/components/DataTable/DataTableBody.tsx +216 -0
  41. package/src/components/DataTable/DataTableCell.tsx +172 -0
  42. package/src/components/DataTable/DataTableColumnResizer.tsx +62 -0
  43. package/src/components/DataTable/DataTableConflictResolver.tsx +478 -0
  44. package/src/components/DataTable/DataTableContextMenu.tsx +219 -0
  45. package/src/components/DataTable/DataTableEditCell.tsx +279 -0
  46. package/src/components/DataTable/DataTableFilterBuilder.tsx +519 -0
  47. package/src/components/DataTable/DataTableFilters.tsx +535 -0
  48. package/src/components/DataTable/DataTableGrouping.tsx +147 -0
  49. package/src/components/DataTable/DataTableHeader.tsx +172 -0
  50. package/src/components/DataTable/DataTablePagination.tsx +125 -0
  51. package/src/components/DataTable/DataTableSelection.tsx +269 -0
  52. package/src/components/DataTable/DataTableSyncStatus.tsx +281 -0
  53. package/src/components/DataTable/DataTableToolbar.tsx +262 -0
  54. package/src/components/DataTable/README.md +446 -0
  55. package/src/components/DataTable/__tests__/DataTableAdvanced.test.tsx +426 -0
  56. package/src/components/DataTable/__tests__/DataTableEdit.test.tsx +329 -0
  57. package/src/components/DataTable/__tests__/useDataTableAdvanced.test.ts +455 -0
  58. package/src/components/DataTable/examples/EditExample.tsx +166 -0
  59. package/src/components/DataTable/formatters/index.ts +335 -0
  60. package/src/components/DataTable/hooks/__tests__/useDataTableEdit.test.ts +239 -0
  61. package/src/components/DataTable/hooks/useDataTable.ts +145 -0
  62. package/src/components/DataTable/hooks/useDataTableAdvanced.ts +342 -0
  63. package/src/components/DataTable/hooks/useDataTableAdvancedFilters.ts +637 -0
  64. package/src/components/DataTable/hooks/useDataTableColumnTemplates.ts +186 -0
  65. package/src/components/DataTable/hooks/useDataTableEdit.ts +167 -0
  66. package/src/components/DataTable/hooks/useDataTableExport.ts +227 -0
  67. package/src/components/DataTable/hooks/useDataTableImport.ts +216 -0
  68. package/src/components/DataTable/hooks/useDataTableOffline.ts +481 -0
  69. package/src/components/DataTable/hooks/useDataTableTheme.ts +213 -0
  70. package/src/components/DataTable/hooks/useDataTableVirtualization.ts +99 -0
  71. package/src/components/DataTable/hooks/useTableLayout.ts +85 -0
  72. package/src/components/DataTable/index.ts +81 -0
  73. package/src/components/DataTable/services/IndexedDBService.ts +504 -0
  74. package/src/components/DataTable/templates/index.tsx +803 -0
  75. package/src/components/DataTable/types.ts +504 -0
  76. package/src/components/DataTable/utils.ts +164 -0
  77. package/src/components/DataTable/workers/exportWorker.ts +213 -0
  78. package/src/components/accordion/index.tsx +61 -0
  79. package/src/components/alert/index.tsx +61 -0
  80. package/src/components/alert-dialog/index.tsx +146 -0
  81. package/src/components/aspect-ratio/index.tsx +12 -0
  82. package/src/components/avatar/index.tsx +54 -0
  83. package/src/components/badge/Badge.stories.tsx +64 -0
  84. package/src/components/badge/index.tsx +38 -0
  85. package/src/components/button/Button.stories.tsx +173 -0
  86. package/src/components/button/index.tsx +56 -0
  87. package/src/components/calendar/index.tsx +73 -0
  88. package/src/components/card/index.tsx +78 -0
  89. package/src/components/checkbox/index.tsx +34 -0
  90. package/src/components/code/index.tsx +229 -0
  91. package/src/components/collapsible/index.tsx +16 -0
  92. package/src/components/command/index.tsx +162 -0
  93. package/src/components/context-menu/index.tsx +204 -0
  94. package/src/components/dialog/index.tsx +126 -0
  95. package/src/components/dropdown-menu/index.tsx +204 -0
  96. package/src/components/error-boundary/ErrorBoundary.tsx +281 -0
  97. package/src/components/error-boundary/index.ts +7 -0
  98. package/src/components/form/index.tsx +183 -0
  99. package/src/components/hover-card/index.tsx +33 -0
  100. package/src/components/index.ts +368 -0
  101. package/src/components/input/Input.stories.tsx +100 -0
  102. package/src/components/input/index.tsx +27 -0
  103. package/src/components/input-otp/index.tsx +277 -0
  104. package/src/components/label/index.tsx +30 -0
  105. package/src/components/language-selector/index.tsx +341 -0
  106. package/src/components/menubar/index.tsx +240 -0
  107. package/src/components/navigation-menu/index.tsx +134 -0
  108. package/src/components/popover/index.tsx +35 -0
  109. package/src/components/progress/index.tsx +32 -0
  110. package/src/components/radio-group/index.tsx +48 -0
  111. package/src/components/scroll-area/index.tsx +52 -0
  112. package/src/components/select/index.tsx +164 -0
  113. package/src/components/separator/index.tsx +35 -0
  114. package/src/components/sheet/index.tsx +147 -0
  115. package/src/components/skeleton/index.tsx +22 -0
  116. package/src/components/slider/index.tsx +32 -0
  117. package/src/components/switch/index.tsx +33 -0
  118. package/src/components/table/index.tsx +117 -0
  119. package/src/components/tabs/index.tsx +59 -0
  120. package/src/components/textarea/index.tsx +30 -0
  121. package/src/components/theme-selector/index.tsx +327 -0
  122. package/src/components/toast/index.tsx +133 -0
  123. package/src/components/toaster/index.tsx +34 -0
  124. package/src/components/toggle/index.tsx +49 -0
  125. package/src/components/tooltip/index.tsx +34 -0
  126. package/src/components/typography/index.tsx +276 -0
  127. package/src/components/waka-3d-pie-chart/index.tsx +486 -0
  128. package/src/components/waka-achievement-unlock/index.tsx +716 -0
  129. package/src/components/waka-activity-feed/index.tsx +686 -0
  130. package/src/components/waka-address-autocomplete/index.tsx +1202 -0
  131. package/src/components/waka-admincrumb/index.tsx +349 -0
  132. package/src/components/waka-alert-stack/index.tsx +827 -0
  133. package/src/components/waka-allocation-matrix/index.tsx +1278 -0
  134. package/src/components/waka-approval-chain/index.tsx +766 -0
  135. package/src/components/waka-audit-log/index.tsx +1475 -0
  136. package/src/components/waka-autocomplete/index.tsx +358 -0
  137. package/src/components/waka-badge-showcase/index.tsx +704 -0
  138. package/src/components/waka-barcode/index.tsx +260 -0
  139. package/src/components/waka-biometric-prompt/index.tsx +765 -0
  140. package/src/components/waka-bottom-sheet/index.tsx +495 -0
  141. package/src/components/waka-breadcrumb/index.tsx +376 -0
  142. package/src/components/waka-breadcrumb-path/index.tsx +513 -0
  143. package/src/components/waka-budget-burn/index.tsx +1234 -0
  144. package/src/components/waka-capacity-planner/index.tsx +1107 -0
  145. package/src/components/waka-carousel/index.tsx +893 -0
  146. package/src/components/waka-cart-summary/index.tsx +1055 -0
  147. package/src/components/waka-challenge-timer/index.tsx +1044 -0
  148. package/src/components/waka-charts/WakaAreaChart.tsx +251 -0
  149. package/src/components/waka-charts/WakaBarChart.tsx +222 -0
  150. package/src/components/waka-charts/WakaChart.tsx +124 -0
  151. package/src/components/waka-charts/WakaLineChart.tsx +219 -0
  152. package/src/components/waka-charts/WakaMiniChart.tsx +133 -0
  153. package/src/components/waka-charts/WakaPieChart.tsx +214 -0
  154. package/src/components/waka-charts/WakaSparkline.tsx +229 -0
  155. package/src/components/waka-charts/dataTableHelpers.ts +109 -0
  156. package/src/components/waka-charts/hooks/useChartTheme.ts +123 -0
  157. package/src/components/waka-charts/hooks/useRechartsLoader.ts +234 -0
  158. package/src/components/waka-charts/index.ts +90 -0
  159. package/src/components/waka-charts/types.ts +330 -0
  160. package/src/components/waka-chat-bubble/index.tsx +1060 -0
  161. package/src/components/waka-checklist/index.tsx +1067 -0
  162. package/src/components/waka-checkout-stepper/index.tsx +976 -0
  163. package/src/components/waka-cohort-table/index.tsx +1011 -0
  164. package/src/components/waka-color-picker/index.tsx +447 -0
  165. package/src/components/waka-combo-counter/index.tsx +864 -0
  166. package/src/components/waka-combobox/index.tsx +497 -0
  167. package/src/components/waka-command-bar/index.tsx +403 -0
  168. package/src/components/waka-compare-period/index.tsx +1230 -0
  169. package/src/components/waka-connection-matrix/index.tsx +1053 -0
  170. package/src/components/waka-contribution-graph/index.tsx +552 -0
  171. package/src/components/waka-cost-breakdown/index.tsx +1065 -0
  172. package/src/components/waka-coupon-input/index.tsx +592 -0
  173. package/src/components/waka-credit-card-input/index.tsx +982 -0
  174. package/src/components/waka-daily-reward/index.tsx +762 -0
  175. package/src/components/waka-date-range-picker/index.tsx +378 -0
  176. package/src/components/waka-datetime-picker/index.tsx +793 -0
  177. package/src/components/waka-datetime-picker.form-integration/index.tsx +402 -0
  178. package/src/components/waka-deployment-lane/index.tsx +673 -0
  179. package/src/components/waka-device-trust/index.tsx +1259 -0
  180. package/src/components/waka-dock/index.tsx +285 -0
  181. package/src/components/waka-drawer/index.tsx +319 -0
  182. package/src/components/waka-empty-state/index.tsx +545 -0
  183. package/src/components/waka-error-shake/index.tsx +398 -0
  184. package/src/components/waka-feature-announcement/index.tsx +991 -0
  185. package/src/components/waka-file-upload/index.tsx +437 -0
  186. package/src/components/waka-floating-nav/index.tsx +413 -0
  187. package/src/components/waka-flow-diagram/index.tsx +508 -0
  188. package/src/components/waka-funnel-chart/index.tsx +823 -0
  189. package/src/components/waka-glow-card/index.tsx +246 -0
  190. package/src/components/waka-goal-progress/index.tsx +1025 -0
  191. package/src/components/waka-haptic-button/index.tsx +388 -0
  192. package/src/components/waka-health-pulse/index.tsx +451 -0
  193. package/src/components/waka-heatmap/index.tsx +1026 -0
  194. package/src/components/waka-hotspot/index.tsx +682 -0
  195. package/src/components/waka-image/index.tsx +373 -0
  196. package/src/components/waka-incident-timeline/index.tsx +686 -0
  197. package/src/components/waka-invoice-preview/index.tsx +829 -0
  198. package/src/components/waka-kanban/index.tsx +646 -0
  199. package/src/components/waka-kpi-dashboard/index.tsx +755 -0
  200. package/src/components/waka-leaderboard/index.tsx +746 -0
  201. package/src/components/waka-level-progress/index.tsx +665 -0
  202. package/src/components/waka-liquid-button/index.tsx +520 -0
  203. package/src/components/waka-loading-orbit/index.tsx +478 -0
  204. package/src/components/waka-loot-box/index.tsx +1091 -0
  205. package/src/components/waka-magic-link/index.tsx +321 -0
  206. package/src/components/waka-magnetic-button/index.tsx +567 -0
  207. package/src/components/waka-mention-input/index.tsx +953 -0
  208. package/src/components/waka-metric-sparkline/index.tsx +627 -0
  209. package/src/components/waka-milestone-road/index.tsx +1064 -0
  210. package/src/components/waka-modal/index.tsx +374 -0
  211. package/src/components/waka-morph-button/index.tsx +495 -0
  212. package/src/components/waka-network-topology/index.tsx +801 -0
  213. package/src/components/waka-notifications/index.tsx +414 -0
  214. package/src/components/waka-number-input/index.tsx +373 -0
  215. package/src/components/waka-orbital-menu/index.tsx +445 -0
  216. package/src/components/waka-order-tracker/index.tsx +1041 -0
  217. package/src/components/waka-pagination/index.tsx +393 -0
  218. package/src/components/waka-password-strength/index.tsx +824 -0
  219. package/src/components/waka-payment-method-picker/index.tsx +715 -0
  220. package/src/components/waka-permission-matrix/index.tsx +1302 -0
  221. package/src/components/waka-phone-input/index.tsx +801 -0
  222. package/src/components/waka-pipeline-view/index.tsx +604 -0
  223. package/src/components/waka-player-card/index.tsx +691 -0
  224. package/src/components/waka-points-popup/index.tsx +366 -0
  225. package/src/components/waka-power-up/index.tsx +1155 -0
  226. package/src/components/waka-presence-indicator/index.tsx +1181 -0
  227. package/src/components/waka-pricing-table/index.tsx +755 -0
  228. package/src/components/waka-product-card/index.tsx +786 -0
  229. package/src/components/waka-progress-onboarding/index.tsx +878 -0
  230. package/src/components/waka-pull-to-refresh/index.tsx +451 -0
  231. package/src/components/waka-qrcode/index.tsx +232 -0
  232. package/src/components/waka-quest-card/index.tsx +1275 -0
  233. package/src/components/waka-quota-bar/index.tsx +693 -0
  234. package/src/components/waka-radar-score/index.tsx +512 -0
  235. package/src/components/waka-rank-badge/index.tsx +813 -0
  236. package/src/components/waka-rating-input/index.tsx +560 -0
  237. package/src/components/waka-reaction-picker/index.tsx +1062 -0
  238. package/src/components/waka-region-map/index.tsx +730 -0
  239. package/src/components/waka-resource-gauge/index.tsx +654 -0
  240. package/src/components/waka-resource-pool/index.tsx +1035 -0
  241. package/src/components/waka-rich-text-editor/index.tsx +594 -0
  242. package/src/components/waka-rollback-slider/index.tsx +891 -0
  243. package/src/components/waka-sankey-diagram/index.tsx +1032 -0
  244. package/src/components/waka-schedule-picker/index.tsx +1060 -0
  245. package/src/components/waka-scratch-card/index.tsx +914 -0
  246. package/src/components/waka-season-pass/index.tsx +886 -0
  247. package/src/components/waka-security-score/index.tsx +1126 -0
  248. package/src/components/waka-segmented-control/index.tsx +238 -0
  249. package/src/components/waka-server-rack/index.tsx +764 -0
  250. package/src/components/waka-session-manager/index.tsx +815 -0
  251. package/src/components/waka-signature-pad/index.tsx +744 -0
  252. package/src/components/waka-skeleton-wave/index.tsx +454 -0
  253. package/src/components/waka-skill-tree/index.tsx +1031 -0
  254. package/src/components/waka-sla-tracker/index.tsx +798 -0
  255. package/src/components/waka-slider-range/index.tsx +765 -0
  256. package/src/components/waka-spin-wheel/index.tsx +671 -0
  257. package/src/components/waka-spinner/index.tsx +284 -0
  258. package/src/components/waka-spotlight/index.tsx +410 -0
  259. package/src/components/waka-stat/index.tsx +428 -0
  260. package/src/components/waka-stats-hexagon/index.tsx +824 -0
  261. package/src/components/waka-status-matrix/index.tsx +565 -0
  262. package/src/components/waka-stepper/index.tsx +489 -0
  263. package/src/components/waka-streak-counter/index.tsx +334 -0
  264. package/src/components/waka-success-explosion/index.tsx +453 -0
  265. package/src/components/waka-swipe-card/index.tsx +574 -0
  266. package/src/components/waka-tabs-morph/index.tsx +509 -0
  267. package/src/components/waka-tag-input/index.tsx +877 -0
  268. package/src/components/waka-team-banner/index.tsx +1183 -0
  269. package/src/components/waka-terminal-output/index.tsx +836 -0
  270. package/src/components/waka-theme-creator/index.tsx +762 -0
  271. package/src/components/waka-theme-manager/index.tsx +654 -0
  272. package/src/components/waka-thread-view/index.tsx +874 -0
  273. package/src/components/waka-tilt-card/index.tsx +250 -0
  274. package/src/components/waka-time-picker/index.tsx +479 -0
  275. package/src/components/waka-timeline/index.tsx +385 -0
  276. package/src/components/waka-tooltip-tour/index.tsx +855 -0
  277. package/src/components/waka-tour-guide/index.tsx +920 -0
  278. package/src/components/waka-tournament-bracket/index.tsx +1276 -0
  279. package/src/components/waka-tree/index.tsx +557 -0
  280. package/src/components/waka-treemap-chart/index.tsx +1031 -0
  281. package/src/components/waka-two-factor-setup/index.tsx +995 -0
  282. package/src/components/waka-typewriter/index.tsx +566 -0
  283. package/src/components/waka-typing-indicator/index.tsx +649 -0
  284. package/src/components/waka-versus-card/index.tsx +1026 -0
  285. package/src/components/waka-video/index.tsx +557 -0
  286. package/src/components/waka-video-call/index.tsx +1087 -0
  287. package/src/components/waka-virtual-list/index.tsx +327 -0
  288. package/src/components/waka-voice-message/index.tsx +1019 -0
  289. package/src/components/waka-welcome-modal/index.tsx +790 -0
  290. package/src/components/waka-xp-bar/index.tsx +799 -0
@@ -0,0 +1,1475 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cn } from "../../utils/cn"
5
+ import {
6
+ Activity,
7
+ LogIn,
8
+ LogOut,
9
+ Edit,
10
+ Trash2,
11
+ Plus,
12
+ Filter,
13
+ Search,
14
+ ChevronDown,
15
+ ChevronUp,
16
+ Download,
17
+ Eye,
18
+ Settings,
19
+ Shield,
20
+ Key,
21
+ UserPlus,
22
+ UserMinus,
23
+ RefreshCw,
24
+ AlertTriangle,
25
+ Info,
26
+ AlertCircle,
27
+ X,
28
+ Calendar,
29
+ MapPin,
30
+ Globe,
31
+ Loader2,
32
+ FileText,
33
+ Lock,
34
+ Unlock,
35
+ Mail,
36
+ CreditCard,
37
+ Upload,
38
+ Database,
39
+ Server,
40
+ } from "lucide-react"
41
+
42
+ // ============================================================================
43
+ // Types
44
+ // ============================================================================
45
+
46
+ export type AuditEventType =
47
+ | "login"
48
+ | "logout"
49
+ | "create"
50
+ | "update"
51
+ | "delete"
52
+ | "view"
53
+ | "export"
54
+ | "import"
55
+ | "settings_change"
56
+ | "permission_change"
57
+ | "password_change"
58
+ | "user_invite"
59
+ | "user_remove"
60
+ | "api_key_create"
61
+ | "api_key_revoke"
62
+ | "mfa_enable"
63
+ | "mfa_disable"
64
+ | "session_revoke"
65
+ | "file_upload"
66
+ | "file_download"
67
+ | "payment"
68
+ | "subscription_change"
69
+ | "data_export"
70
+ | "backup"
71
+ | "restore"
72
+
73
+ export type AuditSeverity = "info" | "warning" | "critical"
74
+
75
+ export interface AuditUser {
76
+ /** Unique user identifier */
77
+ id: string
78
+ /** Display name */
79
+ name: string
80
+ /** Email address */
81
+ email?: string
82
+ /** Avatar URL */
83
+ avatar?: string
84
+ /** User role */
85
+ role?: string
86
+ }
87
+
88
+ export interface AuditLocation {
89
+ /** City name */
90
+ city?: string
91
+ /** Country name */
92
+ country?: string
93
+ /** Country code */
94
+ countryCode?: string
95
+ }
96
+
97
+ export interface AuditEvent {
98
+ /** Unique event identifier */
99
+ id: string
100
+ /** Event type */
101
+ type: AuditEventType
102
+ /** Severity level */
103
+ severity: AuditSeverity
104
+ /** User who performed the action */
105
+ user: AuditUser
106
+ /** Event description */
107
+ description: string
108
+ /** Detailed action information */
109
+ details?: Record<string, any>
110
+ /** IP address */
111
+ ipAddress?: string
112
+ /** User agent string */
113
+ userAgent?: string
114
+ /** Location information */
115
+ location?: AuditLocation
116
+ /** Resource affected (e.g., "User: john@example.com") */
117
+ resource?: string
118
+ /** When the event occurred */
119
+ timestamp: Date
120
+ /** Session ID */
121
+ sessionId?: string
122
+ /** Request ID for tracing */
123
+ requestId?: string
124
+ }
125
+
126
+ export interface AuditFilters {
127
+ /** Filter by event types */
128
+ eventTypes?: AuditEventType[]
129
+ /** Filter by severity levels */
130
+ severities?: AuditSeverity[]
131
+ /** Filter by user IDs */
132
+ userIds?: string[]
133
+ /** Search query */
134
+ search?: string
135
+ /** Start date */
136
+ startDate?: Date
137
+ /** End date */
138
+ endDate?: Date
139
+ }
140
+
141
+ export interface WakaAuditLogProps {
142
+ /** List of audit events */
143
+ events: AuditEvent[]
144
+ /** Callback when an event is clicked */
145
+ onEventClick?: (event: AuditEvent) => void
146
+ /** Callback when filters change */
147
+ onFilterChange?: (filters: AuditFilters) => void
148
+ /** Callback when load more is triggered (pagination/infinite scroll) */
149
+ onLoadMore?: () => void
150
+ /** Callback when export is requested */
151
+ onExport?: (format: "csv" | "json") => void
152
+ /** Whether more events can be loaded */
153
+ hasMore?: boolean
154
+ /** Whether events are currently loading */
155
+ loading?: boolean
156
+ /** Whether to show filter controls */
157
+ showFilters?: boolean
158
+ /** Whether to show search */
159
+ showSearch?: boolean
160
+ /** Whether to show export button */
161
+ showExport?: boolean
162
+ /** Whether to use infinite scroll instead of pagination */
163
+ infiniteScroll?: boolean
164
+ /** Maximum number of events to display */
165
+ maxItems?: number
166
+ /** Available users for filtering */
167
+ users?: AuditUser[]
168
+ /** Custom className */
169
+ className?: string
170
+ }
171
+
172
+ // ============================================================================
173
+ // Event Type Configuration
174
+ // ============================================================================
175
+
176
+ const eventTypeConfig: Record<
177
+ AuditEventType,
178
+ { icon: React.ElementType; color: string; bgColor: string; label: string }
179
+ > = {
180
+ login: {
181
+ icon: LogIn,
182
+ color: "text-green-600 dark:text-green-400",
183
+ bgColor: "bg-green-100 dark:bg-green-900/30",
184
+ label: "Login",
185
+ },
186
+ logout: {
187
+ icon: LogOut,
188
+ color: "text-gray-600 dark:text-gray-400",
189
+ bgColor: "bg-gray-100 dark:bg-gray-900/30",
190
+ label: "Logout",
191
+ },
192
+ create: {
193
+ icon: Plus,
194
+ color: "text-blue-600 dark:text-blue-400",
195
+ bgColor: "bg-blue-100 dark:bg-blue-900/30",
196
+ label: "Create",
197
+ },
198
+ update: {
199
+ icon: Edit,
200
+ color: "text-amber-600 dark:text-amber-400",
201
+ bgColor: "bg-amber-100 dark:bg-amber-900/30",
202
+ label: "Update",
203
+ },
204
+ delete: {
205
+ icon: Trash2,
206
+ color: "text-red-600 dark:text-red-400",
207
+ bgColor: "bg-red-100 dark:bg-red-900/30",
208
+ label: "Delete",
209
+ },
210
+ view: {
211
+ icon: Eye,
212
+ color: "text-purple-600 dark:text-purple-400",
213
+ bgColor: "bg-purple-100 dark:bg-purple-900/30",
214
+ label: "View",
215
+ },
216
+ export: {
217
+ icon: Download,
218
+ color: "text-indigo-600 dark:text-indigo-400",
219
+ bgColor: "bg-indigo-100 dark:bg-indigo-900/30",
220
+ label: "Export",
221
+ },
222
+ import: {
223
+ icon: Upload,
224
+ color: "text-indigo-600 dark:text-indigo-400",
225
+ bgColor: "bg-indigo-100 dark:bg-indigo-900/30",
226
+ label: "Import",
227
+ },
228
+ settings_change: {
229
+ icon: Settings,
230
+ color: "text-slate-600 dark:text-slate-400",
231
+ bgColor: "bg-slate-100 dark:bg-slate-900/30",
232
+ label: "Settings",
233
+ },
234
+ permission_change: {
235
+ icon: Shield,
236
+ color: "text-orange-600 dark:text-orange-400",
237
+ bgColor: "bg-orange-100 dark:bg-orange-900/30",
238
+ label: "Permission",
239
+ },
240
+ password_change: {
241
+ icon: Key,
242
+ color: "text-yellow-600 dark:text-yellow-400",
243
+ bgColor: "bg-yellow-100 dark:bg-yellow-900/30",
244
+ label: "Password",
245
+ },
246
+ user_invite: {
247
+ icon: UserPlus,
248
+ color: "text-teal-600 dark:text-teal-400",
249
+ bgColor: "bg-teal-100 dark:bg-teal-900/30",
250
+ label: "Invite",
251
+ },
252
+ user_remove: {
253
+ icon: UserMinus,
254
+ color: "text-rose-600 dark:text-rose-400",
255
+ bgColor: "bg-rose-100 dark:bg-rose-900/30",
256
+ label: "Remove",
257
+ },
258
+ api_key_create: {
259
+ icon: Key,
260
+ color: "text-cyan-600 dark:text-cyan-400",
261
+ bgColor: "bg-cyan-100 dark:bg-cyan-900/30",
262
+ label: "API Key Create",
263
+ },
264
+ api_key_revoke: {
265
+ icon: Key,
266
+ color: "text-red-600 dark:text-red-400",
267
+ bgColor: "bg-red-100 dark:bg-red-900/30",
268
+ label: "API Key Revoke",
269
+ },
270
+ mfa_enable: {
271
+ icon: Lock,
272
+ color: "text-emerald-600 dark:text-emerald-400",
273
+ bgColor: "bg-emerald-100 dark:bg-emerald-900/30",
274
+ label: "MFA Enable",
275
+ },
276
+ mfa_disable: {
277
+ icon: Unlock,
278
+ color: "text-orange-600 dark:text-orange-400",
279
+ bgColor: "bg-orange-100 dark:bg-orange-900/30",
280
+ label: "MFA Disable",
281
+ },
282
+ session_revoke: {
283
+ icon: RefreshCw,
284
+ color: "text-pink-600 dark:text-pink-400",
285
+ bgColor: "bg-pink-100 dark:bg-pink-900/30",
286
+ label: "Session Revoke",
287
+ },
288
+ file_upload: {
289
+ icon: Upload,
290
+ color: "text-violet-600 dark:text-violet-400",
291
+ bgColor: "bg-violet-100 dark:bg-violet-900/30",
292
+ label: "Upload",
293
+ },
294
+ file_download: {
295
+ icon: Download,
296
+ color: "text-violet-600 dark:text-violet-400",
297
+ bgColor: "bg-violet-100 dark:bg-violet-900/30",
298
+ label: "Download",
299
+ },
300
+ payment: {
301
+ icon: CreditCard,
302
+ color: "text-green-600 dark:text-green-400",
303
+ bgColor: "bg-green-100 dark:bg-green-900/30",
304
+ label: "Payment",
305
+ },
306
+ subscription_change: {
307
+ icon: CreditCard,
308
+ color: "text-blue-600 dark:text-blue-400",
309
+ bgColor: "bg-blue-100 dark:bg-blue-900/30",
310
+ label: "Subscription",
311
+ },
312
+ data_export: {
313
+ icon: Database,
314
+ color: "text-fuchsia-600 dark:text-fuchsia-400",
315
+ bgColor: "bg-fuchsia-100 dark:bg-fuchsia-900/30",
316
+ label: "Data Export",
317
+ },
318
+ backup: {
319
+ icon: Server,
320
+ color: "text-sky-600 dark:text-sky-400",
321
+ bgColor: "bg-sky-100 dark:bg-sky-900/30",
322
+ label: "Backup",
323
+ },
324
+ restore: {
325
+ icon: RefreshCw,
326
+ color: "text-sky-600 dark:text-sky-400",
327
+ bgColor: "bg-sky-100 dark:bg-sky-900/30",
328
+ label: "Restore",
329
+ },
330
+ }
331
+
332
+ // ============================================================================
333
+ // Severity Configuration
334
+ // ============================================================================
335
+
336
+ const severityConfig: Record<
337
+ AuditSeverity,
338
+ { icon: React.ElementType; color: string; bgColor: string; borderColor: string; label: string }
339
+ > = {
340
+ info: {
341
+ icon: Info,
342
+ color: "text-blue-600 dark:text-blue-400",
343
+ bgColor: "bg-blue-100 dark:bg-blue-900/30",
344
+ borderColor: "border-blue-200 dark:border-blue-800",
345
+ label: "Info",
346
+ },
347
+ warning: {
348
+ icon: AlertTriangle,
349
+ color: "text-amber-600 dark:text-amber-400",
350
+ bgColor: "bg-amber-100 dark:bg-amber-900/30",
351
+ borderColor: "border-amber-200 dark:border-amber-800",
352
+ label: "Warning",
353
+ },
354
+ critical: {
355
+ icon: AlertCircle,
356
+ color: "text-red-600 dark:text-red-400",
357
+ bgColor: "bg-red-100 dark:bg-red-900/30",
358
+ borderColor: "border-red-200 dark:border-red-800",
359
+ label: "Critical",
360
+ },
361
+ }
362
+
363
+ // ============================================================================
364
+ // Helper Functions
365
+ // ============================================================================
366
+
367
+ function formatRelativeTime(date: Date): string {
368
+ const now = new Date()
369
+ const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000)
370
+
371
+ if (diffInSeconds < 60) {
372
+ return "just now"
373
+ }
374
+
375
+ const diffInMinutes = Math.floor(diffInSeconds / 60)
376
+ if (diffInMinutes < 60) {
377
+ return `${diffInMinutes}m ago`
378
+ }
379
+
380
+ const diffInHours = Math.floor(diffInMinutes / 60)
381
+ if (diffInHours < 24) {
382
+ return `${diffInHours}h ago`
383
+ }
384
+
385
+ const diffInDays = Math.floor(diffInHours / 24)
386
+ if (diffInDays < 7) {
387
+ return `${diffInDays}d ago`
388
+ }
389
+
390
+ const diffInWeeks = Math.floor(diffInDays / 7)
391
+ if (diffInWeeks < 4) {
392
+ return `${diffInWeeks}w ago`
393
+ }
394
+
395
+ return date.toLocaleDateString()
396
+ }
397
+
398
+ function formatFullTimestamp(date: Date): string {
399
+ return date.toLocaleString(undefined, {
400
+ year: "numeric",
401
+ month: "short",
402
+ day: "numeric",
403
+ hour: "2-digit",
404
+ minute: "2-digit",
405
+ second: "2-digit",
406
+ })
407
+ }
408
+
409
+ function formatLocation(location?: AuditLocation): string | null {
410
+ if (!location) return null
411
+ const parts = []
412
+ if (location.city) parts.push(location.city)
413
+ if (location.country) parts.push(location.country)
414
+ return parts.length > 0 ? parts.join(", ") : null
415
+ }
416
+
417
+ // ============================================================================
418
+ // Avatar Component
419
+ // ============================================================================
420
+
421
+ interface AvatarProps {
422
+ user: AuditUser
423
+ size?: "sm" | "md" | "lg"
424
+ className?: string
425
+ }
426
+
427
+ function Avatar({ user, size = "md", className }: AvatarProps) {
428
+ const sizeClasses = {
429
+ sm: "h-6 w-6 text-[10px]",
430
+ md: "h-10 w-10 text-sm",
431
+ lg: "h-12 w-12 text-base",
432
+ }
433
+
434
+ const getInitials = (name: string) => {
435
+ return name
436
+ .split(" ")
437
+ .map((n) => n[0])
438
+ .join("")
439
+ .toUpperCase()
440
+ .slice(0, 2)
441
+ }
442
+
443
+ return (
444
+ <div
445
+ className={cn(
446
+ "relative flex items-center justify-center rounded-full font-semibold flex-shrink-0",
447
+ sizeClasses[size],
448
+ user.avatar ? "" : "bg-muted text-muted-foreground",
449
+ className
450
+ )}
451
+ >
452
+ {user.avatar ? (
453
+ <img
454
+ src={user.avatar}
455
+ alt={user.name}
456
+ className="h-full w-full rounded-full object-cover"
457
+ />
458
+ ) : (
459
+ getInitials(user.name)
460
+ )}
461
+ </div>
462
+ )
463
+ }
464
+
465
+ // ============================================================================
466
+ // Event Icon Component
467
+ // ============================================================================
468
+
469
+ interface EventIconProps {
470
+ type: AuditEventType
471
+ severity: AuditSeverity
472
+ className?: string
473
+ }
474
+
475
+ function EventIcon({ type, severity, className }: EventIconProps) {
476
+ const config = eventTypeConfig[type]
477
+ const Icon = config.icon
478
+
479
+ // Use severity color for warning/critical events
480
+ const colorClass = severity === "info" ? config.color : severityConfig[severity].color
481
+ const bgColorClass = severity === "info" ? config.bgColor : severityConfig[severity].bgColor
482
+
483
+ return (
484
+ <div
485
+ className={cn(
486
+ "flex h-9 w-9 items-center justify-center rounded-full flex-shrink-0",
487
+ bgColorClass,
488
+ className
489
+ )}
490
+ >
491
+ <Icon className={cn("h-4 w-4", colorClass)} />
492
+ </div>
493
+ )
494
+ }
495
+
496
+ // ============================================================================
497
+ // Severity Badge Component
498
+ // ============================================================================
499
+
500
+ interface SeverityBadgeProps {
501
+ severity: AuditSeverity
502
+ className?: string
503
+ }
504
+
505
+ function SeverityBadge({ severity, className }: SeverityBadgeProps) {
506
+ const config = severityConfig[severity]
507
+
508
+ return (
509
+ <span
510
+ className={cn(
511
+ "inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium",
512
+ config.bgColor,
513
+ config.color,
514
+ className
515
+ )}
516
+ >
517
+ {config.label}
518
+ </span>
519
+ )
520
+ }
521
+
522
+ // ============================================================================
523
+ // Event Details Component
524
+ // ============================================================================
525
+
526
+ interface EventDetailsProps {
527
+ event: AuditEvent
528
+ }
529
+
530
+ function EventDetails({ event }: EventDetailsProps) {
531
+ const locationText = formatLocation(event.location)
532
+
533
+ return (
534
+ <div className="mt-3 p-3 bg-muted/50 rounded-lg text-sm space-y-2 animate-in slide-in-from-top-2 fade-in duration-200">
535
+ {/* Full timestamp */}
536
+ <div className="flex items-center gap-2 text-muted-foreground">
537
+ <Calendar className="h-3.5 w-3.5" />
538
+ <span>{formatFullTimestamp(new Date(event.timestamp))}</span>
539
+ </div>
540
+
541
+ {/* IP Address */}
542
+ {event.ipAddress && (
543
+ <div className="flex items-center gap-2 text-muted-foreground">
544
+ <Globe className="h-3.5 w-3.5" />
545
+ <span>IP: {event.ipAddress}</span>
546
+ </div>
547
+ )}
548
+
549
+ {/* Location */}
550
+ {locationText && (
551
+ <div className="flex items-center gap-2 text-muted-foreground">
552
+ <MapPin className="h-3.5 w-3.5" />
553
+ <span>{locationText}</span>
554
+ </div>
555
+ )}
556
+
557
+ {/* Resource */}
558
+ {event.resource && (
559
+ <div className="flex items-center gap-2 text-muted-foreground">
560
+ <FileText className="h-3.5 w-3.5" />
561
+ <span>Resource: {event.resource}</span>
562
+ </div>
563
+ )}
564
+
565
+ {/* Session ID */}
566
+ {event.sessionId && (
567
+ <div className="flex items-center gap-2 text-muted-foreground">
568
+ <Key className="h-3.5 w-3.5" />
569
+ <span className="font-mono text-xs">Session: {event.sessionId}</span>
570
+ </div>
571
+ )}
572
+
573
+ {/* Request ID */}
574
+ {event.requestId && (
575
+ <div className="flex items-center gap-2 text-muted-foreground">
576
+ <Activity className="h-3.5 w-3.5" />
577
+ <span className="font-mono text-xs">Request: {event.requestId}</span>
578
+ </div>
579
+ )}
580
+
581
+ {/* User Agent */}
582
+ {event.userAgent && (
583
+ <div className="text-muted-foreground">
584
+ <span className="text-xs break-all">{event.userAgent}</span>
585
+ </div>
586
+ )}
587
+
588
+ {/* Additional Details */}
589
+ {event.details && Object.keys(event.details).length > 0 && (
590
+ <div className="pt-2 border-t border-border">
591
+ <div className="text-xs font-medium text-muted-foreground mb-2">Details</div>
592
+ <div className="space-y-1">
593
+ {Object.entries(event.details).map(([key, value]) => (
594
+ <div key={key} className="flex items-start gap-2 text-xs">
595
+ <span className="text-muted-foreground font-medium min-w-[80px]">{key}:</span>
596
+ <span className="text-foreground break-all">
597
+ {typeof value === "object" ? JSON.stringify(value) : String(value)}
598
+ </span>
599
+ </div>
600
+ ))}
601
+ </div>
602
+ </div>
603
+ )}
604
+ </div>
605
+ )
606
+ }
607
+
608
+ // ============================================================================
609
+ // Event Item Component
610
+ // ============================================================================
611
+
612
+ interface EventItemProps {
613
+ event: AuditEvent
614
+ onEventClick?: (event: AuditEvent) => void
615
+ isNew?: boolean
616
+ }
617
+
618
+ function EventItem({ event, onEventClick, isNew }: EventItemProps) {
619
+ const [isExpanded, setIsExpanded] = React.useState(false)
620
+ const config = eventTypeConfig[event.type]
621
+ const severityConf = severityConfig[event.severity]
622
+ const locationText = formatLocation(event.location)
623
+
624
+ const hasDetails =
625
+ event.details ||
626
+ event.ipAddress ||
627
+ event.userAgent ||
628
+ event.sessionId ||
629
+ event.requestId ||
630
+ event.location
631
+
632
+ return (
633
+ <div
634
+ className={cn(
635
+ "relative flex gap-3 p-4 rounded-lg border bg-card transition-all duration-300",
636
+ "hover:bg-accent/50 hover:shadow-sm",
637
+ event.severity === "critical" && "border-l-4 border-l-red-500",
638
+ event.severity === "warning" && "border-l-4 border-l-amber-500",
639
+ isNew && "animate-in slide-in-from-left-5 fade-in duration-500"
640
+ )}
641
+ >
642
+ {/* Timeline indicator */}
643
+ <div className="flex flex-col items-center gap-2">
644
+ <EventIcon type={event.type} severity={event.severity} />
645
+ <div className="flex-1 w-px bg-border" />
646
+ </div>
647
+
648
+ {/* Content */}
649
+ <div className="flex-1 min-w-0">
650
+ {/* Header */}
651
+ <div className="flex items-start justify-between gap-2 mb-1">
652
+ <div className="flex items-center gap-2 flex-wrap">
653
+ <span
654
+ className={cn(
655
+ "inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium",
656
+ config.bgColor,
657
+ config.color
658
+ )}
659
+ >
660
+ {config.label}
661
+ </span>
662
+ {event.severity !== "info" && <SeverityBadge severity={event.severity} />}
663
+ </div>
664
+ <span className="flex-shrink-0 text-xs text-muted-foreground">
665
+ {formatRelativeTime(new Date(event.timestamp))}
666
+ </span>
667
+ </div>
668
+
669
+ {/* Description */}
670
+ <p className="text-sm text-foreground mb-2">{event.description}</p>
671
+
672
+ {/* User info */}
673
+ <div className="flex items-center gap-2 mb-2">
674
+ <Avatar user={event.user} size="sm" />
675
+ <div className="flex flex-col">
676
+ <span className="text-sm font-medium">{event.user.name}</span>
677
+ {event.user.email && (
678
+ <span className="text-xs text-muted-foreground">{event.user.email}</span>
679
+ )}
680
+ </div>
681
+ {event.user.role && (
682
+ <span className="ml-2 text-xs text-muted-foreground bg-muted px-2 py-0.5 rounded">
683
+ {event.user.role}
684
+ </span>
685
+ )}
686
+ </div>
687
+
688
+ {/* Quick info row */}
689
+ <div className="flex items-center gap-3 text-xs text-muted-foreground flex-wrap">
690
+ {event.ipAddress && (
691
+ <span className="inline-flex items-center gap-1">
692
+ <Globe className="h-3 w-3" />
693
+ {event.ipAddress}
694
+ </span>
695
+ )}
696
+ {locationText && (
697
+ <span className="inline-flex items-center gap-1">
698
+ <MapPin className="h-3 w-3" />
699
+ {locationText}
700
+ </span>
701
+ )}
702
+ {event.resource && (
703
+ <span className="inline-flex items-center gap-1">
704
+ <FileText className="h-3 w-3" />
705
+ {event.resource}
706
+ </span>
707
+ )}
708
+ </div>
709
+
710
+ {/* Expandable details */}
711
+ {hasDetails && (
712
+ <div className="mt-2">
713
+ <button
714
+ className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
715
+ onClick={(e) => {
716
+ e.stopPropagation()
717
+ setIsExpanded(!isExpanded)
718
+ }}
719
+ >
720
+ {isExpanded ? (
721
+ <ChevronUp className="h-3 w-3" />
722
+ ) : (
723
+ <ChevronDown className="h-3 w-3" />
724
+ )}
725
+ {isExpanded ? "Hide details" : "Show details"}
726
+ </button>
727
+
728
+ {isExpanded && <EventDetails event={event} />}
729
+ </div>
730
+ )}
731
+ </div>
732
+
733
+ {/* Click handler */}
734
+ {onEventClick && (
735
+ <button
736
+ className="absolute inset-0 opacity-0"
737
+ onClick={() => onEventClick(event)}
738
+ aria-label={`View details for ${event.description}`}
739
+ />
740
+ )}
741
+ </div>
742
+ )
743
+ }
744
+
745
+ // ============================================================================
746
+ // Filter Bar Component
747
+ // ============================================================================
748
+
749
+ interface FilterBarProps {
750
+ filters: AuditFilters
751
+ onFilterChange: (filters: AuditFilters) => void
752
+ users?: AuditUser[]
753
+ showSearch?: boolean
754
+ }
755
+
756
+ function FilterBar({ filters, onFilterChange, users, showSearch }: FilterBarProps) {
757
+ const [isOpen, setIsOpen] = React.useState(false)
758
+ const [searchValue, setSearchValue] = React.useState(filters.search || "")
759
+
760
+ const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
761
+ const value = e.target.value
762
+ setSearchValue(value)
763
+ onFilterChange({ ...filters, search: value || undefined })
764
+ }
765
+
766
+ const toggleEventType = (type: AuditEventType) => {
767
+ const currentTypes = filters.eventTypes || []
768
+ const newTypes = currentTypes.includes(type)
769
+ ? currentTypes.filter((t) => t !== type)
770
+ : [...currentTypes, type]
771
+ onFilterChange({ ...filters, eventTypes: newTypes.length > 0 ? newTypes : undefined })
772
+ }
773
+
774
+ const toggleSeverity = (severity: AuditSeverity) => {
775
+ const currentSeverities = filters.severities || []
776
+ const newSeverities = currentSeverities.includes(severity)
777
+ ? currentSeverities.filter((s) => s !== severity)
778
+ : [...currentSeverities, severity]
779
+ onFilterChange({
780
+ ...filters,
781
+ severities: newSeverities.length > 0 ? newSeverities : undefined,
782
+ })
783
+ }
784
+
785
+ const toggleUser = (userId: string) => {
786
+ const currentUsers = filters.userIds || []
787
+ const newUsers = currentUsers.includes(userId)
788
+ ? currentUsers.filter((u) => u !== userId)
789
+ : [...currentUsers, userId]
790
+ onFilterChange({ ...filters, userIds: newUsers.length > 0 ? newUsers : undefined })
791
+ }
792
+
793
+ const clearFilters = () => {
794
+ setSearchValue("")
795
+ onFilterChange({})
796
+ }
797
+
798
+ const hasActiveFilters =
799
+ (filters.eventTypes && filters.eventTypes.length > 0) ||
800
+ (filters.severities && filters.severities.length > 0) ||
801
+ (filters.userIds && filters.userIds.length > 0) ||
802
+ filters.search ||
803
+ filters.startDate ||
804
+ filters.endDate
805
+
806
+ const eventTypeGroups = [
807
+ {
808
+ label: "Authentication",
809
+ types: ["login", "logout", "password_change", "mfa_enable", "mfa_disable", "session_revoke"] as AuditEventType[],
810
+ },
811
+ {
812
+ label: "Data",
813
+ types: ["create", "update", "delete", "view", "export", "import", "data_export"] as AuditEventType[],
814
+ },
815
+ {
816
+ label: "Users",
817
+ types: ["user_invite", "user_remove", "permission_change"] as AuditEventType[],
818
+ },
819
+ {
820
+ label: "System",
821
+ types: ["settings_change", "api_key_create", "api_key_revoke", "backup", "restore"] as AuditEventType[],
822
+ },
823
+ {
824
+ label: "Files",
825
+ types: ["file_upload", "file_download"] as AuditEventType[],
826
+ },
827
+ {
828
+ label: "Billing",
829
+ types: ["payment", "subscription_change"] as AuditEventType[],
830
+ },
831
+ ]
832
+
833
+ return (
834
+ <div className="space-y-3 mb-4">
835
+ {/* Search and filter toggle */}
836
+ <div className="flex items-center gap-2">
837
+ {showSearch && (
838
+ <div className="relative flex-1">
839
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
840
+ <input
841
+ type="text"
842
+ placeholder="Search audit logs..."
843
+ value={searchValue}
844
+ onChange={handleSearchChange}
845
+ className="w-full pl-9 pr-4 py-2 text-sm rounded-lg border bg-background focus:outline-none focus:ring-2 focus:ring-ring"
846
+ />
847
+ </div>
848
+ )}
849
+
850
+ <button
851
+ className={cn(
852
+ "inline-flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-lg border transition-colors",
853
+ isOpen || hasActiveFilters
854
+ ? "bg-primary text-primary-foreground"
855
+ : "bg-background hover:bg-muted"
856
+ )}
857
+ onClick={() => setIsOpen(!isOpen)}
858
+ >
859
+ <Filter className="h-4 w-4" />
860
+ Filters
861
+ {hasActiveFilters && (
862
+ <span className="inline-flex items-center justify-center h-5 w-5 rounded-full bg-primary-foreground text-primary text-xs">
863
+ {(filters.eventTypes?.length || 0) +
864
+ (filters.severities?.length || 0) +
865
+ (filters.userIds?.length || 0)}
866
+ </span>
867
+ )}
868
+ </button>
869
+
870
+ {hasActiveFilters && (
871
+ <button
872
+ className="inline-flex items-center gap-1 px-3 py-2 text-sm font-medium rounded-lg border bg-background hover:bg-muted transition-colors"
873
+ onClick={clearFilters}
874
+ >
875
+ <X className="h-4 w-4" />
876
+ Clear
877
+ </button>
878
+ )}
879
+ </div>
880
+
881
+ {/* Filter panel */}
882
+ {isOpen && (
883
+ <div className="p-4 bg-muted/50 rounded-lg border space-y-4 animate-in slide-in-from-top-2 fade-in duration-200">
884
+ {/* Severity filters */}
885
+ <div>
886
+ <div className="text-xs font-medium text-muted-foreground mb-2">Severity</div>
887
+ <div className="flex flex-wrap gap-2">
888
+ {(Object.keys(severityConfig) as AuditSeverity[]).map((severity) => {
889
+ const config = severityConfig[severity]
890
+ const isActive = filters.severities?.includes(severity)
891
+
892
+ return (
893
+ <button
894
+ key={severity}
895
+ className={cn(
896
+ "px-3 py-1.5 rounded-lg text-xs font-medium transition-all",
897
+ isActive
898
+ ? cn(config.bgColor, config.color, "ring-2 ring-offset-1", config.borderColor)
899
+ : "bg-background hover:bg-muted text-muted-foreground border"
900
+ )}
901
+ onClick={() => toggleSeverity(severity)}
902
+ >
903
+ {config.label}
904
+ </button>
905
+ )
906
+ })}
907
+ </div>
908
+ </div>
909
+
910
+ {/* Event type filters */}
911
+ <div>
912
+ <div className="text-xs font-medium text-muted-foreground mb-2">Event Types</div>
913
+ <div className="space-y-3">
914
+ {eventTypeGroups.map((group) => (
915
+ <div key={group.label}>
916
+ <div className="text-xs text-muted-foreground mb-1.5">{group.label}</div>
917
+ <div className="flex flex-wrap gap-1.5">
918
+ {group.types.map((type) => {
919
+ const config = eventTypeConfig[type]
920
+ const isActive = filters.eventTypes?.includes(type)
921
+
922
+ return (
923
+ <button
924
+ key={type}
925
+ className={cn(
926
+ "px-2 py-1 rounded text-xs font-medium transition-all",
927
+ isActive
928
+ ? cn(config.bgColor, config.color)
929
+ : "bg-background hover:bg-muted text-muted-foreground border"
930
+ )}
931
+ onClick={() => toggleEventType(type)}
932
+ >
933
+ {config.label}
934
+ </button>
935
+ )
936
+ })}
937
+ </div>
938
+ </div>
939
+ ))}
940
+ </div>
941
+ </div>
942
+
943
+ {/* User filters */}
944
+ {users && users.length > 0 && (
945
+ <div>
946
+ <div className="text-xs font-medium text-muted-foreground mb-2">Users</div>
947
+ <div className="flex flex-wrap gap-2">
948
+ {users.map((user) => {
949
+ const isActive = filters.userIds?.includes(user.id)
950
+
951
+ return (
952
+ <button
953
+ key={user.id}
954
+ className={cn(
955
+ "inline-flex items-center gap-2 px-2 py-1 rounded-lg text-xs font-medium transition-all",
956
+ isActive
957
+ ? "bg-primary text-primary-foreground"
958
+ : "bg-background hover:bg-muted text-muted-foreground border"
959
+ )}
960
+ onClick={() => toggleUser(user.id)}
961
+ >
962
+ <Avatar user={user} size="sm" className="h-4 w-4 text-[8px]" />
963
+ {user.name}
964
+ </button>
965
+ )
966
+ })}
967
+ </div>
968
+ </div>
969
+ )}
970
+
971
+ {/* Date range filters */}
972
+ <div>
973
+ <div className="text-xs font-medium text-muted-foreground mb-2">Date Range</div>
974
+ <div className="flex items-center gap-2">
975
+ <input
976
+ type="date"
977
+ value={filters.startDate?.toISOString().split("T")[0] || ""}
978
+ onChange={(e) =>
979
+ onFilterChange({
980
+ ...filters,
981
+ startDate: e.target.value ? new Date(e.target.value) : undefined,
982
+ })
983
+ }
984
+ className="px-3 py-1.5 text-xs rounded-lg border bg-background focus:outline-none focus:ring-2 focus:ring-ring"
985
+ />
986
+ <span className="text-xs text-muted-foreground">to</span>
987
+ <input
988
+ type="date"
989
+ value={filters.endDate?.toISOString().split("T")[0] || ""}
990
+ onChange={(e) =>
991
+ onFilterChange({
992
+ ...filters,
993
+ endDate: e.target.value ? new Date(e.target.value) : undefined,
994
+ })
995
+ }
996
+ className="px-3 py-1.5 text-xs rounded-lg border bg-background focus:outline-none focus:ring-2 focus:ring-ring"
997
+ />
998
+ </div>
999
+ </div>
1000
+ </div>
1001
+ )}
1002
+ </div>
1003
+ )
1004
+ }
1005
+
1006
+ // ============================================================================
1007
+ // Export Menu Component
1008
+ // ============================================================================
1009
+
1010
+ interface ExportMenuProps {
1011
+ onExport: (format: "csv" | "json") => void
1012
+ }
1013
+
1014
+ function ExportMenu({ onExport }: ExportMenuProps) {
1015
+ const [isOpen, setIsOpen] = React.useState(false)
1016
+
1017
+ return (
1018
+ <div className="relative">
1019
+ <button
1020
+ className="inline-flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-lg border bg-background hover:bg-muted transition-colors"
1021
+ onClick={() => setIsOpen(!isOpen)}
1022
+ >
1023
+ <Download className="h-4 w-4" />
1024
+ Export
1025
+ <ChevronDown className={cn("h-4 w-4 transition-transform", isOpen && "rotate-180")} />
1026
+ </button>
1027
+
1028
+ {isOpen && (
1029
+ <>
1030
+ <div className="fixed inset-0 z-10" onClick={() => setIsOpen(false)} />
1031
+ <div className="absolute right-0 top-full mt-1 z-20 w-40 bg-popover border rounded-lg shadow-lg overflow-hidden animate-in fade-in slide-in-from-top-2 duration-200">
1032
+ <button
1033
+ className="w-full px-3 py-2 text-sm text-left hover:bg-muted transition-colors flex items-center gap-2"
1034
+ onClick={() => {
1035
+ onExport("csv")
1036
+ setIsOpen(false)
1037
+ }}
1038
+ >
1039
+ <FileText className="h-4 w-4" />
1040
+ Export as CSV
1041
+ </button>
1042
+ <button
1043
+ className="w-full px-3 py-2 text-sm text-left hover:bg-muted transition-colors flex items-center gap-2"
1044
+ onClick={() => {
1045
+ onExport("json")
1046
+ setIsOpen(false)
1047
+ }}
1048
+ >
1049
+ <FileText className="h-4 w-4" />
1050
+ Export as JSON
1051
+ </button>
1052
+ </div>
1053
+ </>
1054
+ )}
1055
+ </div>
1056
+ )
1057
+ }
1058
+
1059
+ // ============================================================================
1060
+ // Main Component
1061
+ // ============================================================================
1062
+
1063
+ export function WakaAuditLog({
1064
+ events,
1065
+ onEventClick,
1066
+ onFilterChange,
1067
+ onLoadMore,
1068
+ onExport,
1069
+ hasMore = false,
1070
+ loading = false,
1071
+ showFilters = true,
1072
+ showSearch = true,
1073
+ showExport = true,
1074
+ infiniteScroll = false,
1075
+ maxItems,
1076
+ users,
1077
+ className,
1078
+ }: WakaAuditLogProps) {
1079
+ const [filters, setFilters] = React.useState<AuditFilters>({})
1080
+ const [newEventIds, setNewEventIds] = React.useState<Set<string>>(new Set())
1081
+ const prevEventIdsRef = React.useRef<Set<string>>(new Set())
1082
+ const observerRef = React.useRef<IntersectionObserver | null>(null)
1083
+ const loadMoreRef = React.useRef<HTMLDivElement>(null)
1084
+
1085
+ // Track new events for animation
1086
+ React.useEffect(() => {
1087
+ const currentIds = new Set(events.map((e) => e.id))
1088
+ const newIds = new Set<string>()
1089
+
1090
+ currentIds.forEach((id) => {
1091
+ if (!prevEventIdsRef.current.has(id)) {
1092
+ newIds.add(id)
1093
+ }
1094
+ })
1095
+
1096
+ if (newIds.size > 0 && prevEventIdsRef.current.size > 0) {
1097
+ setNewEventIds(newIds)
1098
+ const timer = setTimeout(() => setNewEventIds(new Set()), 500)
1099
+ return () => clearTimeout(timer)
1100
+ }
1101
+
1102
+ prevEventIdsRef.current = currentIds
1103
+ }, [events])
1104
+
1105
+ // Handle filter changes
1106
+ const handleFilterChange = React.useCallback(
1107
+ (newFilters: AuditFilters) => {
1108
+ setFilters(newFilters)
1109
+ onFilterChange?.(newFilters)
1110
+ },
1111
+ [onFilterChange]
1112
+ )
1113
+
1114
+ // Filter events
1115
+ const filteredEvents = React.useMemo(() => {
1116
+ let result = events
1117
+
1118
+ if (filters.eventTypes && filters.eventTypes.length > 0) {
1119
+ result = result.filter((e) => filters.eventTypes!.includes(e.type))
1120
+ }
1121
+
1122
+ if (filters.severities && filters.severities.length > 0) {
1123
+ result = result.filter((e) => filters.severities!.includes(e.severity))
1124
+ }
1125
+
1126
+ if (filters.userIds && filters.userIds.length > 0) {
1127
+ result = result.filter((e) => filters.userIds!.includes(e.user.id))
1128
+ }
1129
+
1130
+ if (filters.search) {
1131
+ const searchLower = filters.search.toLowerCase()
1132
+ result = result.filter(
1133
+ (e) =>
1134
+ e.description.toLowerCase().includes(searchLower) ||
1135
+ e.user.name.toLowerCase().includes(searchLower) ||
1136
+ e.user.email?.toLowerCase().includes(searchLower) ||
1137
+ e.resource?.toLowerCase().includes(searchLower) ||
1138
+ e.ipAddress?.includes(searchLower)
1139
+ )
1140
+ }
1141
+
1142
+ if (filters.startDate) {
1143
+ result = result.filter((e) => new Date(e.timestamp) >= filters.startDate!)
1144
+ }
1145
+
1146
+ if (filters.endDate) {
1147
+ const endOfDay = new Date(filters.endDate)
1148
+ endOfDay.setHours(23, 59, 59, 999)
1149
+ result = result.filter((e) => new Date(e.timestamp) <= endOfDay)
1150
+ }
1151
+
1152
+ if (maxItems && maxItems > 0) {
1153
+ result = result.slice(0, maxItems)
1154
+ }
1155
+
1156
+ return result
1157
+ }, [events, filters, maxItems])
1158
+
1159
+ // Infinite scroll observer
1160
+ React.useEffect(() => {
1161
+ if (!infiniteScroll || !onLoadMore || !hasMore) return
1162
+
1163
+ observerRef.current = new IntersectionObserver(
1164
+ (entries) => {
1165
+ if (entries[0].isIntersecting && !loading) {
1166
+ onLoadMore()
1167
+ }
1168
+ },
1169
+ { threshold: 0.1 }
1170
+ )
1171
+
1172
+ if (loadMoreRef.current) {
1173
+ observerRef.current.observe(loadMoreRef.current)
1174
+ }
1175
+
1176
+ return () => {
1177
+ observerRef.current?.disconnect()
1178
+ }
1179
+ }, [infiniteScroll, onLoadMore, hasMore, loading])
1180
+
1181
+ if (events.length === 0 && !loading) {
1182
+ return (
1183
+ <div
1184
+ className={cn(
1185
+ "flex flex-col items-center justify-center py-12 text-center",
1186
+ className
1187
+ )}
1188
+ >
1189
+ <Activity className="h-12 w-12 text-muted-foreground/50 mb-4" />
1190
+ <h3 className="text-lg font-medium text-muted-foreground">No audit events</h3>
1191
+ <p className="text-sm text-muted-foreground/70 mt-1">
1192
+ Activity will appear here when events are logged.
1193
+ </p>
1194
+ </div>
1195
+ )
1196
+ }
1197
+
1198
+ return (
1199
+ <div className={cn("w-full", className)}>
1200
+ {/* Header with filters and export */}
1201
+ <div className="flex items-start justify-between gap-4 mb-4">
1202
+ <div className="flex-1">
1203
+ {(showFilters || showSearch) && (
1204
+ <FilterBar
1205
+ filters={filters}
1206
+ onFilterChange={handleFilterChange}
1207
+ users={users}
1208
+ showSearch={showSearch}
1209
+ />
1210
+ )}
1211
+ </div>
1212
+
1213
+ {showExport && onExport && <ExportMenu onExport={onExport} />}
1214
+ </div>
1215
+
1216
+ {/* Event List */}
1217
+ <div className="space-y-3">
1218
+ {filteredEvents.map((event) => (
1219
+ <EventItem
1220
+ key={event.id}
1221
+ event={event}
1222
+ onEventClick={onEventClick}
1223
+ isNew={newEventIds.has(event.id)}
1224
+ />
1225
+ ))}
1226
+ </div>
1227
+
1228
+ {/* Loading State */}
1229
+ {loading && (
1230
+ <div className="flex items-center justify-center py-6">
1231
+ <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
1232
+ </div>
1233
+ )}
1234
+
1235
+ {/* Load More / Infinite Scroll Target */}
1236
+ {!loading && hasMore && (
1237
+ <>
1238
+ {infiniteScroll ? (
1239
+ <div ref={loadMoreRef} className="h-1" />
1240
+ ) : (
1241
+ <div className="flex justify-center pt-4">
1242
+ <button
1243
+ className="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg border bg-background hover:bg-muted transition-colors"
1244
+ onClick={onLoadMore}
1245
+ >
1246
+ <ChevronDown className="h-4 w-4" />
1247
+ Load more
1248
+ </button>
1249
+ </div>
1250
+ )}
1251
+ </>
1252
+ )}
1253
+
1254
+ {/* No results after filtering */}
1255
+ {filteredEvents.length === 0 && events.length > 0 && !loading && (
1256
+ <div className="flex flex-col items-center justify-center py-12 text-center">
1257
+ <Filter className="h-8 w-8 text-muted-foreground mb-3" />
1258
+ <p className="text-sm text-muted-foreground">No events match the current filters</p>
1259
+ </div>
1260
+ )}
1261
+ </div>
1262
+ )
1263
+ }
1264
+
1265
+ // ============================================================================
1266
+ // Hook for managing audit log state
1267
+ // ============================================================================
1268
+
1269
+ export interface UseAuditLogOptions {
1270
+ /** Initial events */
1271
+ initialEvents?: AuditEvent[]
1272
+ /** Maximum events to keep in memory */
1273
+ maxEvents?: number
1274
+ /** Initial filters */
1275
+ initialFilters?: AuditFilters
1276
+ }
1277
+
1278
+ export interface UseAuditLogReturn {
1279
+ /** Current audit events */
1280
+ events: AuditEvent[]
1281
+ /** Current filters */
1282
+ filters: AuditFilters
1283
+ /** Loading state */
1284
+ loading: boolean
1285
+ /** Whether more events can be loaded */
1286
+ hasMore: boolean
1287
+ /** Add a new event */
1288
+ addEvent: (event: AuditEvent) => void
1289
+ /** Add multiple events */
1290
+ addEvents: (events: AuditEvent[]) => void
1291
+ /** Remove an event */
1292
+ removeEvent: (eventId: string) => void
1293
+ /** Set filters */
1294
+ setFilters: (filters: AuditFilters) => void
1295
+ /** Clear all filters */
1296
+ clearFilters: () => void
1297
+ /** Load more events */
1298
+ loadMore: (fetcher: () => Promise<{ events: AuditEvent[]; hasMore: boolean }>) => Promise<void>
1299
+ /** Refresh all events */
1300
+ refresh: (events: AuditEvent[]) => void
1301
+ /** Clear all events */
1302
+ clear: () => void
1303
+ /** Set loading state */
1304
+ setLoading: (loading: boolean) => void
1305
+ /** Set has more state */
1306
+ setHasMore: (hasMore: boolean) => void
1307
+ /** Export events to format */
1308
+ exportEvents: (format: "csv" | "json") => string
1309
+ /** Get filtered events */
1310
+ getFilteredEvents: () => AuditEvent[]
1311
+ }
1312
+
1313
+ export function useAuditLog({
1314
+ initialEvents = [],
1315
+ maxEvents = 1000,
1316
+ initialFilters = {},
1317
+ }: UseAuditLogOptions = {}): UseAuditLogReturn {
1318
+ const [events, setEvents] = React.useState<AuditEvent[]>(initialEvents)
1319
+ const [filters, setFilters] = React.useState<AuditFilters>(initialFilters)
1320
+ const [loading, setLoading] = React.useState(false)
1321
+ const [hasMore, setHasMore] = React.useState(false)
1322
+
1323
+ const addEvent = React.useCallback(
1324
+ (event: AuditEvent) => {
1325
+ setEvents((prev) => {
1326
+ const updated = [event, ...prev]
1327
+ return updated.slice(0, maxEvents)
1328
+ })
1329
+ },
1330
+ [maxEvents]
1331
+ )
1332
+
1333
+ const addEvents = React.useCallback(
1334
+ (newEvents: AuditEvent[]) => {
1335
+ setEvents((prev) => {
1336
+ const updated = [...newEvents, ...prev]
1337
+ return updated.slice(0, maxEvents)
1338
+ })
1339
+ },
1340
+ [maxEvents]
1341
+ )
1342
+
1343
+ const removeEvent = React.useCallback((eventId: string) => {
1344
+ setEvents((prev) => prev.filter((e) => e.id !== eventId))
1345
+ }, [])
1346
+
1347
+ const clearFilters = React.useCallback(() => {
1348
+ setFilters({})
1349
+ }, [])
1350
+
1351
+ const loadMore = React.useCallback(
1352
+ async (fetcher: () => Promise<{ events: AuditEvent[]; hasMore: boolean }>) => {
1353
+ setLoading(true)
1354
+ try {
1355
+ const result = await fetcher()
1356
+ setEvents((prev) => {
1357
+ const existingIds = new Set(prev.map((e) => e.id))
1358
+ const newEvents = result.events.filter((e) => !existingIds.has(e.id))
1359
+ return [...prev, ...newEvents].slice(0, maxEvents)
1360
+ })
1361
+ setHasMore(result.hasMore)
1362
+ } finally {
1363
+ setLoading(false)
1364
+ }
1365
+ },
1366
+ [maxEvents]
1367
+ )
1368
+
1369
+ const refresh = React.useCallback((newEvents: AuditEvent[]) => {
1370
+ setEvents(newEvents)
1371
+ }, [])
1372
+
1373
+ const clear = React.useCallback(() => {
1374
+ setEvents([])
1375
+ }, [])
1376
+
1377
+ const getFilteredEvents = React.useCallback(() => {
1378
+ let result = events
1379
+
1380
+ if (filters.eventTypes && filters.eventTypes.length > 0) {
1381
+ result = result.filter((e) => filters.eventTypes!.includes(e.type))
1382
+ }
1383
+
1384
+ if (filters.severities && filters.severities.length > 0) {
1385
+ result = result.filter((e) => filters.severities!.includes(e.severity))
1386
+ }
1387
+
1388
+ if (filters.userIds && filters.userIds.length > 0) {
1389
+ result = result.filter((e) => filters.userIds!.includes(e.user.id))
1390
+ }
1391
+
1392
+ if (filters.search) {
1393
+ const searchLower = filters.search.toLowerCase()
1394
+ result = result.filter(
1395
+ (e) =>
1396
+ e.description.toLowerCase().includes(searchLower) ||
1397
+ e.user.name.toLowerCase().includes(searchLower) ||
1398
+ e.user.email?.toLowerCase().includes(searchLower)
1399
+ )
1400
+ }
1401
+
1402
+ if (filters.startDate) {
1403
+ result = result.filter((e) => new Date(e.timestamp) >= filters.startDate!)
1404
+ }
1405
+
1406
+ if (filters.endDate) {
1407
+ const endOfDay = new Date(filters.endDate)
1408
+ endOfDay.setHours(23, 59, 59, 999)
1409
+ result = result.filter((e) => new Date(e.timestamp) <= endOfDay)
1410
+ }
1411
+
1412
+ return result
1413
+ }, [events, filters])
1414
+
1415
+ const exportEvents = React.useCallback(
1416
+ (format: "csv" | "json"): string => {
1417
+ const eventsToExport = getFilteredEvents()
1418
+
1419
+ if (format === "json") {
1420
+ return JSON.stringify(eventsToExport, null, 2)
1421
+ }
1422
+
1423
+ // CSV format
1424
+ const headers = [
1425
+ "ID",
1426
+ "Type",
1427
+ "Severity",
1428
+ "Description",
1429
+ "User Name",
1430
+ "User Email",
1431
+ "IP Address",
1432
+ "Location",
1433
+ "Resource",
1434
+ "Timestamp",
1435
+ ]
1436
+
1437
+ const rows = eventsToExport.map((e) => [
1438
+ e.id,
1439
+ e.type,
1440
+ e.severity,
1441
+ `"${e.description.replace(/"/g, '""')}"`,
1442
+ e.user.name,
1443
+ e.user.email || "",
1444
+ e.ipAddress || "",
1445
+ formatLocation(e.location) || "",
1446
+ e.resource || "",
1447
+ new Date(e.timestamp).toISOString(),
1448
+ ])
1449
+
1450
+ return [headers.join(","), ...rows.map((r) => r.join(","))].join("\n")
1451
+ },
1452
+ [getFilteredEvents]
1453
+ )
1454
+
1455
+ return {
1456
+ events,
1457
+ filters,
1458
+ loading,
1459
+ hasMore,
1460
+ addEvent,
1461
+ addEvents,
1462
+ removeEvent,
1463
+ setFilters,
1464
+ clearFilters,
1465
+ loadMore,
1466
+ refresh,
1467
+ clear,
1468
+ setLoading,
1469
+ setHasMore,
1470
+ exportEvents,
1471
+ getFilteredEvents,
1472
+ }
1473
+ }
1474
+
1475
+ export default WakaAuditLog