diff --git a/src/components/FileTree/types.ts b/src/components/FileTree/types.ts index ffdda0d3..9f0fbf56 100644 --- a/src/components/FileTree/types.ts +++ b/src/components/FileTree/types.ts @@ -1,5 +1,6 @@ import { StudioFile } from '@/types' +import { SearchMatch } from '@/types/fuse' export type FileItem = StudioFile & { - matches?: Array<[number, number]> + matches?: SearchMatch[] } diff --git a/src/components/HighlightedText.tsx b/src/components/HighlightedText.tsx index 5057e2ea..261133f2 100644 --- a/src/components/HighlightedText.tsx +++ b/src/components/HighlightedText.tsx @@ -1,4 +1,6 @@ +import { SearchMatch } from '@/types/fuse' import { css } from '@emotion/react' +import { useMemo } from 'react' interface MatchSegment { match: boolean @@ -40,11 +42,21 @@ function splitByMatches(text: string, matches: Array<[number, number]>) { interface HighlightedTextProps { text: string - matches: Array<[number, number]> | undefined + matches: SearchMatch[] | undefined } export function HighlightedText({ text, matches }: HighlightedTextProps) { - const segments = splitByMatches(text, longestMatchOnly(matches ?? [])) + const segments = useMemo(() => { + // When searching multiple properties we need to filter matches by value we are highlighting + const filteredMatches = (matches || []).filter( + (match) => match.value === text + ) + + return splitByMatches( + text, + longestMatchOnly(filteredMatches.flatMap((match) => match.indices)) + ) + }, [text, matches]) return ( <> @@ -54,8 +66,8 @@ export function HighlightedText({ text, matches }: HighlightedTextProps) { diff --git a/src/components/Layout/Sidebar/Sidebar.hooks.ts b/src/components/Layout/Sidebar/Sidebar.hooks.ts index 7ba9bebf..dc71dbc5 100644 --- a/src/components/Layout/Sidebar/Sidebar.hooks.ts +++ b/src/components/Layout/Sidebar/Sidebar.hooks.ts @@ -1,7 +1,8 @@ import { useStudioUIStore } from '@/store/ui' import { StudioFile } from '@/types' import { fileFromFileName } from '@/utils/file' -import Fuse, { FuseResult, IFuseOptions } from 'fuse.js' +import { withMatches } from '@/utils/fuse' +import Fuse, { IFuseOptions } from 'fuse.js' import { orderBy } from 'lodash-es' import { useEffect, useMemo } from 'react' @@ -55,13 +56,6 @@ function useFolderContent() { } } -function withMatches(result: FuseResult) { - return { - ...result.item, - matches: result.matches?.flatMap((match) => match.indices) ?? [], - } -} - export function useFiles(searchTerm: string) { const files = useFolderContent() diff --git a/src/components/MethodBadge.tsx b/src/components/MethodBadge.tsx index 15d9c9a3..055bceee 100644 --- a/src/components/MethodBadge.tsx +++ b/src/components/MethodBadge.tsx @@ -1,16 +1,17 @@ import { Method } from '@/types' import { Text } from '@radix-ui/themes' -import { ComponentProps } from 'react' +import { ComponentProps, ReactNode } from 'react' interface MethodBadgeProps { method: Method + children: ReactNode } -export function MethodBadge({ method }: MethodBadgeProps) { +export function MethodBadge({ method, children }: MethodBadgeProps) { const color = methodColor(method) return ( - {method} + {children} ) } diff --git a/src/components/ResponseStatusBadge.tsx b/src/components/ResponseStatusBadge.tsx index 164064f7..5e2d833b 100644 --- a/src/components/ResponseStatusBadge.tsx +++ b/src/components/ResponseStatusBadge.tsx @@ -1,15 +1,16 @@ import { Text } from '@radix-ui/themes' -import { ComponentProps } from 'react' +import { ComponentProps, ReactNode } from 'react' type Props = ComponentProps & { status?: number + children: ReactNode } -export function ResponseStatusBadge({ status, ...props }: Props) { +export function ResponseStatusBadge({ status, children, ...props }: Props) { const color = statusColor(status) return ( - {status ?? '-'} + {children} ) } diff --git a/src/components/WebLogView/Filter.hooks.ts b/src/components/WebLogView/Filter.hooks.ts index d9361ddb..36043ef2 100644 --- a/src/components/WebLogView/Filter.hooks.ts +++ b/src/components/WebLogView/Filter.hooks.ts @@ -1,5 +1,7 @@ import { ProxyData } from '@/types' +import { withMatches } from '@/utils/fuse' import { isNonStaticAssetResponse } from '@/utils/staticAssets' +import Fuse from 'fuse.js' import { useState, useMemo } from 'react' import { useDebounce } from 'react-use' @@ -30,21 +32,28 @@ export function useFilterRequests({ [filter] ) - const filteredRequests = useMemo(() => { - const lowerCaseFilter = debouncedFilter.toLowerCase().trim() + const searchIndex = useMemo(() => { + return new Fuse(assetsToFilter, { + includeMatches: true, + shouldSort: false, + threshold: 0.2, + + keys: [ + 'request.path', + 'request.host', + 'request.method', + 'response.statusCode', + ], + }) + }, [assetsToFilter]) - if (lowerCaseFilter === '') { + const filteredRequests = useMemo(() => { + if (debouncedFilter.match(/^\s*$/)) { return assetsToFilter } - return assetsToFilter.filter((data) => { - return ( - data.request.url.toLowerCase().includes(lowerCaseFilter) || - data.request.method.toLowerCase().includes(lowerCaseFilter) || - data.response?.statusCode.toString().includes(lowerCaseFilter) - ) - }) - }, [debouncedFilter, assetsToFilter]) + return searchIndex.search(debouncedFilter).map(withMatches) + }, [searchIndex, assetsToFilter, debouncedFilter]) const staticAssetCount = proxyData.length - requestWithoutStaticAssets.length diff --git a/src/components/WebLogView/Row.tsx b/src/components/WebLogView/Row.tsx index 72108fd1..eba46f48 100644 --- a/src/components/WebLogView/Row.tsx +++ b/src/components/WebLogView/Row.tsx @@ -1,13 +1,14 @@ import { Box, Table } from '@radix-ui/themes' -import { ProxyData } from '@/types' +import { ProxyData, ProxyDataWithMatches } from '@/types' import { MethodBadge } from '../MethodBadge' import { ResponseStatusBadge } from '../ResponseStatusBadge' import { TableCellWithTooltip } from '../TableCellWithTooltip' +import { HighlightedText } from '../HighlightedText' interface RowProps { - data: ProxyData + data: ProxyDataWithMatches isSelected?: boolean onSelectRequest: (data: ProxyData) => void } @@ -39,14 +40,25 @@ export function Row({ data, isSelected, onSelectRequest }: RowProps) { marginRight: 'var(--space-2)', }} /> - + + + - + + + - {data.request.host} - {data.request.path} + + + + + + ) } diff --git a/src/components/WebLogView/WebLogView.tsx b/src/components/WebLogView/WebLogView.tsx index cbf804f6..7c66e475 100644 --- a/src/components/WebLogView/WebLogView.tsx +++ b/src/components/WebLogView/WebLogView.tsx @@ -1,22 +1,23 @@ import { Box } from '@radix-ui/themes' -import { Group as GroupType, ProxyData } from '@/types' +import { Group as GroupType, ProxyDataWithMatches } from '@/types' import { Row } from './Row' import { Group } from './Group' import { Table } from '@/components/Table' -import { useMemo } from 'react' +import { memo, useMemo } from 'react' import { useDeepCompareEffect } from 'react-use' interface WebLogViewProps { - requests: ProxyData[] + requests: ProxyDataWithMatches[] groups?: GroupType[] activeGroup?: string selectedRequestId?: string - onSelectRequest: (data: ProxyData | null) => void + onSelectRequest: (data: ProxyDataWithMatches | null) => void onUpdateGroup?: (group: GroupType) => void } -export function WebLogView({ +// Memo improves performance when filtering +export const WebLogView = memo(function WebLogView({ requests, groups, selectedRequestId, @@ -73,12 +74,12 @@ export function WebLogView({ onSelectRequest={onSelectRequest} /> ) -} +}) interface RequestListProps { - requests: ProxyData[] + requests: ProxyDataWithMatches[] selectedRequestId?: string - onSelectRequest: (data: ProxyData) => void + onSelectRequest: (data: ProxyDataWithMatches) => void } function RequestList({ diff --git a/src/types/fuse.ts b/src/types/fuse.ts new file mode 100644 index 00000000..4d05c07a --- /dev/null +++ b/src/types/fuse.ts @@ -0,0 +1,4 @@ +export interface SearchMatch { + indices: Array<[number, number]> + value: string +} diff --git a/src/types/index.ts b/src/types/index.ts index 571fe9e4..48134cf0 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,3 +1,5 @@ +import { SearchMatch } from './fuse' + // TODO: modify json_output.py to use CamelCase instead of snake_case export type Method = | 'GET' @@ -102,3 +104,7 @@ export interface FolderContent { } export type ProxyStatus = 'online' | 'offline' | 'restarting' + +export type ProxyDataWithMatches = ProxyData & { + matches?: SearchMatch[] +} diff --git a/src/utils/fuse.ts b/src/utils/fuse.ts new file mode 100644 index 00000000..7fdd64bd --- /dev/null +++ b/src/utils/fuse.ts @@ -0,0 +1,12 @@ +import { FuseResult } from 'fuse.js' + +export function withMatches(result: FuseResult) { + return { + ...result.item, + matches: + result.matches?.flatMap((match) => ({ + indices: match.indices, + value: match.value, + })) ?? [], + } +}