screenshotCard.tsx 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. import {useState} from 'react';
  2. import LazyLoad from 'react-lazyload';
  3. import styled from '@emotion/styled';
  4. import {openModal} from 'sentry/actionCreators/modal';
  5. import MenuItemActionLink from 'sentry/components/actions/menuItemActionLink';
  6. import {Button} from 'sentry/components/button';
  7. import Card from 'sentry/components/card';
  8. import DateTime from 'sentry/components/dateTime';
  9. import DropdownLink from 'sentry/components/dropdownLink';
  10. import ImageVisualization from 'sentry/components/events/eventTagsAndScreenshot/screenshot/imageVisualization';
  11. import Modal, {
  12. modalCss,
  13. } from 'sentry/components/events/eventTagsAndScreenshot/screenshot/modal';
  14. import Link from 'sentry/components/links/link';
  15. import LoadingIndicator from 'sentry/components/loadingIndicator';
  16. import PanelBody from 'sentry/components/panels/panelBody';
  17. import {IconEllipsis} from 'sentry/icons/iconEllipsis';
  18. import {t} from 'sentry/locale';
  19. import {space} from 'sentry/styles/space';
  20. import type {IssueAttachment, Project} from 'sentry/types';
  21. import {trackAnalytics} from 'sentry/utils/analytics';
  22. import useOrganization from 'sentry/utils/useOrganization';
  23. type Props = {
  24. attachmentIndex: number;
  25. attachments: IssueAttachment[];
  26. eventAttachment: IssueAttachment;
  27. eventId: string;
  28. groupId: string;
  29. onDelete: (attachmentId: string) => void;
  30. projectSlug: Project['slug'];
  31. pageLinks?: string | null | undefined;
  32. };
  33. export function ScreenshotCard({
  34. eventAttachment,
  35. projectSlug,
  36. eventId,
  37. groupId,
  38. onDelete,
  39. pageLinks,
  40. attachmentIndex,
  41. attachments,
  42. }: Props) {
  43. const organization = useOrganization();
  44. const [loadingImage, setLoadingImage] = useState(true);
  45. const downloadUrl = `/api/0/projects/${organization.slug}/${projectSlug}/events/${eventId}/attachments/${eventAttachment.id}/?download=1`;
  46. function handleDelete() {
  47. trackAnalytics('issue_details.attachment_tab.screenshot_modal_deleted', {
  48. organization,
  49. });
  50. onDelete(eventAttachment.id);
  51. }
  52. function openVisualizationModal() {
  53. trackAnalytics('issue_details.attachment_tab.screenshot_modal_opened', {
  54. organization,
  55. });
  56. openModal(
  57. modalProps => (
  58. <Modal
  59. {...modalProps}
  60. orgSlug={organization.slug}
  61. projectSlug={projectSlug}
  62. eventAttachment={eventAttachment}
  63. downloadUrl={downloadUrl}
  64. onDelete={handleDelete}
  65. pageLinks={pageLinks}
  66. attachments={attachments}
  67. attachmentIndex={attachmentIndex}
  68. groupId={groupId}
  69. enablePagination
  70. onDownload={() =>
  71. trackAnalytics('issue_details.attachment_tab.screenshot_modal_download', {
  72. organization,
  73. })
  74. }
  75. />
  76. ),
  77. {modalCss}
  78. );
  79. }
  80. const baseEventsPath = `/organizations/${organization.slug}/issues/${groupId}/events/`;
  81. return (
  82. <Card>
  83. <CardHeader>
  84. <CardContent>
  85. <Title
  86. onClick={() =>
  87. trackAnalytics('issue_details.attachment_tab.screenshot_title_clicked', {
  88. organization,
  89. })
  90. }
  91. to={`${baseEventsPath}${eventId}/`}
  92. >
  93. {eventId}
  94. </Title>
  95. <Detail>
  96. <DateTime date={eventAttachment.dateCreated} />
  97. </Detail>
  98. </CardContent>
  99. </CardHeader>
  100. <CardBody>
  101. <StyledPanelBody
  102. onClick={() => openVisualizationModal()}
  103. data-test-id={`screenshot-${eventAttachment.id}`}
  104. >
  105. <LazyLoad>
  106. <StyledImageVisualization
  107. attachment={eventAttachment}
  108. orgId={organization.slug}
  109. projectSlug={projectSlug}
  110. eventId={eventId}
  111. onLoad={() => setLoadingImage(false)}
  112. onError={() => setLoadingImage(false)}
  113. />
  114. {loadingImage && (
  115. <StyledLoadingIndicator>
  116. <LoadingIndicator mini />
  117. </StyledLoadingIndicator>
  118. )}
  119. </LazyLoad>
  120. </StyledPanelBody>
  121. </CardBody>
  122. <CardFooter>
  123. <div>{eventAttachment.name}</div>
  124. <DropdownLink
  125. caret={false}
  126. customTitle={
  127. <Button
  128. aria-label={t('Actions')}
  129. size="xs"
  130. icon={<IconEllipsis direction="down" size="sm" />}
  131. borderless
  132. />
  133. }
  134. anchorRight
  135. >
  136. <MenuItemActionLink shouldConfirm={false} href={`${downloadUrl}`}>
  137. {t('Download')}
  138. </MenuItemActionLink>
  139. <MenuItemActionLink
  140. shouldConfirm
  141. confirmPriority="danger"
  142. confirmLabel={t('Delete')}
  143. onAction={() => onDelete(eventAttachment.id)}
  144. header={t('This image was captured around the time that the event occurred.')}
  145. message={t('Are you sure you wish to delete this image?')}
  146. >
  147. {t('Delete')}
  148. </MenuItemActionLink>
  149. </DropdownLink>
  150. </CardFooter>
  151. </Card>
  152. );
  153. }
  154. const Title = styled(Link)`
  155. ${p => p.theme.overflowEllipsis};
  156. font-weight: normal;
  157. `;
  158. const Detail = styled('div')`
  159. font-family: ${p => p.theme.text.familyMono};
  160. font-size: ${p => p.theme.fontSizeSmall};
  161. color: ${p => p.theme.gray300};
  162. ${p => p.theme.overflowEllipsis};
  163. line-height: 1.5;
  164. `;
  165. const CardHeader = styled('div')`
  166. display: flex;
  167. padding: ${space(1.5)} ${space(2)};
  168. `;
  169. const CardBody = styled('div')`
  170. background: ${p => p.theme.gray100};
  171. padding: ${space(1.5)} ${space(2)};
  172. max-height: 250px;
  173. min-height: 250px;
  174. overflow: hidden;
  175. border-bottom: 1px solid ${p => p.theme.gray100};
  176. `;
  177. const CardFooter = styled('div')`
  178. display: flex;
  179. justify-content: space-between;
  180. align-items: center;
  181. padding: ${space(1)} ${space(2)};
  182. .dropdown {
  183. height: 24px;
  184. }
  185. `;
  186. const CardContent = styled('div')`
  187. flex-grow: 1;
  188. overflow: hidden;
  189. margin-right: ${space(1)};
  190. `;
  191. const StyledPanelBody = styled(PanelBody)`
  192. height: 100%;
  193. min-height: 48px;
  194. overflow: hidden;
  195. cursor: pointer;
  196. position: relative;
  197. display: flex;
  198. align-items: center;
  199. justify-content: center;
  200. flex: 1;
  201. `;
  202. const StyledLoadingIndicator = styled('div')`
  203. position: absolute;
  204. display: flex;
  205. align-items: center;
  206. justify-content: center;
  207. height: 100%;
  208. `;
  209. const StyledImageVisualization = styled(ImageVisualization)`
  210. height: 100%;
  211. z-index: 1;
  212. border: 0;
  213. `;