mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 09:55:06 +03:00
## What kind of change does this PR introduce? Feature and docs update. ## What is the current behavior? The Dashboard lists Pipelines destinations under Database > Replication. Read replicas have moved to Infrastructure, but the temporary notices remain on the destinations page and new destination sheet. Closes PIPE-1021. ## What is the new behavior? The canonical Dashboard routes are Database > Pipelines, while legacy Replication list and detail URLs permanently redirect to the equivalent Pipelines routes. Navigation, command palette, shortcuts, pipeline links, docs, and current marketing copy use Pipelines. Read-replica notices and their obsolete dismissal state are removed. | Before | After | | --- | --- | | <img width="1024" height="759" alt="Replication Database Agua Basket Supabase" src="https://github.com/user-attachments/assets/53f9f565-1ed1-43e9-a7d9-b66b2a47e948" /> | <img width="1024" height="759" alt="2540" src="https://github.com/user-attachments/assets/14ab2d61-d01c-483f-9d4f-0ac286dae159" /> | The Management API, pipeline behaviour, replication logs, and Postgres replication terminology remain unchanged. ## To test - Open `/project/<ref>/database/pipelines` and confirm the Database navigation, page header, and pipeline breadcrumb say Pipelines. - Open `/project/<ref>/database/replication?source=bookmark#destinations` and a legacy pipeline detail URL. Confirm each redirects to the matching Pipelines URL while preserving parameters and fragments. - From the Pipelines page, open Add destination. Confirm no read-replica migration notice appears. - Open the Pipelines guide and confirm its Dashboard steps lead to Database > Pipelines. ## Before merge - [ ] Get changelog entry reviewed https://github.com/supabase/changelog/pull/262 and prepare to merge simultaneously <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added dedicated **Database > Pipelines** pages for pipeline lists and details. - Added permanent redirects from legacy Replication URLs to their corresponding Pipelines pages. - Read replica management links now open **Settings > Infrastructure**. - **Documentation** - Updated Pipelines setup, monitoring, troubleshooting, and usage guidance to reference the current dashboard locations. - Updated Realtime guidance to use **Database > Publications**. - **Updates** - Renamed dashboard navigation, breadcrumbs, commands, and keyboard shortcuts from **Replication** to **Pipelines**. - Removed the “Read replicas have moved” notification. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
246 lines
7.3 KiB
TypeScript
246 lines
7.3 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 replication list route to Pipelines while preserving query and hash', () => {
|
|
expect(
|
|
matchRedirect({
|
|
pathname: '/project/abc/database/replication',
|
|
search: { source: 'bookmark' },
|
|
isPlatform: true,
|
|
hash: 'destinations',
|
|
})
|
|
).toEqual({
|
|
destination: '/project/abc/database/pipelines?source=bookmark#destinations',
|
|
permanent: true,
|
|
})
|
|
})
|
|
|
|
it('redirects legacy pipeline detail URLs to Pipelines', () => {
|
|
expect(
|
|
matchRedirect({
|
|
pathname: '/project/abc/database/replication/pipeline-1',
|
|
search: {},
|
|
isPlatform: true,
|
|
})
|
|
).toEqual({
|
|
destination: '/project/abc/database/pipelines/pipeline-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()
|
|
})
|
|
})
|
|
|
|
describe('matchRedirect maintenance mode', () => {
|
|
it('sends every other path to /maintenance when enabled', () => {
|
|
expect(
|
|
matchRedirect({
|
|
pathname: '/project/abc/editor',
|
|
search: {},
|
|
isPlatform: true,
|
|
maintenanceMode: true,
|
|
})
|
|
).toEqual({ destination: '/maintenance', permanent: false })
|
|
})
|
|
|
|
it('carries query and hash onto /maintenance', () => {
|
|
expect(
|
|
matchRedirect({
|
|
pathname: '/project/abc/editor',
|
|
search: { a: '1' },
|
|
isPlatform: true,
|
|
maintenanceMode: true,
|
|
hash: 'section',
|
|
})
|
|
).toEqual({ destination: '/maintenance?a=1#section', permanent: false })
|
|
})
|
|
|
|
it('leaves /maintenance and /img reachable when enabled', () => {
|
|
expect(
|
|
matchRedirect({
|
|
pathname: '/maintenance',
|
|
search: {},
|
|
isPlatform: true,
|
|
maintenanceMode: true,
|
|
})
|
|
).toBeNull()
|
|
expect(
|
|
matchRedirect({
|
|
pathname: '/img/supabase-logo.svg',
|
|
search: {},
|
|
isPlatform: true,
|
|
maintenanceMode: true,
|
|
})
|
|
).toBeNull()
|
|
})
|
|
|
|
it('bounces /maintenance back to / when disabled', () => {
|
|
expect(matchRedirect({ pathname: '/maintenance', search: {}, isPlatform: true })).toEqual({
|
|
destination: '/',
|
|
permanent: false,
|
|
})
|
|
expect(
|
|
matchRedirect({
|
|
pathname: '/maintenance',
|
|
search: {},
|
|
isPlatform: true,
|
|
maintenanceMode: false,
|
|
})
|
|
).toEqual({ destination: '/', permanent: false })
|
|
})
|
|
})
|