mirror of
https://github.com/supabase/supabase.git
synced 2026-10-13 13:19:09 +03:00
## What kind of change does this PR introduce? Form handling improvement. ## What is the current behavior? https://github.com/supabase/supabase/pull/43201/ standardised our discard changes behaviour with a shared hook and `DiscardChangesConfirmationDialog` component. But many forms and sheets still: 1. Don’t have any Discard-confirm close behaviour, making it too easy to make accidental discards 2. Use a more complicated, manually-created `CloseConfirmationModal` approach ## What is the new behavior? - Replaced all instances of `#2` above that had `CloseConfirmationModal` with `DiscardChangesConfirmationDialog` and its hook - Improved design system documentation around dirty form dismissal | Before | After | | --- | --- | | <img width="987" height="569" alt="Mercor Apexroles Foo Supabase-9A40EC7C-F335-4B26-B567-450FC0845463" src="https://github.com/user-attachments/assets/363bed82-34d2-4cc8-9164-6d18cfdbdbbc" /> | <img width="987" height="569" alt="Mercor Apexroles Foo Supabase-F427F1FA-DECC-4194-B663-A9E5A6F285A1" src="https://github.com/user-attachments/assets/d49fafdc-a5c2-46df-9b67-ec42bacbe716" /> | ## To test Try editing values these sheets in staging, then blurring the sheet or pressing `esc`: - CreateQueueSheet.tsx - CronJobsTab.tsx - CronJobPage.tsx - EditWrapperSheet.tsx - OverviewTab.tsx - WrappersTab.tsx - CreateFunction/index.tsx - EditHookPanel.tsx - TriggerSheet.tsx - SidePanelEditor.tsx - EditSecretSheet.tsx - PolicyEditorModal/index.tsx - PolicyEditorPanel/index.tsx ## Still to come - [ ] Incrementally take on `#1`: implement `DiscardChangesConfirmationDialog` and its hook in sheets or dialog forms that have no dirty form dismissal handling --------- Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
192 lines
6.3 KiB
TypeScript
192 lines
6.3 KiB
TypeScript
import { useParams } from 'common'
|
|
import { toString as CronToString } from 'cronstrue'
|
|
import { useCronJobQuery } from 'data/database-cron-jobs/database-cron-job-query'
|
|
import { useEdgeFunctionsQuery } from 'data/edge-functions/edge-functions-query'
|
|
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
|
import { Edit3, List } from 'lucide-react'
|
|
import Link from 'next/link'
|
|
import { useRouter } from 'next/router'
|
|
import { useState } from 'react'
|
|
import {
|
|
BreadcrumbItem_Shadcn_ as BreadcrumbItem,
|
|
BreadcrumbLink_Shadcn_ as BreadcrumbLink,
|
|
BreadcrumbList_Shadcn_ as BreadcrumbList,
|
|
BreadcrumbPage_Shadcn_ as BreadcrumbPage,
|
|
BreadcrumbSeparator_Shadcn_ as BreadcrumbSeparator,
|
|
Button,
|
|
cn,
|
|
CodeBlock,
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipTrigger,
|
|
} from 'ui'
|
|
import {
|
|
PageHeader,
|
|
PageHeaderAside,
|
|
PageHeaderBreadcrumb,
|
|
PageHeaderDescription,
|
|
PageHeaderMeta,
|
|
PageHeaderSummary,
|
|
PageHeaderTitle,
|
|
} from 'ui-patterns/PageHeader'
|
|
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
|
|
|
|
import { CreateCronJobSheet } from './CreateCronJobSheet/CreateCronJobSheet'
|
|
import { isSecondsFormat, parseCronJobCommand } from './CronJobs.utils'
|
|
import { PreviousRunsTab } from './PreviousRunsTab'
|
|
|
|
export const CronJobPage = () => {
|
|
const router = useRouter()
|
|
const { ref, id, pageId, childId } = useParams()
|
|
const childLabel = router?.query?.['child-label'] as string
|
|
const { data: project } = useSelectedProjectQuery()
|
|
|
|
const [isEditSheetOpen, setIsEditSheetOpen] = useState(false)
|
|
|
|
const jobId = Number(childId)
|
|
|
|
const { data: job, isPending: isLoading } = useCronJobQuery({
|
|
projectRef: project?.ref,
|
|
connectionString: project?.connectionString,
|
|
id: jobId,
|
|
})
|
|
|
|
const { data: edgeFunctions = [] } = useEdgeFunctionsQuery({ projectRef: project?.ref })
|
|
|
|
// Parse the cron job command to check if it's an edge function
|
|
const cronJobValues = parseCronJobCommand(job?.command || '', project?.ref!)
|
|
const edgeFunction =
|
|
cronJobValues.type === 'edge_function' ? cronJobValues.edgeFunctionName : undefined
|
|
const edgeFunctionSlug = edgeFunction?.split('/functions/v1/').pop()
|
|
const isValidEdgeFunction = edgeFunctions.some((x) => x.slug === edgeFunctionSlug)
|
|
|
|
const pageTitle = childLabel || childId || 'Cron Job'
|
|
|
|
const pageSubtitle = job ? (
|
|
<div className="text-sm text-foreground-light">
|
|
Running{' '}
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<span className="cursor-pointer underline decoration-dotted lowercase">
|
|
{isSecondsFormat(job.schedule)
|
|
? job.schedule.toLowerCase()
|
|
: CronToString(job.schedule.toLowerCase())}
|
|
</span>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="bottom" align="center">
|
|
<div className="text-xs">
|
|
<p className="font-mono mb-1">{job.schedule.toLowerCase()}</p>
|
|
{!isSecondsFormat(job.schedule) && (
|
|
<p className="text-foreground-light">{CronToString(job.schedule.toLowerCase())}</p>
|
|
)}
|
|
</div>
|
|
</TooltipContent>
|
|
</Tooltip>{' '}
|
|
with command{' '}
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<code className="text-code-inline max-w-[200px] inline-block truncate align-bottom cursor-pointer">
|
|
{job.command}
|
|
</code>
|
|
</TooltipTrigger>
|
|
<TooltipContent
|
|
side="bottom"
|
|
align="center"
|
|
className="min-w-[200px] max-w-[400px] text-wrap p-0"
|
|
>
|
|
<p className="text-xs font-mono px-2 py-1 border-b bg-surface-100">Command</p>
|
|
<CodeBlock
|
|
hideLineNumbers
|
|
language="sql"
|
|
value={job.command.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>
|
|
</div>
|
|
) : null
|
|
|
|
// Secondary actions
|
|
const secondaryActions = [
|
|
<Button
|
|
key="edit"
|
|
type="outline"
|
|
icon={<Edit3 strokeWidth={1.5} size="14" />}
|
|
onClick={() => setIsEditSheetOpen(true)}
|
|
>
|
|
Edit
|
|
</Button>,
|
|
<Button key="view-logs" asChild type="outline" icon={<List strokeWidth={1.5} size="14" />}>
|
|
<Link
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
href={`/project/${project?.ref}/logs/pgcron-logs/`}
|
|
>
|
|
View Cron logs
|
|
</Link>
|
|
</Button>,
|
|
...(isValidEdgeFunction
|
|
? [
|
|
<Button key="view-edge-logs" asChild type="outline">
|
|
<Link
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
href={`/project/${project?.ref}/functions/${edgeFunctionSlug}/logs`}
|
|
>
|
|
View Edge Function logs
|
|
</Link>
|
|
</Button>,
|
|
]
|
|
: []),
|
|
]
|
|
|
|
return (
|
|
<>
|
|
<PageHeader size="full" className="pb-6">
|
|
<PageHeaderBreadcrumb>
|
|
<BreadcrumbList>
|
|
<BreadcrumbItem>
|
|
<BreadcrumbLink asChild>
|
|
<Link href={`/project/${ref}/integrations`}>Integrations</Link>
|
|
</BreadcrumbLink>
|
|
</BreadcrumbItem>
|
|
<BreadcrumbSeparator />
|
|
<BreadcrumbItem>
|
|
<BreadcrumbLink asChild>
|
|
<Link href={`/project/${ref}/integrations/${id}/${pageId}`}>Cron</Link>
|
|
</BreadcrumbLink>
|
|
</BreadcrumbItem>
|
|
<BreadcrumbSeparator />
|
|
<BreadcrumbItem>
|
|
<BreadcrumbPage>{childLabel ?? job?.jobname ?? 'Cron Job'}</BreadcrumbPage>
|
|
</BreadcrumbItem>
|
|
</BreadcrumbList>
|
|
</PageHeaderBreadcrumb>
|
|
<PageHeaderMeta>
|
|
<PageHeaderSummary>
|
|
<PageHeaderTitle>{pageTitle}</PageHeaderTitle>
|
|
<PageHeaderDescription>
|
|
{isLoading ? <ShimmeringLoader className="py-0 h-[20px] w-96" /> : pageSubtitle}
|
|
</PageHeaderDescription>
|
|
</PageHeaderSummary>
|
|
{secondaryActions.length > 0 && <PageHeaderAside>{secondaryActions}</PageHeaderAside>}
|
|
</PageHeaderMeta>
|
|
</PageHeader>
|
|
|
|
<PreviousRunsTab />
|
|
|
|
{job && (
|
|
<CreateCronJobSheet
|
|
open={isEditSheetOpen}
|
|
selectedCronJob={job}
|
|
onClose={() => setIsEditSheetOpen(false)}
|
|
/>
|
|
)}
|
|
</>
|
|
)
|
|
}
|