From 4e5d4b2cbb13f7dda021d1b1bcd3920cbeabf52c Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Tue, 24 Dec 2024 10:31:28 +0800 Subject: [PATCH] Add last and next run in CronJobCard (#31305) * Add last and next run in CronJobCard * Remove unnecessary query * Refactor * Remove unused file * Refactor --- .../Integrations/CronJobs/CronJobCard.tsx | 82 +++++++++++++++---- .../Integrations/CronJobs/CronJobs.utils.ts | 40 +++++++++ .../database-cron-jobs-run-query.ts | 53 ++++++++++++ apps/studio/data/database-cron-jobs/keys.ts | 7 ++ 4 files changed, 166 insertions(+), 16 deletions(-) create mode 100644 apps/studio/data/database-cron-jobs/database-cron-jobs-run-query.ts 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
Schedule
- + +
+
+
+ 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,