CronJobPage.tsx 6.2 KB

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