index.tsx 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. import {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 ButtonBar from 'sentry/components/buttonBar';
  7. import DatePageFilter from 'sentry/components/datePageFilter';
  8. import EnvironmentPageFilter from 'sentry/components/environmentPageFilter';
  9. import EventUserFeedback from 'sentry/components/events/userFeedback';
  10. import CompactIssue from 'sentry/components/issues/compactIssue';
  11. import LoadingIndicator from 'sentry/components/loadingIndicator';
  12. import NoProjectMessage from 'sentry/components/noProjectMessage';
  13. import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
  14. import PageFiltersContainer from 'sentry/components/organizations/pageFilters/container';
  15. import PageHeading from 'sentry/components/pageHeading';
  16. import Pagination from 'sentry/components/pagination';
  17. import {Panel} from 'sentry/components/panels';
  18. import ProjectPageFilter from 'sentry/components/projectPageFilter';
  19. import {t} from 'sentry/locale';
  20. import {PageContent} from 'sentry/styles/organization';
  21. import space from 'sentry/styles/space';
  22. import {Organization, UserReport} from 'sentry/types';
  23. import withOrganization from 'sentry/utils/withOrganization';
  24. import AsyncView from 'sentry/views/asyncView';
  25. import {UserFeedbackEmpty} from './userFeedbackEmpty';
  26. import {getQuery} from './utils';
  27. type State = AsyncView['state'] & {
  28. reportList: UserReport[];
  29. };
  30. type Props = RouteComponentProps<{orgId: string}, {}> & {
  31. organization: Organization;
  32. };
  33. class OrganizationUserFeedback extends AsyncView<Props, State> {
  34. getEndpoints(): ReturnType<AsyncView['getEndpoints']> {
  35. const {
  36. organization,
  37. location: {search},
  38. } = this.props;
  39. return [
  40. [
  41. 'reportList',
  42. `/organizations/${organization.slug}/user-feedback/`,
  43. {
  44. query: getQuery(search),
  45. },
  46. ],
  47. ];
  48. }
  49. getTitle() {
  50. return `${t('User Feedback')} - ${this.props.organization.slug}`;
  51. }
  52. get projectIds() {
  53. const {project} = this.props.location.query;
  54. return Array.isArray(project)
  55. ? project
  56. : typeof project === 'string'
  57. ? [project]
  58. : [];
  59. }
  60. renderResults() {
  61. const {orgId} = this.props.params;
  62. return (
  63. <Panel className="issue-list" data-test-id="user-feedback-list">
  64. {this.state.reportList.map(item => {
  65. const issue = item.issue;
  66. return (
  67. <CompactIssue key={item.id} id={issue.id} data={issue} eventId={item.eventID}>
  68. <StyledEventUserFeedback report={item} orgId={orgId} issueId={issue.id} />
  69. </CompactIssue>
  70. );
  71. })}
  72. </Panel>
  73. );
  74. }
  75. renderEmpty() {
  76. return <UserFeedbackEmpty projectIds={this.projectIds} />;
  77. }
  78. renderLoading() {
  79. return this.renderBody();
  80. }
  81. renderStreamBody() {
  82. const {loading, reportList} = this.state;
  83. if (loading) {
  84. return (
  85. <Panel>
  86. <LoadingIndicator />
  87. </Panel>
  88. );
  89. }
  90. if (!reportList.length) {
  91. return this.renderEmpty();
  92. }
  93. return this.renderResults();
  94. }
  95. renderBody() {
  96. const {organization} = this.props;
  97. const {location} = this.props;
  98. const {pathname, search, query} = location;
  99. const {status} = getQuery(search);
  100. const {reportListPageLinks} = this.state;
  101. const unresolvedQuery = omit(query, 'status');
  102. const allIssuesQuery = {...query, status: ''};
  103. return (
  104. <PageFiltersContainer>
  105. <PageContent>
  106. <NoProjectMessage organization={organization}>
  107. <div data-test-id="user-feedback">
  108. <Header>
  109. <PageHeading>{t('User Feedback')}</PageHeading>
  110. </Header>
  111. <Filters>
  112. <PageFilterBar>
  113. <ProjectPageFilter />
  114. <EnvironmentPageFilter />
  115. <DatePageFilter alignDropdown="right" />
  116. </PageFilterBar>
  117. <ButtonBar active={!Array.isArray(status) ? status || '' : ''} merged>
  118. <Button barId="unresolved" to={{pathname, query: unresolvedQuery}}>
  119. {t('Unresolved')}
  120. </Button>
  121. <Button barId="" to={{pathname, query: allIssuesQuery}}>
  122. {t('All Issues')}
  123. </Button>
  124. </ButtonBar>
  125. </Filters>
  126. {this.renderStreamBody()}
  127. <Pagination pageLinks={reportListPageLinks} />
  128. </div>
  129. </NoProjectMessage>
  130. </PageContent>
  131. </PageFiltersContainer>
  132. );
  133. }
  134. }
  135. export default withOrganization(withProfiler(OrganizationUserFeedback));
  136. const Header = styled('div')`
  137. display: flex;
  138. align-items: center;
  139. justify-content: space-between;
  140. margin-bottom: ${space(2)};
  141. `;
  142. const Filters = styled('div')`
  143. display: grid;
  144. grid-template-columns: minmax(0, max-content) max-content;
  145. justify-content: start;
  146. gap: ${space(2)};
  147. margin-bottom: ${space(2)};
  148. @media (max-width: ${p => p.theme.breakpoints.medium}) {
  149. grid-template-columns: minmax(0, 1fr) max-content;
  150. }
  151. @media (max-width: ${p => p.theme.breakpoints.small}) {
  152. grid-template-columns: minmax(0, 1fr);
  153. }
  154. `;
  155. const StyledEventUserFeedback = styled(EventUserFeedback)`
  156. margin: ${space(2)} 0 0;
  157. `;