monitorForm.tsx 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. import {Component, Fragment} from 'react';
  2. import {Observer} from 'mobx-react';
  3. import Access from 'sentry/components/acl/access';
  4. import Field from 'sentry/components/forms/field';
  5. import NumberField from 'sentry/components/forms/fields/numberField';
  6. import SelectField from 'sentry/components/forms/fields/selectField';
  7. import TextField from 'sentry/components/forms/fields/textField';
  8. import Form, {FormProps} from 'sentry/components/forms/form';
  9. import FormModel from 'sentry/components/forms/model';
  10. import {Panel, PanelBody, PanelHeader} from 'sentry/components/panels';
  11. import TextCopyInput from 'sentry/components/textCopyInput';
  12. import {t, tct} from 'sentry/locale';
  13. import {PageFilters, Project, SelectValue} from 'sentry/types';
  14. import withPageFilters from 'sentry/utils/withPageFilters';
  15. import withProjects from 'sentry/utils/withProjects';
  16. import {Monitor, MonitorConfig, MonitorTypes, ScheduleType} from './types';
  17. const SCHEDULE_TYPES: SelectValue<ScheduleType>[] = [
  18. {value: 'crontab', label: 'Crontab'},
  19. {value: 'interval', label: 'Interval'},
  20. ];
  21. const DEFAULT_MONITOR_TYPE = 'cron_job';
  22. const INTERVALS: SelectValue<string>[] = [
  23. {value: 'minute', label: 'minute(s)'},
  24. {value: 'hour', label: 'hour(s)'},
  25. {value: 'day', label: 'day(s)'},
  26. {value: 'week', label: 'week(s)'},
  27. {value: 'month', label: 'month(s)'},
  28. {value: 'year', label: 'year(s)'},
  29. ];
  30. type Props = {
  31. apiEndpoint: string;
  32. apiMethod: FormProps['apiMethod'];
  33. onSubmitSuccess: FormProps['onSubmitSuccess'];
  34. projects: Project[];
  35. selection: PageFilters;
  36. monitor?: Monitor;
  37. submitLabel?: string;
  38. };
  39. type TransformedData = {
  40. config?: Partial<MonitorConfig>;
  41. };
  42. function transformData(_data: Record<string, any>, model: FormModel) {
  43. return model.fields.toJSON().reduce<TransformedData>((data, [k, v]) => {
  44. if (k.indexOf('config.') !== 0) {
  45. data[k] = v;
  46. return data;
  47. }
  48. if (!data.config) {
  49. data.config = {};
  50. }
  51. if (k === 'config.schedule.frequency' || k === 'config.schedule.interval') {
  52. if (!Array.isArray(data.config.schedule)) {
  53. data.config.schedule = [null, null];
  54. }
  55. }
  56. if (k === 'config.schedule.frequency') {
  57. data.config!.schedule![0] = parseInt(v as string, 10);
  58. } else if (k === 'config.schedule.interval') {
  59. data.config!.schedule![1] = v;
  60. } else {
  61. data.config[k.substr(7)] = v;
  62. }
  63. return data;
  64. }, {});
  65. }
  66. class MonitorForm extends Component<Props> {
  67. form = new FormModel({transformData});
  68. formDataFromConfig(type: MonitorTypes, config: MonitorConfig) {
  69. const rv = {};
  70. switch (type) {
  71. case 'cron_job':
  72. rv['config.schedule_type'] = config.schedule_type;
  73. rv['config.checkin_margin'] = config.checkin_margin;
  74. rv['config.max_runtime'] = config.max_runtime;
  75. switch (config.schedule_type) {
  76. case 'interval':
  77. rv['config.schedule.frequency'] = config.schedule[0];
  78. rv['config.schedule.interval'] = config.schedule[1];
  79. break;
  80. case 'crontab':
  81. default:
  82. rv['config.schedule'] = config.schedule;
  83. }
  84. break;
  85. default:
  86. }
  87. return rv;
  88. }
  89. render() {
  90. const {monitor, submitLabel} = this.props;
  91. const selectedProjectId = this.props.selection.projects[0];
  92. const selectedProject = selectedProjectId
  93. ? this.props.projects.find(p => p.id === selectedProjectId + '')
  94. : null;
  95. return (
  96. <Access access={['project:write']}>
  97. {({hasAccess}) => (
  98. <Form
  99. allowUndo
  100. requireChanges
  101. apiEndpoint={this.props.apiEndpoint}
  102. apiMethod={this.props.apiMethod}
  103. model={this.form}
  104. initialData={
  105. monitor
  106. ? {
  107. name: monitor.name,
  108. type: monitor.type ?? DEFAULT_MONITOR_TYPE,
  109. project: monitor.project.slug,
  110. ...this.formDataFromConfig(monitor.type, monitor.config),
  111. }
  112. : {
  113. project: selectedProject ? selectedProject.slug : null,
  114. type: DEFAULT_MONITOR_TYPE,
  115. }
  116. }
  117. onSubmitSuccess={this.props.onSubmitSuccess}
  118. submitLabel={submitLabel}
  119. >
  120. <Panel>
  121. <PanelHeader>{t('Details')}</PanelHeader>
  122. <PanelBody>
  123. {monitor && (
  124. <Field label={t('ID')}>
  125. <div className="controls">
  126. <TextCopyInput>{monitor.id}</TextCopyInput>
  127. </div>
  128. </Field>
  129. )}
  130. <SelectField
  131. name="project"
  132. label={t('Project')}
  133. disabled={!hasAccess}
  134. options={this.props.projects
  135. .filter(p => p.isMember)
  136. .map(p => ({value: p.slug, label: p.slug}))}
  137. help={t('Associate your monitor with the appropriate project.')}
  138. required
  139. />
  140. <TextField
  141. name="name"
  142. placeholder={t('My Cron Job')}
  143. label={t('Name')}
  144. disabled={!hasAccess}
  145. required
  146. />
  147. </PanelBody>
  148. </Panel>
  149. <Panel>
  150. <PanelHeader>{t('Config')}</PanelHeader>
  151. <PanelBody>
  152. <NumberField
  153. name="config.max_runtime"
  154. label={t('Max Runtime')}
  155. disabled={!hasAccess}
  156. help={t(
  157. "The maximum runtime (in minutes) a check-in is allowed before it's marked as a failure."
  158. )}
  159. placeholder="e.g. 30"
  160. />
  161. <SelectField
  162. name="config.schedule_type"
  163. label={t('Schedule Type')}
  164. disabled={!hasAccess}
  165. options={SCHEDULE_TYPES}
  166. required
  167. />
  168. <Observer>
  169. {() => {
  170. switch (this.form.getValue('config.schedule_type')) {
  171. case 'crontab':
  172. return (
  173. <Fragment>
  174. <TextField
  175. name="config.schedule"
  176. label={t('Schedule')}
  177. disabled={!hasAccess}
  178. placeholder="*/5 * * * *"
  179. required
  180. help={tct(
  181. 'Changes to the schedule will apply on the next check-in. See [link:Wikipedia] for crontab syntax.',
  182. {
  183. link: <a href="https://en.wikipedia.org/wiki/Cron" />,
  184. }
  185. )}
  186. />
  187. <NumberField
  188. name="config.checkin_margin"
  189. label={t('Check-in Margin')}
  190. disabled={!hasAccess}
  191. help={t(
  192. "The margin (in minutes) a check-in is allowed to exceed it's scheduled window before being treated as missed."
  193. )}
  194. placeholder="e.g. 30"
  195. />
  196. </Fragment>
  197. );
  198. case 'interval':
  199. return (
  200. <Fragment>
  201. <NumberField
  202. name="config.schedule.frequency"
  203. label={t('Frequency')}
  204. disabled={!hasAccess}
  205. placeholder="e.g. 1"
  206. help={t(
  207. 'The amount of intervals that pass between executions of the cron job.'
  208. )}
  209. required
  210. />
  211. <SelectField
  212. name="config.schedule.interval"
  213. label={t('Interval')}
  214. disabled={!hasAccess}
  215. options={INTERVALS}
  216. help={t(
  217. 'The interval on which the frequency will be applied. 1 time every X amount of (minutes, hours, days)'
  218. )}
  219. required
  220. />
  221. <NumberField
  222. name="config.checkin_margin"
  223. label={t('Check-in Margin')}
  224. disabled={!hasAccess}
  225. help={t(
  226. "The margin (in minutes) a check-in is allowed to exceed it's scheduled window before being treated as missed."
  227. )}
  228. placeholder="e.g. 30"
  229. />
  230. </Fragment>
  231. );
  232. default:
  233. return null;
  234. }
  235. }}
  236. </Observer>
  237. </PanelBody>
  238. </Panel>
  239. </Form>
  240. )}
  241. </Access>
  242. );
  243. }
  244. }
  245. export default withPageFilters(withProjects(MonitorForm));