Просмотр исходного кода

feat(teamStats): Collapse larger tables on team stats issues (#31857)

Scott Cooper 3 лет назад
Родитель
Сommit
02372e5410

+ 82 - 0
static/app/views/organizationStats/teamInsights/collapsePanel.tsx

@@ -0,0 +1,82 @@
+import * as React from 'react';
+import styled from '@emotion/styled';
+
+import {IconChevron, IconList} from 'sentry/icons';
+import {tct} from 'sentry/locale';
+import space from 'sentry/styles/space';
+
+/** The number of elements to display before collapsing */
+export const COLLAPSE_COUNT = 5;
+
+type ChildRenderProps = {
+  isExpanded: boolean;
+  showMoreButton: React.ReactNode;
+};
+
+type Props = {
+  children: (props: ChildRenderProps) => JSX.Element;
+  items: number;
+};
+
+/**
+ * Used to expand results for team insights.
+ *
+ * Our collapsible component was not used because we want our
+ * expand button to be outside the list of children
+ *
+ * This component is not yet generic to use elsewhere. Like the hardcoded COLLAPSE_COUNT
+ */
+function CollapsePanel({items, children}: Props) {
+  const [isExpanded, setIsExpanded] = React.useState(false);
+  function expandResults() {
+    setIsExpanded(true);
+  }
+
+  return children({
+    isExpanded,
+    showMoreButton:
+      isExpanded || items <= COLLAPSE_COUNT ? null : (
+        <ShowMoreButton items={items} onClick={expandResults} />
+      ),
+  });
+}
+
+type ShowMoreButtonProps = {
+  items: number;
+  onClick: () => void;
+};
+
+function ShowMoreButton({items, onClick}: ShowMoreButtonProps) {
+  return (
+    <ShowMore onClick={onClick} role="button" data-test-id="collapse-show-more">
+      <ShowMoreText>
+        <StyledIconList color="gray300" />
+        {tct('Show [count] More', {count: items - COLLAPSE_COUNT})}
+      </ShowMoreText>
+
+      <IconChevron color="gray300" direction="down" />
+    </ShowMore>
+  );
+}
+
+export default CollapsePanel;
+
+const ShowMore = styled('div')`
+  display: flex;
+  align-items: center;
+  padding: ${space(1)} ${space(2)};
+  font-size: ${p => p.theme.fontSizeMedium};
+  color: ${p => p.theme.subText};
+  cursor: pointer;
+  border-top: 1px solid ${p => p.theme.border};
+`;
+
+const StyledIconList = styled(IconList)`
+  margin-right: ${space(1)};
+`;
+
+const ShowMoreText = styled('div')`
+  display: flex;
+  align-items: center;
+  flex-grow: 1;
+`;

+ 44 - 26
static/app/views/organizationStats/teamInsights/teamIssuesBreakdown.tsx

@@ -14,6 +14,7 @@ import ProjectsStore from 'sentry/stores/projectsStore';
 import space from 'sentry/styles/space';
 import {Organization, Project} from 'sentry/types';
 
+import CollapsePanel, {COLLAPSE_COUNT} from './collapsePanel';
 import {ProjectBadge, ProjectBadgeContainer} from './styles';
 import {
   barAxisLabel,
@@ -170,32 +171,49 @@ class TeamIssuesBreakdown extends AsyncComponent<Props, State> {
             />
           )}
         </IssuesChartWrapper>
-        <StyledPanelTable
-          numActions={statuses.length}
-          headers={[
-            t('Project'),
-            ...statuses.map(action => <AlignRight key={action}>{t(action)}</AlignRight>),
-            <AlignRight key="total">
-              {t('total')} <IconArrow direction="down" size="12px" color="gray300" />
-            </AlignRight>,
-          ]}
-          isLoading={loading}
-        >
-          {sortedProjectIds.map(({projectId}) => {
-            const project = projects.find(p => p.id === projectId);
-            return (
-              <Fragment key={projectId}>
-                <ProjectBadgeContainer>
-                  {project && <ProjectBadge avatarSize={18} project={project} />}
-                </ProjectBadgeContainer>
-                {statuses.map(action => (
-                  <AlignRight key={action}>{projectTotals[projectId][action]}</AlignRight>
-                ))}
-                <AlignRight>{projectTotals[projectId].total}</AlignRight>
-              </Fragment>
-            );
-          })}
-        </StyledPanelTable>
+        <CollapsePanel items={sortedProjectIds.length}>
+          {({isExpanded, showMoreButton}) => (
+            <Fragment>
+              <StyledPanelTable
+                numActions={statuses.length}
+                headers={[
+                  t('Project'),
+                  ...statuses.map(action => (
+                    <AlignRight key={action}>{t(action)}</AlignRight>
+                  )),
+                  <AlignRight key="total">
+                    {t('total')}{' '}
+                    <IconArrow direction="down" size="12px" color="gray300" />
+                  </AlignRight>,
+                ]}
+                isLoading={loading}
+              >
+                {sortedProjectIds.map(({projectId}, idx) => {
+                  const project = projects.find(p => p.id === projectId);
+
+                  if (idx >= COLLAPSE_COUNT && !isExpanded) {
+                    return null;
+                  }
+
+                  return (
+                    <Fragment key={projectId}>
+                      <ProjectBadgeContainer>
+                        {project && <ProjectBadge avatarSize={18} project={project} />}
+                      </ProjectBadgeContainer>
+                      {statuses.map(action => (
+                        <AlignRight key={action}>
+                          {projectTotals[projectId][action]}
+                        </AlignRight>
+                      ))}
+                      <AlignRight>{projectTotals[projectId].total}</AlignRight>
+                    </Fragment>
+                  );
+                })}
+              </StyledPanelTable>
+              {!loading && showMoreButton}
+            </Fragment>
+          )}
+        </CollapsePanel>
       </Fragment>
     );
   }

