monitorForm.tsx 10 KB

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