Browse Source

fix(ui): alert wizard polish (#25383)

Added alert wizard illustrations
Tidied up design for alert wizard steps
Improved visual design for metric and issue alerts
Added max-width to alert wizard so it's easier to navigate
Robin Rendle 3 years ago
parent
commit
ec7ab268bd

+ 85 - 79
static/app/views/alerts/wizard/index.tsx

@@ -4,16 +4,13 @@ import styled from '@emotion/styled';
 
 import Feature from 'app/components/acl/feature';
 import CreateAlertButton from 'app/components/createAlertButton';
+import * as Layout from 'app/components/layouts/thirds';
 import ExternalLink from 'app/components/links/externalLink';
 import List from 'app/components/list';
 import ListItem from 'app/components/list/listItem';
-import PageHeading from 'app/components/pageHeading';
-import {PanelBody} from 'app/components/panels';
-import Placeholder from 'app/components/placeholder';
+import {Panel, PanelBody, PanelHeader} from 'app/components/panels';
 import SentryDocumentTitle from 'app/components/sentryDocumentTitle';
-import Tag from 'app/components/tag';
 import {t} from 'app/locale';
-import {PageContent, PageHeader} from 'app/styles/organization';
 import space from 'app/styles/space';
 import {Organization, Project} from 'app/types';
 import BuilderBreadCrumbs from 'app/views/alerts/builder/builderBreadCrumbs';
@@ -25,7 +22,6 @@ import {
   AlertWizardOptions,
   AlertWizardPanelContent,
   AlertWizardRuleTemplates,
-  WebVitalAlertTypes,
 } from './options';
 import RadioPanelGroup from './radioPanelGroup';
 
@@ -94,101 +90,104 @@ class AlertWizard extends React.Component<Props, State> {
     return (
       <React.Fragment>
         <SentryDocumentTitle title={title} projectSlug={projectId} />
-        <PageContent>
-          <Feature features={['organizations:alert-wizard']}>
-            <BuilderBreadCrumbs
-              hasMetricAlerts={hasMetricAlerts}
-              orgSlug={organization.slug}
-              projectSlug={projectId}
-              title={t('Create Alert Rule')}
-            />
-            <StyledPageHeader>
-              <PageHeading>{t('What should we alert you about?')}</PageHeading>
-            </StyledPageHeader>
-            <Styledh2>{t('Errors')}</Styledh2>
-            <WizardBody>
-              <WizardOptions>
-                {AlertWizardOptions.map(({categoryHeading, options}, i) => (
-                  <OptionsWrapper key={categoryHeading}>
-                    {i > 0 && <Styledh2>{categoryHeading}</Styledh2>}
-                    <RadioPanelGroup
-                      choices={options.map(alertType => {
-                        return [
-                          alertType,
-                          AlertWizardAlertNames[alertType],
-                          ...(WebVitalAlertTypes.has(alertType)
-                            ? [<Tag key={alertType}>{t('Web Vital')}</Tag>]
-                            : []),
-                        ] as [AlertType, string, React.ReactNode];
-                      })}
-                      onChange={this.handleChangeAlertOption}
-                      value={alertOption}
-                      label="alert-option"
-                    />
-                  </OptionsWrapper>
-                ))}
-              </WizardOptions>
-              <WizardPanel visible={!!panelContent && !!alertOption}>
-                {panelContent && alertOption && (
+
+        <Feature features={['organizations:alert-wizard']}>
+          <Layout.Header>
+            <Layout.HeaderContent>
+              <BuilderBreadCrumbs
+                hasMetricAlerts={hasMetricAlerts}
+                orgSlug={organization.slug}
+                projectSlug={projectId}
+                title={t('Create Alert Rule')}
+              />
+              <Layout.Title>{t('What should we alert you about?')}</Layout.Title>
+            </Layout.HeaderContent>
+          </Layout.Header>
+          <StyledLayoutBody>
+            <Layout.Main fullWidth>
+              <WizardBody>
+                <WizardOptions>
+                  <Styledh2>{t('Errors')}</Styledh2>
+                  {AlertWizardOptions.map(({categoryHeading, options}, i) => (
+                    <OptionsWrapper key={categoryHeading}>
+                      {i > 0 && <Styledh2>{categoryHeading}</Styledh2>}
+                      <RadioPanelGroup
+                        choices={options.map(alertType => {
+                          return [alertType, AlertWizardAlertNames[alertType]];
+                        })}
+                        onChange={this.handleChangeAlertOption}
+                        value={alertOption}
+                        label="alert-option"
+                      />
+                    </OptionsWrapper>
+                  ))}
+                </WizardOptions>
+                <WizardPanel visible={!!panelContent && !!alertOption}>
                   <WizardPanelBody>
-                    <Styledh2>{AlertWizardAlertNames[alertOption]}</Styledh2>
-                    <PanelDescription>
-                      {panelContent.description}{' '}
-                      {panelContent.docsLink && (
-                        <ExternalLink href={panelContent.docsLink}>
-                          {t('Learn more')}
-                        </ExternalLink>
-                      )}
-                    </PanelDescription>
-                    <WizardBodyPlaceholder height="250px" />
-                    <ExampleHeader>{t('Examples')}</ExampleHeader>
-                    <List symbol="bullet">
-                      {panelContent.examples.map((example, i) => (
-                        <ExampleItem key={i}>{example}</ExampleItem>
-                      ))}
-                    </List>
+                    {panelContent && alertOption && (
+                      <div>
+                        <PanelHeader>{AlertWizardAlertNames[alertOption]}</PanelHeader>
+                        <PanelBody withPadding>
+                          <PanelDescription>
+                            {panelContent.description}{' '}
+                            {panelContent.docsLink && (
+                              <ExternalLink href={panelContent.docsLink}>
+                                {t('Learn more')}
+                              </ExternalLink>
+                            )}
+                          </PanelDescription>
+                          <WizardImage src={panelContent.illustration} />
+                          <ExampleHeader>{t('Examples')}</ExampleHeader>
+                          <ExampleList symbol="bullet">
+                            {panelContent.examples.map((example, i) => (
+                              <ExampleItem key={i}>{example}</ExampleItem>
+                            ))}
+                          </ExampleList>
+                        </PanelBody>
+                      </div>
+                    )}
+                    <WizardButton>{this.renderCreateAlertButton()}</WizardButton>
                   </WizardPanelBody>
-                )}
-                {this.renderCreateAlertButton()}
-              </WizardPanel>
-            </WizardBody>
-          </Feature>
-        </PageContent>
+                </WizardPanel>
+              </WizardBody>
+            </Layout.Main>
+          </StyledLayoutBody>
+        </Feature>
       </React.Fragment>
     );
   }
 }
 
