mirror of
https://github.com/supabase/supabase.git
synced 2026-10-09 11:25:06 +03:00
## What kind of change does this PR introduce? Mechanical cleanup on top of the Button default-variant change (#50160). ## What is the current behavior? Many callsites still pass `variant="default"` even though that is now the component default. ## What is the new behavior? Removes redundant static `variant="default"` from legacy `Button` and `ButtonTooltip` callsites. Keeps explicit defaults where they document the API: - `button-default.tsx` and `button-sizes.tsx` demos - `DocsButton`, which pins neutral styling at the wrapper boundary ## To test Studio: - [Auth → Rate Limits](https://studio-staging-2s957kwc4-supabase.vercel.app/dashboard/project/_/auth/rate-limits): dirty the form so Cancel appears; Cancel stays neutral, Save stays green - [Project Settings → API Keys](https://studio-staging-2s957kwc4-supabase.vercel.app/dashboard/project/_/settings/api-keys): `DocsButton` in the header actions stays neutral Design system: - [Design system → Button](https://design-system-git-dnywh-dc924ac1-supabase.vercel.app/design-system/docs/components/button): `button-default` / `button-sizes` still show explicit default styling; Primary (green) is restricted to the Primary section (and `asChild`) WWW: - [www → Brand assets](https://zone-www-dot-com-git-dnywh-dc924ac1-supabase.vercel.app/brand-assets): Download logo kit / Download button kit stay neutral
245 lines
9.1 KiB
TypeScript
245 lines
9.1 KiB
TypeScript
import dayjs from 'dayjs'
|
|
import { HelpCircle } from 'lucide-react'
|
|
import { useMemo, useState } from 'react'
|
|
import { Calendar, cn } from 'ui'
|
|
|
|
import { type Timezone } from './PITR.types'
|
|
import {
|
|
constrainDateToRange,
|
|
formatNumberToTwoDigits,
|
|
getClientTimezone,
|
|
getDatesBetweenRange,
|
|
} from './PITR.utils'
|
|
import TimeInput from './TimeInput'
|
|
import { TimezoneSelection } from './TimezoneSelection'
|
|
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
|
|
import { FormPanel } from '@/components/ui/Forms/FormPanel'
|
|
import InformationBox from '@/components/ui/InformationBox'
|
|
|
|
type Props = {
|
|
onSubmit: (data: {
|
|
selectedTimezone: Timezone
|
|
recoveryTimeTargetUnix: number
|
|
recoveryTimeString: string
|
|
recoveryTimeStringUtc: string
|
|
}) => void
|
|
earliestAvailableBackupUnix: number
|
|
latestAvailableBackupUnix: number
|
|
disabled?: boolean
|
|
}
|
|
|
|
export function PITRForm({
|
|
onSubmit,
|
|
earliestAvailableBackupUnix,
|
|
latestAvailableBackupUnix,
|
|
disabled = false,
|
|
}: Props) {
|
|
const [selectedTimezone, setSelectedTimezone] = useState<Timezone>(getClientTimezone())
|
|
const earliestAvailableBackup = dayjs
|
|
.unix(earliestAvailableBackupUnix ?? 0)
|
|
.tz(selectedTimezone.utc[0])
|
|
const latestAvailableBackup = dayjs
|
|
.unix(latestAvailableBackupUnix ?? 0)
|
|
.tz(selectedTimezone.utc[0])
|
|
const earliestAvailableBackupAsDate = earliestAvailableBackup.toDate()
|
|
const latestAvailableBackupAsDate = latestAvailableBackup.toDate()
|
|
|
|
const [selectedDateRaw, setSelectedDateRaw] = useState<Date>(latestAvailableBackup.toDate())
|
|
|
|
const selectedDate = dayjs(selectedDateRaw).tz(selectedTimezone.utc[0], true)
|
|
const isSelectedOnEarliestDay = selectedDate.isSame(earliestAvailableBackup, 'day')
|
|
const isSelectedOnLatestDay = selectedDate.isSame(latestAvailableBackup, 'day')
|
|
const availableDates = getDatesBetweenRange(earliestAvailableBackup, latestAvailableBackup)
|
|
|
|
const selectedTime = {
|
|
h: selectedDate.hour(),
|
|
m: selectedDate.minute(),
|
|
s: selectedDate.second(),
|
|
}
|
|
|
|
const earliestAvailableBackupTime = {
|
|
h: earliestAvailableBackup.hour(),
|
|
m: earliestAvailableBackup.minute(),
|
|
s: earliestAvailableBackup.second(),
|
|
}
|
|
|
|
const latestAvailableBackupTime = {
|
|
h: latestAvailableBackup.hour(),
|
|
m: latestAvailableBackup.minute(),
|
|
s: latestAvailableBackup.second(),
|
|
}
|
|
|
|
const recoveryTimeTargetUnix = selectedDate.unix()
|
|
const recoveryTimeString = selectedDate.format('DD MMM YYYY HH:mm:ss')
|
|
const recoveryTimeStringUtc = selectedDate.utc().format('DD MMM YYYY HH:mm:ss')
|
|
|
|
const isWithinRange = useMemo(
|
|
() =>
|
|
(!!selectedDate && selectedDate.isSame(latestAvailableBackup)) ||
|
|
selectedDate.isSame(earliestAvailableBackup) ||
|
|
(selectedDate.isBefore(latestAvailableBackup) &&
|
|
selectedDate.isAfter(earliestAvailableBackup)),
|
|
[selectedDate, latestAvailableBackup, earliestAvailableBackup]
|
|
)
|
|
|
|
const onUpdateDate = (date: Date) => {
|
|
setSelectedDateRaw(
|
|
constrainDateToRange(
|
|
dayjs(date).tz(selectedTimezone.utc[0], true),
|
|
earliestAvailableBackup,
|
|
latestAvailableBackup
|
|
).toDate()
|
|
)
|
|
}
|
|
|
|
const handleSubmit = () => {
|
|
onSubmit({
|
|
selectedTimezone,
|
|
recoveryTimeTargetUnix,
|
|
recoveryTimeString,
|
|
recoveryTimeStringUtc,
|
|
})
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<FormPanel
|
|
disabled={true}
|
|
footer={
|
|
<div className="flex items-center justify-end gap-3 p-6">
|
|
<ButtonTooltip
|
|
disabled={disabled || !selectedDate || !isWithinRange}
|
|
onClick={handleSubmit}
|
|
tooltip={{
|
|
content: {
|
|
hidden: !isWithinRange,
|
|
side: 'bottom',
|
|
text: !isWithinRange
|
|
? 'Selected date is out of range where backups are available'
|
|
: undefined,
|
|
},
|
|
}}
|
|
>
|
|
Continue
|
|
</ButtonTooltip>
|
|
</div>
|
|
}
|
|
>
|
|
<div className="flex flex-col gap-y-6 lg:flex-row lg:gap-y-0 justify-between px-4 md:px-10 py-6 lg:space-x-10">
|
|
<div className="w-full lg:w-1/3 space-y-2 py-2">
|
|
<p className="text-sm text-foreground">Select a date to restore to</p>
|
|
<Calendar
|
|
mode="single"
|
|
required={true}
|
|
selected={selectedDateRaw}
|
|
onSelect={onUpdateDate}
|
|
defaultMonth={latestAvailableBackupAsDate}
|
|
startMonth={earliestAvailableBackupAsDate}
|
|
endMonth={latestAvailableBackupAsDate}
|
|
disabled={[
|
|
{ before: earliestAvailableBackupAsDate },
|
|
{ after: latestAvailableBackupAsDate },
|
|
]}
|
|
classNames={{
|
|
root: 'w-min px-0',
|
|
day: cn(
|
|
'[&:not(:has(:disabled))]:border [&:not(:has(:disabled))]:border-stronger not-last:border-r-0 [&:not(:has(:disabled))]:bg-overlay-hover',
|
|
'rounded-none'
|
|
),
|
|
day_button: 'w-full rounded-none',
|
|
selected: 'bg-brand-500!',
|
|
}}
|
|
/>
|
|
{availableDates.length > 1 && (
|
|
<div className="flex items-center space-x-2">
|
|
<div className="border w-4 h-4 border-stronger bg-overlay-hover" />
|
|
<p className="text-xs text-foreground-light">Point in time back up available</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="w-full lg:w-2/3">
|
|
{!selectedDate ? (
|
|
<div className="h-full flex items-center justify-center">
|
|
<div className="mx-2">
|
|
<InformationBox
|
|
defaultVisibility
|
|
hideCollapse
|
|
icon={<HelpCircle size={14} strokeWidth={2} />}
|
|
title="Select a date which you'd like to restore your database to"
|
|
/>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-8 py-2">
|
|
<div className="flex flex-col gap-y-4">
|
|
<p className="text-sm text-foreground">Enter a time to restore to</p>
|
|
<div className="space-y-1">
|
|
<p className="text-sm text-foreground-light">Time zone</p>
|
|
<div className="w-[350px]">
|
|
<TimezoneSelection
|
|
selectedTimezone={selectedTimezone}
|
|
onSelectTimezone={setSelectedTimezone}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div className="space-y-1">
|
|
<p className="text-sm text-foreground-light">Recovery time</p>
|
|
{isSelectedOnEarliestDay && (
|
|
<p className="text-sm text-foreground-lighter">
|
|
<strong>Earliest backup available for this date</strong>:{' '}
|
|
{earliestAvailableBackup.format('HH:mm:ss')}
|
|
</p>
|
|
)}
|
|
{isSelectedOnLatestDay && (
|
|
<p className="text-sm text-foreground-lighter">
|
|
<strong>Latest backup available for this date</strong>:{' '}
|
|
{latestAvailableBackup.format('HH:mm:ss')}
|
|
</p>
|
|
)}
|
|
<TimeInput
|
|
defaultTime={selectedTime}
|
|
minimumTime={
|
|
isSelectedOnEarliestDay ? earliestAvailableBackupTime : undefined
|
|
}
|
|
maximumTime={isSelectedOnLatestDay ? latestAvailableBackupTime : undefined}
|
|
onChange={({ h, m, s }) => {
|
|
const newDate = dayjs(selectedDateRaw)
|
|
.set('hour', h)
|
|
.set('minute', m)
|
|
.set('second', s)
|
|
|
|
setSelectedDateRaw(newDate.toDate())
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<p className="text-sm text-foreground-light">Database will be restored to:</p>
|
|
<p className="text-3xl">
|
|
<span>{dayjs(selectedDate).format('DD MMM YYYY')}</span>
|
|
<span>
|
|
, {formatNumberToTwoDigits(selectedTime.h)}:
|
|
{formatNumberToTwoDigits(selectedTime.m)}:
|
|
{formatNumberToTwoDigits(selectedTime.s)}
|
|
</span>
|
|
</p>
|
|
|
|
<p className="text-sm text-foreground-lighter mt-4 text-balance">
|
|
Backups are captured every 2 minutes, allowing you to enter a time and restore
|
|
your database to the closest backup point. We'll match the time you enter to the
|
|
closest backup within the 2-minute window
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</FormPanel>
|
|
</div>
|
|
)
|
|
}
|