PreviousRunsTab.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. import { useParams } from 'common'
  2. import dayjs from 'dayjs'
  3. import { CircleCheck, CircleX, Loader } from 'lucide-react'
  4. import { useMemo } from 'react'
  5. import DataGrid, { Column, Row } from 'react-data-grid'
  6. import { cn, LoadingLine, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
  7. import { TimestampInfo } from 'ui-patterns'
  8. import { CodeBlock } from 'ui-patterns/CodeBlock'
  9. import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
  10. import { calculateDuration, formatDate } from './CronJobs.utils'
  11. import CronJobsEmptyState from './CronJobsEmptyState'
  12. import {
  13. CronJobRun,
  14. useCronJobRunsInfiniteQuery,
  15. } from '@/data/database-cron-jobs/database-cron-jobs-runs-infinite-query'
  16. import { useInfiniteScroll } from '@/hooks/misc/useInfiniteScroll'
  17. import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
  18. const cronJobColumns = [
  19. {
  20. id: 'runid',
  21. name: 'RunID',
  22. minWidth: 30,
  23. width: 30,
  24. value: (row: CronJobRun) => (
  25. <div className="flex items-center gap-1.5">
  26. <h3 className="text-xs">{row.runid}</h3>
  27. </div>
  28. ),
  29. },
  30. {
  31. id: 'message',
  32. name: 'Message',
  33. minWidth: 200,
  34. value: (row: CronJobRun) => (
  35. <div className="flex items-center gap-1.5">
  36. {row.return_message ? (
  37. <Tooltip>
  38. <TooltipTrigger asChild>
  39. <span className="text-xs cursor-pointer truncate max-w-[300px]">
  40. {row.return_message}
  41. </span>
  42. </TooltipTrigger>
  43. <TooltipContent
  44. side="bottom"
  45. align="start"
  46. className="min-w-[200px] max-w-[300px] text-wrap p-0"
  47. >
  48. <p className="text-xs font-mono px-2 py-1 border-b bg-surface-100">Message</p>
  49. <CodeBlock
  50. hideLineNumbers
  51. language="sql"
  52. value={row.return_message.trim()}
  53. className={cn(
  54. 'py-0 px-3.5 max-w-full prose dark:prose-dark border-0 rounded-t-none',
  55. '[&>code]:m-0 [&>code>span]:flex [&>code>span]:flex-wrap min-h-11',
  56. '[&>code]:text-xs'
  57. )}
  58. />
  59. </TooltipContent>
  60. </Tooltip>
  61. ) : (
  62. <span>-</span>
  63. )}
  64. </div>
  65. ),
  66. },
  67. {
  68. id: 'status',
  69. name: 'Status',
  70. minWidth: 75,
  71. value: (row: CronJobRun) => <StatusBadge status={row.status} />,
  72. },
  73. {
  74. id: 'start_time',
  75. name: 'Start Time',
  76. minWidth: 120,
  77. value: (row: CronJobRun) => <div className="text-xs">{formatDate(row.start_time)}</div>,
  78. },
  79. {
  80. id: 'end_time',
  81. name: 'End Time',
  82. minWidth: 120,
  83. value: (row: CronJobRun) => (
  84. <div className="flex items-center text-xs">
  85. {row.end_time ? formatDate(row.end_time) : '-'}
  86. </div>
  87. ),
  88. },
  89. {
  90. id: 'duration',
  91. name: 'Duration',
  92. minWidth: 100,
  93. value: (row: CronJobRun) => (
  94. <div className="flex items-center">
  95. <span className="text-xs">
  96. {row.start_time && row.end_time ? calculateDuration(row.start_time, row.end_time) : ''}
  97. </span>
  98. </div>
  99. ),
  100. },
  101. ]
  102. const columns = cronJobColumns.map((col) => {
  103. const result: Column<CronJobRun> = {
  104. key: col.id,
  105. name: col.name,
  106. resizable: true,
  107. minWidth: col.minWidth ?? 120,
  108. headerCellClass: undefined,
  109. renderHeaderCell: () => {
  110. return (
  111. <div
  112. className={cn(
  113. 'flex items-center justify-between font-normal text-xs w-full',
  114. col.id === 'runid' && 'ml-8'
  115. )}
  116. >
  117. <p className="text-foreground!">{col.name}</p>
  118. </div>
  119. )
  120. },
  121. renderCell: (props) => {
  122. const value = col.value(props.row)
  123. if (['start_time', 'end_time'].includes(col.id)) {
  124. const rawValue = (props.row as any)[(col as any).id]
  125. if (rawValue) {
  126. const formattedValue = dayjs(rawValue).valueOf()
  127. return (
  128. <div className="flex items-center">
  129. <TimestampInfo
  130. utcTimestamp={formattedValue}
  131. labelFormat="DD MMM YYYY HH:mm:ss (ZZ)"
  132. className="text-xs"
  133. />
  134. </div>
  135. )
  136. }
  137. }
  138. return value
  139. },
  140. }
  141. return result
  142. })
  143. export const PreviousRunsTab = () => {
  144. const { childId } = useParams()
  145. const { data: project } = useSelectedProjectQuery()
  146. const jobId = Number(childId)
  147. const {
  148. data,
  149. isPending: isLoadingCronJobRuns,
  150. isFetching,
  151. isFetchingNextPage,
  152. hasNextPage,
  153. fetchNextPage,
  154. } = useCronJobRunsInfiniteQuery(
  155. {
  156. projectRef: project?.ref,
  157. connectionString: project?.connectionString,
  158. jobId: jobId,
  159. },
  160. { enabled: !!jobId, staleTime: 30000 }
  161. )
  162. const cronJobRuns = useMemo(() => data?.pages.flatMap((p) => p) || [], [data?.pages])
  163. const handleScroll = useInfiniteScroll({
  164. isLoading: isLoadingCronJobRuns,
  165. isFetchingNextPage,
  166. hasNextPage,
  167. fetchNextPage,
  168. })
  169. return (
  170. <div className="h-full flex flex-col">
  171. <LoadingLine loading={isFetching} />
  172. <DataGrid
  173. className="grow border-t-0"
  174. rowHeight={44}
  175. headerRowHeight={36}
  176. onScroll={handleScroll}
  177. columns={columns}
  178. rows={cronJobRuns ?? []}
  179. rowClass={() => {
  180. return cn(
  181. 'cursor-pointer',
  182. '[&>.rdg-cell]:border-box [&>.rdg-cell]:outline-hidden [&>.rdg-cell]:shadow-none',
  183. '[&>.rdg-cell:first-child>div]:ml-8'
  184. )
  185. }}
  186. renderers={{
  187. renderRow(_idx, props) {
  188. return <Row key={props.row.job_pid} {...props} />
  189. },
  190. noRowsFallback: isLoadingCronJobRuns ? (
  191. <div className="absolute top-14 px-6 w-full">
  192. <GenericSkeletonLoader />
  193. </div>
  194. ) : (
  195. <div className="flex items-center justify-center w-full col-span-6">
  196. <CronJobsEmptyState page="runs" />
  197. </div>
  198. ),
  199. }}
  200. />
  201. </div>
  202. )
  203. }
  204. interface StatusBadgeProps {
  205. status: string
  206. }
  207. function StatusBadge({ status }: StatusBadgeProps) {
  208. if (status === 'succeeded') {
  209. return (
  210. <span className="text-brand-600 flex items-center gap-1 text-xs">
  211. <CircleCheck size={14} /> Succeeded
  212. </span>
  213. )
  214. }
  215. if (status === 'failed') {
  216. return (
  217. <span className="text-destructive flex items-center gap-1 text-xs">
  218. <CircleX size={14} /> Failed
  219. </span>
  220. )
  221. }
  222. if (['running', 'starting', 'sending', 'connecting'].includes(status)) {
  223. return (
  224. <span className="text-_secondary flex items-center gap-1 text-xs">
  225. <Loader size={14} className="animate-spin" /> Running
  226. </span>
  227. )
  228. }
  229. return null
  230. }