index.tsx 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. import type {RouteComponentProps} from 'react-router';
  2. import styled from '@emotion/styled';
  3. import {withProfiler} from '@sentry/react';
  4. import omit from 'lodash/omit';
  5. import {Button} from 'sentry/components/button';
  6. import {EventUserFeedback} from 'sentry/components/events/userFeedback';
  7. import CompactIssue from 'sentry/components/issues/compactIssue';
  8. import * as Layout from 'sentry/components/layouts/thirds';
  9. import LoadingError from 'sentry/components/loadingError';
  10. import LoadingIndicator from 'sentry/components/loadingIndicator';
  11. import NoProjectMessage from 'sentry/components/noProjectMessage';
  12. import {DatePageFilter} from 'sentry/components/organizations/datePageFilter';
  13. import {EnvironmentPageFilter} from 'sentry/components/organizations/environmentPageFilter';
  14. import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
  15. import PageFiltersContainer from 'sentry/components/organizations/pageFilters/container';
  16. import {ProjectPageFilter} from 'sentry/components/organizations/projectPageFilter';
  17. import {PageHeadingQuestionTooltip} from 'sentry/components/pageHeadingQuestionTooltip';
  18. import Pagination from 'sentry/components/pagination';
  19. import Panel from 'sentry/components/panels/panel';
  20. import {SegmentedControl} from 'sentry/components/segmentedControl';
  21. import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
  22. import {Tooltip} from 'sentry/components/tooltip';
  23. import {t} from 'sentry/locale';
  24. import {space} from 'sentry/styles/space';
  25. import type {UserReport} from 'sentry/types';
  26. import {useApiQuery} from 'sentry/utils/queryClient';
  27. import useOrganization from 'sentry/utils/useOrganization';
  28. import {normalizeUrl} from 'sentry/utils/withDomainRequired';
  29. import {UserFeedbackEmpty} from './userFeedbackEmpty';
  30. import {getQuery} from './utils';
  31. interface Props extends RouteComponentProps<{}, {}> {}
  32. function OrganizationUserFeedback({location: {search, pathname, query}, router}: Props) {
  33. const organization = useOrganization();
  34. const {status} = getQuery(search);
  35. const unresolvedQuery = omit(query, 'status');
  36. const allIssuesQuery = {...query, status: ''};
  37. const hasNewFeedback = organization.features.includes('user-feedback-ui');
  38. const {
  39. data: reportList,
  40. isLoading,
  41. isError,
  42. getResponseHeader,
  43. } = useApiQuery<UserReport[]>(
  44. [
  45. `/organizations/${organization.slug}/user-feedback/`,
  46. {
  47. query: getQuery(search),
  48. },
  49. ],
  50. {staleTime: 0}
  51. );
  52. const reportListsPageLinks = getResponseHeader?.('Link');
  53. function getProjectIds() {
  54. const {project} = query;
  55. return Array.isArray(project)
  56. ? project
  57. : typeof project === 'string'
  58. ? [project]
  59. : [];
  60. }
  61. function StreamBody() {
  62. if (isError) {
  63. return <LoadingError />;
  64. }
  65. if (isLoading) {
  66. return (
  67. <Panel>
  68. <LoadingIndicator />
  69. </Panel>
  70. );
  71. }
  72. if (!reportList?.length) {
  73. return <UserFeedbackEmpty projectIds={getProjectIds()} issueTab={false} />;
  74. }
  75. return (
  76. <Panel className="issue-list" data-test-id="user-feedback-list">
  77. {reportList.map(item => {
  78. const issue = item.issue;
  79. return (
  80. <CompactIssue key={item.id} id={issue.id} data={issue} eventId={item.eventID}>
  81. <StyledEventUserFeedback
  82. report={item}
  83. orgSlug={organization.slug}
  84. issueId={issue.id}
  85. />
  86. </CompactIssue>
  87. );
  88. })}
  89. </Panel>
  90. );
  91. }
  92. return (
  93. <SentryDocumentTitle title={`${t('User Feedback')} - ${organization.slug}`}>
  94. <PageFiltersContainer>
  95. <NoProjectMessage organization={organization}>
  96. <Layout.Header>
  97. <Layout.HeaderContent>
  98. <Layout.Title>
  99. {t('User Feedback')}
  100. <PageHeadingQuestionTooltip
  101. docsUrl="https://docs.sentry.io/product/user-feedback/"
  102. title={t(
  103. 'Feedback submitted by users who experienced an error while using your application, including their name, email address, and any additional comments.'
  104. )}
  105. />
  106. </Layout.Title>
  107. </Layout.HeaderContent>
  108. {hasNewFeedback && (
  109. <Layout.HeaderActions>
  110. <Tooltip
  111. title={t('Go back to the new feedback layout.')}
  112. position="left"
  113. isHoverable
  114. >
  115. <Button
  116. size="sm"
  117. priority="default"
  118. to={{
  119. pathname: normalizeUrl(
  120. `/organizations/${organization.slug}/feedback/`
  121. ),
  122. query: {
  123. ...query,
  124. query: undefined,
  125. cursor: undefined,
  126. },
  127. }}
  128. >
  129. {t('Go to New User Feedback')}
  130. </Button>
  131. </Tooltip>
  132. </Layout.HeaderActions>
  133. )}
  134. </Layout.Header>
  135. <Layout.Body data-test-id="user-feedback">
  136. <Layout.Main fullWidth>
  137. <Filters>
  138. <PageFilterBar>
  139. <ProjectPageFilter />
  140. <EnvironmentPageFilter />
  141. <DatePageFilter position="bottom-end" />
  142. </PageFilterBar>
  143. <SegmentedControl
  144. aria-label={t('Issue Status')}
  145. value={!Array.isArray(status) ? status || '' : ''}
  146. onChange={key =>
  147. router.replace({
  148. pathname,
  149. query: key === 'unresolved' ? unresolvedQuery : allIssuesQuery,
  150. })
  151. }
  152. >
  153. <SegmentedControl.Item key="unresolved">
  154. {t('Unresolved')}
  155. </SegmentedControl.Item>
  156. <SegmentedControl.Item key="">{t('All Issues')}</SegmentedControl.Item>
  157. </SegmentedControl>
  158. </Filters>
  159. <StreamBody />
  160. <Pagination pageLinks={reportListsPageLinks} />
  161. </Layout.Main>
  162. </Layout.Body>
  163. </NoProjectMessage>
  164. </PageFiltersContainer>
  165. </SentryDocumentTitle>
  166. );
  167. }
  168. export default withProfiler(OrganizationUserFeedback);
  169. const Filters = styled('div')`
  170. display: grid;
  171. grid-template-columns: minmax(0, max-content) max-content;
  172. justify-content: start;
  173. gap: ${space(2)};
  174. margin-bottom: ${space(2)};
  175. @media (max-width: ${p => p.theme.breakpoints.medium}) {
  176. grid-template-columns: minmax(0, 1fr) max-content;
  177. }
  178. @media (max-width: ${p => p.theme.breakpoints.small}) {
  179. grid-template-columns: minmax(0, 1fr);
  180. }
  181. `;
  182. const StyledEventUserFeedback = styled(EventUserFeedback)`
  183. margin: ${space(2)} 0;
  184. `;