searchBar.tsx 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. import {useCallback} from 'react';
  2. import styled from '@emotion/styled';
  3. // eslint-disable-next-line no-restricted-imports
  4. import {fetchTagValues} from 'sentry/actionCreators/tags';
  5. import {SearchQueryBuilder} from 'sentry/components/searchQueryBuilder';
  6. import SmartSearchBar from 'sentry/components/smartSearchBar';
  7. import type {SearchGroup} from 'sentry/components/smartSearchBar/types';
  8. import {ItemType} from 'sentry/components/smartSearchBar/types';
  9. import {IconStar} from 'sentry/icons';
  10. import {t} from 'sentry/locale';
  11. import {space} from 'sentry/styles/space';
  12. import type {Organization, Tag, TagCollection} from 'sentry/types';
  13. import {SavedSearchType} from 'sentry/types';
  14. import {getUtcDateString} from 'sentry/utils/dates';
  15. import {
  16. DEVICE_CLASS_TAG_VALUES,
  17. FieldKey,
  18. FieldKind,
  19. getFieldDefinition,
  20. isDeviceClass,
  21. } from 'sentry/utils/fields';
  22. import useApi from 'sentry/utils/useApi';
  23. import usePageFilters from 'sentry/utils/usePageFilters';
  24. import type {WithIssueTagsProps} from 'sentry/utils/withIssueTags';
  25. import withIssueTags from 'sentry/utils/withIssueTags';
  26. const getSupportedTags = (supportedTags: TagCollection, org: Organization) => {
  27. const include_priority = org.features.includes('issue-priority-ui');
  28. return Object.fromEntries(
  29. Object.keys(supportedTags)
  30. .map(key => [
  31. key,
  32. {
  33. ...supportedTags[key],
  34. kind:
  35. getFieldDefinition(key)?.kind ??
  36. (supportedTags[key].predefined ? FieldKind.FIELD : FieldKind.TAG),
  37. },
  38. ])
  39. .filter(([key, _]) => (key === FieldKey.ISSUE_PRIORITY ? include_priority : true))
  40. );
  41. };
  42. interface Props extends React.ComponentProps<typeof SmartSearchBar>, WithIssueTagsProps {
  43. organization: Organization;
  44. }
  45. const EXCLUDED_TAGS = ['environment'];
  46. function IssueListSearchBar({organization, tags, ...props}: Props) {
  47. const api = useApi();
  48. const {selection: pageFilters} = usePageFilters();
  49. const tagValueLoader = useCallback(
  50. (key: string, search: string) => {
  51. const orgSlug = organization.slug;
  52. const projectIds = pageFilters.projects.map(id => id.toString());
  53. const endpointParams = {
  54. start: getUtcDateString(pageFilters.datetime.start),
  55. end: getUtcDateString(pageFilters.datetime.end),
  56. statsPeriod: pageFilters.datetime.period,
  57. };
  58. return fetchTagValues({
  59. api,
  60. orgSlug,
  61. tagKey: key,
  62. search,
  63. projectIds,
  64. endpointParams,
  65. });
  66. },
  67. [
  68. api,
  69. organization.slug,
  70. pageFilters.datetime.end,
  71. pageFilters.datetime.period,
  72. pageFilters.datetime.start,
  73. pageFilters.projects,
  74. ]
  75. );
  76. const getTagValues = useCallback(
  77. async (tag: Tag, query: string): Promise<string[]> => {
  78. // device.class is stored as "numbers" in snuba, but we want to suggest high, medium,
  79. // and low search filter values because discover maps device.class to these values.
  80. if (isDeviceClass(tag.key)) {
  81. return DEVICE_CLASS_TAG_VALUES;
  82. }
  83. const values = await tagValueLoader(tag.key, query);
  84. return values.map(({value}) => {
  85. // Truncate results to 5000 characters to avoid exceeding the max url query length
  86. // The message attribute for example can be 8192 characters.
  87. if (typeof value === 'string' && value.length > 5000) {
  88. return value.substring(0, 5000);
  89. }
  90. return value;
  91. });
  92. },
  93. [tagValueLoader]
  94. );
  95. const recommendedGroup: SearchGroup = {
  96. title: t('Popular Filters'),
  97. type: 'header',
  98. icon: <IconStar size="xs" />,
  99. childrenWrapper: RecommendedWrapper,
  100. children: [
  101. {
  102. type: ItemType.RECOMMENDED,
  103. kind: FieldKind.FIELD,
  104. title: t('Issue Category'),
  105. value: 'issue.category:',
  106. },
  107. {
  108. type: ItemType.RECOMMENDED,
  109. kind: FieldKind.FIELD,
  110. title: t('Error Level'),
  111. value: 'level:',
  112. },
  113. {
  114. type: ItemType.RECOMMENDED,
  115. kind: FieldKind.FIELD,
  116. title: t('Assignee'),
  117. value: 'assigned_or_suggested:',
  118. },
  119. {
  120. type: ItemType.RECOMMENDED,
  121. kind: FieldKind.FIELD,
  122. title: t('Unhandled Events'),
  123. value: 'error.unhandled:true ',
  124. },
  125. {
  126. type: ItemType.RECOMMENDED,
  127. kind: FieldKind.FIELD,
  128. title: t('Latest Release'),
  129. value: 'release:latest ',
  130. },
  131. {
  132. type: ItemType.RECOMMENDED,
  133. kind: FieldKind.TAG,
  134. title: t('Custom Tags'),
  135. // Shows only tags when clicked
  136. applyFilter: item => item.kind === FieldKind.TAG,
  137. },
  138. ],
  139. };
  140. if (organization.features.includes('issue-stream-search-query-builder')) {
  141. return (
  142. <SearchQueryBuilder
  143. className={props.className}
  144. initialQuery={props.query ?? ''}
  145. getTagValues={getTagValues}
  146. supportedKeys={getSupportedTags(tags, organization)}
  147. onSearch={props.onSearch}
  148. onBlur={props.onBlur}
  149. onChange={value => {
  150. props.onClose?.(value, {validSearch: true});
  151. }}
  152. />
  153. );
  154. }
  155. return (
  156. <SmartSearchBar
  157. hasRecentSearches
  158. savedSearchType={SavedSearchType.ISSUE}
  159. onGetTagValues={getTagValues}
  160. excludedTags={EXCLUDED_TAGS}
  161. maxMenuHeight={500}
  162. supportedTags={getSupportedTags(tags, organization)}
  163. defaultSearchGroup={recommendedGroup}
  164. organization={organization}
  165. {...props}
  166. />
  167. );
  168. }
  169. export default withIssueTags(IssueListSearchBar);
  170. // Using grid-template-rows to order the items top to bottom, then left to right
  171. const RecommendedWrapper = styled('div')`
  172. display: grid;
  173. grid-template-rows: 1fr 1fr 1fr;
  174. grid-auto-flow: column;
  175. gap: ${space(1)};
  176. padding: ${space(1)};
  177. text-align: left;
  178. line-height: 1.2;
  179. & > li {
  180. ${p => p.theme.overflowEllipsis}
  181. border-radius: ${p => p.theme.borderRadius};
  182. border: 1px solid ${p => p.theme.border};
  183. padding: ${space(1)} ${space(1.5)};
  184. margin: 0;
  185. }
  186. @media (min-width: ${p => p.theme.breakpoints.small}) {
  187. grid-template-rows: 1fr 1fr;
  188. gap: ${space(1.5)};
  189. padding: ${space(1.5)};
  190. text-align: center;
  191. & > li {
  192. padding: ${space(1.5)} ${space(2)};
  193. }
  194. }
  195. `;