wulf-pulse/app/admin/workflow/pipelines/page.tsx

229 lines
8.8 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import Link from 'next/link';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Switch } from '@/components/ui/switch';
import {
ArrowLeft,
Plus,
Workflow,
Play,
Pause,
Trash2,
Settings,
History,
Zap,
GitBranch,
} from 'lucide-react';
interface Pipeline {
id: number;
name: string;
description: string | null;
is_active: boolean;
trigger_source: string;
trigger_conditions: any[];
sort_order: number;
step_count: number;
created_at: string;
updated_at: string;
}
const SOURCE_LABELS: Record<string, { label: string; color: string }> = {
datto_rmm: { label: 'Datto RMM', color: 'bg-blue-100 text-blue-800' },
autotask: { label: 'Autotask', color: 'bg-green-100 text-green-800' },
veeam: { label: 'Veeam', color: 'bg-purple-100 text-purple-800' },
manual: { label: 'Manual', color: 'bg-gray-100 text-gray-800' },
};
export default function PipelinesPage() {
const [pipelines, setPipelines] = useState<Pipeline[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [showCreate, setShowCreate] = useState(false);
const [newPipeline, setNewPipeline] = useState({ name: '', description: '', trigger_source: 'datto_rmm' });
useEffect(() => { loadPipelines(); }, []);
const loadPipelines = async () => {
setIsLoading(true);
try {
const res = await fetch('/api/pipelines');
if (res.ok) {
const data = await res.json();
setPipelines(data.data || []);
}
} catch (err) {
console.error('Failed to load pipelines:', err);
} finally {
setIsLoading(false);
}
};
const togglePipeline = async (id: number, active: boolean) => {
try {
await fetch(`/api/pipelines/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ is_active: active }),
});
setPipelines(prev => prev.map(p => p.id === id ? { ...p, is_active: active } : p));
} catch (err) {
console.error('Failed to toggle pipeline:', err);
}
};
const deletePipeline = async (id: number) => {
if (!confirm('Delete this pipeline and all its steps?')) return;
try {
await fetch(`/api/pipelines/${id}`, { method: 'DELETE' });
setPipelines(prev => prev.filter(p => p.id !== id));
} catch (err) {
console.error('Failed to delete pipeline:', err);
}
};
const createPipeline = async () => {
if (!newPipeline.name) return;
try {
const res = await fetch('/api/pipelines', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...newPipeline, is_active: false }),
});
if (res.ok) {
setShowCreate(false);
setNewPipeline({ name: '', description: '', trigger_source: 'datto_rmm' });
loadPipelines();
}
} catch (err) {
console.error('Failed to create pipeline:', err);
}
};
return (
<div className="container mx-auto p-6 space-y-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Link href="/admin/workflow">
<Button variant="ghost" size="icon"><ArrowLeft className="h-4 w-4" /></Button>
</Link>
<div>
<h1 className="text-2xl font-bold flex items-center gap-2">
<GitBranch className="h-6 w-6" /> Webhook Pipelines
</h1>
<p className="text-muted-foreground text-sm">Automate actions triggered by incoming webhooks</p>
</div>
</div>
<Button onClick={() => setShowCreate(!showCreate)}>
<Plus className="h-4 w-4 mr-2" /> New Pipeline
</Button>
</div>
{showCreate && (
<Card>
<CardHeader>
<CardTitle className="text-lg">Create Pipeline</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="text-sm font-medium">Name</label>
<input
className="w-full mt-1 px-3 py-2 border rounded-md bg-background"
placeholder="e.g., RMM Alert → Ticket"
value={newPipeline.name}
onChange={e => setNewPipeline(p => ({ ...p, name: e.target.value }))}
/>
</div>
<div>
<label className="text-sm font-medium">Trigger Source</label>
<select
className="w-full mt-1 px-3 py-2 border rounded-md bg-background"
value={newPipeline.trigger_source}
onChange={e => setNewPipeline(p => ({ ...p, trigger_source: e.target.value }))}
>
<option value="datto_rmm">Datto RMM</option>
<option value="autotask">Autotask</option>
<option value="veeam">Veeam</option>
<option value="manual">Manual</option>
</select>
</div>
<div>
<label className="text-sm font-medium">Description</label>
<input
className="w-full mt-1 px-3 py-2 border rounded-md bg-background"
placeholder="Optional description"
value={newPipeline.description}
onChange={e => setNewPipeline(p => ({ ...p, description: e.target.value }))}
/>
</div>
</div>
<div className="flex gap-2">
<Button onClick={createPipeline} disabled={!newPipeline.name}>Create</Button>
<Button variant="outline" onClick={() => setShowCreate(false)}>Cancel</Button>
</div>
</CardContent>
</Card>
)}
{isLoading ? (
<div className="text-center py-12 text-muted-foreground">Loading pipelines...</div>
) : pipelines.length === 0 ? (
<Card>
<CardContent className="py-12 text-center text-muted-foreground">
<Workflow className="h-12 w-12 mx-auto mb-4 opacity-30" />
<p>No pipelines yet. Create one to get started.</p>
</CardContent>
</Card>
) : (
<div className="space-y-3">
{pipelines.map(pipeline => {
const source = SOURCE_LABELS[pipeline.trigger_source] || { label: pipeline.trigger_source, color: 'bg-gray-100 text-gray-800' };
return (
<Card key={pipeline.id} className={!pipeline.is_active ? 'opacity-60' : ''}>
<CardContent className="py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4 flex-1">
<Switch
checked={pipeline.is_active}
onCheckedChange={(checked) => togglePipeline(pipeline.id, checked)}
/>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<Link href={`/admin/workflow/pipelines/${pipeline.id}`} className="font-medium hover:underline">
{pipeline.name}
</Link>
<Badge variant="outline" className={source.color}>{source.label}</Badge>
<Badge variant="outline">{pipeline.step_count} steps</Badge>
{pipeline.trigger_conditions?.length > 0 && (
<Badge variant="outline" className="bg-yellow-50 text-yellow-700">
{pipeline.trigger_conditions.length} condition{pipeline.trigger_conditions.length > 1 ? 's' : ''}
</Badge>
)}
</div>
{pipeline.description && (
<p className="text-sm text-muted-foreground mt-1 truncate">{pipeline.description}</p>
)}
</div>
</div>
<div className="flex items-center gap-2">
<Link href={`/admin/workflow/pipelines/${pipeline.id}`}>
<Button variant="ghost" size="icon"><Settings className="h-4 w-4" /></Button>
</Link>
<Button variant="ghost" size="icon" onClick={() => deletePipeline(pipeline.id)}>
<Trash2 className="h-4 w-4 text-red-500" />
</Button>
</div>
</div>
</CardContent>
</Card>
);
})}
</div>
)}
</div>
);
}