diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobCard.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobCard.tsx
index 02539377fcd..a1c980eb923 100644
--- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobCard.tsx
+++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobCard.tsx
@@ -1,4 +1,4 @@
-import { toString as CronToString } from 'cronstrue'
+import dayjs from 'dayjs'
import { Clock, History, Loader2, MoreVertical } from 'lucide-react'
import Link from 'next/link'
import { useState } from 'react'
@@ -7,21 +7,26 @@ import { useParams } from 'common'
import { SQLCodeBlock } from 'components/interfaces/Auth/ThirdPartyAuthForm/SqlCodeBlock'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { CronJob } from 'data/database-cron-jobs/database-cron-jobs-query'
+import { useCronJobRunQuery } from 'data/database-cron-jobs/database-cron-jobs-run-query'
import { useDatabaseCronJobToggleMutation } from 'data/database-cron-jobs/database-cron-jobs-toggle-mutation'
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
import { TelemetryActions } from 'lib/constants/telemetry'
import {
+ Badge,
Button,
+ cn,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
- Input,
Label_Shadcn_,
Switch,
} from 'ui'
+import { TimestampInfo } from 'ui-patterns'
+import { Input } from 'ui-patterns/DataInputs/Input'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
+import { convertCronToString, getNextRun } from './CronJobs.utils'
interface CronJobCardProps {
job: CronJob
@@ -35,17 +40,18 @@ export const CronJobCard = ({ job, onEditCronJob, onDeleteCronJob }: CronJobCard
const [toggleConfirmationModalShown, showToggleConfirmationModal] = useState(false)
+ const { data } = useCronJobRunQuery({
+ projectRef: ref,
+ connectionString: selectedProject?.connectionString,
+ jobId: job.jobid,
+ })
+ const lastRun = data?.start_time ? dayjs(data.start_time).valueOf() : undefined
+ const nextRun = getNextRun(job.schedule, data?.start_time)
+ const schedule = convertCronToString(job.schedule)
+
const { mutate: sendEvent } = useSendEventMutation()
const { mutate: toggleDatabaseCronJob, isLoading } = useDatabaseCronJobToggleMutation()
- // pg_cron can also use "30 seconds" format for schedule. Cronstrue doesn't understand that format so just use the
- // original schedule when cronstrue throws
- let schedule = job.schedule
- try {
- const scheduleString = CronToString(job.schedule)
- schedule = scheduleString
- } catch {}
-
return (
<>
@@ -120,12 +126,56 @@ export const CronJobCard = ({ job, onEditCronJob, onDeleteCronJob }: CronJobCard
+
+
Last run
+
+
+ {lastRun ? (
+ <>
+
+ {data?.status && (
+
+ {data.status}
+
+ )}
+ >
+ ) : (
+ 'Job has not been run yet'
+ )}
+
+
+
+
+
Next run
+
+
+ {nextRun ? (
+
+ ) : (
+ 'Unable to parse next run for job'
+ )}
+
diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.ts b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.ts
index 07210781d20..41fe71637d2 100644
--- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.ts
+++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.ts
@@ -1,4 +1,6 @@
+import parser from 'cron-parser'
import { toString as CronToString } from 'cronstrue'
+import dayjs from 'dayjs'
import { CronJobType } from './CreateCronJobSheet'
import { HTTPHeader } from './CronJobs.constants'
@@ -190,3 +192,41 @@ export const formatScheduleString = (value: string) => {
return ''
}
}
+
+export const convertCronToString = (schedule: string) => {
+ // pg_cron can also use "30 seconds" format for schedule. Cronstrue doesn't understand that format so just use the
+ // original schedule when cronstrue throws
+ try {
+ return CronToString(schedule)
+ } catch (error) {
+ return schedule
+ }
+}
+
+export const getNextRun = (schedule: string, lastRun?: string) => {
+ // cron-parser can only deal with the traditional cron syntax but technically users can also
+ // use strings like "30 seconds" now, For the latter case, we try our best to parse the next run
+ // (can't guarantee as scope is quite big)
+ if (schedule.includes('*')) {
+ try {
+ const interval = parser.parseExpression(schedule)
+ return interval.next().getTime()
+ } catch (error) {
+ return undefined
+ }
+ } else {
+ // [Joshen] Only going to attempt to parse if the schedule is as simple as "n seconds", "n minutes", or "n days"
+ // Returned undefined otherwise - we can revisit this perhaps if we get feedback about this
+ const [value, unit] = schedule.split(' ')
+ if (
+ ['seconds', 'minutes', 'days'].includes(unit) &&
+ !Number.isNaN(Number(value)) &&
+ lastRun !== undefined
+ ) {
+ const parsedLastRun = dayjs(lastRun).add(Number(value), unit as dayjs.ManipulateType)
+ return parsedLastRun.valueOf()
+ } else {
+ return undefined
+ }
+ }
+}
diff --git a/apps/studio/data/database-cron-jobs/database-cron-jobs-run-query.ts b/apps/studio/data/database-cron-jobs/database-cron-jobs-run-query.ts
new file mode 100644
index 00000000000..aaa92df92e2
--- /dev/null
+++ b/apps/studio/data/database-cron-jobs/database-cron-jobs-run-query.ts
@@ -0,0 +1,53 @@
+import { useQuery, UseQueryOptions } from '@tanstack/react-query'
+
+import { executeSql } from 'data/sql/execute-sql-query'
+import { ResponseError } from 'types'
+import { CronJobRun } from './database-cron-jobs-runs-infinite-query'
+import { databaseCronJobsKeys } from './keys'
+
+export type DatabaseCronJobRunVariables = {
+ projectRef?: string
+ connectionString?: string
+ jobId: number
+}
+
+export async function getDatabaseCronJobRun({
+ projectRef,
+ connectionString,
+ jobId,
+}: DatabaseCronJobRunVariables) {
+ if (!projectRef) throw new Error('Project ref is required')
+
+ const query = `
+ SELECT * FROM cron.job_run_details
+ WHERE jobid = '${jobId}'
+ ORDER BY start_time DESC
+ LIMIT 1`
+
+ const { result } = await executeSql({
+ projectRef,
+ connectionString,
+ sql: query,
+ })
+
+ return result[0]
+}
+
+export type DatabaseCronJobRunData = CronJobRun
+export type DatabaseCronJobRunError = ResponseError
+
+export const useCronJobRunQuery = (
+ { projectRef, connectionString, jobId }: DatabaseCronJobRunVariables,
+ {
+ enabled = true,
+ ...options
+ }: UseQueryOptions = {}
+) =>
+ useQuery(
+ databaseCronJobsKeys.run(projectRef, jobId),
+ () => getDatabaseCronJobRun({ projectRef, connectionString, jobId }),
+ {
+ enabled: enabled && typeof projectRef !== 'undefined',
+ ...options,
+ }
+ )
diff --git a/apps/studio/data/database-cron-jobs/keys.ts b/apps/studio/data/database-cron-jobs/keys.ts
index f132226a3d8..60f75b20a65 100644
--- a/apps/studio/data/database-cron-jobs/keys.ts
+++ b/apps/studio/data/database-cron-jobs/keys.ts
@@ -3,6 +3,13 @@ export const databaseCronJobsKeys = {
delete: () => ['cron-jobs', 'delete'] as const,
alter: () => ['cronjobs', 'alter'] as const,
list: (projectRef: string | undefined) => ['projects', projectRef, 'cron-jobs'] as const,
+ run: (projectRef: string | undefined, jobId: number) => [
+ 'projects',
+ projectRef,
+ 'cron-jobs',
+ jobId,
+ 'run',
+ ],
runsInfinite: (projectRef: string | undefined, jobId: number, options?: object) => [
'projects',
projectRef,