screenshotCard.tsx 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. import {useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import {openModal} from 'sentry/actionCreators/modal';
  4. import {Button} from 'sentry/components/button';
  5. import Card from 'sentry/components/card';
  6. import {openConfirmModal} from 'sentry/components/confirm';
  7. import {DateTime} from 'sentry/components/dateTime';
  8. import {DropdownMenu} from 'sentry/components/dropdownMenu';
  9. import ImageVisualization from 'sentry/components/events/eventTagsAndScreenshot/screenshot/imageVisualization';
  10. import ScreenshotModal, {
  11. modalCss,
  12. } from 'sentry/components/events/eventTagsAndScreenshot/screenshot/modal';
  13. import {LazyRender} from 'sentry/components/lazyRender';
  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 {Tooltip} from 'sentry/components/tooltip';
  18. import {IconEllipsis} from 'sentry/icons/iconEllipsis';
  19. import {t} from 'sentry/locale';
  20. import {space} from 'sentry/styles/space';
  21. import type {IssueAttachment} from 'sentry/types/group';
  22. import type {Project} from 'sentry/types/project';
  23. import {trackAnalytics} from 'sentry/utils/analytics';
  24. import {getShortEventId} from 'sentry/utils/events';
  25. import useOrganization from 'sentry/utils/useOrganization';
  26. type Props = {
  27. attachments: IssueAttachment[];
  28. eventAttachment: IssueAttachment;
  29. eventId: string;
  30. groupId: string;
  31. onDelete: (attachment: IssueAttachment) => void;
  32. projectSlug: Project['slug'];
  33. };
  34. export function ScreenshotCard({
  35. eventAttachment,
  36. attachments,
  37. groupId,
  38. projectSlug,
  39. eventId,
  40. onDelete,
  41. }: Props) {
  42. const organization = useOrganization();
  43. const [loadingImage, setLoadingImage] = useState(true);
  44. const downloadUrl = `/api/0/projects/${organization.slug}/${projectSlug}/events/${eventId}/attachments/${eventAttachment.id}/?download=1`;
  45. function handleDelete() {
  46. trackAnalytics('issue_details.attachment_tab.screenshot_modal_deleted', {
  47. organization,
  48. });
  49. onDelete(eventAttachment);
  50. }
  51. function openVisualizationModal() {
  52. trackAnalytics('issue_details.attachment_tab.screenshot_modal_opened', {
  53. organization,
  54. });
  55. openModal(
  56. modalProps => (
  57. <ScreenshotModal
  58. {...modalProps}
  59. projectSlug={projectSlug}
  60. groupId={groupId}
  61. eventAttachment={eventAttachment}
  62. downloadUrl={downloadUrl}
  63. onDelete={handleDelete}
  64. attachments={attachments}
  65. onDownload={() =>
  66. trackAnalytics('issue_details.attachment_tab.screenshot_modal_download', {
  67. organization,
  68. })
  69. }
  70. />
  71. ),
  72. {modalCss}
  73. );
  74. }
  75. return (
  76. <StyledCard>
  77. <CardHeader>
  78. <div>
  79. <AttachmentName>{eventAttachment.name}</AttachmentName>
  80. <div>
  81. <DateTime date={eventAttachment.dateCreated} /> &middot;{' '}
  82. <Link
  83. to={`/organizations/${organization.slug}/issues/${groupId}/events/${eventAttachment.event_id}/`}
  84. >
  85. <Tooltip skipWrapper title={t('View Event')}>
  86. {getShortEventId(eventAttachment.event_id)}
  87. </Tooltip>
  88. </Link>
  89. </div>
  90. </div>
  91. <DropdownMenu
  92. items={[
  93. {
  94. key: 'download',
  95. label: t('Download'),
  96. onAction: () => {
  97. window.open(downloadUrl, '_blank');
  98. },
  99. },
  100. {
  101. key: 'delete',
  102. label: t('Delete'),
  103. onAction: () => {
  104. openConfirmModal({
  105. onConfirm: () => onDelete(eventAttachment),
  106. message: <h6>{t('Are you sure you want to delete this image?')}</h6>,
  107. priority: 'danger',
  108. confirmText: t('Delete'),
  109. });
  110. },
  111. },
  112. ]}
  113. position="bottom-end"
  114. trigger={triggerProps => (
  115. <Button
  116. {...triggerProps}
  117. aria-label={t('Actions')}
  118. size="xs"
  119. borderless
  120. icon={<IconEllipsis direction="down" size="sm" />}
  121. />
  122. )}
  123. />
  124. </CardHeader>
  125. <CardBody>
  126. <StyledPanelBody
  127. onClick={() => openVisualizationModal()}
  128. data-test-id={`screenshot-${eventAttachment.id}`}
  129. >
  130. <LazyRender containerHeight={250} withoutContainer>
  131. <StyledImageVisualization
  132. attachment={eventAttachment}
  133. orgSlug={organization.slug}
  134. projectSlug={projectSlug}
  135. eventId={eventId}
  136. onLoad={() => setLoadingImage(false)}
  137. onError={() => setLoadingImage(false)}
  138. />
  139. {loadingImage && (
  140. <StyledLoadingIndicator>
  141. <LoadingIndicator mini />
  142. </StyledLoadingIndicator>
  143. )}
  144. </LazyRender>
  145. </StyledPanelBody>
  146. </CardBody>
  147. </StyledCard>
  148. );
  149. }
  150. const StyledCard = styled(Card)`
  151. margin: 0;
  152. `;
  153. const AttachmentName = styled('div')`
  154. font-weight: bold;
  155. ${p => p.theme.overflowEllipsis};
  156. `;
  157. const CardHeader = styled('div')`
  158. display: flex;
  159. justify-content: space-between;
  160. padding: ${space(1.5)} ${space(1.5)} ${space(1.5)} ${space(2)};
  161. `;
  162. const CardBody = styled('div')`
  163. background: ${p => p.theme.gray100};
  164. padding: ${space(1)} ${space(1.5)};
  165. max-height: 250px;
  166. min-height: 250px;
  167. overflow: hidden;
  168. border-bottom: 1px solid ${p => p.theme.gray100};
  169. `;
  170. const StyledPanelBody = styled(PanelBody)`
  171. height: 100%;
  172. min-height: 48px;
  173. overflow: hidden;
  174. cursor: pointer;
  175. position: relative;
  176. display: flex;
  177. align-items: center;
  178. justify-content: center;
  179. flex: 1;
  180. `;
  181. const StyledLoadingIndicator = styled('div')`
  182. align-self: center;
  183. `;
  184. const StyledImageVisualization = styled(ImageVisualization)`
  185. height: 100%;
  186. z-index: 1;
  187. border: 0;
  188. `;