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