From 6fea2be680c74879da3486b2fc08ffe7118612f1 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Wed, 29 Jul 2026 18:36:55 +0800 Subject: [PATCH 1/7] Joshen/fe 4027 telemetry for database connections (#48435) ## Context Adding telemetry for the following actions on the database connections page - Toggling of live mode - Applying the various filters - Clicking on the overview metric cards - Clicking of terminate CTA + Confirm terminate ## Summary by CodeRabbit - **Accessibility** - Added a descriptive label to the AI Assistant actions menu trigger for improved screen-reader support. - **Observability** - Added tracking for database connections interactions: live-mode toggles, session filter updates, blocker-view toggles, clicks on observability metric cards, and the session termination flow (both the terminate action and confirmation submission). --- .../DatabaseConnections/Activity.tsx | 28 ++++- .../DatabaseConnections/ActivityRow.tsx | 13 +- .../DatabaseConnections/Overview.tsx | 32 ++++- .../components/ui/AiAssistantDropdown.tsx | 1 + .../[ref]/observability/connections.tsx | 10 +- packages/common/telemetry-constants.ts | 116 +++++++++++++++++- 6 files changed, 186 insertions(+), 14 deletions(-) diff --git a/apps/studio/components/interfaces/Observability/DatabaseConnections/Activity.tsx b/apps/studio/components/interfaces/Observability/DatabaseConnections/Activity.tsx index 4cd5b3f822771..f31ee12d21904 100644 --- a/apps/studio/components/interfaces/Observability/DatabaseConnections/Activity.tsx +++ b/apps/studio/components/interfaces/Observability/DatabaseConnections/Activity.tsx @@ -11,6 +11,7 @@ import { ButtonTooltip } from '@/components/ui/ButtonTooltip' import { useDatabaseRolesQuery } from '@/data/database-roles/database-roles-query' import { useDatabaseActivityQuery } from '@/data/database/activity-query' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' +import { useTrack } from '@/lib/telemetry/track' const DEFAULT_ROLES_FILTER = ['anon', 'authenticated', 'postgres'] @@ -19,6 +20,7 @@ interface ActivityProps { } export const Activity = ({ live }: ActivityProps) => { + const track = useTrack() const { data: project } = useSelectedProjectQuery() const [ @@ -150,6 +152,7 @@ export const Activity = ({ live }: ActivityProps) => { }) const onResetFilters = () => { + track('database_connections_filter_updated', { type: 'reset' }) setQueryStates({ search: '', states: [], @@ -175,7 +178,11 @@ export const Activity = ({ live }: ActivityProps) => { label="State" options={stateOptions} value={statesFilter ?? []} - onChange={(states) => setQueryStates({ states })} + onChange={(states) => { + if (isEqual(states, statesFilter)) return + track('database_connections_filter_updated', { type: 'state' }) + setQueryStates({ states }) + }} isLoading={isPending} popoverClassName="w-60" /> @@ -184,7 +191,11 @@ export const Activity = ({ live }: ActivityProps) => { label="Roles" options={roleOptions} value={rolesFilter ?? []} - onChange={(roles) => setQueryStates({ roles })} + onChange={(roles) => { + if (isEqual(roles, rolesFilter)) return + track('database_connections_filter_updated', { type: 'roles' }) + setQueryStates({ roles }) + }} isLoading={isPending} popoverClassName="w-72" /> @@ -193,14 +204,23 @@ export const Activity = ({ live }: ActivityProps) => { label="Application" options={applicationOptions} value={applicationsFilter ?? []} - onChange={(applications) => setQueryStates({ applications })} + onChange={(applications) => { + if (isEqual(applications, applicationsFilter)) return + track('database_connections_filter_updated', { type: 'application' }) + setQueryStates({ applications }) + }} isLoading={isPending} popoverClassName="w-60" /> 0 ? {rootBlockers.length} : null} - onClick={() => setQueryStates({ view: viewFilter === 'blockers' ? '' : 'blockers' })} + onClick={() => { + track('database_connections_blocker_view_clicked', { + newState: viewFilter === 'blockers' ? 'disabled' : 'enabled', + }) + setQueryStates({ view: viewFilter === 'blockers' ? '' : 'blockers' }) + }} tooltip={{ content: { side: 'bottom', diff --git a/apps/studio/components/interfaces/Observability/DatabaseConnections/ActivityRow.tsx b/apps/studio/components/interfaces/Observability/DatabaseConnections/ActivityRow.tsx index e9bc18e2201db..ee10aafd1d29c 100644 --- a/apps/studio/components/interfaces/Observability/DatabaseConnections/ActivityRow.tsx +++ b/apps/studio/components/interfaces/Observability/DatabaseConnections/ActivityRow.tsx @@ -48,6 +48,7 @@ import { useDatabaseActivityQuery, type DatabaseActivity } from '@/data/database import { useQueryAbortMutation } from '@/data/sql/abort-query-mutation' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { formatSql } from '@/lib/formatSql' +import { useTrack } from '@/lib/telemetry/track' export const GroupedActivityRow = ({ activity }: { activity: DatabaseActivity }) => { const { data: project } = useSelectedProjectQuery() @@ -103,6 +104,7 @@ export const ActivityRow = ({ isLast?: boolean onExpand?: () => void }) => { + const track = useTrack() const { data: project } = useSelectedProjectQuery() const [showTerminateConfirmDialog, setShowTerminateConfirmDialog] = useState(false) const [selectedPid, setSelectedPid] = useQueryState('pid', parseAsInteger) @@ -139,6 +141,8 @@ export const ActivityRow = ({ durationSeconds >= WARN_DURATION_IDLE_TXN)) const onConfirmTerminate = async () => { + const isBlocking = (data ?? []).some((x) => x.blocked_by.includes(activity.pid)) + track('session_terminate_submitted', { activityState: activity.state, isBlocking }) try { await abortQuery({ pid: activity.pid, @@ -391,7 +395,14 @@ export const ActivityRow = ({ setShowTerminateConfirmDialog(true)} + onClick={() => { + const isBlocking = (data ?? []).some((x) => x.blocked_by.includes(activity.pid)) + track('session_terminate_button_clicked', { + activityState: activity.state, + isBlocking, + }) + setShowTerminateConfirmDialog(true) + }} tooltip={{ content: { side: 'left', diff --git a/apps/studio/components/interfaces/Observability/DatabaseConnections/Overview.tsx b/apps/studio/components/interfaces/Observability/DatabaseConnections/Overview.tsx index 6c1c1c9491176..378ae54317ea5 100644 --- a/apps/studio/components/interfaces/Observability/DatabaseConnections/Overview.tsx +++ b/apps/studio/components/interfaces/Observability/DatabaseConnections/Overview.tsx @@ -14,12 +14,14 @@ import { useDatabaseRolesQuery } from '@/data/database-roles/database-roles-quer import { useDatabaseActivityQuery } from '@/data/database/activity-query' import { useMaxConnectionsQuery } from '@/data/database/max-connections-query' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' +import { useTrack } from '@/lib/telemetry/track' interface OverviewProps { live?: boolean } export const Overview = ({ live }: OverviewProps) => { + const track = useTrack() const { data: project } = useSelectedProjectQuery() const [, setSelectedPid] = useQueryState('pid', parseAsInteger) @@ -71,6 +73,24 @@ export const Overview = ({ live }: OverviewProps) => { document.getElementById(pid.toString())?.scrollIntoView({ behavior: 'smooth', block: 'center' }) } + const onSelectLongestBlocked = () => { + if (!longestBlockedQuery) return + track('database_connections_overview_metric_card_clicked', { type: 'longest_blocked' }) + onSelectPid(longestBlockedQuery.activity.pid) + } + + const onSelectTopBlocker = () => { + if (!queryBlockingTheMostQueries) return + track('database_connections_overview_metric_card_clicked', { type: 'top_blocker' }) + onSelectPid(queryBlockingTheMostQueries.activity.pid) + } + + const onSelectLongestRunning = () => { + if (!longestRunningQuery) return + track('database_connections_overview_metric_card_clicked', { type: 'longest_running' }) + onSelectPid(longestRunningQuery.activity.pid) + } + return (
@@ -193,11 +213,11 @@ export const Overview = ({ live }: OverviewProps) => { 'hover:text-foreground hover:underline', 'focus:text-foreground focus:underline' )} - onClick={() => onSelectPid(longestBlockedQuery.activity.pid)} + onClick={() => onSelectLongestBlocked()} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault() - onSelectPid(longestBlockedQuery.activity.pid) + onSelectLongestBlocked() } }} > @@ -243,11 +263,11 @@ export const Overview = ({ live }: OverviewProps) => { role="button" tabIndex={0} className="normal-nums cursor-pointer hover:underline focus:underline" - onClick={() => onSelectPid(queryBlockingTheMostQueries.activity.pid)} + onClick={() => onSelectTopBlocker()} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault() - onSelectPid(queryBlockingTheMostQueries.activity.pid) + onSelectTopBlocker() } }} > @@ -300,11 +320,11 @@ export const Overview = ({ live }: OverviewProps) => { role="button" tabIndex={0} className="normal-nums hover:underline focus:underline cursor-pointer" - onClick={() => onSelectPid(longestRunningQuery.activity.pid)} + onClick={() => onSelectLongestRunning()} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault() - onSelectPid(longestRunningQuery.activity.pid) + onSelectLongestRunning() } }} > diff --git a/apps/studio/components/ui/AiAssistantDropdown.tsx b/apps/studio/components/ui/AiAssistantDropdown.tsx index 512340d4cc652..10acb318719f6 100644 --- a/apps/studio/components/ui/AiAssistantDropdown.tsx +++ b/apps/studio/components/ui/AiAssistantDropdown.tsx @@ -142,6 +142,7 @@ export function AiAssistantDropdown({ } /> diff --git a/apps/studio/components/interfaces/DiskManagement/fields/AutoScaleFields.tsx b/apps/studio/components/interfaces/DiskManagement/fields/AutoScaleFields.tsx index 82bfd945a6d92..6e626f6b2045c 100644 --- a/apps/studio/components/interfaces/DiskManagement/fields/AutoScaleFields.tsx +++ b/apps/studio/components/interfaces/DiskManagement/fields/AutoScaleFields.tsx @@ -70,6 +70,7 @@ export const AutoScaleFields = ({ form }: AutoScaleFieldProps) => { { { { diff --git a/apps/studio/components/interfaces/DiskManagement/fields/ThroughputField.tsx b/apps/studio/components/interfaces/DiskManagement/fields/ThroughputField.tsx index eb854c5872ea7..9b07712f8fb88 100644 --- a/apps/studio/components/interfaces/DiskManagement/fields/ThroughputField.tsx +++ b/apps/studio/components/interfaces/DiskManagement/fields/ThroughputField.tsx @@ -77,6 +77,7 @@ export function ThroughputField({ form, disableInput }: ThroughputFieldProps) {

Higher throughput suits applications with high data transfer needs.

@@ -100,6 +101,7 @@ export function ThroughputField({ form, disableInput }: ThroughputFieldProps) { { setValue('throughput', e.target.valueAsNumber, { diff --git a/apps/studio/components/interfaces/Integrations/Queues/UpgradeDatabaseAlert.tsx b/apps/studio/components/interfaces/Integrations/Queues/UpgradeDatabaseAlert.tsx index b953b74630ce0..e3ca55cb70646 100644 --- a/apps/studio/components/interfaces/Integrations/Queues/UpgradeDatabaseAlert.tsx +++ b/apps/studio/components/interfaces/Integrations/Queues/UpgradeDatabaseAlert.tsx @@ -2,6 +2,7 @@ import Link from 'next/link' import { Button } from 'ui' import { Admonition } from 'ui-patterns/Admonition' +import { getServiceVersionsPath } from '@/components/interfaces/Settings/General/ServiceVersions/ServiceVersions.utils' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' interface UpgradeDatabaseAlertProps { @@ -24,7 +25,7 @@ export const UpgradeDatabaseAlert = ({ minimumVersion = '15.6' }: UpgradeDatabas

) diff --git a/apps/studio/components/interfaces/Integrations/Wrappers/OverviewTab.tsx b/apps/studio/components/interfaces/Integrations/Wrappers/OverviewTab.tsx index ea61852ace7f1..884794f0ef013 100644 --- a/apps/studio/components/interfaces/Integrations/Wrappers/OverviewTab.tsx +++ b/apps/studio/components/interfaces/Integrations/Wrappers/OverviewTab.tsx @@ -14,6 +14,7 @@ import { CreateWrapperSheet } from './CreateWrapperSheet' import { WRAPPERS } from './Wrappers.constants' import { WrapperTable } from './WrapperTable' import { useIsMarketplaceEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext' +import { getServiceVersionsPath } from '@/components/interfaces/Settings/General/ServiceVersions/ServiceVersions.utils' import { ScaffoldContainer, ScaffoldSection } from '@/components/layouts/Scaffold' import { DiscardChangesConfirmationDialog } from '@/components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog' import { ButtonTooltip } from '@/components/ui/ButtonTooltip' @@ -124,7 +125,7 @@ const AddNewWrapperCTA = () => { diff --git a/apps/studio/components/interfaces/Linter/Linter.utils.tsx b/apps/studio/components/interfaces/Linter/Linter.utils.tsx index a9b00f7fda974..2e8a7b0c11c4d 100644 --- a/apps/studio/components/interfaces/Linter/Linter.utils.tsx +++ b/apps/studio/components/interfaces/Linter/Linter.utils.tsx @@ -16,6 +16,7 @@ import { Badge, Button } from 'ui' import { asGraphqlExposureLint, GraphqlExposureLintCTA } from './GraphqlExposureLintCTA' import { LINTER_LEVELS, LintInfo } from '@/components/interfaces/Linter/Linter.constants' +import { getServiceVersionsPath } from '@/components/interfaces/Settings/General/ServiceVersions/ServiceVersions.utils' import { Lint, LINT_TYPES } from '@/data/lint/lint-query' import { DOCS_URL } from '@/lib/constants' @@ -305,7 +306,7 @@ export const lintInfoMap: LintInfo[] = [ name: 'vulnerable_postgres_version', title: 'Postgres version has security patches available', icon: , - link: ({ projectRef }) => `/project/${projectRef}/settings/infrastructure`, + link: ({ projectRef }) => getServiceVersionsPath(projectRef), linkText: 'View settings', docsLink: `${DOCS_URL}/guides/platform/upgrading`, category: 'security', diff --git a/apps/studio/components/interfaces/Organization/Usage/UsageSection/DiskUsage.tsx b/apps/studio/components/interfaces/Organization/Usage/UsageSection/DiskUsage.tsx index 6f30631ff4f44..1f93a706ec9e5 100644 --- a/apps/studio/components/interfaces/Organization/Usage/UsageSection/DiskUsage.tsx +++ b/apps/studio/components/interfaces/Organization/Usage/UsageSection/DiskUsage.tsx @@ -6,6 +6,7 @@ import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' import { SectionContent } from '../SectionContent' import { CategoryAttribute } from '../Usage.constants' +import { getInfrastructurePath } from '@/components/interfaces/Settings/Infrastructure/Infrastructure.utils' import { AlertError } from '@/components/ui/AlertError' import Panel from '@/components/ui/Panel' import { PricingMetric } from '@/data/analytics/org-daily-stats-query' @@ -59,7 +60,6 @@ export const DiskUsage = ({ }) .filter((it) => it.ref === projectRef || !projectRef) : [] - // eslint-disable-next-line react-hooks/exhaustive-deps }, [isSuccess, projects, projectRef]) const hasProjectsExceedingDiskSize = useMemo(() => { @@ -200,9 +200,7 @@ export const DiskUsage = ({
) diff --git a/apps/studio/components/interfaces/QueryPerformance/IndexAdvisor/IndexAdvisorDisabledState.tsx b/apps/studio/components/interfaces/QueryPerformance/IndexAdvisor/IndexAdvisorDisabledState.tsx index 13351f9d8ebbd..ec98dd43e7d03 100644 --- a/apps/studio/components/interfaces/QueryPerformance/IndexAdvisor/IndexAdvisorDisabledState.tsx +++ b/apps/studio/components/interfaces/QueryPerformance/IndexAdvisor/IndexAdvisorDisabledState.tsx @@ -5,6 +5,7 @@ import { Alert, AlertDescription, AlertTitle, Button } from 'ui' import { Markdown } from '../../Markdown' import { getIndexAdvisorExtensions } from './index-advisor.utils' +import { getServiceVersionsPath } from '@/components/interfaces/Settings/General/ServiceVersions/ServiceVersions.utils' import { DocsButton } from '@/components/ui/DocsButton' import { useDatabaseExtensionEnableMutation } from '@/data/database-extensions/database-extension-enable-mutation' import { useDatabaseExtensionsQuery } from '@/data/database-extensions/database-extensions-query' @@ -77,7 +78,7 @@ export const IndexAdvisorDisabledState = () => {
{indexAdvisor === undefined ? ( ) : ( ) : unavailableReason === 'ssl_enforcement_required' && ref ? ( - - )} - - - - - - ) - })} - - ) -} diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.tsx b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.tsx index f8f898ad97842..b59b45ceee616 100644 --- a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.tsx +++ b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.tsx @@ -36,6 +36,7 @@ import { addRegionNodes, generateNodes, getDagreGraphLayout } from './InstanceCo import { LoadBalancerNode, PrimaryNode, RegionNode, ReplicaNode } from './InstanceNode' import MapView from './MapView' import { RestartReplicaConfirmationModal } from './RestartReplicaConfirmationModal' +import { getInfrastructurePath } from '@/components/interfaces/Settings/Infrastructure/Infrastructure.utils' import { AlertError } from '@/components/ui/AlertError' import { ButtonTooltip } from '@/components/ui/ButtonTooltip' import { useLoadBalancersQuery } from '@/data/read-replicas/load-balancers-query' @@ -301,9 +302,7 @@ const InstanceConfigurationUI = ({ diagramOnly = false }: InstanceConfigurationU - - Resize databases - + Resize databases setShowDeleteAllModal(true)}> diff --git a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/BucketCallouts.tsx b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/BucketCallouts.tsx index 21f797b422264..98a0070380e83 100644 --- a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/BucketCallouts.tsx +++ b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/BucketCallouts.tsx @@ -4,6 +4,7 @@ import { Admonition } from 'ui-patterns/Admonition' import { SimpleConfigurationDetails } from './SimpleConfigurationDetails' import { WrapperMeta } from '@/components/interfaces/Integrations/Wrappers/Wrappers.types' +import { getServiceVersionsPath } from '@/components/interfaces/Settings/General/ServiceVersions/ServiceVersions.utils' import { ScaffoldSection } from '@/components/layouts/Scaffold' import { InlineLink } from '@/components/ui/InlineLink' import { DatabaseExtension } from '@/data/database-extensions/database-extensions-query' @@ -48,7 +49,7 @@ export const ExtensionNotInstalled = ({ @@ -97,7 +98,7 @@ export const ExtensionNeedsUpgrade = ({ diff --git a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/CreateAnalyticsBucketForm.tsx b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/CreateAnalyticsBucketForm.tsx index f48a5d1de58ac..25446fb5eb127 100644 --- a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/CreateAnalyticsBucketForm.tsx +++ b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/CreateAnalyticsBucketForm.tsx @@ -24,6 +24,7 @@ import { reservedSuffixes, validBucketNameRegex, } from './CreateAnalyticsBucketForm.utils' +import { getServiceVersionsPath } from '@/components/interfaces/Settings/General/ServiceVersions/ServiceVersions.utils' import { InlineLink } from '@/components/ui/InlineLink' import { useDatabaseExtensionEnableMutation } from '@/data/database-extensions/database-extension-enable-mutation' import { useAnalyticsBucketCreateMutation } from '@/data/storage/analytics-bucket-create-mutation' @@ -226,9 +227,7 @@ export const CreateAnalyticsBucketForm = ({

Update the wrappers extension by upgrading your project from your{' '} - - project settings - {' '} + project settings{' '} before creating an Analytics bucket.{' '} Learn more diff --git a/apps/studio/components/interfaces/Storage/VectorBuckets/VectorBucketDetails/VectorBucketCallouts.tsx b/apps/studio/components/interfaces/Storage/VectorBuckets/VectorBucketDetails/VectorBucketCallouts.tsx index a39c18f825446..acadd747eec08 100644 --- a/apps/studio/components/interfaces/Storage/VectorBuckets/VectorBucketDetails/VectorBucketCallouts.tsx +++ b/apps/studio/components/interfaces/Storage/VectorBuckets/VectorBucketDetails/VectorBucketCallouts.tsx @@ -4,6 +4,7 @@ import { Button } from 'ui' import { Admonition } from 'ui-patterns/Admonition' import { WrapperMeta } from '@/components/interfaces/Integrations/Wrappers/Wrappers.types' +import { getServiceVersionsPath } from '@/components/interfaces/Settings/General/ServiceVersions/ServiceVersions.utils' import { ScaffoldSection } from '@/components/layouts/Scaffold' import { InlineLink } from '@/components/ui/InlineLink' import { DatabaseExtension } from '@/data/database-extensions/database-extensions-query' @@ -40,7 +41,7 @@ export const ExtensionNotInstalled = ({ @@ -84,7 +85,7 @@ export const ExtensionNeedsUpgrade = ({ diff --git a/apps/studio/components/interfaces/Support/CategoryAndSeverityInfo.tsx b/apps/studio/components/interfaces/Support/CategoryAndSeverityInfo.tsx index c8e07af7646ac..b1806a2449791 100644 --- a/apps/studio/components/interfaces/Support/CategoryAndSeverityInfo.tsx +++ b/apps/studio/components/interfaces/Support/CategoryAndSeverityInfo.tsx @@ -186,14 +186,11 @@ const IssueSuggestion = ({ category, projectRef }: { category: string; projectRe High memory or low disk IO bandwidth may be slowing down your database. Verify by checking - the infrastructure activity of your project{' '} - - here - - . + your project's database observability reports{' '} + here. ) } @@ -205,12 +202,9 @@ const IssueSuggestion = ({ category, projectRef }: { category: string; projectRe className={className} title="Have you checked the Query Performance Advisor?" > - Identify slow running queries and get actionable insights on how to optimize them with the - Query Performance Advisor{' '} - - here - - . + Identify slow running queries and get actionable insights on how to optimize them with Query + Performance{' '} + here. ) } diff --git a/apps/studio/components/layouts/ProjectLayout/ConnectingState.tsx b/apps/studio/components/layouts/ProjectLayout/ConnectingState.tsx index 2e67ae92f609f..5ae9884d3d965 100644 --- a/apps/studio/components/layouts/ProjectLayout/ConnectingState.tsx +++ b/apps/studio/components/layouts/ProjectLayout/ConnectingState.tsx @@ -4,6 +4,7 @@ import Link from 'next/link' import { useEffect, useRef } from 'react' import { Badge, Button } from 'ui' +import { getInfrastructurePath } from '@/components/interfaces/Settings/Infrastructure/Infrastructure.utils' import ShimmerLine from '@/components/ui/ShimmerLine' import { useInvalidateProjectDetailsQuery, @@ -89,9 +90,7 @@ const ConnectingState = ({ project }: ConnectingStateProps) => {

diff --git a/apps/studio/components/ui/ComputeBadgeWrapper.tsx b/apps/studio/components/ui/ComputeBadgeWrapper.tsx index 6621861829840..9f8924a3b5951 100644 --- a/apps/studio/components/ui/ComputeBadgeWrapper.tsx +++ b/apps/studio/components/ui/ComputeBadgeWrapper.tsx @@ -5,6 +5,7 @@ import { ComputeBadge } from 'ui-patterns/ComputeBadge' import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' import { getAvailableComputeOptions } from '@/components/interfaces/DiskManagement/DiskManagement.utils' +import { getInfrastructurePath } from '@/components/interfaces/Settings/Infrastructure/Infrastructure.utils' import { ProjectDetail } from '@/data/projects/project-detail-query' import { useOrgSubscriptionQuery } from '@/data/subscriptions/org-subscription-query' import { useProjectAddonsQuery } from '@/data/subscriptions/project-addons-query' @@ -195,9 +196,7 @@ export const ComputeBadgeWrapper = ({ }) }} > - - Upgrade compute - + Upgrade compute diff --git a/apps/studio/components/ui/DatabaseSelector.tsx b/apps/studio/components/ui/DatabaseSelector.tsx index 0543f8f87dd4a..bea0ab15770d1 100644 --- a/apps/studio/components/ui/DatabaseSelector.tsx +++ b/apps/studio/components/ui/DatabaseSelector.tsx @@ -23,6 +23,7 @@ import { } from 'ui' import { Markdown } from '@/components/interfaces/Markdown' +import { getInfrastructurePath } from '@/components/interfaces/Settings/Infrastructure/Infrastructure.utils' import { REPLICA_STATUS } from '@/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.constants' import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query' import { formatDatabaseID, formatDatabaseRegion } from '@/data/read-replicas/replicas.utils' @@ -177,7 +178,7 @@ export const DatabaseSelector = ({ diff --git a/apps/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.tsx b/apps/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.tsx index 9a2acba857a6d..9f665a1d0e558 100644 --- a/apps/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.tsx +++ b/apps/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.tsx @@ -16,7 +16,11 @@ import { } from 'ui' import { RESOURCE_WARNING_MESSAGES } from './ResourceExhaustionWarningBanner.constants' -import { getWarningContent } from './ResourceExhaustionWarningBanner.utils' +import { + getResourceWarningCorrectionUrl, + getWarningContent, + isComputeUpgradeWarning, +} from './ResourceExhaustionWarningBanner.utils' import { mapComputeSizeNameToAddonVariantId } from '@/components/interfaces/DiskManagement/DiskManagement.utils' import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider' import { useResourceWarningsQuery } from '@/data/usage/resource-warnings-query' @@ -26,13 +30,6 @@ import { useTrack } from '@/lib/telemetry/track' import { useAiAssistantStateSnapshot } from '@/state/ai-assistant-state' import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state' -const COMPUTE_UPGRADE_METRICS = ['disk_io', 'cpu', 'ram'] -const COMPUTE_UPGRADE_WARNING_TYPES = [ - 'disk_io_exhaustion', - 'cpu_exhaustion', - 'memory_and_swap_exhaustion', -] - export const ResourceExhaustionWarningBanner = () => { const { ref } = useParams() const router = useRouter() @@ -112,46 +109,18 @@ export const ResourceExhaustionWarningBanner = () => { : RESOURCE_WARNING_MESSAGES[activeWarnings[0] as keyof typeof RESOURCE_WARNING_MESSAGES] ?.metric - const correctionUrlVariants = { - undefined: undefined, - null: '/project/[ref]/settings/[infra-path]', - disk_space: '/project/[ref]/settings/compute-and-disk', - read_only: '/project/[ref]/settings/compute-and-disk', - disk_io: '/project/[ref]/settings/compute-and-disk', - cpu: '/project/[ref]/settings/compute-and-disk', - ram: '/project/[ref]/settings/compute-and-disk', - auth_email_rate_limit: '/project/[ref]/auth/rate-limits', - auth_restricted_email_sending: '/project/[ref]/auth/smtp', - default: (metric: string) => `/project/[ref]/settings/[infra-path]#${metric}`, - } - - const getCorrectionUrl = (metric: string | undefined | null) => { - const variant = metric === undefined ? 'undefined' : metric === null ? 'null' : metric - const url = - correctionUrlVariants[variant as keyof typeof correctionUrlVariants] || - correctionUrlVariants.default(metric as string) - return typeof url === 'function' ? url(metric as string) : url - } - const isFreePlan = organization?.plan?.id === 'free' // True for a single compute warning, or when all active warnings are compute-related - const isComputeUpgradeMetric = - (metric !== null && metric !== undefined && COMPUTE_UPGRADE_METRICS.includes(metric)) || - (activeWarnings.length > 1 && - activeWarnings.every((w) => COMPUTE_UPGRADE_WARNING_TYPES.includes(w))) + const isComputeUpgradeMetric = isComputeUpgradeWarning(metric, activeWarnings) - const correctionUrl = (() => { - if (isComputeUpgradeMetric && isFreePlan) { - return `/org/${organization?.slug ?? '_'}/billing?panel=subscriptionPlan&source=resource_exhaustion_banner` - } - if (isComputeUpgradeMetric && activeWarnings.length > 1) { - return `/project/${ref ?? 'default'}/settings/compute-and-disk` - } - return getCorrectionUrl(metric) - ?.replace('[ref]', ref ?? 'default') - ?.replace('[infra-path]', 'infrastructure') - })() + const correctionUrl = getResourceWarningCorrectionUrl({ + metric, + activeWarnings, + projectRef: ref, + isFreePlan, + organizationSlug: organization?.slug, + }) const buttonText = (() => { if (isComputeUpgradeMetric) return 'Upgrade compute' @@ -182,15 +151,15 @@ export const ResourceExhaustionWarningBanner = () => { warningContent === undefined || (!warningContent?.title && !warningContent?.description) const isUsageOrInfraPage = router.pathname.endsWith('/usage') || router.pathname.endsWith('/infrastructure') - // Compute warnings now link to compute-and-disk, so they should remain visible on infrastructure + // Compute warnings now link to infrastructure, so they should remain visible on usage. const onUsageOrInfraAndNotInReadOnlyMode = isUsageOrInfraPage && !activeWarnings.includes('is_readonly_mode_enabled') && !isComputeUpgradeMetric // Suppress when already on the target page (no-op CTA). Paid-plan compute warnings link to - // compute-and-disk; free-plan links to billing instead, so we keep the banner visible for them. - const onDatabaseSettingsAndInReadOnlyMode = - router.pathname.endsWith('settings/compute-and-disk') && + // infrastructure; free-plan links to billing instead, so we keep the banner visible for them. + const shouldSuppressOnInfrastructurePage = + router.pathname.endsWith('settings/infrastructure') && (activeWarnings.includes('is_readonly_mode_enabled') || (isComputeUpgradeMetric && !isFreePlan)) // these take precedence over each other, so there's only one active warning to check @@ -217,7 +186,7 @@ export const ResourceExhaustionWarningBanner = () => { hasNoWarnings || hasNoWarningContent || onUsageOrInfraAndNotInReadOnlyMode || - onDatabaseSettingsAndInReadOnlyMode || + shouldSuppressOnInfrastructurePage || !isVisible ) { return null diff --git a/apps/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.utils.test.ts b/apps/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.utils.test.ts new file mode 100644 index 0000000000000..0f91e4fb94739 --- /dev/null +++ b/apps/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.utils.test.ts @@ -0,0 +1,79 @@ +import { describe, expect, it } from 'vitest' + +import { + getResourceWarningCorrectionUrl, + isComputeUpgradeWarning, +} from './ResourceExhaustionWarningBanner.utils' + +describe('resource warning correction routes', () => { + it.each(['cpu', 'ram', 'disk_io'])('sends paid-plan %s warnings to Infrastructure', (metric) => { + expect( + getResourceWarningCorrectionUrl({ + metric, + activeWarnings: [`${metric}_warning`], + projectRef: 'project-ref', + isFreePlan: false, + organizationSlug: 'org-slug', + }) + ).toBe('/project/project-ref/settings/infrastructure') + }) + + it('sends free-plan compute warnings to the plan upgrade panel', () => { + expect( + getResourceWarningCorrectionUrl({ + metric: 'cpu', + activeWarnings: ['cpu_exhaustion'], + projectRef: 'project-ref', + isFreePlan: true, + organizationSlug: 'org-slug', + }) + ).toBe('/org/org-slug/billing?panel=subscriptionPlan&source=resource_exhaustion_banner') + }) + + it('treats multiple compute-resource warnings as a compute upgrade', () => { + const activeWarnings = ['cpu_exhaustion', 'memory_and_swap_exhaustion'] + + expect(isComputeUpgradeWarning(null, activeWarnings)).toBe(true) + expect( + getResourceWarningCorrectionUrl({ + metric: null, + activeWarnings, + projectRef: 'project-ref', + isFreePlan: false, + }) + ).toBe('/project/project-ref/settings/infrastructure') + }) + + it.each(['disk_space', 'read_only'])('sends %s warnings to Infrastructure', (metric) => { + expect( + getResourceWarningCorrectionUrl({ + metric, + activeWarnings: ['disk_space_exhaustion'], + projectRef: 'project-ref', + isFreePlan: false, + }) + ).toBe('/project/project-ref/settings/infrastructure') + }) + + it('preserves non-infrastructure correction destinations', () => { + expect( + getResourceWarningCorrectionUrl({ + metric: 'auth_email_rate_limit', + activeWarnings: ['auth_rate_limit_exhaustion'], + projectRef: 'project-ref', + isFreePlan: false, + }) + ).toBe('/project/project-ref/auth/rate-limits') + }) + + it('keeps the fallback metric anchor on Infrastructure', () => { + expect( + getResourceWarningCorrectionUrl({ + metric: 'custom_metric', + activeWarnings: ['custom_warning'], + projectRef: 'project-ref', + isFreePlan: false, + }) + ).toBe('/project/project-ref/settings/infrastructure#custom_metric') + }) +}) diff --git a/apps/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.utils.ts b/apps/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.utils.ts index 671d61616e371..eee6e258a311d 100644 --- a/apps/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.utils.ts +++ b/apps/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.utils.ts @@ -1,6 +1,14 @@ import { RESOURCE_WARNING_MESSAGES } from './ResourceExhaustionWarningBanner.constants' +import { getInfrastructurePath } from '@/components/interfaces/Settings/Infrastructure/Infrastructure.utils' import type { ResourceWarning } from '@/data/usage/resource-warnings-query' +const COMPUTE_UPGRADE_METRICS = ['disk_io', 'cpu', 'ram'] +const COMPUTE_UPGRADE_WARNING_TYPES = [ + 'disk_io_exhaustion', + 'cpu_exhaustion', + 'memory_and_swap_exhaustion', +] + export const getWarningContent = ( resourceWarnings: ResourceWarning, metric: string, @@ -17,3 +25,52 @@ export const getWarningContent = ( contentType ]?.[severity as 'warning' | 'critical'] } + +export const isComputeUpgradeWarning = ( + metric: string | null | undefined, + activeWarnings: string[] +) => + (metric !== null && metric !== undefined && COMPUTE_UPGRADE_METRICS.includes(metric)) || + (activeWarnings.length > 1 && + activeWarnings.every((warning) => COMPUTE_UPGRADE_WARNING_TYPES.includes(warning))) + +export const getResourceWarningCorrectionUrl = ({ + metric, + activeWarnings, + projectRef, + isFreePlan, + organizationSlug, +}: { + metric: string | null | undefined + activeWarnings: string[] + projectRef?: string + isFreePlan: boolean + organizationSlug?: string +}) => { + const isComputeUpgradeMetric = isComputeUpgradeWarning(metric, activeWarnings) + + if (isComputeUpgradeMetric && isFreePlan) { + return `/org/${organizationSlug ?? '_'}/billing?panel=subscriptionPlan&source=resource_exhaustion_banner` + } + + const ref = projectRef ?? 'default' + const infrastructurePath = getInfrastructurePath(ref) + + if (isComputeUpgradeMetric && activeWarnings.length > 1) { + return infrastructurePath + } + + const correctionUrlVariants: Record = { + disk_space: infrastructurePath, + read_only: infrastructurePath, + disk_io: infrastructurePath, + cpu: infrastructurePath, + ram: infrastructurePath, + auth_email_rate_limit: `/project/${ref}/auth/rate-limits`, + auth_restricted_email_sending: `/project/${ref}/auth/smtp`, + } + + if (metric === undefined) return undefined + if (metric === null) return infrastructurePath + return correctionUrlVariants[metric] ?? `${infrastructurePath}#${metric}` +} diff --git a/apps/studio/components/ui/UpgradePlanButton.tsx b/apps/studio/components/ui/UpgradePlanButton.tsx index 0b1a2bacc921f..2967b1b0d7f10 100644 --- a/apps/studio/components/ui/UpgradePlanButton.tsx +++ b/apps/studio/components/ui/UpgradePlanButton.tsx @@ -6,6 +6,7 @@ import { Button } from 'ui' import { ButtonTooltip } from './ButtonTooltip' import { RequestUpgradeToBillingOwners } from './RequestUpgradeToBillingOwners' +import { getInfrastructurePath } from '@/components/interfaces/Settings/Infrastructure/Infrastructure.utils' import { SupportLink } from '@/components/interfaces/Support/SupportLink' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled' @@ -71,7 +72,7 @@ export const UpgradePlanButton = ({ ? `/org/${slug ?? '_'}/billing?panel=costControl&source=${source}` : isOnPaidPlanAndRequestingToPurchaseAddon ? addon === 'computeSize' - ? `/project/${ref ?? '_'}/settings/compute-and-disk` + ? getInfrastructurePath(ref) : `/project/${ref ?? '_'}/settings/addons?panel=${addon}&source=${source}` : `/org/${slug ?? '_'}/billing?panel=subscriptionPlan&source=${source}` diff --git a/apps/studio/pages/project/[ref]/database/settings.tsx b/apps/studio/pages/project/[ref]/database/settings.tsx index 2a99513ac6f37..fda7409aa70e6 100644 --- a/apps/studio/pages/project/[ref]/database/settings.tsx +++ b/apps/studio/pages/project/[ref]/database/settings.tsx @@ -58,7 +58,7 @@ const DatabaseSettings: NextPageWithLayout = () => { {showNewDiskManagementUI ? ( - // This form is hidden if Disk and Compute form is enabled, new form is on ./settings/compute-and-disk + // This form is hidden if Disk and Compute form is enabled, new form is on ./settings/infrastructure ) : ( diff --git a/apps/studio/pages/project/[ref]/observability/database.tsx b/apps/studio/pages/project/[ref]/observability/database.tsx index ebed8ef4feb3d..980782d0b92ff 100644 --- a/apps/studio/pages/project/[ref]/observability/database.tsx +++ b/apps/studio/pages/project/[ref]/observability/database.tsx @@ -17,6 +17,7 @@ import ReportWidget from '@/components/interfaces/Reports/ReportWidget' import { ReportChartUpsell } from '@/components/interfaces/Reports/v2/ReportChartUpsell' import { POOLING_OPTIMIZATIONS } from '@/components/interfaces/Settings/Database/ConnectionPooling/ConnectionPooling.constants' import DiskSizeConfigurationModal from '@/components/interfaces/Settings/Database/DiskSizeConfigurationModal' +import { getInfrastructurePath } from '@/components/interfaces/Settings/Infrastructure/Infrastructure.utils' import { LogsDatePicker } from '@/components/interfaces/Settings/Logs/Logs.DatePickers' import UpgradePrompt from '@/components/interfaces/Settings/Logs/UpgradePrompt' import { DefaultLayout } from '@/components/layouts/DefaultLayout' @@ -373,9 +374,7 @@ const DatabaseUsage = () => {
{project?.cloud_provider === 'AWS' ? ( ) : ( { let redirectUrl if (['cpu', 'ram', 'disk_io'].includes(hash)) { - redirectUrl = `/project/${ref}/settings/infrastructure#${hash}` + redirectUrl = `${getInfrastructurePath(ref)}#${hash}` } else { redirectUrl = `/org/${organization.slug}/usage?projectRef=${ref}` } diff --git a/apps/studio/pages/project/[ref]/settings/compute-and-disk.tsx b/apps/studio/pages/project/[ref]/settings/compute-and-disk.tsx deleted file mode 100644 index 34fca6e71da23..0000000000000 --- a/apps/studio/pages/project/[ref]/settings/compute-and-disk.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { - PageHeader, - PageHeaderDescription, - PageHeaderMeta, - PageHeaderSummary, - PageHeaderTitle, -} from 'ui-patterns/PageHeader' - -import { DiskManagementForm } from '@/components/interfaces/DiskManagement/DiskManagementForm' -import { DefaultLayout } from '@/components/layouts/DefaultLayout' -import SettingsLayout from '@/components/layouts/ProjectSettingsLayout/SettingsLayout' -import type { NextPageWithLayout } from '@/types' - -const ComputeAndDiskSettings: NextPageWithLayout = () => { - return ( - <> - - - - Compute and Disk - - Configure the compute and disk settings for your project. - - - - - - - ) -} - -ComputeAndDiskSettings.getLayout = (page) => ( - - {page} - -) -export default ComputeAndDiskSettings diff --git a/apps/studio/pages/project/[ref]/settings/general.tsx b/apps/studio/pages/project/[ref]/settings/general.tsx index 92bd8b01719e1..5c4af074c56da 100644 --- a/apps/studio/pages/project/[ref]/settings/general.tsx +++ b/apps/studio/pages/project/[ref]/settings/general.tsx @@ -15,6 +15,7 @@ import { DeleteBranchPanel } from '@/components/interfaces/Settings/General/Dele import { DeleteProjectPanel } from '@/components/interfaces/Settings/General/DeleteProjectPanel/DeleteProjectPanel' import { General } from '@/components/interfaces/Settings/General/General' import { Project } from '@/components/interfaces/Settings/General/Project' +import { ServiceVersionsSection } from '@/components/interfaces/Settings/General/ServiceVersions/ServiceVersionsSection' import { TransferProjectPanel } from '@/components/interfaces/Settings/General/TransferProjectPanel/TransferProjectPanel' import { DefaultLayout } from '@/components/layouts/DefaultLayout' import SettingsLayout from '@/components/layouts/ProjectSettingsLayout/SettingsLayout' @@ -55,6 +56,7 @@ const ProjectSettings: NextPageWithLayout = () => { {IS_PLATFORM && ( <> + {/* this is only settable on compliance orgs, currently that means HIPAA orgs */} {!isBranch && hasHipaaAddon && } {projectSettingsCustomDomains && } diff --git a/apps/studio/pages/project/[ref]/settings/infrastructure.tsx b/apps/studio/pages/project/[ref]/settings/infrastructure.tsx index 775a02425b8f3..4883b22859174 100644 --- a/apps/studio/pages/project/[ref]/settings/infrastructure.tsx +++ b/apps/studio/pages/project/[ref]/settings/infrastructure.tsx @@ -1,38 +1,37 @@ -import { InfrastructureActivity } from '@/components/interfaces/Settings/Infrastructure/InfrastructureActivity' -import { InfrastructureInfo } from '@/components/interfaces/Settings/Infrastructure/InfrastructureInfo' +import { + PageHeader, + PageHeaderDescription, + PageHeaderMeta, + PageHeaderSummary, + PageHeaderTitle, +} from 'ui-patterns/PageHeader' + +import { DiskManagementForm } from '@/components/interfaces/DiskManagement/DiskManagementForm' import { DefaultLayout } from '@/components/layouts/DefaultLayout' import SettingsLayout from '@/components/layouts/ProjectSettingsLayout/SettingsLayout' -import { - ScaffoldContainer, - ScaffoldDescription, - ScaffoldDivider, - ScaffoldHeader, - ScaffoldTitle, -} from '@/components/layouts/Scaffold' import type { NextPageWithLayout } from '@/types' -const ProjectInfrastructure: NextPageWithLayout = () => { +const InfrastructureSettings: NextPageWithLayout = () => { return ( <> - - - Infrastructure - - General information regarding your server instance - - - - - - + + + + Infrastructure + + View and configure compute and disk for your project. + + + + + ) } -ProjectInfrastructure.getLayout = (page) => ( +InfrastructureSettings.getLayout = (page) => ( {page} ) - -export default ProjectInfrastructure +export default InfrastructureSettings diff --git a/apps/studio/redirects.shared.test.ts b/apps/studio/redirects.shared.test.ts index 6136ae8095519..1aa495d6e0e81 100644 --- a/apps/studio/redirects.shared.test.ts +++ b/apps/studio/redirects.shared.test.ts @@ -52,6 +52,33 @@ describe('preserveQueryAndHash', () => { }) 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 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({ diff --git a/apps/studio/redirects.shared.ts b/apps/studio/redirects.shared.ts index 4963473bae78c..845f11cb68a75 100644 --- a/apps/studio/redirects.shared.ts +++ b/apps/studio/redirects.shared.ts @@ -114,6 +114,11 @@ export const SHARED_REDIRECTS: StudioRedirect[] = [ destination: '/org/_/billing?panel=subscriptionPlan', permanent: true, }, + { + source: '/project/:ref/settings/compute-and-disk', + destination: '/project/:ref/settings/infrastructure', + permanent: true, + }, { source: '/project/:ref/settings/billing/subscription', has: [{ type: 'query', key: 'panel', value: 'pitr' }], @@ -123,7 +128,7 @@ export const SHARED_REDIRECTS: StudioRedirect[] = [ { source: '/project/:ref/settings/billing/subscription', has: [{ type: 'query', key: 'panel', value: 'computeInstance' }], - destination: '/project/:ref/settings/compute-and-disk', + destination: '/project/:ref/settings/infrastructure#cpu', permanent: true, }, { diff --git a/apps/studio/routeTree.gen.ts b/apps/studio/routeTree.gen.ts index 8f8e52c4d4db9..3e6b5c721d825 100644 --- a/apps/studio/routeTree.gen.ts +++ b/apps/studio/routeTree.gen.ts @@ -104,7 +104,6 @@ import { Route as ProjectRefSettingsIntegrationsRouteImport } from './routes/pro import { Route as ProjectRefSettingsInfrastructureRouteImport } from './routes/project/$ref/settings/infrastructure' import { Route as ProjectRefSettingsGeneralRouteImport } from './routes/project/$ref/settings/general' import { Route as ProjectRefSettingsDashboardRouteImport } from './routes/project/$ref/settings/dashboard' -import { Route as ProjectRefSettingsComputeAndDiskRouteImport } from './routes/project/$ref/settings/compute-and-disk' import { Route as ProjectRefSettingsApiKeysRouteImport } from './routes/project/$ref/settings/api-keys' import { Route as ProjectRefSettingsApiRouteImport } from './routes/project/$ref/settings/api' import { Route as ProjectRefSettingsAddonsRouteImport } from './routes/project/$ref/settings/addons' @@ -808,12 +807,6 @@ const ProjectRefSettingsDashboardRoute = path: '/dashboard', getParentRoute: () => ProjectRefSettingsRoute, } as any) -const ProjectRefSettingsComputeAndDiskRoute = - ProjectRefSettingsComputeAndDiskRouteImport.update({ - id: '/compute-and-disk', - path: '/compute-and-disk', - getParentRoute: () => ProjectRefSettingsRoute, - } as any) const ProjectRefSettingsApiKeysRoute = ProjectRefSettingsApiKeysRouteImport.update({ id: '/api-keys', @@ -2199,7 +2192,6 @@ export interface FileRoutesByFullPath { '/project/$ref/settings/addons': typeof ProjectRefSettingsAddonsRoute '/project/$ref/settings/api': typeof ProjectRefSettingsApiRoute '/project/$ref/settings/api-keys': typeof ProjectRefSettingsApiKeysRouteWithChildren - '/project/$ref/settings/compute-and-disk': typeof ProjectRefSettingsComputeAndDiskRoute '/project/$ref/settings/dashboard': typeof ProjectRefSettingsDashboardRoute '/project/$ref/settings/general': typeof ProjectRefSettingsGeneralRoute '/project/$ref/settings/infrastructure': typeof ProjectRefSettingsInfrastructureRoute @@ -2494,7 +2486,6 @@ export interface FileRoutesByTo { '/project/$ref/realtime/settings': typeof ProjectRefRealtimeSettingsRoute '/project/$ref/settings/addons': typeof ProjectRefSettingsAddonsRoute '/project/$ref/settings/api': typeof ProjectRefSettingsApiRoute - '/project/$ref/settings/compute-and-disk': typeof ProjectRefSettingsComputeAndDiskRoute '/project/$ref/settings/dashboard': typeof ProjectRefSettingsDashboardRoute '/project/$ref/settings/general': typeof ProjectRefSettingsGeneralRoute '/project/$ref/settings/infrastructure': typeof ProjectRefSettingsInfrastructureRoute @@ -2804,7 +2795,6 @@ export interface FileRoutesById { '/project/$ref/settings/addons': typeof ProjectRefSettingsAddonsRoute '/project/$ref/settings/api': typeof ProjectRefSettingsApiRoute '/project/$ref/settings/api-keys': typeof ProjectRefSettingsApiKeysRouteWithChildren - '/project/$ref/settings/compute-and-disk': typeof ProjectRefSettingsComputeAndDiskRoute '/project/$ref/settings/dashboard': typeof ProjectRefSettingsDashboardRoute '/project/$ref/settings/general': typeof ProjectRefSettingsGeneralRoute '/project/$ref/settings/infrastructure': typeof ProjectRefSettingsInfrastructureRoute @@ -3113,7 +3103,6 @@ export interface FileRouteTypes { | '/project/$ref/settings/addons' | '/project/$ref/settings/api' | '/project/$ref/settings/api-keys' - | '/project/$ref/settings/compute-and-disk' | '/project/$ref/settings/dashboard' | '/project/$ref/settings/general' | '/project/$ref/settings/infrastructure' @@ -3408,7 +3397,6 @@ export interface FileRouteTypes { | '/project/$ref/realtime/settings' | '/project/$ref/settings/addons' | '/project/$ref/settings/api' - | '/project/$ref/settings/compute-and-disk' | '/project/$ref/settings/dashboard' | '/project/$ref/settings/general' | '/project/$ref/settings/infrastructure' @@ -3717,7 +3705,6 @@ export interface FileRouteTypes { | '/project/$ref/settings/addons' | '/project/$ref/settings/api' | '/project/$ref/settings/api-keys' - | '/project/$ref/settings/compute-and-disk' | '/project/$ref/settings/dashboard' | '/project/$ref/settings/general' | '/project/$ref/settings/infrastructure' @@ -4650,13 +4637,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ProjectRefSettingsDashboardRouteImport parentRoute: typeof ProjectRefSettingsRoute } - '/project/$ref/settings/compute-and-disk': { - id: '/project/$ref/settings/compute-and-disk' - path: '/compute-and-disk' - fullPath: '/project/$ref/settings/compute-and-disk' - preLoaderRoute: typeof ProjectRefSettingsComputeAndDiskRouteImport - parentRoute: typeof ProjectRefSettingsRoute - } '/project/$ref/settings/api-keys': { id: '/project/$ref/settings/api-keys' path: '/api-keys' @@ -6650,7 +6630,6 @@ interface ProjectRefSettingsRouteChildren { ProjectRefSettingsAddonsRoute: typeof ProjectRefSettingsAddonsRoute ProjectRefSettingsApiRoute: typeof ProjectRefSettingsApiRoute ProjectRefSettingsApiKeysRoute: typeof ProjectRefSettingsApiKeysRouteWithChildren - ProjectRefSettingsComputeAndDiskRoute: typeof ProjectRefSettingsComputeAndDiskRoute ProjectRefSettingsDashboardRoute: typeof ProjectRefSettingsDashboardRoute ProjectRefSettingsGeneralRoute: typeof ProjectRefSettingsGeneralRoute ProjectRefSettingsInfrastructureRoute: typeof ProjectRefSettingsInfrastructureRoute @@ -6667,7 +6646,6 @@ const ProjectRefSettingsRouteChildren: ProjectRefSettingsRouteChildren = { ProjectRefSettingsAddonsRoute: ProjectRefSettingsAddonsRoute, ProjectRefSettingsApiRoute: ProjectRefSettingsApiRoute, ProjectRefSettingsApiKeysRoute: ProjectRefSettingsApiKeysRouteWithChildren, - ProjectRefSettingsComputeAndDiskRoute: ProjectRefSettingsComputeAndDiskRoute, ProjectRefSettingsDashboardRoute: ProjectRefSettingsDashboardRoute, ProjectRefSettingsGeneralRoute: ProjectRefSettingsGeneralRoute, ProjectRefSettingsInfrastructureRoute: ProjectRefSettingsInfrastructureRoute, diff --git a/apps/studio/routes/project/$ref/settings/compute-and-disk.tsx b/apps/studio/routes/project/$ref/settings/compute-and-disk.tsx deleted file mode 100644 index d52452e83d809..0000000000000 --- a/apps/studio/routes/project/$ref/settings/compute-and-disk.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { createFileRoute } from '@tanstack/react-router' - -import ComputeAndDiskPage from '@/pages/project/[ref]/settings/compute-and-disk' - -export const Route = createFileRoute('/project/$ref/settings/compute-and-disk')({ - component: SettingsComputeAndDiskRoute, - staticData: { settingsLayoutTitle: 'Compute and Disk' }, -}) - -function SettingsComputeAndDiskRoute() { - return -} diff --git a/apps/studio/state/shortcuts/registry/project-settings-nav.ts b/apps/studio/state/shortcuts/registry/project-settings-nav.ts index eb941ed35e1ec..eeefb857bfa1c 100644 --- a/apps/studio/state/shortcuts/registry/project-settings-nav.ts +++ b/apps/studio/state/shortcuts/registry/project-settings-nav.ts @@ -10,7 +10,6 @@ import { RegistryDefinations } from '../types' */ export const PROJECT_SETTINGS_NAV_SHORTCUT_IDS = { NAV_PROJECT_SETTINGS_GENERAL: 'nav.project-settings-general', - NAV_PROJECT_SETTINGS_COMPUTE_AND_DISK: 'nav.project-settings-compute-and-disk', NAV_PROJECT_SETTINGS_INFRASTRUCTURE: 'nav.project-settings-infrastructure', NAV_PROJECT_SETTINGS_INTEGRATIONS: 'nav.project-settings-integrations', NAV_PROJECT_SETTINGS_WEBHOOKS: 'nav.project-settings-webhooks', @@ -32,13 +31,6 @@ export const projectSettingsNavRegistry: RegistryDefinations { + const actual = await importOriginal() + return { + ...actual, + IS_PLATFORM: true, + useIsLoggedIn: () => true, + useParams: () => ({ ref: PROJECT_REF }), + } +}) + +vi.mock('@/lib/constants', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + IS_PLATFORM: true, + } +}) + +vi.mock('ui-patterns/Admonition', () => ({ + Admonition: ({ + children, + description, + title, + }: { + children?: ReactNode + description?: ReactNode + title?: ReactNode + }) => ( +
+ {title} + {description} + {children} +
+ ), +})) + +vi.mock('ui-patterns/Chart', () => ({ + Chart: ({ children }: { children: ReactNode }) =>
{children}
, + ChartCard: ({ children }: { children: ReactNode }) =>
{children}
, + ChartContent: ({ children }: { children: ReactNode }) =>
{children}
, + ChartEmptyState: ({ title }: { title: ReactNode }) =>
{title}
, + ChartHeader: ({ children }: { children: ReactNode }) =>
{children}
, + ChartLine: () =>
, + ChartLoadingState: () =>
Loading chart
, + ChartMetric: ({ label, value }: { label: ReactNode; value: ReactNode }) => ( +
+ {label}: {value} +
+ ), +})) + +function mockInfrastructureEndpoints() { + addAPIMock({ + method: 'get', + path: '/platform/projects/:ref', + response: PROJECT, + }) + addAPIMock({ + method: 'get', + path: '/platform/organizations', + response: [ + createMockOrganizationResponse({ + id: 1, + slug: ORGANIZATION_SLUG, + plan: { id: 'pro', name: 'Pro' }, + usage_billing_enabled: true, + }), + ], + }) + addAPIMock({ + method: 'get', + path: '/platform/organizations/:slug/entitlements', + response: { + entitlements: [ + { + config: { enabled: true }, + feature: { + key: 'instances.compute_update_available_sizes', + type: 'boolean', + }, + hasAccess: true, + type: 'boolean', + }, + ], + }, + }) + addAPIMock({ + method: 'get', + path: '/platform/profile/permissions', + response: [ + { + actions: [PermissionAction.UPDATE], + condition: null, + organization_id: 1, + organization_slug: ORGANIZATION_SLUG, + project_ids: [PROJECT.id], + project_refs: [PROJECT_REF], + resources: ['projects'], + restrictive: false, + } satisfies PermissionResponse, + ], + }) + addAPIMock({ + method: 'get', + path: '/platform/projects-resource-warnings', + response: [], + }) + addAPIMock({ + method: 'get', + path: '/platform/projects/:ref/databases', + response: [ + { + cloud_provider: 'AWS', + connection_string_read_only: PROJECT.connectionString, + connectionString: PROJECT.connectionString, + db_host: PROJECT.db_host, + db_name: 'postgres', + db_port: 5432, + db_user: 'postgres', + identifier: PROJECT_REF, + inserted_at: PROJECT.inserted_at, + region: PROJECT.region, + restUrl: PROJECT.restUrl, + size: 't4g.small', + status: 'ACTIVE_HEALTHY', + }, + ], + }) + addAPIMock({ + method: 'get', + path: '/platform/projects/:ref/disk', + response: { + attributes: { + iops: 3000, + size_gb: 100, + throughput_mbps: 125, + throughput_mibps: 125, + type: 'gp3', + }, + }, + }) + addAPIMock({ + method: 'get', + path: '/platform/projects/:ref/disk/util', + response: { + metrics: { + fs_avail_bytes: 60 * 1024 * 1024 * 1024, + fs_size_bytes: 100 * 1024 * 1024 * 1024, + fs_used_bytes: 40 * 1024 * 1024 * 1024, + }, + timestamp: '2026-07-20T00:00:00.000Z', + }, + }) + addAPIMock({ + method: 'get', + path: '/platform/projects/:ref/disk/custom-config', + response: { + growth_percent: 20, + max_size_gb: 1000, + min_increment_gb: 10, + }, + }) + addAPIMock({ + method: 'get', + path: '/platform/projects/:ref/billing/addons', + response: { + available_addons: [ + { + name: 'Compute Instance', + type: 'compute_instance', + variants: COMPUTE_VARIANTS, + }, + ], + ref: PROJECT_REF, + selected_addons: [ + { + type: 'compute_instance', + variant: COMPUTE_VARIANTS[0], + }, + ], + }, + }) + addAPIMock({ + method: 'get', + path: '/platform/projects/:ref/infra-monitoring', + response: () => + HttpResponse.json({ + series: {}, + data: [ + { + period_start: '2026-07-20T00:00:00.000Z', + values: { + max_cpu_usage: '40', + ram_usage: '50', + disk_io_consumption: '60', + pg_database_size: String(40 * 1024 * 1024 * 1024), + disk_fs_used_wal: String(5 * 1024 * 1024 * 1024), + disk_fs_used_system: String(5 * 1024 * 1024 * 1024), + disk_fs_size: String(100 * 1024 * 1024 * 1024), + }, + }, + ], + }), + }) +} + +function renderInfrastructurePage() { + return customRender(, { + profileContext: PROFILE_CONTEXT, + }) +} + +describe('/project/[ref]/settings/infrastructure', () => { + beforeEach(() => { + vi.clearAllMocks() + mockInfrastructureEndpoints() + window.HTMLElement.prototype.scrollIntoView = vi.fn() + }) + + test('loads the complete infrastructure page through MSW and resets an interaction', async () => { + const user = userEvent.setup() + renderInfrastructurePage() + + expect(screen.getByRole('heading', { name: 'Infrastructure', level: 1 })).toBeInTheDocument() + expect(screen.getByText('Scaling')).toBeInTheDocument() + expect(screen.getByText('Compute size')).toBeInTheDocument() + expect(screen.getByRole('heading', { name: 'Disk', level: 2 })).toBeInTheDocument() + // The Advanced section renders only once the project query resolves (isAws) + expect(await screen.findByText('Advanced')).toBeInTheDocument() + + const diskSize = await screen.findByRole('spinbutton', { name: 'Disk size' }) + await waitFor(() => expect(diskSize).toHaveValue(100)) + expect(screen.getByRole('spinbutton', { name: 'IOPS' })).toHaveValue(3000) + expect(screen.getByRole('spinbutton', { name: 'Throughput' })).toHaveValue(125) + expect(screen.getByTestId('metric-CPU')).toHaveTextContent('40%') + expect(screen.getByTestId('metric-Memory')).toHaveTextContent('50%') + expect(screen.getByTestId('metric-Disk')).toHaveTextContent('50%') + + await user.click(screen.getByText('Small')) + expect(screen.getByRole('button', { name: 'Review changes' })).toBeEnabled() + + await user.click(screen.getByRole('button', { name: 'Cancel' })) + await waitFor(() => { + expect(screen.queryByRole('button', { name: 'Review changes' })).not.toBeInTheDocument() + }) + }) + + test('reviews and confirms a compute resize through the add-on API', async () => { + const user = userEvent.setup() + let addonRequest: unknown + + addAPIMock({ + method: 'post', + path: '/platform/projects/:ref/billing/addons', + response: async ({ request }) => { + addonRequest = await request.json() + return new HttpResponse(null, { status: 201 }) + }, + }) + + renderInfrastructurePage() + + await user.click(await screen.findByText('Small')) + await user.click(screen.getByRole('button', { name: 'Review changes' })) + + const dialog = await screen.findByRole('dialog') + expect(within(dialog).getByText('Compute size')).toBeInTheDocument() + expect(within(dialog).getByText('Micro')).toBeInTheDocument() + expect(within(dialog).getByText('Small')).toBeInTheDocument() + + await user.click(within(dialog).getByRole('button', { name: 'Confirm changes' })) + + await waitFor(() => { + expect(addonRequest).toEqual({ + addon_type: 'compute_instance', + addon_variant: 'ci_small', + }) + }) + }) + + test('submits disk and autoscaling changes to both infrastructure APIs', async () => { + const user = userEvent.setup() + let diskRequest: unknown + let autoscaleRequest: unknown + + addAPIMock({ + method: 'post', + path: '/platform/projects/:ref/disk', + response: async ({ request }) => { + diskRequest = await request.json() + return new HttpResponse(null, { status: 201 }) + }, + }) + addAPIMock({ + method: 'post', + path: '/platform/projects/:ref/disk/custom-config', + response: async ({ request }) => { + autoscaleRequest = await request.json() + return HttpResponse.json( + { + growth_percent: 20, + max_size_gb: 1200, + min_increment_gb: 10, + }, + { status: 201 } + ) + }, + }) + + renderInfrastructurePage() + + const diskSize = await screen.findByRole('spinbutton', { name: 'Disk size' }) + await waitFor(() => expect(diskSize).toHaveValue(100)) + await user.clear(diskSize) + await user.type(diskSize, '120') + + const maxDiskSize = screen.getByRole('spinbutton', { name: 'Maximum disk size' }) + await user.clear(maxDiskSize) + await user.type(maxDiskSize, '1200') + + await user.click(screen.getByRole('button', { name: 'Review changes' })) + const dialog = await screen.findByRole('dialog') + expect(within(dialog).getByText('Disk size')).toBeInTheDocument() + expect(within(dialog).getByText('Max disk size')).toBeInTheDocument() + + await user.click(within(dialog).getByRole('button', { name: 'Confirm changes' })) + + await waitFor(() => { + expect(diskRequest).toEqual({ + attributes: { + iops: 3000, + size_gb: 120, + throughput_mbps: 125, + type: 'gp3', + }, + }) + expect(autoscaleRequest).toEqual({ + growth_percent: 20, + max_size_gb: 1200, + min_increment_gb: 10, + }) + }) + }) +}) From 06955798704d454d5b6d13bcd6326d5fdc0c4df6 Mon Sep 17 00:00:00 2001 From: "Andrey A." <56412611+aantti@users.noreply.github.com> Date: Wed, 29 Jul 2026 14:17:18 +0200 Subject: [PATCH 3/7] docs(self-hosted): add a guide covering custom pg extensions (#48203) --- .../NavigationMenu.constants.ts | 4 + .../custom-postgres-extensions.mdx | 258 ++++++++++++++++++ supa-mdx-lint/Rule001HeadingCase.toml | 2 + 3 files changed, 264 insertions(+) create mode 100644 apps/docs/content/guides/self-hosting/custom-postgres-extensions.mdx diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index 1f16c321f6476..cb2ec0b8c1d58 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts @@ -3091,6 +3091,10 @@ export const self_hosting: NavMenuConstant = { { name: 'Configure SAML 2.0 SSO', url: '/guides/self-hosting/self-hosted-saml-sso' }, { name: 'Enable MCP server', url: '/guides/self-hosting/enable-mcp' }, { name: 'Remove superuser access', url: '/guides/self-hosting/remove-superuser-access' }, + { + name: 'Custom Postgres Extensions', + url: '/guides/self-hosting/custom-postgres-extensions', + }, ], }, { diff --git a/apps/docs/content/guides/self-hosting/custom-postgres-extensions.mdx b/apps/docs/content/guides/self-hosting/custom-postgres-extensions.mdx new file mode 100644 index 0000000000000..38a3b4b1eab55 --- /dev/null +++ b/apps/docs/content/guides/self-hosting/custom-postgres-extensions.mdx @@ -0,0 +1,258 @@ +--- +title: 'Custom Postgres Extensions' +description: 'Build a custom Docker image with additional Postgres extensions.' +subtitle: 'Build a custom Docker image with additional Postgres extensions.' +--- + +## Overview + +The `supabase/postgres` image includes a curated set of extensions that are compiled in at build time. There is no runtime mechanism to install a compiled native `.so` extension into a running container: you cannot `apk add` or `apt-get install` an extension package, because the image is built with [Nix](https://nixos.org) and its extension set is fixed when the image is produced. + +To add a native extension that Supabase Postgres doesn't provide, you have to build your own image. This guide walks through that end to end for the current Alpine-based images, using [`pg_uuidv7`](https://github.com/fboulnois/pg_uuidv7) as the running example. + + + +If your extension is written in pure SQL or a trusted procedural language, you don't need a custom image at all. Use [`pg_tle`](https://github.com/aws/pg_tle), which is already bundled and preloaded - run `CREATE EXTENSION pg_tle;` and install your extension through it. + + + +The proper way to add an extension is to compile it into the image's [Nix build](#build-the-extension-into-the-nix-image). That's the most robust route, but it requires Nix, building the image from source, and maintaining a fork. The rest of this guide describes a simpler alternative: build your extension in an ordinary Docker builder and layer it onto the published image. The approach can work for many standard [PGXS](https://www.postgresql.org/docs/current/extend-pgxs.html) extensions - but your mileage may vary, and the constraints in the next section are the trade-off. + + + +Supabase builds, tests, and maintains the official `supabase/postgres` images and their bundled extensions. A custom image built by following this guide is unofficial and unsupported - it's not guaranteed to work, and may break with future changes to the base image. Testing, quality assurance, and ongoing maintenance are your responsibility. + + + +### Why Postgres images need special handling + +The base image is Alpine Linux, but the Postgres binaries and every bundled extension live in a `/nix` store: + +1. The runtime is `glibc`, not `musl`. The Nix-built `postgres` and its extensions are linked against `glibc`. If you compile an extension with Alpine's native toolchain (`apk add build-base`), it links `musl` and fails to load at `CREATE EXTENSION` with an error like `libc.musl-aarch64.so.1: cannot open shared object file`. You must build in a `glibc` environment. +2. Match the major version, and keep the builder's `glibc` no later than the image's. Extensions are ABI-stable across an entire Postgres major version. However, an extension built against a newer `glibc` than the runtime provides will fail to load (`version 'GLIBC_2.xx' not found`). The Supabase image currently ships `glibc 2.40`, so this guide builds on Debian 12 (`postgres:17-bookworm`, `glibc 2.36`), which stays safely below it. Avoid the default `postgres:17` - it's currently Debian 13 and `glibc 2.41`. +3. The module directory is redirected. The running `postgres` is a wrapper script that overrides its library directory via a `NIX_PGLIBDIR` environment variable. Your compiled `.so` must be installed into that directory - not the path `pg_config --pkglibdir` reports. This is why you can't follow an extension's upstream install instructions which typically use `make install` or copy to `pg_config --pkglibdir`. + + + +The tag starts with the Postgres version followed by Supabase's own release numbers. Extension ABI is stable across a major version - but if a future Supabase Postgres image bumps its `glibc`, re-check the rule in point 2. You can read the image's `glibc` version with: + +```sh +docker run --rm --entrypoint sh supabase/postgres:17.6.1.136 -c \ + 'ls -d /nix/store/*glibc-2.*-* 2>/dev/null | grep -oE "glibc-2\.[0-9]+" | sort -uV | tail -1' +``` + + + +## Prerequisites + +- Docker installed and running +- The exact `supabase/postgres` tag your deployment uses. For example, `supabase/postgres:17.6.1.136` +- Extension source that builds with standard [PGXS](https://www.postgresql.org/docs/current/extend-pgxs.html) + + + +Always build against the same major version you run, and rebuild your custom image whenever you upgrade the base image. + + + +## Build the extension + +Use a multi-stage build: a `glibc` builder stage (`postgres:-bookworm`) to compile, and the Supabase image as the runtime stage that installs the artifacts into the correct Nix locations. + +### Step 1: Write the Dockerfile + +The example below builds [`pg_uuidv7`](https://github.com/fboulnois/pg_uuidv7) cloned from its Git repository. + +```dockerfile name=Dockerfile +# syntax=docker/dockerfile:1 +ARG SUPABASE_POSTGRES_TAG=17.6.1.136 +ARG PG_MAJOR=17 + +# --- Builder: glibc image matching the Postgres major version --- +FROM postgres:${PG_MAJOR}-bookworm AS builder +ARG PG_MAJOR +ARG EXT_VERSION=v1.7.0 +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential git ca-certificates postgresql-server-dev-${PG_MAJOR} \ + && rm -rf /var/lib/apt/lists/* +WORKDIR /src +RUN git clone --depth 1 --branch ${EXT_VERSION} https://github.com/fboulnois/pg_uuidv7 . +RUN make + +# --- Runtime: Supabase Alpine image --- +FROM supabase/postgres:${SUPABASE_POSTGRES_TAG} +USER root +COPY --from=builder /src/pg_uuidv7.so /tmp/ +COPY --from=builder /src/pg_uuidv7.control /tmp/ +COPY --from=builder /src/sql/ /tmp/ext-sql/ +# The running postgres wrapper redirects its module dir via NIX_PGLIBDIR. +# Install the .so there; install control/SQL into pg_config --sharedir. +RUN set -eux; \ + PLUGIN_DIR="$(grep -E '^export NIX_PGLIBDIR' /usr/bin/postgres | sed -E "s/.*'([^']*)'.*/\1/")"; \ + SHARE_DIR="$(pg_config --sharedir)/extension"; \ + install -m 755 /tmp/pg_uuidv7.so "$PLUGIN_DIR/"; \ + install -m 644 /tmp/pg_uuidv7.control "$SHARE_DIR/"; \ + install -m 644 /tmp/ext-sql/*.sql "$SHARE_DIR/"; \ + rm -rf /tmp/pg_uuidv7.* /tmp/ext-sql +``` + +The same Dockerfile structure generally works for your own extensions. However, when authoring a new extension from scratch, it's best to use the Nix build instead (refer to [Build the extension into the Nix image](#build-the-extension-into-the-nix-image) below). + +#### Handle dependencies beyond the standard library + +Copying a `.so` onto the image works cleanly only when its sole runtime dependency is the C library (`libc.so.6`) - that's the image's `glibc`, already loaded in the `postgres` process, so it resolves automatically. Any other shared library the extension links (`libcurl`, `OpenSSL`, etc.) must also be present and loadable at runtime. + +The build happens inside Docker - check the compiled `.so` in the builder stage rather than on your host: + +```sh +docker build --target builder -t ext-builder . +docker run --rm --entrypoint sh ext-builder -c 'readelf -d /src/*.so | grep NEEDED' +``` + +- Only `libc.so.6` and `ld-linux-*` - the Dockerfile above is sufficient (`pg_uuidv7` is this case). +- Anything else listed - handle those dependencies with one of the options below. + +Recommended: statically link the extra dependencies into the `.so`, so its only remaining `NEEDED` is `libc.so.6`. After building, re-run the check above and confirm only `libc.so.6` and `ld-linux-*` remain. + +A few things make this more involved than it sounds: + +- The distribution's own static archive is often not enough. A feature-rich `libcurl.a`, for example, includes many dependencies whose static archives aren't all installable - so you typically build a minimal static version of the dependency from source and link that. +- PGXS may ignore the extension's `CFLAGS`. Pass extra include paths via `PG_CPPFLAGS` on the `make` command line rather than editing the Makefile. +- Static linking has limits. It won't help a dependency that `dlopen`s plugins at runtime, and linking a library that's also loaded by another extension (two copies of OpenSSL in one process, say) can clash. +- Some extensions hard-code their own name, so you can't rename one to sidestep a collision with a bundled extension. + +When static linking isn't practical, use the [Nix path](#build-the-extension-into-the-nix-image), which resolves every dependency against the image's own libraries automatically. + +### Step 2: Build the image + +Build against the tag you run in production so the runtime and major version match: + +```sh +docker build \ + --build-arg SUPABASE_POSTGRES_TAG=17.6.1.136 \ + --build-arg PG_MAJOR=17 \ + -t supabase-postgres-custom:17.6.1.136 \ + . +``` + +### Step 3: Use the image in your stack + +Point the `db` service at your custom image in `docker-compose.yml`: + +```yaml name=docker-compose.yml +db: + image: supabase-postgres-custom:17.6.1.136 + # ...leave the rest of the service definition unchanged +``` + +Then recreate the database service so it picks up the new image: + +```sh +sh run.sh recreate db +``` + +### Step 4: Enable the extension + +The Supabase `postgres` role is intentionally not a superuser, and extension creation is gated by [`supautils`](https://github.com/supabase/supautils). A native extension that isn't on the `supautils.privileged_extensions` allow-list can only be created by the `supabase_admin` superuser. You have two options. + +#### Option A: Allow the `postgres` role to create it (no rebuild) + +Append your extension to the allow-list in a custom `supautils` configuration. The Postgres 17 image loads any `.conf` file from `/etc/postgresql-custom/conf.d/`. + +```sh +docker exec supabase-db bash -c ' +CUR=$(psql -U postgres -tAc "show supautils.privileged_extensions" | tr -d "\n") +cat > /etc/postgresql-custom/conf.d/99-custom-extensions.conf < + +`/etc/postgresql-custom/` is on the `db-config` named volume, so this change persists across restarts. Read [Custom Postgres configuration](/docs/guides/self-hosting/postgres-upgrade-17#custom-postgres-configuration) for more on the `conf.d/` mechanism. + + + +#### Option B: Enable it as a superuser + +The image's init scripts run as `supabase_admin` (a superuser) on first boot. Drop a SQL file into the init directory to create the extension automatically for new databases: + +```yaml name=docker-compose.yml +db: + volumes: + # ...keep the existing mounts (the data dir, roles.sql, etc.) and add: + - ./volumes/db/pg_uuidv7.sql:/docker-entrypoint-initdb.d/migrations/99-pg_uuidv7.sql:Z +``` + +```sql name=volumes/db/pg_uuidv7.sql +CREATE EXTENSION IF NOT EXISTS pg_uuidv7; +``` + + + +Init scripts only run when the data directory (`./volumes/db/data`) is empty, that is on first initialization. For an already-initialized database, connect as `supabase_admin` once and run `CREATE EXTENSION` manually. + + + +## Verify the extension + +```sh +docker compose exec db psql -U postgres \ + -c "CREATE EXTENSION IF NOT EXISTS pg_uuidv7;" \ + -c "SELECT uuid_generate_v7();" +``` + +``` + uuid_generate_v7 +-------------------------------------- + 019f897b-1003-7175-b9b5-be46fc0c3990 +(1 row) +``` + +## Extensions that have to be preloaded + +If your extension must be listed in `shared_preload_libraries`, add it with the same `conf.d/` mechanism. The `conf.d/` include runs after the baked-in setting, so restate the current value plus your library: + +```sh +docker exec supabase-db bash -c ' +CUR=$(psql -U postgres -tAc "show shared_preload_libraries" | tr -d "\n") +cat > /etc/postgresql-custom/conf.d/99-preload.conf < + +`shared_preload_libraries` has no append syntax. Include the full existing list plus your library, or you'll disable the extensions Supabase relies on. + + + +## Build the extension into the Nix image + +The Docker approach above layers a separately-compiled `.so` onto the published image. The most reliable way is to add the extension to Supabase's own [Nix](https://nixos.org) build, so it's compiled with the exact same toolchain as everything else in the image. This option sidesteps the `libc`/ABI concerns entirely and is how the bundled extensions are built. It requires Nix, building the image from source, and maintaining a fork of `supabase/postgres`. + +Rough guide to choosing: + +- One-off extension on the stock image, minimal tooling - the Docker approach in this guide. +- Bulletproof ABI match, want it merged upstream, or it needs preload / `supautils` wiring done "the Supabase way" - the Nix build below. + +This guide intentionally doesn't teach Nix. The authoritative, maintained instructions live in the `supabase/postgres` repository: + +- [Adding a new extension package](https://github.com/supabase/postgres/blob/develop/nix/docs/adding-new-package.md) - the main walkthrough (C/C++ and Rust/pgrx patterns, where to register the extension, generating hashes) +- [Creating a `pgrx` extension](https://github.com/supabase/postgres/blob/develop/nix/docs/creating-pgrx-extension.md) - for Rust extensions +- [Building Postgres](https://github.com/supabase/postgres/blob/develop/nix/docs/build-postgres.md) and the full [`nix/docs`](https://github.com/supabase/postgres/tree/develop/nix/docs) directory + +## Keeping the image up to date + +Because your image is pinned to a specific `supabase/postgres` tag and depends on that build's internal Nix paths, treat it as coupled to the base image: + +- Rebuild with the new `SUPABASE_POSTGRES_TAG` (and matching `PG_MAJOR`) every time you upgrade Postgres. +- Re-test `CREATE EXTENSION` after each rebuild. A change to the base OS or Nix layout - as happened when the images moved from Debian to Alpine - can require adjusting the build. diff --git a/supa-mdx-lint/Rule001HeadingCase.toml b/supa-mdx-lint/Rule001HeadingCase.toml index d675f0f6d18d0..5eefe35701799 100644 --- a/supa-mdx-lint/Rule001HeadingCase.toml +++ b/supa-mdx-lint/Rule001HeadingCase.toml @@ -67,6 +67,7 @@ may_uppercase = [ "Disk", "Django", "Docker", + "Dockerfile", "Drain", "Drizzle", "DuckDB", @@ -156,6 +157,7 @@ may_uppercase = [ "Navigable Small World", "Neon", "Next.js", + "Nix", "Node", "Node.js", "Notion", From 67c983caefb722a1a1c9760bacb089d2185c9713 Mon Sep 17 00:00:00 2001 From: "kemal.earth" <606977+kemaldotearth@users.noreply.github.com> Date: Wed, 29 Jul 2026 13:50:56 +0100 Subject: [PATCH 4/7] fix(design-system): small chart and metric card fixes (#48439) ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? This fixes the following: - Our `` and `` which use `` were rendering the wrong font style for the title. The `font-mono` class being overwritten by recent changes, this helps sort cascade so it renders correct. - In our design system, the warning variable for charts was rendering black, this should be fixed to be our warning yellow. - There was an odd padding on `` content area, meaning our line chart wasn't flush to the edges, this required a small extension to `twMerge` so it could resolve. Please have a look around studio in places we have charts to double check nothing is broken. Also compare live design system vs. this branch by checking Logs Bar Chart, Charts and Metrics Card pages. ## Summary by CodeRabbit * **Style** * Refined heading typography for more consistent font and weight styling. * Improved class merging for custom spacing utilities. * **Bug Fixes** * Adjusted composed chart Y-axis sizing for clearer layouts. * Improved warning color fallbacks in log bar charts. --- .../default/block/chart-composed-actions.tsx | 4 ++-- .../registry/default/block/chart-composed-basic.tsx | 4 ++-- .../registry/default/block/chart-composed-table.tsx | 2 +- apps/studio/styles/globals.css | 13 ++++++++++++- packages/ui-patterns/src/LogsBarChart/index.tsx | 4 ++-- packages/ui/src/lib/utils/cn.ts | 10 +++++++++- 6 files changed, 28 insertions(+), 9 deletions(-) diff --git a/apps/design-system/registry/default/block/chart-composed-actions.tsx b/apps/design-system/registry/default/block/chart-composed-actions.tsx index ee4212e416ed3..6e4fbea17bc0b 100644 --- a/apps/design-system/registry/default/block/chart-composed-actions.tsx +++ b/apps/design-system/registry/default/block/chart-composed-actions.tsx @@ -80,7 +80,7 @@ export default function ChartComposedActions() { showYAxis={true} YAxisProps={{ tickFormatter: (value) => `${value}k`, - width: 80, + width: 36, }} isFullHeight={true} /> @@ -92,7 +92,7 @@ export default function ChartComposedActions() { showYAxis={true} YAxisProps={{ tickFormatter: (value) => `${value}k`, - width: 80, + width: 36, }} isFullHeight={true} /> diff --git a/apps/design-system/registry/default/block/chart-composed-basic.tsx b/apps/design-system/registry/default/block/chart-composed-basic.tsx index 607363710eb09..99be1076b0896 100644 --- a/apps/design-system/registry/default/block/chart-composed-basic.tsx +++ b/apps/design-system/registry/default/block/chart-composed-basic.tsx @@ -93,7 +93,7 @@ export default function ComposedChartBasic() { showYAxis={true} YAxisProps={{ tickFormatter: (value) => `${value}k`, - width: 80, + width: 36, }} isFullHeight={true} /> @@ -129,7 +129,7 @@ export default function ComposedChartBasic() { showYAxis={true} YAxisProps={{ tickFormatter: (value) => `${value}k`, - width: 80, + width: 36, }} isFullHeight={true} /> diff --git a/apps/design-system/registry/default/block/chart-composed-table.tsx b/apps/design-system/registry/default/block/chart-composed-table.tsx index a8089ed1f57d0..10ad3b02ed073 100644 --- a/apps/design-system/registry/default/block/chart-composed-table.tsx +++ b/apps/design-system/registry/default/block/chart-composed-table.tsx @@ -73,7 +73,7 @@ export default function ChartComposedTable() { showYAxis={true} YAxisProps={{ tickFormatter: (value) => `${value}k`, - width: 80, + width: 36, }} isFullHeight={true} /> diff --git a/apps/studio/styles/globals.css b/apps/studio/styles/globals.css index b2640fe007573..32d0dd3e28209 100644 --- a/apps/studio/styles/globals.css +++ b/apps/studio/styles/globals.css @@ -208,13 +208,24 @@ body, box-sizing: border-box; } +@layer base { + h1, + h2, + h3, + h4, + h5, + h6 { + @apply font-heading; + } +} + h1, h2, h3, h4, h5, h6 { - @apply font-heading font-semibold; + @apply font-semibold; } .form-group .form-text, diff --git a/packages/ui-patterns/src/LogsBarChart/index.tsx b/packages/ui-patterns/src/LogsBarChart/index.tsx index e8c2e005a6d27..c22c003eda290 100644 --- a/packages/ui-patterns/src/LogsBarChart/index.tsx +++ b/packages/ui-patterns/src/LogsBarChart/index.tsx @@ -13,8 +13,8 @@ const CHART_COLORS = { GREEN_2: 'hsl(var(--brand-500))', RED_1: 'hsl(var(--destructive-default))', RED_2: 'hsl(var(--destructive-500))', - YELLOW_1: 'var(--chart-warning)', - YELLOW_2: 'var(--chart-warning-muted)', + YELLOW_1: 'var(--chart-warning, hsl(var(--warning-default)))', + YELLOW_2: 'var(--chart-warning-muted, hsl(var(--warning-500)))', } type LogsBarChartDatum = { diff --git a/packages/ui/src/lib/utils/cn.ts b/packages/ui/src/lib/utils/cn.ts index 5589c557500dd..a2a546b06c5da 100644 --- a/packages/ui/src/lib/utils/cn.ts +++ b/packages/ui/src/lib/utils/cn.ts @@ -1,5 +1,13 @@ import { ClassValue, clsx } from 'clsx' -import { twMerge } from 'tailwind-merge' +import { extendTailwindMerge } from 'tailwind-merge' + +const twMerge = extendTailwindMerge({ + extend: { + theme: { + spacing: ['card', 'content'], + }, + }, +}) export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) From 4c8ed105d2676b5ba4612b735fbdcf735fc30bcc Mon Sep 17 00:00:00 2001 From: Charis <26616127+charislam@users.noreply.github.com> Date: Wed, 29 Jul 2026 10:43:48 -0400 Subject: [PATCH 5/7] feat(studio): logs SQL execution wiring + source-aware run gestures (#48414) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Feature (SQL editor: execution wiring for logs-source snippets). Part of the stacked SQL-editor "Database vs Logs" query-source series. ## What is the current behavior? The SQL editor only ever runs queries against the user's Postgres database. There is no execution path for a logs (`log_sql`) snippet, and the run-button telemetry event carries no backend discriminator. ## What is the new behavior? - `useRunSource(id)` derives the run backend from the snippet type; a `log_sql` snippet resolves to `{ type: 'logs', dateRange }`, pairing the run with its session time range (default: last hour). - `useLogsSqlExecution` runs a promoted `SafeLogSqlFragment` against the analytics OTEL (ClickHouse) endpoint with the resolved time range as `iso_timestamp_start`/`iso_timestamp_end` request params. The endpoint is **pinned to OTEL** — a snippet's dialect must not flip with org migration. - The run gestures (toolbar button and Cmd+Enter) branch on the source and promote with the matching `acceptUntrusted*` right at the user action, preserving the auditable promotion-at-gesture boundary. pg intellisense is gated off for logs snippets. - The `sql_editor_query_run_button_clicked` telemetry event gains a required `{ source: 'database' | 'logs' }` property, fired from both execution paths. - Capability guard: a `log_sql` snippet is reachable by direct URL regardless of the (later) entry-point flag gating, so `executeLogsQuery` short-circuits when `otelLegacyLogs` is off — recording a clear "not available yet" result message instead of firing a request that would only return an opaque backend error on a non-ClickHouse project. This is a guard on the gesture, not endpoint selection. - Tests: `useRunSource` routing, `useLogsSqlExecution` endpoint/range/structured-error/capability-guard, and a reusable `flags` option on `renderSqlEditorHook`. No UI entry points are added — the feature runs dark until the flag-gated creation/nav PRs later in the stack. ## Additional context Stacked on the query-source series; base branch is `master` now that PR 4 (log date range domain + session state, #48401) is merged. Follow-ups in the stack add the toolbar/creation UI (with a run-affordance gate on `otelLegacyLogs`), nav section, AI dialect support, and reports guard. ## Summary by CodeRabbit * **New Features** * Added support for running log queries directly from the SQL editor. * Log query results, errors, and time ranges are now handled within the editor session. * Added automatic selection between database and log query execution, including support for custom date ranges. * SQL assistance is disabled while editing log queries where database definitions do not apply. * **Tests** * Added coverage for log query execution, date ranges, feature availability, and execution source selection. --- .../interfaces/SQLEditor/SQLEditor.types.ts | 2 +- .../interfaces/SQLEditor/SQLEditor.utils.ts | 2 +- .../interfaces/SQLEditor/SQLEditorContext.tsx | 3 +- .../SQLEditor/SQLEditorControllers.tsx | 62 ++++++-- .../SQLEditor/SQLEditorEditorPanel.tsx | 21 ++- .../interfaces/SQLEditor/SQLEditorLayout.tsx | 33 ++++- .../interfaces/SQLEditor/querySource.ts | 8 ++ .../interfaces/SQLEditor/useAddDefinitions.ts | 15 +- .../SQLEditor/useLogsSqlExecution.test.tsx | 136 ++++++++++++++++++ .../SQLEditor/useLogsSqlExecution.ts | 61 ++++++++ .../SQLEditor/useRunSource.test.tsx | 52 +++++++ .../interfaces/SQLEditor/useRunSource.ts | 27 ++++ .../SQLEditor/useSqlEditorExecution.ts | 2 +- .../tests/lib/sql-editor-test-utils.tsx | 33 +++-- packages/common/telemetry-constants.ts | 4 + 15 files changed, 425 insertions(+), 36 deletions(-) create mode 100644 apps/studio/components/interfaces/SQLEditor/useLogsSqlExecution.test.tsx create mode 100644 apps/studio/components/interfaces/SQLEditor/useLogsSqlExecution.ts create mode 100644 apps/studio/components/interfaces/SQLEditor/useRunSource.test.tsx create mode 100644 apps/studio/components/interfaces/SQLEditor/useRunSource.ts diff --git a/apps/studio/components/interfaces/SQLEditor/SQLEditor.types.ts b/apps/studio/components/interfaces/SQLEditor/SQLEditor.types.ts index bb3c8581a1ea6..38412451c8eef 100644 --- a/apps/studio/components/interfaces/SQLEditor/SQLEditor.types.ts +++ b/apps/studio/components/interfaces/SQLEditor/SQLEditor.types.ts @@ -27,7 +27,7 @@ export type EditorController = { isReady: () => boolean getValue: () => string | undefined getSelectionStartLine: () => number | undefined - getSql: (snippetContent?: UntrustedSqlFragment) => UntrustedSqlFragment | undefined + getSql: (snippetContent?: string) => UntrustedSqlFragment | undefined replaceAll: (text: string, source: string) => void focus: () => void revealLineInCenter: (line: number) => void diff --git a/apps/studio/components/interfaces/SQLEditor/SQLEditor.utils.ts b/apps/studio/components/interfaces/SQLEditor/SQLEditor.utils.ts index de83b8a48e585..aa1ba9120804b 100644 --- a/apps/studio/components/interfaces/SQLEditor/SQLEditor.utils.ts +++ b/apps/studio/components/interfaces/SQLEditor/SQLEditor.utils.ts @@ -457,7 +457,7 @@ export function applyAutoLimit( */ export function getEditorSql( editor: IStandaloneCodeEditor, - snippetContent?: UntrustedSqlFragment + snippetContent?: string ): UntrustedSqlFragment { const selection = editor.getSelection() const selectedValue = selection ? editor.getModel()?.getValueInRange(selection) : undefined diff --git a/apps/studio/components/interfaces/SQLEditor/SQLEditorContext.tsx b/apps/studio/components/interfaces/SQLEditor/SQLEditorContext.tsx index f33ad16992f11..fb9255b01ebd4 100644 --- a/apps/studio/components/interfaces/SQLEditor/SQLEditorContext.tsx +++ b/apps/studio/components/interfaces/SQLEditor/SQLEditorContext.tsx @@ -1,5 +1,4 @@ import type { Monaco } from '@monaco-editor/react' -import type { UntrustedSqlFragment } from '@supabase/pg-meta' import { createContext, use, @@ -103,7 +102,7 @@ export const SQLEditorProvider = ({ [] ) - const getSqlFromEditor = useCallback((snippetContent?: UntrustedSqlFragment) => { + const getSqlFromEditor = useCallback((snippetContent?: string) => { const editorInstance = editorRef.current if (!editorInstance) return undefined return getEditorSql(editorInstance, snippetContent) diff --git a/apps/studio/components/interfaces/SQLEditor/SQLEditorControllers.tsx b/apps/studio/components/interfaces/SQLEditor/SQLEditorControllers.tsx index 26fdb9232069b..b4af89af282b2 100644 --- a/apps/studio/components/interfaces/SQLEditor/SQLEditorControllers.tsx +++ b/apps/studio/components/interfaces/SQLEditor/SQLEditorControllers.tsx @@ -15,17 +15,25 @@ import { } from 'react' import { useSqlEditorDiff, useSqlEditorPrompt } from './hooks' +import { type QuerySource } from './querySource' import type { UtilityTab } from './SQLEditor.types' import { useSQLEditorContext } from './SQLEditorContext' import { useAddDefinitions } from './useAddDefinitions' import { useEditorMount } from './useEditorMount' +import { useLogsSqlExecution } from './useLogsSqlExecution' import { usePrettifyQuery } from './usePrettifyQuery' +import { useRunSource } from './useRunSource' import { useSnippetIdentity } from './useSnippetIdentity' import { useSnippetTitleGenerator } from './useSnippetTitleGenerator' import { useSqlEditorAi } from './useSqlEditorAi' import { useSqlEditorExecution } from './useSqlEditorExecution' import { useSqlEditorShortcuts } from './useSqlEditorShortcuts' import { isValidConnString } from '@/data/fetchers' +import { + untrustedLogSql, + type SafeLogSqlFragment, + type UntrustedLogSqlFragment, +} from '@/data/logs/safe-analytics-sql' import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { useDatabaseSelectorStateSnapshot } from '@/state/database-selector' @@ -73,6 +81,9 @@ type RunContextValue = { potentialIssues: SqlEditorExecution['potentialIssues'] resetPotentialIssues: () => void prettifyQuery: () => void + runSource: QuerySource + executeLogsQuery: (sql: SafeLogSqlFragment) => void + readEditorLogsSql: () => UntrustedLogSqlFragment | undefined } /** Editor-surface UI state: selection, the active results tab, and mount. */ @@ -131,7 +142,9 @@ export const SQLEditorControllersProvider = ({ children }: PropsWithChildren) => const { id, urlId, generatedNewSnippetName, isLoading } = useSnippetIdentity() const { onMount, editorMountCount } = useEditorMount({ id }) - useAddDefinitions(id, monacoRef.current) + const runSource = useRunSource(id) + + useAddDefinitions(id, monacoRef.current, { enabled: runSource.type !== 'logs' }) const { data: databases, isSuccess: isSuccessReadReplicas } = useReadReplicasQuery( { @@ -152,13 +165,31 @@ export const SQLEditorControllersProvider = ({ children }: PropsWithChildren) => return editor.getSql(fallback) }, [editor, id]) - const { executeQuery, isExecuting, potentialIssues, resetPotentialIssues } = - useSqlEditorExecution({ - id, - isDiffOpen, - hasSelection, - setAiTitle, - }) + // Reads the SQL to run from the editor as an UntrustedLogSqlFragment — the logs + // sibling of readEditorSql. Promotion (acceptUntrustedLogsSql) happens at each + // user-action site, never here. + const readEditorLogsSql = useCallback((): UntrustedLogSqlFragment | undefined => { + const snippet = getSqlEditorV2StateSnapshot().snippets[id]?.snippet + const fallback = snippet?.type === 'log_sql' ? snippet.content?.unchecked_sql : undefined + const sql = editor.getSql(fallback) + return sql === undefined ? undefined : untrustedLogSql(sql) + }, [editor, id]) + + const { + executeQuery, + isExecuting: isExecutingDb, + potentialIssues, + resetPotentialIssues, + } = useSqlEditorExecution({ + id, + isDiffOpen, + hasSelection, + setAiTitle, + }) + + const { executeLogsQuery, isExecuting: isExecutingLogs } = useLogsSqlExecution({ id }) + + const isExecuting = isExecutingDb || isExecutingLogs const ai = useSqlEditorAi({ id, editorMountCount, diff, prompt }) const { acceptAiHandler, discardAiHandler } = ai @@ -215,8 +246,21 @@ export const SQLEditorControllersProvider = ({ children }: PropsWithChildren) => potentialIssues, resetPotentialIssues, prettifyQuery, + runSource, + executeLogsQuery, + readEditorLogsSql, }), - [executeQuery, readEditorSql, isExecuting, potentialIssues, resetPotentialIssues, prettifyQuery] + [ + executeQuery, + readEditorSql, + isExecuting, + potentialIssues, + resetPotentialIssues, + prettifyQuery, + runSource, + executeLogsQuery, + readEditorLogsSql, + ] ) const uiValue = useMemo( diff --git a/apps/studio/components/interfaces/SQLEditor/SQLEditorEditorPanel.tsx b/apps/studio/components/interfaces/SQLEditor/SQLEditorEditorPanel.tsx index 78084f48f5c2a..149a713f4e540 100644 --- a/apps/studio/components/interfaces/SQLEditor/SQLEditorEditorPanel.tsx +++ b/apps/studio/components/interfaces/SQLEditor/SQLEditorEditorPanel.tsx @@ -12,6 +12,7 @@ import { useSqlEditorUi, } from './SQLEditorControllers' import ResizableAIWidget from '@/components/ui/AIEditor/ResizableAIWidget' +import { acceptUntrustedLogsSql } from '@/data/logs/safe-analytics-sql' import { detectOS } from '@/lib/helpers' // Load the monaco editor client-side only (does not behave well server-side) @@ -124,16 +125,28 @@ const SQLEditorMainView = () => { const { diff, prompt } = useSqlEditorAssistant() const { isDiffOpen } = diff const { promptState, openPrompt } = prompt - const { executeQuery, readEditorSql, prettifyQuery } = useSqlEditorRun() + const { + executeQuery, + readEditorSql, + prettifyQuery, + runSource, + executeLogsQuery, + readEditorLogsSql, + } = useSqlEditorRun() const { onMount, setHasSelection } = useSqlEditorUi() const os = detectOS() // Run gesture from the editor — promote here, at the user action. const runQuery = useCallback(() => { - const sql = readEditorSql() - if (sql !== undefined) void executeQuery(acceptUntrustedSql(sql)) - }, [executeQuery, readEditorSql]) + if (runSource.type === 'logs') { + const sql = readEditorLogsSql() + if (sql !== undefined) void executeLogsQuery(acceptUntrustedLogsSql(sql)) + } else { + const sql = readEditorSql() + if (sql !== undefined) void executeQuery(acceptUntrustedSql(sql)) + } + }, [executeLogsQuery, executeQuery, readEditorLogsSql, readEditorSql, runSource.type]) return (
diff --git a/apps/studio/components/interfaces/SQLEditor/SQLEditorLayout.tsx b/apps/studio/components/interfaces/SQLEditor/SQLEditorLayout.tsx index 7401f7c38745d..d149bdeb95e9f 100644 --- a/apps/studio/components/interfaces/SQLEditor/SQLEditorLayout.tsx +++ b/apps/studio/components/interfaces/SQLEditor/SQLEditorLayout.tsx @@ -17,6 +17,7 @@ import { import { SQLEditorEditorPanel } from './SQLEditorEditorPanel' import { UtilityActions } from './UtilityPanel/UtilityActions' import { UtilityPanel } from './UtilityPanel/UtilityPanel' +import { acceptUntrustedLogsSql } from '@/data/logs/safe-analytics-sql' const SQLEditorRunWarningModal = () => { const { refocusEditor, clearPendingRunRefocus, markRefocusAfterRun } = useSQLEditorContext() @@ -58,16 +59,38 @@ const SQLEditorToolbar = () => { const { clearPendingRunRefocus, markRefocusAfterRun } = useSQLEditorContext() const { id } = useSqlEditorSnippet() const { diff } = useSqlEditorAssistant() - const { executeQuery, readEditorSql, isExecuting, prettifyQuery } = useSqlEditorRun() + const { + executeQuery, + readEditorSql, + isExecuting, + prettifyQuery, + runSource, + executeLogsQuery, + readEditorLogsSql, + } = useSqlEditorRun() const { hasSelection } = useSqlEditorUi() // Run gesture from the toolbar button — promote here, at the user action. const runQuery = useCallback(() => { markRefocusAfterRun() - const sql = readEditorSql() - if (sql === undefined) return clearPendingRunRefocus() - void executeQuery(acceptUntrustedSql(sql)) - }, [clearPendingRunRefocus, executeQuery, markRefocusAfterRun, readEditorSql]) + if (runSource.type === 'logs') { + const sql = readEditorLogsSql() + if (sql === undefined) return clearPendingRunRefocus() + void executeLogsQuery(acceptUntrustedLogsSql(sql)) + } else { + const sql = readEditorSql() + if (sql === undefined) return clearPendingRunRefocus() + void executeQuery(acceptUntrustedSql(sql)) + } + }, [ + clearPendingRunRefocus, + executeLogsQuery, + executeQuery, + markRefocusAfterRun, + readEditorLogsSql, + readEditorSql, + runSource.type, + ]) return ( { +export const useAddDefinitions = ( + id: string, + monaco: Monaco | null, + { enabled = true }: { enabled?: boolean } = {} +) => { const { data: project } = useSelectedProjectQuery() const snapV2 = useSqlEditorV2StateSnapshot() @@ -29,28 +33,28 @@ export const useAddDefinitions = (id: string, monaco: Monaco | null) => { projectRef: project?.ref, connectionString: project?.connectionString, }, - { enabled: intellisenseEnabled } + { enabled: enabled && intellisenseEnabled } ) const { data: functions, isSuccess: isFunctionsSuccess } = useDatabaseFunctionsQuery( { projectRef: project?.ref, connectionString: project?.connectionString, }, - { enabled: intellisenseEnabled } + { enabled: enabled && intellisenseEnabled } ) const { data: schemas, isSuccess: isSchemasSuccess } = useSchemasQuery( { projectRef: project?.ref, connectionString: project?.connectionString, }, - { enabled: intellisenseEnabled } + { enabled: enabled && intellisenseEnabled } ) const { data: tableColumns, isSuccess: isTableColumnsSuccess } = useTableColumnsQuery( { projectRef: project?.ref, connectionString: project?.connectionString, }, - { enabled: intellisenseEnabled } + { enabled: enabled && intellisenseEnabled } ) const pgInfoRef = useRef(null) @@ -58,6 +62,7 @@ export const useAddDefinitions = (id: string, monaco: Monaco | null) => { const filteredSchemas = useSchemasFilteredForHighAvailability(schemas) const isPgInfoReady = + enabled && intellisenseEnabled && isTableColumnsSuccess && isSchemasSuccess && diff --git a/apps/studio/components/interfaces/SQLEditor/useLogsSqlExecution.test.tsx b/apps/studio/components/interfaces/SQLEditor/useLogsSqlExecution.test.tsx new file mode 100644 index 0000000000000..38e3fb8634b26 --- /dev/null +++ b/apps/studio/components/interfaces/SQLEditor/useLogsSqlExecution.test.tsx @@ -0,0 +1,136 @@ +import { act, waitFor } from '@testing-library/react' +import { HttpResponse } from 'msw' +import { beforeEach, describe, expect, it } from 'vitest' + +import { useLogsSqlExecution } from './useLogsSqlExecution' +import { + acceptUntrustedLogsSql, + untrustedLogSql, + type SafeLogSqlFragment, +} from '@/data/logs/safe-analytics-sql' +import { sqlEditorSessionState } from '@/state/sql-editor/sql-editor-session-state' +import { addAPIMock } from '@/tests/lib/msw' +import { + renderSqlEditorHook, + resetSqlEditorStores, + seedSnippet, + setupSqlEditorMocks, +} from '@/tests/lib/sql-editor-test-utils' + +const SNIPPET_ID = 'logs-execution-snippet' + +/** Promote raw text to the `SafeLogSqlFragment` the run pipeline expects, exactly + * as the toolbar/editor-panel promote it right at the user action. */ +const logsSql = (text: string): SafeLogSqlFragment => acceptUntrustedLogsSql(untrustedLogSql(text)) + +type CapturedBody = { sql: string; iso_timestamp_start: string; iso_timestamp_end: string } + +function mockLogsAllOtel(rows: unknown[] = []) { + const captured: CapturedBody[] = [] + addAPIMock({ + method: 'post', + path: '/platform/projects/:ref/analytics/endpoints/logs.all.otel', + response: async ({ request }) => { + const body = (await request.json()) as CapturedBody + captured.push(body) + return HttpResponse.json({ result: rows }) + }, + }) + return captured +} + +/** Renders the hook with ClickHouse logs enabled by default; pass + * `{ otelLegacyLogs: false }` to exercise the not-available-yet guard. */ +function renderLogsExecution(flags: Record = { otelLegacyLogs: true }) { + return renderSqlEditorHook(() => useLogsSqlExecution({ id: SNIPPET_ID }), { flags }) +} + +beforeEach(() => { + resetSqlEditorStores() + setupSqlEditorMocks() + seedSnippet({ id: SNIPPET_ID, source: 'logs' }) +}) + +describe('useLogsSqlExecution', () => { + it('runs a logs query and writes the result to the session store', async () => { + const rows = [{ event_message: 'hello' }] + const captured = mockLogsAllOtel(rows) + + const { result } = renderLogsExecution() + + act(() => { + result.current.executeLogsQuery(logsSql('select event_message from edge_logs')) + }) + + await waitFor(() => expect(sqlEditorSessionState.results[SNIPPET_ID]).toBeDefined()) + expect(sqlEditorSessionState.results[SNIPPET_ID][0].rows).toEqual(rows) + + expect(captured).toHaveLength(1) + expect(captured[0].sql).toContain('select event_message from edge_logs') + expect(captured[0].iso_timestamp_start.length).toBeGreaterThan(0) + expect(captured[0].iso_timestamp_end.length).toBeGreaterThan(0) + }) + + it('records a structured 200-body error on the result instead of throwing', async () => { + addAPIMock({ + method: 'post', + path: '/platform/projects/:ref/analytics/endpoints/logs.all.otel', + response: async () => + HttpResponse.json({ + error: { + code: 400, + errors: [{ domain: 'global', message: 'Missing column', reason: 'invalid' }], + message: 'Missing column', + status: 'INVALID_ARGUMENT', + }, + }), + }) + + const { result } = renderLogsExecution() + + act(() => { + result.current.executeLogsQuery(logsSql('select does_not_exist from edge_logs')) + }) + + await waitFor(() => expect(sqlEditorSessionState.results[SNIPPET_ID]?.[0]?.error).toBeDefined()) + expect(sqlEditorSessionState.results[SNIPPET_ID][0].error.message).toBe('Missing column') + }) + + it('resolves a relative session range to a from/to window around now', async () => { + const captured = mockLogsAllOtel([]) + sqlEditorSessionState.setLogRange(SNIPPET_ID, { + kind: 'relative', + last: { amount: 2, unit: 'hour' }, + }) + + const { result } = renderLogsExecution() + + act(() => { + result.current.executeLogsQuery(logsSql('select 1')) + }) + + await waitFor(() => expect(captured).toHaveLength(1)) + const from = Date.parse(captured[0].iso_timestamp_start) + const to = Date.parse(captured[0].iso_timestamp_end) + expect(Number.isNaN(from)).toBe(false) + expect(Number.isNaN(to)).toBe(false) + expect(from).toBeLessThan(to) + }) + + it('records an unavailable message and fires no request when ClickHouse logs are off', async () => { + const captured = mockLogsAllOtel([]) + + const { result } = renderLogsExecution({ otelLegacyLogs: false }) + + act(() => { + result.current.executeLogsQuery(logsSql('select 1')) + }) + + await waitFor(() => expect(sqlEditorSessionState.results[SNIPPET_ID]?.[0]?.error).toBeDefined()) + expect(sqlEditorSessionState.results[SNIPPET_ID][0].error.message).toBe( + "Querying logs from the SQL editor isn't available for this project yet." + ) + // The doomed request never left the client. + expect(captured).toHaveLength(0) + }) +}) diff --git a/apps/studio/components/interfaces/SQLEditor/useLogsSqlExecution.ts b/apps/studio/components/interfaces/SQLEditor/useLogsSqlExecution.ts new file mode 100644 index 0000000000000..4c1c006bfd7b0 --- /dev/null +++ b/apps/studio/components/interfaces/SQLEditor/useLogsSqlExecution.ts @@ -0,0 +1,61 @@ +import { useFlag, useParams } from 'common' +import { useCallback } from 'react' + +import { DEFAULT_LOG_DATE_RANGE, resolveLogRunRange } from './querySource' +import { useExecuteLogsSqlMutation } from '@/data/logs/execute-logs-sql-mutation' +import { logsAllEndpointUrl } from '@/data/logs/logs-endpoint' +import { type SafeLogSqlFragment } from '@/data/logs/safe-analytics-sql' +import { useTrack } from '@/lib/telemetry/track' +import { + getSqlEditorSessionSnapshot, + useSqlEditorSessionSnapshot, +} from '@/state/sql-editor/sql-editor-session-state' + +type UseLogsSqlExecutionArgs = { id: string } + +/** + * Logs counterpart to `useSqlEditorExecution`. Runs a promoted + * `SafeLogSqlFragment` against the analytics OTEL endpoint with the snippet's + * active time range attached as request params. + */ +export function useLogsSqlExecution({ id }: UseLogsSqlExecutionArgs) { + const { ref: projectRef } = useParams() + const isOtelLogsEnabled = useFlag('otelLegacyLogs') + const track = useTrack() + const sessionSnap = useSqlEditorSessionSnapshot() + + const { mutate, isPending: isExecuting } = useExecuteLogsSqlMutation({ + onSuccess: (data) => { + sessionSnap.addResult(id, data.rows) + }, + onError: (error) => { + sessionSnap.addResultError(id, error) + }, + }) + + const executeLogsQuery = useCallback( + (sql: SafeLogSqlFragment) => { + if (isExecuting || projectRef === undefined) return + + if (!isOtelLogsEnabled) { + getSqlEditorSessionSnapshot().addResultError(id, { + message: "Querying logs from the SQL editor isn't available for this project yet.", + }) + return + } + + // Re-read imperatively so a range picked immediately before the run is + // honored; relative ranges re-resolve against `now` here. + const range = resolveLogRunRange( + getSqlEditorSessionSnapshot().logRange[id] ?? DEFAULT_LOG_DATE_RANGE + ) + + mutate({ projectRef, sql, range, endpoint: logsAllEndpointUrl(true) }) + + track('sql_editor_query_run_button_clicked', { source: 'logs' }) + }, + [id, isExecuting, isOtelLogsEnabled, mutate, projectRef, track] + ) + + return { executeLogsQuery, isExecuting } +} diff --git a/apps/studio/components/interfaces/SQLEditor/useRunSource.test.tsx b/apps/studio/components/interfaces/SQLEditor/useRunSource.test.tsx new file mode 100644 index 0000000000000..820caa97c00f9 --- /dev/null +++ b/apps/studio/components/interfaces/SQLEditor/useRunSource.test.tsx @@ -0,0 +1,52 @@ +import { beforeEach, describe, expect, it } from 'vitest' + +import { DEFAULT_LOG_DATE_RANGE } from './querySource' +import { useRunSource } from './useRunSource' +import { sqlEditorSessionState } from '@/state/sql-editor/sql-editor-session-state' +import { + renderSqlEditorHook, + resetSqlEditorStores, + seedSnippet, + setupSqlEditorMocks, +} from '@/tests/lib/sql-editor-test-utils' + +beforeEach(() => { + resetSqlEditorStores() + setupSqlEditorMocks() +}) + +describe('useRunSource', () => { + it('resolves a database snippet to a database source', () => { + const id = 'database-snippet' + seedSnippet({ id, source: 'database' }) + + const { result } = renderSqlEditorHook(() => useRunSource(id)) + + expect(result.current).toEqual({ type: 'database' }) + }) + + it('resolves a logs snippet with no session range to the default range', () => { + const id = 'logs-snippet-default-range' + seedSnippet({ id, source: 'logs' }) + + const { result } = renderSqlEditorHook(() => useRunSource(id)) + + expect(result.current).toEqual({ type: 'logs', dateRange: DEFAULT_LOG_DATE_RANGE }) + }) + + it('resolves a logs snippet to its session-stored range when one is set', () => { + const id = 'logs-snippet-custom-range' + seedSnippet({ id, source: 'logs' }) + sqlEditorSessionState.setLogRange(id, { + kind: 'relative', + last: { amount: 2, unit: 'hour' }, + }) + + const { result } = renderSqlEditorHook(() => useRunSource(id)) + + expect(result.current).toEqual({ + type: 'logs', + dateRange: { kind: 'relative', last: { amount: 2, unit: 'hour' } }, + }) + }) +}) diff --git a/apps/studio/components/interfaces/SQLEditor/useRunSource.ts b/apps/studio/components/interfaces/SQLEditor/useRunSource.ts new file mode 100644 index 0000000000000..9bccf7fb62b08 --- /dev/null +++ b/apps/studio/components/interfaces/SQLEditor/useRunSource.ts @@ -0,0 +1,27 @@ +import { useMemo } from 'react' + +import { DEFAULT_LOG_DATE_RANGE, type QuerySource } from './querySource' +import { useSqlEditorSessionSnapshot } from '@/state/sql-editor/sql-editor-session-state' +import { useSqlEditorV2StateSnapshot } from '@/state/sql-editor/sql-editor-state' + +/** + * Resolves where a snippet's query runs. A `log_sql` snippet targets the logs + * backend and carries its session time range (falling back to the default when + * the user hasn't picked one); every other snippet targets the database. Source + * is derived from the snippet's content type and is NOT flag-gated here — a + * URL-opened logs snippet routes correctly even with the feature flag off. + */ +export function useRunSource(id: string): QuerySource { + const snapV2 = useSqlEditorV2StateSnapshot() + const sessionSnap = useSqlEditorSessionSnapshot() + + const snippetType = snapV2.snippets[id]?.snippet.type + const logRange = sessionSnap.logRange[id] + + return useMemo(() => { + if (snippetType === 'log_sql') { + return { type: 'logs', dateRange: logRange ?? DEFAULT_LOG_DATE_RANGE } + } + return { type: 'database' } + }, [snippetType, logRange]) +} diff --git a/apps/studio/components/interfaces/SQLEditor/useSqlEditorExecution.ts b/apps/studio/components/interfaces/SQLEditor/useSqlEditorExecution.ts index 7ded54949db57..5d9c7f45a8d78 100644 --- a/apps/studio/components/interfaces/SQLEditor/useSqlEditorExecution.ts +++ b/apps/studio/components/interfaces/SQLEditor/useSqlEditorExecution.ts @@ -139,7 +139,7 @@ export function useSqlEditorExecution({ }, }) - track('sql_editor_query_run_button_clicked') + track('sql_editor_query_run_button_clicked', { source: 'database' }) }, [ editor, diff --git a/apps/studio/tests/lib/sql-editor-test-utils.tsx b/apps/studio/tests/lib/sql-editor-test-utils.tsx index 0f4a6cf855e24..d2e30d49bab96 100644 --- a/apps/studio/tests/lib/sql-editor-test-utils.tsx +++ b/apps/studio/tests/lib/sql-editor-test-utils.tsx @@ -1,6 +1,7 @@ import { untrustedSql } from '@supabase/pg-meta' import type { QueryClient } from '@tanstack/react-query' import { renderHook, type RenderHookOptions } from '@testing-library/react' +import { FeatureFlagContext } from 'common' import { http, HttpResponse } from 'msw' import { NuqsTestingAdapter } from 'nuqs/adapters/testing' import type { ReactNode } from 'react' @@ -323,6 +324,8 @@ type RenderSqlEditorHookOptions = { aiAssistantState?: ReturnType databaseSelectorState?: ReturnType roleImpersonationState?: ReturnType + /** ConfigCat flags to expose via FeatureFlagContext (e.g. `{ otelLegacyLogs: true }`). */ + flags?: Record } export function renderSqlEditorHook( @@ -336,12 +339,10 @@ export function renderSqlEditorHook( options?.roleImpersonationState ?? createRoleImpersonationState('default', { current: async () => ({}) }) - const wrapper = ({ children }: { children: ReactNode }) => ( - + const flags = options?.flags + + const wrapper = ({ children }: { children: ReactNode }) => { + const tree = ( @@ -351,8 +352,24 @@ export function renderSqlEditorHook( - - ) + ) + + return ( + + {flags ? ( + + {tree} + + ) : ( + tree + )} + + ) + } const result = renderHook(hook, { initialProps: options?.initialProps, diff --git a/packages/common/telemetry-constants.ts b/packages/common/telemetry-constants.ts index 6e91ad815e9e0..16b212fa9489e 100644 --- a/packages/common/telemetry-constants.ts +++ b/packages/common/telemetry-constants.ts @@ -1311,6 +1311,10 @@ export interface ImportDataAddedEvent { */ export interface SqlEditorQueryRunButtonClickedEvent { action: 'sql_editor_query_run_button_clicked' + properties: { + /** Which backend the query ran against. */ + source: 'database' | 'logs' + } groups: TelemetryGroups } From 0d2d47c26f4c14c99785b46c7c8a90df95573153 Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Wed, 29 Jul 2026 23:22:45 +0800 Subject: [PATCH 6/7] docs: point dashboard links at the Infrastructure settings page (#48437) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to #48370, which merged the Compute and Disk settings page into Infrastructure and made `/settings/compute-and-disk` a permanent redirect. **Changed:** - Retargeted all 17 dashboard links from `/dashboard/project/_/settings/compute-and-disk` to `/dashboard/project/_/settings/infrastructure` (15 files across guides, troubleshooting entries, and the `migration_warnings` partial) - Updated link text that named the old page ("Compute and Disk settings" → "Infrastructure settings", plus one stale "Database Settings" label in the compute-and-disk guide) Links to the `/docs/guides/platform/compute-and-disk` docs guide are untouched — that page still exists; only dashboard deep links changed. > [!NOTE] > Best merged after #48370 — until then the Infrastructure page doesn't host the compute and disk config (the old URL keeps working either way via the redirect). ## To test - Spot-check a few changed pages on the preview (e.g. `/guides/platform/database-size`, `/guides/troubleshooting/high-cpu-usage`) and confirm the dashboard links land on the Infrastructure settings page - Confirm the compute-and-disk guide page itself still renders and its docs-internal links are unchanged ## Summary by CodeRabbit - **Documentation** - Updated migration and troubleshooting guidance to direct users to the **Infrastructure** settings page, replacing outdated **Compute and Disk** links. - Refreshed platform/database/performance links for resizing, disk throughput/IOPS, upgrade steps, and related troubleshooting to use the updated **Infrastructure** routes and anchors. - Adjusted “Using the CLI” to point to the current local development getting-started page, and refined wording in the “Hit rate” section. --------- Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com> --- apps/docs/content/_partials/migration_warnings.mdx | 4 ++-- apps/docs/content/guides/database/inspect.mdx | 4 ++-- apps/docs/content/guides/platform/compute-and-disk.mdx | 6 +++--- apps/docs/content/guides/platform/database-size.mdx | 4 ++-- apps/docs/content/guides/platform/performance.mdx | 6 +++--- apps/docs/content/troubleshooting/exhaust-disk-io.mdx | 8 ++++---- apps/docs/content/troubleshooting/exhaust-ram.mdx | 8 ++++---- apps/docs/content/troubleshooting/exhaust-swap.mdx | 6 +++--- .../content/troubleshooting/failed-to-retrieve-tables.mdx | 2 +- ...ry-connection-terminated-due-to-connection-timeout.mdx | 2 +- apps/docs/content/troubleshooting/high-cpu-usage.mdx | 6 +++--- apps/docs/content/troubleshooting/http-api-issues.mdx | 8 ++++---- ...tor-lookup-speeds-by-applying-an-hsnw-index-ohLHUM.mdx | 6 +++--- .../interpreting-supabase-grafana-io-charts-MUynDR.mdx | 2 +- .../project-status-reports-unhealthy-services.mdx | 2 +- 15 files changed, 37 insertions(+), 37 deletions(-) diff --git a/apps/docs/content/_partials/migration_warnings.mdx b/apps/docs/content/_partials/migration_warnings.mdx index c221ef2f1d46a..d02a5041503d2 100644 --- a/apps/docs/content/_partials/migration_warnings.mdx +++ b/apps/docs/content/_partials/migration_warnings.mdx @@ -1,7 +1,7 @@ -- If you're planning to migrate a database larger than 6 GB, we recommend [upgrading to at least a Large compute add-on](/docs/guides/platform/compute-add-ons). This will ensure you have the necessary resources to handle the migration efficiently. +- If you're planning to migrate a database larger than 6 GB, we recommend [upgrading to at least a Large compute add-on](/docs/guides/platform/compute-and-disk). This will ensure you have the necessary resources to handle the migration efficiently. -- We strongly advise you to pre-provision the disk space you will need for your migration. On paid projects, you can do this by navigating to the [Compute and Disk Settings](/dashboard/project/_/settings/compute-and-disk) page. For more information on disk scaling and disk limits, check out our [disk settings](/docs/guides/platform/compute-and-disk#disk) documentation. +- We strongly advise you to pre-provision the disk space you will need for your migration. On paid projects, you can do this by navigating to the [Infrastructure settings](/dashboard/project/_/settings/infrastructure) page. For more information on disk scaling and disk limits, check out our [disk settings](/docs/guides/platform/compute-and-disk#disk) documentation. diff --git a/apps/docs/content/guides/database/inspect.mdx b/apps/docs/content/guides/database/inspect.mdx index 8d01413792cf0..350167b8fc495 100644 --- a/apps/docs/content/guides/database/inspect.mdx +++ b/apps/docs/content/guides/database/inspect.mdx @@ -19,7 +19,7 @@ You can examine your database and queries for these issues using either the [Sup The Supabase CLI comes with a range of tools to help inspect your Postgres instances for potential issues. The CLI gets the information from Postgres internals. Therefore, most tools provided are compatible with any Postgres databases regardless if they are a Supabase project or not. -You can find installation instructions for the Supabase CLI here. +You can find installation instructions for the Supabase CLI here. ### The `inspect db` command @@ -240,7 +240,7 @@ from pg_statio_user_tables; This shows the ratio of data blocks fetched from the Postgres [shared_buffers](https://www.postgresql.org/docs/15/runtime-config-resource.html#RUNTIME-CONFIG-RESOURCE-MEMORY) cache against the data blocks that were read from disk/OS cache. -If either of your index or table hit rate are < 99% then this can indicate your compute plan is too small for your current workload and you would benefit from more memory. [Upgrading your compute](/docs/guides/platform/compute-and-disk#compute) is easy and can be done from your [project dashboard](/dashboard/project/_/settings/compute-and-disk). +If either of your index or table hit rate are < 99% then this can indicate your compute plan is too small for your current workload and you would benefit from more memory. [Upgrading your compute](/docs/guides/platform/compute-and-disk#compute) can be done from your [project dashboard](/dashboard/project/_/settings/infrastructure). ### Optimizing poor performing queries diff --git a/apps/docs/content/guides/platform/compute-and-disk.mdx b/apps/docs/content/guides/platform/compute-and-disk.mdx index a14a36589215f..a8d8762d66655 100644 --- a/apps/docs/content/guides/platform/compute-and-disk.mdx +++ b/apps/docs/content/guides/platform/compute-and-disk.mdx @@ -33,11 +33,11 @@ In paid organizations, Nano Compute are billed at the same price as Micro Comput [^1]: Database max connections are recommended values and can be [customized via `max_connections`](/docs/guides/database/custom-postgres-config) depending on your use case. Be aware of [these considerations](/docs/guides/troubleshooting/how-to-change-max-database-connections-_BQ8P5) before modifying. -[^2]: Database size for each compute instance is the default recommendation but the actual performance of your database has many contributing factors, including resources available to it and the size of the data contained within it. See the [shared responsibility model](/docs/guides/platform/shared-responsibility-model) for more information. +[^2]: Database size for each compute instance is the default recommendation but the actual performance of your database has many contributing factors, including resources available to it and the size of the data contained within it. See the [shared responsibility model](/docs/guides/deployment/shared-responsibility-model) for more information. [^3]: Compute resources on the Free plan are subject to change. -Compute sizes can be changed by first selecting your project in the dashboard [here](/dashboard/project/_/settings/compute-and-disk) and the upgrade process will [incur downtime](/docs/guides/platform/compute-and-disk#upgrades). +Compute sizes can be changed by first selecting your project in the dashboard [here](/dashboard/project/_/settings/infrastructure) and the upgrade process will [incur downtime](/docs/guides/platform/compute-and-disk#upgrades). Compute Size Selection diff --git a/apps/docs/content/guides/platform/database-size.mdx b/apps/docs/content/guides/platform/database-size.mdx index bf2419884186b..0e1f1b0dab5dd 100644 --- a/apps/docs/content/guides/platform/database-size.mdx +++ b/apps/docs/content/guides/platform/database-size.mdx @@ -42,7 +42,7 @@ select pg_size_pretty(sum(size)) as wal_size from pg_ls_waldir(); ### Vacuum operations -Postgres does not immediately reclaim the physical space used by dead tuples (i.e., deleted rows) in the DB. They are marked as "removed" until a [vacuum operation](https://www.postgresql.org/docs/current/routine-vacuuming.html) is executed. As a result, deleting data from your database may not immediately reduce the reported disk usage. You can use the [Supabase CLI](/docs/guides/cli/getting-started) `inspect db bloat` command to view all dead tuples in your database. Alternatively, you can run the [query](https://github.com/supabase/cli/blob/c9cce58025fded16b4c332747f819a44f45c3b83/internal/inspect/bloat/bloat.go#L17) found in the CLI's GitHub repo in the [SQL Editor](/dashboard/project/_/sql/) +Postgres does not immediately reclaim the physical space used by dead tuples (i.e., deleted rows) in the DB. They are marked as "removed" until a [vacuum operation](https://www.postgresql.org/docs/current/routine-vacuuming.html) is executed. As a result, deleting data from your database may not immediately reduce the reported disk usage. You can use the [Supabase CLI](/docs/guides/local-development/cli/getting-started) `inspect db bloat` command to view all dead tuples in your database. Alternatively, you can run the [query](https://github.com/supabase/cli/blob/c9cce58025fded16b4c332747f819a44f45c3b83/internal/inspect/bloat/bloat.go#L17) found in the CLI's GitHub repo in the [SQL Editor](/dashboard/project/_/sql/) ```bash # Login to the CLI @@ -152,7 +152,7 @@ set default_transaction_read_only = 'off'; ### Disk size distribution -You can check the distribution of your disk size on your [project's compute and disk page](/dashboard/project/_/settings/compute-and-disk). +You can check the distribution of your disk size on your [project's Infrastructure page](/dashboard/project/_/settings/infrastructure). ![Disk Size Distribution](/docs/img/guides/platform/database-size/disk-size-distribution.png) diff --git a/apps/docs/content/guides/platform/performance.mdx b/apps/docs/content/guides/platform/performance.mdx index b95198471ff97..ae0005551b2b7 100644 --- a/apps/docs/content/guides/platform/performance.mdx +++ b/apps/docs/content/guides/platform/performance.mdx @@ -12,7 +12,7 @@ Unoptimized queries are a major cause of poor database performance. To analyze t ## Optimizing the number of connections -The default connection limits for Postgres and Supavisor is based on your compute size. See the default connection numbers in the [Compute Add-ons](/docs/guides/platform/compute-add-ons) section. +The default connection limits for Postgres and Supavisor is based on your compute size. See the default connection numbers in the [Compute Add-ons](/docs/guides/platform/compute-and-disk) section. If the number of connections is insufficient, you will receive the following error upon connecting to the DB: @@ -23,7 +23,7 @@ FATAL: remaining connection slots are reserved for non-replication superuser con In such a scenario, you can consider: -- [upgrading to a larger compute add-on](/dashboard/project/_/settings/compute-and-disk) +- [upgrading to a larger compute add-on](/dashboard/project/_/settings/infrastructure) - configuring your clients to use fewer connections - manually configuring the database for a higher number of connections @@ -35,7 +35,7 @@ Depending on the clients involved, you might be able to configure them to work w ### Allowing higher number of connections -You can configure Postgres connection limit among other parameters by using [Custom Postgres Config](/docs/guides/platform/custom-postgres-config#custom-postgres-config). +You can configure Postgres connection limit among other parameters by using [Custom Postgres Config](/docs/guides/database/custom-postgres-config). ### Enterprise diff --git a/apps/docs/content/troubleshooting/exhaust-disk-io.mdx b/apps/docs/content/troubleshooting/exhaust-disk-io.mdx index 73249158f86b8..555860671bb35 100644 --- a/apps/docs/content/troubleshooting/exhaust-disk-io.mdx +++ b/apps/docs/content/troubleshooting/exhaust-disk-io.mdx @@ -7,9 +7,9 @@ database_id = "4844905d-1456-44a1-858e-7a4995e5054c" ## Understanding disk IO and disk IO budget -Disk IO refers to two metrics: throughput in Megabytes per second (MB/s) and IOPS which are Input/Output Operations per Second. Throughput measures how much data you can move each second, while IOPS measures how many read/write operations you can perform each second. Depending on the compute add-on of your instance you will have [different baseline performances](/docs/guides/platform/compute-add-ons#compute-size). +Disk IO refers to two metrics: throughput in Megabytes per second (MB/s) and IOPS which are Input/Output Operations per Second. Throughput measures how much data you can move each second, while IOPS measures how many read/write operations you can perform each second. Depending on the compute add-on of your instance you will have [different baseline performances](/docs/guides/platform/compute-and-disk#compute-size). -Smaller compute instances can burst and exceed their baseline performance for a short period of time every day. This is represented as your Disk IO Budget and once your Disk IO Budget is consumed, your instance reverts back to its baseline performance. Learn more about [choosing the right compute instance for consistent disk performance](/docs/guides/platform/compute-add-ons#choosing-the-right-compute-instance-for-consistent-disk-performance). +Smaller compute instances can burst and exceed their baseline performance for a short period of time every day. This is represented as your Disk IO Budget and once your Disk IO Budget is consumed, your instance reverts back to its baseline performance. Learn more about [choosing the right compute instance for consistent disk performance](/docs/guides/platform/compute-and-disk#choosing-the-right-compute-instance-for-consistent-disk-performance). ## Depleting your disk IO budget @@ -38,5 +38,5 @@ Most operations on your Supabase project require disk IO in some form. Hence, th ## How to fix -1. **Upgrade your compute:** You can get a Compute Add-on for your project. Larger compute options (4XL and above) have more consistent disk performance. See your [upgrade options](/dashboard/project/_/settings/compute-and-disk) by selecting your project. Do reference the [different baseline performances](/docs/guides/platform/compute-add-ons#compute-size) that come with larger Compute Add-ons. -2. **Optimize performance:** Get more out of your instance's resources by optimizing your usage. Have a look at our [performance tuning guide](/docs/guides/platform/performance#examining-query-performance) and our [production readiness guide](/docs/guides/platform/going-into-prod#performance). +1. **Upgrade your compute:** You can get a Compute Add-on for your project. Larger compute options (4XL and above) have more consistent disk performance. See your [upgrade options](/dashboard/project/_/settings/infrastructure) by selecting your project. Do reference the [different baseline performances](/docs/guides/platform/compute-and-disk#compute-size) that come with larger Compute Add-ons. +2. **Optimize performance:** Get more out of your instance's resources by optimizing your usage. Have a look at our [performance tuning guide](/docs/guides/platform/performance#examining-query-performance) and our [production readiness guide](/docs/guides/deployment/going-into-prod#performance). diff --git a/apps/docs/content/troubleshooting/exhaust-ram.mdx b/apps/docs/content/troubleshooting/exhaust-ram.mdx index ccea52cea00b8..51601f7c25628 100644 --- a/apps/docs/content/troubleshooting/exhaust-ram.mdx +++ b/apps/docs/content/troubleshooting/exhaust-ram.mdx @@ -17,7 +17,7 @@ You may observe elevated memory usage even when your database has little to no l ## Issues with high memory usage -Every Supabase project runs in its own dedicated virtual machine. Your instance will have a different set of hardware provisioned depending on your [compute add-on](/docs/guides/platform/compute-add-ons). Depending on your workload, your compute hardware may not be suitable and can result in high RAM usage. +Every Supabase project runs in its own dedicated virtual machine. Your instance will have a different set of hardware provisioned depending on your [compute add-on](/docs/guides/platform/compute-and-disk). Depending on your workload, your compute hardware may not be suitable and can result in high RAM usage. A good proxy for unhealthy memory usage is swap usage. If you run out of RAM, your system will offload memory to your disk's much slower swap partition. If your swap is above 70%, chances are high that your compute hardware is not suitable for your workload. Head over to your project's [Database Health](/dashboard/project/_/observability/database) to see your swap usage. @@ -38,10 +38,10 @@ It is also possible to monitor your resources and set up alerts using Prometheus Everything you do with your Supabase project requires memory in some form. Hence, there can be many reasons for high RAM usage. Here are some common ones: - **Query performance:** Queries that take a long time to complete (>1 second) could be using your RAM inefficiently. Check our guide on [examining query performance](/docs/guides/platform/performance#examining-query-performance). -- **Too many connections:** Every connection to your database consumes memory. You can check the number of active connections under [Database Roles](/dashboard/project/_/database/roles) after you select your project. Read our guide on [too many open connections](/docs/guides/platform/troubleshooting#too-many-open-connections). +- **Too many connections:** Every connection to your database consumes memory. You can check the number of active connections under [Database Roles](/dashboard/project/_/database/roles) after you select your project. Read our guide on [too many open connections](/docs/guides/troubleshooting/http-api-issues#too-many-open-connections). - **Extensions:** Some extensions such as `timescaledb` or `pg_cron` can use a lot of memory. It can also add up when you have too many extensions running. You can manage your database extensions in the dashboard under [Extensions](/dashboard/project/_/database/extensions). ## How to fix your memory issues -1. **Upgrade your compute:** You can get a Compute Add-on for your project. See your [upgrade options](/dashboard/project/_/settings/compute-and-disk) by selecting your project. -2. **Optimize performance:** Get more out of your instance's resources by optimizing your usage. Have a look at our [performance tuning guide](/docs/guides/platform/performance#examining-query-performance) and our [production readiness guide](/docs/guides/platform/going-into-prod#performance). +1. **Upgrade your compute:** You can get a Compute Add-on for your project. See your [upgrade options](/dashboard/project/_/settings/infrastructure) by selecting your project. +2. **Optimize performance:** Get more out of your instance's resources by optimizing your usage. Have a look at our [performance tuning guide](/docs/guides/platform/performance#examining-query-performance) and our [production readiness guide](/docs/guides/deployment/going-into-prod#performance). diff --git a/apps/docs/content/troubleshooting/exhaust-swap.mdx b/apps/docs/content/troubleshooting/exhaust-swap.mdx index 7a730f17b4232..7b3f3b475ae13 100644 --- a/apps/docs/content/troubleshooting/exhaust-swap.mdx +++ b/apps/docs/content/troubleshooting/exhaust-swap.mdx @@ -15,7 +15,7 @@ High Swap is usually not a problem unless other resources (such as RAM) are cons -Every Supabase project runs on its own dedicated virtual machine. The machine's underlying specs and hardware depend on your [compute add-on](/docs/guides/platform/compute-add-ons). If your hardware isn't suitable for your workload, you might experience high Swap usage. +Every Supabase project runs on its own dedicated virtual machine. The machine's underlying specs and hardware depend on your [compute add-on](/docs/guides/platform/compute-and-disk). If your hardware isn't suitable for your workload, you might experience high Swap usage. Swap is a portion of your instance's disk that is reserved for the operating system to use when the available RAM is used. As it uses the disk, Swap is slower to access and is generally used as a last resort. @@ -58,6 +58,6 @@ Everything you do with your Supabase project requires compute. Hence, there can If you find that your RAM and Swap usage are high, you have three options: -1. **Optimize performance:** Get more out of your instance's resources by optimizing your usage. See the [performance tuning guide](/docs/guides/platform/performance#examining-query-performance) and our [production readiness guide](/docs/guides/platform/going-into-prod#performance). -2. **Upgrade your compute:** You can get a Compute Add-on for your project. Follow [this link](/dashboard/project/_/settings/compute-and-disk) and select your project to see your upgrade options. +1. **Optimize performance:** Get more out of your instance's resources by optimizing your usage. See the [performance tuning guide](/docs/guides/platform/performance#examining-query-performance) and our [production readiness guide](/docs/guides/deployment/going-into-prod#performance). +2. **Upgrade your compute:** You can get a Compute Add-on for your project. Follow [this link](/dashboard/project/_/settings/infrastructure) and select your project to see your upgrade options. 3. **Read Replicas:** You can spread the load on your Supabase project by creating a Read Replica. See [the read replicas guide](/docs/guides/platform/read-replicas) for more information. diff --git a/apps/docs/content/troubleshooting/failed-to-retrieve-tables.mdx b/apps/docs/content/troubleshooting/failed-to-retrieve-tables.mdx index b079f6bf7586d..5160ea8cbb5c8 100644 --- a/apps/docs/content/troubleshooting/failed-to-retrieve-tables.mdx +++ b/apps/docs/content/troubleshooting/failed-to-retrieve-tables.mdx @@ -41,6 +41,6 @@ Once you are confident there will not be a crash loop, you can review the follow - If it was unintentional, double check for any recursive calls in your application code, edge functions or database functions. - Consider your table and your query structure - if your tables are very "wide" (lots of columns) or have complicated data types within them, it may be worth revisiting your architecture. - Continue to monitor your project's [query performance tab](/dashboard/project/_/observability/query-performance) and [enable index advisor](/docs/guides/database/extensions/index_advisor) if you haven't already - especially if there are a lot of select queries. -- If after monitoring your changes you still do not notice improvements, consider upgrading compute if you think this level of activity is going to be regular. It will give you more memory overhead to process tasks like this. You can view all compute offerings [here](/dashboard/project/_/settings/compute-and-disk). +- If after monitoring your changes you still do not notice improvements, consider upgrading compute if you think this level of activity is going to be regular. It will give you more memory overhead to process tasks like this. You can view all compute offerings [here](/dashboard/project/_/settings/infrastructure). If you want to effectively monitor your project's performance minute by minute, you can use the [Metrics API](/docs/guides/telemetry/metrics). diff --git a/apps/docs/content/troubleshooting/failed-to-run-sql-query-connection-terminated-due-to-connection-timeout.mdx b/apps/docs/content/troubleshooting/failed-to-run-sql-query-connection-terminated-due-to-connection-timeout.mdx index e33ace0060993..83e85220dc7c0 100644 --- a/apps/docs/content/troubleshooting/failed-to-run-sql-query-connection-terminated-due-to-connection-timeout.mdx +++ b/apps/docs/content/troubleshooting/failed-to-run-sql-query-connection-terminated-due-to-connection-timeout.mdx @@ -12,7 +12,7 @@ message = "Error: Failed to run sql query: Connection terminated due to connecti This error typically happens when the database is overloaded and causing an outage. As the database does not respond in a timely manner there can be a variety of symptoms such as tables not loading, error messages related to retrieving data and the dashboard seems to be unresponsive. - Check your database health in [Database reports](/dashboard/project/_/observability/database). -- If needed, increase resources in [Compute and Disk](/dashboard/project/_/settings/compute-and-disk). +- If needed, increase resources in [Infrastructure](/dashboard/project/_/settings/infrastructure). - Alternatively, you can restart the database in [Project Settings](/dashboard/project/_/settings/general) but this may be only a temporary fix if the project is undersized / unoptimized. Review the appropriate guides based on your scenario: diff --git a/apps/docs/content/troubleshooting/high-cpu-usage.mdx b/apps/docs/content/troubleshooting/high-cpu-usage.mdx index e710d5df09e7e..b80d3dc4e61a3 100644 --- a/apps/docs/content/troubleshooting/high-cpu-usage.mdx +++ b/apps/docs/content/troubleshooting/high-cpu-usage.mdx @@ -9,7 +9,7 @@ Learn what high CPU usage could mean for your Supabase instance and what could h ## The danger of high CPU usage -Every Supabase project runs in its dedicated virtual machine. Your instance will have a different set of hardware provisioned depending on your [compute add-on](/docs/guides/platform/compute-add-ons). Your hardware may not be suitable for the intended workload and may experience high CPU usage. +Every Supabase project runs in its dedicated virtual machine. Your instance will have a different set of hardware provisioned depending on your [compute add-on](/docs/guides/platform/compute-and-disk). Your hardware may not be suitable for the intended workload and may experience high CPU usage. High CPU usage could come with a range of issues: @@ -39,5 +39,5 @@ Everything you do with your Supabase project requires compute. Hence, there can There are two ways to solve high CPU: -1. **Optimize performance:** Get more out of your instance's resources by optimizing your usage. Have a look at our [performance tuning guide](/docs/guides/platform/performance#examining-query-performance) and our [production readiness guide](/docs/guides/platform/going-into-prod#performance). -2. **Upgrade your compute:** You can get a Compute Add-on for your project. Follow [this link](/dashboard/project/_/settings/compute-and-disk) and select your project to see your upgrade options. +1. **Optimize performance:** Get more out of your instance's resources by optimizing your usage. Have a look at our [performance tuning guide](/docs/guides/platform/performance#examining-query-performance) and our [production readiness guide](/docs/guides/deployment/going-into-prod#performance). +2. **Upgrade your compute:** You can get a Compute Add-on for your project. Follow [this link](/dashboard/project/_/settings/infrastructure) and select your project to see your upgrade options. diff --git a/apps/docs/content/troubleshooting/http-api-issues.mdx b/apps/docs/content/troubleshooting/http-api-issues.mdx index ced7f2e4365b0..936ec4d58c522 100644 --- a/apps/docs/content/troubleshooting/http-api-issues.mdx +++ b/apps/docs/content/troubleshooting/http-api-issues.mdx @@ -21,20 +21,20 @@ Symptoms of HTTP API issues include: The most common class of issues that causes HTTP timeouts and 5xx response codes is the under-provisioning of resources for your project. This can cause your project to be unable to service the traffic it is receiving. -Each Supabase project is provisioned with [segregated compute resources](../platform/compute-add-ons). This allows the project to serve unlimited requests, as long as they can be handled using the resources that have been provisioned. Complex queries, or queries that process larger amounts of data, will require higher amounts of resources. As such, the amount of resources that can handle a high volume of basic queries (or queries involving small amounts of data), will likely be unable to handle a similar volume of complex queries. +Each Supabase project is provisioned with [segregated compute resources](../platform/compute-and-disk). This allows the project to serve unlimited requests, as long as they can be handled using the resources that have been provisioned. Complex queries, or queries that process larger amounts of data, will require higher amounts of resources. As such, the amount of resources that can handle a high volume of basic queries (or queries involving small amounts of data), will likely be unable to handle a similar volume of complex queries. You can view the resource utilization of your Supabase Project using the [reports in the Dashboard](/dashboard/project/_/observability/database). Some common solutions for this issue are: -- [Upgrading](/dashboard/project/_/settings/compute-and-disk) to a [larger compute add-on](../platform/compute-add-ons) in order to serve higher volumes of traffic. +- [Upgrading](/dashboard/project/_/settings/infrastructure) to a [larger compute add-on](../platform/compute-and-disk) in order to serve higher volumes of traffic. - [Optimizing the queries](../platform/performance#examining-query-performance) being executed. - [Using fewer Postgres connections](../platform/performance#configuring-clients-to-use-fewer-connections) can reduce the amount of resources needed on the project. - [Restarting](/dashboard/project/_/settings/general) the project. This only temporarily solves the issue by terminating any ongoing workloads that might be tying up your compute resources. - All databases of the project, including [Read replicas](/docs/guides/platform/read-replicas), will be restarted. - If you only want to restart a specific Read Replica, you can do so from the [Infrastructure Settings page](/dashboard/project/_/settings/infrastructure). -If your [Disk IO budget](../platform/compute-add-ons#disk-io) has been drained, you will need to either wait for it to be replenished the next day, or upgrade to a larger compute add-on to increase the budget available to your project. +If your [Disk IO budget](../platform/compute-and-disk#disk) has been drained, you will need to either wait for it to be replenished the next day, or upgrade to a larger compute add-on to increase the budget available to your project. ## Unable to connect to your Supabase project @@ -46,7 +46,7 @@ Errors about too many open connections can be _temporarily_ resolved by [restart - If you're receiving a `No more connections allowed (max_client_conn)` error: - Configure your applications and services to [use fewer connections](../platform/performance#configuring-clients-to-use-fewer-connections). - - [Upgrade](/dashboard/project/_/settings/compute-and-disk) to a [larger compute add-on](../platform/compute-add-ons) to increase the number of available connections. + - [Upgrade](/dashboard/project/_/settings/infrastructure) to a [larger compute add-on](../platform/compute-and-disk) to increase the number of available connections. - If you're receiving a `sorry, too many clients already` or `remaining connection slots are reserved for non-replication superuser connections` error message in addition to the above suggestions, switch to using the [connection pooler](/docs/guides/database/connecting-to-postgres#connection-pool) instead. ### Connection refused diff --git a/apps/docs/content/troubleshooting/increase-vector-lookup-speeds-by-applying-an-hsnw-index-ohLHUM.mdx b/apps/docs/content/troubleshooting/increase-vector-lookup-speeds-by-applying-an-hsnw-index-ohLHUM.mdx index 8138030cd14c4..a35f30c5325ec 100644 --- a/apps/docs/content/troubleshooting/increase-vector-lookup-speeds-by-applying-an-hsnw-index-ohLHUM.mdx +++ b/apps/docs/content/troubleshooting/increase-vector-lookup-speeds-by-applying-an-hsnw-index-ohLHUM.mdx @@ -72,7 +72,7 @@ show maintenance_work_mem; **4. Increase cores for index creation (optional)** -The `max_parallel_maintenance_workers` variable limits the amount of cores that can be used by maintenance operations, including indexing tables. In your session, you should try to set it to a value roughly 1/2 to 2/3s of your [compute core amount](/docs/guides/platform/compute-add-ons): +The `max_parallel_maintenance_workers` variable limits the amount of cores that can be used by maintenance operations, including indexing tables. In your session, you should try to set it to a value roughly 1/2 to 2/3s of your [compute core amount](/docs/guides/platform/compute-and-disk): ```sql set max_parallel_maintenance_workers to ; @@ -100,12 +100,12 @@ show statement_timeout; **6. Consider temporarily upgrading your compute size (optional)** -If your task is particularly long, you can speed it up by boosting your computing power temporarily. Compute size is charged by the hour, so you can increase it for an hour or two to finish your task faster, then scale it back afterwards. Here is a list of [compute add-ons](/docs/guides/platform/compute-add-ons). If you want to temporarily upgrade, you can find the add-ons for your project in your [Dashboard's Add-ons Settings.](https://supabase.green/dashboard/project/_/settings/addons) +If your task is particularly long, you can speed it up by boosting your computing power temporarily. Compute size is charged by the hour, so you can increase it for an hour or two to finish your task faster, then scale it back afterwards. Here is a list of [compute add-ons](/docs/guides/platform/compute-and-disk). If you want to temporarily upgrade, you can find the add-ons for your project in your [Dashboard's Add-ons Settings.](https://supabase.green/dashboard/project/_/settings/addons) **7. Consider increasing disk size (optional)** HSNW indexes can produce temporary files during their construction that may consume a few GBs worth of disk. -Consider increasing the disk size in the [Compute and Disk settings](/dashboard/project/_/settings/compute-and-disk) to accommodate for short-term disk stress. +Consider increasing the disk size in the [Infrastructure settings](/dashboard/project/_/settings/infrastructure) to accommodate for short-term disk stress. Date: Wed, 29 Jul 2026 16:57:22 +0100 Subject: [PATCH 7/7] feat: logs sticky header (#44020) ## I have read the [CONTRIBUTING.md]() file. YES ## What kind of change does this PR introduce? [Supabase Studio > Logs]() ## What is the current behavior? When you click on an log row and you scroll the header disappears so you can not close the log straight away you have to scroll back up. ## What is the new behavior? https://github.com/user-attachments/assets/8cac74d8-e3ce-429c-a9ca-393779d1efd9 ## Additional context ## Summary by CodeRabbit * **UI Improvements** * The log selection tabs now stay visible while scrolling, with a fixed header style, better layering, and a solid background for improved readability. --- .../studio/components/interfaces/Settings/Logs/LogSelection.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/studio/components/interfaces/Settings/Logs/LogSelection.tsx b/apps/studio/components/interfaces/Settings/Logs/LogSelection.tsx index 344ed4c94f9f1..b0ef9ed89a0fe 100644 --- a/apps/studio/components/interfaces/Settings/Logs/LogSelection.tsx +++ b/apps/studio/components/interfaces/Settings/Logs/LogSelection.tsx @@ -92,7 +92,7 @@ const LogSelection = ({ log, onClose, queryType, isLoading, error }: LogSelectio
- + Details