diff --git a/src/components/run/RunsTable.tsx b/src/components/run/RunsTable.tsx index 53d29433..a32fd70e 100644 --- a/src/components/run/RunsTable.tsx +++ b/src/components/run/RunsTable.tsx @@ -1,34 +1,67 @@ -import * as React from 'react'; +import * as React from "react"; import { useCallback, useEffect, useMemo, useState } from "react"; -import { useTranslation } from 'react-i18next'; -import Paper from '@mui/material/Paper'; -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 TablePagination from '@mui/material/TablePagination'; -import TableRow from '@mui/material/TableRow'; -import { Accordion, AccordionSummary, AccordionDetails, Typography, Box, TextField, CircularProgress, Tooltip } from '@mui/material'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import SearchIcon from '@mui/icons-material/Search'; -import { useLocation, useNavigate } from 'react-router-dom'; +import { useTranslation } from "react-i18next"; +import Paper from "@mui/material/Paper"; +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 TablePagination from "@mui/material/TablePagination"; +import TableRow from "@mui/material/TableRow"; +import { + Accordion, + AccordionSummary, + AccordionDetails, + Typography, + Box, + TextField, + CircularProgress, + Tooltip, +} from "@mui/material"; +import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; +import SearchIcon from "@mui/icons-material/Search"; +import { useLocation, useNavigate } from "react-router-dom"; import { useGlobalInfoStore } from "../../context/globalInfo"; import { getStoredRuns } from "../../api/storage"; import { RunSettings } from "./RunSettings"; import { CollapsibleRow } from "./ColapsibleRow"; -import { ArrowDownward, ArrowUpward, UnfoldMore } from '@mui/icons-material'; +import { ArrowDownward, ArrowUpward, UnfoldMore } from "@mui/icons-material"; export const columns: readonly Column[] = [ - { id: 'runStatus', label: 'Status', minWidth: 80 }, - { id: 'name', label: 'Name', minWidth: 80 }, - { id: 'startedAt', label: 'Started At', minWidth: 80 }, - { id: 'finishedAt', label: 'Finished At', minWidth: 80 }, - { id: 'settings', label: 'Settings', minWidth: 80 }, - { id: 'delete', label: 'Delete', minWidth: 80 }, + { + id: "runStatus", + label: "Status", + minWidth: 80, + maxWidth: 120, + flexGrow: 1, + }, + { id: "name", label: "Name", minWidth: 120, maxWidth: 250, flexGrow: 3 }, + { + id: "startedAt", + label: "Started At", + minWidth: 120, + maxWidth: 180, + flexGrow: 2, + }, + { + id: "finishedAt", + label: "Finished At", + minWidth: 120, + maxWidth: 180, + flexGrow: 2, + }, + { + id: "settings", + label: "Settings", + minWidth: 80, + maxWidth: 120, + flexGrow: 1, + }, + { id: "delete", label: "Delete", minWidth: 80, maxWidth: 100, flexGrow: 1 }, ]; -type SortDirection = 'asc' | 'desc' | 'none'; +type SortDirection = "asc" | "desc" | "none"; interface AccordionSortConfig { [robotMetaId: string]: { @@ -38,10 +71,10 @@ interface AccordionSortConfig { } interface Column { - id: 'runStatus' | 'name' | 'startedAt' | 'finishedAt' | 'delete' | 'settings'; + id: "runStatus" | "name" | "startedAt" | "finishedAt" | "delete" | "settings"; label: string; minWidth?: number; - align?: 'right'; + align?: "right"; format?: (value: string) => string; } @@ -81,110 +114,145 @@ export const RunsTable: React.FC = ({ currentInterpretationLog, abortRunHandler, runId, - runningRecordingName + runningRecordingName, }) => { const { t } = useTranslation(); const navigate = useNavigate(); const location = useLocation(); const getUrlParams = () => { - const match = location.pathname.match(/\/runs\/([^\/]+)(?:\/run\/([^\/]+))?/); + const match = location.pathname.match( + /\/runs\/([^\/]+)(?:\/run\/([^\/]+))?/ + ); return { robotMetaId: match?.[1] || null, - urlRunId: match?.[2] || null + urlRunId: match?.[2] || null, }; }; const { robotMetaId: urlRobotMetaId, urlRunId } = getUrlParams(); - const isAccordionExpanded = useCallback((currentRobotMetaId: string) => { - return currentRobotMetaId === urlRobotMetaId; - }, [urlRobotMetaId]); + const isAccordionExpanded = useCallback( + (currentRobotMetaId: string) => { + return currentRobotMetaId === urlRobotMetaId; + }, + [urlRobotMetaId] + ); const [accordionPage, setAccordionPage] = useState(0); const [accordionsPerPage, setAccordionsPerPage] = useState(10); - const [accordionSortConfigs, setAccordionSortConfigs] = useState({}); + const [accordionSortConfigs, setAccordionSortConfigs] = + useState({}); - const handleSort = useCallback((columnId: keyof Data, robotMetaId: string) => { - setAccordionSortConfigs(prevConfigs => { - const currentConfig = prevConfigs[robotMetaId] || { field: null, direction: 'none' }; - const newDirection: SortDirection = - currentConfig.field !== columnId ? 'asc' : - currentConfig.direction === 'none' ? 'asc' : - currentConfig.direction === 'asc' ? 'desc' : 'none'; + const handleSort = useCallback( + (columnId: keyof Data, robotMetaId: string) => { + setAccordionSortConfigs((prevConfigs) => { + const currentConfig = prevConfigs[robotMetaId] || { + field: null, + direction: "none", + }; + const newDirection: SortDirection = + currentConfig.field !== columnId + ? "asc" + : currentConfig.direction === "none" + ? "asc" + : currentConfig.direction === "asc" + ? "desc" + : "none"; - return { - ...prevConfigs, - [robotMetaId]: { - field: newDirection === 'none' ? null : columnId, - direction: newDirection, - } - }; - }); - }, []); + return { + ...prevConfigs, + [robotMetaId]: { + field: newDirection === "none" ? null : columnId, + direction: newDirection, + }, + }; + }); + }, + [] + ); - const translatedColumns = useMemo(() => - columns.map(column => ({ - ...column, - label: t(`runstable.${column.id}`, column.label) - })), + const translatedColumns = useMemo( + () => + columns.map((column) => ({ + ...column, + label: t(`runstable.${column.id}`, column.label), + })), [t] ); const [rows, setRows] = useState([]); - const [searchTerm, setSearchTerm] = useState(''); + const [searchTerm, setSearchTerm] = useState(""); const [paginationStates, setPaginationStates] = useState({}); const { notify, rerenderRuns, setRerenderRuns } = useGlobalInfoStore(); - const handleAccordionChange = useCallback((robotMetaId: string, isExpanded: boolean) => { - navigate(isExpanded ? `/runs/${robotMetaId}` : '/runs'); - }, [navigate]); + const handleAccordionChange = useCallback( + (robotMetaId: string, isExpanded: boolean) => { + navigate(isExpanded ? `/runs/${robotMetaId}` : "/runs"); + }, + [navigate] + ); - const handleAccordionPageChange = useCallback((event: unknown, newPage: number) => { - setAccordionPage(newPage); - }, []); - - const handleAccordionsPerPageChange = useCallback((event: React.ChangeEvent) => { - setAccordionsPerPage(+event.target.value); - setAccordionPage(0); - }, []); + const handleAccordionPageChange = useCallback( + (event: unknown, newPage: number) => { + setAccordionPage(newPage); + }, + [] + ); - const handleChangePage = useCallback((robotMetaId: string, newPage: number) => { - setPaginationStates(prev => ({ - ...prev, - [robotMetaId]: { - ...prev[robotMetaId], - page: newPage + const handleAccordionsPerPageChange = useCallback( + (event: React.ChangeEvent) => { + setAccordionsPerPage(+event.target.value); + setAccordionPage(0); + }, + [] + ); + + const handleChangePage = useCallback( + (robotMetaId: string, newPage: number) => { + setPaginationStates((prev) => ({ + ...prev, + [robotMetaId]: { + ...prev[robotMetaId], + page: newPage, + }, + })); + }, + [] + ); + + const handleChangeRowsPerPage = useCallback( + (robotMetaId: string, newRowsPerPage: number) => { + setPaginationStates((prev) => ({ + ...prev, + [robotMetaId]: { + page: 0, // Reset to first page when changing rows per page + rowsPerPage: newRowsPerPage, + }, + })); + }, + [] + ); + + const getPaginationState = useCallback( + (robotMetaId: string) => { + const defaultState = { page: 0, rowsPerPage: 10 }; + + if (!paginationStates[robotMetaId]) { + setTimeout(() => { + setPaginationStates((prev) => ({ + ...prev, + [robotMetaId]: defaultState, + })); + }, 0); + return defaultState; } - })); - }, []); - - const handleChangeRowsPerPage = useCallback((robotMetaId: string, newRowsPerPage: number) => { - setPaginationStates(prev => ({ - ...prev, - [robotMetaId]: { - page: 0, // Reset to first page when changing rows per page - rowsPerPage: newRowsPerPage - } - })); - }, []); - - const getPaginationState = useCallback((robotMetaId: string) => { - const defaultState = { page: 0, rowsPerPage: 10 }; - - if (!paginationStates[robotMetaId]) { - setTimeout(() => { - setPaginationStates(prev => ({ - ...prev, - [robotMetaId]: defaultState - })); - }, 0); - return defaultState; - } - return paginationStates[robotMetaId]; - }, [paginationStates]); + return paginationStates[robotMetaId]; + }, + [paginationStates] + ); const debouncedSearch = useCallback((fn: Function, delay: number) => { let timeoutId: NodeJS.Timeout; @@ -194,20 +262,26 @@ export const RunsTable: React.FC = ({ }; }, []); - const handleSearchChange = useCallback((event: React.ChangeEvent) => { - const debouncedSetSearch = debouncedSearch((value: string) => { - setSearchTerm(value); - setAccordionPage(0); - setPaginationStates(prev => { - const reset = Object.keys(prev).reduce((acc, robotId) => ({ - ...acc, - [robotId]: { ...prev[robotId], page: 0 } - }), {}); - return reset; - }); - }, 300); - debouncedSetSearch(event.target.value); - }, [debouncedSearch]); + const handleSearchChange = useCallback( + (event: React.ChangeEvent) => { + const debouncedSetSearch = debouncedSearch((value: string) => { + setSearchTerm(value); + setAccordionPage(0); + setPaginationStates((prev) => { + const reset = Object.keys(prev).reduce( + (acc, robotId) => ({ + ...acc, + [robotId]: { ...prev[robotId], page: 0 }, + }), + {} + ); + return reset; + }); + }, 300); + debouncedSetSearch(event.target.value); + }, + [debouncedSearch] + ); const fetchRuns = useCallback(async () => { try { @@ -219,10 +293,10 @@ export const RunsTable: React.FC = ({ })); setRows(parsedRows); } else { - notify('error', t('runstable.notifications.no_runs')); + notify("error", t("runstable.notifications.no_runs")); } } catch (error) { - notify('error', t('runstable.notifications.fetch_error')); + notify("error", t("runstable.notifications.fetch_error")); } }, [notify, t]); @@ -244,7 +318,7 @@ export const RunsTable: React.FC = ({ const handleDelete = useCallback(() => { setRows([]); - notify('success', t('runstable.notifications.delete_success')); + notify("success", t("runstable.notifications.delete_success")); fetchRuns(); }, [notify, t, fetchRuns]); @@ -258,11 +332,11 @@ export const RunsTable: React.FC = ({ const parseDateString = (dateStr: string): Date => { try { - if (dateStr.includes('PM') || dateStr.includes('AM')) { + if (dateStr.includes("PM") || dateStr.includes("AM")) { return new Date(dateStr); } - - return new Date(dateStr.replace(/(\d+)\/(\d+)\//, '$2/$1/')) + + return new Date(dateStr.replace(/(\d+)\/(\d+)\//, "$2/$1/")); } catch { return new Date(0); } @@ -276,121 +350,166 @@ export const RunsTable: React.FC = ({ acc[row.robotMetaId].push(row); return acc; }, {} as Record); - - Object.keys(groupedData).forEach(robotId => { - groupedData[robotId].sort((a, b) => - parseDateString(b.startedAt).getTime() - parseDateString(a.startedAt).getTime() + + Object.keys(groupedData).forEach((robotId) => { + groupedData[robotId].sort( + (a, b) => + parseDateString(b.startedAt).getTime() - + parseDateString(a.startedAt).getTime() ); }); - + const robotEntries = Object.entries(groupedData).map(([robotId, runs]) => ({ robotId, runs, - latestRunDate: parseDateString(runs[0].startedAt).getTime() + latestRunDate: parseDateString(runs[0].startedAt).getTime(), })); - + robotEntries.sort((a, b) => b.latestRunDate - a.latestRunDate); - + return robotEntries.reduce((acc, { robotId, runs }) => { acc[robotId] = runs; return acc; }, {} as Record); }, [filteredRows]); - const renderTableRows = useCallback((data: Data[], robotMetaId: string) => { - const { page, rowsPerPage } = getPaginationState(robotMetaId); - const start = page * rowsPerPage; - const end = start + rowsPerPage; + const renderTableRows = useCallback( + (data: Data[], robotMetaId: string) => { + const { page, rowsPerPage } = getPaginationState(robotMetaId); + const start = page * rowsPerPage; + const end = start + rowsPerPage; - let sortedData = [...data]; - const sortConfig = accordionSortConfigs[robotMetaId]; - - if (sortConfig?.field === 'startedAt' || sortConfig?.field === 'finishedAt') { - if (sortConfig.direction !== 'none') { - sortedData.sort((a, b) => { - const dateA = parseDateString(a[sortConfig.field!]); - const dateB = parseDateString(b[sortConfig.field!]); - - return sortConfig.direction === 'asc' - ? dateA.getTime() - dateB.getTime() - : dateB.getTime() - dateA.getTime(); - }); + let sortedData = [...data]; + const sortConfig = accordionSortConfigs[robotMetaId]; + + if ( + sortConfig?.field === "startedAt" || + sortConfig?.field === "finishedAt" + ) { + if (sortConfig.direction !== "none") { + sortedData.sort((a, b) => { + const dateA = parseDateString(a[sortConfig.field!]); + const dateB = parseDateString(b[sortConfig.field!]); + + return sortConfig.direction === "asc" + ? dateA.getTime() - dateB.getTime() + : dateB.getTime() - dateA.getTime(); + }); + } } - } - - return sortedData - .slice(start, end) - .map((row) => ( - - )); - }, [paginationStates, runId, runningRecordingName, currentInterpretationLog, abortRunHandler, handleDelete, accordionSortConfigs]); - const renderSortIcon = useCallback((column: Column, robotMetaId: string) => { - const sortConfig = accordionSortConfigs[robotMetaId]; - if (column.id !== 'startedAt' && column.id !== 'finishedAt') return null; - - if (sortConfig?.field !== column.id) { - return ( - ( + - ); - } + currentLog={currentInterpretationLog} + abortRunHandler={abortRunHandler} + runningRecordingName={runningRecordingName} + urlRunId={urlRunId} + /> + )); + }, + [ + paginationStates, + runId, + runningRecordingName, + currentInterpretationLog, + abortRunHandler, + handleDelete, + accordionSortConfigs, + getPaginationState, + ] + ); - return sortConfig.direction === 'asc' - ? - : sortConfig.direction === 'desc' - ? - : ; - }, [accordionSortConfigs]); + const renderSortIcon = useCallback( + (column: Column, robotMetaId: string) => { + const sortConfig = accordionSortConfigs[robotMetaId]; + if (column.id !== "startedAt" && column.id !== "finishedAt") return null; + + if (sortConfig?.field !== column.id) { + return ( + + ); + } + + return sortConfig.direction === "asc" ? ( + + ) : sortConfig.direction === "desc" ? ( + + ) : ( + + ); + }, + [accordionSortConfigs] + ); return ( - + - {t('runstable.runs', 'Runs')} + {t("runstable.runs", "Runs")} + startAdornment: ( + + ), }} - sx={{ width: '250px' }} + sx={{ width: "250px" }} /> - + {Object.entries(groupedRows) .slice( accordionPage * accordionsPerPage, accordionPage * accordionsPerPage + accordionsPerPage ) .map(([robotMetaId, data]) => ( - handleAccordionChange(robotMetaId, isExpanded)} + + handleAccordionChange(robotMetaId, isExpanded) + } TransitionProps={{ unmountOnExit: true }} // Optimize accordion rendering > }> - {data[data.length - 1].name} + + {data[data.length - 1].name} + @@ -401,40 +520,64 @@ export const RunsTable: React.FC = ({ sum + (col.flexGrow || 0), 0)) * 100 + }%`, + cursor: + column.id === "startedAt" || + column.id === "finishedAt" + ? "pointer" + : "default", + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", }} onClick={() => { - if (column.id === 'startedAt' || column.id === 'finishedAt') { + if ( + column.id === "startedAt" || + column.id === "finishedAt" + ) { handleSort(column.id, robotMetaId); } }} > - - + {column.label} - + {renderSortIcon(column, robotMetaId)} @@ -443,9 +586,7 @@ export const RunsTable: React.FC = ({ ))} - - {renderTableRows(data, robotMetaId)} - + {renderTableRows(data, robotMetaId)}
= ({ count={data.length} rowsPerPage={getPaginationState(robotMetaId).rowsPerPage} page={getPaginationState(robotMetaId).page} - onPageChange={(_, newPage) => handleChangePage(robotMetaId, newPage)} - onRowsPerPageChange={(event) => + onPageChange={(_, newPage) => + handleChangePage(robotMetaId, newPage) + } + onRowsPerPageChange={(event) => handleChangeRowsPerPage(robotMetaId, +event.target.value) } rowsPerPageOptions={[10, 25, 50, 100]} @@ -475,4 +618,4 @@ export const RunsTable: React.FC = ({ />
); -}; \ No newline at end of file +};