CronJobsTab.tsx191 lines · main
1// @ts-nocheck
2import { useParams } from 'common'
3import { Loader2 } from 'lucide-react'
4import { useRouter } from 'next/router'
5import { parseAsBoolean, parseAsString, useQueryState } from 'nuqs'
6import { MouseEvent, useEffect, useMemo, useState } from 'react'
7import { toast } from 'sonner'
8import { LoadingLine } from 'ui'
9
10import { formatCronJobColumns } from './CronJobs.utils'
11import { CronJobRunDetailsOverflowNotice } from './CronJobsTab.CleanupNotice'
12import { CronJobsTabDataGrid } from './CronJobsTab.DataGrid'
13import { CronJobsTabHeader } from './CronJobsTab.Header'
14import { useCronJobsData } from './CronJobsTab.useCronJobsData'
15import { DeleteCronJob } from './DeleteCronJob'
16import { CreateCronJobSheet } from '@/components/interfaces/Integrations/CronJobs/CreateCronJobSheet/CreateCronJobSheet'
17import { CronJob } from '@/data/database-cron-jobs/database-cron-jobs-infinite-query'
18import { useSendEventMutation } from '@/data/telemetry/send-event-mutation'
19import { useInfiniteScroll } from '@/hooks/misc/useInfiniteScroll'
20import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
21import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
22import { cleanPointerEventsNoneOnBody } from '@/lib/helpers'
23import { createNavigationHandler } from '@/lib/navigation'
24
25const EMPTY_CRON_JOB = { jobname: '', schedule: '', active: true, command: '' }
26
27export const CronjobsTab = () => {
28 const router = useRouter()
29 const { ref } = useParams()
30 const { data: project } = useSelectedProjectQuery()
31 const { data: org } = useSelectedOrganizationQuery()
32
33 const [searchQuery, setSearchQuery] = useQueryState('search', parseAsString.withDefault(''))
34
35 const [search, setSearch] = useState(searchQuery)
36
37 const handleSearchSubmit = () => {
38 const trimmed = search.trim()
39 setSearchQuery(trimmed.length > 0 ? trimmed : null)
40 }
41 const handleClearSearch = () => {
42 setSearch('')
43 setSearchQuery(null)
44 }
45
46 const { grid, count } = useCronJobsData({
47 projectRef: project?.ref,
48 connectionString: project?.connectionString,
49 searchQuery,
50 })
51
52 const [createCronJobSheetShown, setCreateCronJobSheetShown] = useQueryState(
53 'new',
54 parseAsBoolean.withDefault(false).withOptions({ clearOnDefault: true })
55 )
56
57 const [cronJobIdForEditing, setCronJobForEditing] = useQueryState('edit', parseAsString)
58 const cronJobForEditing = grid.rows.find((j) => j.jobid.toString() === cronJobIdForEditing)
59
60 const [, setCronJobForDeletion] = useQueryState('delete', parseAsString)
61
62 const { mutate: sendEvent } = useSendEventMutation()
63
64 const columns = useMemo(
65 () =>
66 formatCronJobColumns({
67 onSelectEdit: (job: CronJob) => {
68 sendEvent({
69 action: 'cron_job_update_clicked',
70 groups: { project: ref ?? 'Unknown', organization: org?.slug ?? 'Unknown' },
71 })
72 setCronJobForEditing(job.jobid.toString())
73 },
74 onSelectDelete: (job: CronJob) => {
75 sendEvent({
76 action: 'cron_job_delete_clicked',
77 groups: { project: ref ?? 'Unknown', organization: org?.slug ?? 'Unknown' },
78 })
79 setCronJobForDeletion(job.jobid.toString())
80 },
81 }),
82 [org?.slug, ref, sendEvent, setCronJobForEditing, setCronJobForDeletion]
83 )
84
85 const handleScroll = useInfiniteScroll({
86 isLoading: grid.isLoading,
87 isFetchingNextPage: grid.isFetchingNextPage,
88 hasNextPage: grid.hasNextPage,
89 fetchNextPage: grid.fetchNextPage,
90 })
91
92 const onOpenCreateJobSheet = () => {
93 sendEvent({
94 action: 'cron_job_create_clicked',
95 groups: { project: project?.ref ?? 'Unknown', organization: org?.slug ?? 'Unknown' },
96 })
97 setCreateCronJobSheetShown(true)
98 }
99
100 // Row click handler
101 const handleRowClick = (row: CronJob, event: MouseEvent<HTMLDivElement>) => {
102 const { jobid, jobname } = row
103 const url = `/project/${ref}/integrations/cron/jobs/${jobid}?child-label=${encodeURIComponent(
104 jobname || `Job #${jobid}`
105 )}`
106
107 sendEvent({
108 action: 'cron_job_history_clicked',
109 groups: { project: ref ?? 'Unknown', organization: org?.slug ?? 'Unknown' },
110 })
111
112 createNavigationHandler(url, router)(event)
113 }
114
115 const onClose = () => {
116 setCronJobForEditing(null)
117 setCreateCronJobSheetShown(false)
118 cleanPointerEventsNoneOnBody(500)
119 }
120
121 useEffect(() => {
122 if (grid.isSuccess && !!cronJobIdForEditing && !cronJobForEditing) {
123 toast('Cron job not found')
124 setCronJobForEditing(null)
125 }
126 }, [cronJobForEditing, cronJobIdForEditing, grid.isSuccess, setCronJobForEditing])
127
128 return (
129 <>
130 <div className="h-full w-full space-y-4">
131 <div className="h-full w-full flex flex-col relative">
132 <CronJobsTabHeader
133 search={search}
134 isRefreshing={grid.isRefetching && !grid.isFetchingNextPage}
135 onSearchChange={setSearch}
136 onSearchSubmit={handleSearchSubmit}
137 onClearSearch={handleClearSearch}
138 onRefresh={grid.refetch}
139 onCreateJob={onOpenCreateJobSheet}
140 />
141 <LoadingLine loading={grid.isLoading || grid.isRefetching || grid.isFetchingNextPage} />
142 {grid.isMinimal && (
143 <CronJobRunDetailsOverflowNotice
144 queryCost={grid.queryCost}
145 refetchJobs={grid.refetch}
146 />
147 )}
148 <CronJobsTabDataGrid
149 columns={columns}
150 rows={grid.rows}
151 isLoading={grid.isLoading}
152 error={grid.error}
153 searchQuery={searchQuery}
154 onScroll={handleScroll}
155 onRowClick={handleRowClick}
156 />
157 <CronJobsFooter count={count} />
158 </div>
159 </div>
160
161 <DeleteCronJob />
162
163 <CreateCronJobSheet
164 open={!!createCronJobSheetShown || !!cronJobForEditing}
165 selectedCronJob={cronJobForEditing ?? EMPTY_CRON_JOB}
166 onClose={onClose}
167 />
168 </>
169 )
170}
171
172// Footer component for displaying job count
173interface CronJobsFooterProps {
174 count: {
175 value: number | undefined
176 isEstimate: boolean
177 isLoading: boolean
178 }
179}
180
181const CronJobsFooter = ({ count }: CronJobsFooterProps) => (
182 <div className="flex justify-between min-h-9 h-9 overflow-hidden items-center px-6 w-full border-t text-xs text-foreground-light">
183 {count.isLoading ? (
184 <span className="flex items-center gap-2">
185 <Loader2 size={14} className="animate-spin" /> Loading...
186 </span>
187 ) : (
188 `Total: ${count.value ?? 0} jobs${count.isEstimate ? ' (estimate)' : ''}`
189 )}
190 </div>
191)