searchBar.tsx 5.6 KB

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