diff --git a/src/components/run/RunsTable.tsx b/src/components/run/RunsTable.tsx index a32fd70e..53d29433 100644 --- a/src/components/run/RunsTable.tsx +++ b/src/components/run/RunsTable.tsx @@ -1,67 +1,34 @@ -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, - 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 }, + { 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 }, ]; -type SortDirection = "asc" | "desc" | "none"; +type SortDirection = 'asc' | 'desc' | 'none'; interface AccordionSortConfig { [robotMetaId: string]: { @@ -71,10 +38,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; } @@ -114,145 +81,110 @@ 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 handleAccordionPageChange = useCallback((event: unknown, newPage: number) => { + setAccordionPage(newPage); + }, []); + + const handleAccordionsPerPageChange = useCallback((event: React.ChangeEvent) => { + setAccordionsPerPage(+event.target.value); + setAccordionPage(0); + }, []); - 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 handleChangePage = useCallback((robotMetaId: string, newPage: number) => { + setPaginationStates(prev => ({ + ...prev, + [robotMetaId]: { + ...prev[robotMetaId], + page: newPage } - return paginationStates[robotMetaId]; - }, - [paginationStates] - ); + })); + }, []); + + 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]); const debouncedSearch = useCallback((fn: Function, delay: number) => { let timeoutId: NodeJS.Timeout; @@ -262,26 +194,20 @@ 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 { @@ -293,10 +219,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]); @@ -318,7 +244,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]); @@ -332,11 +258,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); } @@ -350,166 +276,121 @@ 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]); - return sortedData - .slice(start, end) - .map((row) => ( - { + const sortConfig = accordionSortConfigs[robotMetaId]; + if (column.id !== 'startedAt' && column.id !== 'finishedAt') return null; + + if (sortConfig?.field !== column.id) { + return ( + - )); - }, - [ - paginationStates, - runId, - runningRecordingName, - currentInterpretationLog, - abortRunHandler, - handleDelete, - accordionSortConfigs, - getPaginationState, - ] - ); - - 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 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} @@ -520,64 +401,40 @@ 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", + cursor: column.id === 'startedAt' || column.id === 'finishedAt' ? 'pointer' : 'default' }} onClick={() => { - if ( - column.id === "startedAt" || - column.id === "finishedAt" - ) { + if (column.id === 'startedAt' || column.id === 'finishedAt') { handleSort(column.id, robotMetaId); } }} > - - + {column.label} - + {renderSortIcon(column, robotMetaId)} @@ -586,7 +443,9 @@ 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]} @@ -618,4 +475,4 @@ export const RunsTable: React.FC = ({ />
); -}; +}; \ No newline at end of file