1
0

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:
Márcio Eric 2025-12-23 17:35:35 -03:00
parent f069b87dbf
commit 90b427c5b7

View File

@ -1,5 +1,5 @@
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';
const API_BASE = 'http://localhost:5123';
@ -13,6 +13,9 @@ const WebScraper = () => {
const [logs, setLogs] = useState([]);
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 addLog = (message, type = 'info') => {
@ -71,10 +74,7 @@ const WebScraper = () => {
addLog('Conectado ao servidor', 'success');
connectionRef.current = connection;
// ✅ NOVO: assinar overview
await connection.invoke('SubscribeOverview');
// pega snapshot inicial uma vez
await loadSessions();
})
.catch(err => {
@ -111,7 +111,7 @@ const WebScraper = () => {
const upsertSessionFromEvent = (sid, patch) => {
setSessions(prev => {
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];
copy[idx] = { ...copy[idx], ...patch };
return copy;
@ -150,7 +150,6 @@ const WebScraper = () => {
case 'ItemStarted':
addLog(`📄 Processando: ${event.url}`, 'info');
// Atualiza runtime do painel se essa for a sessão selecionada
setSessionStatus(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) {
setSessionStatus(prev => ({
...(prev ?? {}),
@ -307,7 +305,7 @@ const WebScraper = () => {
setSelectedSessionId(session.sessionId);
setLogs([]);
addLog(`Sessão selecionada: ${session.name}`, 'info');
loadSessionStatus(session.sessionId); // snapshot inicial
loadSessionStatus(session.sessionId);
};
const formatNumber = (num) => {
@ -342,7 +340,7 @@ const WebScraper = () => {
};
const handleSessionAction = (e, session) => {
e.stopPropagation(); // Evita selecionar a sessão ao clicar no botão
e.stopPropagation();
if (session.isRunning) {
stopScrapingById(session.sessionId, session.name);
} else {
@ -350,231 +348,303 @@ const WebScraper = () => {
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-slate-900 to-slate-800 text-white p-6">
<div className="max-w-7xl mx-auto">
{/* Header */}
<div className="mb-8 flex justify-between items-center">
<div>
<h1 className="text-4xl font-bold mb-2 flex items-center gap-3">
<Globe className="text-blue-400" />
Web Scraper Pro
</h1>
<p className="text-slate-400">Gerenciador de scraping com API backend</p>
</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>
// Componente Header
const Header = () => (
<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">
<div className="flex items-center gap-3">
<Globe className="text-blue-400" />
<h1 className="text-lg font-bold tracking-tight text-white">Web Scraper Pro</h1>
</div>
<div className="flex items-center gap-3">
<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'}`}>
<Activity size={16} />
{isConnected ? 'Conectado' : 'Desconectado'}
</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">
{/* 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} />&nbsp;{formatNumber(session.queue?.done || 0)}</span>
<span className="text-yellow-400 flex items-center"><Clock size={16} />&nbsp;{formatNumber(session.queue?.pending || 0)}</span>
<span className="text-red-400 flex items-center"><CircleX size={16} />&nbsp;{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>
)}
// Componente Sidebar
const Sidebar = () => (
<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">
<div className="p-4">
<p className="px-2 text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2">Gerenciamento</p>
<nav className="space-y-1">
<button
onClick={() => setActivePage('dashboard')}
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'}`}
>
<Grid size={18} />
<span className="text-sm font-medium">Dashboard</span>
</button>
<button
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'}`}
>
<Database size={18} />
<span className="text-sm font-bold">Scraper</span>
</button>
<button
onClick={() => setActivePage('schedules')}
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'}`}
>
<Calendar size={18} />
<span className="text-sm font-medium">Agendamentos</span>
</button>
<button
onClick={() => setActivePage('logs')}
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'}`}
>
<Terminal size={18} />
<span className="text-sm font-medium">Logs de Execução</span>
</button>
</nav>
</div>
</aside>
);
return (
<div className="min-h-screen bg-gradient-to-br from-slate-900 to-slate-800 text-white">
<Header />
<Sidebar />
{/* Conteúdo principal */}
<main className="pt-16 pl-0 md:pl-64">
<div className="p-6 lg:p-8">
{/* Breadcrumb e título da página ativa */}
<div className="mb-6">
<div className="flex items-center text-xs text-slate-400 mb-2">
<span className="hover:text-blue-400 transition-colors cursor-default">Extratores</span>
<span className="mx-2 text-slate-600">/</span>
<span className="text-white">{activePage === 'scraper' ? 'Scraper' : activePage.charAt(0).toUpperCase() + activePage.slice(1)}</span>
</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
<div className="flex items-end justify-between gap-4">
<div>
<h2 className="text-2xl md:text-3xl font-bold text-white tracking-tight">
{activePage === 'scraper' ? 'Gerenciador de Sessões' : 'Em breve'}
</h2>
<p className="text-slate-400 text-sm mt-1">
{activePage === 'scraper' ? 'Gerencie e monitore suas sessões de scraping.' : 'Conteúdo a ser implementado.'}
</p>
</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} />&nbsp;{formatNumber(session.queue?.done || 0)}</span>
<span className="text-yellow-400 flex items-center"><Clock size={16} />&nbsp;{formatNumber(session.queue?.pending || 0)}</span>
<span className="text-red-400 flex items-center"><CircleX size={16} />&nbsp;{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>
</main>
</div>
);
};