CronJobPage.tsx191 lines · main
1import { useParams } from 'common'
2import { toString as CronToString } from 'cronstrue'
3import { Edit3, List } from 'lucide-react'
4import Link from 'next/link'
5import { useRouter } from 'next/router'
6import { useState } from 'react'
7import {
8 BreadcrumbItem,
9 BreadcrumbLink,
10 BreadcrumbList,
11 BreadcrumbPage,
12 BreadcrumbSeparator,
13 Button,
14 cn,
15 Tooltip,
16 TooltipContent,
17 TooltipTrigger,
18} from 'ui'
19import { CodeBlock } from 'ui-patterns/CodeBlock'
20import {
21 PageHeader,
22 PageHeaderAside,
23 PageHeaderBreadcrumb,
24 PageHeaderDescription,
25 PageHeaderMeta,
26 PageHeaderSummary,
27 PageHeaderTitle,
28} from 'ui-patterns/PageHeader'
29import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
30
31import { CreateCronJobSheet } from './CreateCronJobSheet/CreateCronJobSheet'
32import { isSecondsFormat, parseCronJobCommand } from './CronJobs.utils'
33import { PreviousRunsTab } from './PreviousRunsTab'
34import { useCronJobQuery } from '@/data/database-cron-jobs/database-cron-job-query'
35import { useEdgeFunctionsQuery } from '@/data/edge-functions/edge-functions-query'
36import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
37
38export const CronJobPage = () => {
39 const router = useRouter()
40 const { ref, id, pageId, childId } = useParams()
41 const childLabel = router?.query?.['child-label'] as string
42 const { data: project } = useSelectedProjectQuery()
43
44 const [isEditSheetOpen, setIsEditSheetOpen] = useState(false)
45
46 const jobId = Number(childId)
47
48 const { data: job, isPending: isLoading } = useCronJobQuery({
49 projectRef: project?.ref,
50 connectionString: project?.connectionString,
51 id: jobId,
52 })
53
54 const { data: edgeFunctions = [] } = useEdgeFunctionsQuery({ projectRef: project?.ref })
55
56 // Parse the cron job command to check if it's an edge function
57 const cronJobValues = parseCronJobCommand(job?.command || '', project?.ref!)
58 const edgeFunction =
59 cronJobValues.type === 'edge_function' ? cronJobValues.edgeFunctionName : undefined
60 const edgeFunctionSlug = edgeFunction?.split('/functions/v1/').pop()
61 const isValidEdgeFunction = edgeFunctions.some((x) => x.slug === edgeFunctionSlug)
62
63 const pageTitle = childLabel || childId || 'Cron Job'
64
65 const pageSubtitle = job ? (
66 <div className="text-sm text-foreground-light">
67 Running{' '}
68 <Tooltip>
69 <TooltipTrigger asChild>
70 <span className="cursor-pointer underline decoration-dotted lowercase">
71 {isSecondsFormat(job.schedule)
72 ? job.schedule.toLowerCase()
73 : CronToString(job.schedule.toLowerCase().replace(/\$/g, 'L'))}
74 </span>
75 </TooltipTrigger>
76 <TooltipContent side="bottom" align="center">
77 <div className="text-xs">
78 <p className="font-mono mb-1">{job.schedule.toLowerCase()}</p>
79 {!isSecondsFormat(job.schedule) && (
80 <p className="text-foreground-light">{CronToString(job.schedule.toLowerCase())}</p>
81 )}
82 </div>
83 </TooltipContent>
84 </Tooltip>{' '}
85 with command{' '}
86 <Tooltip>
87 <TooltipTrigger asChild>
88 <code className="text-code-inline max-w-[200px] inline-block truncate align-bottom cursor-pointer">
89 {job.command}
90 </code>
91 </TooltipTrigger>
92 <TooltipContent
93 side="bottom"
94 align="center"
95 className="min-w-[200px] max-w-[400px] text-wrap p-0"
96 >
97 <p className="text-xs font-mono px-2 py-1 border-b bg-surface-100">Command</p>
98 <CodeBlock
99 hideLineNumbers
100 language="sql"
101 value={job.command.trim()}
102 className={cn(
103 'py-0 px-3.5 max-w-full prose dark:prose-dark border-0 rounded-t-none',
104 '[&>code]:m-0 [&>code>span]:flex [&>code>span]:flex-wrap min-h-11',
105 '[&>code]:text-xs'
106 )}
107 />
108 </TooltipContent>
109 </Tooltip>
110 </div>
111 ) : null
112
113 // Secondary actions
114 const secondaryActions = [
115 <Button
116 key="edit"
117 type="outline"
118 icon={<Edit3 strokeWidth={1.5} size="14" />}
119 onClick={() => setIsEditSheetOpen(true)}
120 >
121 Edit
122 </Button>,
123 <Button key="view-logs" asChild type="outline" icon={<List strokeWidth={1.5} size="14" />}>
124 <Link
125 target="_blank"
126 rel="noopener noreferrer"
127 href={`/project/${project?.ref}/logs/pgcron-logs/`}
128 >
129 View Cron logs
130 </Link>
131 </Button>,
132 ...(isValidEdgeFunction
133 ? [
134 <Button key="view-edge-logs" asChild type="outline">
135 <Link
136 target="_blank"
137 rel="noopener noreferrer"
138 href={`/project/${project?.ref}/functions/${edgeFunctionSlug}/logs`}
139 >
140 View Edge Function logs
141 </Link>
142 </Button>,
143 ]
144 : []),
145 ]
146
147 return (
148 <>
149 <PageHeader size="full" className="pb-6">
150 <PageHeaderBreadcrumb>
151 <BreadcrumbList>
152 <BreadcrumbItem>
153 <BreadcrumbLink asChild>
154 <Link href={`/project/${ref}/integrations`}>Integrations</Link>
155 </BreadcrumbLink>
156 </BreadcrumbItem>
157 <BreadcrumbSeparator />
158 <BreadcrumbItem>
159 <BreadcrumbLink asChild>
160 <Link href={`/project/${ref}/integrations/${id}/${pageId}`}>Cron</Link>
161 </BreadcrumbLink>
162 </BreadcrumbItem>
163 <BreadcrumbSeparator />
164 <BreadcrumbItem>
165 <BreadcrumbPage>{childLabel ?? job?.jobname ?? 'Cron Job'}</BreadcrumbPage>
166 </BreadcrumbItem>
167 </BreadcrumbList>
168 </PageHeaderBreadcrumb>
169 <PageHeaderMeta>
170 <PageHeaderSummary>
171 <PageHeaderTitle>{pageTitle}</PageHeaderTitle>
172 <PageHeaderDescription>
173 {isLoading ? <ShimmeringLoader className="py-0 h-[20px] w-96" /> : pageSubtitle}
174 </PageHeaderDescription>
175 </PageHeaderSummary>
176 {secondaryActions.length > 0 && <PageHeaderAside>{secondaryActions}</PageHeaderAside>}
177 </PageHeaderMeta>
178 </PageHeader>
179
180 <PreviousRunsTab />
181
182 {job && (
183 <CreateCronJobSheet
184 open={isEditSheetOpen}
185 selectedCronJob={job}
186 onClose={() => setIsEditSheetOpen(false)}
187 />
188 )}
189 </>
190 )
191}