123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179 |
- import type {ReactElement} from 'react';
- import {useCallback, useEffect, useRef, useState} from 'react';
- import styled from '@emotion/styled';
- import debounce from 'lodash/debounce';
- import LoadingIndicator from 'sentry/components/loadingIndicator';
- import SearchBar from 'sentry/components/searchBar';
- import {space} from 'sentry/styles/space';
- import useApi from 'sentry/utils/useApi';
- import useKeyPress from 'sentry/utils/useKeyPress';
- type Props = {
- onSelectResult: (value: string) => void;
- path: string;
- placeholder: string;
- suggestionContent: (suggestion: any) => ReactElement;
- createSuggestionPath?: (suggestion: any) => string;
- host?: string;
- onSearch?: (value: string) => void;
- queryParam?: string;
- };
- function DebounceSearch({
- createSuggestionPath,
- onSearch,
- onSelectResult,
- host,
- path,
- placeholder,
- queryParam = '',
- suggestionContent,
- }: Props) {
- const [error, setError] = useState('');
- const [loading, setLoading] = useState(false);
- const [query, setQuery] = useState('');
- const [queryResults, setQueryResults] = useState([]);
- const [showResults, setShowResults] = useState(false);
- const [node, setNode] = useState<HTMLDivElement | null>();
- const setKeyHandlers = useCallback((nodeRef: HTMLDivElement | null) => {
- setNode(nodeRef);
- }, []);
- const downPress = useKeyPress('ArrowDown', node);
- const upPress = useKeyPress('ArrowUp', node);
- const enterPress = useKeyPress('Enter', node);
- const escapePress = useKeyPress('Escape', node);
- const [cursor, setCursor] = useState<number>(0);
- const api = useApi();
- const debouncedSearch = useRef(
- debounce(async (searchHost, value) => {
- // Avoid slow-fetch race conditions
- api.clear();
- setError('');
- setQueryResults([]);
- if (value) {
- try {
- const queryParams = {
- query: [queryParam, value].filter(v => v).join(':'),
- per_page: 10,
- };
- const results = await api.requestPromise(path, {
- method: 'GET',
- host: searchHost,
- data: queryParams,
- });
- setQueryResults(results);
- } catch (err) {
- setError(err.message);
- }
- }
- setLoading(false);
- value ? setShowResults(true) : setShowResults(false);
- }, 300)
- ).current;
- const onChange = useCallback(
- (value: string) => {
- value ? setLoading(true) : setLoading(false);
- value ? setShowResults(true) : setShowResults(false);
- setQuery(value);
- debouncedSearch(host, value);
- },
- [host, debouncedSearch]
- );
- useEffect(() => {
- if (queryResults.length && downPress) {
- setCursor(prevState =>
- prevState < queryResults.length ? prevState + 1 : prevState
- );
- }
- }, [downPress, queryResults.length]);
- useEffect(() => {
- if (queryResults.length && upPress) {
- setCursor(prevState => (prevState > 0 ? prevState - 1 : prevState));
- }
- }, [upPress, queryResults.length]);
- useEffect(() => {
- if (enterPress && cursor === 0) {
- if (onSearch) {
- onSearch(query);
- } else {
- onChange(query);
- }
- } else if (enterPress && cursor <= queryResults.length) {
- const item = queryResults[cursor - 1]!;
- onSelectResult(item);
- }
- }, [cursor, enterPress, onChange, onSearch, onSelectResult, query, queryResults]);
- useEffect(() => {
- api.clear();
- setCursor(0);
- setError('');
- setLoading(false);
- setQueryResults([]);
- setShowResults(false);
- }, [escapePress, debouncedSearch, api, host]);
- const renderSuggestion = (item: any, idx: number) => {
- return (
- <a
- target="_blank"
- href={createSuggestionPath?.(item)}
- rel="noreferrer"
- key={item.id}
- >
- <SuggestionCard highlight={cursor === idx + 1}>
- {suggestionContent(item)}
- </SuggestionCard>
- </a>
- );
- };
- return (
- <div>
- <div ref={setKeyHandlers}>
- <SearchBar
- placeholder={placeholder}
- onChange={onChange}
- style={error ? {border: '1px solid red'} : {}}
- />
- </div>
- <SearchResults>
- {loading && <LoadingIndicator />}
- {!loading && showResults && queryResults.map(renderSuggestion)}
- {!loading && showResults && !queryResults.length && <Card>No results found</Card>}
- </SearchResults>
- {error && <Error>{error}</Error>}
- </div>
- );
- }
- const Card = styled('div')<{highlight?: boolean}>`
- color: ${p => (p.highlight ? p.theme.active : p.theme.textColor)};
- background: ${p => (p.highlight ? p.theme.gray100 : p.theme.background)};
- box-shadow: ${p => p.theme.dropShadowMedium};
- padding: ${space(2)};
- `;
- const Error = styled('div')`
- color: red;
- `;
- const SearchResults = styled('div')`
- margin-bottom: ${space(2)};
- `;
- const SuggestionCard = styled(Card)`
- &:hover {
- color: ${p => p.theme.active};
- background: ${p => p.theme.gray100};
- cursor: pointer;
- }
- `;
- export default DebounceSearch;
|