diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet.tsx index 8f19dc64f8f..3801bc3d52f 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet.tsx @@ -7,12 +7,15 @@ import { toast } from 'sonner' import z from 'zod' import { urlRegex } from 'components/interfaces/Auth/Auth.constants' +import EnableExtensionModal from 'components/interfaces/Database/Extensions/EnableExtensionModal' 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 { useSendEventMutation } from 'data/telemetry/send-event-mutation' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { TELEMETRY_EVENTS, TELEMETRY_VALUES } from 'lib/constants/telemetry' import { Button, Form_Shadcn_, @@ -28,13 +31,9 @@ import { SheetTitle, WarningIcon, } from 'ui' -import { Admonition } from 'ui-patterns' +import { Admonition } from 'ui-patterns/admonition' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' - -import EnableExtensionModal from 'components/interfaces/Database/Extensions/EnableExtensionModal' -import { useSendEventMutation } from 'data/telemetry/send-event-mutation' -import { TELEMETRY_EVENTS, TELEMETRY_VALUES } from 'lib/constants/telemetry' import { CRONJOB_DEFINITIONS } from './CronJobs.constants' import { buildCronQuery, diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobScheduleSection.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobScheduleSection.tsx index 040045578a5..a5245ab1299 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobScheduleSection.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobScheduleSection.tsx @@ -26,7 +26,7 @@ import { } from 'ui' import { Input } from 'ui-patterns/DataInputs/Input' import { CreateCronJobForm } from './CreateCronJobSheet' -import { getScheduleMessage, secondsPattern } from './CronJobs.utils' +import { formatScheduleString, getScheduleMessage, secondsPattern } from './CronJobs.utils' import CronSyntaxChart from './CronSyntaxChart' interface CronJobScheduleSectionProps { @@ -36,14 +36,10 @@ interface CronJobScheduleSectionProps { export const CronJobScheduleSection = ({ form, supportsSeconds }: CronJobScheduleSectionProps) => { const { project } = useProjectContext() - const initialValue = form.getValues('schedule') - const schedule = form.watch('schedule') - const [presetValue, setPresetValue] = useState(initialValue) - const [inputValue, setInputValue] = useState(initialValue) + const [inputValue, setInputValue] = useState('') const [debouncedValue] = useDebounce(inputValue, 750) const [useNaturalLanguage, setUseNaturalLanguage] = useState(false) - const [scheduleString, setScheduleString] = useState('') const PRESETS = [ ...(supportsSeconds ? [{ name: 'Every 30 seconds', expression: '30 seconds' }] : []), @@ -54,15 +50,21 @@ export const CronJobScheduleSection = ({ form, supportsSeconds }: CronJobSchedul { name: 'Every Monday at 2 AM', expression: '0 2 * * 1' }, ] as const - const { complete: generateCronSyntax, isLoading: isGeneratingCron } = useCompletion({ + const { + complete: generateCronSyntax, + isLoading: isGeneratingCron, + stop, + } = useCompletion({ api: `${BASE_PATH}/api/ai/sql/cron`, onResponse: async (response) => { if (response.ok) { // remove quotes from the cron expression const expression = (await response.text()).trim().replace(/^"|"$/g, '') - form.setValue('schedule', expression) - setPresetValue(expression) - setScheduleString(CronToString(expression)) + form.setValue('schedule', expression, { + shouldValidate: true, + shouldDirty: true, + shouldTouch: true, + }) } }, onError: (error) => { @@ -76,47 +78,15 @@ export const CronJobScheduleSection = ({ form, supportsSeconds }: CronJobSchedul }) useEffect(() => { - if (!useNaturalLanguage || !debouncedValue) return - generateCronSyntax(debouncedValue) + if (useNaturalLanguage && debouncedValue) { + generateCronSyntax(debouncedValue) + return () => stop() + } // eslint-disable-next-line react-hooks/exhaustive-deps }, [debouncedValue, useNaturalLanguage]) - useEffect(() => { - if (!inputValue || inputValue.length < 5) return // set a min length before showing invalid message - - // update the cronstrue string when the input value changes - try { - setScheduleString(CronToString(inputValue)) - form.setValue('schedule', inputValue) - } catch (error) { - console.error('Error converting cron expression to string:', error) - } - }, [form, inputValue]) - - useEffect(() => { - if (useNaturalLanguage) return - - setPresetValue(schedule) - - if (!schedule) { - setScheduleString('') - return - } - - try { - // Don't allow seconds-based schedules if seconds aren't supported - if (!supportsSeconds && secondsPattern.test(schedule)) { - setScheduleString('Invalid cron expression') - return - } - - setScheduleString(CronToString(schedule)) - } catch (error) { - setScheduleString('Invalid cron expression') - console.error('Error converting cron expression to string:', error) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [schedule]) + const schedule = form.watch('schedule') + const scheduleString = formatScheduleString(schedule) return ( @@ -136,108 +106,99 @@ export const CronJobScheduleSection = ({ form, supportsSeconds }: CronJobSchedul -
- {useNaturalLanguage ? ( - { - if (e.key === 'Enter') { - e.preventDefault() - } - }} - onChange={(e) => setInputValue(e.target.value)} - /> - ) : ( - { - if (e.key === 'Enter') { - e.preventDefault() - } - }} - /> - )} - - -
- { - setUseNaturalLanguage(!useNaturalLanguage) - setInputValue('') - setPresetValue('') - setScheduleString('') - form.setValue('schedule', '') - }} - /> -

