Files
sentinel/static/app/components/asyncComponentSearchInput.tsx
2024-02-15 17:01:50 +00:00

164 lines
4.2 KiB
TypeScript

import {Component} from 'react';
import type {WithRouterProps} from 'react-router';
import styled from '@emotion/styled';
import debounce from 'lodash/debounce';
import type {Client, ResponseMeta} from 'sentry/api';
import Input from 'sentry/components/input';
import LoadingIndicator from 'sentry/components/loadingIndicator';
import {t} from 'sentry/locale';
// eslint-disable-next-line no-restricted-imports
import withSentryRouter from 'sentry/utils/withSentryRouter';
type RenderProps = {
busy: boolean;
defaultSearchBar: React.ReactNode;
handleChange: (value: string) => void;
value: string;
};
type DefaultProps = {
/**
* Placeholder text in the search input
*/
placeholder: string;
/**
* Time in milliseconds to wait before firing off the request
*/
debounceWait?: number; // optional, otherwise app/views/settings/organizationMembers/organizationMembersList.tsx L:191 is not happy
};
type Props = WithRouterProps &
DefaultProps & {
api: Client;
onError: () => void;
onSuccess: (data: object, resp: ResponseMeta | undefined) => void;
/**
* URL to make the search request to
*/
url: string;
/**
* A render-prop child may be passed to handle custom rendering of the input.
*/
children?: (otps: RenderProps) => React.ReactNode;
className?: string;
onSearchSubmit?: (query: string, event: React.FormEvent) => void;
/**
* Updates URL with search query in the URL param: `query`
*/
updateRoute?: boolean;
};
type State = {
busy: boolean;
query: string;
};
/**
* This is a search input that can be easily used in AsyncComponent/Views.
*
* It probably doesn't make too much sense outside of an AsyncComponent atm.
*/
class AsyncComponentSearchInput extends Component<Props, State> {
static defaultProps: DefaultProps = {
placeholder: t('Search...'),
debounceWait: 200,
};
state: State = {
query: '',
busy: false,
};
immediateQuery = async (searchQuery: string) => {
const {location, api} = this.props;
this.setState({busy: true});
try {
const [data, , resp] = await api.requestPromise(`${this.props.url}`, {
includeAllArgs: true,
method: 'GET',
query: {...location.query, query: searchQuery},
});
// only update data if the request's query matches the current query
if (this.state.query === searchQuery) {
this.props.onSuccess(data, resp);
}
} catch {
this.props.onError();
}
this.setState({busy: false});
};
query = debounce(this.immediateQuery, this.props.debounceWait);
handleChange = (query: string) => {
this.query(query);
this.setState({query});
};
handleInputChange = (evt: React.ChangeEvent<HTMLInputElement>) =>
this.handleChange(evt.target.value);
/**
* This is called when "Enter" (more specifically a form "submit" event) is pressed.
*/
handleSearch = (evt: React.FormEvent<HTMLFormElement>) => {
const {updateRoute, onSearchSubmit} = this.props;
evt.preventDefault();
// Update the URL to reflect search term.
if (updateRoute) {
const {router, location} = this.props;
router.push({
pathname: location.pathname,
query: {
query: this.state.query,
},
});
}
if (typeof onSearchSubmit !== 'function') {
return;
}
onSearchSubmit(this.state.query, evt);
};
render() {
const {placeholder, children, className} = this.props;
const {busy, query} = this.state;
const defaultSearchBar = (
<Form onSubmit={this.handleSearch}>
<Input
value={query}
onChange={this.handleInputChange}
className={className}
placeholder={placeholder}
/>
{busy && <StyledLoadingIndicator size={18} hideMessage mini />}
</Form>
);
return children === undefined
? defaultSearchBar
: children({defaultSearchBar, busy, value: query, handleChange: this.handleChange});
}
}
const StyledLoadingIndicator = styled(LoadingIndicator)`
position: absolute;
right: 25px;
top: 50%;
transform: translateY(-13px);
`;
const Form = styled('form')`
position: relative;
`;
export default withSentryRouter(AsyncComponentSearchInput);