issues.tsx 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  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} 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. if (teams.length === 0) {
  42. return (
  43. <NoProjectMessage organization={organization} superuserNeedsToBeProjectMember />
  44. );
  45. }
  46. return (
  47. <Fragment>
  48. <SentryDocumentTitle title={t('Team Issues')} orgSlug={organization.slug} />
  49. <Header organization={organization} activeTab="issues" />
  50. <Body>
  51. <TeamStatsControls
  52. showEnvironment
  53. location={location}
  54. router={router}
  55. currentTeam={currentTeam}
  56. currentEnvironment={environment}
  57. />
  58. {!initiallyLoaded && <LoadingIndicator />}
  59. {initiallyLoaded && (
  60. <Layout.Main fullWidth>
  61. <DescriptionCard
  62. title={t('All Unresolved Issues')}
  63. description={t(
  64. 'This includes New and Returning issues in the last 7 days as well as those that haven’t been resolved or ignored in the past.'
  65. )}
  66. >
  67. <TeamUnresolvedIssues
  68. projects={projects}
  69. organization={organization}
  70. teamSlug={currentTeam!.slug}
  71. environment={environment}
  72. period={period}
  73. start={start}
  74. end={end}
  75. utc={utc}
  76. />
  77. </DescriptionCard>
  78. <DescriptionCard
  79. title={t('New and Returning Issues')}
  80. description={t(
  81. 'The new, regressed, and unignored issues that were assigned to your team.'
  82. )}
  83. >
  84. <TeamIssuesBreakdown
  85. organization={organization}
  86. projects={projects}
  87. teamSlug={currentTeam!.slug}
  88. environment={environment}
  89. period={period}
  90. start={start?.toString()}
  91. end={end?.toString()}
  92. location={location}
  93. statuses={['new', 'regressed', 'unignored']}
  94. />
  95. </DescriptionCard>
  96. <DescriptionCard
  97. title={t('Issues Triaged')}
  98. description={t(
  99. '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.'
  100. )}
  101. >
  102. <TeamIssuesBreakdown
  103. organization={organization}
  104. projects={projects}
  105. teamSlug={currentTeam!.slug}
  106. environment={environment}
  107. period={period}
  108. start={start?.toString()}
  109. end={end?.toString()}
  110. location={location}
  111. statuses={['resolved', 'ignored', 'deleted']}
  112. />
  113. </DescriptionCard>
  114. <DescriptionCard
  115. title={t('Age of Unresolved Issues')}
  116. description={t('How long ago since unresolved issues were first created.')}
  117. >
  118. <TeamIssuesAge organization={organization} teamSlug={currentTeam!.slug} />
  119. </DescriptionCard>
  120. <DescriptionCard
  121. title={t('Time to Resolution')}
  122. description={t(
  123. `The mean time it took for issues to be resolved by your team.`
  124. )}
  125. >
  126. <TeamResolutionTime
  127. organization={organization}
  128. environment={environment}
  129. teamSlug={currentTeam!.slug}
  130. period={period}
  131. start={start?.toString()}
  132. end={end?.toString()}
  133. location={location}
  134. />
  135. </DescriptionCard>
  136. </Layout.Main>
  137. )}
  138. </Body>
  139. </Fragment>
  140. );
  141. }
  142. export default TeamStatsIssues;
  143. const Body = styled(Layout.Body)`
  144. @media (min-width: ${p => p.theme.breakpoints.medium}) {
  145. display: block;
  146. }
  147. `;