Files
FROM-SQL-INYEC/src/components/ResultTable.jsx
2026-01-22 11:51:52 -03:00

375 lines
17 KiB
JavaScript

import React, { useState } from 'react';
import { AlertCircle, Clock, Database, FileX, Loader2, CheckCircle2, Hash, Copy, Download, Check } from 'lucide-react';
const MAX_DISPLAY_ROWS = 1000;
export default function ResultTable({ result, loading, pollingProgress }) {
const [copied, setCopied] = useState(false);
// Normalize data
let columns = [];
let rows = [];
if (result && !result.error) {
if (result.rows && result.columns) {
columns = result.columns;
rows = result.rows;
} else if (Array.isArray(result.data) && result.data.length > 0) {
columns = Object.keys(result.data[0]);
rows = result.data;
} else if (Array.isArray(result) && result.length > 0) {
columns = Object.keys(result[0]);
rows = result;
}
}
// Function to convert data to CSV
const generateCSV = (cols, dataRows) => {
const header = cols.join(',');
const csvRows = dataRows.map(row => {
return cols.map(col => {
let val = Array.isArray(row) ? row[cols.indexOf(col)] : row[col];
if (val === null || val === undefined) val = '';
val = String(val).replace(/"/g, '""');
if (val.includes(',') || val.includes('"') || val.includes('\n')) {
val = `"${val}"`;
}
return val;
}).join(',');
});
return [header, ...csvRows].join('\n');
};
// Download as CSV
const handleDownloadCSV = (cols, dataRows) => {
const csv = generateCSV(cols, dataRows);
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.setAttribute('href', url);
link.setAttribute('download', `query_result_${Date.now()}.csv`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
};
// Auto-download if too many rows
React.useEffect(() => {
if (rows.length > MAX_DISPLAY_ROWS) {
handleDownloadCSV(columns, rows);
}
}, [result]);
// Copy to clipboard
const handleCopy = (cols, dataRows) => {
const csv = generateCSV(cols, dataRows);
navigator.clipboard.writeText(csv).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
});
};
// Loading state
if (loading) {
return (
<div className="result-container" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}>
<div style={{
width: '5rem', height: '5rem',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginBottom: '1.5rem',
border: '2px solid rgba(59, 130, 246, 0.3)'
}}>
<Loader2 size={32} style={{ color: 'var(--info)', animation: 'spin 1s linear infinite' }} />
</div>
<p style={{ color: 'var(--text-primary)', fontWeight: 500, marginBottom: '0.5rem' }}>
{pollingProgress ? 'Esperando respuesta del agente remoto...' : 'Enviando comando...'}
</p>
{pollingProgress && (
<div style={{ textAlign: 'center' }}>
<div style={{
width: '200px', height: '4px',
backgroundColor: 'var(--border)',
borderRadius: '2px',
overflow: 'hidden',
marginBottom: '0.5rem'
}}>
<div style={{
width: `${(pollingProgress.attempts / pollingProgress.maxAttempts) * 100}%`,
height: '100%',
backgroundColor: 'var(--info)',
transition: 'width 0.3s ease'
}} />
</div>
<p className="text-xs font-mono" style={{ color: 'var(--text-muted)' }}>
Polling: {pollingProgress.attempts} / {pollingProgress.maxAttempts}
</p>
</div>
)}
<style>{`@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }`}</style>
</div>
);
}
// No result yet
if (!result) {
return (
<div className="result-container" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}>
<div style={{ backgroundColor: 'var(--bg-panel)', padding: '2rem', borderRadius: '999px', marginBottom: '1rem', opacity: 0.5 }}>
<Database size={32} />
</div>
<p style={{ color: 'var(--text-muted)' }}>Los resultados aparecerán aquí</p>
</div>
);
}
// Error state
if (result.error) {
return (
<div className="result-container" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: '2rem' }}>
<div style={{
width: '4rem', height: '4rem',
backgroundColor: 'rgba(239, 68, 68, 0.1)',
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginBottom: '1rem'
}}>
<AlertCircle size={32} style={{ color: 'var(--error)' }} />
</div>
<h3 style={{ color: 'var(--text-primary)', fontWeight: 600, marginBottom: '0.75rem' }}>Error en la ejecución</h3>
<code style={{
backgroundColor: 'rgba(69, 10, 10, 0.5)',
color: '#fecaca',
padding: '1rem',
borderRadius: '0.5rem',
border: '1px solid rgba(127, 29, 29, 0.5)',
width: '100%',
maxWidth: '600px',
whiteSpace: 'pre-wrap',
fontSize: '0.875rem',
display: 'block'
}}>
{result.error}
</code>
{result.command_id && (
<p className="text-xs font-mono mt-3" style={{ color: 'var(--text-muted)' }}>
Command ID: #{result.command_id}
</p>
)}
</div>
);
}
// Success message (no data rows)
if (rows.length === 0 && !result.error) {
if (result.command_id || result.message) {
return (
<div className="result-container" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}>
<div style={{
width: '4rem', height: '4rem',
backgroundColor: 'rgba(16, 185, 129, 0.1)',
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginBottom: '1rem',
border: '2px solid rgba(16, 185, 129, 0.3)'
}}>
<CheckCircle2 size={28} style={{ color: 'var(--success)' }} />
</div>
<h3 style={{ color: 'var(--text-primary)', fontWeight: 600, marginBottom: '0.5rem' }}>
{result.message || 'Consulta ejecutada correctamente'}
</h3>
{result.downloadUrl && (
<div style={{ marginTop: '1.5rem', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '1rem' }}>
<a
href={result.downloadUrl}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 px-8 py-3 rounded-lg font-bold transition-all hover:scale-105 shadow-lg shadow-blue-500/20"
style={{
background: 'var(--accent)',
color: 'white',
textDecoration: 'none',
fontSize: '1rem'
}}
>
<Download size={20} />
Descargar Respaldo Ahora
</a>
<p className="text-xs" style={{ color: 'var(--text-muted)' }}>
El archivo se encuentra disponible en el servidor FTP especificado.
</p>
</div>
)}
{result.command_id && !result.downloadUrl && (
<p className="font-mono text-xs flex items-center gap-1" style={{ color: 'var(--text-accent)' }}>
<Hash size={12} /> Command ID: {result.command_id}
</p>
)}
</div>
);
}
return (
<div className="result-container" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}>
<FileX size={48} className="mb-4" style={{ opacity: 0.5, color: 'var(--text-secondary)' }} />
<p style={{ color: 'var(--text-secondary)' }}>La consulta no devolvió resultados.</p>
</div>
);
}
// Large dataset display
if (rows.length > MAX_DISPLAY_ROWS) {
return (
<div className="result-container" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: '2rem' }}>
<div style={{
width: '5rem', height: '5rem',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginBottom: '1.5rem',
border: '2px solid rgba(59, 130, 246, 0.3)'
}}>
<Download size={32} style={{ color: 'var(--info)' }} />
</div>
<h3 style={{ color: 'var(--text-primary)', fontWeight: 600, marginBottom: '0.5rem' }}>Resultado muy grande ({rows.length} filas)</h3>
<p style={{ color: 'var(--text-muted)', textAlign: 'center', maxWidth: '500px', marginBottom: '1.5rem' }}>
Para mantener el rendimiento del navegador, los resultados que superan las {MAX_DISPLAY_ROWS} filas no se muestran en pantalla y se descargan automáticamente como CSV.
</p>
<button
onClick={() => handleDownloadCSV(columns, rows)}
className="flex items-center gap-2 px-6 py-3 rounded-lg font-medium transition-all hover:scale-105"
style={{
background: 'var(--accent)',
color: 'white',
border: 'none',
cursor: 'pointer',
boxShadow: '0 4px 12px rgba(59, 130, 246, 0.3)'
}}
>
<Download size={18} />
Descargar CSV de nuevo
</button>
{result.command_id && (
<p className="text-xs font-mono mt-6" style={{ color: 'var(--text-muted)' }}>
Command ID: #{result.command_id}
</p>
)}
</div>
);
}
// Data table display - DARK THEME
return (
<div className="result-container">
{/* Stats Header */}
<div className="result-header">
<div className="flex gap-4 items-center">
{result.command_id && (
<span className="flex items-center gap-1.5 font-mono" style={{ color: 'var(--text-accent)' }}>
<Hash size={12} /> {result.command_id}
</span>
)}
{result.stats?.row_count !== undefined && (
<span className="flex items-center gap-1.5" style={{ color: 'var(--text-secondary)' }}>
<Database size={12} style={{ color: 'var(--success)' }} />
<span className="font-mono" style={{ color: 'var(--text-primary)' }}>{result.stats.row_count}</span> filas
</span>
)}
{result.stats?.duration && (
<span className="flex items-center gap-1.5" style={{ color: 'var(--text-secondary)' }}>
<Clock size={12} style={{ color: 'var(--info)' }} />
<span className="font-mono" style={{ color: 'var(--text-primary)' }}>{result.stats.duration}</span>
</span>
)}
</div>
<div className="flex items-center gap-2">
{/* Copy Button */}
<button
onClick={() => handleCopy(columns, rows)}
className="flex items-center gap-1 px-3 py-1.5 text-xs rounded transition-colors"
style={{
background: copied ? 'var(--success)' : 'var(--border)',
color: copied ? 'white' : 'var(--text-primary)',
border: 'none',
cursor: 'pointer'
}}
title="Copiar al portapapeles"
>
{copied ? <Check size={14} /> : <Copy size={14} />}
{copied ? 'Copiado!' : 'Copiar'}
</button>
{/* Download CSV Button */}
<button
onClick={() => handleDownloadCSV(columns, rows)}
className="flex items-center gap-1 px-3 py-1.5 text-xs rounded transition-colors"
style={{
background: 'var(--accent)',
color: 'white',
border: 'none',
cursor: 'pointer'
}}
title="Descargar CSV"
>
<Download size={14} />
CSV
</button>
<span className="flex items-center gap-1 ml-2">
<CheckCircle2 size={14} style={{ color: 'var(--success)' }} />
<span className="text-xs" style={{ color: 'var(--success)' }}>Completado</span>
</span>
</div>
</div>
{/* Data Table */}
<div className="table-wrapper">
<table>
<thead>
<tr>
<th style={{ width: '50px', textAlign: 'center' }}>#</th>
{columns.map((col, idx) => (
<th key={idx}>{col}</th>
))}
</tr>
</thead>
<tbody>
{rows.map((row, rIdx) => (
<tr key={rIdx}>
<td style={{ textAlign: 'center', color: 'var(--text-muted)', fontSize: '0.75rem' }}>{rIdx + 1}</td>
{columns.map((col, cIdx) => {
let val;
if (Array.isArray(row)) {
val = row[cIdx];
} else {
val = row[col];
}
return (
<td key={cIdx}>
{val === null ? (
<span style={{ color: 'var(--text-muted)', fontStyle: 'italic' }}>NULL</span>
) : typeof val === 'object' ? (
<span style={{ color: 'var(--text-accent)' }}>{JSON.stringify(val)}</span>
) : (
String(val)
)}
</td>
);
})}
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}