|
@@ -1,20 +1,26 @@
|
|
-import {Fragment, useRef} from 'react';
|
|
|
|
|
|
+import {useRef} from 'react';
|
|
import styled from '@emotion/styled';
|
|
import styled from '@emotion/styled';
|
|
import {Observer} from 'mobx-react';
|
|
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 FieldGroup from 'sentry/components/forms/fieldGroup';
|
|
import NumberField from 'sentry/components/forms/fields/numberField';
|
|
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 SelectField from 'sentry/components/forms/fields/selectField';
|
|
import SentryProjectSelectorField from 'sentry/components/forms/fields/sentryProjectSelectorField';
|
|
import SentryProjectSelectorField from 'sentry/components/forms/fields/sentryProjectSelectorField';
|
|
import TextField from 'sentry/components/forms/fields/textField';
|
|
import TextField from 'sentry/components/forms/fields/textField';
|
|
import Form, {FormProps} from 'sentry/components/forms/form';
|
|
import Form, {FormProps} from 'sentry/components/forms/form';
|
|
import FormModel from 'sentry/components/forms/model';
|
|
import FormModel from 'sentry/components/forms/model';
|
|
import ExternalLink from 'sentry/components/links/externalLink';
|
|
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 TextCopyInput from 'sentry/components/textCopyInput';
|
|
import TimeSince from 'sentry/components/timeSince';
|
|
import TimeSince from 'sentry/components/timeSince';
|
|
import {timezoneOptions} from 'sentry/data/timezones';
|
|
import {timezoneOptions} from 'sentry/data/timezones';
|
|
import {t, tct, tn} from 'sentry/locale';
|
|
import {t, tct, tn} from 'sentry/locale';
|
|
|
|
+import space from 'sentry/styles/space';
|
|
import {SelectValue} from 'sentry/types';
|
|
import {SelectValue} from 'sentry/types';
|
|
import commonTheme from 'sentry/utils/theme';
|
|
import commonTheme from 'sentry/utils/theme';
|
|
import usePageFilters from 'sentry/utils/usePageFilters';
|
|
import usePageFilters from 'sentry/utils/usePageFilters';
|
|
@@ -28,9 +34,9 @@ import {
|
|
ScheduleType,
|
|
ScheduleType,
|
|
} from '../types';
|
|
} 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';
|
|
const DEFAULT_MONITOR_TYPE = 'cron_job';
|
|
@@ -151,47 +157,60 @@ function MonitorForm({
|
|
onSubmitSuccess={onSubmitSuccess}
|
|
onSubmitSuccess={onSubmitSuccess}
|
|
submitLabel={submitLabel}
|
|
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"
|
|
name="project"
|
|
- label={t('Project')}
|
|
|
|
projects={projects.filter(project => project.isMember)}
|
|
projects={projects.filter(project => project.isMember)}
|
|
disabled={!!monitor}
|
|
disabled={!!monitor}
|
|
disabledReason={t('Existing monitors cannot be moved between projects')}
|
|
disabledReason={t('Existing monitors cannot be moved between projects')}
|
|
valueIsSlug
|
|
valueIsSlug
|
|
- help={t(
|
|
|
|
- "Select the project which contains the recurring job you'd like to monitor."
|
|
|
|
- )}
|
|
|
|
required
|
|
required
|
|
|
|
+ stacked
|
|
|
|
+ inline={false}
|
|
/>
|
|
/>
|
|
{monitor && (
|
|
{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
|
|
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 && (
|
|
{monitor !== undefined && monitor.nextCheckIn && (
|
|
- <PanelAlert type="info">
|
|
|
|
|
|
+ <Alert type="info">
|
|
{tct(
|
|
{tct(
|
|
'Any changes you make to the execution schedule will only be applied after the next expected check-in [nextCheckin].',
|
|
'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>
|
|
<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>
|
|
</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>
|
|
</Form>
|
|
);
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
export default MonitorForm;
|
|
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)`
|
|
const StyledNumberField = styled(NumberField)`
|
|
padding: 0;
|
|
padding: 0;
|
|
- border-bottom: none;
|
|
|
|
`;
|
|
`;
|
|
|
|
|
|
const StyledSelectField = styled(SelectField)`
|
|
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;
|
|
`;
|
|
`;
|