Files
Danny WhiteandJoshen Lim 8a5ad58f81 chore(studio): replace CloseConfirmationModal with DiscardChangesConfirmationDialog (#43430)
## 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>
2026-03-06 03:17:35 +00:00

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)}
/>
)}
</>
)
}