Browse Source

feat(ui): Refactor/fix group sidebar headers (#21967)

* Refactor our group sidebar headers into a new component `<SidebarSection>`. This handles the header as well as the content so that we can have more consistent spacing between the sidebar sections.
* Fixes the missing header border. Something must have changed with browser rendering, as the CSS was still there.
* Update to use `requestPromise`
* Removed usage of `<LoadingIndicator>` in favor of `<Placeholder>`

![image](https://user-images.githubusercontent.com/79684/98893687-d3da8400-2457-11eb-8ce5-c7ea2b0c8a66.png)
Billy Vong 4 years ago
parent
commit
714b0eb34e

+ 1 - 1
.github/file-filters.yml

@@ -4,13 +4,13 @@
 # as well as some configuration files that should trigger both
 frontend_lintable: &frontend_lintable
   - '**/*.[tj]{s,sx}'
-  - '**/*.less'
   - '**/tsconfig*.json'
   - '{package,now,vercel}.json'
   - '.github/workflows/js-*.yml'
 
 frontend: &frontend
   - *frontend_lintable
+  - '**/*.less'
   - 'yarn.lock'
   - '.eslint*'
   - 'docs-ui/**'

+ 18 - 16
src/sentry/static/sentry/app/components/group/externalIssuesList.tsx

@@ -18,6 +18,7 @@ import ErrorBoundary from 'app/components/errorBoundary';
 import ExternalIssueActions from 'app/components/group/externalIssueActions';
 import ExternalIssueStore from 'app/stores/externalIssueStore';
 import IssueSyncListElement from 'app/components/issueSyncListElement';
+import Placeholder from 'app/components/placeholder';
 import PluginActions from 'app/components/group/pluginActions';
 import {IconGeneric} from 'app/icons';
 import SentryAppComponentsStore from 'app/stores/sentryAppComponentsStore';
@@ -26,6 +27,8 @@ import SentryAppInstallationStore from 'app/stores/sentryAppInstallationsStore';
 import space from 'app/styles/space';
 import withOrganization from 'app/utils/withOrganization';
 
+import SidebarSection from './sidebarSection';
+
 type Props = AsyncComponent['props'] & {
   group: Group;
   project: Project;
@@ -209,18 +212,25 @@ class ExternalIssueList extends AsyncComponent<Props, State> {
       : null;
   }
 
+  renderLoading() {
+    return (
+      <SidebarSection data-test-id="linked-issues" title={t('Linked Issues')}>
+        <Placeholder height="120px" />
+      </SidebarSection>
+    );
+  }
+
   renderBody() {
     const sentryAppIssues = this.renderSentryAppIssues();
     const integrationIssues = this.renderIntegrationIssues(this.state.integrations);
     const pluginIssues = this.renderPluginIssues();
     const pluginActions = this.renderPluginActions();
+    const showSetup =
+      !sentryAppIssues && !integrationIssues && !pluginIssues && !pluginActions;
 
-    if (!sentryAppIssues && !integrationIssues && !pluginIssues && !pluginActions) {
-      return (
-        <React.Fragment>
-          <h6 data-test-id="linked-issues">
-            <span>Linked Issues</span>
-          </h6>
+    return (
+      <SidebarSection data-test-id="linked-issues" title={t('Linked Issues')}>
+        {showSetup && (
           <AlertLink
             icon={<IconGeneric />}
             priority="muted"
@@ -229,20 +239,12 @@ class ExternalIssueList extends AsyncComponent<Props, State> {
           >
             {t('Set up Issue Tracking')}
           </AlertLink>
-        </React.Fragment>
-      );
-    }
-
-    return (
-      <React.Fragment>
-        <h6 data-test-id="linked-issues">
-          <span>Linked Issues</span>
-        </h6>
+        )}
         {sentryAppIssues && <Wrapper>{sentryAppIssues}</Wrapper>}
         {integrationIssues && <Wrapper>{integrationIssues}</Wrapper>}
         {pluginIssues && <Wrapper>{pluginIssues}</Wrapper>}
         {pluginActions && <Wrapper>{pluginActions}</Wrapper>}
-      </React.Fragment>
+      </SidebarSection>
     );
   }
 }

+ 4 - 3
src/sentry/static/sentry/app/components/group/participants.tsx

@@ -6,13 +6,14 @@ import {tn} from 'app/locale';
 import UserAvatar from 'app/components/avatar/userAvatar';
 import space from 'app/styles/space';
 
+import SidebarSection from './sidebarSection';
+
 type Props = {
   participants: Group['participants'];
 };
 
 const GroupParticipants = ({participants}: Props) => (
-  <div>
-    <h6>{tn('%s Participant', '%s Participants', participants.length)}</h6>
+  <SidebarSection title={tn('%s Participant', '%s Participants', participants.length)}>
     <Faces>
       {participants.map(user => (
         <Face key={user.username}>
@@ -20,7 +21,7 @@ const GroupParticipants = ({participants}: Props) => (
         </Face>
       ))}
     </Faces>
-  </div>
+  </SidebarSection>
 );
 
 export default GroupParticipants;

+ 4 - 9
src/sentry/static/sentry/app/components/group/releaseChart.tsx

@@ -1,14 +1,14 @@
 import React from 'react';
-import styled from '@emotion/styled';
 
 import MiniBarChart from 'app/components/charts/miniBarChart';
 import {t} from 'app/locale';
 import theme from 'app/utils/theme';
-import space from 'app/styles/space';
 import {formatVersion} from 'app/utils/formatters';
 import {Series} from 'app/types/echarts';
 import {Group, TimeseriesValue, Release} from 'app/types';
 
+import SidebarSection from './sidebarSection';
+
 type Markers = React.ComponentProps<typeof MiniBarChart>['markers'];
 
 /**
@@ -101,8 +101,7 @@ function GroupReleaseChart(props: Props) {
   }
 
   return (
-    <Wrapper className={className}>
-      <h6>{title}</h6>
+    <SidebarSection secondary title={title} className={className}>
       <MiniBarChart
         isGroupedByDate
         showTimeInTooltip
@@ -110,12 +109,8 @@ function GroupReleaseChart(props: Props) {
         series={series}
         markers={markers}
       />
-    </Wrapper>
+    </SidebarSection>
   );
 }
 
 export default GroupReleaseChart;
-
-const Wrapper = styled('div')`
-  margin-bottom: ${space(2)};
-`;

+ 54 - 54
src/sentry/static/sentry/app/components/group/releaseStats.tsx

@@ -1,14 +1,14 @@
 import React from 'react';
-import styled from '@emotion/styled';
 
-import LoadingIndicator from 'app/components/loadingIndicator';
 import GroupReleaseChart from 'app/components/group/releaseChart';
+import Placeholder from 'app/components/placeholder';
 import SeenInfo from 'app/components/group/seenInfo';
 import getDynamicText from 'app/utils/getDynamicText';
-import space from 'app/styles/space';
 import {t} from 'app/locale';
 import {Environment, Group, Organization, Project} from 'app/types';
 
+import SidebarSection from './sidebarSection';
+
 type Props = {
   organization: Organization;
   project: Project;
@@ -42,44 +42,44 @@ const GroupReleaseStats = ({
   const hasRelease = new Set(project.features).has('releases');
 
   return (
-    <div className="env-stats">
-      <h6>
-        <span data-test-id="env-label">{environmentLabel}</span>
-      </h6>
-
-      <div className="env-content">
-        {!group || !allEnvironments ? (
-          <LoadingIndicator />
-        ) : (
-          <React.Fragment>
-            <GroupReleaseChart
-              group={allEnvironments}
-              environment={environmentLabel}
-              environmentStats={group.stats}
-              release={group.currentRelease ? group.currentRelease.release : null}
-              releaseStats={group.currentRelease ? group.currentRelease.stats : null}
-              statsPeriod="24h"
-              title={t('Last 24 Hours')}
-              firstSeen={group.firstSeen}
-              lastSeen={group.lastSeen}
-            />
-            <GroupReleaseChart
-              group={allEnvironments}
-              environment={environmentLabel}
-              environmentStats={group.stats}
-              release={group.currentRelease ? group.currentRelease.release : null}
-              releaseStats={group.currentRelease ? group.currentRelease.stats : null}
-              statsPeriod="30d"
-              title={t('Last 30 Days')}
-              className="bar-chart-small"
-              firstSeen={group.firstSeen}
-              lastSeen={group.lastSeen}
-            />
+    <SidebarSection title={<span data-test-id="env-label">{environmentLabel}</span>}>
+      {!group || !allEnvironments ? (
+        <Placeholder height="288px" />
+      ) : (
+        <React.Fragment>
+          <GroupReleaseChart
+            group={allEnvironments}
+            environment={environmentLabel}
+            environmentStats={group.stats}
+            release={group.currentRelease ? group.currentRelease.release : null}
+            releaseStats={group.currentRelease ? group.currentRelease.stats : null}
+            statsPeriod="24h"
+            title={t('Last 24 Hours')}
+            firstSeen={group.firstSeen}
+            lastSeen={group.lastSeen}
+          />
+          <GroupReleaseChart
+            group={allEnvironments}
+            environment={environmentLabel}
+            environmentStats={group.stats}
+            release={group.currentRelease ? group.currentRelease.release : null}
+            releaseStats={group.currentRelease ? group.currentRelease.stats : null}
+            statsPeriod="30d"
+            title={t('Last 30 Days')}
+            className="bar-chart-small"
+            firstSeen={group.firstSeen}
+            lastSeen={group.lastSeen}
+          />
 
-            <Subheading>
-              <span>{t('Last seen')}</span>
-              {environments.length > 0 && <small>({environmentLabel})</small>}
-            </Subheading>
+          <SidebarSection
+            secondary
+            title={
+              <React.Fragment>
+                <span>{t('Last seen')}</span>
+                {environments.length > 0 && <small>({environmentLabel})</small>}
+              </React.Fragment>
+            }
+          >
             <SeenInfo
               orgSlug={orgSlug}
               projectId={projectId}
@@ -94,12 +94,17 @@ const GroupReleaseStats = ({
               release={group.lastRelease || null}
               title={t('Last seen')}
             />
+          </SidebarSection>
 
-            <Subheading>
-              <span>{t('First seen')}</span>
-              {environments.length > 0 && <small>({environmentLabel})</small>}
-            </Subheading>
-
+          <SidebarSection
+            secondary
+            title={
+              <React.Fragment>
+                <span>{t('First seen')}</span>
+                {environments.length > 0 && <small>({environmentLabel})</small>}
+              </React.Fragment>
+            }
+          >
             <SeenInfo
               orgSlug={orgSlug}
               projectId={projectId}
@@ -114,16 +119,11 @@ const GroupReleaseStats = ({
               release={group.firstRelease || null}
               title={t('First seen')}
             />
-          </React.Fragment>
-        )}
-      </div>
-    </div>
+          </SidebarSection>
+        </React.Fragment>
+      )}
+    </SidebarSection>
   );
 };
 
-const Subheading = styled('h6')`
-  margin-bottom: ${space(0.25)} !important;
-  margin-top: ${space(4)};
-`;
-
 export default React.memo(GroupReleaseStats);

+ 138 - 122
src/sentry/static/sentry/app/components/group/sidebar.tsx

@@ -3,6 +3,7 @@ import isEqual from 'lodash/isEqual';
 import isObject from 'lodash/isObject';
 import keyBy from 'lodash/keyBy';
 import pickBy from 'lodash/pickBy';
+import styled from '@emotion/styled';
 
 import {Client} from 'app/api';
 import {addLoadingMessage, clearIndicators} from 'app/actionCreators/indicator';
@@ -14,7 +15,9 @@ import GroupReleaseStats from 'app/components/group/releaseStats';
 import GroupTagDistributionMeter from 'app/components/group/tagDistributionMeter';
 import GuideAnchor from 'app/components/assistant/guideAnchor';
 import LoadingError from 'app/components/loadingError';
+import Placeholder from 'app/components/placeholder';
 import SuggestedOwners from 'app/components/group/suggestedOwners/suggestedOwners';
+import space from 'app/styles/space';
 import withApi from 'app/utils/withApi';
 import {
   Event,
@@ -25,6 +28,8 @@ import {
   TagWithTopValues,
 } from 'app/types';
 
+import SidebarSection from './sidebarSection';
+
 type Props = {
   api: Client;
   organization: Organization;
@@ -32,6 +37,7 @@ type Props = {
   group: Group;
   event: Event | null;
   environments: Environment[];
+  className?: string;
 };
 
 type State = {
@@ -43,74 +49,73 @@ type State = {
 };
 
 class GroupSidebar extends React.Component<Props, State> {
-  constructor(props) {
-    super(props);
-
-    this.state = {
-      participants: [],
-      environments: props.environments,
-    };
-  }
+  state: State = {
+    participants: [],
+    environments: this.props.environments,
+  };
 
-  componentDidMount() {
+  async componentDidMount() {
     const {group, api} = this.props;
 
-    api.request(`/issues/${group.id}/participants/`, {
-      success: data => {
-        this.setState({
-          participants: data,
-          error: false,
-        });
-      },
-      error: () => {
-        this.setState({
-          error: true,
-        });
-      },
-    });
-
-    // Fetch group data for all environments since the one passed in props is filtered for the selected environment
-    // The charts rely on having all environment data as well as the data for the selected env
-    this.props.api.request(`/issues/${group.id}/`, {
-      success: data => {
-        this.setState({
-          allEnvironmentsGroupData: data,
-        });
-      },
-      error: () => {
-        this.setState({
-          error: true,
-        });
-      },
-    });
-
+    this.fetchParticipants();
     this.fetchTagData();
+
+    try {
+      const allEnvironmentsGroupData = await api.requestPromise(`/issues/${group.id}/`);
+      // eslint-disable-next-line react/no-did-mount-set-state
+      this.setState({
+        allEnvironmentsGroupData,
+      });
+    } catch {
+      // eslint-disable-next-line react/no-did-mount-set-state
+      this.setState({
+        error: true,
+      });
+    }
   }
 
-  UNSAFE_componentWillReceiveProps(nextProps) {
+  UNSAFE_componentWillReceiveProps(nextProps: Props) {
     if (!isEqual(nextProps.environments, this.props.environments)) {
       this.setState({environments: nextProps.environments}, this.fetchTagData);
     }
   }
 
-  fetchTagData() {
+  async fetchParticipants() {
+    const {group, api} = this.props;
+
+    try {
+      const participants = await api.requestPromise(`/issues/${group.id}/participants/`);
+      this.setState({
+        participants,
+        error: false,
+      });
+      return participants;
+    } catch {
+      this.setState({
+        error: true,
+      });
+      return [];
+    }
+  }
+
+  async fetchTagData() {
     const {api, group} = this.props;
 
-    // Fetch the top values for the current group's top tags.
-    api.request(`/issues/${group.id}/tags/`, {
-      query: pickBy({
-        key: group.tags.map(data => data.key),
-        environment: this.state.environments.map(env => env.name),
-      }),
-      success: data => {
-        this.setState({tagsWithTopValues: keyBy(data, 'key')});
-      },
-      error: () => {
-        this.setState({
-          error: true,
-        });
-      },
-    });
+    try {
+      // Fetch the top values for the current group's top tags.
+      const data = await api.requestPromise(`/issues/${group.id}/tags/`, {
+        query: pickBy({
+          key: group.tags.map(tag => tag.key),
+          environment: this.state.environments.map(env => env.name),
+        }),
+      });
+      this.setState({tagsWithTopValues: keyBy(data, 'key')});
+    } catch {
+      this.setState({
+        tagsWithTopValues: {},
+        error: true,
+      });
+    }
   }
 
   toggleSubscription() {
@@ -134,20 +139,9 @@ class GroupSidebar extends React.Component<Props, State> {
         },
       },
       {
-        complete: () => {
-          api.request(`/issues/${group.id}/participants/`, {
-            success: data => {
-              this.setState({
-                participants: data,
-                error: false,
-              });
-              clearIndicators();
-            },
-            error: () => {
-              this.setState({error: true});
-              clearIndicators();
-            },
-          });
+        complete: async () => {
+          await this.fetchParticipants();
+          clearIndicators();
         },
       }
     );
@@ -160,28 +154,23 @@ class GroupSidebar extends React.Component<Props, State> {
       // # TODO(dcramer): remove plugin.title check in Sentry 8.22+
       if (issue) {
         issues.push(
-          <dl key={plugin.slug}>
-            <dt>{`${plugin.shortName || plugin.name || plugin.title}: `}</dt>
-            <dd>
-              <a href={issue.url}>
-                {isObject(issue.label) ? issue.label.id : issue.label}
-              </a>
-            </dd>
-          </dl>
+          <React.Fragment key={plugin.slug}>
+            <span>{`${plugin.shortName || plugin.name || plugin.title}: `}</span>
+            <a href={issue.url}>{isObject(issue.label) ? issue.label.id : issue.label}</a>
+          </React.Fragment>
         );
       }
     });
-    if (issues.length) {
-      return (
-        <div>
-          <h6>
-            <span>{t('External Issues')}</span>
-          </h6>
-          {issues}
-        </div>
-      );
+
+    if (!issues.length) {
+      return null;
     }
-    return null;
+
+    return (
+      <SidebarSection title={t('External Issues')}>
+        <ExternalIssues>{issues}</ExternalIssues>
+      </SidebarSection>
+    );
   }
 
   renderParticipantData() {
@@ -199,13 +188,14 @@ class GroupSidebar extends React.Component<Props, State> {
   }
 
   render() {
-    const {event, group, organization, project, environments} = this.props;
+    const {className, event, group, organization, project, environments} = this.props;
     const {allEnvironmentsGroupData, tagsWithTopValues} = this.state;
     const projectId = project.slug;
 
     return (
-      <div className="group-stats">
+      <div className={className}>
         {event && <SuggestedOwners project={project} group={group} event={event} />}
+
         <GroupReleaseStats
           organization={organization}
           project={project}
@@ -222,38 +212,48 @@ class GroupSidebar extends React.Component<Props, State> {
 
         {this.renderPluginIssue()}
 
-        <h6>
-          <GuideAnchor target="tags" position="bottom">
-            <span>{t('Tags')}</span>
-          </GuideAnchor>
-        </h6>
-        {tagsWithTopValues &&
-          group.tags.map(tag => {
-            const tagWithTopValues = tagsWithTopValues[tag.key];
-            const topValues = tagWithTopValues ? tagWithTopValues.topValues : [];
-            const topValuesTotal = tagWithTopValues ? tagWithTopValues.totalValues : 0;
-
-            return (
-              <GroupTagDistributionMeter
-                key={tag.key}
-                tag={tag.key}
-                totalValues={topValuesTotal}
-                topValues={topValues}
-                name={tag.name}
-                data-test-id="group-tag"
-                organization={organization}
-                projectId={projectId}
-                group={group}
-              />
-            );
-          })}
-        {group.tags.length === 0 && (
-          <p data-test-id="no-tags">
-            {environments.length
-              ? t('No tags found in the selected environments')
-              : t('No tags found')}
-          </p>
-        )}
+        <SidebarSection
+          title={
+            <GuideAnchor target="tags" position="bottom">
+              {t('Tags')}
+            </GuideAnchor>
+          }
+        >
+          {!tagsWithTopValues ? (
+            <TagPlaceholders>
+              <Placeholder height="40px" />
+              <Placeholder height="40px" />
+              <Placeholder height="40px" />
+              <Placeholder height="40px" />
+            </TagPlaceholders>
+          ) : (
+            group.tags.map(tag => {
+              const tagWithTopValues = tagsWithTopValues[tag.key];
+              const topValues = tagWithTopValues ? tagWithTopValues.topValues : [];
+              const topValuesTotal = tagWithTopValues ? tagWithTopValues.totalValues : 0;
+
+              return (
+                <GroupTagDistributionMeter
+                  key={tag.key}
+                  tag={tag.key}
+                  totalValues={topValuesTotal}
+                  topValues={topValues}
+                  name={tag.name}
+                  organization={organization}
+                  projectId={projectId}
+                  group={group}
+                />
+              );
+            })
+          )}
+          {group.tags.length === 0 && (
+            <p data-test-id="no-tags">
+              {environments.length
+                ? t('No tags found in the selected environments')
+                : t('No tags found')}
+            </p>
+          )}
+        </SidebarSection>
 
         {this.renderParticipantData()}
       </div>
@@ -261,4 +261,20 @@ class GroupSidebar extends React.Component<Props, State> {
   }
 }
 
-export default withApi(GroupSidebar);
+const TagPlaceholders = styled('div')`
+  display: grid;
+  gap: ${space(1)};
+  grid-auto-flow: row;
+`;
+
+const StyledGroupSidebar = styled(GroupSidebar)`
+  font-size: ${p => p.theme.fontSizeMedium};
+`;
+
+const ExternalIssues = styled('div')`
+  display: grid;
+  grid-template-columns: auto max-content;
+  gap: ${space(2)};
+`;
+
+export default withApi(StyledGroupSidebar);

+ 54 - 0
src/sentry/static/sentry/app/components/group/sidebarSection.tsx

@@ -0,0 +1,54 @@
+import React from 'react';
+import styled from '@emotion/styled';
+
+import space from 'app/styles/space';
+
+const Heading = styled('h5')`
+  display: flex;
+  align-items: center;
+  margin-bottom: ${space(2)};
+  font-size: ${p => p.theme.fontSizeMedium};
+
+  &:after {
+    flex: 1;
+    display: block;
+    content: '';
+    border-top: 1px solid ${p => p.theme.innerBorder};
+    margin-left: ${space(1)};
+  }
+`;
+
+const Subheading = styled('h6')`
+  color: ${p => p.theme.gray300};
+  display: flex;
+  font-size: ${p => p.theme.fontSizeExtraSmall};
+  text-transform: uppercase;
+  justify-content: space-between;
+  margin-bottom: ${space(1)};
+`;
+
+type Props = {
+  title: React.ReactNode;
+  children: React.ReactNode;
+  secondary?: boolean;
+} & Omit<React.HTMLProps<HTMLHeadingElement>, 'title'>;
+
+/**
+ * Used to add a new section in Issue Details's sidebar.
+ */
+function SidebarSection({title, children, secondary, ...props}: Props) {
+  const HeaderComponent = secondary ? Subheading : Heading;
+
+  return (
+    <React.Fragment>
+      <HeaderComponent {...props}>{title}</HeaderComponent>
+      <SectionContent secondary={secondary}>{children}</SectionContent>
+    </React.Fragment>
+  );
+}
+
+const SectionContent = styled('div')<{secondary?: boolean}>`
+  margin-bottom: ${p => (p.secondary ? space(2) : space(3))};
+`;
+
+export default SidebarSection;

+ 40 - 33
src/sentry/static/sentry/app/components/group/suggestedOwners/ownershipRules.tsx

@@ -11,7 +11,7 @@ import Hovercard from 'app/components/hovercard';
 import space from 'app/styles/space';
 import {Project, Organization} from 'app/types';
 
-import {Wrapper, Header, Heading} from './styles';
+import SidebarSection from '../sidebarSection';
 
 type Props = {
   project: Project;
@@ -25,48 +25,55 @@ const OwnershipRules = ({project, organization, issueId}: Props) => {
   };
 
   return (
-    <Wrapper>
-      <Header>
-        <Heading>{t('Ownership Rules')}</Heading>
-        <ClassNames>
-          {({css}) => (
-            <Hovercard
-              body={
-                <HelpfulBody>
-                  <p>
-                    {t(
-                      'Ownership rules allow you to associate file paths and URLs to specific teams or users, so alerts can be routed to the right people.'
-                    )}
-                  </p>
-                  <Button
-                    href="https://docs.sentry.io/workflow/issue-owners/"
-                    priority="primary"
-                  >
-                    {t('Learn more')}
-                  </Button>
-                </HelpfulBody>
-              }
-              containerClassName={css`
-                display: flex;
-                align-items: center;
-              `}
-            >
-              <IconQuestion size="xs" />
-            </Hovercard>
-          )}
-        </ClassNames>
-      </Header>
+    <SidebarSection
+      title={
+        <React.Fragment>
+          {t('Ownership Rules')}
+          <ClassNames>
+            {({css}) => (
+              <Hovercard
+                body={
+                  <HelpfulBody>
+                    <p>
+                      {t(
+                        'Ownership rules allow you to associate file paths and URLs to specific teams or users, so alerts can be routed to the right people.'
+                      )}
+                    </p>
+                    <Button
+                      href="https://docs.sentry.io/workflow/issue-owners/"
+                      priority="primary"
+                    >
+                      {t('Learn more')}
+                    </Button>
+                  </HelpfulBody>
+                }
+                containerClassName={css`
+                  display: flex;
+                  align-items: center;
+                `}
+              >
+                <StyledIconQuestion size="xs" />
+              </Hovercard>
+            )}
+          </ClassNames>
+        </React.Fragment>
+      }
+    >
       <GuideAnchor target="owners" position="bottom" offset={space(3)}>
         <Button onClick={handleOpenCreateOwnershipRule} size="small">
           {t('Create Ownership Rule')}
         </Button>
       </GuideAnchor>
-    </Wrapper>
+    </SidebarSection>
   );
 };
 
 export {OwnershipRules};
 
+const StyledIconQuestion = styled(IconQuestion)`
+  margin-left: ${space(0.5)};
+`;
+
 const HelpfulBody = styled('div')`
   padding: ${space(1)};
   text-align: center;

+ 0 - 22
src/sentry/static/sentry/app/components/group/suggestedOwners/styles.tsx

@@ -1,22 +0,0 @@
-import styled from '@emotion/styled';
-
-import space from 'app/styles/space';
-
-const Heading = styled('h6')`
-  margin: 0 !important;
-  font-weight: 600;
-`;
-
-const Header = styled('div')`
-  display: grid;
-  align-items: center;
-  grid-template-columns: max-content max-content;
-  grid-gap: ${space(0.5)};
-  margin-bottom: ${space(2)};
-`;
-
-const Wrapper = styled('div')`
-  margin-bottom: ${space(3)};
-`;
-
-export {Heading, Header, Wrapper};

+ 13 - 8
src/sentry/static/sentry/app/components/group/suggestedOwners/suggestedAssignees.tsx

@@ -8,7 +8,7 @@ import SuggestedOwnerHovercard from 'app/components/group/suggestedOwnerHovercar
 import {Actor, Commit} from 'app/types';
 import space from 'app/styles/space';
 
-import {Wrapper, Header, Heading} from './styles';
+import SidebarSection from '../sidebarSection';
 
 type Owner = {
   actor: Actor;
@@ -22,11 +22,14 @@ type Props = {
 };
 
 const SuggestedAssignees = ({owners, onAssign}: Props) => (
-  <Wrapper>
-    <Header>
-      <Heading>{t('Suggested Assignees')}</Heading>
-      <StyledSmall>{t('Click to assign')}</StyledSmall>
-    </Header>
+  <SidebarSection
+    title={
+      <React.Fragment>
+        {t('Suggested Assignees')}
+        <Subheading>{t('Click to assign')}</Subheading>
+      </React.Fragment>
+    }
+  >
     <Content>
       {owners.map((owner, i) => (
         <SuggestedOwnerHovercard
@@ -44,15 +47,17 @@ const SuggestedAssignees = ({owners, onAssign}: Props) => (
         </SuggestedOwnerHovercard>
       ))}
     </Content>
-  </Wrapper>
+  </SidebarSection>
 );
 
 export {SuggestedAssignees};
 
-const StyledSmall = styled('small')`
+const Subheading = styled('small')`
   font-size: ${p => p.theme.fontSizeExtraSmall};
   color: ${p => p.theme.gray300};
   line-height: 100%;
+  font-weight: 400;
+  margin-left: ${space(0.5)};
 `;
 
 const Content = styled('div')`

Some files were not shown because too many files changed in this diff