mirror of
https://github.com/vernu/textbee.git
synced 2026-02-20 07:34:00 -05:00
Webhook deliveries UI improvement (#139)
* chore(api): improve webhook delivery history aggregation query * ui(web): improve webhook delivery history page ui * chore(web): clean up unnecessary comments
This commit is contained in:
@@ -84,27 +84,6 @@ export class WebhookService {
|
||||
matchStage.createdAt = { $gte: new Date(start), $lte: new Date(end) }
|
||||
}
|
||||
|
||||
if (status) {
|
||||
switch (status) {
|
||||
case 'delivered':
|
||||
matchStage.deliveredAt = { $ne: null }
|
||||
break
|
||||
case 'failed':
|
||||
matchStage.deliveryAttemptAbortedAt = { $ne: null }
|
||||
break
|
||||
case 'retrying':
|
||||
matchStage.deliveredAt = null
|
||||
matchStage.deliveryAttemptAbortedAt = null
|
||||
matchStage.deliveryAttemptCount = { $gt: 0 }
|
||||
matchStage.nextDeliveryAttemptAt = { $ne: null }
|
||||
break
|
||||
case 'pending':
|
||||
matchStage.deliveredAt = null
|
||||
matchStage.deliveryAttemptAbortedAt = null
|
||||
matchStage.deliveryAttemptCount = 0
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const pageNum = Math.max(1, Number.parseInt(page.toString()) || 1)
|
||||
const limitNum = Math.max(1, Number.parseInt(limit.toString()) || 10)
|
||||
@@ -144,8 +123,52 @@ export class WebhookService {
|
||||
preserveNullAndEmptyArrays: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
$addFields: {
|
||||
computedStatus: {
|
||||
$cond: {
|
||||
if: { $ne: ['$deliveredAt', null] },
|
||||
then: 'delivered',
|
||||
else: {
|
||||
$cond: {
|
||||
if: {
|
||||
$or: [
|
||||
{ $ne: ['$deliveryAttemptAbortedAt', null] },
|
||||
{ $gte: ['$deliveryAttemptCount', 10] }
|
||||
]
|
||||
},
|
||||
then: 'failed',
|
||||
else: {
|
||||
$cond: {
|
||||
if: {
|
||||
$and: [
|
||||
{ $eq: ['$deliveredAt', null] },
|
||||
{ $eq: ['$deliveryAttemptAbortedAt', null] },
|
||||
{ $gt: ['$deliveryAttemptCount', 0] },
|
||||
{ $lt: ['$deliveryAttemptCount', 10] }
|
||||
]
|
||||
},
|
||||
then: 'retrying',
|
||||
else: 'pending'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
// Apply status filter
|
||||
if (status) {
|
||||
commonPipeline.push({
|
||||
$match: {
|
||||
computedStatus: status
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (deviceId) {
|
||||
commonPipeline.push({
|
||||
$match: {
|
||||
|
||||
@@ -53,9 +53,9 @@ const WebhooksHistory = () => {
|
||||
.then((res) => res.data),
|
||||
})
|
||||
|
||||
const [currentDevice, setCurrentDevice] = useState('')
|
||||
const [eventType, setEventType] = useState('MESSAGE_RECEIVED')
|
||||
const [status, setStatus] = useState('delivered')
|
||||
const [currentDevice, setCurrentDevice] = useState('all')
|
||||
const [eventType, setEventType] = useState('all')
|
||||
const [status, setStatus] = useState('all')
|
||||
const [dateRange, setDateRange] = useState<any>('90')
|
||||
const [openCal, setOpenCal] = useState(false)
|
||||
const [dateQuery, setDateQuery] = useState<{ start: string; end: string }>({
|
||||
@@ -67,10 +67,9 @@ const WebhooksHistory = () => {
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (devices?.data?.length) {
|
||||
setCurrentDevice(devices?.data?.[0]?._id)
|
||||
if (devices?.data?.length && currentDevice === 'all') {
|
||||
}
|
||||
}, [devices])
|
||||
}, [devices, currentDevice])
|
||||
|
||||
const {
|
||||
data: webhookNotifications,
|
||||
@@ -86,11 +85,11 @@ const WebhooksHistory = () => {
|
||||
currentDevice,
|
||||
status,
|
||||
],
|
||||
enabled: !!currentDevice,
|
||||
enabled: true,
|
||||
queryFn: () =>
|
||||
httpBrowserClient
|
||||
.get(
|
||||
`${ApiEndpoints.gateway.getWebhookNotifications()}?eventType=${eventType}&page=${page}&limit=${limit}&status=${status}&start=${
|
||||
`${ApiEndpoints.gateway.getWebhookNotifications()}?eventType=${eventType === 'all' ? '' : eventType}&page=${page}&limit=${limit}&status=${status === 'all' ? '' : status}&start=${
|
||||
dateQuery.start
|
||||
}&end=${dateQuery.end}&deviceId=${
|
||||
currentDevice === 'all' ? '' : currentDevice
|
||||
@@ -125,6 +124,15 @@ const WebhooksHistory = () => {
|
||||
case '90':
|
||||
start.setDate(end.getDate() - 90)
|
||||
break
|
||||
case '90_months':
|
||||
start.setMonth(end.getMonth() - 3)
|
||||
break
|
||||
case '180':
|
||||
start.setMonth(end.getMonth() - 6)
|
||||
break
|
||||
case '365':
|
||||
start.setMonth(end.getMonth() - 12)
|
||||
break
|
||||
case 'custom':
|
||||
setDateQuery({ start: '', end: '' })
|
||||
setDateRange('custom')
|
||||
@@ -156,6 +164,9 @@ const WebhooksHistory = () => {
|
||||
<SelectValue placeholder="Select a device" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem key="all" value="all">
|
||||
All devices
|
||||
</SelectItem>
|
||||
{devices?.data?.map((device) => (
|
||||
<SelectItem key={device._id} value={device._id}>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -173,9 +184,6 @@ const WebhooksHistory = () => {
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectItem key="all" value="all">
|
||||
All devices
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
@@ -192,6 +200,12 @@ const WebhooksHistory = () => {
|
||||
<SelectValue placeholder="Message type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="h-1.5 w-1.5 rounded-full bg-gray-500" />
|
||||
All Events
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="MESSAGE_RECEIVED">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="h-1.5 w-1.5 rounded-full bg-green-500" />
|
||||
@@ -218,6 +232,12 @@ const WebhooksHistory = () => {
|
||||
<SelectValue placeholder="Status" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="h-1.5 w-1.5 rounded-full bg-gray-500" />
|
||||
All
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="delivered">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="h-1.5 w-1.5 rounded-full bg-green-500" />
|
||||
@@ -252,8 +272,6 @@ const WebhooksHistory = () => {
|
||||
Date Range
|
||||
</h3>{' '}
|
||||
</div>
|
||||
{/* date range ( today, last 3 days, last 7 days, last 30 days-default, last 90 days, custom)
|
||||
Custom should trigger a popover to set a custom date range ( two date inputs) */}
|
||||
<Select value={dateRange} onValueChange={handleDateRangeChange}>
|
||||
<SelectTrigger className="w-full bg-white/80 dark:bg-black/20 h-9 text-sm border-brand-200 dark:border-brand-800/70">
|
||||
<SelectValue placeholder="Date Range" />
|
||||
@@ -272,96 +290,103 @@ Custom should trigger a popover to set a custom date range ( two date inputs) */
|
||||
Last 90 Days
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="90_months">
|
||||
<div className="flex items-center gap-1.5">
|
||||
Last 3 Months
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="180">
|
||||
<div className="flex items-center gap-1.5">
|
||||
Last 6 Months
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="365">
|
||||
<div className="flex items-center gap-1.5">
|
||||
Last 12 Months
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="custom">
|
||||
<div className="flex items-center gap-1.5">Custom</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="w-full sm:w-fit">
|
||||
<div className="flex items-center gap-2 mb-1.5">
|
||||
<div className="h-3.5 w-3.5 text-brand-500" />
|
||||
<div className="text-sm font-medium text-foreground" />
|
||||
</div>
|
||||
{dateRange === 'custom' && (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
onClick={() => {
|
||||
setOpenCal(true)
|
||||
}}
|
||||
className="flex items-center gap-2 mt-2 w-full border rounded-md px-2 py-1 text-sm"
|
||||
>
|
||||
<Calendar className="h-4 w-4" />
|
||||
{dateQuery.start && dateQuery.end
|
||||
? `${new Date(
|
||||
dateQuery.start
|
||||
).toLocaleDateString()} → ${new Date(
|
||||
dateQuery.end
|
||||
).toLocaleDateString()}`
|
||||
: 'Select custom range'}
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
{openCal && (
|
||||
<PopoverContent className="p-4 w-64">
|
||||
<div className="flex flex-col gap-3">
|
||||
<div>
|
||||
<label
|
||||
className="block text-xs mb-1"
|
||||
htmlFor="start-date"
|
||||
>
|
||||
Start Date
|
||||
</label>
|
||||
<input
|
||||
id="start-date"
|
||||
type="date"
|
||||
className="w-full border rounded px-2 py-1 text-sm"
|
||||
value={dateQuery.start}
|
||||
onChange={(e) =>
|
||||
setDateQuery({
|
||||
...dateQuery,
|
||||
start: e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
className="block text-xs mb-1"
|
||||
htmlFor="end-date"
|
||||
>
|
||||
End Date
|
||||
</label>
|
||||
<input
|
||||
id="end-date"
|
||||
type="date"
|
||||
className="w-full border rounded px-2 py-1 text-sm"
|
||||
value={dateQuery.end}
|
||||
onChange={(e) =>
|
||||
setDateQuery({
|
||||
...dateQuery,
|
||||
end: e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setPage(1)
|
||||
setOpenCal(false)
|
||||
refetch() // react-query refetch
|
||||
}}
|
||||
>
|
||||
Apply
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
)}
|
||||
</Popover>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Popover open={openCal} onOpenChange={setOpenCal}>
|
||||
<PopoverContent className="p-4 w-64">
|
||||
<div className="flex flex-col gap-3">
|
||||
<div>
|
||||
<label
|
||||
className="block text-xs mb-1"
|
||||
htmlFor="start-date"
|
||||
>
|
||||
Start Date
|
||||
</label>
|
||||
<input
|
||||
id="start-date"
|
||||
type="date"
|
||||
className="w-full border rounded px-2 py-1 text-sm"
|
||||
value={dateQuery.start ? dateQuery.start.split('T')[0] : ''}
|
||||
onChange={(e) =>
|
||||
setDateQuery({
|
||||
...dateQuery,
|
||||
start: e.target.value ? new Date(e.target.value).toISOString() : '',
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
className="block text-xs mb-1"
|
||||
htmlFor="end-date"
|
||||
>
|
||||
End Date
|
||||
</label>
|
||||
<input
|
||||
id="end-date"
|
||||
type="date"
|
||||
className="w-full border rounded px-2 py-1 text-sm"
|
||||
value={dateQuery.end ? dateQuery.end.split('T')[0] : ''}
|
||||
onChange={(e) =>
|
||||
setDateQuery({
|
||||
...dateQuery,
|
||||
end: e.target.value ? new Date(e.target.value).toISOString() : '',
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setOpenCal(false)
|
||||
setDateRange('90')
|
||||
const end = new Date()
|
||||
const start = new Date()
|
||||
start.setDate(end.getDate() - 90)
|
||||
setDateQuery({ start: start.toISOString(), end: end.toISOString() })
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setPage(1)
|
||||
setOpenCal(false)
|
||||
refetch()
|
||||
}}
|
||||
disabled={!dateQuery.start || !dateQuery.end}
|
||||
>
|
||||
Apply
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
{isLoadingNotifications || isLoading ? (
|
||||
<ProductClient data={[]} isLoading={true} />
|
||||
) : (
|
||||
@@ -383,7 +408,7 @@ Custom should trigger a popover to set a custom date range ( two date inputs) */
|
||||
</Button>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2 justify-center sm:justify-start">
|
||||
{/* First page */}
|
||||
|
||||
{totalPages > 1 && (
|
||||
<Button
|
||||
onClick={() => handlePageChange(1)}
|
||||
@@ -404,7 +429,7 @@ Custom should trigger a popover to set a custom date range ( two date inputs) */
|
||||
<span className="px-1">...</span>
|
||||
)}
|
||||
|
||||
{/* Middle pages */}
|
||||
|
||||
{Array.from(
|
||||
{
|
||||
length: Math.min(6, totalPages - 2),
|
||||
|
||||
@@ -130,7 +130,7 @@ export default function WebhooksSection() {
|
||||
className='w-full sm:w-auto'
|
||||
>
|
||||
<Bell className='mr-2 h-4 w-4' />
|
||||
Webhook Notification
|
||||
Notification Deliveries
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
} from '@/components/ui/table'
|
||||
import React from 'react'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { SmsModal } from './sms-modal' // Import your modal
|
||||
import { WebhookPayloadModal } from './webhook-payload-modal' // Import your modal
|
||||
|
||||
interface DataTableProps<TData, TValue> {
|
||||
columns: ColumnDef<TData, TValue>[]
|
||||
@@ -39,6 +39,7 @@ export function DataTable<TData, TValue>({
|
||||
[]
|
||||
)
|
||||
const [selectedSms, setSelectedSms] = React.useState<any>(null)
|
||||
const [selectedPayload, setSelectedPayload] = React.useState<any>(null)
|
||||
const [isModalOpen, setIsModalOpen] = React.useState(false)
|
||||
|
||||
const table = useReactTable({
|
||||
@@ -57,6 +58,7 @@ export function DataTable<TData, TValue>({
|
||||
// Assuming your row data has smsData property
|
||||
if (row.original.smsData) {
|
||||
setSelectedSms(row.original.smsData)
|
||||
setSelectedPayload(row.original.payload)
|
||||
setIsModalOpen(true)
|
||||
}
|
||||
}
|
||||
@@ -64,6 +66,7 @@ export function DataTable<TData, TValue>({
|
||||
const handleCloseModal = () => {
|
||||
setIsModalOpen(false)
|
||||
setSelectedSms(null)
|
||||
setSelectedPayload(null)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -133,11 +136,12 @@ export function DataTable<TData, TValue>({
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* SMS Modal */}
|
||||
<SmsModal
|
||||
{/* Webhook Payload Modal */}
|
||||
<WebhookPayloadModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={handleCloseModal}
|
||||
smsData={selectedSms}
|
||||
payload={selectedPayload}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// components/sms-modal.tsx
|
||||
'use client'
|
||||
|
||||
import {
|
||||
@@ -9,13 +8,9 @@ import {
|
||||
} from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
X,
|
||||
Calendar,
|
||||
MessageSquare,
|
||||
Phone,
|
||||
CreditCard,
|
||||
Bell,
|
||||
Copy,
|
||||
} from 'lucide-react'
|
||||
import { useToast } from '@/hooks/use-toast'
|
||||
|
||||
interface SmsData {
|
||||
_id: string
|
||||
@@ -31,13 +26,16 @@ interface SmsData {
|
||||
__v: number
|
||||
}
|
||||
|
||||
interface SmsModalProps {
|
||||
interface WebhookPayloadModalProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
smsData: SmsData | null
|
||||
payload?: any
|
||||
}
|
||||
|
||||
export function SmsModal({ isOpen, onClose, smsData }: SmsModalProps) {
|
||||
export function WebhookPayloadModal({ isOpen, onClose, smsData, payload }: WebhookPayloadModalProps) {
|
||||
const { toast } = useToast()
|
||||
|
||||
if (!smsData) return null
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
@@ -51,23 +49,64 @@ export function SmsModal({ isOpen, onClose, smsData }: SmsModalProps) {
|
||||
})
|
||||
}
|
||||
|
||||
const copyPayloadToClipboard = async () => {
|
||||
if (!payload) return
|
||||
try {
|
||||
const jsonString = JSON.stringify(payload, null, 2)
|
||||
await navigator.clipboard.writeText(jsonString)
|
||||
toast({
|
||||
title: "Copied!",
|
||||
description: "Payload copied to clipboard",
|
||||
duration: 2000,
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('Failed to copy payload:', err)
|
||||
// Fallback for older browsers
|
||||
const textArea = document.createElement('textarea')
|
||||
textArea.value = JSON.stringify(payload, null, 2)
|
||||
document.body.appendChild(textArea)
|
||||
textArea.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(textArea)
|
||||
toast({
|
||||
title: "Copied!",
|
||||
description: "Payload copied to clipboard",
|
||||
duration: 2000,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="sm:max-w-lg max-h-[80vh] overflow-y-auto">
|
||||
<DialogContent className="sm:max-w-4xl max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader className="flex flex-row items-center justify-between space-y-0 pb-4">
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
Webhook Notification
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<h3 className="font-semibold text-sm flex items-center gap-2">
|
||||
Message Content
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
{/* Message Content */}
|
||||
<div className="bg-muted/50 rounded-lg p-4 max-h-32 overflow-y-scroll scrollbar-hide">
|
||||
<p className="text-sm whitespace-pre-wrap">{smsData.message}</p>
|
||||
</div>
|
||||
<div className="space-y-6">
|
||||
{payload && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-semibold text-lg">Payload</h3>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={copyPayloadToClipboard}
|
||||
className="h-8"
|
||||
>
|
||||
<Copy className="h-3 w-3 mr-1" />
|
||||
Copy
|
||||
</Button>
|
||||
</div>
|
||||
<div className="bg-gray-50 dark:bg-gray-900 rounded-md border p-4 overflow-auto max-h-64">
|
||||
<pre className="text-xs font-mono whitespace-pre-wrap text-gray-800 dark:text-gray-200">
|
||||
{JSON.stringify(payload, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{/* Sender Information */}
|
||||
@@ -95,6 +134,7 @@ export function SmsModal({ isOpen, onClose, smsData }: SmsModalProps) {
|
||||
{smsData.status}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-semibold text-sm flex items-center gap-x-2">
|
||||
Created At
|
||||
@@ -5,6 +5,7 @@ import { Button } from '@/components/ui/button'
|
||||
import { DataTable } from './data-table'
|
||||
import type { ColumnDef } from '@tanstack/react-table'
|
||||
import { format } from 'date-fns'
|
||||
import { Eye } from 'lucide-react'
|
||||
|
||||
interface ProductClientProps {
|
||||
data: ProductColumns[]
|
||||
@@ -20,8 +21,11 @@ export type ProductColumns = {
|
||||
}
|
||||
createdAt?: string
|
||||
status: string
|
||||
computedStatus?: string
|
||||
payload?: any
|
||||
}
|
||||
|
||||
|
||||
export const columns: ColumnDef<ProductColumns>[] = [
|
||||
{
|
||||
accessorKey: 'event',
|
||||
@@ -59,6 +63,15 @@ export const columns: ColumnDef<ProductColumns>[] = [
|
||||
accessorKey: 'createdAt',
|
||||
header: 'Created At',
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: 'Actions',
|
||||
cell: ({ row }) => (
|
||||
<Button variant="ghost" size="sm" disabled={!row.original.payload}>
|
||||
<Eye className="h-4 w-4" />
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
]
|
||||
const formatDate = (dateString: string) => {
|
||||
return format(new Date(dateString), 'MMM dd, yyyy h:mm a')
|
||||
@@ -74,7 +87,8 @@ const ProductClient = ({ data, isLoading, status = 'delivered' }) => {
|
||||
` ${d.smsData._id}`,
|
||||
],
|
||||
createdAt: formatDate(d.createdAt.toString()),
|
||||
status,
|
||||
status: d.computedStatus || status,
|
||||
payload: d.payload,
|
||||
}))
|
||||
|
||||
return (
|
||||
|
||||
@@ -10,7 +10,7 @@ export default function MessagingPage() {
|
||||
<div className="flex items-center space-x-2">
|
||||
<MessageSquareTextIcon className="h-6 w-6 text-primary" />
|
||||
<h2 className="text-3xl font-bold tracking-tight">
|
||||
Webhook Notifications
|
||||
Webhook Notification Delivery History
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user