From da30adf4a03a620fdbe8843fb0bb04cfe16ecc98 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Oct 2025 10:54:43 +0000 Subject: [PATCH 1/5] Initial plan From 989c8ebd26b3e9282ffcf0320c56f65fff382df4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Oct 2025 11:03:08 +0000 Subject: [PATCH 2/5] Add search highlighting with colored indicators and match summary Co-authored-by: peckz <18050177+peckz@users.noreply.github.com> --- .../pages/utilities/har-file-viewer.test.tsx | 6 +- components/MatchIndicators.tsx | 67 ++++++ components/MatchSummaryPills.tsx | 95 +++++++++ components/SearchHighlightText.tsx | 58 ++++++ components/utils/har-utils.ts | 73 +++++++ pages/utilities/har-file-viewer.tsx | 190 ++++++++++++------ 6 files changed, 422 insertions(+), 67 deletions(-) create mode 100644 components/MatchIndicators.tsx create mode 100644 components/MatchSummaryPills.tsx create mode 100644 components/SearchHighlightText.tsx diff --git a/__tests__/pages/utilities/har-file-viewer.test.tsx b/__tests__/pages/utilities/har-file-viewer.test.tsx index 0924875..60e6c6d 100644 --- a/__tests__/pages/utilities/har-file-viewer.test.tsx +++ b/__tests__/pages/utilities/har-file-viewer.test.tsx @@ -151,13 +151,9 @@ describe("HARFileViewer", () => { await new Promise((resolve) => setTimeout(resolve, 500)); // Should still see the api request - expect( - screen.getByText("https://example.com/api/test") - ).toBeInTheDocument(); - - // Should not see the css request (it should be filtered out) const rows = screen.queryAllByTestId("table-row"); expect(rows.length).toBe(1); + expect(rows[0]).toHaveTextContent("https://example.com/api/test"); }); test("should clear search query when clear button is clicked", async () => { diff --git a/components/MatchIndicators.tsx b/components/MatchIndicators.tsx new file mode 100644 index 0000000..cfab50a --- /dev/null +++ b/components/MatchIndicators.tsx @@ -0,0 +1,67 @@ +import { MatchCategory } from "@/components/utils/har-utils"; + +interface MatchIndicatorsProps { + categories: MatchCategory[]; + className?: string; +} + +/** + * Displays colored dots to indicate which parts of an entry matched the search. + * Similar to iOS notification indicators. + * + * Color scheme: + * - Blue: URL match + * - Purple: Headers match + * - Orange: Request payload match + * - Green: Response content match + */ +export default function MatchIndicators({ + categories, + className = "", +}: MatchIndicatorsProps) { + if (categories.length === 0) { + return null; + } + + const getCategoryColor = (category: MatchCategory): string => { + switch (category) { + case "url": + return "bg-blue-500"; + case "headers": + return "bg-purple-500"; + case "request": + return "bg-orange-500"; + case "response": + return "bg-green-500"; + default: + return "bg-gray-500"; + } + }; + + const getCategoryTitle = (category: MatchCategory): string => { + switch (category) { + case "url": + return "Match in URL"; + case "headers": + return "Match in headers"; + case "request": + return "Match in request payload"; + case "response": + return "Match in response content"; + default: + return "Match found"; + } + }; + + return ( +
+ {categories.map((category) => ( +
+ ))} +
+ ); +} diff --git a/components/MatchSummaryPills.tsx b/components/MatchSummaryPills.tsx new file mode 100644 index 0000000..de335e5 --- /dev/null +++ b/components/MatchSummaryPills.tsx @@ -0,0 +1,95 @@ +import { + HarEntry, + getMatchCategories, + MatchCategory, +} from "@/components/utils/har-utils"; + +interface MatchSummaryPillsProps { + entries: HarEntry[]; + searchQuery: string; + className?: string; +} + +interface CategoryCount { + category: MatchCategory; + count: number; + label: string; + color: string; +} + +/** + * Displays pill-shaped badges showing the count of matches by category. + * Helps users understand where their search matches are located. + */ +export default function MatchSummaryPills({ + entries, + searchQuery, + className = "", +}: MatchSummaryPillsProps) { + if (!searchQuery) { + return null; + } + + // Count matches by category + const categoryCounts: Record = { + url: 0, + headers: 0, + request: 0, + response: 0, + }; + + entries.forEach((entry) => { + const matchInfo = getMatchCategories(entry, searchQuery); + matchInfo.categories.forEach((category) => { + categoryCounts[category]++; + }); + }); + + // Build display data + const categoryData: CategoryCount[] = [ + { + category: "url", + count: categoryCounts.url, + label: "URLs", + color: "bg-blue-500/10 text-blue-700 dark:text-blue-400 border-blue-200 dark:border-blue-800", + }, + { + category: "headers", + count: categoryCounts.headers, + label: "Headers", + color: "bg-purple-500/10 text-purple-700 dark:text-purple-400 border-purple-200 dark:border-purple-800", + }, + { + category: "request", + count: categoryCounts.request, + label: "Requests", + color: "bg-orange-500/10 text-orange-700 dark:text-orange-400 border-orange-200 dark:border-orange-800", + }, + { + category: "response", + count: categoryCounts.response, + label: "Responses", + color: "bg-green-500/10 text-green-700 dark:text-green-400 border-green-200 dark:border-green-800", + }, + ]; + + // Filter out categories with no matches + const activeCategories = categoryData.filter((cat) => cat.count > 0); + + if (activeCategories.length === 0) { + return null; + } + + return ( +
+ {activeCategories.map((cat) => ( +
+ {cat.label}: {cat.count} +
+ ))} +
+ ); +} diff --git a/components/SearchHighlightText.tsx b/components/SearchHighlightText.tsx new file mode 100644 index 0000000..0f65837 --- /dev/null +++ b/components/SearchHighlightText.tsx @@ -0,0 +1,58 @@ +interface SearchHighlightTextProps { + text: string; + searchQuery: string; + className?: string; +} + +/** + * Component to highlight search matches in text, similar to Chrome's Cmd+F functionality. + * Matches are highlighted with a yellow background. + */ +export default function SearchHighlightText({ + text, + searchQuery, + className = "", +}: SearchHighlightTextProps) { + if (!searchQuery || !text) { + return {text}; + } + + const parts: JSX.Element[] = []; + const lowerText = text.toLowerCase(); + const lowerQuery = searchQuery.toLowerCase(); + + let lastIndex = 0; + let currentIndex = 0; + + while ((currentIndex = lowerText.indexOf(lowerQuery, lastIndex)) !== -1) { + // Add text before match + if (currentIndex > lastIndex) { + parts.push( + + {text.slice(lastIndex, currentIndex)} + + ); + } + + // Add highlighted match + parts.push( + + {text.slice(currentIndex, currentIndex + searchQuery.length)} + + ); + + lastIndex = currentIndex + searchQuery.length; + } + + // Add remaining text + if (lastIndex < text.length) { + parts.push( + {text.slice(lastIndex)} + ); + } + + return {parts}; +} diff --git a/components/utils/har-utils.ts b/components/utils/har-utils.ts index 48c2336..8763327 100644 --- a/components/utils/har-utils.ts +++ b/components/utils/har-utils.ts @@ -112,3 +112,76 @@ export function tryParseJSON(str: string) { return str; } } + +// Search match categories for visual indicators +export type MatchCategory = "url" | "headers" | "request" | "response"; + +export interface MatchInfo { + categories: MatchCategory[]; + hasMatch: boolean; +} + +/** + * Determines which categories of content match the search query for a given entry. + * Used to display colored indicators showing where matches were found. + */ +export function getMatchCategories( + entry: HarEntry, + searchQuery: string +): MatchInfo { + if (!searchQuery) { + return { categories: [], hasMatch: false }; + } + + const query = searchQuery.toLowerCase(); + const categories: MatchCategory[] = []; + + // Check URL + if (entry.request.url.toLowerCase().includes(query)) { + categories.push("url"); + } + + // Check request and response headers + const hasHeaderMatch = + entry.request.headers.some( + (header) => + header.name.toLowerCase().includes(query) || + header.value.toLowerCase().includes(query) + ) || + entry.response.headers.some( + (header) => + header.name.toLowerCase().includes(query) || + header.value.toLowerCase().includes(query) + ); + + if (hasHeaderMatch) { + categories.push("headers"); + } + + // Check request payload + if (entry.request.postData?.text) { + if (entry.request.postData.text.toLowerCase().includes(query)) { + categories.push("request"); + } + } + + // Check response content + if (entry.response.content.text) { + let contentToSearch = entry.response.content.text; + if (isBase64(contentToSearch)) { + try { + contentToSearch = atob(contentToSearch); + } catch (e) { + // If decode fails, search in original + } + } + if (contentToSearch.toLowerCase().includes(query)) { + categories.push("response"); + } + } + + return { + categories, + hasMatch: categories.length > 0, + }; +} diff --git a/pages/utilities/har-file-viewer.tsx b/pages/utilities/har-file-viewer.tsx index 93f01fb..91edb0a 100644 --- a/pages/utilities/har-file-viewer.tsx +++ b/pages/utilities/har-file-viewer.tsx @@ -9,6 +9,7 @@ import { HarTableProps, isBase64, tryParseJSON, + getMatchCategories, } from "@/components/utils/har-utils"; import { cn } from "@/lib/utils"; import { Button } from "@/components/ds/ButtonComponent"; @@ -37,6 +38,9 @@ import { CommandList, } from "@/components/ds/CommandMenu"; import { Checkbox } from "@/components/ds/CheckboxComponent"; +import SearchHighlightText from "@/components/SearchHighlightText"; +import MatchIndicators from "@/components/MatchIndicators"; +import MatchSummaryPills from "@/components/MatchSummaryPills"; interface MultiSelectComboboxProps { data: { value: string; label: string }[]; @@ -335,6 +339,15 @@ export default function HARFileViewer() { )}
+ + {/* Match Summary Pills */} + {debouncedSearchQuery && ( + + )} + {/* Results count */} {(debouncedSearchQuery || activeFilter !== "All" || @@ -595,7 +608,8 @@ const HarTable = ({ - + {searchQuery && } + - {filteredAndSortedEntries.map((entry, index) => ( - - = 400 && tableRowErrorStyles - )} - onClick={() => { - setExpandedRow(expandedRow === index ? null : index); - }} - > - = 400 && tableRowErrorStyles )} + onClick={() => { + setExpandedRow(expandedRow === index ? null : index); + }} > - {entry.request.url} - - - - - - - - - {expandedRow === index && ( - - + )} + + + + + + - )} - - ))} + + {expandedRow === index && ( + + + + )} + + ); + })}
NameName
Status @@ -626,74 +640,92 @@ const HarTable = ({
{ + const matchInfo = searchQuery + ? getMatchCategories(entry, searchQuery) + : { categories: [], hasMatch: false }; + + return ( + +
- {entry.response.status} - - {entry.response.content.mimeType} - - {new Date(entry.startedDateTime).toLocaleTimeString()} - - {(entry.response.content.size / 1024).toFixed(1) + "kB"} - -
-
-
- {entry.time.toFixed(0) + "ms"} -
- + {searchQuery && ( + + + + {searchQuery ? ( + + ) : ( + entry.request.url + )} + + {entry.response.status} + + {entry.response.content.mimeType} + + {new Date(entry.startedDateTime).toLocaleTimeString()} + + {(entry.response.content.size / 1024).toFixed(1) + "kB"} + +
+
+
+ {entry.time.toFixed(0) + "ms"}
+ +
); }; -const ExpandedDetails = ({ entry }: { entry: HarEntry }) => { +const ExpandedDetails = ({ entry, searchQuery }: { entry: HarEntry; searchQuery: string }) => { const [activeTab, setActiveTab] = useState("headers"); const decodeContent = useCallback((content: string) => { @@ -788,9 +820,26 @@ const ExpandedDetails = ({ entry }: { entry: HarEntry }) => { {entry.response.headers.map((header, index) => (
- {header.name}:{" "} + {searchQuery ? ( + + ) : ( + header.name + )} + :{" "} + + + {searchQuery ? ( + + ) : ( + header.value + )} - {header.value}
))} @@ -798,9 +847,26 @@ const ExpandedDetails = ({ entry }: { entry: HarEntry }) => { {entry.request.headers.map((header, index) => (
- {header.name}:{" "} + {searchQuery ? ( + + ) : ( + header.name + )} + :{" "} + + + {searchQuery ? ( + + ) : ( + header.value + )} - {header.value}
))} From e9d0ddf6e270146ad41e35ae06e880892b020a2a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Oct 2025 11:08:26 +0000 Subject: [PATCH 3/5] Fix code review issues: highlight length and table width percentages Co-authored-by: peckz <18050177+peckz@users.noreply.github.com> --- components/SearchHighlightText.tsx | 4 ++-- pages/utilities/har-file-viewer.tsx | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/components/SearchHighlightText.tsx b/components/SearchHighlightText.tsx index 0f65837..8bdfce3 100644 --- a/components/SearchHighlightText.tsx +++ b/components/SearchHighlightText.tsx @@ -40,11 +40,11 @@ export default function SearchHighlightText({ key={`match-${currentIndex}`} className="bg-yellow-300 dark:bg-yellow-600 dark:text-black rounded px-0.5" > - {text.slice(currentIndex, currentIndex + searchQuery.length)} + {text.slice(currentIndex, currentIndex + lowerQuery.length)} ); - lastIndex = currentIndex + searchQuery.length; + lastIndex = currentIndex + lowerQuery.length; } // Add remaining text diff --git a/pages/utilities/har-file-viewer.tsx b/pages/utilities/har-file-viewer.tsx index 91edb0a..f84a027 100644 --- a/pages/utilities/har-file-viewer.tsx +++ b/pages/utilities/har-file-viewer.tsx @@ -608,8 +608,8 @@ const HarTable = ({ - {searchQuery && } - + {searchQuery && } + - - + +
NameName
Status @@ -622,8 +622,8 @@ const HarTable = ({
TypeStarted atTypeStarted at handleSort("size")} @@ -631,7 +631,7 @@ const HarTable = ({ Size {sortField === "size" && (sortOrder === "asc" ? " ▲" : " ▼")} handleSort("time")} > Time {sortField === "time" && (sortOrder === "asc" ? " ▲" : " ▼")} From 2ec1812cfb74d1d81bcd314a674dcf94e8a30330 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Oct 2025 16:34:22 +0000 Subject: [PATCH 4/5] Add 'Search' column header and notification dots on tabs Co-authored-by: peckz <18050177+peckz@users.noreply.github.com> --- pages/utilities/har-file-viewer.tsx | 56 +++++++++++++++++++++++++---- 1 file changed, 49 insertions(+), 7 deletions(-) diff --git a/pages/utilities/har-file-viewer.tsx b/pages/utilities/har-file-viewer.tsx index f84a027..ff3da54 100644 --- a/pages/utilities/har-file-viewer.tsx +++ b/pages/utilities/har-file-viewer.tsx @@ -10,6 +10,7 @@ import { isBase64, tryParseJSON, getMatchCategories, + MatchCategory, } from "@/components/utils/har-utils"; import { cn } from "@/lib/utils"; import { Button } from "@/components/ds/ButtonComponent"; @@ -608,7 +609,7 @@ const HarTable = ({ - {searchQuery && } + {searchQuery && } )} @@ -725,7 +730,15 @@ const HarTable = ({ ); }; -const ExpandedDetails = ({ entry, searchQuery }: { entry: HarEntry; searchQuery: string }) => { +const ExpandedDetails = ({ + entry, + searchQuery, + matchInfo +}: { + entry: HarEntry; + searchQuery: string; + matchInfo: { categories: MatchCategory[]; hasMatch: boolean }; +}) => { const [activeTab, setActiveTab] = useState("headers"); const decodeContent = useCallback((content: string) => { @@ -788,17 +801,32 @@ const ExpandedDetails = ({ entry, searchQuery }: { entry: HarEntry; searchQuery: ); }; - const TabHeader = ({ id, label }: { id: string; label: string }) => { + const TabHeader = ({ + id, + label, + hasMatch + }: { + id: string; + label: string; + hasMatch?: boolean; + }) => { return ( ); }; @@ -806,10 +834,24 @@ const ExpandedDetails = ({ entry, searchQuery }: { entry: HarEntry; searchQuery: return (
- - {entry.request.postData && } + + {entry.request.postData && ( + + )} {entry.response.content.text && ( - + )}
From 22120afd021c5fdd9106cbf989ad975c62148b3c Mon Sep 17 00:00:00 2001 From: Petar Cirkovic Date: Mon, 20 Oct 2025 12:05:53 +0200 Subject: [PATCH 5/5] fix: align pills and search info --- components/MatchIndicators.tsx | 2 +- components/MatchSummaryPills.tsx | 12 ++- components/SearchHighlightText.tsx | 6 +- pages/utilities/har-file-viewer.tsx | 111 +++++++++++++++++----------- 4 files changed, 79 insertions(+), 52 deletions(-) diff --git a/components/MatchIndicators.tsx b/components/MatchIndicators.tsx index cfab50a..90682e7 100644 --- a/components/MatchIndicators.tsx +++ b/components/MatchIndicators.tsx @@ -8,7 +8,7 @@ interface MatchIndicatorsProps { /** * Displays colored dots to indicate which parts of an entry matched the search. * Similar to iOS notification indicators. - * + * * Color scheme: * - Blue: URL match * - Purple: Headers match diff --git a/components/MatchSummaryPills.tsx b/components/MatchSummaryPills.tsx index de335e5..39d6e0e 100644 --- a/components/MatchSummaryPills.tsx +++ b/components/MatchSummaryPills.tsx @@ -51,25 +51,29 @@ export default function MatchSummaryPills({ category: "url", count: categoryCounts.url, label: "URLs", - color: "bg-blue-500/10 text-blue-700 dark:text-blue-400 border-blue-200 dark:border-blue-800", + color: + "bg-blue-500/10 text-blue-700 dark:text-blue-400 border-blue-200 dark:border-blue-800", }, { category: "headers", count: categoryCounts.headers, label: "Headers", - color: "bg-purple-500/10 text-purple-700 dark:text-purple-400 border-purple-200 dark:border-purple-800", + color: + "bg-purple-500/10 text-purple-700 dark:text-purple-400 border-purple-200 dark:border-purple-800", }, { category: "request", count: categoryCounts.request, label: "Requests", - color: "bg-orange-500/10 text-orange-700 dark:text-orange-400 border-orange-200 dark:border-orange-800", + color: + "bg-orange-500/10 text-orange-700 dark:text-orange-400 border-orange-200 dark:border-orange-800", }, { category: "response", count: categoryCounts.response, label: "Responses", - color: "bg-green-500/10 text-green-700 dark:text-green-400 border-green-200 dark:border-green-800", + color: + "bg-green-500/10 text-green-700 dark:text-green-400 border-green-200 dark:border-green-800", }, ]; diff --git a/components/SearchHighlightText.tsx b/components/SearchHighlightText.tsx index 8bdfce3..16e0ed1 100644 --- a/components/SearchHighlightText.tsx +++ b/components/SearchHighlightText.tsx @@ -20,7 +20,7 @@ export default function SearchHighlightText({ const parts: JSX.Element[] = []; const lowerText = text.toLowerCase(); const lowerQuery = searchQuery.toLowerCase(); - + let lastIndex = 0; let currentIndex = 0; @@ -49,9 +49,7 @@ export default function SearchHighlightText({ // Add remaining text if (lastIndex < text.length) { - parts.push( - {text.slice(lastIndex)} - ); + parts.push({text.slice(lastIndex)}); } return {parts}; diff --git a/pages/utilities/har-file-viewer.tsx b/pages/utilities/har-file-viewer.tsx index ff3da54..7586dfa 100644 --- a/pages/utilities/har-file-viewer.tsx +++ b/pages/utilities/har-file-viewer.tsx @@ -340,24 +340,22 @@ export default function HARFileViewer() { )}
- - {/* Match Summary Pills */} - {debouncedSearchQuery && ( - - )} - - {/* Results count */} - {(debouncedSearchQuery || - activeFilter !== "All" || - statusFilter.length > 0) && ( + +
+ {/* Match Summary Pills */} + {debouncedSearchQuery && ( + + )} + + {/* Results count */}

Showing {getFilteredCount()} of {harData.log.entries.length}{" "} requests

- )} +
{/* Filters and View Mode */} @@ -609,8 +607,19 @@ const HarTable = ({
SearchName
@@ -712,7 +713,11 @@ const HarTable = ({ {expandedRow === index && (
- +
- {searchQuery && } - + {searchQuery && ( + + )} + - - + + {searchQuery && ( - )} @@ -712,9 +734,12 @@ const HarTable = ({ {expandedRow === index && ( -
SearchName + Search + + Name +
Status @@ -623,8 +632,16 @@ const HarTable = ({
TypeStarted at + Type + + Started at + handleSort("size")} @@ -632,7 +649,7 @@ const HarTable = ({ Size {sortField === "size" && (sortOrder === "asc" ? " ▲" : " ▼")} handleSort("time")} > Time {sortField === "time" && (sortOrder === "asc" ? " ▲" : " ▼")} @@ -645,7 +662,7 @@ const HarTable = ({ const matchInfo = searchQuery ? getMatchCategories(entry, searchQuery) : { categories: [], hasMatch: false }; - + return (
+
- + @@ -730,12 +755,12 @@ const HarTable = ({ ); }; -const ExpandedDetails = ({ - entry, +const ExpandedDetails = ({ + entry, searchQuery, - matchInfo -}: { - entry: HarEntry; + matchInfo, +}: { + entry: HarEntry; searchQuery: string; matchInfo: { categories: MatchCategory[]; hasMatch: boolean }; }) => { @@ -801,12 +826,12 @@ const ExpandedDetails = ({ ); }; - const TabHeader = ({ - id, - label, - hasMatch - }: { - id: string; + const TabHeader = ({ + id, + label, + hasMatch, + }: { + id: string; label: string; hasMatch?: boolean; }) => { @@ -822,8 +847,8 @@ const ExpandedDetails = ({ > {label} {hasMatch && searchQuery && ( - )} @@ -834,21 +859,21 @@ const ExpandedDetails = ({ return (
- {entry.request.postData && ( - )} {entry.response.content.text && ( -