overview.tsx 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. import {Fragment} from 'react';
  2. import {RouteComponentProps} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import {LocationDescriptorObject} from 'history';
  5. import pick from 'lodash/pick';
  6. import moment from 'moment';
  7. import {Client} from 'app/api';
  8. import {DateTimeObject} from 'app/components/charts/utils';
  9. import TeamSelector from 'app/components/forms/teamSelector';
  10. import * as Layout from 'app/components/layouts/thirds';
  11. import LoadingIndicator from 'app/components/loadingIndicator';
  12. import {getParams} from 'app/components/organizations/globalSelectionHeader/getParams';
  13. import {ChangeData} from 'app/components/organizations/timeRangeSelector';
  14. import PageTimeRangeSelector from 'app/components/pageTimeRangeSelector';
  15. import {t} from 'app/locale';
  16. import space from 'app/styles/space';
  17. import {DateString, Organization, RelativePeriod, TeamWithProjects} from 'app/types';
  18. import localStorage from 'app/utils/localStorage';
  19. import withApi from 'app/utils/withApi';
  20. import withOrganization from 'app/utils/withOrganization';
  21. import withTeamsForUser from 'app/utils/withTeamsForUser';
  22. import DescriptionCard from './descriptionCard';
  23. import HeaderTabs from './headerTabs';
  24. import TeamAlertsTriggered from './teamAlertsTriggered';
  25. import TeamMisery from './teamMisery';
  26. import TeamStability from './teamStability';
  27. const INSIGHTS_DEFAULT_STATS_PERIOD = '8w';
  28. const PAGE_QUERY_PARAMS = [
  29. 'pageStatsPeriod',
  30. 'pageStart',
  31. 'pageEnd',
  32. 'pageUtc',
  33. 'dataCategory',
  34. 'transform',
  35. 'sort',
  36. 'query',
  37. 'cursor',
  38. 'team',
  39. ];
  40. type Props = {
  41. api: Client;
  42. organization: Organization;
  43. teams: TeamWithProjects[];
  44. loadingTeams: boolean;
  45. error: Error | null;
  46. } & RouteComponentProps<{orgId: string}, {}>;
  47. function TeamInsightsOverview({
  48. organization,
  49. teams,
  50. loadingTeams,
  51. location,
  52. router,
  53. }: Props) {
  54. const query = location?.query ?? {};
  55. const localStorageKey = `teamInsightsSelectedTeamId:${organization.slug}`;
  56. let localTeamId: string | null | undefined =
  57. query.team ?? localStorage.getItem(localStorageKey);
  58. if (localTeamId && !teams.find(team => team.id === localTeamId)) {
  59. localTeamId = null;
  60. }
  61. const currentTeamId = localTeamId ?? teams[0]?.id;
  62. const currentTeam = teams.find(team => team.id === currentTeamId);
  63. const projects = currentTeam?.projects ?? [];
  64. function handleChangeTeam(teamId: string) {
  65. localStorage.setItem(localStorageKey, teamId);
  66. setStateOnUrl({team: teamId});
  67. }
  68. function handleUpdateDatetime(datetime: ChangeData): LocationDescriptorObject {
  69. const {start, end, relative, utc} = datetime;
  70. if (start && end) {
  71. const parser = utc ? moment.utc : moment;
  72. return setStateOnUrl({
  73. pageStatsPeriod: undefined,
  74. pageStart: parser(start).format(),
  75. pageEnd: parser(end).format(),
  76. pageUtc: utc ?? undefined,
  77. });
  78. }
  79. return setStateOnUrl({
  80. pageStatsPeriod: (relative as RelativePeriod) || undefined,
  81. pageStart: undefined,
  82. pageEnd: undefined,
  83. pageUtc: undefined,
  84. });
  85. }
  86. function setStateOnUrl(nextState: {
  87. pageStatsPeriod?: RelativePeriod;
  88. pageStart?: DateString;
  89. pageEnd?: DateString;
  90. pageUtc?: boolean | null;
  91. team?: string;
  92. }): LocationDescriptorObject {
  93. const nextQueryParams = pick(nextState, PAGE_QUERY_PARAMS);
  94. const nextLocation = {
  95. ...location,
  96. query: {
  97. ...query,
  98. ...nextQueryParams,
  99. },
  100. };
  101. router.push(nextLocation);
  102. return nextLocation;
  103. }
  104. function dataDatetime(): DateTimeObject {
  105. const {
  106. start,
  107. end,
  108. statsPeriod,
  109. utc: utcString,
  110. } = getParams(query, {
  111. allowEmptyPeriod: true,
  112. allowAbsoluteDatetime: true,
  113. allowAbsolutePageDatetime: true,
  114. });
  115. if (!statsPeriod && !start && !end) {
  116. return {period: INSIGHTS_DEFAULT_STATS_PERIOD};
  117. }
  118. // Following getParams, statsPeriod will take priority over start/end
  119. if (statsPeriod) {
  120. return {period: statsPeriod};
  121. }
  122. const utc = utcString === 'true';
  123. if (start && end) {
  124. return utc
  125. ? {
  126. start: moment.utc(start).format(),
  127. end: moment.utc(end).format(),
  128. utc,
  129. }
  130. : {
  131. start: moment(start).utc().format(),
  132. end: moment(end).utc().format(),
  133. utc,
  134. };
  135. }
  136. return {period: INSIGHTS_DEFAULT_STATS_PERIOD};
  137. }
  138. const {period, start, end, utc} = dataDatetime();
  139. return (
  140. <Fragment>
  141. <BorderlessHeader>
  142. <StyledHeaderContent>
  143. <StyledLayoutTitle>{t('Projects')}</StyledLayoutTitle>
  144. </StyledHeaderContent>
  145. </BorderlessHeader>
  146. <Layout.Header>
  147. <HeaderTabs organization={organization} activeTab="teamInsights" />
  148. </Layout.Header>
  149. <Body>
  150. {loadingTeams && <LoadingIndicator />}
  151. {!loadingTeams && (
  152. <Layout.Main fullWidth>
  153. <ControlsWrapper>
  154. <TeamSelector
  155. name="select-team"
  156. value={currentTeam?.slug}
  157. isLoading={loadingTeams}
  158. onChange={choice => handleChangeTeam(choice.actor.id)}
  159. teamFilter={filterTeam => filterTeam.isMember}
  160. />
  161. <StyledPageTimeRangeSelector
  162. organization={organization}
  163. relative={period ?? ''}
  164. start={start ?? null}
  165. end={end ?? null}
  166. utc={utc ?? null}
  167. onUpdate={handleUpdateDatetime}
  168. showAbsolute={false}
  169. relativeOptions={{
  170. '14d': t('Last 2 weeks'),
  171. '4w': t('Last 4 weeks'),
  172. [INSIGHTS_DEFAULT_STATS_PERIOD]: t('Last 8 weeks'),
  173. '12w': t('Last 12 weeks'),
  174. }}
  175. />
  176. </ControlsWrapper>
  177. <SectionTitle>{t('Project Health')}</SectionTitle>
  178. <DescriptionCard
  179. title={t('Crash Free Sessions')}
  180. description={t(
  181. 'The percentage of healthy, errored, and abnormal sessions that did not cause a crash.'
  182. )}
  183. >
  184. <TeamStability
  185. projects={projects}
  186. organization={organization}
  187. period={period}
  188. start={start}
  189. end={end}
  190. utc={utc}
  191. />
  192. </DescriptionCard>
  193. <DescriptionCard
  194. title={t('User Misery')}
  195. description={t(
  196. 'User Misery shows the number of unique users that experienced load times 4x the project’s configured threshold.'
  197. )}
  198. >
  199. <TeamMisery
  200. organization={organization}
  201. projects={projects}
  202. period={period}
  203. start={start?.toString()}
  204. end={end?.toString()}
  205. location={location}
  206. />
  207. </DescriptionCard>
  208. <DescriptionCard
  209. title={t('Metric Alerts Triggered')}
  210. description={t(
  211. 'These are the alerts triggered from the Alert Rules your team created.'
  212. )}
  213. >
  214. <TeamAlertsTriggered
  215. organization={organization}
  216. teamSlug={currentTeam!.slug}
  217. period={period}
  218. start={start?.toString()}
  219. end={end?.toString()}
  220. location={location}
  221. />
  222. </DescriptionCard>
  223. </Layout.Main>
  224. )}
  225. </Body>
  226. </Fragment>
  227. );
  228. }
  229. export {TeamInsightsOverview};
  230. export default withApi(withOrganization(withTeamsForUser(TeamInsightsOverview)));
  231. const Body = styled(Layout.Body)`
  232. margin-bottom: -20px;
  233. @media (min-width: ${p => p.theme.breakpoints[1]}) {
  234. display: block;
  235. }
  236. `;
  237. const BorderlessHeader = styled(Layout.Header)`
  238. border-bottom: 0;
  239. `;
  240. const StyledHeaderContent = styled(Layout.HeaderContent)`
  241. margin-bottom: 0;
  242. `;
  243. const StyledLayoutTitle = styled(Layout.Title)`
  244. margin-top: ${space(0.5)};
  245. `;
  246. const ControlsWrapper = styled('div')`
  247. display: grid;
  248. grid-template-columns: 0.5fr 1fr;
  249. align-items: center;
  250. gap: ${space(1)};
  251. margin-bottom: ${space(2)};
  252. `;
  253. const StyledPageTimeRangeSelector = styled(PageTimeRangeSelector)`
  254. flex-grow: 1;
  255. `;
  256. const SectionTitle = styled(Layout.Title)`
  257. margin-bottom: ${space(1)} !important;
  258. `;