diff --git a/projects/frontend/src/components/Dashboard/StatsOverview.jsx b/projects/frontend/src/components/Dashboard/StatsOverview.jsx
index da825d9..802e3d5 100644
--- a/projects/frontend/src/components/Dashboard/StatsOverview.jsx
+++ b/projects/frontend/src/components/Dashboard/StatsOverview.jsx
@@ -671,7 +671,9 @@ const StatsOverview = () => {
+ {detail.runtime_seconds ? detail.runtime_seconds.toFixed(2) : '0.00'}s
+
))}
@@ -704,7 +706,9 @@ const StatsOverview = () => {
)}
-
{workflow.runtime_seconds?.toFixed(2)}s
+
+ {workflow.runtime_seconds ? workflow.runtime_seconds.toFixed(2) : '0.00'}s
+
{new Date(workflow.timestamp).toLocaleTimeString()}
diff --git a/projects/frontend/src/components/FileList/FileList.jsx b/projects/frontend/src/components/FileList/FileList.jsx
index 933ead5..ee7aafe 100644
--- a/projects/frontend/src/components/FileList/FileList.jsx
+++ b/projects/frontend/src/components/FileList/FileList.jsx
@@ -2,7 +2,7 @@ import Tooltip from '@/components/shared/Tooltip2';
import { useTriageMode } from '@/contexts/TriageModeContext';
import { useUser } from '@/contexts/UserContext';
import { createClient } from 'graphql-ws';
-import { AlertTriangle, ChevronDown, Clock, Eye, Search, Tag, X } from 'lucide-react';
+import { AlertTriangle, ChevronDown, ChevronUp, Clock, Eye, Search, Tag, X } from 'lucide-react';
import React, { useEffect, useRef, useState } from 'react';
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
import AutoSizer from 'react-virtualized-auto-sizer';
@@ -120,6 +120,32 @@ const Row = React.memo(({ index, style, data }) => {
);
});
+// Sortable header component
+const SortableHeader = ({ children, column, currentSort, currentDirection, onSort, className = "" }) => {
+ const isActive = currentSort === column;
+ const nextDirection = isActive && currentDirection === 'asc' ? 'desc' : 'asc';
+
+ return (
+
onSort(column, nextDirection)}
+ >
+
{children}
+
+ {isActive ? (
+ currentDirection === 'asc' ? (
+
+ ) : (
+
+ )
+ ) : (
+
+ )}
+
+
+ );
+};
+
const FileList = () => {
const navigate = useNavigate();
@@ -138,7 +164,8 @@ const FileList = () => {
const [agentIdFilter, setAgentIdFilter] = useState(() => searchParams.get('agent_id') || '');
const [pathFilter, setPathFilter] = useState(() => searchParams.get('path') || '');
const [objectIdFilter, setObjectIdFilter] = useState(() => searchParams.get('object_id') || '');
- const [sortNewestFirst, setSortNewestFirst] = useState(true);
+ const [sortColumn, setSortColumn] = useState(() => searchParams.get('sort_column') || 'timestamp');
+ const [sortDirection, setSortDirection] = useState(() => searchParams.get('sort_direction') || 'desc');
const [viewFilter, setViewFilter] = useState(() => searchParams.get('view_state') || 'unviewed_by_me');
const [showOnlyWithFindings, setShowOnlyWithFindings] = useState(false);
@@ -147,6 +174,12 @@ const FileList = () => {
const [isTagDropdownOpen, setIsTagDropdownOpen] = useState(false);
const tagDropdownRef = useRef(null);
+ // Handle column sorting
+ const handleSort = (column, direction) => {
+ setSortColumn(column);
+ setSortDirection(direction);
+ };
+
const handleRowClick = (e, file, index) => {
if (isTriageMode) {
if (e.shiftKey && selectedIndex !== -1) {
@@ -269,8 +302,9 @@ const FileList = () => {
params.set('tag', selectedTag);
}
- // Add sort order to URL params
- params.set('sort', sortNewestFirst ? 'newest' : 'oldest');
+ // Add sort column and direction to URL params
+ params.set('sort_column', sortColumn);
+ params.set('sort_direction', sortDirection);
// Add findings filter to URL params
if (showOnlyWithFindings) {
@@ -279,7 +313,7 @@ const FileList = () => {
// Use replace: true to avoid adding to browser history for every filter change
setSearchParams(params, { replace: true });
- }, [fileTypeFilter, agentIdFilter, pathFilter, viewFilter, objectIdFilter, selectedTag, sortNewestFirst, showOnlyWithFindings]);
+ }, [fileTypeFilter, agentIdFilter, pathFilter, viewFilter, objectIdFilter, selectedTag, sortColumn, sortDirection, showOnlyWithFindings]);
// Watch for URL changes and update the state
useEffect(() => {
@@ -290,7 +324,8 @@ const FileList = () => {
const viewStateParam = searchParams.get('view_state');
const objectIdParam = searchParams.get('object_id');
const tagParam = searchParams.get('tag');
- const sortParam = searchParams.get('sort');
+ const sortColumnParam = searchParams.get('sort_column');
+ const sortDirectionParam = searchParams.get('sort_direction');
const findingsParam = searchParams.get('findings');
// Update component state based on URL params
@@ -301,9 +336,12 @@ const FileList = () => {
setObjectIdFilter(objectIdParam || '');
setSelectedTag(tagParam || '');
- // Update sort order from URL
- if (sortParam !== null) {
- setSortNewestFirst(sortParam === 'newest');
+ // Update sort column and direction from URL
+ if (sortColumnParam !== null) {
+ setSortColumn(sortColumnParam);
+ }
+ if (sortDirectionParam !== null) {
+ setSortDirection(sortDirectionParam);
}
// Update findings filter from URL
@@ -422,9 +460,32 @@ const FileList = () => {
return true;
})
.sort((a, b) => {
- const dateA = new Date(a.timestamp);
- const dateB = new Date(b.timestamp);
- return sortNewestFirst ? dateB - dateA : dateA - dateB;
+ let comparison = 0;
+
+ switch (sortColumn) {
+ case 'agent_id':
+ comparison = a.agent_id.toString().localeCompare(b.agent_id.toString());
+ break;
+ case 'size':
+ comparison = a.size - b.size;
+ break;
+ case 'timestamp':
+ comparison = new Date(a.timestamp) - new Date(b.timestamp);
+ break;
+ case 'magic_type':
+ comparison = (a.magic_type || '').localeCompare(b.magic_type || '');
+ break;
+ case 'findings':
+ comparison = a.findingsByObjectId_aggregate.aggregate.count - b.findingsByObjectId_aggregate.aggregate.count;
+ break;
+ case 'path':
+ comparison = a.path.localeCompare(b.path);
+ break;
+ default:
+ comparison = new Date(a.timestamp) - new Date(b.timestamp);
+ }
+
+ return sortDirection === 'asc' ? comparison : -comparison;
});
@@ -806,17 +867,6 @@ const FileList = () => {
/>
-
{/* Headers - Keep these outside the virtualized area */}
-
+
{/* Virtualized List or No Findings Message */}
{isLoading ? (
diff --git a/projects/frontend/src/components/Findings/Table.jsx b/projects/frontend/src/components/Findings/Table.jsx
index 119aad4..f35ffc9 100644
--- a/projects/frontend/src/components/Findings/Table.jsx
+++ b/projects/frontend/src/components/Findings/Table.jsx
@@ -1,36 +1,104 @@
import Tooltip from '@/components/shared/Tooltip2';
-import { Bot, HelpCircle, ThumbsDown, ThumbsUp } from 'lucide-react';
+import { Bot, ChevronDown, ChevronUp, HelpCircle, ThumbsDown, ThumbsUp } from 'lucide-react';
import React from 'react';
import { useFileNavigation } from './navigation';
+// Sortable header component
+const SortableHeader = ({ children, column, currentSort, currentDirection, onSort, className = "" }) => {
+ const isActive = currentSort === column;
+ const nextDirection = isActive && currentDirection === 'asc' ? 'desc' : 'asc';
+
+ return (
+
onSort(column, nextDirection)}
+ >
+
{children}
+
+ {isActive ? (
+ currentDirection === 'asc' ? (
+
+ ) : (
+
+ )
+ ) : (
+
+ )}
+
+
+ );
+};
-export const TableHeaders = ({ isTriageMode }) => (
+
+export const TableHeaders = ({ isTriageMode, sortColumn, sortDirection, onSort }) => (
{isTriageMode && (
)}
-
+
Severity
-
-
+
+
Timestamp
-
-
+
+
Name
-
-
+
+
Category
-
-
+
+
Origin
-
-
+
+
File Path
-
-
+
+
+ Actions
+
);