Files
supabase/apps/studio/redirects.shared.test.ts
Danny White 7fd7ace118 feat(studio): add Infrastructure replica detail route and redirects (#49045)
## What kind of change does this PR introduce?

Feature. Stack 3 of 5 for
[PIPE-1007](https://linear.app/supabase/issue/PIPE-1007/move-read-replicas-out-of-replication-into-infrastructure).

## What is the current behavior?

Replica detail lives at `/database/replication/replica/:id`.

## What is the new behavior?

Detail moves to `/settings/infrastructure/replica/:id`. Old URLs
redirect. List and diagram View/Manage replica links follow.

## Additional context

Stacked on [#49044](https://github.com/supabase/supabase/pull/49044).
Please review, but do not merge. Merge 2→5 in succession once they are
all reviewed, so users never sit on a split create/list vs detail path.

Replication still lists and creates replicas until
[#49046](https://github.com/supabase/supabase/pull/49046).

## To test

`infrastructure:read_replicas` is an enabled-feature, on by default.
There is no Feature Preview or ConfigCat switch. You should already see
the Infrastructure Read replicas section. If you do not, your profile
lists `infrastructure:read_replicas` in `disabled_features`.

From
[Infrastructure](https://studio-staging-git-danny-pipe-1007-03-detail-redirects-supabase.vercel.app/dashboard/project/_/settings/infrastructure),
open View replica on a row. Confirm you land on
`/settings/infrastructure/replica/:id`. If you have an old bookmark,
`/database/replication/replica/:id` should redirect there.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added read replica management to Infrastructure settings, including
replica creation, status monitoring, details, restart, and removal
actions.
* Added eligibility guidance and estimated pricing details during
replica setup.
* Added support for topology and replica information within
infrastructure configuration.
* **Improvements**
* Legacy database replication links now permanently redirect to the
corresponding Infrastructure pages.
* Added clearer empty, loading, error, and transition states for read
replica management.
* **Tests**
* Expanded coverage for replica navigation, redirects, eligibility
warnings, and empty states.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-21 12:44:41 +10:00

160 lines
5.0 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { matchRedirect, preserveQueryAndHash } from './redirects.shared'
describe('preserveQueryAndHash', () => {
it('carries incoming query params onto the destination', () => {
expect(preserveQueryAndHash('/org', { foo: '1', bar: 'x' })).toBe('/org?foo=1&bar=x')
})
it('returns the destination untouched when there is nothing to carry', () => {
expect(preserveQueryAndHash('/org', {})).toBe('/org')
expect(preserveQueryAndHash('/org', new URLSearchParams())).toBe('/org')
})
it('drops params consumed by the matched rule', () => {
expect(
preserveQueryAndHash(
'/new/new-project',
{ next: 'new-project', a: '1' },
{
consumedKeys: ['next'],
}
)
).toBe('/new/new-project?a=1')
})
it("lets the destination's own params win on conflict", () => {
expect(
preserveQueryAndHash('/org/_/billing?panel=subscriptionPlan', { panel: 'other', x: '1' })
).toBe('/org/_/billing?panel=subscriptionPlan&x=1')
})
it('preserves repeated keys and array values', () => {
expect(preserveQueryAndHash('/dest', new URLSearchParams('f=a&f=b'))).toBe('/dest?f=a&f=b')
expect(preserveQueryAndHash('/dest', { f: ['a', 'b'] })).toBe('/dest?f=a&f=b')
})
it('carries the incoming hash', () => {
expect(preserveQueryAndHash('/dest', { a: '1' }, { hash: 'section' })).toBe('/dest?a=1#section')
expect(preserveQueryAndHash('/dest', {}, { hash: 'section' })).toBe('/dest#section')
})
it("lets the destination's own hash win over the incoming one", () => {
expect(preserveQueryAndHash('/org/slug/billing#invoices', { a: '1' }, { hash: 'other' })).toBe(
'/org/slug/billing?a=1#invoices'
)
})
it('skips undefined values in a record search', () => {
expect(preserveQueryAndHash('/dest', { a: undefined, b: '1' })).toBe('/dest?b=1')
})
})
describe('matchRedirect query/hash preservation', () => {
it('redirects the legacy compute and disk route while preserving query and hash', () => {
expect(
matchRedirect({
pathname: '/project/abc/settings/compute-and-disk',
search: { upgrade: 'micro' },
isPlatform: true,
hash: 'disk',
})
).toEqual({
destination: '/project/abc/settings/infrastructure?upgrade=micro#disk',
permanent: true,
})
})
it('redirects legacy replication replica detail to infrastructure', () => {
expect(
matchRedirect({
pathname: '/project/abc/database/replication/replica/replica-1',
search: {},
isPlatform: true,
})
).toEqual({
destination: '/project/abc/settings/infrastructure/replica/replica-1',
permanent: true,
})
})
it('redirects the legacy compute billing panel to the CPU section', () => {
expect(
matchRedirect({
pathname: '/project/abc/settings/billing/subscription',
search: { panel: 'computeInstance', source: 'banner' },
isPlatform: true,
})
).toEqual({
destination: '/project/abc/settings/infrastructure?source=banner#cpu',
permanent: true,
})
})
it('carries the incoming query and hash through a plain rule', () => {
expect(
matchRedirect({
pathname: '/project/abc/sql/quickstarts',
search: { template: 'countries', flag: 'true' },
isPlatform: true,
hash: 'top',
})
).toEqual({
destination: '/project/abc/sql/examples?template=countries&flag=true#top',
permanent: true,
})
})
it('consumes `has` query keys but keeps the rest', () => {
expect(
matchRedirect({
pathname: '/',
search: { next: 'new-project', projectName: 'foo' },
isPlatform: true,
})
).toEqual({ destination: '/new/new-project?projectName=foo', permanent: false })
})
it("keeps the destination's own params when the incoming query repeats them", () => {
expect(
matchRedirect({
pathname: '/project/abc/settings/billing/subscription',
search: { panel: 'pitr', source: 'email' },
isPlatform: true,
})
).toEqual({
destination: '/project/abc/settings/addons?panel=pitr&source=email',
permanent: true,
})
})
it('keeps a destination hash (e.g. billing#invoices) over the incoming hash', () => {
expect(
matchRedirect({
pathname: '/org/my-org/invoices',
search: {},
isPlatform: true,
hash: 'ignored',
})
).toEqual({ destination: '/org/my-org/billing#invoices', permanent: true })
})
it('leaves plain redirects without query or hash untouched', () => {
expect(matchRedirect({ pathname: '/', search: {}, isPlatform: true })).toEqual({
destination: '/org',
permanent: false,
})
expect(matchRedirect({ pathname: '/', search: {}, isPlatform: false })).toEqual({
destination: '/project/default',
permanent: false,
})
})
it('still returns null for non-matching paths', () => {
expect(
matchRedirect({ pathname: '/project/abc/editor', search: { a: '1' }, isPlatform: true })
).toBeNull()
})
})