2026-01-26 13:12:17 +00:00
|
|
|
import React, { useState, useEffect } from 'react';
|
2026-01-07 09:31:02 +00:00
|
|
|
import { Card } from '../ui/card';
|
|
|
|
|
import { Button } from '../ui/button';
|
|
|
|
|
import { Input } from '../ui/input';
|
2026-02-08 23:00:21 +00:00
|
|
|
import { EmptyState } from '../ui/empty-state';
|
2026-01-26 13:12:17 +00:00
|
|
|
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';
|
2026-01-07 09:31:02 +00:00
|
|
|
|
|
|
|
|
export const WebhooksView: React.FC = () => {
|
|
|
|
|
const { addToast } = useToast();
|
2026-01-26 13:12:17 +00:00
|
|
|
const [webhooks, setWebhooks] = useState<Webhook[]>([]);
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
2026-01-07 09:31:02 +00:00
|
|
|
const [newUrl, setNewUrl] = useState('');
|
|
|
|
|
|
2026-01-26 13:12:17 +00:00
|
|
|
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 () => {
|
2026-01-13 18:47:57 +00:00
|
|
|
if (!newUrl) return;
|
2026-01-26 13:12:17 +00:00
|
|
|
try {
|
|
|
|
|
await webhookService.create(newUrl);
|
|
|
|
|
setNewUrl('');
|
|
|
|
|
addToast('Webhook generated successfully', 'success');
|
|
|
|
|
fetchWebhooks();
|
|
|
|
|
} catch (e) {
|
|
|
|
|
addToast('Failed to create webhook', 'error');
|
|
|
|
|
}
|
2026-01-07 09:31:02 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleTest = (_id: string) => {
|
2026-01-26 13:12:17 +00:00
|
|
|
addToast(`Sending test payload to endpoint...`, 'info');
|
2026-01-07 09:31:02 +00:00
|
|
|
};
|
|
|
|
|
|
2026-01-26 13:12:17 +00:00
|
|
|
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');
|
|
|
|
|
}
|
2026-01-07 09:31:02 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
2026-01-26 13:12:17 +00:00
|
|
|
<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>
|
2026-01-13 18:47:57 +00:00
|
|
|
</div>
|
|
|
|
|
|
2026-01-26 13:12:17 +00:00
|
|
|
<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
|
2026-01-13 18:47:57 +00:00
|
|
|
</h3>
|
|
|
|
|
<div className="flex gap-4">
|
|
|
|
|
<Input
|
2026-01-26 13:12:17 +00:00
|
|
|
placeholder="https://api.domain.com/webhook"
|
2026-01-13 18:47:57 +00:00
|
|
|
value={newUrl}
|
|
|
|
|
onChange={(e) => setNewUrl(e.target.value)}
|
2026-01-26 13:12:17 +00:00
|
|
|
className="flex-1 font-mono text-sm"
|
2026-01-13 18:47:57 +00:00
|
|
|
/>
|
2026-01-26 13:12:17 +00:00
|
|
|
<Button onClick={handleCreate} disabled={!newUrl} className="shadow-glow-cyan">
|
|
|
|
|
Create Hook
|
2026-01-13 18:47:57 +00:00
|
|
|
</Button>
|
2026-01-07 09:31:02 +00:00
|
|
|
</div>
|
2026-01-13 18:47:57 +00:00
|
|
|
</Card>
|
2026-01-07 09:31:02 +00:00
|
|
|
|
2026-01-13 18:47:57 +00:00
|
|
|
<div className="space-y-4">
|
2026-01-26 13:12:17 +00:00
|
|
|
{loading ? (
|
|
|
|
|
<div className="flex justify-center py-12">
|
|
|
|
|
<Loader2 className="w-8 h-8 text-primary animate-spin" />
|
|
|
|
|
</div>
|
|
|
|
|
) : webhooks.length === 0 ? (
|
2026-02-08 23:00:21 +00:00
|
|
|
<EmptyState
|
|
|
|
|
icon={<Activity className="w-full h-full" />}
|
|
|
|
|
title="No endpoints registered"
|
|
|
|
|
description="Ready to stream real-time events to your external infra."
|
|
|
|
|
size="lg"
|
|
|
|
|
/>
|
2026-01-26 13:12:17 +00:00
|
|
|
) : (
|
|
|
|
|
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 */}
|
refactor(tokens): complete design token migration to semantic system
Sprint 3.1 — Default colors → semantic (~15 files, ~99 replacements):
- lime-500 → success, red-500 → destructive, cyan-500 → primary
Sprint 3.2 — Hardcoded colors → semantic (~13 files, ~99 replacements):
- text-white → text-foreground, bg-black → bg-background, bg-white → bg-card
Sprint 3.3 — Legacy kodo-* → semantic (~27 files, ~122 replacements):
- bg-kodo-ink → bg-card, bg-kodo-void → bg-background, text-kodo-steel → text-muted-foreground
- Preserved kodo-cyan/magenta/lime/gold palette accents and gradients
Sprint 3.4 — Arbitrary values → Tailwind scale (5 replacements):
- min-h-[600px] → min-h-layout-page, min-h-[400px] → min-h-layout-page-sm
- left-[50%] → left-1/2, min-h-[80px] → min-h-20, min-h-[40px] → min-h-10
Sprint 3.5 — Border-radius standardization (4 replacements):
- Modal/dialog skeletons: rounded-lg → rounded-xl (convention)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-09 22:05:09 +00:00
|
|
|
<div className={cn("absolute left-0 top-0 bottom-0 w-1", hook.status === 'active' ? 'bg-success shadow-status-dot-lime' : 'bg-destructive')} />
|
2026-01-26 13:12:17 +00:00
|
|
|
|
|
|
|
|
<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">
|
refactor(tokens): complete design token migration to semantic system
Sprint 3.1 — Default colors → semantic (~15 files, ~99 replacements):
- lime-500 → success, red-500 → destructive, cyan-500 → primary
Sprint 3.2 — Hardcoded colors → semantic (~13 files, ~99 replacements):
- text-white → text-foreground, bg-black → bg-background, bg-white → bg-card
Sprint 3.3 — Legacy kodo-* → semantic (~27 files, ~122 replacements):
- bg-kodo-ink → bg-card, bg-kodo-void → bg-background, text-kodo-steel → text-muted-foreground
- Preserved kodo-cyan/magenta/lime/gold palette accents and gradients
Sprint 3.4 — Arbitrary values → Tailwind scale (5 replacements):
- min-h-[600px] → min-h-layout-page, min-h-[400px] → min-h-layout-page-sm
- left-[50%] → left-1/2, min-h-[80px] → min-h-20, min-h-[40px] → min-h-10
Sprint 3.5 — Border-radius standardization (4 replacements):
- Modal/dialog skeletons: rounded-lg → rounded-xl (convention)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-09 22:05:09 +00:00
|
|
|
<div className={cn("text-xs font-bold px-2 py-0.5 rounded border uppercase tracking-wider", hook.status === 'active' ? 'border-success/30 text-success bg-success/10' : 'border-destructive/30 text-destructive bg-destructive/10')}>
|
2026-01-26 13:12:17 +00:00
|
|
|
{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>
|
2026-01-13 18:47:57 +00:00
|
|
|
</div>
|
2026-01-26 13:12:17 +00:00
|
|
|
|
|
|
|
|
<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>
|
2026-02-08 22:26:34 +00:00
|
|
|
<Button variant="outline" size="sm" onClick={() => handleDelete(hook.id)} className="border-white/10 hover:bg-destructive/20 hover:text-destructive hover:border-destructive/50">
|
2026-01-26 13:12:17 +00:00
|
|
|
<Trash2 className="w-3 h-3" />
|
|
|
|
|
</Button>
|
2026-01-13 18:47:57 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-01-26 13:12:17 +00:00
|
|
|
</Card>
|
|
|
|
|
))
|
|
|
|
|
)}
|
2026-01-13 18:47:57 +00:00
|
|
|
</div>
|
2026-01-07 09:31:02 +00:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|