Add navigation functionality and refactor layout structure
Introduced a sidebar and header component to improve layout organization and UI consistency. Added state management for navigating between pages (dashboard, scraper, schedules, logs) with placeholder content for future implementations.
This commit is contained in:
parent
f069b87dbf
commit
90b427c5b7
524
src/App.js
524
src/App.js
@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { Database, Globe, Download, Play, Pause, Trash2, Plus, RefreshCw, Activity, Clock, CircleX, CircleCheck } from 'lucide-react';
|
import { Database, Globe, Download, Play, Pause, Trash2, Plus, RefreshCw, Activity, Clock, CircleX, CircleCheck, Grid, Calendar, Terminal } from 'lucide-react';
|
||||||
import * as signalR from '@microsoft/signalr';
|
import * as signalR from '@microsoft/signalr';
|
||||||
|
|
||||||
const API_BASE = 'http://localhost:5123';
|
const API_BASE = 'http://localhost:5123';
|
||||||
@ -13,6 +13,9 @@ const WebScraper = () => {
|
|||||||
const [logs, setLogs] = useState([]);
|
const [logs, setLogs] = useState([]);
|
||||||
const [isConnected, setIsConnected] = useState(false);
|
const [isConnected, setIsConnected] = useState(false);
|
||||||
|
|
||||||
|
// Estado de navegação simples para futuras páginas
|
||||||
|
const [activePage, setActivePage] = useState('scraper'); // 'dashboard' | 'scraper' | 'schedules' | 'logs'
|
||||||
|
|
||||||
const connectionRef = useRef(null);
|
const connectionRef = useRef(null);
|
||||||
|
|
||||||
const addLog = (message, type = 'info') => {
|
const addLog = (message, type = 'info') => {
|
||||||
@ -71,10 +74,7 @@ const WebScraper = () => {
|
|||||||
addLog('Conectado ao servidor', 'success');
|
addLog('Conectado ao servidor', 'success');
|
||||||
connectionRef.current = connection;
|
connectionRef.current = connection;
|
||||||
|
|
||||||
// ✅ NOVO: assinar overview
|
|
||||||
await connection.invoke('SubscribeOverview');
|
await connection.invoke('SubscribeOverview');
|
||||||
|
|
||||||
// pega snapshot inicial uma vez
|
|
||||||
await loadSessions();
|
await loadSessions();
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
@ -111,7 +111,7 @@ const WebScraper = () => {
|
|||||||
const upsertSessionFromEvent = (sid, patch) => {
|
const upsertSessionFromEvent = (sid, patch) => {
|
||||||
setSessions(prev => {
|
setSessions(prev => {
|
||||||
const idx = prev.findIndex(s => s.sessionId === sid);
|
const idx = prev.findIndex(s => s.sessionId === sid);
|
||||||
if (idx === -1) return prev; // ou: return [...prev, {sessionId:sid, name:'', ...patch}]
|
if (idx === -1) return prev;
|
||||||
const copy = [...prev];
|
const copy = [...prev];
|
||||||
copy[idx] = { ...copy[idx], ...patch };
|
copy[idx] = { ...copy[idx], ...patch };
|
||||||
return copy;
|
return copy;
|
||||||
@ -150,7 +150,6 @@ const WebScraper = () => {
|
|||||||
case 'ItemStarted':
|
case 'ItemStarted':
|
||||||
addLog(`📄 Processando: ${event.url}`, 'info');
|
addLog(`📄 Processando: ${event.url}`, 'info');
|
||||||
|
|
||||||
// Atualiza runtime do painel se essa for a sessão selecionada
|
|
||||||
setSessionStatus(prev => {
|
setSessionStatus(prev => {
|
||||||
if (!prev || prev.runtime?.sessionId !== sid) return prev;
|
if (!prev || prev.runtime?.sessionId !== sid) return prev;
|
||||||
|
|
||||||
@ -186,7 +185,6 @@ const WebScraper = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// também atualiza painel se for a sessão selecionada
|
|
||||||
if (selectedSession?.sessionId === event.sessionId) {
|
if (selectedSession?.sessionId === event.sessionId) {
|
||||||
setSessionStatus(prev => ({
|
setSessionStatus(prev => ({
|
||||||
...(prev ?? {}),
|
...(prev ?? {}),
|
||||||
@ -307,7 +305,7 @@ const WebScraper = () => {
|
|||||||
setSelectedSessionId(session.sessionId);
|
setSelectedSessionId(session.sessionId);
|
||||||
setLogs([]);
|
setLogs([]);
|
||||||
addLog(`Sessão selecionada: ${session.name}`, 'info');
|
addLog(`Sessão selecionada: ${session.name}`, 'info');
|
||||||
loadSessionStatus(session.sessionId); // snapshot inicial
|
loadSessionStatus(session.sessionId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatNumber = (num) => {
|
const formatNumber = (num) => {
|
||||||
@ -342,7 +340,7 @@ const WebScraper = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSessionAction = (e, session) => {
|
const handleSessionAction = (e, session) => {
|
||||||
e.stopPropagation(); // Evita selecionar a sessão ao clicar no botão
|
e.stopPropagation();
|
||||||
if (session.isRunning) {
|
if (session.isRunning) {
|
||||||
stopScrapingById(session.sessionId, session.name);
|
stopScrapingById(session.sessionId, session.name);
|
||||||
} else {
|
} else {
|
||||||
@ -350,231 +348,303 @@ const WebScraper = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
// Componente Header
|
||||||
<div className="min-h-screen bg-gradient-to-br from-slate-900 to-slate-800 text-white p-6">
|
const Header = () => (
|
||||||
<div className="max-w-7xl mx-auto">
|
<header className="h-16 shrink-0 bg-slate-900/80 backdrop-blur border-b border-slate-700 flex items-center justify-between px-6 fixed top-0 left-0 right-0 z-20">
|
||||||
{/* Header */}
|
<div className="flex items-center gap-3">
|
||||||
<div className="mb-8 flex justify-between items-center">
|
<Globe className="text-blue-400" />
|
||||||
<div>
|
<h1 className="text-lg font-bold tracking-tight text-white">Web Scraper Pro</h1>
|
||||||
<h1 className="text-4xl font-bold mb-2 flex items-center gap-3">
|
</div>
|
||||||
<Globe className="text-blue-400" />
|
<div className="flex items-center gap-3">
|
||||||
Web Scraper Pro
|
<div className={`flex items-center gap-2 px-3.5 py-1.5 rounded border text-sm font-medium ${isConnected ? 'bg-green-900/20 border-green-800 text-green-400' : 'bg-red-900/20 border-red-800 text-red-400'}`}>
|
||||||
</h1>
|
<Activity size={16} />
|
||||||
<p className="text-slate-400">Gerenciador de scraping com API backend</p>
|
{isConnected ? 'Conectado' : 'Desconectado'}
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className={`flex items-center gap-2 px-4 py-2 rounded-lg ${isConnected ? 'bg-green-900/30 text-green-400' : 'bg-red-900/30 text-red-400'}`}>
|
|
||||||
<Activity size={16} />
|
|
||||||
{isConnected ? 'Conectado' : 'Desconectado'}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={loadSessions}
|
|
||||||
className="flex items-center gap-2 px-4 py-2 bg-slate-700 hover:bg-slate-600 rounded-lg transition"
|
|
||||||
>
|
|
||||||
<RefreshCw size={16} />
|
|
||||||
Atualizar
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={loadSessions}
|
||||||
|
className="flex items-center gap-2 px-3.5 py-1.5 bg-slate-800 border border-slate-700 hover:border-blue-500/50 text-slate-300 hover:text-white rounded transition-colors text-sm"
|
||||||
|
>
|
||||||
|
<RefreshCw size={16} />
|
||||||
|
Atualizar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
|
||||||
<div className="grid grid-cols-3 gap-6">
|
// Componente Sidebar
|
||||||
{/* Lista de Sessões */}
|
const Sidebar = () => (
|
||||||
<div className="bg-slate-800 rounded-lg p-6 shadow-xl">
|
<aside className="w-64 bg-slate-900/60 backdrop-blur border-r border-slate-700 flex flex-col shrink-0 fixed top-16 bottom-0 left-0 z-10">
|
||||||
<h2 className="text-xl font-bold mb-4 flex items-center gap-2">
|
<div className="p-4">
|
||||||
<Database size={20} />
|
<p className="px-2 text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2">Gerenciamento</p>
|
||||||
Sessões
|
<nav className="space-y-1">
|
||||||
</h2>
|
<button
|
||||||
<div className="space-y-2">
|
onClick={() => setActivePage('dashboard')}
|
||||||
{sessions.map((session) => (
|
className={`w-full text-left flex items-center gap-3 px-3 py-2 rounded-lg transition-colors ${activePage === 'dashboard' ? 'bg-blue-500/10 text-blue-400 border border-blue-500/20' : 'text-slate-400 hover:bg-slate-800 hover:text-white'}`}
|
||||||
<div
|
>
|
||||||
key={session.sessionId}
|
<Grid size={18} />
|
||||||
onClick={() => selectSession(session)}
|
<span className="text-sm font-medium">Dashboard</span>
|
||||||
className={`p-4 rounded-lg cursor-pointer transition ${selectedSession?.sessionId === session.sessionId
|
</button>
|
||||||
? 'bg-blue-600'
|
<button
|
||||||
: 'bg-slate-700 hover:bg-slate-600'
|
onClick={() => setActivePage('scraper')}
|
||||||
}`}
|
className={`w-full text-left flex items-center gap-3 px-3 py-2 rounded-lg transition-colors ${activePage === 'scraper' ? 'bg-blue-500/10 text-blue-400 border border-blue-500/20' : 'text-slate-400 hover:bg-slate-800 hover:text-white'}`}
|
||||||
>
|
>
|
||||||
<div className="flex justify-between items-center mb-2">
|
<Database size={18} />
|
||||||
<span className="font-bold">{session.name}</span>
|
<span className="text-sm font-bold">Scraper</span>
|
||||||
<div className="flex items-center gap-2">
|
</button>
|
||||||
{session.isRunning && (
|
<button
|
||||||
<span className="px-2 py-1 bg-green-500 text-xs rounded">
|
onClick={() => setActivePage('schedules')}
|
||||||
Rodando
|
className={`w-full text-left flex items-center gap-3 px-3 py-2 rounded-lg transition-colors ${activePage === 'schedules' ? 'bg-blue-500/10 text-blue-400 border border-blue-500/20' : 'text-slate-400 hover:bg-slate-800 hover:text-white'}`}
|
||||||
</span>
|
>
|
||||||
)}
|
<Calendar size={18} />
|
||||||
<button
|
<span className="text-sm font-medium">Agendamentos</span>
|
||||||
onClick={(e) => handleSessionAction(e, session)}
|
</button>
|
||||||
className={`flex items-center gap-1 px-2 py-1 rounded text-xs transition ${
|
<button
|
||||||
session.isRunning
|
onClick={() => setActivePage('logs')}
|
||||||
? 'bg-orange-600 hover:bg-orange-700'
|
className={`w-full text-left flex items-center gap-3 px-3 py-2 rounded-lg transition-colors ${activePage === 'logs' ? 'bg-blue-500/10 text-blue-400 border border-blue-500/20' : 'text-slate-400 hover:bg-slate-800 hover:text-white'}`}
|
||||||
: 'bg-blue-600 hover:bg-blue-700'
|
>
|
||||||
}`}
|
<Terminal size={18} />
|
||||||
title={session.isRunning ? 'Parar (Graceful)' : 'Iniciar Scraping'}
|
<span className="text-sm font-medium">Logs de Execução</span>
|
||||||
>
|
</button>
|
||||||
{session.isRunning ? <Pause size={14} /> : <Play size={14} />}
|
</nav>
|
||||||
{session.isRunning ? 'Parar' : 'Iniciar'}
|
</div>
|
||||||
</button>
|
</aside>
|
||||||
</div>
|
);
|
||||||
</div>
|
|
||||||
<div className="text-sm text-slate-300">
|
return (
|
||||||
<div>Total: {formatNumber(session.queue?.total || 0)}</div>
|
<div className="min-h-screen bg-gradient-to-br from-slate-900 to-slate-800 text-white">
|
||||||
<div className="flex gap-4 mt-1">
|
<Header />
|
||||||
<span className="text-green-400 flex items-center"><CircleCheck size={16} /> {formatNumber(session.queue?.done || 0)}</span>
|
<Sidebar />
|
||||||
<span className="text-yellow-400 flex items-center"><Clock size={16} /> {formatNumber(session.queue?.pending || 0)}</span>
|
|
||||||
<span className="text-red-400 flex items-center"><CircleX size={16} /> {formatNumber(session.queue?.failed || 0)}</span>
|
{/* Conteúdo principal */}
|
||||||
</div>
|
<main className="pt-16 pl-0 md:pl-64">
|
||||||
</div>
|
<div className="p-6 lg:p-8">
|
||||||
</div>
|
{/* Breadcrumb e título da página ativa */}
|
||||||
))}
|
<div className="mb-6">
|
||||||
{sessions.length === 0 && (
|
<div className="flex items-center text-xs text-slate-400 mb-2">
|
||||||
<div className="text-slate-500 text-center py-8">
|
<span className="hover:text-blue-400 transition-colors cursor-default">Extratores</span>
|
||||||
Nenhuma sessão disponível
|
<span className="mx-2 text-slate-600">/</span>
|
||||||
</div>
|
<span className="text-white">{activePage === 'scraper' ? 'Scraper' : activePage.charAt(0).toUpperCase() + activePage.slice(1)}</span>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="flex items-end justify-between gap-4">
|
||||||
|
<div>
|
||||||
{/* Detalhes e Controles */}
|
<h2 className="text-2xl md:text-3xl font-bold text-white tracking-tight">
|
||||||
<div className="col-span-2 space-y-6">
|
{activePage === 'scraper' ? 'Gerenciador de Sessões' : 'Em breve'}
|
||||||
{selectedSession ? (
|
</h2>
|
||||||
<>
|
<p className="text-slate-400 text-sm mt-1">
|
||||||
{/* Controles */}
|
{activePage === 'scraper' ? 'Gerencie e monitore suas sessões de scraping.' : 'Conteúdo a ser implementado.'}
|
||||||
<div className="bg-slate-800 rounded-lg p-6 shadow-xl">
|
|
||||||
<h3 className="text-lg font-bold mb-4">Controles - {selectedSession.name}</h3>
|
|
||||||
|
|
||||||
<div className="flex gap-3 mb-4">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={newUrl}
|
|
||||||
onChange={(e) => setNewUrl(e.target.value)}
|
|
||||||
placeholder="https://site.com/page"
|
|
||||||
className="flex-1 px-4 py-2 bg-slate-700 rounded-lg border border-slate-600 focus:border-blue-500 focus:outline-none"
|
|
||||||
onKeyPress={(e) => e.key === 'Enter' && addUrlToQueue()}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
onClick={addUrlToQueue}
|
|
||||||
className="flex items-center gap-2 px-6 py-2 bg-green-600 hover:bg-green-700 rounded-lg font-medium transition"
|
|
||||||
disabled={!newUrl}
|
|
||||||
>
|
|
||||||
<Plus size={18} />
|
|
||||||
Adicionar
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex gap-3">
|
|
||||||
{!selectedSession.isRunning ? (
|
|
||||||
<button
|
|
||||||
onClick={startScraping}
|
|
||||||
className="flex items-center gap-2 px-6 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg font-medium transition"
|
|
||||||
>
|
|
||||||
<Play size={18} />
|
|
||||||
Iniciar Scraping
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
onClick={stopScraping}
|
|
||||||
className="flex items-center gap-2 px-6 py-2 bg-orange-600 hover:bg-orange-700 rounded-lg font-medium transition"
|
|
||||||
>
|
|
||||||
<Pause size={18} />
|
|
||||||
Parar (Graceful)
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Estatísticas */}
|
|
||||||
{sessionStatus && (
|
|
||||||
<>
|
|
||||||
<div className="bg-slate-800 rounded-lg p-6 shadow-xl">
|
|
||||||
<h3 className="text-lg font-bold mb-4">Progresso</h3>
|
|
||||||
|
|
||||||
{/* Barra de Progresso */}
|
|
||||||
<div className="mb-4">
|
|
||||||
<div className="flex justify-between text-sm mb-2">
|
|
||||||
<span>Processado</span>
|
|
||||||
<span>{getProgressPercent()}%</span>
|
|
||||||
</div>
|
|
||||||
<div className="w-full bg-slate-700 rounded-full h-4 overflow-hidden">
|
|
||||||
<div
|
|
||||||
className="bg-gradient-to-r from-blue-500 to-green-500 h-full transition-all duration-500"
|
|
||||||
style={{ width: `${getProgressPercent()}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-5 gap-3">
|
|
||||||
<div className="bg-slate-700 rounded-lg p-3">
|
|
||||||
<div className="text-slate-400 text-xs mb-1">Total</div>
|
|
||||||
<div className="text-2xl font-bold">{formatNumber(sessionStatus.counts?.total)}</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-slate-700 rounded-lg p-3">
|
|
||||||
<div className="text-slate-400 text-xs mb-1">Concluído</div>
|
|
||||||
<div className="text-2xl font-bold text-green-400">{formatNumber(sessionStatus.counts?.done)}</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-slate-700 rounded-lg p-3">
|
|
||||||
<div className="text-slate-400 text-xs mb-1">Pendente</div>
|
|
||||||
<div className="text-2xl font-bold text-yellow-400">{formatNumber(sessionStatus.counts?.pending)}</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-slate-700 rounded-lg p-3">
|
|
||||||
<div className="text-slate-400 text-xs mb-1">Processando</div>
|
|
||||||
<div className="text-2xl font-bold text-blue-400">{formatNumber(sessionStatus.counts?.processing)}</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-slate-700 rounded-lg p-3">
|
|
||||||
<div className="text-slate-400 text-xs mb-1">Falhas</div>
|
|
||||||
<div className="text-2xl font-bold text-red-400">{formatNumber(sessionStatus.counts?.failed)}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* URL Atual */}
|
|
||||||
{sessionStatus.runtime?.isRunning && sessionStatus.runtime?.currentUrl && (
|
|
||||||
<div className="mt-4 p-3 bg-slate-700 rounded-lg">
|
|
||||||
<div className="text-xs text-slate-400 mb-1">Processando agora:</div>
|
|
||||||
<div className="text-sm text-blue-300 truncate" title={sessionStatus.runtime.currentUrl}>
|
|
||||||
{sessionStatus.runtime.currentUrl}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-slate-400 mt-1">
|
|
||||||
Iniciado: {new Date(sessionStatus.runtime.currentStartedAt).toLocaleTimeString('pt-BR')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Logs */}
|
|
||||||
<div className="bg-slate-800 rounded-lg p-6 shadow-xl">
|
|
||||||
<h2 className="text-xl font-bold mb-4">Logs em Tempo Real</h2>
|
|
||||||
<div className="space-y-1 max-h-96 overflow-y-auto font-mono text-xs">
|
|
||||||
{logs.map((log, idx) => (
|
|
||||||
<div key={idx} className={`
|
|
||||||
${log.type === 'error' ? 'text-red-400' : ''}
|
|
||||||
${log.type === 'success' ? 'text-green-400' : ''}
|
|
||||||
${log.type === 'warning' ? 'text-yellow-400' : ''}
|
|
||||||
${log.type === 'info' ? 'text-slate-300' : ''}
|
|
||||||
`}>
|
|
||||||
[{log.time}] {log.message}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{logs.length === 0 && (
|
|
||||||
<div className="text-slate-500 text-center py-8">
|
|
||||||
Nenhum evento ainda. Selecione uma sessão e inicie o scraping.
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className="bg-slate-800 rounded-lg p-12 shadow-xl text-center">
|
|
||||||
<Globe size={64} className="mx-auto mb-4 text-slate-600" />
|
|
||||||
<h3 className="text-xl font-bold mb-2">Selecione uma Sessão</h3>
|
|
||||||
<p className="text-slate-400">
|
|
||||||
Escolha uma sessão à esquerda para começar
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Render da página ativa */}
|
||||||
|
{activePage === 'scraper' ? (
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
<div className="grid grid-cols-3 gap-6">
|
||||||
|
{/* Lista de Sessões */}
|
||||||
|
<div className="bg-slate-800 rounded-lg p-6 shadow-xl">
|
||||||
|
<h2 className="text-xl font-bold mb-4 flex items-center gap-2">
|
||||||
|
<Database size={20} />
|
||||||
|
Sessões
|
||||||
|
</h2>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{sessions.map((session) => (
|
||||||
|
<div
|
||||||
|
key={session.sessionId}
|
||||||
|
onClick={() => selectSession(session)}
|
||||||
|
className={`p-4 rounded-lg cursor-pointer transition ${selectedSession?.sessionId === session.sessionId
|
||||||
|
? 'bg-blue-600'
|
||||||
|
: 'bg-slate-700 hover:bg-slate-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex justify-between items-center mb-2">
|
||||||
|
<span className="font-bold">{session.name}</span>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{session.isRunning && (
|
||||||
|
<span className="px-2 py-1 bg-green-500 text-xs rounded">
|
||||||
|
Rodando
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={(e) => handleSessionAction(e, session)}
|
||||||
|
className={`flex items-center gap-1 px-2 py-1 rounded text-xs transition ${
|
||||||
|
session.isRunning
|
||||||
|
? 'bg-orange-600 hover:bg-orange-700'
|
||||||
|
: 'bg-blue-600 hover:bg-blue-700'
|
||||||
|
}`}
|
||||||
|
title={session.isRunning ? 'Parar (Graceful)' : 'Iniciar Scraping'}
|
||||||
|
>
|
||||||
|
{session.isRunning ? <Pause size={14} /> : <Play size={14} />}
|
||||||
|
{session.isRunning ? 'Parar' : 'Iniciar'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-slate-300">
|
||||||
|
<div>Total: {formatNumber(session.queue?.total || 0)}</div>
|
||||||
|
<div className="flex gap-4 mt-1">
|
||||||
|
<span className="text-green-400 flex items-center"><CircleCheck size={16} /> {formatNumber(session.queue?.done || 0)}</span>
|
||||||
|
<span className="text-yellow-400 flex items-center"><Clock size={16} /> {formatNumber(session.queue?.pending || 0)}</span>
|
||||||
|
<span className="text-red-400 flex items-center"><CircleX size={16} /> {formatNumber(session.queue?.failed || 0)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{sessions.length === 0 && (
|
||||||
|
<div className="text-slate-500 text-center py-8">
|
||||||
|
Nenhuma sessão disponível
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Detalhes e Controles */}
|
||||||
|
<div className="col-span-2 space-y-6">
|
||||||
|
{selectedSession ? (
|
||||||
|
<>
|
||||||
|
{/* Controles */}
|
||||||
|
<div className="bg-slate-800 rounded-lg p-6 shadow-xl">
|
||||||
|
<h3 className="text-lg font-bold mb-4">Controles - {selectedSession.name}</h3>
|
||||||
|
|
||||||
|
<div className="flex gap-3 mb-4">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={newUrl}
|
||||||
|
onChange={(e) => setNewUrl(e.target.value)}
|
||||||
|
placeholder="https://site.com/page"
|
||||||
|
className="flex-1 px-4 py-2 bg-slate-700 rounded-lg border border-slate-600 focus:border-blue-500 focus:outline-none"
|
||||||
|
onKeyPress={(e) => e.key === 'Enter' && addUrlToQueue()}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={addUrlToQueue}
|
||||||
|
className="flex items-center gap-2 px-6 py-2 bg-green-600 hover:bg-green-700 rounded-lg font-medium transition"
|
||||||
|
disabled={!newUrl}
|
||||||
|
>
|
||||||
|
<Plus size={18} />
|
||||||
|
Adicionar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-3">
|
||||||
|
{!selectedSession.isRunning ? (
|
||||||
|
<button
|
||||||
|
onClick={startScraping}
|
||||||
|
className="flex items-center gap-2 px-6 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg font-medium transition"
|
||||||
|
>
|
||||||
|
<Play size={18} />
|
||||||
|
Iniciar Scraping
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={stopScraping}
|
||||||
|
className="flex items-center gap-2 px-6 py-2 bg-orange-600 hover:bg-orange-700 rounded-lg font-medium transition"
|
||||||
|
>
|
||||||
|
<Pause size={18} />
|
||||||
|
Parar (Graceful)
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Estatísticas */}
|
||||||
|
{sessionStatus && (
|
||||||
|
<>
|
||||||
|
<div className="bg-slate-800 rounded-lg p-6 shadow-xl">
|
||||||
|
<h3 className="text-lg font-bold mb-4">Progresso</h3>
|
||||||
|
|
||||||
|
{/* Barra de Progresso */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className="flex justify-between text-sm mb-2">
|
||||||
|
<span>Processado</span>
|
||||||
|
<span>{getProgressPercent()}%</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-slate-700 rounded-full h-4 overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="bg-gradient-to-r from-blue-500 to-green-500 h-full transition-all duration-500"
|
||||||
|
style={{ width: `${getProgressPercent()}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-5 gap-3">
|
||||||
|
<div className="bg-slate-700 rounded-lg p-3">
|
||||||
|
<div className="text-slate-400 text-xs mb-1">Total</div>
|
||||||
|
<div className="text-2xl font-bold">{formatNumber(sessionStatus.counts?.total)}</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-slate-700 rounded-lg p-3">
|
||||||
|
<div className="text-slate-400 text-xs mb-1">Concluído</div>
|
||||||
|
<div className="text-2xl font-bold text-green-400">{formatNumber(sessionStatus.counts?.done)}</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-slate-700 rounded-lg p-3">
|
||||||
|
<div className="text-slate-400 text-xs mb-1">Pendente</div>
|
||||||
|
<div className="text-2xl font-bold text-yellow-400">{formatNumber(sessionStatus.counts?.pending)}</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-slate-700 rounded-lg p-3">
|
||||||
|
<div className="text-slate-400 text-xs mb-1">Processando</div>
|
||||||
|
<div className="text-2xl font-bold text-blue-400">{formatNumber(sessionStatus.counts?.processing)}</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-slate-700 rounded-lg p-3">
|
||||||
|
<div className="text-slate-400 text-xs mb-1">Falhas</div>
|
||||||
|
<div className="text-2xl font-bold text-red-400">{formatNumber(sessionStatus.counts?.failed)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* URL Atual */}
|
||||||
|
{sessionStatus.runtime?.isRunning && sessionStatus.runtime?.currentUrl && (
|
||||||
|
<div className="mt-4 p-3 bg-slate-700 rounded-lg">
|
||||||
|
<div className="text-xs text-slate-400 mb-1">Processando agora:</div>
|
||||||
|
<div className="text-sm text-blue-300 truncate" title={sessionStatus.runtime.currentUrl}>
|
||||||
|
{sessionStatus.runtime.currentUrl}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-slate-400 mt-1">
|
||||||
|
Iniciado: {new Date(sessionStatus.runtime.currentStartedAt).toLocaleTimeString('pt-BR')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Logs */}
|
||||||
|
<div className="bg-slate-800 rounded-lg p-6 shadow-xl">
|
||||||
|
<h2 className="text-xl font-bold mb-4">Logs em Tempo Real</h2>
|
||||||
|
<div className="space-y-1 max-h-96 overflow-y-auto font-mono text-xs">
|
||||||
|
{logs.map((log, idx) => (
|
||||||
|
<div key={idx} className={`
|
||||||
|
${log.type === 'error' ? 'text-red-400' : ''}
|
||||||
|
${log.type === 'success' ? 'text-green-400' : ''}
|
||||||
|
${log.type === 'warning' ? 'text-yellow-400' : ''}
|
||||||
|
${log.type === 'info' ? 'text-slate-300' : ''}
|
||||||
|
`}>
|
||||||
|
[{log.time}] {log.message}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{logs.length === 0 && (
|
||||||
|
<div className="text-slate-500 text-center py-8">
|
||||||
|
Nenhum evento ainda. Selecione uma sessão e inicie o scraping.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="bg-slate-800 rounded-lg p-12 shadow-xl text-center">
|
||||||
|
<Globe size={64} className="mx-auto mb-4 text-slate-600" />
|
||||||
|
<h3 className="text-xl font-bold mb-2">Selecione uma Sessão</h3>
|
||||||
|
<p className="text-slate-400">
|
||||||
|
Escolha uma sessão à esquerda para começar
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="bg-slate-800/50 border border-slate-700 rounded-xl p-8 text-slate-300">
|
||||||
|
Conteúdo da página "{activePage}" em breve.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user