asyncComponentSearchInput.tsx 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. import {Component} from 'react';
  2. import {WithRouterProps} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import debounce from 'lodash/debounce';
  5. import {Client, ResponseMeta} from 'sentry/api';
  6. import Input from 'sentry/components/input';
  7. import LoadingIndicator from 'sentry/components/loadingIndicator';
  8. import {t} from 'sentry/locale';
  9. // eslint-disable-next-line no-restricted-imports
  10. import withSentryRouter from 'sentry/utils/withSentryRouter';
  11. type RenderProps = {
  12. busy: boolean;
  13. defaultSearchBar: React.ReactNode;
  14. handleChange: (value: string) => void;
  15. value: string;
  16. };
  17. type DefaultProps = {
  18. /**
  19. * Placeholder text in the search input
  20. */
  21. placeholder: string;
  22. /**
  23. * Time in milliseconds to wait before firing off the request
  24. */
  25. debounceWait?: number; // optional, otherwise app/views/settings/organizationMembers/organizationMembersList.tsx L:191 is not happy
  26. };
  27. type Props = WithRouterProps &
  28. DefaultProps & {
  29. api: Client;
  30. onError: () => void;
  31. onSuccess: (data: object, resp: ResponseMeta | undefined) => void;
  32. /**
  33. * URL to make the search request to
  34. */
  35. url: string;
  36. /**
  37. * A render-prop child may be passed to handle custom rendering of the input.
  38. */
  39. children?: (otps: RenderProps) => React.ReactNode;
  40. className?: string;
  41. onSearchSubmit?: (query: string, event: React.FormEvent) => void;
  42. /**
  43. * Updates URL with search query in the URL param: `query`
  44. */
  45. updateRoute?: boolean;
  46. };
  47. type State = {
  48. busy: boolean;
  49. query: string;
  50. };
  51. /**
  52. * This is a search input that can be easily used in AsyncComponent/Views.
  53. *
  54. * It probably doesn't make too much sense outside of an AsyncComponent atm.
  55. */
  56. class AsyncComponentSearchInput extends Component<Props, State> {
  57. static defaultProps: DefaultProps = {
  58. placeholder: t('Search...'),
  59. debounceWait: 200,
  60. };
  61. state: State = {
  62. query: '',
  63. busy: false,
  64. };
  65. immediateQuery = async (searchQuery: string) => {
  66. const {location, api} = this.props;
  67. this.setState({busy: true});
  68. try {
  69. const [data, , resp] = await api.requestPromise(`${this.props.url}`, {
  70. includeAllArgs: true,
  71. method: 'GET',
  72. query: {...location.query, query: searchQuery},
  73. });
  74. // only update data if the request's query matches the current query
  75. if (this.state.query === searchQuery) {
  76. this.props.onSuccess(data, resp);
  77. }
  78. } catch {
  79. this.props.onError();
  80. }
  81. this.setState({busy: false});
  82. };
  83. query = debounce(this.immediateQuery, this.props.debounceWait);
  84. handleChange = (query: string) => {
  85. this.query(query);
  86. this.setState({query});
  87. };
  88. handleInputChange = (evt: React.ChangeEvent<HTMLInputElement>) =>
  89. this.handleChange(evt.target.value);
  90. /**
  91. * This is called when "Enter" (more specifically a form "submit" event) is pressed.
  92. */
  93. handleSearch = (evt: React.FormEvent<HTMLFormElement>) => {
  94. const {updateRoute, onSearchSubmit} = this.props;
  95. evt.preventDefault();
  96. // Update the URL to reflect search term.
  97. if (updateRoute) {
  98. const {router, location} = this.props;
  99. router.push({
  100. pathname: location.pathname,
  101. query: {
  102. query: this.state.query,
  103. },
  104. });
  105. }
  106. if (typeof onSearchSubmit !== 'function') {
  107. return;
  108. }
  109. onSearchSubmit(this.state.query, evt);
  110. };
  111. render() {
  112. const {placeholder, children, className} = this.props;
  113. const {busy, query} = this.state;
  114. const defaultSearchBar = (
  115. <Form onSubmit={this.handleSearch}>
  116. <Input
  117. value={query}
  118. onChange={this.handleInputChange}
  119. className={className}
  120. placeholder={placeholder}
  121. />
  122. {busy && <StyledLoadingIndicator size={18} hideMessage mini />}
  123. </Form>
  124. );
  125. return children === undefined
  126. ? defaultSearchBar
  127. : children({defaultSearchBar, busy, value: query, handleChange: this.handleChange});
  128. }
  129. }
  130. const StyledLoadingIndicator = styled(LoadingIndicator)`
  131. position: absolute;
  132. right: 25px;
  133. top: 50%;
  134. transform: translateY(-13px);
  135. `;
  136. const Form = styled('form')`
  137. position: relative;
  138. `;
  139. export default withSentryRouter(AsyncComponentSearchInput);