groupEventAttachments.tsx 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. import styled from '@emotion/styled';
  2. import {Flex} from 'sentry/components/container/flex';
  3. import EmptyStateWarning from 'sentry/components/emptyStateWarning';
  4. import LoadingError from 'sentry/components/loadingError';
  5. import LoadingIndicator from 'sentry/components/loadingIndicator';
  6. import Pagination from 'sentry/components/pagination';
  7. import {IconFilter} from 'sentry/icons';
  8. import {t} from 'sentry/locale';
  9. import {space} from 'sentry/styles/space';
  10. import type {Group, IssueAttachment} from 'sentry/types/group';
  11. import type {Project} from 'sentry/types/project';
  12. import {useLocation} from 'sentry/utils/useLocation';
  13. import useOrganization from 'sentry/utils/useOrganization';
  14. import {useEventQuery} from 'sentry/views/issueDetails/streamline/eventSearch';
  15. import {useIssueDetailsEventView} from 'sentry/views/issueDetails/streamline/hooks/useIssueDetailsDiscoverQuery';
  16. import {useHasStreamlinedUI} from 'sentry/views/issueDetails/utils';
  17. import GroupEventAttachmentsFilter, {
  18. EventAttachmentFilter,
  19. } from './groupEventAttachmentsFilter';
  20. import GroupEventAttachmentsTable from './groupEventAttachmentsTable';
  21. import {ScreenshotCard} from './screenshotCard';
  22. import {useDeleteGroupEventAttachment} from './useDeleteGroupEventAttachment';
  23. import {useGroupEventAttachments} from './useGroupEventAttachments';
  24. type GroupEventAttachmentsProps = {
  25. group: Group;
  26. project: Project;
  27. };
  28. function GroupEventAttachments({project, group}: GroupEventAttachmentsProps) {
  29. const location = useLocation();
  30. const organization = useOrganization();
  31. const hasStreamlinedUI = useHasStreamlinedUI();
  32. const eventQuery = useEventQuery({groupId: group.id});
  33. const eventView = useIssueDetailsEventView({group});
  34. const activeAttachmentsTab =
  35. (location.query.attachmentFilter as EventAttachmentFilter | undefined) ??
  36. EventAttachmentFilter.ALL;
  37. const {attachments, isPending, isError, getResponseHeader, refetch} =
  38. useGroupEventAttachments({
  39. group,
  40. activeAttachmentsTab,
  41. });
  42. const {mutate: deleteAttachment} = useDeleteGroupEventAttachment();
  43. const handleDelete = (attachment: IssueAttachment) => {
  44. deleteAttachment({
  45. attachment,
  46. projectSlug: project.slug,
  47. activeAttachmentsTab,
  48. group,
  49. orgSlug: organization.slug,
  50. cursor: location.query.cursor as string | undefined,
  51. // We only want to filter by date/query/environment if we're using the Streamlined UI
  52. environment: hasStreamlinedUI ? (eventView.environment as string[]) : undefined,
  53. start: hasStreamlinedUI ? eventView.start : undefined,
  54. end: hasStreamlinedUI ? eventView.end : undefined,
  55. statsPeriod: hasStreamlinedUI ? eventView.statsPeriod : undefined,
  56. eventQuery: hasStreamlinedUI ? eventQuery : undefined,
  57. });
  58. };
  59. const renderAttachmentsTable = () => {
  60. if (isError) {
  61. return <LoadingError onRetry={refetch} message={t('Error loading attachments')} />;
  62. }
  63. return (
  64. <GroupEventAttachmentsTable
  65. isLoading={isPending}
  66. attachments={attachments}
  67. projectSlug={project.slug}
  68. groupId={group.id}
  69. onDelete={handleDelete}
  70. emptyMessage={
  71. activeAttachmentsTab === EventAttachmentFilter.CRASH_REPORTS
  72. ? t('No matching crash reports found')
  73. : t('No matching attachments found')
  74. }
  75. />
  76. );
  77. };
  78. const renderScreenshotGallery = () => {
  79. if (isError) {
  80. return <LoadingError onRetry={refetch} message={t('Error loading screenshots')} />;
  81. }
  82. if (isPending) {
  83. return <LoadingIndicator />;
  84. }
  85. if (attachments.length > 0) {
  86. return (
  87. <ScreenshotGrid>
  88. {attachments.map(screenshot => {
  89. return (
  90. <ScreenshotCard
  91. key={screenshot.id}
  92. eventAttachment={screenshot}
  93. eventId={screenshot.event_id}
  94. projectSlug={project.slug}
  95. groupId={group.id}
  96. onDelete={handleDelete}
  97. attachments={attachments}
  98. />
  99. );
  100. })}
  101. </ScreenshotGrid>
  102. );
  103. }
  104. return (
  105. <EmptyStateWarning>
  106. <p>{t('No screenshots found')}</p>
  107. </EmptyStateWarning>
  108. );
  109. };
  110. return (
  111. <Wrapper>
  112. {hasStreamlinedUI ? (
  113. <Flex justify="space-between">
  114. <FilterMessage align="center" gap={space(1)}>
  115. <IconFilter size="xs" />
  116. {t('Results are filtered by the selections above.')}
  117. </FilterMessage>
  118. <GroupEventAttachmentsFilter />
  119. </Flex>
  120. ) : (
  121. <GroupEventAttachmentsFilter />
  122. )}
  123. {activeAttachmentsTab === EventAttachmentFilter.SCREENSHOT
  124. ? renderScreenshotGallery()
  125. : renderAttachmentsTable()}
  126. <NoMarginPagination pageLinks={getResponseHeader?.('Link')} />
  127. </Wrapper>
  128. );
  129. }
  130. export default GroupEventAttachments;
  131. const ScreenshotGrid = styled('div')`
  132. display: grid;
  133. grid-template-columns: minmax(100px, 1fr);
  134. grid-template-rows: repeat(2, max-content);
  135. gap: ${space(2)};
  136. @media (min-width: ${p => p.theme.breakpoints.small}) {
  137. grid-template-columns: repeat(3, minmax(100px, 1fr));
  138. }
  139. @media (min-width: ${p => p.theme.breakpoints.xlarge}) {
  140. grid-template-columns: repeat(4, minmax(100px, 1fr));
  141. }
  142. @media (min-width: ${p => p.theme.breakpoints.xxlarge}) {
  143. grid-template-columns: repeat(6, minmax(100px, 1fr));
  144. }
  145. `;
  146. const NoMarginPagination = styled(Pagination)`
  147. margin: 0;
  148. `;
  149. const Wrapper = styled('div')`
  150. display: flex;
  151. flex-direction: column;
  152. gap: ${space(2)};
  153. `;
  154. const FilterMessage = styled(Flex)``;