import { IconChevronDown } from '@tabler/icons'; import Dropdown from 'components/Dropdown/index'; import { IconGrpcBidiStreaming, IconGrpcClientStreaming, IconGrpcServerStreaming, IconGrpcUnary } from 'components/Icons/Grpc'; import SearchInput from 'components/SearchInput/index'; import { search } from 'fast-fuzzy'; import React, { forwardRef, useEffect, useRef, useState } from 'react'; import { useTheme } from 'providers/Theme'; import StyledWrapper from './StyledWrapper'; const MethodDropdown = ({ grpcMethods, selectedGrpcMethod, onMethodSelect, onMethodDropdownCreate }) => { const { theme } = useTheme(); const [searchText, setSearchText] = useState(''); const [focusedIndex, setFocusedIndex] = useState(-1); const searchInputRef = useRef(); const listRef = useRef(); useEffect(() => { const activeItem = listRef.current?.querySelector(`[data-index="${focusedIndex}"]`); if (activeItem) { activeItem.scrollIntoView({ block: 'nearest' }); } }, [focusedIndex]); const groupMethodsByService = (methods) => { if (!methods || !methods.length) return {}; const groupedMethods = {}; methods.forEach((method) => { const pathWithoutLeadingSlash = method.path.startsWith('/') ? method.path.slice(1) : method.path; const parts = pathWithoutLeadingSlash.split('/'); const serviceName = parts[0] || 'Default'; const methodName = parts[1] || method.path; const enhancedMethod = { ...method, serviceName, methodName }; if (!groupedMethods[serviceName]) { groupedMethods[serviceName] = []; } groupedMethods[serviceName].push(enhancedMethod); }); return groupedMethods; }; const getIconForMethodType = (type) => { switch (type) { case 'unary': return ; case 'client-streaming': return ; case 'server-streaming': return ; case 'bidi-streaming': return ; default: return ; } }; const MethodsDropdownIcon = forwardRef((props, ref) => { return (
{selectedGrpcMethod &&
{getIconForMethodType(selectedGrpcMethod.type)}
} {selectedGrpcMethod ? (selectedGrpcMethod.path.split('.').at(-1) || selectedGrpcMethod.path) : 'Select Method'}
); }); const handleGrpcMethodSelect = (method) => { const methodType = method.type; onMethodSelect({ path: method.path, type: methodType }); }; const filteredMethods = searchText ? search(String(searchText), grpcMethods, { keySelector: (obj) => obj.path }) : grpcMethods; const groupedMethods = groupMethodsByService(filteredMethods); // Flatten grouped methods for keyboard navigation const flatMethodList = Object.values(groupedMethods).flat(); const handleKeyDown = (e) => { if (!flatMethodList.length) return; if (e.key === 'ArrowDown') { e.preventDefault(); setFocusedIndex((prev) => prev < flatMethodList.length - 1 ? prev + 1 : flatMethodList.length - 1); } else if (e.key === 'ArrowUp') { e.preventDefault(); setFocusedIndex((prev) => prev >= 0 ? prev - 1 : -1); } else if (e.key === 'Enter' && focusedIndex >= 0) { e.preventDefault(); handleGrpcMethodSelect(flatMethodList[focusedIndex]); } }; const focusSearchInput = () => { setTimeout(() => { if (searchInputRef.current) { searchInputRef.current.focus(); } }, 0); // 0ms to ensure the dropdown is fully rendered and focused }; const handleDropdownShow = () => { focusSearchInput(); setSearchText(''); setFocusedIndex(-1); }; const handleSearchChange = (e) => { // auto focus the first method when the search input is not empty if (e.target.value.trim().length > 0) { setFocusedIndex(0); } else { setFocusedIndex(-1); } }; if (!grpcMethods || grpcMethods.length === 0) { return null; } return (
} placement="bottom-end" style={{ maxWidth: 'unset' }} onShow={handleDropdownShow}>
{Object.entries(groupedMethods).map(([serviceName, methods], serviceIndex) => (
{serviceName || 'Default Service'}
{methods.map((method, methodIndex) => { const globalMethodIndex = Object.values(groupedMethods) .slice(0, serviceIndex) .reduce((acc, group) => acc + group.length, 0) + methodIndex; const isSelected = selectedGrpcMethod && selectedGrpcMethod.path === method.path; const isFocused = focusedIndex === globalMethodIndex; return (
handleGrpcMethodSelect(method)} data-index={globalMethodIndex} data-testid="grpc-method-item" >
{getIconForMethodType(method.type)}
{method.methodName}
{method.type}
); })}
))} {filteredMethods.length === 0 && (
No methods found for the search term
)}
); }; export default MethodDropdown;