diff --git a/apps/studio/components/interfaces/Database/Backups/DatabaseBackupsNav.tsx b/apps/studio/components/interfaces/Database/Backups/DatabaseBackupsNav.tsx index f0fe0b1d5e4..70a5894d1ff 100644 --- a/apps/studio/components/interfaces/Database/Backups/DatabaseBackupsNav.tsx +++ b/apps/studio/components/interfaces/Database/Backups/DatabaseBackupsNav.tsx @@ -1,36 +1,56 @@ +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import { useFlag } from 'hooks/ui/useFlag' import Link from 'next/link' import React from 'react' -import { useParams } from 'common' -import { NavMenu, NavMenuItem } from 'ui' +import { Badge, NavMenu, NavMenuItem } from 'ui' type Props = { - active: 'scheduled' | 'pitr' + active: 'pitr' | 'scheduled' | 'rtnp' } function DatabaseBackupsNav({ active }: Props) { - const { ref } = useParams() + const isCloneToNewProjectEnabled = useFlag('clonetonewproject') + const ref = useProjectContext()?.project?.ref const navMenuItems = [ { + enabled: true, id: 'scheduled', label: 'Scheduled backups', href: `/project/${ref}/database/backups/scheduled`, }, { + enabled: true, id: 'pitr', label: 'Point in time', href: `/project/${ref}/database/backups/pitr`, }, - ] + { + enabled: isCloneToNewProjectEnabled, + id: 'rtnp', + label: ( +
+ Restore to new project{' '} + + New + +
+ ), + href: `/project/${ref}/database/backups/restore-to-new-project`, + }, + ] as const return ( - {navMenuItems.map((item) => ( - - {item.label} - - ))} + {navMenuItems.map( + (item) => + item.enabled && ( + + {item.label} + + ) + )} ) } diff --git a/apps/studio/components/interfaces/Database/Backups/PITR/PITRSelection.tsx b/apps/studio/components/interfaces/Database/Backups/PITR/PITRSelection.tsx index c5af3d64a9c..bfc8fb9a35b 100644 --- a/apps/studio/components/interfaces/Database/Backups/PITR/PITRSelection.tsx +++ b/apps/studio/components/interfaces/Database/Backups/PITR/PITRSelection.tsx @@ -1,23 +1,14 @@ -import { PermissionAction } from '@supabase/shared-types/out/constants' import { useQueryClient } from '@tanstack/react-query' -import { format } from 'date-fns' -import dayjs from 'dayjs' -import { ChevronLeft, ChevronRight, HelpCircle } from 'lucide-react' import Link from 'next/link' import { useRouter } from 'next/router' import { useState } from 'react' -import DatePicker from 'react-datepicker' import { useParams } from 'common' -import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { FormHeader } from 'components/ui/Forms/FormHeader' -import { FormPanel } from 'components/ui/Forms/FormPanel' -import InformationBox from 'components/ui/InformationBox' import { useBackupsQuery } from 'data/database/backups-query' import { usePitrRestoreMutation } from 'data/database/pitr-restore-mutation' import { setProjectStatus } from 'data/projects/projects-query' import { useReadReplicasQuery } from 'data/read-replicas/replicas-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { PROJECT_STATUS } from 'lib/constants' import { Alert, @@ -31,15 +22,9 @@ import { import BackupsEmpty from '../BackupsEmpty' import BackupsStorageAlert from '../BackupsStorageAlert' import type { Timezone } from './PITR.types' -import { - constrainDateToRange, - formatNumberToTwoDigits, - getClientTimezone, - getDatesBetweenRange, -} from './PITR.utils' +import { getClientTimezone } from './PITR.utils' import PITRStatus from './PITRStatus' -import TimeInput from './TimeInput' -import { TimezoneSelection } from './TimezoneSelection' +import { PITRForm } from './pitr-form' const PITRSelection = () => { const router = useRouter() @@ -51,11 +36,11 @@ const PITRSelection = () => { const [showConfiguration, setShowConfiguration] = useState(false) const [showConfirmation, setShowConfirmation] = useState(false) const [selectedTimezone, setSelectedTimezone] = useState(getClientTimezone()) - - const canTriggerPhysicalBackups = useCheckPermissions( - PermissionAction.INFRA_EXECUTE, - 'queue_job.restore.prepare' - ) + const [selectedRecoveryPoint, setSelectedRecoveryPoint] = useState<{ + recoveryTimeTargetUnix: number + recoveryTimeString: string + recoveryTimeStringUtc: string + }>() const hasReadReplicas = (databases ?? []).length > 1 @@ -76,67 +61,16 @@ const PITRSelection = () => { const { earliestPhysicalBackupDateUnix, latestPhysicalBackupDateUnix } = backups?.physicalBackupData ?? {} const hasNoBackupsAvailable = !earliestPhysicalBackupDateUnix || !latestPhysicalBackupDateUnix - const earliestAvailableBackup = dayjs - .unix(earliestPhysicalBackupDateUnix ?? 0) - .tz(selectedTimezone.utc[0]) - const latestAvailableBackup = dayjs - .unix(latestPhysicalBackupDateUnix ?? 0) - .tz(selectedTimezone.utc[0]) - - const [selectedDateRaw, setSelectedDateRaw] = useState(latestAvailableBackup.toDate()) - const selectedDate = dayjs(selectedDateRaw).tz(selectedTimezone.utc[0], true) // true to keep local time and just change +whatever - - 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(), - } - - // This will be the actual unix timestamp for the backup - const recoveryTimeTargetUnix = selectedDate.unix() - // Formatting from the unix again just to double check correctness - const recoveryTimeString = selectedDate.format('DD MMM YYYY HH:mm:ss') - const recoveryTimeStringUtc = selectedDate.utc().format('DD MMM YYYY HH:mm:ss') - - const isSelectedOutOfRange = - selectedDate && - (selectedDate.isBefore(earliestAvailableBackup) || selectedDate.isAfter(latestAvailableBackup)) - - const onUpdateDate = (date: Date) => { - setSelectedDateRaw( - constrainDateToRange( - dayjs(date).tz(selectedTimezone.utc[0], true), - earliestAvailableBackup, - latestAvailableBackup - ).toDate() - ) - } - - const onCancel = () => { - setShowConfiguration(false) - setSelectedTimezone(getClientTimezone()) - } const onConfirmRestore = async () => { if (!ref) return console.error('Project ref is required') - if (!recoveryTimeTargetUnix) return console.error('Recovery time target unix is required') - restoreFromPitr({ ref, recovery_time_target_unix: recoveryTimeTargetUnix }) + if (!selectedRecoveryPoint?.recoveryTimeTargetUnix) + return console.error('Recovery time target unix is required') + + restoreFromPitr({ + ref, + recovery_time_target_unix: selectedRecoveryPoint.recoveryTimeTargetUnix, + }) } return ( @@ -177,173 +111,14 @@ const PITRSelection = () => { onSetConfiguration={() => setShowConfiguration(true)} /> ) : ( - - - - setShowConfirmation(true)} - tooltip={{ - content: { - side: 'bottom', - text: isSelectedOutOfRange - ? 'Selected date is out of range where backups are available' - : !canTriggerPhysicalBackups - ? 'You need additional permissions to trigger a restore' - : undefined, - }, - }} - > - Review restore details - - - } - > -
-
- 'cursor-pointer'} - minDate={earliestAvailableBackup.toDate()} - maxDate={latestAvailableBackup.toDate()} - highlightDates={availableDates.map((date) => date.toDate())} - renderCustomHeader={({ - date, - decreaseMonth, - increaseMonth, - prevMonthButtonDisabled, - nextMonthButtonDisabled, - }) => ( -
-
- - - {format(date, 'MMMM yyyy')} - - -
-
- )} - /> -
-
-

Point in time back up available

-
-
- -
- {!selectedDate ? ( -
-
- } - title="Select a date which you'd like to restore your database to" - /> -
-
- ) : ( -
-
-

Restore database to

-

- {dayjs(selectedDate).format('DD MMM YYYY')} - - , {formatNumberToTwoDigits(selectedTime.h)}: - {formatNumberToTwoDigits(selectedTime.m)}: - {formatNumberToTwoDigits(selectedTime.s)} - -

-
-
-
-

Time zone

-
- -
-
-
-
-

Recovery time

- { - const newDate = dayjs(selectedDateRaw) - .set('hour', h) - .set('minute', m) - .set('second', s) - - setSelectedDateRaw(newDate.toDate()) - }} - /> -
- -

- Enter a time within the available range to restore from.
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 -

-
-
-

