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 && (
)}
);
};