Files
parcer/src/components/run/RunContent.tsx

971 lines
34 KiB
TypeScript
Raw Normal View History

2025-04-30 19:58:27 +05:30
import {
Box,
Tabs,
Typography,
Tab,
Paper,
Button,
CircularProgress,
Accordion,
AccordionSummary,
AccordionDetails,
Card,
CardHeader,
CardContent,
Grid,
IconButton,
Chip,
ButtonGroup
} from "@mui/material";
2024-06-24 22:38:29 +05:30
import Highlight from "react-highlight";
import * as React from "react";
import { Data } from "./RunsTable";
import { TabPanel, TabContext } from "@mui/lab";
2024-11-12 10:35:49 +05:30
import ImageIcon from '@mui/icons-material/Image';
2025-04-29 00:33:04 +05:30
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import DownloadIcon from '@mui/icons-material/Download';
import FullscreenIcon from '@mui/icons-material/Fullscreen';
import DataObjectIcon from '@mui/icons-material/DataObject';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
2024-10-11 01:19:05 +05:30
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';
2024-10-24 04:40:05 +05:30
import 'highlight.js/styles/github.css';
2024-12-21 17:41:53 +05:30
import { useTranslation } from "react-i18next";
2024-06-24 22:38:29 +05:30
interface RunContentProps {
row: Data,
currentLog: string,
interpretationInProgress: boolean,
logEndRef: React.RefObject<HTMLDivElement>,
abortRunHandler: () => void,
}
2024-09-27 23:40:01 +05:30
export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRef, abortRunHandler }: RunContentProps) => {
2024-12-21 17:41:53 +05:30
const { t } = useTranslation();
const [tab, setTab] = React.useState<string>('output');
2025-04-30 19:57:38 +05:30
2025-04-29 00:33:04 +05:30
const [schemaData, setSchemaData] = useState<any[]>([]);
const [schemaColumns, setSchemaColumns] = useState<string[]>([]);
2025-04-30 19:57:38 +05:30
2025-04-29 00:33:04 +05:30
const [listData, setListData] = useState<any[][]>([]);
const [listColumns, setListColumns] = useState<string[][]>([]);
const [currentListIndex, setCurrentListIndex] = useState<number>(0);
const [expandedView, setExpandedView] = useState<string | null>(null);
2025-04-30 19:57:38 +05:30
2025-04-29 00:33:04 +05:30
const [viewMode, setViewMode] = useState<'horizontal' | 'vertical'>('vertical');
const [legacyData, setLegacyData] = useState<any[]>([]);
const [legacyColumns, setLegacyColumns] = useState<string[]>([]);
const [isLegacyData, setIsLegacyData] = useState<boolean>(false);
2024-06-24 22:38:29 +05:30
useEffect(() => {
setTab(tab);
2024-11-12 10:35:49 +05:30
}, [interpretationInProgress]);
2024-06-24 22:38:29 +05:30
2024-11-20 05:14:34 +05:30
useEffect(() => {
2025-04-29 00:33:04 +05:30
if (!row.serializableOutput) return;
2025-04-30 19:57:38 +05:30
if (!row.serializableOutput.scrapeSchema &&
!row.serializableOutput.scrapeList &&
Object.keys(row.serializableOutput).length > 0) {
2025-04-29 00:33:04 +05:30
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);
}
2025-04-30 19:57:38 +05:30
2025-04-29 00:33:04 +05:30
if (row.serializableOutput.scrapeList) {
processScrapeList(row.serializableOutput.scrapeList);
}
}, [row.serializableOutput]);
const processLegacyData = (legacyOutput: Record<string, any>) => {
let allData: any[] = [];
2025-04-30 19:57:38 +05:30
2025-04-29 00:33:04 +05:30
Object.keys(legacyOutput).forEach(key => {
const data = legacyOutput[key];
2024-11-20 05:14:34 +05:30
if (Array.isArray(data)) {
const filteredData = data.filter(row =>
Object.values(row).some(value => value !== undefined && value !== "")
);
2025-04-29 00:33:04 +05:30
allData = [...allData, ...filteredData];
2024-10-11 01:19:05 +05:30
}
2025-04-29 00:33:04 +05:30
});
2025-04-30 19:57:38 +05:30
2025-04-29 00:33:04 +05:30
if (allData.length > 0) {
const allColumns = new Set<string>();
allData.forEach(item => {
Object.keys(item).forEach(key => allColumns.add(key));
});
2025-04-30 19:57:38 +05:30
2025-04-29 00:33:04 +05:30
setLegacyData(allData);
setLegacyColumns(Array.from(allColumns));
2024-10-11 01:19:05 +05:30
}
2025-04-29 00:33:04 +05:30
};
2024-11-20 05:14:18 +05:30
2025-04-29 00:33:04 +05:30
const processDataCategory = (
categoryData: Record<string, any>,
setData: React.Dispatch<React.SetStateAction<any[]>>,
setColumns: React.Dispatch<React.SetStateAction<string[]>>
) => {
let allData: any[] = [];
2025-04-30 19:57:38 +05:30
2025-04-29 00:33:04 +05:30
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];
}
});
2025-04-30 19:57:38 +05:30
2025-04-29 00:33:04 +05:30
if (allData.length > 0) {
const allColumns = new Set<string>();
allData.forEach(item => {
Object.keys(item).forEach(key => allColumns.add(key));
});
2025-04-30 19:57:38 +05:30
2025-04-29 00:33:04 +05:30
setData(allData);
setColumns(Array.from(allColumns));
}
};
const processScrapeList = (scrapeListData: any) => {
const tablesList: any[][] = [];
const columnsList: string[][] = [];
2025-04-30 19:57:38 +05:30
2025-04-29 00:33:04 +05:30
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 !== "")
);
2025-04-30 19:57:38 +05:30
2025-04-29 00:33:04 +05:30
if (filteredData.length > 0) {
tablesList.push(filteredData);
2025-04-30 19:57:38 +05:30
2025-04-29 00:33:04 +05:30
const tableColumns = new Set<string>();
filteredData.forEach(item => {
Object.keys(item).forEach(key => tableColumns.add(key));
});
2025-04-30 19:57:38 +05:30
2025-04-29 00:33:04 +05:30
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 !== "")
);
2025-04-30 19:57:38 +05:30
2025-04-29 00:33:04 +05:30
if (filteredData.length > 0) {
tablesList.push(filteredData);
2025-04-30 19:57:38 +05:30
2025-04-29 00:33:04 +05:30
const tableColumns = new Set<string>();
filteredData.forEach(item => {
Object.keys(item).forEach(key => tableColumns.add(key));
});
2025-04-30 19:57:38 +05:30
2025-04-29 00:33:04 +05:30
columnsList.push(Array.from(tableColumns));
}
}
});
}
2025-04-30 19:57:38 +05:30
2025-04-29 00:33:04 +05:30
setListData(tablesList);
setListColumns(columnsList);
setCurrentListIndex(0);
};
2024-10-11 01:19:05 +05:30
2024-11-11 23:56:56 +05:30
// Function to convert table data to CSV format
const convertToCSV = (data: any[], columns: string[]): string => {
2024-11-12 10:35:49 +05:30
const header = columns.join(',');
const rows = data.map(row =>
2025-04-29 00:33:04 +05:30
columns.map(col => JSON.stringify(row[col] || "", null, 2)).join(',')
2024-11-12 10:35:49 +05:30
);
return [header, ...rows].join('\n');
};
2024-11-11 23:56:56 +05:30
2025-04-29 00:33:04 +05:30
// Function to download a specific dataset as CSV
const downloadCSV = (data: any[], columns: string[], filename: string) => {
const csvContent = convertToCSV(data, columns);
2024-11-12 10:35:49 +05:30
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
2024-11-11 23:56:56 +05:30
const link = document.createElement("a");
link.href = url;
2025-04-29 00:33:04 +05:30
link.setAttribute("download", filename);
2024-11-11 23:56:56 +05:30
document.body.appendChild(link);
2024-11-12 10:35:49 +05:30
link.click();
document.body.removeChild(link);
};
2024-11-11 23:56:56 +05:30
2025-04-29 00:33:04 +05:30
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);
2025-04-30 19:57:38 +05:30
2025-04-29 00:33:04 +05:30
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 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;
2025-04-30 19:57:38 +05:30
2025-04-29 00:33:04 +05:30
const currentData = isPaginatedList ? listData[currentListIndex] : data;
const currentColumns = isPaginatedList ? listColumns[currentListIndex] : columns;
2025-04-30 19:57:38 +05:30
2025-04-29 00:33:04 +05:30
if (!currentData || currentData.length === 0) return null;
2025-04-30 19:57:38 +05:30
2025-04-29 00:33:04 +05:30
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' sx={{ ml: 2 }}>
{title}
</Typography>
</Box>
</AccordionSummary>
<AccordionDetails>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
<ButtonGroup size="small" variant="outlined">
<Button
startIcon={<DownloadIcon />}
onClick={() => {
if (isPaginatedList) {
2025-04-30 19:57:38 +05:30
downloadCSV(currentData, currentColumns, `list_table_${currentListIndex + 1}.csv`);
2025-04-29 00:33:04 +05:30
} else {
downloadCSV(data, columns, csvFilename);
}
}}
sx={{ borderColor: '#FF00C3', color: '#FF00C3' }}
>
CSV
</Button>
<Button
startIcon={<DataObjectIcon />}
onClick={() => {
if (isPaginatedList) {
2025-04-30 19:57:38 +05:30
downloadJSON(currentData, `list_table_${currentListIndex + 1}.json`);
2025-04-29 00:33:04 +05:30
} else {
downloadJSON(data, jsonFilename);
}
}}
sx={{ borderColor: '#FF00C3', color: '#FF00C3' }}
>
JSON
</Button>
</ButtonGroup>
2025-04-30 19:57:38 +05:30
2025-04-29 00:33:04 +05:30
{isPaginatedList && listData.length > 1 && (
<ButtonGroup size="small">
<Button
onClick={() => navigateListTable('prev')}
disabled={currentListIndex === 0}
2025-04-30 19:57:38 +05:30
sx={{
borderColor: '#FF00C3',
2025-04-29 00:33:04 +05:30
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}
2025-04-30 19:57:38 +05:30
sx={{
2025-04-29 00:33:04 +05:30
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 renderDataCard = (
data: any[],
columns: string[],
title: string,
dataType: 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;
2025-04-30 19:57:38 +05:30
2025-04-29 00:33:04 +05:30
const currentData = isPaginatedList ? listData[currentListIndex] : data;
const currentColumns = isPaginatedList ? listColumns[currentListIndex] : columns;
2025-04-30 19:57:38 +05:30
2025-04-29 00:33:04 +05:30
if (!currentData || currentData.length === 0) return null;
2025-04-30 19:57:38 +05:30
2025-04-29 00:33:04 +05:30
const previewData = currentData.slice(0, 1);
const previewColumns = currentColumns.slice(0, 3);
2025-04-30 19:57:38 +05:30
2025-04-29 00:33:04 +05:30
const showMoreColumns = currentColumns.length > 3;
2025-04-30 19:57:38 +05:30
2025-04-29 00:33:04 +05:30
return (
2025-04-30 19:57:38 +05:30
<Card sx={{
width: '100%',
2025-04-29 00:33:04 +05:30
mb: 3,
height: '100%',
display: 'flex',
flexDirection: 'column',
boxShadow: 3
}}>
<CardHeader
title={title}
action={
<Box>
2025-04-30 19:57:38 +05:30
<IconButton
size="small"
2025-04-29 00:33:04 +05:30
onClick={() => {
if (isPaginatedList) {
2025-04-30 19:57:38 +05:30
downloadCSV(currentData, currentColumns, `list_table_${currentListIndex + 1}.csv`);
2025-04-29 00:33:04 +05:30
} else {
downloadCSV(data, columns, csvFilename);
}
}}
title={t('run_content.captured_data.download_csv')}
>
<DownloadIcon />
</IconButton>
2025-04-30 19:57:38 +05:30
<IconButton
size="small"
2025-04-29 00:33:04 +05:30
onClick={() => {
if (isPaginatedList) {
2025-04-30 19:57:38 +05:30
downloadJSON(currentData, `list_table_${currentListIndex + 1}.json`);
2025-04-29 00:33:04 +05:30
} else {
downloadJSON(data, jsonFilename);
}
}}
title="Download JSON"
sx={{ mx: 0.5 }}
>
<DataObjectIcon />
</IconButton>
2025-04-30 19:57:38 +05:30
<IconButton
2025-04-29 00:33:04 +05:30
size="small"
onClick={() => {
if (isPaginatedList) {
setExpandedView(`list-${currentListIndex}`);
} else {
setExpandedView(dataType);
}
}}
title={t('run_content.captured_data.view_full')}
>
<FullscreenIcon />
</IconButton>
</Box>
}
sx={{ pb: 1 }}
/>
<CardContent sx={{ pt: 0, pb: 1, flexGrow: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
{isPaginatedList ? (
2025-04-30 19:57:38 +05:30
<Chip
label={listData.length > 1
2025-04-29 00:33:04 +05:30
? `Table ${currentListIndex + 1} of ${listData.length} (${currentData.length} ${currentData.length === 1 ? 'item' : 'items'})`
: `${currentData.length} ${currentData.length === 1 ? 'item' : 'items'}`
2025-04-30 19:57:38 +05:30
}
size="small"
sx={{ backgroundColor: '#FF00C3', color: 'white' }}
2025-04-29 00:33:04 +05:30
/>
) : (
2025-04-30 19:57:38 +05:30
<Chip
label={`${data.length} ${data.length === 1 ? 'item' : 'items'}`}
size="small"
sx={{ backgroundColor: '#FF00C3', color: 'white' }}
2025-04-29 00:33:04 +05:30
/>
)}
2025-04-30 19:57:38 +05:30
2025-04-29 00:33:04 +05:30
{isPaginatedList && listData.length > 1 && (
<ButtonGroup size="small">
<Button
onClick={() => navigateListTable('prev')}
disabled={currentListIndex === 0}
2025-04-30 19:57:38 +05:30
sx={{
borderColor: '#FF00C3',
2025-04-29 00:33:04 +05:30
color: currentListIndex === 0 ? 'gray' : '#FF00C3',
'&.Mui-disabled': {
borderColor: 'rgba(0, 0, 0, 0.12)'
},
padding: '0 8px',
minWidth: 'auto'
}}
>
&lt;
</Button>
<Button
onClick={() => navigateListTable('next')}
disabled={currentListIndex === listData.length - 1}
2025-04-30 19:57:38 +05:30
sx={{
borderColor: '#FF00C3',
2025-04-29 00:33:04 +05:30
color: currentListIndex === listData.length - 1 ? 'gray' : '#FF00C3',
'&.Mui-disabled': {
borderColor: 'rgba(0, 0, 0, 0.12)'
},
padding: '0 8px',
minWidth: 'auto'
}}
>
&gt;
</Button>
</ButtonGroup>
)}
</Box>
<TableContainer component={Paper} sx={{ maxHeight: 180 }}>
<Table size="small" aria-label="preview table">
<TableHead>
<TableRow>
{previewColumns.map((column) => (
<TableCell key={column}>{column}</TableCell>
))}
{showMoreColumns && <TableCell>...</TableCell>}
</TableRow>
</TableHead>
<TableBody>
{previewData.map((row, index) => (
<TableRow key={index}>
{previewColumns.map((column) => (
<TableCell key={column}>
{row[column] === undefined || row[column] === "" ? "-" : row[column]}
</TableCell>
))}
{showMoreColumns && <TableCell>...</TableCell>}
</TableRow>
))}
{currentData.length > 1 && (
<TableRow>
<TableCell colSpan={previewColumns.length + (showMoreColumns ? 1 : 0)} align="center">
2025-04-30 19:57:38 +05:30
<Button
size="small"
2025-04-29 00:33:04 +05:30
onClick={() => {
if (isPaginatedList) {
setExpandedView(`list-${currentListIndex}`);
} else {
setExpandedView(dataType);
}
}}
sx={{ color: '#FF00C3', mt: 1 }}
>
View all {currentData.length} items
</Button>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
</CardContent>
</Card>
);
};
const renderExpandedView = (dataTypeWithIndex: string) => {
if (expandedView !== dataTypeWithIndex) return null;
let data: any[] = [];
let columns: string[] = [];
let title = "";
let csvFilename = "";
let jsonFilename = "";
if (dataTypeWithIndex.startsWith('list-')) {
const indexStr = dataTypeWithIndex.split('-')[1];
const index = parseInt(indexStr, 10);
2025-04-30 19:57:38 +05:30
2025-04-29 00:33:04 +05:30
if (index >= 0 && index < listData.length) {
data = listData[index];
columns = listColumns[index];
2025-04-30 19:57:38 +05:30
title = `${t('run_content.captured_data.list_title')} - Table ${index + 1}`;
csvFilename = `list_table_${index + 1}.csv`;
jsonFilename = `list_table_${index + 1}.json`;
2025-04-29 00:33:04 +05:30
}
} else {
switch (dataTypeWithIndex) {
case 'schema':
data = schemaData;
columns = schemaColumns;
title = t('run_content.captured_data.schema_title');
csvFilename = 'schema_data.csv';
jsonFilename = 'schema_data.json';
break;
case 'list':
if (listData.length > 0 && listColumns.length > 0) {
data = listData[currentListIndex];
columns = listColumns[currentListIndex];
}
title = t('run_content.captured_data.list_title');
csvFilename = 'list_data.csv';
jsonFilename = 'list_data.json';
break;
case 'legacy':
data = legacyData;
columns = legacyColumns;
title = t('run_content.captured_data.title');
csvFilename = 'data.csv';
jsonFilename = 'data.json';
break;
}
}
return (
2025-04-30 19:57:38 +05:30
<Box sx={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0,0,0,0.7)',
2025-04-29 00:33:04 +05:30
zIndex: 9999,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
p: 4
}}>
2025-04-30 19:57:38 +05:30
<Box sx={{
bgcolor: 'background.paper',
borderRadius: 1,
boxShadow: 24,
p: 4,
width: '90%',
2025-04-29 00:33:04 +05:30
maxWidth: '1200px',
maxHeight: '90vh',
overflow: 'auto'
}}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 3 }}>
<Typography variant="h5">{title}</Typography>
<Box>
<ButtonGroup variant="outlined" size="small" sx={{ mr: 2 }}>
2025-04-30 19:57:38 +05:30
<Button
2025-04-29 00:33:04 +05:30
onClick={() => downloadCSV(data, columns, csvFilename)}
startIcon={<DownloadIcon />}
>
CSV
</Button>
2025-04-30 19:57:38 +05:30
<Button
2025-04-29 00:33:04 +05:30
onClick={() => downloadJSON(data, jsonFilename)}
startIcon={<DataObjectIcon />}
>
JSON
</Button>
</ButtonGroup>
2025-04-30 19:57:38 +05:30
<Button
variant="outlined"
color="secondary"
2025-04-29 00:33:04 +05:30
onClick={() => setExpandedView(null)}
>
Close
</Button>
</Box>
</Box>
2025-04-30 19:57:38 +05:30
2025-04-29 00:33:04 +05:30
<TableContainer component={Paper} sx={{ maxHeight: 'calc(90vh - 150px)' }}>
<Table stickyHeader aria-label="expanded data table">
<TableHead>
<TableRow>
{columns.map((column) => (
<TableCell key={column}>{column}</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{data.map((row, index) => (
<TableRow key={index}>
{columns.map((column) => (
<TableCell key={column}>
{row[column] === undefined || row[column] === "" ? "-" : row[column]}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Box>
</Box>
);
};
2025-04-30 19:24:38 +05:30
const hasData = schemaData.length > 0 || listData.length > 0 || legacyData.length > 0;
2025-04-29 00:33:04 +05:30
const hasScreenshots = row.binaryOutput && Object.keys(row.binaryOutput).length > 0;
2024-06-24 22:38:29 +05:30
return (
<Box sx={{ width: '100%' }}>
<TabContext value={tab}>
2024-09-27 23:40:01 +05:30
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
2025-01-09 19:18:04 +05:30
<Tabs
value={tab}
onChange={(e, newTab) => setTab(newTab)}
aria-label="run-content-tabs"
2025-01-08 12:50:46 +05:30
sx={{
2025-01-09 19:18:04 +05:30
'& .MuiTabs-indicator': {
2025-04-29 00:33:04 +05:30
backgroundColor: '#FF00C3',
2025-01-08 12:50:46 +05:30
},
2025-01-09 19:18:04 +05:30
'& .MuiTab-root': {
'&.Mui-selected': {
color: '#FF00C3',
},
2025-01-08 12:50:46 +05:30
}
2025-01-09 19:18:04 +05:30
}}
>
<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>
2024-06-24 22:38:29 +05:30
</Box>
2024-09-27 23:40:01 +05:30
<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}
2024-09-27 23:40:01 +05:30
</Highlight>
<div style={{ float: "left", clear: "both" }}
ref={logEndRef} />
</div>
</Box>
{row.status === 'running' ? <Button
2024-09-27 23:40:01 +05:30
color="error"
onClick={abortRunHandler}
>
2024-12-21 17:41:53 +05:30
{t('run_content.buttons.stop')}
2024-09-27 23:40:01 +05:30
</Button> : null}
</TabPanel>
2025-04-29 00:33:04 +05:30
<TabPanel value='output' sx={{ width: '100%', maxWidth: '900px' }}>
{row.status === 'running' || row.status === 'queued' ? (
2025-01-26 20:56:16 +05:30
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<CircularProgress size={22} sx={{ marginRight: '10px' }} />
{t('run_content.loading')}
</Box>
2025-04-29 00:33:04 +05:30
) : (!hasData && !hasScreenshots
2025-04-30 19:57:38 +05:30
? <Typography>{t('run_content.empty_output')}</Typography>
2025-01-26 20:56:16 +05:30
: null)}
2024-06-24 22:38:29 +05:30
2025-04-29 00:33:04 +05:30
{hasData && (
<Box sx={{ mb: 3 }}>
{isLegacyData && (
viewMode === 'vertical' ? (
renderDataTable(
legacyData,
legacyColumns,
t('run_content.captured_data.title'),
'data.csv',
'data.json'
2024-09-27 23:40:01 +05:30
)
2025-04-29 00:33:04 +05:30
) : (
<Grid container spacing={3}>
<Grid item xs={12} md={12}>
{renderDataCard(
legacyData,
legacyColumns,
t('run_content.captured_data.title'),
'legacy',
'data.csv',
'data.json'
)}
</Grid>
</Grid>
)
)}
2025-04-30 19:57:38 +05:30
2025-04-29 00:33:04 +05:30
{!isLegacyData && (
viewMode === 'vertical' ? (
<>
{renderDataTable(
schemaData,
schemaColumns,
t('run_content.captured_data.schema_title'),
'schema_data.csv',
'schema_data.json'
)}
2025-04-30 19:57:38 +05:30
2025-04-29 00:33:04 +05:30
{listData.length > 0 && renderDataTable(
2025-04-30 19:57:38 +05:30
[],
[],
2025-04-29 00:33:04 +05:30
t('run_content.captured_data.list_title'),
'list_data.csv',
'list_data.json',
2025-04-30 19:57:38 +05:30
true
2025-04-29 00:33:04 +05:30
)}
</>
) : (
<Grid container spacing={3}>
{(() => {
const dataCategoriesCount = [
2025-04-30 19:57:38 +05:30
schemaData.length > 0,
listData.length > 0,
2025-04-29 00:33:04 +05:30
].filter(Boolean).length;
2025-04-30 19:57:38 +05:30
2025-04-29 00:33:04 +05:30
const columnWidth = dataCategoriesCount === 1 ? 12 : dataCategoriesCount === 2 ? 6 : 4;
2025-04-30 19:57:38 +05:30
2025-04-29 00:33:04 +05:30
return (
<>
{schemaData.length > 0 && (
<Grid item xs={12} md={columnWidth} sx={{ display: 'flex' }}>
{renderDataCard(
schemaData,
schemaColumns,
t('run_content.captured_data.schema_title'),
'schema',
'schema_data.csv',
'schema_data.json'
)}
</Grid>
)}
2025-04-30 19:57:38 +05:30
2025-04-29 00:33:04 +05:30
{listData.length > 0 && (
<Grid item xs={12} md={columnWidth} sx={{ display: 'flex' }}>
{renderDataCard(
2025-04-30 19:57:38 +05:30
[],
[],
2025-04-29 00:33:04 +05:30
t('run_content.captured_data.list_title'),
'list',
'list_data.csv',
'list_data.json',
2025-04-30 19:57:38 +05:30
true
2025-04-29 00:33:04 +05:30
)}
</Grid>
)}
</>
);
})()}
</Grid>
)
)}
2025-04-30 19:57:38 +05:30
2025-04-29 00:33:04 +05:30
{renderExpandedView('schema')}
{renderExpandedView('legacy')}
2025-04-30 19:57:38 +05:30
2025-04-29 00:33:04 +05:30
{listData.map((_, index) => renderExpandedView(`list-${index}`))}
</Box>
)}
2025-04-30 19:57:38 +05:30
2025-04-29 00:33:04 +05:30
{hasScreenshots && (
<>
{viewMode === 'vertical' ? (
<>
{Object.keys(row.binaryOutput).map((key, index) => {
try {
const imageUrl = row.binaryOutput[key];
return (
<Accordion defaultExpanded sx={{ mb: 2 }} key={`screenshot-${key}`}>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls={`screenshot-${key}-content`}
id={`screenshot-${key}-header`}
>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<ImageIcon sx={{ color: '#FF00C3' }} />
<Typography variant='h6' sx={{ ml: 2 }}>
2025-04-30 19:57:38 +05:30
Screenshot {index + 1}
2025-04-29 00:33:04 +05:30
</Typography>
</Box>
</AccordionSummary>
<AccordionDetails>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
<ButtonGroup size="small" variant="outlined">
<Button
startIcon={<DownloadIcon />}
href={imageUrl}
download={key}
sx={{ borderColor: '#FF00C3', color: '#FF00C3' }}
>
{t('run_content.captured_screenshot.download')}
</Button>
</ButtonGroup>
</Box>
<Box>
2025-04-30 19:57:38 +05:30
<img
src={imageUrl}
alt={`Screenshot ${key}`}
style={{
2025-04-29 00:33:04 +05:30
maxWidth: '100%',
height: 'auto',
border: '1px solid #e0e0e0',
borderRadius: '4px'
2025-04-30 19:57:38 +05:30
}}
2025-04-29 00:33:04 +05:30
/>
</Box>
</AccordionDetails>
</Accordion>
);
} catch (e) {
console.log(e);
return (
<Typography key={`screenshot-error-${key}`} color="error">
{key}: {t('run_content.captured_screenshot.render_failed')}
</Typography>
);
}
})}
</>
) : (
<Grid container spacing={3}>
{Object.keys(row.binaryOutput).map((key) => {
try {
const imageUrl = row.binaryOutput[key];
return (
<Grid item xs={12} md={6} key={`screenshot-${key}`}>
<Card sx={{ height: '100%', boxShadow: 3 }}>
<CardHeader
avatar={<ImageIcon sx={{ color: '#FF00C3' }} />}
title={`Screenshot ${key}`}
action={
<IconButton
size="small"
href={imageUrl}
download={key}
title={t('run_content.captured_screenshot.download')}
>
<DownloadIcon />
</IconButton>
}
/>
<CardContent sx={{ p: 1 }}>
<Box sx={{ position: 'relative', width: '100%', height: 'auto', overflow: 'hidden' }}>
2025-04-30 19:57:38 +05:30
<img
src={imageUrl}
alt={`Screenshot ${key}`}
style={{
2025-04-29 00:33:04 +05:30
width: '100%',
height: 'auto',
objectFit: 'contain',
border: '1px solid #e0e0e0',
borderRadius: '4px'
2025-04-30 19:57:38 +05:30
}}
2025-04-29 00:33:04 +05:30
/>
</Box>
</CardContent>
</Card>
</Grid>
);
} catch (e) {
console.log(e);
return (
<Box key={`screenshot-error-${key}`}>
<Typography color="error">
{key}: {t('run_content.captured_screenshot.render_failed')}
</Typography>
</Box>
);
}
})}
</Grid>
)}
</>
)}
2024-09-27 23:40:01 +05:30
</TabPanel>
2024-06-24 22:38:29 +05:30
</TabContext>
</Box>
);
};