import { zodResolver } from '@hookform/resolvers/zod' import { toString as CronToString } from 'cronstrue' import { SubmitHandler, useForm } from 'react-hook-form' import { toast } from 'sonner' import z from 'zod' import { PermissionAction } from '@supabase/shared-types/out/constants' import { urlRegex } from 'components/interfaces/Auth/Auth.constants' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { useDatabaseCronJobCreateMutation } from 'data/database-cron-jobs/database-cron-jobs-create-mutation' import { CronJob } from 'data/database-cron-jobs/database-cron-jobs-query' import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useState } from 'react' import { Button, Form_Shadcn_, FormControl_Shadcn_, FormField_Shadcn_, Input_Shadcn_, RadioGroupStacked, RadioGroupStackedItem, Separator, SheetFooter, SheetHeader, SheetSection, SheetTitle, WarningIcon, } from 'ui' import { Admonition } from 'ui-patterns' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { CRONJOB_DEFINITIONS } from './CronJobs.constants' import { buildCronQuery, buildHttpRequestCommand, parseCronJobCommand } from './CronJobs.utils' import { CronJobScheduleSection } from './CronJobScheduleSection' import { EdgeFunctionSection } from './EdgeFunctionSection' import { HTTPHeaderFieldsSection } from './HttpHeaderFieldsSection' import { HTTPParameterFieldsSection } from './HttpParameterFieldsSection' import { HttpRequestSection } from './HttpRequestSection' import { SqlFunctionSection } from './SqlFunctionSection' import { SqlSnippetSection } from './SqlSnippetSection' import EnableExtensionModal from 'components/interfaces/Database/Extensions/EnableExtensionModal' export interface CreateCronJobSheetProps { selectedCronJob?: Pick isClosing: boolean setIsClosing: (v: boolean) => void onClose: () => void } const edgeFunctionSchema = z.object({ type: z.literal('edge_function'), method: z.enum(['GET', 'POST']), edgeFunctionName: z.string().trim().min(1, 'Please select one of the listed Edge Functions'), timeoutMs: z.coerce.number().int().gte(1000).lte(5000).default(1000), httpHeaders: z.array(z.object({ name: z.string(), value: z.string() })), httpParameters: z.array(z.object({ name: z.string(), value: z.string() })), }) const httpRequestSchema = z.object({ type: z.literal('http_request'), method: z.enum(['GET', 'POST']), endpoint: z .string() .trim() .min(1, 'Please provide a URL') .regex(urlRegex(), 'Please provide a valid URL') .refine((value) => value.startsWith('http'), 'Please include HTTP/HTTPs to your URL'), timeoutMs: z.coerce.number().int().gte(1000).lte(5000).default(1000), httpHeaders: z.array(z.object({ name: z.string(), value: z.string() })), httpParameters: z.array(z.object({ name: z.string(), value: z.string() })), }) const sqlFunctionSchema = z.object({ type: z.literal('sql_function'), schema: z.string().trim().min(1, 'Please select one of the listed database schemas'), functionName: z.string().trim().min(1, 'Please select one of the listed database functions'), }) const sqlSnippetSchema = z.object({ type: z.literal('sql_snippet'), snippet: z.string().trim().min(1), }) const FormSchema = z.object({ name: z.string().trim().min(1, 'Please provide a name for your cron job'), schedule: z .string() .trim() .min(1) .refine((value) => { try { CronToString(value) } catch { return false } return true }, 'The schedule needs to be in a Cron format.'), values: z.discriminatedUnion('type', [ edgeFunctionSchema, httpRequestSchema, sqlFunctionSchema, sqlSnippetSchema, ]), }) export type CreateCronJobForm = z.infer export type CronJobType = CreateCronJobForm['values'] const FORM_ID = 'create-cron-job-sidepanel' export const CreateCronJobSheet = ({ selectedCronJob, isClosing, setIsClosing, onClose, }: CreateCronJobSheetProps) => { const isEditing = !!selectedCronJob?.jobname const [showEnableExtensionModal, setShowEnableExtensionModal] = useState(false) const { mutate: upsertCronJob, isLoading } = useDatabaseCronJobCreateMutation() const canToggleExtensions = useCheckPermissions( PermissionAction.TENANT_SQL_ADMIN_WRITE, 'extensions' ) const cronJobValues = parseCronJobCommand(selectedCronJob?.command || '') const form = useForm({ resolver: zodResolver(FormSchema), defaultValues: { name: selectedCronJob?.jobname || '', schedule: selectedCronJob?.schedule || '', values: cronJobValues, }, }) const { project } = useProjectContext() const isEdited = form.formState.isDirty // if the form hasn't been touched and the user clicked esc or the backdrop, close the sheet if (!isEdited && isClosing) { onClose() } const onClosePanel = () => { if (isEdited) { setIsClosing(true) } else { onClose() } } const onSubmit: SubmitHandler = async ({ name, schedule, values }) => { let command = '' if (values.type === 'edge_function') { command = buildHttpRequestCommand( values.method, values.edgeFunctionName, values.httpHeaders, values.httpParameters, values.timeoutMs ) } else if (values.type === 'http_request') { command = buildHttpRequestCommand( values.method, values.endpoint, values.httpHeaders, values.httpParameters, values.timeoutMs ) } else if (values.type === 'sql_function') { command = `'CALL ${values.schema}.${values.functionName}()'` } else { command = `$$${values.snippet}$$` } const query = buildCronQuery(name, schedule, command) upsertCronJob( { projectRef: project!.ref, connectionString: project?.connectionString, query, }, { onSuccess: () => { if (isEditing) { toast.success(`Successfully updated cron job ${name}`) } else { toast.success(`Successfully created cron job ${name}`) } onClose() }, } ) } 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 cronType = form.watch('values.type') return ( <>
{isEditing ? `Edit ${selectedCronJob.jobname}` : `Create a new cron job`}
( )} /> ( {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' && }
setIsClosing(false)} onConfirm={() => onClose()} >

There are unsaved changes. Are you sure you want to close the panel? Your changes will be lost.

{pgNetExtension && ( setShowEnableExtensionModal(false)} /> )} ) }