performanceCardTable.tsx 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821
  1. import {Fragment} from 'react';
  2. import * as React from 'react';
  3. import {Link} from 'react-router';
  4. import {css} from '@emotion/react';
  5. import styled from '@emotion/styled';
  6. import {Location} from 'history';
  7. import Alert from 'sentry/components/alert';
  8. import AsyncComponent from 'sentry/components/asyncComponent';
  9. import LoadingIndicator from 'sentry/components/loadingIndicator';
  10. import NotAvailable from 'sentry/components/notAvailable';
  11. import {PanelItem} from 'sentry/components/panels';
  12. import PanelTable from 'sentry/components/panels/panelTable';
  13. import {IconArrow, IconWarning} from 'sentry/icons';
  14. import {t, tct} from 'sentry/locale';
  15. import overflowEllipsis from 'sentry/styles/overflowEllipsis';
  16. import space from 'sentry/styles/space';
  17. import {Organization, ReleaseProject} from 'sentry/types';
  18. import DiscoverQuery, {TableData} from 'sentry/utils/discover/discoverQuery';
  19. import EventView from 'sentry/utils/discover/eventView';
  20. import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers';
  21. import {MobileVital, WebVital} from 'sentry/utils/discover/fields';
  22. import {
  23. MOBILE_VITAL_DETAILS,
  24. WEB_VITAL_DETAILS,
  25. } from 'sentry/utils/performance/vitals/constants';
  26. import {PROJECT_PERFORMANCE_TYPE} from 'sentry/views/performance/utils';
  27. type PerformanceCardTableProps = {
  28. allReleasesEventView: EventView;
  29. allReleasesTableData: TableData | null;
  30. isLoading: boolean;
  31. location: Location;
  32. organization: Organization;
  33. performanceType: string;
  34. project: ReleaseProject;
  35. releaseEventView: EventView;
  36. thisReleaseTableData: TableData | null;
  37. };
  38. function PerformanceCardTable({
  39. organization,
  40. location,
  41. project,
  42. releaseEventView,
  43. allReleasesTableData,
  44. thisReleaseTableData,
  45. performanceType,
  46. isLoading,
  47. }: PerformanceCardTableProps) {
  48. const miseryRenderer =
  49. allReleasesTableData?.meta &&
  50. getFieldRenderer('user_misery', allReleasesTableData.meta);
  51. function renderChange(
  52. allReleasesScore: number,
  53. thisReleaseScore: number,
  54. meta: string
  55. ) {
  56. if (allReleasesScore === undefined || thisReleaseScore === undefined) {
  57. return <StyledNotAvailable />;
  58. }
  59. const trend = allReleasesScore - thisReleaseScore;
  60. const trendSeconds = trend >= 1000 ? trend / 1000 : trend;
  61. const trendPercentage = (allReleasesScore - thisReleaseScore) * 100;
  62. const valPercentage = Math.round(Math.abs(trendPercentage));
  63. const val = Math.abs(trendSeconds).toFixed(2);
  64. if (trend === 0) {
  65. return <SubText>{`0${meta === 'duration' ? 'ms' : '%'}`}</SubText>;
  66. }
  67. return (
  68. <TrendText color={trend >= 0 ? 'success' : 'error'}>
  69. {`${meta === 'duration' ? val : valPercentage}${
  70. meta === 'duration' ? (trend >= 1000 ? 's' : 'ms') : '%'
  71. }`}
  72. <StyledIconArrow
  73. color={trend >= 0 ? 'success' : 'error'}
  74. direction={trend >= 0 ? 'down' : 'up'}
  75. size="xs"
  76. />
  77. </TrendText>
  78. );
  79. }
  80. function userMiseryTrend() {
  81. const allReleasesUserMisery = allReleasesTableData?.data?.[0]?.user_misery;
  82. const thisReleaseUserMisery = thisReleaseTableData?.data?.[0]?.user_misery;
  83. return (
  84. <StyledPanelItem>
  85. {renderChange(
  86. allReleasesUserMisery as number,
  87. thisReleaseUserMisery as number,
  88. 'number' as string
  89. )}
  90. </StyledPanelItem>
  91. );
  92. }
  93. function renderFrontendPerformance() {
  94. const webVitals = [
  95. {title: WebVital.FCP, field: 'p75_measurements_fcp'},
  96. {title: WebVital.FID, field: 'p75_measurements_fid'},
  97. {title: WebVital.LCP, field: 'p75_measurements_lcp'},
  98. {title: WebVital.CLS, field: 'p75_measurements_cls'},
  99. ];
  100. const spans = [
  101. {title: 'HTTP', column: 'p75(spans.http)', field: 'p75_spans_http'},
  102. {title: 'Browser', column: 'p75(spans.browser)', field: 'p75_spans_browser'},
  103. {title: 'Resource', column: 'p75(spans.resource)', field: 'p75_spans_resource'},
  104. ];
  105. const webVitalTitles = webVitals.map((vital, idx) => {
  106. const newView = releaseEventView.withColumns([
  107. {kind: 'field', field: `p75(${vital.title})`},
  108. ]);
  109. return (
  110. <SubTitle key={idx}>
  111. <Link to={newView.getResultsViewUrlTarget(organization.slug)}>
  112. {WEB_VITAL_DETAILS[vital.title].name} (
  113. {WEB_VITAL_DETAILS[vital.title].acronym})
  114. </Link>
  115. </SubTitle>
  116. );
  117. });
  118. const spanTitles = spans.map((span, idx) => {
  119. const newView = releaseEventView.withColumns([
  120. {kind: 'field', field: `${span.column}`},
  121. ]);
  122. return (
  123. <SubTitle key={idx}>
  124. <Link to={newView.getResultsViewUrlTarget(organization.slug)}>
  125. {t(span.title)}
  126. </Link>
  127. </SubTitle>
  128. );
  129. });
  130. const webVitalsRenderer = webVitals.map(
  131. vital =>
  132. allReleasesTableData?.meta &&
  133. getFieldRenderer(vital.field, allReleasesTableData?.meta)
  134. );
  135. const spansRenderer = spans.map(
  136. span =>
  137. allReleasesTableData?.meta &&
  138. getFieldRenderer(span.field, allReleasesTableData?.meta)
  139. );
  140. const webReleaseTrend = webVitals.map(vital => {
  141. return {
  142. allReleasesRow: {
  143. data: allReleasesTableData?.data?.[0]?.[vital.field],
  144. meta: allReleasesTableData?.meta?.[vital.field],
  145. },
  146. thisReleaseRow: {
  147. data: thisReleaseTableData?.data?.[0]?.[vital.field],
  148. meta: thisReleaseTableData?.meta?.[vital.field],
  149. },
  150. };
  151. });
  152. const spansReleaseTrend = spans.map(span => {
  153. return {
  154. allReleasesRow: {
  155. data: allReleasesTableData?.data?.[0]?.[span.field],
  156. meta: allReleasesTableData?.meta?.[span.field],
  157. },
  158. thisReleaseRow: {
  159. data: thisReleaseTableData?.data?.[0]?.[span.field],
  160. meta: thisReleaseTableData?.meta?.[span.field],
  161. },
  162. };
  163. });
  164. const emptyColumn = (
  165. <div>
  166. <SingleEmptySubText>
  167. <StyledNotAvailable tooltip={t('No results found')} />
  168. </SingleEmptySubText>
  169. <StyledPanelItem>
  170. <TitleSpace />
  171. {webVitals.map((vital, index) => (
  172. <MultipleEmptySubText key={vital[index]}>
  173. {<StyledNotAvailable tooltip={t('No results found')} />}
  174. </MultipleEmptySubText>
  175. ))}
  176. </StyledPanelItem>
  177. <StyledPanelItem>
  178. <TitleSpace />
  179. {spans.map((span, index) => (
  180. <MultipleEmptySubText key={span[index]}>
  181. {<StyledNotAvailable tooltip={t('No results found')} />}
  182. </MultipleEmptySubText>
  183. ))}
  184. </StyledPanelItem>
  185. </div>
  186. );
  187. return (
  188. <Fragment>
  189. <div>
  190. <PanelItem>{t('User Misery')}</PanelItem>
  191. <StyledPanelItem>
  192. <div>{t('Web Vitals')}</div>
  193. {webVitalTitles}
  194. </StyledPanelItem>
  195. <StyledPanelItem>
  196. <div>{t('Span Operations')}</div>
  197. {spanTitles}
  198. </StyledPanelItem>
  199. </div>
  200. {allReleasesTableData?.data.length === 0
  201. ? emptyColumn
  202. : allReleasesTableData?.data.map((dataRow, idx) => {
  203. const allReleasesMisery = miseryRenderer?.(dataRow, {
  204. organization,
  205. location,
  206. });
  207. const allReleasesWebVitals = webVitalsRenderer?.map(renderer =>
  208. renderer?.(dataRow, {organization, location})
  209. );
  210. const allReleasesSpans = spansRenderer?.map(renderer =>
  211. renderer?.(dataRow, {organization, location})
  212. );
  213. return (
  214. <div key={idx}>
  215. <UserMiseryPanelItem>{allReleasesMisery}</UserMiseryPanelItem>
  216. <StyledPanelItem>
  217. <TitleSpace />
  218. {allReleasesWebVitals.map(webVital => webVital)}
  219. </StyledPanelItem>
  220. <StyledPanelItem>
  221. <TitleSpace />
  222. {allReleasesSpans.map(span => span)}
  223. </StyledPanelItem>
  224. </div>
  225. );
  226. })}
  227. {thisReleaseTableData?.data.length === 0
  228. ? emptyColumn
  229. : thisReleaseTableData?.data.map((dataRow, idx) => {
  230. const thisReleasesMisery = miseryRenderer?.(dataRow, {
  231. organization,
  232. location,
  233. });
  234. const thisReleasesWebVitals = webVitalsRenderer?.map(renderer =>
  235. renderer?.(dataRow, {organization, location})
  236. );
  237. const thisReleasesSpans = spansRenderer?.map(renderer =>
  238. renderer?.(dataRow, {organization, location})
  239. );
  240. return (
  241. <div key={idx}>
  242. <div>
  243. <UserMiseryPanelItem>{thisReleasesMisery}</UserMiseryPanelItem>
  244. <StyledPanelItem>
  245. <TitleSpace />
  246. {thisReleasesWebVitals.map(webVital => webVital)}
  247. </StyledPanelItem>
  248. <StyledPanelItem>
  249. <TitleSpace />
  250. {thisReleasesSpans.map(span => span)}
  251. </StyledPanelItem>
  252. </div>
  253. </div>
  254. );
  255. })}
  256. <div>
  257. {userMiseryTrend()}
  258. <StyledPanelItem>
  259. <TitleSpace />
  260. {webReleaseTrend?.map(row =>
  261. renderChange(
  262. row.allReleasesRow?.data as number,
  263. row.thisReleaseRow?.data as number,
  264. row.allReleasesRow?.meta as string
  265. )
  266. )}
  267. </StyledPanelItem>
  268. <StyledPanelItem>
  269. <TitleSpace />
  270. {spansReleaseTrend?.map(row =>
  271. renderChange(
  272. row.allReleasesRow?.data as number,
  273. row.thisReleaseRow?.data as number,
  274. row.allReleasesRow?.meta as string
  275. )
  276. )}
  277. </StyledPanelItem>
  278. </div>
  279. </Fragment>
  280. );
  281. }
  282. function renderBackendPerformance() {
  283. const spans = [
  284. {title: 'HTTP', column: 'p75(spans.http)', field: 'p75_spans_http'},
  285. {title: 'DB', column: 'p75(spans.db)', field: 'p75_spans_db'},
  286. ];
  287. const spanTitles = spans.map((span, idx) => {
  288. const newView = releaseEventView.withColumns([
  289. {kind: 'field', field: `${span.column}`},
  290. ]);
  291. return (
  292. <SubTitle key={idx}>
  293. <Link to={newView.getResultsViewUrlTarget(organization.slug)}>
  294. {t(span.title)}
  295. </Link>
  296. </SubTitle>
  297. );
  298. });
  299. const apdexRenderer =
  300. allReleasesTableData?.meta && getFieldRenderer('apdex', allReleasesTableData.meta);
  301. const spansRenderer = spans.map(
  302. span =>
  303. allReleasesTableData?.meta &&
  304. getFieldRenderer(span.field, allReleasesTableData?.meta)
  305. );
  306. const spansReleaseTrend = spans.map(span => {
  307. return {
  308. allReleasesRow: {
  309. data: allReleasesTableData?.data?.[0]?.[span.field],
  310. meta: allReleasesTableData?.meta?.[span.field],
  311. },
  312. thisReleaseRow: {
  313. data: thisReleaseTableData?.data?.[0]?.[span.field],
  314. meta: thisReleaseTableData?.meta?.[span.field],
  315. },
  316. };
  317. });
  318. function apdexTrend() {
  319. const allReleasesApdex = allReleasesTableData?.data?.[0]?.apdex;
  320. const thisReleaseApdex = thisReleaseTableData?.data?.[0]?.apdex;
  321. return (
  322. <StyledPanelItem>
  323. {renderChange(
  324. allReleasesApdex as number,
  325. thisReleaseApdex as number,
  326. 'string' as string
  327. )}
  328. </StyledPanelItem>
  329. );
  330. }
  331. const emptyColumn = (
  332. <div>
  333. <SingleEmptySubText>
  334. <StyledNotAvailable tooltip={t('No results found')} />
  335. </SingleEmptySubText>
  336. <SingleEmptySubText>
  337. <StyledNotAvailable tooltip={t('No results found')} />
  338. </SingleEmptySubText>
  339. <StyledPanelItem>
  340. <TitleSpace />
  341. {spans.map((span, index) => (
  342. <MultipleEmptySubText key={span[index]}>
  343. {<StyledNotAvailable tooltip={t('No results found')} />}
  344. </MultipleEmptySubText>
  345. ))}
  346. </StyledPanelItem>
  347. </div>
  348. );
  349. return (
  350. <Fragment>
  351. <div>
  352. <PanelItem>{t('User Misery')}</PanelItem>
  353. <StyledPanelItem>
  354. <div>{t('Apdex')}</div>
  355. </StyledPanelItem>
  356. <StyledPanelItem>
  357. <div>{t('Span Operations')}</div>
  358. {spanTitles}
  359. </StyledPanelItem>
  360. </div>
  361. {allReleasesTableData?.data.length === 0
  362. ? emptyColumn
  363. : allReleasesTableData?.data.map((dataRow, idx) => {
  364. const allReleasesMisery = miseryRenderer?.(dataRow, {
  365. organization,
  366. location,
  367. });
  368. const allReleasesApdex = apdexRenderer?.(dataRow, {organization, location});
  369. const allReleasesSpans = spansRenderer?.map(renderer =>
  370. renderer?.(dataRow, {organization, location})
  371. );
  372. return (
  373. <div key={idx}>
  374. <UserMiseryPanelItem>{allReleasesMisery}</UserMiseryPanelItem>
  375. <ApdexPanelItem>{allReleasesApdex}</ApdexPanelItem>
  376. <StyledPanelItem>
  377. <TitleSpace />
  378. {allReleasesSpans.map(span => span)}
  379. </StyledPanelItem>
  380. </div>
  381. );
  382. })}
  383. {thisReleaseTableData?.data.length === 0
  384. ? emptyColumn
  385. : thisReleaseTableData?.data.map((dataRow, idx) => {
  386. const thisReleasesMisery = miseryRenderer?.(dataRow, {
  387. organization,
  388. location,
  389. });
  390. const thisReleasesApdex = apdexRenderer?.(dataRow, {
  391. organization,
  392. location,
  393. });
  394. const thisReleasesSpans = spansRenderer?.map(renderer =>
  395. renderer?.(dataRow, {organization, location})
  396. );
  397. return (
  398. <div key={idx}>
  399. <UserMiseryPanelItem>{thisReleasesMisery}</UserMiseryPanelItem>
  400. <ApdexPanelItem>{thisReleasesApdex}</ApdexPanelItem>
  401. <StyledPanelItem>
  402. <TitleSpace />
  403. {thisReleasesSpans.map(span => span)}
  404. </StyledPanelItem>
  405. </div>
  406. );
  407. })}
  408. <div>
  409. {userMiseryTrend()}
  410. {apdexTrend()}
  411. <StyledPanelItem>
  412. <TitleSpace />
  413. {spansReleaseTrend?.map(row =>
  414. renderChange(
  415. row.allReleasesRow?.data as number,
  416. row.thisReleaseRow?.data as number,
  417. row.allReleasesRow?.meta as string
  418. )
  419. )}
  420. </StyledPanelItem>
  421. </div>
  422. </Fragment>
  423. );
  424. }
  425. function renderMobilePerformance() {
  426. const mobileVitals = [
  427. MobileVital.AppStartCold,
  428. MobileVital.AppStartWarm,
  429. MobileVital.FramesSlow,
  430. MobileVital.FramesFrozen,
  431. ];
  432. const mobileVitalTitles = mobileVitals.map(mobileVital => {
  433. return (
  434. <PanelItem key={mobileVital}>{MOBILE_VITAL_DETAILS[mobileVital].name}</PanelItem>
  435. );
  436. });
  437. const mobileVitalFields = [
  438. 'p75_measurements_app_start_cold',
  439. 'p75_measurements_app_start_warm',
  440. 'p75_measurements_frames_slow',
  441. 'p75_measurements_frames_frozen',
  442. ];
  443. const mobileVitalsRenderer = mobileVitalFields.map(
  444. field =>
  445. allReleasesTableData?.meta && getFieldRenderer(field, allReleasesTableData?.meta)
  446. );
  447. const mobileReleaseTrend = mobileVitalFields.map(field => {
  448. return {
  449. allReleasesRow: {
  450. data: allReleasesTableData?.data?.[0]?.[field],
  451. meta: allReleasesTableData?.meta?.[field],
  452. },
  453. thisReleaseRow: {
  454. data: thisReleaseTableData?.data?.[0]?.[field],
  455. meta: thisReleaseTableData?.meta?.[field],
  456. },
  457. };
  458. });
  459. const emptyColumn = (
  460. <div>
  461. <SingleEmptySubText>
  462. <StyledNotAvailable tooltip={t('No results found')} />
  463. </SingleEmptySubText>
  464. {mobileVitalFields.map((vital, index) => (
  465. <SingleEmptySubText key={vital[index]}>
  466. <StyledNotAvailable tooltip={t('No results found')} />
  467. </SingleEmptySubText>
  468. ))}
  469. </div>
  470. );
  471. return (
  472. <Fragment>
  473. <div>
  474. <PanelItem>{t('User Misery')}</PanelItem>
  475. {mobileVitalTitles}
  476. </div>
  477. {allReleasesTableData?.data.length === 0
  478. ? emptyColumn
  479. : allReleasesTableData?.data.map((dataRow, idx) => {
  480. const allReleasesMisery = miseryRenderer?.(dataRow, {
  481. organization,
  482. location,
  483. });
  484. const allReleasesMobile = mobileVitalsRenderer?.map(renderer =>
  485. renderer?.(dataRow, {organization, location})
  486. );
  487. return (
  488. <div key={idx}>
  489. <UserMiseryPanelItem>{allReleasesMisery}</UserMiseryPanelItem>
  490. {allReleasesMobile.map((mobileVital, i) => (
  491. <StyledPanelItem key={i}>{mobileVital}</StyledPanelItem>
  492. ))}
  493. </div>
  494. );
  495. })}
  496. {thisReleaseTableData?.data.length === 0
  497. ? emptyColumn
  498. : thisReleaseTableData?.data.map((dataRow, idx) => {
  499. const thisReleasesMisery = miseryRenderer?.(dataRow, {
  500. organization,
  501. location,
  502. });
  503. const thisReleasesMobile = mobileVitalsRenderer?.map(renderer =>
  504. renderer?.(dataRow, {organization, location})
  505. );
  506. return (
  507. <div key={idx}>
  508. <UserMiseryPanelItem>{thisReleasesMisery}</UserMiseryPanelItem>
  509. {thisReleasesMobile.map((mobileVital, i) => (
  510. <StyledPanelItem key={i}>{mobileVital}</StyledPanelItem>
  511. ))}
  512. </div>
  513. );
  514. })}
  515. <div>
  516. {userMiseryTrend()}
  517. {mobileReleaseTrend?.map((row, idx) => (
  518. <StyledPanelItem key={idx}>
  519. {renderChange(
  520. row.allReleasesRow?.data as number,
  521. row.thisReleaseRow?.data as number,
  522. row.allReleasesRow?.meta as string
  523. )}
  524. </StyledPanelItem>
  525. ))}
  526. </div>
  527. </Fragment>
  528. );
  529. }
  530. function renderUnknownPerformance() {
  531. const emptyColumn = (
  532. <div>
  533. <SingleEmptySubText>
  534. <StyledNotAvailable tooltip={t('No results found')} />
  535. </SingleEmptySubText>
  536. </div>
  537. );
  538. return (
  539. <Fragment>
  540. <div>
  541. <PanelItem>{t('User Misery')}</PanelItem>
  542. </div>
  543. {allReleasesTableData?.data.length === 0
  544. ? emptyColumn
  545. : allReleasesTableData?.data.map((dataRow, idx) => {
  546. const allReleasesMisery = miseryRenderer?.(dataRow, {
  547. organization,
  548. location,
  549. });
  550. return (
  551. <div key={idx}>
  552. <UserMiseryPanelItem>{allReleasesMisery}</UserMiseryPanelItem>
  553. </div>
  554. );
  555. })}
  556. {thisReleaseTableData?.data.length === 0
  557. ? emptyColumn
  558. : thisReleaseTableData?.data.map((dataRow, idx) => {
  559. const thisReleasesMisery = miseryRenderer?.(dataRow, {
  560. organization,
  561. location,
  562. });
  563. return (
  564. <div key={idx}>
  565. <UserMiseryPanelItem>{thisReleasesMisery}</UserMiseryPanelItem>
  566. </div>
  567. );
  568. })}
  569. <div>{userMiseryTrend()}</div>
  570. </Fragment>
  571. );
  572. }
  573. const loader = <StyledLoadingIndicator />;
  574. const platformPerformanceRender = {
  575. [PROJECT_PERFORMANCE_TYPE.FRONTEND]: {
  576. title: t('Frontend Performance'),
  577. section: renderFrontendPerformance(),
  578. },
  579. [PROJECT_PERFORMANCE_TYPE.BACKEND]: {
  580. title: t('Backend Performance'),
  581. section: renderBackendPerformance(),
  582. },
  583. [PROJECT_PERFORMANCE_TYPE.MOBILE]: {
  584. title: t('Mobile Performance'),
  585. section: renderMobilePerformance(),
  586. },
  587. [PROJECT_PERFORMANCE_TYPE.ANY]: {
  588. title: t('[Unknown] Performance'),
  589. section: renderUnknownPerformance(),
  590. },
  591. };
  592. const isUnknownPlatform = performanceType === PROJECT_PERFORMANCE_TYPE.ANY;
  593. return (
  594. <Fragment>
  595. <HeadCellContainer>
  596. {platformPerformanceRender[performanceType].title}
  597. </HeadCellContainer>
  598. {isUnknownPlatform && (
  599. <StyledAlert type="warning" icon={<IconWarning size="md" />} system>
  600. {tct(
  601. 'For more performance metrics, specify which platform this project is using in [link]',
  602. {
  603. link: (
  604. <Link to={`/settings/${organization.slug}/projects/${project.slug}/`}>
  605. {t('project settings.')}
  606. </Link>
  607. ),
  608. }
  609. )}
  610. </StyledAlert>
  611. )}
  612. <StyledPanelTable
  613. isLoading={isLoading}
  614. headers={[
  615. <Cell key="description" align="left">
  616. {t('Description')}
  617. </Cell>,
  618. <Cell key="releases" align="right">
  619. {t('All Releases')}
  620. </Cell>,
  621. <Cell key="release" align="right">
  622. {t('This Release')}
  623. </Cell>,
  624. <Cell key="change" align="right">
  625. {t('Change')}
  626. </Cell>,
  627. ]}
  628. disablePadding
  629. loader={loader}
  630. disableTopBorder={isUnknownPlatform}
  631. >
  632. {platformPerformanceRender[performanceType].section}
  633. </StyledPanelTable>
  634. </Fragment>
  635. );
  636. }
  637. type Props = AsyncComponent['props'] & {
  638. allReleasesEventView: EventView;
  639. location: Location;
  640. organization: Organization;
  641. performanceType: string;
  642. project: ReleaseProject;
  643. releaseEventView: EventView;
  644. };
  645. function PerformanceCardTableWrapper({
  646. organization,
  647. project,
  648. allReleasesEventView,
  649. releaseEventView,
  650. performanceType,
  651. location,
  652. }: Props) {
  653. return (
  654. <DiscoverQuery
  655. eventView={allReleasesEventView}
  656. orgSlug={organization.slug}
  657. location={location}
  658. >
  659. {({isLoading, tableData: allReleasesTableData}) => (
  660. <DiscoverQuery
  661. eventView={releaseEventView}
  662. orgSlug={organization.slug}
  663. location={location}
  664. >
  665. {({isLoading: isReleaseLoading, tableData: thisReleaseTableData}) => (
  666. <PerformanceCardTable
  667. isLoading={isLoading || isReleaseLoading}
  668. organization={organization}
  669. location={location}
  670. project={project}
  671. allReleasesEventView={allReleasesEventView}
  672. releaseEventView={releaseEventView}
  673. allReleasesTableData={allReleasesTableData}
  674. thisReleaseTableData={thisReleaseTableData}
  675. performanceType={performanceType}
  676. />
  677. )}
  678. </DiscoverQuery>
  679. )}
  680. </DiscoverQuery>
  681. );
  682. }
  683. export default PerformanceCardTableWrapper;
  684. const emptyFieldCss = p => css`
  685. color: ${p.theme.chartOther};
  686. text-align: right;
  687. `;
  688. const StyledLoadingIndicator = styled(LoadingIndicator)`
  689. margin: 70px auto;
  690. `;
  691. const HeadCellContainer = styled('div')`
  692. font-size: ${p => p.theme.fontSizeExtraLarge};
  693. padding: ${space(2)};
  694. border-top: 1px solid ${p => p.theme.border};
  695. border-left: 1px solid ${p => p.theme.border};
  696. border-right: 1px solid ${p => p.theme.border};
  697. border-top-left-radius: ${p => p.theme.borderRadius};
  698. border-top-right-radius: ${p => p.theme.borderRadius};
  699. `;
  700. const StyledPanelTable = styled(PanelTable)<{disableTopBorder: boolean}>`
  701. border-top-left-radius: 0;
  702. border-top-right-radius: 0;
  703. border-top: ${p => (p.disableTopBorder ? 'none' : `1px solid ${p.theme.border}`)};
  704. @media (max-width: ${p => p.theme.breakpoints[2]}) {
  705. grid-template-columns: min-content 1fr 1fr 1fr;
  706. }
  707. `;
  708. const StyledPanelItem = styled(PanelItem)`
  709. display: block;
  710. white-space: nowrap;
  711. width: 100%;
  712. `;
  713. const SubTitle = styled('div')`
  714. margin-left: ${space(3)};
  715. `;
  716. const TitleSpace = styled('div')`
  717. height: 24px;
  718. `;
  719. const UserMiseryPanelItem = styled(PanelItem)`
  720. justify-content: flex-end;
  721. `;
  722. const ApdexPanelItem = styled(PanelItem)`
  723. text-align: right;
  724. `;
  725. const SingleEmptySubText = styled(PanelItem)`
  726. display: block;
  727. ${emptyFieldCss}
  728. `;
  729. const MultipleEmptySubText = styled('div')`
  730. ${emptyFieldCss}
  731. `;
  732. const Cell = styled('div')<{align: 'left' | 'right'}>`
  733. text-align: ${p => p.align};
  734. margin-left: ${p => p.align === 'left' && space(2)};
  735. padding-right: ${p => p.align === 'right' && space(2)};
  736. ${overflowEllipsis}
  737. `;
  738. const StyledAlert = styled(Alert)`
  739. border-top: 1px solid ${p => p.theme.border};
  740. border-right: 1px solid ${p => p.theme.border};
  741. border-left: 1px solid ${p => p.theme.border};
  742. margin-bottom: 0;
  743. `;
  744. const StyledNotAvailable = styled(NotAvailable)`
  745. text-align: right;
  746. `;
  747. const SubText = styled('div')`
  748. color: ${p => p.theme.subText};
  749. text-align: right;
  750. `;
  751. const TrendText = styled('div')<{color: string}>`
  752. color: ${p => p.theme[p.color]};
  753. text-align: right;
  754. `;
  755. const StyledIconArrow = styled(IconArrow)<{color: string}>`
  756. color: ${p => p.theme[p.color]};
  757. margin-left: ${space(0.5)};
  758. `;