From 1ae738ebc2cf55366ea42ee58ce823b1661d008d Mon Sep 17 00:00:00 2001 From: Rohit Date: Thu, 23 Jan 2025 19:12:31 +0530 Subject: [PATCH 1/5] feat: memoize recordings table --- src/components/robot/RecordingsTable.tsx | 309 +++++++++++++---------- 1 file changed, 175 insertions(+), 134 deletions(-) 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) => ( + + ))} )} + Date: Thu, 23 Jan 2025 19:21:53 +0530 Subject: [PATCH 2/5] feat: add debounced search term --- src/components/robot/RecordingsTable.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/robot/RecordingsTable.tsx b/src/components/robot/RecordingsTable.tsx index 4d928289..c679ec14 100644 --- a/src/components/robot/RecordingsTable.tsx +++ b/src/components/robot/RecordingsTable.tsx @@ -225,8 +225,8 @@ export const RecordingsTable = ({ handleEditRecording, handleRunRecording, handl // Filter rows based on search term const filteredRows = useMemo(() => { - const searchLower = searchTerm.toLowerCase(); - return searchTerm + const searchLower = debouncedSearchTerm.toLowerCase(); + return debouncedSearchTerm ? rows.filter(row => row.name.toLowerCase().includes(searchLower)) : rows; }, [rows, debouncedSearchTerm]); From 86d0d2c4c366cfa341f1aa8a7e617ff8e54a999f Mon Sep 17 00:00:00 2001 From: Rohit Date: Thu, 23 Jan 2025 19:50:09 +0530 Subject: [PATCH 3/5] feat: memoize runs table --- src/components/run/RunsTable.tsx | 243 ++++++++++++++++++------------- 1 file changed, 145 insertions(+), 98 deletions(-) diff --git a/src/components/run/RunsTable.tsx b/src/components/run/RunsTable.tsx index 1264dd3c..0bb1edca 100644 --- a/src/components/run/RunsTable.tsx +++ b/src/components/run/RunsTable.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { useEffect, useState } 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'; @@ -69,90 +69,149 @@ export const RunsTable: React.FC = ({ 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 debouncedSearch = useCallback((fn: Function, delay: number) => { + let timeoutId: NodeJS.Timeout; + return (...args: any[]) => { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => fn(...args), delay); + }; + }, []); - 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 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(() => { - if (rows.length === 0 || rerenderRuns) { - fetchRuns(); - setRerenderRuns(false); - } - }, [rerenderRuns, rows.length, setRerenderRuns]); + let mounted = true; - const handleDelete = () => { + if (rows.length === 0 || rerenderRuns) { + fetchRuns().then(() => { + if (mounted) { + setRerenderRuns(false); + } + }); + } + + 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)} + +
+
+
+ ))} +
+
); From 7289dadfcb9f2dfdef40e68d27182b0c2bab9d82 Mon Sep 17 00:00:00 2001 From: Rohit Date: Thu, 23 Jan 2025 20:08:38 +0530 Subject: [PATCH 4/5] feat: incr limit for pagination --- src/components/run/RunsTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/run/RunsTable.tsx b/src/components/run/RunsTable.tsx index 0bb1edca..a83f391f 100644 --- a/src/components/run/RunsTable.tsx +++ b/src/components/run/RunsTable.tsx @@ -262,7 +262,7 @@ export const RunsTable: React.FC = ({ page={page} onPageChange={handleChangePage} onRowsPerPageChange={handleChangeRowsPerPage} - rowsPerPageOptions={[10, 25, 50]} + rowsPerPageOptions={[10, 25, 50, 100]} />
); From 1c78b0fe10eef271c7d75a63e3aee0ac9e87cfbc Mon Sep 17 00:00:00 2001 From: Karishma Shukla Date: Sat, 25 Jan 2025 23:30:38 +0530 Subject: [PATCH 5/5] feat: use `libgbm1` instead of `libgbm-dev` --- server/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/Dockerfile b/server/Dockerfile index e7d1a061..65e472eb 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -25,7 +25,7 @@ RUN mkdir -p /tmp/chromium-data-dir && \ # Install dependencies RUN apt-get update && apt-get install -y \ - libgbm-dev \ + libgbm1 \ libnss3 \ libatk1.0-0 \ libatk-bridge2.0-0 \ @@ -48,4 +48,4 @@ RUN apt-get update && apt-get install -y \ EXPOSE ${BACKEND_PORT:-8080} # Start the backend using the start script -CMD ["npm", "run", "server"] \ No newline at end of file +CMD ["npm", "run", "server"]