groupEvents.tsx 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. import {useCallback, useMemo} from 'react';
  2. import type {RouteComponentProps} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import orderBy from 'lodash/orderBy';
  5. import {useFetchIssueTags} from 'sentry/actionCreators/group';
  6. import {fetchTagValues} from 'sentry/actionCreators/tags';
  7. import EventSearchBar from 'sentry/components/events/searchBar';
  8. import * as Layout from 'sentry/components/layouts/thirds';
  9. import {SearchQueryBuilder} from 'sentry/components/searchQueryBuilder';
  10. import type {FilterKeySection} from 'sentry/components/searchQueryBuilder/types';
  11. import {t} from 'sentry/locale';
  12. import {space} from 'sentry/styles/space';
  13. import type {Group, Tag, TagCollection} from 'sentry/types/group';
  14. import {browserHistory} from 'sentry/utils/browserHistory';
  15. import {FieldKind, ISSUE_EVENT_PROPERTY_FIELDS} from 'sentry/utils/fields';
  16. import normalizeUrl from 'sentry/utils/url/normalizeUrl';
  17. import useApi from 'sentry/utils/useApi';
  18. import useCleanQueryParamsOnRouteLeave from 'sentry/utils/useCleanQueryParamsOnRouteLeave';
  19. import useOrganization from 'sentry/utils/useOrganization';
  20. import {Dataset} from 'sentry/views/alerts/rules/metric/types';
  21. import {mergeAndSortTagValues} from 'sentry/views/issueDetails/utils';
  22. import {makeGetIssueTagValues} from 'sentry/views/issueList/utils/getIssueTagValues';
  23. import AllEventsTable from './allEventsTable';
  24. interface Props extends RouteComponentProps<{groupId: string}, {}> {
  25. environments: string[];
  26. group: Group;
  27. }
  28. const EXCLUDED_TAGS = [
  29. 'environment',
  30. 'issue',
  31. 'issue.id',
  32. 'performance.issue_ids',
  33. 'transaction.op',
  34. 'transaction.status',
  35. ];
  36. function getFilterKeySections(tags: TagCollection): FilterKeySection[] {
  37. const allTags: Tag[] = Object.values(tags).filter(
  38. tag => !EXCLUDED_TAGS.includes(tag.key)
  39. );
  40. const eventFields = orderBy(
  41. allTags.filter(tag => tag.kind === FieldKind.EVENT_FIELD),
  42. ['key']
  43. ).map(tag => tag.key);
  44. const eventTags = orderBy(
  45. allTags.filter(tag => tag.kind === FieldKind.TAG),
  46. ['totalValues', 'key'],
  47. ['desc', 'asc']
  48. ).map(tag => tag.key);
  49. return [
  50. {
  51. value: FieldKind.EVENT_FIELD,
  52. label: t('Event Filters'),
  53. children: eventFields,
  54. },
  55. {
  56. value: FieldKind.TAG,
  57. label: t('Event Tags'),
  58. children: eventTags,
  59. },
  60. ];
  61. }
  62. function UpdatedSearchBar({
  63. query,
  64. group,
  65. environments,
  66. handleSearch,
  67. }: {
  68. environments: string[];
  69. group: Group;
  70. handleSearch: (value: string) => void;
  71. query: string;
  72. }) {
  73. const api = useApi();
  74. const organization = useOrganization();
  75. const {data = []} = useFetchIssueTags({
  76. orgSlug: organization.slug,
  77. groupId: group.id,
  78. environment: environments,
  79. });
  80. const filterKeys = useMemo<TagCollection>(() => {
  81. const tags = [
  82. ...data.map(tag => ({...tag, kind: FieldKind.TAG})),
  83. ...ISSUE_EVENT_PROPERTY_FIELDS.map(tag => ({
  84. key: tag,
  85. name: tag,
  86. kind: FieldKind.EVENT_FIELD,
  87. })),
  88. ].filter(tag => !EXCLUDED_TAGS.includes(tag.key));
  89. return tags.reduce<TagCollection>((acc, tag) => {
  90. acc[tag.key] = tag;
  91. return acc;
  92. }, {});
  93. }, [data]);
  94. const tagValueLoader = useCallback(
  95. async (key: string, search: string) => {
  96. const orgSlug = organization.slug;
  97. const projectIds = [group.project.id];
  98. const [eventsDatasetValues, issuePlatformDatasetValues] = await Promise.all([
  99. fetchTagValues({
  100. api,
  101. orgSlug,
  102. tagKey: key,
  103. search,
  104. projectIds,
  105. dataset: Dataset.ERRORS,
  106. }),
  107. fetchTagValues({
  108. api,
  109. orgSlug,
  110. tagKey: key,
  111. search,
  112. projectIds,
  113. dataset: Dataset.ISSUE_PLATFORM,
  114. }),
  115. ]);
  116. return mergeAndSortTagValues(eventsDatasetValues, issuePlatformDatasetValues);
  117. },
  118. [api, group.project.id, organization.slug]
  119. );
  120. const getTagValues = useMemo(
  121. () => makeGetIssueTagValues(tagValueLoader),
  122. [tagValueLoader]
  123. );
  124. const filterKeySections = useMemo(() => getFilterKeySections(filterKeys), [filterKeys]);
  125. return (
  126. <SearchQueryBuilder
  127. initialQuery={query}
  128. onSearch={handleSearch}
  129. filterKeys={filterKeys}
  130. filterKeySections={filterKeySections}
  131. getTagValues={getTagValues}
  132. placeholder={t('Search events...')}
  133. searchSource="issue_events_tab"
  134. />
  135. );
  136. }
  137. function GroupEvents({params, location, group, environments}: Props) {
  138. const organization = useOrganization();
  139. const {groupId} = params;
  140. useCleanQueryParamsOnRouteLeave({
  141. fieldsToClean: ['cursor', 'query'],
  142. shouldClean: newLocation => newLocation.pathname.includes(`/issues/${group.id}/`),
  143. });
  144. const handleSearch = useCallback(
  145. (query: string) =>
  146. browserHistory.push(
  147. normalizeUrl({
  148. pathname: `/organizations/${organization.slug}/issues/${groupId}/events/`,
  149. query: {...location.query, query},
  150. })
  151. ),
  152. [location, organization, groupId]
  153. );
  154. const query = location.query?.query ?? '';
  155. return (
  156. <Layout.Body>
  157. <Layout.Main fullWidth>
  158. <AllEventsFilters>
  159. {organization.features.includes('issue-stream-search-query-builder') ? (
  160. <UpdatedSearchBar
  161. environments={environments}
  162. group={group}
  163. handleSearch={handleSearch}
  164. query={query}
  165. />
  166. ) : (
  167. <EventSearchBar
  168. organization={organization}
  169. defaultQuery=""
  170. onSearch={handleSearch}
  171. excludedTags={EXCLUDED_TAGS}
  172. query={query}
  173. hasRecentSearches={false}
  174. searchSource="issue_events_tab"
  175. />
  176. )}
  177. </AllEventsFilters>
  178. <AllEventsTable
  179. issueId={group.id}
  180. location={location}
  181. organization={organization}
  182. group={group}
  183. excludedTags={EXCLUDED_TAGS}
  184. />
  185. </Layout.Main>
  186. </Layout.Body>
  187. );
  188. }
  189. const AllEventsFilters = styled('div')`
  190. margin-bottom: ${space(2)};
  191. `;
  192. export default GroupEvents;