issues.tsx 6.0 KB

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