| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239 |
- import { motion } from 'framer-motion'
- import { useEffect, useState } from 'react'
- import { UseFormReturn } from 'react-hook-form'
- import {
- Accordion,
- AccordionContent,
- AccordionItem,
- AccordionTrigger,
- Button,
- cn,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
- Input,
- SheetSection,
- Switch,
- } from 'ui'
- import { useDebounce } from 'use-debounce'
- import { formatScheduleString, getScheduleMessage } from '../CronJobs.utils'
- import CronSyntaxChart from '../CronSyntaxChart'
- import { type CreateCronJobForm } from './CreateCronJobSheet.constants'
- import { useSqlCronGenerateMutation } from '@/data/ai/sql-cron-mutation'
- import { useCronTimezoneQuery } from '@/data/database-cron-jobs/database-cron-timezone-query'
- import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
- interface CronJobScheduleSectionProps {
- form: UseFormReturn<CreateCronJobForm>
- supportsSeconds: boolean
- }
- export const CronJobScheduleSection = ({ form, supportsSeconds }: CronJobScheduleSectionProps) => {
- const { data: project } = useSelectedProjectQuery()
- const [inputValue, setInputValue] = useState('')
- const [debouncedValue] = useDebounce(inputValue, 750)
- const [useNaturalLanguage, setUseNaturalLanguage] = useState(false)
- const PRESETS = [
- ...(supportsSeconds ? [{ name: 'Every 30 seconds', expression: '30 seconds' }] : []),
- { name: 'Every minute', expression: '* * * * *' },
- { name: 'Every 5 minutes', expression: '*/5 * * * *' },
- { name: 'Every first of the month, at 00:00', expression: '0 0 1 * *' },
- { name: 'Every night at midnight', expression: '0 0 * * *' },
- { name: 'Every Monday at 2 AM', expression: '0 2 * * 1' },
- ] as const
- const { mutate: generateCronSyntax, isPending: isGeneratingCron } = useSqlCronGenerateMutation({
- onSuccess: (expression) => {
- form.setValue('schedule', expression, {
- shouldValidate: true,
- shouldDirty: true,
- shouldTouch: true,
- })
- },
- })
- const { data: timezone } = useCronTimezoneQuery({
- projectRef: project?.ref,
- connectionString: project?.connectionString,
- })
- useEffect(() => {
- if (useNaturalLanguage && debouncedValue) {
- generateCronSyntax({ prompt: debouncedValue })
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [debouncedValue, useNaturalLanguage])
- const schedule = form.watch('schedule')
- const scheduleString = formatScheduleString(schedule)
- return (
- <SheetSection>
- <FormField
- control={form.control}
- name="schedule"
- render={({ field }) => {
- return (
- <FormItem className="flex flex-col gap-1">
- <div className="flex flex-row justify-between">
- <FormLabel>Schedule</FormLabel>
- <span className="text-foreground-lighter text-xs">
- {useNaturalLanguage
- ? 'Describe your schedule in words'
- : 'Enter a cron expression'}
- </span>
- </div>
- <FormControl>
- {useNaturalLanguage ? (
- <Input
- value={inputValue}
- placeholder="E.g. every 5 minutes"
- onKeyDown={(e) => {
- if (e.key === 'Enter') {
- e.preventDefault()
- }
- }}
- onChange={(e) => setInputValue(e.target.value)}
- />
- ) : (
- <Input
- {...field}
- autoComplete="off"
- placeholder="* * * * *"
- onKeyDown={(e) => {
- if (e.key === 'Enter') {
- e.preventDefault()
- }
- }}
- />
- )}
- </FormControl>
- <FormMessage />
- <div className="flex flex-col gap-y-4 mt-3 mb-2">
- <div className="flex items-center gap-2">
- <Switch
- checked={useNaturalLanguage}
- onCheckedChange={() => {
- setUseNaturalLanguage(!useNaturalLanguage)
- setInputValue('')
- }}
- />
- <p className="text-sm text-foreground-light">Use natural language</p>
- </div>
- <ul className="flex gap-2 flex-wrap mt-2">
- {PRESETS.map((preset) => (
- <li key={preset.name}>
- <Button
- type="outline"
- onClick={() => {
- if (useNaturalLanguage) {
- setUseNaturalLanguage(false)
- }
- form.setValue('schedule', preset.expression, {
- shouldValidate: true,
- shouldDirty: true,
- shouldTouch: true,
- })
- }}
- >
- {preset.name}
- </Button>
- </li>
- ))}
- </ul>
- <Accordion type="single" collapsible>
- <AccordionItem value="item-1" className="border-none">
- <AccordionTrigger className="text-xs text-foreground-light font-normal gap-2 justify-start py-1 ">
- View syntax chart
- </AccordionTrigger>
- <AccordionContent asChild className="pb-0!">
- <CronSyntaxChart />
- </AccordionContent>
- </AccordionItem>
- </Accordion>
- </div>
- <div className="bg-surface-100 p-4 rounded-sm grid gap-y-4 border">
- <h4 className="text-sm text-foreground">
- Schedule {timezone ? `(${timezone})` : ''}
- </h4>
- <span
- className={cn(
- 'text-xl font-mono',
- scheduleString
- ? isGeneratingCron
- ? 'animate-pulse text-foreground-lighter'
- : 'text-foreground'
- : 'text-foreground-lighter'
- )}
- >
- {isGeneratingCron ? <CronSyntaxLoader /> : schedule || '* * * * * *'}
- </span>
- {!inputValue && !isGeneratingCron && !scheduleString ? (
- <span className="text-sm text-foreground-light">
- Describe your schedule above
- </span>
- ) : (
- <span className="text-sm text-foreground-light flex items-center gap-2">
- {isGeneratingCron ? <LoadingDots /> : getScheduleMessage(scheduleString)}
- </span>
- )}
- </div>
- </FormItem>
- )
- }}
- />
- </SheetSection>
- )
- }
- const CronSyntaxLoader = () => {
- return (
- <div className="flex gap-2">
- {['*', '*', '*', '*', '*'].map((char, i) => (
- <motion.span
- key={i}
- initial={{ opacity: 0.3 }}
- animate={{ opacity: 1 }}
- transition={{
- duration: 0.6,
- repeat: Infinity,
- repeatType: 'reverse',
- delay: i * 0.15,
- }}
- >
- {char}
- </motion.span>
- ))}
- </div>
- )
- }
- const LoadingDots = () => {
- return (
- <span className="inline-flex items-center">
- {[0, 1, 2].map((i) => (
- <motion.span
- key={i}
- initial={{ opacity: 0 }}
- animate={{ opacity: 1 }}
- transition={{
- duration: 0.5,
- repeat: Infinity,
- repeatType: 'reverse',
- delay: i * 0.2,
- }}
- >
- .
- </motion.span>
- ))}
- </span>
- )
- }
|