From f975862a806645ce42eeda4d532d4a5848bfc358 Mon Sep 17 00:00:00 2001 From: Rohit Date: Tue, 29 Apr 2025 00:33:04 +0530 Subject: [PATCH] feat: revamp run content ui --- src/components/run/RunContent.tsx | 1077 ++++++++++++++++++++++++++--- 1 file changed, 969 insertions(+), 108 deletions(-) diff --git a/src/components/run/RunContent.tsx b/src/components/run/RunContent.tsx index 77bce544..664163aa 100644 --- a/src/components/run/RunContent.tsx +++ b/src/components/run/RunContent.tsx @@ -1,10 +1,22 @@ -import { Box, Tabs, Typography, Tab, Paper, Button, CircularProgress } from "@mui/material"; +import { Box, Tabs, Typography, Tab, Paper, Button, CircularProgress, Accordion, AccordionSummary, AccordionDetails, Divider, Card, CardHeader, CardContent, Grid, IconButton, Chip, 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 ArticleIcon from '@mui/icons-material/Article'; import ImageIcon from '@mui/icons-material/Image'; +import ListIcon from '@mui/icons-material/List'; +import SchemaIcon from '@mui/icons-material/Schema'; +import MoreHorizIcon from '@mui/icons-material/MoreHoriz'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import CloudDownloadIcon from '@mui/icons-material/CloudDownload'; +import DownloadIcon from '@mui/icons-material/Download'; +import FullscreenIcon from '@mui/icons-material/Fullscreen'; +import ViewModuleIcon from '@mui/icons-material/ViewModule'; +import ViewListIcon from '@mui/icons-material/ViewList'; +import DataObjectIcon from '@mui/icons-material/DataObject'; +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'; @@ -26,53 +38,685 @@ interface RunContentProps { export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRef, abortRunHandler }: RunContentProps) => { const { t } = useTranslation(); const [tab, setTab] = React.useState('output'); - const [tableData, setTableData] = useState([]); - const [columns, setColumns] = useState([]); + + const [schemaData, setSchemaData] = useState([]); + const [schemaColumns, setSchemaColumns] = useState([]); + + const [listData, setListData] = useState([]); + const [listColumns, setListColumns] = useState([]); + const [currentListIndex, setCurrentListIndex] = useState(0); + + const [otherData, setOtherData] = useState([]); + const [otherColumns, setOtherColumns] = useState([]); + + const [expandedView, setExpandedView] = useState(null); + + const [viewMode, setViewMode] = useState<'horizontal' | 'vertical'>('vertical'); + + const [legacyData, setLegacyData] = useState([]); + const [legacyColumns, setLegacyColumns] = useState([]); + const [isLegacyData, setIsLegacyData] = useState(false); useEffect(() => { setTab(tab); }, [interpretationInProgress]); useEffect(() => { - if (row.serializableOutput && Object.keys(row.serializableOutput).length > 0) { - const firstKey = Object.keys(row.serializableOutput)[0]; - const data = row.serializableOutput[firstKey]; - if (Array.isArray(data)) { - // Filter out completely empty rows - const filteredData = data.filter(row => - Object.values(row).some(value => value !== undefined && value !== "") - ); - setTableData(filteredData); - if (filteredData.length > 0) { - setColumns(Object.keys(filteredData[0])); - } - } + if (!row.serializableOutput) return; + + if (!row.serializableOutput.scrapeSchema && + !row.serializableOutput.scrapeList && + !row.serializableOutput.other && + 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); + } + + if (row.serializableOutput.other && Object.keys(row.serializableOutput.other).length > 0) { + processDataCategory(row.serializableOutput.other, setOtherData, setOtherColumns); } }, [row.serializableOutput]); + const processLegacyData = (legacyOutput: Record) => { + 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(); + allData.forEach(item => { + Object.keys(item).forEach(key => allColumns.add(key)); + }); + + setLegacyData(allData); + setLegacyColumns(Array.from(allColumns)); + } + }; + + const processDataCategory = ( + categoryData: Record, + setData: React.Dispatch>, + setColumns: React.Dispatch> + ) => { + 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(); + 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(); + 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(); + 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(',') + columns.map(col => JSON.stringify(row[col] || "", null, 2)).join(',') ); return [header, ...rows].join('\n'); }; - const downloadCSV = () => { - const csvContent = convertToCSV(tableData, columns); + // 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", "data.csv"); + 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 downloadAllJSON = () => { + let allData; + + if (isLegacyData) { + allData = { data: legacyData }; + } else { + allData = { + schema: schemaData, + list: listData.flat(), + other: otherData + }; + } + + const blob = new Blob([JSON.stringify(allData, null, 2)], { type: 'application/json;charset=utf-8;' }); + const url = URL.createObjectURL(blob); + + const link = document.createElement("a"); + link.href = url; + link.setAttribute("download", "all_data.json"); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }; + + 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, + icon: React.ReactNode, + 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 ( + + } + aria-controls={`${title.toLowerCase()}-content`} + id={`${title.toLowerCase()}-header`} + > + + {icon} + + {title} + + {isPaginatedList ? ( + 1 + ? `Table ${currentListIndex + 1} of ${listData.length} (${currentData.length} ${currentData.length === 1 ? 'item' : 'items'})` + : `${currentData.length} ${currentData.length === 1 ? 'item' : 'items'}` + } + size="small" + sx={{ ml: 2, backgroundColor: '#FF00C3', color: 'white' }} + /> + ) : ( + + )} + + + + + + + + + + {isPaginatedList && listData.length > 1 && ( + + + + + )} + + + + + + {(isPaginatedList ? currentColumns : columns).map((column) => ( + {column} + ))} + + + + {(isPaginatedList ? currentData : data).map((row, index) => ( + + {(isPaginatedList ? currentColumns : columns).map((column) => ( + + {row[column] === undefined || row[column] === "" ? "-" : row[column]} + + ))} + + ))} + +
+
+
+
+ ); + }; + + const renderDataCard = ( + data: any[], + columns: string[], + title: string, + icon: React.ReactNode, + 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; + + const currentData = isPaginatedList ? listData[currentListIndex] : data; + const currentColumns = isPaginatedList ? listColumns[currentListIndex] : columns; + + if (!currentData || currentData.length === 0) return null; + + const previewData = currentData.slice(0, 1); + const previewColumns = currentColumns.slice(0, 3); + + const showMoreColumns = currentColumns.length > 3; + + return ( + + + { + if (isPaginatedList) { + downloadCSV(currentData, currentColumns, `list_table_${currentListIndex+1}.csv`); + } else { + downloadCSV(data, columns, csvFilename); + } + }} + title={t('run_content.captured_data.download_csv')} + > + + + { + if (isPaginatedList) { + downloadJSON(currentData, `list_table_${currentListIndex+1}.json`); + } else { + downloadJSON(data, jsonFilename); + } + }} + title="Download JSON" + sx={{ mx: 0.5 }} + > + + + { + if (isPaginatedList) { + setExpandedView(`list-${currentListIndex}`); + } else { + setExpandedView(dataType); + } + }} + title={t('run_content.captured_data.view_full')} + > + + + + } + sx={{ pb: 1 }} + /> + + + {isPaginatedList ? ( + 1 + ? `Table ${currentListIndex + 1} of ${listData.length} (${currentData.length} ${currentData.length === 1 ? 'item' : 'items'})` + : `${currentData.length} ${currentData.length === 1 ? 'item' : 'items'}` + } + size="small" + sx={{ backgroundColor: '#FF00C3', color: 'white' }} + /> + ) : ( + + )} + + {isPaginatedList && listData.length > 1 && ( + + + + + )} + + + + + + {previewColumns.map((column) => ( + {column} + ))} + {showMoreColumns && ...} + + + + {previewData.map((row, index) => ( + + {previewColumns.map((column) => ( + + {row[column] === undefined || row[column] === "" ? "-" : row[column]} + + ))} + {showMoreColumns && ...} + + ))} + {currentData.length > 1 && ( + + + + + + )} + +
+
+
+
+ ); + }; + + 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); + + if (index >= 0 && index < listData.length) { + data = listData[index]; + columns = listColumns[index]; + title = `${t('run_content.captured_data.list_title')} - Table ${index+1}`; + csvFilename = `list_table_${index+1}.csv`; + jsonFilename = `list_table_${index+1}.json`; + } + } 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 'other': + data = otherData; + columns = otherColumns; + title = t('run_content.captured_data.other_title'); + csvFilename = 'other_data.csv'; + jsonFilename = 'other_data.json'; + break; + case 'legacy': + data = legacyData; + columns = legacyColumns; + title = t('run_content.captured_data.title'); + csvFilename = 'data.csv'; + jsonFilename = 'data.json'; + break; + } + } + + return ( + + + + {title} + + + + + + + + + + + + + + {columns.map((column) => ( + {column} + ))} + + + + {data.map((row, index) => ( + + {columns.map((column) => ( + + {row[column] === undefined || row[column] === "" ? "-" : row[column]} + + ))} + + ))} + +
+
+
+
+ ); + }; + + const hasData = schemaData.length > 0 || listData.length > 0 || otherData.length > 0 || legacyData.length > 0; + const hasScreenshots = row.binaryOutput && Object.keys(row.binaryOutput).length > 0; + return ( @@ -82,11 +726,9 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe onChange={(e, newTab) => setTab(newTab)} aria-label="run-content-tabs" sx={{ - // Remove the default blue indicator '& .MuiTabs-indicator': { - backgroundColor: '#FF00C3', // Change to pink + backgroundColor: '#FF00C3', }, - // Remove default transition effects '& .MuiTab-root': { '&.Mui-selected': { color: '#FF00C3', @@ -147,103 +789,322 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe {t('run_content.buttons.stop')} : null} - + {row.status === 'running' || row.status === 'queued' ? ( {t('run_content.loading')} - ) : (!row || !row.serializableOutput || !row.binaryOutput - || (Object.keys(row.serializableOutput).length === 0 && Object.keys(row.binaryOutput).length === 0) + ) : (!hasData && !hasScreenshots ? {t('run_content.empty_output')} : null)} - {row.serializableOutput && - Object.keys(row.serializableOutput).length !== 0 && -
- - - {t('run_content.captured_data.title')} - - - - - {t('run_content.captured_data.download_json')} - + {hasData && ( + + + + + {t('run_content.captured_data.title')} - - {t('run_content.captured_data.download_csv')} - - - {tableData.length > 0 ? ( - - - - - {columns.map((column) => ( - {column} - ))} - - - - {tableData.map((row, index) => ( - - {columns.map((column) => ( - - {row[column] === undefined || row[column] === "" ? "-" : row[column]} - - ))} - - ))} - -
-
- ) : ( - -
-                    {JSON.stringify(row.serializableOutput, null, 2)}
-                  
+ + setViewMode('horizontal')} + color={viewMode === 'horizontal' ? 'primary' : 'default'} + sx={{ color: viewMode === 'horizontal' ? '#FF00C3' : 'inherit' }} + > + + + setViewMode('vertical')} + color={viewMode === 'vertical' ? 'primary' : 'default'} + sx={{ color: viewMode === 'vertical' ? '#FF00C3' : 'inherit' }} + > + + + - )} -
- } - {row.binaryOutput && Object.keys(row.binaryOutput).length !== 0 && -
- - - {t('run_content.captured_screenshot.title')} - - {Object.keys(row.binaryOutput).map((key) => { - try { - const imageUrl = row.binaryOutput[key]; - return ( - - - {t('run_content.captured_screenshot.download')} - - {key} - + + + {isLegacyData && ( + viewMode === 'vertical' ? ( + renderDataTable( + legacyData, + legacyColumns, + t('run_content.captured_data.title'), + , + 'data.csv', + 'data.json' ) - } catch (e) { - console.log(e) - return - {key}: {t('run_content.captured_screenshot.render_failed')} + ) : ( + + + {renderDataCard( + legacyData, + legacyColumns, + t('run_content.captured_data.title'), + , + 'legacy', + 'data.csv', + 'data.json' + )} + + + ) + )} + + {!isLegacyData && ( + viewMode === 'vertical' ? ( + <> + {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 + )} + + {renderDataTable( + otherData, + otherColumns, + t('run_content.captured_data.other_title'), + , + 'other_data.csv', + 'other_data.json' + )} + + ) : ( + + {(() => { + const dataCategoriesCount = [ + schemaData.length > 0, + listData.length > 0, + otherData.length > 0 + ].filter(Boolean).length; + + const columnWidth = dataCategoriesCount === 1 ? 12 : dataCategoriesCount === 2 ? 6 : 4; + + return ( + <> + {schemaData.length > 0 && ( + + {renderDataCard( + schemaData, + schemaColumns, + t('run_content.captured_data.schema_title'), + , + 'schema', + 'schema_data.csv', + 'schema_data.json' + )} + + )} + + {listData.length > 0 && ( + + {renderDataCard( + [], + [], + t('run_content.captured_data.list_title'), + , + 'list', + 'list_data.csv', + 'list_data.json', + true + )} + + )} + + {otherData.length > 0 && ( + + {renderDataCard( + otherData, + otherColumns, + t('run_content.captured_data.other_title'), + , + 'other', + 'other_data.csv', + 'other_data.json' + )} + + )} + + ); + })()} + + ) + )} + + {renderExpandedView('schema')} + {renderExpandedView('other')} + {renderExpandedView('legacy')} + + {listData.map((_, index) => renderExpandedView(`list-${index}`))} + + )} + + {hasScreenshots && ( + <> + + + + + {t('run_content.captured_screenshot.title')} + - } - })} -
- } + + setViewMode('horizontal')} + color={viewMode === 'horizontal' ? 'primary' : 'default'} + sx={{ color: viewMode === 'horizontal' ? '#FF00C3' : 'inherit' }} + > + + + setViewMode('vertical')} + color={viewMode === 'vertical' ? 'primary' : 'default'} + sx={{ color: viewMode === 'vertical' ? '#FF00C3' : 'inherit' }} + > + + + +
+ + + {viewMode === 'vertical' ? ( + <> + {Object.keys(row.binaryOutput).map((key, index) => { + try { + const imageUrl = row.binaryOutput[key]; + return ( + + } + aria-controls={`screenshot-${key}-content`} + id={`screenshot-${key}-header`} + > + + + + Screenshot {index+1} + + + + + + + + + + + {`Screenshot + + + + ); + } catch (e) { + console.log(e); + return ( + + {key}: {t('run_content.captured_screenshot.render_failed')} + + ); + } + })} + + ) : ( + + {Object.keys(row.binaryOutput).map((key) => { + try { + const imageUrl = row.binaryOutput[key]; + return ( + + + } + title={`Screenshot ${key}`} + action={ + + + + } + /> + + + {`Screenshot + + + + + ); + } catch (e) { + console.log(e); + return ( + + + {key}: {t('run_content.captured_screenshot.render_failed')} + + + ); + } + })} + + )} + + )}