diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobCard.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobCard.tsx
index 879ffd756e4..22f57fd3754 100644
--- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobCard.tsx
+++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobCard.tsx
@@ -22,6 +22,9 @@ import {
DropdownMenuTrigger,
Label_Shadcn_,
Switch,
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
} from 'ui'
import { TimestampInfo } from 'ui-patterns'
import { Input } from 'ui-patterns/DataInputs/Input'
@@ -53,6 +56,17 @@ export const CronJobCard = ({ job, onEditCronJob, onDeleteCronJob }: CronJobCard
const { mutate: sendEvent } = useSendEventMutation()
const { mutate: toggleDatabaseCronJob, isLoading } = useDatabaseCronJobToggleMutation()
+ const onEdit = () => {
+ sendEvent({
+ action: 'cron_job_update_clicked',
+ groups: {
+ project: selectedProject?.ref ?? 'Unknown',
+ organization: org?.slug ?? 'Unknown',
+ },
+ })
+ onEditCronJob(job)
+ }
+
return (
<>
@@ -64,10 +78,10 @@ export const CronJobCard = ({ job, onEditCronJob, onDeleteCronJob }: CronJobCard
- {job.jobname ?? 'No name provided'}
+ {job.jobname || 'No name provided'}
{isLoading ? (
@@ -102,7 +116,7 @@ export const CronJobCard = ({ job, onEditCronJob, onDeleteCronJob }: CronJobCard
}}
>
History
@@ -112,20 +126,21 @@ export const CronJobCard = ({ job, onEditCronJob, onDeleteCronJob }: CronJobCard
} className="px-1.5" />
- {
- sendEvent({
- action: 'cron_job_update_clicked',
- groups: {
- project: selectedProject?.ref ?? 'Unknown',
- organization: org?.slug ?? 'Unknown',
- },
- })
- onEditCronJob(job)
- }}
- >
- Edit cron job
-
+ {job.jobname ? (
+ Edit cron job
+ ) : (
+
+
+
+ Edit cron job
+
+
+
+ This cron job doesn’t have a name and can’t be edited. Create a new one and
+ delete this job.
+
+
+ )}
{
diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.test.ts b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.test.ts
index c85514b75d8..fc7c9ffc289 100644
--- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.test.ts
+++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.test.ts
@@ -41,11 +41,16 @@ describe('parseCronJobCommand', () => {
})
it('should return a edge function config when the command posts to its own supabase.co project', () => {
- const command = `select net.http_post( url:='https://random_project_ref.supabase.co/functions/v1/_', headers:=jsonb_build_object(), body:='', timeout_milliseconds:=5000 );`
+ const command = `select net.http_post( url:='https://random_project_ref.supabase.co/functions/v1/_', headers:=jsonb_build_object('Authorization', 'Bearer something'), body:='', timeout_milliseconds:=5000 );`
expect(parseCronJobCommand(command, 'random_project_ref')).toStrictEqual({
edgeFunctionName: 'https://random_project_ref.supabase.co/functions/v1/_',
method: 'POST',
- httpHeaders: [],
+ httpHeaders: [
+ {
+ name: 'Authorization',
+ value: 'Bearer something',
+ },
+ ],
httpBody: '',
timeoutMs: 5000,
type: 'edge_function',
@@ -53,6 +58,42 @@ describe('parseCronJobCommand', () => {
})
})
+ it('should return a edge function config when the body is missing', () => {
+ const command = `select net.http_post( url:='https://random_project_ref.supabase.co/functions/v1/_', headers:=jsonb_build_object('Authorization', 'Bearer something'), timeout_milliseconds:=5000 );`
+ expect(parseCronJobCommand(command, 'random_project_ref')).toStrictEqual({
+ edgeFunctionName: 'https://random_project_ref.supabase.co/functions/v1/_',
+ method: 'POST',
+ httpHeaders: [
+ {
+ name: 'Authorization',
+ value: 'Bearer something',
+ },
+ ],
+ httpBody: '',
+ timeoutMs: 5000,
+ type: 'edge_function',
+ snippet: command,
+ })
+ })
+
+ it("should return a HTTP request config when there's a query parameter or hash in the URL (also handles edge function)", () => {
+ const command = `select net.http_post( url:='https://random_project_ref.supabase.co/functions/v1/_?first=1#second=2', headers:=jsonb_build_object('Authorization', 'Bearer something'), timeout_milliseconds:=5000 )`
+ expect(parseCronJobCommand(command, 'random_project_ref')).toStrictEqual({
+ endpoint: 'https://random_project_ref.supabase.co/functions/v1/_?first=1#second=2',
+ method: 'POST',
+ httpHeaders: [
+ {
+ name: 'Authorization',
+ value: 'Bearer something',
+ },
+ ],
+ httpBody: '',
+ timeoutMs: 5000,
+ type: 'http_request',
+ snippet: command,
+ })
+ })
+
it('should return a HTTP request config when the command posts to another supabase.co project', () => {
const command = `select net.http_post( url:='https://another_project_ref.supabase.co/functions/v1/_', headers:=jsonb_build_object(), body:='', timeout_milliseconds:=5000 );`
expect(parseCronJobCommand(command, 'random_project_ref')).toStrictEqual({
diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.ts b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.ts
index 6ce169aa2c1..08bca3ff2de 100644
--- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.ts
+++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.ts
@@ -85,7 +85,22 @@ export const parseCronJobCommand = (originalCommand: string, projectRef: string)
}
}
- if (url.includes(`${projectRef}.supabase.`) && url.includes('/functions/v1/')) {
+ // If there's a search param or hash in the edge function URL, let it be handled by the HTTP Request case.
+ // Otherwise, the params/hash may be lost during editing of the cron job.
+ let searchParams = ''
+ let urlHash = ''
+ try {
+ const urlObject = new URL(url)
+ searchParams = urlObject.search
+ urlHash = urlObject.hash
+ } catch {}
+
+ if (
+ url.includes(`${projectRef}.supabase.`) &&
+ url.includes('/functions/v1/') &&
+ searchParams.length === 0 &&
+ urlHash.length === 0
+ ) {
return {
type: 'edge_function',
method: method === 'http_get' ? 'GET' : 'POST',
@@ -168,10 +183,10 @@ export const cronPattern =
/^(\*|(\d+|\*\/\d+)|\d+\/\d+|\d+-\d+|\d+(,\d+)*)(\s+(\*|(\d+|\*\/\d+)|\d+\/\d+|\d+-\d+|\d+(,\d+)*)){4}$/
// detect seconds like "10 seconds" or normal cron syntax like "*/5 * * * *"
-export const secondsPattern = /^\d+\s+seconds$/
+export const secondsPattern = /^\d+\s+seconds*$/
export function isSecondsFormat(schedule: string): boolean {
- return secondsPattern.test(schedule.trim())
+ return secondsPattern.test(schedule.trim().toLocaleLowerCase())
}
export function getScheduleMessage(scheduleString: string) {
@@ -230,11 +245,11 @@ export const getNextRun = (schedule: string, lastRun?: string) => {
return undefined
}
} else {
- // [Joshen] Only going to attempt to parse if the schedule is as simple as "n seconds", "n minutes", or "n days"
+ // [Joshen] Only going to attempt to parse if the schedule is as simple as "n second" or "n seconds"
// Returned undefined otherwise - we can revisit this perhaps if we get feedback about this
- const [value, unit] = schedule.split(' ')
+ const [value, unit] = schedule.toLocaleLowerCase().split(' ')
if (
- ['seconds', 'minutes', 'days'].includes(unit) &&
+ ['second', 'seconds'].includes(unit) &&
!Number.isNaN(Number(value)) &&
lastRun !== undefined
) {
diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/DeleteCronJob.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/DeleteCronJob.tsx
index fbdc7691214..2235589b616 100644
--- a/apps/studio/components/interfaces/Integrations/CronJobs/DeleteCronJob.tsx
+++ b/apps/studio/components/interfaces/Integrations/CronJobs/DeleteCronJob.tsx
@@ -6,6 +6,7 @@ import { CronJob } from 'data/database-cron-jobs/database-cron-jobs-query'
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
import TextConfirmModal from 'ui-patterns/Dialogs/TextConfirmModal'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
+import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
interface DeleteCronJobProps {
cronJob: CronJob
@@ -43,6 +44,22 @@ export const DeleteCronJob = ({ cronJob, visible, onClose }: DeleteCronJobProps)
return null
}
+ // Cron job name is optional. If the cron job has no name, show a simplified modal which doesn't require the user to input the name.
+ if (!cronJob.jobname) {
+ return (
+ onClose()}
+ onConfirm={handleDelete}
+ title={`Delete the cron job`}
+ loading={isLoading}
+ confirmLabel={`Delete`}
+ alert={{ title: 'You cannot recover this cron job once deleted.' }}
+ />
+ )
+ }
+
return (
): boolean {
}
export const PreviousRunsTab = () => {
- const { childId: jobName } = useParams()
+ const { childId } = useParams()
const { project } = useProjectContext()
+ const jobId = Number(childId)
+
const { data: cronJobs, isLoading: isLoadingCronJobs } = useCronJobsQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
- const currentJobState = cronJobs?.find((job) => job.jobname === jobName)
-
const {
data,
isLoading: isLoadingCronJobRuns,
@@ -153,9 +153,9 @@ export const PreviousRunsTab = () => {
{
projectRef: project?.ref,
connectionString: project?.connectionString,
- jobId: Number(currentJobState?.jobid),
+ jobId: jobId,
},
- { enabled: !!currentJobState?.jobid, staleTime: 30 }
+ { enabled: !!jobId, staleTime: 30 }
)
useEffect(() => {
@@ -177,6 +177,7 @@ export const PreviousRunsTab = () => {
[fetchNextPage, isLoadingCronJobRuns]
)
+ const currentJobState = cronJobs?.find((job) => job.jobid === jobId)
const cronJobRuns = useMemo(() => data?.pages.flatMap((p) => p) || [], [data?.pages])
return (
@@ -224,7 +225,9 @@ export const PreviousRunsTab = () => {
{currentJobState?.schedule ? (
<>
- {currentJobState.schedule}
+
+ {currentJobState.schedule.toLocaleLowerCase()}
+
{isSecondsFormat(currentJobState.schedule)
? ''
diff --git a/apps/studio/components/layouts/Integrations/tabs.tsx b/apps/studio/components/layouts/Integrations/tabs.tsx
index 3fc557ca5a5..ed8fbb47dcb 100644
--- a/apps/studio/components/layouts/Integrations/tabs.tsx
+++ b/apps/studio/components/layouts/Integrations/tabs.tsx
@@ -25,7 +25,7 @@ interface IntegrationTabsProps {
export const IntegrationTabs = ({ scroll, isSticky }: IntegrationTabsProps) => {
const navRef = useRef(null)
const { project } = useProjectContext()
- const { id, pageId, childId } = useParams()
+ const { id, pageId, childId, childLabel } = useParams()
const isMobile = useBreakpoint('md')
const { installedIntegrations } = useInstalledIntegrations()
@@ -97,7 +97,9 @@ export const IntegrationTabs = ({ scroll, isSticky }: IntegrationTabsProps) => {
>
{tab.childIcon}
- {childId}
+
+ {childLabel ? childLabel : childId}
+
>
diff --git a/apps/studio/data/database-cron-jobs/database-cron-jobs-query.ts b/apps/studio/data/database-cron-jobs/database-cron-jobs-query.ts
index 2112c894261..0217f664301 100644
--- a/apps/studio/data/database-cron-jobs/database-cron-jobs-query.ts
+++ b/apps/studio/data/database-cron-jobs/database-cron-jobs-query.ts
@@ -17,7 +17,7 @@ export type CronJob = {
database: string
username: string
active: boolean
- jobname: string
+ jobname: string | null
}
const cronJobSqlQuery = `select * from cron.job order by jobid;`