detailLayout.tsx 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. import React from 'react';
  2. import styled from '@emotion/styled';
  3. import Breadcrumbs from 'sentry/components/breadcrumbs';
  4. import Button from 'sentry/components/button';
  5. import Duration from 'sentry/components/duration';
  6. import FeatureBadge from 'sentry/components/featureBadge';
  7. import UserBadge from 'sentry/components/idBadge/userBadge';
  8. import * as Layout from 'sentry/components/layouts/thirds';
  9. import {KeyMetricData, KeyMetrics} from 'sentry/components/replays/keyMetrics';
  10. import {useReplayContext} from 'sentry/components/replays/replayContext';
  11. import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
  12. import TimeSince from 'sentry/components/timeSince';
  13. import {t} from 'sentry/locale';
  14. import {RawCrumb} from 'sentry/types/breadcrumbs';
  15. import {Event} from 'sentry/types/event';
  16. import getUrlPathname from 'sentry/utils/getUrlPathname';
  17. type Props = {
  18. children: React.ReactNode;
  19. orgId: string;
  20. crumbs?: RawCrumb[];
  21. event?: Event;
  22. };
  23. function DetailLayout({children, event, orgId, crumbs}: Props) {
  24. const title = event ? `${event.id} - Replays - ${orgId}` : `Replays - ${orgId}`;
  25. return (
  26. <SentryDocumentTitle title={title}>
  27. <React.Fragment>
  28. <Layout.Header>
  29. <Layout.HeaderContent>
  30. <Breadcrumbs
  31. crumbs={[
  32. {
  33. to: `/organizations/${orgId}/replays/`,
  34. label: t('Replays'),
  35. },
  36. {
  37. label: (
  38. <React.Fragment>
  39. {t('Replay Details')}
  40. <FeatureBadge type="alpha" />
  41. </React.Fragment>
  42. ),
  43. },
  44. ]}
  45. />
  46. </Layout.HeaderContent>
  47. <ButtonWrapper>
  48. <Button
  49. title={t('Send us feedback via email')}
  50. href="mailto:replay-feedback@sentry.io?subject=Replay Details Feedback"
  51. >
  52. {t('Give Feedback')}
  53. </Button>
  54. </ButtonWrapper>
  55. <Layout.HeaderContent>
  56. <EventHeader event={event} />
  57. </Layout.HeaderContent>
  58. <Layout.HeaderActions>
  59. <EventMetaData event={event} crumbs={crumbs} />
  60. </Layout.HeaderActions>
  61. </Layout.Header>
  62. {children}
  63. </React.Fragment>
  64. </SentryDocumentTitle>
  65. );
  66. }
  67. function EventHeader({event}: Pick<Props, 'event'>) {
  68. if (!event) {
  69. return null;
  70. }
  71. const urlTag = event.tags.find(({key}) => key === 'url');
  72. const pathname = getUrlPathname(urlTag?.value ?? '') ?? '';
  73. return (
  74. <UserBadge
  75. avatarSize={32}
  76. user={{
  77. username: event.user?.username ?? '',
  78. id: event.user?.id ?? '',
  79. ip_address: event.user?.ip_address ?? '',
  80. name: event.user?.name ?? '',
  81. email: event.user?.email ?? '',
  82. }}
  83. // this is the subheading for the avatar, so displayEmail in this case is a misnomer
  84. displayEmail={pathname}
  85. />
  86. );
  87. }
  88. function EventMetaData({event, crumbs}: Pick<Props, 'event' | 'crumbs'>) {
  89. const {duration} = useReplayContext();
  90. if (!event) {
  91. return null;
  92. }
  93. const errors = crumbs?.filter(crumb => crumb.type === 'error').length;
  94. return (
  95. <KeyMetrics>
  96. <KeyMetricData
  97. keyName={t('Timestamp')}
  98. value={<TimeSince date={event.dateReceived} />}
  99. />
  100. <KeyMetricData
  101. keyName={t('Duration')}
  102. value={
  103. <Duration
  104. seconds={Math.floor(msToSec(duration || 0)) || 1}
  105. abbreviation
  106. exact
  107. />
  108. }
  109. />
  110. <KeyMetricData keyName={t('Errors')} value={errors} />
  111. </KeyMetrics>
  112. );
  113. }
  114. function msToSec(ms: number) {
  115. return ms / 1000;
  116. }
  117. // TODO(replay); This could make a lot of sense to put inside HeaderActions by default
  118. const ButtonWrapper = styled(Layout.HeaderActions)`
  119. align-items: end;
  120. `;
  121. export default DetailLayout;