screenshotCard.tsx 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  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';
  17. import {IconEllipsis} from 'sentry/icons/iconEllipsis';
  18. import {t} from 'sentry/locale';
  19. import space from 'sentry/styles/space';
  20. import {IssueAttachment, Project} from 'sentry/types';
  21. import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
  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. trackAdvancedAnalyticsEvent('issue_details.attachment_tab.screenshot_modal_deleted', {
  48. organization,
  49. });
  50. onDelete(eventAttachment.id);
  51. }
  52. function openVisualizationModal() {
  53. trackAdvancedAnalyticsEvent('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. trackAdvancedAnalyticsEvent(
  72. 'issue_details.attachment_tab.screenshot_modal_download',
  73. {
  74. organization,
  75. }
  76. )
  77. }
  78. />
  79. ),
  80. {modalCss}
  81. );
  82. }
  83. const baseEventsPath = `/organizations/${organization.slug}/issues/${groupId}/events/`;
  84. return (
  85. <Card>
  86. <CardHeader>
  87. <CardContent>
  88. <Title
  89. onClick={() =>
  90. trackAdvancedAnalyticsEvent(
  91. 'issue_details.attachment_tab.screenshot_title_clicked',
  92. {
  93. organization,
  94. }
  95. )
  96. }
  97. to={`${baseEventsPath}${eventId}/`}
  98. >
  99. {eventId}
  100. </Title>
  101. <Detail>
  102. <DateTime date={eventAttachment.dateCreated} />
  103. </Detail>
  104. </CardContent>
  105. </CardHeader>
  106. <CardBody>
  107. <StyledPanelBody
  108. onClick={() => openVisualizationModal()}
  109. data-test-id={`screenshot-${eventAttachment.id}`}
  110. >
  111. <LazyLoad>
  112. <StyledImageVisualization
  113. attachment={eventAttachment}
  114. orgId={organization.slug}
  115. projectId={projectSlug}
  116. eventId={eventId}
  117. onLoad={() => setLoadingImage(false)}
  118. onError={() => setLoadingImage(false)}
  119. />
  120. {loadingImage && (
  121. <StyledLoadingIndicator>
  122. <LoadingIndicator mini />
  123. </StyledLoadingIndicator>
  124. )}
  125. </LazyLoad>
  126. </StyledPanelBody>
  127. </CardBody>
  128. <CardFooter>
  129. <div>{eventAttachment.name}</div>
  130. <DropdownLink
  131. caret={false}
  132. customTitle={
  133. <Button
  134. aria-label={t('Actions')}
  135. size="xs"
  136. icon={<IconEllipsis direction="down" size="sm" />}
  137. borderless
  138. />
  139. }
  140. anchorRight
  141. >
  142. <MenuItemActionLink shouldConfirm={false} href={`${downloadUrl}`}>
  143. {t('Download')}
  144. </MenuItemActionLink>
  145. <MenuItemActionLink
  146. shouldConfirm
  147. confirmPriority="danger"
  148. confirmLabel={t('Delete')}
  149. onAction={() => onDelete(eventAttachment.id)}
  150. header={t('This image was captured around the time that the event occurred.')}
  151. message={t('Are you sure you wish to delete this image?')}
  152. >
  153. {t('Delete')}
  154. </MenuItemActionLink>
  155. </DropdownLink>
  156. </CardFooter>
  157. </Card>
  158. );
  159. }
  160. const Title = styled(Link)`
  161. ${p => p.theme.overflowEllipsis};
  162. font-weight: normal;
  163. `;
  164. const Detail = styled('div')`
  165. font-family: ${p => p.theme.text.familyMono};
  166. font-size: ${p => p.theme.fontSizeSmall};
  167. color: ${p => p.theme.gray300};
  168. ${p => p.theme.overflowEllipsis};
  169. line-height: 1.5;
  170. `;
  171. const CardHeader = styled('div')`
  172. display: flex;
  173. padding: ${space(1.5)} ${space(2)};
  174. `;
  175. const CardBody = styled('div')`
  176. background: ${p => p.theme.gray100};
  177. padding: ${space(1.5)} ${space(2)};
  178. max-height: 250px;
  179. min-height: 250px;
  180. overflow: hidden;
  181. border-bottom: 1px solid ${p => p.theme.gray100};
  182. `;
  183. const CardFooter = styled('div')`
  184. display: flex;
  185. justify-content: space-between;
  186. align-items: center;
  187. padding: ${space(1)} ${space(2)};
  188. .dropdown {
  189. height: 24px;
  190. }
  191. `;
  192. const CardContent = styled('div')`
  193. flex-grow: 1;
  194. overflow: hidden;
  195. margin-right: ${space(1)};
  196. `;
  197. const StyledPanelBody = styled(PanelBody)`
  198. height: 100%;
  199. min-height: 48px;
  200. overflow: hidden;
  201. cursor: pointer;
  202. position: relative;
  203. display: flex;
  204. align-items: center;
  205. justify-content: center;
  206. flex: 1;
  207. `;
  208. const StyledLoadingIndicator = styled('div')`
  209. position: absolute;
  210. display: flex;
  211. align-items: center;
  212. justify-content: center;
  213. height: 100%;
  214. `;
  215. const StyledImageVisualization = styled(ImageVisualization)`
  216. height: 100%;
  217. z-index: 1;
  218. border: 0;
  219. `;