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) => ( + + ))} )} +