Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,28 +1,30 @@
import matter from 'gray-matter'
import Link from 'next/link'
import { notFound } from 'next/navigation'
import { readFile } from 'node:fs/promises'
import { join, relative } from 'node:path'
import rehypeSlug from 'rehype-slug'
import emoji from 'remark-emoji'
import Link from 'next/link'
// End of third-party imports

import { IS_PROD, isFeatureEnabled } from 'common'
import { Button } from 'ui'
import { Admonition } from 'ui-patterns'

import { Guide, GuideArticle, GuideHeader, GuideFooter, GuideMdxContent } from '~/features/ui/guide'
import { newEditLink } from '~/features/helpers.edit-link'
import {
genGuideMeta,
genGuidesStaticParams,
removeRedundantH1,
} from '~/features/docs/GuidesMdx.utils'
import { newEditLink } from '~/features/helpers.edit-link'
import { REVALIDATION_TAGS } from '~/features/helpers.fetch'
import { Guide, GuideArticle, GuideFooter, GuideHeader, GuideMdxContent } from '~/features/ui/guide'
import { GUIDES_DIRECTORY, isValidGuideFrontmatter } from '~/lib/docs'
import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform'
import { linkTransform, type UrlTransformFunction } from '~/lib/mdx/plugins/rehypeLinkTransform'
import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition'
import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle'
import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs'
import { octokit } from '~/lib/octokit'
import { SerializeOptions } from '~/types/next-mdx-remote-serialize'
import { IS_PROD } from 'common'
import type { SerializeOptions } from '~/types/next-mdx-remote-serialize'

// We fetch these docs at build time from an external repo
const org = 'supabase'
Expand Down Expand Up @@ -85,7 +87,10 @@ async function getLatestRelease(after: string | null = null) {
after,
request: {
fetch: (url: RequestInfo | URL, options?: RequestInit) =>
fetch(url, { ...options, next: { tags: [REVALIDATION_TAGS.WRAPPERS] } }),
fetch(url, {
...options,
next: { tags: [REVALIDATION_TAGS.WRAPPERS] },
}),
},
})

Expand Down Expand Up @@ -243,6 +248,10 @@ interface Params {
}

const WrappersDocs = async (props: { params: Promise<Params> }) => {
if (!isFeatureEnabled('docs:fdw')) {
notFound()
}

const params = await props.params
const { isExternal, meta, assetsBaseUrl, ...data } = await getContent(params)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ComponentProps } from 'react'
// End of third-party imports

