+
{isTask && (
- Recording parameters:
+
+ Recording parameters:
+
{params?.map((item, index) => (
setShowInterpreterSettings(!showInterpreterSettings)} />}
+ control={
+
+ setShowInterpreterSettings(!showInterpreterSettings)
+ }
+ />
+ }
label="Developer Mode Settings"
- sx={{ margin: '20px 0px' }}
+ sx={{ margin: "20px 0px" }}
/>
{showInterpreterSettings && (
)}
-
+
);
diff --git a/src/components/run/RunsTable.tsx b/src/components/run/RunsTable.tsx
index b652aef0..92233214 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';
@@ -9,7 +9,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 { Accordion, AccordionSummary, AccordionDetails, Typography, Box, TextField } from '@mui/material';
+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 { useNavigate } from 'react-router-dom';
@@ -17,8 +17,8 @@ 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';
-// Export columns before the component
export const columns: readonly Column[] = [
{ id: 'runStatus', label: 'Status', minWidth: 80 },
{ id: 'name', label: 'Name', minWidth: 80 },
@@ -28,6 +28,15 @@ export const columns: readonly Column[] = [
{ id: 'delete', label: 'Delete', minWidth: 80 },
];
+type SortDirection = 'asc' | 'desc' | 'none';
+
+interface AccordionSortConfig {
+ [robotMetaId: string]: {
+ field: keyof Data | null;
+ direction: SortDirection;
+ };
+}
+
interface Column {
id: 'runStatus' | 'name' | 'startedAt' | 'finishedAt' | 'delete' | 'settings';
label: string;
@@ -70,91 +79,223 @@ export const RunsTable: React.FC
= ({
const { t } = useTranslation();
const navigate = useNavigate();
- // Update column labels using translation if needed
- const translatedColumns = columns.map(column => ({
- ...column,
- label: t(`runstable.${column.id}`, column.label)
- }));
+ 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';
+
+ return {
+ ...prevConfigs,
+ [robotMetaId]: {
+ field: newDirection === 'none' ? null : columnId,
+ direction: newDirection,
+ }
+ };
+ });
+ }, []);
+
+ 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(() => {
+ let result = rows.filter((row) =>
+ row.name.toLowerCase().includes(searchTerm.toLowerCase())
+ );
+ return result;
+ }, [rows, searchTerm]);
// Group filtered rows by robot meta id
- const groupedRows = filteredRows.reduce((acc, row) => {
- if (!acc[row.robotMetaId]) {
- acc[row.robotMetaId] = [];
+ 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 parseDateString = (dateStr: string): Date => {
+ try {
+ if (dateStr.includes('PM') || dateStr.includes('AM')) {
+ return new Date(dateStr);
+ }
+
+ return new Date(dateStr.replace(/(\d+)\/(\d+)\//, '$2/$1/'))
+ } catch {
+ return new Date(0);
}
- acc[row.robotMetaId].push(row);
- return acc;
- }, {} as Record);
+ };
+
+ const renderTableRows = useCallback((data: Data[], robotMetaId: string) => {
+ 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();
+ });
+ }
+ }
+
+ return sortedData
+ .slice(start, end)
+ .map((row) => (
+
+ ));
+ }, [page, rowsPerPage, runId, runningRecordingName, currentInterpretationLog, abortRunHandler, handleDelete, accordionSortConfigs]);
+
+ 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]);
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
return (
-
+
{t('runstable.runs', 'Runs')}
@@ -162,9 +303,14 @@ export const RunsTable: React.FC = ({
sx={{ width: '250px' }}
/>
+
- {Object.entries(groupedRows).map(([id, data]) => (
- handleAccordionChange(id, isExpanded)}>
+ {Object.entries(groupedRows).map(([robotMetaId, data]) => (
+ handleAccordionChange(robotMetaId, isExpanded)}
+ TransitionProps={{ unmountOnExit: true }} // Optimize accordion rendering
+ >
}>
{data[data.length - 1].name}
@@ -177,41 +323,65 @@ export const RunsTable: React.FC = ({
{
+ if (column.id === 'startedAt' || column.id === 'finishedAt') {
+ handleSort(column.id, robotMetaId);
+ }
+ }}
>
- {column.label}
+
+
+ {column.label}
+
+ {renderSortIcon(column, robotMetaId)}
+
+
+
))}
- {data
- .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
- .map((row) => (
-
- ))}
+ {renderTableRows(data, robotMetaId)}
))}
+
);
diff --git a/src/context/globalInfo.tsx b/src/context/globalInfo.tsx
index 92ce7e62..28d65b34 100644
--- a/src/context/globalInfo.tsx
+++ b/src/context/globalInfo.tsx
@@ -10,6 +10,8 @@ interface GlobalInfo {
notification: AlertSnackbarProps;
notify: (severity: 'error' | 'warning' | 'info' | 'success', message: string) => void;
closeNotify: () => void;
+ isLogin: boolean;
+ setIsLogin: (isLogin: boolean) => void;
recordings: string[];
setRecordings: (recordings: string[]) => void;
rerenderRuns: boolean;
@@ -53,6 +55,7 @@ class GlobalInfoStore implements Partial {
recordingName = '';
initialUrl = 'https://';
recordingUrl = 'https://';
+ isLogin = false;
currentWorkflowActionsState = {
hasScrapeListAction: false,
hasScreenshotAction: false,
@@ -75,6 +78,7 @@ export const GlobalInfoProvider = ({ children }: { children: JSX.Element }) => {
const [recordingLength, setRecordingLength] = useState(globalInfoStore.recordingLength);
const [recordingId, setRecordingId] = useState(globalInfoStore.recordingId);
const [recordingName, setRecordingName] = useState(globalInfoStore.recordingName);
+ const [isLogin, setIsLogin] = useState(globalInfoStore.isLogin);
const [initialUrl, setInitialUrl] = useState(globalInfoStore.initialUrl);
const [recordingUrl, setRecordingUrl] = useState(globalInfoStore.recordingUrl);
const [currentWorkflowActionsState, setCurrentWorkflowActionsState] = useState(globalInfoStore.currentWorkflowActionsState);
@@ -127,6 +131,8 @@ export const GlobalInfoProvider = ({ children }: { children: JSX.Element }) => {
setInitialUrl,
recordingUrl,
setRecordingUrl,
+ isLogin,
+ setIsLogin,
currentWorkflowActionsState,
setCurrentWorkflowActionsState,
shouldResetInterpretationLog,
diff --git a/src/context/theme-provider.tsx b/src/context/theme-provider.tsx
index d53ced24..75dd4b8b 100644
--- a/src/context/theme-provider.tsx
+++ b/src/context/theme-provider.tsx
@@ -86,6 +86,12 @@ const darkTheme = createTheme({
main: "#ff00c3",
contrastText: "#ffffff",
},
+ error: {
+ main: '#f44336',
+ light: '#e57373',
+ dark: '#d32f2f',
+ contrastText: '#ffffff',
+ },
background: {
default: '#121212',
paper: '#1e1e1e',
@@ -124,6 +130,14 @@ const darkTheme = createTheme({
backgroundColor: 'rgba(255, 0, 195, 0.08)',
borderColor: '#ff66d9',
},
+ '&.MuiButton-outlinedError': {
+ borderColor: '#f44336',
+ color: '#f44336',
+ "&:hover": {
+ backgroundColor: 'rgba(244, 67, 54, 0.08)',
+ borderColor: '#d32f2f',
+ },
+ },
},
},
},
@@ -144,6 +158,12 @@ const darkTheme = createTheme({
"&:hover": {
backgroundColor: 'rgba(255, 0, 195, 0.08)',
},
+ '&.MuiIconButton-colorError': {
+ color: '#f44336',
+ "&:hover": {
+ backgroundColor: 'rgba(244, 67, 54, 0.08)',
+ },
+ },
},
},
},
@@ -218,7 +238,7 @@ const darkTheme = createTheme({
});
const ThemeModeContext = createContext({
- toggleTheme: () => {},
+ toggleTheme: () => { },
darkMode: false,
});
diff --git a/src/pages/PageWrappper.tsx b/src/pages/PageWrapper.tsx
similarity index 96%
rename from src/pages/PageWrappper.tsx
rename to src/pages/PageWrapper.tsx
index d778c186..1e73f26a 100644
--- a/src/pages/PageWrappper.tsx
+++ b/src/pages/PageWrapper.tsx
@@ -12,7 +12,7 @@ import Login from './Login';
import Register from './Register';
import UserRoute from '../routes/userRoute';
import { Routes, Route, useNavigate, Navigate } from 'react-router-dom';
-import { Runs } from '../components/run/Runs';
+import { NotFoundPage } from '../components/dashboard/NotFound';
export const PageWrapper = () => {
const [open, setOpen] = useState(false);
@@ -75,6 +75,7 @@ export const PageWrapper = () => {
path="/register"
element={}
/>
+ } />