breadcrumbsDrawer.tsx 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. import {useMemo, useState} from 'react';
  2. import {useTheme} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import ProjectAvatar from 'sentry/components/avatar/projectAvatar';
  5. import {Button} from 'sentry/components/button';
  6. import ButtonBar from 'sentry/components/buttonBar';
  7. import {CompactSelect} from 'sentry/components/compactSelect';
  8. import BreadcrumbsTimeline from 'sentry/components/events/breadcrumbs/breadcrumbsTimeline';
  9. import {
  10. BREADCRUMB_TIME_DISPLAY_LOCALSTORAGE_KEY,
  11. BREADCRUMB_TIME_DISPLAY_OPTIONS,
  12. BreadcrumbTimeDisplay,
  13. type EnhancedCrumb,
  14. useBreadcrumbFilters,
  15. } from 'sentry/components/events/breadcrumbs/utils';
  16. import {
  17. CrumbContainer,
  18. EventDrawerBody,
  19. EventDrawerContainer,
  20. EventDrawerHeader,
  21. EventNavigator,
  22. Header,
  23. NavigationCrumbs,
  24. SearchInput,
  25. ShortId,
  26. } from 'sentry/components/events/eventDrawer';
  27. import {
  28. applyBreadcrumbSearch,
  29. BREADCRUMB_SORT_LOCALSTORAGE_KEY,
  30. BREADCRUMB_SORT_OPTIONS,
  31. BreadcrumbSort,
  32. } from 'sentry/components/events/interfaces/breadcrumbs';
  33. import useFocusControl from 'sentry/components/events/useFocusControl';
  34. import {InputGroup} from 'sentry/components/inputGroup';
  35. import {IconClock, IconFilter, IconSearch, IconSort, IconTimer} from 'sentry/icons';
  36. import {t} from 'sentry/locale';
  37. import {space} from 'sentry/styles/space';
  38. import type {Event} from 'sentry/types/event';
  39. import type {Group} from 'sentry/types/group';
  40. import type {Project} from 'sentry/types/project';
  41. import {trackAnalytics} from 'sentry/utils/analytics';
  42. import {getShortEventId} from 'sentry/utils/events';
  43. import {useLocalStorageState} from 'sentry/utils/useLocalStorageState';
  44. import useOrganization from 'sentry/utils/useOrganization';
  45. export const enum BreadcrumbControlOptions {
  46. SEARCH = 'search',
  47. FILTER = 'filter',
  48. SORT = 'sort',
  49. }
  50. interface BreadcrumbsDrawerProps {
  51. breadcrumbs: EnhancedCrumb[];
  52. event: Event;
  53. group: Group;
  54. project: Project;
  55. focusControl?: BreadcrumbControlOptions;
  56. }
  57. export function BreadcrumbsDrawer({
  58. breadcrumbs,
  59. event,
  60. project,
  61. group,
  62. focusControl: initialFocusControl,
  63. }: BreadcrumbsDrawerProps) {
  64. const organization = useOrganization();
  65. const theme = useTheme();
  66. const [container, setContainer] = useState<HTMLElement | null>(null);
  67. const [search, setSearch] = useState('');
  68. const [filters, setFilters] = useState<string[]>([]);
  69. const [sort, setSort] = useLocalStorageState<BreadcrumbSort>(
  70. BREADCRUMB_SORT_LOCALSTORAGE_KEY,
  71. BreadcrumbSort.NEWEST
  72. );
  73. const {getFocusProps} = useFocusControl(initialFocusControl);
  74. const [timeDisplay, setTimeDisplay] = useLocalStorageState<BreadcrumbTimeDisplay>(
  75. BREADCRUMB_TIME_DISPLAY_LOCALSTORAGE_KEY,
  76. BreadcrumbTimeDisplay.ABSOLUTE
  77. );
  78. const {filterOptions, applyFilters} = useBreadcrumbFilters(breadcrumbs);
  79. const displayCrumbs = useMemo(() => {
  80. const sortedCrumbs =
  81. sort === BreadcrumbSort.OLDEST ? breadcrumbs : [...breadcrumbs].reverse();
  82. const filteredCrumbs = applyFilters(sortedCrumbs, filters);
  83. const searchedCrumbs = applyBreadcrumbSearch(filteredCrumbs, search);
  84. return searchedCrumbs;
  85. }, [breadcrumbs, sort, filters, search, applyFilters]);
  86. const startTimeString = useMemo(
  87. () =>
  88. timeDisplay === BreadcrumbTimeDisplay.RELATIVE
  89. ? displayCrumbs?.at(0)?.breadcrumb?.timestamp
  90. : undefined,
  91. [displayCrumbs, timeDisplay]
  92. );
  93. const actions = (
  94. <ButtonBar gap={1}>
  95. <InputGroup>
  96. <SearchInput
  97. size="xs"
  98. value={search}
  99. onChange={e => {
  100. setSearch(e.target.value);
  101. trackAnalytics('breadcrumbs.drawer.action', {
  102. control: BreadcrumbControlOptions.SEARCH,
  103. organization,
  104. });
  105. }}
  106. aria-label={t('Search All Breadcrumbs')}
  107. {...getFocusProps(BreadcrumbControlOptions.SEARCH)}
  108. />
  109. <InputGroup.TrailingItems disablePointerEvents>
  110. <IconSearch size="xs" />
  111. </InputGroup.TrailingItems>
  112. </InputGroup>
  113. <CompactSelect
  114. size="xs"
  115. onChange={options => {
  116. const newFilters = options.map(({value}) => value);
  117. setFilters(newFilters);
  118. trackAnalytics('breadcrumbs.drawer.action', {
  119. control: BreadcrumbControlOptions.FILTER,
  120. organization,
  121. });
  122. }}
  123. multiple
  124. options={filterOptions}
  125. maxMenuHeight={400}
  126. trigger={props => (
  127. <VisibleFocusButton
  128. size="xs"
  129. borderless
  130. style={{background: filters.length > 0 ? theme.purple100 : 'transparent'}}
  131. icon={<IconFilter />}
  132. aria-label={t('Filter All Breadcrumbs')}
  133. {...props}
  134. {...getFocusProps(BreadcrumbControlOptions.FILTER)}
  135. >
  136. {filters.length > 0 ? filters.length : null}
  137. </VisibleFocusButton>
  138. )}
  139. />
  140. <CompactSelect
  141. size="xs"
  142. trigger={props => (
  143. <VisibleFocusButton
  144. size="xs"
  145. borderless
  146. icon={<IconSort />}
  147. aria-label={t('Sort All Breadcrumbs')}
  148. {...props}
  149. {...getFocusProps(BreadcrumbControlOptions.SORT)}
  150. />
  151. )}
  152. onChange={selectedOption => {
  153. setSort(selectedOption.value);
  154. trackAnalytics('breadcrumbs.drawer.action', {
  155. control: BreadcrumbControlOptions.SORT,
  156. value: selectedOption.value,
  157. organization,
  158. });
  159. }}
  160. value={sort}
  161. options={BREADCRUMB_SORT_OPTIONS}
  162. />
  163. <CompactSelect
  164. size="xs"
  165. trigger={props => (
  166. <Button
  167. size="xs"
  168. borderless
  169. icon={
  170. timeDisplay === BreadcrumbTimeDisplay.ABSOLUTE ? (
  171. <IconClock size="xs" />
  172. ) : (
  173. <IconTimer size="xs" />
  174. )
  175. }
  176. aria-label={t('Change Time Format for All Breadcrumbs')}
  177. {...props}
  178. />
  179. )}
  180. onChange={selectedOption => {
  181. setTimeDisplay(selectedOption.value);
  182. trackAnalytics('breadcrumbs.drawer.action', {
  183. control: 'time_display',
  184. value: selectedOption.value,
  185. organization,
  186. });
  187. }}
  188. value={timeDisplay}
  189. options={Object.values(BREADCRUMB_TIME_DISPLAY_OPTIONS)}
  190. />
  191. </ButtonBar>
  192. );
  193. return (
  194. <EventDrawerContainer>
  195. <EventDrawerHeader>
  196. <NavigationCrumbs
  197. crumbs={[
  198. {
  199. label: (
  200. <CrumbContainer>
  201. <ProjectAvatar project={project} />
  202. <ShortId>{group.shortId}</ShortId>
  203. </CrumbContainer>
  204. ),
  205. },
  206. {label: getShortEventId(event.id)},
  207. {label: t('Breadcrumbs')},
  208. ]}
  209. />
  210. </EventDrawerHeader>
  211. <EventNavigator>
  212. <Header>{t('Breadcrumbs')}</Header>
  213. {actions}
  214. </EventNavigator>
  215. <EventDrawerBody ref={setContainer}>
  216. <TimelineContainer>
  217. {displayCrumbs.length === 0 ? (
  218. <EmptyMessage>
  219. {t('No breadcrumbs found.')}
  220. <Button
  221. priority="link"
  222. onClick={() => {
  223. setFilters([]);
  224. setSearch('');
  225. trackAnalytics('breadcrumbs.drawer.action', {
  226. control: 'clear_filters',
  227. organization,
  228. });
  229. }}
  230. >
  231. {t('Clear Filters?')}
  232. </Button>
  233. </EmptyMessage>
  234. ) : (
  235. <BreadcrumbsTimeline
  236. breadcrumbs={displayCrumbs}
  237. startTimeString={startTimeString}
  238. containerElement={container}
  239. />
  240. )}
  241. </TimelineContainer>
  242. </EventDrawerBody>
  243. </EventDrawerContainer>
  244. );
  245. }
  246. const VisibleFocusButton = styled(Button)`
  247. box-shadow: ${p => (p.autoFocus ? p.theme.button.default.focusBorder : 'transparent')} 0
  248. 0 0 1px;
  249. `;
  250. const TimelineContainer = styled('div')`
  251. grid-column: span 2;
  252. `;
  253. const EmptyMessage = styled('div')`
  254. display: flex;
  255. flex-direction: column;
  256. justify-content: center;
  257. align-items: center;
  258. color: ${p => p.theme.subText};
  259. padding: ${space(3)} ${space(1)};
  260. `;