mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
## 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
82 lines
2.5 KiB
TypeScript
82 lines
2.5 KiB
TypeScript
import { beforeEach, describe, expect, it } from 'vitest'
|
|
|
|
import {
|
|
clearPersistedTransitionStartTime,
|
|
getPersistedTransitionStartTime,
|
|
getRemainingTransitionTimeMs,
|
|
hoursToMilliseconds,
|
|
minutesToMilliseconds,
|
|
} from './project-transition-state'
|
|
|
|
describe('project-transition-state', () => {
|
|
beforeEach(() => {
|
|
window.localStorage.clear()
|
|
})
|
|
|
|
describe('getPersistedTransitionStartTime', () => {
|
|
it('stores the current time when no start time exists', () => {
|
|
const startTime = getPersistedTransitionStartTime('transition-key', 1_000)
|
|
|
|
expect(startTime).toBe(1_000)
|
|
expect(window.localStorage.getItem('transition-key')).toBe('1000')
|
|
})
|
|
|
|
it('reuses an existing stored start time', () => {
|
|
window.localStorage.setItem('transition-key', '2_000'.replace('_', ''))
|
|
|
|
const startTime = getPersistedTransitionStartTime('transition-key', 3_000)
|
|
|
|
expect(startTime).toBe(2_000)
|
|
expect(window.localStorage.getItem('transition-key')).toBe('2000')
|
|
})
|
|
|
|
it('replaces malformed stored values with the current time', () => {
|
|
window.localStorage.setItem('transition-key', 'not-a-number')
|
|
|
|
const startTime = getPersistedTransitionStartTime('transition-key', 3_000)
|
|
|
|
expect(startTime).toBe(3_000)
|
|
expect(window.localStorage.getItem('transition-key')).toBe('3000')
|
|
})
|
|
|
|
it('replaces stale stored values when they exceed the allowed max age', () => {
|
|
window.localStorage.setItem('transition-key', '1_000'.replace('_', ''))
|
|
|
|
const startTime = getPersistedTransitionStartTime(
|
|
'transition-key',
|
|
hoursToMilliseconds(30),
|
|
hoursToMilliseconds(24)
|
|
)
|
|
|
|
expect(startTime).toBe(hoursToMilliseconds(30))
|
|
expect(window.localStorage.getItem('transition-key')).toBe(String(hoursToMilliseconds(30)))
|
|
})
|
|
})
|
|
|
|
it('removes a persisted transition start time', () => {
|
|
window.localStorage.setItem('transition-key', '1000')
|
|
|
|
clearPersistedTransitionStartTime('transition-key')
|
|
|
|
expect(window.localStorage.getItem('transition-key')).toBeNull()
|
|
})
|
|
|
|
it('returns the remaining threshold time and clamps at zero', () => {
|
|
expect(
|
|
getRemainingTransitionTimeMs({
|
|
startTimeMs: 1_000,
|
|
thresholdMs: minutesToMilliseconds(10),
|
|
now: 4_000,
|
|
})
|
|
).toBe(minutesToMilliseconds(10) - 3_000)
|
|
|
|
expect(
|
|
getRemainingTransitionTimeMs({
|
|
startTimeMs: 1_000,
|
|
thresholdMs: minutesToMilliseconds(10),
|
|
now: minutesToMilliseconds(10) + 5_000,
|
|
})
|
|
).toBe(0)
|
|
})
|
|
})
|