diff --git a/web-report/src/AppProvider.tsx b/web-report/src/AppProvider.tsx index a73695c..3b17f91 100644 --- a/web-report/src/AppProvider.tsx +++ b/web-report/src/AppProvider.tsx @@ -28,6 +28,9 @@ type WindowWithFileSystemApi = Window & { }) => Promise; }; +export type StatusFilterState = "inactive" | "active" | "removed"; +export type StatusFilterMap = Record; + type AppContextType = { data: WebFuzzingCommonsReport | null; loading: boolean; @@ -35,7 +38,8 @@ type AppContextType = { testFiles: Record; loadTestFile: (path: string) => Promise; transformedReport: ITransformedReport[]; - filterEndpoints: (activeFilters: Record) => ITransformedReport[]; + statusFilters: StatusFilterMap; + setStatusFilters: (filters: StatusFilterMap) => void; filteredEndpoints: ITransformedReport[]; invalidReportErrors: ZodIssue[] | null; lowCodeMode: boolean; @@ -322,31 +326,19 @@ export const AppProvider = ({ children }: AppProviderProps) => { const clearReviewMessage = useCallback(() => setReviewMessage(null), []); - const [filteredEndpoints, setFilteredEndpoints] = useState(transformedReport); + const [statusFilters, setStatusFilters] = useState({}); - useEffect(() => { - if (data) { - setFilteredEndpoints(transformedReport); - } - }, [data, transformedReport]); - - const filterEndpoints = (activeFilters: Record) => { - // Filter the endpoints based on the active filters - const filtered = transformedReport.filter(endpoint => { - // If no filters are active, show all endpoints - if (Object.keys(activeFilters).length === 0) { - return true; - } + const filteredEndpoints = useMemo(() => { + const activeFilters = statusFilters; + return transformedReport.filter(endpoint => { + if (Object.keys(activeFilters).length === 0) return true; - // Check if any status code or fault code is marked as "removed" const hasRemovedStatusCode = endpoint.httpStatusCodes.some(code => activeFilters[code.code] === "removed" ); const hasRemovedFaultCode = endpoint.faults.some(code => activeFilters[-code.code] === "removed" ); - - // Check if any status code or fault code is marked as "active" const hasActiveStatusCode = endpoint.httpStatusCodes.some(code => activeFilters[code.code] === "active" ); @@ -354,40 +346,18 @@ export const AppProvider = ({ children }: AppProviderProps) => { activeFilters[-code.code] === "active" ); - const hasActiveFilter = activeFilters && Object.values(activeFilters).some((value) => value === "active"); - const hasRemovedFilter = activeFilters && Object.values(activeFilters).some((value) => value === "removed"); + const hasActiveFilter = Object.values(activeFilters).some(v => v === "active"); + const hasRemovedFilter = Object.values(activeFilters).some(v => v === "removed"); - if (!hasActiveFilter && !hasRemovedFilter) { - // If no filters are active, show all endpoints - return true; - } + if (!hasActiveFilter && !hasRemovedFilter) return true; if (hasActiveFilter) { - if (hasRemovedFilter) { - // If there are both active and removed filters, check if the endpoint matches any of them - if(hasRemovedFaultCode || hasRemovedStatusCode) { - return false; - } - - return !!(hasActiveStatusCode || hasActiveFaultCode); - - } else { - // If there are only active filters, check if the endpoint matches any of them - return !!(hasActiveStatusCode || hasActiveFaultCode); - - } - } else if (hasRemovedFilter) { - // If there are only removed filters, check if the endpoint matches any of them - return !(hasRemovedStatusCode || hasRemovedFaultCode); - - } else { - // If there are no active or removed filters, show all endpoints - return true; + if (hasRemovedFilter && (hasRemovedStatusCode || hasRemovedFaultCode)) return false; + return hasActiveStatusCode || hasActiveFaultCode; } + return !(hasRemovedStatusCode || hasRemovedFaultCode); }); - setFilteredEndpoints(filtered) - return filtered; - } + }, [statusFilters, transformedReport]); const value: AppContextType = { data, @@ -395,7 +365,8 @@ export const AppProvider = ({ children }: AppProviderProps) => { error, testFiles, transformedReport, - filterEndpoints, + statusFilters, + setStatusFilters, filteredEndpoints, invalidReportErrors, lowCodeMode, diff --git a/web-report/src/components/EndpointAccordion.tsx b/web-report/src/components/EndpointAccordion.tsx index 25d0518..a05abf8 100644 --- a/web-report/src/components/EndpointAccordion.tsx +++ b/web-report/src/components/EndpointAccordion.tsx @@ -37,11 +37,14 @@ export const EndpointAccordion: React.FC = ({ } ); - const [selectedCode, setSelectedCode] = useState(sortedStatusCodes[0]?.code || 0); - const [isFault, setIsFault] = useState(false); + type Selection = {code: number | string; isFault: boolean}; + const [selection, setSelection] = useState(null); - const selectedTestCases = statusCodes.find((code) => code.code === selectedCode)?.testCases || []; - const selectedFaultTestCases = faults.find((code) => code.code === selectedCode)?.testCases || []; + const toggleSelection = (code: number | string, fault: boolean) => { + setSelection(prev => + prev && prev.code === code && prev.isFault === fault ? null : {code, isFault: fault} + ); + }; const sortedFaults = faults.sort((a, b) => { const codeA = Number(a.code); @@ -51,13 +54,22 @@ export const EndpointAccordion: React.FC = ({ } return codeA - codeB; }); - const getSelectedStyle = (code: number | string, fault: boolean) => { - const isSelected = selectedCode === code && isFault === fault; + const getSelectedStyle = (code: number | string, fault: boolean) => { + const isSelected = selection?.code === code && selection?.isFault === fault; return isSelected ? "ring-2 ring-offset-2 ring-offset-white ring-blue-400 shadow-md" : ""; - } + const selectedGroup = selection + ? (selection.isFault + ? sortedFaults.find(f => f.code === selection.code) + : sortedStatusCodes.find(s => s.code === selection.code)) + : null; + + const nonEmptyStatusGroups = sortedStatusCodes.filter(c => c.testCases.length > 0); + const nonEmptyFaultGroups = sortedFaults.filter(f => f.testCases.length > 0); + const hasAnyTestCases = nonEmptyStatusGroups.length > 0 || nonEmptyFaultGroups.length > 0; + const faultColors = ["bg-red-300", "bg-red-500", "bg-red-700"]; return ( @@ -82,10 +94,7 @@ export const EndpointAccordion: React.FC = ({
{ sortedStatusCodes.map((code, index) => ( - { - setSelectedCode(code.code); - setIsFault(false); - }} + toggleSelection(code.code, false)} className={`${getColor(code.code, true, false)} ${getSelectedStyle(code.code, false)} ${getHoverColor(code.code, false)} cursor-pointer text-white font-mono text-base border-2 border-black shadow-[2px_2px_0px_0px_rgba(0,0,0,1)]`}> {code.code == -1 ? "NO-RESPONSE" : `H${code.code}`} @@ -103,10 +112,7 @@ export const EndpointAccordion: React.FC = ({
{ sortedFaults.map((fault, index) => ( - { - setSelectedCode(fault.code) - setIsFault(true); - }} + toggleSelection(fault.code, true)} className={`${faultColors[index % faultColors.length]} ${getSelectedStyle(fault.code, true)} hover:bg-red-400 cursor-pointer text-white text-base font-mono border-2 border-black shadow-[2px_2px_0px_0px_rgba(0,0,0,1)]`}> F{fault.code} @@ -118,15 +124,30 @@ export const EndpointAccordion: React.FC = ({ }
-
Click to show test cases.
+
+ {selection ? "Click the highlighted code again to clear the filter." : "Showing all test cases. Click a code to filter."} +
- { - (selectedTestCases.length > 0 || selectedFaultTestCases.length > 0) && + {hasAnyTestCases && (
- 0 && !isFault ? selectedTestCases : selectedFaultTestCases}/> + {selection && selectedGroup && selectedGroup.testCases.length > 0 && ( + + )} + {!selection && ( + <> + {nonEmptyStatusGroups.map(c => ( + + ))} + {nonEmptyFaultGroups.map(f => ( + + ))} + + )}
- } + )}
) diff --git a/web-report/src/components/StatusCodeFilterButton.tsx b/web-report/src/components/StatusCodeFilterButton.tsx index 86572f1..eb95072 100644 --- a/web-report/src/components/StatusCodeFilterButton.tsx +++ b/web-report/src/components/StatusCodeFilterButton.tsx @@ -1,17 +1,15 @@ -import { useState } from "react" import {Badge} from "@/components/ui/badge.tsx"; type FilterState = "inactive" | "active" | "removed" interface StatusCodeFilterButtonProps { code: number - initialState?: FilterState + state: FilterState onChange: (code: number, state: FilterState) => void, isFault?: boolean } -export function StatusCodeFilterButton({ code, initialState = "inactive", onChange, isFault }: StatusCodeFilterButtonProps) { - const [state, setState] = useState(initialState) +export function StatusCodeFilterButton({ code, state, onChange, isFault }: StatusCodeFilterButtonProps) { const getBackgroundColor = () => { if(isFault) { @@ -40,7 +38,6 @@ export function StatusCodeFilterButton({ code, initialState = "inactive", onChan const toggleState = () => { const newState: FilterState = state === "inactive" ? "active" : state === "active" ? "removed" : "inactive" - setState(newState) onChange(code, newState) } diff --git a/web-report/src/components/StatusCodeFilters.tsx b/web-report/src/components/StatusCodeFilters.tsx index ce18bd0..a0433b1 100644 --- a/web-report/src/components/StatusCodeFilters.tsx +++ b/web-report/src/components/StatusCodeFilters.tsx @@ -1,4 +1,4 @@ -import {useState} from "react" +import {useState, useMemo} from "react" import {StatusCodeFilterButton} from "./StatusCodeFilterButton" import {ITransformedReport} from "@/lib/utils.tsx"; import {StatusCodeModal} from "@/components/StatusCodeModal.tsx"; @@ -7,36 +7,27 @@ type FilterState = "inactive" | "active" | "removed" interface StatusCodeFiltersProps { data: ITransformedReport[] + filters: Record onFiltersChange: (activeFilters: Record) => void } -export function StatusCodeFilters({data, onFiltersChange}: StatusCodeFiltersProps) { +export function StatusCodeFilters({data, filters, onFiltersChange}: StatusCodeFiltersProps) { - // Extract all unique status codes from endpoints - const allStatusCodes = [...new Map( - data.flatMap(endpoint => endpoint.httpStatusCodes) - .map(item => [item.code, item]) - ).values()].sort((a, b) => a.code - b.code); + const allStatusCodes = useMemo(() => + [...new Map( + data.flatMap(endpoint => endpoint.httpStatusCodes) + .map(item => [item.code, item]) + ).values()].sort((a, b) => a.code - b.code), + [data] + ); - const allFaultCodes = [...new Set(data.map(item => { - return item.faults.map(fault => fault.code) - }).flat())].sort((a, b) => a - b); + const allFaultCodes = useMemo(() => + [...new Set(data.flatMap(item => item.faults.map(fault => fault.code)))].sort((a, b) => a - b), + [data] + ); - const initialFilters = allStatusCodes.reduce((acc, code) => { - acc[code.code] = "inactive" - return acc - }, {} as Record) - allFaultCodes.forEach(code => { - initialFilters[-code] = "inactive" - }) - const [filters, setFilters] = useState>(initialFilters) - - // Handle filter state change const handleFilterChange = (code: number, state: FilterState) => { - const newFilters = {...filters, [code]: state} - - setFilters(newFilters) - onFiltersChange(newFilters) + onFiltersChange({...filters, [code]: state}); } const [isModalOpen, setIsModalOpen] = useState(false) @@ -51,7 +42,7 @@ export function StatusCodeFilters({data, onFiltersChange}: StatusCodeFiltersProp ))} @@ -64,7 +55,7 @@ export function StatusCodeFilters({data, onFiltersChange}: StatusCodeFiltersProp diff --git a/web-report/src/pages/Endpoints.tsx b/web-report/src/pages/Endpoints.tsx index 440ffcf..fb04526 100644 --- a/web-report/src/pages/Endpoints.tsx +++ b/web-report/src/pages/Endpoints.tsx @@ -10,11 +10,11 @@ interface IProps { export const Endpoints: React.FC = ({addTestTab}) => { - const {transformedReport, filteredEndpoints, filterEndpoints} = useAppContext(); + const {transformedReport, filteredEndpoints, statusFilters, setStatusFilters} = useAppContext(); return (
- +

# Endpoints: