betterstart-cli 0.0.28 → 0.0.29

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 (82) hide show
  1. package/dist/assets/adapters/next/integrations/mailchimp/actions/mailchimp.ts +0 -1
  2. package/dist/assets/adapters/next/plugins/blog/schemas/menus.json +1 -1
  3. package/dist/assets/adapters/next/plugins/blog/schemas/posts.json +1 -1
  4. package/dist/assets/adapters/next/templates/init/admin-globals.css +1 -1
  5. package/dist/assets/adapters/next/templates/init/components/layouts/admin-nav-link.tsx +12 -2
  6. package/dist/assets/adapters/next/templates/init/components/layouts/admin-settings-sidebar.tsx +75 -0
  7. package/dist/assets/adapters/next/templates/init/components/layouts/admin-sidebar-nav-link.tsx +12 -2
  8. package/dist/assets/adapters/next/templates/init/components/layouts/admin-sidebar.tsx +2 -8
  9. package/dist/assets/adapters/next/templates/init/components/shared/data-table/data-grid.tsx +14 -5
  10. package/dist/assets/adapters/next/templates/init/components/shared/data-table/data-table.tsx +111 -112
  11. package/dist/assets/adapters/next/templates/init/components/shared/dev-mode/dev-mode-types.ts +4 -0
  12. package/dist/assets/adapters/next/templates/init/components/shared/dev-mode-integrate.tsx +1 -1
  13. package/dist/assets/adapters/next/templates/init/components/shared/entity-versions/entity-versions-drawer.tsx +1 -1
  14. package/dist/assets/adapters/next/templates/init/components/shared/media/media-url-importer.tsx +45 -47
  15. package/dist/assets/adapters/next/templates/init/components/shared/page-header.tsx +1 -1
  16. package/dist/assets/adapters/next/templates/init/data/webhook-events.ts +5 -0
  17. package/dist/assets/adapters/next/templates/init/hooks/use-table-utils.ts +27 -7
  18. package/dist/assets/adapters/next/templates/init/hooks/use-webhooks.ts +211 -0
  19. package/dist/assets/adapters/next/templates/init/lib/actions/audit/types.ts +2 -0
  20. package/dist/assets/adapters/next/templates/init/lib/actions/forms/index.ts +0 -1
  21. package/dist/assets/adapters/next/templates/init/lib/actions/forms/types.ts +0 -4
  22. package/dist/assets/adapters/next/templates/init/lib/actions/forms/upsert-form-settings.ts +0 -2
  23. package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/create-webhook.ts +55 -0
  24. package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/delete-webhook.ts +26 -0
  25. package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/dispatch.ts +135 -0
  26. package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/get-webhook-deliveries.ts +124 -0
  27. package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/get-webhook-secret.ts +28 -0
  28. package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/get-webhook-subscriptions.ts +32 -0
  29. package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/get-webhooks.ts +154 -0
  30. package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/index.ts +30 -0
  31. package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/internal-get-active-endpoints.ts +20 -0
  32. package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/regenerate-webhook-secret.ts +35 -0
  33. package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/register.ts +70 -0
  34. package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/send-test-webhook.ts +81 -0
  35. package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/types.ts +104 -0
  36. package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/update-webhook.ts +72 -0
  37. package/dist/assets/adapters/next/templates/init/lib/db/core/schema.ts +71 -2
  38. package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/register.ts +2 -0
  39. package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/runner.ts +3 -1
  40. package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/types.ts +6 -1
  41. package/dist/assets/adapters/next/templates/init/pages/auth-gate-rsc.tsx +1 -1
  42. package/dist/assets/adapters/next/templates/init/pages/media/media-page-content.tsx +1 -1
  43. package/dist/assets/adapters/next/templates/init/pages/media/media-page-skeleton.tsx +1 -1
  44. package/dist/assets/adapters/next/templates/init/pages/settings/audit-log/audit-log-columns.tsx +150 -0
  45. package/dist/assets/adapters/next/templates/init/pages/settings/audit-log/audit-log-page-content.tsx +137 -0
  46. package/dist/assets/adapters/next/templates/init/pages/{audit-log → settings/audit-log}/audit-log-page-skeleton.tsx +1 -1
  47. package/dist/assets/adapters/next/templates/init/pages/settings/audit-log/audit-log-table.tsx +295 -0
  48. package/dist/assets/adapters/next/templates/init/pages/settings/forms/edit-form-notifications-dialog.tsx +128 -0
  49. package/dist/assets/adapters/next/templates/init/pages/settings/forms/forms-settings-page-content.tsx +111 -0
  50. package/dist/assets/adapters/next/templates/init/pages/settings/forms/forms-settings-page-skeleton.tsx +7 -0
  51. package/dist/assets/adapters/next/templates/init/pages/settings/forms/forms-settings-page.tsx +14 -0
  52. package/dist/assets/adapters/next/templates/init/pages/settings/settings-layout.tsx +10 -0
  53. package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/delete-webhook-dialog.tsx +82 -0
  54. package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhook-actions.tsx +53 -0
  55. package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhook-enabled-switch.tsx +18 -0
  56. package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhook-endpoint-dialog.tsx +370 -0
  57. package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhooks-columns.tsx +258 -0
  58. package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhooks-logs-columns.tsx +200 -0
  59. package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhooks-logs-page-content.tsx +117 -0
  60. package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhooks-logs-page.tsx +14 -0
  61. package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhooks-logs-table.tsx +253 -0
  62. package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhooks-page-content.tsx +161 -0
  63. package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhooks-page-skeleton.tsx +7 -0
  64. package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhooks-page.tsx +14 -0
  65. package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhooks-table.tsx +264 -0
  66. package/dist/assets/adapters/next/templates/init/pages/users/users-page-skeleton.tsx +1 -1
  67. package/dist/assets/adapters/next/templates/init/pages/users/users-table.tsx +103 -109
  68. package/dist/assets/adapters/next/templates/init/types/webhooks.ts +7 -0
  69. package/dist/assets/adapters/next/templates/init/utils/webhook/signature.ts +13 -0
  70. package/dist/assets/shared-assets/react-admin/ui/card.tsx +1 -1
  71. package/dist/assets/shared-assets/react-admin/ui/drawer.tsx +2 -2
  72. package/dist/assets/shared-assets/react-admin/ui/input-group.tsx +3 -3
  73. package/dist/assets/shared-assets/react-admin/ui/sidebar.tsx +1 -1
  74. package/dist/cli.js +806 -554
  75. package/dist/cli.js.map +1 -1
  76. package/package.json +1 -1
  77. package/dist/assets/adapters/next/templates/init/lib/actions/forms/test-form-webhook.ts +0 -40
  78. package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-page-content.tsx +0 -550
  79. package/dist/assets/adapters/next/templates/init/utils/webhook/webhook.ts +0 -28
  80. /package/dist/assets/adapters/next/templates/init/pages/{audit-log → settings/audit-log}/audit-log-detail-drawer.tsx +0 -0
  81. /package/dist/assets/adapters/next/templates/init/pages/{audit-log → settings/audit-log}/audit-log-detail-row.tsx +0 -0
  82. /package/dist/assets/adapters/next/templates/init/pages/{audit-log → settings/audit-log}/audit-log-page.tsx +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "betterstart-cli",
