Skip to content

feat: sort functionality for runs table data #407

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jan 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions public/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"delete": "Löschen",
"settings": "Einstellungen",
"search": "Ausführungen suchen...",
"sort_tooltip": "Zum Sortieren klicken",
"notifications": {
"no_runs": "Keine Ausführungen gefunden. Bitte versuchen Sie es erneut.",
"delete_success": "Ausführung erfolgreich gelöscht"
Expand Down
1 change: 1 addition & 0 deletions public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"delete":"Delete",
"settings":"Settings",
"search":"Search Runs...",
"sort_tooltip": "Click to sort",
"notifications": {
"no_runs": "No runs found. Please try again.",
"delete_success": "Run deleted successfully"
Expand Down
1 change: 1 addition & 0 deletions public/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"delete": "Eliminar",
"settings": "Ajustes",
"search": "Buscar ejecuciones...",
"sort_tooltip": "Haga clic para ordenar",
"notifications": {
"no_runs": "No se encontraron ejecuciones. Por favor, inténtelo de nuevo.",
"delete_success": "Ejecución eliminada con éxito"
Expand Down
1 change: 1 addition & 0 deletions public/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"delete": "削除",
"settings": "設定",
"search": "実行を検索...",
"sort_tooltip": "クリックして並べ替え",
"notifications": {
"no_runs": "実行が見つかりません。もう一度お試しください。",
"delete_success": "実行が正常に削除されました"
Expand Down
1 change: 1 addition & 0 deletions public/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"delete": "删除",
"settings": "设置",
"search": "搜索运行记录...",
"sort_tooltip": "点击排序",
"notifications": {
"no_runs": "未找到运行记录。请重试。",
"delete_success": "运行记录删除成功"
Expand Down
149 changes: 134 additions & 15 deletions src/components/run/RunsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ 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, CircularProgress } 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';
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 const columns: readonly Column[] = [
{ id: 'runStatus', label: 'Status', minWidth: 80 },
Expand All @@ -27,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;
Expand Down Expand Up @@ -69,6 +79,26 @@ export const RunsTable: React.FC<RunsTableProps> = ({
const { t } = useTranslation();
const navigate = useNavigate();

const [accordionSortConfigs, setAccordionSortConfigs] = useState<AccordionSortConfig>({});

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,
Expand Down Expand Up @@ -157,12 +187,12 @@ export const RunsTable: React.FC<RunsTableProps> = ({
}, [notify, t, fetchRuns]);

// Filter rows based on search term
const filteredRows = useMemo(() =>
rows.filter((row) =>
const filteredRows = useMemo(() => {
let result = rows.filter((row) =>
row.name.toLowerCase().includes(searchTerm.toLowerCase())
),
[rows, searchTerm]
);
);
return result;
}, [rows, searchTerm]);

// Group filtered rows by robot meta id
const groupedRows = useMemo(() =>
Expand All @@ -176,11 +206,39 @@ export const RunsTable: React.FC<RunsTableProps> = ({
[filteredRows]
);

const renderTableRows = useCallback((data: Data[]) => {
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);
}
};
Comment on lines +209 to +219
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance date parsing robustness.

The current date parsing implementation has potential issues:

  1. The date format detection is fragile
  2. Silently falling back to epoch time (Date(0)) could hide parsing errors

Consider using a robust date parsing library:

-  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);
-    }
-  };
+  import { parseISO, parse } from 'date-fns';
+  
+  const parseDateString = (dateStr: string): Date => {
+    try {
+      // First try ISO format
+      const isoDate = parseISO(dateStr);
+      if (!isNaN(isoDate.getTime())) return isoDate;
+      
+      // Then try common formats
+      const formats = [
+        'MM/dd/yyyy h:mm a',
+        'dd/MM/yyyy HH:mm',
+      ];
+      
+      for (const format of formats) {
+        const parsedDate = parse(dateStr, format, new Date());
+        if (!isNaN(parsedDate.getTime())) return parsedDate;
+      }
+      
+      throw new Error(`Unable to parse date: ${dateStr}`);
+    } catch (error) {
+      console.error(`Date parsing error: ${error}`);
+      throw error; // Let the caller handle the error
+    }
+  };

Committable suggestion skipped: line range outside the PR's diff.


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 data
return sortedData
.slice(start, end)
.map((row) => (
<CollapsibleRow
Expand All @@ -193,7 +251,33 @@ export const RunsTable: React.FC<RunsTableProps> = ({
runningRecordingName={runningRecordingName}
/>
));
}, [page, rowsPerPage, runId, runningRecordingName, currentInterpretationLog, abortRunHandler, handleDelete]);
}, [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 (
<UnfoldMore
fontSize="small"
sx={{
opacity: 0.3,
transition: 'opacity 0.2s',
'.MuiTableCell-root:hover &': {
opacity: 1
}
}}
/>
);
}

return sortConfig.direction === 'asc'
? <ArrowUpward fontSize="small" />
: sortConfig.direction === 'desc'
? <ArrowDownward fontSize="small" />
: <UnfoldMore fontSize="small" />;
}, [accordionSortConfigs]);

if (isLoading) {
return (
Expand Down Expand Up @@ -221,10 +305,10 @@ export const RunsTable: React.FC<RunsTableProps> = ({
</Box>

<TableContainer component={Paper} sx={{ width: '100%', overflow: 'hidden' }}>
{Object.entries(groupedRows).map(([id, data]) => (
{Object.entries(groupedRows).map(([robotMetaId, data]) => (
<Accordion
key={id}
onChange={(event, isExpanded) => handleAccordionChange(id, isExpanded)}
key={robotMetaId}
onChange={(event, isExpanded) => handleAccordionChange(robotMetaId, isExpanded)}
TransitionProps={{ unmountOnExit: true }} // Optimize accordion rendering
>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
Expand All @@ -239,15 +323,50 @@ export const RunsTable: React.FC<RunsTableProps> = ({
<TableCell
key={column.id}
align={column.align}
style={{ minWidth: column.minWidth }}
style={{
minWidth: column.minWidth,
cursor: column.id === 'startedAt' || column.id === 'finishedAt' ? 'pointer' : 'default'
}}
onClick={() => {
if (column.id === 'startedAt' || column.id === 'finishedAt') {
handleSort(column.id, robotMetaId);
}
}}
>
{column.label}
<Tooltip
title={
(column.id === 'startedAt' || column.id === 'finishedAt')
? t('runstable.sort_tooltip')
: ''
}
>
<Box sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
'&:hover': {
'& .sort-icon': {
opacity: 1
}
}
}}>
{column.label}
<Box className="sort-icon" sx={{
display: 'flex',
alignItems: 'center',
opacity: accordionSortConfigs[robotMetaId]?.field === column.id ? 1 : 0.3,
transition: 'opacity 0.2s'
}}>
{renderSortIcon(column, robotMetaId)}
</Box>
</Box>
</Tooltip>
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{renderTableRows(data)}
{renderTableRows(data, robotMetaId)}
</TableBody>
</Table>
</AccordionDetails>
Expand Down