screenshotCard.tsx 6.4 KB

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