mirror of
https://github.com/freedbygrace/DynamoDNS.git
synced 2026-08-30 20:29:13 +00:00
Add webhook delivery log page
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/46ce7574-6e2a-407f-94e2-683704669d2c.jpg
This commit is contained in:
@@ -15,6 +15,7 @@ import ProvidersPage from "@/pages/providers";
|
|||||||
import SettingsPage from "@/pages/settings";
|
import SettingsPage from "@/pages/settings";
|
||||||
import OrganizationsPage from "@/pages/organizations";
|
import OrganizationsPage from "@/pages/organizations";
|
||||||
import WebhooksPage from "@/pages/webhooks";
|
import WebhooksPage from "@/pages/webhooks";
|
||||||
|
import WebhookLogsPage from "@/pages/webhook-logs";
|
||||||
import { AuthProvider } from "@/hooks/use-auth";
|
import { AuthProvider } from "@/hooks/use-auth";
|
||||||
import { ProtectedRoute } from "@/lib/protected-route";
|
import { ProtectedRoute } from "@/lib/protected-route";
|
||||||
import { ThemeProvider } from "@/hooks/use-theme";
|
import { ThemeProvider } from "@/hooks/use-theme";
|
||||||
@@ -34,6 +35,7 @@ function Router() {
|
|||||||
<ProtectedRoute path="/users-roles" component={UsersRolesPage} />
|
<ProtectedRoute path="/users-roles" component={UsersRolesPage} />
|
||||||
<ProtectedRoute path="/providers" component={ProvidersPage} />
|
<ProtectedRoute path="/providers" component={ProvidersPage} />
|
||||||
<ProtectedRoute path="/webhooks" component={WebhooksPage} />
|
<ProtectedRoute path="/webhooks" component={WebhooksPage} />
|
||||||
|
<ProtectedRoute path="/webhook-logs" component={WebhookLogsPage} />
|
||||||
<ProtectedRoute path="/settings" component={SettingsPage} />
|
<ProtectedRoute path="/settings" component={SettingsPage} />
|
||||||
<Route component={NotFound} />
|
<Route component={NotFound} />
|
||||||
</Switch>
|
</Switch>
|
||||||
|
|||||||
@@ -314,7 +314,7 @@ export default function UsersRolesPage() {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Role Permissions</CardTitle>
|
<CardTitle>Role Permissions</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Understanding the different access levels in DynamiDNS
|
Understanding the different access levels in DynamoDNS
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
|||||||
@@ -0,0 +1,470 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||||
|
import { MainLayout } from "@/components/layouts/main-layout";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogFooter,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Pagination } from "@/components/shared/pagination";
|
||||||
|
import { Loader2, RefreshCw, CheckCircle, XCircle, Info, Search, Code, Clock, Webhook, Calendar } from "lucide-react";
|
||||||
|
import { formatDistanceToNow, format } from "date-fns";
|
||||||
|
import { queryClient, apiRequest } from "@/lib/queryClient";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
|
||||||
|
type WebhookDeliveryLog = {
|
||||||
|
id: string;
|
||||||
|
webhookId: string;
|
||||||
|
webhookUrl: string;
|
||||||
|
payload: any;
|
||||||
|
status: boolean;
|
||||||
|
statusCode: number | null;
|
||||||
|
error: string | null;
|
||||||
|
responseBody: string | null;
|
||||||
|
retryCount: number;
|
||||||
|
createdAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Webhook = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
organizationId: string;
|
||||||
|
events: string[];
|
||||||
|
active: boolean;
|
||||||
|
secretKey?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function WebhookLogsPage() {
|
||||||
|
const { toast } = useToast();
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [pageSize, setPageSize] = useState(10);
|
||||||
|
const [selectedLog, setSelectedLog] = useState<WebhookDeliveryLog | null>(null);
|
||||||
|
const [detailsOpen, setDetailsOpen] = useState(false);
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const [statusFilter, setStatusFilter] = useState<"all" | "success" | "failed">("all");
|
||||||
|
const [sortOrder, setSortOrder] = useState<"newest" | "oldest">("newest");
|
||||||
|
|
||||||
|
// Fetch all webhook logs across all webhooks
|
||||||
|
const { data: allLogs = [], isLoading } = useQuery({
|
||||||
|
queryKey: ["/api/webhook-logs"],
|
||||||
|
select: (data: WebhookDeliveryLog[]) => {
|
||||||
|
// Apply filters
|
||||||
|
let filtered = data;
|
||||||
|
|
||||||
|
// Status filter
|
||||||
|
if (statusFilter === "success") {
|
||||||
|
filtered = filtered.filter(log => log.status);
|
||||||
|
} else if (statusFilter === "failed") {
|
||||||
|
filtered = filtered.filter(log => !log.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search filter (search in webhook URL or response)
|
||||||
|
if (searchTerm) {
|
||||||
|
const term = searchTerm.toLowerCase();
|
||||||
|
filtered = filtered.filter(log =>
|
||||||
|
log.webhookUrl.toLowerCase().includes(term) ||
|
||||||
|
(log.responseBody && log.responseBody.toLowerCase().includes(term)) ||
|
||||||
|
(log.error && log.error.toLowerCase().includes(term))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply sorting
|
||||||
|
return [...filtered].sort((a, b) => {
|
||||||
|
const dateA = new Date(a.createdAt).getTime();
|
||||||
|
const dateB = new Date(b.createdAt).getTime();
|
||||||
|
return sortOrder === "newest" ? dateB - dateA : dateA - dateB;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch webhooks (for name display)
|
||||||
|
const { data: webhooks = [] } = useQuery<Webhook[]>({
|
||||||
|
queryKey: ["/api/webhooks"],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Retry mutation
|
||||||
|
const retryMutation = useMutation({
|
||||||
|
mutationFn: async (logId: string) => {
|
||||||
|
const res = await apiRequest("POST", `/api/webhook-logs/${logId}/retry`);
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast({
|
||||||
|
title: "Webhook retry initiated",
|
||||||
|
description: "The webhook delivery retry has been initiated.",
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["/api/webhook-logs"] });
|
||||||
|
setDetailsOpen(false);
|
||||||
|
},
|
||||||
|
onError: (error: Error) => {
|
||||||
|
toast({
|
||||||
|
title: "Retry failed",
|
||||||
|
description: error.message,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleRetry = (log: WebhookDeliveryLog) => {
|
||||||
|
retryMutation.mutate(log.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const viewDetails = (log: WebhookDeliveryLog) => {
|
||||||
|
setSelectedLog(log);
|
||||||
|
setDetailsOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate pagination
|
||||||
|
const totalPages = Math.ceil(allLogs.length / pageSize);
|
||||||
|
const startIndex = (currentPage - 1) * pageSize;
|
||||||
|
const paginatedLogs = allLogs.slice(startIndex, startIndex + pageSize);
|
||||||
|
|
||||||
|
// Get webhook name by ID
|
||||||
|
const getWebhookName = (webhookId: string) => {
|
||||||
|
const webhook = webhooks.find(w => w.id === webhookId);
|
||||||
|
return webhook ? webhook.name : `Webhook ${webhookId}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MainLayout
|
||||||
|
title="Webhook Logs Dashboard"
|
||||||
|
description="View and manage webhook delivery logs across all organizations."
|
||||||
|
>
|
||||||
|
{/* Filters and controls */}
|
||||||
|
<div className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<div className="flex flex-1 items-center space-x-2">
|
||||||
|
<div className="relative flex-1">
|
||||||
|
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
placeholder="Search webhook URLs or responses..."
|
||||||
|
className="pl-8"
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Select
|
||||||
|
value={statusFilter}
|
||||||
|
onValueChange={(value) => setStatusFilter(value as "all" | "success" | "failed")}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-[160px]">
|
||||||
|
<SelectValue placeholder="Filter by status" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">All statuses</SelectItem>
|
||||||
|
<SelectItem value="success">Successful</SelectItem>
|
||||||
|
<SelectItem value="failed">Failed</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Select
|
||||||
|
value={sortOrder}
|
||||||
|
onValueChange={(value) => setSortOrder(value as "newest" | "oldest")}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-[160px]">
|
||||||
|
<SelectValue placeholder="Sort by" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="newest">Newest first</SelectItem>
|
||||||
|
<SelectItem value="oldest">Oldest first</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => queryClient.invalidateQueries({ queryKey: ["/api/webhook-logs"] })}
|
||||||
|
title="Refresh logs"
|
||||||
|
>
|
||||||
|
<RefreshCw className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Select
|
||||||
|
value={pageSize.toString()}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
setPageSize(parseInt(value));
|
||||||
|
setCurrentPage(1);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-[110px]">
|
||||||
|
<SelectValue placeholder="Page size" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="10">10 per page</SelectItem>
|
||||||
|
<SelectItem value="25">25 per page</SelectItem>
|
||||||
|
<SelectItem value="50">50 per page</SelectItem>
|
||||||
|
<SelectItem value="100">100 per page</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Logs table */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<CardTitle>Webhook Delivery Logs</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Comprehensive view of all webhook delivery attempts across the system
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="p-0">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex justify-center items-center py-8">
|
||||||
|
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||||||
|
</div>
|
||||||
|
) : allLogs.length === 0 ? (
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<Webhook className="mx-auto h-12 w-12 text-muted-foreground" />
|
||||||
|
<h3 className="mt-4 text-lg font-medium">No webhook logs found</h3>
|
||||||
|
<p className="mt-2 text-sm text-muted-foreground">
|
||||||
|
{searchTerm || statusFilter !== "all"
|
||||||
|
? "Try adjusting your search or filters"
|
||||||
|
: "Webhook logs will appear here after webhooks are triggered"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Status</TableHead>
|
||||||
|
<TableHead>Webhook</TableHead>
|
||||||
|
<TableHead>URL</TableHead>
|
||||||
|
<TableHead>Response</TableHead>
|
||||||
|
<TableHead>Retries</TableHead>
|
||||||
|
<TableHead>Time</TableHead>
|
||||||
|
<TableHead className="text-right">Actions</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{paginatedLogs.map((log) => (
|
||||||
|
<TableRow key={log.id} className="cursor-pointer hover:bg-muted/50" onClick={() => viewDetails(log)}>
|
||||||
|
<TableCell>
|
||||||
|
{log.status ? (
|
||||||
|
<Badge variant="outline" className="bg-success/10 text-success border-success/20">
|
||||||
|
<CheckCircle className="w-3.5 h-3.5 mr-1" />
|
||||||
|
Success
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge variant="outline" className="bg-destructive/10 text-destructive border-destructive/20">
|
||||||
|
<XCircle className="w-3.5 h-3.5 mr-1" />
|
||||||
|
Failed
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="font-medium">
|
||||||
|
{getWebhookName(log.webhookId)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="max-w-[200px] truncate">
|
||||||
|
{log.webhookUrl}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{log.status ? (
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{log.statusCode || 200} OK
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-sm text-destructive">
|
||||||
|
{log.error || `Error ${log.statusCode || 'unknown'}`}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{log.retryCount > 0 ? (
|
||||||
|
<Badge variant="outline" className="text-muted-foreground">
|
||||||
|
<RefreshCw className="w-3 h-3 mr-1" /> {log.retryCount}
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
<span className="text-sm text-muted-foreground">None</span>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex items-center text-sm text-muted-foreground">
|
||||||
|
<Calendar className="w-3.5 h-3.5 mr-1.5" />
|
||||||
|
<span title={format(new Date(log.createdAt), 'PPpp')}>
|
||||||
|
{formatDistanceToNow(new Date(log.createdAt), { addSuffix: true })}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
viewDetails(log);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
View Details
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pagination */}
|
||||||
|
{allLogs.length > pageSize && (
|
||||||
|
<div className="mt-4 p-4 border-t">
|
||||||
|
<Pagination
|
||||||
|
currentPage={currentPage}
|
||||||
|
totalPages={totalPages}
|
||||||
|
onPageChange={setCurrentPage}
|
||||||
|
itemLabel="logs"
|
||||||
|
totalItems={allLogs.length}
|
||||||
|
itemsPerPage={pageSize}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Details Dialog */}
|
||||||
|
{selectedLog && (
|
||||||
|
<Dialog open={detailsOpen} onOpenChange={setDetailsOpen}>
|
||||||
|
<DialogContent className="max-w-3xl max-h-[90vh] overflow-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
{selectedLog.status ? (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<CheckCircle className="w-5 h-5 mr-2 text-success" />
|
||||||
|
Successful Webhook Delivery
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<XCircle className="w-5 h-5 mr-2 text-destructive" />
|
||||||
|
Failed Webhook Delivery
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Detailed information about this webhook delivery attempt
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-medium mb-1">Webhook</h3>
|
||||||
|
<p className="text-sm">{getWebhookName(selectedLog.webhookId)}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-medium mb-1">Time</h3>
|
||||||
|
<p className="text-sm">{format(new Date(selectedLog.createdAt), 'PPpp')}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-medium mb-1">Status</h3>
|
||||||
|
<p className="text-sm">
|
||||||
|
{selectedLog.status
|
||||||
|
? `Success (${selectedLog.statusCode || 200})`
|
||||||
|
: `Failed${selectedLog.statusCode ? ` (${selectedLog.statusCode})` : ''}`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-medium mb-1">Retry Count</h3>
|
||||||
|
<p className="text-sm">{selectedLog.retryCount}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-medium mb-1">Webhook URL</h3>
|
||||||
|
<div className="text-sm bg-muted p-3 rounded-md">
|
||||||
|
{selectedLog.webhookUrl}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!selectedLog.status && selectedLog.error && (
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-medium mb-1 text-destructive">Error</h3>
|
||||||
|
<div className="text-sm bg-destructive/10 text-destructive p-3 rounded-md">
|
||||||
|
{selectedLog.error}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-medium mb-1">Payload</h3>
|
||||||
|
<pre className="text-xs bg-muted p-3 rounded-md overflow-auto max-h-40">
|
||||||
|
{JSON.stringify(selectedLog.payload, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedLog.responseBody && (
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-medium mb-1">Response Body</h3>
|
||||||
|
<pre className="text-xs bg-muted p-3 rounded-md overflow-auto max-h-40">
|
||||||
|
{typeof selectedLog.responseBody === 'string'
|
||||||
|
? (() => {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(JSON.parse(selectedLog.responseBody), null, 2);
|
||||||
|
} catch {
|
||||||
|
return selectedLog.responseBody;
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
: JSON.stringify(selectedLog.responseBody, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
{!selectedLog.status && (
|
||||||
|
<Button
|
||||||
|
onClick={() => handleRetry(selectedLog)}
|
||||||
|
disabled={retryMutation.isPending}
|
||||||
|
className="mr-auto"
|
||||||
|
>
|
||||||
|
{retryMutation.isPending ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
Retrying...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<RefreshCw className="mr-2 h-4 w-4" />
|
||||||
|
Retry Webhook
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button variant="outline" onClick={() => setDetailsOpen(false)}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)}
|
||||||
|
</MainLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user