-const StyledPageHeader = styled(PageHeader)`
-  margin-bottom: ${space(4)};
-`;
-
-const WizardBodyPlaceholder = styled(Placeholder)`
-  background-color: ${p => p.theme.border};
-  opacity: 0.6;
+const StyledLayoutBody = styled(Layout.Body)`
+  margin-bottom: -${space(3)};
 `;
 
 const Styledh2 = styled('h2')`
   font-weight: normal;
-  font-size: ${p => p.theme.fontSizeExtraLarge};
+  font-size: ${p => p.theme.fontSizeLarge};
   margin-bottom: ${space(1)} !important;
 `;
 
 const WizardBody = styled('div')`
   display: flex;
+  padding-top: ${space(1)};
 `;
 
 const WizardOptions = styled('div')`
   flex: 3;
   margin-right: ${space(3)};
-  border-right: 1px solid ${p => p.theme.innerBorder};
   padding-right: ${space(3)};
+  max-width: 300px;
+`;
+
+const WizardImage = styled('img')`
+  max-height: 300px;
 `;
 
-const WizardPanel = styled('div')<{visible?: boolean}>`
+const WizardPanel = styled(Panel)<{visible?: boolean}>`
+  max-width: 700px;
   position: sticky;
   top: 20px;
-  padding: 0;
   flex: 5;
   display: flex;
   ${p => !p.visible && 'visibility: hidden'};
@@ -209,19 +208,21 @@ const WizardPanel = styled('div')<{visible?: boolean}>`
   }
 `;
 
+const ExampleList = styled(List)`
+  margin-bottom: ${space(2)} !important;
+`;
+
 const WizardPanelBody = styled(PanelBody)`
-  margin-bottom: ${space(2)};
   flex: 1;
   min-width: 100%;
 `;
 
-const PanelDescription = styled('div')`
-  color: ${p => p.theme.subText};
+const PanelDescription = styled('p')`
   margin-bottom: ${space(2)};
 `;
 
 const ExampleHeader = styled('div')`
-  margin: ${space(2)} 0;
+  margin: 0 0 ${space(1)} 0;
   font-size: ${p => p.theme.fontSizeLarge};
 `;
 
@@ -237,4 +238,9 @@ const OptionsWrapper = styled('div')`
   }
 `;
 
