CronJobsTab.CleanupNotice.tsx 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. import { getScheduleDeleteCronJobRunDetailsSql } from '@supabase/pg-meta'
  2. import { CheckCircle2, XCircle } from 'lucide-react'
  3. import {
  4. Button,
  5. Dialog,
  6. DialogContent,
  7. DialogHeader,
  8. DialogSection,
  9. DialogSectionSeparator,
  10. DialogTitle,
  11. DialogTrigger,
  12. Progress,
  13. Select,
  14. SelectContent,
  15. SelectItem,
  16. SelectTrigger,
  17. SelectValue,
  18. Tooltip,
  19. TooltipContent,
  20. TooltipTrigger,
  21. } from 'ui'
  22. import { Admonition } from 'ui-patterns/admonition'
  23. import { CodeBlock } from 'ui-patterns/CodeBlock'
  24. import { CLEANUP_INTERVALS } from './CronJobsTab.constants'
  25. import {
  26. useCronJobsCleanupActions,
  27. type BatchDeletionProgress,
  28. } from './CronJobsTab.useCleanupActions'
  29. import { InlineLinkClassName } from '@/components/ui/InlineLink'
  30. import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
  31. interface CronJobRunDetailsOverflowNoticeV2Props {
  32. queryCost?: number
  33. refetchJobs: () => void
  34. }
  35. export const CronJobRunDetailsOverflowNotice = (props: CronJobRunDetailsOverflowNoticeV2Props) => {
  36. return (
  37. <Admonition
  38. type="note"
  39. className="rounded-none border-x-0 border-t-0 py-2 [&>svg]:top-[0.6rem] [&>svg]:left-10 pl-10 pr-10"
  40. layout="horizontal"
  41. actions={<CronJobRunDetailsOverflowDialog {...props} />}
  42. >
  43. <p className="text-xs">Last run for each cron job omitted due to high query cost</p>
  44. </Admonition>
  45. )
  46. }
  47. const CronJobRunDetailsOverflowDialog = ({
  48. queryCost,
  49. refetchJobs,
  50. }: CronJobRunDetailsOverflowNoticeV2Props) => {
  51. const { data: project } = useSelectedProjectQuery()
  52. const {
  53. cleanupInterval,
  54. cleanupState,
  55. isScheduling,
  56. isScheduleSuccess,
  57. setCleanupInterval,
  58. runBatchedDeletion,
  59. scheduleCleanup,
  60. cancelDeletion,
  61. } = useCronJobsCleanupActions({
  62. projectRef: project?.ref,
  63. connectionString: project?.connectionString,
  64. })
  65. const isDeleting = cleanupState.status === 'deleting'
  66. const isDeleteSuccess = cleanupState.status === 'delete-success'
  67. const isDeleteError = cleanupState.status === 'delete-error'
  68. const isBusy = isDeleting || isScheduling
  69. const canSchedule = isDeleteSuccess || isScheduleSuccess
  70. return (
  71. <Dialog>
  72. <DialogTrigger asChild>
  73. <Button type="default">Learn more</Button>
  74. </DialogTrigger>
  75. <DialogContent
  76. aria-describedby={undefined}
  77. onOpenAutoFocus={(event) => event.preventDefault()}
  78. >
  79. <DialogHeader>
  80. <DialogTitle>Last run for cron jobs omitted for overview</DialogTitle>
  81. </DialogHeader>
  82. <DialogSectionSeparator />
  83. <DialogSection className="flex flex-col gap-y-2">
  84. <p className="text-sm">
  85. The dashboard fetches data for the cron jobs overview by running a join between the{' '}
  86. <code className="text-code-inline">cron.job</code> and{' '}
  87. <code className="text-code-inline break-keep!">cron.job_run_details</code> tables to
  88. show each cron job's latest run.
  89. </p>
  90. <p className="text-sm">
  91. However, the join was skipped as the{' '}
  92. <Tooltip>
  93. <TooltipTrigger className={InlineLinkClassName}>estimated query cost</TooltipTrigger>
  94. <TooltipContent side="bottom" className="flex flex-col gap-y-1">
  95. <p>Estimated cost: {queryCost?.toLocaleString()}</p>
  96. <p className="text-foreground-light">
  97. Determined via the <code className="text-code-inline">EXPLAIN</code> command
  98. </p>
  99. </TooltipContent>
  100. </Tooltip>{' '}
  101. exceeds safety thresholds, likely due to the size of{' '}
  102. <code className="text-code-inline break-keep!">cron.job_run_details</code> table.
  103. </p>
  104. </DialogSection>
  105. <DialogSectionSeparator />
  106. <DialogSection className="flex flex-col gap-y-4">
  107. <p className="font-mono text-foreground-lighter uppercase tracking-tight text-sm">
  108. Suggested steps
  109. </p>
  110. <p className="text-sm">
  111. We recommend removing the old run history now, then scheduling a cron job that keeps
  112. trimming the <code className="text-code-inline">cron.job_run_details</code> table
  113. automatically. This also prevents unnecessary bloat on the database.
  114. </p>
  115. <div className="flex flex-col gap-y-2 text-sm">
  116. <p className="text-foreground">Step 1: Delete older entries</p>
  117. {isDeleting ? (
  118. <DeletionProgress progress={cleanupState.progress} onCancel={cancelDeletion} />
  119. ) : isDeleteSuccess ? (
  120. <DeletionSuccess totalRowsDeleted={cleanupState.totalRowsDeleted} />
  121. ) : isDeleteError ? (
  122. <DeletionError
  123. error={cleanupState.error}
  124. onRetry={() => runBatchedDeletion(cleanupInterval)}
  125. />
  126. ) : (
  127. <div className="flex flex-col gap-2 sm:flex-row sm:items-center">
  128. <div className="sm:w-64">
  129. <Select
  130. disabled={isBusy}
  131. value={cleanupInterval}
  132. onValueChange={setCleanupInterval}
  133. >
  134. <SelectTrigger className="w-full">
  135. <SelectValue placeholder="Select an interval" />
  136. </SelectTrigger>
  137. <SelectContent>
  138. {CLEANUP_INTERVALS.map((option) => (
  139. <SelectItem key={option.value} value={option.value}>
  140. {option.label}
  141. </SelectItem>
  142. ))}
  143. </SelectContent>
  144. </Select>
  145. </div>
  146. <Button
  147. type="default"
  148. disabled={isBusy}
  149. onClick={() => runBatchedDeletion(cleanupInterval)}
  150. >
  151. Delete rows now
  152. </Button>
  153. </div>
  154. )}
  155. </div>
  156. <div className="flex flex-col gap-y-2 text-sm">
  157. <p className="text-foreground">Step 2: Schedule an automated cleanup</p>
  158. {!canSchedule ? (
  159. <p className="text-foreground-lighter text-xs">
  160. Complete step 1 to enable scheduling a daily cleanup job.
  161. </p>
  162. ) : isScheduleSuccess ? (
  163. <ScheduleSuccess />
  164. ) : (
  165. <>
  166. <CodeBlock
  167. hideLineNumbers
  168. language="sql"
  169. value={getScheduleDeleteCronJobRunDetailsSql(cleanupInterval)}
  170. className="py-3 px-4 text-xs"
  171. wrapperClassName="max-w-full"
  172. />
  173. <Button
  174. block
  175. size="small"
  176. type="default"
  177. className="mt-1"
  178. loading={isScheduling}
  179. disabled={isScheduling}
  180. onClick={async () => {
  181. await scheduleCleanup({
  182. interval: cleanupInterval,
  183. onSuccess: () => refetchJobs(),
  184. })
  185. }}
  186. >
  187. Schedule cleanup job
  188. </Button>
  189. </>
  190. )}
  191. </div>
  192. </DialogSection>
  193. </DialogContent>
  194. </Dialog>
  195. )
  196. }
  197. interface DeletionProgressProps {
  198. progress: BatchDeletionProgress
  199. onCancel: () => void
  200. }
  201. const DeletionProgress = ({ progress, onCancel }: DeletionProgressProps) => {
  202. const { currentBatch, totalBatches, totalRowsDeleted } = progress
  203. const percentComplete =
  204. totalBatches > 0 ? Math.min(Math.round((currentBatch / totalBatches) * 100), 100) : 0
  205. return (
  206. <div className="space-y-2">
  207. <div className="flex items-center gap-3">
  208. <Progress value={percentComplete} className="flex-1 h-2" />
  209. <span className="text-xs text-foreground-light whitespace-nowrap">
  210. {percentComplete}% ({currentBatch}/{totalBatches} batches)
  211. </span>
  212. </div>
  213. <div className="flex items-center justify-between">
  214. <span className="text-xs text-foreground-light">
  215. Deleted {totalRowsDeleted.toLocaleString()} rows so far...
  216. </span>
  217. <Button type="outline" size="tiny" onClick={onCancel}>
  218. Cancel
  219. </Button>
  220. </div>
  221. </div>
  222. )
  223. }
  224. interface DeletionSuccessProps {
  225. totalRowsDeleted: number
  226. }
  227. const DeletionSuccess = ({ totalRowsDeleted }: DeletionSuccessProps) => (
  228. <div className="flex items-center gap-2 text-brand">
  229. <CheckCircle2 size={16} />
  230. <span className="text-sm">Successfully deleted {totalRowsDeleted.toLocaleString()} rows.</span>
  231. </div>
  232. )
  233. interface DeletionErrorProps {
  234. error: string
  235. onRetry: () => void
  236. }
  237. const DeletionError = ({ error, onRetry }: DeletionErrorProps) => (
  238. <div className="space-y-2">
  239. <div className="flex items-center gap-2 text-destructive">
  240. <XCircle size={16} />
  241. <span className="text-sm">Deletion failed: {error}</span>
  242. </div>
  243. <Button type="default" size="small" onClick={onRetry}>
  244. Retry
  245. </Button>
  246. </div>
  247. )
  248. const ScheduleSuccess = () => (
  249. <div className="space-y-2">
  250. <div className="flex items-center gap-2 text-brand">
  251. <CheckCircle2 size={16} />
  252. <span className="text-sm">Daily cleanup job scheduled successfully.</span>
  253. </div>
  254. <div className="flex items-center gap-2">
  255. <p className="text-foreground-lighter text-xs">
  256. New cleanup job should now be visible in the cron jobs overview.
  257. </p>
  258. </div>
  259. </div>
  260. )