performanceCardTable.tsx 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823
  1. import {Fragment} from 'react';
  2. import {Link} from 'react-router';
  3. import {css} from '@emotion/react';
  4. import styled from '@emotion/styled';
  5. import {Location} from 'history';
  6. import Alert from 'sentry/components/alert';
  7. import {AsyncComponentProps} from 'sentry/components/asyncComponent';
  8. import LoadingIndicator from 'sentry/components/loadingIndicator';
  9. import NotAvailable from 'sentry/components/notAvailable';
  10. import {PanelItem} from 'sentry/components/panels';
  11. import PanelTable from 'sentry/components/panels/panelTable';
  12. import {IconArrow} from 'sentry/icons';
  13. import {t, tct} from 'sentry/locale';
  14. import space from 'sentry/styles/space';
  15. import {Organization, ReleaseProject} from 'sentry/types';
  16. import DiscoverQuery, {TableData} from 'sentry/utils/discover/discoverQuery';
  17. import EventView from 'sentry/utils/discover/eventView';
  18. import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers';
  19. import {MobileVital, WebVital} from 'sentry/utils/fields';
  20. import {
  21. MOBILE_VITAL_DETAILS,
  22. WEB_VITAL_DETAILS,
  23. } from 'sentry/utils/performance/vitals/constants';
  24. import {PROJECT_PERFORMANCE_TYPE} from 'sentry/views/performance/utils';
  25. type PerformanceCardTableProps = {
  26. allReleasesEventView: EventView;
  27. allReleasesTableData: TableData | null;
  28. isLoading: boolean;
  29. location: Location;
  30. organization: Organization;
  31. performanceType: string;
  32. project: ReleaseProject;
  33. releaseEventView: EventView;
  34. thisReleaseTableData: TableData | null;
  35. };
  36. function PerformanceCardTable({
  37. organization,
  38. location,
  39. project,
  40. releaseEventView,
  41. allReleasesTableData,
  42. thisReleaseTableData,
  43. performanceType,
  44. isLoading,
  45. }: PerformanceCardTableProps) {
  46. const miseryRenderer =
  47. allReleasesTableData?.meta &&
  48. getFieldRenderer('user_misery()', allReleasesTableData.meta, false);
  49. function renderChange(
  50. allReleasesScore: number,
  51. thisReleaseScore: number,
  52. meta: string
  53. ) {
  54. if (allReleasesScore === undefined || thisReleaseScore === undefined) {
  55. return <StyledNotAvailable />;
  56. }
  57. const trend = allReleasesScore - thisReleaseScore;
  58. const trendSeconds = trend >= 1000 ? trend / 1000 : trend;
  59. const trendPercentage = (allReleasesScore - thisReleaseScore) * 100;
  60. const valPercentage = Math.round(Math.abs(trendPercentage));
  61. const val = Math.abs(trendSeconds).toFixed(2);
  62. if (trend === 0) {
  63. return <SubText>{`0${meta === 'duration' ? 'ms' : '%'}`}</SubText>;
  64. }
  65. return (
  66. <TrendText color={trend >= 0 ? 'success' : 'error'}>
  67. {`${meta === 'duration' ? val : valPercentage}${
  68. meta === 'duration' ? (trend >= 1000 ? 's' : 'ms') : '%'
  69. }`}
  70. <StyledIconArrow
  71. color={trend >= 0 ? 'success' : 'error'}
  72. direction={trend >= 0 ? 'down' : 'up'}
  73. size="xs"
  74. />
  75. </TrendText>
  76. );
  77. }
  78. function userMiseryTrend() {
  79. const allReleasesUserMisery = allReleasesTableData?.data?.[0]?.['user_misery()'];
  80. const thisReleaseUserMisery = thisReleaseTableData?.data?.[0]?.['user_misery()'];
  81. return (
  82. <StyledPanelItem>
  83. {renderChange(
  84. allReleasesUserMisery as number,
  85. thisReleaseUserMisery as number,
  86. 'number' as string
  87. )}
  88. </StyledPanelItem>
  89. );
  90. }
  91. function renderFrontendPerformance() {
  92. const webVitals = [
  93. {title: WebVital.FCP, field: 'p75(measurements.fcp)'},
  94. {title: WebVital.FID, field: 'p75(measurements.fid)'},
  95. {title: WebVital.LCP, field: 'p75(measurements.lcp)'},
  96. {title: WebVital.CLS, field: 'p75(measurements.cls)'},
  97. ];
  98. const spans = [
  99. {title: 'HTTP', column: 'p75(spans.http)', field: 'p75(spans.http)'},
  100. {title: 'Browser', column: 'p75(spans.browser)', field: 'p75(spans.browser)'},
  101. {title: 'Resource', column: 'p75(spans.resource)', field: 'p75(spans.resource)'},
  102. ];
  103. const webVitalTitles = webVitals.map((vital, idx) => {
  104. const newView = releaseEventView.withColumns([
  105. {kind: 'field', field: `p75(${vital.title})`},
  106. ]);
  107. return (
  108. <SubTitle key={idx}>
  109. <Link to={newView.getResultsViewUrlTarget(organization.slug)}>
  110. {WEB_VITAL_DETAILS[vital.title].name} (
  111. {WEB_VITAL_DETAILS[vital.title].acronym})
  112. </Link>
  113. </SubTitle>
  114. );
  115. });
  116. const spanTitles = spans.map((span, idx) => {
  117. const newView = releaseEventView.withColumns([
  118. {kind: 'field', field: `${span.column}`},
  119. ]);
  120. return (
  121. <SubTitle key={idx}>
  122. <Link to={newView.getResultsViewUrlTarget(organization.slug)}>
  123. {span.title}
  124. </Link>
  125. </SubTitle>
  126. );
  127. });
  128. const webVitalsRenderer = webVitals.map(
  129. vital =>
  130. allReleasesTableData?.meta &&
  131. getFieldRenderer(vital.field, allReleasesTableData?.meta, false)
  132. );
  133. const spansRenderer = spans.map(
  134. span =>
  135. allReleasesTableData?.meta &&
  136. getFieldRenderer(span.field, allReleasesTableData?.meta, false)
  137. );
  138. const webReleaseTrend = webVitals.map(vital => {
  139. return {
  140. allReleasesRow: {
  141. data: allReleasesTableData?.data?.[0]?.[vital.field],
  142. meta: allReleasesTableData?.meta?.[vital.field],
  143. },
  144. thisReleaseRow: {
  145. data: thisReleaseTableData?.data?.[0]?.[vital.field],
  146. meta: thisReleaseTableData?.meta?.[vital.field],
  147. },
  148. };
  149. });
  150. const spansReleaseTrend = spans.map(span => {
  151. return {
  152. allReleasesRow: {
  153. data: allReleasesTableData?.data?.[0]?.[span.field],
  154. meta: allReleasesTableData?.meta?.[span.field],
  155. },
  156. thisReleaseRow: {
  157. data: thisReleaseTableData?.data?.[0]?.[span.field],
  158. meta: thisReleaseTableData?.meta?.[span.field],
  159. },
  160. };
  161. });
  162. const emptyColumn = (
  163. <div>
  164. <SingleEmptySubText>
  165. <StyledNotAvailable tooltip={t('No results found')} />
  166. </SingleEmptySubText>
  167. <StyledPanelItem>
  168. <TitleSpace />
  169. {webVitals.map((vital, index) => (
  170. <MultipleEmptySubText key={vital[index]}>
  171. {<StyledNotAvailable tooltip={t('No results found')} />}
  172. </MultipleEmptySubText>
  173. ))}
  174. </StyledPanelItem>
  175. <StyledPanelItem>
  176. <TitleSpace />
  177. {spans.map((span, index) => (
  178. <MultipleEmptySubText key={span[index]}>
  179. {<StyledNotAvailable tooltip={t('No results found')} />}
  180. </MultipleEmptySubText>
  181. ))}
  182. </StyledPanelItem>
  183. </div>
  184. );
  185. return (
  186. <Fragment>
  187. <div>
  188. <PanelItem>{t('User Misery')}</PanelItem>
  189. <StyledPanelItem>
  190. <div>{t('Web Vitals')}</div>
  191. {webVitalTitles}
  192. </StyledPanelItem>
  193. <StyledPanelItem>
  194. <div>{t('Span Operations')}</div>
  195. {spanTitles}
  196. </StyledPanelItem>
  197. </div>
  198. {allReleasesTableData?.data.length === 0
  199. ? emptyColumn
  200. : allReleasesTableData?.data.map((dataRow, idx) => {
  201. const allReleasesMisery = miseryRenderer?.(dataRow, {
  202. organization,
  203. location,
  204. });
  205. const allReleasesWebVitals = webVitalsRenderer?.map(renderer =>
  206. renderer?.(dataRow, {organization, location})
  207. );
  208. const allReleasesSpans = spansRenderer?.map(renderer =>
  209. renderer?.(dataRow, {organization, location})
  210. );
  211. return (
  212. <div key={idx}>
  213. <UserMiseryPanelItem>{allReleasesMisery}</UserMiseryPanelItem>
  214. <StyledPanelItem>
  215. <TitleSpace />
  216. {allReleasesWebVitals.map(webVital => webVital)}
  217. </StyledPanelItem>
  218. <StyledPanelItem>
  219. <TitleSpace />
  220. {allReleasesSpans.map(span => span)}
  221. </StyledPanelItem>
  222. </div>
  223. );
  224. })}
  225. {thisReleaseTableData?.data.length === 0
  226. ? emptyColumn
  227. : thisReleaseTableData?.data.map((dataRow, idx) => {
  228. const thisReleasesMisery = miseryRenderer?.(dataRow, {
  229. organization,
  230. location,
  231. });
  232. const thisReleasesWebVitals = webVitalsRenderer?.map(renderer =>
  233. renderer?.(dataRow, {organization, location})
  234. );
  235. const thisReleasesSpans = spansRenderer?.map(renderer =>
  236. renderer?.(dataRow, {organization, location})
  237. );
  238. return (
  239. <div key={idx}>
  240. <div>
  241. <UserMiseryPanelItem>{thisReleasesMisery}</UserMiseryPanelItem>
  242. <StyledPanelItem>
  243. <TitleSpace />
  244. {thisReleasesWebVitals.map(webVital => webVital)}
  245. </StyledPanelItem>
  246. <StyledPanelItem>
  247. <TitleSpace />
  248. {thisReleasesSpans.map(span => span)}
  249. </StyledPanelItem>
  250. </div>
  251. </div>
  252. );
  253. })}
  254. <div>
  255. {userMiseryTrend()}
  256. <StyledPanelItem>
  257. <TitleSpace />
  258. {webReleaseTrend?.map(row =>
  259. renderChange(
  260. row.allReleasesRow?.data as number,
  261. row.thisReleaseRow?.data as number,
  262. row.allReleasesRow?.meta as string
  263. )
  264. )}
  265. </StyledPanelItem>
  266. <StyledPanelItem>
  267. <TitleSpace />
  268. {spansReleaseTrend?.map(row =>
  269. renderChange(
  270. row.allReleasesRow?.data as number,
  271. row.thisReleaseRow?.data as number,
  272. row.allReleasesRow?.meta as string
  273. )
  274. )}
  275. </StyledPanelItem>
  276. </div>
  277. </Fragment>
  278. );
  279. }
  280. function renderBackendPerformance() {
  281. const spans = [
  282. {title: 'HTTP', column: 'p75(spans.http)', field: 'p75_spans_http'},
  283. {title: 'DB', column: 'p75(spans.db)', field: 'p75_spans_db'},
  284. ];
  285. const spanTitles = spans.map((span, idx) => {
  286. const newView = releaseEventView.withColumns([
  287. {kind: 'field', field: `${span.column}`},
  288. ]);
  289. return (
  290. <SubTitle key={idx}>
  291. <Link to={newView.getResultsViewUrlTarget(organization.slug)}>
  292. {span.title}
  293. </Link>
  294. </SubTitle>
  295. );
  296. });
  297. const apdexRenderer =
  298. allReleasesTableData?.meta &&
  299. getFieldRenderer('apdex', allReleasesTableData.meta, false);
  300. const spansRenderer = spans.map(
  301. span =>
  302. allReleasesTableData?.meta &&
  303. getFieldRenderer(span.field, allReleasesTableData?.meta, false)
  304. );
  305. const spansReleaseTrend = spans.map(span => {
  306. return {
  307. allReleasesRow: {
  308. data: allReleasesTableData?.data?.[0]?.[span.field],
  309. meta: allReleasesTableData?.meta?.[span.field],
  310. },
  311. thisReleaseRow: {
  312. data: thisReleaseTableData?.data?.[0]?.[span.field],
  313. meta: thisReleaseTableData?.meta?.[span.field],
  314. },
  315. };
  316. });
  317. function apdexTrend() {
  318. const allReleasesApdex = allReleasesTableData?.data?.[0]?.apdex;
  319. const thisReleaseApdex = thisReleaseTableData?.data?.[0]?.apdex;
  320. return (
  321. <StyledPanelItem>
  322. {renderChange(
  323. allReleasesApdex as number,
  324. thisReleaseApdex as number,
  325. 'string' as string
  326. )}
  327. </StyledPanelItem>
  328. );
  329. }
  330. const emptyColumn = (
  331. <div>
  332. <SingleEmptySubText>
  333. <StyledNotAvailable tooltip={t('No results found')} />
  334. </SingleEmptySubText>
  335. <SingleEmptySubText>
  336. <StyledNotAvailable tooltip={t('No results found')} />
  337. </SingleEmptySubText>
  338. <StyledPanelItem>
  339. <TitleSpace />
  340. {spans.map((span, index) => (
  341. <MultipleEmptySubText key={span[index]}>
  342. {<StyledNotAvailable tooltip={t('No results found')} />}
  343. </MultipleEmptySubText>
  344. ))}
  345. </StyledPanelItem>
  346. </div>
  347. );
  348. return (
  349. <Fragment>
  350. <div>
  351. <PanelItem>{t('User Misery')}</PanelItem>
  352. <StyledPanelItem>
  353. <div>{t('Apdex')}</div>
  354. </StyledPanelItem>
  355. <StyledPanelItem>
  356. <div>{t('Span Operations')}</div>
  357. {spanTitles}
  358. </StyledPanelItem>
  359. </div>
  360. {allReleasesTableData?.data.length === 0
  361. ? emptyColumn
  362. : allReleasesTableData?.data.map((dataRow, idx) => {
  363. const allReleasesMisery = miseryRenderer?.(dataRow, {
  364. organization,
  365. location,
  366. });
  367. const allReleasesApdex = apdexRenderer?.(dataRow, {organization, location});
  368. const allReleasesSpans = spansRenderer?.map(renderer =>
  369. renderer?.(dataRow, {organization, location})
  370. );
  371. return (
  372. <div key={idx}>
  373. <UserMiseryPanelItem>{allReleasesMisery}</UserMiseryPanelItem>
  374. <ApdexPanelItem>{allReleasesApdex}</ApdexPanelItem>
  375. <StyledPanelItem>
  376. <TitleSpace />
  377. {allReleasesSpans.map(span => span)}
  378. </StyledPanelItem>
  379. </div>
  380. );
  381. })}
  382. {thisReleaseTableData?.data.length === 0
  383. ? emptyColumn
  384. : thisReleaseTableData?.data.map((dataRow, idx) => {
  385. const thisReleasesMisery = miseryRenderer?.(dataRow, {
  386. organization,
  387. location,
  388. });
  389. const thisReleasesApdex = apdexRenderer?.(dataRow, {
  390. organization,
  391. location,
  392. });
  393. const thisReleasesSpans = spansRenderer?.map(renderer =>
  394. renderer?.(dataRow, {organization, location})
  395. );
  396. return (
  397. <div key={idx}>
  398. <UserMiseryPanelItem>{thisReleasesMisery}</UserMiseryPanelItem>
  399. <ApdexPanelItem>{thisReleasesApdex}</ApdexPanelItem>
  400. <StyledPanelItem>
  401. <TitleSpace />
  402. {thisReleasesSpans.map(span => span)}
  403. </StyledPanelItem>
  404. </div>
  405. );
  406. })}
  407. <div>
  408. {userMiseryTrend()}
  409. {apdexTrend()}
  410. <StyledPanelItem>
  411. <TitleSpace />
  412. {spansReleaseTrend?.map(row =>
  413. renderChange(
  414. row.allReleasesRow?.data as number,
  415. row.thisReleaseRow?.data as number,
  416. row.allReleasesRow?.meta as string
  417. )
  418. )}
  419. </StyledPanelItem>
  420. </div>
  421. </Fragment>
  422. );
  423. }
  424. function renderMobilePerformance() {
  425. const mobileVitals = [
  426. MobileVital.AppStartCold,
  427. MobileVital.AppStartWarm,
  428. MobileVital.FramesSlow,
  429. MobileVital.FramesFrozen,
  430. ];
  431. const mobileVitalTitles = mobileVitals.map(mobileVital => {
  432. return (
  433. <PanelItem key={mobileVital}>{MOBILE_VITAL_DETAILS[mobileVital].name}</PanelItem>
  434. );
  435. });
  436. const mobileVitalFields = [
  437. 'p75(measurements.app_start_cold)',
  438. 'p75(measurements.app_start_warm)',
  439. 'p75(measurements.frames_slow)',
  440. 'p75(measurements.frames_frozen)',
  441. ];
  442. const mobileVitalsRenderer = mobileVitalFields.map(
  443. field =>
  444. allReleasesTableData?.meta &&
  445. getFieldRenderer(field, allReleasesTableData?.meta, false)
  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" showIcon 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. interface Props extends AsyncComponentProps {
  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. useEvents
  659. >
  660. {({isLoading, tableData: allReleasesTableData}) => (
  661. <DiscoverQuery
  662. eventView={releaseEventView}
  663. orgSlug={organization.slug}
  664. location={location}
  665. useEvents
  666. >
  667. {({isLoading: isReleaseLoading, tableData: thisReleaseTableData}) => (
  668. <PerformanceCardTable
  669. isLoading={isLoading || isReleaseLoading}
  670. organization={organization}
  671. location={location}
  672. project={project}
  673. allReleasesEventView={allReleasesEventView}
  674. releaseEventView={releaseEventView}
  675. allReleasesTableData={allReleasesTableData}
  676. thisReleaseTableData={thisReleaseTableData}
  677. performanceType={performanceType}
  678. />
  679. )}
  680. </DiscoverQuery>
  681. )}
  682. </DiscoverQuery>
  683. );
  684. }
  685. export default PerformanceCardTableWrapper;
  686. const emptyFieldCss = p => css`
  687. color: ${p.theme.chartOther};
  688. text-align: right;
  689. `;
  690. const StyledLoadingIndicator = styled(LoadingIndicator)`
  691. margin: 70px auto;
  692. `;
  693. const HeadCellContainer = styled('div')`
  694. font-size: ${p => p.theme.fontSizeExtraLarge};
  695. padding: ${space(2)};
  696. border-top: 1px solid ${p => p.theme.border};
  697. border-left: 1px solid ${p => p.theme.border};
  698. border-right: 1px solid ${p => p.theme.border};
  699. border-top-left-radius: ${p => p.theme.borderRadius};
  700. border-top-right-radius: ${p => p.theme.borderRadius};
  701. `;
  702. const StyledPanelTable = styled(PanelTable)<{disableTopBorder: boolean}>`
  703. border-top-left-radius: 0;
  704. border-top-right-radius: 0;
  705. border-top: ${p => (p.disableTopBorder ? 'none' : `1px solid ${p.theme.border}`)};
  706. @media (max-width: ${p => p.theme.breakpoints.large}) {
  707. grid-template-columns: min-content 1fr 1fr 1fr;
  708. }
  709. `;
  710. const StyledPanelItem = styled(PanelItem)`
  711. display: block;
  712. white-space: nowrap;
  713. width: 100%;
  714. `;
  715. const SubTitle = styled('div')`
  716. margin-left: ${space(3)};
  717. `;
  718. const TitleSpace = styled('div')`
  719. height: 24px;
  720. `;
  721. const UserMiseryPanelItem = styled(PanelItem)`
  722. justify-content: flex-end;
  723. `;
  724. const ApdexPanelItem = styled(PanelItem)`
  725. text-align: right;
  726. `;
  727. const SingleEmptySubText = styled(PanelItem)`
  728. display: block;
  729. ${emptyFieldCss}
  730. `;
  731. const MultipleEmptySubText = styled('div')`
  732. ${emptyFieldCss}
  733. `;
  734. const Cell = styled('div')<{align: 'left' | 'right'}>`
  735. text-align: ${p => p.align};
  736. margin-left: ${p => p.align === 'left' && space(2)};
  737. padding-right: ${p => p.align === 'right' && space(2)};
  738. ${p => p.theme.overflowEllipsis}
  739. `;
  740. const StyledAlert = styled(Alert)`
  741. border-top: 1px solid ${p => p.theme.border};
  742. border-right: 1px solid ${p => p.theme.border};
  743. border-left: 1px solid ${p => p.theme.border};
  744. margin-bottom: 0;
  745. `;
  746. const StyledNotAvailable = styled(NotAvailable)`
  747. text-align: right;
  748. `;
  749. const SubText = styled('div')`
  750. color: ${p => p.theme.subText};
  751. text-align: right;
  752. `;
  753. const TrendText = styled('div')<{color: string}>`
  754. color: ${p => p.theme[p.color]};
  755. text-align: right;
  756. `;
  757. const StyledIconArrow = styled(IconArrow)<{color: string}>`
  758. color: ${p => p.theme[p.color]};
  759. margin-left: ${space(0.5)};
  760. `;