index.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  1. import {Fragment} from 'react';
  2. import {browserHistory} from 'react-router';
  3. import {withTheme} from '@emotion/react';
  4. import styled from '@emotion/styled';
  5. import {Location} from 'history';
  6. import round from 'lodash/round';
  7. import ErrorPanel from 'app/components/charts/errorPanel';
  8. import {ChartContainer} from 'app/components/charts/styles';
  9. import TransitionChart from 'app/components/charts/transitionChart';
  10. import TransparentLoadingMask from 'app/components/charts/transparentLoadingMask';
  11. import Count from 'app/components/count';
  12. import NotAvailable from 'app/components/notAvailable';
  13. import {Panel, PanelTable} from 'app/components/panels';
  14. import Placeholder from 'app/components/placeholder';
  15. import Radio from 'app/components/radio';
  16. import {PlatformKey} from 'app/data/platformCategories';
  17. import {IconArrow, IconWarning} from 'app/icons';
  18. import {t} from 'app/locale';
  19. import overflowEllipsis from 'app/styles/overflowEllipsis';
  20. import space from 'app/styles/space';
  21. import {
  22. ReleaseComparisonChartType,
  23. ReleaseProject,
  24. ReleaseWithHealth,
  25. SessionApiResponse,
  26. SessionField,
  27. } from 'app/types';
  28. import {defined, percent} from 'app/utils';
  29. import {decodeScalar} from 'app/utils/queryString';
  30. import {getCount, getCrashFreeRate, getCrashFreeSeries} from 'app/utils/sessions';
  31. import {Color, Theme} from 'app/utils/theme';
  32. import {displayCrashFreeDiff, displayCrashFreePercent} from 'app/views/releases/utils';
  33. import {generateReleaseMarkLines, releaseComparisonChartLabels} from '../../utils';
  34. import {
  35. fillChartDataFromSessionsResponse,
  36. initSessionsBreakdownChartData,
  37. } from '../chart/utils';
  38. import SessionsChart from './sessionsChart';
  39. type ComparisonRow = {
  40. type: ReleaseComparisonChartType;
  41. thisRelease: React.ReactNode;
  42. allReleases: React.ReactNode;
  43. diff: React.ReactNode;
  44. diffDirection: 'up' | 'down' | null;
  45. diffColor: Color | null;
  46. };
  47. type Props = {
  48. release: ReleaseWithHealth;
  49. project: ReleaseProject;
  50. releaseSessions: SessionApiResponse | null;
  51. allSessions: SessionApiResponse | null;
  52. platform: PlatformKey;
  53. location: Location;
  54. loading: boolean;
  55. reloading: boolean;
  56. errored: boolean;
  57. theme: Theme;
  58. };
  59. function ReleaseComparisonChart({
  60. release,
  61. project,
  62. releaseSessions,
  63. allSessions,
  64. platform,
  65. location,
  66. loading,
  67. reloading,
  68. errored,
  69. theme,
  70. }: Props) {
  71. const activeChart = decodeScalar(
  72. location.query.chart,
  73. ReleaseComparisonChartType.CRASH_FREE_SESSIONS
  74. ) as ReleaseComparisonChartType;
  75. const releaseCrashFreeSessions = getCrashFreeRate(
  76. releaseSessions?.groups,
  77. SessionField.SESSIONS
  78. );
  79. const allCrashFreeSessions = getCrashFreeRate(
  80. allSessions?.groups,
  81. SessionField.SESSIONS
  82. );
  83. const diffCrashFreeSessions =
  84. defined(releaseCrashFreeSessions) && defined(allCrashFreeSessions)
  85. ? releaseCrashFreeSessions - allCrashFreeSessions
  86. : null;
  87. const releaseCrashFreeUsers = getCrashFreeRate(
  88. releaseSessions?.groups,
  89. SessionField.USERS
  90. );
  91. const allCrashFreeUsers = getCrashFreeRate(allSessions?.groups, SessionField.USERS);
  92. const diffCrashFreeUsers =
  93. defined(releaseCrashFreeUsers) && defined(allCrashFreeUsers)
  94. ? releaseCrashFreeUsers - allCrashFreeUsers
  95. : null;
  96. const releaseSessionsCount = getCount(releaseSessions?.groups, SessionField.SESSIONS);
  97. const allSessionsCount = getCount(allSessions?.groups, SessionField.SESSIONS);
  98. const diffSessionsCount =
  99. defined(releaseSessions) && defined(allSessions)
  100. ? percent(releaseSessionsCount - allSessionsCount, allSessionsCount)
  101. : null;
  102. const releaseUsersCount = getCount(releaseSessions?.groups, SessionField.USERS);
  103. const allUsersCount = getCount(allSessions?.groups, SessionField.USERS);
  104. const diffUsersCount =
  105. defined(releaseUsersCount) && defined(allUsersCount)
  106. ? percent(releaseUsersCount - allUsersCount, allUsersCount)
  107. : null;
  108. const charts: ComparisonRow[] = [
  109. {
  110. type: ReleaseComparisonChartType.CRASH_FREE_SESSIONS,
  111. thisRelease: defined(releaseCrashFreeSessions)
  112. ? displayCrashFreePercent(releaseCrashFreeSessions)
  113. : null,
  114. allReleases: defined(allCrashFreeSessions)
  115. ? displayCrashFreePercent(allCrashFreeSessions)
  116. : null,
  117. diff: defined(diffCrashFreeSessions)
  118. ? displayCrashFreeDiff(diffCrashFreeSessions, releaseCrashFreeSessions)
  119. : null,
  120. diffDirection: diffCrashFreeSessions
  121. ? diffCrashFreeSessions > 0
  122. ? 'up'
  123. : 'down'
  124. : null,
  125. diffColor: diffCrashFreeSessions
  126. ? diffCrashFreeSessions > 0
  127. ? 'green300'
  128. : 'red300'
  129. : null,
  130. },
  131. {
  132. type: ReleaseComparisonChartType.CRASH_FREE_USERS,
  133. thisRelease: defined(releaseCrashFreeUsers)
  134. ? displayCrashFreePercent(releaseCrashFreeUsers)
  135. : null,
  136. allReleases: defined(allCrashFreeUsers)
  137. ? displayCrashFreePercent(allCrashFreeUsers)
  138. : null,
  139. diff: defined(diffCrashFreeUsers)
  140. ? displayCrashFreeDiff(diffCrashFreeUsers, releaseCrashFreeUsers)
  141. : null,
  142. diffDirection: diffCrashFreeUsers ? (diffCrashFreeUsers > 0 ? 'up' : 'down') : null,
  143. diffColor: diffCrashFreeUsers
  144. ? diffCrashFreeUsers > 0
  145. ? 'green300'
  146. : 'red300'
  147. : null,
  148. },
  149. {
  150. type: ReleaseComparisonChartType.SESSION_COUNT,
  151. thisRelease: defined(releaseSessionsCount) ? (
  152. <Count value={releaseSessionsCount} />
  153. ) : null,
  154. allReleases: defined(allSessionsCount) ? <Count value={allSessionsCount} /> : null,
  155. diff: defined(diffSessionsCount)
  156. ? `${Math.abs(round(diffSessionsCount, 0))}%`
  157. : null,
  158. diffDirection: defined(diffSessionsCount)
  159. ? diffSessionsCount > 0
  160. ? 'up'
  161. : 'down'
  162. : null,
  163. diffColor: null,
  164. },
  165. {
  166. type: ReleaseComparisonChartType.USER_COUNT,
  167. thisRelease: defined(releaseUsersCount) ? (
  168. <Count value={releaseUsersCount} />
  169. ) : null,
  170. allReleases: defined(allUsersCount) ? <Count value={allUsersCount} /> : null,
  171. diff: defined(diffUsersCount) ? `${Math.abs(round(diffUsersCount, 0))}%` : null,
  172. diffDirection: defined(diffUsersCount)
  173. ? diffUsersCount > 0
  174. ? 'up'
  175. : 'down'
  176. : null,
  177. diffColor: null,
  178. },
  179. ];
  180. function getSeries(chartType: ReleaseComparisonChartType) {
  181. if (!releaseSessions) {
  182. return {};
  183. }
  184. const markLines = generateReleaseMarkLines(release, project.slug, theme);
  185. switch (chartType) {
  186. case ReleaseComparisonChartType.CRASH_FREE_SESSIONS:
  187. return {
  188. series: [
  189. {
  190. seriesName: t('This Release'),
  191. connectNulls: true,
  192. data: getCrashFreeSeries(
  193. releaseSessions?.groups,
  194. releaseSessions?.intervals,
  195. SessionField.SESSIONS
  196. ),
  197. },
  198. ],
  199. previousSeries: [
  200. {
  201. seriesName: t('All Releases'),
  202. data: getCrashFreeSeries(
  203. allSessions?.groups,
  204. allSessions?.intervals,
  205. SessionField.SESSIONS
  206. ),
  207. },
  208. ],
  209. markLines,
  210. };
  211. case ReleaseComparisonChartType.CRASH_FREE_USERS:
  212. return {
  213. series: [
  214. {
  215. seriesName: t('This Release'),
  216. connectNulls: true,
  217. data: getCrashFreeSeries(
  218. releaseSessions?.groups,
  219. releaseSessions?.intervals,
  220. SessionField.USERS
  221. ),
  222. },
  223. ],
  224. previousSeries: [
  225. {
  226. seriesName: t('All Releases'),
  227. data: getCrashFreeSeries(
  228. allSessions?.groups,
  229. allSessions?.intervals,
  230. SessionField.USERS
  231. ),
  232. },
  233. ],
  234. markLines,
  235. };
  236. case ReleaseComparisonChartType.SESSION_COUNT:
  237. return {
  238. series: Object.values(
  239. fillChartDataFromSessionsResponse({
  240. response: releaseSessions,
  241. field: SessionField.SESSIONS,
  242. groupBy: 'session.status',
  243. chartData: initSessionsBreakdownChartData(),
  244. })
  245. ),
  246. markLines,
  247. };
  248. case ReleaseComparisonChartType.USER_COUNT:
  249. return {
  250. series: Object.values(
  251. fillChartDataFromSessionsResponse({
  252. response: releaseSessions,
  253. field: SessionField.USERS,
  254. groupBy: 'session.status',
  255. chartData: initSessionsBreakdownChartData(),
  256. })
  257. ),
  258. markLines,
  259. };
  260. default:
  261. return {};
  262. }
  263. }
  264. function handleChartChange(chartType: ReleaseComparisonChartType) {
  265. browserHistory.push({
  266. ...location,
  267. query: {
  268. ...location.query,
  269. chart: chartType,
  270. },
  271. });
  272. }
  273. const {series, previousSeries, markLines} = getSeries(activeChart);
  274. const chart = charts.find(ch => ch.type === activeChart);
  275. if (errored || !chart) {
  276. return (
  277. <Panel>
  278. <ErrorPanel>
  279. <IconWarning color="gray300" size="lg" />
  280. </ErrorPanel>
  281. </Panel>
  282. );
  283. }
  284. return (
  285. <Fragment>
  286. <ChartPanel>
  287. <ChartContainer>
  288. <TransitionChart loading={loading} reloading={reloading}>
  289. <TransparentLoadingMask visible={reloading} />
  290. <SessionsChart
  291. series={[...(series ?? []), ...(markLines ?? [])]}
  292. previousSeries={previousSeries ?? []}
  293. chartType={activeChart}
  294. platform={platform}
  295. value={chart.thisRelease}
  296. diff={
  297. <Change color={defined(chart.diffColor) ? chart.diffColor : undefined}>
  298. {chart.diff}{' '}
  299. {defined(chart.diffDirection) && (
  300. <IconArrow direction={chart.diffDirection} size="xs" />
  301. )}
  302. </Change>
  303. }
  304. />
  305. </TransitionChart>
  306. </ChartContainer>
  307. </ChartPanel>
  308. <ChartTable
  309. headers={[
  310. <Cell key="stability" align="left">
  311. {t('Stability')}
  312. </Cell>,
  313. <Cell key="releases" align="right">
  314. {t('All Releases')}
  315. </Cell>,
  316. <Cell key="release" align="right">
  317. {t('This Release')}
  318. </Cell>,
  319. <Cell key="change" align="right">
  320. {t('Change')}
  321. </Cell>,
  322. ]}
  323. >
  324. {charts.map(
  325. ({type, thisRelease, allReleases, diff, diffDirection, diffColor}) => {
  326. return (
  327. <Fragment key={type}>
  328. <Cell align="left">
  329. <ChartToggle htmlFor={type}>
  330. <Radio
  331. id={type}
  332. disabled={false}
  333. checked={type === activeChart}
  334. onChange={() => handleChartChange(type)}
  335. />
  336. {releaseComparisonChartLabels[type]}
  337. </ChartToggle>
  338. </Cell>
  339. <Cell align="right">
  340. {loading ? <Placeholder height="20px" /> : allReleases}
  341. </Cell>
  342. <Cell align="right">
  343. {loading ? <Placeholder height="20px" /> : thisRelease}
  344. </Cell>
  345. <Cell align="right">
  346. {loading ? (
  347. <Placeholder height="20px" />
  348. ) : defined(diff) ? (
  349. <Change color={defined(diffColor) ? diffColor : undefined}>
  350. {defined(diffDirection) && (
  351. <IconArrow direction={diffDirection} size="xs" />
  352. )}{' '}
  353. {diff}
  354. </Change>
  355. ) : (
  356. <NotAvailable />
  357. )}
  358. </Cell>
  359. </Fragment>
  360. );
  361. }
  362. )}
  363. </ChartTable>
  364. </Fragment>
  365. );
  366. }
  367. const ChartPanel = styled(Panel)`
  368. margin-bottom: 0;
  369. border-bottom-left-radius: 0;
  370. border-bottom: none;
  371. border-bottom-right-radius: 0;
  372. `;
  373. const ChartTable = styled(PanelTable)`
  374. border-top-left-radius: 0;
  375. border-top-right-radius: 0;
  376. @media (max-width: ${p => p.theme.breakpoints[2]}) {
  377. grid-template-columns: min-content 1fr 1fr 1fr;
  378. }
  379. `;
  380. const Cell = styled('div')<{align: 'left' | 'right'}>`
  381. text-align: ${p => p.align};
  382. ${overflowEllipsis}
  383. `;
  384. const ChartToggle = styled('label')`
  385. display: flex;
  386. align-items: center;
  387. font-weight: 400;
  388. margin-bottom: 0;
  389. input {
  390. flex-shrink: 0;
  391. margin-right: ${space(1)} !important;
  392. &:hover {
  393. cursor: pointer;
  394. }
  395. }
  396. &:hover {
  397. cursor: pointer;
  398. }
  399. `;
  400. const Change = styled('div')<{color?: Color}>`
  401. font-size: ${p => p.theme.fontSizeLarge};
  402. ${p => p.color && `color: ${p.theme[p.color]}`}
  403. `;
  404. export default withTheme(ReleaseComparisonChart);