PluginProbe
WPBot – AI ChatBot for Live Support, Lead Generation, WordPress Automation, AI Services / 8.8.1
WPBot – AI ChatBot for Live Support, Lead Generation, WordPress Automation, AI Services v8.8.1
8.8.1 8.8.0 8.7.9 8.7.8 8.7.7 8.7.6 8.7.5 8.7.4 8.7.3 8.7.2 8.7.1 8.7.0 8.6.9 8.6.8 8.6.7 8.6.6 8.6.5 8.6.4 8.6.2 8.6.1 8.6.0 8.5.9 8.5.8 8.5.7 8.5.6 All 537 releases
chatbot / addons / automator / src / ActivityLogsApp.jsx

ActivityLogsApp.jsx in WPBot – AI ChatBot for Live Support, Lead Generation, WordPress Automation, AI Services 8.8.1, at addons/automator/src/ActivityLogsApp.jsx

424 lines 19.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import React, { useState, useEffect, useRef } from 'react';
2 import {
3 RefreshCw, Trash2, CheckCircle, XCircle, Clock,
4 Filter, ChevronDown, ChevronRight, Activity
5 } from 'lucide-react';
6
7 const PER_PAGE = 25;
8
9 const ActivityLogsApp = () => {
10 const [logs, setLogs] = useState([]);
11 const [summary, setSummary] = useState({ total: 0, success: 0, failed: 0, last_run: null });
12 const [workflows, setWorkflows] = useState([]);
13 const [loading, setLoading] = useState(true);
14 const [filters, setFilters] = useState({ status: '', workflow_id: '' });
15 const [page, setPage] = useState(1);
16 const [totalPages, setTotalPages] = useState(1);
17 const [total, setTotal] = useState(0);
18 const [expandedRow, setExpandedRow] = useState(null);
19 const [autoRefresh, setAutoRefresh] = useState(false);
20
21 const pageRef = useRef(page);
22 const filtersRef = useRef(filters);
23 const intervalRef = useRef(null);
24 useEffect(() => { pageRef.current = page; }, [page]);
25 useEffect(() => { filtersRef.current = filters; }, [filters]);
26
27 const apiUrl = () => window.wpbotAutomator.apiUrl.replace(/\/$/, '');
28 const headers = () => ({ 'X-WP-Nonce': window.wpbotAutomator.nonce });
29
30 const fetchSummary = async () => {
31 try {
32 const res = await fetch(`${apiUrl()}/logs/summary`, { headers: headers() });
33 const data = await res.json();
34 setSummary(data);
35 } catch (e) { console.error(e); }
36 };
37
38 const fetchWorkflows = async () => {
39 try {
40 const res = await fetch(`${apiUrl()}/workflows`, { headers: headers() });
41 const data = await res.json();
42 setWorkflows(Array.isArray(data) ? data : []);
43 } catch (e) { console.error(e); }
44 };
45
46 const fetchLogs = async (currentPage, currentFilters) => {
47 setLoading(true);
48 try {
49 const params = new URLSearchParams({ per_page: PER_PAGE, page: currentPage });
50 if (currentFilters.status) params.set('status', currentFilters.status);
51 if (currentFilters.workflow_id) params.set('workflow_id', currentFilters.workflow_id);
52
53 const res = await fetch(`${apiUrl()}/logs?${params}`, { headers: headers() });
54 const data = await res.json();
55 setLogs(data.logs || []);
56 setTotal(data.total || 0);
57 setTotalPages(data.total_pages || 1);
58 } catch (e) {
59 console.error(e);
60 } finally {
61 setLoading(false);
62 }
63 };
64
65 useEffect(() => {
66 fetchWorkflows();
67 fetchSummary();
68 fetchLogs(1, { status: '', workflow_id: '' });
69 }, []);
70
71 useEffect(() => {
72 clearInterval(intervalRef.current);
73 if (!autoRefresh) return;
74 intervalRef.current = setInterval(() => {
75 fetchSummary();
76 fetchLogs(pageRef.current, filtersRef.current);
77 }, 10000);
78 return () => clearInterval(intervalRef.current);
79 }, [autoRefresh]);
80
81 const applyFilter = (key, value) => {
82 const next = { ...filtersRef.current, [key]: value };
83 setFilters(next);
84 setPage(1);
85 fetchSummary();
86 fetchLogs(1, next);
87 };
88
89 const clearFilters = () => {
90 const next = { status: '', workflow_id: '' };
91 setFilters(next);
92 setPage(1);
93 fetchLogs(1, next);
94 };
95
96 const goToPage = (p) => {
97 setPage(p);
98 fetchLogs(p, filtersRef.current);
99 };
100
101 const handleRefresh = () => {
102 fetchSummary();
103 fetchLogs(pageRef.current, filtersRef.current);
104 };
105
106 const handleClearLogs = async () => {
107 if (!window.confirm('Clear all logs? This cannot be undone.')) return;
108 await fetch(`${apiUrl()}/logs`, { method: 'DELETE', headers: headers() });
109 setSummary({ total: 0, success: 0, failed: 0, last_run: null });
110 setLogs([]);
111 setTotal(0);
112 setTotalPages(1);
113 setPage(1);
114 };
115
116 const formatRelative = (datetime) => {
117 if (!datetime) return '-';
118 const d = new Date(datetime.replace(' ', 'T') + 'Z');
119 const diff = Math.floor((Date.now() - d) / 1000);
120 if (diff < 60) return `${diff}s ago`;
121 if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
122 if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
123 return d.toLocaleDateString();
124 };
125
126 const formatAbsolute = (datetime) => {
127 if (!datetime) return '';
128 return new Date(datetime.replace(' ', 'T') + 'Z').toLocaleString();
129 };
130
131 const humanize = (str) => {
132 if (!str) return '-';
133 return str.replace(/[_-]/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
134 };
135
136 const formatTrigger = (trigger) => {
137 if (!trigger) return '-';
138 const parts = trigger.split(':');
139 return humanize(parts[parts.length - 1]);
140 };
141
142 const parseLogData = (raw) => {
143 if (!raw) return null;
144 try { return JSON.parse(raw); } catch { return raw; }
145 };
146
147 const successRate = summary.total > 0
148 ? Math.round((summary.success / summary.total) * 100)
149 : null;
150
151 const hasFilters = filters.status || filters.workflow_id;
152
153 return (
154 <div className="p-8 bg-background min-h-screen text-foreground font-sans">
155 <div className="max-w-7xl mx-auto">
156
157 {/* Header */}
158 <div className="flex items-center justify-between mb-8">
159 <div>
160 <h1 className="text-3xl font-bold tracking-tight flex items-center gap-3">
161 <Activity className="w-8 h-8 text-primary" />
162 Activity Log
163 </h1>
164 <p className="text-muted-foreground mt-1">Real-time view of every workflow execution — what ran, what succeeded, and what failed.</p>
165 </div>
166 <div className="flex items-center gap-3">
167 {/* Auto-refresh toggle */}
168 <label className="flex items-center gap-2 text-sm text-muted-foreground cursor-pointer select-none">
169 <div
170 onClick={() => setAutoRefresh(v => !v)}
171 className={`w-9 h-5 rounded-full relative cursor-pointer transition-colors ${autoRefresh ? 'bg-primary' : 'bg-border'}`}
172 >
173 <div className={`absolute top-0.5 w-4 h-4 bg-white rounded-full shadow transition-transform ${autoRefresh ? 'translate-x-4' : 'translate-x-0.5'}`} />
174 </div>
175 Auto-refresh
176 </label>
177 <button
178 onClick={handleRefresh}
179 disabled={loading}
180 className="flex items-center gap-2 px-3 py-2 border border-border rounded-lg hover:bg-muted transition-colors text-sm"
181 >
182 <RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
183 Refresh
184 </button>
185 <button
186 onClick={handleClearLogs}
187 className="flex items-center gap-2 px-3 py-2 border border-rose-200 text-rose-600 rounded-lg hover:bg-rose-50 transition-colors text-sm"
188 >
189 <Trash2 className="w-4 h-4" />
190 Clear All
191 </button>
192 </div>
193 </div>
194
195 {/* Stats */}
196 <div className="grid grid-cols-3 gap-4 mb-6">
197 <div className="bg-card border border-border rounded-xl p-5">
198 <div className="text-sm text-muted-foreground mb-1">Total Runs</div>
199 <div className="text-3xl font-bold">{summary.total.toLocaleString()}</div>
200 <div className="text-xs text-muted-foreground mt-2">
201 {summary.last_run ? `Last run: ${formatRelative(summary.last_run)}` : 'No runs yet'}
202 </div>
203 </div>
204 <div className="bg-card border border-emerald-200 rounded-xl p-5">
205 <div className="text-sm text-emerald-600 mb-1 font-medium">Successful</div>
206 <div className="text-3xl font-bold text-emerald-600">{summary.success.toLocaleString()}</div>
207 <div className="text-xs text-muted-foreground mt-2">
208 {successRate !== null ? `${successRate}% success rate` : 'No data yet'}
209 </div>
210 </div>
211 <div className="bg-card border border-rose-200 rounded-xl p-5">
212 <div className="text-sm text-rose-600 mb-1 font-medium">Failed</div>
213 <div className="text-3xl font-bold text-rose-600">{summary.failed.toLocaleString()}</div>
214 <div className="text-xs text-muted-foreground mt-2">
215 {successRate !== null ? `${100 - successRate}% failure rate` : 'No data yet'}
216 </div>
217 </div>
218 </div>
219
220 {/* Filters */}
221 <div className="flex items-center gap-3 mb-4 flex-wrap">
222 <Filter className="w-4 h-4 text-muted-foreground shrink-0" />
223 <select
224 value={filters.status}
225 onChange={e => applyFilter('status', e.target.value)}
226 className="border border-border rounded-lg px-3 py-1.5 text-sm bg-background focus:outline-none focus:ring-2 focus:ring-primary/20"
227 >
228 <option value="">All Statuses</option>
229 <option value="success">Success</option>
230 <option value="failed">Failed</option>
231 </select>
232 <select
233 value={filters.workflow_id}
234 onChange={e => applyFilter('workflow_id', e.target.value)}
235 className="border border-border rounded-lg px-3 py-1.5 text-sm bg-background focus:outline-none focus:ring-2 focus:ring-primary/20"
236 >
237 <option value="">All Workflows</option>
238 {workflows.map(w => (
239 <option key={w.id} value={w.id}>{w.name}</option>
240 ))}
241 </select>
242 {hasFilters && (
243 <button
244 onClick={clearFilters}
245 className="text-xs text-primary hover:underline"
246 >
247 Clear filters
248 </button>
249 )}
250 <span className="ml-auto text-sm text-muted-foreground">
251 {total.toLocaleString()} {total === 1 ? 'entry' : 'entries'}
252 </span>
253 </div>
254
255 {/* Table */}
256 <div className="bg-card border border-border rounded-xl shadow-sm overflow-hidden">
257 {loading && logs.length === 0 ? (
258 <div className="py-24 text-center text-muted-foreground">
259 <RefreshCw className="w-8 h-8 mx-auto mb-3 animate-spin opacity-40" />
260 Loading logs…
261 </div>
262 ) : logs.length === 0 ? (
263 <div className="py-24 text-center">
264 <Clock className="w-12 h-12 mx-auto mb-4 text-muted-foreground opacity-25" />
265 <p className="font-medium text-muted-foreground">No logs found{hasFilters ? ' for this filter' : ''}</p>
266 <p className="text-xs text-muted-foreground mt-1">
267 {hasFilters ? 'Try clearing the filters.' : 'Logs appear here automatically when your workflows run.'}
268 </p>
269 </div>
270 ) : (
271 <>
272 <table className="w-full text-sm">
273 <thead className="bg-muted/40 border-b border-border">
274 <tr>
275 <th className="w-6 px-3 py-3" />
276 <th className="px-4 py-3 text-left text-xs font-semibold text-muted-foreground uppercase tracking-wide">Time</th>
277 <th className="px-4 py-3 text-left text-xs font-semibold text-muted-foreground uppercase tracking-wide">Workflow</th>
278 <th className="px-4 py-3 text-left text-xs font-semibold text-muted-foreground uppercase tracking-wide">Trigger</th>
279 <th className="px-4 py-3 text-left text-xs font-semibold text-muted-foreground uppercase tracking-wide">Action</th>
280 <th className="px-4 py-3 text-left text-xs font-semibold text-muted-foreground uppercase tracking-wide">Status</th>
281 <th className="px-4 py-3 text-left text-xs font-semibold text-muted-foreground uppercase tracking-wide">Message</th>
282 </tr>
283 </thead>
284 <tbody className="divide-y divide-border">
285 {logs.map((log) => {
286 const expanded = expandedRow === log.id;
287 const parsed = parseLogData(log.log_data);
288 const isFailed = log.status !== 'success';
289
290 return (
291 <React.Fragment key={log.id}>
292 <tr
293 onClick={() => setExpandedRow(expanded ? null : log.id)}
294 className={`cursor-pointer transition-colors hover:bg-muted/20 ${expanded ? 'bg-muted/10' : ''} ${isFailed ? 'border-l-2 border-l-rose-400' : ''}`}
295 >
296 <td className="px-3 py-3 text-muted-foreground">
297 {expanded
298 ? <ChevronDown className="w-3.5 h-3.5" />
299 : <ChevronRight className="w-3.5 h-3.5" />}
300 </td>
301 <td className="px-4 py-3">
302 <span
303 className="text-xs font-mono text-muted-foreground"
304 title={formatAbsolute(log.created_at)}
305 >
306 {formatRelative(log.created_at)}
307 </span>
308 </td>
309 <td className="px-4 py-3 font-medium max-w-[160px] truncate">
310 {log.workflow_name || `Workflow #${log.workflow_id}`}
311 </td>
312 <td className="px-4 py-3">
313 <span className="bg-muted/70 text-muted-foreground px-2 py-0.5 rounded text-xs font-mono">
314 {formatTrigger(log.trigger_type)}
315 </span>
316 </td>
317 <td className="px-4 py-3">
318 {log.action_id ? (
319 <span className="bg-blue-50 text-blue-700 border border-blue-100 px-2 py-0.5 rounded text-xs">
320 {humanize(log.action_id)}
321 </span>
322 ) : (
323 <span className="text-muted-foreground text-xs">—</span>
324 )}
325 </td>
326 <td className="px-4 py-3">
327 {isFailed ? (
328 <span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-rose-100 text-rose-700">
329 <XCircle className="w-3 h-3" /> Failed
330 </span>
331 ) : (
332 <span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-emerald-100 text-emerald-700">
333 <CheckCircle className="w-3 h-3" /> Success
334 </span>
335 )}
336 </td>
337 <td className="px-4 py-3 text-muted-foreground text-xs max-w-[220px] truncate">
338 {log.error_message || (parsed && parsed.message) || '—'}
339 </td>
340 </tr>
341
342 {expanded && (
343 <tr className="bg-muted/5">
344 <td colSpan="7" className="px-6 py-4">
345 <div className="space-y-3 max-w-4xl">
346 {/* Meta row */}
347 <div className="flex flex-wrap gap-x-6 gap-y-1 text-xs text-muted-foreground">
348 <span><strong className="text-foreground">Log ID:</strong> {log.id}</span>
349 <span><strong className="text-foreground">Time:</strong> {formatAbsolute(log.created_at)}</span>
350 <span><strong className="text-foreground">Workflow:</strong> {log.workflow_name || `#${log.workflow_id}`}</span>
351 <span><strong className="text-foreground">Trigger:</strong> {log.trigger_type || '—'}</span>
352 {log.action_id && <span><strong className="text-foreground">Action:</strong> {log.action_id}</span>}
353 </div>
354
355 {/* Error box */}
356 {log.error_message && (
357 <div className="bg-rose-50 border border-rose-200 rounded-lg p-3">
358 <div className="text-xs font-bold text-rose-700 mb-1">Error</div>
359 <div className="text-xs text-rose-600 font-mono break-all">{log.error_message}</div>
360 </div>
361 )}
362
363 {/* Result data */}
364 {parsed && (
365 <div className="bg-muted/50 border border-border rounded-lg p-3">
366 <div className="text-xs font-bold text-muted-foreground mb-2">Result Data</div>
367 <pre className="text-xs font-mono text-foreground/80 overflow-auto max-h-48 whitespace-pre-wrap break-all">
368 {typeof parsed === 'object'
369 ? JSON.stringify(parsed, null, 2)
370 : parsed}
371 </pre>
372 </div>
373 )}
374 </div>
375 </td>
376 </tr>
377 )}
378 </React.Fragment>
379 );
380 })}
381 </tbody>
382 </table>
383
384 {/* Pagination */}
385 {totalPages > 1 && (
386 <div className="flex items-center justify-between px-4 py-3 border-t border-border">
387 <div className="text-xs text-muted-foreground">
388 Page {page} of {totalPages} &mdash; {total.toLocaleString()} total entries
389 </div>
390 <div className="flex items-center gap-1">
391 <button onClick={() => goToPage(1)} disabled={page === 1}
392 className="px-2 py-1 text-xs border border-border rounded hover:bg-muted disabled:opacity-40 disabled:cursor-not-allowed">«</button>
393 <button onClick={() => goToPage(page - 1)} disabled={page === 1}
394 className="px-2 py-1 text-xs border border-border rounded hover:bg-muted disabled:opacity-40 disabled:cursor-not-allowed">‹</button>
395 {Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
396 const start = Math.max(1, Math.min(page - 2, totalPages - 4));
397 const p = start + i;
398 return p <= totalPages ? (
399 <button key={p} onClick={() => goToPage(p)}
400 className={`px-2.5 py-1 text-xs border rounded transition-colors ${
401 p === page
402 ? 'bg-primary text-primary-foreground border-primary'
403 : 'border-border hover:bg-muted'
404 }`}
405 >{p}</button>
406 ) : null;
407 })}
408 <button onClick={() => goToPage(page + 1)} disabled={page === totalPages}
409 className="px-2 py-1 text-xs border border-border rounded hover:bg-muted disabled:opacity-40 disabled:cursor-not-allowed">›</button>
410 <button onClick={() => goToPage(totalPages)} disabled={page === totalPages}
411 className="px-2 py-1 text-xs border border-border rounded hover:bg-muted disabled:opacity-40 disabled:cursor-not-allowed">»</button>
412 </div>
413 </div>
414 )}
415 </>
416 )}
417 </div>
418 </div>
419 </div>
420 );
421 };
422
423 export default ActivityLogsApp;
424