groupEventAttachments.tsx 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. import {WithRouterProps} from 'react-router';
  2. import styled from '@emotion/styled';
  3. import pick from 'lodash/pick';
  4. import xor from 'lodash/xor';
  5. import {addErrorMessage} from 'sentry/actionCreators/indicator';
  6. import DeprecatedAsyncComponent from 'sentry/components/deprecatedAsyncComponent';
  7. import EmptyStateWarning from 'sentry/components/emptyStateWarning';
  8. import * as Layout from 'sentry/components/layouts/thirds';
  9. import LoadingIndicator from 'sentry/components/loadingIndicator';
  10. import Pagination from 'sentry/components/pagination';
  11. import Panel from 'sentry/components/panels/panel';
  12. import PanelBody from 'sentry/components/panels/panelBody';
  13. import {t} from 'sentry/locale';
  14. import {space} from 'sentry/styles/space';
  15. import {IssueAttachment, Project} from 'sentry/types';
  16. import {decodeList} from 'sentry/utils/queryString';
  17. // eslint-disable-next-line no-restricted-imports
  18. import withSentryRouter from 'sentry/utils/withSentryRouter';
  19. import GroupEventAttachmentsFilter, {
  20. crashReportTypes,
  21. SCREENSHOT_TYPE,
  22. } from './groupEventAttachmentsFilter';
  23. import GroupEventAttachmentsTable from './groupEventAttachmentsTable';
  24. import {ScreenshotCard} from './screenshotCard';
  25. type Props = {
  26. project: Project;
  27. } & WithRouterProps<{groupId: string; orgId: string}> &
  28. DeprecatedAsyncComponent['props'];
  29. enum EventAttachmentFilter {
  30. ALL = 'all',
  31. CRASH_REPORTS = 'onlyCrash',
  32. SCREENSHOTS = 'screenshot',
  33. }
  34. type State = {
  35. deletedAttachments: string[];
  36. eventAttachments?: IssueAttachment[];
  37. } & DeprecatedAsyncComponent['state'];
  38. export const MAX_SCREENSHOTS_PER_PAGE = 12;
  39. class GroupEventAttachments extends DeprecatedAsyncComponent<Props, State> {
  40. getDefaultState() {
  41. return {
  42. ...super.getDefaultState(),
  43. deletedAttachments: [],
  44. };
  45. }
  46. getActiveAttachmentsTab() {
  47. const {location} = this.props;
  48. const types = decodeList(location.query.types);
  49. if (types.length === 0) {
  50. return EventAttachmentFilter.ALL;
  51. }
  52. if (types[0] === SCREENSHOT_TYPE) {
  53. return EventAttachmentFilter.SCREENSHOTS;
  54. }
  55. if (xor(crashReportTypes, types).length === 0) {
  56. return EventAttachmentFilter.CRASH_REPORTS;
  57. }
  58. return EventAttachmentFilter.ALL;
  59. }
  60. getEndpoints(): ReturnType<DeprecatedAsyncComponent['getEndpoints']> {
  61. const {params, location} = this.props;
  62. if (this.getActiveAttachmentsTab() === EventAttachmentFilter.SCREENSHOTS) {
  63. return [
  64. [
  65. 'eventAttachments',
  66. `/issues/${params.groupId}/attachments/`,
  67. {
  68. query: {
  69. ...location.query,
  70. types: undefined, // need to explicitly set this to undefined because AsyncComponent adds location query back into the params
  71. screenshot: 1,
  72. per_page: MAX_SCREENSHOTS_PER_PAGE,
  73. },
  74. },
  75. ],
  76. ];
  77. }
  78. return [
  79. [
  80. 'eventAttachments',
  81. `/issues/${params.groupId}/attachments/`,
  82. {
  83. query: {
  84. ...pick(location.query, ['cursor', 'environment', 'types']),
  85. per_page: 50,
  86. },
  87. },
  88. ],
  89. ];
  90. }
  91. handleDelete = async (deletedAttachmentId: string) => {
  92. const {params, project} = this.props;
  93. const attachment = this.state?.eventAttachments?.find(
  94. item => item.id === deletedAttachmentId
  95. );
  96. if (!attachment) {
  97. return;
  98. }
  99. this.setState(prevState => ({
  100. deletedAttachments: [...prevState.deletedAttachments, deletedAttachmentId],
  101. }));
  102. try {
  103. await this.api.requestPromise(
  104. `/projects/${params.orgId}/${project.slug}/events/${attachment.event_id}/attachments/${attachment.id}/`,
  105. {
  106. method: 'DELETE',
  107. }
  108. );
  109. } catch (error) {
  110. addErrorMessage('An error occurred while deleteting the attachment');
  111. }
  112. };
  113. renderNoQueryResults() {
  114. return (
  115. <EmptyStateWarning>
  116. <p>{t('No crash reports found')}</p>
  117. </EmptyStateWarning>
  118. );
  119. }
  120. renderNoScreenshotsResults() {
  121. return (
  122. <EmptyStateWarning>
  123. <p>{t('No screenshots found')}</p>
  124. </EmptyStateWarning>
  125. );
  126. }
  127. renderEmpty() {
  128. return (
  129. <EmptyStateWarning>
  130. <p>{t('No attachments found')}</p>
  131. </EmptyStateWarning>
  132. );
  133. }
  134. renderLoading() {
  135. return this.renderBody();
  136. }
  137. renderInnerBody() {
  138. const {project, params} = this.props;
  139. const {loading, eventAttachments, deletedAttachments} = this.state;
  140. if (loading) {
  141. return <LoadingIndicator />;
  142. }
  143. if (eventAttachments && eventAttachments.length > 0) {
  144. return (
  145. <GroupEventAttachmentsTable
  146. attachments={eventAttachments}
  147. orgId={params.orgId}
  148. projectSlug={project.slug}
  149. groupId={params.groupId}
  150. onDelete={this.handleDelete}
  151. deletedAttachments={deletedAttachments}
  152. />
  153. );
  154. }
  155. if (this.getActiveAttachmentsTab() === EventAttachmentFilter.CRASH_REPORTS) {
  156. return this.renderNoQueryResults();
  157. }
  158. return this.renderEmpty();
  159. }
  160. renderScreenshotGallery() {
  161. const {eventAttachments, loading} = this.state;
  162. const {project, params} = this.props;
  163. if (loading) {
  164. return <LoadingIndicator />;
  165. }
  166. if (eventAttachments && eventAttachments.length > 0) {
  167. return (
  168. <ScreenshotGrid>
  169. {eventAttachments?.map((screenshot, index) => {
  170. return (
  171. <ScreenshotCard
  172. key={`${index}-${screenshot.id}`}
  173. eventAttachment={screenshot}
  174. eventId={screenshot.event_id}
  175. projectSlug={project.slug}
  176. groupId={params.groupId}
  177. onDelete={this.handleDelete}
  178. pageLinks={this.state.eventAttachmentsPageLinks}
  179. attachments={eventAttachments}
  180. attachmentIndex={index}
  181. />
  182. );
  183. })}
  184. </ScreenshotGrid>
  185. );
  186. }
  187. return this.renderNoScreenshotsResults();
  188. }
  189. renderAttachmentsTable() {
  190. return (
  191. <Panel className="event-list">
  192. <PanelBody>{this.renderInnerBody()}</PanelBody>
  193. </Panel>
  194. );
  195. }
  196. renderBody() {
  197. const {project} = this.props;
  198. return (
  199. <Layout.Body>
  200. <Layout.Main fullWidth>
  201. <GroupEventAttachmentsFilter project={project} />
  202. {this.getActiveAttachmentsTab() === EventAttachmentFilter.SCREENSHOTS
  203. ? this.renderScreenshotGallery()
  204. : this.renderAttachmentsTable()}
  205. <Pagination pageLinks={this.state.eventAttachmentsPageLinks} />
  206. </Layout.Main>
  207. </Layout.Body>
  208. );
  209. }
  210. }
  211. export default withSentryRouter(GroupEventAttachments);
  212. const ScreenshotGrid = styled('div')`
  213. display: grid;
  214. grid-template-columns: minmax(100px, 1fr);
  215. grid-template-rows: repeat(2, max-content);
  216. gap: ${space(2)};
  217. @media (min-width: ${p => p.theme.breakpoints.small}) {
  218. grid-template-columns: repeat(3, minmax(100px, 1fr));
  219. }
  220. @media (min-width: ${p => p.theme.breakpoints.large}) {
  221. grid-template-columns: repeat(4, minmax(100px, 1fr));
  222. }
  223. @media (min-width: ${p => p.theme.breakpoints.xxlarge}) {
  224. grid-template-columns: repeat(6, minmax(100px, 1fr));
  225. }
  226. `;