import { useState } from 'react'; import { autoUpdate, flip, FloatingPortal, offset, shift, size, useClick, useDismiss, useFloating, useInteractions, useRole, } from '@floating-ui/react'; import { Input } from './Input'; import { Button } from './Button'; import { X, Filter, ChevronDown, Check } from 'lucide-react'; import type { HttpMethod, NetworkEventSource } from '../../shared/client'; import { createDefaultFilter, DEFAULT_REQUEST_TYPES, } from '../state/filter'; import type { AdvancedFilterState, FilterState, RequestTypeFilter, } from '../state/filter'; type FilterBarProps = { filter: FilterState; onFilterChange: (filter: FilterState) => void; }; const HTTP_METHODS: HttpMethod[] = [ 'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', ]; const SOURCES: NetworkEventSource[] = ['builtin', 'nitro']; const getTypeLabel = (type: RequestTypeFilter) => { switch (type) { case 'http': return 'XHR'; case 'websocket': return 'WS'; case 'sse': return 'SSE'; } }; const getSourceLabel = (source: NetworkEventSource) => { switch (source) { case 'builtin': return 'Built-in'; case 'nitro': return 'Nitro'; } }; const getAdvancedFilterCount = (advanced: AdvancedFilterState) => { return [ advanced.methods.size > 0, advanced.sources.size > 0, advanced.status.trim() !== '', advanced.domain.trim() !== '', advanced.contentType.trim() !== '', advanced.failedOnly, advanced.inFlightOnly, advanced.overriddenOnly, advanced.minSize.trim() !== '', advanced.maxSize.trim() !== '', advanced.minDuration.trim() !== '', advanced.maxDuration.trim() !== '', ].filter(Boolean).length; }; const getActiveFilterCount = (filter: FilterState) => { const typeFilterCount = filter.types.size < DEFAULT_REQUEST_TYPES.length ? 1 : 0; return typeFilterCount + getAdvancedFilterCount(filter.advanced); }; const FilterField = ({ label, value, placeholder, onChange, }: { label: string; value: string; placeholder: string; onChange: (value: string) => void; }) => ( ); const FilterPanelLabel = ({ children }: { children: string }) => (
{children}
); const FilterPanelSeparator = () => (
); const FilterCheckbox = ({ checked, onCheckedChange, children, }: { checked: boolean; onCheckedChange: (checked: boolean) => void; children: string; }) => ( ); export const FilterBar = ({ filter, onFilterChange }: FilterBarProps) => { const [isFilterPanelOpen, setIsFilterPanelOpen] = useState(false); const { refs, floatingStyles, context } = useFloating({ open: isFilterPanelOpen, onOpenChange: setIsFilterPanelOpen, whileElementsMounted: autoUpdate, middleware: [ offset(5), flip({ padding: 8 }), shift({ padding: 8 }), size({ padding: 8, apply({ availableHeight, elements }) { elements.floating.style.maxHeight = `${availableHeight}px`; }, }), ], }); const click = useClick(context); const dismiss = useDismiss(context); const role = useRole(context); const { getReferenceProps, getFloatingProps } = useInteractions([ click, dismiss, role, ]); const handleTextChange = (text: string) => { onFilterChange({ ...filter, text }); }; const updateAdvancedFilter = (patch: Partial) => { onFilterChange({ ...filter, advanced: { ...filter.advanced, ...patch, }, }); }; const toggleType = (type: RequestTypeFilter) => { const newTypes = new Set(filter.types); if (newTypes.has(type)) { newTypes.delete(type); } else { newTypes.add(type); } onFilterChange({ ...filter, types: newTypes }); }; const toggleMethod = (method: HttpMethod) => { const methods = new Set(filter.advanced.methods); if (methods.has(method)) { methods.delete(method); } else { methods.add(method); } updateAdvancedFilter({ methods }); }; const toggleSource = (source: NetworkEventSource) => { const sources = new Set(filter.advanced.sources); if (sources.has(source)) { sources.delete(source); } else { sources.add(source); } updateAdvancedFilter({ sources }); }; const clearFilters = () => { onFilterChange(createDefaultFilter()); }; const activeFilterCount = getActiveFilterCount(filter); const hasActiveFilters = filter.text.trim() !== '' || activeFilterCount > 0; return (
handleTextChange(e.target.value)} className="h-8 text-sm bg-gray-700 border-gray-600 text-gray-100 placeholder:text-gray-400" />
{isFilterPanelOpen && (
Request Type {DEFAULT_REQUEST_TYPES.map((type) => ( toggleType(type)} > {getTypeLabel(type)} ))} Method
{HTTP_METHODS.map((method) => ( toggleMethod(method)} > {method} ))}
Request Source {SOURCES.map((source) => ( toggleSource(source)} > {getSourceLabel(source)} ))} updateAdvancedFilter({ failedOnly: checked }) } > Failed only updateAdvancedFilter({ inFlightOnly: checked }) } > In-flight only updateAdvancedFilter({ overriddenOnly: checked }) } > Overridden only
updateAdvancedFilter({ status })} /> updateAdvancedFilter({ domain })} /> updateAdvancedFilter({ contentType }) } /> updateAdvancedFilter({ minSize })} /> updateAdvancedFilter({ maxSize })} /> updateAdvancedFilter({ minDuration }) } /> updateAdvancedFilter({ maxDuration }) } />
)} {hasActiveFilters && ( )}
); };