mirror of
https://github.com/supabase/supabase.git
synced 2026-10-15 22:29:11 +03:00
Merge branch 'master' of github.com:supabase/supabase
This commit is contained in:
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.
|
||||
|
||||
|
||||
@@ -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')
|
||||
`,
|
||||
},
|
||||
}),
|
||||
|
||||
+2
@@ -66,3 +66,5 @@ final data = res.data;`,
|
||||
)`,
|
||||
},
|
||||
]
|
||||
|
||||
export const HTTP_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'] as const
|
||||
+1
-1
@@ -148,7 +148,7 @@ export const EdgeFunctionDetails = () => {
|
||||
<FormControl_Shadcn_>
|
||||
<Input_Shadcn_
|
||||
{...field}
|
||||
className="w-full"
|
||||
className="w-64"
|
||||
disabled={!canUpdateEdgeFunction}
|
||||
/>
|
||||
</FormControl_Shadcn_>
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
export type ResponseData = {
|
||||
status: number
|
||||
headers: Record<string, string>
|
||||
body: string
|
||||
}
|
||||
|
||||
export type ErrorWithStatus = Error & {
|
||||
cause?: {
|
||||
status: number
|
||||
}
|
||||
}
|
||||
+443
@@ -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
|
||||
|
||||
+2
@@ -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: {
|
||||
|
||||
+656
-302
File diff suppressed because it is too large.
Load diff
@@ -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 = () => {
|
||||
|
||||
+3
-3
@@ -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: '',
|
||||
},
|
||||
],
|
||||
|
||||
+9
-11
@@ -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))
|
||||
@@ -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
|
||||
+4
-4
@@ -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: {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
+11
-14
@@ -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)
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
@@ -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();
|
||||
|
||||
Vendored
+216
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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')"
|
||||
@@ -75,6 +75,7 @@ allow_list = [
|
||||
"[Pp]laintext",
|
||||
"[Pp]olyfill(s|ed)?",
|
||||
"[Pp]oolers?",
|
||||
"[Pp]resign(ed|ing)?",
|
||||
"[Pp]sycopg",
|
||||
"[Qq]uickstarts?",
|
||||
"[Rr]ealtime",
|
||||
|
||||
+4
@@ -21,4 +21,8 @@ words = [
|
||||
"PostgreSQL",
|
||||
"Postgres",
|
||||
],
|
||||
[
|
||||
"concurrent clients",
|
||||
"concurrent connections",
|
||||
],
|
||||
]
|
||||
Reference in new issue
Block a user