Use natural language

-
- -
-
    - {PRESETS.map((preset) => ( -
  • - -
  • - ))} -
-
- - - - View syntax chart - - - - - - -
+ {useNaturalLanguage ? ( + { + if (e.key === 'Enter') { + e.preventDefault() + } + }} + onChange={(e) => setInputValue(e.target.value)} + /> + ) : ( + { + if (e.key === 'Enter') { + e.preventDefault() + } + }} + /> + )}
+ +
+
+ { + setUseNaturalLanguage(!useNaturalLanguage) + setInputValue('') + }} + /> +

Use natural language

+
+
    + {PRESETS.map((preset) => ( +
  • + +
  • + ))} +
+ + + + View syntax chart + + + + + + +

Schedule {timezone ? `(${timezone})` : ''}

- {scheduleString ? ( - - {isGeneratingCron ? : presetValue || '* * * * *'} - - ) : ( - - {isGeneratingCron ? : presetValue || '* * * * *'} - - )} + + {isGeneratingCron ? : schedule || '* * * * * *'} + + {!inputValue && !isGeneratingCron && !scheduleString ? ( Describe your schedule above ) : ( - {isGeneratingCron ? ( - - ) : ( - getScheduleMessage(scheduleString, schedule) - )} + {isGeneratingCron ? : getScheduleMessage(scheduleString)} )}
diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.ts b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.ts index b62ce3febdb..07210781d20 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.ts +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.ts @@ -1,3 +1,5 @@ +import { toString as CronToString } from 'cronstrue' + import { CronJobType } from './CreateCronJobSheet' import { HTTPHeader } from './CronJobs.constants' @@ -75,6 +77,7 @@ export const parseCronJobCommand = (originalCommand: string): CronJobType => { edgeFunctionName: url, httpHeaders: headersObjs, httpBody: body, + // @ts-ignore timeoutMs: +matches[5] ?? 1000, } } @@ -85,6 +88,7 @@ export const parseCronJobCommand = (originalCommand: string): CronJobType => { endpoint: url, httpHeaders: headersObjs, httpBody: body, + // @ts-ignore timeoutMs: +matches[5] ?? 1000, } } @@ -153,13 +157,14 @@ export function isSecondsFormat(schedule: string): boolean { return secondsPattern.test(schedule.trim()) } -export function getScheduleMessage(scheduleString: string, schedule: string) { +export function getScheduleMessage(scheduleString: string) { if (!scheduleString) { return 'Enter a valid cron expression above' } - if (secondsPattern.test(schedule)) { - return `The cron will be run every ${schedule}` + // if the schedule is in seconds format, scheduleString is same as the schedule + if (secondsPattern.test(scheduleString)) { + return `The cron will run every ${scheduleString}` } if (scheduleString.includes('Invalid cron expression')) { @@ -171,5 +176,17 @@ export function getScheduleMessage(scheduleString: string, schedule: string) { .map((s, i) => (i === 0 ? s.toLowerCase() : s)) .join(' ') - return `The cron will be run ${readableSchedule}.` + return `The cron will run ${readableSchedule}.` +} + +export const formatScheduleString = (value: string) => { + try { + if (secondsPattern.test(value)) { + return value + } else { + return CronToString(value) + } + } catch (error) { + return '' + } } diff --git a/apps/studio/components/interfaces/Integrations/Landing/Integrations.constants.tsx b/apps/studio/components/interfaces/Integrations/Landing/Integrations.constants.tsx index 185d58209a0..f8c4a3afa48 100644 --- a/apps/studio/components/interfaces/Integrations/Landing/Integrations.constants.tsx +++ b/apps/studio/components/interfaces/Integrations/Landing/Integrations.constants.tsx @@ -1,10 +1,10 @@ import { Clock5, Layers, Timer, Vault, Webhook } from 'lucide-react' +import dynamic from 'next/dynamic' import Image from 'next/image' import { ComponentType, ReactNode } from 'react' import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { BASE_PATH } from 'lib/constants' -import dynamic from 'next/dynamic' import { cn } from 'ui' import { WRAPPERS } from '../Wrappers/Wrappers.constants' import { WrapperMeta } from '../Wrappers/Wrappers.types' @@ -120,7 +120,7 @@ const supabaseIntegrations: IntegrationDefinition[] = [ description: 'Schedule and automate tasks to run maintenance routines at specified intervals.', docsUrl: 'https://github.com/citusdata/pg_cron', author: { - name: 'pg_cron', + name: 'Citus Data', websiteUrl: 'https://github.com/citusdata/pg_cron', }, navigation: [ diff --git a/apps/studio/components/ui/ErrorBoundaryState.tsx b/apps/studio/components/ui/ErrorBoundaryState.tsx index dfbaf1f3485..cf29098fe42 100644 --- a/apps/studio/components/ui/ErrorBoundaryState.tsx +++ b/apps/studio/components/ui/ErrorBoundaryState.tsx @@ -1,15 +1,31 @@ +import { isError } from 'lodash' import { ExternalLink } from 'lucide-react' import Link from 'next/link' import { useRouter } from 'next/router' -import type { FallbackProps } from 'react-error-boundary' -import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button } from 'ui' -import { WarningIcon } from 'ui' +import { + AlertDescription_Shadcn_, + AlertTitle_Shadcn_, + Alert_Shadcn_, + Button, + WarningIcon, +} from 'ui' + +// More correct version of FallbackProps from react-error-boundary +export type FallbackProps = { + error: unknown + resetErrorBoundary: (...args: any[]) => void +} export const ErrorBoundaryState = ({ error, resetErrorBoundary }: FallbackProps) => { const router = useRouter() - const message = `Path name: ${router.pathname}\n\n${error.stack}` - const isRemoveChildError = error.message.includes("Failed to execute 'removeChild' on 'Node'") + const checkIsError = isError(error) + + const errorMessage = checkIsError ? error.message : '' + const urlMessage = checkIsError ? `Path name: ${router.pathname}\n\n${error?.stack}` : '' + const isRemoveChildError = checkIsError + ? errorMessage.includes("Failed to execute 'removeChild' on 'Node'") + : false return (
@@ -18,7 +34,7 @@ export const ErrorBoundaryState = ({ error, resetErrorBoundary }: FallbackProps) Application error: a client-side exception has occurred (see browser console for more information)

-

Error: {error.message}

+

Error: {errorMessage}

{isRemoveChildError && ( @@ -48,7 +64,7 @@ export const ErrorBoundaryState = ({ error, resetErrorBoundary }: FallbackProps)