3
- "version": "0.0.28",
3
+ "version": "0.0.29",
4
4
  "description": "Scaffold a plugin-based Admin into any Next.js 16 application",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,40 +0,0 @@
1
- 'use server'
2
-
3
- import { getFormSettings } from './get-form-settings'
4
-
5
- export async function testFormWebhook(
6
- formName: string
7
- ): Promise<{ success: boolean; error?: string }> {
8
- try {
9
- const settings = await getFormSettings(formName)
10
- if (!settings?.webhookUrl) {
11
- return { success: false, error: 'No webhook URL configured' }
12
- }
13
-
14
- const formData = new URLSearchParams()
15
- formData.append('form_name', formName)
16
- formData.append('test', 'true')
17
- formData.append('message', 'This is a test webhook from BetterStart')
18
- formData.append('timestamp', new Date().toISOString())
19
-
20
- const response = await fetch(settings.webhookUrl, {
21
- method: 'POST',
22
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
23
- body: formData.toString()
24
- })
25
-
26
- if (!response.ok) {
27
- return {
28
- success: false,
29
- error: `Webhook returned status ${response.status}`
30
- }
31
- }
32
-
33
- return { success: true }
34
- } catch (error) {
35
- return {
36
- success: false,
37
- error: error instanceof Error ? error.message : 'Failed to send test webhook'
38
- }
39
- }
40
- }
@@ -1,550 +0,0 @@
1
- 'use client'
2
-
3
- import type { AuditLogEntry, AuditLogQuery, AuditLogSortField } from '@admin/actions/audit'
4
- import { AUDIT_LOG_ACTIONS, AUDIT_LOG_CATEGORIES } from '@admin/actions/audit'
5
- import {
6
- DataGrid,
7
- DataGridBody,
8
- DataGridCell,
9
- DataGridHead,
10
- DataGridHeader,
11
- DataGridRow
12
- } from '@admin/components/shared/data-table/data-grid'
13
- import { DataTablePagination } from '@admin/components/shared/data-table/data-table-pagination'
14
- import { EntityFiltersBar } from '@admin/components/shared/entity-filters-bar'
15
- import { PageHeader } from '@admin/components/shared/page-header'
16
- import { SortIndicator } from '@admin/components/shared/sort-indicator'
17
- import { Badge } from '@admin/components/ui/badge'
18
- import { Button } from '@admin/components/ui/button'
19
- import { Card, CardContent } from '@admin/components/ui/card'
20
- import { useAuditLogs, useCleanupAuditLogs } from '@admin/hooks/use-audit-log'
21
- import { useTableUtils } from '@admin/hooks/use-table-utils'
22
- import { cn } from '@admin/utils/shared/cn'
23
- import { type ColumnDef, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table'
24
- import { GripVertical } from 'lucide-react'
25
- import { parseAsInteger, parseAsString, useQueryStates } from 'nuqs'
26
- import * as React from 'react'
27
- import { toast } from 'sonner'
28
- import { AuditLogDetailDrawer } from './audit-log-detail-drawer'
29
-
30
- const DEFAULT_PAGE_SIZE = 20
31
- const AUDIT_LOG_SORT_FIELDS = [
32
- 'createdAt',
33
- 'event',
34
- 'target',
35
- 'actor',
36
- 'sourceType'
37
- ] as const satisfies AuditLogSortField[]
38
- type AuditLogSortDirection = NonNullable<AuditLogQuery['dir']>
39
-
40
- const auditLogParsers = {
41
- q: parseAsString.withDefault(''),
42
- category: parseAsString.withDefault(''),
43
- action: parseAsString.withDefault(''),
44
- createdAtFrom: parseAsString.withDefault(''),
45
- createdAtTo: parseAsString.withDefault(''),
46
- page: parseAsInteger.withDefault(0),
47
- size: parseAsInteger.withDefault(DEFAULT_PAGE_SIZE),
48
- sort: parseAsString.withDefault(''),
49
- dir: parseAsString.withDefault('')
50
- }
51
-
52
- function isAuditLogSortField(value: string): value is AuditLogSortField {
53
- return (AUDIT_LOG_SORT_FIELDS as readonly string[]).includes(value)
54
- }
55
-
56
- function isAuditLogSortDirection(value: string): value is AuditLogSortDirection {
57
- return value === 'asc' || value === 'desc'
58
- }
59
-
60
- function formatDateTime(value: string): string {
61
- const date = new Date(value)
62
- if (Number.isNaN(date.getTime())) return value
63
-
64
- return new Intl.DateTimeFormat('en-US', {
65
- month: 'short',
66
- day: '2-digit',
67
- year: 'numeric',
68
- hour: 'numeric',
69
- minute: '2-digit'
70
- }).format(date)
71
- }
72
-
73
- function formatEventLabel(auditLog: AuditLogEntry): string {
74
- return `${auditLog.category}.${auditLog.action}`
75
- }
76
-
77
- function formatFilterLabel(value: string): string {
78
- return value
79
- .split('_')
80
- .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
81
- .join(' ')
82
- }
83
-
84
- const categorySelectOptions = AUDIT_LOG_CATEGORIES.map((value) => ({
85
- label: formatFilterLabel(value),
86
- value
87
- }))
88
- const actionSelectOptions = AUDIT_LOG_ACTIONS.map((value) => ({
89
- label: formatFilterLabel(value),
90
- value
91
- }))
92
-
93
- function SortableAuditLogHeader({
94
- column,
95
- label
96
- }: {
97
- column: {
98
- getIsSorted: () => false | 'asc' | 'desc'
99
- toggleSorting: (desc?: boolean) => void
100
- }
101
- label: string
102
- }) {
103
- const sortDirection = column.getIsSorted()
104
- const sortLabel =
105
- sortDirection === 'asc'
106
- ? 'sorted ascending'
107
- : sortDirection === 'desc'
108
- ? 'sorted descending'
109
- : 'not sorted'
110
-
111
- return (
112
- <Button
113
- variant="ghost"
114
- onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
115
- className="w-full min-w-0 justify-start overflow-hidden px-0! border-none rounded-none bg-transparent! scale-100! group"
116
- aria-label={`Sort by ${label}, ${sortLabel}`}
117
- aria-sort={
118
- sortDirection === 'asc' ? 'ascending' : sortDirection === 'desc' ? 'descending' : 'none'
119
- }
120
- >
121
- <span className="truncate">{label}</span>
122
- <SortIndicator direction={sortDirection} />
123
- </Button>
124
- )
125
- }
126
-
127
- function createAuditLogColumns(): ColumnDef<AuditLogEntry>[] {
128
- return [
129
- {
130
- accessorKey: 'createdAt',
131
- id: 'createdAt',
132
- size: 180,
133
- header: ({ column }) => <SortableAuditLogHeader column={column} label="Time" />,
134
- cell: ({ row }) => (
135
- <span className="whitespace-nowrap text-muted-foreground">
136
- {formatDateTime(row.original.createdAt)}
137
- </span>
138
- )
139
- },
140
- {
141
- id: 'event',
142
- size: 160,
143
- header: ({ column }) => <SortableAuditLogHeader column={column} label="Event" />,
144
- cell: ({ row }) => <Badge variant="secondary">{formatEventLabel(row.original)}</Badge>
145
- },
146
- {
147
- id: 'target',
148
- size: 240,
149
- header: ({ column }) => <SortableAuditLogHeader column={column} label="Target" />,
150
- cell: ({ row }) => (
151
- <div className="flex min-w-0 flex-col">
152
- <span className="truncate font-medium">
153
- {row.original.targetLabel ?? row.original.targetId ?? 'Unknown'}
154
- </span>
155
- <span className="truncate text-xs text-muted-foreground">{row.original.targetType}</span>
156
- </div>
157
- )
158
- },
159
- {
160
- id: 'actor',
161
- size: 220,
162
- header: ({ column }) => <SortableAuditLogHeader column={column} label="Actor" />,
163
- cell: ({ row }) => (
164
- <div className="flex min-w-0 flex-col">
165
- <span className="truncate">
166
- {row.original.actorEmail ?? row.original.actorName ?? 'Unknown'}
167
- </span>
168
- <span className="truncate text-xs text-muted-foreground">
169
- {row.original.actorRole ?? 'No role'}
170
- </span>
171
- </div>
172
- )
173
- },
174
- {
175
- accessorKey: 'sourceType',
176
- id: 'sourceType',
177
- size: 180,
178
- header: ({ column }) => <SortableAuditLogHeader column={column} label="Source" />,
179
- cell: ({ row }) => (
180
- <div className="text-muted-foreground">
181
- <div className="truncate">{row.original.sourceType}</div>
182
- <div className="truncate text-xs">{row.original.ipAddress ?? 'No IP'}</div>
183
- </div>
184
- )
185
- },
186
- {
187
- accessorKey: 'changedFields',
188
- id: 'changedFields',
189
- size: 180,
190
- enableSorting: false,
191
- header: 'Fields',
192
- cell: ({ row }) =>
193
- row.original.changedFields.length > 0 ? (
194
- <div className="flex flex-wrap gap-1">
195
- {row.original.changedFields.slice(0, 3).map((field) => (
196
- <Badge key={field} variant="outline">
197
- {field}
198
- </Badge>
199
- ))}
200
- {row.original.changedFields.length > 3 ? (
201
- <Badge variant="outline">+{row.original.changedFields.length - 3}</Badge>
202
- ) : null}
203
- </div>
204
- ) : (
205
- <span className="text-muted-foreground">None</span>
206
- )
207
- }
208
- ]
209
- }
210
-
211
- export function AuditLogPageContent() {
212
- const [selectedAuditLog, setSelectedAuditLog] = React.useState<AuditLogEntry | null>(null)
213
- const [filters, setFilters] = useQueryStates(auditLogParsers)
214
- const safePageIndex = Number.isFinite(filters.page) && filters.page > 0 ? filters.page : 0
215
- const safePageSize =
216
- filters.size === -1 || (Number.isFinite(filters.size) && filters.size > 0)
217
- ? filters.size
218
- : DEFAULT_PAGE_SIZE
219
- const safeSortField = isAuditLogSortField(filters.sort) ? filters.sort : undefined
220
- const safeSortDir = safeSortField
221
- ? isAuditLogSortDirection(filters.dir)
222
- ? filters.dir
223
- : 'asc'
224
- : undefined
225
- const query = React.useMemo<AuditLogQuery>(
226
- () => ({
227
- search: filters.q || undefined,
228
- category: filters.category ? (filters.category as AuditLogQuery['category']) : undefined,
229
- action: filters.action ? (filters.action as AuditLogQuery['action']) : undefined,
230
- createdAtFrom: filters.createdAtFrom || undefined,
231
- createdAtTo: filters.createdAtTo || undefined,
232
- page: safePageIndex,
233
- pageSize: safePageSize,
234
- sort: safeSortField,
235
- dir: safeSortDir
236
- }),
237
- [filters, safePageIndex, safePageSize, safeSortDir, safeSortField]
238
- )
239
- const auditLogsQuery = useAuditLogs(query)
240
- const cleanupMutation = useCleanupAuditLogs()
241
- const auditLogs = auditLogsQuery.data?.auditLogs ?? []
242
- const totalRows = auditLogsQuery.data?.total ?? 0
243
- const effectivePageSize = safePageSize === -1 ? totalRows : safePageSize
244
- const pageCount = effectivePageSize > 0 ? Math.ceil(totalRows / effectivePageSize) : 1
245
- const shouldShowPagination = totalRows > DEFAULT_PAGE_SIZE
246
- const activeFilterCount = [
247
- filters.q,
248
- filters.category,
249
- filters.action,
250
- filters.createdAtFrom || filters.createdAtTo
251
- ].filter(Boolean).length
252
-
253
- const {
254
- columnSizing,
255
- handleColumnSizingChange,
256
- handlePaginationChange,
257
- handleSortingChange,
258
- hoveredColumnId,
259
- registerTable,
260
- resetColumnSizingToContainer,
261
- setHoveredColumnId,
262
- sorting,
263
- tableContainerRef
264
- } = useTableUtils<AuditLogEntry>({
265
- data: auditLogs,
266
- pageIndex: safePageIndex,
267
- pageSize: effectivePageSize,
268
- setPageIndex: (nextPageIndex) => setFilters({ page: nextPageIndex }),
269
- setSortingParams: ({
270
- pageIndex: nextPageIndex,
271
- sortDir: nextSortDir,
272
- sortField: nextSortField
273
- }) =>
274
- setFilters({
275
- page: nextPageIndex,
276
- sort: nextSortField,
277
- dir: nextSortDir
278
- }),
279
- sortField: safeSortField,
280
- sortDir: safeSortDir
281
- })
282
- const columns = React.useMemo(() => createAuditLogColumns(), [])
283
-
284
- const table = useReactTable({
285
- data: auditLogs,
286
- columns,
287
- columnResizeMode: 'onChange',
288
- defaultColumn: {
289
- minSize: 48,
290
- size: 220
291
- },
292
- getCoreRowModel: getCoreRowModel(),
293
- manualPagination: true,
294
- manualSorting: true,
295
- pageCount,
296
- rowCount: totalRows,
297
- onColumnSizingChange: handleColumnSizingChange,
298
- onPaginationChange: handlePaginationChange,
299
- onSortingChange: handleSortingChange,
300
- state: {
301
- sorting,
302
- columnSizing,
303
- pagination: {
304
- pageIndex: safePageIndex,
305
- pageSize: effectivePageSize
306
- }
307
- }
308
- })
309
- registerTable(table)
310
- const gridTemplateColumns = table
311
- .getVisibleLeafColumns()
312
- .map((column) => `${column.getSize()}px`)
313
- .join(' ')
314
-
315
- const updateFilter = React.useCallback(
316
- (updates: Partial<typeof filters>) => {
317
- React.startTransition(() => {
318
- void setFilters({ ...updates, page: 0 })
319
- })
320
- },
321
- [setFilters]
322
- )
323
-
324
- const handleSearch = React.useCallback(
325
- (value: string | null) => {
326
- updateFilter({ q: value ?? '' })
327
- },
328
- [updateFilter]
329
- )
330
-
331
- const clearFilters = React.useCallback(() => {
332
- React.startTransition(() => {
333
- void setFilters({
334
- q: null,
335
- category: null,
336
- action: null,
337
- createdAtFrom: null,
338
- createdAtTo: null,
339
- page: 0
340
- })
341
- })
342
- }, [setFilters])
343
-
344
- const _runCleanup = React.useCallback(() => {
345
- cleanupMutation.mutate(undefined, {
346
- onSuccess: (result) => {
347
- if (!result.success) {
348
- toast.error(result.error ?? 'Failed to clean up audit logs')
349
- return
350
- }
351
- if (result.skipped) {
352
- toast.info('Audit retention is not enabled')
353
- return
354
- }
355
- toast.success(`Deleted ${result.deletedCount ?? 0} audit log rows`)
356
- },
357
- onError: () => {
358
- toast.error('Failed to clean up audit logs')
359
- }
360
- })
361
- }, [cleanupMutation])
362
-
363
- const filtersConfig = [
364
- {
365
- key: 'category',
366
- label: 'Category',
367
- type: 'select' as const,
368
- value: filters.category,
369
- options: categorySelectOptions
370
- },
371
- {
372
- key: 'action',
373
- label: 'Action',
374
- type: 'select' as const,
375
- value: filters.action,
376
- options: actionSelectOptions
377
- },
378
- {
379
- key: 'createdAt',
380
- label: 'Created',
381
- type: 'date-range' as const,
382
- from: filters.createdAtFrom,
383
- to: filters.createdAtTo
384
- }
385
- ]
386
-
387
- return (
388
- <React.Fragment>
389
- <PageHeader title="Audit Log" />
390
-
391
- <EntityFiltersBar
392
- filters={filtersConfig}
393
- activeFilterCount={activeFilterCount}
394
- onFilterChange={updateFilter}
395
- onClearAll={clearFilters}
396
- search={filters.q}
397
- searchPlaceholder="Search audit logs..."
398
- isPending={auditLogsQuery.isFetching}
399
- onSearch={handleSearch}
400
- />
401
- <main className="space-y-4 px-4 pb-4 flex-1">
402
- <div className="flex flex-col gap-6 pt-px pb-2 h-full">
403
- <Card className="min-h-0 flex-1 p-0">
404
- <CardContent ref={tableContainerRef} className="flex min-h-0 flex-1 flex-col p-0">
405
- <DataGrid
406
- containerClassName="min-h-0 flex-1"
407
- gridTemplateColumns={gridTemplateColumns}
408
- style={{ width: table.getTotalSize() }}
409
- >
410
- <DataGridHeader>
411
- {table.getHeaderGroups().map((headerGroup) => (
412
- <DataGridRow key={headerGroup.id}>
413
- {headerGroup.headers.map((header, headerIndex) => {
414
- const resizeLabel =
415
- typeof header.column.columnDef.header === 'string'
416
- ? header.column.columnDef.header
417
- : header.column.id
418
- const nextHeader = headerGroup.headers[headerIndex + 1]
419
- const canResizeColumn =
420
- header.column.getCanResize() && nextHeader?.column.getCanResize() === true
421
- return (
422
- <DataGridHead
423
- key={header.id}
424
- className="group/resize-header relative overflow-hidden"
425
- data-column-id={header.column.id}
426
- onMouseEnter={() => setHoveredColumnId(header.column.id)}
427
- onMouseLeave={() =>
428
- setHoveredColumnId((current) =>
429
- current === header.column.id ? null : current
430
- )
431
- }
432
- >
433
- {header.isPlaceholder
434
- ? null
435
- : flexRender(header.column.columnDef.header, header.getContext())}
436
- {canResizeColumn ? (
437
- <button
438
- type="button"
439
- aria-label={`Resize ${resizeLabel} column`}
440
- className="group/resize-handle absolute right-0 top-0 z-10 flex h-full w-5 cursor-col-resize touch-none select-none items-center justify-center text-muted-foreground opacity-0 transition-opacity hover:text-foreground focus-visible:text-foreground focus-visible:opacity-100 focus-visible:outline-none group-focus-within/resize-header:opacity-100 data-[column-hovered=true]:opacity-100 data-[resizing=true]:opacity-100"
441
- data-resize-handle
442
- data-column-hovered={
443
- hoveredColumnId === header.column.id ? 'true' : 'false'
444
- }
445
- data-resizing={header.column.getIsResizing() ? 'true' : 'false'}
446
- onDoubleClick={resetColumnSizingToContainer}
447
- onMouseDown={header.getResizeHandler()}
448
- onTouchStart={header.getResizeHandler()}
449
- >
450
- <GripVertical
451
- aria-hidden="true"
452
- className={cn('size-3.5', {
453
- 'text-ring': header.column.getIsResizing()
454
- })}
455
- />
456
- </button>
457
- ) : null}
458
- </DataGridHead>
459
- )
460
- })}
461
- </DataGridRow>
462
- ))}
463
- </DataGridHeader>
464
- <DataGridBody>
465
- {auditLogsQuery.isPending ? (
466
- <DataGridRow>
467
- <DataGridCell
468
- className="flex h-24 items-center justify-center text-center"
469
- style={{ gridColumn: '1 / -1' }}
470
- >
471
- <div className="text-muted-foreground">Loading Audit Logs...</div>
472
- </DataGridCell>
473
- </DataGridRow>
474
- ) : auditLogsQuery.error ? (
475
- <DataGridRow>
476
- <DataGridCell
477
- className="flex h-24 items-center justify-center text-center"
478
- style={{ gridColumn: '1 / -1' }}
479
- >
480
- <div className="text-destructive">
481
- Error loading Audit Logs: {auditLogsQuery.error.message}
482
- </div>
483
- </DataGridCell>
484
- </DataGridRow>
485
- ) : table.getRowModel().rows.length ? (
486
- table.getRowModel().rows.map((row) => (
487
- <DataGridRow
488
- key={row.id}
489
- className="cursor-pointer hover:bg-muted/50"
490
- onClick={(event) => {
491
- const target = event.target as HTMLElement
492
- if (!event.currentTarget.contains(target)) return
493
- if (target.closest('button, a, input, [role="checkbox"]')) return
494
- setSelectedAuditLog(row.original)
495
- }}
496
- >
497
- {row.getVisibleCells().map((cell) => (
498
- <DataGridCell
499
- key={cell.id}
500
- data-column-id={cell.column.id}
501
- onMouseEnter={() => setHoveredColumnId(cell.column.id)}
502
- onMouseLeave={() =>
503
- setHoveredColumnId((current) =>
504
- current === cell.column.id ? null : current
505
- )
506
- }
507
- >
508
- {flexRender(cell.column.columnDef.cell, cell.getContext())}
509
- </DataGridCell>
510
- ))}
511
- </DataGridRow>
512
- ))
513
- ) : (
514
- <DataGridRow>
515
- <DataGridCell
516
- className="flex h-24 items-center justify-center text-center"
517
- style={{ gridColumn: '1 / -1' }}
518
- >
519
- No Audit Logs found.
520
- </DataGridCell>
521
- </DataGridRow>
522
- )}
523
- </DataGridBody>
524
- </DataGrid>
525
- </CardContent>
526
- </Card>
527
-
528
- {shouldShowPagination ? (
529
- <DataTablePagination
530
- table={table}
531
- paginationMode="controlled"
532
- pageSize={safePageSize}
533
- pageIndex={safePageIndex}
534
- pageCount={pageCount}
535
- setPageIndex={(nextPageIndex) => setFilters({ page: nextPageIndex })}
536
- setPageSize={(nextPageSize) => setFilters({ page: 0, size: nextPageSize })}
537
- />
538
- ) : null}
539
- </div>
540
- </main>
541
- <AuditLogDetailDrawer
542
- auditLog={selectedAuditLog}
543
- open={!!selectedAuditLog}
544
- onOpenChange={(open) => {
545
- if (!open) setSelectedAuditLog(null)
546
- }}
547
- />
548
- </React.Fragment>
549
- )
550
- }
@@ -1,28 +0,0 @@
1
- /**
2
- * Send payload to webhook URL (fire-and-forget, non-blocking)
3
- * @param webhookUrl - The webhook URL to send data to
4
- * @param payload - The form data to send as URL-encoded
5
- */
6
- export function sendWebhook(
7
- webhookUrl: string | null | undefined,
8
- payload: Record<string, unknown>
9
- ): void {
10
- if (!webhookUrl) return // Fire-and-forget: runs in background, doesn't block
11
- ;(async () => {
12
- try {
13
- const formData = new URLSearchParams()
14
- for (const [key, value] of Object.entries(payload)) {
15
- if (value === null || value === undefined) continue
16
- const stringValue = typeof value === 'object' ? JSON.stringify(value) : String(value)
17
- formData.append(key, stringValue)
18
- }
19
- await fetch(webhookUrl, {
20
- method: 'POST',
21
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
22
- body: formData.toString()
23
- })
24
- } catch (error) {
25
- console.error('Webhook failed:', error)
26
- }
27
- })()
28
- }