Files
2024-02-15 17:01:50 +00:00

228 lines
7.3 KiB
TypeScript

import type {RouteComponentProps} from 'react-router';
import styled from '@emotion/styled';
import {withProfiler} from '@sentry/react';
import omit from 'lodash/omit';
import {Button} from 'sentry/components/button';
import {EventUserFeedback} from 'sentry/components/events/userFeedback';
import CompactIssue from 'sentry/components/issues/compactIssue';
import * as Layout from 'sentry/components/layouts/thirds';
import LoadingIndicator from 'sentry/components/loadingIndicator';
import NoProjectMessage from 'sentry/components/noProjectMessage';
import {DatePageFilter} from 'sentry/components/organizations/datePageFilter';
import {EnvironmentPageFilter} from 'sentry/components/organizations/environmentPageFilter';
import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
import PageFiltersContainer from 'sentry/components/organizations/pageFilters/container';
import {ProjectPageFilter} from 'sentry/components/organizations/projectPageFilter';
import {PageHeadingQuestionTooltip} from 'sentry/components/pageHeadingQuestionTooltip';
import Pagination from 'sentry/components/pagination';
import Panel from 'sentry/components/panels/panel';
import {SegmentedControl} from 'sentry/components/segmentedControl';
import {Tooltip} from 'sentry/components/tooltip';
import {t} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import type {Organization, UserReport} from 'sentry/types';
import {normalizeUrl} from 'sentry/utils/withDomainRequired';
import withOrganization from 'sentry/utils/withOrganization';
import type {AsyncViewState} from 'sentry/views/deprecatedAsyncView';
import DeprecatedAsyncView from 'sentry/views/deprecatedAsyncView';
import {UserFeedbackEmpty} from './userFeedbackEmpty';
import {getQuery} from './utils';
interface State extends AsyncViewState {
reportList: UserReport[];
}
interface Props extends RouteComponentProps<{}, {}> {
organization: Organization;
}
class OrganizationUserFeedback extends DeprecatedAsyncView<Props, State> {
getEndpoints(): ReturnType<DeprecatedAsyncView['getEndpoints']> {
const {
organization,
location: {search},
} = this.props;
return [
[
'reportList',
`/organizations/${organization.slug}/user-feedback/`,
{
query: getQuery(search),
},
],
];
}
getTitle() {
return `${t('User Feedback')} - ${this.props.organization.slug}`;
}
get projectIds() {
const {project} = this.props.location.query;
return Array.isArray(project)
? project
: typeof project === 'string'
? [project]
: [];
}
renderResults() {
const {organization} = this.props;
return (
<Panel className="issue-list" data-test-id="user-feedback-list">
{this.state.reportList.map(item => {
const issue = item.issue;
return (
<CompactIssue key={item.id} id={issue.id} data={issue} eventId={item.eventID}>
<StyledEventUserFeedback
report={item}
orgSlug={organization.slug}
issueId={issue.id}
/>
</CompactIssue>
);
})}
</Panel>
);
}
renderEmpty() {
return <UserFeedbackEmpty projectIds={this.projectIds} />;
}
renderLoading() {
return this.renderBody();
}
renderStreamBody() {
const {loading, reportList} = this.state;
if (loading) {
return (
<Panel>
<LoadingIndicator />
</Panel>
);
}
if (!reportList.length) {
return this.renderEmpty();
}
return this.renderResults();
}
renderBody() {
const {organization, router} = this.props;
const {location} = this.props;
const {pathname, search, query} = location;
const {status} = getQuery(search);
const {reportListPageLinks} = this.state;
const unresolvedQuery = omit(query, 'status');
const allIssuesQuery = {...query, status: ''};
const hasNewFeedback = organization.features.includes('user-feedback-ui');
return (
<PageFiltersContainer>
<NoProjectMessage organization={organization}>
<Layout.Header>
<Layout.HeaderContent>
<Layout.Title>
{t('User Feedback')}
<PageHeadingQuestionTooltip
docsUrl="https://docs.sentry.io/product/user-feedback/"
title={t(
'Feedback submitted by users who experienced an error while using your application, including their name, email address, and any additional comments.'
)}
/>
</Layout.Title>
</Layout.HeaderContent>
{hasNewFeedback && (
<Layout.HeaderActions>
<Tooltip
title={t('Go back to the new feedback layout.')}
position="left"
isHoverable
>
<Button
size="sm"
priority="default"
to={{
pathname: normalizeUrl(
`/organizations/${organization.slug}/feedback/`
),
query: {
...location.query,
query: undefined,
cursor: undefined,
},
}}
>
{t('Go to New User Feedback')}
</Button>
</Tooltip>
</Layout.HeaderActions>
)}
</Layout.Header>
<Layout.Body data-test-id="user-feedback">
<Layout.Main fullWidth>
<Filters>
<PageFilterBar>
<ProjectPageFilter />
<EnvironmentPageFilter />
<DatePageFilter position="bottom-end" />
</PageFilterBar>
<SegmentedControl
aria-label={t('Issue Status')}
value={!Array.isArray(status) ? status || '' : ''}
onChange={key =>
router.replace({
pathname,
query: key === 'unresolved' ? unresolvedQuery : allIssuesQuery,
})
}
>
<SegmentedControl.Item key="unresolved">
{t('Unresolved')}
</SegmentedControl.Item>
<SegmentedControl.Item key="">{t('All Issues')}</SegmentedControl.Item>
</SegmentedControl>
</Filters>
{this.renderStreamBody()}
<Pagination pageLinks={reportListPageLinks} />
</Layout.Main>
</Layout.Body>
</NoProjectMessage>
</PageFiltersContainer>
);
}
}
export default withOrganization(withProfiler(OrganizationUserFeedback));
const Filters = styled('div')`
display: grid;
grid-template-columns: minmax(0, max-content) max-content;
justify-content: start;
gap: ${space(2)};
margin-bottom: ${space(2)};
@media (max-width: ${p => p.theme.breakpoints.medium}) {
grid-template-columns: minmax(0, 1fr) max-content;
}
@media (max-width: ${p => p.theme.breakpoints.small}) {
grid-template-columns: minmax(0, 1fr);
}
`;
const StyledEventUserFeedback = styled(EventUserFeedback)`
margin: ${space(2)} 0;
`;