Spaces:
Running
Running
import React, { useState, useEffect, useCallback } from "react"; | |
import { debugLog } from "../utils/config"; | |
import TroubleshootingGuide from "./TroubleshootingGuide"; | |
import "./DataViewer.css"; | |
const DataViewer = ({ s3Url, onDownload, showPreviewOnly = false }) => { | |
const [data, setData] = useState([]); | |
const [columns, setColumns] = useState([]); | |
const [loading, setLoading] = useState(true); | |
const [error, setError] = useState(null); | |
const [currentPage, setCurrentPage] = useState(1); | |
const [rowsPerPage] = useState(10); | |
const fetchData = useCallback(async () => { | |
try { | |
setLoading(true); | |
setError(null); | |
debugLog("Fetching data from S3 URL:", s3Url); | |
// Try multiple approaches to fetch the data | |
let response; | |
// Method 1: Direct fetch with CORS | |
try { | |
response = await fetch(s3Url, { | |
method: "GET", | |
headers: { | |
Accept: "text/csv,text/plain,application/octet-stream,*/*", | |
}, | |
mode: "cors", | |
}); | |
} catch (corsError) { | |
debugLog("CORS fetch failed, trying no-cors:", corsError); | |
// Method 2: Try with no-cors mode (limited but might work) | |
try { | |
response = await fetch(s3Url, { | |
method: "GET", | |
mode: "no-cors", | |
}); | |
} catch (noCorsError) { | |
debugLog("No-cors fetch also failed:", noCorsError); | |
throw new Error( | |
"Unable to preview data due to CORS restrictions. You can still download the file directly." | |
); | |
} | |
} | |
if (!response.ok && response.status !== 0) { | |
// If direct fetch fails, provide helpful error messages | |
if (response.status === 403 || response.status === 401) { | |
throw new Error( | |
"Access denied. The file may require authentication or have expired." | |
); | |
} else if (response.status === 404) { | |
throw new Error( | |
"File not found. The download link may have expired." | |
); | |
} else { | |
throw new Error( | |
`Unable to fetch data (${response.status}). You can still download the file directly.` | |
); | |
} | |
} | |
// For no-cors responses, we can't read the content | |
if (response.type === "opaque") { | |
throw new Error( | |
"Preview not available due to CORS restrictions. Please download the file to view the data." | |
); | |
} | |
const csvText = await response.text(); | |
if (!csvText || csvText.trim().length === 0) { | |
throw new Error("The downloaded file appears to be empty"); | |
} | |
const parsedData = parseCSV(csvText); | |
if (parsedData.length > 0) { | |
setColumns(Object.keys(parsedData[0])); | |
setData(parsedData); | |
debugLog("Data parsed successfully:", { | |
rows: parsedData.length, | |
columns: Object.keys(parsedData[0]).length, | |
sampleData: parsedData.slice(0, 2), | |
}); | |
} else { | |
throw new Error("No valid data rows found in the file"); | |
} | |
} catch (err) { | |
setError(err.message); | |
debugLog("Error fetching data:", err); | |
} finally { | |
setLoading(false); | |
} | |
}, [s3Url]); | |
useEffect(() => { | |
if (s3Url) { | |
fetchData(); | |
} | |
}, [s3Url, fetchData]); | |
const parseCSV = (csvText) => { | |
try { | |
const lines = csvText.trim().split("\n"); | |
if (lines.length < 2) return []; | |
// Handle different CSV formats and potential quotes | |
const parseCSVLine = (line) => { | |
const result = []; | |
let current = ""; | |
let inQuotes = false; | |
for (let i = 0; i < line.length; i++) { | |
const char = line[i]; | |
if (char === '"') { | |
inQuotes = !inQuotes; | |
} else if (char === "," && !inQuotes) { | |
result.push(current.trim()); | |
current = ""; | |
} else { | |
current += char; | |
} | |
} | |
result.push(current.trim()); | |
return result.map((value) => value.replace(/^"(.*)"$/, "$1")); // Remove outer quotes | |
}; | |
const headers = parseCSVLine(lines[0]); | |
const rows = []; | |
for (let i = 1; i < lines.length; i++) { | |
if (lines[i].trim() === "") continue; // Skip empty lines | |
const values = parseCSVLine(lines[i]); | |
if (values.length > 0 && values.some((val) => val.trim() !== "")) { | |
const row = {}; | |
headers.forEach((header, index) => { | |
row[header] = values[index] || ""; | |
}); | |
rows.push(row); | |
} | |
} | |
return rows; | |
} catch (err) { | |
debugLog("Error parsing CSV:", err); | |
throw new Error( | |
"Failed to parse CSV data. The file format may be invalid." | |
); | |
} | |
}; | |
const getPaginatedData = () => { | |
const startIndex = (currentPage - 1) * rowsPerPage; | |
const endIndex = startIndex + rowsPerPage; | |
return data.slice(startIndex, endIndex); | |
}; | |
const totalPages = Math.ceil(data.length / rowsPerPage); | |
const handleDownload = () => { | |
if (onDownload) { | |
onDownload(); | |
} else { | |
window.open(s3Url, "_blank"); | |
} | |
}; | |
if (loading) { | |
return ( | |
<div className="data-viewer loading"> | |
<div className="spinner"></div> | |
<p>Loading data preview...</p> | |
</div> | |
); | |
} | |
if (error) { | |
return ( | |
<div className="data-viewer error"> | |
<div className="status-message error"> | |
<div className="status-message-icon">β</div> | |
<div className="status-message-content"> | |
<h4>Unable to Preview Data</h4> | |
<p>{error}</p> | |
<div | |
className="error-help" | |
style={{ marginTop: "0.75rem", fontSize: "0.875rem" }} | |
> | |
<strong> | |
Don't worry! Your data has been generated successfully. | |
</strong> | |
<br /> | |
<strong>Possible solutions:</strong> | |
<ul | |
style={{ | |
marginTop: "0.5rem", | |
paddingLeft: "1.5rem", | |
textAlign: "left", | |
}} | |
> | |
{!showPreviewOnly && ( | |
<li> | |
<strong>Download the file directly</strong> using the button | |
below | |
</li> | |
)} | |
<li> | |
The preview may fail due to browser security restrictions | |
</li> | |
<li> | |
Your generated data is still available and ready | |
{showPreviewOnly ? "" : " to download"} | |
</li> | |
</ul> | |
</div> | |
</div> | |
</div> | |
<div | |
className="error-actions" | |
style={{ marginTop: "1.5rem", textAlign: "center" }} | |
> | |
{!showPreviewOnly && ( | |
<button | |
className="btn btn-primary btn-large" | |
onClick={handleDownload} | |
style={{ | |
marginRight: "0.75rem", | |
padding: "12px 24px", | |
fontSize: "1rem", | |
}} | |
> | |
π₯ Download Generated Data | |
</button> | |
)} | |
<button | |
className="btn btn-secondary" | |
onClick={() => fetchData()} | |
style={{ marginLeft: showPreviewOnly ? "0" : "0.75rem" }} | |
> | |
π Try Preview Again | |
</button> | |
</div> | |
<div | |
className="success-note" | |
style={{ | |
marginTop: "1.5rem", | |
padding: "1rem", | |
background: "var(--success-light, #e8f5e8)", | |
borderRadius: "8px", | |
border: "1px solid var(--success, #28a745)", | |
textAlign: "center", | |
}} | |
> | |
<div | |
style={{ | |
color: "var(--success, #28a745)", | |
fontWeight: "bold", | |
marginBottom: "0.5rem", | |
}} | |
> | |
β Data Generation Completed Successfully! | |
</div> | |
<div style={{ fontSize: "0.875rem", color: "var(--text-secondary)" }}> | |
The preview failed, but your synthetic data has been generated and | |
is ready for download. | |
</div> | |
</div> | |
<TroubleshootingGuide | |
generatedDataLink={s3Url} | |
onDownload={handleDownload} | |
/> | |
</div> | |
); | |
} | |
if (data.length === 0) { | |
return ( | |
<div className="data-viewer empty"> | |
<div className="status-message warning"> | |
<div className="status-message-icon">β οΈ</div> | |
<div className="status-message-content"> | |
<h4>No Data Available</h4> | |
<p>The generated file appears to be empty.</p> | |
</div> | |
</div> | |
{!showPreviewOnly && ( | |
<div className="download-section" style={{ marginTop: "1rem" }}> | |
<button className="btn btn-primary" onClick={handleDownload}> | |
π₯ Download File | |
</button> | |
</div> | |
)} | |
</div> | |
); | |
} | |
return ( | |
<div className="data-viewer"> | |
<div className="data-viewer-header"> | |
<div className="data-info"> | |
<h4>π Generated Data {showPreviewOnly ? "Preview" : ""}</h4> | |
<p> | |
Showing {getPaginatedData().length} of {data.length} rows β’{" "} | |
{columns.length} columns | |
</p> | |
</div> | |
{!showPreviewOnly && ( | |
<button | |
className="btn btn-success download-btn" | |
onClick={handleDownload} | |
> | |
π₯ Download Complete File | |
</button> | |
)} | |
</div> | |
<div className="data-table-container"> | |
<table className="data-table"> | |
<thead> | |
<tr> | |
{columns.map((column, index) => ( | |
<th key={index} title={column}> | |
{column} | |
</th> | |
))} | |
</tr> | |
</thead> | |
<tbody> | |
{getPaginatedData().map((row, index) => ( | |
<tr key={index}> | |
{columns.map((column, colIndex) => ( | |
<td key={colIndex} title={row[column]}> | |
{row[column]} | |
</td> | |
))} | |
</tr> | |
))} | |
</tbody> | |
</table> | |
</div> | |
{totalPages > 1 && ( | |
<div className="pagination"> | |
<button | |
className="btn btn-secondary" | |
onClick={() => setCurrentPage((prev) => Math.max(prev - 1, 1))} | |
disabled={currentPage === 1} | |
> | |
β Previous | |
</button> | |
<span className="pagination-info"> | |
Page {currentPage} of {totalPages} | |
</span> | |
<button | |
className="btn btn-secondary" | |
onClick={() => | |
setCurrentPage((prev) => Math.min(prev + 1, totalPages)) | |
} | |
disabled={currentPage === totalPages} | |
> | |
Next β | |
</button> | |
</div> | |
)} | |
{showPreviewOnly && data.length > 0 && ( | |
<div | |
className="preview-note" | |
style={{ | |
marginTop: "1rem", | |
padding: "0.75rem", | |
background: "var(--bg-tertiary)", | |
borderRadius: "8px", | |
fontSize: "0.875rem", | |
color: "var(--text-secondary)", | |
textAlign: "center", | |
}} | |
> | |
π‘ Showing first {Math.min(data.length, rowsPerPage * totalPages)}{" "} | |
rows. Download the complete file to view all {data.length} rows. | |
</div> | |
)} | |
</div> | |
); | |
}; | |
export default DataViewer; | |