betterstart-cli 0.0.28 → 0.0.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/adapters/next/integrations/mailchimp/actions/mailchimp.ts +0 -1
- package/dist/assets/adapters/next/plugins/blog/schemas/menus.json +1 -1
- package/dist/assets/adapters/next/plugins/blog/schemas/posts.json +1 -1
- package/dist/assets/adapters/next/templates/init/admin-globals.css +1 -1
- package/dist/assets/adapters/next/templates/init/components/layouts/admin-nav-link.tsx +12 -2
- package/dist/assets/adapters/next/templates/init/components/layouts/admin-settings-sidebar.tsx +75 -0
- package/dist/assets/adapters/next/templates/init/components/layouts/admin-sidebar-nav-link.tsx +12 -2
- package/dist/assets/adapters/next/templates/init/components/layouts/admin-sidebar.tsx +2 -8
- package/dist/assets/adapters/next/templates/init/components/shared/data-table/data-grid.tsx +14 -5
- package/dist/assets/adapters/next/templates/init/components/shared/data-table/data-table.tsx +111 -112
- package/dist/assets/adapters/next/templates/init/components/shared/dev-mode/dev-mode-types.ts +4 -0
- package/dist/assets/adapters/next/templates/init/components/shared/dev-mode-integrate.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/components/shared/entity-versions/entity-versions-drawer.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/components/shared/media/media-url-importer.tsx +45 -47
- package/dist/assets/adapters/next/templates/init/components/shared/page-header.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/data/webhook-events.ts +5 -0
- package/dist/assets/adapters/next/templates/init/hooks/use-table-utils.ts +27 -7
- package/dist/assets/adapters/next/templates/init/hooks/use-webhooks.ts +211 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/audit/types.ts +2 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/forms/index.ts +0 -1
- package/dist/assets/adapters/next/templates/init/lib/actions/forms/types.ts +0 -4
- package/dist/assets/adapters/next/templates/init/lib/actions/forms/upsert-form-settings.ts +0 -2
- package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/create-webhook.ts +55 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/delete-webhook.ts +26 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/dispatch.ts +135 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/get-webhook-deliveries.ts +124 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/get-webhook-secret.ts +28 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/get-webhook-subscriptions.ts +32 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/get-webhooks.ts +154 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/index.ts +30 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/internal-get-active-endpoints.ts +20 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/regenerate-webhook-secret.ts +35 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/register.ts +70 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/send-test-webhook.ts +81 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/types.ts +104 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/update-webhook.ts +72 -0
- package/dist/assets/adapters/next/templates/init/lib/db/core/schema.ts +71 -2
- package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/register.ts +2 -0
- package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/runner.ts +3 -1
- package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/types.ts +6 -1
- package/dist/assets/adapters/next/templates/init/pages/auth-gate-rsc.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/pages/media/media-page-content.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/pages/media/media-page-skeleton.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/pages/settings/audit-log/audit-log-columns.tsx +150 -0
- package/dist/assets/adapters/next/templates/init/pages/settings/audit-log/audit-log-page-content.tsx +137 -0
- package/dist/assets/adapters/next/templates/init/pages/{audit-log → settings/audit-log}/audit-log-page-skeleton.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/pages/settings/audit-log/audit-log-table.tsx +295 -0
- package/dist/assets/adapters/next/templates/init/pages/settings/forms/edit-form-notifications-dialog.tsx +128 -0
- package/dist/assets/adapters/next/templates/init/pages/settings/forms/forms-settings-page-content.tsx +111 -0
- package/dist/assets/adapters/next/templates/init/pages/settings/forms/forms-settings-page-skeleton.tsx +7 -0
- package/dist/assets/adapters/next/templates/init/pages/settings/forms/forms-settings-page.tsx +14 -0
- package/dist/assets/adapters/next/templates/init/pages/settings/settings-layout.tsx +10 -0
- package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/delete-webhook-dialog.tsx +82 -0
- package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhook-actions.tsx +53 -0
- package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhook-enabled-switch.tsx +18 -0
- package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhook-endpoint-dialog.tsx +370 -0
- package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhooks-columns.tsx +258 -0
- package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhooks-logs-columns.tsx +200 -0
- package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhooks-logs-page-content.tsx +117 -0
- package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhooks-logs-page.tsx +14 -0
- package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhooks-logs-table.tsx +253 -0
- package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhooks-page-content.tsx +161 -0
- package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhooks-page-skeleton.tsx +7 -0
- package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhooks-page.tsx +14 -0
- package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhooks-table.tsx +264 -0
- package/dist/assets/adapters/next/templates/init/pages/users/users-page-skeleton.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/pages/users/users-table.tsx +103 -109
- package/dist/assets/adapters/next/templates/init/types/webhooks.ts +7 -0
- package/dist/assets/adapters/next/templates/init/utils/webhook/signature.ts +13 -0
- package/dist/assets/shared-assets/react-admin/ui/card.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/drawer.tsx +2 -2
- package/dist/assets/shared-assets/react-admin/ui/input-group.tsx +3 -3
- package/dist/assets/shared-assets/react-admin/ui/sidebar.tsx +1 -1
- package/dist/cli.js +947 -620
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/dist/assets/adapters/next/templates/init/lib/actions/forms/test-form-webhook.ts +0 -40
- package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-page-content.tsx +0 -550
- package/dist/assets/adapters/next/templates/init/utils/webhook/webhook.ts +0 -28
- /package/dist/assets/adapters/next/templates/init/pages/{audit-log → settings/audit-log}/audit-log-detail-drawer.tsx +0 -0
- /package/dist/assets/adapters/next/templates/init/pages/{audit-log → settings/audit-log}/audit-log-detail-row.tsx +0 -0
- /package/dist/assets/adapters/next/templates/init/pages/{audit-log → settings/audit-log}/audit-log-page.tsx +0 -0
package/package.json
CHANGED
|
@@ -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
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|