mirror of
https://github.com/supabase/supabase.git
synced 2026-05-10 02:39:56 -04:00
73692b0a4d
## What kind of change does this PR introduce? Bug fix / UX improvement for long-running project transitions. Resolves DEPR-362. ## What is the current behaviour? - `PausingState` does not preserve elapsed time across refreshes, so the stuck escalation can disappear for the same user. - `RestoringState` relies on weaker frontend heuristics and always showed a support CTA in the footer even before the restore was clearly long-running. ## What is the new behaviour? - `PausingState` - Persists a per-project pause start time in local storage so the stuck CTA survives refreshes in the same browser. - Escalates after 10 minutes. - Clears the stored timer when pausing succeeds or fails. - `RestoringState` - Persists a per-project restore start time in local storage so the stuck CTA survives refreshes in the same browser. - Removes the always-visible footer CTA and only escalates once restoration is genuinely long-running. - Computes the long-running threshold from volume size using a shared restore estimate: `max(10, ceil(estimateRestoreTime(sizeGb) * 1.5))`. - Clears the stored timer when restoration succeeds or fails. - Shared changes - Extracts reusable transition timing helpers and restore estimate helpers with unit tests. - Reuses the same restore estimate formula for branch restore timing and restore escalation, so the two do not drift. | `PausingState` | `RestoringState` | | --- | --- | | <img width="1570" height="906" alt="Krosno Toolshed Supabase-C6D7E29F-C38D-43E1-8AF9-C612B6A2FD8D" src="https://github.com/user-attachments/assets/e0bd9434-09b6-4cf6-bffa-07a0ddcdf5db" /> | <img width="1570" height="906" alt="Krosno Toolshed Supabase-51F4763D-B798-4B41-A92D-43B3CF8ECDAF" src="https://github.com/user-attachments/assets/d0e47356-dcc3-42aa-b602-802a35249a16" /> | ## Additional context - This PR intentionally stays frontend-only. - We are not exposing backend lifecycle timestamps here; local storage is the stopgap to improve the same-browser experience now. - If you need to test the frontend blocker states locally, use [`dnywh/chore/depr-362-blocker-preview-mocks`](https://github.com/supabase/supabase/tree/dnywh/chore/depr-362-blocker-preview-mocks) and append one of the following query params to a project URL: - `?mockProjectBlockingState=pausing` - `?mockProjectBlockingState=pausing-long-running` - `?mockProjectBlockingState=restoring` - `?mockProjectBlockingState=restoring-long-running` - I know these two views are quite differently stylistically, and will consolidate later - References DEPR-434
67 lines
2.1 KiB
TypeScript
67 lines
2.1 KiB
TypeScript
import {
|
|
DISK_LIMITS,
|
|
DISK_PRICING,
|
|
DiskType,
|
|
PLAN_DETAILS,
|
|
} from '../DiskManagement/ui/DiskManagement.constants'
|
|
import { DiskAttributesData } from '@/data/config/disk-attributes-query'
|
|
import { DesiredInstanceSize, instanceSizeSpecs } from '@/data/projects/new-project.constants'
|
|
import { estimateRestoreTimeFromSizeGb } from '@/lib/restore-estimate'
|
|
|
|
// Ref: https://supabase.com/docs/guides/platform/compute-and-disk
|
|
const maxDiskForCompute = new Map([
|
|
[10, instanceSizeSpecs.micro],
|
|
[50, instanceSizeSpecs.small],
|
|
[100, instanceSizeSpecs.medium],
|
|
[200, instanceSizeSpecs.large],
|
|
[500, instanceSizeSpecs.xlarge],
|
|
[1_000, instanceSizeSpecs['2xlarge']],
|
|
[2_000, instanceSizeSpecs['4xlarge']],
|
|
[4_000, instanceSizeSpecs['8xlarge']],
|
|
[6_000, instanceSizeSpecs['12xlarge']],
|
|
[10_000, instanceSizeSpecs['16xlarge']],
|
|
])
|
|
|
|
export const estimateComputeSize = (
|
|
projectDiskSize: number,
|
|
branchComputeSize?: DesiredInstanceSize
|
|
) => {
|
|
if (branchComputeSize) {
|
|
return instanceSizeSpecs[branchComputeSize]
|
|
}
|
|
// Fallback to estimating based on volume size
|
|
for (const [disk, compute] of maxDiskForCompute) {
|
|
if (projectDiskSize <= disk) {
|
|
return compute
|
|
}
|
|
}
|
|
return instanceSizeSpecs['24xlarge']
|
|
}
|
|
|
|
export const estimateDiskCost = (disk: DiskAttributesData['attributes']) => {
|
|
const diskType = disk.type as DiskType
|
|
|
|
const pricing = DISK_PRICING[diskType]
|
|
const includedGB = PLAN_DETAILS['pro'].includedDiskGB[diskType]
|
|
const priceSize = Math.max(disk.size_gb - includedGB, 0) * pricing.storage
|
|
const includedIOPS = DISK_LIMITS[diskType].includedIops
|
|
const priceIOPS = Math.max(disk.iops - includedIOPS, 0) * pricing.iops
|
|
|
|
const priceThroughput =
|
|
diskType === DiskType.GP3 && 'throughput_mbps' in disk
|
|
? Math.max(disk.throughput_mbps - DISK_LIMITS[DiskType.GP3].includedThroughput, 0) *
|
|
DISK_PRICING[DiskType.GP3].throughput
|
|
: 0
|
|
|
|
return {
|
|
total: priceSize + priceIOPS + priceThroughput,
|
|
size: priceSize,
|
|
iops: priceIOPS,
|
|
throughput: priceThroughput,
|
|
}
|
|
}
|
|
|
|
export const estimateRestoreTime = (disk: DiskAttributesData['attributes']) => {
|
|
return estimateRestoreTimeFromSizeGb(disk.size_gb)
|
|
}
|