+ 94 - 126
static/app/views/organizationStats/teamInsights/teamMisery.tsx

@@ -1,4 +1,4 @@
-import {Fragment, useState} from 'react';
+import {Fragment} from 'react';
 import {css} from '@emotion/react';
 import styled from '@emotion/styled';
 import {Location} from 'history';
@@ -9,7 +9,7 @@ import {DateTimeObject} from 'sentry/components/charts/utils';
 import Link from 'sentry/components/links/link';
 import LoadingError from 'sentry/components/loadingError';
 import PanelTable from 'sentry/components/panels/panelTable';
-import {IconChevron, IconList, IconStar} from 'sentry/icons';
+import {IconStar} from 'sentry/icons';
 import {t, tct} from 'sentry/locale';
 import overflowEllipsis from 'sentry/styles/overflowEllipsis';
 import space from 'sentry/styles/space';
@@ -23,6 +23,7 @@ import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers';
 import type {Color} from 'sentry/utils/theme';
 import {transactionSummaryRouteWithQuery} from 'sentry/views/performance/transactionSummary/utils';
 
+import CollapsePanel, {COLLAPSE_COUNT} from './collapsePanel';
 import {ProjectBadge, ProjectBadgeContainer} from './styles';
 import {groupByTrend} from './utils';
 
@@ -37,9 +38,6 @@ type TeamMiseryProps = {
   period?: string | null;
 };
 
