groupEventAttachments.tsx 6.5 KB

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