| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248 |
- import { useParams } from 'common'
- import dayjs from 'dayjs'
- import { CircleCheck, CircleX, Loader } from 'lucide-react'
- import { useMemo } from 'react'
- import DataGrid, { Column, Row } from 'react-data-grid'
- import { cn, LoadingLine, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
- import { TimestampInfo } from 'ui-patterns'
- import { CodeBlock } from 'ui-patterns/CodeBlock'
- import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
- import { calculateDuration, formatDate } from './CronJobs.utils'
- import CronJobsEmptyState from './CronJobsEmptyState'
- import {
- CronJobRun,
- useCronJobRunsInfiniteQuery,
- } from '@/data/database-cron-jobs/database-cron-jobs-runs-infinite-query'
- import { useInfiniteScroll } from '@/hooks/misc/useInfiniteScroll'
- import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
- const cronJobColumns = [
- {
- id: 'runid',
- name: 'RunID',
- minWidth: 30,
- width: 30,
- value: (row: CronJobRun) => (
- <div className="flex items-center gap-1.5">
- <h3 className="text-xs">{row.runid}</h3>
- </div>
- ),
- },
- {
- id: 'message',
- name: 'Message',
- minWidth: 200,
- value: (row: CronJobRun) => (
- <div className="flex items-center gap-1.5">
- {row.return_message ? (
- <Tooltip>
- <TooltipTrigger asChild>
- <span className="text-xs cursor-pointer truncate max-w-[300px]">
- {row.return_message}
- </span>
- </TooltipTrigger>
- <TooltipContent
- side="bottom"
- align="start"
- className="min-w-[200px] max-w-[300px] text-wrap p-0"
- >
- <p className="text-xs font-mono px-2 py-1 border-b bg-surface-100">Message</p>
- <CodeBlock
- hideLineNumbers
- language="sql"
- value={row.return_message.trim()}
- className={cn(
- 'py-0 px-3.5 max-w-full prose dark:prose-dark border-0 rounded-t-none',
- '[&>code]:m-0 [&>code>span]:flex [&>code>span]:flex-wrap min-h-11',
- '[&>code]:text-xs'
- )}
- />
- </TooltipContent>
- </Tooltip>
- ) : (
- <span>-</span>
- )}
- </div>
- ),
- },
- {
- id: 'status',
- name: 'Status',
- minWidth: 75,
- value: (row: CronJobRun) => <StatusBadge status={row.status} />,
- },
- {
- id: 'start_time',
- name: 'Start Time',
- minWidth: 120,
- value: (row: CronJobRun) => <div className="text-xs">{formatDate(row.start_time)}</div>,
- },
- {
- id: 'end_time',
- name: 'End Time',
- minWidth: 120,
- value: (row: CronJobRun) => (
- <div className="flex items-center text-xs">
- {row.end_time ? formatDate(row.end_time) : '-'}
- </div>
- ),
- },
- {
- id: 'duration',
- name: 'Duration',
- minWidth: 100,
- value: (row: CronJobRun) => (
- <div className="flex items-center">
- <span className="text-xs">
- {row.start_time && row.end_time ? calculateDuration(row.start_time, row.end_time) : ''}
- </span>
- </div>
- ),
- },
- ]
- const columns = cronJobColumns.map((col) => {
- const result: Column<CronJobRun> = {
- key: col.id,
- name: col.name,
- resizable: true,
- minWidth: col.minWidth ?? 120,
- headerCellClass: undefined,
- renderHeaderCell: () => {
- return (
- <div
- className={cn(
- 'flex items-center justify-between font-normal text-xs w-full',
- col.id === 'runid' && 'ml-8'
- )}
- >
- <p className="text-foreground!">{col.name}</p>
- </div>
- )
- },
- renderCell: (props) => {
- const value = col.value(props.row)
- if (['start_time', 'end_time'].includes(col.id)) {
- const rawValue = (props.row as any)[(col as any).id]
- if (rawValue) {
- const formattedValue = dayjs(rawValue).valueOf()
- return (
- <div className="flex items-center">
- <TimestampInfo
- utcTimestamp={formattedValue}
- labelFormat="DD MMM YYYY HH:mm:ss (ZZ)"
- className="text-xs"
- />
- </div>
- )
- }
- }
- return value
- },
- }
- return result
- })
- export const PreviousRunsTab = () => {
- const { childId } = useParams()
- const { data: project } = useSelectedProjectQuery()
- const jobId = Number(childId)
- const {
- data,
- isPending: isLoadingCronJobRuns,
- isFetching,
- isFetchingNextPage,
- hasNextPage,
- fetchNextPage,
- } = useCronJobRunsInfiniteQuery(
- {
- projectRef: project?.ref,
- connectionString: project?.connectionString,
- jobId: jobId,
- },
- { enabled: !!jobId, staleTime: 30000 }
- )
- const cronJobRuns = useMemo(() => data?.pages.flatMap((p) => p) || [], [data?.pages])
- const handleScroll = useInfiniteScroll({
- isLoading: isLoadingCronJobRuns,
- isFetchingNextPage,
- hasNextPage,
- fetchNextPage,
- })
- return (
- <div className="h-full flex flex-col">
- <LoadingLine loading={isFetching} />
- <DataGrid
- className="grow border-t-0"
- rowHeight={44}
- headerRowHeight={36}
- onScroll={handleScroll}
- columns={columns}
- rows={cronJobRuns ?? []}
- rowClass={() => {
- return cn(
- 'cursor-pointer',
- '[&>.rdg-cell]:border-box [&>.rdg-cell]:outline-hidden [&>.rdg-cell]:shadow-none',
- '[&>.rdg-cell:first-child>div]:ml-8'
- )
- }}
- renderers={{
- renderRow(_idx, props) {
- return <Row key={props.row.job_pid} {...props} />
- },
- noRowsFallback: isLoadingCronJobRuns ? (
- <div className="absolute top-14 px-6 w-full">
- <GenericSkeletonLoader />
- </div>
- ) : (
- <div className="flex items-center justify-center w-full col-span-6">
- <CronJobsEmptyState page="runs" />
- </div>
- ),
- }}
- />
- </div>
- )
- }
- interface StatusBadgeProps {
- status: string
- }
- function StatusBadge({ status }: StatusBadgeProps) {
- if (status === 'succeeded') {
- return (
- <span className="text-brand-600 flex items-center gap-1 text-xs">
- <CircleCheck size={14} /> Succeeded
- </span>
- )
- }
- if (status === 'failed') {
- return (
- <span className="text-destructive flex items-center gap-1 text-xs">
- <CircleX size={14} /> Failed
- </span>
- )
- }
- if (['running', 'starting', 'sending', 'connecting'].includes(status)) {
- return (
- <span className="text-_secondary flex items-center gap-1 text-xs">
- <Loader size={14} className="animate-spin" /> Running
- </span>
- )
- }
- return null
- }
|