issues.tsx 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. import {Fragment} from 'react';
  2. import {RouteComponentProps} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import * as Layout from 'sentry/components/layouts/thirds';
  5. import LoadingError from 'sentry/components/loadingError';
  6. import LoadingIndicator from 'sentry/components/loadingIndicator';
  7. import NoProjectMessage from 'sentry/components/noProjectMessage';
  8. import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
  9. import {t, tct} from 'sentry/locale';
  10. import {TeamWithProjects} from 'sentry/types';
  11. import localStorage from 'sentry/utils/localStorage';
  12. import useRouteAnalyticsEventNames from 'sentry/utils/routeAnalytics/useRouteAnalyticsEventNames';
  13. import useOrganization from 'sentry/utils/useOrganization';
  14. import {useUserTeams} from 'sentry/utils/useUserTeams';
  15. import Header from '../header';
  16. import TeamStatsControls from './controls';
  17. import DescriptionCard from './descriptionCard';
  18. import TeamIssuesAge from './teamIssuesAge';
  19. import TeamIssuesBreakdown from './teamIssuesBreakdown';
  20. import TeamResolutionTime from './teamResolutionTime';
  21. import {TeamUnresolvedIssues} from './teamUnresolvedIssues';
  22. import {dataDatetime} from './utils';
  23. type Props = RouteComponentProps<{}, {}>;
  24. function TeamStatsIssues({location, router}: Props) {
  25. const organization = useOrganization();
  26. const {teams, isLoading, isError} = useUserTeams();
  27. useRouteAnalyticsEventNames('team_insights.viewed', 'Team Insights: Viewed');
  28. const query = location?.query ?? {};
  29. const localStorageKey = `teamInsightsSelectedTeamId:${organization.slug}`;
  30. let localTeamId: string | null | undefined =
  31. query.team ?? localStorage.getItem(localStorageKey);
  32. if (localTeamId && !teams.find(team => team.id === localTeamId)) {
  33. localTeamId = null;
  34. }
  35. const currentTeamId = localTeamId ?? teams[0]?.id;
  36. const currentTeam = teams.find(team => team.id === currentTeamId) as
  37. | TeamWithProjects
  38. | undefined;
  39. const projects = currentTeam?.projects ?? [];
  40. const environment = query.environment;
  41. const {period, start, end, utc} = dataDatetime(query);
  42. const hasEscalatingIssues = organization.features.includes('escalating-issues');
  43. if (teams.length === 0) {
  44. return (
  45. <NoProjectMessage organization={organization} superuserNeedsToBeProjectMember />
  46. );
  47. }
  48. if (isError) {
  49. return <LoadingError />;
  50. }
  51. return (
  52. <Fragment>
  53. <SentryDocumentTitle title={t('Team Issues')} orgSlug={organization.slug} />
  54. <Header organization={organization} activeTab="issues" />
  55. <Body>
  56. <TeamStatsControls
  57. showEnvironment
  58. location={location}
  59. router={router}
  60. currentTeam={currentTeam}
  61. currentEnvironment={environment}
  62. />
  63. {isLoading && <LoadingIndicator />}
  64. {!isLoading && (
  65. <Layout.Main fullWidth>
  66. <DescriptionCard
  67. title={t('All Unresolved Issues')}
  68. description={tct(
  69. 'This includes New and Returning issues in the last 7 days as well as those that haven’t been resolved or [status] in the past.',
  70. {
  71. status: hasEscalatingIssues ? 'archived' : 'ignored',
  72. }
  73. )}
  74. >
  75. <TeamUnresolvedIssues
  76. projects={projects}
  77. organization={organization}
  78. teamSlug={currentTeam!.slug}
  79. environment={environment}
  80. period={period}
  81. start={start}
  82. end={end}
  83. utc={utc}
  84. />
  85. </DescriptionCard>
  86. <DescriptionCard
  87. title={t('New and Returning Issues')}
  88. description={tct(
  89. 'The new, regressed, and [status] issues that were assigned to your team.',
  90. {
  91. status: hasEscalatingIssues ? 'escalating' : 'unignored',
  92. }
  93. )}
  94. >
  95. <TeamIssuesBreakdown
  96. organization={organization}
  97. projects={projects}
  98. teamSlug={currentTeam!.slug}
  99. environment={environment}
  100. period={period}
  101. start={start?.toString()}
  102. end={end?.toString()}
  103. statuses={['new', 'regressed', 'unignored']}
  104. />
  105. </DescriptionCard>
  106. <DescriptionCard
  107. title={t('Issues Triaged')}
  108. description={t(
  109. 'How many new and returning issues were reviewed by your team each week. Reviewing an issue includes marking as reviewed, resolving, assigning to another team, or deleting.'
  110. )}
  111. >
  112. <TeamIssuesBreakdown
  113. organization={organization}
  114. projects={projects}
  115. teamSlug={currentTeam!.slug}
  116. environment={environment}
  117. period={period}
  118. start={start?.toString()}
  119. end={end?.toString()}
  120. statuses={['resolved', 'ignored', 'deleted']}
  121. />
  122. </DescriptionCard>
  123. <DescriptionCard
  124. title={t('Age of Unresolved Issues')}
  125. description={t('How long ago since unresolved issues were first created.')}
  126. >
  127. <TeamIssuesAge organization={organization} teamSlug={currentTeam!.slug} />
  128. </DescriptionCard>
  129. <DescriptionCard
  130. title={t('Time to Resolution')}
  131. description={t(
  132. `The mean time it took for issues to be resolved by your team.`
  133. )}
  134. >
  135. <TeamResolutionTime
  136. organization={organization}
  137. environment={environment}
  138. teamSlug={currentTeam!.slug}
  139. period={period}
  140. start={start?.toString()}
  141. end={end?.toString()}
  142. />
  143. </DescriptionCard>
  144. </Layout.Main>
  145. )}
  146. </Body>
  147. </Fragment>
  148. );
  149. }
  150. export default TeamStatsIssues;
  151. const Body = styled(Layout.Body)`
  152. @media (min-width: ${p => p.theme.breakpoints.medium}) {
  153. display: block;
  154. }
  155. `;