group.tsx 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768
  1. import {Component, Fragment} from 'react';
  2. import {css, Theme} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import classNames from 'classnames';
  5. import type {LocationDescriptor} from 'history';
  6. import AssigneeSelector from 'sentry/components/assigneeSelector';
  7. import GuideAnchor from 'sentry/components/assistant/guideAnchor';
  8. import Count from 'sentry/components/count';
  9. import DropdownMenu from 'sentry/components/dropdownMenu';
  10. import EventOrGroupExtraDetails from 'sentry/components/eventOrGroupExtraDetails';
  11. import EventOrGroupHeader from 'sentry/components/eventOrGroupHeader';
  12. import Link from 'sentry/components/links/link';
  13. import MenuItem from 'sentry/components/menuItem';
  14. import {getRelativeSummary} from 'sentry/components/organizations/timeRangeSelector/utils';
  15. import {PanelItem} from 'sentry/components/panels';
  16. import Placeholder from 'sentry/components/placeholder';
  17. import ProgressBar from 'sentry/components/progressBar';
  18. import GroupChart from 'sentry/components/stream/groupChart';
  19. import GroupCheckBox from 'sentry/components/stream/groupCheckBox';
  20. import TimeSince from 'sentry/components/timeSince';
  21. import {DEFAULT_STATS_PERIOD} from 'sentry/constants';
  22. import {t} from 'sentry/locale';
  23. import GroupStore from 'sentry/stores/groupStore';
  24. import SelectedGroupStore from 'sentry/stores/selectedGroupStore';
  25. import space from 'sentry/styles/space';
  26. import {
  27. Group,
  28. GroupReprocessing,
  29. InboxDetails,
  30. NewQuery,
  31. Organization,
  32. PageFilters,
  33. User,
  34. } from 'sentry/types';
  35. import {defined, percent, valueIsEqual} from 'sentry/utils';
  36. import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
  37. import {callIfFunction} from 'sentry/utils/callIfFunction';
  38. import EventView from 'sentry/utils/discover/eventView';
  39. import {queryToObj} from 'sentry/utils/stream';
  40. import withOrganization from 'sentry/utils/withOrganization';
  41. import withPageFilters from 'sentry/utils/withPageFilters';
  42. import {TimePeriodType} from 'sentry/views/alerts/rules/metric/details/constants';
  43. import {
  44. DISCOVER_EXCLUSION_FIELDS,
  45. getTabs,
  46. isForReviewQuery,
  47. Query,
  48. } from 'sentry/views/issueList/utils';
  49. export const DEFAULT_STREAM_GROUP_STATS_PERIOD = '24h';
  50. const defaultProps = {
  51. statsPeriod: DEFAULT_STREAM_GROUP_STATS_PERIOD,
  52. canSelect: true,
  53. withChart: true,
  54. useFilteredStats: false,
  55. useTintRow: true,
  56. narrowGroups: false,
  57. };
  58. type Props = {
  59. id: string;
  60. organization: Organization;
  61. selection: PageFilters;
  62. customStatsPeriod?: TimePeriodType;
  63. displayReprocessingLayout?: boolean;
  64. hasGuideAnchor?: boolean;
  65. index?: number;
  66. memberList?: User[];
  67. query?: string;
  68. // TODO(ts): higher order functions break defaultprops export types
  69. queryFilterDescription?: string;
  70. showInboxTime?: boolean;
  71. } & Partial<typeof defaultProps>;
  72. type State = {
  73. actionTaken: boolean;
  74. data: Group;
  75. reviewed: boolean;
  76. };
  77. class StreamGroup extends Component<Props, State> {
  78. static defaultProps = defaultProps;
  79. state: State = this.getInitialState();
  80. getInitialState(): State {
  81. const {id, useFilteredStats} = this.props;
  82. const data = GroupStore.get(id) as Group;
  83. return {
  84. data: {
  85. ...data,
  86. filtered: useFilteredStats ? data.filtered : null,
  87. },
  88. reviewed: false,
  89. actionTaken: false,
  90. };
  91. }
  92. componentWillReceiveProps(nextProps: Props) {
  93. if (
  94. nextProps.id !== this.props.id ||
  95. nextProps.useFilteredStats !== this.props.useFilteredStats
  96. ) {
  97. const data = GroupStore.get(this.props.id) as Group;
  98. this.setState({
  99. data: {
  100. ...data,
  101. filtered: nextProps.useFilteredStats ? data.filtered : null,
  102. },
  103. });
  104. }
  105. }
  106. shouldComponentUpdate(nextProps: Props, nextState: State) {
  107. if (nextProps.statsPeriod !== this.props.statsPeriod) {
  108. return true;
  109. }
  110. if (!valueIsEqual(this.state.data, nextState.data)) {
  111. return true;
  112. }
  113. return false;
  114. }
  115. componentWillUnmount() {
  116. callIfFunction(this.listener);
  117. }
  118. listener = GroupStore.listen(itemIds => this.onGroupChange(itemIds), undefined);
  119. onGroupChange(itemIds: Set<string>) {
  120. const {id, query} = this.props;
  121. if (!itemIds.has(id)) {
  122. return;
  123. }
  124. const actionTaken = this.state.data.status !== 'unresolved';
  125. const data = GroupStore.get(id) as Group;
  126. this.setState(state => {
  127. // When searching is:for_review and the inbox reason is removed
  128. const reviewed =
  129. state.reviewed ||
  130. (isForReviewQuery(query) &&
  131. (state.data.inbox as InboxDetails)?.reason !== undefined &&
  132. data.inbox === false);
  133. return {data, reviewed, actionTaken};
  134. });
  135. }
  136. /** Shared between two events */
  137. sharedAnalytics() {
  138. const {query, organization} = this.props;
  139. const {data} = this.state;
  140. const tab = getTabs(organization).find(([tabQuery]) => tabQuery === query)?.[1];
  141. const owners = data?.owners || [];
  142. return {
  143. organization,
  144. group_id: data.id,
  145. tab: tab?.analyticsName || 'other',
  146. was_shown_suggestion: owners.length > 0,
  147. };
  148. }
  149. trackClick = () => {
  150. const {query, organization} = this.props;
  151. const {data} = this.state;
  152. if (query === Query.FOR_REVIEW) {
  153. trackAdvancedAnalyticsEvent('inbox_tab.issue_clicked', {
  154. organization,
  155. group_id: data.id,
  156. });
  157. }
  158. if (query !== undefined) {
  159. trackAdvancedAnalyticsEvent('issues_stream.issue_clicked', this.sharedAnalytics());
  160. }
  161. };
  162. trackAssign: React.ComponentProps<typeof AssigneeSelector>['onAssign'] = (
  163. type,
  164. _assignee,
  165. suggestedAssignee
  166. ) => {
  167. const {query} = this.props;
  168. if (query !== undefined) {
  169. trackAdvancedAnalyticsEvent('issues_stream.issue_assigned', {
  170. ...this.sharedAnalytics(),
  171. did_assign_suggestion: !!suggestedAssignee,
  172. assigned_suggestion_reason: suggestedAssignee?.suggestedReason,
  173. assigned_type: type,
  174. });
  175. }
  176. };
  177. toggleSelect = (evt: React.MouseEvent<HTMLDivElement>) => {
  178. const targetElement = evt.target as Partial<HTMLElement>;
  179. if (targetElement?.tagName?.toLowerCase() === 'a') {
  180. return;
  181. }
  182. if (targetElement?.tagName?.toLowerCase() === 'input') {
  183. return;
  184. }
  185. let e = targetElement;
  186. while (e.parentElement) {
  187. if (e?.tagName?.toLowerCase() === 'a') {
  188. return;
  189. }
  190. e = e.parentElement!;
  191. }
  192. SelectedGroupStore.toggleSelect(this.state.data.id);
  193. };
  194. getDiscoverUrl(isFiltered?: boolean): LocationDescriptor {
  195. const {organization, query, selection, customStatsPeriod} = this.props;
  196. const {data} = this.state;
  197. // when there is no discover feature open events page
  198. const hasDiscoverQuery = organization.features.includes('discover-basic');
  199. const queryTerms: string[] = [];
  200. if (isFiltered && typeof query === 'string') {
  201. const queryObj = queryToObj(query);
  202. for (const queryTag in queryObj) {
  203. if (!DISCOVER_EXCLUSION_FIELDS.includes(queryTag)) {
  204. const queryVal = queryObj[queryTag].includes(' ')
  205. ? `"${queryObj[queryTag]}"`
  206. : queryObj[queryTag];
  207. queryTerms.push(`${queryTag}:${queryVal}`);
  208. }
  209. }
  210. if (queryObj.__text) {
  211. queryTerms.push(queryObj.__text);
  212. }
  213. }
  214. const commonQuery = {projects: [Number(data.project.id)]};
  215. const searchQuery = (queryTerms.length ? ' ' : '') + queryTerms.join(' ');
  216. if (hasDiscoverQuery) {
  217. const {period, start, end, utc} = customStatsPeriod ?? (selection.datetime || {});
  218. const discoverQuery: NewQuery = {
  219. ...commonQuery,
  220. id: undefined,
  221. name: data.title || data.type,
  222. fields: ['title', 'release', 'environment', 'user', 'timestamp'],
  223. orderby: '-timestamp',
  224. query: `issue.id:${data.id}${searchQuery}`,
  225. version: 2,
  226. };
  227. if (!!start && !!end) {
  228. discoverQuery.start = new Date(start).toISOString();
  229. discoverQuery.end = new Date(end).toISOString();
  230. if (utc) {
  231. discoverQuery.utc = true;
  232. }
  233. } else {
  234. discoverQuery.range = period || DEFAULT_STATS_PERIOD;
  235. }
  236. const discoverView = EventView.fromSavedQuery(discoverQuery);
  237. return discoverView.getResultsViewUrlTarget(organization.slug);
  238. }
  239. return {
  240. pathname: `/organizations/${organization.slug}/issues/${data.id}/events/`,
  241. query: {
  242. ...commonQuery,
  243. query: searchQuery,
  244. },
  245. };
  246. }
  247. renderReprocessingColumns() {
  248. const {data} = this.state;
  249. const {statusDetails, count} = data as GroupReprocessing;
  250. const {info, pendingEvents} = statusDetails;
  251. if (!info) {
  252. return null;
  253. }
  254. const {totalEvents, dateCreated} = info;
  255. const remainingEventsToReprocess = totalEvents - pendingEvents;
  256. const remainingEventsToReprocessPercent = percent(
  257. remainingEventsToReprocess,
  258. totalEvents
  259. );
  260. return (
  261. <Fragment>
  262. <StartedColumn>
  263. <TimeSince date={dateCreated} />
  264. </StartedColumn>
  265. <EventsReprocessedColumn>
  266. {!defined(count) ? (
  267. <Placeholder height="17px" />
  268. ) : (
  269. <Fragment>
  270. <Count value={remainingEventsToReprocess} />
  271. {'/'}
  272. <Count value={totalEvents} />
  273. </Fragment>
  274. )}
  275. </EventsReprocessedColumn>
  276. <ProgressColumn>
  277. <ProgressBar value={remainingEventsToReprocessPercent} />
  278. </ProgressColumn>
  279. </Fragment>
  280. );
  281. }
  282. render() {
  283. const {data, reviewed, actionTaken} = this.state;
  284. const {
  285. index,
  286. query,
  287. hasGuideAnchor,
  288. canSelect,
  289. memberList,
  290. withChart,
  291. statsPeriod,
  292. selection,
  293. organization,
  294. displayReprocessingLayout,
  295. showInboxTime,
  296. useFilteredStats,
  297. useTintRow,
  298. customStatsPeriod,
  299. queryFilterDescription,
  300. narrowGroups,
  301. } = this.props;
  302. const {period, start, end} = selection.datetime || {};
  303. const summary =
  304. customStatsPeriod?.label.toLowerCase() ??
  305. (!!start && !!end
  306. ? 'time range'
  307. : getRelativeSummary(period || DEFAULT_STATS_PERIOD).toLowerCase());
  308. // Use data.filtered to decide on which value to use
  309. // In case of the query has filters but we avoid showing both sets of filtered/unfiltered stats
  310. // we use useFilteredStats param passed to Group for deciding
  311. const primaryCount = data.filtered ? data.filtered.count : data.count;
  312. const secondaryCount = data.filtered ? data.count : undefined;
  313. const primaryUserCount = data.filtered ? data.filtered.userCount : data.userCount;
  314. const secondaryUserCount = data.filtered ? data.userCount : undefined;
  315. const showSecondaryPoints = Boolean(
  316. withChart && data && data.filtered && statsPeriod && useFilteredStats
  317. );
  318. return (
  319. <Wrapper
  320. data-test-id="group"
  321. onClick={displayReprocessingLayout ? undefined : this.toggleSelect}
  322. reviewed={reviewed}
  323. unresolved={data.status === 'unresolved'}
  324. actionTaken={actionTaken}
  325. useTintRow={useTintRow ?? true}
  326. >
  327. {canSelect && (
  328. <GroupCheckBoxWrapper>
  329. <GroupCheckBox id={data.id} disabled={!!displayReprocessingLayout} />
  330. </GroupCheckBoxWrapper>
  331. )}
  332. <GroupSummary canSelect={!!canSelect}>
  333. <EventOrGroupHeader
  334. index={index}
  335. organization={organization}
  336. includeLink
  337. data={data}
  338. query={query}
  339. size="normal"
  340. onClick={this.trackClick}
  341. />
  342. <EventOrGroupExtraDetails data={data} showInboxTime={showInboxTime} />
  343. </GroupSummary>
  344. {hasGuideAnchor && <GuideAnchor target="issue_stream" />}
  345. {withChart && !displayReprocessingLayout && (
  346. <ChartWrapper
  347. className={`hidden-xs hidden-sm ${narrowGroups ? 'hidden-md' : ''}`}
  348. >
  349. {!data.filtered?.stats && !data.stats ? (
  350. <Placeholder height="24px" />
  351. ) : (
  352. <GroupChart
  353. statsPeriod={statsPeriod!}
  354. data={data}
  355. showSecondaryPoints={showSecondaryPoints}
  356. showMarkLine
  357. />
  358. )}
  359. </ChartWrapper>
  360. )}
  361. {displayReprocessingLayout ? (
  362. this.renderReprocessingColumns()
  363. ) : (
  364. <Fragment>
  365. <EventUserWrapper>
  366. {!defined(primaryCount) ? (
  367. <Placeholder height="18px" />
  368. ) : (
  369. <DropdownMenu isNestedDropdown>
  370. {({isOpen, getRootProps, getActorProps, getMenuProps}) => {
  371. const topLevelCx = classNames('dropdown', {
  372. 'anchor-middle': true,
  373. open: isOpen,
  374. });
  375. return (
  376. <GuideAnchor target="dynamic_counts" disabled={!hasGuideAnchor}>
  377. <span
  378. {...getRootProps({
  379. className: topLevelCx,
  380. })}
  381. >
  382. <span {...getActorProps({})}>
  383. <div className="dropdown-actor-title">
  384. <PrimaryCount value={primaryCount} />
  385. {secondaryCount !== undefined && useFilteredStats && (
  386. <SecondaryCount value={secondaryCount} />
  387. )}
  388. </div>
  389. </span>
  390. {useFilteredStats && (
  391. <StyledDropdownList
  392. {...getMenuProps({className: 'dropdown-menu inverted'})}
  393. >
  394. {data.filtered && (
  395. <Fragment>
  396. <StyledMenuItem to={this.getDiscoverUrl(true)}>
  397. <MenuItemText>
  398. {queryFilterDescription ??
  399. t('Matching search filters')}
  400. </MenuItemText>
  401. <MenuItemCount value={data.filtered.count} />
  402. </StyledMenuItem>
  403. <MenuItem divider />
  404. </Fragment>
  405. )}
  406. <StyledMenuItem to={this.getDiscoverUrl()}>
  407. <MenuItemText>{t(`Total in ${summary}`)}</MenuItemText>
  408. <MenuItemCount value={data.count} />
  409. </StyledMenuItem>
  410. {data.lifetime && (
  411. <Fragment>
  412. <MenuItem divider />
  413. <StyledMenuItem>
  414. <MenuItemText>{t('Since issue began')}</MenuItemText>
  415. <MenuItemCount value={data.lifetime.count} />
  416. </StyledMenuItem>
  417. </Fragment>
  418. )}
  419. </StyledDropdownList>
  420. )}
  421. </span>
  422. </GuideAnchor>
  423. );
  424. }}
  425. </DropdownMenu>
  426. )}
  427. </EventUserWrapper>
  428. <EventUserWrapper>
  429. {!defined(primaryUserCount) ? (
  430. <Placeholder height="18px" />
  431. ) : (
  432. <DropdownMenu isNestedDropdown>
  433. {({isOpen, getRootProps, getActorProps, getMenuProps}) => {
  434. const topLevelCx = classNames('dropdown', {
  435. 'anchor-middle': true,
  436. open: isOpen,
  437. });
  438. return (
  439. <span
  440. {...getRootProps({
  441. className: topLevelCx,
  442. })}
  443. >
  444. <span {...getActorProps({})}>
  445. <div className="dropdown-actor-title">
  446. <PrimaryCount value={primaryUserCount} />
  447. {secondaryUserCount !== undefined && useFilteredStats && (
  448. <SecondaryCount dark value={secondaryUserCount} />
  449. )}
  450. </div>
  451. </span>
  452. {useFilteredStats && (
  453. <StyledDropdownList
  454. {...getMenuProps({className: 'dropdown-menu inverted'})}
  455. >
  456. {data.filtered && (
  457. <Fragment>
  458. <StyledMenuItem to={this.getDiscoverUrl(true)}>
  459. <MenuItemText>
  460. {queryFilterDescription ??
  461. t('Matching search filters')}
  462. </MenuItemText>
  463. <MenuItemCount value={data.filtered.userCount} />
  464. </StyledMenuItem>
  465. <MenuItem divider />
  466. </Fragment>
  467. )}
  468. <StyledMenuItem to={this.getDiscoverUrl()}>
  469. <MenuItemText>{t(`Total in ${summary}`)}</MenuItemText>
  470. <MenuItemCount value={data.userCount} />
  471. </StyledMenuItem>
  472. {data.lifetime && (
  473. <Fragment>
  474. <MenuItem divider />
  475. <StyledMenuItem>
  476. <MenuItemText>{t('Since issue began')}</MenuItemText>
  477. <MenuItemCount value={data.lifetime.userCount} />
  478. </StyledMenuItem>
  479. </Fragment>
  480. )}
  481. </StyledDropdownList>
  482. )}
  483. </span>
  484. );
  485. }}
  486. </DropdownMenu>
  487. )}
  488. </EventUserWrapper>
  489. <AssigneeWrapper className="hidden-xs hidden-sm">
  490. <AssigneeSelector
  491. id={data.id}
  492. memberList={memberList}
  493. onAssign={this.trackAssign}
  494. />
  495. </AssigneeWrapper>
  496. </Fragment>
  497. )}
  498. </Wrapper>
  499. );
  500. }
  501. }
  502. export default withPageFilters(withOrganization(StreamGroup));
  503. // Position for wrapper is relative for overlay actions
  504. const Wrapper = styled(PanelItem)<{
  505. actionTaken: boolean;
  506. reviewed: boolean;
  507. unresolved: boolean;
  508. useTintRow: boolean;
  509. }>`
  510. position: relative;
  511. padding: ${space(1.5)} 0;
  512. line-height: 1.1;
  513. ${p =>
  514. p.useTintRow &&
  515. (p.reviewed || !p.unresolved) &&
  516. !p.actionTaken &&
  517. css`
  518. animation: tintRow 0.2s linear forwards;
  519. position: relative;
  520. /*
  521. * A mask that fills the entire row and makes the text opaque. Doing this because
  522. * opacity adds a stacking context in CSS so we need to apply it to another element.
  523. */
  524. &:after {
  525. content: '';
  526. pointer-events: none;
  527. position: absolute;
  528. left: 0;
  529. right: 0;
  530. top: 0;
  531. bottom: 0;
  532. width: 100%;
  533. height: 100%;
  534. background-color: ${p.theme.bodyBackground};
  535. opacity: 0.4;
  536. }
  537. @keyframes tintRow {
  538. 0% {
  539. background-color: ${p.theme.bodyBackground};
  540. }
  541. 100% {
  542. background-color: ${p.theme.backgroundSecondary};
  543. }
  544. }
  545. `};
  546. `;
  547. const GroupSummary = styled('div')<{canSelect: boolean}>`
  548. overflow: hidden;
  549. margin-left: ${p => space(p.canSelect ? 1 : 2)};
  550. margin-right: ${space(1)};
  551. flex: 1;
  552. width: 66.66%;
  553. @media (min-width: ${p => p.theme.breakpoints.medium}) {
  554. width: 50%;
  555. }
  556. `;
  557. const GroupCheckBoxWrapper = styled('div')`
  558. margin-left: ${space(2)};
  559. align-self: flex-start;
  560. height: 15px;
  561. display: flex;
  562. align-items: center;
  563. & input[type='checkbox'] {
  564. margin: 0;
  565. display: block;
  566. }
  567. `;
  568. const primaryStatStyle = (theme: Theme) => css`
  569. font-size: ${theme.fontSizeLarge};
  570. font-variant-numeric: tabular-nums;
  571. `;
  572. const PrimaryCount = styled(Count)`
  573. ${p => primaryStatStyle(p.theme)};
  574. `;
  575. const secondaryStatStyle = (theme: Theme) => css`
  576. font-size: ${theme.fontSizeLarge};
  577. font-variant-numeric: tabular-nums;
  578. :before {
  579. content: '/';
  580. padding-left: ${space(0.25)};
  581. padding-right: 2px;
  582. color: ${theme.gray300};
  583. }
  584. `;
  585. const SecondaryCount = styled(({value, ...p}) => <Count {...p} value={value} />)`
  586. ${p => secondaryStatStyle(p.theme)}
  587. `;
  588. const StyledDropdownList = styled('ul')`
  589. z-index: ${p => p.theme.zIndex.hovercard};
  590. `;
  591. interface MenuItemProps extends React.HTMLAttributes<HTMLDivElement> {
  592. to?: LocationDescriptor;
  593. }
  594. const StyledMenuItem = styled(({to, children, ...p}: MenuItemProps) => (
  595. <MenuItem noAnchor>
  596. {to ? (
  597. // @ts-expect-error allow target _blank for this link to open in new window
  598. <Link to={to} target="_blank">
  599. <div {...p}>{children}</div>
  600. </Link>
  601. ) : (
  602. <div className="dropdown-toggle">
  603. <div {...p}>{children}</div>
  604. </div>
  605. )}
  606. </MenuItem>
  607. ))`
  608. margin: 0;
  609. display: flex;
  610. flex-direction: row;
  611. justify-content: space-between;
  612. `;
  613. const menuItemStatStyles = css`
  614. text-align: right;
  615. font-weight: bold;
  616. font-variant-numeric: tabular-nums;
  617. padding-left: ${space(1)};
  618. `;
  619. const MenuItemCount = styled(({value, ...p}) => (
  620. <div {...p}>
  621. <Count value={value} />
  622. </div>
  623. ))`
  624. ${menuItemStatStyles};
  625. color: ${p => p.theme.subText};
  626. `;
  627. const MenuItemText = styled('div')`
  628. white-space: nowrap;
  629. font-weight: normal;
  630. text-align: left;
  631. padding-right: ${space(1)};
  632. color: ${p => p.theme.textColor};
  633. `;
  634. const ChartWrapper = styled('div')`
  635. width: 200px;
  636. margin: 0 ${space(2)};
  637. align-self: center;
  638. `;
  639. const EventUserWrapper = styled('div')`
  640. display: flex;
  641. justify-content: flex-end;
  642. align-self: center;
  643. width: 60px;
  644. margin: 0 ${space(2)};
  645. @media (min-width: ${p => p.theme.breakpoints.xlarge}) {
  646. width: 80px;
  647. }
  648. `;
  649. const AssigneeWrapper = styled('div')`
  650. width: 80px;
  651. margin: 0 ${space(2)};
  652. align-self: center;
  653. `;
  654. // Reprocessing
  655. const StartedColumn = styled('div')`
  656. align-self: center;
  657. margin: 0 ${space(2)};
  658. color: ${p => p.theme.gray500};
  659. ${p => p.theme.overflowEllipsis};
  660. width: 85px;
  661. @media (min-width: ${p => p.theme.breakpoints.small}) {
  662. display: block;
  663. width: 140px;
  664. }
  665. `;
  666. const EventsReprocessedColumn = styled('div')`
  667. align-self: center;
  668. margin: 0 ${space(2)};
  669. color: ${p => p.theme.gray500};
  670. ${p => p.theme.overflowEllipsis};
  671. width: 75px;
  672. @media (min-width: ${p => p.theme.breakpoints.small}) {
  673. width: 140px;
  674. }
  675. `;
  676. const ProgressColumn = styled('div')`
  677. margin: 0 ${space(2)};
  678. align-self: center;
  679. display: none;
  680. @media (min-width: ${p => p.theme.breakpoints.small}) {
  681. display: block;
  682. width: 160px;
  683. }
  684. `;