diff --git a/src/components/robot/RecordingsTable.tsx b/src/components/robot/RecordingsTable.tsx
index e7409b89..4d928289 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 { IconButton, Button, Box, Typography, TextField, MenuItem, Menu, ListItemIcon, ListItemText, CircularProgress, RadioGroup, FormControlLabel, Radio } from "@mui/material";
@@ -52,6 +52,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, handleScheduleRecording, handleIntegrateRecording, handleSettingsRecording, handleEditRobot, handleDuplicateRobot }: RecordingsTableProps) => {
const { t } = useTranslation();
const [page, setPage] = React.useState(0);
@@ -59,83 +117,77 @@ export const RecordingsTable = ({ handleEditRecording, handleRunRecording, handl
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, setRecordings, browserId, setBrowserId, setInitialUrl, recordingUrl, setRecordingUrl, isLogin, setIsLogin, recordingName, setRecordingName, recordingId, 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);
@@ -151,14 +203,61 @@ export const RecordingsTable = ({ handleEditRecording, handleRunRecording, handl
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 = searchTerm.toLowerCase();
+ return searchTerm
+ ? 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 (
@@ -200,7 +299,7 @@ export const RecordingsTable = ({ handleEditRecording, handleRunRecording, handl
- {rows.length === 0 ? (
+ {isLoading ? (
@@ -210,99 +309,32 @@ export const RecordingsTable = ({ handleEditRecording, handleRunRecording, handl
{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) => (
+
+ ))}
)}
+