searchBar.tsx 5.1 KB

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