eventDetailsHeader.tsx 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. import {Fragment, useEffect} from 'react';
  2. import {css, useTheme} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import {Button} from 'sentry/components/button';
  5. import {Flex} from 'sentry/components/container/flex';
  6. import ErrorBoundary from 'sentry/components/errorBoundary';
  7. import {DatePageFilter} from 'sentry/components/organizations/datePageFilter';
  8. import {EnvironmentPageFilter} from 'sentry/components/organizations/environmentPageFilter';
  9. import {t} from 'sentry/locale';
  10. import {space} from 'sentry/styles/space';
  11. import type {Event} from 'sentry/types/event';
  12. import type {Group} from 'sentry/types/group';
  13. import type {Project} from 'sentry/types/project';
  14. import {getConfigForIssueType} from 'sentry/utils/issueTypeConfig';
  15. import {useLocation} from 'sentry/utils/useLocation';
  16. import {useNavigate} from 'sentry/utils/useNavigate';
  17. import useOrganization from 'sentry/utils/useOrganization';
  18. import {MetricIssuesChart} from 'sentry/views/issueDetails/metricIssues/metricIssueChart';
  19. import {useIssueDetails} from 'sentry/views/issueDetails/streamline/context';
  20. import {EventGraph} from 'sentry/views/issueDetails/streamline/eventGraph';
  21. import {
  22. EventSearch,
  23. useEventQuery,
  24. } from 'sentry/views/issueDetails/streamline/eventSearch';
  25. import {IssueCronCheckTimeline} from 'sentry/views/issueDetails/streamline/issueCronCheckTimeline';
  26. import IssueTagsPreview from 'sentry/views/issueDetails/streamline/issueTagsPreview';
  27. import {IssueUptimeCheckTimeline} from 'sentry/views/issueDetails/streamline/issueUptimeCheckTimeline';
  28. import {OccurrenceSummary} from 'sentry/views/issueDetails/streamline/occurrenceSummary';
  29. import {getDetectorDetails} from 'sentry/views/issueDetails/streamline/sidebar/detectorSection';
  30. import {ToggleSidebar} from 'sentry/views/issueDetails/streamline/sidebar/toggleSidebar';
  31. import {useEnvironmentsFromUrl} from 'sentry/views/issueDetails/utils';
  32. interface EventDetailsHeaderProps {
  33. group: Group;
  34. project: Project;
  35. event?: Event;
  36. }
  37. export function EventDetailsHeader({group, event, project}: EventDetailsHeaderProps) {
  38. const organization = useOrganization();
  39. const navigate = useNavigate();
  40. const location = useLocation();
  41. const environments = useEnvironmentsFromUrl();
  42. const searchQuery = useEventQuery({groupId: group.id});
  43. const issueTypeConfig = getConfigForIssueType(group, project);
  44. const {dispatch} = useIssueDetails();
  45. useEffect(() => {
  46. if (event) {
  47. // Since detector details are identical across the issue but only provided at the event level,
  48. // we need to persist the details in state to prevent breakage when an event is unloaded.
  49. const detectorDetails = getDetectorDetails({
  50. event,
  51. organization,
  52. project,
  53. });
  54. dispatch({
  55. type: 'UPDATE_DETECTOR_DETAILS',
  56. detectorDetails,
  57. });
  58. }
  59. }, [event, organization, project, dispatch]);
  60. const searchText = t(
  61. 'Filter %s\u2026',
  62. issueTypeConfig.customCopy.eventUnits.toLocaleLowerCase()
  63. );
  64. const hasHeader =
  65. issueTypeConfig.header.filterBar.enabled ||
  66. issueTypeConfig.header.graph.enabled ||
  67. issueTypeConfig.header.occurrenceSummary.enabled;
  68. if (!hasHeader) {
  69. return null;
  70. }
  71. return (
  72. <PageErrorBoundary mini message={t('There was an error loading the event filters')}>
  73. <FilterContainer
  74. role="group"
  75. aria-description={t('Event filtering controls')}
  76. hasFilterBar={issueTypeConfig.header.filterBar.enabled}
  77. >
  78. {issueTypeConfig.header.filterBar.enabled && (
  79. <Fragment>
  80. <EnvironmentSelector group={group} event={event} project={project} />
  81. <DateFilter
  82. triggerProps={{
  83. borderless: true,
  84. style: {
  85. borderRadius: 0,
  86. },
  87. }}
  88. />
  89. <Flex style={{gridArea: 'search'}}>
  90. <SearchFilter
  91. group={group}
  92. handleSearch={query => {
  93. navigate(
  94. {...location, query: {...location.query, query}},
  95. {replace: true}
  96. );
  97. }}
  98. environments={environments}
  99. query={searchQuery}
  100. queryBuilderProps={{
  101. disallowFreeText: true,
  102. placeholder: searchText,
  103. label: searchText,
  104. }}
  105. />
  106. <ToggleSidebar />
  107. </Flex>
  108. </Fragment>
  109. )}
  110. {issueTypeConfig.header.graph.enabled && (
  111. <GraphSection>
  112. {issueTypeConfig.header.graph.type === 'discover-events' && (
  113. <EventGraph event={event} group={group} style={{flex: 1}} />
  114. )}
  115. {issueTypeConfig.header.graph.type === 'detector-history' && (
  116. <MetricIssuesChart group={group} project={project} />
  117. )}
  118. {issueTypeConfig.header.graph.type === 'uptime-checks' && (
  119. <IssueUptimeCheckTimeline group={group} />
  120. )}
  121. {issueTypeConfig.header.graph.type === 'cron-checks' && (
  122. <IssueCronCheckTimeline group={group} />
  123. )}
  124. {issueTypeConfig.header.tagDistribution.enabled && (
  125. <IssueTagsPreview
  126. groupId={group.id}
  127. environments={environments}
  128. project={project}
  129. />
  130. )}
  131. </GraphSection>
  132. )}
  133. {issueTypeConfig.header.occurrenceSummary.enabled && (
  134. <OccurrenceSummarySection group={group} event={event} />
  135. )}
  136. </FilterContainer>
  137. </PageErrorBoundary>
  138. );
  139. }
  140. function EnvironmentSelector({group, event, project}: EventDetailsHeaderProps) {
  141. const theme = useTheme();
  142. const issueTypeConfig = getConfigForIssueType(group, project);
  143. const isFixedEnvironment = issueTypeConfig.header.filterBar.fixedEnvironment;
  144. const eventEnvironment = event?.tags?.find(tag => tag.key === 'environment')?.value;
  145. const environmentCss = css`
  146. grid-area: env;
  147. &:before {
  148. right: 0;
  149. top: ${space(1)};
  150. bottom: ${space(1)};
  151. width: 1px;
  152. content: '';
  153. position: absolute;
  154. background: ${theme.translucentInnerBorder};
  155. }
  156. `;
  157. return isFixedEnvironment ? (
  158. <Button
  159. disabled
  160. borderless
  161. title={t('This issue only occurs in a single environment')}
  162. css={environmentCss}
  163. >
  164. {eventEnvironment ?? t('All Envs')}
  165. </Button>
  166. ) : (
  167. <EnvironmentPageFilter
  168. css={environmentCss}
  169. triggerProps={{
  170. borderless: true,
  171. style: {
  172. borderRadius: 0,
  173. },
  174. }}
  175. />
  176. );
  177. }
  178. const FilterContainer = styled('div')<{
  179. hasFilterBar: boolean;
  180. }>`
  181. padding-left: 24px;
  182. display: grid;
  183. grid-template-columns: auto auto minmax(100px, 1fr) auto;
  184. grid-template-rows: ${p => (p.hasFilterBar ? 'minmax(38px, auto) auto auto' : 'auto')};
  185. grid-template-areas:
  186. 'env date search toggle'
  187. 'graph graph graph graph'
  188. 'timeline timeline timeline timeline';
  189. border: 0px solid ${p => p.theme.translucentBorder};
  190. border-width: 0 1px 1px 0;
  191. `;
  192. const SearchFilter = styled(EventSearch)`
  193. border-color: transparent;
  194. border-radius: 0;
  195. box-shadow: none;
  196. `;
  197. const DateFilter = styled(DatePageFilter)`
  198. grid-area: date;
  199. &:before {
  200. right: 0;
  201. top: ${space(1)};
  202. bottom: ${space(1)};
  203. width: 1px;
  204. content: '';
  205. position: absolute;
  206. background: ${p => p.theme.translucentInnerBorder};
  207. }
  208. `;
  209. const GraphSection = styled('div')`
  210. grid-area: graph;
  211. display: flex;
  212. &:not(:first-child) {
  213. border-top: 1px solid ${p => p.theme.translucentBorder};
  214. }
  215. `;
  216. const OccurrenceSummarySection = styled(OccurrenceSummary)`
  217. grid-area: timeline;
  218. padding: ${space(1)};
  219. padding-left: 0;
  220. &:not(:first-child) {
  221. border-top: 1px solid ${p => p.theme.translucentBorder};
  222. }
  223. `;
  224. const PageErrorBoundary = styled(ErrorBoundary)`
  225. margin: 0;
  226. border: 0px solid ${p => p.theme.translucentBorder};
  227. border-width: 0 1px 1px 0;
  228. border-radius: 0;
  229. padding: ${space(1.5)} 24px;
  230. `;