Files
parcer/src/components/run/RunContent.tsx
2025-04-30 22:08:39 +05:30

598 lines
20 KiB
TypeScript

import {
Box,
Tabs,
Typography,
Tab,
Paper,
Button,
CircularProgress,
Accordion,
AccordionSummary,
AccordionDetails,
ButtonGroup
} from "@mui/material";
import Highlight from "react-highlight";
import * as React from "react";
import { Data } from "./RunsTable";
import { TabPanel, TabContext } from "@mui/lab";
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
import { useEffect, useState } from "react";
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import 'highlight.js/styles/github.css';
import { useTranslation } from "react-i18next";
interface RunContentProps {
row: Data,
currentLog: string,
interpretationInProgress: boolean,
logEndRef: React.RefObject<HTMLDivElement>,
abortRunHandler: () => void,
}
export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRef, abortRunHandler }: RunContentProps) => {
const { t } = useTranslation();
const [tab, setTab] = React.useState<string>('output');
const [schemaData, setSchemaData] = useState<any[]>([]);
const [schemaColumns, setSchemaColumns] = useState<string[]>([]);
const [listData, setListData] = useState<any[][]>([]);
const [listColumns, setListColumns] = useState<string[][]>([]);
const [currentListIndex, setCurrentListIndex] = useState<number>(0);
const [screenshotKeys, setScreenshotKeys] = useState<string[]>([]);
const [currentScreenshotIndex, setCurrentScreenshotIndex] = useState<number>(0);
const [legacyData, setLegacyData] = useState<any[]>([]);
const [legacyColumns, setLegacyColumns] = useState<string[]>([]);
const [isLegacyData, setIsLegacyData] = useState<boolean>(false);
useEffect(() => {
setTab(tab);
}, [interpretationInProgress]);
useEffect(() => {
if (!row.serializableOutput) return;
if (!row.serializableOutput.scrapeSchema &&
!row.serializableOutput.scrapeList &&
Object.keys(row.serializableOutput).length > 0) {
setIsLegacyData(true);
processLegacyData(row.serializableOutput);
return;
}
setIsLegacyData(false);
if (row.serializableOutput.scrapeSchema && Object.keys(row.serializableOutput.scrapeSchema).length > 0) {
processDataCategory(row.serializableOutput.scrapeSchema, setSchemaData, setSchemaColumns);
}
if (row.serializableOutput.scrapeList) {
processScrapeList(row.serializableOutput.scrapeList);
}
}, [row.serializableOutput]);
useEffect(() => {
if (row.binaryOutput && Object.keys(row.binaryOutput).length > 0) {
setScreenshotKeys(Object.keys(row.binaryOutput));
setCurrentScreenshotIndex(0);
}
}, [row.binaryOutput]);
const processLegacyData = (legacyOutput: Record<string, any>) => {
let allData: any[] = [];
Object.keys(legacyOutput).forEach(key => {
const data = legacyOutput[key];
if (Array.isArray(data)) {
const filteredData = data.filter(row =>
Object.values(row).some(value => value !== undefined && value !== "")
);
allData = [...allData, ...filteredData];
}
});
if (allData.length > 0) {
const allColumns = new Set<string>();
allData.forEach(item => {
Object.keys(item).forEach(key => allColumns.add(key));
});
setLegacyData(allData);
setLegacyColumns(Array.from(allColumns));
}
};
const processDataCategory = (
categoryData: Record<string, any>,
setData: React.Dispatch<React.SetStateAction<any[]>>,
setColumns: React.Dispatch<React.SetStateAction<string[]>>
) => {
let allData: any[] = [];
Object.keys(categoryData).forEach(key => {
const data = categoryData[key];
if (Array.isArray(data)) {
const filteredData = data.filter(row =>
Object.values(row).some(value => value !== undefined && value !== "")
);
allData = [...allData, ...filteredData];
}
});
if (allData.length > 0) {
const allColumns = new Set<string>();
allData.forEach(item => {
Object.keys(item).forEach(key => allColumns.add(key));
});
setData(allData);
setColumns(Array.from(allColumns));
}
};
const processScrapeList = (scrapeListData: any) => {
const tablesList: any[][] = [];
const columnsList: string[][] = [];
if (Array.isArray(scrapeListData)) {
scrapeListData.forEach(tableData => {
if (Array.isArray(tableData) && tableData.length > 0) {
const filteredData = tableData.filter(row =>
Object.values(row).some(value => value !== undefined && value !== "")
);
if (filteredData.length > 0) {
tablesList.push(filteredData);
const tableColumns = new Set<string>();
filteredData.forEach(item => {
Object.keys(item).forEach(key => tableColumns.add(key));
});
columnsList.push(Array.from(tableColumns));
}
}
});
} else if (typeof scrapeListData === 'object') {
Object.keys(scrapeListData).forEach(key => {
const tableData = scrapeListData[key];
if (Array.isArray(tableData) && tableData.length > 0) {
const filteredData = tableData.filter(row =>
Object.values(row).some(value => value !== undefined && value !== "")
);
if (filteredData.length > 0) {
tablesList.push(filteredData);
const tableColumns = new Set<string>();
filteredData.forEach(item => {
Object.keys(item).forEach(key => tableColumns.add(key));
});
columnsList.push(Array.from(tableColumns));
}
}
});
}
setListData(tablesList);
setListColumns(columnsList);
setCurrentListIndex(0);
};
// Function to convert table data to CSV format
const convertToCSV = (data: any[], columns: string[]): string => {
const header = columns.join(',');
const rows = data.map(row =>
columns.map(col => JSON.stringify(row[col] || "", null, 2)).join(',')
);
return [header, ...rows].join('\n');
};
// Function to download a specific dataset as CSV
const downloadCSV = (data: any[], columns: string[], filename: string) => {
const csvContent = convertToCSV(data, columns);
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.setAttribute("download", filename);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
const downloadJSON = (data: any[], filename: string) => {
const jsonContent = JSON.stringify(data, null, 2);
const blob = new Blob([jsonContent], { type: 'application/json;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.setAttribute("download", filename);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
setTimeout(() => {
URL.revokeObjectURL(url);
}, 100);
};
const navigateListTable = (direction: 'next' | 'prev') => {
if (direction === 'next' && currentListIndex < listData.length - 1) {
setCurrentListIndex(currentListIndex + 1);
} else if (direction === 'prev' && currentListIndex > 0) {
setCurrentListIndex(currentListIndex - 1);
}
};
const navigateScreenshots = (direction: 'next' | 'prev') => {
if (direction === 'next' && currentScreenshotIndex < screenshotKeys.length - 1) {
setCurrentScreenshotIndex(currentScreenshotIndex + 1);
} else if (direction === 'prev' && currentScreenshotIndex > 0) {
setCurrentScreenshotIndex(currentScreenshotIndex - 1);
}
};
const renderDataTable = (
data: any[],
columns: string[],
title: string,
csvFilename: string,
jsonFilename: string,
isPaginatedList: boolean = false
) => {
if (!isPaginatedList && data.length === 0) return null;
if (isPaginatedList && (listData.length === 0 || currentListIndex >= listData.length)) return null;
const currentData = isPaginatedList ? listData[currentListIndex] : data;
const currentColumns = isPaginatedList ? listColumns[currentListIndex] : columns;
if (!currentData || currentData.length === 0) return null;
return (
<Accordion defaultExpanded sx={{ mb: 2 }}>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls={`${title.toLowerCase()}-content`}
id={`${title.toLowerCase()}-header`}
>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Typography variant='h6'>
{title}
</Typography>
</Box>
</AccordionSummary>
<AccordionDetails>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
<Box>
<Button
component="a"
onClick={() => downloadJSON(data, jsonFilename)}
sx={{
color: '#FF00C3',
textTransform: 'none',
mr: 2,
p: 0,
minWidth: 'auto',
backgroundColor: 'transparent',
'&:hover': {
backgroundColor: 'transparent',
textDecoration: 'underline'
}
}}
>
Download as JSON
</Button>
<Button
component="a"
onClick={() => downloadCSV(data, columns, csvFilename)}
sx={{
color: '#FF00C3',
textTransform: 'none',
p: 0,
minWidth: 'auto',
backgroundColor: 'transparent',
'&:hover': {
backgroundColor: 'transparent',
textDecoration: 'underline'
}
}}
>
Download as CSV
</Button>
</Box>
{isPaginatedList && listData.length > 1 && (
<ButtonGroup size="small">
<Button
onClick={() => navigateListTable('prev')}
disabled={currentListIndex === 0}
sx={{
borderColor: '#FF00C3',
color: currentListIndex === 0 ? 'gray' : '#FF00C3',
'&.Mui-disabled': {
borderColor: 'rgba(0, 0, 0, 0.12)'
}
}}
>
<ArrowBackIcon />
</Button>
<Button
onClick={() => navigateListTable('next')}
disabled={currentListIndex === listData.length - 1}
sx={{
color: currentListIndex === listData.length - 1 ? 'gray' : '#FF00C3',
'&.Mui-disabled': {
borderColor: 'rgba(0, 0, 0, 0.12)'
}
}}
>
<ArrowForwardIcon />
</Button>
</ButtonGroup>
)}
</Box>
<TableContainer component={Paper} sx={{ maxHeight: 320 }}>
<Table stickyHeader aria-label="sticky table">
<TableHead>
<TableRow>
{(isPaginatedList ? currentColumns : columns).map((column) => (
<TableCell key={column}>{column}</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{(isPaginatedList ? currentData : data).map((row, index) => (
<TableRow key={index}>
{(isPaginatedList ? currentColumns : columns).map((column) => (
<TableCell key={column}>
{row[column] === undefined || row[column] === "" ? "-" : row[column]}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</AccordionDetails>
</Accordion>
);
};
const hasData = schemaData.length > 0 || listData.length > 0 || legacyData.length > 0;
const hasScreenshots = row.binaryOutput && Object.keys(row.binaryOutput).length > 0;
return (
<Box sx={{ width: '100%' }}>
<TabContext value={tab}>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs
value={tab}
onChange={(e, newTab) => setTab(newTab)}
aria-label="run-content-tabs"
sx={{
'& .MuiTabs-indicator': {
backgroundColor: '#FF00C3',
},
'& .MuiTab-root': {
'&.Mui-selected': {
color: '#FF00C3',
},
}
}}
>
<Tab
label={t('run_content.tabs.output_data')}
value='output'
sx={{
color: (theme) => theme.palette.mode === 'dark' ? '#fff' : '#000',
'&:hover': {
color: '#FF00C3'
},
'&.Mui-selected': {
color: '#FF00C3',
}
}}
/>
<Tab
label={t('run_content.tabs.log')}
value='log'
sx={{
color: (theme) => theme.palette.mode === 'dark' ? '#fff' : '#000',
'&:hover': {
color: '#FF00C3'
},
'&.Mui-selected': {
color: '#FF00C3',
}
}}
/>
</Tabs>
</Box>
<TabPanel value='log'>
<Box sx={{
margin: 1,
background: '#19171c',
overflowY: 'scroll',
overflowX: 'scroll',
width: '700px',
height: 'fit-content',
maxHeight: '450px',
}}>
<div>
<Highlight className="javascript">
{row.status === 'running' ? currentLog : row.log}
</Highlight>
<div style={{ float: "left", clear: "both" }}
ref={logEndRef} />
</div>
</Box>
{row.status === 'running' ? <Button
color="error"
onClick={abortRunHandler}
>
{t('run_content.buttons.stop')}
</Button> : null}
</TabPanel>
<TabPanel value='output' sx={{ width: '100%', maxWidth: '900px' }}>
{row.status === 'running' || row.status === 'queued' ? (
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<CircularProgress size={22} sx={{ marginRight: '10px' }} />
{t('run_content.loading')}
</Box>
) : (!hasData && !hasScreenshots
? <Typography>{t('run_content.empty_output')}</Typography>
: null)}
{hasData && (
<Box sx={{ mb: 3 }}>
{isLegacyData && (
renderDataTable(
legacyData,
legacyColumns,
t('run_content.captured_data.title'),
'data.csv',
'data.json'
)
)}
{!isLegacyData && (
<>
{renderDataTable(
schemaData,
schemaColumns,
t('run_content.captured_data.schema_title'),
'schema_data.csv',
'schema_data.json'
)}
{listData.length > 0 && renderDataTable(
[],
[],
t('run_content.captured_data.list_title'),
'list_data.csv',
'list_data.json',
true
)}
</>
)}
</Box>
)}
{hasScreenshots && (
<>
<Accordion defaultExpanded sx={{ mb: 2 }}>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls="screenshot-content"
id="screenshot-header"
>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Typography variant='h6'>
{t('run_content.captured_screenshot.title', 'Screenshots')}
</Typography>
</Box>
</AccordionSummary>
<AccordionDetails>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
<Button
onClick={() => {
fetch(row.binaryOutput[screenshotKeys[currentScreenshotIndex]])
.then(response => response.blob())
.then(blob => {
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = screenshotKeys[currentScreenshotIndex];
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
})
.catch(err => console.error('Download failed:', err));
}}
sx={{
color: '#FF00C3',
textTransform: 'none',
p: 0,
minWidth: 'auto',
backgroundColor: 'transparent',
'&:hover': {
backgroundColor: 'transparent',
textDecoration: 'underline'
}
}}
>
{t('run_content.captured_screenshot.download', 'Download')}
</Button>
{screenshotKeys.length > 1 && (
<ButtonGroup size="small">
<Button
onClick={() => navigateScreenshots('prev')}
disabled={currentScreenshotIndex === 0}
sx={{
borderColor: '#FF00C3',
color: currentScreenshotIndex === 0 ? 'gray' : '#FF00C3',
'&.Mui-disabled': {
borderColor: 'rgba(0, 0, 0, 0.12)'
}
}}
>
<ArrowBackIcon />
</Button>
<Button
onClick={() => navigateScreenshots('next')}
disabled={currentScreenshotIndex === screenshotKeys.length - 1}
sx={{
borderColor: '#FF00C3',
color: currentScreenshotIndex === screenshotKeys.length - 1 ? 'gray' : '#FF00C3',
'&.Mui-disabled': {
borderColor: 'rgba(0, 0, 0, 0.12)'
}
}}
>
<ArrowForwardIcon />
</Button>
</ButtonGroup>
)}
</Box>
<Box sx={{ mt: 1 }}>
<Box>
<img
src={row.binaryOutput[screenshotKeys[currentScreenshotIndex]]}
alt={`Screenshot ${screenshotKeys[currentScreenshotIndex]}`}
style={{
maxWidth: '100%',
height: 'auto',
border: '1px solid #e0e0e0',
borderRadius: '4px'
}}
/>
</Box>
</Box>
</AccordionDetails>
</Accordion>
</>
)}
</TabPanel>
</TabContext>
</Box>
);
};