Browse Source

ref(stats): Convert teamStability to functional (#51125)

Spencer Murray 1 year ago
parent
commit
3a9666e140

+ 8 - 8
static/app/views/organizationStats/teamInsights/teamStability.spec.jsx

@@ -3,7 +3,7 @@ import {render, screen} from 'sentry-test/reactTestingLibrary';
 import TeamStability from 'sentry/views/organizationStats/teamInsights/teamStability';
 
 describe('TeamStability', () => {
-  it('should comparse selected past crash rate with current week', () => {
+  it('should comparse selected past crash rate with current week', async () => {
     const sessionsApi = MockApiClient.addMockResponse({
       url: `/organizations/org-slug/sessions/`,
       body: TestStubs.SessionStatusCountByProjectInPeriod(),
@@ -13,18 +13,18 @@ describe('TeamStability', () => {
       <TeamStability
         projects={[project]}
         organization={TestStubs.Organization()}
-        period="7d"
+        period="2w"
       />
     );
 
     expect(screen.getByText('project-slug')).toBeInTheDocument();
-    expect(screen.getAllByText('90%')).toHaveLength(2);
-    expect(screen.getByText('0%')).toBeInTheDocument(2);
-    expect(sessionsApi).toHaveBeenCalledTimes(3);
+    expect(await screen.findAllByText('90%')).toHaveLength(2);
+    expect(await screen.findByText('0%')).toBeInTheDocument(2);
+    expect(sessionsApi).toHaveBeenCalledTimes(2);
   });
 
-  it('should render no sessions', () => {
-    const noSessionProject = TestStubs.Project({hasSessions: false, id: 123});
+  it('should render no sessions', async () => {
+    const noSessionProject = TestStubs.Project({hasSessions: false, id: 321});
     render(
       <TeamStability
         projects={[noSessionProject]}
@@ -33,7 +33,7 @@ describe('TeamStability', () => {
       />
     );
 
-    expect(screen.getAllByText('\u2014')).toHaveLength(3);
+    expect(await screen.findAllByText('\u2014')).toHaveLength(3);
   });
 
   it('should render no projects', () => {

+ 127 - 162
static/app/views/organizationStats/teamInsights/teamStability.tsx

@@ -1,14 +1,12 @@
 import {Fragment} from 'react';
 import {css} from '@emotion/react';
 import styled from '@emotion/styled';
-import isEqual from 'lodash/isEqual';
 import round from 'lodash/round';
 
-import AsyncComponent from 'sentry/components/asyncComponent';
 import {Button} from 'sentry/components/button';
 import MiniBarChart from 'sentry/components/charts/miniBarChart';
-import SessionsRequest from 'sentry/components/charts/sessionsRequest';
 import {DateTimeObject} from 'sentry/components/charts/utils';
+import LoadingError from 'sentry/components/loadingError';
 import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
 import PanelTable from 'sentry/components/panels/panelTable';
 import Placeholder from 'sentry/components/placeholder';
@@ -23,6 +21,7 @@ import {
   SessionStatus,
 } from 'sentry/types';
 import {formatFloat} from 'sentry/utils/formatters';
+import {useApiQuery} from 'sentry/utils/queryClient';
 import {getCountSeries, getCrashFreeRate, getSeriesSum} from 'sentry/utils/sessions';
 import {ColorOrAlias} from 'sentry/utils/theme';
 import {displayCrashFreePercent} from 'sentry/views/releases/utils';
@@ -30,90 +29,80 @@ import {displayCrashFreePercent} from 'sentry/views/releases/utils';
 import {ProjectBadge, ProjectBadgeContainer} from './styles';
 import {groupByTrend} from './utils';
 
-type Props = AsyncComponent['props'] & {
+interface TeamStabilityProps extends DateTimeObject {
   organization: Organization;
   projects: Project[];
   period?: string | null;
-} & DateTimeObject;
-
-type State = AsyncComponent['state'] & {
-  /** weekly selected date range */
-  periodSessions: SessionApiResponse | null;
-  /** Locked to last 7 days */
-  weekSessions: SessionApiResponse | null;
-};
-
-class TeamStability extends AsyncComponent<Props, State> {
-  shouldRenderBadRequests = true;
-
-  getDefaultState(): State {
-    return {
-      ...super.getDefaultState(),
-      weekSessions: null,
-      periodSessions: null,
-    };
-  }
-
-  getEndpoints() {
-    const {organization, start, end, period, utc, projects} = this.props;
-
-    const projectsWithSessions = projects.filter(project => project.hasSessions);
-
-    if (projectsWithSessions.length === 0) {
-      return [];
-    }
+}
 
-    const datetime = {start, end, period, utc};
-    const commonQuery = {
-      environment: [],
-      project: projectsWithSessions.map(p => p.id),
-      field: 'sum(session)',
-      groupBy: ['session.status', 'project'],
-      interval: '1d',
-    };
-
-    const endpoints: ReturnType<AsyncComponent['getEndpoints']> = [
-      [
-        'periodSessions',
-        `/organizations/${organization.slug}/sessions/`,
-        {
-          query: {
-            ...commonQuery,
-            ...normalizeDateTimeParams(datetime),
-          },
+function TeamStability({
+  organization,
+  projects,
+  period,
+  start,
+  end,
+  utc,
+}: TeamStabilityProps) {
+  const projectsWithSessions = projects.filter(project => project.hasSessions);
+  const datetime = {start, end, period, utc};
+  const commonQuery = {
+    environment: [],
+    project: projectsWithSessions.map(p => p.id),
+    field: 'sum(session)',
+    groupBy: ['session.status', 'project'],
+    interval: '1d',
+  };
+
+  const {
+    data: periodSessions,
+    isLoading: isPeriodSessionsLoading,
+    isError: isPeriodSessionsError,
+    refetch: refetchPeriodSessions,
+  } = useApiQuery<SessionApiResponse>(
+    [
+      `/organizations/${organization.slug}/sessions/`,
+      {
+        query: {
+          ...commonQuery,
+          ...normalizeDateTimeParams(datetime),
         },
-      ],
-      [
-        'weekSessions',
-        `/organizations/${organization.slug}/sessions/`,
-        {
-          query: {
-            ...commonQuery,
-            statsPeriod: '7d',
-          },
+      },
+    ],
+    {staleTime: 5000}
+  );
+
+  const {
+    data: weekSessions,
+    isLoading: isWeekSessionsLoading,
+    isError: isWeekSessionsError,
+    refetch: refetchWeekSessions,
+  } = useApiQuery<SessionApiResponse>(
+    [
+      `/organizations/${organization.slug}/sessions/`,
+      {
+        query: {
+          ...commonQuery,
+          statsPeriod: '7d',
         },
-      ],
-    ];
+      },
+    ],
+    {staleTime: 5000}
+  );
 
-    return endpoints;
-  }
+  const isLoading = isPeriodSessionsLoading || isWeekSessionsLoading;
 
-  componentDidUpdate(prevProps: Props) {
-    const {projects, start, end, period, utc} = this.props;
-
-    if (
-      prevProps.start !== start ||
-      prevProps.end !== end ||
-      prevProps.period !== period ||
-      prevProps.utc !== utc ||
-      !isEqual(prevProps.projects, projects)
-    ) {
-      this.remountComponent();
-    }
+  if (isPeriodSessionsError || isWeekSessionsError) {
+    return (
+      <LoadingError
+        onRetry={() => {
+          refetchPeriodSessions();
+          refetchWeekSessions();
+        }}
+      />
+    );
   }
 
-  getScore(projectId: number, dataset: 'week' | 'period'): number | null {
-    const {periodSessions, weekSessions} = this.state;
+  function getScore(projectId: number, dataset: 'week' | 'period'): number | null {
     const sessions = dataset === 'week' ? weekSessions : periodSessions;
     const projectGroups = sessions?.groups.filter(
       group => group.by.project === projectId
@@ -122,9 +111,9 @@ class TeamStability extends AsyncComponent<Props, State> {
     return getCrashFreeRate(projectGroups, SessionFieldWithOperation.SESSIONS);
   }
 
-  getTrend(projectId: number): number | null {
-    const periodScore = this.getScore(projectId, 'period');
-    const weekScore = this.getScore(projectId, 'week');
+  function getTrend(projectId: number): number | null {
+    const periodScore = getScore(projectId, 'period');
+    const weekScore = getScore(projectId, 'week');
 
     if (periodScore === null || weekScore === null) {
       return null;
@@ -133,7 +122,7 @@ class TeamStability extends AsyncComponent<Props, State> {
     return weekScore - periodScore;
   }
 
-  getMiniBarChartSeries(project: Project, response: SessionApiResponse) {
+  function getMiniBarChartSeries(project: Project, response: SessionApiResponse) {
     const sumSessions = getSeriesSum(
       response.groups.filter(group => group.by.project === Number(project.id)),
       SessionFieldWithOperation.SESSIONS,
@@ -168,14 +157,8 @@ class TeamStability extends AsyncComponent<Props, State> {
     return [{seriesName: t('Crash Free Sessions'), data}];
   }
 
-  renderLoading() {
-    return this.renderBody();
-  }
-
-  renderScore(projectId: string, dataset: 'week' | 'period') {
-    const {loading} = this.state;
-
-    if (loading) {
+  function renderScore(projectId: string, dataset: 'week' | 'period') {
+    if (isLoading) {
       return (
         <div>
           <Placeholder width="80px" height="25px" />
@@ -183,7 +166,7 @@ class TeamStability extends AsyncComponent<Props, State> {
       );
     }
 
-    const score = this.getScore(Number(projectId), dataset);
+    const score = getScore(Number(projectId), dataset);
 
     if (score === null) {
       return '\u2014';
@@ -192,10 +175,8 @@ class TeamStability extends AsyncComponent<Props, State> {
     return displayCrashFreePercent(score);
   }
 
-  renderTrend(projectId: string) {
-    const {loading} = this.state;
-
-    if (loading) {
+  function renderTrend(projectId: string) {
+    if (isLoading) {
       return (
         <div>
           <Placeholder width="80px" height="25px" />
@@ -203,7 +184,7 @@ class TeamStability extends AsyncComponent<Props, State> {
       );
     }
 
-    const trend = this.getTrend(Number(projectId));
+    const trend = getTrend(Number(projectId));
 
     if (trend === null) {
       return '\u2014';
@@ -217,73 +198,57 @@ class TeamStability extends AsyncComponent<Props, State> {
     );
   }
 
-  renderBody() {
-    const {organization, projects, period} = this.props;
-
-    const sortedProjects = projects
-      .map(project => ({project, trend: this.getTrend(Number(project.id)) ?? 0}))
-      .sort((a, b) => Math.abs(b.trend) - Math.abs(a.trend));
-
-    const groupedProjects = groupByTrend(sortedProjects);
-
-    return (
-      <SessionsRequest
-        api={this.api}
-        project={projects.map(({id}) => Number(id))}
-        organization={organization}
-        interval="1d"
-        groupBy={['session.status', 'project']}
-        field={[SessionFieldWithOperation.SESSIONS]}
-        statsPeriod={period}
-      >
-        {({response, loading}) => (
-          <StyledPanelTable
-            isEmpty={projects.length === 0}
-            emptyMessage={t('No projects with release health enabled')}
-            emptyAction={
-              <Button
-                size="sm"
-                external
-                href="https://docs.sentry.io/platforms/dotnet/guides/nlog/configuration/releases/#release-health"
-              >
-                {t('Learn More')}
-              </Button>
-            }
-            headers={[
-              t('Project'),
-              <RightAligned key="last">{tct('Last [period]', {period})}</RightAligned>,
-              <RightAligned key="avg">{tct('[period] Avg', {period})}</RightAligned>,
-              <RightAligned key="curr">{t('Last 7 Days')}</RightAligned>,
-              <RightAligned key="diff">{t('Difference')}</RightAligned>,
-            ]}
-          >
-            {groupedProjects.map(({project}) => (
-              <Fragment key={project.id}>
-                <ProjectBadgeContainer>
-                  <ProjectBadge avatarSize={18} project={project} />
-                </ProjectBadgeContainer>
-
-                <div>
-                  {response && !loading && (
-                    <MiniBarChart
-                      isGroupedByDate
-                      showTimeInTooltip
-                      series={this.getMiniBarChartSeries(project, response)}
-                      height={25}
-                      tooltipFormatter={(value: number) => `${value.toLocaleString()}%`}
-                    />
-                  )}
-                </div>
-                <ScoreWrapper>{this.renderScore(project.id, 'period')}</ScoreWrapper>
-                <ScoreWrapper>{this.renderScore(project.id, 'week')}</ScoreWrapper>
-                <ScoreWrapper>{this.renderTrend(project.id)}</ScoreWrapper>
-              </Fragment>
-            ))}
-          </StyledPanelTable>
-        )}
-      </SessionsRequest>
-    );
-  }
+  const sortedProjects = projects
+    .map(project => ({project, trend: getTrend(Number(project.id)) ?? 0}))
+    .sort((a, b) => Math.abs(b.trend) - Math.abs(a.trend));
+
+  const groupedProjects = groupByTrend(sortedProjects);
+
+  return (
+    <StyledPanelTable
+      isEmpty={projects.length === 0}
+      emptyMessage={t('No projects with release health enabled')}
+      emptyAction={
+        <Button
+          size="sm"
+          external
+          href="https://docs.sentry.io/platforms/dotnet/guides/nlog/configuration/releases/#release-health"
+        >
+          {t('Learn More')}
+        </Button>
+      }
+      headers={[
+        t('Project'),
+        <RightAligned key="last">{tct('Last [period]', {period})}</RightAligned>,
+        <RightAligned key="avg">{tct('[period] Avg', {period})}</RightAligned>,
+        <RightAligned key="curr">{t('Last 7 Days')}</RightAligned>,
+        <RightAligned key="diff">{t('Difference')}</RightAligned>,
+      ]}
+    >
+      {groupedProjects.map(({project}) => (
+        <Fragment key={project.id}>
+          <ProjectBadgeContainer>
+            <ProjectBadge avatarSize={18} project={project} />
+          </ProjectBadgeContainer>
+
+          <div>
+            {periodSessions && weekSessions && !isLoading && (
+              <MiniBarChart
+                isGroupedByDate
+                showTimeInTooltip
+                series={getMiniBarChartSeries(project, periodSessions)}
+                height={25}
+                tooltipFormatter={(value: number) => `${value.toLocaleString()}%`}
+              />
+            )}
+          </div>
+          <ScoreWrapper>{renderScore(project.id, 'period')}</ScoreWrapper>
+          <ScoreWrapper>{renderScore(project.id, 'week')}</ScoreWrapper>
+          <ScoreWrapper>{renderTrend(project.id)}</ScoreWrapper>
+        </Fragment>
+      ))}
+    </StyledPanelTable>
+  );
 }
 
 export default TeamStability;