Files
Joshen Lim 09a1f431bc Mass update for all lucide icon imports to use the component with Icon suffix (#51659)
## Context

This is largely just a mechanical change here:
Opting to use lucide icons with the `Icon` suffix across all icon
imports within `studio`
- so that it’s clearer which components are icons + reduces the
possibility of name collision with our own components
- e.g <User /> vs <UserIcon />

Also added a eslint rule for this:
<img width="1069" height="167" alt="image"
src="https://github.com/user-attachments/assets/7f36f443-5ca2-4bf6-8d22-86e774513bfc"
/>

And updated `AGENTS.md` as such:
```
Icons come from `lucide-react`, always imported with the `Icon` suffix (`import { CheckIcon, Trash2Icon } from 'lucide-react'`) — never the bare names (`Check`) or `Check as CheckIcon` aliases (lint-enforced).
```
2026-10-14 12:56:55 +02:00

246 lines
9.1 KiB
TypeScript

import dayjs from 'dayjs'
import { HelpCircleIcon } from 'lucide-react'
import { useMemo, useState } from 'react'
import { Calendar, cn } from 'ui'
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'
import type { Timezone } from '@/lib/constants/timezones'
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={<HelpCircleIcon 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}
date={selectedDate}
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>
)
}