Skip to content
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
67 changes: 19 additions & 48 deletions web-report/src/AppProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,18 @@ type WindowWithFileSystemApi = Window & {
}) => Promise<FileSystemApiHandle>;
};

export type StatusFilterState = "inactive" | "active" | "removed";
export type StatusFilterMap = Record<number, StatusFilterState>;

type AppContextType = {
data: WebFuzzingCommonsReport | null;
loading: boolean;
error: string | null;
testFiles: Record<string, string>;
loadTestFile: (path: string) => Promise<void>;
transformedReport: ITransformedReport[];
filterEndpoints: (activeFilters: Record<number, string>) => ITransformedReport[];
statusFilters: StatusFilterMap;
setStatusFilters: (filters: StatusFilterMap) => void;
filteredEndpoints: ITransformedReport[];
invalidReportErrors: ZodIssue[] | null;
lowCodeMode: boolean;
Expand Down Expand Up @@ -322,80 +326,47 @@ export const AppProvider = ({ children }: AppProviderProps) => {

const clearReviewMessage = useCallback(() => setReviewMessage(null), []);

const [filteredEndpoints, setFilteredEndpoints] = useState(transformedReport);
const [statusFilters, setStatusFilters] = useState<StatusFilterMap>({});

useEffect(() => {
if (data) {
setFilteredEndpoints(transformedReport);
}
}, [data, transformedReport]);

const filterEndpoints = (activeFilters: Record<number, string>) => {
// 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"
);
const hasActiveFaultCode = endpoint.faults.some(code =>
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,
loading,
error,
testFiles,
transformedReport,
filterEndpoints,
statusFilters,
setStatusFilters,
filteredEndpoints,
invalidReportErrors,
lowCodeMode,
Expand Down
63 changes: 42 additions & 21 deletions web-report/src/components/EndpointAccordion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,14 @@ export const EndpointAccordion: React.FC<IEndpointAccordionProps> = ({
}
);

const [selectedCode, setSelectedCode] = useState<number | string>(sortedStatusCodes[0]?.code || 0);
const [isFault, setIsFault] = useState(false);
type Selection = {code: number | string; isFault: boolean};
const [selection, setSelection] = useState<Selection | null>(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);
Expand All @@ -51,13 +54,22 @@ export const EndpointAccordion: React.FC<IEndpointAccordionProps> = ({
}
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 (
<AccordionItem value={value} className="border-2 border-black mb-4 overflow-hidden" data-testid={endpoint}>
Expand All @@ -82,10 +94,7 @@ export const EndpointAccordion: React.FC<IEndpointAccordionProps> = ({
<div className="flex flex-wrap gap-2">
{
sortedStatusCodes.map((code, index) => (
<Badge key={index} onClick={() => {
setSelectedCode(code.code);
setIsFault(false);
}}
<Badge key={index} onClick={() => 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}`}
</Badge>
Expand All @@ -103,10 +112,7 @@ export const EndpointAccordion: React.FC<IEndpointAccordionProps> = ({
<div className="flex flex-wrap gap-2">
{
sortedFaults.map((fault, index) => (
<Badge key={index} onClick={() => {
setSelectedCode(fault.code)
setIsFault(true);
}}
<Badge key={index} onClick={() => 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}
</Badge>
Expand All @@ -118,15 +124,30 @@ export const EndpointAccordion: React.FC<IEndpointAccordionProps> = ({
}
</div>
</div>
<div className="text-xs text-gray-500 mt-1">Click to show test cases.</div>
<div className="text-xs text-gray-500 mt-1">
{selection ? "Click the highlighted code again to clear the filter." : "Showing all test cases. Click a code to filter."}
</div>

{
(selectedTestCases.length > 0 || selectedFaultTestCases.length > 0) &&
{hasAnyTestCases && (
<div className="mt-6">
<TestCases addTestTab={addTestTab} isFault={isFault} code={selectedCode}
testCases={selectedTestCases.length > 0 && !isFault ? selectedTestCases : selectedFaultTestCases}/>
{selection && selectedGroup && selectedGroup.testCases.length > 0 && (
<TestCases addTestTab={addTestTab} isFault={selection.isFault} code={selection.code}
testCases={selectedGroup.testCases}/>
)}
{!selection && (
<>
{nonEmptyStatusGroups.map(c => (
<TestCases key={`s-${c.code}`} addTestTab={addTestTab} isFault={false}
code={c.code} testCases={c.testCases}/>
))}
{nonEmptyFaultGroups.map(f => (
<TestCases key={`f-${f.code}`} addTestTab={addTestTab} isFault={true}
code={f.code} testCases={f.testCases}/>
))}
</>
)}
</div>
}
)}
</AccordionContent>
</AccordionItem>
)
Expand Down
7 changes: 2 additions & 5 deletions web-report/src/components/StatusCodeFilterButton.tsx
Original file line number Diff line number Diff line change
@@ -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<FilterState>(initialState)
export function StatusCodeFilterButton({ code, state, onChange, isFault }: StatusCodeFilterButtonProps) {

const getBackgroundColor = () => {
if(isFault) {
Expand Down Expand Up @@ -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)
}

Expand Down
43 changes: 17 additions & 26 deletions web-report/src/components/StatusCodeFilters.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -7,36 +7,27 @@ type FilterState = "inactive" | "active" | "removed"

interface StatusCodeFiltersProps {
data: ITransformedReport[]
filters: Record<number, FilterState>
onFiltersChange: (activeFilters: Record<number, FilterState>) => 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<number, FilterState>)
allFaultCodes.forEach(code => {
initialFilters[-code] = "inactive"
})
const [filters, setFilters] = useState<Record<number, FilterState>>(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)
Expand All @@ -51,7 +42,7 @@ export function StatusCodeFilters({data, onFiltersChange}: StatusCodeFiltersProp
<StatusCodeFilterButton
key={code.code}
code={code.code}
initialState={filters[code.code] || "inactive"}
state={filters[code.code] ?? "inactive"}
onChange={handleFilterChange}
/>
))}
Expand All @@ -64,7 +55,7 @@ export function StatusCodeFilters({data, onFiltersChange}: StatusCodeFiltersProp
<StatusCodeFilterButton
key={-code}
code={-code}
initialState={filters[-code] || "inactive"}
state={filters[-code] ?? "inactive"}
onChange={handleFilterChange}
isFault={true}
/>
Expand Down
4 changes: 2 additions & 2 deletions web-report/src/pages/Endpoints.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ interface IProps {

export const Endpoints: React.FC<IProps> = ({addTestTab}) => {

const {transformedReport, filteredEndpoints, filterEndpoints} = useAppContext();
const {transformedReport, filteredEndpoints, statusFilters, setStatusFilters} = useAppContext();

return (
<div className="border-2 border-black p-3 sm:p-6 rounded-none">
<StatusCodeFilters data={transformedReport} onFiltersChange={filterEndpoints}/>
<StatusCodeFilters data={transformedReport} filters={statusFilters} onFiltersChange={setStatusFilters}/>
<div className="flex items-center mb-2">
<h3 className="text-sm font-medium text-gray-700 mr-3"># Endpoints:</h3>
<div className="flex flex-wrap gap-2 font-bold font-mono">
Expand Down
Loading