content.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. import React from 'react';
  2. import styled from '@emotion/styled';
  3. import {Location} from 'history';
  4. import GuideAnchor from 'app/components/assistant/guideAnchor';
  5. import Button from 'app/components/button';
  6. import Collapsible from 'app/components/collapsible';
  7. import Count from 'app/components/count';
  8. import GlobalSelectionLink from 'app/components/globalSelectionLink';
  9. import ProjectBadge from 'app/components/idBadge/projectBadge';
  10. import NotAvailable from 'app/components/notAvailable';
  11. import {PanelItem} from 'app/components/panels';
  12. import Placeholder from 'app/components/placeholder';
  13. import Tooltip from 'app/components/tooltip';
  14. import {t, tct} from 'app/locale';
  15. import overflowEllipsis from 'app/styles/overflowEllipsis';
  16. import space from 'app/styles/space';
  17. import {Organization, Release, ReleaseProject} from 'app/types';
  18. import {defined} from 'app/utils';
  19. import {getReleaseNewIssuesUrl, getReleaseUnhandledIssuesUrl} from '../../utils';
  20. import {ReleaseHealthRequestRenderProps} from '../../utils/releaseHealthRequest';
  21. import CrashFree from '../crashFree';
  22. import HealthStatsChart from '../healthStatsChart';
  23. import HealthStatsPeriod from '../healthStatsPeriod';
  24. import ReleaseAdoption from '../releaseAdoption';
  25. import {DisplayOption} from '../utils';
  26. import Header from './header';
  27. import ProjectLink from './projectLink';
  28. type Props = {
  29. projects: Array<ReleaseProject>;
  30. releaseVersion: Release['version'];
  31. organization: Organization;
  32. activeDisplay: DisplayOption;
  33. location: Location;
  34. showPlaceholders: boolean;
  35. isTopRelease: boolean;
  36. getHealthData: ReleaseHealthRequestRenderProps['getHealthData'];
  37. };
  38. const Content = ({
  39. projects,
  40. releaseVersion,
  41. location,
  42. organization,
  43. activeDisplay,
  44. showPlaceholders,
  45. isTopRelease,
  46. getHealthData,
  47. }: Props) => {
  48. return (
  49. <React.Fragment>
  50. <Header>
  51. <Layout>
  52. <Column>{t('Project Name')}</Column>
  53. <AdoptionColumn>
  54. <GuideAnchor
  55. target="release_adoption"
  56. position="bottom"
  57. disabled={!(isTopRelease && window.innerWidth >= 800)}
  58. >
  59. {t('Adoption')}
  60. </GuideAnchor>
  61. </AdoptionColumn>
  62. <CrashFreeRateColumn>{t('Crash Free Rate')}</CrashFreeRateColumn>
  63. <CountColumn>
  64. <span>{t('Count')}</span>
  65. <HealthStatsPeriod location={location} />
  66. </CountColumn>
  67. <CrashesColumn>{t('Crashes')}</CrashesColumn>
  68. <NewIssuesColumn>{t('New Issues')}</NewIssuesColumn>
  69. <ViewColumn />
  70. </Layout>
  71. </Header>
  72. <ProjectRows>
  73. <Collapsible
  74. expandButton={({onExpand, numberOfHiddenItems}) => (
  75. <ExpandButtonWrapper>
  76. <Button priority="primary" size="xsmall" onClick={onExpand}>
  77. {tct('Show [numberOfHiddenItems] More', {numberOfHiddenItems})}
  78. </Button>
  79. </ExpandButtonWrapper>
  80. )}
  81. collapseButton={({onCollapse}) => (
  82. <CollapseButtonWrapper>
  83. <Button priority="primary" size="xsmall" onClick={onCollapse}>
  84. {t('Collapse')}
  85. </Button>
  86. </CollapseButtonWrapper>
  87. )}
  88. >
  89. {projects.map((project, index) => {
  90. const {id, slug, newGroups} = project;
  91. const crashCount = getHealthData.getCrashCount(
  92. releaseVersion,
  93. id,
  94. DisplayOption.SESSIONS
  95. );
  96. const crashFreeRate = getHealthData.getCrashFreeRate(
  97. releaseVersion,
  98. id,
  99. activeDisplay
  100. );
  101. const get24hCountByRelease = getHealthData.get24hCountByRelease(
  102. releaseVersion,
  103. id,
  104. activeDisplay
  105. );
  106. const get24hCountByProject = getHealthData.get24hCountByProject(
  107. id,
  108. activeDisplay
  109. );
  110. const timeSeries = getHealthData.getTimeSeries(
  111. releaseVersion,
  112. id,
  113. activeDisplay
  114. );
  115. const adoption = getHealthData.getAdoption(releaseVersion, id, activeDisplay);
  116. // we currently don't support sub-hour session intervals, we rather hide the count histogram than to show only two bars
  117. const hasCountHistogram =
  118. timeSeries?.[0].data.length > 7 &&
  119. timeSeries[0].data.some(item => item.value > 0);
  120. return (
  121. <ProjectRow key={`${releaseVersion}-${slug}-health`}>
  122. <Layout>
  123. <Column>
  124. <ProjectBadge project={project} avatarSize={16} />
  125. </Column>
  126. <AdoptionColumn>
  127. {showPlaceholders ? (
  128. <StyledPlaceholder width="150px" />
  129. ) : get24hCountByProject ? (
  130. <AdoptionWrapper>
  131. <ReleaseAdoption
  132. adoption={adoption ?? 0}
  133. releaseCount={get24hCountByRelease ?? 0}
  134. projectCount={get24hCountByProject ?? 0}
  135. displayOption={activeDisplay}
  136. />
  137. <Count value={get24hCountByRelease ?? 0} />
  138. </AdoptionWrapper>
  139. ) : (
  140. <NotAvailable />
  141. )}
  142. </AdoptionColumn>
  143. <CrashFreeRateColumn>
  144. {showPlaceholders ? (
  145. <StyledPlaceholder width="60px" />
  146. ) : defined(crashFreeRate) ? (
  147. <CrashFree percent={crashFreeRate} />
  148. ) : (
  149. <NotAvailable />
  150. )}
  151. </CrashFreeRateColumn>
  152. <CountColumn>
  153. {showPlaceholders ? (
  154. <StyledPlaceholder />
  155. ) : hasCountHistogram ? (
  156. <ChartWrapper>
  157. <HealthStatsChart
  158. data={timeSeries}
  159. height={20}
  160. activeDisplay={activeDisplay}
  161. />
  162. </ChartWrapper>
  163. ) : (
  164. <NotAvailable />
  165. )}
  166. </CountColumn>
  167. <CrashesColumn>
  168. {showPlaceholders ? (
  169. <StyledPlaceholder width="30px" />
  170. ) : defined(crashCount) ? (
  171. <Tooltip title={t('Open in Issues')}>
  172. <GlobalSelectionLink
  173. to={getReleaseUnhandledIssuesUrl(
  174. organization.slug,
  175. project.id,
  176. releaseVersion
  177. )}
  178. >
  179. <Count value={crashCount} />
  180. </GlobalSelectionLink>
  181. </Tooltip>
  182. ) : (
  183. <NotAvailable />
  184. )}
  185. </CrashesColumn>
  186. <NewIssuesColumn>
  187. <Tooltip title={t('Open in Issues')}>
  188. <GlobalSelectionLink
  189. to={getReleaseNewIssuesUrl(
  190. organization.slug,
  191. project.id,
  192. releaseVersion
  193. )}
  194. >
  195. <Count value={newGroups || 0} />
  196. </GlobalSelectionLink>
  197. </Tooltip>
  198. </NewIssuesColumn>
  199. <ViewColumn>
  200. <GuideAnchor
  201. disabled={!isTopRelease || index !== 0}
  202. target="view_release"
  203. >
  204. <ProjectLink
  205. orgSlug={organization.slug}
  206. project={project}
  207. releaseVersion={releaseVersion}
  208. location={location}
  209. />
  210. </GuideAnchor>
  211. </ViewColumn>
  212. </Layout>
  213. </ProjectRow>
  214. );
  215. })}
  216. </Collapsible>
  217. </ProjectRows>
  218. </React.Fragment>
  219. );
  220. };
  221. export default Content;
  222. const ProjectRows = styled('div')`
  223. position: relative;
  224. `;
  225. const ExpandButtonWrapper = styled('div')`
  226. position: absolute;
  227. width: 100%;
  228. bottom: 0;
  229. display: flex;
  230. align-items: center;
  231. justify-content: center;
  232. background-image: linear-gradient(
  233. 180deg,
  234. hsla(0, 0%, 100%, 0.15) 0,
  235. ${p => p.theme.white}
  236. );
  237. background-repeat: repeat-x;
  238. border-bottom: ${space(1)} solid ${p => p.theme.white};
  239. border-top: ${space(1)} solid transparent;
  240. border-bottom-right-radius: ${p => p.theme.borderRadius};
  241. @media (max-width: ${p => p.theme.breakpoints[1]}) {
  242. border-bottom-left-radius: ${p => p.theme.borderRadius};
  243. }
  244. `;
  245. const CollapseButtonWrapper = styled('div')`
  246. display: flex;
  247. align-items: center;
  248. justify-content: center;
  249. height: 41px;
  250. `;
  251. const ProjectRow = styled(PanelItem)`
  252. padding: ${space(1)} ${space(2)};
  253. @media (min-width: ${p => p.theme.breakpoints[1]}) {
  254. font-size: ${p => p.theme.fontSizeMedium};
  255. }
  256. `;
  257. const Layout = styled('div')`
  258. display: grid;
  259. grid-template-columns: 1fr 1.4fr 0.6fr 0.7fr;
  260. grid-column-gap: ${space(1)};
  261. align-items: center;
  262. width: 100%;
  263. @media (min-width: ${p => p.theme.breakpoints[0]}) {
  264. grid-template-columns: 1fr 1fr 1fr 0.5fr 0.5fr 0.5fr;
  265. }
  266. @media (min-width: ${p => p.theme.breakpoints[1]}) {
  267. grid-template-columns: 1fr 0.8fr 1fr 0.5fr 0.5fr 0.6fr;
  268. }
  269. @media (min-width: ${p => p.theme.breakpoints[3]}) {
  270. grid-template-columns: 1fr 0.8fr 1fr 1fr 0.5fr 0.5fr 0.5fr;
  271. }
  272. `;
  273. const Column = styled('div')`
  274. ${overflowEllipsis};
  275. line-height: 20px;
  276. `;
  277. const NewIssuesColumn = styled(Column)`
  278. @media (min-width: ${p => p.theme.breakpoints[0]}) {
  279. text-align: right;
  280. }
  281. `;
  282. const AdoptionColumn = styled(Column)`
  283. display: none;
  284. @media (min-width: ${p => p.theme.breakpoints[0]}) {
  285. display: flex;
  286. /* Chart tooltips need overflow */
  287. overflow: visible;
  288. }
  289. `;
  290. const AdoptionWrapper = styled('span')`
  291. display: inline-grid;
  292. grid-template-columns: 70px 1fr;
  293. grid-gap: ${space(1)};
  294. align-items: center;
  295. @media (min-width: ${p => p.theme.breakpoints[3]}) {
  296. grid-template-columns: 90px 1fr;
  297. }
  298. `;
  299. const CrashFreeRateColumn = styled(Column)`
  300. @media (min-width: ${p => p.theme.breakpoints[0]}) {
  301. text-align: center;
  302. }
  303. `;
  304. const CountColumn = styled(Column)`
  305. display: none;
  306. @media (min-width: ${p => p.theme.breakpoints[3]}) {
  307. display: flex;
  308. /* Chart tooltips need overflow */
  309. overflow: visible;
  310. }
  311. `;
  312. const CrashesColumn = styled(Column)`
  313. display: none;
  314. @media (min-width: ${p => p.theme.breakpoints[0]}) {
  315. display: block;
  316. text-align: right;
  317. }
  318. `;
  319. const ViewColumn = styled(Column)`
  320. text-align: right;
  321. `;
  322. const ChartWrapper = styled('div')`
  323. flex: 1;
  324. g > .barchart-rect {
  325. background: ${p => p.theme.gray200};
  326. fill: ${p => p.theme.gray200};
  327. }
  328. `;
  329. const StyledPlaceholder = styled(Placeholder)`
  330. height: 15px;
  331. display: inline-block;
  332. position: relative;
  333. top: ${space(0.25)};
  334. `;