- {isSelectedOnEarliestDay && ( -

- Earliest backup available for this date:{' '} - {earliestAvailableBackup.format('HH:mm:ss')} -

- )} - {isSelectedOnLatestDay && ( -

- Latest backup available for this date:{' '} - {latestAvailableBackup.format('HH:mm:ss')} -

- )} -
-
-
- )} -
-
- + { + setSelectedRecoveryPoint(recoveryPoint) + setShowConfirmation(true) + }} + /> )} )} @@ -379,12 +154,12 @@ const PITRSelection = () => {
-

{selectedTimezone?.text}

-

{recoveryTimeString}

+

Local Time

+

{selectedRecoveryPoint?.recoveryTimeString}

(UTC+00:00)

-

{recoveryTimeStringUtc}

+

{selectedRecoveryPoint?.recoveryTimeStringUtc}

diff --git a/apps/studio/components/interfaces/Database/Backups/PITR/TimezoneSelection.tsx b/apps/studio/components/interfaces/Database/Backups/PITR/TimezoneSelection.tsx index d8b943ae0bb..d527da4b6d4 100644 --- a/apps/studio/components/interfaces/Database/Backups/PITR/TimezoneSelection.tsx +++ b/apps/studio/components/interfaces/Database/Backups/PITR/TimezoneSelection.tsx @@ -61,7 +61,7 @@ export const TimezoneSelection = ({ value={option} onSelect={(text) => { const selectedTimezone = ALL_TIMEZONES.find( - (option) => option.text.toLocaleLowerCase() === text + (option) => option.text === text ) if (selectedTimezone) { onSelectTimezone(selectedTimezone) diff --git a/apps/studio/components/interfaces/Database/Backups/PITR/pitr-form.tsx b/apps/studio/components/interfaces/Database/Backups/PITR/pitr-form.tsx new file mode 100644 index 00000000000..5bd537845cb --- /dev/null +++ b/apps/studio/components/interfaces/Database/Backups/PITR/pitr-form.tsx @@ -0,0 +1,264 @@ +import { ButtonTooltip } from 'components/ui/ButtonTooltip' +import DatePicker from 'react-datepicker' +import { FormPanel } from 'components/ui/Forms/FormPanel' +import { format } from 'date-fns' +import InformationBox from 'components/ui/InformationBox' +import dayjs from 'dayjs' +import { ChevronLeft, ChevronRight, HelpCircle } from 'lucide-react' +import { + constrainDateToRange, + formatNumberToTwoDigits, + getClientTimezone, + getDatesBetweenRange, +} from './PITR.utils' +import { TimezoneSelection } from './TimezoneSelection' +import TimeInput from './TimeInput' +import { Timezone } from './PITR.types' +import { useMemo, useState } from 'react' + +type Props = { + onSubmit: (data: { + recoveryTimeTargetUnix: number + recoveryTimeString: string + recoveryTimeStringUtc: string + }) => void + earliestAvailableBackupUnix: number + latestAvailableBackupUnix: number +} + +export function PITRForm({ + onSubmit, + earliestAvailableBackupUnix, + latestAvailableBackupUnix, +}: Props) { + const [selectedTimezone, setSelectedTimezone] = useState(getClientTimezone()) + const earliestAvailableBackup = dayjs + .unix(earliestAvailableBackupUnix ?? 0) + .tz(selectedTimezone.utc[0]) + const latestAvailableBackup = dayjs + .unix(latestAvailableBackupUnix ?? 0) + .tz(selectedTimezone.utc[0]) + + const [selectedDateRaw, setSelectedDateRaw] = useState(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({ + recoveryTimeTargetUnix, + recoveryTimeString, + recoveryTimeStringUtc, + }) + } + + return ( +
+ + + Continue + +
+ } + > +
+
+ 'cursor-pointer'} + minDate={earliestAvailableBackup.toDate()} + maxDate={latestAvailableBackup.toDate()} + highlightDates={availableDates.map((date) => date.toDate())} + renderCustomHeader={({ + date, + decreaseMonth, + increaseMonth, + prevMonthButtonDisabled, + nextMonthButtonDisabled, + }) => ( +
+
+ + + {format(date, 'MMMM yyyy')} + + +
+
+ )} + /> + {availableDates.length > 1 && ( +
+
+

Point in time back up available

+
+ )} +
+ +
+ {!selectedDate ? ( +
+
+ } + title="Select a date which you'd like to restore your database to" + /> +
+
+ ) : ( +
+
+

Date to restore to

+

+ {dayjs(selectedDate).format('DD MMM YYYY')} + + , {formatNumberToTwoDigits(selectedTime.h)}: + {formatNumberToTwoDigits(selectedTime.m)}: + {formatNumberToTwoDigits(selectedTime.s)} + +

+
+
+
+

Time zone

+
+ +
+
+
+
+

Recovery time

+ { + const newDate = dayjs(selectedDateRaw) + .set('hour', h) + .set('minute', m) + .set('second', s) + + setSelectedDateRaw(newDate.toDate()) + }} + /> +
+ +

+ Enter a time within the available range to restore from.
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 +

+
+
+

+ {isSelectedOnEarliestDay && ( +

+ Earliest backup available for this date:{' '} + {earliestAvailableBackup.format('HH:mm:ss')} +

+ )} + {isSelectedOnLatestDay && ( +

+ Latest backup available for this date:{' '} + {latestAvailableBackup.format('HH:mm:ss')} +

+ )} +
+
+
+ )} +
+
+ +
+ ) +} diff --git a/apps/studio/components/interfaces/Settings/Logs/Logs.constants.ts b/apps/studio/components/interfaces/Settings/Logs/Logs.constants.ts index 4168bea354c..2da4d335752 100644 --- a/apps/studio/components/interfaces/Settings/Logs/Logs.constants.ts +++ b/apps/studio/components/interfaces/Settings/Logs/Logs.constants.ts @@ -363,6 +363,7 @@ export enum LogsTableName { POSTGREST = 'postgrest_logs', SUPAVISOR = 'supavisor_logs', WAREHOUSE = 'warehouse_logs', + CRON_JOBS = 'cron_job_run_details', } export const LOGS_TABLES = { @@ -376,6 +377,7 @@ export const LOGS_TABLES = { postgrest: LogsTableName.POSTGREST, supavisor: LogsTableName.SUPAVISOR, warehouse: LogsTableName.WAREHOUSE, + cron: LogsTableName.CRON_JOBS, } export const LOGS_SOURCE_DESCRIPTION = { @@ -389,6 +391,7 @@ export const LOGS_SOURCE_DESCRIPTION = { [LogsTableName.POSTGREST]: 'RESTful API web server logs', [LogsTableName.SUPAVISOR]: 'Cloud-native Postgres connection pooler logs', [LogsTableName.WAREHOUSE]: 'Logs obtained from a data warehouse collection', + [LogsTableName.CRON_JOBS]: 'Logs obtained from cron job runs', } export const genQueryParams = (params: { [k: string]: string }) => { diff --git a/apps/studio/components/interfaces/Settings/Logs/Logs.types.ts b/apps/studio/components/interfaces/Settings/Logs/Logs.types.ts index f9c055618af..3ba1eef4fb8 100644 --- a/apps/studio/components/interfaces/Settings/Logs/Logs.types.ts +++ b/apps/studio/components/interfaces/Settings/Logs/Logs.types.ts @@ -88,6 +88,7 @@ export type QueryType = | 'supavisor' | 'postgrest' | 'warehouse' + | 'cron' export type Mode = 'simple' | 'custom' diff --git a/apps/studio/components/interfaces/Settings/Logs/Logs.utils.ts b/apps/studio/components/interfaces/Settings/Logs/Logs.utils.ts index b4c4732ee15..6a4c84ec55c 100644 --- a/apps/studio/components/interfaces/Settings/Logs/Logs.utils.ts +++ b/apps/studio/components/interfaces/Settings/Logs/Logs.utils.ts @@ -211,6 +211,9 @@ limit ${limit} ${orderBy} limit ${limit} ` + + case 'cron_job_run_details': + return `select status, start_time, end_time, jobid from ${table} ${where} ${orderBy} limit ${limit}` } } diff --git a/apps/studio/components/layouts/LogsLayout/LogsSidebarMenuV2.tsx b/apps/studio/components/layouts/LogsLayout/LogsSidebarMenuV2.tsx index ae123766bdf..0eda209a230 100644 --- a/apps/studio/components/layouts/LogsLayout/LogsSidebarMenuV2.tsx +++ b/apps/studio/components/layouts/LogsLayout/LogsSidebarMenuV2.tsx @@ -164,6 +164,12 @@ export function LogsSidebarMenuV2() { url: `/project/${ref}/logs/edge-functions-logs`, items: [], }, + { + name: 'Cron Jobs', + key: 'cron-logs', + url: `/project/${ref}/logs/cron-logs`, + items: [], + }, ] const filteredLogs = BASE_COLLECTIONS.filter((collection) => { diff --git a/apps/studio/data/projects/clone-mutation.ts b/apps/studio/data/projects/clone-mutation.ts new file mode 100644 index 00000000000..3a42a389506 --- /dev/null +++ b/apps/studio/data/projects/clone-mutation.ts @@ -0,0 +1,65 @@ +import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' +import { toast } from 'sonner' + +import { handleError, post } from 'data/fetchers' +import type { ResponseError } from 'types' +import { projectKeys } from './keys' + +export type ProjectCloneVariables = { + projectRef: string + newProjectName: string + newDbPass: string + cloneBackupId?: number + recoveryTimeTarget?: number +} + +export async function triggerClone({ + projectRef, + newProjectName, + cloneBackupId, + newDbPass, + recoveryTimeTarget, +}: ProjectCloneVariables) { + if (!projectRef) throw new Error('Project ref is required') + const { data, error } = await post('/platform/database/{ref}/clone', { + params: { path: { ref: projectRef } }, + body: { cloneBackupId, newProjectName, newDbPass, recoveryTimeTarget }, + }) + + if (error) handleError(error) + return data +} + +type ProjectCloneData = Awaited> + +export const useProjectCloneMutation = ({ + onSuccess, + onError, + ...options +}: Omit< + UseMutationOptions, + 'mutationFn' +> = {}) => { + const queryClient = useQueryClient() + + return useMutation( + (vars) => triggerClone(vars), + { + async onSuccess(data, variables, context) { + await queryClient.invalidateQueries({ + queryKey: projectKeys.listCloneBackups(variables.projectRef), + }) + await onSuccess?.(data, variables, context) + }, + async onError(data, variables, context) { + if (onError === undefined) { + console.error(data) + toast.error(`Failed to trigger clone: ${data.message}`) + } else { + onError(data, variables, context) + } + }, + ...options, + } + ) +} diff --git a/apps/studio/data/projects/clone-query.ts b/apps/studio/data/projects/clone-query.ts new file mode 100644 index 00000000000..d7f0114e1b6 --- /dev/null +++ b/apps/studio/data/projects/clone-query.ts @@ -0,0 +1,29 @@ +import { get, handleError } from 'data/fetchers' +import { projectKeys } from './keys' +import { useQuery, UseQueryOptions } from '@tanstack/react-query' +import { ResponseError } from 'types' + +export async function getCloneBackups(projectRef?: string) { + if (!projectRef) throw new Error('Project ref is required') + + const { data, error } = await get(`/platform/database/{ref}/clone`, { + params: { path: { ref: projectRef } }, + }) + + if (error) handleError(error) + return data +} + +export type CloneBackupsData = Awaited> +export type CloneBackupsError = ResponseError + +export const useCloneBackupsQuery = ( + { projectRef }: { projectRef?: string }, + options: UseQueryOptions = {} +) => { + return useQuery( + projectKeys.listCloneBackups(projectRef), + () => getCloneBackups(projectRef), + { enabled: !!projectRef, ...options } + ) +} diff --git a/apps/studio/data/projects/clone-status-query.ts b/apps/studio/data/projects/clone-status-query.ts new file mode 100644 index 00000000000..9f9b6a5253b --- /dev/null +++ b/apps/studio/data/projects/clone-status-query.ts @@ -0,0 +1,29 @@ +import { get, handleError } from 'data/fetchers' +import { projectKeys } from './keys' +import { useQuery, UseQueryOptions } from '@tanstack/react-query' +import { ResponseError } from 'types' + +export async function getCloneStatus(projectRef?: string) { + if (!projectRef) throw new Error('Project ref is required') + + const { data, error } = await get(`/platform/database/{ref}/clone/status`, { + params: { path: { ref: projectRef } }, + }) + + if (error) handleError(error) + return data +} + +export type CloneStatusData = Awaited> +export type CloneStatusError = ResponseError + +export const useCloneStatusQuery = ( + { projectRef }: { projectRef?: string }, + options: UseQueryOptions = {} +) => { + return useQuery( + projectKeys.listCloneStatus(projectRef), + () => getCloneStatus(projectRef), + { enabled: !!projectRef, ...options } + ) +} diff --git a/apps/studio/data/projects/keys.ts b/apps/studio/data/projects/keys.ts index 87d8627132d..726558d8c5c 100644 --- a/apps/studio/data/projects/keys.ts +++ b/apps/studio/data/projects/keys.ts @@ -18,4 +18,10 @@ export const projectKeys = { ['projects', projectRef, 'pause-status'] as const, orgProjects: (slug: string | undefined) => ['projects', 'org', slug] as const, + + // Clone to new project + listCloneBackups: (projectRef: string | undefined) => + ['projects', projectRef, 'clone-backups'] as const, + listCloneStatus: (projectRef: string | undefined) => + ['projects', projectRef, 'clone-status'] as const, } diff --git a/apps/studio/data/projects/new-project.constants.ts b/apps/studio/data/projects/new-project.constants.ts new file mode 100644 index 00000000000..82a51c5f84b --- /dev/null +++ b/apps/studio/data/projects/new-project.constants.ts @@ -0,0 +1,97 @@ +import { components } from 'api-types' +import { PROVIDERS } from 'lib/constants' + +type DesiredInstanceSize = components['schemas']['DesiredInstanceSize'] + +export const instanceSizeSpecs: Record< + DesiredInstanceSize, + { + label: string + ram: string + cpu: string + priceHourly: number + priceMonthly: number + cloud_providers: string[] + } +> = { + micro: { + label: 'Micro', + ram: '1 GB', + cpu: '2-core', + priceHourly: 0.01344, + priceMonthly: 10, + cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.FLY.id], + }, + small: { + label: 'Small', + ram: '2 GB', + cpu: '2-core', + priceHourly: 0.0206, + priceMonthly: 15, + cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.FLY.id], + }, + medium: { + label: 'Medium', + ram: '4 GB', + cpu: '2-core', + priceHourly: 0.0822, + priceMonthly: 60, + cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.FLY.id], + }, + large: { + label: 'Large', + ram: '8 GB', + cpu: '2-core', + priceHourly: 0.1517, + priceMonthly: 110, + cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.FLY.id], + }, + xlarge: { + label: 'XL', + ram: '16 GB', + cpu: '4-core', + priceHourly: 0.2877, + priceMonthly: 210, + cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.FLY.id], + }, + '2xlarge': { + label: '2XL', + ram: '32 GB', + cpu: '8-core', + priceHourly: 0.562, + priceMonthly: 410, + cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.FLY.id], + }, + '4xlarge': { + label: '4XL', + ram: '64 GB', + cpu: '16-core', + priceHourly: 1.32, + priceMonthly: 960, + cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.FLY.id], + }, + '8xlarge': { + label: '8XL', + ram: '128 GB', + cpu: '32-core', + priceHourly: 2.562, + priceMonthly: 1870, + cloud_providers: [PROVIDERS.AWS.id], + }, + '12xlarge': { + label: '12XL', + ram: '192 GB', + cpu: '48-core', + priceHourly: 3.836, + priceMonthly: 2800, + cloud_providers: [PROVIDERS.AWS.id], + }, + '16xlarge': { + label: '16XL', + ram: '256 GB', + cpu: '64-core', + priceHourly: 5.12, + priceMonthly: 3730, + cloud_providers: [PROVIDERS.AWS.id], + }, +} diff --git a/apps/studio/pages/new/[slug].tsx b/apps/studio/pages/new/[slug].tsx index cf59f410810..8c2724a60e6 100644 --- a/apps/studio/pages/new/[slug].tsx +++ b/apps/studio/pages/new/[slug].tsx @@ -84,6 +84,7 @@ import { Input } from 'ui-patterns/DataInputs/Input' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { InfoTooltip } from 'ui-patterns/info-tooltip' import { useOverdueInvoicesQuery } from 'data/invoices/invoices-overdue-query' +import { instanceSizeSpecs } from 'data/projects/new-project.constants' type DesiredInstanceSize = components['schemas']['DesiredInstanceSize'] @@ -100,99 +101,6 @@ const sizes: DesiredInstanceSize[] = [ '16xlarge', ] -const instanceSizeSpecs: Record< - DesiredInstanceSize, - { - label: string - ram: string - cpu: string - priceHourly: number - priceMonthly: number - cloud_providers: string[] - } -> = { - micro: { - label: 'Micro', - ram: '1 GB', - cpu: '2-core', - priceHourly: 0.01344, - priceMonthly: 10, - cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.FLY.id], - }, - small: { - label: 'Small', - ram: '2 GB', - cpu: '2-core', - priceHourly: 0.0206, - priceMonthly: 15, - cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.FLY.id], - }, - medium: { - label: 'Medium', - ram: '4 GB', - cpu: '2-core', - priceHourly: 0.0822, - priceMonthly: 60, - cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.FLY.id], - }, - large: { - label: 'Large', - ram: '8 GB', - cpu: '2-core', - priceHourly: 0.1517, - priceMonthly: 110, - cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.FLY.id], - }, - xlarge: { - label: 'XL', - ram: '16 GB', - cpu: '4-core', - priceHourly: 0.2877, - priceMonthly: 210, - cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.FLY.id], - }, - '2xlarge': { - label: '2XL', - ram: '32 GB', - cpu: '8-core', - priceHourly: 0.562, - priceMonthly: 410, - cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.FLY.id], - }, - '4xlarge': { - label: '4XL', - ram: '64 GB', - cpu: '16-core', - priceHourly: 1.32, - priceMonthly: 960, - cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.FLY.id], - }, - '8xlarge': { - label: '8XL', - ram: '128 GB', - cpu: '32-core', - priceHourly: 2.562, - priceMonthly: 1870, - cloud_providers: [PROVIDERS.AWS.id], - }, - '12xlarge': { - label: '12XL', - ram: '192 GB', - cpu: '48-core', - priceHourly: 3.836, - priceMonthly: 2800, - cloud_providers: [PROVIDERS.AWS.id], - }, - '16xlarge': { - label: '16XL', - ram: '256 GB', - cpu: '64-core', - priceHourly: 5.12, - priceMonthly: 3730, - cloud_providers: [PROVIDERS.AWS.id], - }, -} - const Wizard: NextPageWithLayout = () => { const router = useRouter() const { slug, projectName } = useParams() diff --git a/apps/studio/pages/project/[ref]/database/backups/restore-to-new-project.tsx b/apps/studio/pages/project/[ref]/database/backups/restore-to-new-project.tsx new file mode 100644 index 00000000000..632ac45122a --- /dev/null +++ b/apps/studio/pages/project/[ref]/database/backups/restore-to-new-project.tsx @@ -0,0 +1,588 @@ +import { zodResolver } from '@hookform/resolvers/zod' +import { PermissionAction } from '@supabase/shared-types/out/constants' +import BackupsEmpty from 'components/interfaces/Database/Backups/BackupsEmpty' + +import DatabaseBackupsNav from 'components/interfaces/Database/Backups/DatabaseBackupsNav' +import DatabaseLayout from 'components/layouts/DatabaseLayout/DatabaseLayout' +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold' +import AlertError from 'components/ui/AlertError' +import { FormHeader } from 'components/ui/Forms/FormHeader' +import NoPermission from 'components/ui/NoPermission' +import Panel from 'components/ui/Panel' +import PasswordStrengthBar from 'components/ui/PasswordStrengthBar' +import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' +import UpgradeToPro from 'components/ui/UpgradeToPro' +import { useProjectCloneMutation } from 'data/projects/clone-mutation' +import { useCloneBackupsQuery } from 'data/projects/clone-query' +import { useCloneStatusQuery } from 'data/projects/clone-status-query' +import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' +import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions' +import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' +import { PROJECT_STATUS } from 'lib/constants' +import { getDatabaseMajorVersion, passwordStrength } from 'lib/helpers' +import Link from 'next/link' +import { useState, useRef } from 'react' +import { useForm } from 'react-hook-form' +import type { NextPageWithLayout } from 'types' +import { + Badge, + Button, + Dialog, + DialogContent, + DialogTitle, + DialogHeader, + DialogDescription, + DialogFooter, + DialogSection, + Input, + Form_Shadcn_, + FormControl_Shadcn_, + FormField_Shadcn_, + Input_Shadcn_, + Alert_Shadcn_, + AlertTitle_Shadcn_, + AlertDescription_Shadcn_, +} from 'ui' +import { Admonition, TimestampInfo } from 'ui-patterns' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' +import { z } from 'zod' +import { debounce } from 'lodash' +import generator from 'generate-password-browser' +import { toast } from 'sonner' +import { Loader2 } from 'lucide-react' +import { PITRForm } from 'components/interfaces/Database/Backups/PITR/pitr-form' +import { instanceSizeSpecs } from 'data/projects/new-project.constants' +import { Markdown } from 'components/interfaces/Markdown' + +const RestoreToNewProjectPage: NextPageWithLayout = () => { + return ( + + +
+
+ + +
+ +
+
+
+
+
+ ) +} + +RestoreToNewProjectPage.getLayout = (page) => ( + {page} +) + +const RestoreToNewProject = () => { + const { project } = useProjectContext() + const organization = useSelectedOrganization() + const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: organization?.slug }) + const isFreePlan = subscription?.plan?.id === 'free' + + const { + data: cloneBackups, + error, + isLoading: cloneBackupsLoading, + isError, + } = useCloneBackupsQuery( + { + projectRef: project?.ref, + }, + { + enabled: !isFreePlan, + } + ) + + const plan = subscription?.plan?.id + const isActiveHealthy = project?.status === PROJECT_STATUS.ACTIVE_HEALTHY + + const isPermissionsLoaded = usePermissionsLoaded() + const canReadPhysicalBackups = useCheckPermissions(PermissionAction.READ, 'physical_backups') + const canTriggerPhysicalBackups = useCheckPermissions( + PermissionAction.INFRA_EXECUTE, + 'queue_job.restore.prepare' + ) + const hasPITREnabled = cloneBackups?.pitr_enabled + + const dbVersion = getDatabaseMajorVersion(project?.dbVersion ?? '') + const IS_PG15_OR_ABOVE = dbVersion >= 15 + const PHYSICAL_BACKUPS_ENABLED = project?.is_physical_backups_enabled + + const { + data: cloneStatus, + refetch: refetchCloneStatus, + isLoading: cloneStatusLoading, + } = useCloneStatusQuery({ + projectRef: project?.ref, + }) + const lastClone = cloneStatus?.clones?.[cloneStatus?.clones.length - 1] + const [selectedBackupId, setSelectedBackupId] = useState(null) + const [showConfirmationDialog, setShowConfirmationDialog] = useState(false) + const [showNewProjectDialog, setShowNewProjectDialog] = useState(false) + const [recoveryTimeTarget, setRecoveryTimeTarget] = useState(null) + const IS_CLONED_PROJECT = (cloneStatus?.cloned_from?.source_project as any)?.ref ? true : false + + const isLoading = !isPermissionsLoaded || cloneBackupsLoading || cloneStatusLoading + + /** + * New project will have the same compute size and disk size as the original project + */ + function getAdditionalMonthlySpend() { + const currentProjectComputeSize = project?.infra_compute_size + if (!currentProjectComputeSize) { + return null + } + + if (currentProjectComputeSize === 'nano') { + return null + } + + const additionalMonthlySpend = instanceSizeSpecs[currentProjectComputeSize] + + return additionalMonthlySpend + } + + const { mutate: triggerClone, isLoading: cloneMutationLoading } = useProjectCloneMutation({ + onError: (error) => { + console.error('error', error) + toast.error('Failed to restore to new project') + }, + onSuccess: () => { + toast.success('Restoration process started') + refetchCloneStatus() + setShowNewProjectDialog(false) + }, + }) + + const FormSchema = z.object({ + name: z.string().min(1), + password: z.string().min(1), + }) + + const form = useForm>({ + resolver: zodResolver(FormSchema), + defaultValues: { + name: '', + password: '', + }, + }) + + const [passwordStrengthScore, setPasswordStrengthScore] = useState(0) + const [passwordStrengthMessage, setPasswordStrengthMessage] = useState('') + + const delayedCheckPasswordStrength = useRef( + debounce((value: string) => checkPasswordStrength(value), 300) + ).current + + async function checkPasswordStrength(value: string) { + const { message, strength } = await passwordStrength(value) + setPasswordStrengthScore(strength) + setPasswordStrengthMessage(message) + } + + function generateStrongPassword() { + const password = generator.generate({ + length: 16, + numbers: true, + uppercase: true, + }) + + form.setValue('password', password) + delayedCheckPasswordStrength(password) + } + + function AdditionalMonthlySpend() { + const additionalMonthlySpend = getAdditionalMonthlySpend() + if (!additionalMonthlySpend) { + return null + } + + return ( +
+

+ The new project will have the same compute size and disk size as this project. You will be + able to update the compute size and disk size after the new project is created in{' '} + + Project Settings > Compute and Disk + +

+
+

Additional Monthly Compute + Disk Cost

+

${additionalMonthlySpend.priceMonthly}

+
+
+ ) + } + + if (isLoading) { + return + } + + if (!canReadPhysicalBackups) { + return + } + + if (!canTriggerPhysicalBackups) { + return + } + + if (!IS_PG15_OR_ABOVE) { + return ( + + + + ) + } + + if (!PHYSICAL_BACKUPS_ENABLED) { + return ( + + Physical backups must be enabled to restore your database to a new project. +
Find out more about how backups work at supabase{' '} + + in our docs + + . + + } + /> + ) + } + + if (plan === 'free') { + return ( + + ) + } + + if (IS_CLONED_PROJECT) { + return ( + + + + ) + } + + if (isError) { + return + } + + if (!isActiveHealthy) { + return ( + + ) + } + + if (lastClone?.status === 'FAILED') { + return ( + + + + ) + } + + if (lastClone?.status === 'COMPLETED') { + return ( + + + + ) + } + + if (lastClone?.status === 'IN_PROGRESS') { + return ( + + + Restoration in progress + + The new project is being created. +
+ + Go to new project + +
+
+ ) + } + + if ( + !isLoading && + hasPITREnabled && + !cloneBackups?.physicalBackupData.earliestPhysicalBackupDateUnix + ) { + return ( + + ) + } + + if (!isLoading && !hasPITREnabled && cloneBackups?.backups.length === 0) { + return + } + + return ( + <> + + + + Confirm restore to a new project + + This process will create a new project and restore your database to it. + + + +
    +
  • + Project organization will stay the same: {organization?.name} +
  • +
  • + Project region will stay the same: {project?.region || ''} +
  • +
  • + A project can only be restored to another project once.
    + + This is a temporary limitation. Please contact us if you need to restore a project + to multiple other projects. + +
  • +
+
    +
  • What will be transferred?
  • +
      +
    • Database schema (tables, views, procedures)
    • +
    • All data and indexes
    • +
    • Database roles, permissions and users
    • +
    +
+
    +
  • What needs manual reconfiguration?
  • +
      +
    • Storage objects & settings
    • +
    • Edge Functions
    • +
    • Auth settings & API keys
    • +
    • Realtime settings
    • +
    • Database extensions and settings
    • +
    • Read replicas
    • +
    +
+
+ + + + + +
+
+ + + + Create new project + + This process will create a new project and restore your database to it. + + + +
{ + if (!project?.ref) { + toast.error('Project ref is required') + return + } + + if (hasPITREnabled && recoveryTimeTarget) { + triggerClone({ + projectRef: project?.ref, + newProjectName: data.name, + newDbPass: data.password, + recoveryTimeTarget: recoveryTimeTarget, + cloneBackupId: undefined, + }) + } else if (selectedBackupId) { + triggerClone({ + projectRef: project?.ref, + cloneBackupId: selectedBackupId, + newProjectName: data.name, + newDbPass: data.password, + recoveryTimeTarget: undefined, + }) + } else { + toast.error('No backup or point in time selected') + return + } + })} + > + + ( + + + + + + )} + /> + ( + + + 0} + onChange={(e) => { + const value = e.target.value + field.onChange(value) + if (value == '') { + setPasswordStrengthScore(-1) + setPasswordStrengthMessage('') + } else delayedCheckPasswordStrength(value) + }} + descriptionText={ + + } + /> + + + )} + /> + + + + + + + +
+
+
+ {hasPITREnabled ? ( + <> + { + setShowConfirmationDialog(true) + setRecoveryTimeTarget(v.recoveryTimeTargetUnix) + }} + earliestAvailableBackupUnix={ + cloneBackups?.physicalBackupData.earliestPhysicalBackupDateUnix || 0 + } + latestAvailableBackupUnix={ + cloneBackups?.physicalBackupData.latestPhysicalBackupDateUnix || 0 + } + /> + + ) : ( + <> + + {cloneBackups?.backups.length === 0 ? ( + <> + + + ) : ( +
+ {/*
{JSON.stringify({ cloneStatus }, null, 2)}
*/} + {cloneBackups?.backups.map((backup) => { + if (!backup.isPhysicalBackup) return null + return ( +
+
+ +
+ {JSON.stringify(backup.status).replaceAll('"', '')} + {(backup.status as any) === 'COMPLETED' && ( + + )} +
+ ) + })} +
+ )} +
+ + )} + + ) +} + +export default RestoreToNewProjectPage diff --git a/apps/studio/pages/project/[ref]/logs/cron-logs.tsx b/apps/studio/pages/project/[ref]/logs/cron-logs.tsx new file mode 100644 index 00000000000..cb80975342b --- /dev/null +++ b/apps/studio/pages/project/[ref]/logs/cron-logs.tsx @@ -0,0 +1,26 @@ +// select * from cron.job_run_details where jobid = '1' order by start_time desc limit 10 + +import { useRouter } from 'next/router' + +import LogsPreviewer from 'components/interfaces/Settings/Logs/LogsPreviewer' +import LogsLayout from 'components/layouts/LogsLayout/LogsLayout' +import type { NextPageWithLayout } from 'types' +import { LogsTableName } from 'components/interfaces/Settings/Logs/Logs.constants' + +export const LogPage: NextPageWithLayout = () => { + const router = useRouter() + const { ref } = router.query + + return ( + + ) +} + +LogPage.getLayout = (page) => {page} + +export default LogPage diff --git a/apps/studio/styles/date-picker.scss b/apps/studio/styles/date-picker.scss index f21c7c88557..7825357eb34 100644 --- a/apps/studio/styles/date-picker.scss +++ b/apps/studio/styles/date-picker.scss @@ -96,7 +96,7 @@ } .react-datepicker__day--highlighted { - @apply bg-brand-600 text-foreground-light; + @apply bg-overlay-hover text-foreground-light; } .react-datepicker__aria-live { diff --git a/packages/api-types/types/api.d.ts b/packages/api-types/types/api.d.ts index e4922dc9677..081229cb3ec 100644 --- a/packages/api-types/types/api.d.ts +++ b/packages/api-types/types/api.d.ts @@ -185,6 +185,16 @@ export interface paths { /** Restore project with a physical backup */ post: operations['BackupsController_restorePhysicalBackup'] } + '/platform/database/{ref}/clone': { + /** List valid backups to clone from */ + get: operations['CloneController_getValidBackups'] + /** Clone the current project from a backup */ + post: operations['CloneController_cloneCurrentProject'] + } + '/platform/database/{ref}/clone/status': { + /** Retrieve the current status of an existing cloning process */ + get: operations['CloneController_cloneProjectStatus'] + } '/platform/database/{ref}/hook-enable': { /** Enables Database Webhooks on the project */ post: operations['HooksController_enableHooks'] @@ -330,6 +340,10 @@ export interface paths { /** Gets invoice with the given invoice ID */ get: operations['OrgInvoicesController_getInvoice'] } + '/platform/organizations/{slug}/billing/invoices/{invoiceId}/payment-link': { + /** Gets the payment link to manually pay the given invoice */ + get: operations['OrgInvoicesController_getInvoicePaymentLink'] + } '/platform/organizations/{slug}/billing/invoices/upcoming': { /** Gets the upcoming invoice */ get: operations['OrgInvoicesController_getUpcomingInvoice'] @@ -752,18 +766,22 @@ export interface paths { delete: operations['v1-delete-a-warehouse-token'] } '/platform/projects/{ref}/analytics/warehouse/collections': { - /** Lists project's warehouse collections from logflare */ - get: operations['v1-list-all-warehouse-collections'] - /** Create a warehouse collection */ - post: operations['v1-create-a-warehouse-collection'] + /** Lists project's telemetry collections from logflare */ + get: operations['v1-list-all-telemetry-collections'] + /** Create a telemetry collection */ + post: operations['v1-create-a-telemetry-collection'] } '/platform/projects/{ref}/analytics/warehouse/collections/{token}': { - /** Get a warehouse collection */ - get: operations['v1-get-a-warehouse-collection'] - /** Delete a warehouse collection */ - delete: operations['v1-delete-a-warehouse-collection'] - /** Update a warehouse collection */ - patch: operations['v1-update-a-warehouse-collection'] + /** Get a telemetry collection */ + get: operations['v1-get-a-telemetry-collection'] + /** Delete a telemetry collection */ + delete: operations['v1-delete-a-telemetry-collection'] + /** Update a telemetry collection */ + patch: operations['v1-update-a-telemetry-collection'] + } + '/platform/projects/{ref}/analytics/warehouse/collections/{token}/schema': { + /** Get a telemetry collection schema */ + get: operations['v1-get-a-telemetry-collection-schema'] } '/platform/projects/{ref}/analytics/warehouse/endpoints': { /** Lists project's warehouse endpoints from logflare */ @@ -907,6 +925,12 @@ export interface paths { /** Modify database disk */ post: operations['DiskController_modifyDisk'] } + '/platform/projects/{ref}/disk/custom-config': { + /** Gets disk autoscale config */ + get: operations['DiskController_getDiskAutoscaleConfig'] + /** Updates disk autoscale config */ + post: operations['DiskController_updateDiskAutoscaleConfig'] + } '/platform/projects/{ref}/disk/util': { /** Get disk utilization */ get: operations['DiskController_getDiskUtilization'] @@ -967,6 +991,10 @@ export interface paths { /** Gets project's settings */ get: operations['SettingsController_getProjectApi'] } + '/platform/projects/{ref}/settings/sensitivity': { + /** Updates the given project sensitivity */ + patch: operations['SensitivityController_updateProjectSensitivity'] + } '/platform/projects/{ref}/status': { /** Gets project's status */ get: operations['ProjectStatusController_getStatus'] @@ -1191,6 +1219,10 @@ export interface paths { /** Processes Orb events */ post: operations['OrbWebhooksController_processEvent'] } + '/system/organizations/{slug}': { + /** Deletes organization */ + delete: operations['OrganizationSlugSystemController_deleteOrganization'] + } '/system/organizations/{slug}/billing/subscription': { /** Gets the current subscription */ get: operations['OrgSubscriptionSystemController_getSubscription'] @@ -1217,6 +1249,10 @@ export interface paths { /** Create a project */ post: operations['SystemProjectsController_createProject'] } + '/system/projects/{ref}': { + /** Deletes the given project */ + delete: operations['ProjectRefSystemController_deleteProject'] + } '/system/projects/{ref}/billing/addons': { /** Updates project addon */ post: operations['AddonsController_updateAddon'] @@ -1225,6 +1261,14 @@ export interface paths { /** Removes project addon */ delete: operations['AddonsController_removeAddon'] } + '/system/projects/{ref}/config/email-restrictions': { + /** Gets the current email restrictions for a project. */ + get: operations['ProjectEmailRestrictionsController_getEmailRestrictions'] + /** Updates the current email restrictions for a project. */ + put: operations['ProjectEmailRestrictionsController_updateEmailRestrictions'] + /** Removes the current email restrictions for a project. */ + delete: operations['ProjectEmailRestrictionsController_deleteEmailRestrictions'] + } '/system/projects/{ref}/config/update-jwt/complete': { /** Handle update project jwt on completion */ post: operations['ProjectUpdateJwtController_completeUpdateJwt'] @@ -1239,6 +1283,12 @@ export interface paths { /** Modify database disk */ post: operations['SystemProjectDiskController_modifyDisk'] } + '/system/projects/{ref}/disk/custom-config': { + /** Gets disk autoscale config */ + get: operations['SystemProjectDiskController_getDiskAutoscaleConfig'] + /** Updates disk autoscale config */ + post: operations['SystemProjectDiskController_updateDiskAutoscaleConfig'] + } '/system/projects/{ref}/disk/util': { /** Get disk utilization */ get: operations['SystemProjectDiskController_getDiskUtilization'] @@ -1310,6 +1360,10 @@ export interface paths { /** Processes a project's WAL verification report. */ put: operations['WalVerificationReportingController_processWalVerification'] } + '/system/projects/email-abuse': { + /** Reports email abuse from a postmark */ + post: operations['ProjectEmailAbuseController_reportEmailAbuseWebhookPostmark'] + } '/system/stripe/webhooks': { /** Processes Stripe event */ post: operations['StripeWebhooksController_processEvent'] @@ -1404,6 +1458,16 @@ export interface paths { /** Restore project with a physical backup */ post: operations['BackupsController_restorePhysicalBackup'] } + '/v0/database/{ref}/clone': { + /** List valid backups to clone from */ + get: operations['CloneController_getValidBackups'] + /** Clone the current project from a backup */ + post: operations['CloneController_cloneCurrentProject'] + } + '/v0/database/{ref}/clone/status': { + /** Retrieve the current status of an existing cloning process */ + get: operations['CloneController_cloneProjectStatus'] + } '/v0/database/{ref}/hook-enable': { /** Enables Database Webhooks on the project */ post: operations['HooksController_enableHooks'] @@ -1701,18 +1765,22 @@ export interface paths { delete: operations['v1-delete-a-warehouse-token'] } '/v0/projects/{ref}/analytics/warehouse/collections': { - /** Lists project's warehouse collections from logflare */ - get: operations['v1-list-all-warehouse-collections'] - /** Create a warehouse collection */ - post: operations['v1-create-a-warehouse-collection'] + /** Lists project's telemetry collections from logflare */ + get: operations['v1-list-all-telemetry-collections'] + /** Create a telemetry collection */ + post: operations['v1-create-a-telemetry-collection'] } '/v0/projects/{ref}/analytics/warehouse/collections/{token}': { - /** Get a warehouse collection */ - get: operations['v1-get-a-warehouse-collection'] - /** Delete a warehouse collection */ - delete: operations['v1-delete-a-warehouse-collection'] - /** Update a warehouse collection */ - patch: operations['v1-update-a-warehouse-collection'] + /** Get a telemetry collection */ + get: operations['v1-get-a-telemetry-collection'] + /** Delete a telemetry collection */ + delete: operations['v1-delete-a-telemetry-collection'] + /** Update a telemetry collection */ + patch: operations['v1-update-a-telemetry-collection'] + } + '/v0/projects/{ref}/analytics/warehouse/collections/{token}/schema': { + /** Get a telemetry collection schema */ + get: operations['v1-get-a-telemetry-collection-schema'] } '/v0/projects/{ref}/analytics/warehouse/endpoints': { /** Lists project's warehouse endpoints from logflare */ @@ -1838,6 +1906,12 @@ export interface paths { /** Modify database disk */ post: operations['DiskController_modifyDisk'] } + '/v0/projects/{ref}/disk/custom-config': { + /** Gets disk autoscale config */ + get: operations['DiskController_getDiskAutoscaleConfig'] + /** Updates disk autoscale config */ + post: operations['DiskController_updateDiskAutoscaleConfig'] + } '/v0/projects/{ref}/disk/util': { /** Get disk utilization */ get: operations['DiskController_getDiskUtilization'] @@ -1890,6 +1964,10 @@ export interface paths { /** Gets project's settings */ get: operations['SettingsController_getProjectApi'] } + '/v0/projects/{ref}/settings/sensitivity': { + /** Updates the given project sensitivity */ + patch: operations['SensitivityController_updateProjectSensitivity'] + } '/v0/projects/{ref}/status': { /** Gets project's status */ get: operations['ProjectStatusController_getStatus'] @@ -2047,6 +2125,8 @@ export interface paths { post: operations['ApiKeysController_createApiKey'] } '/v1/projects/{ref}/api-keys/{id}': { + /** [Alpha] Get API key */ + get: operations['ApiKeysController_getApiKey'] /** [Alpha] Deletes an API key for the project */ delete: operations['ApiKeysController_deleteApiKey'] /** [Alpha] Updates an API key for the project */ @@ -2388,7 +2468,8 @@ export interface components { name: string prefix?: string | null secret_jwt_template?: components['schemas']['ApiKeySecretJWTTemplate'] | null - type?: unknown + /** @enum {string|null} */ + type?: 'publishable' | 'secret' | 'legacy' | null updated_at?: string | null } ApiKeySecretJWTTemplate: { @@ -2744,6 +2825,14 @@ export interface components { result: components['schemas']['CustomHostnameDetails'] success: boolean } + CloneProjectDto: { + /** @default 0 */ + cloneBackupId?: number + newDbPass: string + newProjectName: string + /** @default 0 */ + recoveryTimeTarget?: number + } Column: { id: number name: string @@ -2931,7 +3020,7 @@ export interface components { /** @enum {string} */ tier: 'tier_payg' | 'tier_pro' | 'tier_free' | 'tier_team' | 'tier_enterprise' } - CreateOrganizationBodyV1: { + CreateOrganizationV1Dto: { name: string } CreatePolicyBody: { @@ -3353,6 +3442,14 @@ export interface components { DetachPaymentMethodBody: { card_id: string } + DiskAutoscaleConfig: { + /** @description Growth percentage for disk autoscaling */ + growth_percent?: number | null + /** @description Maximum limit the disk size will grow to in GB */ + max_size_gb?: number | null + /** @description Minimum increment size for disk autoscaling in GB */ + min_increment_gb?: number | null + } DiskRequestAttributesGP3: { iops: number size_gb: number @@ -3433,6 +3530,14 @@ export interface components { downloadName?: string transform?: components['schemas']['StorageObjectTransformOptions'] } + EmailRestrictionsResponseBody: { + inserted_at?: string + is_restricted: boolean + project_id: number + project_ref: string + restricted_to_quota?: string + updated_at?: string + } EventBody: { eventType: string message: string @@ -3499,6 +3604,7 @@ export interface components { import_map?: boolean import_map_path?: string name: string + resource_multiplier?: string slug: string /** @enum {string} */ status: 'ACTIVE' | 'REMOVED' | 'THROTTLED' @@ -3515,6 +3621,7 @@ export interface components { import_map?: boolean import_map_path?: string name: string + resource_multiplier?: string slug: string /** @enum {string} */ status: 'ACTIVE' | 'REMOVED' | 'THROTTLED' @@ -3998,6 +4105,9 @@ export interface components { subscription: string | null subtotal: number } + InvoicePaymentLinkResponse: { + redirectUrl: string + } JoinResponse: { billing_email: string id: number @@ -4121,6 +4231,9 @@ export interface components { MarkDefaultPaymentMethodBody: { payment_method_id: string } + MarkSensitiveBody: { + is_sensitive: boolean + } Member: { gotrue_id: string is_sso_user: boolean | null @@ -4779,6 +4892,16 @@ export interface components { db_schema: string endpoint: string } + ProjectClonedResponse: { + source_project_ref: string + target_project_ref: string + } + ProjectClonedStatusResponse: { + cloned_from: Record + clones: components['schemas']['TargetCloneStatus'][] + id: number + ref: string + } ProjectCreationVersionInfo: { postgres_engine: components['schemas']['PostgresEngine'] release_channel: components['schemas']['ReleaseChannel'] @@ -4807,7 +4930,6 @@ export interface components { inserted_at: string is_branch_enabled: boolean is_physical_backups_enabled: boolean - kpsVersion?: string lastDatabaseResizeAt?: string maxDatabasePreprovisionGb?: number name: string @@ -4816,7 +4938,6 @@ export interface components { ref: string region: string restUrl: string - serviceVersions?: components['schemas']['ServiceVersions'] /** @enum {string} */ status: | 'ACTIVE_HEALTHY' @@ -4904,6 +5025,10 @@ export interface components { | 'unsupported_reg_types' | 'auth_otp_long_expiry' | 'auth_otp_short_length' + | 'ssl_not_enforced' + | 'network_restrictions_not_set' + | 'password_requirements_min_length' + | 'pitr_not_enabled' remediation: Record title: string } @@ -4918,6 +5043,8 @@ export interface components { ref: string } ProjectResourceWarningsResponse: { + /** @enum {string|null} */ + auth_email_offender: 'critical' | 'warning' | null /** @enum {string|null} */ auth_rate_limit_exhaustion: 'critical' | 'warning' | null /** @enum {string|null} */ @@ -4952,6 +5079,9 @@ export interface components { ssl_enforced: boolean status: string } + ProjectSensitivityResponse: { + is_sensitive: boolean + } ProjectServiceApiKeyResponse: { api_key: string name: string @@ -4967,6 +5097,7 @@ export interface components { db_port: number db_user: string inserted_at: string + is_sensitive?: boolean jwt_secret?: string name: string ref: string @@ -5680,6 +5811,18 @@ export interface components { | 'REFERENCES' | 'TRIGGER' } + TargetClonedProject: { + ref: string + } + TargetCloneStatus: { + inserted_at: string | null + project_id: number + /** @enum {string} */ + status: 'COMPLETED' | 'IN_PROGRESS' | 'FAILED' | 'REMOVED' + target_project: components['schemas']['TargetClonedProject'] + target_project_id: number + updated_at: string | null + } TaxId: { country: string type: string @@ -6028,6 +6171,23 @@ export interface components { | '4_origin_setup_completed' | '5_services_reconfigured' } + UpdateDiskAutoscaleConfig: { + /** + * @description Growth percentage for disk autoscaling + * @default 50 + */ + growth_percent?: number | null + /** + * @description Maximum limit the disk size will grow to in GB + * @default 61440 + */ + max_size_gb?: number | null + /** + * @description Minimum increment size for disk autoscaling in GB + * @default 4 + */ + min_increment_gb?: number | null + } UpdateFunctionBody: { args?: string[] /** @enum {string} */ @@ -6360,6 +6520,9 @@ export interface components { /** @enum {string} */ restriction_status: 'grace_period' | 'grace_period_over' | 'null' | 'restricted' } + UpdateRestrictionsRequestBody: { + restricted_to_quota: string + } UpdateRestrictionsResponse: { message?: string restriction_data?: components['schemas']['RestrictionData'] @@ -6651,13 +6814,25 @@ export interface components { V1CreateFunctionBody: { body: string name: string + resource_multiplier?: string slug: string verify_jwt?: boolean } - V1CreateProjectBody: { + V1CreateProjectBodyDto: { /** @description Database password */ db_pass: string - desired_instance_size?: components['schemas']['DesiredInstanceSize'] + /** @enum {string} */ + desired_instance_size?: + | 'micro' + | 'small' + | 'medium' + | 'large' + | 'xlarge' + | '2xlarge' + | '4xlarge' + | '8xlarge' + | '12xlarge' + | '16xlarge' /** * @deprecated * @description This field is deprecated and is ignored in this request @@ -6670,14 +6845,16 @@ export interface components { /** * @deprecated * @description Subscription Plan is now set on organization level and is ignored in this request - * @example free * @enum {string} */ plan?: 'free' | 'pro' - postgres_engine?: components['schemas']['PostgresEngine'] + /** + * @description Postgres engine version. If not provided, the latest version will be used. + * @enum {string} + */ + postgres_engine?: '15' /** * @description Region you want your server to reside in - * @example us-east-1 * @enum {string} */ region: @@ -6699,8 +6876,13 @@ export interface components { | 'ca-central-1' | 'ap-south-1' | 'sa-east-1' - release_channel?: components['schemas']['ReleaseChannel'] /** + * @description Release channel. If not provided, GA will be used. + * @enum {string} + */ + release_channel?: 'internal' | 'alpha' | 'beta' | 'ga' | 'withdrawn' + /** + * Format: uri * @description Template URL used to create the project from the CLI. * @example https://github.com/supabase/supabase/tree/master/examples/slack-clone/nextjs-slack-clone */ @@ -6763,7 +6945,42 @@ export interface components { * @example 2023-03-29T16:32:59Z */ created_at: string - database?: components['schemas']['V1DatabaseResponse'] + /** @description Id of your project */ + id: string + /** @description Name of your project */ + name: string + /** @description Slug of your organization */ + organization_id: string + /** + * @description Region of your project + * @example us-east-1 + */ + region: string + /** @enum {string} */ + status: + | 'ACTIVE_HEALTHY' + | 'ACTIVE_UNHEALTHY' + | 'COMING_UP' + | 'GOING_DOWN' + | 'INACTIVE' + | 'INIT_FAILED' + | 'REMOVED' + | 'RESTARTING' + | 'UNKNOWN' + | 'UPGRADING' + | 'PAUSING' + | 'RESTORING' + | 'RESTORE_FAILED' + | 'PAUSE_FAILED' + | 'RESIZING' + } + V1ProjectWithDatabaseResponse: { + /** + * @description Creation timestamp + * @example 2023-03-29T16:32:59Z + */ + created_at: string + database: components['schemas']['V1DatabaseResponse'] /** @description Id of your project */ id: string /** @description Name of your project */ @@ -6822,6 +7039,7 @@ export interface components { V1UpdateFunctionBody: { body?: string name?: string + resource_multiplier?: string verify_jwt?: boolean } ValidateQueryBody: { @@ -7762,6 +7980,71 @@ export interface operations { } } } + /** List valid backups to clone from */ + CloneController_getValidBackups: { + parameters: { + path: { + /** @description Project ref */ + ref: string + } + } + responses: { + 200: { + content: { + 'application/json': components['schemas']['BackupsResponse'] + } + } + /** @description Failed to list available valid backups */ + 500: { + content: never + } + } + } + /** Clone the current project from a backup */ + CloneController_cloneCurrentProject: { + parameters: { + path: { + /** @description Project ref */ + ref: string + } + } + requestBody: { + content: { + 'application/json': components['schemas']['CloneProjectDto'] + } + } + responses: { + 201: { + content: { + 'application/json': components['schemas']['ProjectClonedResponse'] + } + } + /** @description Failed to clone the current project */ + 500: { + content: never + } + } + } + /** Retrieve the current status of an existing cloning process */ + CloneController_cloneProjectStatus: { + parameters: { + path: { + /** @description Project ref */ + ref: string + } + } + responses: { + 200: { + content: { + 'application/json': components['schemas']['ProjectClonedStatusResponse'] + } + } + /** @description Failed to retrieve clone project status */ + 500: { + content: never + } + } + } /** Enables Database Webhooks on the project */ HooksController_enableHooks: { parameters: { @@ -8559,6 +8842,29 @@ export interface operations { } } } + /** Gets the payment link to manually pay the given invoice */ + OrgInvoicesController_getInvoicePaymentLink: { + parameters: { + path: { + invoiceId: string + slug: string + } + } + responses: { + 200: { + content: { + 'application/json': components['schemas']['InvoicePaymentLinkResponse'] + } + } + 403: { + content: never + } + /** @description Failed to retrieve invoice payment link */ + 500: { + content: never + } + } + } /** Gets the upcoming invoice */ OrgInvoicesController_getUpcomingInvoice: { parameters: { @@ -11811,8 +12117,8 @@ export interface operations { } } } - /** Lists project's warehouse collections from logflare */ - 'v1-list-all-warehouse-collections': { + /** Lists project's telemetry collections from logflare */ + 'v1-list-all-telemetry-collections': { parameters: { path: { /** @description Project ref */ @@ -11828,14 +12134,14 @@ export interface operations { 403: { content: never } - /** @description Failed to fetch warehouse collections */ + /** @description Failed to fetch telemetry collections */ 500: { content: never } } } - /** Create a warehouse collection */ - 'v1-create-a-warehouse-collection': { + /** Create a telemetry collection */ + 'v1-create-a-telemetry-collection': { responses: { 201: { content: { @@ -11845,14 +12151,14 @@ export interface operations { 403: { content: never } - /** @description Failed to create warehouse collection */ + /** @description Failed to create telemetry collection */ 500: { content: never } } } - /** Get a warehouse collection */ - 'v1-get-a-warehouse-collection': { + /** Get a telemetry collection */ + 'v1-get-a-telemetry-collection': { responses: { 200: { content: { @@ -11862,14 +12168,14 @@ export interface operations { 403: { content: never } - /** @description Failed to fetch warehouse collection */ + /** @description Failed to fetch telemetry collection */ 500: { content: never } } } - /** Delete a warehouse collection */ - 'v1-delete-a-warehouse-collection': { + /** Delete a telemetry collection */ + 'v1-delete-a-telemetry-collection': { responses: { 200: { content: { @@ -11879,14 +12185,14 @@ export interface operations { 403: { content: never } - /** @description Failed to delete warehouse collection */ + /** @description Failed to delete telemetry collection */ 500: { content: never } } } - /** Update a warehouse collection */ - 'v1-update-a-warehouse-collection': { + /** Update a telemetry collection */ + 'v1-update-a-telemetry-collection': { responses: { 200: { content: { @@ -11896,7 +12202,24 @@ export interface operations { 403: { content: never } - /** @description Failed to update warehouse collection */ + /** @description Failed to update telemetry collection */ + 500: { + content: never + } + } + } + /** Get a telemetry collection schema */ + 'v1-get-a-telemetry-collection-schema': { + responses: { + 200: { + content: { + 'application/json': components['schemas']['LFSource'] + } + } + 403: { + content: never + } + /** @description Failed to fetch telemetry collection schema */ 500: { content: never } @@ -12912,6 +13235,57 @@ export interface operations { } } } + /** Gets disk autoscale config */ + DiskController_getDiskAutoscaleConfig: { + parameters: { + path: { + /** @description Project ref */ + ref: string + } + } + responses: { + 200: { + content: { + 'application/json': components['schemas']['DiskAutoscaleConfig'] + } + } + 403: { + content: never + } + /** @description Failed to get project disk autoscale config */ + 500: { + content: never + } + } + } + /** Updates disk autoscale config */ + DiskController_updateDiskAutoscaleConfig: { + parameters: { + path: { + /** @description Project ref */ + ref: string + } + } + requestBody: { + content: { + 'application/json': components['schemas']['UpdateDiskAutoscaleConfig'] + } + } + responses: { + 201: { + content: { + 'application/json': components['schemas']['DiskAutoscaleConfig'] + } + } + 403: { + content: never + } + /** @description Failed to update project disk autoscale config */ + 500: { + content: never + } + } + } /** Get disk utilization */ DiskController_getDiskUtilization: { parameters: { @@ -13246,6 +13620,34 @@ export interface operations { } } } + /** Updates the given project sensitivity */ + SensitivityController_updateProjectSensitivity: { + parameters: { + path: { + /** @description Project ref */ + ref: string + } + } + requestBody: { + content: { + 'application/json': components['schemas']['MarkSensitiveBody'] + } + } + responses: { + 200: { + content: { + 'application/json': components['schemas']['ProjectSensitivityResponse'] + } + } + 403: { + content: never + } + /** @description Failed to update project */ + 500: { + content: never + } + } + } /** Gets project's status */ ProjectStatusController_getStatus: { parameters: { @@ -14479,6 +14881,24 @@ export interface operations { } } } + /** Deletes organization */ + OrganizationSlugSystemController_deleteOrganization: { + parameters: { + path: { + /** @description Organization slug */ + slug: string + } + } + responses: { + 200: { + content: never + } + /** @description Failed to delete organization */ + 500: { + content: never + } + } + } /** Gets the current subscription */ OrgSubscriptionSystemController_getSubscription: { parameters: { @@ -14627,6 +15047,20 @@ export interface operations { } } } + /** Deletes the given project */ + ProjectRefSystemController_deleteProject: { + parameters: { + path: { + /** @description Project ref */ + ref: string + } + } + responses: { + 204: { + content: never + } + } + } /** Updates project addon */ AddonsController_updateAddon: { parameters: { @@ -14669,6 +15103,71 @@ export interface operations { } } } + /** Gets the current email restrictions for a project. */ + ProjectEmailRestrictionsController_getEmailRestrictions: { + parameters: { + path: { + /** @description Project ref */ + ref: string + } + } + responses: { + 200: { + content: { + 'application/json': components['schemas']['EmailRestrictionsResponseBody'] + } + } + /** @description Failed to retrieve email restrictions. */ + 500: { + content: never + } + } + } + /** Updates the current email restrictions for a project. */ + ProjectEmailRestrictionsController_updateEmailRestrictions: { + parameters: { + path: { + /** @description Project ref */ + ref: string + } + } + requestBody: { + content: { + 'application/json': components['schemas']['UpdateRestrictionsRequestBody'] + } + } + responses: { + 200: { + content: { + 'application/json': components['schemas']['EmailRestrictionsResponseBody'] + } + } + /** @description Failed to update email restrictions. */ + 500: { + content: never + } + } + } + /** Removes the current email restrictions for a project. */ + ProjectEmailRestrictionsController_deleteEmailRestrictions: { + parameters: { + path: { + /** @description Project ref */ + ref: string + } + } + responses: { + 200: { + content: { + 'application/json': components['schemas']['EmailRestrictionsResponseBody'] + } + } + /** @description Failed to remove email restrictions. */ + 500: { + content: never + } + } + } /** Handle update project jwt on completion */ ProjectUpdateJwtController_completeUpdateJwt: { parameters: { @@ -14757,6 +15256,57 @@ export interface operations { } } } + /** Gets disk autoscale config */ + SystemProjectDiskController_getDiskAutoscaleConfig: { + parameters: { + path: { + /** @description Project ref */ + ref: string + } + } + responses: { + 200: { + content: { + 'application/json': components['schemas']['DiskAutoscaleConfig'] + } + } + 403: { + content: never + } + /** @description Failed to get project disk autoscale config */ + 500: { + content: never + } + } + } + /** Updates disk autoscale config */ + SystemProjectDiskController_updateDiskAutoscaleConfig: { + parameters: { + path: { + /** @description Project ref */ + ref: string + } + } + requestBody: { + content: { + 'application/json': components['schemas']['UpdateDiskAutoscaleConfig'] + } + } + responses: { + 201: { + content: { + 'application/json': components['schemas']['DiskAutoscaleConfig'] + } + } + 403: { + content: never + } + /** @description Failed to update project disk autoscale config */ + 500: { + content: never + } + } + } /** Get disk utilization */ SystemProjectDiskController_getDiskUtilization: { parameters: { @@ -14833,6 +15383,7 @@ export interface operations { import_map?: boolean entrypoint_path?: string import_map_path?: string + resource_multiplier?: string } path: { /** @description Project ref */ @@ -14913,6 +15464,7 @@ export interface operations { import_map?: boolean entrypoint_path?: string import_map_path?: string + resource_multiplier?: string } path: { /** @description Project ref */ @@ -15134,6 +15686,18 @@ export interface operations { } } } + /** Reports email abuse from a postmark */ + ProjectEmailAbuseController_reportEmailAbuseWebhookPostmark: { + responses: { + 201: { + content: never + } + /** @description Failed to report email abuse */ + 500: { + content: never + } + } + } /** Processes Stripe event */ StripeWebhooksController_processEvent: { parameters: { @@ -15328,7 +15892,7 @@ export interface operations { 'v1-create-an-organization': { requestBody: { content: { - 'application/json': components['schemas']['CreateOrganizationBodyV1'] + 'application/json': components['schemas']['CreateOrganizationV1Dto'] } } responses: { @@ -15381,7 +15945,7 @@ export interface operations { responses: { 200: { content: { - 'application/json': components['schemas']['V1ProjectResponse'][] + 'application/json': components['schemas']['V1ProjectWithDatabaseResponse'][] } } } @@ -15390,7 +15954,7 @@ export interface operations { 'v1-create-a-project': { requestBody: { content: { - 'application/json': components['schemas']['V1CreateProjectBody'] + 'application/json': components['schemas']['V1CreateProjectBodyDto'] } } responses: { @@ -15412,7 +15976,7 @@ export interface operations { responses: { 200: { content: { - 'application/json': components['schemas']['V1ProjectResponse'] + 'application/json': components['schemas']['V1ProjectWithDatabaseResponse'] } } /** @description Failed to retrieve project */ @@ -15443,6 +16007,9 @@ export interface operations { /** Get project api keys */ 'v1-get-project-api-keys': { parameters: { + query: { + reveal: boolean + } path: { /** @description Project ref */ ref: string @@ -15459,6 +16026,9 @@ export interface operations { /** [Alpha] Creates a new API key for the project */ ApiKeysController_createApiKey: { parameters: { + query: { + reveal: boolean + } path: { /** @description Project ref */ ref: string @@ -15477,9 +16047,32 @@ export interface operations { } } } + /** [Alpha] Get API key */ + ApiKeysController_getApiKey: { + parameters: { + query: { + reveal: boolean + } + path: { + /** @description Project ref */ + ref: string + id: string + } + } + responses: { + 200: { + content: { + 'application/json': components['schemas']['ApiKeyResponse'] + } + } + } + } /** [Alpha] Deletes an API key for the project */ ApiKeysController_deleteApiKey: { parameters: { + query: { + reveal: boolean + } path: { /** @description Project ref */ ref: string @@ -15500,6 +16093,9 @@ export interface operations { /** [Alpha] Updates an API key for the project */ ApiKeysController_updateApiKey: { parameters: { + query: { + reveal: boolean + } path: { /** @description Project ref */ ref: string @@ -16804,7 +17400,7 @@ export interface operations { 'v1-get-a-snippet': { parameters: { path: { - id: string + id: Record } } responses: { diff --git a/packages/ui-patterns/TimestampInfo/index.tsx b/packages/ui-patterns/TimestampInfo/index.tsx index 505a610611e..c7f49108553 100644 --- a/packages/ui-patterns/TimestampInfo/index.tsx +++ b/packages/ui-patterns/TimestampInfo/index.tsx @@ -84,12 +84,22 @@ export const TimestampInfo = ({ setCopied(false) }, 1000) }} - className={cn('grid grid-cols-2 gap-2 hover:bg-surface-100 px-2 py-1', { - 'bg-surface-100': copied, - })} + className={cn( + 'relative cursor-default grid grid-cols-2 gap-2 hover:bg-surface-100 px-2 py-1', + { + 'bg-surface-100': copied, + } + )} > {label}: - {copied ? 'Copied!' : value} +
+ {copied && ( + + Copied! + + )} + {value} +
) }