veza/apps/web/src/components/developer/WebhooksView.tsx

143 lines
6.1 KiB
TypeScript
Raw Normal View History

import React, { useState, useEffect } from 'react';
import { Card } from '../ui/card';
import { Button } from '../ui/button';
import { Input } from '../ui/input';
import { Plus, Trash2, Zap, Terminal, Activity, Loader2 } from 'lucide-react';
import { useToast } from '../../components/feedback/ToastProvider';
import { cn } from '@/lib/utils';
import { webhookService, Webhook } from '../../services/webhookService';
export const WebhooksView: React.FC = () => {
const { addToast } = useToast();
const [webhooks, setWebhooks] = useState<Webhook[]>([]);
const [loading, setLoading] = useState(true);
const [newUrl, setNewUrl] = useState('');
const fetchWebhooks = async () => {
setLoading(true);
try {
const data = await webhookService.list();
setWebhooks(data);
} catch (e) {
addToast('Failed to load webhooks', 'error');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchWebhooks();
}, []);
const handleCreate = async () => {
if (!newUrl) return;
try {
await webhookService.create(newUrl);
setNewUrl('');
addToast('Webhook generated successfully', 'success');
fetchWebhooks();
} catch (e) {
addToast('Failed to create webhook', 'error');
}
};
const handleTest = (_id: string) => {
addToast(`Sending test payload to endpoint...`, 'info');
};
const handleDelete = async (id: string) => {
try {
await webhookService.delete(id);
setWebhooks(webhooks.filter((w) => w.id !== id));
addToast('Webhook disconnected', 'info');
} catch (e) {
addToast('Failed to delete webhook', 'error');
}
};
return (
<div className="space-y-6 pb-20 container mx-auto px-4 py-8 max-w-5xl">
<div className="flex items-end justify-between">
<div>
<h2 className="text-3xl font-display font-bold text-white mb-1">Webhooks</h2>
<p className="text-muted-foreground font-mono text-xs flex items-center gap-2">
<Terminal className="w-3 h-3" /> EVENT SUBSCRIPTION PROTOCOL
</p>
</div>
</div>
<Card variant="glass" className="p-6 border-primary/20 bg-black/40 relative overflow-hidden group">
<div className="absolute inset-x-0 bottom-0 h-1 bg-gradient-to-r from-transparent via-primary/50 to-transparent opacity-50 group-hover:opacity-100 transition-opacity" />
<h3 className="font-bold text-white mb-4 flex items-center gap-2 text-sm uppercase tracking-widest">
<Plus className="w-4 h-4 text-primary" /> Register Endpoint
</h3>
<div className="flex gap-4">
<Input
placeholder="https://api.domain.com/webhook"
value={newUrl}
onChange={(e) => setNewUrl(e.target.value)}
className="flex-1 font-mono text-sm"
/>
<Button onClick={handleCreate} disabled={!newUrl} className="shadow-glow-cyan">
Create Hook
</Button>
</div>
</Card>
<div className="space-y-4">
{loading ? (
<div className="flex justify-center py-12">
<Loader2 className="w-8 h-8 text-primary animate-spin" />
</div>
) : webhooks.length === 0 ? (
<Card variant="glass" className="p-12 text-center border-dashed border-white/10 bg-black/20">
<Activity className="w-12 h-12 text-muted-foreground mx-auto mb-4 opacity-20" />
<h4 className="text-white font-bold">No endpoints registered</h4>
<p className="text-muted-foreground text-sm max-w-sm mx-auto mt-2 font-mono">
Ready to stream real-time events to your external infra.
</p>
</Card>
) : (
webhooks.map((hook) => (
<Card key={hook.id} variant="glass" className="group overflow-hidden relative border-white/5 hover:border-white/10 transition-all bg-black/40">
{/* Status Indicator Bar */}
<div className={cn("absolute left-0 top-0 bottom-0 w-1", hook.status === 'active' ? 'bg-lime-500 shadow-status-dot-lime' : 'bg-red-500')} />
<div className="flex flex-col md:flex-row items-start md:items-center justify-between p-6 pl-8 gap-4">
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<div className={cn("text-xs font-bold px-2 py-0.5 rounded border uppercase tracking-wider", hook.status === 'active' ? 'border-lime-500/30 text-lime-500 bg-lime-500/10' : 'border-destructive/30 text-destructive bg-destructive/10')}>
{hook.status}
</div>
<span className="font-mono text-white text-sm break-all">{hook.url}</span>
</div>
<div className="flex flex-wrap gap-4 text-xs text-muted-foreground items-center">
<div className="flex gap-2">
<span className="text-primary/70">events:</span>
{hook.events.map(e => <span key={e} className="text-white bg-white/5 px-1 rounded">{e}</span>)}
</div>
<div className="w-1 h-1 rounded-full bg-white/20" />
<div className="flex items-center gap-1">
<Activity className="w-3 h-3" /> Last trigger: {hook.lastTriggered}
</div>
</div>
</div>
<div className="flex gap-2 w-full md:w-auto opacity-60 group-hover:opacity-100 transition-opacity">
<Button variant="outline" size="sm" onClick={() => handleTest(hook.id)} className="border-white/10 hover:bg-primary/20 hover:text-primary hover:border-primary/50">
<Zap className="w-3 h-3 mr-2" /> Test
</Button>
<Button variant="outline" size="sm" onClick={() => handleDelete(hook.id)} className="border-white/10 hover:bg-destructive/20 hover:text-destructive hover:border-destructive/50">
<Trash2 className="w-3 h-3" />
</Button>
</div>
</div>
</Card>
))
)}
</div>
</div>
);
};