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

feat(crons): Simplified monitor creation experience (#45628)

Simpler flow for creation, eventually will have a quickstart section at
the bottom to help with onboarding.

Before:
<img width="1280" alt="image"
src="https://user-images.githubusercontent.com/9372512/224238917-b8656423-d74a-47fb-8213-d38ec126bf54.png">

After:
<img width="942" alt="image"
src="https://user-images.githubusercontent.com/9372512/224447828-ba8b5cf3-64c5-4b95-9340-20826ea9c818.png">
<img width="858" alt="image"
src="https://user-images.githubusercontent.com/9372512/224447842-689deecf-ba3f-4973-979b-924a7af45493.png">
David Wang 2 лет назад
Родитель
Сommit
6e7027c950
2 измененных файлов с 191 добавлено и 154 удалено
  1. 189 141
      static/app/views/monitors/components/monitorForm.tsx
  2. 2 13
      static/app/views/monitors/create.tsx

+ 189 - 141
static/app/views/monitors/components/monitorForm.tsx

@@ -1,20 +1,26 @@
-import {Fragment, useRef} from 'react';
+import {useRef} from 'react';
 import styled from '@emotion/styled';
 import {Observer} from 'mobx-react';
 
+import Alert from 'sentry/components/alert';
+import {RadioOption} from 'sentry/components/forms/controls/radioGroup';
 import FieldGroup from 'sentry/components/forms/fieldGroup';
 import NumberField from 'sentry/components/forms/fields/numberField';
+import RadioField from 'sentry/components/forms/fields/radioField';
 import SelectField from 'sentry/components/forms/fields/selectField';
 import SentryProjectSelectorField from 'sentry/components/forms/fields/sentryProjectSelectorField';
 import TextField from 'sentry/components/forms/fields/textField';
 import Form, {FormProps} from 'sentry/components/forms/form';
 import FormModel from 'sentry/components/forms/model';
 import ExternalLink from 'sentry/components/links/externalLink';
-import {Panel, PanelAlert, PanelBody, PanelHeader} from 'sentry/components/panels';
+import List from 'sentry/components/list';
+import ListItem from 'sentry/components/list/listItem';
+import Text from 'sentry/components/text';
 import TextCopyInput from 'sentry/components/textCopyInput';
 import TimeSince from 'sentry/components/timeSince';
 import {timezoneOptions} from 'sentry/data/timezones';
 import {t, tct, tn} from 'sentry/locale';
+import space from 'sentry/styles/space';
 import {SelectValue} from 'sentry/types';
 import commonTheme from 'sentry/utils/theme';
 import usePageFilters from 'sentry/utils/usePageFilters';
@@ -28,9 +34,9 @@ import {
   ScheduleType,
 } from '../types';
 
-const SCHEDULE_TYPES: SelectValue<ScheduleType>[] = [
-  {value: ScheduleType.CRONTAB, label: 'Crontab'},
-  {value: ScheduleType.INTERVAL, label: 'Interval'},
+const SCHEDULE_OPTIONS: RadioOption<string>[] = [
+  [ScheduleType.INTERVAL, t('Interval')],
+  [ScheduleType.CRONTAB, t('Crontab')],
 ];
 
 const DEFAULT_MONITOR_TYPE = 'cron_job';
@@ -151,47 +157,60 @@ function MonitorForm({
       onSubmitSuccess={onSubmitSuccess}
       submitLabel={submitLabel}
     >
-      <Panel>
-        <PanelHeader>{t('Details')}</PanelHeader>
-
-        <PanelBody>
-          <SentryProjectSelectorField
+      <StyledList symbol="colored-numeric">
+        <StyledListItem>{t('Add a name and project')}</StyledListItem>
+        <ListItemSubText>
+          {t('The monitor name will show up in alerts and notifications')}
+        </ListItemSubText>
+        <InputGroup>
+          <StyledTextField
+            name="name"
+            placeholder={t('My Cron Job')}
+            required
+            stacked
+            inline={false}
+          />
+          <StyledSentryProjectSelectorField
             name="project"
-            label={t('Project')}
             projects={projects.filter(project => project.isMember)}
             disabled={!!monitor}
             disabledReason={t('Existing monitors cannot be moved between projects')}
             valueIsSlug
-            help={t(
-              "Select the project which contains the recurring job you'd like to monitor."
-            )}
             required
+            stacked
+            inline={false}
           />
           {monitor && (
-            <FieldGroup
-              label={t('Monitor Slug')}
-              flexibleControlStateSize
-              help={t(
-                'The monitor slug is the organization-wide unique identifier for your monitor.'
-              )}
-            >
-              <TextCopyInput>{monitor.slug}</TextCopyInput>
-            </FieldGroup>
+            <StyledFieldGroup flexibleControlStateSize stacked inline={false}>
+              <StyledTextCopyInput>{monitor.slug}</StyledTextCopyInput>
+            </StyledFieldGroup>
           )}
-          <TextField
-            name="name"
-            placeholder={t('My Cron Job')}
-            label={t('Name your cron monitor')}
+        </InputGroup>
+
+        <StyledListItem>{t('Choose your schedule type')}</StyledListItem>
+        <ListItemSubText>
+          {tct('You can use our simple schedule or [link:the crontab syntax].', {
+            link: <ExternalLink href="https://en.wikipedia.org/wiki/Cron" />,
+          })}
+        </ListItemSubText>
+        <InputGroup>
+          <RadioField
+            name="config.schedule_type"
+            choices={SCHEDULE_OPTIONS}
+            defaultValue={ScheduleType.CRONTAB}
+            orientInline
             required
+            stacked
+            inline={false}
           />
-        </PanelBody>
-      </Panel>
-      <Panel>
-        <PanelHeader>{t('Config')}</PanelHeader>
-
-        <PanelBody>
+        </InputGroup>
+        <StyledListItem>{t('Choose your schedule')}</StyledListItem>
+        <ListItemSubText>
+          {t('How often you expect your recurring jobs to run.')}
+        </ListItemSubText>
+        <InputGroup>
           {monitor !== undefined && monitor.nextCheckIn && (
-            <PanelAlert type="info">
+            <Alert type="info">
               {tct(
                 'Any changes you make to the execution schedule will only be applied after the next expected check-in [nextCheckin].',
                 {
@@ -202,130 +221,159 @@ function MonitorForm({
                   ),
                 }
               )}
-            </PanelAlert>
+            </Alert>
           )}
-          <NumberField
-            name="config.max_runtime"
-            label={t('Max Runtime')}
-            help={t(
-              "Set the number of minutes a recurring job is allowed to run before it's considered failed."
-            )}
-            placeholder="e.g. 30"
-          />
-          <SelectField
-            name="config.schedule_type"
-            label={t('Schedule Type')}
-            options={SCHEDULE_TYPES}
-            defaultValue={ScheduleType.CRONTAB}
-            required
-          />
           <Observer>
             {() => {
-              switch (form.current.getValue('config.schedule_type')) {
-                case 'crontab':
-                  return (
-                    <Fragment>
-                      <TextField
-                        name="config.schedule"
-                        label={t('Schedule')}
-                        placeholder="*/5 * * * *"
-                        required
-                        help={tct(
-                          'Any schedule changes will be applied to the next check-in. See [link:Wikipedia] for crontab syntax.',
-                          {
-                            link: (
-                              <ExternalLink href="https://en.wikipedia.org/wiki/Cron" />
-                            ),
-                          }
-                        )}
-                        css={{input: {fontFamily: commonTheme.text.familyMono}}}
-                      />
-                      <SelectField
-                        name="config.timezone"
-                        label={t('Timezone')}
-                        defaultValue="UTC"
-                        options={timezoneOptions}
-                        help={tct(
-                          "The timezone of your execution environment. Be sure to set this correctly, otherwise the schedule may be mismatched and check-ins will be marked as missed! Use [code:timedatectl] or similar to determine your machine's timezone.",
-                          {code: <code />}
-                        )}
-                      />
-                      <NumberField
-                        name="config.checkin_margin"
-                        label={t('Check-in Margin')}
-                        help={t(
-                          "The max error margin (in minutes) before a check-in is considered missed. If you don't expect your job to start immediately at the scheduled time, expand this margin to account for delays."
-                        )}
-                        placeholder="e.g. 30"
-                      />
-                    </Fragment>
-                  );
-                case 'interval':
-                  return (
-                    <Fragment>
-                      <CombinedField>
-                        <FieldGroup
-                          label={t('Frequency')}
-                          help={t(
-                            'The amount of time between each job execution. Example, every 5 hours.'
-                          )}
-                          stacked
-                          required
-                        />
-                        <StyledNumberField
-                          name="config.schedule.frequency"
-                          label={t('Frequency')}
-                          placeholder="e.g. 1"
-                          hideLabel
-                          required
-                        />
-                        <StyledSelectField
-                          name="config.schedule.interval"
-                          label={t('Interval')}
-                          options={getIntervals(
-                            Number(
-                              form.current.getValue('config.schedule.frequency') ?? 1
-                            )
-                          )}
-                          hideLabel
-                          required
-                        />
-                      </CombinedField>
-                      <NumberField
-                        name="config.checkin_margin"
-                        label={t('Check-in Margin')}
-                        help={t(
-                          "The max error margin (in minutes) before a check-in is considered missed. If you don't expect your job to start immediately at the scheduled time, expand this margin to account for delays."
-                        )}
-                        placeholder="e.g. 30"
-                      />
-                    </Fragment>
-                  );
-                default:
-                  return null;
+              const schedule_type = form.current.getValue('config.schedule_type');
+              if (schedule_type === 'crontab') {
+                return (
+                  <ScheduleGroupInputs>
+                    <StyledTextField
+                      name="config.schedule"
+                      placeholder="*/5 * * * *"
+                      css={{input: {fontFamily: commonTheme.text.familyMono}}}
+                      required
+                      stacked
+                      inline={false}
+                    />
+                    <StyledSelectField
+                      name="config.timezone"
+                      defaultValue="UTC"
+                      options={timezoneOptions}
+                      required
+                      stacked
+                      inline={false}
+                    />
+                  </ScheduleGroupInputs>
+                );
+              }
+              if (schedule_type === 'interval') {
+                return (
+                  <ScheduleGroupInputs interval>
+                    <LabelText>{t('Every')}</LabelText>
+                    <StyledNumberField
+                      name="config.schedule.frequency"
+                      placeholder="e.g. 1"
+                      required
+                      stacked
+                      inline={false}
+                    />
+                    <StyledSelectField
+                      name="config.schedule.interval"
+                      options={getIntervals(
+                        Number(form.current.getValue('config.schedule.frequency') ?? 1)
+                      )}
+                      required
+                      stacked
+                      inline={false}
+                    />
+                  </ScheduleGroupInputs>
+                );
               }
+              return null;
             }}
           </Observer>
-        </PanelBody>
-      </Panel>
+        </InputGroup>
+        <StyledListItem>{t('Set a missed status')}</StyledListItem>
+        <ListItemSubText>
+          {t('How long to wait before we consider a check-in as missed.')}
+        </ListItemSubText>
+        <InputGroup>
+          <LabeledInputs>
+            <StyledNumberField
+              name="config.checkin_margin"
+              placeholder="e.g. 30"
+              stacked
+              inline={false}
+            />
+            <LabelText>{t('Minutes')}</LabelText>
+          </LabeledInputs>
+        </InputGroup>
+        <StyledListItem>{t('Set a failed status')}</StyledListItem>
+        <ListItemSubText>
+          {t(
+            "How long a check-in is allowed to run before it's considered failed. If the job encounters an error it will also fail."
+          )}
+        </ListItemSubText>
+        <InputGroup>
+          <LabeledInputs>
+            <StyledNumberField
+              name="config.max_runtime"
+              placeholder="e.g. 30"
+              stacked
+              inline={false}
+            />
+            <LabelText>{t('Minutes')}</LabelText>
+          </LabeledInputs>
+        </InputGroup>
+      </StyledList>
     </Form>
   );
 }
 
 export default MonitorForm;
 
-const CombinedField = styled('div')`
-  display: grid;
-  grid-template-columns: 50% 1fr 1fr;
-  align-items: center;
-  border-bottom: 1px solid ${p => p.theme.innerBorder};
+const StyledList = styled(List)`
+  width: 500px;
+`;
+
+const StyledTextCopyInput = styled(TextCopyInput)`
+  padding: 0;
 `;
 
 const StyledNumberField = styled(NumberField)`
   padding: 0;
-  border-bottom: none;
 `;
 
 const StyledSelectField = styled(SelectField)`
-  padding-left: 0;
+  padding: 0;
+`;
+
+const StyledFieldGroup = styled(FieldGroup)`
+  padding: 0;
+`;
+
+const StyledTextField = styled(TextField)`
+  padding: 0;
+`;
+
+const StyledSentryProjectSelectorField = styled(SentryProjectSelectorField)`
+  padding: 0;
+`;
+
+const StyledListItem = styled(ListItem)`
+  font-size: ${p => p.theme.fontSizeExtraLarge};
+  font-weight: bold;
+  line-height: 1.3;
+`;
+
+const LabelText = styled(Text)`
+  font-weight: bold;
+  color: ${p => p.theme.subText};
+`;
+
+const ListItemSubText = styled(LabelText)`
+  font-weight: normal;
+  padding-left: ${space(4)};
+`;
+
+const InputGroup = styled('div')`
+  padding-left: ${space(4)};
+  margin-top: ${space(1)};
+  margin-bottom: ${space(4)};
+  display: flex;
+  flex-direction: column;
+  gap: ${space(1)};
+`;
+
+const LabeledInputs = styled('div')`
+  display: grid;
+  grid-template-columns: 1fr auto;
+  align-items: center;
+  gap: ${space(1)};
+`;
+
+const ScheduleGroupInputs = styled(LabeledInputs)<{interval?: boolean}>`
+  grid-template-columns: ${p => p.interval && 'auto'} 1fr 2fr;
 `;

+ 2 - 13
static/app/views/monitors/create.tsx

@@ -1,6 +1,5 @@
 import {Fragment} from 'react';
 import {browserHistory} from 'react-router';
-import styled from '@emotion/styled';
 
 import Breadcrumbs from 'sentry/components/breadcrumbs';
 import * as Layout from 'sentry/components/layouts/thirds';
@@ -37,11 +36,11 @@ function CreateMonitor({}: Props) {
                 to: `/organizations/${orgSlug}/crons/`,
               },
               {
-                label: t('Set Up Cron Monitor'),
+                label: t('Add Monitor'),
               },
             ]}
           />
-          <Layout.Title>{t('Set Up Cron Monitor')}</Layout.Title>
+          <Layout.Title>{t('Add Monitor')}</Layout.Title>
         </Layout.HeaderContent>
         <Layout.HeaderActions>
           <CronsFeedbackButton />
@@ -49,11 +48,6 @@ function CreateMonitor({}: Props) {
       </Layout.Header>
       <Layout.Body>
         <Layout.Main fullWidth>
-          <HelpText>
-            {t(
-              `Sentry will tell you if your recurring jobs are running on schedule, failing, or succeeding.`
-            )}
-          </HelpText>
           <MonitorForm
             apiMethod="POST"
             apiEndpoint={`/organizations/${orgSlug}/monitors/`}
@@ -67,8 +61,3 @@ function CreateMonitor({}: Props) {
 }
 
 export default CreateMonitor;
-
-const HelpText = styled('p')`
-  color: ${p => p.theme.subText};
-  max-width: 760px;
-`;