-/** The number of elements to display before collapsing */
-const COLLAPSE_COUNT = 5;
-
 function TeamMisery({
   organization,
   location,
@@ -50,14 +48,9 @@ function TeamMisery({
   period,
   error,
 }: TeamMiseryProps) {
-  const [isExpanded, setIsExpanded] = useState(false);
   const miseryRenderer =
     periodTableData?.meta && getFieldRenderer('user_misery', periodTableData.meta);
 
-  function expandResults() {
-    setIsExpanded(true);
-  }
-
   // Calculate trend, so we can sort based on it
   const sortedTableData = (periodTableData?.data ?? [])
     .map(dataRow => {
@@ -84,104 +77,99 @@ function TeamMisery({
   }
 
   return (
-    <Fragment>
-      <StyledPanelTable
-        isEmpty={projects.length === 0 || periodTableData?.data.length === 0}
-        emptyMessage={t('No key transactions starred by this team')}
-        emptyAction={
-          <Button
-            size="small"
-            external
-            href="https://docs.sentry.io/product/performance/transaction-summary/#starring-key-transactions"
+    <CollapsePanel items={groupedData.length}>
+      {({isExpanded, showMoreButton}) => (
+        <Fragment>
+          <StyledPanelTable
+            isEmpty={projects.length === 0 || periodTableData?.data.length === 0}
+            emptyMessage={t('No key transactions starred by this team')}
+            emptyAction={
+              <Button
+                size="small"
+                external
+                href="https://docs.sentry.io/product/performance/transaction-summary/#starring-key-transactions"
+              >
+                {t('Learn More')}
+              </Button>
+            }
+            headers={[
+              <FlexCenter key="transaction">
+                <StyledIconStar isSolid color="yellow300" /> {t('Key transaction')}
+              </FlexCenter>,
+              t('Project'),
+              tct('Last [period]', {period}),
+              t('Last 7 Days'),
+              <RightAligned key="change">{t('Change')}</RightAligned>,
+            ]}
+            isLoading={isLoading}
           >
-            {t('Learn More')}
-          </Button>
-        }
-        headers={[
-          <FlexCenter key="transaction">
-            <StyledIconStar isSolid color="yellow300" /> {t('Key transaction')}
-          </FlexCenter>,
-          t('Project'),
-          tct('Last [period]', {period}),
-          t('Last 7 Days'),
-          <RightAligned key="change">{t('Change')}</RightAligned>,
-        ]}
-        isLoading={isLoading}
-      >
-        {groupedData.map((dataRow, idx) => {
-          const project = projects.find(({slug}) => dataRow.project === slug);
-          const {trend, project: projectId, transaction} = dataRow;
-
-          const weekRow = weekTableData?.data.find(
-            row => row.project === projectId && row.transaction === transaction
-          );
-          if (!weekRow || trend === null) {
-            return null;
-          }
-
-          const periodMisery = miseryRenderer?.(dataRow, {organization, location});
-          const weekMisery =
-            weekRow && miseryRenderer?.(weekRow, {organization, location});
-          const trendValue = Math.round(Math.abs(trend));
-
-          if (idx >= COLLAPSE_COUNT && !isExpanded) {
-            return null;
-          }
-
-          return (
-            <Fragment key={idx}>
-              <KeyTransactionTitleWrapper>
-                <div>
-                  <StyledIconStar isSolid color="yellow300" />
-                </div>
-                <TransactionWrapper>
-                  <Link
-                    to={transactionSummaryRouteWithQuery({
-                      orgSlug: organization.slug,
-                      transaction: dataRow.transaction as string,
-                      projectID: project?.id,
-                      query: {query: 'transaction.duration:<15m'},
-                    })}
-                  >
-                    {dataRow.transaction}
-                  </Link>
-                </TransactionWrapper>
-              </KeyTransactionTitleWrapper>
-              <FlexCenter>
-                <ProjectBadgeContainer>
-                  {project && <ProjectBadge avatarSize={18} project={project} />}
-                </ProjectBadgeContainer>
-              </FlexCenter>
-              <FlexCenter>{periodMisery}</FlexCenter>
-              <FlexCenter>{weekMisery ?? '\u2014'}</FlexCenter>
-              <ScoreWrapper>
-                {trendValue === 0 ? (
-                  <SubText>
-                    {`0\u0025 `}
-                    {t('change')}
-                  </SubText>
-                ) : (
-                  <TrendText color={trend >= 0 ? 'green300' : 'red300'}>
-                    {`${trendValue}\u0025 `}
-                    {trend >= 0 ? t('better') : t('worse')}
-                  </TrendText>
-                )}
-              </ScoreWrapper>
-            </Fragment>
-          );
-        })}
-      </StyledPanelTable>
-      {groupedData.length >= COLLAPSE_COUNT && !isExpanded && !isLoading && (
-        <ShowMore onClick={expandResults}>
-          <ShowMoreText>
-            <StyledIconList color="gray300" />
-            {tct('Show [count] More', {count: groupedData.length - 1 - COLLAPSE_COUNT})}
-          </ShowMoreText>
-
-          <IconChevron color="gray300" direction="down" />
-        </ShowMore>
+            {groupedData.map((dataRow, idx) => {
+              const project = projects.find(({slug}) => dataRow.project === slug);
+              const {trend, project: projectId, transaction} = dataRow;
+
+              const weekRow = weekTableData?.data.find(
+                row => row.project === projectId && row.transaction === transaction
+              );
+              if (!weekRow || trend === null) {
+                return null;
+              }
+
+              const periodMisery = miseryRenderer?.(dataRow, {organization, location});
+              const weekMisery =
+                weekRow && miseryRenderer?.(weekRow, {organization, location});
+              const trendValue = Math.round(Math.abs(trend));
+
+              if (idx >= COLLAPSE_COUNT && !isExpanded) {
+                return null;
+              }
+
+              return (
+                <Fragment key={idx}>
+                  <KeyTransactionTitleWrapper>
+                    <div>
+                      <StyledIconStar isSolid color="yellow300" />
+                    </div>
+                    <TransactionWrapper>
+                      <Link
+                        to={transactionSummaryRouteWithQuery({
+                          orgSlug: organization.slug,
+                          transaction: dataRow.transaction as string,
+                          projectID: project?.id,
+                          query: {query: 'transaction.duration:<15m'},
+                        })}
+                      >
+                        {dataRow.transaction}
+                      </Link>
+                    </TransactionWrapper>
+                  </KeyTransactionTitleWrapper>
+                  <FlexCenter>
+                    <ProjectBadgeContainer>
+                      {project && <ProjectBadge avatarSize={18} project={project} />}
+                    </ProjectBadgeContainer>
+                  </FlexCenter>
+                  <FlexCenter>{periodMisery}</FlexCenter>
+                  <FlexCenter>{weekMisery ?? '\u2014'}</FlexCenter>
+                  <ScoreWrapper>
+                    {trendValue === 0 ? (
+                      <SubText>
+                        {`0\u0025 `}
+                        {t('change')}
+                      </SubText>
+                    ) : (
+                      <TrendText color={trend >= 0 ? 'green300' : 'red300'}>
+                        {`${trendValue}\u0025 `}
+                        {trend >= 0 ? t('better') : t('worse')}
+                      </TrendText>
+                    )}
+                  </ScoreWrapper>
+                </Fragment>
+              );
+            })}
+          </StyledPanelTable>
+          {!isLoading && showMoreButton}
+        </Fragment>
       )}
-    </Fragment>
+    </CollapsePanel>
   );
 }
 
@@ -340,23 +328,3 @@ const SubText = styled('div')`
 const TrendText = styled('div')<{color: Color}>`
   color: ${p => p.theme[p.color]};
 `;
-
-const ShowMore = styled('div')`
-  display: flex;
-  align-items: center;
-  padding: ${space(1)} ${space(2)};
-  font-size: ${p => p.theme.fontSizeMedium};
-  color: ${p => p.theme.subText};
-  cursor: pointer;
-  border-top: 1px solid ${p => p.theme.border};
-`;
-
-const StyledIconList = styled(IconList)`
-  margin-right: ${space(1)};
-`;
-
-const ShowMoreText = styled('div')`
-  display: flex;
-  align-items: center;
-  flex-grow: 1;
-`;

+ 65 - 47
static/app/views/organizationStats/teamInsights/teamUnresolvedIssues.tsx

@@ -15,6 +15,7 @@ import {Organization, Project} from 'sentry/types';
 import {formatPercentage} from 'sentry/utils/formatters';
 import type {Color} from 'sentry/utils/theme';
 
+import CollapsePanel, {COLLAPSE_COUNT} from './collapsePanel';
 import {ProjectBadge, ProjectBadgeContainer} from './styles';
 import {barAxisLabel, convertDayValueObjectToSeries, groupByTrend} from './utils';
 
@@ -28,6 +29,7 @@ type UnresolvedCount = {unresolved: number};
 type ProjectReleaseCount = Record<string, Record<string, UnresolvedCount>>;
 
 type State = AsyncComponent['state'] & {
+  expandTable: boolean;
   /** weekly selected date range */
   periodIssues: ProjectReleaseCount | null;
 };
@@ -39,6 +41,7 @@ class TeamUnresolvedIssues extends AsyncComponent<Props, State> {
     return {
       ...super.getDefaultState(),
       periodIssues: null,
+      expandTable: false,
     };
   }
 
@@ -85,6 +88,10 @@ class TeamUnresolvedIssues extends AsyncComponent<Props, State> {
     return Math.round(total / entries.length);
   }
 
+  handleExpandTable = () => {
+    this.setState({expandTable: true});
+  };
+
   renderLoading() {
     return this.renderBody();
   }
@@ -160,53 +167,64 @@ class TeamUnresolvedIssues extends AsyncComponent<Props, State> {
             />
           )}
         </ChartWrapper>
-        <StyledPanelTable
-          isEmpty={projects.length === 0}
-          isLoading={loading}
-          headers={[
-            t('Project'),
-            <RightAligned key="last">
-              {tct('Last [period] Average', {period})}
-            </RightAligned>,
-            <RightAligned key="curr">{t('Today')}</RightAligned>,
-            <RightAligned key="diff">{t('Change')}</RightAligned>,
-          ]}
-        >
-          {groupedProjects.map(({project}) => {
-            const totals = projectTotals[project.id] ?? {};
-
-            return (
-              <Fragment key={project.id}>
-                <ProjectBadgeContainer>
-                  <ProjectBadge avatarSize={18} project={project} />
-                </ProjectBadgeContainer>
-
-                <ScoreWrapper>{totals.periodAvg}</ScoreWrapper>
-                <ScoreWrapper>{totals.today}</ScoreWrapper>
-                <ScoreWrapper>
-                  <SubText
-                    color={
-                      totals.percentChange === 0
-                        ? 'gray300'
-                        : totals.percentChange > 0
-                        ? 'red300'
-                        : 'green300'
-                    }
-                  >
-                    {formatPercentage(
-                      Number.isNaN(totals.percentChange) ? 0 : totals.percentChange,
-                      0
-                    )}
-                    <PaddedIconArrow
-                      direction={totals.percentChange > 0 ? 'up' : 'down'}
-                      size="xs"
-                    />
-                  </SubText>
-                </ScoreWrapper>
-              </Fragment>
-            );
-          })}
-        </StyledPanelTable>
+        <CollapsePanel items={groupedProjects.length}>
+          {({isExpanded, showMoreButton}) => (
+            <Fragment>
+              <StyledPanelTable
+                isEmpty={projects.length === 0}
+                isLoading={loading}
+                headers={[
+                  t('Project'),
+                  <RightAligned key="last">
+                    {tct('Last [period] Average', {period})}
+                  </RightAligned>,
+                  <RightAligned key="curr">{t('Today')}</RightAligned>,
+                  <RightAligned key="diff">{t('Change')}</RightAligned>,
+                ]}
+              >
+                {groupedProjects.map(({project}, idx) => {
+                  const totals = projectTotals[project.id] ?? {};
+
+                  if (idx >= COLLAPSE_COUNT && !isExpanded) {
+                    return null;
+                  }
+
+                  return (
+                    <Fragment key={project.id}>
+                      <ProjectBadgeContainer>
+                        <ProjectBadge avatarSize={18} project={project} />
+                      </ProjectBadgeContainer>
+
+                      <ScoreWrapper>{totals.periodAvg}</ScoreWrapper>
+                      <ScoreWrapper>{totals.today}</ScoreWrapper>
+                      <ScoreWrapper>
+                        <SubText
+                          color={
+                            totals.percentChange === 0
+                              ? 'gray300'
+                              : totals.percentChange > 0
+                              ? 'red300'
+                              : 'green300'
+                          }
+                        >
+                          {formatPercentage(
+                            Number.isNaN(totals.percentChange) ? 0 : totals.percentChange,
+                            0
+                          )}
+                          <PaddedIconArrow
+                            direction={totals.percentChange > 0 ? 'up' : 'down'}
+                            size="xs"
+                          />
+                        </SubText>
+                      </ScoreWrapper>
+                    </Fragment>
+                  );
+                })}
+              </StyledPanelTable>
+              {!loading && showMoreButton}
+            </Fragment>
+          )}
+        </CollapsePanel>
       </div>
     );
   }

+ 26 - 0
tests/js/spec/views/organizationStats/teamInsights/collapsePanel.spec.jsx

@@ -0,0 +1,26 @@
+import {Fragment} from 'react';
+
+import {mountWithTheme, screen, userEvent} from 'sentry-test/reactTestingLibrary';
+
+import CollapsePanel from 'sentry/views/organizationStats/teamInsights/collapsePanel';
+
+describe('CollapsePanel', () => {
+  it('should expand on click', () => {
+    mountWithTheme(
+      <CollapsePanel items={10}>
+        {({isExpanded, showMoreButton}) => (
+          <Fragment>
+            <div>expanded: {isExpanded.toString()}</div> {showMoreButton}
+          </Fragment>
+        )}
+      </CollapsePanel>
+    );
+
+    expect(screen.getByText('expanded: false')).toBeInTheDocument();
+
+    expect(screen.getByTestId('collapse-show-more')).toBeInTheDocument();
+    userEvent.click(screen.getByTestId('collapse-show-more'));
+
+    expect(screen.getByText('expanded: true')).toBeInTheDocument();
+  });
+});