mirror of
https://github.com/freedbygrace/DynamoDNS.git
synced 2026-07-27 03:48:56 +00:00
Add webhook delivery logs view
Replit-Commit-Author: Agent Replit-Commit-Session-Id: 9111ef36-26c8-4085-84ca-a35dc1fec1b5 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/7083d608-d6d3-4a6a-9a27-6286c5109627/a3ee9264-1cdc-49be-a7fe-9a0df39659bd.jpg
This commit is contained in:
@@ -76,8 +76,11 @@ export default function WebhooksPage() {
|
||||
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
|
||||
const [isTestResultOpen, setIsTestResultOpen] = useState(false);
|
||||
const [isHistorySheetOpen, setIsHistorySheetOpen] = useState(false);
|
||||
const [isLogsDialogOpen, setIsLogsDialogOpen] = useState(false);
|
||||
const [currentWebhook, setCurrentWebhook] = useState<Webhook | null>(null);
|
||||
const [currentWebhookId, setCurrentWebhookId] = useState<string | null>(null);
|
||||
const [testResult, setTestResult] = useState<any>(null);
|
||||
const [selectedLog, setSelectedLog] = useState<WebhookDeliveryLog | null>(null);
|
||||
|
||||
// Form state for create/edit
|
||||
const [formData, setFormData] = useState({
|
||||
@@ -229,6 +232,21 @@ export default function WebhooksPage() {
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// Fetch webhook delivery logs
|
||||
const {
|
||||
data: webhookLogs = [],
|
||||
isLoading: isLogsLoading,
|
||||
error: logsError,
|
||||
} = useQuery({
|
||||
queryKey: ["/api/webhooks", currentWebhookId, "logs"],
|
||||
queryFn: async () => {
|
||||
if (!currentWebhookId) return [];
|
||||
const res = await apiRequest("GET", `/api/webhooks/${currentWebhookId}/logs`);
|
||||
return await res.json();
|
||||
},
|
||||
enabled: !!currentWebhookId && isLogsDialogOpen,
|
||||
});
|
||||
|
||||
const handleCreateSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -271,6 +289,15 @@ export default function WebhooksPage() {
|
||||
setCurrentWebhook(webhook);
|
||||
setIsDeleteOpen(true);
|
||||
};
|
||||
|
||||
const handleViewLogs = (webhookId: string) => {
|
||||
setCurrentWebhookId(webhookId);
|
||||
const webhook = webhooks.find((w: Webhook) => w.id === webhookId);
|
||||
if (webhook) {
|
||||
setCurrentWebhook(webhook);
|
||||
}
|
||||
setIsLogsDialogOpen(true);
|
||||
};
|
||||
|
||||
// Event options for the select component
|
||||
const eventOptions = [
|
||||
@@ -281,8 +308,11 @@ export default function WebhooksPage() {
|
||||
];
|
||||
|
||||
// Helper to format the date
|
||||
const formatDate = (dateString: string | null | undefined) => {
|
||||
const formatDate = (dateString: string | Date | null | undefined) => {
|
||||
if (!dateString) return "Never";
|
||||
if (dateString instanceof Date) {
|
||||
return dateString.toLocaleString();
|
||||
}
|
||||
return new Date(dateString).toLocaleString();
|
||||
};
|
||||
|
||||
@@ -387,6 +417,14 @@ export default function WebhooksPage() {
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex justify-end space-x-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleViewLogs(webhook.id)}
|
||||
>
|
||||
<Activity className="h-4 w-4" />
|
||||
<span className="sr-only">Logs</span>
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
@@ -725,6 +763,178 @@ export default function WebhooksPage() {
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Webhook Delivery Logs Dialog */}
|
||||
<Dialog
|
||||
open={isLogsDialogOpen}
|
||||
onOpenChange={(open) => {
|
||||
setIsLogsDialogOpen(open);
|
||||
if (!open) {
|
||||
setCurrentWebhookId(null);
|
||||
setSelectedLog(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent className="sm:max-w-[700px] max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Delivery Logs - {currentWebhook?.name}</DialogTitle>
|
||||
<DialogDescription>
|
||||
View webhook delivery history and status
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{isLogsLoading ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||||
</div>
|
||||
) : logsError ? (
|
||||
<div className="py-4 text-center">
|
||||
<p className="text-destructive">Error loading logs</p>
|
||||
<p className="text-sm text-muted-foreground mt-2">
|
||||
{(logsError as Error).message}
|
||||
</p>
|
||||
</div>
|
||||
) : webhookLogs.length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
<Activity className="mx-auto h-12 w-12 text-muted-foreground opacity-50" />
|
||||
<h3 className="mt-4 text-lg font-medium">No delivery logs found</h3>
|
||||
<p className="mt-2 text-sm text-muted-foreground max-w-sm mx-auto">
|
||||
This webhook hasn't been triggered yet or logs have been cleared.
|
||||
</p>
|
||||
<Button
|
||||
onClick={() => handleTestWebhook(currentWebhookId!)}
|
||||
className="mt-4"
|
||||
variant="outline"
|
||||
>
|
||||
<RefreshCw className="mr-2 h-4 w-4" /> Test Webhook Now
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
<div className="border rounded-md overflow-hidden">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Event</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Time</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{webhookLogs.map((log: WebhookDeliveryLog) => (
|
||||
<TableRow key={log.id}>
|
||||
<TableCell className="font-medium">{log.event}</TableCell>
|
||||
<TableCell>
|
||||
{log.status ? (
|
||||
<Badge className="bg-green-500 text-white">Success</Badge>
|
||||
) : (
|
||||
<Badge variant="destructive">Failed</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-muted-foreground">
|
||||
{formatDate(log.createdAt)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => setSelectedLog(log)}
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
<span className="sr-only">View Details</span>
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{selectedLog && (
|
||||
<div className="space-y-4 border p-4 rounded-md">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-medium">Delivery Details</h3>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => setSelectedLog(null)}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium mb-1">Status</h4>
|
||||
<div className="flex items-center gap-2">
|
||||
{selectedLog.status ? (
|
||||
<Badge className="bg-green-500 text-white">
|
||||
Success ({selectedLog.statusCode || 200})
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="destructive">
|
||||
Failed {selectedLog.statusCode && `(${selectedLog.statusCode})`}
|
||||
</Badge>
|
||||
)}
|
||||
{selectedLog.retryCount > 0 && (
|
||||
<Badge variant="outline">
|
||||
Retry {selectedLog.retryCount}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedLog.message && (
|
||||
<div>
|
||||
<h4 className="text-sm font-medium mb-1">Message</h4>
|
||||
<div className="bg-muted p-3 rounded-md text-sm">
|
||||
{selectedLog.message}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-medium mb-1">Payload</h4>
|
||||
<div className="bg-muted p-3 rounded-md overflow-x-auto">
|
||||
<pre className="text-xs whitespace-pre-wrap">
|
||||
{JSON.stringify(selectedLog.payload, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedLog.responseBody && (
|
||||
<div>
|
||||
<h4 className="text-sm font-medium mb-1">Response</h4>
|
||||
<div className="bg-muted p-3 rounded-md overflow-x-auto">
|
||||
<pre className="text-xs whitespace-pre-wrap">
|
||||
{selectedLog.responseBody}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedLog.signature && (
|
||||
<div>
|
||||
<h4 className="text-sm font-medium mb-1">Signature</h4>
|
||||
<div className="bg-muted p-3 rounded-md text-xs font-mono truncate">
|
||||
{selectedLog.signature}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setIsLogsDialogOpen(false)}>
|
||||
Close
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user