betterstart-cli 0.0.27 → 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.
- package/dist/assets/adapters/next/integrations/mailchimp/actions/mailchimp.ts +0 -1
- package/dist/assets/adapters/next/integrations/vercel-blob/actions/vercel-blob.ts +39 -0
- package/dist/assets/adapters/next/integrations/vercel-blob/integration.ts +36 -0
- 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 +2191 -1304
- 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
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { WebhookEndpointListItem } from '@admin/actions/webhooks'
|
|
4
|
+
import { SortIndicator } from '@admin/components/shared/sort-indicator'
|
|
5
|
+
import { Badge } from '@admin/components/ui/badge'
|
|
6
|
+
import { Button } from '@admin/components/ui/button'
|
|
7
|
+
import type { ColumnDef } from '@tanstack/react-table'
|
|
8
|
+
import { formatDistanceToNow } from 'date-fns'
|
|
9
|
+
import { WebhookActions } from './webhook-actions'
|
|
10
|
+
import { WebhookEnabledSwitch } from './webhook-enabled-switch'
|
|
11
|
+
|
|
12
|
+
interface WebhooksColumnsOptions {
|
|
13
|
+
onEdit: (webhook: WebhookEndpointListItem) => void
|
|
14
|
+
onDelete: (webhook: WebhookEndpointListItem) => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function createWebhooksColumns({
|
|
18
|
+
onEdit,
|
|
19
|
+
onDelete
|
|
20
|
+
}: WebhooksColumnsOptions): ColumnDef<WebhookEndpointListItem>[] {
|
|
21
|
+
return [
|
|
22
|
+
{
|
|
23
|
+
accessorKey: 'name',
|
|
24
|
+
size: 240,
|
|
25
|
+
header: ({ column }) => {
|
|
26
|
+
const sortDirection = column.getIsSorted()
|
|
27
|
+
const sortLabel =
|
|
28
|
+
sortDirection === 'asc'
|
|
29
|
+
? 'sorted ascending'
|
|
30
|
+
: sortDirection === 'desc'
|
|
31
|
+
? 'sorted descending'
|
|
32
|
+
: 'not sorted'
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<Button
|
|
36
|
+
variant="ghost"
|
|
37
|
+
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
|
38
|
+
className="w-full min-w-0 justify-start overflow-hidden px-0! border-none rounded-none bg-transparent! scale-100! group"
|
|
39
|
+
aria-label={`Sort by Name, ${sortLabel}`}
|
|
40
|
+
aria-sort={
|
|
41
|
+
sortDirection === 'asc'
|
|
42
|
+
? 'ascending'
|
|
43
|
+
: sortDirection === 'desc'
|
|
44
|
+
? 'descending'
|
|
45
|
+
: 'none'
|
|
46
|
+
}
|
|
47
|
+
>
|
|
48
|
+
<span className="truncate">Name</span>
|
|
49
|
+
<SortIndicator direction={sortDirection} />
|
|
50
|
+
</Button>
|
|
51
|
+
)
|
|
52
|
+
},
|
|
53
|
+
cell: ({ row }) => <span className="truncate font-medium">{row.original.name}</span>,
|
|
54
|
+
enableHiding: false
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
accessorKey: 'url',
|
|
58
|
+
size: 320,
|
|
59
|
+
header: ({ column }) => {
|
|
60
|
+
const sortDirection = column.getIsSorted()
|
|
61
|
+
const sortLabel =
|
|
62
|
+
sortDirection === 'asc'
|
|
63
|
+
? 'sorted ascending'
|
|
64
|
+
: sortDirection === 'desc'
|
|
65
|
+
? 'sorted descending'
|
|
66
|
+
: 'not sorted'
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<Button
|
|
70
|
+
variant="ghost"
|
|
71
|
+
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
|
72
|
+
className="w-full min-w-0 justify-start overflow-hidden px-0! border-none rounded-none bg-transparent! scale-100! group"
|
|
73
|
+
aria-label={`Sort by URL, ${sortLabel}`}
|
|
74
|
+
aria-sort={
|
|
75
|
+
sortDirection === 'asc'
|
|
76
|
+
? 'ascending'
|
|
77
|
+
: sortDirection === 'desc'
|
|
78
|
+
? 'descending'
|
|
79
|
+
: 'none'
|
|
80
|
+
}
|
|
81
|
+
>
|
|
82
|
+
<span className="truncate">URL</span>
|
|
83
|
+
<SortIndicator direction={sortDirection} />
|
|
84
|
+
</Button>
|
|
85
|
+
)
|
|
86
|
+
},
|
|
87
|
+
cell: ({ row }) => (
|
|
88
|
+
<span className="truncate font-mono text-xs text-muted-foreground">{row.original.url}</span>
|
|
89
|
+
)
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
accessorKey: 'enabled',
|
|
93
|
+
size: 110,
|
|
94
|
+
header: ({ column }) => {
|
|
95
|
+
const sortDirection = column.getIsSorted()
|
|
96
|
+
const sortLabel =
|
|
97
|
+
sortDirection === 'asc'
|
|
98
|
+
? 'sorted ascending'
|
|
99
|
+
: sortDirection === 'desc'
|
|
100
|
+
? 'sorted descending'
|
|
101
|
+
: 'not sorted'
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<Button
|
|
105
|
+
variant="ghost"
|
|
106
|
+
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
|
107
|
+
className="w-full min-w-0 justify-start overflow-hidden px-0! border-none rounded-none bg-transparent! scale-100! group"
|
|
108
|
+
aria-label={`Sort by Enabled, ${sortLabel}`}
|
|
109
|
+
aria-sort={
|
|
110
|
+
sortDirection === 'asc'
|
|
111
|
+
? 'ascending'
|
|
112
|
+
: sortDirection === 'desc'
|
|
113
|
+
? 'descending'
|
|
114
|
+
: 'none'
|
|
115
|
+
}
|
|
116
|
+
>
|
|
117
|
+
<span className="truncate">Enabled</span>
|
|
118
|
+
<SortIndicator direction={sortDirection} />
|
|
119
|
+
</Button>
|
|
120
|
+
)
|
|
121
|
+
},
|
|
122
|
+
cell: ({ row }) => <WebhookEnabledSwitch webhook={row.original} />
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
accessorKey: 'eventCount',
|
|
126
|
+
size: 100,
|
|
127
|
+
header: ({ column }) => {
|
|
128
|
+
const sortDirection = column.getIsSorted()
|
|
129
|
+
const sortLabel =
|
|
130
|
+
sortDirection === 'asc'
|
|
131
|
+
? 'sorted ascending'
|
|
132
|
+
: sortDirection === 'desc'
|
|
133
|
+
? 'sorted descending'
|
|
134
|
+
: 'not sorted'
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<Button
|
|
138
|
+
variant="ghost"
|
|
139
|
+
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
|
140
|
+
className="w-full min-w-0 justify-start overflow-hidden px-0! border-none rounded-none bg-transparent! scale-100! group"
|
|
141
|
+
aria-label={`Sort by Events, ${sortLabel}`}
|
|
142
|
+
aria-sort={
|
|
143
|
+
sortDirection === 'asc'
|
|
144
|
+
? 'ascending'
|
|
145
|
+
: sortDirection === 'desc'
|
|
146
|
+
? 'descending'
|
|
147
|
+
: 'none'
|
|
148
|
+
}
|
|
149
|
+
>
|
|
150
|
+
<span className="truncate">Events</span>
|
|
151
|
+
<SortIndicator direction={sortDirection} />
|
|
152
|
+
</Button>
|
|
153
|
+
)
|
|
154
|
+
},
|
|
155
|
+
cell: ({ row }) => <span className="tabular-nums">{row.original.eventCount}</span>
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
accessorKey: 'lastDeliveryAt',
|
|
159
|
+
size: 220,
|
|
160
|
+
header: ({ column }) => {
|
|
161
|
+
const sortDirection = column.getIsSorted()
|
|
162
|
+
const sortLabel =
|
|
163
|
+
sortDirection === 'asc'
|
|
164
|
+
? 'sorted ascending'
|
|
165
|
+
: sortDirection === 'desc'
|
|
166
|
+
? 'sorted descending'
|
|
167
|
+
: 'not sorted'
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<Button
|
|
171
|
+
variant="ghost"
|
|
172
|
+
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
|
173
|
+
className="w-full min-w-0 justify-start overflow-hidden px-0! border-none rounded-none bg-transparent! scale-100! group"
|
|
174
|
+
aria-label={`Sort by Last delivery, ${sortLabel}`}
|
|
175
|
+
aria-sort={
|
|
176
|
+
sortDirection === 'asc'
|
|
177
|
+
? 'ascending'
|
|
178
|
+
: sortDirection === 'desc'
|
|
179
|
+
? 'descending'
|
|
180
|
+
: 'none'
|
|
181
|
+
}
|
|
182
|
+
>
|
|
183
|
+
<span className="truncate">Last delivery</span>
|
|
184
|
+
<SortIndicator direction={sortDirection} />
|
|
185
|
+
</Button>
|
|
186
|
+
)
|
|
187
|
+
},
|
|
188
|
+
cell: ({ row }) =>
|
|
189
|
+
row.original.lastDeliveryAt ? (
|
|
190
|
+
<span className="flex min-w-0 items-center gap-2">
|
|
191
|
+
<Badge variant={row.original.lastDeliverySuccess ? 'outline' : 'destructive'}>
|
|
192
|
+
{row.original.lastDeliverySuccess ? 'Delivered' : 'Failed'}
|
|
193
|
+
</Badge>
|
|
194
|
+
<span className="truncate text-xs text-muted-foreground">
|
|
195
|
+
{formatDistanceToNow(new Date(row.original.lastDeliveryAt), {
|
|
196
|
+
addSuffix: true
|
|
197
|
+
})}
|
|
198
|
+
</span>
|
|
199
|
+
</span>
|
|
200
|
+
) : (
|
|
201
|
+
<span className="text-muted-foreground">—</span>
|
|
202
|
+
)
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
accessorKey: 'createdAt',
|
|
206
|
+
size: 120,
|
|
207
|
+
header: ({ column }) => {
|
|
208
|
+
const sortDirection = column.getIsSorted()
|
|
209
|
+
const sortLabel =
|
|
210
|
+
sortDirection === 'asc'
|
|
211
|
+
? 'sorted ascending'
|
|
212
|
+
: sortDirection === 'desc'
|
|
213
|
+
? 'sorted descending'
|
|
214
|
+
: 'not sorted'
|
|
215
|
+
|
|
216
|
+
return (
|
|
217
|
+
<Button
|
|
218
|
+
variant="ghost"
|
|
219
|
+
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
|
220
|
+
className="w-full min-w-0 justify-start overflow-hidden px-0! border-none rounded-none bg-transparent! scale-100! group"
|
|
221
|
+
aria-label={`Sort by Created, ${sortLabel}`}
|
|
222
|
+
aria-sort={
|
|
223
|
+
sortDirection === 'asc'
|
|
224
|
+
? 'ascending'
|
|
225
|
+
: sortDirection === 'desc'
|
|
226
|
+
? 'descending'
|
|
227
|
+
: 'none'
|
|
228
|
+
}
|
|
229
|
+
>
|
|
230
|
+
<span className="truncate">Created</span>
|
|
231
|
+
<SortIndicator direction={sortDirection} />
|
|
232
|
+
</Button>
|
|
233
|
+
)
|
|
234
|
+
},
|
|
235
|
+
cell: ({ row }) => (
|
|
236
|
+
<div className="text-sm">
|
|
237
|
+
{new Date(row.original.createdAt).toLocaleDateString('en-US', {
|
|
238
|
+
month: 'short',
|
|
239
|
+
day: 'numeric',
|
|
240
|
+
year: 'numeric'
|
|
241
|
+
})}
|
|
242
|
+
</div>
|
|
243
|
+
)
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
id: 'actions',
|
|
247
|
+
size: 64,
|
|
248
|
+
enableResizing: false,
|
|
249
|
+
meta: { width: '64px' },
|
|
250
|
+
header: () => <span className="sr-only">Actions</span>,
|
|
251
|
+
cell: ({ row }) => (
|
|
252
|
+
<div className="flex justify-end">
|
|
253
|
+
<WebhookActions webhook={row.original} onEdit={onEdit} onDelete={onDelete} />
|
|
254
|
+
</div>
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
]
|
|
258
|
+
}
|
package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhooks-logs-columns.tsx
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { WebhookDeliveryEntry } from '@admin/actions/webhooks'
|
|
4
|
+
import { SortIndicator } from '@admin/components/shared/sort-indicator'
|
|
5
|
+
import { Badge } from '@admin/components/ui/badge'
|
|
6
|
+
import { Button } from '@admin/components/ui/button'
|
|
7
|
+
import type { ColumnDef } from '@tanstack/react-table'
|
|
8
|
+
import { formatDistanceToNow } from 'date-fns'
|
|
9
|
+
|
|
10
|
+
export const logsColumns: ColumnDef<WebhookDeliveryEntry>[] = [
|
|
11
|
+
{
|
|
12
|
+
accessorKey: 'statusCode',
|
|
13
|
+
size: 110,
|
|
14
|
+
header: ({ column }) => {
|
|
15
|
+
const sortDirection = column.getIsSorted()
|
|
16
|
+
const sortLabel =
|
|
17
|
+
sortDirection === 'asc'
|
|
18
|
+
? 'sorted ascending'
|
|
19
|
+
: sortDirection === 'desc'
|
|
20
|
+
? 'sorted descending'
|
|
21
|
+
: 'not sorted'
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<Button
|
|
25
|
+
variant="ghost"
|
|
26
|
+
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
|
27
|
+
className="w-full min-w-0 justify-start overflow-hidden px-0! border-none rounded-none bg-transparent! scale-100! group"
|
|
28
|
+
aria-label={`Sort by Status, ${sortLabel}`}
|
|
29
|
+
aria-sort={
|
|
30
|
+
sortDirection === 'asc' ? 'ascending' : sortDirection === 'desc' ? 'descending' : 'none'
|
|
31
|
+
}
|
|
32
|
+
>
|
|
33
|
+
<span className="truncate">Status</span>
|
|
34
|
+
<SortIndicator direction={sortDirection} />
|
|
35
|
+
</Button>
|
|
36
|
+
)
|
|
37
|
+
},
|
|
38
|
+
cell: ({ row }) => (
|
|
39
|
+
<Badge
|
|
40
|
+
variant={row.original.success ? 'outline' : 'destructive'}
|
|
41
|
+
title={row.original.error ?? undefined}
|
|
42
|
+
>
|
|
43
|
+
{row.original.statusCode ?? 'error'}
|
|
44
|
+
</Badge>
|
|
45
|
+
)
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
accessorKey: 'event',
|
|
49
|
+
size: 260,
|
|
50
|
+
header: ({ column }) => {
|
|
51
|
+
const sortDirection = column.getIsSorted()
|
|
52
|
+
const sortLabel =
|
|
53
|
+
sortDirection === 'asc'
|
|
54
|
+
? 'sorted ascending'
|
|
55
|
+
: sortDirection === 'desc'
|
|
56
|
+
? 'sorted descending'
|
|
57
|
+
: 'not sorted'
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<Button
|
|
61
|
+
variant="ghost"
|
|
62
|
+
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
|
63
|
+
className="w-full min-w-0 justify-start overflow-hidden px-0! border-none rounded-none bg-transparent! scale-100! group"
|
|
64
|
+
aria-label={`Sort by Event, ${sortLabel}`}
|
|
65
|
+
aria-sort={
|
|
66
|
+
sortDirection === 'asc' ? 'ascending' : sortDirection === 'desc' ? 'descending' : 'none'
|
|
67
|
+
}
|
|
68
|
+
>
|
|
69
|
+
<span className="truncate">Event</span>
|
|
70
|
+
<SortIndicator direction={sortDirection} />
|
|
71
|
+
</Button>
|
|
72
|
+
)
|
|
73
|
+
},
|
|
74
|
+
cell: ({ row }) => <span className="truncate font-mono text-xs">{row.original.event}</span>
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
accessorKey: 'webhookName',
|
|
78
|
+
size: 220,
|
|
79
|
+
header: ({ column }) => {
|
|
80
|
+
const sortDirection = column.getIsSorted()
|
|
81
|
+
const sortLabel =
|
|
82
|
+
sortDirection === 'asc'
|
|
83
|
+
? 'sorted ascending'
|
|
84
|
+
: sortDirection === 'desc'
|
|
85
|
+
? 'sorted descending'
|
|
86
|
+
: 'not sorted'
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<Button
|
|
90
|
+
variant="ghost"
|
|
91
|
+
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
|
92
|
+
className="w-full min-w-0 justify-start overflow-hidden px-0! border-none rounded-none bg-transparent! scale-100! group"
|
|
93
|
+
aria-label={`Sort by Endpoint, ${sortLabel}`}
|
|
94
|
+
aria-sort={
|
|
95
|
+
sortDirection === 'asc' ? 'ascending' : sortDirection === 'desc' ? 'descending' : 'none'
|
|
96
|
+
}
|
|
97
|
+
>
|
|
98
|
+
<span className="truncate">Endpoint</span>
|
|
99
|
+
<SortIndicator direction={sortDirection} />
|
|
100
|
+
</Button>
|
|
101
|
+
)
|
|
102
|
+
},
|
|
103
|
+
cell: ({ row }) => <span className="truncate">{row.original.webhookName}</span>
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
accessorKey: 'attempt',
|
|
107
|
+
size: 100,
|
|
108
|
+
header: ({ column }) => {
|
|
109
|
+
const sortDirection = column.getIsSorted()
|
|
110
|
+
const sortLabel =
|
|
111
|
+
sortDirection === 'asc'
|
|
112
|
+
? 'sorted ascending'
|
|
113
|
+
: sortDirection === 'desc'
|
|
114
|
+
? 'sorted descending'
|
|
115
|
+
: 'not sorted'
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<Button
|
|
119
|
+
variant="ghost"
|
|
120
|
+
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
|
121
|
+
className="w-full min-w-0 justify-start overflow-hidden px-0! border-none rounded-none bg-transparent! scale-100! group"
|
|
122
|
+
aria-label={`Sort by Attempt, ${sortLabel}`}
|
|
123
|
+
aria-sort={
|
|
124
|
+
sortDirection === 'asc' ? 'ascending' : sortDirection === 'desc' ? 'descending' : 'none'
|
|
125
|
+
}
|
|
126
|
+
>
|
|
127
|
+
<span className="truncate">Attempt</span>
|
|
128
|
+
<SortIndicator direction={sortDirection} />
|
|
129
|
+
</Button>
|
|
130
|
+
)
|
|
131
|
+
},
|
|
132
|
+
cell: ({ row }) => <span className="tabular-nums">{row.original.attempt}</span>
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
accessorKey: 'durationMs',
|
|
136
|
+
size: 110,
|
|
137
|
+
header: ({ column }) => {
|
|
138
|
+
const sortDirection = column.getIsSorted()
|
|
139
|
+
const sortLabel =
|
|
140
|
+
sortDirection === 'asc'
|
|
141
|
+
? 'sorted ascending'
|
|
142
|
+
: sortDirection === 'desc'
|
|
143
|
+
? 'sorted descending'
|
|
144
|
+
: 'not sorted'
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<Button
|
|
148
|
+
variant="ghost"
|
|
149
|
+
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
|
150
|
+
className="w-full min-w-0 justify-start overflow-hidden px-0! border-none rounded-none bg-transparent! scale-100! group"
|
|
151
|
+
aria-label={`Sort by Duration, ${sortLabel}`}
|
|
152
|
+
aria-sort={
|
|
153
|
+
sortDirection === 'asc' ? 'ascending' : sortDirection === 'desc' ? 'descending' : 'none'
|
|
154
|
+
}
|
|
155
|
+
>
|
|
156
|
+
<span className="truncate">Duration</span>
|
|
157
|
+
<SortIndicator direction={sortDirection} />
|
|
158
|
+
</Button>
|
|
159
|
+
)
|
|
160
|
+
},
|
|
161
|
+
cell: ({ row }) => (
|
|
162
|
+
<span className="tabular-nums text-muted-foreground">{row.original.durationMs} ms</span>
|
|
163
|
+
)
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
accessorKey: 'createdAt',
|
|
167
|
+
size: 160,
|
|
168
|
+
header: ({ column }) => {
|
|
169
|
+
const sortDirection = column.getIsSorted()
|
|
170
|
+
const sortLabel =
|
|
171
|
+
sortDirection === 'asc'
|
|
172
|
+
? 'sorted ascending'
|
|
173
|
+
: sortDirection === 'desc'
|
|
174
|
+
? 'sorted descending'
|
|
175
|
+
: 'not sorted'
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<Button
|
|
179
|
+
variant="ghost"
|
|
180
|
+
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
|
181
|
+
className="w-full min-w-0 justify-start overflow-hidden px-0! border-none rounded-none bg-transparent! scale-100! group"
|
|
182
|
+
aria-label={`Sort by When, ${sortLabel}`}
|
|
183
|
+
aria-sort={
|
|
184
|
+
sortDirection === 'asc' ? 'ascending' : sortDirection === 'desc' ? 'descending' : 'none'
|
|
185
|
+
}
|
|
186
|
+
>
|
|
187
|
+
<span className="truncate">When</span>
|
|
188
|
+
<SortIndicator direction={sortDirection} />
|
|
189
|
+
</Button>
|
|
190
|
+
)
|
|
191
|
+
},
|
|
192
|
+
cell: ({ row }) => (
|
|
193
|
+
<span className="truncate text-muted-foreground">
|
|
194
|
+
{formatDistanceToNow(new Date(row.original.createdAt), {
|
|
195
|
+
addSuffix: true
|
|
196
|
+
})}
|
|
197
|
+
</span>
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
]
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { EntityFiltersBar } from '@admin/components/shared/entity-filters-bar'
|
|
4
|
+
import { PageHeader } from '@admin/components/shared/page-header'
|
|
5
|
+
import { useWebhookOptions } from '@admin/hooks/use-webhooks'
|
|
6
|
+
import { useIsFetching } from '@tanstack/react-query'
|
|
7
|
+
import { parseAsInteger, parseAsString, useQueryState, useQueryStates } from 'nuqs'
|
|
8
|
+
import * as React from 'react'
|
|
9
|
+
import { logsColumns } from './webhooks-logs-columns'
|
|
10
|
+
import { WebhooksLogsTable } from './webhooks-logs-table'
|
|
11
|
+
|
|
12
|
+
const filterParsers = {
|
|
13
|
+
status: parseAsString.withDefault(''),
|
|
14
|
+
endpoint: parseAsString.withDefault(''),
|
|
15
|
+
createdAtFrom: parseAsString.withDefault(''),
|
|
16
|
+
createdAtTo: parseAsString.withDefault('')
|
|
17
|
+
} as const
|
|
18
|
+
|
|
19
|
+
export function WebhooksLogsPageContent() {
|
|
20
|
+
const isSearchFetching = useIsFetching({ queryKey: ['webhook-deliveries'] }) > 0
|
|
21
|
+
const [search, setSearch] = useQueryState('q', parseAsString.withDefault(''))
|
|
22
|
+
const [, setPageIndex] = useQueryState('page', parseAsInteger.withDefault(0))
|
|
23
|
+
|
|
24
|
+
const [filterValues, setFilters] = useQueryStates(filterParsers)
|
|
25
|
+
const status = filterValues.status
|
|
26
|
+
const endpoint = filterValues.endpoint
|
|
27
|
+
const createdAtFrom = filterValues.createdAtFrom
|
|
28
|
+
const createdAtTo = filterValues.createdAtTo
|
|
29
|
+
|
|
30
|
+
const { data: endpointOptions } = useWebhookOptions()
|
|
31
|
+
|
|
32
|
+
const activeFilterCount = [search, status, endpoint, createdAtFrom || createdAtTo].filter(
|
|
33
|
+
Boolean
|
|
34
|
+
).length
|
|
35
|
+
|
|
36
|
+
function handleSearch(value: string | null) {
|
|
37
|
+
React.startTransition(() => {
|
|
38
|
+
void setPageIndex(0)
|
|
39
|
+
void setSearch(value)
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function handleClearAll() {
|
|
44
|
+
React.startTransition(() => {
|
|
45
|
+
void setPageIndex(0)
|
|
46
|
+
void setSearch(null)
|
|
47
|
+
void setFilters({
|
|
48
|
+
status: null,
|
|
49
|
+
endpoint: null,
|
|
50
|
+
createdAtFrom: null,
|
|
51
|
+
createdAtTo: null
|
|
52
|
+
})
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const filters = [
|
|
57
|
+
{
|
|
58
|
+
key: 'status',
|
|
59
|
+
label: 'Results',
|
|
60
|
+
type: 'select' as const,
|
|
61
|
+
value: status,
|
|
62
|
+
options: [
|
|
63
|
+
{ label: 'Success', value: 'success' },
|
|
64
|
+
{ label: 'Failed', value: 'failed' }
|
|
65
|
+
]
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
key: 'endpoint',
|
|
69
|
+
label: 'Endpoints',
|
|
70
|
+
type: 'select' as const,
|
|
71
|
+
value: endpoint,
|
|
72
|
+
options: (endpointOptions?.webhooks ?? []).map((webhook) => ({
|
|
73
|
+
label: webhook.name,
|
|
74
|
+
value: webhook.id
|
|
75
|
+
}))
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
key: 'createdAt',
|
|
79
|
+
label: 'Created',
|
|
80
|
+
type: 'date-range' as const,
|
|
81
|
+
from: createdAtFrom,
|
|
82
|
+
to: createdAtTo
|
|
83
|
+
}
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<React.Fragment>
|
|
88
|
+
<PageHeader title="Webhook Logs" />
|
|
89
|
+
|
|
90
|
+
<EntityFiltersBar
|
|
91
|
+
filters={filters}
|
|
92
|
+
activeFilterCount={activeFilterCount}
|
|
93
|
+
onFilterChange={(updates) => {
|
|
94
|
+
React.startTransition(() => {
|
|
95
|
+
void setPageIndex(0)
|
|
96
|
+
void setFilters(updates)
|
|
97
|
+
})
|
|
98
|
+
}}
|
|
99
|
+
onClearAll={handleClearAll}
|
|
100
|
+
search={search}
|
|
101
|
+
searchPlaceholder="Search deliveries..."
|
|
102
|
+
isPending={isSearchFetching}
|
|
103
|
+
onSearch={handleSearch}
|
|
104
|
+
/>
|
|
105
|
+
<main className="space-y-4 px-4 pb-4 flex-1">
|
|
106
|
+
<WebhooksLogsTable
|
|
107
|
+
columns={logsColumns}
|
|
108
|
+
search={search}
|
|
109
|
+
status={status}
|
|
110
|
+
endpoint={endpoint}
|
|
111
|
+
createdAtFrom={createdAtFrom}
|
|
112
|
+
createdAtTo={createdAtTo}
|
|
113
|
+
/>
|
|
114
|
+
</main>
|
|
115
|
+
</React.Fragment>
|
|
116
|
+
)
|
|
117
|
+
}
|
package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhooks-logs-page.tsx
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { requireRole, UserRole } from '@admin/auth/middleware'
|
|
2
|
+
import * as React from 'react'
|
|
3
|
+
import { WebhooksLogsPageContent } from '../webhooks-logs-page-content'
|
|
4
|
+
import { WebhooksPageSkeleton } from '../webhooks-page-skeleton'
|
|
5
|
+
|
|
6
|
+
export default async function WebhooksLogsPage() {
|
|
7
|
+
await requireRole([UserRole.ADMIN])
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<React.Suspense fallback={<WebhooksPageSkeleton />}>
|
|
11
|
+
<WebhooksLogsPageContent />
|
|
12
|
+
</React.Suspense>
|
|
13
|
+
)
|
|
14
|
+
}
|