forked from supabase/supabase
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Cron tab for previous runs (supabase#30690)
* Redo cron ui to use a data table * Add component for form header * Add next run column * add cron jobs page * Load jobs from url, add runs footer, link to logs * Type issue * Fix height of tab area * Use nuqs with history * improve pgcron logs, add severity filters * Check for v1.5 to see if seconds are supported * fix cron jobs logs table name * Add type to the table * Move expression warning to own function * Fit to new layout * Types * Fix long code blocks * Revert some of the changes. * Use job name as a tab name. Other minor fixes. * Use Infinite query for the cron runs. * Revert some extra changes. Will be added to another PR. * Rename pg functions --------- Co-authored-by: Terry Sutton <[email protected]> Co-authored-by: Jordi Enric <[email protected]>
- Loading branch information
1 parent
2195581
commit 492c81c
Showing
19 changed files
with
493 additions
and
46 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
14 changes: 14 additions & 0 deletions
14
apps/studio/components/interfaces/Integrations/CronJobs/CronJobsEmptyState.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
export default function CronJobsEmptyState({ page }: { page: string }) { | ||
return ( | ||
<div className=" text-center h-full w-full items-center justify-center rounded-md px-4 py-12 "> | ||
<p className="text-sm text-foreground"> | ||
{page === 'jobs' ? 'No cron jobs created yet' : 'No runs for this cron job yet'} | ||
</p> | ||
<p className="text-sm text-foreground-lighter"> | ||
{page === 'jobs' | ||
? 'Create one by clicking "Create a new cron job"' | ||
: 'Check the schedule of your cron jobs to see when they run'} | ||
</p> | ||
</div> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
283 changes: 283 additions & 0 deletions
283
apps/studio/components/interfaces/Integrations/CronJobs/PreviousRunsTab.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,283 @@ | ||
import { toString as CronToString } from 'cronstrue' | ||
import { List } from 'lucide-react' | ||
import Link from 'next/link' | ||
import { UIEvent, useCallback, useMemo } from 'react' | ||
import DataGrid, { Column, Row } from 'react-data-grid' | ||
|
||
import { useParams } from 'common' | ||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' | ||
import { useCronJobsQuery } from 'data/database-cron-jobs/database-cron-jobs-query' | ||
import { | ||
CronJobRun, | ||
useCronJobRunsInfiniteQuery, | ||
} from 'data/database-cron-jobs/database-cron-jobs-runs-infinite-query' | ||
import { | ||
Badge, | ||
Button, | ||
cn, | ||
LoadingLine, | ||
SimpleCodeBlock, | ||
Tooltip_Shadcn_, | ||
TooltipContent_Shadcn_, | ||
TooltipTrigger_Shadcn_, | ||
} from 'ui' | ||
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' | ||
import { calculateDuration, formatDate, isSecondsFormat } from './CronJobs.utils' | ||
import CronJobsEmptyState from './CronJobsEmptyState' | ||
|
||
const cronJobColumns = [ | ||
{ | ||
id: 'runid', | ||
name: 'RunID', | ||
minWidth: 60, | ||
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"> | ||
<Tooltip_Shadcn_> | ||
<TooltipTrigger_Shadcn_ asChild> | ||
<span className="text-xs cursor-pointer truncate max-w-[300px]"> | ||
{row.return_message} | ||
</span> | ||
</TooltipTrigger_Shadcn_> | ||
<TooltipContent_Shadcn_ side="bottom" align="center" className="max-w-[300px] text-wrap"> | ||
<SimpleCodeBlock | ||
showCopy={true} | ||
className="sql" | ||
parentClassName="!p-0 [&>div>span]:text-xs" | ||
> | ||
{row.return_message} | ||
</SimpleCodeBlock> | ||
</TooltipContent_Shadcn_> | ||
</Tooltip_Shadcn_> | ||
</div> | ||
), | ||
}, | ||
|
||
{ | ||
id: 'status', | ||
name: 'Status', | ||
minWidth: 75, | ||
value: (row: CronJobRun) => ( | ||
<Badge variant={row.status === 'succeeded' ? 'success' : 'warning'}>{row.status}</Badge> | ||
), | ||
}, | ||
{ | ||
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="text-xs">{formatDate(row.end_time)}</div>, | ||
}, | ||
|
||
{ | ||
id: 'duration', | ||
name: 'Duration', | ||
minWidth: 100, | ||
value: (row: CronJobRun) => ( | ||
<div className="text-xs">{calculateDuration(row.start_time, row.end_time)}</div> | ||
), | ||
}, | ||
] | ||
|
||
const columns = cronJobColumns.map((col) => { | ||
const result: Column<CronJobRun> = { | ||
key: col.id, | ||
name: col.name, | ||
resizable: true, | ||
minWidth: col.minWidth ?? 120, | ||
headerCellClass: 'first:pl-6 cursor-pointer', | ||
renderHeaderCell: () => { | ||
return ( | ||
<div className="flex items-center justify-between font-mono font-normal text-xs w-full"> | ||
<div className="flex items-center gap-x-2"> | ||
<p className="!text-foreground">{col.name}</p> | ||
</div> | ||
</div> | ||
) | ||
}, | ||
renderCell: (props) => { | ||
const value = col.value(props.row) | ||
|
||
return ( | ||
<div | ||
className={cn( | ||
'w-full flex flex-col justify-center font-mono text-xs', | ||
typeof value === 'number' ? 'text-right' : '' | ||
)} | ||
> | ||
<span>{value}</span> | ||
</div> | ||
) | ||
}, | ||
} | ||
return result | ||
}) | ||
|
||
function isAtBottom({ currentTarget }: UIEvent<HTMLDivElement>): boolean { | ||
return currentTarget.scrollTop + 10 >= currentTarget.scrollHeight - currentTarget.clientHeight | ||
} | ||
|
||
export const PreviousRunsTab = () => { | ||
const { childId: jobName } = useParams() | ||
const { project } = useProjectContext() | ||
|
||
const { data: cronJobs, isLoading: isLoadingCronJobs } = useCronJobsQuery({ | ||
projectRef: project?.ref, | ||
connectionString: project?.connectionString, | ||
}) | ||
|
||
const currentJobState = cronJobs?.find((job) => job.jobname === jobName) | ||
|
||
const { | ||
data, | ||
isLoading: isLoadingCronJobRuns, | ||
fetchNextPage, | ||
isFetching, | ||
} = useCronJobRunsInfiniteQuery( | ||
{ | ||
projectRef: project?.ref, | ||
connectionString: project?.connectionString, | ||
jobId: Number(currentJobState?.jobid), | ||
}, | ||
{ enabled: !!currentJobState?.jobid, staleTime: 30 } | ||
) | ||
|
||
const handleScroll = useCallback( | ||
(event: UIEvent<HTMLDivElement>) => { | ||
if (isLoadingCronJobRuns || !isAtBottom(event)) return | ||
// the cancelRefetch is to prevent the query from being refetched when the user scrolls back up and down again, | ||
// resulting in multiple fetchNextPage calls | ||
fetchNextPage({ cancelRefetch: false }) | ||
}, | ||
[fetchNextPage, isLoadingCronJobRuns] | ||
) | ||
|
||
const cronJobRuns = useMemo(() => data?.pages.flatMap((p) => p) || [], [data?.pages]) | ||
|
||
return ( | ||
<div className="h-full flex flex-col"> | ||
<div className="mt-4 h-full"> | ||
<LoadingLine loading={isFetching} /> | ||
<DataGrid | ||
className="flex-grow h-full" | ||
rowHeight={44} | ||
headerRowHeight={36} | ||
onScroll={handleScroll} | ||
columns={columns} | ||
rows={cronJobRuns ?? []} | ||
rowClass={() => { | ||
const isSelected = false | ||
return cn([ | ||
`${isSelected ? 'bg-surface-300 dark:bg-surface-300' : 'bg-200'} `, | ||
`${isSelected ? '[&>div:first-child]:border-l-4 border-l-secondary [&>div]:border-l-foreground' : ''}`, | ||
'[&>.rdg-cell]:border-box [&>.rdg-cell]:outline-none [&>.rdg-cell]:shadow-none', | ||
'[&>.rdg-cell:first-child>div]:ml-4', | ||
]) | ||
}} | ||
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> | ||
|
||
<div className="px-6 py-6 flex gap-12 border-t"> | ||
{isLoadingCronJobs ? ( | ||
<GenericSkeletonLoader /> | ||
) : ( | ||
<> | ||
<div className="grid gap-2 w-56"> | ||
<h3 className="text-sm">Schedule</h3> | ||
<p className="text-xs text-foreground-light"> | ||
{currentJobState?.schedule ? ( | ||
<> | ||
<span className="font-mono text-lg">{currentJobState.schedule}</span> | ||
<p> | ||
{isSecondsFormat(currentJobState.schedule) | ||
? '' | ||
: CronToString(currentJobState.schedule.toLowerCase())} | ||
</p> | ||
</> | ||
) : ( | ||
<span>Loading schedule...</span> | ||
)} | ||
</p> | ||
</div> | ||
|
||
<div className="grid gap-2"> | ||
<h3 className="text-sm">Command</h3> | ||
<Tooltip_Shadcn_> | ||
<TooltipTrigger_Shadcn_ className=" text-left p-0! cursor-pointer truncate max-w-[300px] h-12 relative"> | ||
<SimpleCodeBlock | ||
showCopy={false} | ||
className="sql" | ||
parentClassName=" [&>div>span]:text-xs bg-alternative-200 !p-2 rounded-md" | ||
> | ||
{currentJobState?.command} | ||
</SimpleCodeBlock> | ||
<div className="bottom-0 left-0 right-0 h-8 bg-gradient-to-t from-background-200 to-transparent absolute " /> | ||
</TooltipTrigger_Shadcn_> | ||
<TooltipContent_Shadcn_ | ||
side="bottom" | ||
align="center" | ||
className="max-w-[400px] text-wrap" | ||
> | ||
<SimpleCodeBlock | ||
showCopy={false} | ||
className="sql" | ||
parentClassName=" [&>div>span]:text-xs bg-alternative-200 !p-2 rounded-md" | ||
> | ||
{currentJobState?.command} | ||
</SimpleCodeBlock> | ||
</TooltipContent_Shadcn_> | ||
</Tooltip_Shadcn_> | ||
{/* <div className="text-xs text-foreground-light"> | ||
<SimpleCodeBlock | ||
showCopy={false} | ||
className="sql" | ||
parentClassName=" [&>div>span]:text-xs bg-alternative-200 !p-2 rounded-md" | ||
> | ||
{currentJobState?.command} | ||
</SimpleCodeBlock> | ||
</div> */} | ||
</div> | ||
|
||
<div className="grid gap-2"> | ||
<h3 className="text-sm">Explore</h3> | ||
<Button asChild type="outline" icon={<List strokeWidth={1.5} size="14" />}> | ||
{/* [Terry] need to link to the exact jobid, but not currently supported */} | ||
<Link target="_blank" href={`/project/${project?.ref}/logs/pgcron-logs/`}> | ||
View logs | ||
</Link> | ||
</Button> | ||
</div> | ||
</> | ||
)} | ||
</div> | ||
</div> | ||
) | ||
} |
Oops, something went wrong.