diff --git a/pages/utilities/har-file-viewer.tsx b/pages/utilities/har-file-viewer.tsx index 1d99bb0..b12aaca 100644 --- a/pages/utilities/har-file-viewer.tsx +++ b/pages/utilities/har-file-viewer.tsx @@ -21,7 +21,86 @@ import PageHeader from "@/components/PageHeader"; import CallToActionGrid from "@/components/CallToActionGrid"; import HarFileViewerSEO from "@/components/seo/HarFileViewerSEO"; import { HarWaterfall } from "@/components/har-waterfall"; -import { Table, BarChart3 } from "lucide-react"; +import { Table, BarChart3, Filter } from "lucide-react"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ds/PopoverComponent"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ds/CommandMenu"; +import { Checkbox } from "@/components/ds/CheckboxComponent"; + +interface MultiSelectComboboxProps { + data: { value: string; label: string }[]; + selectedValues: string[]; + onSelectionChange: (values: string[]) => void; +} + +function MultiSelectCombobox({ + data, + selectedValues, + onSelectionChange, +}: MultiSelectComboboxProps) { + const [open, setOpen] = useState(false); + + const handleToggle = (value: string) => { + const newSelection = selectedValues.includes(value) + ? selectedValues.filter((v) => v !== value) + : [...selectedValues, value]; + onSelectionChange(newSelection); + }; + + return ( + + + + + + + + + No status codes found. + + {data.map((item) => ( + handleToggle(item.value)} + className="flex items-center space-x-2" + > + handleToggle(item.value)} + /> + {item.label} + + ))} + + + + + + ); +} export default function HARFileViewer() { const [status, setStatus] = useState<"idle" | "unsupported" | "hover">( @@ -30,6 +109,7 @@ export default function HARFileViewer() { const [harData, setHarData] = useState(null); const [activeFilter, setActiveFilter] = useState("All"); const [viewMode, setViewMode] = useState<"table" | "waterfall">("table"); + const [statusFilter, setStatusFilter] = useState([]); const handleFileUpload = useCallback((file: File | undefined) => { if (!file) { @@ -47,6 +127,7 @@ export default function HARFileViewer() { const json = JSON.parse(e.target?.result as string); setHarData(json); setStatus("idle"); + setStatusFilter([]); // Reset status filter when new file is loaded } catch (error) { console.error("Error parsing HAR file:", error); setStatus("unsupported"); @@ -179,6 +260,8 @@ export default function HARFileViewer() { ) : ( void; } -const HarTable = ({ entries, activeFilter }: HarTableComponentProps) => { +const HarTable = ({ + entries, + activeFilter, + statusFilter, + onStatusFilterChange, +}: HarTableComponentProps) => { const [expandedRow, setExpandedRow] = useState(null); const [sortField, setSortField] = useState(null); const [sortOrder, setSortOrder] = useState("asc"); @@ -215,17 +305,62 @@ const HarTable = ({ entries, activeFilter }: HarTableComponentProps) => { setExpandedRow(null); }, [activeFilter]); + // Helper function to get status text + const getStatusText = (statusCode: number): string => { + const statusTexts: Record = { + 101: "Switching Protocols", + 200: "OK", + 201: "Created", + 204: "No Content", + 206: "Partial Content", + 301: "Moved Permanently", + 302: "Found", + 304: "Not Modified", + 400: "Bad Request", + 401: "Unauthorized", + 403: "Forbidden", + 404: "Not Found", + 500: "Internal Server Error", + 502: "Bad Gateway", + 503: "Service Unavailable", + }; + return statusTexts[statusCode] || "Unknown"; + }; + + // Extract unique status codes for the filter + const uniqueStatusCodes = useMemo(() => { + const statusCodes = new Set( + entries.map((entry) => entry.response.status.toString()) + ); + return Array.from(statusCodes) + .sort((a, b) => parseInt(a) - parseInt(b)) + .map((code) => ({ + value: code, + label: `${code} - ${getStatusText(parseInt(code))}`, + })); + }, [entries]); + const maxTime = useMemo( () => Math.max(...entries.map((entry) => entry.time)), [entries] ); const filteredAndSortedEntries = useMemo(() => { - const result = - activeFilter === "All" - ? entries - : entries.filter((entry) => getFilterType(entry) === activeFilter); + let result = entries; + + // Apply content type filter + if (activeFilter !== "All") { + result = result.filter((entry) => getFilterType(entry) === activeFilter); + } + // Apply status code filter + if (statusFilter.length > 0) { + result = result.filter((entry) => + statusFilter.includes(entry.response.status.toString()) + ); + } + + // Apply sorting if (sortField) { result.sort((a, b) => { let comparison = 0; @@ -241,7 +376,7 @@ const HarTable = ({ entries, activeFilter }: HarTableComponentProps) => { } return result; - }, [entries, activeFilter, sortField, sortOrder]); + }, [entries, activeFilter, statusFilter, sortField, sortOrder]); const handleSort = (field: SortField) => { if (sortField === field) { @@ -254,28 +389,39 @@ const HarTable = ({ entries, activeFilter }: HarTableComponentProps) => { const tableHeaderStyles = "border p-2 px-3 text-left text-[14px]"; const tableHeaderSortableStyles = `${tableHeaderStyles} cursor-pointer hover:bg-muted-foreground/10`; - const tableCellStyles = "border p-2 px-3 min-w-[80px] max-w-[320px] w-full truncate"; //prettier-ignore + const tableCellStyles = "border p-2 px-3 truncate"; //prettier-ignore const tableRowStyles = "text-[13px] leading-[1] hover:bg-muted-foreground/10"; const tableRowOddStyles = "bg-muted"; const tableRowErrorStyles = "bg-red-500/10"; return (
- +
- - - - + + + +
NameStatusTypeStarted atName +
+ Status +
+ +
+
+
TypeStarted at handleSort("size")} > Size {sortField === "size" && (sortOrder === "asc" ? " ▲" : " ▼")} handleSort("time")} > Time {sortField === "time" && (sortOrder === "asc" ? " ▲" : " ▼")}