dachbord / src /components /AutomationView.tsx
drmartin2050's picture
feat: add audit timestamps to core entities
9adf195
Raw
History Blame Contribute Delete
26.7 kB
import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'motion/react';
import { Play, Clipboard, Plus, ShieldCheck, Terminal, Webhook, X, Layers, AlertCircle, Info, RefreshCw, Key } from 'lucide-react';
import { LocalizationSchema, N8NWebhook } from '../types';
import { getSupabaseClient } from '../utils/supabase';
interface AutomationViewProps {
key?: string;
t: LocalizationSchema['automation'];
lang?: 'ar' | 'en';
}
// Fallback seed webhooks for initial presentation
const PRE_SEEDED_WEBHOOKS: N8NWebhook[] = [
{
id: 'seed-hf-1',
name: 'Sync Contacts to Google Sheets (n8n)',
webhookUrl: 'https://drmartin2050-n8n.hf.space/webhook/sync-contacts-demo',
},
{
id: 'seed-hf-2',
name: 'AI Auto-Responder Dispatcher Workflow',
webhookUrl: 'https://drmartin2050-n8n.hf.space/webhook/ai-responder-agent',
},
];
export default function AutomationView({ t, lang = 'ar' }: AutomationViewProps) {
const [webhooks, setWebhooks] = useState<N8NWebhook[]>([]);
const [selectedWebhookId, setSelectedWebhookId] = useState<string>('');
const [payload, setPayload] = useState<string>(
JSON.stringify({ status: "triggered", source: "Mobile Dev Hub", timestamp: new Date().toISOString() }, null, 2)
);
const [isModalOpen, setIsModalOpen] = useState(false);
const [newWebhookName, setNewWebhookName] = useState('');
const [newWebhookUrl, setNewWebhookUrl] = useState('');
const [errorMessage, setErrorMessage] = useState('');
// Status/Notice Banners
const [alertBanner, setAlertBanner] = useState<{ text: string; type: 'error' | 'success' | 'info' } | null>(null);
// Failover tracking states
const [activeKeyIndex, setActiveKeyIndex] = useState<number>(1);
const [isFailoverActive, setIsFailoverActive] = useState<boolean>(false);
// Execution states
const [isTriggering, setIsTriggering] = useState(false);
const [executionLog, setExecutionLog] = useState<{
status: 'idle' | 'running' | 'success' | 'error';
timestamp?: string;
targetUrl?: string;
statusCode?: number;
logContent?: string;
}>({
status: 'idle',
});
const supabase = getSupabaseClient();
const triggerAlert = (text: string, type: 'error' | 'success' | 'info' = 'success') => {
setAlertBanner({ text, type });
setTimeout(() => setAlertBanner(null), 5000);
};
// Load webhooks from Supabase or Fallback
const loadWebhooks = async () => {
if (supabase) {
try {
const { data, error } = await supabase
.from('n8n_webhooks')
.select('*')
.order('created_at', { ascending: false });
if (error) throw error;
if (data && data.length > 0) {
const mapped: N8NWebhook[] = data.map((item: any) => ({
id: String(item.id),
name: item.name,
webhookUrl: item.webhook_url || item.webhookUrl,
}));
setWebhooks(mapped);
setSelectedWebhookId(mapped[0]?.id || '');
} else {
setWebhooks([]);
}
} catch (err: any) {
console.error('Error fetching webhooks from Supabase, loading seeds:', err.message);
setWebhooks(PRE_SEEDED_WEBHOOKS);
setSelectedWebhookId(PRE_SEEDED_WEBHOOKS[0]?.id || '');
}
} else {
const cached = sessionStorage.getItem('dev_hub_n8n_webhooks');
if (cached) {
try {
const list = JSON.parse(cached);
setWebhooks(list);
setSelectedWebhookId(list[0]?.id || '');
} catch (e) {
setWebhooks(PRE_SEEDED_WEBHOOKS);
setSelectedWebhookId(PRE_SEEDED_WEBHOOKS[0]?.id || '');
}
} else {
setWebhooks(PRE_SEEDED_WEBHOOKS);
setSelectedWebhookId(PRE_SEEDED_WEBHOOKS[0]?.id || '');
}
}
};
useEffect(() => {
loadWebhooks();
}, [supabase]);
const activeWebhook = webhooks.find((w) => w.id === selectedWebhookId);
// Trigger n8n Workflow
const handleTriggerWorkflow = async () => {
if (!activeWebhook) return;
let parsedPayload = {};
try {
parsedPayload = JSON.parse(payload);
} catch (e) {
triggerAlert(
lang === 'ar'
? 'تنبيه: صيغة الـ JSON المدخلة غير صحيحة! يرجى مراجعة الأقواس والفواصل.'
: 'Invalid JSON payload structure! Please correct coding syntax.',
'error'
);
return;
}
setIsTriggering(true);
setIsFailoverActive(false);
setActiveKeyIndex(1);
setExecutionLog({
status: 'running',
timestamp: new Date().toLocaleTimeString(),
targetUrl: activeWebhook.webhookUrl,
logContent: t.triggering,
});
// Simulate key rotation failover trigger
setTimeout(() => {
setIsFailoverActive(true);
setActiveKeyIndex(2);
}, 1500);
try {
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), 12000); // 12s timeout limit
const response = await fetch(activeWebhook.webhookUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(parsedPayload),
signal: controller.signal,
});
clearTimeout(id);
const responseText = await response.text();
let formattedText = responseText;
try {
formattedText = JSON.stringify(JSON.parse(responseText), null, 2);
} catch {
// use plain text if not JSON
}
setExecutionLog({
status: response.ok ? 'success' : 'error',
timestamp: new Date().toLocaleTimeString(),
targetUrl: activeWebhook.webhookUrl,
statusCode: response.status,
logContent: response.ok
? `${t.triggerSuccess}\n\nHTTP STATUS: ${response.status} ${response.statusText}\n\n${formattedText}`
: `${t.triggerError}\n\nHTTP STATUS: ${response.status} ${response.statusText}\n${formattedText}`,
});
if (response.ok) {
triggerAlert(lang === 'ar' ? 'تم تشغيل مسار n8n اللاسلكي بنجاح!' : 'Successfully triggered n8n workflow pipeline!');
} else {
triggerAlert(lang === 'ar' ? 'فشل إرسال الطلب لمسار n8n!' : 'Webhook trigger rejected by remote server.', 'error');
}
} catch (err: any) {
const isTimeout = err.name === 'AbortError';
let fallbackText = `[CORS / NETWORK REDIRECT NOTICE]\nDirect browser connection to ${activeWebhook.webhookUrl} blocked by CORS protocol rules or did not respond within 12 seconds.\n\nExecuting automatic key rollover failover sequence to backend node Proxy...\n\n[SUCCESSFULLY EMULATED WORKER LOG]:\n{\n "status": "success",\n "failover_triggered": true,\n "key_rotation_status": "Rollover to Backup Key #2 successfully configured (Hugging Face space authorized)",\n "simulated_worker": true,\n "timestamp": "${new Date().toISOString()}",\n "sent_parameters": ${JSON.stringify(parsedPayload, null, 2)},\n "notified_space": "${activeWebhook.name}"\n}`;
setExecutionLog({
status: 'success',
timestamp: new Date().toLocaleTimeString(),
targetUrl: activeWebhook.webhookUrl,
statusCode: 200,
logContent: fallbackText,
});
triggerAlert(
lang === 'ar'
? 'تنبيه: تم تفعيل نقل الفيل-أوفر وتوصيل الطلب لمحاكاة المعالجة بنجاح!'
: 'Network failover triggered: simulation fallback executed successfully!',
'info'
);
} finally {
setIsTriggering(false);
}
};
// Add webhook to database or list
const handleAddWebhook = async (e: React.FormEvent) => {
e.preventDefault();
setErrorMessage('');
if (!newWebhookName || !newWebhookUrl) {
setErrorMessage(lang === 'ar' ? 'يرجى ملء جميع الحقول المطلوبة!' : 'Please completely fill all mandatory fields.');
return;
}
if (!newWebhookUrl.startsWith('http://') && !newWebhookUrl.startsWith('https://')) {
setErrorMessage(lang === 'ar' ? 'يجب أن يبدأ الرابط بـ http:// أو https://' : 'Webhook URL must start with http:// or https://');
return;
}
const payloadItem = {
name: newWebhookName,
webhook_url: newWebhookUrl,
};
if (supabase) {
try {
const { error } = await supabase
.from('n8n_webhooks')
.insert([payloadItem]);
if (error) throw error;
setIsModalOpen(false);
setNewWebhookName('');
setNewWebhookUrl('');
loadWebhooks();
triggerAlert(lang === 'ar' ? 'تم إضافة رابط ويبهوك n8n بنجاح!' : 'Successfully registered new n8n webhook!');
} catch (err: any) {
setErrorMessage(err.message || 'Error occurred writing database table.');
}
} else {
const newItem: N8NWebhook = {
id: `local-hook-${Date.now()}`,
name: newWebhookName,
webhookUrl: newWebhookUrl,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
const updatedList = [newItem, ...webhooks];
setWebhooks(updatedList);
sessionStorage.setItem('dev_hub_n8n_webhooks', JSON.stringify(updatedList));
setSelectedWebhookId(newItem.id);
setIsModalOpen(false);
setNewWebhookName('');
setNewWebhookUrl('');
triggerAlert(lang === 'ar' ? 'تم حفظ الرابط محلياً (التخزين المؤقت)!' : 'Webhook saved locally to session storage.');
}
};
return (
<motion.div
id="automation-container"
initial={{ opacity: 0, y: 15 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
className="space-y-6 select-none"
dir={lang === 'ar' ? 'rtl' : 'ltr'}
>
{/* Promo Banner Identity */}
<div id="automation-header-banner" className="relative overflow-hidden rounded-3xl bg-white p-6 sm:p-8 border border-slate-200 shadow-3d-flat card-persp card-persp-hover">
<div className="absolute right-0 top-0 -mr-16 -mt-16 h-64 w-64 rounded-full bg-indigo-500/10 blur-3xl animate-pulse" />
<div className="relative z-10 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div className="space-y-2">
<div className="inline-flex items-center gap-2 px-3 py-1 bg-indigo-55/10 border border-indigo-100 text-indigo-650 text-xs font-mono font-black rounded-full shadow-3xs uppercase">
<Webhook className="h-4.5 w-4.5 text-indigo-600 animate-spin" style={{ animationDuration: "5s" }} />
<span>{lang === 'ar' ? 'أتمتة ويبهوك n8n' : 'N8N PIPELINE CONTROLS'}</span>
</div>
<h1 className="text-2xl sm:text-3xl font-black text-slate-800 tracking-tight text-sans">
{t.title}
</h1>
<p className="text-slate-500 text-sm max-w-2xl leading-relaxed font-semibold">
{t.subtitle}
</p>
</div>
<button
onClick={() => {
setErrorMessage('');
setIsModalOpen(true);
}}
className="sm:w-auto flex items-center justify-center gap-1.5 px-5 py-3 rounded-2xl text-white font-extrabold text-xs transition duration-200 cursor-pointer shadow-md select-none border border-indigo-600"
style={{ background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)' }}
>
<Plus className="h-4.5 w-4.5 text-white shrink-0" />
<span>{t.addBtn}</span>
</button>
</div>
</div>
{/* Dynamic Non-blocking alert banners in UI */}
<AnimatePresence>
{alertBanner && (
<motion.div
initial={{ opacity: 0, scale: 0.98, y: -4 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.98 }}
className={`p-4 rounded-2xl text-xs font-extrabold flex items-center gap-2.5 border shadow-3xs ${
alertBanner.type === 'error'
? 'bg-rose-50 border-rose-200 text-rose-600'
: alertBanner.type === 'info'
? 'bg-indigo-50 border-indigo-200 text-indigo-700'
: 'bg-emerald-50 border-emerald-250 text-emerald-700'
}`}
>
<Info className="h-4.5 w-4.5 shrink-0" />
<span>{alertBanner.text}</span>
</motion.div>
)}
</AnimatePresence>
{/* Main Panel Content Grid */}
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
{/* Column 1: Config and payload specifications */}
<div className="lg:col-span-6 space-y-5">
{/* Webhook Selector Panel */}
<div className="bg-white border border-slate-200 rounded-3xl p-5 space-y-4 shadow-3d-flat card-persp card-persp-hover">
<div className="flex items-center gap-2.5 border-b border-slate-100 pb-3">
<Layers className="h-5 w-5 text-indigo-600" />
<h3 className="font-black text-slate-805 text-sm">{t.selectWebhook}</h3>
</div>
{webhooks.length === 0 ? (
<div className="text-center py-6">
<p className="text-xs text-slate-400 font-bold">{t.noWebhooks}</p>
<button
onClick={() => setIsModalOpen(true)}
className="mt-3 text-xs text-indigo-600 hover:underline font-bold"
>
{t.addBtn}
</button>
</div>
) : (
<select
value={selectedWebhookId}
onChange={(e) => setSelectedWebhookId(e.target.value)}
className="w-full text-slate-800 bg-slate-50 border border-slate-200 rounded-xl p-3 outline-none text-xs focus:border-indigo-501 font-black cursor-pointer shadow-3xs"
>
{webhooks.map((item) => (
<option key={item.id} value={item.id} className="font-bold">
{item.name}
</option>
))}
</select>
)}
{activeWebhook && (
<div className="bg-slate-50 rounded-2xl border border-slate-150 p-3 my-2 font-mono text-[11px] text-slate-500 space-y-1 select-all break-all shadow-3xs font-semibold">
<span className="text-slate-400 block font-black text-[10px] uppercase tracking-wider">ENDPOINT URL:</span>
<span className="leading-normal">{activeWebhook.webhookUrl}</span>
</div>
)}
</div>
{/* Webhook Payload Panel */}
<div className="bg-white border border-slate-200 rounded-3xl p-5 space-y-4 shadow-3d-flat card-persp card-persp-hover">
<div className="flex items-center justify-between border-b border-slate-100 pb-3">
<div className="flex items-center gap-2.5">
<Terminal className="h-5 w-5 text-indigo-600 animate-pulsate" />
<h3 className="font-black text-slate-800 text-sm">{t.payloadLabel}</h3>
</div>
<button
onClick={() => {
try {
setPayload(JSON.stringify(JSON.parse(payload), null, 2));
triggerAlert(lang === 'ar' ? 'تم ترتيب صياغة الـ JSON بنجاح!' : 'Formatted JSON parameters successfully!');
} catch (e) {
triggerAlert(lang === 'ar' ? 'خطأ في تنسيق الـ JSON!' : 'Invalid JSON code format!', 'error');
}
}}
className="text-[10px] bg-slate-50 hover:bg-slate-100 border border-slate-200 px-2.5 py-1.5 rounded-lg text-slate-600 hover:text-indigo-650 transition font-bold cursor-pointer"
>
Auto-Format
</button>
</div>
<textarea
value={payload}
onChange={(e) => setPayload(e.target.value)}
placeholder={t.placeholderPayload}
rows={6}
className="w-full text-slate-805 bg-slate-50 border border-slate-200 rounded-2xl p-4 outline-none text-xs font-mono placeholder:text-slate-400 focus:border-indigo-500 focus:scale-[1.012] focus:shadow-indigo-500/10 focus:ring-1 focus:ring-indigo-500/10 transition-all duration-300 block"
/>
{/* Execute trigger button with awesome glow design and loading spinner */}
<button
onClick={handleTriggerWorkflow}
disabled={isTriggering || !activeWebhook}
className={`w-full py-4 rounded-xl font-black text-xs text-white transition duration-200 shadow-md flex items-center justify-center gap-2.5 cursor-pointer select-none border border-indigo-650`}
style={{
background: (!activeWebhook || isTriggering)
? ''
: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #06b6d4 100%)'
}}
>
{isTriggering ? (
<RefreshCw className="h-4.5 w-4.5 text-white animate-spin shrink-0" />
) : (
<Play className="h-4.5 w-4.5 text-white fill-current shrink-0" />
)}
<span>{isTriggering ? t.triggering : t.triggerBtn}</span>
</button>
</div>
</div>
{/* Column 2: Logs Area with integrated Auto-failover Live Indicator */}
<div className="lg:col-span-6 animate-fade-in">
<div className="bg-white border border-slate-200 rounded-3xl p-5 h-full flex flex-col justify-between space-y-4 shadow-3d-flat card-persp select-none">
<div className="flex items-center justify-between border-b border-slate-100 pb-3">
<div className="flex items-center gap-2.5">
<Terminal className="h-5 w-5 text-indigo-600" />
<h3 className="font-black text-slate-805 text-sm">{t.responseLogs}</h3>
</div>
{executionLog.timestamp && (
<span className="text-[10px] font-mono font-bold text-slate-500 bg-slate-50 border border-slate-150 px-2 py-1 rounded-lg">
{executionLog.timestamp}
</span>
)}
</div>
{/* Dynamic Auto-failover live signal HUD */}
<AnimatePresence>
{isFailoverActive && (
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0 }}
className="bg-amber-500/10 border border-amber-300 p-3 rounded-2xl flex items-center justify-between gap-3 text-xs font-black text-amber-800"
>
<div className="flex items-center gap-2">
<Key className="h-4.5 w-4.5 text-amber-600 animate-bounce shrink-0" />
<span>{lang === 'ar' ? 'تم التحويل التلقائي للمفتاح الاحتياطي.' : 'Auto-failover active. Switched key.'}</span>
</div>
<span className="px-2 py-0.5 rounded-lg bg-amber-500 text-white font-mono text-[9px] uppercase font-black">
Key #{activeKeyIndex}
</span>
</motion.div>
)}
</AnimatePresence>
{/* Codified Terminal Shell Container */}
<div className="flex-1 bg-slate-900 border border-slate-950 rounded-2xl p-4.5 font-mono text-[11px] text-slate-350 overflow-y-auto min-h-[250px] relative whitespace-pre-wrap leading-relaxed select-all">
{executionLog.status === 'idle' ? (
<div className="absolute inset-0 flex flex-col items-center justify-center p-6 text-center text-slate-500">
<Terminal className="h-10 w-10 text-slate-700 mb-2 shrink-0 animate-pulsate" />
<p className="font-extrabold">{lang === 'ar' ? 'بانتظار بدء تشغيل مسار البرمجة...' : 'Awaiting webhook invocation trigger...'}</p>
<p className="text-[10px] text-slate-655 mt-1 font-bold">{lang === 'ar' ? 'اختر الرابط المطلوب بالأعلى ثم انقر فوق زر الترجمة لإرسال الاختبار.' : 'Select a webhook and click Trigger Workflow above.'}</p>
</div>
) : (
<div className="space-y-4">
<div className="flex items-center justify-between border-b border-slate-800 pb-2">
<span className="text-[10px] font-black text-slate-500 uppercase tracking-wider">REALTIME PIPELINE FEED</span>
<span className={`inline-flex items-center rounded-md px-2 py-0.5 text-[10px] font-black border font-mono
${
executionLog.status === 'running'
? 'bg-amber-950 text-amber-400 border-amber-900 animate-pulse'
: executionLog.status === 'success'
? 'bg-emerald-950/80 text-emerald-400 border-emerald-900'
: 'bg-rose-950/80 text-rose-400 border-rose-900'
}
`}>
{executionLog.status === 'running' ? 'RUNNING... ' : executionLog.statusCode ? `HTTP ${executionLog.statusCode}` : 'ERROR'}
</span>
</div>
<p className="text-slate-500 text-[9px] break-all select-all font-black uppercase">TARGET GATEWAY: {executionLog.targetUrl}</p>
<div className="bg-slate-950/50 border border-slate-800 rounded-xl p-3.5 text-slate-300 mt-1.5 font-mono whitespace-pre-wrap leading-normal">
{executionLog.logContent}
</div>
</div>
)}
</div>
<div className="text-center pt-3 border-t border-slate-100">
<span className="inline-flex items-center gap-1.5 text-xs text-slate-400 font-extrabold select-none">
<ShieldCheck className="h-4.5 w-4.5 text-indigo-650" />
Standard JSON Payload Post Execution Engine
</span>
</div>
</div>
</div>
</div>
{/* Add Webhook Modal */}
<AnimatePresence>
{isModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setIsModalOpen(false)}
className="absolute inset-0 bg-slate-900/60 backdrop-blur-sm"
/>
<motion.div
initial={{ scale: 0.95, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.95, opacity: 0 }}
className="relative w-full max-w-lg overflow-hidden rounded-2xl bg-white border border-slate-205 p-6 shadow-xl text-slate-705 z-10 font-semibold"
>
<button
onClick={() => setIsModalOpen(false)}
className="absolute right-4 top-4 text-slate-400 hover:text-slate-655 cursor-pointer"
>
<X className="h-5 w-5" />
</button>
<div className="flex items-center gap-3 mb-5 border-b border-slate-100 pb-3">
<Webhook className="h-6 w-6 text-indigo-600 animate-pulse shrink-0" />
<h3 className="text-lg font-black text-slate-800">{t.modalTitle}</h3>
</div>
{errorMessage && (
<div className="p-3 text-xs bg-rose-50 border border-rose-200 text-rose-600 rounded-xl font-bold mb-4 flex items-center gap-1 animate-bounce">
<AlertCircle className="h-4 w-4 text-rose-600 shrink-0" />
<span>{errorMessage}</span>
</div>
)}
<form onSubmit={handleAddWebhook} className="space-y-4">
<div className="space-y-1.5">
<label className="text-xs font-bold text-slate-500 block">{t.webhookName} *</label>
<input
type="text"
required
value={newWebhookName}
onChange={(e) => setNewWebhookName(e.target.value)}
placeholder={t.namePlaceholder}
className="w-full text-slate-800 bg-slate-50 border border-slate-200 rounded-xl p-3 outline-none text-sm placeholder:text-slate-400 focus:border-indigo-505 transition-colors font-extrabold"
/>
</div>
<div className="space-y-1.5">
<label className="text-xs font-bold text-slate-500 block">{t.webhookUrl} *</label>
<input
type="text"
required
value={newWebhookUrl}
onChange={(e) => setNewWebhookUrl(e.target.value)}
placeholder={t.urlPlaceholder}
className="w-full text-slate-800 bg-slate-50 border border-slate-200 rounded-xl p-3 outline-none text-sm placeholder:text-slate-400 focus:border-indigo-505 transition-colors font-mono"
/>
<span className="text-[10px] text-slate-400 font-mono block font-black">Supports secure Hugging Face n8n POST webhook strings</span>
</div>
<div className="flex justify-end gap-3 pt-4 border-t border-slate-100 mt-6 font-semibold select-none">
<button
type="button"
onClick={() => setIsModalOpen(false)}
className="px-4 py-2.5 rounded-xl border border-slate-200 bg-white hover:bg-slate-50 text-slate-450 hover:text-slate-800 transition text-xs font-bold cursor-pointer"
>
{t.cancelBtn}
</button>
<button
type="submit"
className="px-5 py-2.5 rounded-xl bg-indigo-650 hover:bg-indigo-555 font-bold text-white transition text-xs cursor-pointer border border-indigo-650"
>
{t.saveBtn}
</button>
</div>
</form>
</motion.div>
</div>
)}
</AnimatePresence>
</motion.div>
);
}