229 lines
8.8 KiB
TypeScript
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>
|
|
);
|
|
}
|