mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
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 -->
This commit is contained in:
1 parent
e9abda6c79
commit
7fd7ace118
12 files changed
+271
-199
No files matched your search
@@ -180,7 +180,7 @@ These are the layout-only TanStack files. Most hold a single product layout comp
|
||||
- [x] A `routes/project/$ref/database/publications/$id.tsx` ← `pages/project/[ref]/database/publications/[id].tsx`
|
||||
- [x] A `routes/project/$ref/database/replication/index.tsx` ← `pages/project/[ref]/database/replication/index.tsx`
|
||||
- [x] A `routes/project/$ref/database/replication/$pipelineId.tsx` ← `pages/project/[ref]/database/replication/[pipelineId].tsx`
|
||||
- [x] A `routes/project/$ref/database/replication/replica/$replicaId.tsx` ← `pages/project/[ref]/database/replication/replica/[replicaId].tsx`
|
||||
- [x] A `routes/project/$ref/database/replication/replica/$replicaId.tsx` ← `pages/project/[ref]/database/replication/replica/[replicaId].tsx` (redirects to Infrastructure)
|
||||
- [x] A `routes/project/$ref/database/triggers/index.tsx` ← `pages/project/[ref]/database/triggers/index.tsx`
|
||||
- [x] A `routes/project/$ref/database/triggers/data.tsx` ← `pages/project/[ref]/database/triggers/data.tsx` (sub-shell at `database/triggers.tsx` provides PageLayout + nav, parent shell provides DatabaseLayout)
|
||||
- [x] A `routes/project/$ref/database/triggers/event.tsx` ← `pages/project/[ref]/database/triggers/event.tsx` (same as data)
|
||||
@@ -296,7 +296,8 @@ These are the layout-only TanStack files. Most hold a single product layout comp
|
||||
- [x] A `routes/project/$ref/settings/addons.tsx` ← `pages/project/[ref]/settings/addons.tsx`
|
||||
- [x] A `routes/project/$ref/settings/api.tsx` ← `pages/project/[ref]/settings/api.tsx` (sets `skipSettingsLayout: true` — page is a useEffect redirect)
|
||||
- [x] A `routes/project/$ref/settings/dashboard.tsx` ← `pages/project/[ref]/settings/dashboard.tsx`
|
||||
- [x] A `routes/project/$ref/settings/infrastructure.tsx` ← `pages/project/[ref]/settings/infrastructure.tsx`
|
||||
- [x] A `routes/project/$ref/settings/infrastructure/index.tsx` ← `pages/project/[ref]/settings/infrastructure.tsx`
|
||||
- [x] A `routes/project/$ref/settings/infrastructure/replica/$replicaId.tsx` ← `pages/project/[ref]/settings/infrastructure/replica/[replicaId].tsx`
|
||||
- [x] A `routes/project/$ref/settings/integrations.tsx` ← `pages/project/[ref]/settings/integrations.tsx`
|
||||
- [x] A `routes/project/$ref/settings/log-drains.tsx` ← `pages/project/[ref]/settings/log-drains.tsx`
|
||||
- [x] A `routes/project/$ref/settings/api-keys/index.tsx` ← `pages/project/[ref]/settings/api-keys/index.tsx` (under `api-keys.tsx` sub-shell with ApiKeysLayout)
|
||||
|
||||
+2
-3
@@ -33,6 +33,7 @@ import {
|
||||
} from './InstanceConfiguration.constants'
|
||||
import { formatSeconds } from './InstanceConfiguration.utils'
|
||||
import { metricColor } from './InstanceNode.utils'
|
||||
import { getReadReplicaPath } from '@/components/interfaces/Settings/Infrastructure/Infrastructure.utils'
|
||||
import { REPLICA_STATUS } from '@/components/interfaces/Settings/Infrastructure/ReadReplicas/ReadReplicas.constants'
|
||||
import { SparkBar } from '@/components/ui/SparkBar'
|
||||
import {
|
||||
@@ -417,9 +418,7 @@ export const ReplicaNode = ({ data }: NodeProps<Node<ReplicaNodeData>>) => {
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem className="gap-x-2">
|
||||
<Link href={`/project/${ref}/database/replication/replica/${id}`}>
|
||||
Manage replica
|
||||
</Link>
|
||||
<Link href={getReadReplicaPath(ref, id)}>Manage replica</Link>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
+2
-3
@@ -27,6 +27,7 @@ import { TimestampInfo } from 'ui-patterns/TimestampInfo'
|
||||
|
||||
import { AVAILABLE_REPLICA_REGIONS } from './InstanceConfiguration.constants'
|
||||
import GeographyData from './MapData.json'
|
||||
import { getReadReplicaPath } from '@/components/interfaces/Settings/Infrastructure/Infrastructure.utils'
|
||||
import { REPLICA_STATUS } from '@/components/interfaces/Settings/Infrastructure/ReadReplicas/ReadReplicas.constants'
|
||||
import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query'
|
||||
import { formatDatabaseID } from '@/data/read-replicas/replicas.utils'
|
||||
@@ -303,9 +304,7 @@ const MapView = () => {
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem className="gap-x-2">
|
||||
<Link
|
||||
href={`/project/${ref}/database/replication/replica/${database.identifier}`}
|
||||
>
|
||||
<Link href={getReadReplicaPath(ref, database.identifier)}>
|
||||
Manage replica
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
+2
-7
@@ -24,6 +24,7 @@ import { DropReplicaConfirmationModal } from './DropReplicaConfirmationModal'
|
||||
import { REPLICA_STATUS } from './ReadReplicas.constants'
|
||||
import { getIsInTransition, getStatusLabel } from './ReadReplicas.utils'
|
||||
import { RestartReplicaConfirmationModal } from './RestartReplicaConfirmationModal'
|
||||
import { getReadReplicaPath } from '@/components/interfaces/Settings/Infrastructure/Infrastructure.utils'
|
||||
import { useReplicationLagQuery } from '@/data/read-replicas/replica-lag-query'
|
||||
import { type Database } from '@/data/read-replicas/replicas-query'
|
||||
import { formatDatabaseID } from '@/data/read-replicas/replicas.utils'
|
||||
@@ -105,10 +106,6 @@ export const ReadReplicaRow = ({ replica, onUpdateReplica }: ReadReplicaRow) =>
|
||||
)}
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<Minus size={18} className="text-foreground-lighter" />
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
<Button
|
||||
@@ -117,9 +114,7 @@ export const ReadReplicaRow = ({ replica, onUpdateReplica }: ReadReplicaRow) =>
|
||||
className="relative"
|
||||
disabled={status === 'GOING_DOWN'}
|
||||
>
|
||||
<Link href={`/project/${ref}/database/replication/replica/${replica.identifier}`}>
|
||||
View replica
|
||||
</Link>
|
||||
<Link href={getReadReplicaPath(ref, replica.identifier)}>View replica</Link>
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
|
||||
@@ -1,186 +1,34 @@
|
||||
import { useParams } from 'common'
|
||||
import { Database } from 'icons'
|
||||
import { Loader2, Trash } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { AWS_REGIONS } from 'shared-data'
|
||||
import { Badge, Button } from 'ui'
|
||||
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
|
||||
import { useEffect } from 'react'
|
||||
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
|
||||
|
||||
import { DropReplicaConfirmationModal } from '@/components/interfaces/Settings/Infrastructure/ReadReplicas/DropReplicaConfirmationModal'
|
||||
import { ReadReplicaDetails } from '@/components/interfaces/Settings/Infrastructure/ReadReplicas/ReadReplicaDetails'
|
||||
import { REPLICA_STATUS } from '@/components/interfaces/Settings/Infrastructure/ReadReplicas/ReadReplicas.constants'
|
||||
import {
|
||||
getIsInTransition,
|
||||
getStatusLabel,
|
||||
} from '@/components/interfaces/Settings/Infrastructure/ReadReplicas/ReadReplicas.utils'
|
||||
import { RestartReplicaConfirmationModal } from '@/components/interfaces/Settings/Infrastructure/ReadReplicas/RestartReplicaConfirmationModal'
|
||||
import { getReadReplicaPath } from '@/components/interfaces/Settings/Infrastructure/Infrastructure.utils'
|
||||
import DatabaseLayout from '@/components/layouts/DatabaseLayout/DatabaseLayout'
|
||||
import { DefaultLayout } from '@/components/layouts/DefaultLayout'
|
||||
import { PageLayout } from '@/components/layouts/PageLayout/PageLayout'
|
||||
import { ScaffoldDescription, ScaffoldTitle } from '@/components/layouts/Scaffold'
|
||||
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
|
||||
import CopyButton from '@/components/ui/CopyButton'
|
||||
import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query'
|
||||
import {
|
||||
ReplicaInitializationStatus,
|
||||
useReadReplicasStatusesQuery,
|
||||
} from '@/data/read-replicas/replicas-status-query'
|
||||
import type { NextPageWithLayout } from '@/types'
|
||||
|
||||
const DatabaseReadReplicaPage: NextPageWithLayout = () => {
|
||||
/** @deprecated Redirects to Settings → Infrastructure replica detail. */
|
||||
const DatabaseReadReplicaRedirectPage: NextPageWithLayout = () => {
|
||||
const router = useRouter()
|
||||
const { ref, replicaId } = useParams()
|
||||
|
||||
const [showConfirmRestart, setShowConfirmRestart] = useState(false)
|
||||
const [showConfirmDrop, setShowConfirmDrop] = useState(false)
|
||||
const [statusRefetchInterval, setStatusRefetchInterval] = useState<number | false>(5000)
|
||||
|
||||
const {
|
||||
data: databases = [],
|
||||
isPending: isLoadingDatabases,
|
||||
isSuccess: isSuccessDatabases,
|
||||
} = useReadReplicasQuery({ projectRef: ref })
|
||||
const replica = databases.find((x) => x.identifier === replicaId)
|
||||
const { identifier, region, status: baseStatus } = replica ?? {}
|
||||
|
||||
const { data: statuses = [], isSuccess: isSuccessReplicasStatuses } =
|
||||
useReadReplicasStatusesQuery({ projectRef: ref }, { refetchInterval: statusRefetchInterval })
|
||||
const replicaStatus = statuses.find((x) => x.identifier === identifier)
|
||||
const status = replicaStatus?.status ?? baseStatus
|
||||
const initStatus = replicaStatus?.replicaInitializationStatus?.status
|
||||
|
||||
const regionLabel = Object.values(AWS_REGIONS).find((x) => x.code === region)?.displayName
|
||||
const statusLabel = useMemo(() => getStatusLabel({ initStatus, status }), [initStatus, status])
|
||||
const isInTransition = useMemo(
|
||||
() => getIsInTransition({ initStatus, status }),
|
||||
[initStatus, status]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSuccessReplicasStatuses) return
|
||||
|
||||
const pollReplicas = async () => {
|
||||
const fixedStatuses = [
|
||||
REPLICA_STATUS.ACTIVE_HEALTHY,
|
||||
REPLICA_STATUS.ACTIVE_UNHEALTHY,
|
||||
REPLICA_STATUS.INIT_READ_REPLICA_FAILED,
|
||||
]
|
||||
const replicasInTransition = statuses.filter((db) => {
|
||||
const { status } = db.replicaInitializationStatus || {}
|
||||
return (
|
||||
!fixedStatuses.includes(db.status) || status === ReplicaInitializationStatus.InProgress
|
||||
)
|
||||
})
|
||||
const hasTransientStatus = replicasInTransition.length > 0
|
||||
|
||||
// If all replicas are active healthy, stop fetching statuses
|
||||
if (!hasTransientStatus && statuses.length === databases.length) {
|
||||
setStatusRefetchInterval(false)
|
||||
}
|
||||
}
|
||||
|
||||
pollReplicas()
|
||||
}, [databases.length, isSuccessReplicasStatuses, statuses])
|
||||
if (!ref || !replicaId) return
|
||||
router.replace(getReadReplicaPath(ref, replicaId))
|
||||
}, [ref, replicaId, router])
|
||||
|
||||
return (
|
||||
<PageLayout
|
||||
title={
|
||||
<div className="flex items-center gap-x-3">
|
||||
<ScaffoldTitle>Read Replica</ScaffoldTitle>
|
||||
{isSuccessDatabases && (
|
||||
<>
|
||||
<Badge
|
||||
variant={
|
||||
statusLabel === 'Healthy'
|
||||
? 'success'
|
||||
: statusLabel === 'Failed'
|
||||
? 'destructive'
|
||||
: 'default'
|
||||
}
|
||||
>
|
||||
{statusLabel}
|
||||
</Badge>
|
||||
{isInTransition && <Loader2 size={14} className="animate-spin" />}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
subtitle={
|
||||
isLoadingDatabases ? (
|
||||
<ShimmeringLoader className="py-[11px]" />
|
||||
) : (
|
||||
<div className="flex items-center gap-x-2 mt-0!">
|
||||
<ScaffoldDescription>ID: {identifier}</ScaffoldDescription>
|
||||
<CopyButton iconOnly variant="default" text={identifier ?? ''} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
icon={
|
||||
<div className="shrink-0 w-10 h-10 relative bg-surface-100 border rounded-md flex items-center justify-center">
|
||||
<Database size={20} className="text-foreground-light" />
|
||||
</div>
|
||||
}
|
||||
breadcrumbs={[
|
||||
{
|
||||
label: 'Replication',
|
||||
href: `/project/${ref}/database/replication`,
|
||||
},
|
||||
{
|
||||
label: `Read Replica - ${regionLabel}`,
|
||||
},
|
||||
]}
|
||||
secondaryActions={
|
||||
<ButtonTooltip
|
||||
variant="default"
|
||||
className="w-7"
|
||||
icon={<Trash />}
|
||||
tooltip={{
|
||||
content: { side: 'bottom', text: 'Drop replica' },
|
||||
}}
|
||||
onClick={() => setShowConfirmDrop(true)}
|
||||
/>
|
||||
}
|
||||
primaryActions={[
|
||||
<Button asChild key="logs" variant="default">
|
||||
<Link
|
||||
href={`/project/${ref}/logs/postgres-logs${!!identifier ? `?db=${identifier}` : ''}`}
|
||||
>
|
||||
View logs
|
||||
</Link>
|
||||
</Button>,
|
||||
<Button
|
||||
key="drop"
|
||||
variant="default"
|
||||
disabled={status !== 'ACTIVE_HEALTHY'}
|
||||
onClick={() => setShowConfirmRestart(true)}
|
||||
>
|
||||
Restart replica
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<ReadReplicaDetails />
|
||||
|
||||
<DropReplicaConfirmationModal
|
||||
selectedReplica={showConfirmDrop ? replica : undefined}
|
||||
onSuccess={() => router.push(`/project/${ref}/database/replication`)}
|
||||
onCancel={() => setShowConfirmDrop(false)}
|
||||
/>
|
||||
|
||||
<RestartReplicaConfirmationModal
|
||||
selectedReplica={showConfirmRestart ? replica : undefined}
|
||||
onSuccess={() => setStatusRefetchInterval(5000)}
|
||||
onCancel={() => setShowConfirmRestart(false)}
|
||||
/>
|
||||
</PageLayout>
|
||||
<div className="p-6">
|
||||
<GenericSkeletonLoader />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
DatabaseReadReplicaPage.getLayout = (page) => (
|
||||
DatabaseReadReplicaRedirectPage.getLayout = (page) => (
|
||||
<DefaultLayout>
|
||||
<DatabaseLayout title="Replication">{page}</DatabaseLayout>
|
||||
</DefaultLayout>
|
||||
)
|
||||
|
||||
export default DatabaseReadReplicaPage
|
||||
export default DatabaseReadReplicaRedirectPage
|
||||
@@ -0,0 +1,176 @@
|
||||
import { useParams } from 'common'
|
||||
import { Database } from 'icons'
|
||||
import { Loader2, Trash } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { AWS_REGIONS } from 'shared-data'
|
||||
import { Badge, Button } from 'ui'
|
||||
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
|
||||
|
||||
import { getInfrastructurePath } from '@/components/interfaces/Settings/Infrastructure/Infrastructure.utils'
|
||||
import { DropReplicaConfirmationModal } from '@/components/interfaces/Settings/Infrastructure/ReadReplicas/DropReplicaConfirmationModal'
|
||||
import { ReadReplicaDetails } from '@/components/interfaces/Settings/Infrastructure/ReadReplicas/ReadReplicaDetails'
|
||||
import { REPLICA_STATUS } from '@/components/interfaces/Settings/Infrastructure/ReadReplicas/ReadReplicas.constants'
|
||||
import {
|
||||
getIsInTransition,
|
||||
getStatusLabel,
|
||||
} from '@/components/interfaces/Settings/Infrastructure/ReadReplicas/ReadReplicas.utils'
|
||||
import { RestartReplicaConfirmationModal } from '@/components/interfaces/Settings/Infrastructure/ReadReplicas/RestartReplicaConfirmationModal'
|
||||
import { DefaultLayout } from '@/components/layouts/DefaultLayout'
|
||||
import { PageLayout } from '@/components/layouts/PageLayout/PageLayout'
|
||||
import SettingsLayout from '@/components/layouts/ProjectSettingsLayout/SettingsLayout'
|
||||
import { ScaffoldDescription, ScaffoldTitle } from '@/components/layouts/Scaffold'
|
||||
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
|
||||
import CopyButton from '@/components/ui/CopyButton'
|
||||
import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query'
|
||||
import {
|
||||
ReplicaInitializationStatus,
|
||||
useReadReplicasStatusesQuery,
|
||||
} from '@/data/read-replicas/replicas-status-query'
|
||||
import type { NextPageWithLayout } from '@/types'
|
||||
|
||||
const STATUS_BADGE_VARIANT: Record<string, 'success' | 'destructive' | 'default'> = {
|
||||
Healthy: 'success',
|
||||
Failed: 'destructive',
|
||||
}
|
||||
|
||||
const InfrastructureReadReplicaPage: NextPageWithLayout = () => {
|
||||
const router = useRouter()
|
||||
const { ref, replicaId } = useParams()
|
||||
|
||||
const [showConfirmRestart, setShowConfirmRestart] = useState(false)
|
||||
const [showConfirmDrop, setShowConfirmDrop] = useState(false)
|
||||
const [statusRefetchInterval, setStatusRefetchInterval] = useState<number | false>(5000)
|
||||
|
||||
const {
|
||||
data: databases = [],
|
||||
isPending: isLoadingDatabases,
|
||||
isSuccess: isSuccessDatabases,
|
||||
} = useReadReplicasQuery({ projectRef: ref })
|
||||
const replica = databases.find((x) => x.identifier === replicaId)
|
||||
const { identifier, region, status: baseStatus } = replica ?? {}
|
||||
|
||||
const { data: statuses = [], isSuccess: isSuccessReplicasStatuses } =
|
||||
useReadReplicasStatusesQuery({ projectRef: ref }, { refetchInterval: statusRefetchInterval })
|
||||
const replicaStatus = statuses.find((x) => x.identifier === identifier)
|
||||
const status = replicaStatus?.status ?? baseStatus
|
||||
const initStatus = replicaStatus?.replicaInitializationStatus?.status
|
||||
|
||||
const regionLabel = Object.values(AWS_REGIONS).find((x) => x.code === region)?.displayName
|
||||
const statusLabel = useMemo(() => getStatusLabel({ initStatus, status }), [initStatus, status])
|
||||
const isInTransition = useMemo(
|
||||
() => getIsInTransition({ initStatus, status }),
|
||||
[initStatus, status]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSuccessReplicasStatuses) return
|
||||
|
||||
const fixedStatuses = [
|
||||
REPLICA_STATUS.ACTIVE_HEALTHY,
|
||||
REPLICA_STATUS.ACTIVE_UNHEALTHY,
|
||||
REPLICA_STATUS.INIT_READ_REPLICA_FAILED,
|
||||
]
|
||||
const replicasInTransition = statuses.filter((db) => {
|
||||
const { status: init } = db.replicaInitializationStatus || {}
|
||||
return !fixedStatuses.includes(db.status) || init === ReplicaInitializationStatus.InProgress
|
||||
})
|
||||
const hasTransientStatus = replicasInTransition.length > 0
|
||||
|
||||
if (!hasTransientStatus && statuses.length === databases.length) {
|
||||
setStatusRefetchInterval(false)
|
||||
}
|
||||
}, [databases.length, isSuccessReplicasStatuses, statuses])
|
||||
|
||||
return (
|
||||
<PageLayout
|
||||
title={
|
||||
<div className="flex items-center gap-x-3">
|
||||
<ScaffoldTitle>Read Replica</ScaffoldTitle>
|
||||
{isSuccessDatabases && (
|
||||
<>
|
||||
<Badge variant={STATUS_BADGE_VARIANT[statusLabel] ?? 'default'}>{statusLabel}</Badge>
|
||||
{isInTransition && <Loader2 size={14} className="animate-spin" />}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
subtitle={
|
||||
isLoadingDatabases ? (
|
||||
<ShimmeringLoader className="py-[11px]" />
|
||||
) : (
|
||||
<div className="flex items-center gap-x-2 mt-0!">
|
||||
<ScaffoldDescription>ID: {identifier}</ScaffoldDescription>
|
||||
<CopyButton iconOnly variant="default" text={identifier ?? ''} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
icon={
|
||||
<div className="shrink-0 w-10 h-10 relative bg-surface-100 border rounded-md flex items-center justify-center">
|
||||
<Database size={20} className="text-foreground-light" />
|
||||
</div>
|
||||
}
|
||||
breadcrumbs={[
|
||||
{
|
||||
label: 'Infrastructure',
|
||||
href: getInfrastructurePath(ref),
|
||||
},
|
||||
{
|
||||
label: regionLabel ? `Read Replica - ${regionLabel}` : 'Read Replica',
|
||||
},
|
||||
]}
|
||||
secondaryActions={
|
||||
<ButtonTooltip
|
||||
variant="default"
|
||||
className="w-7"
|
||||
icon={<Trash />}
|
||||
disabled={!replica}
|
||||
tooltip={{
|
||||
content: { side: 'bottom', text: 'Drop replica' },
|
||||
}}
|
||||
onClick={() => setShowConfirmDrop(true)}
|
||||
/>
|
||||
}
|
||||
primaryActions={[
|
||||
<Button asChild key="logs" variant="default">
|
||||
<Link
|
||||
href={`/project/${ref}/logs/postgres-logs${!!identifier ? `?db=${identifier}` : ''}`}
|
||||
>
|
||||
View logs
|
||||
</Link>
|
||||
</Button>,
|
||||
<Button
|
||||
key="restart"
|
||||
variant="default"
|
||||
disabled={status !== 'ACTIVE_HEALTHY'}
|
||||
onClick={() => setShowConfirmRestart(true)}
|
||||
>
|
||||
Restart replica
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<ReadReplicaDetails />
|
||||
|
||||
<DropReplicaConfirmationModal
|
||||
selectedReplica={showConfirmDrop ? replica : undefined}
|
||||
onSuccess={() => router.push(getInfrastructurePath(ref))}
|
||||
onCancel={() => setShowConfirmDrop(false)}
|
||||
/>
|
||||
|
||||
<RestartReplicaConfirmationModal
|
||||
selectedReplica={showConfirmRestart ? replica : undefined}
|
||||
onSuccess={() => setStatusRefetchInterval(5000)}
|
||||
onCancel={() => setShowConfirmRestart(false)}
|
||||
/>
|
||||
</PageLayout>
|
||||
)
|
||||
}
|
||||
|
||||
InfrastructureReadReplicaPage.getLayout = (page) => (
|
||||
<DefaultLayout>
|
||||
<SettingsLayout title="Infrastructure">{page}</SettingsLayout>
|
||||
</DefaultLayout>
|
||||
)
|
||||
|
||||
export default InfrastructureReadReplicaPage
|
||||
@@ -66,6 +66,19 @@ describe('matchRedirect query/hash preservation', () => {
|
||||
})
|
||||
})
|
||||
|
||||
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({
|
||||
|
||||
@@ -119,6 +119,11 @@ export const SHARED_REDIRECTS: StudioRedirect[] = [
|
||||
destination: '/project/:ref/settings/infrastructure',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/project/:ref/database/replication/replica/:replicaId',
|
||||
destination: '/project/:ref/settings/infrastructure/replica/:replicaId',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/project/:ref/settings/billing/subscription',
|
||||
has: [{ type: 'query', key: 'panel', value: 'pitr' }],
|
||||
|
||||
@@ -106,7 +106,7 @@ import { Route as ProjectRefSqlExamplesRouteImport } from './routes/project/$ref
|
||||
import { Route as ProjectRefSqlIdRouteImport } from './routes/project/$ref/sql/$id'
|
||||
import { Route as ProjectRefSettingsLogDrainsRouteImport } from './routes/project/$ref/settings/log-drains'
|
||||
import { Route as ProjectRefSettingsIntegrationsRouteImport } from './routes/project/$ref/settings/integrations'
|
||||
import { Route as ProjectRefSettingsInfrastructureRouteImport } from './routes/project/$ref/settings/infrastructure'
|
||||
import { Route as ProjectRefSettingsInfrastructureIndexRouteImport } from './routes/project/$ref/settings/infrastructure/index'
|
||||
import { Route as ProjectRefSettingsGeneralRouteImport } from './routes/project/$ref/settings/general'
|
||||
import { Route as ProjectRefSettingsDashboardRouteImport } from './routes/project/$ref/settings/dashboard'
|
||||
import { Route as ProjectRefSettingsApiKeysRouteImport } from './routes/project/$ref/settings/api-keys'
|
||||
@@ -285,6 +285,7 @@ import { Route as ApiPlatformAuthRefUsersIndexRouteImport } from './routes/api/p
|
||||
import { Route as ProjectRefStorageVectorsBucketsBucketIdRouteImport } from './routes/project/$ref/storage/vectors/buckets/$bucketId'
|
||||
import { Route as ProjectRefStorageFilesBucketsBucketIdRouteImport } from './routes/project/$ref/storage/files/buckets/$bucketId'
|
||||
import { Route as ProjectRefStorageAnalyticsBucketsBucketIdRouteImport } from './routes/project/$ref/storage/analytics/buckets/$bucketId'
|
||||
import { Route as ProjectRefSettingsInfrastructureReplicaReplicaIdRouteImport } from './routes/project/$ref/settings/infrastructure/replica/$replicaId'
|
||||
import { Route as ProjectRefDatabaseReplicationReplicaReplicaIdRouteImport } from './routes/project/$ref/database/replication/replica/$replicaId'
|
||||
import { Route as ApiV1ProjectsRefTypesTypescriptRouteImport } from './routes/api/v1/projects/$ref/types/typescript'
|
||||
import { Route as ApiV1ProjectsRefDatabaseMigrationsRouteImport } from './routes/api/v1/projects/$ref/database/migrations'
|
||||
@@ -823,10 +824,10 @@ const ProjectRefSettingsIntegrationsRoute =
|
||||
path: '/integrations',
|
||||
getParentRoute: () => ProjectRefSettingsRoute,
|
||||
} as any)
|
||||
const ProjectRefSettingsInfrastructureRoute =
|
||||
ProjectRefSettingsInfrastructureRouteImport.update({
|
||||
id: '/infrastructure',
|
||||
path: '/infrastructure',
|
||||
const ProjectRefSettingsInfrastructureIndexRoute =
|
||||
ProjectRefSettingsInfrastructureIndexRouteImport.update({
|
||||
id: '/infrastructure/',
|
||||
path: '/infrastructure/',
|
||||
getParentRoute: () => ProjectRefSettingsRoute,
|
||||
} as any)
|
||||
const ProjectRefSettingsGeneralRoute =
|
||||
@@ -1851,6 +1852,12 @@ const ProjectRefStorageAnalyticsBucketsBucketIdRoute =
|
||||
path: '/analytics/buckets/$bucketId',
|
||||
getParentRoute: () => ProjectRefStorageRoute,
|
||||
} as any)
|
||||
const ProjectRefSettingsInfrastructureReplicaReplicaIdRoute =
|
||||
ProjectRefSettingsInfrastructureReplicaReplicaIdRouteImport.update({
|
||||
id: '/infrastructure/replica/$replicaId',
|
||||
path: '/infrastructure/replica/$replicaId',
|
||||
getParentRoute: () => ProjectRefSettingsRoute,
|
||||
} as any)
|
||||
const ProjectRefDatabaseReplicationReplicaReplicaIdRoute =
|
||||
ProjectRefDatabaseReplicationReplicaReplicaIdRouteImport.update({
|
||||
id: '/replication/replica/$replicaId',
|
||||
@@ -2249,7 +2256,7 @@ export interface FileRoutesByFullPath {
|
||||
'/project/$ref/settings/api-keys': typeof ProjectRefSettingsApiKeysRouteWithChildren
|
||||
'/project/$ref/settings/dashboard': typeof ProjectRefSettingsDashboardRoute
|
||||
'/project/$ref/settings/general': typeof ProjectRefSettingsGeneralRoute
|
||||
'/project/$ref/settings/infrastructure': typeof ProjectRefSettingsInfrastructureRoute
|
||||
'/project/$ref/settings/infrastructure/': typeof ProjectRefSettingsInfrastructureIndexRoute
|
||||
'/project/$ref/settings/integrations': typeof ProjectRefSettingsIntegrationsRoute
|
||||
'/project/$ref/settings/log-drains': typeof ProjectRefSettingsLogDrainsRoute
|
||||
'/project/$ref/sql/$id': typeof ProjectRefSqlIdRoute
|
||||
@@ -2356,6 +2363,7 @@ export interface FileRoutesByFullPath {
|
||||
'/api/v1/projects/$ref/database/migrations': typeof ApiV1ProjectsRefDatabaseMigrationsRoute
|
||||
'/api/v1/projects/$ref/types/typescript': typeof ApiV1ProjectsRefTypesTypescriptRoute
|
||||
'/project/$ref/database/replication/replica/$replicaId': typeof ProjectRefDatabaseReplicationReplicaReplicaIdRoute
|
||||
'/project/$ref/settings/infrastructure/replica/$replicaId': typeof ProjectRefSettingsInfrastructureReplicaReplicaIdRoute
|
||||
'/project/$ref/storage/analytics/buckets/$bucketId': typeof ProjectRefStorageAnalyticsBucketsBucketIdRoute
|
||||
'/project/$ref/storage/files/buckets/$bucketId': typeof ProjectRefStorageFilesBucketsBucketIdRoute
|
||||
'/project/$ref/storage/vectors/buckets/$bucketId': typeof ProjectRefStorageVectorsBucketsBucketIdRoute
|
||||
@@ -2549,7 +2557,7 @@ export interface FileRoutesByTo {
|
||||
'/project/$ref/settings/api': typeof ProjectRefSettingsApiRoute
|
||||
'/project/$ref/settings/dashboard': typeof ProjectRefSettingsDashboardRoute
|
||||
'/project/$ref/settings/general': typeof ProjectRefSettingsGeneralRoute
|
||||
'/project/$ref/settings/infrastructure': typeof ProjectRefSettingsInfrastructureRoute
|
||||
'/project/$ref/settings/infrastructure': typeof ProjectRefSettingsInfrastructureIndexRoute
|
||||
'/project/$ref/settings/integrations': typeof ProjectRefSettingsIntegrationsRoute
|
||||
'/project/$ref/settings/log-drains': typeof ProjectRefSettingsLogDrainsRoute
|
||||
'/project/$ref/sql/$id': typeof ProjectRefSqlIdRoute
|
||||
@@ -2656,6 +2664,7 @@ export interface FileRoutesByTo {
|
||||
'/api/v1/projects/$ref/database/migrations': typeof ApiV1ProjectsRefDatabaseMigrationsRoute
|
||||
'/api/v1/projects/$ref/types/typescript': typeof ApiV1ProjectsRefTypesTypescriptRoute
|
||||
'/project/$ref/database/replication/replica/$replicaId': typeof ProjectRefDatabaseReplicationReplicaReplicaIdRoute
|
||||
'/project/$ref/settings/infrastructure/replica/$replicaId': typeof ProjectRefSettingsInfrastructureReplicaReplicaIdRoute
|
||||
'/project/$ref/storage/analytics/buckets/$bucketId': typeof ProjectRefStorageAnalyticsBucketsBucketIdRoute
|
||||
'/project/$ref/storage/files/buckets/$bucketId': typeof ProjectRefStorageFilesBucketsBucketIdRoute
|
||||
'/project/$ref/storage/vectors/buckets/$bucketId': typeof ProjectRefStorageVectorsBucketsBucketIdRoute
|
||||
@@ -2866,7 +2875,7 @@ export interface FileRoutesById {
|
||||
'/project/$ref/settings/api-keys': typeof ProjectRefSettingsApiKeysRouteWithChildren
|
||||
'/project/$ref/settings/dashboard': typeof ProjectRefSettingsDashboardRoute
|
||||
'/project/$ref/settings/general': typeof ProjectRefSettingsGeneralRoute
|
||||
'/project/$ref/settings/infrastructure': typeof ProjectRefSettingsInfrastructureRoute
|
||||
'/project/$ref/settings/infrastructure/': typeof ProjectRefSettingsInfrastructureIndexRoute
|
||||
'/project/$ref/settings/integrations': typeof ProjectRefSettingsIntegrationsRoute
|
||||
'/project/$ref/settings/log-drains': typeof ProjectRefSettingsLogDrainsRoute
|
||||
'/project/$ref/sql/$id': typeof ProjectRefSqlIdRoute
|
||||
@@ -2973,6 +2982,7 @@ export interface FileRoutesById {
|
||||
'/api/v1/projects/$ref/database/migrations': typeof ApiV1ProjectsRefDatabaseMigrationsRoute
|
||||
'/api/v1/projects/$ref/types/typescript': typeof ApiV1ProjectsRefTypesTypescriptRoute
|
||||
'/project/$ref/database/replication/replica/$replicaId': typeof ProjectRefDatabaseReplicationReplicaReplicaIdRoute
|
||||
'/project/$ref/settings/infrastructure/replica/$replicaId': typeof ProjectRefSettingsInfrastructureReplicaReplicaIdRoute
|
||||
'/project/$ref/storage/analytics/buckets/$bucketId': typeof ProjectRefStorageAnalyticsBucketsBucketIdRoute
|
||||
'/project/$ref/storage/files/buckets/$bucketId': typeof ProjectRefStorageFilesBucketsBucketIdRoute
|
||||
'/project/$ref/storage/vectors/buckets/$bucketId': typeof ProjectRefStorageVectorsBucketsBucketIdRoute
|
||||
@@ -3182,7 +3192,7 @@ export interface FileRouteTypes {
|
||||
| '/project/$ref/settings/api-keys'
|
||||
| '/project/$ref/settings/dashboard'
|
||||
| '/project/$ref/settings/general'
|
||||
| '/project/$ref/settings/infrastructure'
|
||||
| '/project/$ref/settings/infrastructure/'
|
||||
| '/project/$ref/settings/integrations'
|
||||
| '/project/$ref/settings/log-drains'
|
||||
| '/project/$ref/sql/$id'
|
||||
@@ -3289,6 +3299,7 @@ export interface FileRouteTypes {
|
||||
| '/api/v1/projects/$ref/database/migrations'
|
||||
| '/api/v1/projects/$ref/types/typescript'
|
||||
| '/project/$ref/database/replication/replica/$replicaId'
|
||||
| '/project/$ref/settings/infrastructure/replica/$replicaId'
|
||||
| '/project/$ref/storage/analytics/buckets/$bucketId'
|
||||
| '/project/$ref/storage/files/buckets/$bucketId'
|
||||
| '/project/$ref/storage/vectors/buckets/$bucketId'
|
||||
@@ -3589,6 +3600,7 @@ export interface FileRouteTypes {
|
||||
| '/api/v1/projects/$ref/database/migrations'
|
||||
| '/api/v1/projects/$ref/types/typescript'
|
||||
| '/project/$ref/database/replication/replica/$replicaId'
|
||||
| '/project/$ref/settings/infrastructure/replica/$replicaId'
|
||||
| '/project/$ref/storage/analytics/buckets/$bucketId'
|
||||
| '/project/$ref/storage/files/buckets/$bucketId'
|
||||
| '/project/$ref/storage/vectors/buckets/$bucketId'
|
||||
@@ -3798,7 +3810,7 @@ export interface FileRouteTypes {
|
||||
| '/project/$ref/settings/api-keys'
|
||||
| '/project/$ref/settings/dashboard'
|
||||
| '/project/$ref/settings/general'
|
||||
| '/project/$ref/settings/infrastructure'
|
||||
| '/project/$ref/settings/infrastructure/'
|
||||
| '/project/$ref/settings/integrations'
|
||||
| '/project/$ref/settings/log-drains'
|
||||
| '/project/$ref/sql/$id'
|
||||
@@ -3905,6 +3917,7 @@ export interface FileRouteTypes {
|
||||
| '/api/v1/projects/$ref/database/migrations'
|
||||
| '/api/v1/projects/$ref/types/typescript'
|
||||
| '/project/$ref/database/replication/replica/$replicaId'
|
||||
| '/project/$ref/settings/infrastructure/replica/$replicaId'
|
||||
| '/project/$ref/storage/analytics/buckets/$bucketId'
|
||||
| '/project/$ref/storage/files/buckets/$bucketId'
|
||||
| '/project/$ref/storage/vectors/buckets/$bucketId'
|
||||
@@ -4748,11 +4761,11 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof ProjectRefSettingsIntegrationsRouteImport
|
||||
parentRoute: typeof ProjectRefSettingsRoute
|
||||
}
|
||||
'/project/$ref/settings/infrastructure': {
|
||||
id: '/project/$ref/settings/infrastructure'
|
||||
'/project/$ref/settings/infrastructure/': {
|
||||
id: '/project/$ref/settings/infrastructure/'
|
||||
path: '/infrastructure'
|
||||
fullPath: '/project/$ref/settings/infrastructure'
|
||||
preLoaderRoute: typeof ProjectRefSettingsInfrastructureRouteImport
|
||||
fullPath: '/project/$ref/settings/infrastructure/'
|
||||
preLoaderRoute: typeof ProjectRefSettingsInfrastructureIndexRouteImport
|
||||
parentRoute: typeof ProjectRefSettingsRoute
|
||||
}
|
||||
'/project/$ref/settings/general': {
|
||||
@@ -6001,6 +6014,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof ProjectRefStorageAnalyticsBucketsBucketIdRouteImport
|
||||
parentRoute: typeof ProjectRefStorageRoute
|
||||
}
|
||||
'/project/$ref/settings/infrastructure/replica/$replicaId': {
|
||||
id: '/project/$ref/settings/infrastructure/replica/$replicaId'
|
||||
path: '/infrastructure/replica/$replicaId'
|
||||
fullPath: '/project/$ref/settings/infrastructure/replica/$replicaId'
|
||||
preLoaderRoute: typeof ProjectRefSettingsInfrastructureReplicaReplicaIdRouteImport
|
||||
parentRoute: typeof ProjectRefSettingsRoute
|
||||
}
|
||||
'/project/$ref/database/replication/replica/$replicaId': {
|
||||
id: '/project/$ref/database/replication/replica/$replicaId'
|
||||
path: '/replication/replica/$replicaId'
|
||||
@@ -6802,7 +6822,8 @@ interface ProjectRefSettingsRouteChildren {
|
||||
ProjectRefSettingsApiKeysRoute: typeof ProjectRefSettingsApiKeysRouteWithChildren
|
||||
ProjectRefSettingsDashboardRoute: typeof ProjectRefSettingsDashboardRoute
|
||||
ProjectRefSettingsGeneralRoute: typeof ProjectRefSettingsGeneralRoute
|
||||
ProjectRefSettingsInfrastructureRoute: typeof ProjectRefSettingsInfrastructureRoute
|
||||
ProjectRefSettingsInfrastructureIndexRoute: typeof ProjectRefSettingsInfrastructureIndexRoute
|
||||
ProjectRefSettingsInfrastructureReplicaReplicaIdRoute: typeof ProjectRefSettingsInfrastructureReplicaReplicaIdRoute
|
||||
ProjectRefSettingsIntegrationsRoute: typeof ProjectRefSettingsIntegrationsRoute
|
||||
ProjectRefSettingsLogDrainsRoute: typeof ProjectRefSettingsLogDrainsRoute
|
||||
ProjectRefSettingsBillingUsageRoute: typeof ProjectRefSettingsBillingUsageRoute
|
||||
@@ -6818,7 +6839,10 @@ const ProjectRefSettingsRouteChildren: ProjectRefSettingsRouteChildren = {
|
||||
ProjectRefSettingsApiKeysRoute: ProjectRefSettingsApiKeysRouteWithChildren,
|
||||
ProjectRefSettingsDashboardRoute: ProjectRefSettingsDashboardRoute,
|
||||
ProjectRefSettingsGeneralRoute: ProjectRefSettingsGeneralRoute,
|
||||
ProjectRefSettingsInfrastructureRoute: ProjectRefSettingsInfrastructureRoute,
|
||||
ProjectRefSettingsInfrastructureIndexRoute:
|
||||
ProjectRefSettingsInfrastructureIndexRoute,
|
||||
ProjectRefSettingsInfrastructureReplicaReplicaIdRoute:
|
||||
ProjectRefSettingsInfrastructureReplicaReplicaIdRoute,
|
||||
ProjectRefSettingsIntegrationsRoute: ProjectRefSettingsIntegrationsRoute,
|
||||
ProjectRefSettingsLogDrainsRoute: ProjectRefSettingsLogDrainsRoute,
|
||||
ProjectRefSettingsBillingUsageRoute: ProjectRefSettingsBillingUsageRoute,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import DatabaseReadReplicaPage from '@/pages/project/[ref]/database/replication/replica/[replicaId]'
|
||||
import DatabaseReadReplicaRedirectPage from '@/pages/project/[ref]/database/replication/replica/[replicaId]'
|
||||
|
||||
export const Route = createFileRoute('/project/$ref/database/replication/replica/$replicaId')({
|
||||
component: DatabaseReplicationReplicaRoute,
|
||||
@@ -10,5 +10,5 @@ export const Route = createFileRoute('/project/$ref/database/replication/replica
|
||||
})
|
||||
|
||||
function DatabaseReplicationReplicaRoute() {
|
||||
return <DatabaseReadReplicaPage dehydratedState={undefined} />
|
||||
return <DatabaseReadReplicaRedirectPage dehydratedState={undefined} />
|
||||
}
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import ProjectInfrastructure from '@/pages/project/[ref]/settings/infrastructure'
|
||||
|
||||
export const Route = createFileRoute('/project/$ref/settings/infrastructure')({
|
||||
export const Route = createFileRoute('/project/$ref/settings/infrastructure/')({
|
||||
component: SettingsInfrastructureRoute,
|
||||
staticData: { settingsLayoutTitle: 'Infrastructure' },
|
||||
})
|
||||
@@ -0,0 +1,12 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import InfrastructureReadReplicaPage from '@/pages/project/[ref]/settings/infrastructure/replica/[replicaId]'
|
||||
|
||||
export const Route = createFileRoute('/project/$ref/settings/infrastructure/replica/$replicaId')({
|
||||
component: InfrastructureReadReplicaRoute,
|
||||
staticData: { settingsLayoutTitle: 'Infrastructure' },
|
||||
})
|
||||
|
||||
function InfrastructureReadReplicaRoute() {
|
||||
return <InfrastructureReadReplicaPage dehydratedState={undefined} />
|
||||
}
|
||||
Reference in new issue
Block a user