import {Fragment, useContext, useEffect, useRef} from 'react'; import {browserHistory} from 'react-router'; import {css} from '@emotion/react'; import styled from '@emotion/styled'; import {Observer} from 'mobx-react'; import NumberField from 'sentry/components/forms/fields/numberField'; 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 from 'sentry/components/forms/form'; import FormContext from 'sentry/components/forms/formContext'; import FormModel, {FieldValue} from 'sentry/components/forms/model'; import Panel from 'sentry/components/panels/panel'; import PanelBody from 'sentry/components/panels/panelBody'; import Placeholder from 'sentry/components/placeholder'; import {timezoneOptions} from 'sentry/data/timezones'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import {isActiveSuperuser} from 'sentry/utils/isActiveSuperuser'; import {useApiQuery} from 'sentry/utils/queryClient'; import commonTheme from 'sentry/utils/theme'; import {useDimensions} from 'sentry/utils/useDimensions'; import useOrganization from 'sentry/utils/useOrganization'; import usePageFilters from 'sentry/utils/usePageFilters'; import useProjects from 'sentry/utils/useProjects'; import {normalizeUrl} from 'sentry/utils/withDomainRequired'; import { DEFAULT_CRONTAB, DEFAULT_MONITOR_TYPE, mapMonitorFormErrors, transformMonitorFormData, } from 'sentry/views/monitors/components/monitorForm'; import {MockCheckInTimeline} from 'sentry/views/monitors/components/overviewTimeline/checkInTimeline'; import { GridLineOverlay, GridLineTimeLabels, } from 'sentry/views/monitors/components/overviewTimeline/gridLines'; import {TimelinePlaceholder} from 'sentry/views/monitors/components/overviewTimeline/timelinePlaceholder'; import {getConfigFromTimeRange} from 'sentry/views/monitors/components/overviewTimeline/utils'; import {Monitor, ScheduleType} from 'sentry/views/monitors/types'; import {crontabAsText, getScheduleIntervals} from 'sentry/views/monitors/utils'; const NUM_SAMPLE_TICKS = 9; interface ScheduleConfig { cronSchedule?: FieldValue; intervalFrequency?: FieldValue; intervalUnit?: FieldValue; scheduleType?: FieldValue; } function isValidConfig(schedule: ScheduleConfig) { const {scheduleType, cronSchedule, intervalFrequency, intervalUnit} = schedule; return !!( (scheduleType === ScheduleType.CRONTAB && cronSchedule) || (scheduleType === ScheduleType.INTERVAL && intervalFrequency && intervalUnit) ); } const DEFAULT_SCHEDULE_CONFIG = { scheduleType: 'crontab', cronSchedule: DEFAULT_CRONTAB, intervalFrequency: '1', intervalUnit: 'day', }; interface Props { schedule: ScheduleConfig; } function MockTimelineVisualization({schedule}: Props) { const {scheduleType, cronSchedule, intervalFrequency, intervalUnit} = schedule; const organization = useOrganization(); const {form} = useContext(FormContext); const query = { num_ticks: NUM_SAMPLE_TICKS, schedule_type: scheduleType, schedule: scheduleType === 'interval' ? [intervalFrequency, intervalUnit] : cronSchedule, }; const elementRef = useRef(null); const {width: timelineWidth} = useDimensions({elementRef}); const sampleDataQueryKey = [ `/organizations/${organization.slug}/monitors-schedule-data/`, {query}, ] as const; const {data, isLoading, isError, error} = useApiQuery(sampleDataQueryKey, { staleTime: 0, enabled: isValidConfig(schedule), retry: false, }); const errorMessage = isError || !isValidConfig(schedule) ? error?.responseJSON?.schedule?.[0] ?? t('Invalid Schedule') : null; useEffect(() => { if (!form) { return; } if (scheduleType === ScheduleType.INTERVAL) { form.setError('config.schedule.frequency', errorMessage); } else if (scheduleType === ScheduleType.CRONTAB) { form.setError('config.schedule', errorMessage); } }, [errorMessage, form, scheduleType]); const mockTimestamps = data?.map(ts => new Date(ts * 1000)); const start = mockTimestamps?.[0]; const end = mockTimestamps?.[mockTimestamps.length - 1]; const timeWindowConfig = start && end ? getConfigFromTimeRange(start, end, timelineWidth) : undefined; return ( {isLoading || !start || !end || !timeWindowConfig || !mockTimestamps ? ( {errorMessage ? : } ) : ( )} ); } const TimelineContainer = styled(Panel)` display: grid; grid-template-columns: 1fr; grid-template-rows: 40px 100px; align-items: center; `; const StyledGridLineTimeLabels = styled(GridLineTimeLabels)` grid-column: 0; `; const StyledGridLineOverlay = styled(GridLineOverlay)` grid-column: 0; `; const TimelineWidthTracker = styled('div')` position: absolute; width: 100%; grid-row: 1; grid-column: 0; `; export default function MonitorCreateForm() { const organization = useOrganization(); const {projects} = useProjects(); const {selection} = usePageFilters(); const form = useRef( new FormModel({ transformData: transformMonitorFormData, mapFormErrors: mapMonitorFormErrors, }) ); const selectedProjectId = selection.projects[0]; const selectedProject = selectedProjectId ? projects.find(p => p.id === selectedProjectId + '') : null; const isSuperuser = isActiveSuperuser(); const filteredProjects = projects.filter(project => isSuperuser || project.isMember); function onCreateMonitor(data: Monitor) { const endpointOptions = { query: { project: selection.projects, environment: selection.environments, }, }; browserHistory.push( normalizeUrl({ pathname: `/organizations/${organization.slug}/crons/${data.slug}/`, query: endpointOptions.query, }) ); } function changeScheduleType(type: ScheduleType) { form.current.setValue('config.schedule_type', type); } return (
{t('SCHEDULE')} {() => { const currScheduleType = form.current.getValue('config.schedule_type'); const selectedCrontab = currScheduleType === ScheduleType.CRONTAB; const parsedSchedule = form.current.getError('config.schedule') ? '' : crontabAsText( form.current.getValue('config.schedule')?.toString() ?? '' ); return ( changeScheduleType(ScheduleType.CRONTAB)} > {t('Crontab Schedule')} {parsedSchedule} changeScheduleType(ScheduleType.INTERVAL)} > {t('Interval Schedule')} ); }} {() => { const scheduleType = form.current.getValue('config.schedule_type'); const cronSchedule = form.current.getValue('config.schedule'); const intervalFrequency = form.current.getValue('config.schedule.frequency'); const intervalUnit = form.current.getValue('config.schedule.interval'); const schedule = { scheduleType, cronSchedule, intervalFrequency, intervalUnit, }; return ; }}
); } const FieldContainer = styled('div')` width: 800px; `; const SchedulePanel = styled(Panel)<{highlighted: boolean}>` border-radius: 0 ${space(0.75)} ${space(0.75)} 0; ${p => p.highlighted && css` border: 2px solid ${p.theme.purple300}; `}; &:first-child { border-radius: ${space(0.75)} 0 0 ${space(0.75)}; } `; const ScheduleLabel = styled('div')` font-weight: bold; margin-bottom: ${space(2)}; `; const Label = styled('div')` font-weight: bold; color: ${p => p.theme.subText}; `; const LabelText = styled(Label)` margin-top: ${space(2)}; margin-bottom: ${space(1)}; `; const ScheduleOptions = styled('div')` display: grid; grid-template-columns: 1fr 1fr; `; const MultiColumnInput = styled('div')<{columns?: string}>` display: grid; align-items: center; gap: ${space(1)}; grid-template-columns: ${p => p.columns}; `; const CronstrueText = styled(LabelText)` font-weight: normal; font-size: ${p => p.theme.fontSizeExtraSmall}; font-family: ${p => p.theme.text.familyMono}; grid-column: auto / span 2; `; const StyledNumberField = styled(NumberField)` padding: 0; `; const StyledSelectField = styled(SelectField)` padding: 0; `; const StyledTextField = styled(TextField)` padding: 0; `; const StyledSentryProjectSelectorField = styled(SentryProjectSelectorField)` padding: 0; `;