CronJobsTab.tsx 6.5 KB

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