searchBar.tsx 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. import {useState} from 'react';
  2. import {browserHistory} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import {Location} from 'history';
  5. import debounce from 'lodash/debounce';
  6. import BaseSearchBar from 'sentry/components/searchBar';
  7. import {DEFAULT_DEBOUNCE_DURATION} from 'sentry/constants';
  8. import {t} from 'sentry/locale';
  9. import {Organization} from 'sentry/types';
  10. import EventView from 'sentry/utils/discover/eventView';
  11. import {doDiscoverQuery} from 'sentry/utils/discover/genericDiscoverQuery';
  12. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  13. import useApi from 'sentry/utils/useApi';
  14. import {transactionSummaryRouteWithQuery} from 'sentry/views/performance/transactionSummary/utils';
  15. import SearchDropdown from '../smartSearchBar/searchDropdown';
  16. import {ItemType, SearchGroup} from '../smartSearchBar/types';
  17. type SearchBarProps = {
  18. eventView: EventView;
  19. location: Location;
  20. onSearch: (query: string) => void;
  21. organization: Organization;
  22. query: string;
  23. };
  24. function SearchBar(props: SearchBarProps) {
  25. const {organization, eventView: _eventView, onSearch, query: searchQuery} = props;
  26. const [searchResults, setSearchResults] = useState<SearchGroup[]>([]);
  27. const [loading, setLoading] = useState(false);
  28. const [searchString, setSearchString] = useState(searchQuery);
  29. const api = useApi();
  30. const eventView = _eventView.clone();
  31. const prepareQuery = (query: string) => {
  32. const prependedChar = query[0] === '*' ? '' : '*';
  33. const appendedChar = query[query.length - 1] === '*' ? '' : '*';
  34. return `${prependedChar}${query}${appendedChar}`;
  35. };
  36. const getSuggestedTransactions = debounce(
  37. async query => {
  38. if (query.length === 0) {
  39. onSearch('');
  40. }
  41. if (query.length < 3) {
  42. setSearchResults([]);
  43. return;
  44. }
  45. setSearchString(query);
  46. const projectIdStrings = (eventView.project as Readonly<number>[])?.map(String);
  47. try {
  48. setLoading(true);
  49. const conditions = new MutableSearch('');
  50. conditions.addFilterValues('transaction', [prepareQuery(query)], false);
  51. conditions.addFilterValues('event.type', ['transaction']);
  52. // clear any active requests
  53. if (Object.keys(api.activeRequests).length) {
  54. api.clear();
  55. }
  56. const useEvents = organization.features.includes(
  57. 'performance-frontend-use-events-endpoint'
  58. );
  59. const url = useEvents
  60. ? `/organizations/${organization.slug}/events/`
  61. : `/organizations/${organization.slug}/eventsv2/`;
  62. const [results] = await doDiscoverQuery<{
  63. data: Array<{'count()': number; project_id: number; transaction: string}>;
  64. }>(api, url, {
  65. field: ['transaction', 'project_id', 'count()'],
  66. project: projectIdStrings,
  67. sort: '-count()',
  68. query: conditions.formatString(),
  69. statsPeriod: eventView.statsPeriod,
  70. referrer: 'api.performance.transaction-name-search-bar',
  71. });
  72. const parsedResults = results.data.reduce(
  73. (searchGroup: SearchGroup, item) => {
  74. searchGroup.children.push({
  75. value: `${item.transaction}:${item.project_id}`,
  76. title: item.transaction,
  77. type: ItemType.LINK,
  78. desc: '',
  79. });
  80. return searchGroup;
  81. },
  82. {
  83. title: 'All Transactions',
  84. children: [],
  85. icon: null,
  86. type: 'header',
  87. }
  88. );
  89. setSearchResults([parsedResults]);
  90. } catch (_) {
  91. throw new Error('Unable to fetch event field values');
  92. } finally {
  93. setLoading(false);
  94. }
  95. },
  96. DEFAULT_DEBOUNCE_DURATION,
  97. {leading: true}
  98. );
  99. const handleSearch = (query: string) => {
  100. const lastIndex = query.lastIndexOf(':');
  101. const transactionName = query.slice(0, lastIndex);
  102. setSearchResults([]);
  103. setSearchString(transactionName);
  104. onSearch(`transaction:${transactionName}`);
  105. };
  106. const navigateToTransactionSummary = (name: string) => {
  107. const lastIndex = name.lastIndexOf(':');
  108. const transactionName = name.slice(0, lastIndex);
  109. const projectId = name.slice(lastIndex + 1);
  110. const query = eventView.generateQueryStringObject();
  111. setSearchResults([]);
  112. const next = transactionSummaryRouteWithQuery({
  113. orgSlug: organization.slug,
  114. transaction: String(transactionName),
  115. projectID: projectId,
  116. query,
  117. });
  118. browserHistory.push(next);
  119. };
  120. return (
  121. <Container data-test-id="transaction-search-bar">
  122. <BaseSearchBar
  123. placeholder={t('Search Transactions')}
  124. onChange={getSuggestedTransactions}
  125. query={searchString}
  126. />
  127. <SearchDropdown
  128. css={{
  129. display: searchResults[0]?.children.length ? 'block' : 'none',
  130. maxHeight: '300px',
  131. overflowY: 'auto',
  132. }}
  133. searchSubstring={searchString}
  134. loading={loading}
  135. items={searchResults}
  136. onClick={handleSearch}
  137. onIconClick={navigateToTransactionSummary}
  138. />
  139. </Container>
  140. );
  141. }
  142. const Container = styled('div')`
  143. position: relative;
  144. `;
  145. export default SearchBar;