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
112
src/App.js
112
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,33 +348,98 @@ 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">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-4xl font-bold mb-2 flex items-center gap-3">
|
|
||||||
<Globe className="text-blue-400" />
|
<Globe className="text-blue-400" />
|
||||||
Web Scraper Pro
|
<h1 className="text-lg font-bold tracking-tight text-white">Web Scraper Pro</h1>
|
||||||
</h1>
|
|
||||||
<p className="text-slate-400">Gerenciador de scraping com API backend</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<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'}`}>
|
<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} />
|
<Activity size={16} />
|
||||||
{isConnected ? 'Conectado' : 'Desconectado'}
|
{isConnected ? 'Conectado' : 'Desconectado'}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={loadSessions}
|
onClick={loadSessions}
|
||||||
className="flex items-center gap-2 px-4 py-2 bg-slate-700 hover:bg-slate-600 rounded-lg transition"
|
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} />
|
<RefreshCw size={16} />
|
||||||
Atualizar
|
Atualizar
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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 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>
|
</div>
|
||||||
|
|
||||||
|
{/* Render da página ativa */}
|
||||||
|
{activePage === 'scraper' ? (
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
<div className="grid grid-cols-3 gap-6">
|
<div className="grid grid-cols-3 gap-6">
|
||||||
{/* Lista de Sessões */}
|
{/* Lista de Sessões */}
|
||||||
<div className="bg-slate-800 rounded-lg p-6 shadow-xl">
|
<div className="bg-slate-800 rounded-lg p-6 shadow-xl">
|
||||||
@ -575,6 +638,13 @@ const WebScraper = () => {
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user