Merge branch 'master' of github.com:supabase/supabase

This commit is contained in:
Terry Sutton committed 2025-02-28 14:40:01 -03:30
commit eccc8cd970
55 files changed
+2445 -627

No files matched your search

@@ -24,7 +24,7 @@ enable row level security;
</Admonition>
RLS is incredibly powerful and flexible, allowing you to write complex SQL rules that fit your unique business needs. RLS can be [combined with Supabase Auth](/docs/guides/database/postgres/row-level-security) for end-to-end user security from the browser to the database.
RLS is incredibly powerful and flexible, allowing you to write complex SQL rules that fit your unique business needs. RLS can be combined with [Supabase Auth](/docs/guides/auth) for end-to-end user security from the browser to the database.
RLS is a Postgres primitive and can provide "[defense in depth](<https://en.wikipedia.org/wiki/Defense_in_depth_(computing)>)" to protect your data from malicious actors even when accessed through third-party tooling.
+1 -1
View File
@@ -77,7 +77,7 @@ Before deploying the Edge Function, we need to ensure it bundles the Wasm module
```toml
[functions.wasm-add]
static_files = [ "./functions/wasm-add/add-wasm/pkg/*.wasm"]
static_files = [ "./functions/wasm-add/add-wasm/pkg/*"]
```
Deploy the function by running:
@@ -98,7 +98,9 @@ The compute size of your project sets the upper limit for disk throughput and IO
| 12XL | 14,250 MiB/s | 50,000 IOPS |
| 16XL | 19,000 MiB/s | 80,000 IOPS |
Smaller compute instances like Nano, Micro, and Small have baseline performance levels that can occasionally be exceeded. Larger compute-instances (4XL and above) are designed for sustained, high performance with specific IOPS and throughput limits. If you hit your IOPS or throughput limit, throttling will occur.
Smaller compute instances like Nano, Micro, Small, and Medium have baseline performance levels that can occasionally be exceeded for short periods of time. If it does exceed the baseline, you should consider upgrading your instance size for a more reliable performance.
Larger compute instances (4XL and above) are designed for sustained, high performance with specific IOPS and throughput limits which you can [configure](/docs/guides/platform/manage-your-usage/disk-throughput). If you hit your IOPS or throughput limit, throttling will occur.
### Choosing the right compute instance for consistent disk performance
@@ -106,7 +108,7 @@ If you need consistent disk performance, choose the 4XL or larger compute instan
### Provisioned disk throughput and IOPS
The default disk type is gp3, which comes with a baseline throughput of 125 MiB/s and a default IOPS of 3,000. You can provision additional IOPS and throughput from the [Database Settings](/dashboard/project/_/settings/database) page, but keep in mind that the effective IOPS and throughput will be limited by the compute instance size.
The default disk type is gp3, which comes with a baseline throughput of 125 MiB/s and a default IOPS of 3,000. You can provision additional IOPS and throughput from the [Database Settings](https://supabase.com/dashboard/project/_/settings/compute-and-disk) page, but keep in mind that the effective IOPS and throughput will be limited by the compute instance size. This requires Large compute size or above.
<Admonition type="caution">
@@ -12,6 +12,10 @@ sdk = [ "auth-getuser" ]
[[errors]]
http_status_code = 401
message = "invalid claim: missing sub"
[[errors]]
http_status_code = 403
message = "invalid claim: missing sub"
---
The missing sub claim error is returned when `supabase.auth.getUser()` is called with an invalid JWT in the session or when the user attempts to register/sign in but hasn't completed the sign in when the `getUser` call is made.
@@ -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 Megabits per Second and IOPS which are Input/Output Operations per Second. Depending on the compute add-on of your instance you will have [different baseline performances](https://supabase.com/docs/guides/platform/compute-add-ons#disk-io).
Disk IO refers to two metrics: throughput in Megabits per Second and IOPS which are Input/Output Operations per Second. Depending on the compute add-on of your instance you will have [different baseline performances](/docs/guides/platform/compute-add-ons#compute-size).
Smaller compute instances can burst and exceed their baseline performance for a short quota of time every day. This quota is represented as your Disk IO Budget and once your Disk IO Budget is consumed, your instance reverts back to its baseline performance. You can read more about this under [Bursting and Disk IO Budget](https://supabase.com/docs/guides/platform/compute-add-ons#bursting-and-disk-io-budget).
Smaller compute instances can burst and exceed their baseline performance for a short quota 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).
## Depleting your disk IO budget
@@ -17,26 +17,26 @@ Running out of Disk IO Budget means that your instance is using more disk than i
- Response times on requests can increase noticeably
- CPU usage rises noticeably due to IO wait
- Disruption of [daily backup](https://supabase.com/docs/guides/platform/backups#daily-backups) routines
- Disruption of internal Postgres processes such as [autovacuuming](https://supabase.com/docs/guides/platform/database-size#vacuum-operations)
- Disruption of [daily backup](/docs/guides/platform/backups#daily-backups) routines
- Disruption of internal Postgres processes such as [autovacuuming](/docs/guides/platform/database-size#vacuum-operations)
- Your instance may become unresponsive
## Monitor your disk IO budget
To check your Disk IO Budget on the Supabase Platform, head over to [Database Health in the Reports section](https://supabase.com/dashboard/project/_/reports/database).
It is also possible to monitor your resources and set up alerts using Prometheus/Grafana. With Grafana you will be able to see how much of your RAM is used for caching and you can track other metrics such as your Swap usage. Read the [Metrics Guide](https://supabase.com/docs/guides/platform/metrics) to learn more.
It is also possible to monitor your resources and set up alerts using Prometheus/Grafana. With Grafana you will be able to pinpoint potential causes and see more fine-grained metrics like how much of your RAM is used for caching and your Swap usage. Read the [Metrics Guide](/docs/guides/platform/metrics) to learn more.
## Common reasons for high disk IO usage
Most operations on your Supabase project require disk IO in some form. Hence, there can be many reasons for high disk IO usage. Here are some common ones:
- **High Memory Usage:** Every Supabase project has 1GB of disk allocated for swapping. When your memory usage is high, the operating system might frequently move parts of the memory back and forth of the swap space on the disk.
- **Low Cache Usage:** If your cache hit rate is low, many of your database requests might go straight to the disk. Go to the [Cache Hit Rate Guide](https://supabase.com/docs/guides/platform/performance#hit-rate) to learn more.
- **Query performance:** Queries that take a long time to complete (>1 second) could be using your disk inefficiently. Check our guide on [examining query performance](https://supabase.com/docs/guides/platform/performance#examining-query-performance).
- **Low Cache Usage:** If your cache hit rate is low, many of your database requests might go straight to the disk. Go to the [Cache Hit Rate Guide](/docs/guides/platform/performance#hit-rate) to learn more.
- **Query performance:** Queries that take a long time to complete (>1 second) could be using your disk inefficiently. Check our guide on [examining query performance](/docs/guides/platform/performance#examining-query-performance).
- **High popularity:** Congrats! Your side project turned out to be a real success and is getting more requests than it can handle.
## How to fix
1. **Upgrade your compute:** You can get a Compute Add-on for your project. See your [upgrade options](https://supabase.com/dashboard/project/_/settings/compute-and-disk) by selecting your project. Do reference the [different baseline performances](https://supabase.com/docs/guides/platform/compute-add-ons#disk-io) 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](https://supabase.com/docs/guides/platform/performance#examining-query-performance) and our [production readiness guide](https://supabase.com/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](https://supabase.com/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).
@@ -158,7 +158,12 @@ export async function getAllTroubleshootingEntriesInternal() {
].includes(child.type)
)
}
if (node.type === 'link' || node.type === 'image') {
canonicalizeUrl(node)
}
})
const contentWithoutJsx = toMarkdown(mdxTree, {
extensions: [gfmToMarkdown()],
})
@@ -174,6 +179,16 @@ export async function getAllTroubleshootingEntriesInternal() {
return (await Promise.all(troubleshootingFiles)).filter((x) => x != null)
}
/**
*
* @param {import('mdast').Image | import('mdast').Link} node
*/
function canonicalizeUrl(node) {
if (node.url.startsWith('/')) {
node.url === 'https://supabase.com' + node.url
}
}
/**
* @param {TroubleshootingEntry} entry
*/
@@ -348,3 +348,8 @@ export const CONNECTION_TYPES = [
{ key: 'mobiles', label: 'Mobile Frameworks', obj: MOBILES },
{ key: 'orms', label: 'ORMs', obj: ORMS },
]
// [Joshen] Consolidating reusable copy here
export const PGBOUNCER_ENABLED_BUT_NO_IPV4_ADDON_TEXT =
'Purchase IPv4 add-on or use Supavisor if on a IPv4 network'
export const IPV4_ADDON_TEXT = 'Connections are IPv4 proxied with IPv4 add-on'
@@ -4,7 +4,9 @@ import { forwardRef, HTMLAttributes, useMemo } from 'react'
import { useParams } from 'common'
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
import { usePoolingConfigurationQuery } from 'data/database/pooling-configuration-query'
import { usePgbouncerConfigQuery } from 'data/database/pgbouncer-config-query'
import { useSupavisorConfigurationQuery } from 'data/database/supavisor-configuration-query'
import { useFlag } from 'hooks/ui/useFlag'
import { pluckObjectFields } from 'lib/helpers'
import { cn } from 'ui'
import type { projectKeys } from './Connect.types'
@@ -23,18 +25,36 @@ interface ConnectContentTabProps extends HTMLAttributes<HTMLDivElement> {
const ConnectTabContent = forwardRef<HTMLDivElement, ConnectContentTabProps>(
({ projectKeys, filePath, ...props }, ref) => {
const { ref: projectRef } = useParams()
const allowPgBouncerSelection = useFlag('dualPoolerSupport')
const { data: settings } = useProjectSettingsV2Query({ projectRef })
const { data: poolingInfo } = usePoolingConfigurationQuery({ projectRef })
const { data: pgbouncerConfig } = usePgbouncerConfigQuery({ projectRef })
const { data: supavisorConfig } = useSupavisorConfigurationQuery({ projectRef })
const isPgBouncerEnabled = allowPgBouncerSelection && !!pgbouncerConfig?.pgbouncer_enabled
const DB_FIELDS = ['db_host', 'db_name', 'db_port', 'db_user', 'inserted_at']
const emptyState = { db_user: '', db_host: '', db_port: '', db_name: '' }
const connectionInfo = pluckObjectFields(settings || emptyState, DB_FIELDS)
const poolingConfiguration = poolingInfo?.find((x) => x.database_type === 'PRIMARY')
const poolingConfiguration = isPgBouncerEnabled
? pgbouncerConfig
: supavisorConfig?.find((x) => x.database_type === 'PRIMARY')
const connectionStrings =
poolingConfiguration !== undefined
? getConnectionStringsV2(connectionInfo, poolingConfiguration, { projectRef })
? getConnectionStringsV2({
connectionInfo,
poolingInfo: {
connectionString:
'connection_string' in poolingConfiguration
? poolingConfiguration.connection_string
: poolingConfiguration.connectionString,
db_host: poolingConfiguration.db_host,
db_name: poolingConfiguration.db_name,
db_port: poolingConfiguration.db_port,
db_user: poolingConfiguration.db_user,
},
metadata: { projectRef },
})
: { direct: { uri: '' }, pooler: { uri: '' } }
const connectionStringsPooler = connectionStrings.pooler
const connectionStringsDirect = connectionStrings.direct
@@ -3,9 +3,10 @@ import Link from 'next/link'
import { useParams } from 'common'
import Panel from 'components/ui/Panel'
import { usePoolingConfigurationQuery } from 'data/database/pooling-configuration-query'
import { useSupavisorConfigurationQuery } from 'data/database/supavisor-configuration-query'
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
import {
Badge,
Button,
cn,
CodeBlock,
@@ -21,6 +22,7 @@ import { DirectConnectionIcon, TransactionIcon } from './PoolerIcons'
interface ConnectionPanelProps {
type?: 'direct' | 'transaction' | 'session'
badge?: string
title: string
description: string
connectionString: string
@@ -28,7 +30,7 @@ interface ConnectionPanelProps {
type: 'error' | 'success'
title: string
description?: string
link?: { text: string; url: string }
links?: { text: string; url: string }[]
}
notice?: string[]
parameters?: Array<{
@@ -100,6 +102,7 @@ export const CodeBlockFileHeader = ({ title }: { title: string }) => {
export const ConnectionPanel = ({
type = 'direct',
badge,
title,
description,
connectionString,
@@ -113,14 +116,19 @@ export const ConnectionPanel = ({
const { ref: projectRef } = useParams()
const state = useDatabaseSelectorStateSnapshot()
const { data: poolingInfo } = usePoolingConfigurationQuery({ projectRef })
const { data: poolingInfo } = useSupavisorConfigurationQuery({ projectRef })
const poolingConfiguration = poolingInfo?.find((x) => x.identifier === state.selectedDatabaseId)
const isSessionMode = poolingConfiguration?.pool_mode === 'session'
const links = ipv4Status.links ?? []
return (
<div className="flex flex-col gap-5 lg:grid lg:grid-cols-2 lg:gap-20 w-full">
<div className="flex flex-col">
<h1 className="text-sm mb-2">{title}</h1>
<div className="flex items-center gap-x-2 mb-2">
<h1 className="text-sm">{title}</h1>
{!!badge && <Badge>{badge}</Badge>}
</div>
<p className="text-sm text-foreground-light mb-4">{description}</p>
<div className="flex flex-col -space-y-px">
{fileTitle && <CodeBlockFileHeader title={fileTitle} />}
@@ -216,21 +224,15 @@ export const ConnectionPanel = ({
{ipv4Status.description && (
<span className="text-xs text-foreground-lighter">{ipv4Status.description}</span>
)}
{ipv4Status.type === 'error' && (
<span className="text-xs text-foreground-lighter">
Use Session Pooler if on a IPv4 network or purchase IPv4 addon
</span>
)}
{ipv4Status.link && (
<div className="mt-2">
<Button asChild type="default" size="tiny">
<Link
href={ipv4Status.link.url}
className="text-xs text-light hover:text-foreground"
>
{ipv4Status.link.text}
</Link>
</Button>
{links.length > 0 && (
<div className="flex items-center gap-x-2 mt-2">
{links.map((link) => (
<Button key={link.text} asChild type="default" size="tiny">
<Link href={link.url} className="text-xs text-light hover:text-foreground">
{link.text}
</Link>
</Button>
))}
</div>
)}
</div>
@@ -285,7 +287,7 @@ export const ConnectionPanel = ({
<p className="text-xs text-foreground-lighter max-w-xs">
If you wish to use a Direct Connection with these, please purchase{' '}
<Link
href={ipv4Status.link?.url ?? '/'}
href={`/project/${projectRef}/settings/addons?panel=ipv4`}
className="text-xs text-light hover:text-foreground"
>
IPv4 support
@@ -6,11 +6,13 @@ import { getAddons } from 'components/interfaces/Billing/Subscription/Subscripti
import AlertError from 'components/ui/AlertError'
import DatabaseSelector from 'components/ui/DatabaseSelector'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
import { usePoolingConfigurationQuery } from 'data/database/pooling-configuration-query'
import { usePgbouncerConfigQuery } from 'data/database/pgbouncer-config-query'
import { useSupavisorConfigurationQuery } from 'data/database/supavisor-configuration-query'
import { useReadReplicasQuery } from 'data/read-replicas/replicas-query'
import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query'
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { useFlag } from 'hooks/ui/useFlag'
import { pluckObjectFields } from 'lib/helpers'
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
import {
@@ -32,6 +34,8 @@ import {
CONNECTION_PARAMETERS,
DATABASE_CONNECTION_TYPES,
DatabaseConnectionType,
IPV4_ADDON_TEXT,
PGBOUNCER_ENABLED_BUT_NO_IPV4_ADDON_TEXT,
} from './Connect.constants'
import { CodeBlockFileHeader, ConnectionPanel } from './ConnectionPanel'
import { getConnectionStrings } from './DatabaseSettings.utils'
@@ -54,19 +58,28 @@ export const DatabaseConnectionString = () => {
const { ref: projectRef } = useParams()
const org = useSelectedOrganization()
const state = useDatabaseSelectorStateSnapshot()
const allowPgBouncerSelection = useFlag('dualPoolerSupport')
const [selectedTab, setSelectedTab] = useState<DatabaseConnectionType>('uri')
const {
data: poolingInfo,
error: poolingInfoError,
isLoading: isLoadingPoolingInfo,
isError: isErrorPoolingInfo,
isSuccess: isSuccessPoolingInfo,
} = usePoolingConfigurationQuery({
projectRef,
})
const poolingConfiguration = poolingInfo?.find((x) => x.identifier === state.selectedDatabaseId)
data: pgbouncerConfig,
error: pgbouncerError,
isLoading: isLoadingPgbouncerConfig,
isError: isErrorPgbouncerConfig,
isSuccess: isSuccessPgBouncerConfig,
} = usePgbouncerConfigQuery({ projectRef })
const {
data: supavisorConfig,
error: supavisorConfigError,
isLoading: isLoadingSupavisorConfig,
isError: isErrorSupavisorConfig,
isSuccess: isSuccessSupavisorConfig,
} = useSupavisorConfigurationQuery({ projectRef })
const isPgBouncerEnabled = allowPgBouncerSelection && !!pgbouncerConfig?.pgbouncer_enabled
const poolingConfiguration = isPgBouncerEnabled
? pgbouncerConfig
: supavisorConfig?.find((x) => x.identifier === state.selectedDatabaseId)
const {
data: databases,
@@ -76,10 +89,19 @@ export const DatabaseConnectionString = () => {
isSuccess: isSuccessReadReplicas,
} = useReadReplicasQuery({ projectRef })
const error = poolingInfoError || readReplicasError
const isLoading = isLoadingPoolingInfo || isLoadingReadReplicas
const isError = isErrorPoolingInfo || isErrorReadReplicas
const isSuccess = isSuccessPoolingInfo && isSuccessReadReplicas
const poolerError = isPgBouncerEnabled ? pgbouncerError : supavisorConfigError
const isLoadingPoolerConfig = isPgBouncerEnabled
? isLoadingPgbouncerConfig
: isLoadingSupavisorConfig
const isErrorPoolerConfig = isPgBouncerEnabled ? isErrorPgbouncerConfig : isErrorSupavisorConfig
const isSuccessPoolerConfig = isPgBouncerEnabled
? isSuccessPgBouncerConfig
: isSuccessSupavisorConfig
const error = poolerError || readReplicasError
const isLoading = isLoadingPoolerConfig || isLoadingReadReplicas
const isError = isErrorPoolerConfig || isErrorReadReplicas
const isSuccess = isSuccessPoolerConfig && isSuccessReadReplicas
const selectedDatabase = (databases ?? []).find(
(db) => db.identifier === state.selectedDatabaseId
@@ -109,9 +131,20 @@ export const DatabaseConnectionString = () => {
}
const connectionStrings =
isSuccessPoolingInfo && poolingConfiguration !== undefined
? getConnectionStrings(connectionInfo, poolingConfiguration, {
projectRef,
isSuccessSupavisorConfig && poolingConfiguration !== undefined
? getConnectionStrings({
connectionInfo,
poolingInfo: {
connectionString:
'connection_string' in poolingConfiguration
? poolingConfiguration.connection_string
: poolingConfiguration.connectionString,
db_host: poolingConfiguration.db_host,
db_name: poolingConfiguration.db_name,
db_port: poolingConfiguration.db_port,
db_user: poolingConfiguration.db_user,
},
metadata: { projectRef },
})
: {
direct: {
@@ -173,6 +206,22 @@ export const DatabaseConnectionString = () => {
// [Refactor] See if we can do this in an immutable way, technically not a good practice to do this
let stepNumber = 0
const ipv4AddOnUrl = {
text: 'IPv4 add-on',
url: `/project/${projectRef}/settings/addons?panel=ipv4`,
}
const ipv4SettingsUrl = {
text: 'IPv4 settings',
url: `/project/${projectRef}/settings/addons?panel=ipv4`,
}
const poolerSettingsUrl = {
text: 'Pooler settings',
url: `/project/${projectRef}/settings/database#connection-pooling`,
}
const buttonLinks = !ipv4Addon
? [ipv4AddOnUrl, ...(isPgBouncerEnabled ? [poolerSettingsUrl] : [])]
: [ipv4SettingsUrl, ...(isPgBouncerEnabled ? [poolerSettingsUrl] : [])]
return (
<div className="flex flex-col">
<div
@@ -279,16 +328,13 @@ export const DatabaseConnectionString = () => {
ipv4Status={{
type: !ipv4Addon ? 'error' : 'success',
title: !ipv4Addon ? 'Not IPv4 compatible' : 'IPv4 compatible',
description: ipv4Addon && 'Connections are IPv4 proxied with IPv4 addon.',
link: !ipv4Addon
? {
text: 'IPv4 addon',
url: `/project/${projectRef}/settings/addons?panel=ipv4`,
}
: {
text: 'IPv4 settings',
url: `/project/${projectRef}/settings/addons?panel=ipv4`,
},
description:
isPgBouncerEnabled && !ipv4Addon
? PGBOUNCER_ENABLED_BUT_NO_IPV4_ADDON_TEXT
: !isPgBouncerEnabled
? 'Use Session Pooler if on a IPv4 network or purchase IPv4 add-on'
: IPV4_ADDON_TEXT,
links: buttonLinks,
}}
parameters={[
{ ...CONNECTION_PARAMETERS.host, value: connectionInfo.db_host },
@@ -303,13 +349,20 @@ export const DatabaseConnectionString = () => {
lang={lang}
type="transaction"
title="Transaction pooler"
badge={isPgBouncerEnabled ? 'Dedicated Pooler' : 'Supavisor'}
fileTitle={fileTitle}
description="Ideal for stateless applications like serverless functions where each interaction with Postgres is brief and isolated."
connectionString={connectionStrings['pooler'][selectedTab]}
ipv4Status={{
type: 'success',
title: 'IPv4 compatible',
description: 'Transaction pooler connections are IPv4 proxied for free.',
type: isPgBouncerEnabled && !ipv4Addon ? 'error' : 'success',
title: isPgBouncerEnabled && !ipv4Addon ? 'IPv4 incompatible' : 'IPv4 compatible',
description:
isPgBouncerEnabled && !ipv4Addon
? PGBOUNCER_ENABLED_BUT_NO_IPV4_ADDON_TEXT
: !isPgBouncerEnabled
? 'Transaction pooler connections are IPv4 proxied for free.'
: IPV4_ADDON_TEXT,
links: isPgBouncerEnabled ? buttonLinks : undefined,
}}
notice={['Does not support PREPARE statements']}
parameters={[
@@ -341,13 +394,24 @@ export const DatabaseConnectionString = () => {
lang={lang}
type="session"
title="Session pooler"
badge={isPgBouncerEnabled ? 'Dedicated Pooler' : 'Supavisor'}
fileTitle={fileTitle}
description="Only recommended as an alternative to Direct Connection, when connecting via an IPv4 network."
description={
isPgBouncerEnabled
? 'Recommended if you need to use prepared statements, or other features that are only available in Session mode.'
: 'Only recommended as an alternative to Direct Connection, when connecting via an IPv4 network.'
}
connectionString={connectionStrings['pooler'][selectedTab].replace('6543', '5432')}
ipv4Status={{
type: 'success',
title: 'IPv4 compatible',
description: 'Session pooler connections are IPv4 proxied for free.',
type: isPgBouncerEnabled && !ipv4Addon ? 'error' : 'success',
title: isPgBouncerEnabled && !ipv4Addon ? 'IPv4 incompatible' : 'IPv4 compatible',
description:
isPgBouncerEnabled && !ipv4Addon
? PGBOUNCER_ENABLED_BUT_NO_IPV4_ADDON_TEXT
: !isPgBouncerEnabled
? 'Session pooler connections are IPv4 proxied for free'
: IPV4_ADDON_TEXT,
links: isPgBouncerEnabled ? buttonLinks : undefined,
}}
parameters={[
{ ...CONNECTION_PARAMETERS.host, value: poolingConfiguration?.db_host ?? '' },
@@ -1,5 +1,3 @@
import type { PoolingConfiguration } from 'data/database/pooling-configuration-query'
type ConnectionStrings = {
psql: string
uri: string
@@ -12,19 +10,29 @@ type ConnectionStrings = {
sqlalchemy: string
}
export const getConnectionStrings = (
export const getConnectionStrings = ({
connectionInfo,
poolingInfo,
metadata,
}: {
connectionInfo: {
db_user: string
db_port: number
db_host: string
db_name: string
},
poolingInfo: PoolingConfiguration,
}
poolingInfo: {
connectionString: string
db_user: string
db_port: number
db_host: string
db_name: string
}
metadata: {
projectRef?: string
pgVersion?: string
}
): {
}): {
direct: ConnectionStrings
pooler: ConnectionStrings
} => {
@@ -54,6 +54,7 @@ const FlowingLine = ({
y1="-20"
x2="2"
y2={y2 - y1 + 26}
// [Joshen] JFYI this is causing some looped console errors whenever the Connect UI is opens
animate={{
y1: [-20, y2 - y1 + 26],
y2: [0, y2 - y1 + 46],
@@ -1,13 +1,13 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import dayjs from 'dayjs'
import { Download } from 'lucide-react'
import { useParams } from 'common'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { useBackupDownloadMutation } from 'data/database/backup-download-mutation'
import type { DatabaseBackup } from 'data/database/backups-query'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { Badge } from 'ui'
import { useParams } from 'common'
import { TimestampInfo } from 'ui-patterns'
interface BackupItemProps {
index: number
@@ -36,8 +36,9 @@ const BackupItem = ({ index, isHealthy, backup, onSelectBackup }: BackupItemProp
},
})
const generateSideButtons = (backup: any) => {
if (backup.status === 'COMPLETED')
const generateSideButtons = (backup: DatabaseBackup) => {
// [Joshen] API typing is incorrect here, status is getting typed as Record<string, never>
if ((backup as any).status === 'COMPLETED')
return (
<div className="flex space-x-4">
<ButtonTooltip
@@ -84,20 +85,18 @@ const BackupItem = ({ index, isHealthy, backup, onSelectBackup }: BackupItemProp
return <Badge variant="warning">Backup In Progress...</Badge>
}
const generateBackupName = (backup: any) => {
if (backup.status == 'COMPLETED') {
return `${dayjs(backup.inserted_at).format('DD MMM YYYY HH:mm:ss')} UTC`
}
return dayjs(backup.inserted_at).format('DD MMM YYYY')
}
return (
<div
className={`flex h-12 items-center justify-between px-6 ${
index ? 'border-t border-default' : ''
}`}
>
<p className="text-sm text-foreground ">{generateBackupName(backup)}</p>
<TimestampInfo
displayAs="utc"
utcTimestamp={backup.inserted_at}
labelFormat="DD MMM YYYY HH:mm:ss (ZZ)"
className="text-left !text-sm font-mono tracking-tight"
/>
<div>{generateSideButtons(backup)}</div>
</div>
)
@@ -5,10 +5,12 @@ import { UseFormReturn } from 'react-hook-form'
import { components } from 'api-types'
import { useParams } from 'common'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { DocsButton } from 'components/ui/DocsButton'
import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query'
import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { getCloudProviderArchitecture } from 'lib/cloudprovider-utils'
import { InstanceSpecs } from 'lib/constants'
import { cn, FormField_Shadcn_, RadioGroupCard, RadioGroupCardItem, Skeleton } from 'ui'
import { ComputeBadge } from 'ui-patterns'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
@@ -22,8 +24,6 @@ import {
import { BillingChangeBadge } from '../ui/BillingChangeBadge'
import FormMessage from '../ui/FormMessage'
import { NoticeBar } from '../ui/NoticeBar'
import { InstanceSpecs } from 'lib/constants'
import { DocsButton } from 'components/ui/DocsButton'
/**
* to do: this could be a type from api-types
@@ -196,7 +196,6 @@ export function ComputeSizeField({ form, disabled }: ComputeSizeFieldProps) {
lockedOption && 'opacity-50'
)}
disabled={disabled || lockedOption}
// @ts-expect-error
label={
<>
{showUpgradeBadge && compute.identifier === 'ci_micro' && (
@@ -57,11 +57,6 @@ export const DISK_LIMITS = {
},
}
export const DISK_TYPE_LABELS = {
[DiskType.GP3]: 'General Purpose SSD (gp3)',
[DiskType.IO2]: 'Provisioned IOPS SSD (io2)',
}
interface PlanDetails {
includedDiskGB: { gp3: number; io2: number }
}
@@ -342,10 +342,31 @@ let { data: ${resourceId}, error } = await supabase
bash: {
language: 'bash',
code: `
curl '${endpoint}/rest/v1/${resourceId}?id=eq.1&select=*' \\
curl --get '${endpoint}/rest/v1/${resourceId}' \\
-H "apikey: ${apiKey}" \\
-H "Authorization: Bearer ${apiKey}" \\
-H "Range: 0-9"
-H "Range: 0-9" \\
-d "select=*" \\
\\
\`# Filters\` \\
-d "column=eq.Equal+to" \\
-d "column=gt.Greater+than" \\
-d "column=lt.Less+than" \\
-d "column=gte.Greater+than+or+equal+to" \\
-d "column=lte.Less+than+or+equal+to" \\
-d "column=like.*CaseSensitive*" \\
-d "column=ilike.*CaseInsensitive*" \\
-d "column=is.null" \\
-d "column=in.(Array,Values)" \\
-d "column=neq.Not+equal+to" \\
\\
\`# Arrays\` \\
-d "array_column=cs.{array,contains}" \\
-d "array_column=cd.{contained,by}" \\
\\
\`# Logical operators\` \\
-d "column=not.like.Negate+filter" \\
-d "or=(some_column.eq.Some+value,other_column.eq.Other+value)"
`,
},
js: {
@@ -354,6 +375,7 @@ curl '${endpoint}/rest/v1/${resourceId}?id=eq.1&select=*' \\
let { data: ${resourceId}, error } = await supabase
.from('${resourceId}')
.select("*")
// Filters
.eq('column', 'Equal to')
.gt('column', 'Greater than')
@@ -365,9 +387,14 @@ let { data: ${resourceId}, error } = await supabase
.is('column', null)
.in('column', ['Array', 'Values'])
.neq('column', 'Not equal to')
// Arrays
.contains('array_column', ['array', 'contains'])
.containedBy('array_column', ['contained', 'by'])
// Logical operators
.not('column', 'like', 'Negate filter')
.or('some_column.eq.Some value, other_column.eq.Other value')
`,
},
}),
@@ -66,3 +66,5 @@ final data = res.data;`,
)`,
},
]
export const HTTP_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'] as const
@@ -148,7 +148,7 @@ export const EdgeFunctionDetails = () => {
<FormControl_Shadcn_>
<Input_Shadcn_
{...field}
className="w-full"
className="w-64"
disabled={!canUpdateEdgeFunction}
/>
</FormControl_Shadcn_>
@@ -0,0 +1,11 @@
export type ResponseData = {
status: number
headers: Record<string, string>
body: string
}
export type ErrorWithStatus = Error & {
cause?: {
status: number
}
}
@@ -0,0 +1,443 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { Loader2, Plus, Send, X } from 'lucide-react'
import { useState } from 'react'
import { useFieldArray, useForm } from 'react-hook-form'
import * as z from 'zod'
import { useParams } from 'common'
import { RoleImpersonationPopover } from 'components/interfaces/RoleImpersonationSelector'
import { useSessionAccessTokenQuery } from 'data/auth/session-access-token-query'
import { useProjectPostgrestConfigQuery } from 'data/config/project-postgrest-config-query'
import { getAPIKeys, useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
import { constructHeaders } from 'data/fetchers'
import { BASE_PATH, IS_PLATFORM } from 'lib/constants'
import { prettifyJSON } from 'lib/helpers'
import { getRoleImpersonationJWT } from 'lib/role-impersonation'
import { useGetImpersonatedRole } from 'state/role-impersonation-state'
import {
Badge,
Button,
CodeBlock,
Form_Shadcn_,
FormControl_Shadcn_,
FormField_Shadcn_,
Input_Shadcn_ as Input,
Label_Shadcn_ as Label,
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
Select_Shadcn_ as Select,
SelectContent_Shadcn_ as SelectContent,
SelectItem_Shadcn_ as SelectItem,
SelectTrigger_Shadcn_ as SelectTrigger,
SelectValue_Shadcn_ as SelectValue,
Sheet,
SheetContent,
SheetFooter,
SheetHeader,
SheetTitle,
Tabs_Shadcn_ as Tabs,
TabsContent_Shadcn_ as TabsContent,
TabsList_Shadcn_ as TabsList,
TabsTrigger_Shadcn_ as TabsTrigger,
TextArea_Shadcn_ as Textarea,
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { HTTP_METHODS } from './EdgeFunctionDetails.constants'
import { ErrorWithStatus, ResponseData } from './EdgeFunctionDetails.types'
interface EdgeFunctionTesterSheetProps {
visible: boolean
onClose: () => void
}
const FormSchema = z.object({
method: z.enum(HTTP_METHODS),
body: z
.string()
.optional()
.transform((str) => str || '{}'),
headers: z.array(
z.object({
key: z.string(),
value: z.string(),
})
),
queryParams: z.array(
z.object({
key: z.string(),
value: z.string(),
})
),
})
type FormValues = z.infer<typeof FormSchema>
export const EdgeFunctionTesterSheet = ({ visible, onClose }: EdgeFunctionTesterSheetProps) => {
const { ref: projectRef, functionSlug } = useParams()
const [response, setResponse] = useState<ResponseData | null>(null)
const [error, setError] = useState<string | null>(null)
const [isLoading, setIsLoading] = useState(false)
const { data: config } = useProjectPostgrestConfigQuery({ projectRef })
const { data: settings } = useProjectSettingsV2Query({ projectRef })
const { data: accessToken } = useSessionAccessTokenQuery({ enabled: IS_PLATFORM })
const getImpersonatedRole = useGetImpersonatedRole()
const { serviceKey } = getAPIKeys(settings)
const protocol = settings?.app_config?.protocol ?? 'https'
const endpoint = settings?.app_config?.endpoint ?? ''
const url = `${protocol}://${endpoint}/functions/v1/${functionSlug}`
const form = useForm<FormValues>({
resolver: zodResolver(FormSchema),
defaultValues: {
method: 'POST',
body: '{ "name": "Functions" }',
headers: [{ key: '', value: '' }],
queryParams: [{ key: '', value: '' }],
},
})
const { method } = form.watch()
const {
fields: headerFields,
append: appendHeader,
remove: removeHeader,
} = useFieldArray({
control: form.control,
name: 'headers',
})
const {
fields: queryParamFields,
append: appendQueryParam,
remove: removeQueryParam,
} = useFieldArray({
control: form.control,
name: 'queryParams',
})
const addKeyValuePair = (type: 'headers' | 'queryParams') => {
if (type === 'headers') {
appendHeader({ key: '', value: '' })
} else {
appendQueryParam({ key: '', value: '' })
}
}
const removeKeyValuePair = (index: number, type: 'headers' | 'queryParams') => {
if (type === 'headers') {
removeHeader(index)
} else {
removeQueryParam(index)
}
}
const onSubmit = async (values: FormValues) => {
try {
setIsLoading(true)
setError(null)
setResponse(null)
// Validate that the body is valid JSON
try {
JSON.parse(values.body)
} catch (e) {
form.setError('body', { message: 'Must be a valid JSON string' })
return
}
let testAuthorization: string | undefined
const role = getImpersonatedRole()
if (
projectRef !== undefined &&
config?.jwt_secret !== undefined &&
role !== undefined &&
role.type === 'postgrest'
) {
try {
const token = await getRoleImpersonationJWT(projectRef, config.jwt_secret, role)
testAuthorization = 'Bearer ' + token
} catch (err: any) {
console.error('Failed to generate JWT:', {
error: err.message,
roleDetails: role,
})
}
}
// Construct custom headers
const customHeaders: Record<string, string> = {}
headerFields.forEach(({ key, value }) => {
if (key && value) {
customHeaders[key] = value
}
})
// Construct query parameters
const queryString = queryParamFields
.filter(({ key, value }) => key && value)
.map(({ key, value }) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join('&')
const finalUrl = queryString ? `${url}?${queryString}` : url
const defaultHeaders = await constructHeaders()
const res = await fetch(`${BASE_PATH}/api/edge-functions/test`, {
method: 'POST',
headers: {
...defaultHeaders,
'Content-Type': 'application/json',
},
body: JSON.stringify({
url: finalUrl,
method: values.method,
body: values.body,
headers: {
...(accessToken && {
Authorization: `Bearer ${accessToken}`,
}),
'x-test-authorization': testAuthorization ?? `Bearer ${serviceKey?.api_key}`,
'Content-Type': 'application/json',
...customHeaders,
},
}),
})
const data = await res.json()
if (!res.ok) {
throw new Error(data.error?.message || 'Failed to test edge function', {
cause: { status: data.status },
})
}
setResponse(data)
} catch (err) {
setError(err instanceof Error ? err.message : 'An unknown error occurred')
if (err instanceof Error) {
const errorWithStatus = err as ErrorWithStatus
setResponse({
status: errorWithStatus.cause?.status || 500,
headers: {},
body: '',
})
}
} finally {
setIsLoading(false)
}
}
const renderKeyValuePairs = (type: 'headers' | 'queryParams', label: string) => (
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-foreground text-sm">{label}</Label>
<Button
type="default"
size="tiny"
icon={<Plus size={14} />}
onClick={() => addKeyValuePair(type)}
>
Add {label}
</Button>
</div>
<div className="border rounded-md bg-surface-200">
{(type === 'headers' ? headerFields : queryParamFields).map((field, index) => (
<div key={field.id} className="grid grid-cols-[1fr,1fr,32px] border-b last:border-b-0">
<FormField_Shadcn_
control={form.control}
name={`${type}.${index}.key`}
render={({ field }) => (
<FormControl_Shadcn_>
<Input
{...field}
size="tiny"
placeholder="Enter key..."
disabled={isLoading}
className="h-auto py-2 font-mono rounded-none shadow-none bg-transparent border-l-0 border-r-1 border-t-0 border-b-0 border-border"
/>
</FormControl_Shadcn_>
)}
/>
<FormField_Shadcn_
control={form.control}
name={`${type}.${index}.value`}
render={({ field }) => (
<FormControl_Shadcn_>
<Input
{...field}
size="tiny"
placeholder="Enter value..."
disabled={isLoading}
className="h-auto py-2 font-mono rounded-none shadow-none bg-transparent border-none"
/>
</FormControl_Shadcn_>
)}
/>
<div className="flex items-center justify-center">
{(type === 'headers' ? headerFields : queryParamFields).length > 1 && (
<Button
type="text"
size="tiny"
icon={<X strokeWidth={1.5} size={14} />}
className="w-6 h-6"
onClick={() => removeKeyValuePair(index, type)}
/>
)}
</div>
</div>
))}
</div>
</div>
)
return (
<Sheet open={visible} onOpenChange={onClose}>
<SheetContent size="default" className="flex flex-col gap-0 p-0">
<SheetHeader>
<SheetTitle>Test {functionSlug}</SheetTitle>
</SheetHeader>
<Form_Shadcn_ {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex-1 overflow-y-auto flex flex-col"
>
<ResizablePanelGroup direction="vertical">
<ResizablePanel>
<div className="flex flex-col gap-y-4 p-5 h-full overflow-y-auto">
<FormField_Shadcn_
control={form.control}
name="method"
render={({ field }) => (
<FormItemLayout layout="vertical" label="HTTP Method">
<FormControl_Shadcn_>
<Select
value={field.value}
onValueChange={field.onChange}
disabled={isLoading}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select method" />
</SelectTrigger>
<SelectContent>
{HTTP_METHODS.map((m) => (
<SelectItem key={m} value={m}>
{m}
</SelectItem>
))}
</SelectContent>
</Select>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
{method !== 'GET' && (
<FormField_Shadcn_
control={form.control}
name="body"
render={({ field }) => (
<FormItemLayout layout="vertical" label="Request Body">
<FormControl_Shadcn_>
<Textarea
{...field}
placeholder="Request body (JSON)"
rows={3}
disabled={isLoading}
className="font-mono text-xs"
/>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
)}
{renderKeyValuePairs('headers', 'Headers')}
{renderKeyValuePairs('queryParams', 'Query Parameters')}
</div>
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel defaultSize={41} minSize={41} maxSize={83}>
<div className="h-full bg-surface-100 border-t flex-1 flex flex-col overflow-hidden">
{response ? (
<div className="h-full bg-surface-100 flex flex-col overflow-hidden">
{error ? (
<>
<div className="flex gap-2 items-center p-5 text-sm pb-3">
Function responded with
<Badge variant={response.status >= 400 ? 'destructive' : 'success'}>
{response.status}
</Badge>
</div>
<p className="px-5 text-sm text-foreground-light">{error}</p>
</>
) : (
<Tabs
defaultValue="body"
className="h-full flex-1 flex flex-col overflow-hidden"
>
<TabsList className="gap-4 px-5 pt-2">
<div className="flex items-center gap-4 flex-1">
<TabsTrigger className="text-sm" value="body">
Body
</TabsTrigger>
<TabsTrigger className="text-sm" value="headers">
Headers
</TabsTrigger>
</div>
<Badge
variant={response.status >= 400 ? 'destructive' : 'success'}
className="-translate-y-1"
>
{response.status}
</Badge>
</TabsList>
<TabsContent value="body" className="mt-0 flex-1 overflow-auto p-0">
<CodeBlock
language="json"
hideLineNumbers
className="rounded-md !border-none !px-4 !py-3 h-full"
value={prettifyJSON(response.body)}
/>
</TabsContent>
<TabsContent value="headers" className="mt-0 flex-1 overflow-auto p-0">
<CodeBlock
language="json"
hideLineNumbers
className="rounded-md !border-none !px-4 !py-3 h-full"
value={prettifyJSON(JSON.stringify(response.headers, null, 2))}
/>
</TabsContent>
</Tabs>
)}
</div>
) : isLoading ? (
<div className="h-full flex flex-col items-center justify-center gap-2">
<Loader2 size={24} className="text-foreground-muted animate-spin" />
<p className="text-sm text-foreground-light">Sending request...</p>
</div>
) : (
<div className="h-full flex flex-col items-center justify-center gap-2">
<Send size={24} className="text-foreground-muted" />
<p className="text-sm text-foreground-light">Send your first test request</p>
</div>
)}
</div>
</ResizablePanel>
</ResizablePanelGroup>
<SheetFooter className="px-5 py-3 border-t">
<div className="flex items-center gap-2">
<RoleImpersonationPopover />
<Button type="primary" htmlType="submit" loading={isLoading} disabled={isLoading}>
Send Request
</Button>
</div>
</SheetFooter>
</form>
</Form_Shadcn_>
</SheetContent>
</Sheet>
)
}
@@ -722,10 +722,31 @@ let { data: ${resourceId}, error } = await supabase
key: 'with-filtering',
title: 'With filtering',
bash: `
curl '${endpoint}/rest/v1/${resourceId}?id=eq.1&select=*' \\
curl --get '${endpoint}/rest/v1/${resourceId}' \\
-H "apikey: ${apikey}" \\
-H "Authorization: Bearer ${apikey}" \\
-H "Range: 0-9"
-H "Range: 0-9" \\
-d "select=*" \\
\\
\`# Filters\` \\
-d "column=eq.Equal+to" \\
-d "column=gt.Greater+than" \\
-d "column=lt.Less+than" \\
-d "column=gte.Greater+than+or+equal+to" \\
-d "column=lte.Less+than+or+equal+to" \\
-d "column=like.*CaseSensitive*" \\
-d "column=ilike.*CaseInsensitive*" \\
-d "column=is.null" \\
-d "column=in.(Array,Values)" \\
-d "column=neq.Not+equal+to" \\
\\
\`# Arrays\` \\
-d "array_column=cs.{array,contains}" \\
-d "array_column=cd.{contained,by}" \\
\\
\`# Logical operators\` \\
-d "column=not.like.Negate+filter" \\
-d "or=(some_column.eq.Some+value,other_column.eq.Other+value)"
`,
js: `
let { data: ${resourceId}, error } = await supabase
@@ -747,6 +768,10 @@ let { data: ${resourceId}, error } = await supabase
// Arrays
.contains('array_column', ['array', 'contains'])
.containedBy('array_column', ['contained', 'by'])
// Logical operators
.not('column', 'like', 'Negate filter')
.or('some_column.eq.Some value, other_column.eq.Other value')
`,
},
]
@@ -4,6 +4,8 @@ import { useEffect, useState } from 'react'
import { toast } from 'sonner'
import { useParams } from 'common'
import { InlineLink } from 'components/ui/InlineLink'
import { usePgbouncerConfigQuery } from 'data/database/pgbouncer-config-query'
import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query'
import { useProjectAddonRemoveMutation } from 'data/subscriptions/project-addon-remove-mutation'
import { useProjectAddonUpdateMutation } from 'data/subscriptions/project-addon-update-mutation'
@@ -11,15 +13,17 @@ import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query'
import type { AddonVariantId } from 'data/subscriptions/types'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { useFlag } from 'hooks/ui/useFlag'
import { formatCurrency } from 'lib/helpers'
import { ExternalLink } from 'lucide-react'
import { useAddonsPagePanel } from 'state/addons-page'
import { Button, Radio, SidePanel, cn } from 'ui'
import { ExternalLink } from 'lucide-react'
import { Admonition } from 'ui-patterns'
const IPv4SidePanel = () => {
const { ref: projectRef } = useParams()
const organization = useSelectedOrganization()
const allowPgBouncerSelection = useFlag('dualPoolerSupport')
const [selectedOption, setSelectedOption] = useState<string>('ipv4_none')
@@ -29,6 +33,7 @@ const IPv4SidePanel = () => {
const visible = panel === 'ipv4'
const { data: addons, isLoading } = useProjectAddonsQuery({ projectRef })
const { data: pgbouncerConfig } = usePgbouncerConfigQuery({ projectRef })
const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: organization?.slug })
const { mutate: updateAddon, isLoading: isUpdating } = useProjectAddonUpdateMutation({
onSuccess: () => {
@@ -59,6 +64,7 @@ const IPv4SidePanel = () => {
const isFreePlan = subscription?.plan?.id === 'free'
const hasChanges = selectedOption !== (subscriptionIpV4Option?.variant.identifier ?? 'ipv4_none')
const selectedIPv4 = availableOptions.find((option) => option.identifier === selectedOption)
const isPgBouncerEnabled = allowPgBouncerSelection && !!pgbouncerConfig?.pgbouncer_enabled
useEffect(() => {
if (visible) {
@@ -117,14 +123,31 @@ const IPv4SidePanel = () => {
database via a IPv4 address.
</p>
<p className="text-sm">
If you are connecting via our connection pooler, you do not need this add-on as our
pooler resolves to IPv4 addresses. You can check your connection info in your{' '}
<Link href={`/project/${projectRef}/settings/database`} className="text-brand">
project database settings
</Link>
.
</p>
{isPgBouncerEnabled ? (
<Admonition
type="default"
title="The Dedicated Pooler does not support IPv4 addresses"
description="If you are connecting to your database via the Dedicated Pooler, you may need this add-on if your network does not support communicating via IPv6. Alternatively, you may consider switching to Supavisor which support IPv4 addresses."
>
<Button asChild type="default" className="mt-2">
<Link
href={`/project/${projectRef}/settings/database#connection-pooler`}
className="!no-underline"
>
Connection Pooler settings
</Link>
</Button>
</Admonition>
) : (
<p className="text-sm">
If you are connecting via the Supavisor connection pooler, you do not need this add-on
as our pooler resolves to IPv4 addresses. You can check your connection info in your{' '}
<InlineLink href={`/project/${projectRef}/settings/database#connection-pooler`}>
project database settings
</InlineLink>
.
</p>
)}
<div className={cn('!mt-8 pb-4', isFreePlan && 'opacity-75')}>
<Radio.Group
@@ -1,5 +1,7 @@
// https://supabase.com/docs/guides/platform/performance#optimizing-the-number-of-connections
// https://github.com/supabase/infrastructure/blob/develop/worker/src/lib/constants.ts#L544-L596
// https://github.com/supabase/supabase-admin-api/blob/master/optimizations/pgbouncer.go
// [Joshen] This matches for both Supavisor and PgBouncer
export const POOLING_OPTIMIZATIONS = {
ci_nano: {
@@ -3,7 +3,7 @@ import { AlertTriangleIcon } from 'lucide-react'
import { useParams } from 'common'
import { Markdown } from 'components/interfaces/Markdown'
import { DocsButton } from 'components/ui/DocsButton'
import { usePoolingConfigurationQuery } from 'data/database/pooling-configuration-query'
import { useSupavisorConfigurationQuery } from 'data/database/supavisor-configuration-query'
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
import { useDatabaseSettingsStateSnapshot } from 'state/database-settings'
import {
@@ -27,7 +27,7 @@ export const PoolingModesModal = () => {
const snap = useDatabaseSettingsStateSnapshot()
const state = useDatabaseSelectorStateSnapshot()
const { data } = usePoolingConfigurationQuery({ projectRef: projectRef })
const { data } = useSupavisorConfigurationQuery({ projectRef: projectRef })
const primaryConfig = data?.find((x) => x.identifier === state.selectedDatabaseId)
const navigateToPoolerSettings = () => {
@@ -90,8 +90,8 @@ export const INFRA_ACTIVITY_METRICS: CategoryMeta[] = [
url: 'https://supabase.com/docs/guides/platform/compute-add-ons#disk-throughput-and-iops',
},
{
name: 'Interpreting Disk IO budget',
url: 'https://supabase.com/docs/guides/platform/compute-add-ons#bursting-and-disk-io-budget',
name: 'High Disk I/O',
url: 'https://supabase.com/docs/guides/troubleshooting/exhaust-disk-io',
},
{
name: 'Metrics',
@@ -99,7 +99,7 @@ export const INFRA_ACTIVITY_METRICS: CategoryMeta[] = [
},
],
description:
'The disk performance of your workload is determined by the Disk IO bandwidth.\nSmaller compute instances (below 4XL) can burst up to their largest throughput and IOPS for 30 minutes in a day. Beyond that, the performance reverts to the baseline. Your disk IO budget gets replenished throughout the day.',
'The disk performance of your workload is determined by the Disk IO bandwidth.\nSmaller compute instances (below 4XL) can burst above their baseline disk throughput and IOPS for short periods of time. Beyond that, the performance reverts to the baseline.',
chartDescription: '',
},
],
@@ -288,11 +288,9 @@ const InfrastructureActivity = () => {
</p>
) : (
<p className="text-sm text-foreground-light">
Your current compute can burst up to{' '}
{currentComputeInstanceSpecs.max_disk_io_mbs?.toLocaleString()} Mbps for
30 minutes a day and reverts to the baseline performance of{' '}
Your current compute can burst above the baseline disk throughput of{' '}
{currentComputeInstanceSpecs.baseline_disk_io_mbs?.toLocaleString()}{' '}
Mbps.
Mbps for short periods of time.
</p>
)}
</div>
@@ -306,6 +304,13 @@ const InfrastructureActivity = () => {
'Micro'}
</p>
</div>
<div className="flex items-center justify-between border-b py-1">
<p className="text-xs text-foreground-light">Baseline IO Bandwidth</p>
<p className="text-xs">
{currentComputeInstanceSpecs.baseline_disk_io_mbs?.toLocaleString()}{' '}
Mbps
</p>
</div>
<div className="flex items-center justify-between border-b py-1">
<p className="text-xs text-foreground-light">
Maximum IO Bandwidth (burst limit)
@@ -314,13 +319,6 @@ const InfrastructureActivity = () => {
{currentComputeInstanceSpecs.max_disk_io_mbs?.toLocaleString()} Mbps
</p>
</div>
<div className="flex items-center justify-between border-b py-1">
<p className="text-xs text-foreground-light">Baseline IO Bandwidth</p>
<p className="text-xs">
{currentComputeInstanceSpecs.baseline_disk_io_mbs?.toLocaleString()}{' '}
Mbps
</p>
</div>
{currentComputeInstanceSpecs.max_disk_io_mbs !==
currentComputeInstanceSpecs?.baseline_disk_io_mbs && (
<div className="flex items-center justify-between py-1">
@@ -1,17 +1,22 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useEffect, type PropsWithChildren } from 'react'
import { Send } from 'lucide-react'
import { useRouter } from 'next/router'
import { useEffect, useState, type PropsWithChildren } from 'react'
import { toast } from 'sonner'
import { useParams } from 'common'
import { useIsAPIDocsSidePanelEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { EdgeFunctionTesterSheet } from 'components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionTesterSheet'
import { PageLayout } from 'components/layouts/PageLayout/PageLayout'
import APIDocsButton from 'components/ui/APIDocsButton'
import { DocsButton } from 'components/ui/DocsButton'
import NoPermission from 'components/ui/NoPermission'
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
import { useEdgeFunctionQuery } from 'data/edge-functions/edge-function-query'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { withAuth } from 'hooks/misc/withAuth'
import { useRouter } from 'next/router'
import { toast } from 'sonner'
import { useFlag } from 'hooks/ui/useFlag'
import { Button } from 'ui'
import ProjectLayout from '../ProjectLayout/ProjectLayout'
import EdgeFunctionsLayout from './EdgeFunctionsLayout'
@@ -25,22 +30,28 @@ const EdgeFunctionDetailsLayout = ({
}: PropsWithChildren<EdgeFunctionDetailsLayoutProps>) => {
const router = useRouter()
const { functionSlug, ref } = useParams()
const edgeFunctionCreate = useFlag('edgeFunctionCreate')
const isNewAPIDocsEnabled = useIsAPIDocsSidePanelEnabled()
const canReadFunctions = useCheckPermissions(PermissionAction.FUNCTIONS_READ, '*')
const [isOpen, setIsOpen] = useState(false)
const {
data: selectedFunction,
error,
isError,
} = useEdgeFunctionQuery({ projectRef: ref, slug: functionSlug })
const { data: settings } = useProjectSettingsV2Query({ projectRef: ref })
const name = selectedFunction?.name || ''
const breadcrumbItems = [
{
label: 'Edge Functions',
href: `/project/${ref}/functions`,
},
]
const navigationItems = functionSlug
? [
{
@@ -101,10 +112,16 @@ const EdgeFunctionDetailsLayout = ({
/>
)}
<DocsButton href="https://supabase.com/docs/guides/functions" />
{edgeFunctionCreate && !!functionSlug && (
<Button type="default" icon={<Send />} onClick={() => setIsOpen(true)}>
Test
</Button>
)}
</div>
}
>
{children}
<EdgeFunctionTesterSheet visible={isOpen} onClose={() => setIsOpen(false)} />
</PageLayout>
</EdgeFunctionsLayout>
)
@@ -1,8 +1,8 @@
import { useParams } from 'common'
import { ChevronLeft } from 'lucide-react'
import Link from 'next/link'
import { Fragment, ReactNode } from 'react'
import Link from 'next/link'
import { useParams } from 'common'
import { cn } from 'ui'
import {
Breadcrumb,
@@ -27,7 +27,6 @@ interface PageHeaderProps {
secondaryActions?: ReactNode
className?: string
isCompact?: boolean
pageMeta?: ReactNode
}
export const PageHeader = ({
@@ -39,7 +38,6 @@ export const PageHeader = ({
secondaryActions,
className,
isCompact = false,
pageMeta,
}: PageHeaderProps) => {
const { ref } = useParams()
@@ -185,7 +185,7 @@ const ProjectLayout = forwardRef<HTMLDivElement, PropsWithChildren<ProjectLayout
<ResizableHandle
withHandle
disabled={resizableSidebar ? false : true}
className="hidden md:block"
className="hidden md:flex"
/>
)}
<ResizablePanel order={2} id="panel-right" className="h-full flex flex-col w-full">
@@ -1,19 +1,5 @@
import * as z from 'zod'
// This validator validates a string to be a positive integer or if it's an empty string, transforms it to a null
export const StringToPositiveNumber = z.union([
// parse the value if it's a number
z.number().positive().int(),
// parse the value if it's a non-empty string
z.string().min(1).pipe(z.coerce.number().positive().int()),
// transform a non-empty string into a null value
z
.string()
.max(0, 'The field accepts only a number')
.transform((v) => null),
z.null(),
])
export const StringNumberOrNull = z
.string()
.transform((v) => (v === '' ? null : v))
@@ -22,3 +8,10 @@ export const StringNumberOrNull = z
message: 'Invalid number',
})
.transform((value) => (value === null ? null : Number(value)))
/**
* [Joshen] After wrangling with RHF I think this is the easiest way to handle nullable number fields
* - Declare the field normally as you would in the zod form schema (e.g field: z.number().nullable())
* - In the InputField, add a form.register call `{...form.register('field_name', { setValueAs: setValueAsNullableNumber })}`
*/
export const setValueAsNullableNumber = (v: any) => (v === '' || v === null ? null : parseInt(v))
+10 -5
View File
@@ -36,11 +36,7 @@ function Panel(props: PropsWithChildren<PanelProps>) {
</div>
)}
{props.children}
{props.footer && (
<div className="bg-surface-100 border-t border-default">
<div className="flex h-12 items-center px-4 md:px-6">{props.footer}</div>
</div>
)}
{props.footer && <Footer>{props.footer}</Footer>}
</div>
)
@@ -55,6 +51,14 @@ function Content({ children, className }: { children: ReactNode; className?: str
return <div className={cn('px-4 md:px-6 py-4', className)}>{children}</div>
}
function Footer({ children }: { children: ReactNode; className?: string }) {
return (
<div className="bg-surface-100 border-t border-default">
<div className="flex h-12 items-center px-4 md:px-6">{children}</div>
</div>
)
}
const PanelNotice = forwardRef<
HTMLDivElement,
{
@@ -139,5 +143,6 @@ const PanelNotice = forwardRef<
PanelNotice.displayName = 'PanelNotice'
Panel.Content = Content
Panel.Footer = Footer
Panel.Notice = PanelNotice
export default Panel
@@ -42,7 +42,7 @@ export const RESOURCE_WARNING_MESSAGES: Record<string, ResourceWarningMessage> =
},
},
docsUrl: 'https://supabase.com/docs/guides/platform/database-size#disabling-read-only-mode',
buttonText: 'View Compute and Disk',
buttonText: 'Learn more',
metric: 'read_only',
},
disk_io_exhaustion: {
@@ -70,7 +70,7 @@ export const RESOURCE_WARNING_MESSAGES: Record<string, ResourceWarningMessage> =
},
},
docsUrl: 'https://supabase.com/docs/guides/troubleshooting/exhaust-disk-io',
buttonText: 'Check usage',
buttonText: 'Learn more',
metric: 'disk_io',
},
disk_space_exhaustion: {
@@ -125,7 +125,7 @@ export const RESOURCE_WARNING_MESSAGES: Record<string, ResourceWarningMessage> =
},
},
docsUrl: 'https://supabase.com/docs/guides/troubleshooting/high-cpu-usage',
buttonText: 'Check usage',
buttonText: 'Learn more',
metric: 'cpu',
},
memory_and_swap_exhaustion: {
@@ -153,7 +153,7 @@ export const RESOURCE_WARNING_MESSAGES: Record<string, ResourceWarningMessage> =
},
},
docsUrl: 'https://supabase.com/docs/guides/troubleshooting/exhaust-ram',
buttonText: 'Check usage',
buttonText: 'Learn more',
metric: 'ram',
},
auth_rate_limit_exhaustion: {
+1 -1
View File
@@ -2,9 +2,9 @@ import { useQuery, UseQueryOptions } from '@tanstack/react-query'
import type { components } from 'data/api'
import { get, handleError } from 'data/fetchers'
import { useIsOrioleDb } from 'hooks/misc/useSelectedProject'
import type { ResponseError } from 'types'
import { databaseKeys } from './keys'
import { useIsOrioleDb } from 'hooks/misc/useSelectedProject'
export type BackupsVariables = {
projectRef?: string
@@ -4,12 +4,12 @@ import { get, handleError } from 'data/fetchers'
import { ResponseError } from 'types'
import { databaseKeys } from './keys'
export type ProjectPgbouncerConfigVariables = {
export type PgbouncerConfigVariables = {
projectRef?: string
}
export async function getProjectPgbouncerConfig(
{ projectRef }: ProjectPgbouncerConfigVariables,
export async function getPgbouncerConfig(
{ projectRef }: PgbouncerConfigVariables,
signal?: AbortSignal
) {
if (!projectRef) throw new Error('projectRef is required')
@@ -22,19 +22,19 @@ export async function getProjectPgbouncerConfig(
return data
}
export type ProjectPgbouncerConfigData = Awaited<ReturnType<typeof getProjectPgbouncerConfig>>
export type ProjectPgbouncerConfigError = ResponseError
export type PgbouncerConfigData = Awaited<ReturnType<typeof getPgbouncerConfig>>
export type PgbouncerConfigError = ResponseError
export const useProjectPgbouncerConfigQuery = <TData = ProjectPgbouncerConfigData>(
{ projectRef }: ProjectPgbouncerConfigVariables,
export const usePgbouncerConfigQuery = <TData = PgbouncerConfigData>(
{ projectRef }: PgbouncerConfigVariables,
{
enabled = true,
...options
}: UseQueryOptions<ProjectPgbouncerConfigData, ProjectPgbouncerConfigError, TData> = {}
}: UseQueryOptions<PgbouncerConfigData, PgbouncerConfigError, TData> = {}
) =>
useQuery<ProjectPgbouncerConfigData, ProjectPgbouncerConfigError, TData>(
useQuery<PgbouncerConfigData, PgbouncerConfigError, TData>(
databaseKeys.pgbouncerConfig(projectRef),
({ signal }) => getProjectPgbouncerConfig({ projectRef }, signal),
({ signal }) => getPgbouncerConfig({ projectRef }, signal),
{
enabled: enabled && typeof projectRef !== 'undefined',
...options,
@@ -0,0 +1,73 @@
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
import type { components } from 'data/api'
import { handleError, patch } from 'data/fetchers'
import type { ResponseError } from 'types'
import { databaseKeys } from './keys'
export type PgbouncerConfigurationUpdateVariables = {
ref: string
} & components['schemas']['UpdatePgbouncerConfigBody']
export async function updatePgbouncerConfiguration({
ref,
pool_mode,
default_pool_size,
pgbouncer_enabled,
ignore_startup_parameters,
max_client_conn,
}: PgbouncerConfigurationUpdateVariables) {
if (!ref) return console.error('Project ref is required')
const { data, error } = await patch('/platform/projects/{ref}/config/pgbouncer', {
params: { path: { ref } },
body: {
default_pool_size,
pool_mode,
pgbouncer_enabled,
ignore_startup_parameters,
max_client_conn,
},
})
if (error) handleError(error)
return data
}
type PgbouncerConfigurationUpdateData = Awaited<ReturnType<typeof updatePgbouncerConfiguration>>
export const usePgbouncerConfigurationUpdateMutation = ({
onSuccess,
onError,
...options
}: Omit<
UseMutationOptions<
PgbouncerConfigurationUpdateData,
ResponseError,
PgbouncerConfigurationUpdateVariables
>,
'mutationFn'
> = {}) => {
const queryClient = useQueryClient()
return useMutation<
PgbouncerConfigurationUpdateData,
ResponseError,
PgbouncerConfigurationUpdateVariables
>((vars) => updatePgbouncerConfiguration(vars), {
async onSuccess(data, variables, context) {
const { ref } = variables
await queryClient.invalidateQueries(databaseKeys.pgbouncerConfig(ref))
await onSuccess?.(data, variables, context)
},
async onError(data, variables, context) {
if (onError === undefined) {
toast.error(`Failed to update PgBouncer configuration: ${data.message}`)
} else {
onError(data, variables, context)
}
},
...options,
})
}
@@ -4,12 +4,12 @@ import { get, handleError } from 'data/fetchers'
import { ResponseError } from 'types'
import { databaseKeys } from './keys'
export type ProjectPgbouncerStatusVariables = {
export type PgbouncerStatusVariables = {
projectRef?: string
}
export async function getProjectPgbouncerStatus(
{ projectRef }: ProjectPgbouncerStatusVariables,
export async function getPgbouncerStatus(
{ projectRef }: PgbouncerStatusVariables,
signal?: AbortSignal
) {
if (!projectRef) throw new Error('projectRef is required')
@@ -22,19 +22,19 @@ export async function getProjectPgbouncerStatus(
return data
}
export type ProjectPgbouncerStatusData = Awaited<ReturnType<typeof getProjectPgbouncerStatus>>
export type ProjectPgbouncerStatusError = ResponseError
export type PgbouncerStatusData = Awaited<ReturnType<typeof getPgbouncerStatus>>
export type PgbouncerStatusError = ResponseError
export const useProjectPgbouncerStatusQuery = <TData = ProjectPgbouncerStatusData>(
{ projectRef }: ProjectPgbouncerStatusVariables,
export const usePgbouncerStatusQuery = <TData = PgbouncerStatusData>(
{ projectRef }: PgbouncerStatusVariables,
{
enabled = true,
...options
}: UseQueryOptions<ProjectPgbouncerStatusData, ProjectPgbouncerStatusError, TData> = {}
}: UseQueryOptions<PgbouncerStatusData, PgbouncerStatusError, TData> = {}
) =>
useQuery<ProjectPgbouncerStatusData, ProjectPgbouncerStatusError, TData>(
useQuery<PgbouncerStatusData, PgbouncerStatusError, TData>(
databaseKeys.pgbouncerStatus(projectRef),
({ signal }) => getProjectPgbouncerStatus({ projectRef }, signal),
({ signal }) => getPgbouncerStatus({ projectRef }, signal),
{
enabled: enabled && typeof projectRef !== 'undefined',
...options,
@@ -1,48 +0,0 @@
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
import type { components } from 'data/api'
import { get, handleError } from 'data/fetchers'
import type { ResponseError } from 'types'
import { databaseKeys } from './keys'
export type PoolingConfigurationVariables = {
projectRef?: string
}
export type PoolingConfiguration = components['schemas']['SupavisorConfigResponse']
/**
* Should rename this to Supavisor eventually since we're supporting both types of poolers
*/
export async function getPoolingConfiguration(
{ projectRef }: PoolingConfigurationVariables,
signal?: AbortSignal
) {
if (!projectRef) throw new Error('Project ref is required')
const { data, error } = await get(`/platform/projects/{ref}/config/supavisor`, {
params: { path: { ref: projectRef } },
signal,
})
if (error) handleError(error)
return data
}
export type PoolingConfigurationData = Awaited<ReturnType<typeof getPoolingConfiguration>>
export type PoolingConfigurationError = ResponseError
export const usePoolingConfigurationQuery = <TData = PoolingConfigurationData>(
{ projectRef }: PoolingConfigurationVariables,
{
enabled = true,
...options
}: UseQueryOptions<PoolingConfigurationData, PoolingConfigurationError, TData> = {}
) =>
useQuery<PoolingConfigurationData, PoolingConfigurationError, TData>(
databaseKeys.poolingConfiguration(projectRef),
({ signal }) => getPoolingConfiguration({ projectRef }, signal),
{
enabled: enabled && typeof projectRef !== 'undefined',
...options,
}
)
@@ -0,0 +1,42 @@
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
import { get, handleError } from 'data/fetchers'
import type { ResponseError } from 'types'
import { databaseKeys } from './keys'
type SupavisorConfigurationVariables = {
projectRef?: string
}
export async function getSupavisorConfiguration(
{ projectRef }: SupavisorConfigurationVariables,
signal?: AbortSignal
) {
if (!projectRef) throw new Error('Project ref is required')
const { data, error } = await get(`/platform/projects/{ref}/config/supavisor`, {
params: { path: { ref: projectRef } },
signal,
})
if (error) handleError(error)
return data
}
export type SupavisorConfigurationData = Awaited<ReturnType<typeof getSupavisorConfiguration>>
export type SupavisorConfigurationError = ResponseError
export const useSupavisorConfigurationQuery = <TData = SupavisorConfigurationData>(
{ projectRef }: SupavisorConfigurationVariables,
{
enabled = true,
...options
}: UseQueryOptions<SupavisorConfigurationData, SupavisorConfigurationError, TData> = {}
) =>
useQuery<SupavisorConfigurationData, SupavisorConfigurationError, TData>(
databaseKeys.poolingConfiguration(projectRef),
({ signal }) => getSupavisorConfiguration({ projectRef }, signal),
{
enabled: enabled && typeof projectRef !== 'undefined',
...options,
}
)
@@ -6,18 +6,15 @@ import { handleError, patch } from 'data/fetchers'
import type { ResponseError } from 'types'
import { databaseKeys } from './keys'
export type PoolingConfigurationUpdateVariables = {
type SupavisorConfigurationUpdateVariables = {
ref: string
} & components['schemas']['UpdateSupavisorConfigBody']
/**
* Should rename this to Supavisor eventually since we're supporting both types of poolers
*/
export async function updatePoolingConfiguration({
export async function updateSupavisorConfiguration({
ref,
pool_mode,
default_pool_size,
}: PoolingConfigurationUpdateVariables) {
}: SupavisorConfigurationUpdateVariables) {
if (!ref) return console.error('Project ref is required')
const { data, error } = await patch('/platform/projects/{ref}/config/supavisor', {
@@ -29,27 +26,27 @@ export async function updatePoolingConfiguration({
return data
}
type PoolingConfigurationUpdateData = Awaited<ReturnType<typeof updatePoolingConfiguration>>
type SupavisorConfigurationUpdateData = Awaited<ReturnType<typeof updateSupavisorConfiguration>>
export const usePoolingConfigurationUpdateMutation = ({
export const useSupavisorConfigurationUpdateMutation = ({
onSuccess,
onError,
...options
}: Omit<
UseMutationOptions<
PoolingConfigurationUpdateData,
SupavisorConfigurationUpdateData,
ResponseError,
PoolingConfigurationUpdateVariables
SupavisorConfigurationUpdateVariables
>,
'mutationFn'
> = {}) => {
const queryClient = useQueryClient()
return useMutation<
PoolingConfigurationUpdateData,
SupavisorConfigurationUpdateData,
ResponseError,
PoolingConfigurationUpdateVariables
>((vars) => updatePoolingConfiguration(vars), {
SupavisorConfigurationUpdateVariables
>((vars) => updateSupavisorConfiguration(vars), {
async onSuccess(data, variables, context) {
const { ref } = variables
await queryClient.invalidateQueries(databaseKeys.poolingConfiguration(ref))
@@ -57,7 +54,7 @@ export const usePoolingConfigurationUpdateMutation = ({
},
async onError(data, variables, context) {
if (onError === undefined) {
toast.error(`Failed to update pooling configuration: ${data.message}`)
toast.error(`Failed to update Supavisor configuration: ${data.message}`)
} else {
onError(data, variables, context)
}
+3 -3
View File
@@ -10,9 +10,9 @@ import { lintKeys } from './keys'
export type ProjectLintsVariables = {
projectRef?: string
}
export type ProjectLintResponse = components['schemas']['ProjectLintResponse']
export type Lint = ProjectLintResponse
export type LINT_TYPES = ProjectLintResponse['name']
type ProjectLintResponse = components['schemas']['GetProjectLintsResponseDto']
export type Lint = ProjectLintResponse[0]
export type LINT_TYPES = ProjectLintResponse[0]['name']
export async function getProjectLints({ projectRef }: ProjectLintsVariables, signal?: AbortSignal) {
if (!projectRef) throw new Error('Project ref is required')
@@ -50,6 +50,7 @@ export const useOrgSubscriptionQuery = <TData = OrgSubscriptionData>(
({ signal }) => getOrgSubscription({ orgSlug }, signal),
{
enabled: enabled && canReadSubscriptions && typeof orgSlug !== 'undefined',
staleTime: 60 * 60 * 1000, // 60 minutes
...options,
}
)
@@ -9,6 +9,9 @@ export type ProjectAddonsVariables = {
projectRef?: string
}
// [Joshen] For any customer facing text - let's use "Add-on" hyphenated
// Will need to address consistency across the dashboard
export async function getProjectAddons(
{ projectRef }: ProjectAddonsVariables,
signal?: AbortSignal
@@ -36,6 +39,7 @@ export const useProjectAddonsQuery = <TData = ProjectAddonsData>(
({ signal }) => getProjectAddons({ projectRef }, signal),
{
enabled: enabled && IS_PLATFORM && typeof projectRef !== 'undefined',
staleTime: 60 * 60 * 1000, // 60 minutes
...options,
}
)
+1
View File
@@ -19,6 +19,7 @@ const HOSTED_SUPPORTED_API_URLS = [
'/ai/docs',
'/get-ip-address',
'/get-utc-time',
'/edge-functions/test',
]
export function middleware(request: NextRequest) {
@@ -0,0 +1,112 @@
import { NextApiRequest, NextApiResponse } from 'next'
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { method } = req
switch (method) {
case 'POST':
return handlePost(req, res)
default:
return new Response(
JSON.stringify({ data: null, error: { message: `Method ${method} Not Allowed` } }),
{
status: 405,
headers: { 'Content-Type': 'application/json', Allow: 'POST' },
}
)
}
}
async function handlePost(req: NextApiRequest, res: NextApiResponse) {
try {
const { url, method, body: requestBody, headers: customHeaders } = req.body
// Remove any undefined or null values from custom headers
const sanitizedCustomHeaders = Object.entries(customHeaders || {}).reduce(
(acc, [key, value]) => {
if (value !== undefined && value !== null && value !== '') {
acc[key] = value as string
}
return acc
},
{} as Record<string, string>
)
// Only use custom headers and ensure Content-Type is set
const requestHeaders: Record<string, string> = {
'Content-Type': 'application/json',
...sanitizedCustomHeaders,
}
// Use the test authorization header if provided
if (sanitizedCustomHeaders['x-test-authorization']) {
requestHeaders['Authorization'] = sanitizedCustomHeaders['x-test-authorization']
// Remove the x-test-authorization header as we've moved it to Authorization
delete requestHeaders['x-test-authorization']
}
// Prepare the request body based on method and Content-Type
let finalBody = undefined
if (method !== 'GET' && method !== 'HEAD') {
if (requestHeaders['Content-Type'] === 'application/json') {
finalBody = typeof requestBody === 'string' ? requestBody : JSON.stringify(requestBody)
} else {
finalBody = requestBody
}
}
const response = await fetch(url, {
method,
headers: requestHeaders,
body: finalBody,
})
// Handle non-JSON responses
let responseBody: string
const contentType = response.headers.get('content-type')
if (contentType?.includes('application/json')) {
// If JSON, parse and stringify to ensure it's valid JSON
const jsonBody = await response.json()
responseBody = JSON.stringify(jsonBody)
} else {
// For non-JSON responses, get raw text
responseBody = await response.text()
}
if (!response.ok) {
// Try to parse error response if it's JSON
try {
const errorBody = JSON.parse(responseBody)
return res.status(response.status).json({
status: response.status,
error: { message: errorBody?.error || 'Edge function returned an error' },
})
} catch (parseError) {
// If not JSON, return the raw error
return res.status(response.status).json({
status: response.status,
error: { message: responseBody || 'Edge function returned an error' },
})
}
}
const responseHeaders: Record<string, string> = {}
response.headers.forEach((value, key) => {
responseHeaders[key] = value
})
return res.status(response.status).json({
status: response.status,
headers: responseHeaders,
body: responseBody,
})
} catch (error: any) {
return res.status(500).json({
status: 500,
error: {
message: error.message || 'Failed to test edge function',
},
})
}
}
@@ -8,16 +8,16 @@ import LinterDataGrid from 'components/interfaces/Linter/LinterDataGrid'
import LinterFilters from 'components/interfaces/Linter/LinterFilters'
import LinterPageFooter from 'components/interfaces/Linter/LinterPageFooter'
import AdvisorsLayout from 'components/layouts/AdvisorsLayout/AdvisorsLayout'
import DefaultLayout from 'components/layouts/DefaultLayout'
import { FormHeader } from 'components/ui/Forms/FormHeader'
import { Lint, useProjectLintsQuery } from 'data/lint/lint-query'
import { useSelectedProject } from 'hooks/misc/useSelectedProject'
import type { NextPageWithLayout } from 'types'
import { LoadingLine } from 'ui'
import DefaultLayout from 'components/layouts/DefaultLayout'
const ProjectLints: NextPageWithLayout = () => {
const project = useSelectedProject()
const { ref, preset, id } = useParams()
const { preset, id } = useParams()
// need to maintain a list of filters for each tab
const [filters, setFilters] = useState<{ level: LINTER_LEVELS; filters: string[] }[]>([
@@ -4,12 +4,12 @@ import LogsPreviewer from 'components/interfaces/Settings/Logs/LogsPreviewer'
import DefaultLayout from 'components/layouts/DefaultLayout'
import LogsLayout from 'components/layouts/LogsLayout/LogsLayout'
import { Loading } from 'components/ui/Loading'
import { usePoolingConfigurationQuery } from 'data/database/pooling-configuration-query'
import { useSupavisorConfigurationQuery } from 'data/database/supavisor-configuration-query'
import type { NextPageWithLayout } from 'types'
export const LogPage: NextPageWithLayout = () => {
const { ref } = useParams()
const { isLoading } = usePoolingConfigurationQuery({ projectRef: ref ?? 'default' })
const { isLoading } = useSupavisorConfigurationQuery({ projectRef: ref ?? 'default' })
// this prevents initial load of pooler logs before config has been retrieved
if (isLoading) return <Loading />
@@ -1,11 +1,4 @@
const wasmBuffer = await Deno.readFile(
new URL("./add-wasm/pkg/add_wasm_bg.wasm", import.meta.url),
);
const { instance } = await WebAssembly.instantiate(
wasmBuffer,
);
const { add } = instance.exports;
import { add } from "./add-wasm/pkg/add_wasm.js";
Deno.serve(async (req) => {
const { a, b } = await req.json();
+216
View File
@@ -317,6 +317,43 @@ export interface paths {
patch: operations['v1-update-auth-service-config']
trace?: never
}
'/v1/projects/{ref}/config/auth/signing-keys': {
parameters: {
query?: never
header?: never
path?: never
cookie?: never
}
/** [Alpha] List all signing keys for the project */
get: operations['listSigningKeysForProject']
put?: never
/** [Alpha] Create a new signing key for the project in standby status */
post: operations['createSigningKeyForProject']
delete?: never
options?: never
head?: never
patch?: never
trace?: never
}
'/v1/projects/{ref}/config/auth/signing-keys/{id}': {
parameters: {
query?: never
header?: never
path?: never
cookie?: never
}
/** [Alpha] Get information about a signing key */
get: operations['getSigningKeyForProject']
put?: never
post?: never
/** [Alpha] Remove a signing key from a project, where the status is previously_used */
delete: operations['deleteSigningKey']
options?: never
head?: never
/** [Alpha] Update a signing key, mainly its status */
patch: operations['patchSigningKey']
trace?: never
}
'/v1/projects/{ref}/config/auth/sso/providers': {
parameters: {
query?: never
@@ -1517,6 +1554,12 @@ export interface components {
name: string
value: string
}
CreateSigningKeyBody: {
/** @enum {string} */
algorithm: 'EdDSA' | 'ES256' | 'RS256' | 'HS256'
/** @enum {string} */
status?: 'in_use' | 'standby'
}
CreateThirdPartyAuthBody: {
custom_jwks?: Record<string, never>
jwks_url?: string
@@ -1852,6 +1895,34 @@ export interface components {
| 'ap-south-1'
| 'sa-east-1'
}
SigningKeyResponse: {
/** @enum {string} */
algorithm: 'EdDSA' | 'ES256' | 'RS256' | 'HS256'
/** Format: date-time */
created_at: string
/** Format: uuid */
id: string
public_jwk?: unknown
/** @enum {string} */
status: 'in_use' | 'previously_used' | 'revoked' | 'standby'
/** Format: date-time */
updated_at: string
}
SigningKeysResponse: {
keys: {
/** @enum {string} */
algorithm: 'EdDSA' | 'ES256' | 'RS256' | 'HS256'
/** Format: date-time */
created_at: string
/** Format: uuid */
id: string
public_jwk?: unknown
/** @enum {string} */
status: 'in_use' | 'previously_used' | 'revoked' | 'standby'
/** Format: date-time */
updated_at: string
}[]
}
SnippetContent: {
favorite: boolean
schema_version: string
@@ -2223,6 +2294,10 @@ export interface components {
saml?: components['schemas']['SamlDescriptor']
updated_at?: string
}
UpdateSigningKeyBody: {
/** @enum {string} */
status: 'in_use' | 'previously_used' | 'revoked' | 'standby'
}
UpdateStorageConfigBody: {
features?: components['schemas']['StorageFeatures']
/** Format: int64 */
@@ -3266,6 +3341,147 @@ export interface operations {
}
}
}
listSigningKeysForProject: {
parameters: {
query?: never
header?: never
path: {
ref: string
}
cookie?: never
}
requestBody?: never
responses: {
200: {
headers: {
[name: string]: unknown
}
content: {
'application/json': components['schemas']['SigningKeysResponse']
}
}
403: {
headers: {
[name: string]: unknown
}
content?: never
}
}
}
createSigningKeyForProject: {
parameters: {
query?: never
header?: never
path: {
ref: string
}
cookie?: never
}
requestBody: {
content: {
'application/json': components['schemas']['CreateSigningKeyBody']
}
}
responses: {
/** @description [Alpha] Create a new signing key for the project in standby status */
201: {
headers: {
[name: string]: unknown
}
content: {
'application/json': components['schemas']['CreateSigningKeyBody']
}
}
403: {
headers: {
[name: string]: unknown
}
content?: never
}
}
}
getSigningKeyForProject: {
parameters: {
query?: never
header?: never
path: {
id: string
ref: string
}
cookie?: never
}
requestBody?: never
responses: {
200: {
headers: {
[name: string]: unknown
}
content: {
'application/json': components['schemas']['SigningKeyResponse']
}
}
}
}
deleteSigningKey: {
parameters: {
query?: never
header?: never
path: {
id: string
ref: string
}
cookie?: never
}
requestBody?: never
responses: {
200: {
headers: {
[name: string]: unknown
}
content: {
'application/json': components['schemas']['SigningKeyResponse']
}
}
403: {
headers: {
[name: string]: unknown
}
content?: never
}
}
}
patchSigningKey: {
parameters: {
query?: never
header?: never
path: {
id: string
ref: string
}
cookie?: never
}
requestBody: {
content: {
'application/json': components['schemas']['UpdateSigningKeyBody']
}
}
responses: {
200: {
headers: {
[name: string]: unknown
}
content: {
'application/json': components['schemas']['SigningKeyResponse']
}
}
403: {
headers: {
[name: string]: unknown
}
content?: never
}
}
}
'v1-list-all-sso-provider': {
parameters: {
query?: never
+464 -54
View File
@@ -2899,6 +2899,40 @@ export interface paths {
patch?: never
trace?: never
}
'/platform/projects/{ref}/run-lints/leaked-service-key': {
parameters: {
query?: never
header?: never
path?: never
cookie?: never
}
/** Run project leaked service key lint */
get: operations['ProjectRunLintsController_runLeakedServiceKeyLint']
put?: never
post?: never
delete?: never
options?: never
head?: never
patch?: never
trace?: never
}
'/platform/projects/{ref}/run-lints/no-backup-admin': {
parameters: {
query?: never
header?: never
path?: never
cookie?: never
}
/** Run project backup admin lint */
get: operations['ProjectRunLintsController_runAuthBackupAdminLint']
put?: never
post?: never
delete?: never
options?: never
head?: never
patch?: never
trace?: never
}
'/platform/projects/{ref}/service-versions': {
parameters: {
query?: never
@@ -3100,9 +3134,11 @@ export interface paths {
path?: never
cookie?: never
}
get?: never
/** Gets a pipeline by id */
get: operations['ReplicationPipelinesController_getPipeline']
put?: never
post?: never
/** Updates a pipeline */
post: operations['ReplicationPipelinesController_updatePipeline']
/** Deletes a pipeline */
delete: operations['ReplicationPipelinesController_deletePipeline']
options?: never
@@ -3186,9 +3222,11 @@ export interface paths {
path?: never
cookie?: never
}
get?: never
/** Gets a sink by id */
get: operations['ReplicationSinksController_getSink']
put?: never
post?: never
/** Updates a sink */
post: operations['ReplicationSinksController_updateSink']
/** Deletes a sink */
delete: operations['ReplicationSinksController_deleteSink']
options?: never
@@ -3961,6 +3999,59 @@ export interface components {
result: Record<string, never>[]
retention_period: number
}
AuthBackupAdminLintResponseDto: {
lints: {
cache_key: string
categories: ('PERFORMANCE' | 'SECURITY')[]
description: string
detail: string
/** @enum {string} */
facing: 'EXTERNAL'
/** @enum {string} */
level: 'ERROR' | 'WARN' | 'INFO'
metadata?: {
entity?: string
fkey_columns?: number[]
fkey_name?: string
name?: string
schema?: string
/** @enum {string} */
type?: 'table' | 'view' | 'auth' | 'function' | 'extension' | 'compliance'
}
/** @enum {string} */
name:
| 'unindexed_foreign_keys'
| 'auth_users_exposed'
| 'auth_rls_initplan'
| 'no_primary_key'
| 'unused_index'
| 'multiple_permissive_policies'
| 'policy_exists_rls_disabled'
| 'rls_enabled_no_policy'
| 'duplicate_index'
| 'security_definer_view'
| 'function_search_path_mutable'
| 'rls_disabled_in_public'
| 'extension_in_public'
| 'rls_references_user_metadata'
| 'materialized_view_in_api'
| 'foreign_table_in_api'
| 'unsupported_reg_types'
| 'auth_otp_long_expiry'
| 'auth_otp_short_length'
| 'ssl_not_enforced'
| 'network_restrictions_not_set'
| 'password_requirements_min_length'
| 'pitr_not_enabled'
| 'auth_leaked_password_protection'
| 'auth_insufficient_mfa_options'
| 'auth_password_policy_missing'
| 'leaked_service_key'
| 'no_backup_admin'
remediation: string
title: string
}[]
}
AutoApiService: {
app: {
id?: number
@@ -4479,7 +4570,7 @@ export interface components {
database_type: 'PRIMARY' | 'READ_REPLICA'
DatabaseDetailResponse: {
/** @enum {string} */
cloud_provider: 'AWS' | 'FLY'
cloud_provider: 'AWS' | 'AWS_K8S' | 'FLY'
connectionString: string
db_host: string
db_name: string
@@ -4668,6 +4759,59 @@ export interface components {
private: number
shared: number
}
GetLeakedServiceKeyLintResponseDto: {
lints: {
cache_key: string
categories: ('PERFORMANCE' | 'SECURITY')[]
description: string
detail: string
/** @enum {string} */
facing: 'EXTERNAL'
/** @enum {string} */
level: 'ERROR' | 'WARN' | 'INFO'
metadata?: {
entity?: string
fkey_columns?: number[]
fkey_name?: string
name?: string
schema?: string
/** @enum {string} */
type?: 'table' | 'view' | 'auth' | 'function' | 'extension' | 'compliance'
}
/** @enum {string} */
name:
| 'unindexed_foreign_keys'
| 'auth_users_exposed'
| 'auth_rls_initplan'
| 'no_primary_key'
| 'unused_index'
| 'multiple_permissive_policies'
| 'policy_exists_rls_disabled'
| 'rls_enabled_no_policy'
| 'duplicate_index'
| 'security_definer_view'
| 'function_search_path_mutable'
| 'rls_disabled_in_public'
| 'extension_in_public'
| 'rls_references_user_metadata'
| 'materialized_view_in_api'
| 'foreign_table_in_api'
| 'unsupported_reg_types'
| 'auth_otp_long_expiry'
| 'auth_otp_short_length'
| 'ssl_not_enforced'
| 'network_restrictions_not_set'
| 'password_requirements_min_length'
| 'pitr_not_enabled'
| 'auth_leaked_password_protection'
| 'auth_insufficient_mfa_options'
| 'auth_password_policy_missing'
| 'leaked_service_key'
| 'no_backup_admin'
remediation: string
title: string
}[]
}
GetOAuthAuthorizationResponse: {
approved_at?: string
approved_organization_slug?: string
@@ -4728,6 +4872,57 @@ export interface components {
GetProjectByFlyExtensionIdResponse: {
ref: string
}
GetProjectLintsResponseDto: {
cache_key: string
categories: ('PERFORMANCE' | 'SECURITY')[]
description: string
detail: string
/** @enum {string} */
facing: 'EXTERNAL'
/** @enum {string} */
level: 'ERROR' | 'WARN' | 'INFO'
metadata?: {
entity?: string
fkey_columns?: number[]
fkey_name?: string
name?: string
schema?: string
/** @enum {string} */
type?: 'table' | 'view' | 'auth' | 'function' | 'extension' | 'compliance'
}
/** @enum {string} */
name:
| 'unindexed_foreign_keys'
| 'auth_users_exposed'
| 'auth_rls_initplan'
| 'no_primary_key'
| 'unused_index'
| 'multiple_permissive_policies'
| 'policy_exists_rls_disabled'
| 'rls_enabled_no_policy'
| 'duplicate_index'
| 'security_definer_view'
| 'function_search_path_mutable'
| 'rls_disabled_in_public'
| 'extension_in_public'
| 'rls_references_user_metadata'
| 'materialized_view_in_api'
| 'foreign_table_in_api'
| 'unsupported_reg_types'
| 'auth_otp_long_expiry'
| 'auth_otp_short_length'
| 'ssl_not_enforced'
| 'network_restrictions_not_set'
| 'password_requirements_min_length'
| 'pitr_not_enabled'
| 'auth_leaked_password_protection'
| 'auth_insufficient_mfa_options'
| 'auth_password_policy_missing'
| 'leaked_service_key'
| 'no_backup_admin'
remediation: string
title: string
}[]
GetPublicUrlBody: {
options?: components['schemas']['PublicUrlOptions']
path: string
@@ -5470,6 +5665,7 @@ export interface components {
defaultPaymentMethodId: string | null
}
PgbouncerConfigResponse: {
connection_string: string
connectionString: string
db_dns_name: string
db_host: string
@@ -5897,52 +6093,6 @@ export interface components {
supabase_project_ref: string
updated_at: string
}
ProjectLintMetadata: {
entity?: string
fkey_columns?: number[]
fkey_name?: string
name?: string
schema?: string
/** @enum {string} */
type?: 'table' | 'view' | 'auth' | 'function' | 'extension'
}
ProjectLintResponse: {
cache_key: string
categories: ('PERFORMANCE' | 'SECURITY')[]
description: string
detail: string
facing: string
/** @enum {string} */
level: 'ERROR' | 'WARN' | 'INFO'
metadata: components['schemas']['ProjectLintMetadata'] | null
/** @enum {string} */
name:
| 'unindexed_foreign_keys'
| 'auth_users_exposed'
| 'auth_rls_initplan'
| 'no_primary_key'
| 'unused_index'
| 'multiple_permissive_policies'
| 'policy_exists_rls_disabled'
| 'rls_enabled_no_policy'
| 'duplicate_index'
| 'security_definer_view'
| 'function_search_path_mutable'
| 'rls_disabled_in_public'
| 'extension_in_public'
| 'rls_references_user_metadata'
| 'materialized_view_in_api'
| 'foreign_table_in_api'
| 'unsupported_reg_types'
| 'auth_otp_long_expiry'
| 'auth_otp_short_length'
| 'ssl_not_enforced'
| 'network_restrictions_not_set'
| 'password_requirements_min_length'
| 'pitr_not_enabled'
remediation: Record<string, never>
title: string
}
ProjectRefResponse: {
id: number
name: string
@@ -6076,6 +6226,22 @@ export interface components {
* @enum {string}
*/
ReleaseChannel: 'internal' | 'alpha' | 'beta' | 'ga' | 'withdrawn' | 'preview'
ReplicationPipelineResponse: {
config: {
config: {
max_fill_secs: number
max_size: number
}
}
id: number
publication_name: string
replicator_id: number
sink_id: number
sink_name: string
source_id: number
source_name: string
tenant_id: string
}
ReplicationPipelinesResponse: {
pipelines: {
config: {
@@ -6107,6 +6273,18 @@ export interface components {
}[]
}[]
}
ReplicationSinkResponse: {
config: {
big_query: {
dataset_id: string
project_id: string
service_account_key: string
}
}
id: number
name: string
tenant_id: string
}
ReplicationSinksResponse: {
sinks: {
config: {
@@ -6889,6 +7067,31 @@ export interface components {
/** @description Whether to only allow private channels */
private_only?: boolean
}
UpdateReplicationPipelineBody: {
/** @description Pipeline config */
config?: {
config: {
max_fill_secs: number
max_size: number
}
}
/** @description Publication name */
publication_name?: string
/** @description Sink id */
sink_id?: number
/** @description Source id */
source_id?: number
}
UpdateReplicationSinkBody: {
/** @description BigQuery dataset id */
dataset_id: string
/** @description BigQuery project id */
project_id: string
/** @description BigQuery service account key */
service_account_key: string
/** @description Sink name */
sink_name: string
}
UpdateSchemaBody: {
name?: string
owner?: string
@@ -14111,7 +14314,9 @@ export interface operations {
parameters: {
query?: never
header?: never
path?: never
path: {
ref: string
}
cookie?: never
}
requestBody?: never
@@ -14137,7 +14342,9 @@ export interface operations {
parameters: {
query?: never
header?: never
path?: never
path: {
ref: string
}
cookie?: never
}
requestBody: {
@@ -15050,6 +15257,7 @@ export interface operations {
| 'physical_replication_lag_physical_replica_lag_seconds'
| 'pg_stat_database_num_backends'
| 'supavisor_connections_active'
| 'pgbouncer_pools_client_active_connections'
databaseIdentifier?: string
endDate: string
interval?: '1m' | '5m' | '10m' | '30m' | '1h' | '1d'
@@ -15412,7 +15620,63 @@ export interface operations {
[name: string]: unknown
}
content: {
'application/json': components['schemas']['ProjectLintResponse'][]
'application/json': components['schemas']['GetProjectLintsResponseDto']
}
}
403: {
headers: {
[name: string]: unknown
}
content?: never
}
}
}
ProjectRunLintsController_runLeakedServiceKeyLint: {
parameters: {
query?: never
header?: never
path: {
/** @description Project ref */
ref: string
}
cookie?: never
}
requestBody?: never
responses: {
200: {
headers: {
[name: string]: unknown
}
content: {
'application/json': components['schemas']['GetLeakedServiceKeyLintResponseDto']
}
}
403: {
headers: {
[name: string]: unknown
}
content?: never
}
}
}
ProjectRunLintsController_runAuthBackupAdminLint: {
parameters: {
query?: never
header?: never
path: {
/** @description Project ref */
ref: string
}
cookie?: never
}
requestBody?: never
responses: {
200: {
headers: {
[name: string]: unknown
}
content: {
'application/json': components['schemas']['AuthBackupAdminLintResponseDto']
}
}
403: {
@@ -15783,6 +16047,79 @@ export interface operations {
}
}
}
ReplicationPipelinesController_getPipeline: {
parameters: {
query?: never
header?: never
path: {
pipeline_id: number
ref: string
}
cookie?: never
}
requestBody?: never
responses: {
/** @description Returns the pipeline. */
200: {
headers: {
[name: string]: unknown
}
content: {
'application/json': components['schemas']['ReplicationPipelineResponse']
}
}
403: {
headers: {
[name: string]: unknown
}
content?: never
}
/** @description Failed to get pipeline */
500: {
headers: {
[name: string]: unknown
}
content?: never
}
}
}
ReplicationPipelinesController_updatePipeline: {
parameters: {
query?: never
header?: never
path: {
pipeline_id: number
ref: string
}
cookie?: never
}
requestBody: {
content: {
'application/json': components['schemas']['UpdateReplicationPipelineBody']
}
}
responses: {
201: {
headers: {
[name: string]: unknown
}
content?: never
}
403: {
headers: {
[name: string]: unknown
}
content?: never
}
/** @description Failed to update pipeline */
500: {
headers: {
[name: string]: unknown
}
content?: never
}
}
}
ReplicationPipelinesController_deletePipeline: {
parameters: {
query?: never
@@ -15990,6 +16327,79 @@ export interface operations {
}
}
}
ReplicationSinksController_getSink: {
parameters: {
query?: never
header?: never
path: {
ref: string
sink_id: number
}
cookie?: never
}
requestBody?: never
responses: {
/** @description Returns the sink. */
200: {
headers: {
[name: string]: unknown
}
content: {
'application/json': components['schemas']['ReplicationSinkResponse']
}
}
403: {
headers: {
[name: string]: unknown
}
content?: never
}
/** @description Failed to get sink */
500: {
headers: {
[name: string]: unknown
}
content?: never
}
}
}
ReplicationSinksController_updateSink: {
parameters: {
query?: never
header?: never
path: {
ref: string
sink_id: number
}
cookie?: never
}
requestBody: {
content: {
'application/json': components['schemas']['UpdateReplicationSinkBody']
}
}
responses: {
201: {
headers: {
[name: string]: unknown
}
content?: never
}
403: {
headers: {
[name: string]: unknown
}
content?: never
}
/** @description Failed to update sink */
500: {
headers: {
[name: string]: unknown
}
content?: never
}
}
}
ReplicationSinksController_deleteSink: {
parameters: {
query?: never
+13 -13
View File
@@ -6,7 +6,6 @@ import { cn } from 'ui/src/lib/utils'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import utc from 'dayjs/plugin/utc'
import { Clipboard } from 'lucide-react'
dayjs.extend(relativeTime)
dayjs.extend(utc)
@@ -94,7 +93,7 @@ export const TimestampInfo = ({
const [copied, setCopied] = useState(false)
return (
<span
<div
onPointerDown={(e) => {
e.stopPropagation()
}}
@@ -112,23 +111,24 @@ export const TimestampInfo = ({
}, 1000)
}}
className={cn(
'relative cursor-default grid grid-cols-2 gap-2 hover:bg-surface-100 px-2 py-1 group',
'relative cursor-pointer flex gap-y-2 gap-x-0.5 hover:bg-surface-100 px-2 py-1 group',
{ 'bg-surface-100': copied }
)}
>
<span className="text-right truncate">{label}:</span>
<div className="relative">
<div className="flex items-center gap-x-2 text-left truncate">
<p>{label}</p>
<div className="border-t w-full border-dashed" />
</div>
<div className="relative flex items-center gap-x-2 flex-grow">
<div className="border-t w-full border-dashed z-10" />
{copied && (
<span className="absolute inset-0 flex items-center text-brand-600 bg-surface-100">
<span className="flex items-center justify-end w-full absolute inset-0 flex items-right text-brand-600 bg-surface-100">
Copied!
</span>
)}
<span className="flex items-center gap-x-2">
{value}
<Clipboard size={12} className="opacity-0 group-hover:opacity-100 transition" />
</span>
<span className="flex items-center gap-x-2 justify-end whitespace-nowrap">{value}</span>
</div>
</span>
</div>
)
}
@@ -145,9 +145,9 @@ export const TimestampInfo = ({
: timestampUtcFormatter({ utcTimestamp, format: labelFormat })}
</span>
</TooltipTrigger>
<TooltipContent align={align} side="right" className="font-mono p-0 py-1">
<TooltipContent align={align} side="right" className="font-mono p-0 py-1 min-w-80">
<TooltipRow label="UTC" value={utc} />
<TooltipRow label={`${localTimezone}`} value={local} />
<TooltipRow label={localTimezone} value={local} />
<TooltipRow label="Relative" value={relative} />
<TooltipRow label="Timestamp" value={String(utcTimestamp)} />
</TooltipContent>
@@ -16,7 +16,7 @@ RadioGroupCard.displayName = RadioGroupPrimitive.Root.displayName
interface RadioGroupCardItemProps {
image?: React.ReactNode
label: string
label: string | React.ReactNode
showIndicator?: boolean
}
+1 -1
View File
@@ -16,5 +16,5 @@ rules.filler = "include('supa-mdx-lint/Rule004ExcludeWords/filler.toml')"
rules.first_person = "include('supa-mdx-lint/Rule004ExcludeWords/first_person.toml')"
rules.gender_neutral = "include('supa-mdx-lint/Rule004ExcludeWords/gender_neutral.toml')"
rules.inclusive_wording = "include('supa-mdx-lint/Rule004ExcludeWords/inclusive_wording.toml')"
rules.preferred_spelling = "include('supa-mdx-lint/Rule004ExcludeWords/preferred_spelling.toml')"
rules.preferred_usage = "include('supa-mdx-lint/Rule004ExcludeWords/preferred_usage.toml')"
rules.slang = "include('supa-mdx-lint/Rule004ExcludeWords/slang.toml')"
+1
View File
@@ -75,6 +75,7 @@ allow_list = [
"[Pp]laintext",
"[Pp]olyfill(s|ed)?",
"[Pp]oolers?",
"[Pp]resign(ed|ing)?",
"[Pp]sycopg",
"[Qq]uickstarts?",
"[Rr]ealtime",
@@ -21,4 +21,8 @@ words = [
"PostgreSQL",
"Postgres",
],
[
"concurrent clients",
"concurrent connections",
],
]