searchBar.tsx 5.4 KB

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