diff --git a/src/components/robot/RecordingsTable.tsx b/src/components/robot/RecordingsTable.tsx
index a7594b086..484b1667b 100644
--- a/src/components/robot/RecordingsTable.tsx
+++ b/src/components/robot/RecordingsTable.tsx
@@ -8,7 +8,7 @@ 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 { useEffect } from "react";
+import { memo, useCallback, useEffect, useMemo } from "react";
import { WorkflowFile } from "maxun-core";
import SearchIcon from '@mui/icons-material/Search';
import {
@@ -76,6 +76,64 @@ interface RecordingsTableProps {
handleDuplicateRobot: (id: string, name: string, params: string[]) => void;
}
+// Virtualized row component for efficient rendering
+const TableRowMemoized = memo(({ row, columns, handlers }: any) => {
+ return (
+
+ {columns.map((column: Column) => {
+ const value: any = row[column.id];
+ if (value !== undefined) {
+ return (
+
+ {value}
+
+ );
+ } else {
+ switch (column.id) {
+ case 'interpret':
+ return (
+
+ handlers.handleRunRecording(row.id, row.name, row.params || [])} />
+
+ );
+ case 'schedule':
+ return (
+
+ handlers.handleScheduleRecording(row.id, row.name, row.params || [])} />
+
+ );
+ case 'integrate':
+ return (
+
+ handlers.handleIntegrateRecording(row.id, row.name, row.params || [])} />
+
+ );
+ case 'options':
+ return (
+
+ handlers.handleEditRobot(row.id, row.name, row.params || [])}
+ handleDuplicate={() => handlers.handleDuplicateRobot(row.id, row.name, row.params || [])}
+ handleDelete={() => handlers.handleDelete(row.id)}
+ />
+
+ );
+ case 'settings':
+ return (
+
+ handlers.handleSettingsRecording(row.id, row.name, row.params || [])} />
+
+ );
+ default:
+ return null;
+ }
+ }
+ })}
+
+ );
+});
+
+
export const RecordingsTable = ({
handleEditRecording,
handleRunRecording,
@@ -90,31 +148,16 @@ export const RecordingsTable = ({
const [rows, setRows] = React.useState([]);
const [isModalOpen, setModalOpen] = React.useState(false);
const [searchTerm, setSearchTerm] = React.useState('');
+ const [isLoading, setIsLoading] = React.useState(true);
- const columns: readonly Column[] = [
+ const columns = useMemo(() => [
{ id: 'interpret', label: t('recordingtable.run'), minWidth: 80 },
{ id: 'name', label: t('recordingtable.name'), minWidth: 80 },
- {
- id: 'schedule',
- label: t('recordingtable.schedule'),
- minWidth: 80,
- },
- {
- id: 'integrate',
- label: t('recordingtable.integrate'),
- minWidth: 80,
- },
- {
- id: 'settings',
- label: t('recordingtable.settings'),
- minWidth: 80,
- },
- {
- id: 'options',
- label: t('recordingtable.options'),
- minWidth: 80,
- },
- ];
+ { id: 'schedule', label: t('recordingtable.schedule'), minWidth: 80 },
+ { id: 'integrate', label: t('recordingtable.integrate'), minWidth: 80 },
+ { id: 'settings', label: t('recordingtable.settings'), minWidth: 80 },
+ { id: 'options', label: t('recordingtable.options'), minWidth: 80 },
+ ], [t]);
const {
notify,
@@ -132,54 +175,63 @@ export const RecordingsTable = ({
setRecordingId } = useGlobalInfoStore();
const navigate = useNavigate();
- const handleChangePage = (event: unknown, newPage: number) => {
+ const handleChangePage = useCallback((event: unknown, newPage: number) => {
setPage(newPage);
- };
+ }, []);
const handleChangeRowsPerPage = (event: React.ChangeEvent) => {
setRowsPerPage(+event.target.value);
setPage(0);
};
- const handleSearchChange = (event: React.ChangeEvent) => {
+ const handleSearchChange = useCallback((event: React.ChangeEvent) => {
setSearchTerm(event.target.value);
setPage(0);
- };
+ }, []);
- const fetchRecordings = async () => {
- const recordings = await getStoredRecordings();
- if (recordings) {
- const parsedRows: Data[] = [];
- recordings.map((recording: any, index: number) => {
- if (recording && recording.recording_meta) {
- parsedRows.push({
- id: index,
- ...recording.recording_meta,
- content: recording.recording
- });
- }
- });
- setRecordings(parsedRows.map((recording) => recording.name));
- setRows(parsedRows);
- } else {
- console.log('No recordings found.');
+ const fetchRecordings = useCallback(async () => {
+ setIsLoading(true);
+ try {
+ const recordings = await getStoredRecordings();
+ if (recordings) {
+ const parsedRows = recordings
+ .map((recording: any, index: number) => {
+ if (recording?.recording_meta) {
+ return {
+ id: index,
+ ...recording.recording_meta,
+ content: recording.recording
+ };
+ }
+ return null;
+ })
+ .filter(Boolean);
+
+ setRecordings(parsedRows.map((recording) => recording.name));
+ setRows(parsedRows);
+ }
+ } catch (error) {
+ console.error('Error fetching recordings:', error);
+ notify('error', t('recordingtable.notifications.fetch_error'));
+ } finally {
+ setIsLoading(false);
}
- }
+ }, [setRecordings, notify, t]);
- const handleNewRecording = async () => {
+ const handleNewRecording = useCallback(async () => {
if (browserId) {
setBrowserId(null);
await stopRecording(browserId);
}
setModalOpen(true);
- };
+ }, [browserId]);
- const handleStartRecording = () => {
+ const handleStartRecording = useCallback(() => {
setBrowserId('new-recording');
setRecordingName('');
setRecordingId('');
navigate('/recording');
- }
+ }, [navigate]);
const startRecording = () => {
setModalOpen(false);
@@ -195,14 +247,61 @@ export const RecordingsTable = ({
if (rows.length === 0) {
fetchRecordings();
}
- }, []);
+ }, [fetchRecordings]);
+
+ function useDebounce(value: T, delay: number): T {
+ const [debouncedValue, setDebouncedValue] = React.useState(value);
+
+ useEffect(() => {
+ const handler = setTimeout(() => {
+ setDebouncedValue(value);
+ }, delay);
+
+ return () => {
+ clearTimeout(handler);
+ };
+ }, [value, delay]);
+
+ return debouncedValue;
+ }
+ const debouncedSearchTerm = useDebounce(searchTerm, 300);
// Filter rows based on search term
- const filteredRows = rows.filter((row) =>
- row.name.toLowerCase().includes(searchTerm.toLowerCase())
- );
-
+ const filteredRows = useMemo(() => {
+ const searchLower = debouncedSearchTerm.toLowerCase();
+ return debouncedSearchTerm
+ ? rows.filter(row => row.name.toLowerCase().includes(searchLower))
+ : rows;
+ }, [rows, debouncedSearchTerm]);
+
+ const visibleRows = useMemo(() => {
+ const start = page * rowsPerPage;
+ return filteredRows.slice(start, start + rowsPerPage);
+ }, [filteredRows, page, rowsPerPage]);
+
+ const handlers = useMemo(() => ({
+ handleRunRecording,
+ handleScheduleRecording,
+ handleIntegrateRecording,
+ handleSettingsRecording,
+ handleEditRobot,
+ handleDuplicateRobot,
+ handleDelete: async (id: string) => {
+ const hasRuns = await checkRunsForRecording(id);
+ if (hasRuns) {
+ notify('warning', t('recordingtable.notifications.delete_warning'));
+ return;
+ }
+
+ const success = await deleteRecordingFromStorage(id);
+ if (success) {
+ setRows([]);
+ notify('success', t('recordingtable.notifications.delete_success'));
+ fetchRecordings();
+ }
+ }
+ }), [handleRunRecording, handleScheduleRecording, handleIntegrateRecording, handleSettingsRecording, handleEditRobot, handleDuplicateRobot, notify, t]);
return (
@@ -244,7 +343,7 @@ export const RecordingsTable = ({
- {rows.length === 0 ? (
+ {isLoading ? (
@@ -254,99 +353,32 @@ export const RecordingsTable = ({
{columns.map((column) => (
-
{column.label}
-
+
))}
- {filteredRows.length !== 0 ? filteredRows
- .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
- .map((row) => {
- return (
-
- {columns.map((column) => {
- // @ts-ignore
- const value: any = row[column.id];
- if (value !== undefined) {
- return (
-
- {value}
-
- );
- } else {
- switch (column.id) {
- case 'interpret':
- return (
-
- handleRunRecording(row.id, row.name, row.params || [])} />
-
- );
- case 'schedule':
- return (
-
- handleScheduleRecording(row.id, row.name, row.params || [])} />
-
- );
- case 'integrate':
- return (
-
- handleIntegrateRecording(row.id, row.name, row.params || [])} />
-
- );
- case 'options':
- return (
-
- handleEditRobot(row.id, row.name, row.params || [])}
- handleDuplicate={() => {
- handleDuplicateRobot(row.id, row.name, row.params || []);
- }}
- handleDelete={() => {
-
- checkRunsForRecording(row.id).then((result: boolean) => {
- if (result) {
- notify('warning', t('recordingtable.notifications.delete_warning'));
- }
- })
-
- deleteRecordingFromStorage(row.id).then((result: boolean) => {
- if (result) {
- setRows([]);
- notify('success', t('recordingtable.notifications.delete_success'));
- fetchRecordings();
- }
- })
- }}
- />
-
- );
- case 'settings':
- return (
-
- handleSettingsRecording(row.id, row.name, row.params || [])} />
-
- );
- default:
- return null;
- }
- }
- })}
-
- );
- })
- : null}
+ {visibleRows.map((row) => (
+
+ ))}
)}
+
= ({
const { t } = useTranslation();
const navigate = useNavigate();
- const translatedColumns = 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 [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(10);
const [rows, setRows] = useState([]);
const [searchTerm, setSearchTerm] = useState('');
+ const [isLoading, setIsLoading] = useState(true);
const { notify, rerenderRuns, setRerenderRuns } = useGlobalInfoStore();
- const handleAccordionChange = (robotMetaId: string, isExpanded: boolean) => {
- if (isExpanded) {
- navigate(`/runs/${robotMetaId}`);
- } else {
- navigate(`/runs`);
- }
- };
+ const handleAccordionChange = useCallback((robotMetaId: string, isExpanded: boolean) => {
+ navigate(isExpanded ? `/runs/${robotMetaId}` : '/runs');
+ }, [navigate]);
- const handleChangePage = (event: unknown, newPage: number) => {
+ const handleChangePage = useCallback((event: unknown, newPage: number) => {
setPage(newPage);
- };
+ }, []);
- const handleChangeRowsPerPage = (event: React.ChangeEvent) => {
+ const handleChangeRowsPerPage = useCallback((event: React.ChangeEvent) => {
setRowsPerPage(+event.target.value);
setPage(0);
- };
+ }, []);
- const handleSearchChange = (event: React.ChangeEvent) => {
- setSearchTerm(event.target.value);
- setPage(0);
- };
-
- const fetchRuns = async () => {
- const runs = await getStoredRuns();
- if (runs) {
- const parsedRows: Data[] = runs.map((run: any, index: number) => ({
- id: index,
- ...run,
- }));
- setRows(parsedRows);
- } else {
- notify('error', t('runstable.notifications.no_runs'));
+ const debouncedSearch = useCallback((fn: Function, delay: number) => {
+ let timeoutId: NodeJS.Timeout;
+ return (...args: any[]) => {
+ clearTimeout(timeoutId);
+ timeoutId = setTimeout(() => fn(...args), delay);
+ };
+ }, []);
+
+ const handleSearchChange = useCallback((event: React.ChangeEvent) => {
+ const debouncedSetSearch = debouncedSearch((value: string) => {
+ setSearchTerm(value);
+ setPage(0);
+ }, 300);
+ debouncedSetSearch(event.target.value);
+ }, [debouncedSearch]);
+
+ const fetchRuns = useCallback(async () => {
+ try {
+ setIsLoading(true);
+ const runs = await getStoredRuns();
+ if (runs) {
+ const parsedRows: Data[] = runs.map((run: any, index: number) => ({
+ id: index,
+ ...run,
+ }));
+ setRows(parsedRows);
+ } else {
+ notify('error', t('runstable.notifications.no_runs'));
+ }
+ } catch (error) {
+ notify('error', t('runstable.notifications.fetch_error'));
+ } finally {
+ setIsLoading(false);
}
- };
+ }, [notify, t]);
useEffect(() => {
+ let mounted = true;
+
if (rows.length === 0 || rerenderRuns) {
- fetchRuns();
- setRerenderRuns(false);
+ fetchRuns().then(() => {
+ if (mounted) {
+ setRerenderRuns(false);
+ }
+ });
}
- }, [rerenderRuns, rows.length, setRerenderRuns]);
- const handleDelete = () => {
+ return () => {
+ mounted = false;
+ };
+ }, [rerenderRuns, rows.length, setRerenderRuns, fetchRuns]);
+
+ const handleDelete = useCallback(() => {
setRows([]);
notify('success', t('runstable.notifications.delete_success'));
fetchRuns();
- };
+ }, [notify, t, fetchRuns]);
// Filter rows based on search term
- const filteredRows = rows.filter((row) =>
- row.name.toLowerCase().includes(searchTerm.toLowerCase())
+ const filteredRows = useMemo(() =>
+ rows.filter((row) =>
+ row.name.toLowerCase().includes(searchTerm.toLowerCase())
+ ),
+ [rows, searchTerm]
);
// Group filtered rows by robot meta id
- const groupedRows = filteredRows.reduce((acc, row) => {
- if (!acc[row.robotMetaId]) {
- acc[row.robotMetaId] = [];
- }
- acc[row.robotMetaId].push(row);
- return acc;
- }, {} as Record);
+ const groupedRows = useMemo(() =>
+ filteredRows.reduce((acc, row) => {
+ if (!acc[row.robotMetaId]) {
+ acc[row.robotMetaId] = [];
+ }
+ acc[row.robotMetaId].push(row);
+ return acc;
+ }, {} as Record),
+ [filteredRows]
+ );
+
+ const renderTableRows = useCallback((data: Data[]) => {
+ const start = page * rowsPerPage;
+ const end = start + rowsPerPage;
+
+ return data
+ .slice(start, end)
+ .map((row) => (
+
+ ));
+ }, [page, rowsPerPage, runId, runningRecordingName, currentInterpretationLog, abortRunHandler, handleDelete]);
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
return (
-
+
{t('runstable.runs', 'Runs')}
@@ -160,62 +219,50 @@ export const RunsTable: React.FC = ({
sx={{ width: '250px' }}
/>
- {rows.length === 0 ? (
-
-
-
- ) : (
-
- {Object.entries(groupedRows).map(([id, data]) => (
- handleAccordionChange(id, isExpanded)}>
- }>
- {data[data.length - 1].name}
-
-
-
-
-
-
- {translatedColumns.map((column) => (
-
- {column.label}
-
- ))}
-
-
-
- {data
- .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
- .map((row) => (
-
- ))}
-
-
-
-
- ))}
-
- )}
+
+
+ {Object.entries(groupedRows).map(([id, data]) => (
+ handleAccordionChange(id, isExpanded)}
+ TransitionProps={{ unmountOnExit: true }} // Optimize accordion rendering
+ >
+ }>
+ {data[data.length - 1].name}
+
+
+
+
+
+
+ {translatedColumns.map((column) => (
+
+ {column.label}
+
+ ))}
+
+
+
+ {renderTableRows(data)}
+
+
+
+
+ ))}
+
+
);