Browse Source

feat(workflow): Restyle alert history table (#26807)

Scott Cooper 3 years ago
parent
commit
ccb8bd6e2e

+ 1 - 3
static/app/views/alerts/list/header.tsx

@@ -107,9 +107,7 @@ const BorderlessHeader = styled(Layout.Header)`
   border-bottom: 0;
 
   /* Not enough buttons to change direction for tablet view */
-  @media (min-width: ${p => p.theme.breakpoints[0]}) {
-    grid-template-columns: 1fr auto;
-  }
+  grid-template-columns: 1fr auto;
 `;
 
 const StyledLayoutHeaderContent = styled(Layout.HeaderContent)`

+ 27 - 30
static/app/views/alerts/list/index.tsx

@@ -14,7 +14,7 @@ import ExternalLink from 'app/components/links/externalLink';
 import LoadingIndicator from 'app/components/loadingIndicator';
 import GlobalSelectionHeader from 'app/components/organizations/globalSelectionHeader';
 import Pagination from 'app/components/pagination';
-import {Panel, PanelBody, PanelHeader} from 'app/components/panels';
+import {PanelTable} from 'app/components/panels';
 import SearchBar from 'app/components/searchBar';
 import SentryDocumentTitle from 'app/components/sentryDocumentTitle';
 import {IconCheckmark, IconInfo} from 'app/icons';
@@ -33,7 +33,6 @@ import {Incident} from '../types';
 import AlertHeader from './header';
 import Onboarding from './onboarding';
 import AlertListRow from './row';
-import {TableLayout} from './styles';
 
 const DOCS_URL =
   'https://docs.sentry.io/workflow/alerts-notifications/alerts/?_ga=2.21848383.580096147.1592364314-1444595810.1582160976';
@@ -274,40 +273,38 @@ class IncidentsList extends AsyncComponent<Props, State & AsyncComponent['state'
     return (
       <Fragment>
         {this.tryRenderOnboarding() ?? (
-          <Panel>
-            {!loading && (
-              <PanelHeader>
-                <TableLayout>
-                  <div>{t('Alert')}</div>
-                  <div>{t('Alert Rule')}</div>
-                  <div>{t('Project')}</div>
-                  <div>{t('Team')}</div>
-                </TableLayout>
-              </PanelHeader>
-            )}
+          <PanelTable
+            isLoading={loading}
+            headers={[
+              t('Alert Rule'),
+              t('Triggered'),
+              t('Duration'),
+              t('Project'),
+              t('Alert ID'),
+              t('Team'),
+            ]}
+          >
             {showLoadingIndicator ? (
               <LoadingIndicator />
             ) : (
               this.tryRenderEmpty() ?? (
-                <PanelBody>
-                  <Projects orgId={orgId} slugs={Array.from(allProjectsFromIncidents)}>
-                    {({initiallyLoaded, projects}) =>
-                      incidentList.map(incident => (
-                        <AlertListRow
-                          key={incident.id}
-                          projectsLoaded={initiallyLoaded}
-                          projects={projects as Project[]}
-                          incident={incident}
-                          orgId={orgId}
-                          organization={organization}
-                        />
-                      ))
-                    }
-                  </Projects>
-                </PanelBody>
+                <Projects orgId={orgId} slugs={Array.from(allProjectsFromIncidents)}>
+                  {({initiallyLoaded, projects}) =>
+                    incidentList.map(incident => (
+                      <AlertListRow
+                        key={incident.id}
+                        projectsLoaded={initiallyLoaded}
+                        projects={projects as Project[]}
+                        incident={incident}
+                        orgId={orgId}
+                        organization={organization}
+                      />
+                    ))
+                  }
+                </Projects>
               )
             )}
-          </Panel>
+          </PanelTable>
         )}
         <Pagination pageLinks={incidentListPageLinks} />
       </Fragment>

+ 60 - 51
static/app/views/alerts/list/row.tsx

@@ -1,4 +1,4 @@
-import {Component} from 'react';
+import {Component, Fragment} from 'react';
 import styled from '@emotion/styled';
 import memoize from 'lodash/memoize';
 import moment from 'moment';
@@ -8,10 +8,12 @@ import Duration from 'app/components/duration';
 import ErrorBoundary from 'app/components/errorBoundary';
 import IdBadge from 'app/components/idBadge';
 import Link from 'app/components/links/link';
-import {PanelItem} from 'app/components/panels';
+import Tag from 'app/components/tag';
 import TimeSince from 'app/components/timeSince';
-import {t, tct} from 'app/locale';
+import {t} from 'app/locale';
+import TeamStore from 'app/stores/teamStore';
 import overflowEllipsis from 'app/styles/overflowEllipsis';
+import space from 'app/styles/space';
 import {Actor, Organization, Project} from 'app/types';
 import {getUtcDateString} from 'app/utils/dates';
 import getDynamicText from 'app/utils/getDynamicText';
@@ -24,8 +26,6 @@ import {
 import {Incident, IncidentStatus} from '../types';
 import {getIncidentMetricPreset, isIssueAlert} from '../utils';
 
-import {TableLayout} from './styles';
-
 /**
  * Retrieve the start/end for showing the graph of the metric
  * Will show at least 150 and no more than 10,000 data points
@@ -93,71 +93,80 @@ class AlertListRow extends Component<Props> {
           pathname: `/organizations/${orgId}/alerts/${incident.identifier}/`,
         };
     const ownerId = incident.alertRule.owner?.split(':')[1];
+    let teamName = '';
+    if (ownerId) {
+      teamName = TeamStore.getById(ownerId)?.name ?? '';
+    }
     const teamActor = ownerId
-      ? {type: 'team' as Actor['type'], id: ownerId, name: ''}
+      ? {type: 'team' as Actor['type'], id: ownerId, name: teamName}
       : null;
 
     return (
       <ErrorBoundary>
-        <IncidentPanelItem>
-          <TableLayout>
-            <Title>
-              <Link to={alertLink}>Alert #{incident.id}</Link>
-              <div>
-                {t('Triggered ')}{' '}
-                {getDynamicText({
-                  value: <TimeSince date={incident.dateStarted} extraShort />,
-                  fixed: '1w ago',
-                })}
-                <StyledTimeSeparator> | </StyledTimeSeparator>
-                {incident.status === IncidentStatus.CLOSED
-                  ? tct('Active for [duration]', {
-                      duration: (
-                        <Duration
-                          seconds={getDynamicText({value: duration, fixed: 1200})}
-                        />
-                      ),
-                    })
-                  : t('Still Active')}
-              </div>
-            </Title>
-
-            <div>{incident.title}</div>
-
-            <ProjectBadge
-              avatarSize={18}
-              project={!projectsLoaded ? {slug} : this.getProject(slug, projects)}
-            />
-
-            <FlexCenter>
-              {teamActor ? <ActorAvatar actor={teamActor} size={24} /> : '-'}
-            </FlexCenter>
-          </TableLayout>
-        </IncidentPanelItem>
+        <Title>
+          <Link to={alertLink}>{incident.title}</Link>
+        </Title>
+
+        <NoWrap>
+          {getDynamicText({
+            value: <TimeSince date={incident.dateStarted} extraShort />,
+            fixed: '1w ago',
+          })}
+        </NoWrap>
+        <NoWrap>
+          {incident.status === IncidentStatus.CLOSED ? (
+            <Duration seconds={getDynamicText({value: duration, fixed: 1200})} />
+          ) : (
+            <Tag type="warning">{t('Still Active')}</Tag>
+          )}
+        </NoWrap>
+
+        <ProjectBadge
+          avatarSize={18}
+          project={!projectsLoaded ? {slug} : this.getProject(slug, projects)}
+        />
+        <div>#{incident.id}</div>
+
+        <FlexCenter>
+          {teamActor ? (
+            <Fragment>
+              <StyledActorAvatar actor={teamActor} size={24} hasTooltip={false} />{' '}
+              <TeamWrapper>{teamActor.name}</TeamWrapper>
+            </Fragment>
+          ) : (
+            '-'
+          )}
+        </FlexCenter>
       </ErrorBoundary>
     );
   }
 }
 
-const ProjectBadge = styled(IdBadge)`
-  flex-shrink: 0;
-`;
-
-const StyledTimeSeparator = styled('span')`
-  color: ${p => p.theme.gray200};
+const Title = styled('div')`
+  ${overflowEllipsis}
+  min-width: 130px;
 `;
 
-const Title = styled('span')`
-  ${overflowEllipsis}
+const NoWrap = styled('div')`
+  white-space: nowrap;
 `;
 
-const IncidentPanelItem = styled(PanelItem)`
-  font-size: ${p => p.theme.fontSizeMedium};
+const ProjectBadge = styled(IdBadge)`
+  flex-shrink: 0;
 `;
 
 const FlexCenter = styled('div')`
+  ${overflowEllipsis}
   display: flex;
   align-items: center;
 `;
 
+const TeamWrapper = styled('span')`
+  ${overflowEllipsis}
+`;
+
+const StyledActorAvatar = styled(ActorAvatar)`
+  margin-right: ${space(1)};
+`;
+
 export default AlertListRow;

+ 0 - 13
static/app/views/alerts/list/styles.tsx

@@ -1,13 +0,0 @@
-import styled from '@emotion/styled';
-
-import space from 'app/styles/space';
-
-const TableLayout = styled('div')`
-  display: grid;
-  grid-template-columns: 3fr 2fr 2fr 0.5fr;
-  grid-column-gap: ${space(1.5)};
-  width: 100%;
-  align-items: center;
-`;
-
-export {TableLayout};

+ 27 - 1
tests/js/spec/views/alerts/list/index.spec.jsx

@@ -2,6 +2,7 @@ import {mountWithTheme} from 'sentry-test/enzyme';
 import {initializeOrg} from 'sentry-test/initializeOrg';
 
 import ProjectsStore from 'app/stores/projectsStore';
+import TeamStore from 'app/stores/teamStore';
 import IncidentsList from 'app/views/alerts/list';
 
 describe('IncidentsList', function () {
@@ -95,7 +96,7 @@ describe('IncidentsList', function () {
     await tick();
     wrapper.update();
 
-    const items = wrapper.find('IncidentPanelItem');
+    const items = wrapper.find('AlertListRow');
 
     expect(items).toHaveLength(2);
     expect(items.at(0).text()).toContain('First incident');
@@ -264,4 +265,29 @@ describe('IncidentsList', function () {
       })
     );
   });
+
+  it('displays owner from alert rule', async () => {
+    const team = TestStubs.Team();
+    MockApiClient.addMockResponse({
+      url: '/organizations/org-slug/incidents/',
+      body: [
+        TestStubs.Incident({
+          id: '123',
+          identifier: '1',
+          title: 'First incident',
+          projects: projects1,
+          alertRule: TestStubs.IncidentRule({owner: `team:${team.id}`}),
+        }),
+      ],
+    });
+    TeamStore.getById = jest.fn().mockReturnValue(team);
+    const org = {
+      ...organization,
+      features: ['incidents', 'team-alerts-ownership'],
+    };
+
+    wrapper = await createWrapper({organization: org});
+    expect(wrapper.find('TeamWrapper').text()).toBe(team.name);
+    expect(wrapper).toSnapshot();
+  });
 });