+const WizardButton = styled('div')`
+  border-top: 1px solid ${p => p.theme.border};
+  padding: ${space(1.5)} ${space(1.5)} ${space(1.5)} ${space(1.5)};
+`;
+
 export default AlertWizard;

+ 32 - 5
static/app/views/alerts/wizard/options.tsx

@@ -1,3 +1,15 @@
+import diagramApdex from 'sentry-images/spot/alerts-wizard-apdex.svg';
+import diagramCLS from 'sentry-images/spot/alerts-wizard-cls.svg';
+import diagramCustom from 'sentry-images/spot/alerts-wizard-custom.svg';
+import diagramErrors from 'sentry-images/spot/alerts-wizard-errors.svg';
+import diagramFailureRate from 'sentry-images/spot/alerts-wizard-failure-rate.svg';
+import diagramFID from 'sentry-images/spot/alerts-wizard-fid.svg';
+import diagramIssues from 'sentry-images/spot/alerts-wizard-issues.svg';
+import diagramLCP from 'sentry-images/spot/alerts-wizard-lcp.svg';
+import diagramThroughput from 'sentry-images/spot/alerts-wizard-throughput.svg';
+import diagramTransactionDuration from 'sentry-images/spot/alerts-wizard-transaction-duration.svg';
+import diagramUsers from 'sentry-images/spot/alerts-wizard-users-experiencing-errors.svg';
+
 import {t} from 'app/locale';
 import {Dataset, EventTypes} from 'app/views/settings/incidentRules/types';
 
