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,72 @@
|
|
|
1
|
+
'use server'
|
|
2
|
+
|
|
3
|
+
import { requireRole, UserRole } from '@admin/auth/middleware'
|
|
4
|
+
import db from '@admin/db'
|
|
5
|
+
import { webhookSubscriptions, webhooks } from '@admin/db/schema'
|
|
6
|
+
import { and, eq, notInArray } from 'drizzle-orm'
|
|
7
|
+
import { updateTag } from 'next/cache'
|
|
8
|
+
import type { UpdateWebhookInput, WebhookResult } from './types'
|
|
9
|
+
import { webhooksCacheTags } from './types'
|
|
10
|
+
|
|
11
|
+
export async function updateWebhook(id: string, input: UpdateWebhookInput): Promise<WebhookResult> {
|
|
12
|
+
await requireRole([UserRole.ADMIN])
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const updated = await db.transaction(async (tx) => {
|
|
16
|
+
const [row] = await tx
|
|
17
|
+
.update(webhooks)
|
|
18
|
+
.set({
|
|
19
|
+
...(input.name !== undefined ? { name: input.name } : {}),
|
|
20
|
+
...(input.url !== undefined ? { url: input.url } : {}),
|
|
21
|
+
...(input.enabled !== undefined ? { enabled: input.enabled } : {}),
|
|
22
|
+
updatedAt: new Date().toISOString()
|
|
23
|
+
})
|
|
24
|
+
.where(eq(webhooks.id, id))
|
|
25
|
+
.returning()
|
|
26
|
+
|
|
27
|
+
if (!row) throw new Error('Webhook not found')
|
|
28
|
+
|
|
29
|
+
if (input.events !== undefined) {
|
|
30
|
+
const events = input.events
|
|
31
|
+
if (events.length > 0) {
|
|
32
|
+
await tx
|
|
33
|
+
.delete(webhookSubscriptions)
|
|
34
|
+
.where(
|
|
35
|
+
and(
|
|
36
|
+
eq(webhookSubscriptions.webhookId, id),
|
|
37
|
+
notInArray(webhookSubscriptions.event, events)
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
await tx
|
|
41
|
+
.insert(webhookSubscriptions)
|
|
42
|
+
.values(events.map((event) => ({ webhookId: id, event })))
|
|
43
|
+
.onConflictDoNothing()
|
|
44
|
+
} else {
|
|
45
|
+
await tx.delete(webhookSubscriptions).where(eq(webhookSubscriptions.webhookId, id))
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return row
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
updateTag(webhooksCacheTags.endpoints)
|
|
53
|
+
updateTag(webhooksCacheTags.subscriptions)
|
|
54
|
+
return {
|
|
55
|
+
success: true,
|
|
56
|
+
webhook: {
|
|
57
|
+
id: updated.id,
|
|
58
|
+
name: updated.name,
|
|
59
|
+
url: updated.url,
|
|
60
|
+
enabled: updated.enabled,
|
|
61
|
+
createdAt: updated.createdAt,
|
|
62
|
+
updatedAt: updated.updatedAt
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error('Error updating webhook:', error)
|
|
67
|
+
return {
|
|
68
|
+
success: false,
|
|
69
|
+
error: error instanceof Error ? error.message : 'Failed to update webhook'
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -80,8 +80,6 @@ export const formSettings = pgTable(
|
|
|
80
80
|
{
|
|
81
81
|
id: uuid().defaultRandom().primaryKey().notNull(),
|
|
82
82
|
formName: varchar('formName', { length: 100 }).notNull(),
|
|
83
|
-
webhookUrl: text('webhookUrl'),
|
|
84
|
-
webhookEnabled: boolean('webhookEnabled').default(false).notNull(),
|
|
85
83
|
notificationEmails: text('notificationEmails'),
|
|
86
84
|
createdAt: timestamp('createdAt', { precision: 3, mode: 'string' })
|
|
87
85
|
.default(sql`CURRENT_TIMESTAMP`)
|
|
@@ -98,6 +96,77 @@ export const formSettings = pgTable(
|
|
|
98
96
|
]
|
|
99
97
|
)
|
|
100
98
|
|
|
99
|
+
// ============================================================================
|
|
100
|
+
// Webhooks (endpoint registry, event subscriptions, delivery log)
|
|
101
|
+
// ============================================================================
|
|
102
|
+
|
|
103
|
+
export const webhooks = pgTable('Webhooks', {
|
|
104
|
+
id: uuid().defaultRandom().primaryKey().notNull(),
|
|
105
|
+
name: varchar('name', { length: 200 }).notNull(),
|
|
106
|
+
url: text('url').notNull(),
|
|
107
|
+
secret: text('secret').notNull(),
|
|
108
|
+
enabled: boolean('enabled').default(true).notNull(),
|
|
109
|
+
createdAt: timestamp('createdAt', { precision: 3, mode: 'string' })
|
|
110
|
+
.default(sql`CURRENT_TIMESTAMP`)
|
|
111
|
+
.notNull(),
|
|
112
|
+
updatedAt: timestamp('updatedAt', { precision: 3, mode: 'string' })
|
|
113
|
+
.default(sql`CURRENT_TIMESTAMP`)
|
|
114
|
+
.notNull()
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
export const webhookSubscriptions = pgTable(
|
|
118
|
+
'WebhookSubscriptions',
|
|
119
|
+
{
|
|
120
|
+
id: uuid().defaultRandom().primaryKey().notNull(),
|
|
121
|
+
webhookId: uuid('webhookId')
|
|
122
|
+
.notNull()
|
|
123
|
+
.references(() => webhooks.id, { onDelete: 'cascade' }),
|
|
124
|
+
event: varchar('event', { length: 200 }).notNull(),
|
|
125
|
+
createdAt: timestamp('createdAt', { precision: 3, mode: 'string' })
|
|
126
|
+
.default(sql`CURRENT_TIMESTAMP`)
|
|
127
|
+
.notNull()
|
|
128
|
+
},
|
|
129
|
+
(table) => [
|
|
130
|
+
uniqueIndex('WebhookSubscriptions_webhookId_event_key').using(
|
|
131
|
+
'btree',
|
|
132
|
+
table.webhookId.asc().nullsLast().op('uuid_ops'),
|
|
133
|
+
table.event.asc().nullsLast().op('text_ops')
|
|
134
|
+
),
|
|
135
|
+
index('WebhookSubscriptions_event_idx').using(
|
|
136
|
+
'btree',
|
|
137
|
+
table.event.asc().nullsLast().op('text_ops')
|
|
138
|
+
)
|
|
139
|
+
]
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
export const webhookDeliveries = pgTable(
|
|
143
|
+
'WebhookDeliveries',
|
|
144
|
+
{
|
|
145
|
+
id: uuid().defaultRandom().primaryKey().notNull(),
|
|
146
|
+
webhookId: uuid('webhookId')
|
|
147
|
+
.notNull()
|
|
148
|
+
.references(() => webhooks.id, { onDelete: 'cascade' }),
|
|
149
|
+
event: varchar('event', { length: 200 }).notNull(),
|
|
150
|
+
eventId: varchar('eventId', { length: 64 }).notNull(),
|
|
151
|
+
attempt: integer('attempt').notNull(),
|
|
152
|
+
success: boolean('success').notNull(),
|
|
153
|
+
statusCode: integer('statusCode'),
|
|
154
|
+
durationMs: integer('durationMs').notNull(),
|
|
155
|
+
error: text('error'),
|
|
156
|
+
createdAt: timestamp('createdAt', { precision: 3, mode: 'string' })
|
|
157
|
+
.default(sql`CURRENT_TIMESTAMP`)
|
|
158
|
+
.notNull()
|
|
159
|
+
},
|
|
160
|
+
(table) => [
|
|
161
|
+
index('WebhookDeliveries_webhookId_createdAt_idx').using(
|
|
162
|
+
'btree',
|
|
163
|
+
table.webhookId.asc().nullsLast().op('uuid_ops'),
|
|
164
|
+
table.createdAt.desc().nullsLast()
|
|
165
|
+
),
|
|
166
|
+
index('WebhookDeliveries_createdAt_idx').using('btree', table.createdAt.desc().nullsLast())
|
|
167
|
+
]
|
|
168
|
+
)
|
|
169
|
+
|
|
101
170
|
// ============================================================================
|
|
102
171
|
// Entity Versions
|
|
103
172
|
// ============================================================================
|
|
@@ -8,5 +8,7 @@
|
|
|
8
8
|
// console.log('Post created', { userId: user.id, entityId, data })
|
|
9
9
|
// })
|
|
10
10
|
import { registerAuditHooks } from '@admin/actions/audit/register'
|
|
11
|
+
import { registerWebhookHooks } from '@admin/actions/webhooks/register'
|
|
11
12
|
|
|
12
13
|
registerAuditHooks()
|
|
14
|
+
registerWebhookHooks()
|
|
@@ -42,6 +42,8 @@ function getAdminActionFromLifecycleEvent(
|
|
|
42
42
|
if (event === 'afterDelete') return 'delete'
|
|
43
43
|
if (event === 'afterPublish') return 'publish'
|
|
44
44
|
if (event === 'afterUnpublish') return 'unpublish'
|
|
45
|
+
if (event === 'afterRestore') return 'restore'
|
|
46
|
+
if (event === 'afterReorder') return 'reorder'
|
|
45
47
|
return null
|
|
46
48
|
}
|
|
47
49
|
|
|
@@ -76,7 +78,7 @@ function toAdminEventContext(
|
|
|
76
78
|
if (!action) return null
|
|
77
79
|
|
|
78
80
|
return {
|
|
79
|
-
category: context.schemaType === 'form' ? 'form' : 'entity',
|
|
81
|
+
category: context.auditCategory ?? (context.schemaType === 'form' ? 'form' : 'entity'),
|
|
80
82
|
action,
|
|
81
83
|
targetType: context.entityType,
|
|
82
84
|
targetId: context.entityId ?? null,
|
|
@@ -16,6 +16,10 @@ export type LifecycleHookEvent =
|
|
|
16
16
|
| 'afterPublish'
|
|
17
17
|
| 'beforeUnpublish'
|
|
18
18
|
| 'afterUnpublish'
|
|
19
|
+
| 'beforeRestore'
|
|
20
|
+
| 'afterRestore'
|
|
21
|
+
| 'beforeReorder'
|
|
22
|
+
| 'afterReorder'
|
|
19
23
|
|
|
20
24
|
export type BeforeLifecycleHookEvent = Extract<LifecycleHookEvent, `before${string}`>
|
|
21
25
|
export type AfterLifecycleHookEvent = Extract<LifecycleHookEvent, `after${string}`>
|
|
@@ -23,7 +27,8 @@ export type AfterLifecycleHookEvent = Extract<LifecycleHookEvent, `after${string
|
|
|
23
27
|
export interface LifecycleHookContext {
|
|
24
28
|
user: User | null
|
|
25
29
|
entityType: string
|
|
26
|
-
schemaType?: 'entity' | 'form' | undefined
|
|
30
|
+
schemaType?: 'entity' | 'form' | 'single' | undefined
|
|
31
|
+
auditCategory?: AuditLogCategory | undefined
|
|
27
32
|
timestamp: string
|
|
28
33
|
entityId?: string | undefined
|
|
29
34
|
input?: Record<string, unknown> | undefined
|
|
@@ -8,7 +8,7 @@ export async function AuthGateRsc({ children }: { children: React.ReactNode }) {
|
|
|
8
8
|
return (
|
|
9
9
|
<SidebarProvider className="min-h-svh">
|
|
10
10
|
<AdminSidebar />
|
|
11
|
-
<div className="relative flex min-w-0 flex-1 flex-col [--admin-shell-content-left:0px] md:[--admin-shell-content-left:var(--sidebar-width)] md:peer-data-[state=collapsed]:[--admin-shell-content-left:var(--sidebar-width-icon)]">
|
|
11
|
+
<div className="relative flex min-w-0 flex-1 flex-col [--admin-shell-content-left:0px] md:[--admin-shell-content-left:var(--sidebar-width)] md:peer-data-[state=collapsed]:[--admin-shell-content-left:var(--sidebar-width-icon)] pb-14">
|
|
12
12
|
{children}
|
|
13
13
|
</div>
|
|
14
14
|
</SidebarProvider>
|
|
@@ -237,7 +237,7 @@ export function MediaPageContent() {
|
|
|
237
237
|
selectedIds={selected}
|
|
238
238
|
isSelectMode={isSelectMode}
|
|
239
239
|
onItemClick={handleItemClick}
|
|
240
|
-
className="lg:grid-cols-
|
|
240
|
+
className="lg:grid-cols-12 pb-12 pt-px px-px"
|
|
241
241
|
/>
|
|
242
242
|
{total > DEFAULT_MEDIA_PAGE_SIZE ? (
|
|
243
243
|
<MediaGridPagination
|
package/dist/assets/adapters/next/templates/init/pages/settings/audit-log/audit-log-columns.tsx
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { AuditLogEntry, AuditLogSortField } from '@admin/actions/audit'
|
|
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
|
+
|
|
9
|
+
export const AUDIT_LOG_SORT_FIELDS = [
|
|
10
|
+
'createdAt',
|
|
11
|
+
'event',
|
|
12
|
+
'target',
|
|
13
|
+
'actor',
|
|
14
|
+
'sourceType'
|
|
15
|
+
] as const satisfies AuditLogSortField[]
|
|
16
|
+
|
|
17
|
+
function formatDateTime(value: string): string {
|
|
18
|
+
const date = new Date(value)
|
|
19
|
+
if (Number.isNaN(date.getTime())) return value
|
|
20
|
+
|
|
21
|
+
return new Intl.DateTimeFormat('en-US', {
|
|
22
|
+
month: 'short',
|
|
23
|
+
day: '2-digit',
|
|
24
|
+
year: 'numeric',
|
|
25
|
+
hour: 'numeric',
|
|
26
|
+
minute: '2-digit'
|
|
27
|
+
}).format(date)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function formatEventLabel(auditLog: AuditLogEntry): string {
|
|
31
|
+
return `${auditLog.category}.${auditLog.action}`
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function SortableAuditLogHeader({
|
|
35
|
+
column,
|
|
36
|
+
label
|
|
37
|
+
}: {
|
|
38
|
+
column: {
|
|
39
|
+
getIsSorted: () => false | 'asc' | 'desc'
|
|
40
|
+
toggleSorting: (desc?: boolean) => void
|
|
41
|
+
}
|
|
42
|
+
label: string
|
|
43
|
+
}) {
|
|
44
|
+
const sortDirection = column.getIsSorted()
|
|
45
|
+
const sortLabel =
|
|
46
|
+
sortDirection === 'asc'
|
|
47
|
+
? 'sorted ascending'
|
|
48
|
+
: sortDirection === 'desc'
|
|
49
|
+
? 'sorted descending'
|
|
50
|
+
: 'not sorted'
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<Button
|
|
54
|
+
variant="ghost"
|
|
55
|
+
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
|
56
|
+
className="w-full min-w-0 justify-start overflow-hidden px-0! border-none rounded-none bg-transparent! scale-100! group"
|
|
57
|
+
aria-label={`Sort by ${label}, ${sortLabel}`}
|
|
58
|
+
aria-sort={
|
|
59
|
+
sortDirection === 'asc' ? 'ascending' : sortDirection === 'desc' ? 'descending' : 'none'
|
|
60
|
+
}
|
|
61
|
+
>
|
|
62
|
+
<span className="truncate">{label}</span>
|
|
63
|
+
<SortIndicator direction={sortDirection} />
|
|
64
|
+
</Button>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function createAuditLogColumns(): ColumnDef<AuditLogEntry>[] {
|
|
69
|
+
return [
|
|
70
|
+
{
|
|
71
|
+
id: 'event',
|
|
72
|
+
size: 160,
|
|
73
|
+
header: ({ column }) => <SortableAuditLogHeader column={column} label="Event" />,
|
|
74
|
+
cell: ({ row }) => <Badge variant="secondary">{formatEventLabel(row.original)}</Badge>
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: 'target',
|
|
78
|
+
size: 240,
|
|
79
|
+
header: ({ column }) => <SortableAuditLogHeader column={column} label="Target" />,
|
|
80
|
+
cell: ({ row }) => (
|
|
81
|
+
<div className="flex min-w-0 flex-col">
|
|
82
|
+
<span className="truncate font-medium">
|
|
83
|
+
{row.original.targetLabel ?? row.original.targetId ?? 'Unknown'}
|
|
84
|
+
</span>
|
|
85
|
+
<span className="truncate text-xs text-muted-foreground">{row.original.targetType}</span>
|
|
86
|
+
</div>
|
|
87
|
+
)
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: 'actor',
|
|
91
|
+
size: 220,
|
|
92
|
+
header: ({ column }) => <SortableAuditLogHeader column={column} label="Actor" />,
|
|
93
|
+
cell: ({ row }) => (
|
|
94
|
+
<div className="flex min-w-0 flex-col">
|
|
95
|
+
<span className="truncate">
|
|
96
|
+
{row.original.actorEmail ?? row.original.actorName ?? 'Unknown'}
|
|
97
|
+
</span>
|
|
98
|
+
<span className="truncate text-xs text-muted-foreground">
|
|
99
|
+
{row.original.actorRole ?? 'No role'}
|
|
100
|
+
</span>
|
|
101
|
+
</div>
|
|
102
|
+
)
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
accessorKey: 'createdAt',
|
|
106
|
+
id: 'createdAt',
|
|
107
|
+
size: 180,
|
|
108
|
+
header: ({ column }) => <SortableAuditLogHeader column={column} label="Time" />,
|
|
109
|
+
cell: ({ row }) => (
|
|
110
|
+
<span className="whitespace-nowrap text-muted-foreground">
|
|
111
|
+
{formatDateTime(row.original.createdAt)}
|
|
112
|
+
</span>
|
|
113
|
+
)
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
accessorKey: 'sourceType',
|
|
117
|
+
id: 'sourceType',
|
|
118
|
+
size: 180,
|
|
119
|
+
header: ({ column }) => <SortableAuditLogHeader column={column} label="Source" />,
|
|
120
|
+
cell: ({ row }) => (
|
|
121
|
+
<div className="text-muted-foreground">
|
|
122
|
+
<div className="truncate">{row.original.sourceType}</div>
|
|
123
|
+
<div className="truncate text-xs">{row.original.ipAddress ?? 'No IP'}</div>
|
|
124
|
+
</div>
|
|
125
|
+
)
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
accessorKey: 'changedFields',
|
|
129
|
+
id: 'changedFields',
|
|
130
|
+
size: 180,
|
|
131
|
+
enableSorting: false,
|
|
132
|
+
header: 'Fields',
|
|
133
|
+
cell: ({ row }) =>
|
|
134
|
+
row.original.changedFields.length > 0 ? (
|
|
135
|
+
<div className="flex flex-wrap gap-1">
|
|
136
|
+
{row.original.changedFields.slice(0, 3).map((field) => (
|
|
137
|
+
<Badge key={field} variant="outline">
|
|
138
|
+
{field}
|
|
139
|
+
</Badge>
|
|
140
|
+
))}
|
|
141
|
+
{row.original.changedFields.length > 3 ? (
|
|
142
|
+
<Badge variant="outline">+{row.original.changedFields.length - 3}</Badge>
|
|
143
|
+
) : null}
|
|
144
|
+
</div>
|
|
145
|
+
) : (
|
|
146
|
+
<span className="text-muted-foreground">None</span>
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
]
|
|
150
|
+
}
|
package/dist/assets/adapters/next/templates/init/pages/settings/audit-log/audit-log-page-content.tsx
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { AuditLogEntry } from '@admin/actions/audit'
|
|
4
|
+
import { AUDIT_LOG_ACTIONS, AUDIT_LOG_CATEGORIES } from '@admin/actions/audit'
|
|
5
|
+
import { EntityFiltersBar } from '@admin/components/shared/entity-filters-bar'
|
|
6
|
+
import { PageHeader } from '@admin/components/shared/page-header'
|
|
7
|
+
import { useIsFetching } from '@tanstack/react-query'
|
|
8
|
+
import { parseAsInteger, parseAsString, useQueryState, useQueryStates } from 'nuqs'
|
|
9
|
+
import * as React from 'react'
|
|
10
|
+
import { createAuditLogColumns } from './audit-log-columns'
|
|
11
|
+
import { AuditLogDetailDrawer } from './audit-log-detail-drawer'
|
|
12
|
+
import { AuditLogTable } from './audit-log-table'
|
|
13
|
+
|
|
14
|
+
const filterParsers = {
|
|
15
|
+
category: parseAsString.withDefault(''),
|
|
16
|
+
action: parseAsString.withDefault(''),
|
|
17
|
+
createdAtFrom: parseAsString.withDefault(''),
|
|
18
|
+
createdAtTo: parseAsString.withDefault('')
|
|
19
|
+
} as const
|
|
20
|
+
|
|
21
|
+
function formatFilterLabel(value: string): string {
|
|
22
|
+
return value
|
|
23
|
+
.split('_')
|
|
24
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
25
|
+
.join(' ')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const categorySelectOptions = AUDIT_LOG_CATEGORIES.map((value) => ({
|
|
29
|
+
label: formatFilterLabel(value),
|
|
30
|
+
value
|
|
31
|
+
}))
|
|
32
|
+
const actionSelectOptions = AUDIT_LOG_ACTIONS.map((value) => ({
|
|
33
|
+
label: formatFilterLabel(value),
|
|
34
|
+
value
|
|
35
|
+
}))
|
|
36
|
+
|
|
37
|
+
export function AuditLogPageContent() {
|
|
38
|
+
const isSearchFetching = useIsFetching({ queryKey: ['audit-log'] }) > 0
|
|
39
|
+
const [search, setSearch] = useQueryState('q', parseAsString.withDefault(''))
|
|
40
|
+
const [, setPageIndex] = useQueryState('page', parseAsInteger.withDefault(0))
|
|
41
|
+
const [filterValues, setFilters] = useQueryStates(filterParsers)
|
|
42
|
+
const category = filterValues.category
|
|
43
|
+
const action = filterValues.action
|
|
44
|
+
const createdAtFrom = filterValues.createdAtFrom
|
|
45
|
+
const createdAtTo = filterValues.createdAtTo
|
|
46
|
+
|
|
47
|
+
const [selectedAuditLog, setSelectedAuditLog] = React.useState<AuditLogEntry | null>(null)
|
|
48
|
+
const columns = React.useMemo(() => createAuditLogColumns(), [])
|
|
49
|
+
|
|
50
|
+
const activeFilterCount = [search, category, action, createdAtFrom || createdAtTo].filter(
|
|
51
|
+
Boolean
|
|
52
|
+
).length
|
|
53
|
+
|
|
54
|
+
function handleSearch(value: string | null) {
|
|
55
|
+
React.startTransition(() => {
|
|
56
|
+
void setPageIndex(0)
|
|
57
|
+
void setSearch(value)
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function handleClearAll() {
|
|
62
|
+
React.startTransition(() => {
|
|
63
|
+
void setPageIndex(0)
|
|
64
|
+
void setSearch(null)
|
|
65
|
+
void setFilters({
|
|
66
|
+
category: null,
|
|
67
|
+
action: null,
|
|
68
|
+
createdAtFrom: null,
|
|
69
|
+
createdAtTo: null
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const filters = [
|
|
75
|
+
{
|
|
76
|
+
key: 'category',
|
|
77
|
+
label: 'Category',
|
|
78
|
+
type: 'select' as const,
|
|
79
|
+
value: category,
|
|
80
|
+
options: categorySelectOptions
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
key: 'action',
|
|
84
|
+
label: 'Action',
|
|
85
|
+
type: 'select' as const,
|
|
86
|
+
value: action,
|
|
87
|
+
options: actionSelectOptions
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
key: 'createdAt',
|
|
91
|
+
label: 'Created',
|
|
92
|
+
type: 'date-range' as const,
|
|
93
|
+
from: createdAtFrom,
|
|
94
|
+
to: createdAtTo
|
|
95
|
+
}
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<React.Fragment>
|
|
100
|
+
<PageHeader title="Audit Log" />
|
|
101
|
+
|
|
102
|
+
<EntityFiltersBar
|
|
103
|
+
filters={filters}
|
|
104
|
+
activeFilterCount={activeFilterCount}
|
|
105
|
+
onFilterChange={(updates) => {
|
|
106
|
+
React.startTransition(() => {
|
|
107
|
+
void setPageIndex(0)
|
|
108
|
+
void setFilters(updates)
|
|
109
|
+
})
|
|
110
|
+
}}
|
|
111
|
+
onClearAll={handleClearAll}
|
|
112
|
+
search={search}
|
|
113
|
+
searchPlaceholder="Search audit logs..."
|
|
114
|
+
isPending={isSearchFetching}
|
|
115
|
+
onSearch={handleSearch}
|
|
116
|
+
/>
|
|
117
|
+
<main className="space-y-4 px-4 pb-4 flex-1">
|
|
118
|
+
<AuditLogTable
|
|
119
|
+
columns={columns}
|
|
120
|
+
search={search}
|
|
121
|
+
category={category}
|
|
122
|
+
action={action}
|
|
123
|
+
createdAtFrom={createdAtFrom}
|
|
124
|
+
createdAtTo={createdAtTo}
|
|
125
|
+
onRowClick={setSelectedAuditLog}
|
|
126
|
+
/>
|
|
127
|
+
</main>
|
|
128
|
+
<AuditLogDetailDrawer
|
|
129
|
+
auditLog={selectedAuditLog}
|
|
130
|
+
open={!!selectedAuditLog}
|
|
131
|
+
onOpenChange={(open) => {
|
|
132
|
+
if (!open) setSelectedAuditLog(null)
|
|
133
|
+
}}
|
|
134
|
+
/>
|
|
135
|
+
</React.Fragment>
|
|
136
|
+
)
|
|
137
|
+
}
|