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