mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
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:
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,
|
||||
|
||||
Reference in new issue
Block a user