@@ -69,7 +81,7 @@ type PanelContent = {
 export const AlertWizardPanelContent: Record<AlertType, PanelContent> = {
   issues: {
     description: t(
-      'Issues are groups of errors that have a similar stacktrace. You can set an alert for new issues, issue state changes, and frequency of errors or users affected by an issue.'
+      'Issues are groups of errors that have a similar stacktrace. Set an alert for new issues, when an issue changes state, frequency of errors, or users affected by an issue.'
     ),
     examples: [
       t("When the triggering event's level is fatal."),
@@ -78,6 +90,7 @@ export const AlertWizardPanelContent: Record<AlertType, PanelContent> = {
         'Create a JIRA ticket when an issue changes state from resolved to unresolved and is unassigned.'
       ),
     ],
+    illustration: diagramIssues,
   },
   num_errors: {
     description: t(
@@ -87,6 +100,7 @@ export const AlertWizardPanelContent: Record<AlertType, PanelContent> = {
       t('When the signup page has more than 10k errors in 5 minutes.'),
       t('When there are more than 500k errors in 10 minutes from a specific file.'),
     ],
+    illustration: diagramErrors,
   },
   users_experiencing_errors: {
     description: t(
@@ -96,13 +110,17 @@ export const AlertWizardPanelContent: Record<AlertType, PanelContent> = {
       t('When 100k users experience an error in 1 hour.'),
       t('When 100 users experience a problem on the Checkout page.'),
     ],
+    illustration: diagramUsers,
   },
   throughput: {
-    description: t('Throughput is the number of transactions in a period of time.'),
+    description: t(
+      'Throughput is the total number of transactions in a project and you can alert when it reaches a threshold within a period of time.'
+    ),
     examples: [
       t('When number of transactions on a key page exceeds 100k per minute.'),
       t('When number of transactions drops below a threshold.'),
     ],
+    illustration: diagramThroughput,
   },
   trans_duration: {
     description: t(
@@ -112,6 +130,7 @@ export const AlertWizardPanelContent: Record<AlertType, PanelContent> = {
       t('When any transaction is slower than 3 seconds.'),
       t('When the 75th percentile response time is higher than 250 milliseconds.'),
     ],
+    illustration: diagramTransactionDuration,
   },
   apdex: {
     description: t(
@@ -119,27 +138,33 @@ export const AlertWizardPanelContent: Record<AlertType, PanelContent> = {
     ),
     examples: [t('When apdex is below 300.')],
     docsLink: 'https://docs.sentry.io/product/performance/metrics/#apdex',
+    illustration: diagramApdex,
   },
   failure_rate: {
-    description: t('Failure rate is the percentage of unsuccessful transactions.'),
+    description: t(
+      'Failure rate is the percentage of unsuccessful transactions. Sentry treats transactions with a status other than “ok,” “canceled,” and “unknown” as failures.'
+    ),
     examples: [t('When the failure rate for an important endpoint reaches 10%.')],
     docsLink: 'https://docs.sentry.io/product/performance/metrics/#failure-rate',
+    illustration: diagramFailureRate,
   },
   lcp: {
     description: t(
-      'Largest Contentful Paint (LCP) measures loading performance. It marks the point when the largest image or text block is visible within the viewport. A fast LCP helps reassure the user that the page is useful, and we recommend LCP is less than 2.5 seconds.'
+      'Largest Contentful Paint (LCP) measures loading performance. It marks the point when the largest image or text block is visible within the viewport. A fast LCP helps reassure the user that the page is useful, and so we recommend an LCP of less than 2.5 seconds.'
     ),
     examples: [
       t('When the 75th percentile LCP of your homepage is longer than 2.5 seconds.'),
     ],
     docsLink: 'https://docs.sentry.io/product/performance/web-vitals',
+    illustration: diagramLCP,
   },
   fid: {
     description: t(
-      'First Input Delay (FID) measures interactivity as the response time when the user tries to interact with the viewport. A low FID helps ensure that a page is useful, and recommend it be less than 100 milliseconds.'
+      'First Input Delay (FID) measures interactivity as the response time when the user tries to interact with the viewport. A low FID helps ensure that a page is useful, and we recommend a FID of less than 100 milliseconds.'
     ),
     examples: [t('When the average FID of a page is longer than 4 seconds.')],
     docsLink: 'https://docs.sentry.io/product/performance/web-vitals',
+    illustration: diagramFID,
   },
   cls: {
     description: t(
@@ -147,6 +172,7 @@ export const AlertWizardPanelContent: Record<AlertType, PanelContent> = {
     ),
     examples: [t('When the CLS of a page is more than 0.5.')],
     docsLink: 'https://docs.sentry.io/product/performance/web-vitals',
+    illustration: diagramCLS,
   },
   fcp: {
     description: t(
@@ -163,6 +189,7 @@ export const AlertWizardPanelContent: Record<AlertType, PanelContent> = {
       t('When the 95th percentile FP of a page is longer than 250 milliseconds.'),
       t('When the average TTFB of a page is longer than 600 millliseconds.'),
     ],
+    illustration: diagramCustom,
   },
 };
 

+ 23 - 15
static/app/views/alerts/wizard/radioPanelGroup.tsx

@@ -1,7 +1,6 @@
 import React from 'react';
 import styled from '@emotion/styled';
 
-import {Panel, PanelBody} from 'app/components/panels';
 import Radio from 'app/components/radio';
 import space from 'app/styles/space';
 
@@ -28,18 +27,16 @@ const RadioPanelGroup = <C extends string>({
   <Container {...props} role="radiogroup" aria-labelledby={label}>
     {(choices || []).map(([id, name, extraContent], index) => (
       <RadioPanel key={index}>
-        <PanelBody>
-          <RadioLineItem role="radio" index={index} aria-checked={value === id}>
-            <Radio
-              radioSize="small"
-              aria-label={id}
-              checked={value === id}
-              onChange={(e: React.FormEvent<HTMLInputElement>) => onChange(id, e)}
-            />
-            <div>{name}</div>
-            {extraContent}
-          </RadioLineItem>
-        </PanelBody>
+        <RadioLineItem role="radio" index={index} aria-checked={value === id}>
+          <Radio
+            radioSize="small"
+            aria-label={id}
+            checked={value === id}
+            onChange={(e: React.FormEvent<HTMLInputElement>) => onChange(id, e)}
+          />
+          <div>{name}</div>
+          {extraContent}
+        </RadioLineItem>
       </RadioPanel>
     ))}
   </Container>
@@ -68,13 +65,24 @@ export const RadioLineItem = styled('label')<{
   margin: 0;
   color: ${p => p.theme.subText};
   transition: color 0.3s ease-in;
-  padding: ${space(1.5)};
+  padding: 0;
+  position: relative;
+
+  &:hover,
+  &:focus {
+    color: ${p => p.theme.textColor};
+  }
+
+  svg {
+    display: none;
+    opacity: 0;
+  }
 
   &[aria-checked='true'] {
     color: ${p => p.theme.textColor};
   }
 `;
 
-const RadioPanel = styled(Panel)`
+const RadioPanel = styled('div')`
   margin: 0;
 `;

+ 9 - 3
static/app/views/settings/incidentRules/ruleConditionsFormForWizard.tsx

@@ -130,7 +130,7 @@ class RuleConditionsFormForWizard extends React.PureComponent<Props, State> {
 
     return (
       <React.Fragment>
-        <Panel>
+        <ChartPanel>
           <StyledPanelBody>
             {this.props.thresholdChart({
               footer: (
@@ -175,8 +175,8 @@ class RuleConditionsFormForWizard extends React.PureComponent<Props, State> {
               ),
             })}
           </StyledPanelBody>
-        </Panel>
-        <StyledListItem>{t('Select Events')}</StyledListItem>
+        </ChartPanel>
+        <StyledListItem>{t('Filter events')}</StyledListItem>
         <FormRow>
           <SelectField
             name="environment"
@@ -308,6 +308,11 @@ class RuleConditionsFormForWizard extends React.PureComponent<Props, State> {
   }
 }
 
+const ChartPanel = styled(Panel)`
+  margin-bottom: ${space(4)};
+  min-height: 335px;
+`;
+
 const StyledPanelBody = styled(PanelBody)`
   ol,
   h4 {
@@ -325,6 +330,7 @@ const StyledSearchBar = styled(SearchBar)`
 
 const StyledListItem = styled(ListItem)`
   margin-bottom: ${space(1)};
+  font-size: ${p => p.theme.fontSizeExtraLarge};
 `;
 
 const FormRow = styled('div')`

+ 7 - 4
static/app/views/settings/incidentRules/ruleForm/index.tsx

@@ -703,7 +703,7 @@ class RuleFormContainer extends AsyncComponent<Props, State> {
                 </Confirm>
               ) : null
             }
-            submitLabel={t('Save Rule')}
+            submitLabel={t('Create Rule')}
           >
             <Feature organization={organization} features={['alert-wizard']}>
               {({hasFeature}) =>
@@ -718,9 +718,11 @@ class RuleFormContainer extends AsyncComponent<Props, State> {
                       onFilterSearch={this.handleFilterUpdate}
                       allowChangeEventTypes={dataset === Dataset.ERRORS}
                     />
-                    <StyledListItem>{t('Set Thesholds and Actions')}</StyledListItem>
+                    <StyledListItem>
+                      {t('Set thresholds to trigger alert')}
+                    </StyledListItem>
                     {triggerForm(hasAccess)}
-                    <StyledListItem>{t('Add a Name and Team')}</StyledListItem>
+                    <StyledListItem>{t('Add a name and team')}</StyledListItem>
                     {ruleNameOwnerForm(hasAccess)}
                   </List>
                 ) : (
@@ -747,7 +749,8 @@ class RuleFormContainer extends AsyncComponent<Props, State> {
 }
 
 const StyledListItem = styled(ListItem)`
-  margin-bottom: ${space(1)};
+  margin: ${space(2)} 0 ${space(1)} 0;
+  font-size: ${p => p.theme.fontSizeExtraLarge};
 `;
 
 const ChartHeader = styled('div')`

+ 2 - 3
static/app/views/settings/incidentRules/ruleNameOwnerForm.tsx

@@ -1,7 +1,7 @@
 import React from 'react';
 
 import Feature from 'app/components/acl/feature';
-import {Panel, PanelBody, PanelHeader} from 'app/components/panels';
+import {Panel, PanelBody} from 'app/components/panels';
 import SelectMembers from 'app/components/selectMembers';
 import {t} from 'app/locale';
 import {Organization, Project} from 'app/types';
@@ -21,7 +21,6 @@ class RuleNameOwnerForm extends React.PureComponent<Props> {
 
     return (
       <Panel>
-        <PanelHeader>{t('Give your rule a name')}</PanelHeader>
         <PanelBody>
           <TextField
             disabled={disabled}
@@ -35,7 +34,7 @@ class RuleNameOwnerForm extends React.PureComponent<Props> {
             <FormField
               name="owner"
               label={t('Team')}
-              help={t('The team that owns this alert')}
+              help={t('The team that can edit this alert.')}
             >
               {({model}) => {
                 const owner = model.getValue('owner');

+ 104 - 101
static/app/views/settings/incidentRules/triggers/actionsPanel/index.tsx

@@ -5,8 +5,9 @@ import * as Sentry from '@sentry/react';
 import {addErrorMessage} from 'app/actionCreators/indicator';
 import Button from 'app/components/button';
 import SelectControl from 'app/components/forms/selectControl';
+import ListItem from 'app/components/list/listItem';
 import LoadingIndicator from 'app/components/loadingIndicator';
-import {Panel, PanelBody, PanelHeader, PanelItem} from 'app/components/panels';
+import {PanelItem} from 'app/components/panels';
 import {IconAdd} from 'app/icons';
 import {t} from 'app/locale';
 import space from 'app/styles/space';
@@ -15,8 +16,6 @@ import {uniqueId} from 'app/utils/guid';
 import {removeAtArrayIndex} from 'app/utils/removeAtArrayIndex';
 import {replaceAtArrayIndex} from 'app/utils/replaceAtArrayIndex';
 import withOrganization from 'app/utils/withOrganization';
-import FieldHelp from 'app/views/settings/components/forms/field/fieldHelp';
-import FieldLabel from 'app/views/settings/components/forms/field/fieldLabel';
 import ActionTargetSelector from 'app/views/settings/incidentRules/triggers/actionsPanel/actionTargetSelector';
 import DeleteActionButton from 'app/views/settings/incidentRules/triggers/actionsPanel/deleteActionButton';
 import {
@@ -249,108 +248,100 @@ class ActionsPanel extends React.PureComponent<Props> {
       .sort((a, b) => a.dateCreated - b.dateCreated);
 
     return (
-      <Panel>
-        <PanelHeader>{t('Actions')}</PanelHeader>
-        <PanelBody withPadding>
-          <FieldLabel>{t('Add an action')}</FieldLabel>
-          <FieldHelp>
-            {t(
-              'We can send you an email or activate an integration when any of the thresholds above are met.'
-            )}
-          </FieldHelp>
-        </PanelBody>
-        <PanelBody>
-          {loading && <LoadingIndicator />}
-          {actions.map(({action, actionIdx, triggerIndex, availableAction}) => {
-            return (
-              <PanelItemWrapper key={action.id ?? action.unsavedId}>
-                <RuleRowContainer>
-                  <PanelItemGrid>
-                    <PanelItemSelects>
-                      <SelectControl
-                        name="select-level"
-                        aria-label={t('Select a status level')}
-                        isDisabled={disabled || loading}
-                        placeholder={t('Select Level')}
-                        onChange={this.handleChangeActionLevel.bind(
-                          this,
-                          triggerIndex,
-                          actionIdx
-                        )}
-                        value={triggerIndex}
-                        options={levels}
-                      />
+      <React.Fragment>
+        <PerformActionsListItem>{t('Perform actions')}</PerformActionsListItem>
+        <AlertParagraph>
+          {t(
+            'When any of the thresholds above are met, perform an action such as sending an email or using an integration.'
+          )}
+        </AlertParagraph>
+        {loading && <LoadingIndicator />}
+        {actions.map(({action, actionIdx, triggerIndex, availableAction}) => {
+          return (
+            <div key={action.id ?? action.unsavedId}>
+              <RuleRowContainer>
+                <PanelItemGrid>
+                  <PanelItemSelects>
+                    <SelectControl
+                      name="select-level"
+                      aria-label={t('Select a status level')}
+                      isDisabled={disabled || loading}
+                      placeholder={t('Select Level')}
+                      onChange={this.handleChangeActionLevel.bind(
+                        this,
+                        triggerIndex,
+                        actionIdx
+                      )}
+                      value={triggerIndex}
+                      options={levels}
+                    />
+                    <SelectControl
+                      name="select-action"
+                      aria-label={t('Select an Action')}
+                      isDisabled={disabled || loading}
+                      placeholder={t('Select Action')}
+                      onChange={this.handleChangeActionType.bind(
+                        this,
+                        triggerIndex,
+                        actionIdx
+                      )}
+                      value={getActionUniqueKey(action)}
+                      options={items ?? []}
+                    />
 
+                    {availableAction && availableAction.allowedTargetTypes.length > 1 ? (
                       <SelectControl
-                        name="select-action"
-                        aria-label={t('Select an Action')}
                         isDisabled={disabled || loading}
-                        placeholder={t('Select Action')}
-                        onChange={this.handleChangeActionType.bind(
-                          this,
-                          triggerIndex,
-                          actionIdx
+                        value={action.targetType}
+                        options={availableAction?.allowedTargetTypes?.map(
+                          allowedType => ({
+                            value: allowedType,
+                            label: TargetLabel[allowedType],
+                          })
                         )}
-                        value={getActionUniqueKey(action)}
-                        options={items ?? []}
-                      />
-
-                      {availableAction &&
-                      availableAction.allowedTargetTypes.length > 1 ? (
-                        <SelectControl
-                          isDisabled={disabled || loading}
-                          value={action.targetType}
-                          options={availableAction?.allowedTargetTypes?.map(
-                            allowedType => ({
-                              value: allowedType,
-                              label: TargetLabel[allowedType],
-                            })
-                          )}
-                          onChange={this.handleChangeTarget.bind(
-                            this,
-                            triggerIndex,
-                            actionIdx
-                          )}
-                        />
-                      ) : null}
-                      <ActionTargetSelector
-                        action={action}
-                        availableAction={availableAction}
-                        disabled={disabled}
-                        loading={loading}
-                        onChange={this.handleChangeTargetIdentifier.bind(
+                        onChange={this.handleChangeTarget.bind(
                           this,
                           triggerIndex,
                           actionIdx
                         )}
-                        organization={organization}
-                        project={project}
                       />
-                    </PanelItemSelects>
-                    <DeleteActionButton
-                      triggerIndex={triggerIndex}
-                      index={actionIdx}
-                      onClick={this.handleDeleteAction}
+                    ) : null}
+                    <ActionTargetSelector
+                      action={action}
+                      availableAction={availableAction}
                       disabled={disabled}
+                      loading={loading}
+                      onChange={this.handleChangeTargetIdentifier.bind(
+                        this,
+                        triggerIndex,
+                        actionIdx
+                      )}
+                      organization={organization}
+                      project={project}
                     />
-                  </PanelItemGrid>
-                </RuleRowContainer>
-              </PanelItemWrapper>
-            );
-          })}
-          <StyledPanelItem>
-            <Button
-              type="button"
-              disabled={disabled || loading}
-              size="small"
-              icon={<IconAdd isCircled color="gray300" />}
-              onClick={this.handleAddAction}
-            >
-              {t('Add New Action')}
-            </Button>
-          </StyledPanelItem>
-        </PanelBody>
-      </Panel>
+                  </PanelItemSelects>
+                  <DeleteActionButton
+                    triggerIndex={triggerIndex}
+                    index={actionIdx}
+                    onClick={this.handleDeleteAction}
+                    disabled={disabled}
+                  />
+                </PanelItemGrid>
+              </RuleRowContainer>
+            </div>
+          );
+        })}
+        <ActionSection>
+          <Button
+            type="button"
+            disabled={disabled || loading}
+            icon={<IconAdd isCircled color="gray300" />}
+            onClick={this.handleAddAction}
+          >
+            {t('Add Action')}
+          </Button>
+        </ActionSection>
+      </React.Fragment>
     );
   }
 }
@@ -359,15 +350,22 @@ const ActionsPanelWithSpace = styled(ActionsPanel)`
   margin-top: ${space(4)};
 `;
 
-const PanelItemWrapper = styled(`div`)`
-  padding: ${space(0.5)} ${space(2)} ${space(1)};
+const ActionSection = styled('div')`
+  margin-top: ${space(1)};
+  margin-bottom: ${space(3)};
+`;
+
+const AlertParagraph = styled('p')`
+  color: ${p => p.theme.subText};
+  margin-left: ${space(4)};
+  margin-bottom: ${space(1)};
 `;
 
 const PanelItemGrid = styled(PanelItem)`
   display: flex;
   align-items: center;
-  padding: ${space(1)};
   border-bottom: 0;
+  padding: ${space(1)};
 `;
 
 const PanelItemSelects = styled('div')`
@@ -383,14 +381,19 @@ const PanelItemSelects = styled('div')`
   }
 `;
 
-const StyledPanelItem = styled(PanelItem)`
-  padding: ${space(1)} ${space(2)} ${space(2)};
-`;
-
 const RuleRowContainer = styled('div')`
   background-color: ${p => p.theme.backgroundSecondary};
   border-radius: ${p => p.theme.borderRadius};
   border: 1px ${p => p.theme.border} solid;
 `;
 
+const StyledListItem = styled(ListItem)`
+  margin: ${space(2)} 0 ${space(3)} 0;
+  font-size: ${p => p.theme.fontSizeExtraLarge};
+`;
+
+const PerformActionsListItem = styled(StyledListItem)`
+  margin-bottom: 0;
+`;
+
 export default withOrganization(ActionsPanelWithSpace);

+ 2 - 2
static/app/views/settings/incidentRules/triggers/form.tsx

@@ -182,7 +182,7 @@ class TriggerFormContainer extends React.Component<TriggerFormContainerProps> {
               triggerLabel={
                 <React.Fragment>
                   <TriggerIndicator size={12} />
-                  {isCritical ? t('Critical Status') : t('Warning Status')}
+                  {isCritical ? t('Critical') : t('Warning')}
                 </React.Fragment>
               }
               placeholder={isCritical ? '300' : t('None')}
@@ -208,7 +208,7 @@ class TriggerFormContainer extends React.Component<TriggerFormContainerProps> {
           triggerLabel={
             <React.Fragment>
               <ResolvedIndicator size={12} />
-              {t('Resolved Status')}
+              {t('Resolved')}
             </React.Fragment>
           }
           placeholder={t('Automatic')}

+ 1 - 3
static/app/views/settings/incidentRules/triggers/index.tsx

@@ -1,7 +1,6 @@
 import React from 'react';
 
-import {Panel, PanelBody, PanelHeader} from 'app/components/panels';
-import {t} from 'app/locale';
+import {Panel, PanelBody} from 'app/components/panels';
 import {Organization, Project} from 'app/types';
 import {removeAtArrayIndex} from 'app/utils/removeAtArrayIndex';
 import {replaceAtArrayIndex} from 'app/utils/replaceAtArrayIndex';
@@ -106,7 +105,6 @@ class Triggers extends React.Component<Props> {
     return (
       <React.Fragment>
         <Panel>
-          <PanelHeader>{t('Set A Threshold')}</PanelHeader>
           <PanelBody>
             <TriggerForm
               disabled={disabled}

+ 47 - 38
static/app/views/settings/projectAlerts/create.tsx

@@ -2,10 +2,9 @@ import React from 'react';
 import {RouteComponentProps} from 'react-router';
 import styled from '@emotion/styled';
 
-import PageHeading from 'app/components/pageHeading';
+import * as Layout from 'app/components/layouts/thirds';
 import SentryDocumentTitle from 'app/components/sentryDocumentTitle';
 import {t} from 'app/locale';
-import {PageContent, PageHeader} from 'app/styles/organization';
 import space from 'app/styles/space';
 import {Organization, Project} from 'app/types';
 import {trackAnalyticsEvent} from 'app/utils/analytics';
@@ -120,48 +119,58 @@ class Create extends React.Component<Props, State> {
     return (
       <React.Fragment>
         <SentryDocumentTitle title={title} projectSlug={projectId} />
-        <PageContent>
-          <BuilderBreadCrumbs
-            hasMetricAlerts={hasMetricAlerts}
-            orgSlug={organization.slug}
-            alertName={wizardAlertType && AlertWizardAlertNames[wizardAlertType]}
-            title={wizardAlertType ? t('Create Alert Rule') : title}
-            projectSlug={projectId}
-          />
-          <StyledPageHeader>
-            <PageHeading>
-              {wizardAlertType ? t('Set Alert Conditions') : title}
-            </PageHeading>
-          </StyledPageHeader>
-          {shouldShowAlertTypeChooser && (
-            <AlertTypeChooser
-              organization={organization}
-              selected={alertType}
-              onChange={this.handleChangeAlertType}
-            />
-          )}
-
-          {(!hasMetricAlerts || alertType === 'issue') && (
-            <IssueRuleEditor {...this.props} project={project} />
-          )}
-
-          {hasMetricAlerts && alertType === 'metric' && (
-            <IncidentRulesCreate
-              {...this.props}
-              eventView={eventView}
-              wizardTemplate={wizardTemplate}
-              sessionId={this.sessionId}
-              project={project}
+
+        <Layout.Header>
+          <Layout.HeaderContent>
+            <BuilderBreadCrumbs
+              hasMetricAlerts={hasMetricAlerts}
+              orgSlug={organization.slug}
+              alertName={wizardAlertType && AlertWizardAlertNames[wizardAlertType]}
+              title={wizardAlertType ? t('Create Alert Rule') : title}
+              projectSlug={projectId}
             />
-          )}
-        </PageContent>
+
+            <Layout.Title>
+              {wizardAlertType ? t('Set Alert Conditions') : title}
+            </Layout.Title>
+          </Layout.HeaderContent>
+        </Layout.Header>
+        <AlertConditionsBody>
+          <Layout.Main fullWidth>
+            {shouldShowAlertTypeChooser && (
+              <AlertTypeChooser
+                organization={organization}
+                selected={alertType}
+                onChange={this.handleChangeAlertType}
+              />
+            )}
+
+            {(!hasMetricAlerts || alertType === 'issue') && (
+              <IssueRuleEditor {...this.props} project={project} />
+            )}
+
+            {hasMetricAlerts && alertType === 'metric' && (
+              <IncidentRulesCreate
+                {...this.props}
+                eventView={eventView}
+                wizardTemplate={wizardTemplate}
+                sessionId={this.sessionId}
+                project={project}
+              />
+            )}
+          </Layout.Main>
+        </AlertConditionsBody>
       </React.Fragment>
     );
   }
 }
 
-const StyledPageHeader = styled(PageHeader)`
-  margin-bottom: ${space(4)};
+const AlertConditionsBody = styled(Layout.Body)`
+  margin-bottom: -${space(3)};
+
+  *:not(img) {
+    max-width: 1000px;
+  }
 `;
 
 export default Create;

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