// @ts-nocheck import { zodResolver } from '@hookform/resolvers/zod' import { PermissionAction } from '@supabase/shared-types/out/constants' import { useWatch } from '@ui/components/shadcn/ui/form' import { useParams } from 'common' import { parseAsString, useQueryState } from 'nuqs' import { useEffect, useState } from 'react' import { SubmitHandler, useForm } from 'react-hook-form' import { toast } from 'sonner' import { Button, Form, FormControl, FormField, Input, RadioGroupStacked, RadioGroupStackedItem, Separator, Sheet, SheetContent, SheetFooter, SheetHeader, SheetSection, SheetTitle, WarningIcon, } from 'ui' import { Admonition } from 'ui-patterns/admonition' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { CRONJOB_DEFINITIONS } from '../CronJobs.constants' import { buildCronQuery, buildHttpRequestCommand, parseCronJobCommand } from '../CronJobs.utils' import { EdgeFunctionSection } from '../EdgeFunctionSection' import { HttpBodyFieldSection } from '../HttpBodyFieldSection' import { HTTPHeaderFieldsSection } from '../HttpHeaderFieldsSection' import { HttpRequestSection } from '../HttpRequestSection' import { SqlFunctionSection } from '../SqlFunctionSection' import { SqlSnippetSection } from '../SqlSnippetSection' import { FormSchema, type CreateCronJobForm, type CronJobType, } from './CreateCronJobSheet.constants' import { CronJobScheduleSection } from './CronJobScheduleSection' import { EnableExtensionModal } from '@/components/interfaces/Database/Extensions/EnableExtensionModal' import { DiscardChangesConfirmationDialog } from '@/components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog' import { ButtonTooltip } from '@/components/ui/ButtonTooltip' import { getDatabaseCronJob } from '@/data/database-cron-jobs/database-cron-job-query' import { useDatabaseCronJobCreateMutation } from '@/data/database-cron-jobs/database-cron-jobs-create-mutation' import { CronJob } from '@/data/database-cron-jobs/database-cron-jobs-infinite-query' import { useDatabaseExtensionsQuery } from '@/data/database-extensions/database-extensions-query' import { useSendEventMutation } from '@/data/telemetry/send-event-mutation' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { useConfirmOnClose } from '@/hooks/ui/useConfirmOnClose' import { isGreaterThanOrEqual } from '@/lib/semver' interface CreateCronJobSheetProps { open: boolean selectedCronJob?: Pick onClose: () => void } const FORM_ID = 'create-cron-job-sidepanel' const buildCommand = (values: CronJobType) => { let command = '' if (values.type === 'edge_function') { command = buildHttpRequestCommand( values.method, values.edgeFunctionName, values.httpHeaders, values.httpBody, values.timeoutMs ) } else if (values.type === 'http_request') { command = buildHttpRequestCommand( values.method, values.endpoint, values.httpHeaders, values.httpBody, values.timeoutMs ) } else if (values.type === 'sql_function') { command = `SELECT ${values.schema}.${values.functionName}()` } return command } export const CreateCronJobSheet = ({ open, selectedCronJob, onClose }: CreateCronJobSheetProps) => { const { childId } = useParams() const { data: project } = useSelectedProjectQuery() const { data: org } = useSelectedOrganizationQuery() const [searchQuery] = useQueryState('search', parseAsString.withDefault('')) const [isLoadingGetCronJob, setIsLoadingGetCronJob] = useState(false) const jobId = Number(childId) const isEditing = !!selectedCronJob?.jobname const [showEnableExtensionModal, setShowEnableExtensionModal] = useState(false) const { data = [] } = useDatabaseExtensionsQuery({ projectRef: project?.ref, connectionString: project?.connectionString, }) const pgNetExtension = data.find((ext) => ext.name === 'pg_net') const pgNetExtensionInstalled = pgNetExtension?.installed_version != undefined const pgCronExtension = data.find((ext) => ext.name === 'pg_cron') const supportsSeconds = pgCronExtension?.installed_version ? isGreaterThanOrEqual(pgCronExtension.installed_version, '1.5') : false const { mutate: sendEvent } = useSendEventMutation() const { mutate: upsertCronJob, isPending: isUpserting } = useDatabaseCronJobCreateMutation() const isLoading = isLoadingGetCronJob || isUpserting const { can: canToggleExtensions } = useAsyncCheckPermissions( PermissionAction.TENANT_SQL_ADMIN_WRITE, 'extensions' ) const cronJobValues = parseCronJobCommand(selectedCronJob?.command || '', project?.ref!) const defaultValues = { name: selectedCronJob?.jobname || '', schedule: selectedCronJob?.schedule || '*/5 * * * *', supportsSeconds, values: cronJobValues, } const form = useForm({ resolver: zodResolver(FormSchema as any), defaultValues, }) const [ cronType, endpoint, edgeFunctionName, method, httpHeaders, httpBody, timeoutMs, schema, functionName, ] = useWatch({ control: form.control, name: [ 'values.type', 'values.endpoint', 'values.edgeFunctionName', 'values.method', 'values.httpHeaders', 'values.httpBody', 'values.timeoutMs', 'values.schema', 'values.functionName', ], }) const { confirmOnClose, handleOpenChange, modalProps } = useConfirmOnClose({ checkIsDirty: () => form.formState.isDirty, onClose: () => onClose(), }) const onSubmit: SubmitHandler = async ({ name, schedule, values }) => { if (!project) return console.error('Project is required') if (!isEditing) { try { setIsLoadingGetCronJob(true) const checkExistingJob = await getDatabaseCronJob({ projectRef: project.ref, connectionString: project.connectionString, name, }) const nameExists = !!checkExistingJob if (nameExists) { return form.setError( 'name', { type: 'manual', message: 'A cron job with this name already exists', }, { shouldFocus: true } ) } } catch (error: any) { toast.error(`Failed to validate cron job name: ${error.message}`) return } finally { setIsLoadingGetCronJob(false) } } const query = buildCronQuery(name, schedule, values.snippet) upsertCronJob( { projectRef: project!.ref, connectionString: project?.connectionString, query, searchTerm: searchQuery, // [Joshen] Only need to invalidate a specific cron job if in the job's previous run tab identifier: !!jobId ? jobId : undefined, }, { onSuccess: () => { if (isEditing) { toast.success(`Successfully updated cron job ${name}`) } else { toast.success(`Successfully created cron job ${name}`) } if (isEditing) { sendEvent({ action: 'cron_job_updated', properties: { type: values.type, schedule: schedule, }, groups: { project: project?.ref ?? 'Unknown', organization: org?.slug ?? 'Unknown', }, }) } else { sendEvent({ action: 'cron_job_created', properties: { type: values.type, schedule: schedule, }, groups: { project: project?.ref ?? 'Unknown', organization: org?.slug ?? 'Unknown', }, }) } onClose() }, } ) setIsLoadingGetCronJob(false) } // update the snippet field when the user changes the any values in the form useEffect(() => { const command = buildCommand({ type: cronType, method, edgeFunctionName, timeoutMs, httpHeaders, httpBody, functionName, schema, endpoint, snippet: '', }) if (command) { form.setValue('values.snippet', command) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [ cronType, edgeFunctionName, endpoint, method, // for some reason, the httpHeaders are not memoized and cause the useEffect to trigger even when the value is the same JSON.stringify(httpHeaders), httpBody, timeoutMs, schema, functionName, form, ]) useEffect(() => { if (open && !!pgCronExtension) form.reset(defaultValues) // eslint-disable-next-line react-hooks/exhaustive-deps }, [open]) return ( <>
{isEditing ? `Edit ${selectedCronJob.jobname}` : `Create a new cron job`}
( Cron jobs cannot be renamed once created )} /> ( field.onChange(value)} > {CRONJOB_DEFINITIONS.map((definition) => (
{definition.icon}

{definition.label}

{definition.description}

{!pgNetExtensionInstalled && (definition.value === 'http_request' || definition.value === 'edge_function') ? (
pg_net needs to be installed to use this type
) : null}
))}
)} /> {!pgNetExtensionInstalled && ( Enable pg_net for HTTP requests or Edge Functions } description={
This will allow you to send HTTP requests or trigger an edge function within your cron jobs setShowEnableExtensionModal(true)} tooltip={{ content: { side: 'bottom', text: !canToggleExtensions ? 'You need additional permissions to enable database extensions' : undefined, }, }} > Install pg_net extension
} /> )}
{cronType === 'http_request' && ( <> )} {cronType === 'edge_function' && ( <> )} {cronType === 'sql_function' && } {cronType === 'sql_snippet' && }
{pgNetExtension && ( setShowEnableExtensionModal(false)} /> )} ) }