import { isFeatureEnabled } from 'common/enabled-features'
import type { IconPanel } from 'ui-patterns/IconPanel'
Expand All @@ -14,13 +15,14 @@ const {
docsAuthTroubleshooting: authTroubleshootingEnabled,
docsCompliance: complianceEnabled,
docsContribution: contributionEnabled,
'docsSelf-hosting': selfHostingEnabled,
docsFdw: fdwEnabled,
docsFrameworkQuickstarts: frameworkQuickstartsEnabled,
docsFullPlatform: fullPlatformEnabled,
docsLocalDevelopment: localDevelopmentEnabled,
docsMobileTutorials: mobileTutorialsEnabled,
docsPgtap: pgTapEnabled,
docsProductionChecklist: productionChecklistEnabled,
'docsSelf-hosting': selfHostingEnabled,
docsWebApps: webAppsEnabled,
integrationsPartners: integrationsEnabled,
sdkCsharp: sdkCsharpEnabled,
Expand All @@ -38,6 +40,7 @@ const {
'docs:auth_troubleshooting',
'docs:compliance',
'docs:contribution',
'docs:fdw',
'docs:self-hosting',
'docs:framework_quickstarts',
'docs:full_platform',
Expand Down Expand Up @@ -1260,6 +1263,7 @@ export const database: NavMenuConstant = {
{
name: 'Foreign Data Wrappers',
url: undefined,
enabled: fdwEnabled,
items: [
{
name: 'Overview',
Expand Down
13 changes: 8 additions & 5 deletions apps/docs/layouts/MainSkeleton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@

import dynamic from 'next/dynamic'
import { usePathname } from 'next/navigation'
import { memo, useEffect, type PropsWithChildren, type ReactNode } from 'react'
import { memo, type PropsWithChildren, type ReactNode, useEffect } from 'react'
// End of third-party imports

import { isFeatureEnabled } from 'common'
import { cn } from 'ui'

import { type NavMenuSection } from '~/components/Navigation/Navigation.types'
import type { NavMenuSection } from '~/components/Navigation/Navigation.types'
import DefaultNavigationMenu, {
MenuId,
type MenuId,
} from '~/components/Navigation/NavigationMenu/NavigationMenu'
import { getMenuId } from '~/components/Navigation/NavigationMenu/NavigationMenu.utils'
import TopNavBar from '~/components/Navigation/NavigationMenu/TopNavBar'
Expand All @@ -17,6 +18,8 @@ import { menuState, useMenuMobileOpen } from '~/hooks/useMenuState'

const Footer = dynamic(() => import('~/components/Navigation/Footer'))

const footerEnabled = isFeatureEnabled('docs:footer')

const levelsData = {
home: {
icon: 'home',
Expand Down Expand Up @@ -375,7 +378,7 @@ function SidebarSkeleton({
menuId: _menuId,
menuName,
NavigationMenu,
hideFooter = false,
hideFooter = !footerEnabled,
className,
hideSideNav,
additionalNavItems,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useParams } from 'common'
import { InlineLink } from 'components/ui/InlineLink'
import { TableState } from './ReplicationPipelineStatus.types'
import { isValidRetryPolicy } from './ReplicationPipelineStatus.utils'
import { TableState } from './ReplicationPipelineStatus/ReplicationPipelineStatus.types'
import { isValidRetryPolicy } from './ReplicationPipelineStatus/ReplicationPipelineStatus.utils'
import { RetryCountdown } from './RetryCountdown'
import { RetryOptionsDropdown } from './RetryOptionsDropdown'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import {
Ban,
ChevronLeft,
ExternalLink,
Info,
Pause,
Play,
RotateCcw,
Search,
WifiOff,
X,
} from 'lucide-react'
import Link from 'next/link'
Expand All @@ -31,17 +33,18 @@ import {
import { Badge, Button, cn } from 'ui'
import { GenericSkeletonLoader } from 'ui-patterns'
import { Input } from 'ui-patterns/DataInputs/Input'
import { ErroredTableDetails } from './ErroredTableDetails'
import { ErroredTableDetails } from '../ErroredTableDetails'
import {
PIPELINE_ACTIONABLE_STATES,
PIPELINE_ERROR_MESSAGES,
getStatusName,
} from './Pipeline.utils'
import { PipelineStatus, PipelineStatusName } from './PipelineStatus'
import { STATUS_REFRESH_FREQUENCY_MS } from './Replication.constants'
import { TableState } from './ReplicationPipelineStatus.types'
} from '../Pipeline.utils'
import { PipelineStatus, PipelineStatusName } from '../PipelineStatus'
import { STATUS_REFRESH_FREQUENCY_MS } from '../Replication.constants'
import { UpdateVersionModal } from '../UpdateVersionModal'
import { SlotLagMetrics, TableState } from './ReplicationPipelineStatus.types'
import { getDisabledStateConfig, getStatusConfig } from './ReplicationPipelineStatus.utils'
import { UpdateVersionModal } from './UpdateVersionModal'
import { SlotLagMetricsInline, SlotLagMetricsList } from './SlotLagMetrics'

/**
* Component for displaying replication pipeline status and table replication details.
Expand Down Expand Up @@ -82,7 +85,6 @@ export const ReplicationPipelineStatus = () => {

const {
data: replicationStatusData,
error: statusError,
isLoading: isStatusLoading,
isError: isStatusError,
} = useReplicationPipelineReplicationStatusQuery(
Expand All @@ -107,12 +109,14 @@ export const ReplicationPipelineStatus = () => {
const config = getDisabledStateConfig({ requestStatus, statusName })

const tableStatuses = replicationStatusData?.table_statuses || []
const applyLagMetrics = replicationStatusData?.apply_lag
const filteredTableStatuses =
filterString.length === 0
? tableStatuses
: tableStatuses.filter((table: TableState) =>
: tableStatuses.filter((table) =>
table.table_name.toLowerCase().includes(filterString.toLowerCase())
)
const tablesWithLag = tableStatuses.filter((table) => Boolean(table.table_sync_lag))

const isPipelineRunning = statusName === 'started'
const hasTableData = tableStatuses.length > 0
Expand All @@ -121,6 +125,10 @@ export const ReplicationPipelineStatus = () => {
requestStatus === PipelineStatusRequestStatus.StopRequested ||
requestStatus === PipelineStatusRequestStatus.RestartRequested
const showDisabledState = !isPipelineRunning || isEnablingDisabling
const refreshIntervalLabel =
STATUS_REFRESH_FREQUENCY_MS >= 1000
? `${Math.round(STATUS_REFRESH_FREQUENCY_MS / 1000)}s`
: `${STATUS_REFRESH_FREQUENCY_MS}ms`

const logsUrl = `/project/${projectRef}/logs/etl-replication-logs${
pipelineId ? `?f=${encodeURIComponent(JSON.stringify({ pipeline_id: pipelineId }))}` : ''
Expand Down Expand Up @@ -242,46 +250,105 @@ export const ReplicationPipelineStatus = () => {
</Button>
</div>
</div>

{(isPipelineLoading || isStatusLoading) && <GenericSkeletonLoader />}

{isPipelineError && (
<AlertError error={pipelineError} subject={PIPELINE_ERROR_MESSAGES.RETRIEVE_PIPELINE} />
)}

{isStatusError && (
<AlertError
error={statusError}
subject={PIPELINE_ERROR_MESSAGES.RETRIEVE_REPLICATION_STATUS}
/>
<div className="flex items-center gap-2 rounded-lg border border-warning-400 bg-warning-50 px-3 py-2 text-xs text-warning-800">
<WifiOff size={14} />
<span className="font-medium">Live updates paused</span>
<span className="text-warning-700">Retrying automatically</span>
</div>
)}

{hasTableData && (
<div className="flex flex-col gap-y-4">
{showDisabledState && (
{showDisabledState && (
<div
className={cn(
'p-4 border border-default rounded-lg flex items-center justify-between',
config.colors.bg
)}
>
<div className="flex items-center gap-x-3">
<div
className={cn(
'p-4 border border-default rounded-lg flex items-center justify-between',
config.colors.bg
'w-10 h-10 rounded-full flex items-center justify-center',
config.colors.iconBg
)}
>
<div className="flex items-center gap-x-3">
<div
className={cn(
'w-10 h-10 rounded-full flex items-center justify-center',
config.colors.iconBg
)}
>
<div className={config.colors.icon}>{config.icon}</div>
<div className={config.colors.icon}>{config.icon}</div>
</div>
<div className="flex-1">
<h4 className={`text-sm font-medium ${config.colors.text}`}>{config.title}</h4>
<p className={`text-sm ${config.colors.subtext}`}>{config.message}</p>
</div>
</div>
</div>
)}

{(isPipelineLoading || isStatusLoading) && (
<div className="space-y-3">
<div className="flex items-center gap-x-3">
<div className="h-6 w-40 rounded bg-surface-200" />
<div className="h-5 w-24 rounded bg-surface-200" />
</div>
<GenericSkeletonLoader />
</div>
)}

{applyLagMetrics && (
<div className="border border-default rounded-lg bg-surface-100 px-4 py-4 space-y-3">
<div className="flex flex-wrap items-baseline justify-between gap-y-1">
<div>
<h4 className="text-sm font-semibold text-foreground">Replication lag</h4>
<p className="text-xs text-foreground-light">
Snapshot of how far this pipeline is trailing behind right now.
</p>
</div>
<p className="text-xs text-foreground-lighter">
Updates every {refreshIntervalLabel}
</p>
</div>

{isStatusError && (
<p className="text-xs text-warning-700">
Unable to refresh data. Showing the last values we received.
</p>
)}

<SlotLagMetricsList metrics={applyLagMetrics} />

{tablesWithLag.length > 0 && (
<>
<div className="border-t border-default/40" />
<div className="space-y-3 text-xs text-foreground">
<div className="flex items-start gap-2 rounded-md border border-default/50 bg-surface-200/60 px-3 py-2 text-foreground-light">
<Info size={14} className="mt-0.5" />
<span>
During initial sync, tables can copy and stream independently before
reconciling with the overall pipeline.
</span>
</div>
<div className="flex-1">
<h4 className={`text-sm font-medium ${config.colors.text}`}>{config.title}</h4>
<p className={`text-sm ${config.colors.subtext}`}>{config.message}</p>
<div className="rounded border border-default/50 bg-surface-200/40">
<ul className="divide-y divide-default/40">
{tablesWithLag.map((table) => (
<li key={`${table.table_id}-${table.table_name}`} className="px-3 py-2">
<SlotLagMetricsInline
tableName={table.table_name}
metrics={table.table_sync_lag as SlotLagMetrics}
/>
</li>
))}
</ul>
</div>
</div>
</div>
</>
)}
</div>
)}

{hasTableData && (
<div className="flex flex-col gap-y-3">
<div className="w-full overflow-hidden overflow-x-auto">
{/* [Joshen] Should update to use new Table components next time */}
<Table
Expand All @@ -304,8 +371,8 @@ export const ReplicationPipelineStatus = () => {
</Table.td>
</Table.tr>
)}
{filteredTableStatuses.map((table: TableState, index: number) => {
const statusConfig = getStatusConfig(table.state)
{filteredTableStatuses.map((table, index) => {
const statusConfig = getStatusConfig(table.state as TableState['state'])
return (
<Table.tr key={`${table.table_name}-${index}`} className="border-t">
<Table.td className="align-top">
Expand Down Expand Up @@ -342,7 +409,7 @@ export const ReplicationPipelineStatus = () => {
Status unavailable while pipeline is {config.badge.toLowerCase()}
</p>
) : (
<div className="space-y-1">
<div className="space-y-3">
<div className="text-sm text-foreground">
{statusConfig.description}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@ export type RetryPolicy =
| { policy: 'manual_retry' }
| { policy: 'timed_retry'; next_retry: string }

export type SlotLagMetrics = {
restart_lsn_bytes: number
confirmed_flush_lsn_bytes: number
safe_wal_size_bytes: number
write_lag?: number
flush_lag?: number
}

export type SlotLagMetricKey = keyof SlotLagMetrics

export type TableState = {
table_id: number
table_name: string
Expand All @@ -12,4 +22,5 @@ export type TableState = {
| { name: 'copied_table' }
| { name: 'following_wal'; lag: number }
| { name: 'error'; reason: string; solution?: string; retry_policy: RetryPolicy }
table_sync_lag?: SlotLagMetrics
}
Loading
Loading