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.
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 +947 -620
  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
@@ -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
+ }
@@ -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
+ }
@@ -0,0 +1,253 @@
1
+ 'use client'
2
+
3
+ import type { WebhookDeliveryEntry } from '@admin/actions/webhooks'
4
+ import {
5
+ DataGrid,
6
+ DataGridBody,
7
+ DataGridCell,
8
+ DataGridHead,
9
+ DataGridHeader,
10
+ DataGridRow
11
+ } from '@admin/components/shared/data-table/data-grid'
12
+ import { DataTablePagination } from '@admin/components/shared/data-table/data-table-pagination'
13
+ import { useTableUtils } from '@admin/hooks/use-table-utils'
14
+ import { useWebhookDeliveries } from '@admin/hooks/use-webhooks'
15
+ import { cn } from '@admin/utils/shared/cn'
16
+ import { type ColumnDef, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table'
17
+ import { GripVertical } from 'lucide-react'
18
+ import { parseAsInteger, parseAsString, useQueryStates } from 'nuqs'
19
+
20
+ interface WebhooksLogsTableProps<TValue> extends React.HTMLAttributes<HTMLDivElement> {
21
+ columns: ColumnDef<WebhookDeliveryEntry, TValue>[]
22
+ search?: string
23
+ status?: string
24
+ endpoint?: string
25
+ createdAtFrom?: string
26
+ createdAtTo?: string
27
+ }
28
+
29
+ export function WebhooksLogsTable<TValue>({
30
+ columns,
31
+ search,
32
+ status,
33
+ endpoint,
34
+ createdAtFrom,
35
+ createdAtTo,
36
+ className
37
+ }: WebhooksLogsTableProps<TValue>) {
38
+ const defaultPageSize = 20
39
+ const [{ page: pageIndex, size: pageSize, sort: sortField, dir: sortDir }, setTableQuery] =
40
+ useQueryStates({
41
+ page: parseAsInteger.withDefault(0),
42
+ size: parseAsInteger.withDefault(defaultPageSize),
43
+ sort: parseAsString.withDefault(''),
44
+ dir: parseAsString.withDefault('')
45
+ })
46
+
47
+ const safePageIndex = Number.isFinite(pageIndex) && pageIndex > 0 ? pageIndex : 0
48
+ const safePageSize =
49
+ pageSize === -1 || (Number.isFinite(pageSize) && pageSize > 0) ? pageSize : defaultPageSize
50
+ const { data, error, isPending } = useWebhookDeliveries(
51
+ search,
52
+ status,
53
+ endpoint,
54
+ createdAtFrom,
55
+ createdAtTo,
56
+ safePageIndex,
57
+ safePageSize,
58
+ sortField || undefined,
59
+ (sortDir as 'asc' | 'desc') || undefined
60
+ )
61
+
62
+ const tableData = data?.deliveries ?? []
63
+ const totalRows = data?.total ?? 0
64
+ const effectivePageSize = safePageSize === -1 ? totalRows : safePageSize
65
+ const pageCount = effectivePageSize > 0 ? Math.ceil(totalRows / effectivePageSize) : 1
66
+ const shouldShowPagination = totalRows > defaultPageSize
67
+
68
+ const {
69
+ columnSizing,
70
+ handleColumnSizingChange,
71
+ handlePaginationChange,
72
+ handleSortingChange,
73
+ hoveredColumnId,
74
+ registerTable,
75
+ resetColumnSizingToContainer,
76
+ setHoveredColumnId,
77
+ sorting,
78
+ tableContainerRef
79
+ } = useTableUtils<WebhookDeliveryEntry>({
80
+ data: tableData,
81
+ pageIndex: safePageIndex,
82
+ pageSize: effectivePageSize,
83
+ setPageIndex: (nextPageIndex) => setTableQuery({ page: nextPageIndex }),
84
+ setSortingParams: ({
85
+ pageIndex: nextPageIndex,
86
+ sortDir: nextSortDir,
87
+ sortField: nextSortField
88
+ }) =>
89
+ setTableQuery({
90
+ page: nextPageIndex,
91
+ sort: nextSortField,
92
+ dir: nextSortDir
93
+ }),
94
+ sortField,
95
+ sortDir
96
+ })
97
+
98
+ const table = useReactTable({
99
+ data: tableData,
100
+ columns,
101
+ columnResizeMode: 'onChange',
102
+ defaultColumn: {
103
+ minSize: 48,
104
+ size: 220
105
+ },
106
+ getCoreRowModel: getCoreRowModel(),
107
+ manualPagination: true,
108
+ manualSorting: true,
109
+ pageCount,
110
+ rowCount: totalRows,
111
+ onSortingChange: handleSortingChange,
112
+ onColumnSizingChange: handleColumnSizingChange,
113
+ onPaginationChange: handlePaginationChange,
114
+ state: {
115
+ sorting,
116
+ columnSizing,
117
+ pagination: {
118
+ pageIndex: safePageIndex,
119
+ pageSize: effectivePageSize
120
+ }
121
+ }
122
+ })
123
+ registerTable(table)
124
+ const gridTemplateColumns = table
125
+ .getVisibleLeafColumns()
126
+ .map((column) => `${column.getSize()}px`)
127
+ .join(' ')
128
+
129
+ return (
130
+ <div className={cn('flex flex-col gap-6 pt-px pb-2 h-full', className)}>
131
+ <DataGrid
132
+ containerClassName="min-h-0 flex-1"
133
+ containerRef={tableContainerRef}
134
+ gridTemplateColumns={gridTemplateColumns}
135
+ style={{ width: table.getTotalSize() }}
136
+ >
137
+ <DataGridHeader>
138
+ {table.getHeaderGroups().map((headerGroup) => (
139
+ <DataGridRow key={headerGroup.id}>
140
+ {headerGroup.headers.map((header, headerIndex) => {
141
+ const resizeLabel =
142
+ typeof header.column.columnDef.header === 'string'
143
+ ? header.column.columnDef.header
144
+ : header.column.id
145
+ const nextHeader = headerGroup.headers[headerIndex + 1]
146
+ const canResizeColumn =
147
+ header.column.getCanResize() && nextHeader?.column.getCanResize() === true
148
+ return (
149
+ <DataGridHead
150
+ key={header.id}
151
+ className="group/resize-header relative overflow-hidden"
152
+ data-column-id={header.column.id}
153
+ onMouseEnter={() => setHoveredColumnId(header.column.id)}
154
+ onMouseLeave={() =>
155
+ setHoveredColumnId((current) =>
156
+ current === header.column.id ? null : current
157
+ )
158
+ }
159
+ >
160
+ {header.isPlaceholder
161
+ ? null
162
+ : flexRender(header.column.columnDef.header, header.getContext())}
163
+ {canResizeColumn ? (
164
+ <button
165
+ type="button"
166
+ aria-label={`Resize ${resizeLabel} column`}
167
+ 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"
168
+ data-resize-handle
169
+ data-column-hovered={
170
+ hoveredColumnId === header.column.id ? 'true' : 'false'
171
+ }
172
+ data-resizing={header.column.getIsResizing() ? 'true' : 'false'}
173
+ onDoubleClick={resetColumnSizingToContainer}
174
+ onMouseDown={header.getResizeHandler()}
175
+ onTouchStart={header.getResizeHandler()}
176
+ >
177
+ <GripVertical
178
+ aria-hidden="true"
179
+ className={cn('size-3.5', {
180
+ 'text-ring': header.column.getIsResizing()
181
+ })}
182
+ />
183
+ </button>
184
+ ) : null}
185
+ </DataGridHead>
186
+ )
187
+ })}
188
+ </DataGridRow>
189
+ ))}
190
+ </DataGridHeader>
191
+ <DataGridBody>
192
+ {isPending ? (
193
+ <DataGridRow>
194
+ <DataGridCell
195
+ className="flex h-24 items-center justify-center text-center"
196
+ style={{ gridColumn: '1 / -1' }}
197
+ >
198
+ <div className="text-muted-foreground">Loading deliveries…</div>
199
+ </DataGridCell>
200
+ </DataGridRow>
201
+ ) : error ? (
202
+ <DataGridRow>
203
+ <DataGridCell
204
+ className="flex h-24 items-center justify-center text-center"
205
+ style={{ gridColumn: '1 / -1' }}
206
+ >
207
+ <div className="text-destructive">Error loading deliveries: {error.message}</div>
208
+ </DataGridCell>
209
+ </DataGridRow>
210
+ ) : table.getRowModel().rows?.length ? (
211
+ table.getRowModel().rows.map((row) => (
212
+ <DataGridRow key={row.id} className="hover:bg-muted/50">
213
+ {row.getVisibleCells().map((cell) => (
214
+ <DataGridCell
215
+ key={cell.id}
216
+ data-column-id={cell.column.id}
217
+ onMouseEnter={() => setHoveredColumnId(cell.column.id)}
218
+ onMouseLeave={() =>
219
+ setHoveredColumnId((current) => (current === cell.column.id ? null : current))
220
+ }
221
+ >
222
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
223
+ </DataGridCell>
224
+ ))}
225
+ </DataGridRow>
226
+ ))
227
+ ) : (
228
+ <DataGridRow>
229
+ <DataGridCell
230
+ className="flex h-24 items-center justify-center text-center"
231
+ style={{ gridColumn: '1 / -1' }}
232
+ >
233
+ No deliveries found.
234
+ </DataGridCell>
235
+ </DataGridRow>
236
+ )}
237
+ </DataGridBody>
238
+ </DataGrid>
239
+
240
+ {shouldShowPagination ? (
241
+ <DataTablePagination
242
+ table={table}
243
+ paginationMode="controlled"
244
+ pageSize={safePageSize}
245
+ pageIndex={safePageIndex}
246
+ pageCount={pageCount}
247
+ setPageIndex={(nextPageIndex) => setTableQuery({ page: nextPageIndex })}
248
+ setPageSize={(nextPageSize) => setTableQuery({ page: 0, size: nextPageSize })}
249
+ />
250
+ ) : null}
251
+ </div>
252
+ )
253
+ }