Add last and next run in CronJobCard (#31305)

* Add last and next run in CronJobCard

* Remove unnecessary query

* Refactor

* Remove unused file

* Refactor
This commit is contained in:
Joshen Lim authored and GitHub committed 2024-12-24 10:31:28 +08:00
1 parent a8d3479f17
commit 4e5d4b2cbb
4 files changed
+166 -16

No files matched your search

@@ -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 (
<>
<div className="bg-surface-100 border-default overflow-hidden border shadow px-5 py-4 flex flex-row rounded-md space-x-4">
@@ -120,12 +126,56 @@ export const CronJobCard = ({ job, onEditCronJob, onDeleteCronJob }: CronJobCard
<div className="grid grid-cols-10 gap-3 items-center">
<span className="text-foreground-light col-span-1">Schedule</span>
<div className="col-span-9">
<Input
title={schedule}
readOnly
className="input-mono [&>div>div>div>input]:text-xs [&>div>div>div>input]:opacity-100 flex-1"
value={schedule}
/>
<Input readOnly title={schedule} value={schedule} className="w-96" />
</div>
</div>
<div className="grid grid-cols-10 gap-3 items-center">
<span className="text-foreground-light col-span-1">Last run</span>
<div className="col-span-9">
<div
className={cn(
'border border-control bg-foreground/[0.026] rounded-md px-3 py-1.5 w-96',
!lastRun && 'text-foreground-lighter'
)}
>
{lastRun ? (
<>
<TimestampInfo
utcTimestamp={lastRun}
labelFormat="DD MMM YYYY HH:mm:ss (ZZ)"
className="font-sans text-sm"
/>
{data?.status && (
<Badge variant="success" className="capitalize ml-2">
{data.status}
</Badge>
)}
</>
) : (
'Job has not been run yet'
)}
</div>
</div>
</div>
<div className="grid grid-cols-10 gap-3 items-center">
<span className="text-foreground-light col-span-1">Next run</span>
<div className="col-span-9">
<div
className={cn(
'border border-control bg-foreground/[0.026] rounded-md px-3 py-1.5 w-96',
!nextRun && 'text-foreground-lighter'
)}
>
{nextRun ? (
<TimestampInfo
utcTimestamp={nextRun}
labelFormat="DD MMM YYYY HH:mm:ss (ZZ)"
className="font-sans text-sm"
/>
) : (
'Unable to parse next run for job'
)}
</div>
</div>
</div>
<div className="grid grid-cols-10 gap-3">
@@ -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
}
}
}
@@ -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 = <TData = DatabaseCronJobRunData>(
{ projectRef, connectionString, jobId }: DatabaseCronJobRunVariables,
{
enabled = true,
...options
}: UseQueryOptions<DatabaseCronJobRunData, DatabaseCronJobRunError, TData> = {}
) =>
useQuery<DatabaseCronJobRunData, DatabaseCronJobRunError, TData>(
databaseCronJobsKeys.run(projectRef, jobId),
() => getDatabaseCronJobRun({ projectRef, connectionString, jobId }),
{
enabled: enabled && typeof projectRef !== 'undefined',
...options,
}
)
@@ -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,