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 = () => {
Status: {detail.status}
-
{detail.runtime_seconds.toFixed(2)}s
+
+ {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 = () => { />
- - -
diff --git a/projects/frontend/src/components/Findings/FindingsFilters.jsx b/projects/frontend/src/components/Findings/FindingsFilters.jsx index 2e82d5f..5ed56aa 100644 --- a/projects/frontend/src/components/Findings/FindingsFilters.jsx +++ b/projects/frontend/src/components/Findings/FindingsFilters.jsx @@ -4,7 +4,9 @@ import { useSearchParams } from 'react-router-dom'; const FindingsFilters = ({ findings, - onFilteredDataChange + onFilteredDataChange, + sortColumn, + sortDirection }) => { const [searchParams, setSearchParams] = useSearchParams(); @@ -121,9 +123,9 @@ const FindingsFilters = ({ } }, [categoryFilter, severityFilter, originFilter, triageFilter, triageSourceFilter, objectIdFilter, setSearchParams, searchParams]); - // Memoized filtered findings calculation + // Memoized filtered and sorted findings calculation const filteredFindings = useMemo(() => { - return findings.filter(finding => { + const filtered = findings.filter(finding => { // Category filter if (categoryFilter !== 'all' && finding.category !== categoryFilter) return false; @@ -173,7 +175,44 @@ const FindingsFilters = ({ return true; }); - }, [findings, categoryFilter, severityFilter, originFilter, triageFilter, triageSourceFilter, objectIdFilter]); + + // Apply sorting + return filtered.sort((a, b) => { + let comparison = 0; + + switch (sortColumn) { + case 'severity': + comparison = a.severity - b.severity; + break; + case 'created_at': + comparison = new Date(a.created_at) - new Date(b.created_at); + break; + case 'finding_name': + comparison = a.finding_name.localeCompare(b.finding_name); + break; + case 'category': + comparison = a.category.localeCompare(b.category); + break; + case 'origin_name': + comparison = a.origin_name.localeCompare(b.origin_name); + break; + case 'file_path': + const pathA = a.files_enriched?.path || ''; + const pathB = b.files_enriched?.path || ''; + comparison = pathA.localeCompare(pathB); + break; + case 'triage_value': + const triageA = a.finding_triage_histories.length > 0 ? a.finding_triage_histories[0].value : ''; + const triageB = b.finding_triage_histories.length > 0 ? b.finding_triage_histories[0].value : ''; + comparison = triageA.localeCompare(triageB); + break; + default: + comparison = new Date(a.created_at) - new Date(b.created_at); + } + + return sortDirection === 'asc' ? comparison : -comparison; + }); + }, [findings, categoryFilter, severityFilter, originFilter, triageFilter, triageSourceFilter, objectIdFilter, sortColumn, sortDirection]); // Notify parent component of filtered data changes useEffect(() => { diff --git a/projects/frontend/src/components/Findings/FindingsList.jsx b/projects/frontend/src/components/Findings/FindingsList.jsx index 930768f..d66bf01 100644 --- a/projects/frontend/src/components/Findings/FindingsList.jsx +++ b/projects/frontend/src/components/Findings/FindingsList.jsx @@ -43,6 +43,40 @@ const FindingsList = () => { const lastDirection = useRef('down'); const listRef = useRef(); + // Sorting state + const [sortColumn, setSortColumn] = useState(() => searchParams.get('sort_column') || 'created_at'); + const [sortDirection, setSortDirection] = useState(() => searchParams.get('sort_direction') || 'desc'); + + // Handle column sorting + const handleSort = (column, direction) => { + setSortColumn(column); + setSortDirection(direction); + }; + + // Update URL parameters when sort changes + useEffect(() => { + const newParams = new URLSearchParams(searchParams); + newParams.set('sort_column', sortColumn); + newParams.set('sort_direction', sortDirection); + + if (newParams.toString() !== searchParams.toString()) { + setSearchParams(newParams, { replace: true }); + } + }, [sortColumn, sortDirection, searchParams, setSearchParams]); + + // Update sort state when URL parameters change + useEffect(() => { + const urlSortColumn = searchParams.get('sort_column'); + const urlSortDirection = searchParams.get('sort_direction'); + + if (urlSortColumn && urlSortColumn !== sortColumn) { + setSortColumn(urlSortColumn); + } + if (urlSortDirection && urlSortDirection !== sortDirection) { + setSortDirection(urlSortDirection); + } + }, [searchParams]); + // Function to handle multi-selection triage const handleBulkTriage = (value) => { selectedFindings.forEach(findingId => { @@ -494,12 +528,19 @@ const FindingsList = () => { {/* Findings Table */}
{/* 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
-
+ + + Actions +
);