Merge remote-tracking branch 'origin/master' into kemal/depr-406-pro-upgrade-enhancement-better-notification-regarding

This commit is contained in:
kemal committed 2026-03-24 12:06:31 +00:00
commit c232826c8a
279 files changed
+66940 -42346

No files matched your search

+16 -4
View File
@@ -117,10 +117,6 @@ await waitForApiResponse(page, 'pg-meta', ref, 'tables')
// ✅ Acceptable - waiting for client-side debounce
await page.getByRole('textbox').fill('search term')
await page.waitForTimeout(300) // Allow debounce to complete
// ✅ Acceptable - waiting for clipboard API
await page.evaluate(() => navigator.clipboard.readText())
await page.waitForTimeout(500)
```
## Test Structure
@@ -274,6 +270,22 @@ import {
} from '../utils/wait-for-response.js'
```
### Use the existing assertions utilities
#### Clipboard assertions
```ts
// ❌ Avoid - brittle hard coded timeout
await page.evaluate(() => navigator.clipboard.readText())
await page.waitForTimeout(500)
// ✅ Good - this utility function uses Playwright auto-retries mechanisms
await expectClipboardValue({
page,
value: 'expectedValue'
})
```
## API Mocking
### Mock APIs for isolated testing
@@ -21,6 +21,7 @@ jobs:
sparse-checkout: |
apps/docs
patches
packages/generator
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
name: Install pnpm
+33
View File
@@ -1963,6 +1963,17 @@ export const Index: Record<string, any> = {
subcategory: "undefined",
chunks: []
},
"key-value-field-array-demo": {
name: "key-value-field-array-demo",
type: "components:example",
registryDependencies: ["button","form","input"],
component: React.lazy(() => import("@/registry/default/example/key-value-field-array-demo")),
source: "",
files: ["registry/default/example/key-value-field-array-demo.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"field-choice-card": {
name: "field-choice-card",
type: "components:example",
@@ -2678,6 +2689,28 @@ export const Index: Record<string, any> = {
subcategory: "undefined",
chunks: []
},
"error-display-demo": {
name: "error-display-demo",
type: "components:example",
registryDependencies: ["error-display"],
component: React.lazy(() => import("@/registry/default/example/error-display-demo")),
source: "",
files: ["registry/default/example/error-display-demo.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"error-display-with-children": {
name: "error-display-with-children",
type: "components:example",
registryDependencies: ["error-display"],
component: React.lazy(() => import("@/registry/default/example/error-display-with-children")),
source: "",
files: ["registry/default/example/error-display-with-children.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"logs-bar-chart": {
name: "logs-bar-chart",
type: "components:example",
+5
View File
@@ -212,6 +212,11 @@ export const docsConfig: DocsConfig = {
href: '/docs/fragments/status-codes',
items: [],
},
{
title: 'Key/Value Field Array',
href: '/docs/fragments/key-value-field-array',
items: [],
},
],
},
{
@@ -0,0 +1,43 @@
---
title: Key/Value Field Array
description: A shared form fragment for repeated text key/value pairs.
component: true
fragment: true
---
<ComponentPreview
name="key-value-field-array-demo"
description="A shared form fragment for repeated text key/value pairs."
peekCode
showDottedGrid
wide
/>
## Usage
Use `KeyValueFieldArray` when each row is two text inputs backed by `react-hook-form`, such as HTTP headers, query parameters, or configuration parameters.
```tsx
import { KeyValueFieldArray } from 'ui-patterns/form/KeyValueFieldArray/KeyValueFieldArray'
```
```tsx
<KeyValueFieldArray
control={form.control}
name="headers"
keyFieldName="name"
valueFieldName="value"
createEmptyRow={() => ({ name: '', value: '' })}
keyPlaceholder="Header name"
valuePlaceholder="Header value"
addLabel="Add header"
/>
```
`KeyValueFieldArray` owns the row add/remove behavior and renders the per-input form messages for you. Compose it inside `FormItemLayout` when you want the standard label, description, and message treatment around the entire section.
## When to use it
- Use a plain field array for repeated single values such as redirect URIs.
- Use `KeyValueFieldArray` for repeated text/text pairs such as headers, parameters, and config entries.
- Build a custom row UI instead when each row mixes different controls, such as a text input paired with a `Select`.
@@ -27,6 +27,15 @@ Forms in side panels (Sheets) use `FormItemLayout` with `layout="horizontal"` on
wide
/>
## Field Arrays
The form previews above include both repeated-field patterns used across Studio:
- **Field Array** for repeated single-value rows such as redirect URIs.
- **Key/Value Field Array** for repeated text pairs such as headers, parameters, and config entries.
Use the shared [Key/Value Field Array](../fragments/key-value-field-array) fragment when each row is two text inputs managed by `react-hook-form`. Keep using a plain field array when each row is just one input, and build a custom row when the cells are mixed controls.
## Best Practices
1. **Always use FormItemLayout**: Use `FormItemLayout` instead of manually composing `FormItem`, `FormLabel`, `FormMessage`, and `FormDescription`.
@@ -1,6 +1,6 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { format } from 'date-fns'
import { CalendarIcon, ExternalLink, Plus, Trash2, Upload } from 'lucide-react'
import { CalendarIcon, ExternalLink, Plus, Trash, Upload } from 'lucide-react'
import { useRef, useState } from 'react'
import { useFieldArray, useForm } from 'react-hook-form'
import {
@@ -30,6 +30,7 @@ import {
} from 'ui'
import { Input } from 'ui-patterns/DataInputs/Input'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { KeyValueFieldArray } from 'ui-patterns/form/KeyValueFieldArray/KeyValueFieldArray'
import {
MultiSelector,
MultiSelectorContent,
@@ -61,6 +62,7 @@ const formSchema = z.object({
password: z.string().min(8, 'Password must be at least 8 characters'),
duration: z.number().min(5).max(30),
redirectUris: z.array(z.object({ value: z.string().url('Must be a valid URL') })),
httpHeaders: z.array(z.object({ key: z.string(), value: z.string() })),
apiKey: z.string().optional(),
})
@@ -91,6 +93,7 @@ export default function FormPatternsPageLayout() {
password: '',
duration: 10,
redirectUris: [{ value: '' }],
httpHeaders: [{ key: '', value: '' }],
apiKey: fakeApiKey,
},
})
@@ -285,7 +288,7 @@ export default function FormPatternsPageLayout() {
<Button
type="default"
size="tiny"
icon={<Trash2 size={12} />}
icon={<Trash size={12} />}
onClick={() => {
setLogoFile(undefined)
setLogoUrl(undefined)
@@ -389,7 +392,7 @@ export default function FormPatternsPageLayout() {
<Button
type="default"
size="tiny"
icon={<Trash2 size={12} />}
icon={<Trash size={12} />}
onClick={() => {
setUploadedFiles((prev) =>
prev.filter((_, i) => i !== idx)
@@ -669,7 +672,8 @@ export default function FormPatternsPageLayout() {
<Button
type="default"
size="tiny"
icon={<Trash2 size={12} />}
htmlType="button"
icon={<Trash size={12} />}
onClick={() => remove(index)}
/>
)}
@@ -679,6 +683,7 @@ export default function FormPatternsPageLayout() {
))}
<Button
type="default"
htmlType="button"
icon={<Plus />}
onClick={() => append({ value: '' })}
>
@@ -690,6 +695,33 @@ export default function FormPatternsPageLayout() {
/>
</CardContent>
{/* Key/Value Field Array */}
<CardContent>
<FormField_Shadcn_
control={form.control}
name="httpHeaders"
render={() => (
<FormItemLayout
layout="flex-row-reverse"
label="Key/Value Field Array"
description="Repeated text pairs for headers, parameters, and config entries"
>
<KeyValueFieldArray
control={form.control}
name="httpHeaders"
keyFieldName="key"
valueFieldName="value"
createEmptyRow={() => ({ key: '', value: '' })}
keyPlaceholder="Header name"
valuePlaceholder="Header value"
addLabel="Add header"
removeLabel="Remove header"
/>
</FormItemLayout>
)}
/>
</CardContent>
{/* Action Field */}
<CardContent>
<FormItemLayout
@@ -1,6 +1,6 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { format } from 'date-fns'
import { CalendarIcon, ExternalLink, Plus, Trash2, Upload } from 'lucide-react'
import { CalendarIcon, ExternalLink, Plus, Trash, Upload } from 'lucide-react'
import { useRef, useState } from 'react'
import { useFieldArray, useForm } from 'react-hook-form'
import {
@@ -34,6 +34,7 @@ import {
} from 'ui'
import { Input } from 'ui-patterns/DataInputs/Input'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { KeyValueFieldArray } from 'ui-patterns/form/KeyValueFieldArray/KeyValueFieldArray'
import {
MultiSelector,
MultiSelectorContent,
@@ -58,6 +59,7 @@ const formSchema = z.object({
password: z.string().min(8, 'Password must be at least 8 characters'),
duration: z.number().min(5).max(30),
redirectUris: z.array(z.object({ value: z.string().url('Must be a valid URL') })),
httpHeaders: z.array(z.object({ key: z.string(), value: z.string() })),
apiKey: z.string().optional(),
})
@@ -89,6 +91,7 @@ export default function FormPatternsSidePanel() {
password: '',
duration: 10,
redirectUris: [{ value: '' }],
httpHeaders: [{ key: '', value: '' }],
apiKey: fakeApiKey,
},
})
@@ -303,7 +306,7 @@ export default function FormPatternsSidePanel() {
<Button
type="default"
size="tiny"
icon={<Trash2 size={12} />}
icon={<Trash size={12} />}
onClick={() => {
setLogoFile(undefined)
setLogoUrl(undefined)
@@ -409,7 +412,7 @@ export default function FormPatternsSidePanel() {
<Button
type="default"
size="tiny"
icon={<Trash2 size={12} />}
icon={<Trash size={12} />}
onClick={() => {
setUploadedFiles((prev) => prev.filter((_, i) => i !== idx))
}}
@@ -702,7 +705,8 @@ export default function FormPatternsSidePanel() {
<Button
type="default"
size="tiny"
icon={<Trash2 size={12} />}
htmlType="button"
icon={<Trash size={12} />}
onClick={() => remove(index)}
/>
)}
@@ -712,6 +716,7 @@ export default function FormPatternsSidePanel() {
))}
<Button
type="default"
htmlType="button"
icon={<Plus />}
onClick={() => append({ value: '' })}
>
@@ -725,6 +730,37 @@ export default function FormPatternsSidePanel() {
<Separator className="-mx-5 w-[calc(100%+2.5rem)]" />
{/* Key/Value Field Array */}
<SheetSection>
<FormField_Shadcn_
control={form.control}
name="httpHeaders"
render={() => (
<FormItemLayout
layout="horizontal"
label="Key/Value Field Array"
description="Repeated text pairs for headers, parameters, and config entries"
>
<div className="col-span-6">
<KeyValueFieldArray
control={form.control}
name="httpHeaders"
keyFieldName="key"
valueFieldName="value"
createEmptyRow={() => ({ key: '', value: '' })}
keyPlaceholder="Header name"
valuePlaceholder="Header value"
addLabel="Add header"
removeLabel="Remove header"
/>
</div>
</FormItemLayout>
)}
/>
</SheetSection>
<Separator className="-mx-5 w-[calc(100%+2.5rem)]" />
{/* Action Field */}
<SheetSection>
<FormItemLayout
@@ -0,0 +1,57 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'
import { Button, Form_Shadcn_ } from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { KeyValueFieldArray } from 'ui-patterns/form/KeyValueFieldArray/KeyValueFieldArray'
import { z } from 'zod'
const formSchema = z.object({
headers: z.array(
z.object({
name: z.string(),
value: z.string(),
})
),
})
export default function KeyValueFieldArrayDemo() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
headers: [{ name: 'x-client-info', value: 'studio-docs' }],
},
})
function onSubmit(values: z.infer<typeof formSchema>) {
console.log(values)
}
return (
<Form_Shadcn_ {...form}>
<form className="w-full max-w-2xl" onSubmit={form.handleSubmit(onSubmit)}>
<FormItemLayout
label="HTTP headers"
description="Use KeyValueFieldArray for repeated text key/value pairs."
>
<KeyValueFieldArray
control={form.control}
name="headers"
keyFieldName="name"
valueFieldName="value"
createEmptyRow={() => ({ name: '', value: '' })}
keyPlaceholder="Header name"
valuePlaceholder="Header value"
addLabel="Add header"
removeLabel="Remove header"
/>
</FormItemLayout>
<div className="mt-4">
<Button size="tiny" type="primary" htmlType="submit">
Submit
</Button>
</div>
</form>
</Form_Shadcn_>
)
}
+6
View File
@@ -1107,6 +1107,12 @@ export const examples: Registry = [
type: 'components:example',
files: ['example/form-item-layout-demo.tsx'],
},
{
name: 'key-value-field-array-demo',
type: 'components:example',
registryDependencies: ['button', 'form', 'input'],
files: ['example/key-value-field-array-demo.tsx'],
},
{
name: 'field-choice-card',
type: 'components:example',
@@ -0,0 +1,103 @@
import matter from 'gray-matter'
import { cache } from 'react'
const SKILLS_REPO = {
org: 'supabase',
repo: 'agent-skills',
branch: 'main',
path: 'skills',
}
interface SkillMetadata {
name?: string
title?: string
description?: string
}
interface SkillSummary {
name: string
description: string
installCommand: string
}
interface GitHubContentItem {
name: string
path: string
type: 'file' | 'dir'
}
async function fetchGitHubDirectory(path: string): Promise<GitHubContentItem[]> {
const url = `https://api.github.com/repos/${SKILLS_REPO.org}/${SKILLS_REPO.repo}/contents/${path}?ref=${SKILLS_REPO.branch}`
let response: Response
try {
response = await fetch(url, {
headers: {
Accept: 'application/vnd.github.v3+json',
'User-Agent': 'Supabase-Docs',
},
next: { revalidate: 3600 },
})
} catch (err) {
throw new Error(`Failed to fetch agent skills directory from GitHub (network error)`, {
cause: err,
})
}
if (!response.ok) {
throw new Error(
`Failed to fetch agent skills directory from GitHub: ${response.status} ${response.statusText}`
)
}
const data = await response.json()
if (!Array.isArray(data)) {
throw new Error('Expected directory listing from GitHub agent skills repo')
}
return data
}
async function fetchGitHubFile(path: string): Promise<string> {
const url = `https://raw.githubusercontent.com/${SKILLS_REPO.org}/${SKILLS_REPO.repo}/${SKILLS_REPO.branch}/${path}`
let response: Response
try {
response = await fetch(url, {
next: { revalidate: 3600 },
})
} catch (err) {
throw new Error(`Failed to fetch agent skill file from GitHub (network error)`, { cause: err })
}
if (!response.ok) {
throw new Error(
`Failed to fetch agent skill file from GitHub: ${response.status} ${response.statusText}`
)
}
return await response.text()
}
async function getAiSkillsImpl(): Promise<SkillSummary[]> {
const directories = await fetchGitHubDirectory(SKILLS_REPO.path)
const skillDirs = directories.filter((item) => item.type === 'dir')
const skills = await Promise.all(
skillDirs.map(async (item) => {
const skillPath = `${SKILLS_REPO.path}/${item.name}/SKILL.md`
const rawContent = await fetchGitHubFile(skillPath)
const { data } = matter(rawContent) as { data: SkillMetadata }
return {
name: item.name,
description: data.description || '',
installCommand: `npx skills add supabase/agent-skills --skill ${item.name}`,
}
})
)
return skills.sort((a, b) => a.name.localeCompare(b.name))
}
export const getAiSkills = cache(getAiSkillsImpl)
@@ -0,0 +1,60 @@
import { getAiSkills } from './AiSkills.utils'
import { CopyButton } from './CopyButton'
export async function AiSkillsIndex() {
let skills: Awaited<ReturnType<typeof getAiSkills>> = []
try {
skills = await getAiSkills()
} catch {
// Swallow errors from getAiSkills to keep the page usable
}
if (!skills.length) {
return (
<div className="not-prose text-sm text-foreground-lighter">
Unable to load AI skills at the moment.
</div>
)
}
return (
<div className="not-prose overflow-x-auto">
<table className="w-full text-sm border-collapse">
<thead>
<tr className="border-b border-default">
<th className="text-left py-2 pr-4 text-foreground-lighter font-medium">Skill</th>
<th className="text-left py-2 pr-4 text-foreground-lighter font-medium">Description</th>
<th className="text-left py-2 text-foreground-lighter font-medium">Install command</th>
</tr>
</thead>
<tbody>
{skills.map((skill) => (
<tr key={skill.name} className="border-b border-default">
<td className="py-3 pr-4 font-mono text-xs whitespace-nowrap">
<a
href={`https://github.com/supabase/agent-skills/tree/main/skills/${skill.name}`}
target="_blank"
rel="noopener noreferrer"
className="text-foreground hover:text-brand transition-colors"
>
{skill.name}
</a>
</td>
<td className="py-3 pr-4 text-foreground-lighter">{skill.description}</td>
<td className="w-px p-0">
<div className="h-full max-w-xs overflow-x-auto flex items-center py-3">
<div className="flex items-center gap-2 whitespace-nowrap">
<CopyButton text={skill.installCommand} />
<code className="font-mono text-xs text-foreground-lighter">
{skill.installCommand}
</code>
</div>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)
}
@@ -0,0 +1,29 @@
'use client'
import { useState } from 'react'
import { Check, Copy } from 'lucide-react'
import { cn } from 'ui'
export function CopyButton({ text }: { text: string }) {
const [copied, setCopied] = useState(false)
const handleCopy = async () => {
await navigator.clipboard.writeText(text)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
return (
<button
type="button"
onClick={handleCopy}
className={cn(
'inline-flex items-center rounded-md border p-1.5 transition-colors',
'border-default bg-surface-100 text-foreground-lighter hover:bg-surface-200 hover:text-foreground'
)}
title={copied ? 'Copied!' : 'Copy to clipboard'}
>
{copied ? <Check size={14} /> : <Copy size={14} />}
</button>
)
}
@@ -3,7 +3,9 @@ import Layout from '~/layouts/guides'
import { getAiPrompts } from '../getting-started/ai-prompts/[slug]/AiPrompts.utils'
export default async function GettingStartedLayout({ children }: { children: React.ReactNode }) {
const additionalNavItems = { prompts: await getPrompts() }
const additionalNavItems = {
prompts: await getPrompts(),
}
return <Layout additionalNavItems={additionalNavItems}>{children}</Layout>
}
+12
View File
@@ -176,6 +176,18 @@ function useBreadcrumbs() {
return breadcrumbs
}
// TODO: Breadcrumbs currently can't infer the "AI Tools" parent for /guides/getting-started/ai-* routes,
// so we special-case these paths here. Remove when Breadcrumbs can derive this hierarchy from NavigationMenu.
const isAiSkillsPage = pathname.startsWith('/guides/getting-started/ai-skills')
if (isAiSkillsPage) {
const breadcrumbs = [
{ name: 'Getting started', url: '/guides/getting-started' },
{ name: 'AI Tools' },
{ name: 'Agent Skills', url: '/guides/getting-started/ai-skills' },
]
return breadcrumbs
}
const menuId = getMenuId(pathname)
const menu = NavItems[menuId]
return findMenuItemByUrl(menu, pathname, [])
@@ -499,6 +499,10 @@ export const gettingstarted: NavMenuConstant = {
name: 'Prompts',
url: '/guides/getting-started/ai-prompts' as `/${string}`,
},
{
name: 'Agent Skills',
url: '/guides/getting-started/ai-skills' as `/${string}`,
},
{
name: 'Supabase MCP server',
url: '/guides/getting-started/mcp' as `/${string}`,
@@ -773,6 +777,12 @@ export const auth: NavMenuConstant = {
],
},
{
name: 'Custom OAuth/OIDC Providers',
url: '/guides/auth/custom-oauth-providers',
enabled: allAuthProvidersEnabled,
},
{
name: 'Anonymous Sign-Ins',
url: '/guides/auth/auth-anonymous',
@@ -6,6 +6,12 @@ Supabase Auth works with many popular Auth methods, including Social and Phone A
<AuthProviders type="social" />
<Admonition type="tip">
You can also add any OAuth2 or OIDC-compatible identity provider using [Custom OAuth/OIDC Providers](/docs/guides/auth/custom-oauth-providers).
</Admonition>
### Phone Auth
<AuthProviders type="phone" />
@@ -0,0 +1,320 @@
---
id: 'custom-oauth-providers'
title: 'Custom OAuth/OIDC Providers'
description: 'Add any OAuth2 or OIDC-compatible identity provider to your Supabase project'
---
Custom OAuth/OIDC providers let you integrate any standards-compliant identity provider with Supabase Auth, beyond the ones Supabase supports out of the box.
Each custom provider uses a `custom:` prefix in its identifier (for example, `custom:my-idp` or `custom:github-enterprise`). This prefix distinguishes custom providers from built-in providers.
There are two provider types:
- **OAuth2**: for generic OAuth2 providers where you supply the authorization, token, and userinfo endpoints manually.
- **OIDC**: for providers that support [OpenID Connect](https://openid.net/connect/) discovery. You supply only the issuer URL and endpoints are resolved automatically.
<Admonition type="note">
You can add up to 3 custom providers per project. If you need more, [contact support](/dashboard/support/new).
</Admonition>
## Creating a provider
The create form displays a read-only **Callback URL**. Copy this URL and configure it as the redirect/callback URI in your external identity provider before completing setup.
### OAuth2 provider
Use an OAuth2 provider when your identity provider does not support OpenID Connect discovery. You must supply the authorization, token, and userinfo endpoint URLs explicitly.
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="dashboard"
queryGroup="setup-method"
>
<TabPanel id="dashboard" label="Dashboard">
1. Go to [Auth Providers](/dashboard/project/_/auth/providers) in the Dashboard.
2. Click **New Provider**. Select **Manual configuration** as the configuration method.
3. Enter a unique identifier (must start with `custom:`, for example `custom:my-oauth-provider`).
4. Enter the provider's **Client ID** and **Client Secret**.
5. Enter the **Authorization URL**, **Token URL**, and **UserInfo URL**.
6. Click **Create and enable provider**.
</TabPanel>
<TabPanel id="js" label="JavaScript">
```js
const { data, error } = await supabase.auth.admin.customProviders.createProvider({
provider_type: 'oauth2',
identifier: 'custom:my-oauth-provider',
name: 'My OAuth Provider',
client_id: 'your-client-id',
client_secret: 'your-client-secret',
authorization_url: 'https://provider.example.com/oauth/authorize',
token_url: 'https://provider.example.com/oauth/token',
userinfo_url: 'https://provider.example.com/oauth/userinfo',
scopes: ['profile', 'email'],
})
```
</TabPanel>
</Tabs>
### OIDC provider
Use an OIDC provider when your identity provider supports OpenID Connect. Supply the `issuer` URL and the discovery document, JWKS, and endpoints are resolved automatically.
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="dashboard"
queryGroup="setup-method"
>
<TabPanel id="dashboard" label="Dashboard">
1. Go to [Auth Providers](/dashboard/project/_/auth/providers) in the Dashboard.
2. Click **New Provider**. Select **Auto-discovery (OIDC)** as the configuration method.
3. Enter a unique identifier (must start with `custom:`, for example `custom:my-regional-provider`).
4. Enter the provider's **Client ID** and **Client Secret**.
5. Enter the **Issuer URL**. The discovery document and endpoints are resolved automatically.
6. Click **Create and enable provider**.
</TabPanel>
<TabPanel id="js" label="JavaScript">
```js
const { data, error } = await supabase.auth.admin.customProviders.createProvider({
provider_type: 'oidc',
identifier: 'custom:my-regional-provider',
name: 'Regional Provider',
client_id: 'your-client-id',
client_secret: 'your-client-secret',
issuer: 'https://auth.example.com',
scopes: ['openid', 'profile', 'email'],
})
```
</TabPanel>
</Tabs>
OIDC providers have the following automatic behavior:
- The discovery document is fetched from `{issuer}/.well-known/openid-configuration` (or from `discovery_url` if set).
- The `openid` scope is always included. It is automatically added if missing from the `scopes` array.
- ID tokens are verified against the provider's JWKS (fetched from the discovery document's `jwks_uri`).
## Provider identifiers
Every custom provider identifier must start with the `custom:` prefix. Identifiers are 2–50 characters, lowercase alphanumeric with hyphens and colons allowed. Examples:
- `custom:my-provider`
- `custom:github-enterprise`
## User sign-in
Once a custom provider is created and enabled, users sign in via the standard OAuth authorize endpoint:
```
GET https://your-project.supabase.co/auth/v1/authorize?provider=custom:my-provider
```
Or using the Supabase client libraries:
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
queryGroup="language"
>
<TabPanel id="js" label="JavaScript">
```js
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'custom:my-provider',
})
```
</TabPanel>
<TabPanel id="flutter" label="Flutter">
```dart
await supabase.auth.signInWithOAuth(
OAuthProvider('custom:my-provider'),
);
```
</TabPanel>
<TabPanel id="swift" label="Swift">
```swift
try await supabase.auth.signInWithOAuth(
provider: "custom:my-provider",
redirectTo: URL(string: "my-custom-scheme://my-app-host")
)
```
</TabPanel>
<TabPanel id="kotlin" label="Kotlin">
```kotlin
supabase.auth.signInWith(CustomProvider("custom:my-provider"))
```
</TabPanel>
</Tabs>
## Managing providers
### List providers
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="dashboard"
queryGroup="setup-method"
>
<TabPanel id="dashboard" label="Dashboard">
Go to [Auth Providers](/dashboard/project/_/auth/providers) in the Dashboard. All custom providers are listed under **Custom OAuth Providers**.
</TabPanel>
<TabPanel id="js" label="JavaScript">
```js
// List all custom providers
const { data, error } = await supabase.auth.admin.customProviders.listProviders()
// Filter by provider type
const { data, error } = await supabase.auth.admin.customProviders.listProviders({
type: 'oidc',
})
```
</TabPanel>
</Tabs>
### Update a provider
Update any provider fields except `provider_type` and `identifier`. Only provided fields are changed (partial update). To rotate a client secret, update only the `client_secret` field.
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="dashboard"
queryGroup="setup-method"
>
<TabPanel id="dashboard" label="Dashboard">
1. Go to [Auth Providers](/dashboard/project/_/auth/providers) in the Dashboard.
2. Click the three-dot menu (⋮) next to the provider and select **Update**.
3. Modify the fields you want to change.
4. Click **Update provider**.
</TabPanel>
<TabPanel id="js" label="JavaScript">
```js
const { data, error } = await supabase.auth.admin.customProviders.updateProvider(
'custom:my-provider',
{
name: 'Updated Provider Name',
scopes: ['profile', 'email', 'groups'],
enabled: false,
}
)
```
</TabPanel>
</Tabs>
### Delete a provider
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="dashboard"
queryGroup="setup-method"
>
<TabPanel id="dashboard" label="Dashboard">
1. Go to [Auth Providers](/dashboard/project/_/auth/providers) in the Dashboard.
2. Click the three-dot menu (⋮) next to the provider and select **Delete**.
3. Confirm the deletion.
</TabPanel>
<TabPanel id="js" label="JavaScript">
```js
const { data, error } =
await supabase.auth.admin.customProviders.deleteProvider('custom:my-provider')
```
</TabPanel>
</Tabs>
## Advanced configuration
### PKCE
PKCE (Proof Key for Code Exchange) is enabled by default (`pkce_enabled: true`) for all custom providers. The auth server automatically generates a code challenge and verifier during the authorization flow, protecting against authorization code interception attacks. This is handled entirely server-side, no client-side PKCE logic is needed.
To disable PKCE for a specific provider, set `pkce_enabled: false` when creating or updating it. This is not recommended unless the identity provider does not support PKCE.
### Authorization params
Extra query parameters appended to the provider's authorization URL during the OAuth flow. All values must be strings.
```json
{
"prompt": "consent",
"access_type": "offline",
"login_hint": "user@example.com"
}
```
The following reserved parameters are managed by the auth server and cannot be overridden: `client_id`, `client_secret`, `redirect_uri`, `response_type`, `state`, `code_challenge`, `code_challenge_method`, `code_verifier`, `nonce`.
### Multi-platform apps
If your app uses different client IDs for different platforms (for example, web vs mobile), use `acceptable_client_ids` to list additional client IDs that should be accepted for audience validation in OIDC ID tokens:
```js
const { data, error } = await supabase.auth.admin.customProviders.createProvider({
provider_type: 'oidc',
identifier: 'custom:multi-platform-app',
name: 'Multi-Platform App',
client_id: 'web-client-id',
client_secret: 'your-client-secret',
issuer: 'https://app.example.com',
scopes: ['openid', 'profile', 'email'],
acceptable_client_ids: ['ios-client-id', 'android-client-id'],
})
```
### Email-optional providers
By default, providers must return an email address. Set `email_optional` to `true` when creating or updating a provider to allow sign-in without an email. This applies to both OAuth2 and OIDC providers.
### OIDC-specific options
| Field | Type | Default | Description |
| ------------------ | -------- | ------- | ------------------------------------------------------------------------------------- |
| `discovery_url` | `string` | `null` | Override the discovery document URL if the provider uses a non-standard location. |
| `skip_nonce_check` | `bool` | `false` | Skip nonce validation on ID tokens. Use only for providers that do not support nonce. |
## Error reference
| Error code | HTTP status | Description |
| ---------------------------- | ----------- | ------------------------------------------------------------------------------------------ |
| `validation_failed` | 400 | Invalid parameters: missing required fields, bad format, reserved params, or invalid URLs. |
| `conflict` | 400 | A provider with the same identifier already exists. |
| `over_custom_provider_quota` | 400 | Maximum number of custom providers reached. |
| `custom_provider_not_found` | 404 | No provider exists with the given identifier. |
@@ -97,7 +97,7 @@ With Deep Linking, you can configure this redirect to open a specific page. This
export default function Auth() {
// Handle linking into app from email app.
const url = Linking.useURL();
const url = Linking.useLinkingURL();
if (url) createSessionFromUrl(url);
return (
@@ -45,6 +45,12 @@ Supabase supports a suite of social providers. Follow these guides to configure
</NavData>
</div>
<Admonition type="tip">
Need to integrate with a provider not listed here? You can add any OAuth2 or OIDC-compatible provider using [Custom OAuth/OIDC Providers](/docs/guides/auth/custom-oauth-providers).
</Admonition>
## Provider tokens
You can use the provider token and provider refresh token returned to make API calls to the OAuth provider. For example, you can use the Google provider token to access Google APIs on behalf of your user.
@@ -0,0 +1,48 @@
---
title: Agent Skills
---
Agent Skills are folders of instructions, scripts, and resources that agents can discover and use to do things more accurately and efficiently. Agents are increasingly capable, but often don't have the context they need to do real work reliably. Skills solve this by giving agents access to procedural knowledge and company-, team-, and user-specific context they can load on demand. Agents with access to a set of skills can extend their capabilities based on the task they're working on.
## Installing skills
Install all Supabase skills using the skills CLI:
```bash
npx skills add supabase/agent-skills
```
To install a specific skill from the repository:
```bash
npx skills add supabase/agent-skills --skill SKILL_NAME
```
### Claude Code plugin
You can also install the skills as Claude Code plugins:
```bash
/plugin marketplace add supabase/agent-skills
/plugin install postgres-best-practices@supabase-agent-skills
```
Skills work with 18+ AI agents including Claude Code, GitHub Copilot, Cursor, Cline, and many others.
## Available skills
<AiSkillsIndex />
## Finding more skills
Browse the [skills.sh directory](https://skills.sh) to discover skills from the community. You can also search for skills using the CLI:
```bash
npx skills find QUERY
```
## Learn more
- [Agent Skills Repository](https://github.com/supabase/agent-skills)
- [Agent Skills Documentation](https://agentskills.io/home)
- [Agent Skills Overview](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview)
@@ -162,16 +162,16 @@ meta="name=src/routes/+page.server.ts"
Change the email template to support a server-side authentication flow.
Before we proceed, let's change the email template to support sending a token hash:
Before proceeding, change the email template to support sending a token hash:
- Go to the [**Auth** > **Emails**](/dashboard/project/_/auth/templates) page in the project dashboard.
- Select the **Confirm signup** template.
- Change `{{ .ConfirmationURL }}` to `{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=email`.
- Repeat the previous step for **Magic link** template.
<Admonition type="tip">
<Admonition type="tip" label="Did you know?">
**Did you know?** You can also customize emails sent out to new users, including the email's looks, content, and query parameters. Check out the [settings of your project](/dashboard/project/_/auth/templates).
You can also customize emails sent out to new users, including the email's looks, content, and query parameters. Check out the [settings of your project](/dashboard/project/_/auth/templates).
</Admonition>
@@ -140,7 +140,7 @@ The Fair Use Policy is generally applied to all projects of the restricted organ
To remove restrictions, you will need to address the issue that caused the restriction. This could be reducing your usage, paying overdue invoices, updating your payment method, or any other issue that caused the restriction. Once the issue is resolved, the restriction will be lifted.
Restrictions due to usage limits are lifted with the next billing cycle as your quota refills at the beginning of each cycle. You can see when your current billing cycle ends on the [billing page](/dashboard/org/_/billing) under "Upcoming Invoice". You can also lift restrictions immediately by [upgrading](/dashboard/org/_/billing?panel=subscriptionPlan) to Pro (if on Free Plan) or by [disabling spend cap](/dashboard/org/_/billing?panel=costControl) (if on Pro Plan with spend cap enabled).
Restrictions due to usage limits are lifted once your quota refills at the start of the next billing cycle. Note that there may be a short delay after your billing period resets before restrictions are fully lifted. You can see when your current billing cycle ends on the [billing page](/dashboard/org/_/billing) under "Upcoming Invoice". You can also lift restrictions immediately by [upgrading](/dashboard/org/_/billing?panel=subscriptionPlan) to Pro (if on Free Plan) or by [disabling spend cap](/dashboard/org/_/billing?panel=costControl) (if on Pro Plan with spend cap enabled).
## Reports and invoices
@@ -60,7 +60,7 @@ height={625}
| Action | Description | More information |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Configure connection limit | Adjust the "Max concurrent connections" setting to increase or decrease the connection limit for your project | [Realtime Settings Guide](/docs/guides/realtime/settings) |
| Upgrade plan | Increase available client connections. Connection limits vary by plan: Free (200), Pro (500), Pro no spend cap (10,000), Team (10,000), Enterprise (10,000+) | [Pricing and Plans](/docs/guides/platform/pricing) |
| Upgrade plan | Increase available client connections. Connection limits vary by plan: Free (200), Pro (500), Pro no spend cap (10,000), Team (10,000), Enterprise (10,000+) | [Pricing and Plans](/pricing) |
| Review quotas | Understand connection limits and other Realtime quotas for your plan | [Realtime Quotas Reference](/docs/guides/realtime/quotas) |
| Understand connection quota | Learn how the concurrent connections quota works and how to configure it for your plan | [Concurrent Peak Connections Quota Troubleshooting](/docs/troubleshooting/realtime-concurrent-peak-connections-quota-jdDqcp) |
| Fix silent disconnections | Fix connection issues in background applications using heartbeat callbacks and Web Workers | [Handling Silent Disconnections in Background Apps](/docs/troubleshooting/realtime-handling-silent-disconnections-in-backgrounded-applications-592794) |
@@ -1,93 +1,139 @@
---
title = "Edge Function 401 error response"
topics = [ "functions" ]
keywords = [ "401", "error" ]
database_id = "12e09c3f-a48b-4aea-9df3-90a25ef0021e"
keywords = ["401", "error", "JWT", "authorization"]
[[errors]]
http_status_code = 401
message = "Invalid JWT"
---
The function rejected the request for lacking the appropriate authorization headers.
A 401 response from an Edge Function means either:
## Context for the error
- The function failed the [legacy auth verification check](/docs/guides/functions/development-tips#skipping-authorization-checks)
- Your function's logic deliberately returned a 401 response
#### The JWT verification check
## Quick triage
By default, edge functions are configured to check requests for a valid [legacy key](/docs/guides/api/api-keys#overview).
Check the response body returned by the request
#### How the check causes 401 errors
The check fails and return a 401 permission error if:
- The request includes an invalid legacy key, or
- The request uses the newer asymmetric keys instead of a legacy key.
#### What this check actually does
This validation provides limited security value. It only confirms that the request includes a legacy token associated with your project, such as the anon key.
It does not validate the user's identity or permissions beyond that.
Because the security check is weak, it was deprecated when used with the new asymmetric keys.
## Solving the error
### Step 1: Identifying the error
<Admonition type="note">
If the tests return a 401 but don't match the criteria below, the error is coming from your app logic, not the JWT check.
</Admonition>
### Inspecting the return message
When an edge function fails due to a platform 401 error, it returns the error:
### Case 1: `"Invalid JWT"` or `"Missing authorization header"`
```json
{
"code": 401,
"message": "Invalid JWT"
}
{ "code": 401, "message": "Invalid JWT" }
```
### Inspecting the logs
```json
{ "code": 401, "message": "Missing authorization header" }
```
Your code may return a 401 error due to its own logic
Both of these messages come from the [legacy auth verification check](/docs/guides/functions/development-tips#skipping-authorization-checks)
Go to: [Built-in JWT check failures](#built-in-jwt-check-failures)
### Case 2: Custom message or empty body
If the response body contains a message you coded, or nothing at all, then your function code _did_ execute and returned a 401 itself.
Go to: [Your function returned a 401](#your-function-returned-a-401)
### Case 3: Not sure
Run this query in [Log Explorer](/dashboard/project/_/logs/explorer?q=SELECT%0A++++cast%28timestamp+AS+datetime%29++AS+timestamp%2C%0A++++req.pathname+++++++++++++++++AS+function_name%2C%0A%0A++++CASE%0A++++++++WHEN+metadata.execution_id+IS+NOT+NULL%0A++++++++++++THEN+%27your_code_returned_401%27%0A++++++++WHEN+metadata.execution_id+IS+NULL%0A+++++++++AND+%28new_auth.prefix+IS+NOT+NULL+OR+legacy_payload.algorithm+<>+%27HS256%27%29%0A++++++++++++THEN+%27incompatible_keys%27%0A++++++++WHEN+metadata.execution_id+IS+NULL%0A++++++++AND+%0A++++++++++++%28%0A++++++++++++++++%28legacy_auth_data.invalid+IS+NOT+NULL+OR+new_auth.error+IS+NOT+NULL%29%0A++++++++++++++++++++OR%0A++++++++++++++++legacy_payload.algorithm+%3D+%27HS256%27%0A++++++++++++%29%0A++++++++++++THEN+%27invalid_key%27%0A++++++++WHEN+metadata.execution_id+IS+NULL%0A+++++++++AND+legacy_auth_data+++++++IS+NULL%0A+++++++++AND+new_auth.prefix+IS+NULL%0A++++++++++++THEN+%27missing_auth_header%27%0A++++END+AS+cause%0A%0AFROM+function_edge_logs%0A%0A++++--+unnesting+metadata%0A++++CROSS+JOIN+UNNEST%28metadata%29++++++++++AS+metadata%0A++++CROSS+JOIN+UNNEST%28metadata.request%29++AS+req%0A++++CROSS+JOIN+UNNEST%28metadata.response%29+AS+res%0A++++--+unnesting+auth+details%0A++++LEFT+JOIN+UNNEST%28req.sb%29++++++++++++++++++++AS+sb%0A++++LEFT+JOIN+UNNEST%28sb.apikey%29+++++++++++++++++AS+apikey%0A++++LEFT+JOIN+UNNEST%28apikey.authorization%29++++++AS+new_auth%0A++++LEFT+JOIN+UNNEST%28sb.jwt%29++++++++++++++++++++AS+legacy_jwt%0A++++LEFT+JOIN+UNNEST%28legacy_jwt.authorization%29++AS+legacy_auth_data%0A++++LEFT+JOIN+UNNEST%28legacy_auth_data.payload%29++AS+legacy_payload%0A%0AWHERE+res.status_code+%3D+401%0AORDER+BY+timestamp+DESC%0ALIMIT+200) to classify recent 401s:
```sql
select
cast(timestamp as datetime) as timestamp,
req.pathname as function_name,
case
when metadata.execution_id is not null then 'your_code_returned_401'
when metadata.execution_id is null
and (
new_auth.prefix is not null
or legacy_payload.algorithm != 'HS256'
) then 'incompatible_keys'
when metadata.execution_id is null
and (
(legacy_auth_data.invalid is not null or new_auth.error is not null)
or legacy_payload.algorithm = 'HS256'
) then 'invalid_key'
when metadata.execution_id is null
and legacy_auth_data is null
and new_auth.prefix is null then 'missing_auth_header'
end as cause
from
function_edge_logs
-- unnesting metadata
cross join UNNEST(metadata) as metadata
cross join UNNEST(metadata.request) as req
cross join UNNEST(metadata.response) as res
-- unnesting auth details
left join UNNEST(req.sb) as sb
left join UNNEST(sb.apikey) as apikey
left join UNNEST(apikey.authorization) as new_auth
left join UNNEST(sb.jwt) as legacy_jwt
left join UNNEST(legacy_jwt.authorization) as legacy_auth_data
left join UNNEST(legacy_auth_data.payload) as legacy_payload
where res.status_code = 401
order by timestamp desc
limit 50;
```
Depending on the output, you can use this table to find the appropriate debugging section:
| Value | Go to |
| ------------------------ | ------------------------------------------------------------- |
| `your_code_returned_401` | [Your function returned a 401](#your-function-returned-a-401) |
| `incompatible_keys` | [Incompatible key format](#incompatible-key-format) |
| `invalid_key` | [Invalid key](#invalid-key) |
| `missing_auth_header` | [Missing Authorization header](#missing-authorization-header) |
---
## Your function returned a 401
Your function ran, and somewhere in your code, its logic returned a 401.
**Example:**
```js
return new Response(JSON.stringify(data), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 401, // app logic returning a 401
status: 401, // <-- you set this
})
```
A 401 status code alone doesn't confirm a JWT check failure by itself. The log must also lack an `execution_id`.
**How to fix:**
![image](/docs/img/troubleshooting/401_in_the_logs.png)
1. Search your function code for `401`. Look for explicit status codes on `Response` objects.
2. Trace the condition that triggered it. If you're interacting with a third-party API in your code, that service may be returning 401 that you're forwarding in the response object.
3. Add logging before the return so future occurrences leave a trace:
Instead of manually reviewing the logs, you can run the below query in the [log explorer](</dashboard/project/_/logs/explorer?q=SELECT+DISTINCT%0A++++req.pathname+AS+function_name,%0A++++res.status_code%0A++FROM+function_edge_logs%0A++CROSS+JOIN+UNNEST(metadata)+AS+metadata+%0A++CROSS+JOIN+UNNEST(metadata.request)+AS+req+%0A++CROSS+JOIN+UNNEST(metadata.response)+AS+res+%0AWHERE+status_code+=+401+AND+metadata.execution_id+IS+NULL%0ALIMIT+10;&its=&ite=%3E>) to get a list of functions that were impacted by the check:
```sql
select distinct
req.pathname as function_name,
res.status_code
from
function_edge_logs
cross join UNNEST(metadata) as metadata
cross join UNNEST(metadata.request) as req
cross join UNNEST(metadata.response) as res
where status_code = 401 and metadata.execution_id is null
limit 10;
```js
console.error('Returning 401 - reason:', reason)
```
### Step 2: Disabling the JWT check
See: [Error handling in Edge Functions](/docs/guides/functions/error-handling)
The JWT check provides minimal security benefits, so we now recommend handling authentication through app logic instead. See the [Edge Function Auth Doc](/docs/guides/functions/auth) for details.
---
If you've migrated to [asymmetric keys (publishable/secret)](/dashboard/project/_/settings/api-keys) or no longer need the JWT check, you can disable it using one of the three options below:
## Built-in JWT check failures
Supabase Edge Functions have a legacy auth verification check that runs before your code. When it fails, your function never executes, and you get a 401 with `"Invalid JWT"` or `"Missing authorization header"` directly from the platform.
<Admonition type="deprecation">
Supabase now recommends turning off this built-in check and managing authentication directly in your function code, giving you more control over access. See [Securing Edge Functions](/docs/guides/functions/auth).
</Admonition>
The subsections below cover specific failure modes.
### Incompatible key format
Your project uses the [new asymmetric keys](/blog/jwt-signing-keys) for authentication. However, the [legacy auth verification check](/docs/guides/functions/development-tips#skipping-authorization-checks) only understands the legacy format.
**Fix:** Disable the built-in JWT check using one of the below methods and optionally [handle auth in your function code](/docs/guides/functions/auth)
<Accordion
type="default"
@@ -99,51 +145,49 @@ If you've migrated to [asymmetric keys (publishable/secret)](/dashboard/project/
>
<div className="border-b mt-3 pb-3">
<AccordionItem
header="In the Dashboard"
header="Method A: Dashboard"
id="item-1"
>
In the [Function Dashboard](/dashboard/project/_/functions/), under the offending function's detail tab, you can toggle off the check:
![image](/docs/img/troubleshooting/401_edge_functions_toggle_off_JWT_check.png)
In the [Functions Dashboard](/dashboard/project/_/functions/), open the affected function's `detail tab` and toggle off JWT verification.
![image](/docs/img/troubleshooting/401_edge_functions_toggle_off_JWT_check.png)
</AccordionItem>
</div>
<div className="border-b mt-3 pb-3">
<AccordionItem
header="Method B: Supabase CLI"
id="item-2"
>
Redeploy the edge function from the [Supabase CLI](/docs/guides/functions/quickstart) with the `--no-verify-jwt` flag
```sh
supabase functions deploy YOUR_FUNCTION_NAME --no-verify-jwt
```
</AccordionItem>
</div>
<div className="border-b mt-3 pb-3">
<AccordionItem
header="With the Management API"
id="item-2"
>
1. Generate a secret token in the [Account Preferences Dashboard](/dashboard/account/tokens)
2. Get your project ID from the [General Settings](/dashboard/project/_/settings/general)
3. Substitute in the `PROJECT_ID`, `FUNCTION_NAME`, and `SECRET_TOKEN` in below [management endpoint](/docs/reference/api/v1-update-a-function). Then execute the request from a terminal environment
```sh
# remember to substitute in your PROJECT_ID, FUNCTION_NAME, and SECRET_TOKEN
curl 'https://api.supabase.com/v1/projects/PROJECT_ID/functions/FUNCTION_NAME?verify_jwt=false' \
--request PATCH \
--header 'Content-Type: application/vnd.denoland.eszip' \
--header 'Authorization: Bearer YOUR_SECRET_TOKEN' \
--data '""'
````
</AccordionItem>
</div>
<div className="border-b mt-3 pb-3">
<AccordionItem
header="With the Supabase CLI"
header="Method C: Management API"
id="item-3"
>
Disable the legacy auth check with the [Supabase Management API](/docs/reference/api/introduction):
With the [Supabase CLI](/docs/guides/local-development/cli/getting-started), you can redeploy your edge function without the JWT check:
1. Generate a token at [Account Preferences](/dashboard/account/tokens).
2. Get your project ID from [General Settings](/dashboard/project/_/settings/general).
3. Run:
```sh
supabase functions deploy hello-world --no-verify-jwt
curl 'https://api.supabase.com/v1/projects/PROJECT_ID/functions/FUNCTION_NAME' \
--request PATCH \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_SECRET_TOKEN' \
--data '{"verify_jwt": false}'
```
</AccordionItem>
@@ -151,12 +195,43 @@ supabase functions deploy hello-world --no-verify-jwt
</div>
</Accordion>
If you would like to continue using the JWT check, make sure that your [Supabase Client](/docs/guides/api/rest/client-libs) only uses your [legacy keys](/dashboard/project/_/settings/api-keys/legacy).
### Invalid key
The built-in check is enabled and the key you sent doesn't match your project's keys.
**Fix (recommended):** Disable the built-in check using the steps in [Incompatible key format](#incompatible-key-format).
**Fix (alternative):** If you want to keep the built-in check, ensure you're sending a valid key. Use one of your [legacy API keys](/dashboard/project/_/settings/api-keys/legacy) with the [Supabase client library](/docs/guides/api/rest/client-libs) when making your request.
```js
const supabase = createClient('https://xyzcompany.supabase.co', 'anon-key-or-service_role-key')
```
### Missing authorization header
The built-in check is enabled but your request has no `Authorization` header at all.
If you're using a [Supabase client library](/docs/guides/api/rest/client-libs), the header is added automatically. If you're calling the function from an external client (cURL, fetch, etc.), you need to supply it:
```sh
curl -L -X POST 'https://PROJECT_REF.supabase.co/functions/v1/hello-world' \
-H 'Authorization: Bearer YOUR_ANON_OR_SERVICE_ROLE_KEY' \
--data '{"name":"Functions"}'
```
Alternatively, you can disable the built-in check entirely (see [Incompatible key format](#incompatible-key-format)).
---
## Additional resources
- [Securing Edge Functions](/docs/guides/functions/auth)
- [Debugging Edge Functions](/docs/guides/functions/logging)
- [Quickstart Deployment: Dashboard](/docs/guides/functions/quickstart-dashboard)
- [Quickstart Deployment: CLI](/docs/guides/functions/quickstart)
````
- [Securing Edge Functions with Auth](/docs/guides/functions/auth)
- [Logging Edge Function Requests](/docs/guides/functions/logging)
- [Error Handling Edge Functions](/docs/guides/functions/error-handling)
- [Quickstart: Dashboard deployment](/docs/guides/functions/quickstart-dashboard)
- [Quickstart: CLI deployment](/docs/guides/functions/quickstart)
## Still stuck?
- Check the [Discord](https://discord.com/channels/839993398554656828/1006358244786196510), [Supabase GitHub Discussions](https://github.com/orgs/supabase/discussions), and [Reddit page](https://www.reddit.com/r/Supabase/) for similar reports that can help with debugging
- Open a [support ticket](/dashboard/support/new) for your project if the problem persists and you believe it is a platform issue
@@ -7,6 +7,7 @@ import { IconPanel } from 'ui-patterns/IconPanel'
import SqlToRest from 'ui-patterns/SqlToRest'
import { Heading } from 'ui/src/components/CustomHTMLElements'
import { AiPromptsIndex } from '~/app/guides/getting-started/ai-prompts/[slug]/AiPromptsIndex'
import { AiSkillsIndex } from '~/app/guides/getting-started/ai-skills/AiSkillsIndex'
import { AppleSecretGenerator } from '~/components/AppleSecretGenerator'
import AuthProviders from '~/components/AuthProviders'
import { AuthSmsProviderConfig } from '~/components/AuthSmsProviderConfig'
@@ -44,6 +45,7 @@ const components = {
AccordionItem,
Admonition: AdmonitionWithMargin,
AiPromptsIndex,
AiSkillsIndex,
AuthSmsProviderConfig,
AppleSecretGenerator,
AuthProviders,
+34 -9
View File
@@ -265,15 +265,35 @@ function normalizeComment(original: TypedocComment | Comment | undefined): Comme
if (t.name) {
return [t.name, t.content.map((p) => p.text).join('')]
}
const first = t.content[0]
// TypeDoc may produce a tag with an empty content array (e.g. a tag with
// no body text). Guard here so the rest of the parsing can safely assume
// a non-empty first element.
if (!first) return ['', '']
const newline = first.text.indexOf('\n')
const name = (newline >= 0 ? first.text.slice(0, newline) : first.text).trim()
const tail = newline >= 0 ? first.text.slice(newline + 1) : ''
const body = [{ ...first, text: tail }, ...t.content.slice(1)].map((p) => p.text).join('')
if (!t.content[0]) return ['', '']
// The tag name may span multiple content nodes (e.g. "With " + "`select()`"
// when the name contains inline code). Scan through nodes until a newline.
const nameParts: string[] = []
let bodyStartIdx = 0
let bodyStartText = ''
let foundNewline = false
for (let i = 0; i < t.content.length; i++) {
const part = t.content[i]
const newline = part.text.indexOf('\n')
if (newline >= 0) {
nameParts.push(part.text.slice(0, newline))
bodyStartIdx = i
bodyStartText = part.text.slice(newline + 1)
foundNewline = true
break
} else {
nameParts.push(part.text)
}
}
const name = nameParts.join('').trim()
const body = foundNewline
? [{ text: bodyStartText }, ...t.content.slice(bodyStartIdx + 1)]
.map((p) => p.text)
.join('')
: ''
return [name, body]
}
@@ -539,9 +559,14 @@ function parseMethod(
let { params, ret, comment } = parseSignature(signature, map)
// When a method has multiple overload signatures, TypeDoc places the shared
// JSDoc on the method node rather than any individual signature.
if (!comment && node.comment) {
comment = normalizeComment(node.comment)
// JSDoc on the method node rather than any individual signature. Always merge
// node.comment as the base so that block tags (@remarks, @example, etc.) are
// not lost when overload signatures already carry a minimal summary comment.
if (node.comment) {
const nodeComment = normalizeComment(node.comment)
if (nodeComment) {
comment = { ...nodeComment, ...comment }
}
}
const types: MethodTypes = {
File diff suppressed because it is too large. Load diff
+1
View File
@@ -51,6 +51,7 @@ Chris Martin
Chris Stockton
Chris Ward
Clayton Kast
Colum Ferry
Craig Cannon
Cuong Do
Daniel Velázquez Lara
+41 -2
View File
@@ -4565,6 +4565,44 @@
"x-endpoint-owners": ["security"]
}
},
"/v1/projects/{ref}/database/openapi": {
"get": {
"description": "Returns the PostgREST OpenAPI specification for the project. This is the replacement for querying `/rest/v1/` directly with the anon key.",
"operationId": "v1-get-database-openapi",
"parameters": [
{
"name": "ref",
"required": true,
"in": "path",
"description": "Project ref",
"schema": { "minLength": 20, "maxLength": 20, "pattern": "^[a-z]+$", "type": "string" }
},
{
"name": "schema",
"required": false,
"in": "query",
"description": "The database schema to generate the OpenAPI spec for",
"schema": { "default": "public", "type": "string" }
}
],
"responses": {
"200": {
"description": "",
"content": { "application/json": { "schema": { "type": "object" } } }
},
"401": { "description": "Unauthorized" },
"403": { "description": "Forbidden action" },
"429": { "description": "Rate limit exceeded" },
"500": { "description": "Failed to fetch PostgREST OpenAPI spec" }
},
"security": [{ "bearer": [] }, { "fga_permissions": ["database_read"] }],
"summary": "Get PostgREST OpenAPI spec",
"tags": ["Database"],
"x-badges": [{ "name": "OAuth scope: database:read", "position": "after" }],
"x-endpoint-owners": ["management-api"],
"x-oauth-scope": "database:read"
}
},
"/v1/projects/{ref}/functions": {
"get": {
"description": "Returns all functions you've previously added to the specified project.",
@@ -10814,9 +10852,10 @@
"type": "object",
"properties": {
"name": { "type": "string" },
"status": { "type": "string", "enum": ["AVAILABLE", "PENDING", "REMOVED", "FAILED"] }
"status": { "type": "string", "enum": ["AVAILABLE", "PENDING", "REMOVED", "FAILED"] },
"completed_on": { "type": "string", "format": "date-time", "nullable": true }
},
"required": ["name", "status"]
"required": ["name", "status", "completed_on"]
},
"V1UndoBody": {
"type": "object",
+1 -1
View File
@@ -78,7 +78,7 @@ flags:
name: --workdir <string>
description: path to a Supabase project directory
default_value: ''
- id: "yes"
- id: 'yes'
name: --yes
description: answer yes to all prompts
default_value: 'false'
+6
View File
@@ -262,6 +262,12 @@
"slug": "v1-get-database-metadata",
"type": "operation"
},
{
"id": "v1-get-database-openapi",
"title": "Get database openapi",
"slug": "v1-get-database-openapi",
"type": "operation"
},
{
"id": "v1-get-jit-access",
"title": "Get jit access",
File diff suppressed because it is too large. Load diff
@@ -23,7 +23,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 88,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L88"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L88"
}
],
"type": {
@@ -42,7 +42,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 89,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L89"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L89"
}
],
"type": {
@@ -61,7 +61,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 90,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L90"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L90"
}
],
"type": {
@@ -80,7 +80,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 91,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L91"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L91"
}
],
"type": {
@@ -99,7 +99,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 92,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L92"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L92"
}
],
"type": {
@@ -118,7 +118,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 93,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L93"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L93"
}
],
"type": {
@@ -137,7 +137,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 94,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L94"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L94"
}
],
"type": {
@@ -156,7 +156,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 95,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L95"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L95"
}
],
"type": {
@@ -175,7 +175,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 96,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L96"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L96"
}
],
"type": {
@@ -194,7 +194,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 97,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L97"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L97"
}
],
"type": {
@@ -213,7 +213,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 98,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L98"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L98"
}
],
"type": {
@@ -232,7 +232,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 99,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L99"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L99"
}
],
"type": {
@@ -251,7 +251,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 100,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L100"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L100"
}
],
"type": {
@@ -270,7 +270,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 101,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L101"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L101"
}
],
"type": {
@@ -289,7 +289,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 102,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L102"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L102"
}
],
"type": {
@@ -309,7 +309,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 87,
"character": 12,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L87"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L87"
}
]
},
@@ -339,7 +339,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 34,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L34"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/FunctionsClient.ts#L34"
}
],
"signatures": [
@@ -373,7 +373,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 34,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L34"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/FunctionsClient.ts#L34"
}
],
"parameters": [
@@ -416,7 +416,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 42,
"character": 6,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L42"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/FunctionsClient.ts#L42"
}
],
"type": {
@@ -645,7 +645,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 41,
"character": 6,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L41"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/FunctionsClient.ts#L41"
}
],
"type": {
@@ -682,7 +682,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 43,
"character": 6,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L43"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/FunctionsClient.ts#L43"
}
],
"type": {
@@ -705,7 +705,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 40,
"character": 7,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L40"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/FunctionsClient.ts#L40"
}
]
}
@@ -735,7 +735,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 19,
"character": 12,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L19"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/FunctionsClient.ts#L19"
}
],
"type": {
@@ -964,7 +964,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 17,
"character": 12,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L17"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/FunctionsClient.ts#L17"
}
],
"type": {
@@ -1000,7 +1000,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 18,
"character": 12,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L18"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/FunctionsClient.ts#L18"
}
],
"type": {
@@ -1023,7 +1023,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 16,
"character": 12,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L16"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/FunctionsClient.ts#L16"
}
],
"type": {
@@ -1042,7 +1042,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 75,
"character": 8,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L75"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/FunctionsClient.ts#L75"
}
],
"signatures": [
@@ -1076,7 +1076,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 75,
"character": 8,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L75"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/FunctionsClient.ts#L75"
}
],
"typeParameters": [
@@ -1175,7 +1175,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 60,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L60"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/FunctionsClient.ts#L60"
}
],
"signatures": [
@@ -1209,7 +1209,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 60,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L60"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/FunctionsClient.ts#L60"
}
],
"parameters": [
@@ -1260,7 +1260,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 15,
"character": 13,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L15"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/FunctionsClient.ts#L15"
}
]
},
@@ -1301,7 +1301,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 32,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L32"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L32"
}
],
"signatures": [
@@ -1316,7 +1316,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 32,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L32"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L32"
}
],
"parameters": [
@@ -1387,7 +1387,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 31,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L31"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L31"
}
],
"type": {
@@ -1411,7 +1411,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 30,
"character": 13,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L30"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L30"
}
],
"extendedTypes": [
@@ -1480,7 +1480,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 50,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L50"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L50"
}
],
"signatures": [
@@ -1495,7 +1495,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 50,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L50"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L50"
}
],
"parameters": [
@@ -1543,7 +1543,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 31,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L31"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L31"
}
],
"type": {
@@ -1572,7 +1572,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 49,
"character": 13,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L49"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L49"
}
],
"extendedTypes": [
@@ -1621,7 +1621,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 82,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L82"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L82"
}
],
"signatures": [
@@ -1636,7 +1636,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 82,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L82"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L82"
}
],
"parameters": [
@@ -1684,7 +1684,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 31,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L31"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L31"
}
],
"type": {
@@ -1713,7 +1713,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 81,
"character": 13,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L81"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L81"
}
],
"extendedTypes": [
@@ -1762,7 +1762,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 66,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L66"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L66"
}
],
"signatures": [
@@ -1777,7 +1777,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 66,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L66"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L66"
}
],
"parameters": [
@@ -1825,7 +1825,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 31,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L31"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L31"
}
],
"type": {
@@ -1854,7 +1854,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 65,
"character": 13,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L65"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L65"
}
],
"extendedTypes": [
@@ -1877,7 +1877,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 105,
"character": 12,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L105"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L105"
}
],
"type": {
@@ -1910,7 +1910,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 121,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L121"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L121"
}
],
"type": {
@@ -2019,7 +2019,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 109,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L109"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L109"
}
],
"type": {
@@ -2035,7 +2035,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 109,
"character": 12,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L109"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L109"
}
],
"indexSignatures": [
@@ -2050,7 +2050,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 109,
"character": 14,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L109"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L109"
}
],
"parameters": [
@@ -2096,7 +2096,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 113,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L113"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L113"
}
],
"type": {
@@ -2146,7 +2146,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 117,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L117"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L117"
}
],
"type": {
@@ -2177,7 +2177,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 132,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L132"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L132"
}
],
"type": {
@@ -2211,7 +2211,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 137,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L137"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L137"
}
],
"type": {
@@ -2231,7 +2231,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 105,
"character": 36,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L105"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L105"
}
]
}
@@ -2248,7 +2248,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 16,
"character": 12,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L16"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L16"
}
],
"typeParameters": [
@@ -23,7 +23,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 88,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L88"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L88"
}
],
"type": {
@@ -42,7 +42,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 89,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L89"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L89"
}
],
"type": {
@@ -61,7 +61,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 90,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L90"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L90"
}
],
"type": {
@@ -80,7 +80,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 91,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L91"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L91"
}
],
"type": {
@@ -99,7 +99,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 92,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L92"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L92"
}
],
"type": {
@@ -118,7 +118,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 93,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L93"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L93"
}
],
"type": {
@@ -137,7 +137,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 94,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L94"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L94"
}
],
"type": {
@@ -156,7 +156,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 95,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L95"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L95"
}
],
"type": {
@@ -175,7 +175,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 96,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L96"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L96"
}
],
"type": {
@@ -194,7 +194,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 97,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L97"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L97"
}
],
"type": {
@@ -213,7 +213,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 98,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L98"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L98"
}
],
"type": {
@@ -232,7 +232,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 99,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L99"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L99"
}
],
"type": {
@@ -251,7 +251,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 100,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L100"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L100"
}
],
"type": {
@@ -270,7 +270,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 101,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L101"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L101"
}
],
"type": {
@@ -289,7 +289,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 102,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L102"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L102"
}
],
"type": {
@@ -309,7 +309,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 87,
"character": 12,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L87"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L87"
}
]
},
@@ -339,7 +339,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 34,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L34"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/FunctionsClient.ts#L34"
}
],
"signatures": [
@@ -373,7 +373,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 34,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L34"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/FunctionsClient.ts#L34"
}
],
"parameters": [
@@ -416,7 +416,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 42,
"character": 6,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L42"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/FunctionsClient.ts#L42"
}
],
"type": {
@@ -645,7 +645,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 41,
"character": 6,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L41"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/FunctionsClient.ts#L41"
}
],
"type": {
@@ -682,7 +682,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 43,
"character": 6,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L43"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/FunctionsClient.ts#L43"
}
],
"type": {
@@ -705,7 +705,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 40,
"character": 7,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L40"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/FunctionsClient.ts#L40"
}
]
}
@@ -735,7 +735,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 19,
"character": 12,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L19"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/FunctionsClient.ts#L19"
}
],
"type": {
@@ -964,7 +964,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 17,
"character": 12,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L17"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/FunctionsClient.ts#L17"
}
],
"type": {
@@ -1000,7 +1000,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 18,
"character": 12,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L18"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/FunctionsClient.ts#L18"
}
],
"type": {
@@ -1023,7 +1023,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 16,
"character": 12,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L16"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/FunctionsClient.ts#L16"
}
],
"type": {
@@ -1042,7 +1042,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 75,
"character": 8,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L75"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/FunctionsClient.ts#L75"
}
],
"signatures": [
@@ -1076,7 +1076,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 75,
"character": 8,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L75"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/FunctionsClient.ts#L75"
}
],
"typeParameters": [
@@ -1175,7 +1175,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 60,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L60"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/FunctionsClient.ts#L60"
}
],
"signatures": [
@@ -1209,7 +1209,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 60,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L60"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/FunctionsClient.ts#L60"
}
],
"parameters": [
@@ -1260,7 +1260,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 15,
"character": 13,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L15"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/FunctionsClient.ts#L15"
}
]
},
@@ -1301,7 +1301,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 32,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L32"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L32"
}
],
"signatures": [
@@ -1316,7 +1316,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 32,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L32"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L32"
}
],
"parameters": [
@@ -1387,7 +1387,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 31,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L31"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L31"
}
],
"type": {
@@ -1411,7 +1411,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 30,
"character": 13,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L30"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L30"
}
],
"extendedTypes": [
@@ -1480,7 +1480,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 50,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L50"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L50"
}
],
"signatures": [
@@ -1495,7 +1495,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 50,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L50"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L50"
}
],
"parameters": [
@@ -1543,7 +1543,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 31,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L31"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L31"
}
],
"type": {
@@ -1572,7 +1572,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 49,
"character": 13,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L49"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L49"
}
],
"extendedTypes": [
@@ -1621,7 +1621,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 82,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L82"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L82"
}
],
"signatures": [
@@ -1636,7 +1636,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 82,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L82"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L82"
}
],
"parameters": [
@@ -1684,7 +1684,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 31,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L31"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L31"
}
],
"type": {
@@ -1713,7 +1713,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 81,
"character": 13,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L81"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L81"
}
],
"extendedTypes": [
@@ -1762,7 +1762,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 66,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L66"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L66"
}
],
"signatures": [
@@ -1777,7 +1777,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 66,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L66"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L66"
}
],
"parameters": [
@@ -1825,7 +1825,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 31,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L31"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L31"
}
],
"type": {
@@ -1854,7 +1854,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 65,
"character": 13,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L65"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L65"
}
],
"extendedTypes": [
@@ -1877,7 +1877,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 105,
"character": 12,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L105"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L105"
}
],
"type": {
@@ -1910,7 +1910,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 121,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L121"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L121"
}
],
"type": {
@@ -2019,7 +2019,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 109,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L109"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L109"
}
],
"type": {
@@ -2035,7 +2035,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 109,
"character": 12,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L109"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L109"
}
],
"indexSignatures": [
@@ -2050,7 +2050,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 109,
"character": 14,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L109"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L109"
}
],
"parameters": [
@@ -2096,7 +2096,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 113,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L113"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L113"
}
],
"type": {
@@ -2146,7 +2146,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 117,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L117"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L117"
}
],
"type": {
@@ -2177,7 +2177,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 132,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L132"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L132"
}
],
"type": {
@@ -2211,7 +2211,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 137,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L137"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L137"
}
],
"type": {
@@ -2231,7 +2231,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 105,
"character": 36,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L105"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L105"
}
]
}
@@ -2248,7 +2248,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 16,
"character": 12,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L16"
"url": "https://github.com/supabase/supabase-js/blob/9ee9997608ae9f6d28717dcda4c4a85a66208069/packages/core/functions-js/src/types.ts#L16"
}
],
"typeParameters": [
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+96
View File
@@ -1424,6 +1424,38 @@
"summary": "Retrieve object info",
"tags": ["object"],
"parameters": [
{
"schema": { "type": "integer", "minimum": 0 },
"example": 100,
"in": "query",
"name": "height",
"required": false
},
{
"schema": { "type": "integer", "minimum": 0 },
"example": 100,
"in": "query",
"name": "width",
"required": false
},
{
"schema": { "type": "string", "enum": ["cover", "contain", "fill"] },
"in": "query",
"name": "resize",
"required": false
},
{
"schema": { "type": "string", "enum": ["origin", "avif"] },
"in": "query",
"name": "format",
"required": false
},
{
"schema": { "type": "integer", "minimum": 20, "maximum": 100 },
"in": "query",
"name": "quality",
"required": false
},
{
"schema": { "type": "string" },
"example": "avatars",
@@ -1462,6 +1494,38 @@
"tags": ["object"],
"description": "Object Info",
"parameters": [
{
"schema": { "type": "integer", "minimum": 0 },
"example": 100,
"in": "query",
"name": "height",
"required": false
},
{
"schema": { "type": "integer", "minimum": 0 },
"example": 100,
"in": "query",
"name": "width",
"required": false
},
{
"schema": { "type": "string", "enum": ["cover", "contain", "fill"] },
"in": "query",
"name": "resize",
"required": false
},
{
"schema": { "type": "string", "enum": ["origin", "avif"] },
"in": "query",
"name": "format",
"required": false
},
{
"schema": { "type": "integer", "minimum": 20, "maximum": 100 },
"in": "query",
"name": "quality",
"required": false
},
{
"schema": { "type": "string" },
"example": "avatars",
@@ -1648,6 +1712,38 @@
"tags": ["object"],
"description": "returns object info",
"parameters": [
{
"schema": { "type": "integer", "minimum": 0 },
"example": 100,
"in": "query",
"name": "height",
"required": false
},
{
"schema": { "type": "integer", "minimum": 0 },
"example": 100,
"in": "query",
"name": "width",
"required": false
},
{
"schema": { "type": "string", "enum": ["cover", "contain", "fill"] },
"in": "query",
"name": "resize",
"required": false
},
{
"schema": { "type": "string", "enum": ["origin", "avif"] },
"in": "query",
"name": "format",
"required": false
},
{
"schema": { "type": "integer", "minimum": 20, "maximum": 100 },
"in": "query",
"name": "quality",
"required": false
},
{
"schema": { "type": "string" },
"example": "avatars",
File diff suppressed because it is too large. Load diff
@@ -16518,6 +16518,78 @@
"x-endpoint-owners": ["security"]
}
},
"/v1/projects/{ref}/database/openapi": {
"get": {
"description": "Returns the PostgREST OpenAPI specification for the project. This is the replacement for querying `/rest/v1/` directly with the anon key.",
"operationId": "v1-get-database-openapi",
"parameters": [
{
"name": "ref",
"required": true,
"in": "path",
"description": "Project ref",
"schema": {
"minLength": 20,
"maxLength": 20,
"pattern": "^[a-z]+$",
"type": "string"
}
},
{
"name": "schema",
"required": false,
"in": "query",
"description": "The database schema to generate the OpenAPI spec for",
"schema": {
"default": "public",
"type": "string"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
}
},
"401": {
"description": "Unauthorized"
},
"403": {
"description": "Forbidden action"
},
"429": {
"description": "Rate limit exceeded"
},
"500": {
"description": "Failed to fetch PostgREST OpenAPI spec"
}
},
"security": [
{
"bearer": []
},
{
"fga_permissions": ["database_read"]
}
],
"summary": "Get PostgREST OpenAPI spec",
"tags": ["Database"],
"x-badges": [
{
"name": "OAuth scope: database:read",
"position": "after"
}
],
"x-endpoint-owners": ["management-api"],
"x-oauth-scope": "database:read"
}
},
"/v1/projects/{ref}/functions": {
"get": {
"description": "Returns all functions you've previously added to the specified project.",
@@ -20653,9 +20725,14 @@
"status": {
"type": "string",
"enum": ["AVAILABLE", "PENDING", "REMOVED", "FAILED"]
},
"completed_on": {
"type": "string",
"format": "date-time",
"nullable": true
}
},
"required": ["name", "status"]
"required": ["name", "status", "completed_on"]
}
}
}
@@ -20729,9 +20806,14 @@
"status": {
"type": "string",
"enum": ["AVAILABLE", "PENDING", "REMOVED", "FAILED"]
},
"completed_on": {
"type": "string",
"format": "date-time",
"nullable": true
}
},
"required": ["name", "status"]
"required": ["name", "status", "completed_on"]
}
}
}
@@ -29558,9 +29640,14 @@
"status": {
"type": "string",
"enum": ["AVAILABLE", "PENDING", "REMOVED", "FAILED"]
},
"completed_on": {
"type": "string",
"format": "date-time",
"nullable": true
}
},
"required": ["name", "status"]
"required": ["name", "status", "completed_on"]
},
"V1UndoBody": {
"type": "object",
@@ -2351,6 +2351,54 @@
"summary": "Retrieve object info",
"tags": ["object"],
"parameters": [
{
"schema": {
"type": "integer",
"minimum": 0
},
"example": 100,
"in": "query",
"name": "height",
"required": false
},
{
"schema": {
"type": "integer",
"minimum": 0
},
"example": 100,
"in": "query",
"name": "width",
"required": false
},
{
"schema": {
"type": "string",
"enum": ["cover", "contain", "fill"]
},
"in": "query",
"name": "resize",
"required": false
},
{
"schema": {
"type": "string",
"enum": ["origin", "avif"]
},
"in": "query",
"name": "format",
"required": false
},
{
"schema": {
"type": "integer",
"minimum": 20,
"maximum": 100
},
"in": "query",
"name": "quality",
"required": false
},
{
"schema": {
"type": "string"
@@ -2412,6 +2460,54 @@
"tags": ["object"],
"description": "Object Info",
"parameters": [
{
"schema": {
"type": "integer",
"minimum": 0
},
"example": 100,
"in": "query",
"name": "height",
"required": false
},
{
"schema": {
"type": "integer",
"minimum": 0
},
"example": 100,
"in": "query",
"name": "width",
"required": false
},
{
"schema": {
"type": "string",
"enum": ["cover", "contain", "fill"]
},
"in": "query",
"name": "resize",
"required": false
},
{
"schema": {
"type": "string",
"enum": ["origin", "avif"]
},
"in": "query",
"name": "format",
"required": false
},
{
"schema": {
"type": "integer",
"minimum": 20,
"maximum": 100
},
"in": "query",
"name": "quality",
"required": false
},
{
"schema": {
"type": "string"
@@ -2766,6 +2862,54 @@
"tags": ["object"],
"description": "returns object info",
"parameters": [
{
"schema": {
"type": "integer",
"minimum": 0
},
"example": 100,
"in": "query",
"name": "height",
"required": false
},
{
"schema": {
"type": "integer",
"minimum": 0
},
"example": 100,
"in": "query",
"name": "width",
"required": false
},
{
"schema": {
"type": "string",
"enum": ["cover", "contain", "fill"]
},
"in": "query",
"name": "resize",
"required": false
},
{
"schema": {
"type": "string",
"enum": ["origin", "avif"]
},
"in": "query",
"name": "format",
"required": false
},
{
"schema": {
"type": "integer",
"minimum": 20,
"maximum": 100
},
"in": "query",
"name": "quality",
"required": false
},
{
"schema": {
"type": "string"
+6 -16
View File
@@ -3,9 +3,9 @@
"react-hooks/exhaustive-deps": 204,
"import/no-anonymous-default-export": 57,
"@tanstack/query/exhaustive-deps": 12,
"@typescript-eslint/no-explicit-any": 1204,
"@typescript-eslint/no-explicit-any": 1190,
"no-restricted-imports": 59,
"no-restricted-exports": 278,
"no-restricted-exports": 273,
"react/no-unstable-nested-components": 66
},
"ruleFiles": {
@@ -319,6 +319,7 @@
"components/interfaces/Docs/ResourceContent.tsx": 1,
"components/interfaces/Docs/RpcContent.tsx": 4,
"components/interfaces/Docs/Snippets.ts": 1,
"components/interfaces/ErrorHandling/error-mappings.tsx": 1,
"components/interfaces/Functions/CommandRender.tsx": 4,
"components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx": 1,
"components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.utils.tsx": 3,
@@ -416,7 +417,6 @@
"components/interfaces/SQLEditor/UtilityPanel/UtilityPanel.tsx": 1,
"components/interfaces/SQLEditor/hooks.ts": 1,
"components/interfaces/SQLEditor/useAddDefinitions.ts": 1,
"components/interfaces/Settings/Addons/Addons.tsx": 1,
"components/interfaces/Settings/Addons/CustomDomainSidePanel.tsx": 1,
"components/interfaces/Settings/Addons/PITRSidePanel.tsx": 1,
"components/interfaces/Settings/Database/DatabaseSettings/ResetDbPassword.tsx": 2,
@@ -476,7 +476,6 @@
"components/interfaces/TableGridEditor/SidePanelEditor/ActionBar.tsx": 1,
"components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnDefaultValue.tsx": 2,
"components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.tsx": 6,
"components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.utils.ts": 2,
"components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnType.tsx": 1,
"components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/InputWithSuggestions.tsx": 2,
"components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/ForeignRowSelector/ForeignRowSelector.tsx": 1,
@@ -491,7 +490,7 @@
"components/interfaces/TableGridEditor/SidePanelEditor/SchemaEditor.tsx": 2,
"components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx": 14,
"components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.types.ts": 1,
"components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.tsx": 15,
"components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.tsx": 14,
"components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadSheetFileUpload.tsx": 3,
"components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadSheetTextInput.tsx": 1,
"components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetImport.tsx": 4,
@@ -499,10 +498,7 @@
"components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetImport.utils.tsx": 18,
"components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetImportPreview.tsx": 2,
"components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetPreviewGrid.tsx": 2,
"components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/Column.tsx": 3,
"components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/ColumnManagement.tsx": 3,
"components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/ForeignKeysManagement/ForeignKeyRow.tsx": 1,
"components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx": 2,
"components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.types.ts": 1,
"components/interfaces/TableGridEditor/TableDefinition.tsx": 3,
"components/interfaces/TableGridEditor/TableEntity.utils.ts": 1,
@@ -536,7 +532,7 @@
"components/layouts/SQLEditorLayout/SQLEditorNavV2/SQLEditorTreeViewItem.tsx": 2,
"components/layouts/SQLEditorLayout/SQLEditorNavV2/SearchList.tsx": 1,
"components/layouts/TableEditorLayout/EntityListItem.tsx": 1,
"components/layouts/TableEditorLayout/TableEditorMenu.tsx": 2,
"components/layouts/TableEditorLayout/TableEditorMenu.tsx": 1,
"components/layouts/Tabs/NewTab.tsx": 1,
"components/to-be-cleaned/KeyMap.tsx": 3,
"components/ui/AIAssistantPanel/AIAssistant.utils.ts": 1,
@@ -571,7 +567,7 @@
"components/ui/DatePicker/index.tsx": 2,
"components/ui/DateRangePicker.tsx": 1,
"components/ui/DebouncedComponent.tsx": 1,
"components/ui/DownloadResultsButton.tsx": 2,
"components/ui/DownloadResultsButton.tsx": 1,
"components/ui/Error.tsx": 1,
"components/ui/ErrorBoundary/ErrorBoundary.tsx": 2,
"components/ui/ErrorBoundary/GlobalErrorBoundaryState.tsx": 1,
@@ -696,7 +692,6 @@
"pages/api/ai/feedback/rate.ts": 2,
"pages/api/check-cname.ts": 1,
"pages/api/edge-functions/test.ts": 1,
"pages/api/integrations/stripe-sync.ts": 1,
"pages/api/platform/auth/[ref]/users/[id]/factors.ts": 1,
"pages/api/platform/database/[ref]/pooling.ts": 1,
"pages/api/platform/integrations/github/authorization.ts": 1,
@@ -968,8 +963,6 @@
"components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadSheetImportConfiguration.tsx": 1,
"components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadSheetTextInput.tsx": 1,
"components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetPreviewGrid.tsx": 1,
"components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/Column.tsx": 1,
"components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/ColumnManagement.tsx": 1,
"components/interfaces/TableGridEditor/ViewEntityAutofixSecurityModal.tsx": 1,
"components/layouts/APIAuthorizationLayout.tsx": 1,
"components/layouts/APIKeys/APIKeysLayout.tsx": 1,
@@ -987,7 +980,6 @@
"components/layouts/JWTKeys/JWTKeysLayout.tsx": 1,
"components/layouts/LinkAwsMarketplaceLayout.tsx": 1,
"components/layouts/LogsLayout/LogsLayout.tsx": 1,
"components/layouts/Navigation/FloatingMobileToolbar/FloatingMobileToolbar.tsx": 1,
"components/layouts/Navigation/NavigationBar/MobileNavigationBar.tsx": 1,
"components/layouts/Navigation/NavigationBar/MobileViewNav.tsx": 1,
"components/layouts/Navigation/NavigationBar/NavigationIconLink.tsx": 1,
@@ -999,11 +991,9 @@
"components/layouts/ProjectLayout/ConnectingState.tsx": 1,
"components/layouts/ProjectLayout/OrganizationSettingsLayout.tsx": 1,
"components/layouts/ProjectLayout/RestartingState.tsx": 1,
"components/layouts/ProjectLayout/RestoringState.tsx": 1,
"components/layouts/ProjectSettingsLayout/SettingsLayout.tsx": 1,
"components/layouts/RealtimeLayout/RealtimeLayout.tsx": 1,
"components/layouts/RedeemCreditsLayout.tsx": 1,
"components/layouts/ReportsLayout/ReportsLayout.tsx": 1,
"components/layouts/ReportsLayout/ReportsMenu.tsx": 1,
"components/layouts/SQLEditorLayout/SQLEditorLayout.tsx": 1,
"components/layouts/SQLEditorLayout/SQLEditorNavV2/SQLEditorLoadingSnippets.tsx": 1,
@@ -7,13 +7,15 @@ import { createPortal } from 'react-dom'
import { useTableEditorStateSnapshot } from 'state/table-editor'
import { Button } from 'ui'
import { DiscardChangesConfirmationDialog } from '@/components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog'
import { useConfirmOnClose } from '@/hooks/ui/useConfirmOnClose'
import { getModKeyLabel } from '@/lib/helpers'
export const SaveQueueActionBar = () => {
const modKey = getModKeyLabel()
const snap = useTableEditorStateSnapshot()
const isQueueOperationsEnabled = useIsQueueOperationsEnabled()
const { handleSave } = useOperationQueueActions()
const { handleSave, handleCancel } = useOperationQueueActions()
useOperationQueueShortcuts()
@@ -24,46 +26,65 @@ export const SaveQueueActionBar = () => {
const isVisible =
isQueueOperationsEnabled && snap.hasPendingOperations && !isOperationQueuePanelOpen
const { confirmOnClose, modalProps: closeConfirmationModalProps } = useConfirmOnClose({
checkIsDirty: () => true,
onClose: () => handleCancel(),
})
const content = (
<AnimatePresence>
{isVisible && (
<div className="fixed bottom-12 z-50 left-1/2 -translate-x-1/2">
<motion.div
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 16 }}
transition={{
type: 'spring',
stiffness: 420,
damping: 30,
mass: 0.4,
}}
>
<div className="flex items-center gap-8 pl-4 pr-2 py-2 bg-surface-100 border rounded-lg shadow-lg">
<span className="text-xs text-foreground-light max-w-40 truncate">
{operationCount} pending change{operationCount !== 1 ? 's' : ''}
</span>
<div className="flex items-center gap-2">
<Button type="default" size="tiny" onClick={() => snap.toggleViewOperationQueue()}>
Review{' '}
<span className="text-[10px] text-foreground/40 ml-1.5">{`${modKey}.`}</span>
</Button>
<Button
size="tiny"
type="primary"
onClick={handleSave}
disabled={isSaving}
loading={isSaving}
>
Save{operationCount > 1 && ' all'}
<span className="text-[10px] text-foreground/40 ml-1.5">{`${modKey}S`}</span>
</Button>
<>
<AnimatePresence>
{isVisible && (
<div className="fixed bottom-12 z-50 left-1/2 -translate-x-1/2">
<motion.div
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 16 }}
transition={{
type: 'spring',
stiffness: 420,
damping: 30,
mass: 0.4,
}}
>
<div className="flex items-center gap-x-12 pl-4 pr-2 py-2 bg-surface-100 border rounded-lg shadow-lg">
<div className="flex items-center gap-x-2">
<span className="text-xs text-foreground-light max-w-40 truncate">
{operationCount} pending change{operationCount !== 1 ? 's' : ''}
</span>
<Button
type="default"
size="tiny"
disabled={isSaving}
onClick={() => snap.toggleViewOperationQueue()}
>
Review{' '}
<span className="text-[10px] text-foreground/40 ml-1.5">{`${modKey}.`}</span>
</Button>
</div>
<div className="flex items-center gap-x-2">
<Button type="default" onClick={confirmOnClose} disabled={isSaving}>
Cancel
</Button>
<Button
size="tiny"
type="primary"
onClick={handleSave}
disabled={isSaving}
loading={isSaving}
>
Save
<span className="text-[10px] text-foreground/40 ml-1.5">{`${modKey}S`}</span>
</Button>
</div>
</div>
</div>
</motion.div>
</div>
)}
</AnimatePresence>
</motion.div>
</div>
)}
</AnimatePresence>
<DiscardChangesConfirmationDialog {...closeConfirmationModalProps} />
</>
)
if (typeof document === 'undefined' || !document.body) return null
@@ -65,17 +65,19 @@ export const HeaderNew = ({
return (
<div>
<div className="flex flex-wrap min-h-10 items-center bg-dash-sidebar dark:bg-surface-100 px-1.5 py-1.5 gap-2">
<div className="flex flex-wrap min-h-10 items-center bg-dash-sidebar dark:bg-surface-100 py-1.5 gap-2">
{customHeader ? (
customHeader
<div className="flex-1 px-1.5">{customHeader}</div>
) : snap.selectedRows.size > 0 ? (
<RowHeader tableQueriesEnabled={tableQueriesEnabled} />
<div className="flex-1 px-1.5">
<RowHeader tableQueriesEnabled={tableQueriesEnabled} />
</div>
) : (
<div className="flex-1 min-w-[300px] flex items-center gap-2">
<div className="w-full flex items-center gap-2 px-1.5 pb-1.5 border-b border-border">
<FilterPopoverNew isRefetching={isRefetching} />
</div>
)}
<div className="flex items-center gap-2 overflow-x-auto">
<div className="flex items-center gap-2 overflow-x-auto px-1.5">
{!customHeader && snap.selectedRows.size === 0 && (
<SortPopover tableQueriesEnabled={tableQueriesEnabled} />
)}
@@ -207,7 +207,7 @@ export const FilterPopoverNew = ({ isRefetching = false }: FilterPopoverProps) =
actions={actions}
isLoading={isGenerating}
variant="pill"
className="bg-transparent border-0 overflow-visible"
className="bg-transparent border-0 overflow-visible px-1.5"
icon={icon}
/>
</div>
@@ -1,16 +1,15 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { useConsentState } from 'common'
import { LOCAL_STORAGE_KEYS } from 'common/constants/local-storage'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { InlineLink } from 'components/ui/InlineLink'
import { useSendResetMutation } from 'data/telemetry/send-reset-mutation'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { X } from 'lucide-react'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import * as z from 'zod'
import { useConsentState } from 'common'
import { LOCAL_STORAGE_KEYS } from 'common/constants/local-storage'
import { useSendResetMutation } from 'data/telemetry/send-reset-mutation'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { Alert_Shadcn_, AlertDescription_Shadcn_, AlertTitle_Shadcn_, Badge } from 'ui'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { Card, CardContent, FormControl_Shadcn_, FormField_Shadcn_, Form_Shadcn_, Switch } from 'ui'
import { Card, CardContent, Form_Shadcn_, FormControl_Shadcn_, FormField_Shadcn_, Switch } from 'ui'
import { Admonition } from 'ui-patterns/admonition'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import {
PageSection,
@@ -20,6 +19,7 @@ import {
PageSectionSummary,
PageSectionTitle,
} from 'ui-patterns/PageSection'
import * as z from 'zod'
export const PrivacyUpdateBanner = () => {
const [privacyUpdateAcknowledged, setPrivacyUpdateAcknowledged] = useLocalStorageQuery(
@@ -30,25 +30,20 @@ export const PrivacyUpdateBanner = () => {
if (privacyUpdateAcknowledged) return null
return (
<Alert_Shadcn_ className="mb-4 relative">
<AlertTitle_Shadcn_>
<Badge variant="default" className="mr-2">
NOTICE
</Badge>
Privacy Policy Update – Effective March 16, 2026
</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_>
We've updated our{' '}
<a href="https://supabase.com/privacy" target="_blank" className="text hover:text-brand">
Privacy Policy
</a>{' '}
to clarify our use of analytics, cookies, and advertising audience management tools. By
continuing to use Supabase after March 16, you agree to the new terms. Questions? Contact{' '}
<a href="mailto:privacy@supabase.com" target="_blank" className="text hover:text-brand">
our team
</a>
.
</AlertDescription_Shadcn_>
<Admonition
type="note"
title="Updates to our privacy policy"
description={
<>
We’ve clarified how we use analytics, cookies, and advertising tools in our{' '}
<InlineLink href="https://supabase.com/privacy">privacy policy</InlineLink>, effective
March 16, 2026. By continuing to use Supabase, you agree to the updated terms.{' '}
<InlineLink href="mailto:privacy@supabase.com">Contact us</InlineLink> with any
questions.{' '}
</>
}
className="mb-10 relative"
>
<ButtonTooltip
type="text"
icon={<X />}
@@ -56,7 +51,7 @@ export const PrivacyUpdateBanner = () => {
onClick={() => setPrivacyUpdateAcknowledged(true)}
tooltip={{ content: { side: 'bottom', text: 'Dismiss' } }}
/>
</Alert_Shadcn_>
</Admonition>
)
}
@@ -119,6 +119,12 @@ export const useIsPlatformWebhooksEnabled = () => {
return platformWebhooksEnabled && flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_PLATFORM_WEBHOOKS]
}
export const useIsJitDbAccessEnabled = () => {
const { flags } = useFeaturePreviewContext()
const jitDbAccessEnabled = useFlag('jitDbAccess')
return jitDbAccessEnabled && flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_JIT_DB_ACCESS]
}
export const useIsTableFilterBarEnabled = () => {
const { flags } = useFeaturePreviewContext()
return flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_TABLE_FILTER_BAR]
@@ -29,6 +29,7 @@ import { AdvisorRulesPreview } from './AdvisorRulesPreview'
import { Branching2Preview } from './Branching2Preview'
import { CLSPreview } from './CLSPreview'
import { useFeaturePreviewContext, useFeaturePreviewModal } from './FeaturePreviewContext'
import { JitDbAccessPreview } from './JitDbAccessPreview'
import { FloatingMobileToolbarPreview } from './FloatingMobileToolbarPreview'
import { PgDeltaDiffPreview } from './PgDeltaDiffPreview'
import { PlatformWebhooksPreview } from './PlatformWebhooksPreview'
@@ -49,6 +50,7 @@ const FEATURE_PREVIEW_KEY_TO_CONTENT: {
[LOCAL_STORAGE_KEYS.UI_PREVIEW_QUEUE_OPERATIONS]: <QueueOperationsPreview />,
[LOCAL_STORAGE_KEYS.UI_PREVIEW_TABLE_FILTER_BAR]: <TableFilterBarPreview />,
[LOCAL_STORAGE_KEYS.UI_PREVIEW_PLATFORM_WEBHOOKS]: <PlatformWebhooksPreview />,
[LOCAL_STORAGE_KEYS.UI_PREVIEW_JIT_DB_ACCESS]: <JitDbAccessPreview />,
[LOCAL_STORAGE_KEYS.UI_PREVIEW_FLOATING_MOBILE_TOOLBAR]: <FloatingMobileToolbarPreview />,
}
@@ -0,0 +1,23 @@
import { useParams } from 'common'
import { InlineLink } from '@/components/ui/InlineLink'
export const JitDbAccessPreview = () => {
const { ref = '_' } = useParams()
return (
<div>
<p className="text-sm text-foreground-light mb-4">
Grant project members temporary database role access through Just-in-Time (JIT) controls in{' '}
<InlineLink href={`/project/${ref}/database/settings`}>Database Settings</InlineLink>.
</p>
<div className="space-y-2 !mt-4">
<p className="text-sm">Enabling this preview will:</p>
<ul className="list-disc pl-6 text-sm text-foreground-light space-y-1">
<li>Show JIT database access controls in Database Settings</li>
<li>Allow configuring role grants and member-level JIT rules</li>
</ul>
</div>
</div>
)
}
@@ -15,10 +15,11 @@ export type FeaturePreview = {
export const useFeaturePreviews = (): FeaturePreview[] => {
const isUnifiedLogsPreviewAvailable = useFlag('unifiedLogs')
const tableEditorNewFilterBar = useFlag('tableEditorNewFilterBar')
const pgDeltaDiffEnabled = useFlag('pgdeltaDiff')
const showFloatingMobileToolbar = useFlag('enableFloatingMobileToolbar')
const platformWebhooksEnabled = useFlag('platformWebhooks')
const jitDbAccessEnabled = useFlag('jitDbAccess')
return [
{
@@ -67,6 +68,15 @@ export const useFeaturePreviews = (): FeaturePreview[] => {
isDefaultOptIn: false,
enabled: platformWebhooksEnabled,
},
{
key: LOCAL_STORAGE_KEYS.UI_PREVIEW_JIT_DB_ACCESS,
name: 'JIT database access',
discussionsUrl: undefined,
isNew: true,
isPlatformOnly: true,
isDefaultOptIn: false,
enabled: jitDbAccessEnabled,
},
{
key: LOCAL_STORAGE_KEYS.UI_PREVIEW_API_SIDE_PANEL,
name: 'Project API documentation',
@@ -101,7 +111,7 @@ export const useFeaturePreviews = (): FeaturePreview[] => {
enabled: true,
isNew: true,
isPlatformOnly: false,
isDefaultOptIn: tableEditorNewFilterBar,
isDefaultOptIn: true,
},
{
key: LOCAL_STORAGE_KEYS.UI_PREVIEW_FLOATING_MOBILE_TOOLBAR,
@@ -5,7 +5,7 @@ import {
BillingCustomerDataForm,
type BillingCustomerDataFormValues,
} from 'components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerDataForm'
import { TAX_IDS } from 'components/interfaces/Organization/BillingSettings/BillingCustomerData/TaxID.constants'
import { resolveStoredTaxId } from 'components/interfaces/Organization/BillingSettings/BillingCustomerData/TaxID.utils'
import { useBillingCustomerDataForm } from 'components/interfaces/Organization/BillingSettings/BillingCustomerData/useBillingCustomerDataForm'
import { useOrganizationCustomerProfileQuery } from 'data/organizations/organization-customer-profile-query'
import { useOrganizationCustomerProfileUpdateMutation } from 'data/organizations/organization-customer-profile-update-mutation'
@@ -82,9 +82,8 @@ export function UpdateBillingAddressModal() {
tax_id_type: taxId?.type,
tax_id_value: taxId?.value,
tax_id_name: taxId
? TAX_IDS.find(
(option) => option.type === taxId.type && option.countryIso2 === taxId.country
)?.name || ''
? (resolveStoredTaxId(taxId.type, taxId.country, customerProfile?.address?.country)?.name ??
'')
: '',
}),
[customerProfile, taxId]
@@ -1,6 +1,7 @@
import { Badge } from 'ui'
import {
PageSection,
PageSectionAside,
PageSectionContent,
PageSectionDescription,
PageSectionMeta,
@@ -8,6 +9,8 @@ import {
PageSectionTitle,
} from 'ui-patterns/PageSection'
import { DocsButton } from 'components/ui/DocsButton'
import { DOCS_URL } from 'lib/constants'
import { CustomAuthProvidersList } from './CustomAuthProvidersList'
export const CustomAuthProviders = () => {
@@ -23,6 +26,9 @@ export const CustomAuthProviders = () => {
Configure OAuth/OIDC providers for this project using your own issuer or endpoints.
</PageSectionDescription>
</PageSectionSummary>
<PageSectionAside>
<DocsButton href={`${DOCS_URL}/guides/auth/custom-oauth-providers`} />
</PageSectionAside>
</PageSectionMeta>
<PageSectionContent>
<CustomAuthProvidersList />
@@ -132,7 +132,7 @@ export const OAuthAppsList = () => {
return filtered.sort((a, b) => {
if (sortCol === 'name') {
return a.client_name.localeCompare(b.client_name) * orderMultiplier
return (a.client_name || '').localeCompare(b.client_name || '') * orderMultiplier
}
if (sortCol === 'client_type') {
return a.client_type.localeCompare(b.client_type) * orderMultiplier
@@ -216,6 +216,9 @@ export const SmtpForm = () => {
)
}
const showFooterMessage =
form.formState.isDirty && ((enableSmtp && !isSmtpEnabled(authConfig)) || !enableSmtp)
return (
<PageSection>
<PageSectionContent>
@@ -462,26 +465,22 @@ export const SmtpForm = () => {
)}
<CardFooter
className={cn(
form.formState.isDirty ? 'justify-between' : 'justify-end',
'gap-x-2'
)}
className={cn(showFooterMessage ? 'justify-between' : 'justify-end', 'gap-x-2')}
>
{form.formState.isDirty && (
<p className="text-sm text-foreground-light">
{enableSmtp ? (
<>
Rate limit for sending emails will be increased to 30 and{' '}
<InlineLink href={`/project/${projectRef}/auth/rate-limits`}>
can be adjusted
</InlineLink>{' '}
after enabling custom SMTP
</>
) : (
'Rate limit for sending emails will be reduced to 2 after disabling custom SMTP'
)}
</p>
)}
{showFooterMessage &&
(enableSmtp ? (
<p className="text-sm text-foreground-light">
Rate limit for sending emails will be increased to 30 and{' '}
<InlineLink href={`/project/${projectRef}/auth/rate-limits`}>
can be adjusted
</InlineLink>{' '}
after enabling custom SMTP
</p>
) : (
<p className="text-sm text-foreground-light">
Rate limit for sending emails will be reduced to 2 after disabling custom SMTP
</p>
))}
<div className="flex items-center gap-x-2">
{form.formState.isDirty && (
<Button
@@ -13,6 +13,10 @@ import {
} from '@stripe/stripe-js'
import { Form } from '@ui/components/shadcn/ui/form'
import { TAX_IDS } from 'components/interfaces/Organization/BillingSettings/BillingCustomerData/TaxID.constants'
import {
resolveStoredTaxId,
getEffectiveTaxCountry,
} from 'components/interfaces/Organization/BillingSettings/BillingCustomerData/TaxID.utils'
import type { CustomerAddress, CustomerTaxId } from 'data/organizations/types'
import { getURL } from 'lib/helpers'
import { Check, ChevronsUpDown } from 'lucide-react'
@@ -96,10 +100,8 @@ export const NewPaymentMethodElement = forwardRef(
resolver: zodResolver(BillingCustomerDataSchema),
defaultValues: {
tax_id_name: currentTaxId
? TAX_IDS.find(
(option) =>
option.type === currentTaxId.type && option.countryIso2 === currentTaxId.country
)?.name || ''
? (resolveStoredTaxId(currentTaxId.type, currentTaxId.country, currentAddress?.country)
?.name ?? '')
: '',
tax_id_type: currentTaxId ? currentTaxId.type : '',
tax_id_value: currentTaxId ? currentTaxId.value : '',
@@ -172,7 +174,7 @@ export const NewPaymentMethodElement = forwardRef(
function getConfiguredTaxId(): CustomerTaxId | null {
return purchasingAsBusiness && selectedTaxId
? {
country: selectedTaxId.countryIso2,
country: getEffectiveTaxCountry(selectedTaxId),
type: selectedTaxId.type,
value: form.getValues('tax_id_value'),
}
@@ -44,8 +44,6 @@ export function ConnectConfigSection({
}: ConnectConfigSectionProps) {
if (activeFields.length === 0) return null
const formLayoutClassName = 'md:[&>div:first-child]:!w-1/3 xl:[&>div:first-child]:!w-2/5'
return (
<div className="flex flex-col gap-y-4">
{activeFields.map((field) => {
@@ -246,7 +244,7 @@ interface ModeSelectorProps {
export function ModeSelector({ modes, selected, onChange }: ModeSelectorProps) {
return (
<div className="grid grid-cols-4 rounded-lg border">
<div className="grid grid-cols-4 rounded-lg border overflow-hidden">
{modes.map((mode) => (
<button
key={mode.id}
@@ -18,8 +18,10 @@ export const resolveConnectionString = ({
}: {
connectionMethod: ConnectionStringMethod
useSharedPooler: boolean
connectionStringPooler: ConnectionStringPooler
connectionStringPooler: ConnectionStringPooler | undefined
}) => {
if (!connectionStringPooler) return ''
if (connectionMethod === 'direct') {
return connectionStringPooler.direct ?? ''
}
@@ -471,7 +471,7 @@ describe('connect.resolver:getDefaultState', () => {
steps: [],
}
const state = getDefaultState(schema)
const state = getDefaultState({ schema })
expect(state.mode).toBe('framework')
})
@@ -501,7 +501,7 @@ describe('connect.resolver:getDefaultState', () => {
steps: [],
}
const state = getDefaultState(schema)
const state = getDefaultState({ schema })
expect(state.framework).toBe('nextjs')
expect(state.library).toBe('supabasejs')
expect(state.mcpReadonly).toBe(false)
@@ -514,7 +514,7 @@ describe('connect.resolver:getDefaultState', () => {
steps: [],
}
const state = getDefaultState(schema)
const state = getDefaultState({ schema })
expect(state.mode).toBe('direct')
})
})
@@ -196,7 +196,7 @@ function resolveFieldOptions(field: { options?: unknown }, state: ConnectState):
/**
* Gets default state for the schema, using first mode and default field values.
*/
export function getDefaultState(schema: ConnectSchema): ConnectState {
export function getDefaultState({ schema }: { schema: ConnectSchema }): ConnectState {
const defaultMode = schema.modes[0]?.id ?? 'direct'
const state: ConnectState = { mode: defaultMode }
@@ -170,6 +170,7 @@ describe('connect.schema:steps resolution', () => {
const steps = resolveSteps(connectSchema, state)
expect(steps.find((s) => s.id === 'shadcn-add')).toBeDefined()
expect(steps.find((s) => s.id === 'shadcn-env')).toBeDefined()
expect(steps.find((s) => s.id === 'shadcn-explore')).toBeDefined()
})
@@ -186,6 +187,7 @@ describe('connect.schema:steps resolution', () => {
const steps = resolveSteps(connectSchema, state)
expect(steps.find((s) => s.id === 'shadcn-add')).toBeDefined()
expect(steps.find((s) => s.id === 'shadcn-env')).toBeDefined()
})
test('should resolve default steps for other frameworks', () => {
@@ -337,6 +339,14 @@ describe('connect.schema:step content paths', () => {
expect(exploreStep?.content).toBe('steps/shadcn/explore')
})
test('shadcn env step should have valid content path', () => {
const state: ConnectState = { mode: 'framework', framework: 'nextjs', frameworkUi: true }
const steps = resolveSteps(connectSchema, state)
const envStep = steps.find((s) => s.id === 'shadcn-env')
expect(envStep?.content).toBe('steps/shadcn/env')
})
test('direct connection step should have valid content path', () => {
const state: ConnectState = { mode: 'direct' }
const steps = resolveSteps(connectSchema, state)
@@ -53,6 +53,13 @@ const frameworkShadcnStep: StepDefinition = {
content: 'steps/shadcn/command',
}
const frameworkShadcnEnvStep: StepDefinition = {
id: 'shadcn-env',
title: 'Set env variables',
description: 'Add the following values to your env file.',
content: 'steps/shadcn/env',
}
const frameworkShadcnExploreStep: StepDefinition = {
id: 'shadcn-explore',
title: 'Check out more UI components',
@@ -173,7 +180,7 @@ export const connectSchema: ConnectSchema = {
id: 'direct',
label: 'Direct',
description: 'Connection string',
fields: ['connectionMethod', 'useSharedPooler', 'connectionType'],
fields: ['connectionSource', 'connectionMethod', 'useSharedPooler', 'connectionType'],
},
{
id: 'orm',
@@ -226,6 +233,13 @@ export const connectSchema: ConnectSchema = {
},
// Direct connection fields
connectionSource: {
id: 'connectionSource',
type: 'select',
label: 'Source',
options: { source: 'connectionSources' },
defaultValue: undefined,
},
connectionMethod: {
id: 'connectionMethod',
type: 'radio-list',
@@ -297,6 +311,7 @@ export const connectSchema: ConnectSchema = {
true: [
frameworkInstallStep,
frameworkShadcnStep,
frameworkShadcnEnvStep,
frameworkShadcnExploreStep,
skillsInstallStep,
],
@@ -308,6 +323,7 @@ export const connectSchema: ConnectSchema = {
true: [
frameworkInstallStep,
frameworkShadcnStep,
frameworkShadcnEnvStep,
frameworkShadcnExploreStep,
skillsInstallStep,
],
@@ -1,7 +1,10 @@
import { useParams } from 'common'
import { useMemo } from 'react'
import { CodeBlock } from 'ui'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import { getConnectionStrings } from '../../../DatabaseSettings.utils'
import { getAddons } from '@/components/interfaces/Billing/Subscription/Subscription.utils'
import {
type ConnectionStringMethod,
type DatabaseConnectionType,
@@ -15,6 +18,12 @@ import {
PASSWORD_PLACEHOLDER,
resolveConnectionString,
} from '@/components/interfaces/ConnectSheet/ConnectionString.utils'
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 { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
import { pluckObjectFields } from '@/lib/helpers'
const buildPsqlCommand = (params: { host: string; port: string; database: string; user: string }) =>
`psql -h ${params.host} -p ${params.port} -d ${params.database} -U ${params.user}`
@@ -22,15 +31,93 @@ const buildPsqlCommand = (params: { host: string; port: string; database: string
const buildJdbcString = (params: { host: string; port: string; database: string; user: string }) =>
`jdbc:postgresql://${params.host}:${params.port}/${params.database}?user=${params.user}&password=${PASSWORD_PLACEHOLDER}`
/**
* [Joshen] ConnectStepsSection does something similar but since only this page needs to consider connection strings
* from all databases (including read replicas), am opting to separate the logic for retrieving connection strings here
*
* We can however, consider to shift this logic into ConnectStepsSection, such that we can consider read replicas for
* the other tabs like "Framework" and "ORM" too. However, leaving them out for now and only updating "Direct"
*/
const useConnectionStringDatabases = () => {
const { ref: projectRef } = useParams()
const { hasAccess: allowPgBouncerSelection } = useCheckEntitlements('dedicated_pooler')
const { data: databases = [] } = useReadReplicasQuery({ projectRef })
const { data: pgbouncerConfig } = usePgbouncerConfigQuery({ projectRef })
const { data: supavisorConfig } = useSupavisorConfigurationQuery({ projectRef })
const { data: addons } = useProjectAddonsQuery({ projectRef })
const { ipv4: ipv4Addon } = getAddons(addons?.selected_addons ?? [])
const DB_FIELDS = ['db_host', 'db_name', 'db_port', 'db_user', 'inserted_at']
const emptyState = { db_user: '', db_host: '', db_port: '', db_name: '' }
return Object.fromEntries(
databases.map((db) => {
const connectionInfo = pluckObjectFields(db || emptyState, DB_FIELDS)
const poolingConfigurationShared = supavisorConfig?.find(
(x) => x.identifier === db.identifier
)
const poolingConfigurationDedicated = allowPgBouncerSelection ? pgbouncerConfig : undefined
const connectionStringsShared = getConnectionStrings({
connectionInfo,
poolingInfo: {
connectionString: poolingConfigurationShared?.connection_string ?? '',
db_host: poolingConfigurationShared?.db_host ?? '',
db_name: poolingConfigurationShared?.db_name ?? '',
db_port: poolingConfigurationShared?.db_port ?? 0,
db_user: poolingConfigurationShared?.db_user ?? '',
},
metadata: { projectRef: db.identifier },
})
const connectionStringsDedicated =
poolingConfigurationDedicated !== undefined
? getConnectionStrings({
connectionInfo,
poolingInfo: {
connectionString: poolingConfigurationDedicated.connection_string.replace(
projectRef ?? '_',
db.identifier
),
db_host: poolingConfigurationDedicated.db_host,
db_name: poolingConfigurationDedicated.db_name,
db_port: poolingConfigurationDedicated.db_port,
db_user: poolingConfigurationDedicated.db_user,
},
metadata: { projectRef: db.identifier },
})
: undefined
return [
db.identifier,
{
transactionShared: connectionStringsShared.pooler.uri,
sessionShared: connectionStringsShared.pooler.uri.replace('6543', '5432'),
transactionDedicated: connectionStringsDedicated?.pooler.uri,
sessionDedicated: connectionStringsDedicated?.pooler.uri.replace('6543', '5432'),
ipv4SupportedForDedicatedPooler: !!ipv4Addon,
direct: connectionStringsShared.direct.uri,
},
]
})
)
}
/**
* Step component for direct database connections.
* Uses state to determine which connection string to show.
*/
function DirectConnectionContent({ state, connectionStringPooler }: StepContentProps) {
function DirectConnectionContent({ state }: StepContentProps) {
const connectionSource = state.connectionSource
const connectionType = (state.connectionType as DatabaseConnectionType) ?? 'uri'
const connectionMethod = (state.connectionMethod as ConnectionStringMethod) ?? 'direct'
const useSharedPooler = Boolean(state.useSharedPooler)
const connectionStrings = useConnectionStringDatabases()
const connectionStringPooler =
connectionStrings[connectionSource as keyof typeof connectionStrings]
// Determine which connection string to use
const resolvedConnectionString = useMemo(
() =>
@@ -0,0 +1,52 @@
import { MultipleCodeBlock } from 'ui-patterns/MultipleCodeBlock'
import type { StepContentProps } from '@/components/interfaces/ConnectSheet/Connect.types'
function getEnvFile(
state: StepContentProps['state'],
projectKeys: StepContentProps['projectKeys']
) {
if (state.framework === 'nextjs') {
return {
name: '.env.local',
language: 'bash',
code: [
`NEXT_PUBLIC_SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}`,
projectKeys.publishableKey
? `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY=${projectKeys.publishableKey}`
: `NEXT_PUBLIC_SUPABASE_ANON_KEY=${projectKeys.anonKey ?? 'your-anon-key'}`,
'',
].join('\n'),
}
}
if (state.framework === 'react') {
const isCreateReactApp = state.frameworkVariant === 'create-react-app'
const envFileName = isCreateReactApp ? '.env.local' : '.env'
const keyPrefix = isCreateReactApp ? 'REACT_APP' : 'VITE'
return {
name: envFileName,
language: 'bash',
code: [
`${keyPrefix}_SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}`,
projectKeys.publishableKey
? `${keyPrefix}_SUPABASE_PUBLISHABLE_DEFAULT_KEY=${projectKeys.publishableKey}`
: `${keyPrefix}_SUPABASE_ANON_KEY=${projectKeys.anonKey ?? 'your-anon-key'}`,
'',
].join('\n'),
}
}
return null
}
function ShadcnEnvContent({ state, projectKeys }: StepContentProps) {
const envFile = getEnvFile(state, projectKeys)
if (!envFile) return null
return <MultipleCodeBlock files={[envFile]} />
}
export default ShadcnEnvContent
@@ -1,8 +1,16 @@
import { act, renderHook } from '@testing-library/react'
import { describe, expect, test } from 'vitest'
import { describe, expect, test, vi } from 'vitest'
import { useConnectState } from './useConnectState'
vi.mock('common', () => ({
useParams: () => ({ ref: 'test-ref' }),
}))
vi.mock('@/data/read-replicas/replicas-query', () => ({
useReadReplicasQuery: () => ({ data: [] }),
}))
describe('useConnectState', () => {
// ============================================================================
// Initial State Tests
@@ -371,6 +379,7 @@ describe('useConnectState', () => {
const stepIds = result.current.resolvedSteps.map((s) => s.id)
expect(stepIds).toContain('shadcn-add')
expect(stepIds).toContain('shadcn-env')
})
})
@@ -1,3 +1,4 @@
import { useParams } from 'common'
import { useCallback, useMemo, useState } from 'react'
import { FEATURE_GROUPS_PLATFORM, MCP_CLIENTS } from 'ui-patterns/McpUrlBuilder'
@@ -24,6 +25,8 @@ import type {
ResolvedStep,
} from './Connect.types'
import { resolveFrameworkLibraryKey } from './Connect.utils'
import { Database, useReadReplicasQuery } from '@/data/read-replicas/replicas-query'
import { formatDatabaseID, formatDatabaseRegion } from '@/data/read-replicas/replicas.utils'
// ============================================================================
// Data Source Helpers
@@ -33,7 +36,15 @@ import { resolveFrameworkLibraryKey } from './Connect.utils'
* Get field options from a data source reference.
* This maps source names to actual data.
*/
function getFieldOptionsFromSource(source: string, state: ConnectState): FieldOption[] {
function getFieldOptionsFromSource({
source,
state,
databases,
}: {
source: string
state: ConnectState
databases: Database[]
}): FieldOption[] {
switch (source) {
case 'frameworks':
return [...FRAMEWORKS, ...MOBILES].map((f) => ({
@@ -98,6 +109,16 @@ function getFieldOptionsFromSource(source: string, state: ConnectState): FieldOp
description: m.description,
}))
case 'connectionSources':
return databases.map((db) => {
const region = formatDatabaseRegion(db?.region ?? '')
const id = formatDatabaseID(db.identifier ?? '')
const label = db.identifier.includes('-rr-')
? `Read Replica (${region} - ${id}}`
: 'Primary Database'
return { value: db.identifier, label }
})
case 'connectionTypes':
return DATABASE_CONNECTION_TYPES.map((t) => ({
value: t.id,
@@ -133,7 +154,15 @@ function getFieldOptionsFromSource(source: string, state: ConnectState): FieldOp
/**
* Resolve field options, handling both static options and data source references.
*/
function resolveFieldOptionsWithSource(field: ResolvedField, state: ConnectState): FieldOption[] {
function resolveFieldOptionsWithSource({
field,
state,
databases,
}: {
field: ResolvedField
state: ConnectState
databases: Database[]
}): FieldOption[] {
// If already resolved (from conditional resolution)
if (field.resolvedOptions.length > 0) {
return field.resolvedOptions
@@ -142,7 +171,7 @@ function resolveFieldOptionsWithSource(field: ResolvedField, state: ConnectState
// Check if it's a source reference
const options = connectSchema.fields[field.id]?.options
if (options && typeof options === 'object' && 'source' in options) {
return getFieldOptionsFromSource(options.source as string, state)
return getFieldOptionsFromSource({ source: options.source as string, state, databases })
}
return []
@@ -163,8 +192,11 @@ export interface UseConnectStateReturn {
}
export function useConnectState(initialState?: Partial<ConnectState>): UseConnectStateReturn {
const { ref: projectRef } = useParams()
const { data: databases = [] } = useReadReplicasQuery({ projectRef })
const [state, setState] = useState<ConnectState>(() => {
const defaults = getDefaultState(connectSchema)
const defaults = getDefaultState({ schema: connectSchema })
// Set initial framework if mode is framework
if (defaults.mode === 'framework' && !defaults.framework) {
@@ -246,41 +278,45 @@ export function useConnectState(initialState?: Partial<ConnectState>): UseConnec
})
}, [])
const setMode = useCallback((mode: ConnectMode) => {
setState((prev) => {
const next: ConnectState = { ...prev, mode }
const setMode = useCallback(
(mode: ConnectMode) => {
setState((prev) => {
const next: ConnectState = { ...prev, mode }
// Initialize mode-specific defaults
if (mode === 'framework' && !next.framework) {
const firstFramework = FRAMEWORKS[0]
next.framework = firstFramework?.key ?? ''
if (firstFramework?.children?.length > 1) {
next.frameworkVariant = firstFramework.children[0]?.key ?? ''
// Initialize mode-specific defaults
if (mode === 'framework' && !next.framework) {
const firstFramework = FRAMEWORKS[0]
next.framework = firstFramework?.key ?? ''
if (firstFramework?.children?.length > 1) {
next.frameworkVariant = firstFramework.children[0]?.key ?? ''
}
const libraryKey = resolveFrameworkLibraryKey({
framework: next.framework,
frameworkVariant: next.frameworkVariant,
library: next.library,
})
if (libraryKey) next.library = libraryKey
}
const libraryKey = resolveFrameworkLibraryKey({
framework: next.framework,
frameworkVariant: next.frameworkVariant,
library: next.library,
})
if (libraryKey) next.library = libraryKey
}
if (mode === 'direct') {
next.connectionMethod = next.connectionMethod ?? 'direct'
next.connectionType = next.connectionType ?? 'uri'
}
if (mode === 'direct') {
next.connectionMethod = next.connectionMethod ?? 'direct'
next.connectionType = next.connectionType ?? 'uri'
next.connectionSource = projectRef ?? '_'
}
if (mode === 'orm' && !next.orm) {
next.orm = ORMS[0]?.key ?? ''
}
if (mode === 'orm' && !next.orm) {
next.orm = ORMS[0]?.key ?? ''
}
if (mode === 'mcp' && !next.mcpClient) {
next.mcpClient = MCP_CLIENTS[0]?.key ?? ''
}
if (mode === 'mcp' && !next.mcpClient) {
next.mcpClient = MCP_CLIENTS[0]?.key ?? ''
}
return next
})
}, [])
return next
})
},
[projectRef]
)
const activeFields = useMemo(() => getActiveFields(connectSchema, state), [state])
@@ -290,9 +326,9 @@ export function useConnectState(initialState?: Partial<ConnectState>): UseConnec
(fieldId: string): FieldOption[] => {
const field = activeFields.find((f) => f.id === fieldId)
if (!field) return []
return resolveFieldOptionsWithSource(field, state)
return resolveFieldOptionsWithSource({ field, state, databases })
},
[activeFields, state]
[activeFields, state, databases]
)
return {
@@ -1,4 +1,5 @@
import dayjs from 'dayjs'
import { guessLocalTimezone } from 'lib/dayjs'
import { ALL_TIMEZONES } from './PITR.constants'
import type { Time } from './PITR.types'
import type { ProjectSelectedAddon } from 'data/subscriptions/types'
@@ -17,7 +18,7 @@ export const getDatesBetweenRange = (startDate: dayjs.Dayjs, endDate: dayjs.Dayj
}
export const getClientTimezone = () => {
const defaultTz = dayjs.tz.guess()
const defaultTz = guessLocalTimezone()
const utcTz = ALL_TIMEZONES.find((option) => option.value === 'UTC')
const timezone = ALL_TIMEZONES.find((option) => {
if (option.utc.includes(defaultTz)) return option
@@ -1,28 +1,16 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
import { ChevronDown, Plus, Trash } from 'lucide-react'
import { useFieldArray, UseFormReturn } from 'react-hook-form'
import {
Button,
cn,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
FormControl_Shadcn_,
FormField_Shadcn_,
Input_Shadcn_,
useWatch_Shadcn_,
} from 'ui'
import { UseFormReturn } from 'react-hook-form'
import { useWatch_Shadcn_ } from 'ui'
import { KeyValueFieldArray } from 'ui-patterns/form/KeyValueFieldArray/KeyValueFieldArray'
import { WebhookFormValues } from './EditHookPanel.constants'
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
import {
FormSection,
FormSectionContent,
FormSectionLabel,
} from '@/components/ui/Forms/FormSection'
import { buildEdgeFunctionHeaderAddActions } from '@/components/interfaces/Functions/httpHeaderAddActions'
import { getKeys, useAPIKeysQuery } from '@/data/api-keys/api-keys-query'
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
import { uuidv4 } from '@/lib/helpers'
@@ -44,126 +32,31 @@ export const HTTPHeaders = ({ form }: HTTPHeadersProps) => {
const apiKey = secretKey?.api_key ?? serviceKey?.api_key ?? '[YOUR API KEY]'
const functionType = useWatch_Shadcn_({ control: form.control, name: 'function_type' })
const {
fields: headerFields,
append: appendHeader,
remove: removeHeader,
} = useFieldArray({
control: form.control,
name: 'httpHeaders',
})
const onAddHeaders = (headers?: { id: string; name: string; value: string }[]) => {
if (headers) {
headers.forEach((header) => appendHeader(header))
} else {
appendHeader({ id: uuidv4(), name: '', value: '' })
}
}
const addActions =
functionType === 'supabase_function'
? buildEdgeFunctionHeaderAddActions({
apiKey,
includeApiKeyHeader: serviceKey?.type === 'secret',
createRow: (name: string, value: string) => ({ id: uuidv4(), name, value }),
})
: []
return (
<FormSection
header={<FormSectionLabel className="lg:!col-span-4">HTTP Headers</FormSectionLabel>}
>
<FormSectionContent loading={false} className="lg:!col-span-8">
<div className="space-y-2">
{headerFields.map((field, index) => (
<div key={field.id} className="flex items-center space-x-2">
<FormField_Shadcn_
control={form.control}
name={`httpHeaders.${index}.name`}
render={({ field }) => (
<FormControl_Shadcn_>
<Input_Shadcn_ {...field} className="w-full" placeholder="Header name" />
</FormControl_Shadcn_>
)}
/>
<FormField_Shadcn_
control={form.control}
name={`httpHeaders.${index}.value`}
render={({ field }) => (
<FormControl_Shadcn_>
<Input_Shadcn_ {...field} className="w-full" placeholder="Header value" />
</FormControl_Shadcn_>
)}
/>
<ButtonTooltip
type="text"
icon={<Trash />}
className="py-4"
onClick={() => removeHeader(index)}
tooltip={{ content: { side: 'bottom', text: 'Remove header' } }}
/>
</div>
))}
<div className="flex items-center">
<Button
type="default"
size="tiny"
icon={<Plus />}
className={cn(functionType === 'supabase_function' && 'rounded-r-none px-3')}
onClick={() => onAddHeaders()}
>
Add a new header
</Button>
{functionType === 'supabase_function' && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="default"
icon={<ChevronDown />}
className="rounded-l-none px-[4px] py-[5px]"
/>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" side="bottom">
<DropdownMenuItem
key="add-auth-header"
onClick={() => {
onAddHeaders([
{
id: uuidv4(),
name: 'Authorization',
value: `Bearer ${apiKey}`,
},
...(serviceKey?.type === 'secret'
? [{ id: uuidv4(), name: 'apikey', value: apiKey }]
: []),
])
}}
>
<div className="space-y-1">
<p className="block text-foreground">Add auth header with secret key</p>
<p className="text-foreground-light">
Required if your edge function enforces JWT verification
</p>
</div>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
key="add-source-header"
onClick={() =>
onAddHeaders([
{
id: uuidv4(),
name: 'x-supabase-webhook-source',
value: `[Use a secret value]`,
},
])
}
>
<div className="space-y-1">
<p className="block text-foreground">Add custom source header</p>
<p className="text-foreground-light">
Useful to verify that the edge function was triggered from this webhook
</p>
</div>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
</div>
<KeyValueFieldArray
control={form.control}
name="httpHeaders"
keyFieldName="name"
valueFieldName="value"
createEmptyRow={() => ({ id: uuidv4(), name: '', value: '' })}
keyPlaceholder="Header name"
valuePlaceholder="Header value"
addLabel="Add a new header"
addActions={addActions}
/>
</FormSectionContent>
</FormSection>
)
@@ -1,8 +1,3 @@
import { Check, ChevronsUpDown, Loader2 } from 'lucide-react'
import Link from 'next/link'
import { Fragment, useEffect, useMemo, useState } from 'react'
import { toast } from 'sonner'
import CodeEditor from 'components/ui/CodeEditor/CodeEditor'
import { DocsButton } from 'components/ui/DocsButton'
import { useDatabaseIndexCreateMutation } from 'data/database-indexes/index-create-mutation'
@@ -11,32 +6,36 @@ import { useTableColumnsQuery } from 'data/database/table-columns-query'
import { useEntityTypesQuery } from 'data/entity-types/entity-types-infinite-query'
import { useIsOrioleDb, useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { DOCS_URL } from 'lib/constants'
import { Check, ChevronsUpDown, Loader2 } from 'lucide-react'
import Link from 'next/link'
import { Fragment, useEffect, useMemo, useState } from 'react'
import { toast } from 'sonner'
import {
Button,
cn,
Command_Shadcn_,
CommandEmpty_Shadcn_,
CommandGroup_Shadcn_,
CommandInput_Shadcn_,
CommandItem_Shadcn_,
CommandList_Shadcn_,
Command_Shadcn_,
Popover_Shadcn_,
PopoverContent_Shadcn_,
PopoverTrigger_Shadcn_,
Popover_Shadcn_,
ScrollArea,
Select_Shadcn_,
SelectContent_Shadcn_,
SelectItem_Shadcn_,
SelectSeparator_Shadcn_,
SelectTrigger_Shadcn_,
SelectValue_Shadcn_,
Select_Shadcn_,
SidePanel,
cn,
} from 'ui'
import { Admonition } from 'ui-patterns'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { MultiSelectOption } from 'ui-patterns/MultiSelectDeprecated'
import { MultiSelectV2 } from 'ui-patterns/MultiSelectDeprecated/MultiSelectV2'
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { INDEX_TYPES } from './Indexes.constants'
interface CreateIndexSidePanelProps {
@@ -54,6 +53,7 @@ export const CreateIndexSidePanel = ({ visible, onClose }: CreateIndexSidePanelP
const [selectedIndexType, setSelectedIndexType] = useState<string>(INDEX_TYPES[0].value)
const [schemaDropdownOpen, setSchemaDropdownOpen] = useState(false)
const [tableDropdownOpen, setTableDropdownOpen] = useState(false)
const [schemaSearchTerm, setSchemaSearchTerm] = useState('')
const [searchTerm, setSearchTerm] = useState('')
const { data: schemas } = useSchemasQuery({
@@ -89,11 +89,17 @@ export const CreateIndexSidePanel = ({ visible, onClose }: CreateIndexSidePanelP
() => entities?.pages.flatMap((page) => page.data.entities) || [],
[entities?.pages]
)
function handleSearchChange(value: string) {
setSearchTerm(value)
}
function handleSchemaSelect(schema: string) {
setSelectedSchema(schema)
setSearchTerm('')
setSchemaSearchTerm('')
setSchemaDropdownOpen(false)
}
const columns = tableColumns?.[0]?.columns ?? []
const columnOptions: MultiSelectOption[] = columns
.filter((column): column is NonNullable<typeof column> => column !== null)
@@ -132,6 +138,8 @@ CREATE INDEX ON "${selectedSchema}"."${selectedEntity}" USING ${selectedIndexTyp
setSelectedEntity('')
setSelectedColumns([])
setSelectedIndexType(INDEX_TYPES[0].value)
setSchemaSearchTerm('')
setSearchTerm('')
}
}, [visible])
@@ -139,6 +147,7 @@ CREATE INDEX ON "${selectedSchema}"."${selectedEntity}" USING ${selectedIndexTyp
setSelectedEntity('')
setSelectedColumns([])
setSelectedIndexType(INDEX_TYPES[0].value)
setSearchTerm('')
}, [selectedSchema])
useEffect(() => {
@@ -146,7 +155,11 @@ CREATE INDEX ON "${selectedSchema}"."${selectedEntity}" USING ${selectedIndexTyp
setSelectedIndexType(INDEX_TYPES[0].value)
}, [selectedEntity])
const isSelectEntityDisabled = entityTypes.length === 0
useEffect(() => {
if (!schemaDropdownOpen) setSchemaSearchTerm('')
}, [schemaDropdownOpen])
const isSelectEntityDisabled = entityTypes.length === 0 && searchTerm.trim().length === 0
return (
<SidePanel
@@ -188,34 +201,33 @@ CREATE INDEX ON "${selectedSchema}"."${selectedEntity}" USING ${selectedIndexTyp
>
<Command_Shadcn_>
<CommandInput_Shadcn_
placeholder="Find table..."
value={searchTerm}
onValueChange={handleSearchChange}
placeholder="Find schema..."
value={schemaSearchTerm}
onValueChange={setSchemaSearchTerm}
/>
<CommandList_Shadcn_>
<CommandList_Shadcn_
className={cn((schemas ?? []).length > 7 && '!max-h-[210px] overflow-y-auto')}
onWheel={(event) => event.stopPropagation()}
>
<CommandEmpty_Shadcn_>No schemas found</CommandEmpty_Shadcn_>
<CommandGroup_Shadcn_>
<ScrollArea className={(schemas || []).length > 7 ? 'h-[210px]' : ''}>
{(schemas ?? []).map((schema) => (
<CommandItem_Shadcn_
key={schema.name}
className="cursor-pointer flex items-center justify-between space-x-2 w-full"
onSelect={() => {
setSelectedSchema(schema.name)
setSchemaDropdownOpen(false)
}}
onClick={() => {
setSelectedSchema(schema.name)
setSchemaDropdownOpen(false)
}}
>
<span>{schema.name}</span>
{selectedEntity === schema.name && (
<Check className="text-brand" strokeWidth={2} size={16} />
)}
</CommandItem_Shadcn_>
))}
</ScrollArea>
{(schemas ?? []).map((schema) => (
<CommandItem_Shadcn_
key={schema.name}
className="cursor-pointer flex items-center justify-between space-x-2 w-full"
onSelect={() => {
handleSchemaSelect(schema.name)
}}
onClick={() => {
handleSchemaSelect(schema.name)
}}
>
<span>{schema.name}</span>
{selectedSchema === schema.name && (
<Check className="text-brand" strokeWidth={2} size={16} />
)}
</CommandItem_Shadcn_>
))}
</CommandGroup_Shadcn_>
</CommandList_Shadcn_>
</Command_Shadcn_>
@@ -274,7 +286,10 @@ CREATE INDEX ON "${selectedSchema}"."${selectedEntity}" USING ${selectedIndexTyp
value={searchTerm}
onValueChange={handleSearchChange}
/>
<CommandList_Shadcn_>
<CommandList_Shadcn_
className={cn(entityTypes.length > 7 && '!max-h-[210px] overflow-y-auto')}
onWheel={(event) => event.stopPropagation()}
>
<CommandEmpty_Shadcn_>
{isLoadingEntities ? (
<div className="flex items-center gap-2 text-center justify-center">
@@ -286,27 +301,25 @@ CREATE INDEX ON "${selectedSchema}"."${selectedEntity}" USING ${selectedIndexTyp
)}
</CommandEmpty_Shadcn_>
<CommandGroup_Shadcn_>
<ScrollArea className={(entityTypes || []).length > 7 ? 'h-[210px]' : ''}>
{(entityTypes ?? []).map((entity) => (
<CommandItem_Shadcn_
key={entity.name}
className="cursor-pointer flex items-center justify-between space-x-2 w-full"
onSelect={() => {
setSelectedEntity(entity.name)
setTableDropdownOpen(false)
}}
onClick={() => {
setSelectedEntity(entity.name)
setTableDropdownOpen(false)
}}
>
<span>{entity.name}</span>
{selectedEntity === entity.name && (
<Check className="text-brand" strokeWidth={2} size={16} />
)}
</CommandItem_Shadcn_>
))}
</ScrollArea>
{entityTypes.map((entity) => (
<CommandItem_Shadcn_
key={entity.name}
className="cursor-pointer flex items-center justify-between space-x-2 w-full"
onSelect={() => {
setSelectedEntity(entity.name)
setTableDropdownOpen(false)
}}
onClick={() => {
setSelectedEntity(entity.name)
setTableDropdownOpen(false)
}}
>
<span>{entity.name}</span>
{selectedEntity === entity.name && (
<Check className="text-brand" strokeWidth={2} size={16} />
)}
</CommandItem_Shadcn_>
))}
</CommandGroup_Shadcn_>
</CommandList_Shadcn_>
</Command_Shadcn_>
@@ -208,7 +208,9 @@ export const ReadReplicaEligibilityWarnings = () => {
source="read-replicas"
addon="spendCap"
className="mt-2"
/>
>
Disable spend cap
</UpgradePlanButton>
</Admonition>
)
}
@@ -0,0 +1,111 @@
import { BaseEdge, EdgeLabelRenderer, getSmoothStepPath, type EdgeProps } from '@xyflow/react'
import { useParams } from 'common'
import { useReplicationLagQuery } from 'data/read-replicas/replica-lag-query'
import { formatDatabaseID } from 'data/read-replicas/replicas.utils'
import { Loader2, X } from 'lucide-react'
import { cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
type EdgeData = {
type: string
identifier: string
isComingUp: boolean
isReplicating: boolean
isFailed: boolean
shiftEdgeEnd: boolean
}
export const SmoothstepEdge = ({
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
style = {},
markerEnd,
data,
}: EdgeProps) => {
const { ref } = useParams()
const { data: project } = useSelectedProjectQuery()
const { type, identifier, isComingUp, isReplicating, isFailed, shiftEdgeEnd } = (data ||
{}) as EdgeData
const formattedId = type === 'replica' ? formatDatabaseID(identifier ?? '') : identifier
const [edgePath, labelX, labelY] = getSmoothStepPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
})
const {
data: lagDuration,
isPending: isLoading,
isError,
} = useReplicationLagQuery(
{
id: identifier,
projectRef: ref,
connectionString: project?.connectionString,
},
{ enabled: type === 'replica' && isReplicating, refetchInterval: 10000 }
)
const lagValue = Number(lagDuration?.toFixed(2) ?? 0).toLocaleString()
const hasReplicationLagData = data !== undefined && !isError && isReplicating
return (
<>
<BaseEdge path={edgePath} markerEnd={markerEnd} style={style} />
{isFailed && (
<EdgeLabelRenderer>
<div
className={cn('bg-surface-100 p-1 rounded absolute nodrag nopan border')}
style={{
transform: `translate(-50%, -50%) translate(${targetX - 30}px,${targetY}px)`,
}}
>
<X size={12} strokeWidth={4} className="text-destructive" />
</div>
</EdgeLabelRenderer>
)}
{(isComingUp || hasReplicationLagData) && (
<EdgeLabelRenderer>
<Tooltip>
<TooltipTrigger asChild>
<div
className={cn(
'bg-surface-100 py-1 rounded absolute nodrag nopan border',
isLoading || isComingUp ? 'px-1' : 'px-1.5'
)}
style={{
transform: `translate(-50%, -50%) translate(${shiftEdgeEnd ? targetX - 30 : labelX}px,${shiftEdgeEnd ? targetY : labelY}px)`,
pointerEvents: 'all',
}}
>
{isLoading || isComingUp ? (
<Loader2 size={12} className="animate-spin" />
) : (
<p className="font-mono text-xs">{lagValue}s</p>
)}
</div>
</TooltipTrigger>
{!isComingUp && (
<TooltipContent side="bottom" align="center">
{isLoading
? `Checking replication lag for replica ID: ${formattedId}`
: `Replication lag (seconds) for replica ID: ${formattedId}`}
</TooltipContent>
)}
</Tooltip>
</EdgeLabelRenderer>
)}
</>
)
}
@@ -1,7 +1,7 @@
import { useParams } from 'common'
import { AnalyticsBucket, BigQuery, Database } from 'icons'
import { PropsWithChildren, useMemo } from 'react'
import { Handle, Position } from 'reactflow'
import { Handle, Position } from '@xyflow/react'
import { AWS_REGIONS } from 'shared-data'
import { cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
@@ -59,7 +59,11 @@ export const PrimaryDatabaseNode = () => {
src={`${BASE_PATH}/img/regions/${project?.region}.svg`}
/>
)}
{hasReplication && <Handle type="source" position={Position.Right} className="opacity-25" />}
<Handle
type="source"
position={Position.Right}
className={hasReplication ? 'opacity-25' : 'opacity-0'}
/>
</NodeContainer>
)
}
@@ -1,5 +1,5 @@
import dagre from '@dagrejs/dagre'
import { Edge, Node, Position } from 'reactflow'
import { Edge, Node, Position } from '@xyflow/react'
import { NODE_WIDTH } from './Nodes'
@@ -2,7 +2,7 @@ import { useQueryClient } from '@tanstack/react-query'
import { useParams } from 'common'
import { useTheme } from 'next-themes'
import { useEffect, useMemo } from 'react'
import ReactFlow, { Background, ReactFlowProvider, useReactFlow } from 'reactflow'
import { Background, ColorMode, ReactFlow, ReactFlowProvider, useReactFlow } from '@xyflow/react'
import { getStatusName } from '../Pipeline.utils'
import { PrimaryDatabaseNode, ReadReplicaNode, ReplicationNode } from './Nodes'
@@ -14,7 +14,10 @@ import { ReplicationPipelineStatusResponse } from '@/data/replication/pipeline-s
import { useReplicationPipelinesQuery } from '@/data/replication/pipelines-query'
import { timeout } from '@/lib/helpers'
import 'reactflow/dist/style.css'
import '@xyflow/react/dist/style.css'
import { SmoothstepEdge } from './Edges'
import { REPLICA_STATUS } from '@/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.constants'
export const ReplicationDiagram = () => {
return (
@@ -30,6 +33,8 @@ const nodeTypes = {
readReplica: ReadReplicaNode,
}
const edgeTypes = { smoothstep: SmoothstepEdge }
const ReplicationDiagramContent = () => {
const reactFlow = useReactFlow()
const { resolvedTheme } = useTheme()
@@ -39,7 +44,10 @@ const ReplicationDiagramContent = () => {
const { data: databases = [], isSuccess: isSuccessReplicas } = useReadReplicasQuery({
projectRef,
})
const readReplicas = databases.filter((x) => x.identifier !== projectRef)
const readReplicas = useMemo(
() => databases.filter((x) => x.identifier !== projectRef),
[databases, projectRef]
)
const { data, isSuccess: isSuccessDestinations } = useReplicationDestinationsQuery({
projectRef,
@@ -82,6 +90,20 @@ const ReplicationDiagramContent = () => {
opacity: isReplicating ? 1 : 0.4,
strokeDasharray: isReplicating ? undefined : '5 5',
},
data: {
type: 'replica',
identifier: x.identifier,
shiftEdgeEnd: readReplicas.length + destinations.length > 1,
isReplicating,
isComingUp: [
REPLICA_STATUS.COMING_UP,
REPLICA_STATUS.INIT_READ_REPLICA,
REPLICA_STATUS.UNKNOWN,
].includes(x.status),
isFailed: [REPLICA_STATUS.ACTIVE_UNHEALTHY, REPLICA_STATUS.INIT_FAILED].includes(
x.status
),
},
}
}),
...destinations.map((x) => {
@@ -103,6 +125,14 @@ const ReplicationDiagramContent = () => {
opacity: isReplicating ? 1 : 0.4,
strokeDasharray: isReplicating ? undefined : '5 5',
},
data: {
type: 'etl',
identifier: x.id,
shiftEdgeEnd: readReplicas.length + destinations.length > 1,
isReplicating,
isComingUp: ['starting'].includes(statusName ?? ''),
isFailed: ['failed'].includes(statusName ?? ''),
},
}
}),
]
@@ -122,12 +152,16 @@ const ReplicationDiagramContent = () => {
}
useEffect(() => {
if (nodes.length > 0 && isSuccessDestinations && isSuccessReplicas) setReactFlow()
if (nodes.length > 0 && isSuccessDestinations && isSuccessReplicas) {
setReactFlow()
}
}, [nodes, isSuccessDestinations, isSuccessReplicas])
return (
<div className="nowheel relative min-h-[350px]">
<ReactFlow
// FIXME: https://github.com/xyflow/xyflow/issues/4876
colorMode={'' as unknown as ColorMode}
fitView
fitViewOptions={{ minZoom: 0.8, maxZoom: 0.9 }}
className="bg"
@@ -137,10 +171,11 @@ const ReplicationDiagramContent = () => {
nodesConnectable={false}
zoomOnDoubleClick={false}
edgesFocusable={false}
edgesUpdatable={false}
edgesReconnectable={false}
defaultNodes={[]}
defaultEdges={[]}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
proOptions={{ hideAttribution: true }}
>
<Background color={backgroundPatternColor} />
@@ -0,0 +1,202 @@
import {
BaseEdge,
Edge,
EdgeLabelRenderer,
EdgeProps,
getSmoothStepPath,
Position,
useReactFlow,
} from '@xyflow/react'
import { ArrowLeft, ArrowRight } from 'lucide-react'
import { useEffect, useRef, useState } from 'react'
import { Badge, cn } from 'ui'
import { EdgeData } from './Schemas.constants'
import { useQuerySchemaState } from '@/hooks/misc/useSchemaQueryState'
import { useStaticEffectEvent } from '@/hooks/useStaticEffectEvent'
export const DefaultEdge = ({
id,
animated,
data,
deletable,
selectable,
source,
sourceX,
sourceY,
sourceHandleId,
sourcePosition = Position.Bottom,
target,
targetX,
targetY,
targetHandleId,
targetPosition = Position.Top,
selected,
pathOptions,
...props
}: EdgeProps<Edge<EdgeData>>) => {
const [edgePath, labelX, labelY] = getSmoothStepPath({
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
borderRadius: pathOptions?.borderRadius,
offset: pathOptions?.offset,
stepPosition: pathOptions?.stepPosition,
})
return (
<>
<BaseEdge
id={id}
path={edgePath}
className={cn(selected ? '!stroke-brand' : undefined)}
{...props}
/>
{data && selected ? (
<EdgeRelationInfo
source={source}
target={target}
edgePath={edgePath}
labelX={labelX}
labelY={labelY}
sourceX={sourceX}
targetX={targetX}
data={data}
/>
) : null}
</>
)
}
const EdgeRelationInfo = ({
data,
edgePath,
source,
target,
labelX,
labelY,
targetX,
sourceX,
}: {
data: EdgeData
edgePath: string
source: string
target: string
labelX: number
labelY: number
sourceX: number
targetX: number
}) => {
const [show, setShow] = useState(false)
const reactFlowInstance = useReactFlow()
const checkIfShouldBeDisplayed = useStaticEffectEvent(
(relationInfoElement: HTMLDivElement | null) => {
if (!relationInfoElement) return
const sourceNode = reactFlowInstance.getNode(source)
const targetNode = reactFlowInstance.getNode(target)
if (!sourceNode || !targetNode) return
const relationInfoRect = relationInfoElement.getBoundingClientRect()
// Get the origin position of the relation information badge in the ReactFlow coordinates
const relationInfoOriginPositionInReactFlow = reactFlowInstance.screenToFlowPosition({
x: relationInfoRect.x,
y: relationInfoRect.y,
})
// Get the end position (origin + dimensions) of the relation information badge in the ReactFlow coordinates
const relationInfoTargetPositionInReactFlow = reactFlowInstance.screenToFlowPosition({
x: relationInfoRect.x + relationInfoRect.width,
y: relationInfoRect.y + relationInfoRect.height,
})
// Create a ReactFlow Rect from the computed position above
const relationInfoReactFlowRect = {
x: relationInfoOriginPositionInReactFlow.x,
y: relationInfoOriginPositionInReactFlow.y,
width: relationInfoTargetPositionInReactFlow.x - relationInfoOriginPositionInReactFlow.x,
height: relationInfoTargetPositionInReactFlow.y - relationInfoOriginPositionInReactFlow.y,
}
// Check whether the relation information badge is intersecting with either the source or target node
const isNodeIntersectingWithSource = reactFlowInstance.isNodeIntersecting(
sourceNode,
relationInfoReactFlowRect
)
const isNodeIntersectingWithTarget = reactFlowInstance.isNodeIntersecting(
targetNode,
relationInfoReactFlowRect
)
// If it is, hide it as they are too close
setShow(!isNodeIntersectingWithSource && !isNodeIntersectingWithTarget)
}
)
return (
<EdgeLabelRenderer>
<Badge
ref={checkIfShouldBeDisplayed}
className={cn(
'absolute pointer-events-auto z-50 p-1 rounded-[4px] gap-1 outline outline-1 outline-brand',
show ? 'opacity-100' : 'opacity-0'
)}
style={{
transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)`,
}}
>
{
// Show the columns in the order of the schema instead of the Postgre relation order
sourceX < targetX ? (
<>
<EdgeNodeData
schema={data.sourceSchemaName}
table={data.sourceName}
column={data.sourceColumnName}
/>
<ArrowRight size={12} />
<EdgeNodeData
schema={data.targetSchemaName}
table={data.targetName}
column={data.targetColumnName}
/>
</>
) : (
<>
<EdgeNodeData
schema={data.targetSchemaName}
table={data.targetName}
column={data.targetColumnName}
/>
<ArrowLeft size={12} />
<EdgeNodeData
schema={data.sourceSchemaName}
table={data.sourceName}
column={data.sourceColumnName}
/>
</>
)
}
</Badge>
</EdgeLabelRenderer>
)
}
const EdgeNodeData = ({
schema,
table,
column,
}: {
schema: string
table: string
column: string
}) => {
const { selectedSchema } = useQuerySchemaState()
return (
<Badge className="normal-case text-[8px]">
{selectedSchema === schema ? '' : `${schema}.`}
{table}.{column}
</Badge>
)
}
@@ -1,13 +1,23 @@
import type { PostgresSchema, PostgresTable } from '@supabase/postgres-meta'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import {
Background,
BackgroundVariant,
ColorMode,
Edge,
MiniMap,
Node,
OnSelectionChangeParams,
ReactFlow,
useReactFlow,
} from '@xyflow/react'
import { toPng, toSvg } from 'html-to-image'
import { Check, Copy, Download, Loader2, Plus } from 'lucide-react'
import { useTheme } from 'next-themes'
import Link from 'next/link'
import { useEffect, useMemo, useRef, useState } from 'react'
import ReactFlow, { Background, BackgroundVariant, MiniMap, useReactFlow } from 'reactflow'
import 'reactflow/dist/style.css'
import '@xyflow/react/dist/style.css'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import AlertError from 'components/ui/AlertError'
@@ -34,10 +44,13 @@ import {
import { Admonition } from 'ui-patterns/admonition'
import { SidePanelEditor } from '../../TableGridEditor/SidePanelEditor/SidePanelEditor'
import { DefaultEdge } from './DefaultEdge'
import { SchemaGraphContextProvider, SchemaGraphContextType } from './SchemaGraphContext'
import { SchemaGraphLegend } from './SchemaGraphLegend'
import { EdgeData, TableNodeData } from './Schemas.constants'
import { getGraphDataFromTables, getLayoutedElementsViaDagre } from './Schemas.utils'
import { TableNode } from './SchemaTableNode'
import { useStaticEffectEvent } from '@/hooks/useStaticEffectEvent'
// [Joshen] Persisting logic: Only save positions to local storage WHEN a node is moved OR when explicitly clicked to reset layout
@@ -70,6 +83,12 @@ export const SchemaGraph = () => {
}),
[]
)
const edgeTypes = useMemo(
() => ({
default: DefaultEdge,
}),
[]
)
const {
data: schemas,
@@ -115,14 +134,17 @@ export const SchemaGraph = () => {
const nodes = reactFlowInstance.getNodes()
const edges = reactFlowInstance.getEdges()
getLayoutedElementsViaDagre(nodes, edges)
getLayoutedElementsViaDagre(
nodes.filter((item) => item.type === 'table') as Node<TableNodeData>[],
edges
)
reactFlowInstance.setNodes(nodes)
reactFlowInstance.setEdges(edges)
setTimeout(() => reactFlowInstance.fitView({}))
saveNodePositions()
}
const saveNodePositions = () => {
const saveNodePositions = useStaticEffectEvent(() => {
if (schema === undefined) return console.error('Schema is required')
const nodes = reactFlowInstance.getNodes()
@@ -132,7 +154,18 @@ export const SchemaGraph = () => {
}, {})
setStoredPositions(nodesPositionData)
}
}
})
const [selectedEdge, setSelectedEdge] = useState<Edge | undefined>(undefined)
const handleSelectionChange = useStaticEffectEvent(
(params: OnSelectionChangeParams<Node<TableNodeData>, Edge<EdgeData>>) => {
if (params.edges.length === 1) {
setSelectedEdge(params.edges[0])
return
}
setSelectedEdge(undefined)
}
)
const downloadImage = (format: 'png' | 'svg') => {
const reactflowViewport = document.querySelector('.react-flow__viewport') as HTMLElement
@@ -223,6 +256,7 @@ export const SchemaGraph = () => {
const schemaGraphPanelEditorContext = useMemo<SchemaGraphContextType>(
() => ({
selectedEdge,
isDownloading,
onEditColumn: (tableId, columnId) => {
const table = tables.find((table) => table.id === tableId)
@@ -242,7 +276,7 @@ export const SchemaGraph = () => {
snap.onEditTable()
},
}),
[tables, snap, isDownloading]
[tables, snap, isDownloading, selectedEdge]
)
return (
@@ -368,20 +402,24 @@ export const SchemaGraph = () => {
) : (
<SchemaGraphContextProvider value={schemaGraphPanelEditorContext}>
<div className="w-full h-full">
<ReactFlow
<ReactFlow<Node<TableNodeData>, Edge<EdgeData>>
// FIXME: https://github.com/xyflow/xyflow/issues/4876
colorMode={'' as unknown as ColorMode}
defaultNodes={[]}
defaultEdges={[]}
defaultEdgeOptions={{
type: 'smoothstep',
type: 'default',
animated: true,
deletable: false,
}}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
fitView
minZoom={0.8}
maxZoom={1.8}
proOptions={{ hideAttribution: true }}
onNodeDragStop={() => saveNodePositions()}
onNodeDragStop={saveNodePositions}
onSelectionChange={handleSelectionChange}
>
<Background
gap={16}
@@ -1,7 +1,9 @@
import { Edge } from '@xyflow/react'
import { createContext, useContext, type ReactNode } from 'react'
export type SchemaGraphContextType = {
isDownloading: boolean
selectedEdge: Edge | undefined
onEditColumn: (tableId: number, columnId: string) => void
onEditTable: (tableId: number) => void
}
@@ -1,4 +1,5 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { Handle, Node, NodeProps } from '@xyflow/react'
import { buildTableEditorUrl } from 'components/grid/SupabaseGrid.utils'
import { TableEditor } from 'icons'
import {
@@ -13,7 +14,6 @@ import {
Table2,
} from 'lucide-react'
import { useRouter } from 'next/router'
import { Handle, NodeProps } from 'reactflow'
import {
Button,
cn,
@@ -28,6 +28,7 @@ import {
} from 'ui'
import { useSchemaGraphContext } from './SchemaGraphContext'
import { TableNodeData } from './Schemas.constants'
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
@@ -35,30 +36,13 @@ import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
export const TABLE_NODE_WIDTH = 320
export const TABLE_NODE_ROW_HEIGHT = 40
export type TableNodeData = {
id: number
schema: string
name: string
ref?: string
isForeign: boolean
description: string
columns: {
id: string
isPrimary: boolean
isNullable: boolean
isUnique: boolean
isIdentity: boolean
name: string
format: string
}[]
}
export const TableNode = ({
id,
data,
targetPosition,
sourcePosition,
placeholder,
}: NodeProps<TableNodeData> & { placeholder?: boolean }) => {
}: NodeProps<Node<TableNodeData>> & { placeholder?: boolean }) => {
// Important styles is a nasty hack to use Handles (required for edges calculations), but do not show them in the UI.
// ref: https://github.com/wbkd/react-flow/discussions/2698
const hiddenNodeConnector = '!h-px !w-px !min-w-0 !min-h-0 !cursor-grab !border-0 !opacity-0'
@@ -71,10 +55,18 @@ export const TableNode = ({
const router = useRouter()
const itemHeight = 'h-[22px]'
const hasEdgesSelected =
schemaGraphContext.selectedEdge?.source === id || schemaGraphContext.selectedEdge?.target === id
return (
<>
<article>
{data.isForeign ? (
<header className="text-[0.55rem] px-2 py-1 border-[0.5px] rounded-[4px] bg-alternative flex gap-1 items-center">
<header
className={cn(
'text-[0.55rem] px-2 py-1 border-[0.5px] rounded-[4px] bg-alternative flex gap-1 items-center',
hasEdgesSelected ? 'outline outline-1 outline-brand' : undefined
)}
>
{data.name}
{targetPosition && (
<Handle
@@ -87,7 +79,10 @@ export const TableNode = ({
</header>
) : (
<div
className="border-[0.5px] overflow-hidden rounded-[4px] shadow-sm"
className={cn(
'border-[0.5px] overflow-hidden rounded-[4px] shadow-sm',
hasEdgesSelected ? 'outline outline-1 outline-brand' : undefined
)}
style={{ width: TABLE_NODE_WIDTH / 2 }}
>
<header
@@ -216,7 +211,13 @@ export const TableNode = ({
</div>
<div className="flex w-full justify-between min-w-0">
<span
className="text-ellipsis overflow-hidden whitespace-nowrap min-w-0 max-w-[80%]"
className={cn(
'text-ellipsis overflow-hidden whitespace-nowrap min-w-0 max-w-[80%]',
schemaGraphContext.selectedEdge?.sourceHandle === column.id ||
schemaGraphContext.selectedEdge?.targetHandle === column.id
? 'text-brand'
: undefined
)}
title={column.name}
>
{column.name}
@@ -230,7 +231,7 @@ export const TableNode = ({
type="target"
id={column.id}
position={targetPosition}
className={cn(hiddenNodeConnector, '!left-0')}
className={cn(hiddenNodeConnector)}
/>
)}
{sourcePosition && (
@@ -238,7 +239,7 @@ export const TableNode = ({
type="source"
id={column.id}
position={sourcePosition}
className={cn(hiddenNodeConnector, '!right-0')}
className={cn(hiddenNodeConnector)}
/>
)}
<DropdownMenu>
@@ -289,6 +290,6 @@ export const TableNode = ({
))}
</div>
)}
</>
</article>
)
}
@@ -0,0 +1,26 @@
export type TableNodeData = {
id: number
schema: string
name: string
ref?: string
isForeign: boolean
description: string
columns: {
id: string
isPrimary: boolean
isNullable: boolean
isUnique: boolean
isIdentity: boolean
name: string
format: string
}[]
}
export type EdgeData = {
sourceName: string
sourceSchemaName: string
sourceColumnName: string
targetName: string
targetSchemaName: string
targetColumnName: string
}
@@ -1,14 +1,15 @@
import dagre from '@dagrejs/dagre'
import type { PostgresSchema, PostgresTable } from '@supabase/postgres-meta'
import { Edge, Node, Position } from '@xyflow/react'
import { uniqBy } from 'lodash'
import { Edge, Node, Position } from 'reactflow'
import 'reactflow/dist/style.css'
import '@xyflow/react/dist/style.css'
import { LOCAL_STORAGE_KEYS } from 'common'
import { tryParseJson } from 'lib/helpers'
import { TABLE_NODE_ROW_HEIGHT, TABLE_NODE_WIDTH, TableNodeData } from './SchemaTableNode'
import { TableNodeData } from './Schemas.constants'
import { TABLE_NODE_ROW_HEIGHT, TABLE_NODE_WIDTH } from './SchemaTableNode'
const NODE_SEP = 25
const RANK_SEP = 50
@@ -107,6 +108,15 @@ export async function getGraphDataFromTables(
sourceHandle,
target: targetId,
targetHandle: targetId,
deletable: false,
data: {
sourceName: rel.source_table_name,
sourceSchemaName: rel.source_schema,
sourceColumnName: rel.source_column_name,
targetName: rel.target_table_name,
targetSchemaName: rel.target_table_schema,
targetColumnName: rel.target_column_name,
},
})
}
@@ -132,6 +142,15 @@ export async function getGraphDataFromTables(
sourceHandle,
target,
targetHandle,
type: 'default',
data: {
sourceName: rel.source_table_name,
sourceSchemaName: rel.source_schema,
sourceColumnName: rel.source_column_name,
targetName: rel.target_table_name,
targetSchemaName: rel.target_table_schema,
targetColumnName: rel.target_column_name,
},
})
}
}
@@ -163,7 +182,7 @@ function findTablesHandleIds(
return []
}
export const getLayoutedElementsViaDagre = (nodes: Node[], edges: Edge[]) => {
export const getLayoutedElementsViaDagre = (nodes: Node<TableNodeData>[], edges: Edge[]) => {
const dagreGraph = new dagre.graphlib.Graph()
dagreGraph.setDefaultEdgeLabel(() => ({}))
dagreGraph.setGraph({
@@ -204,7 +223,7 @@ export const getLayoutedElementsViaDagre = (nodes: Node[], edges: Edge[]) => {
}
const getLayoutedElementsViaLocalStorage = (
nodes: Node[],
nodes: Node<TableNodeData>[],
edges: Edge[],
positions: { [key: string]: { x: number; y: number } }
) => {
@@ -34,6 +34,7 @@ export const INVOCATION_TABS: InvocationTab[] = [
language: 'js',
hideLineNumbers: true,
code: ({ functionName }) => `import { createClient } from '@supabase/supabase-js'
const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_ANON_KEY)
const { data, error } = await supabase.functions.invoke('${functionName}', {
body: { name: 'Functions' },
@@ -13,6 +13,7 @@ import {
Card,
CardContent,
CardFooter,
CardHeader,
cn,
CodeBlock,
copyToClipboard,
@@ -182,11 +183,22 @@ export const EdgeFunctionDetails = () => {
layout="flex-row-reverse"
description={
<>
Requires a JWT signed{' '}
<em className="text-brand not-italic">only by the legacy secret</em>{' '}
in the <code>Authorization</code> header. The <code>anon</code> key
satisfies this. Recommended: OFF with JWT and custom auth logic in
your function code.
<p className="mb-2">
Requires a JWT signed{' '}
<em className="text-foreground not-italic">
only by the legacy secret
</em>{' '}
in the{' '}
<code className="text-code-inline !break-keep">
Authorization
</code>{' '}
header. The <code className="text-code-inline">anon</code> key
satisfies this.
</p>
<p>
Recommended: OFF with JWT and custom auth logic in your function
code.
</p>
</>
}
>
@@ -265,15 +277,16 @@ export const EdgeFunctionDetails = () => {
})
return (
<TabsContent key={tab.id} value={tab.id} className="mt-4 px-6">
<TabsContent key={tab.id} value={tab.id}>
<CodeBlock
value={code}
wrapperClassName="[&>div]:top-0 [&>div]:right-3 px-6"
className={cn(
'p-0 text-xs !mt-0 border-none [&>code]:!whitespace-pre-wrap',
'p-0 text-xs !mt-0 border-none ',
showKey ? '[&>code]:break-all' : '[&>code]:break-words'
)}
language={tab.language}
wrapLines={true}
wrapLines={false}
hideLineNumbers={tab.hideLineNumbers}
handleCopy={() => {
copyToClipboard(
@@ -313,7 +326,7 @@ export const EdgeFunctionDetails = () => {
description: 'Download the function to your local machine',
jsx: () => (
<>
<span className="text-brand-600">supabase</span> functions download{' '}
<span className="text-brand">supabase</span> functions download{' '}
{selectedFunction?.slug}
</>
),
@@ -16,8 +16,7 @@ export const generateCLICommands = ({
jsx: () => {
return (
<>
<span className="text-brand-600">supabase</span> functions deploy{' '}
{selectedFunction?.slug}
<span className="text-brand">supabase</span> functions deploy {selectedFunction?.slug}
</>
)
},
@@ -29,8 +28,7 @@ export const generateCLICommands = ({
jsx: () => {
return (
<>
<span className="text-brand-600">supabase</span> functions delete{' '}
{selectedFunction?.slug}
<span className="text-brand">supabase</span> functions delete {selectedFunction?.slug}
</>
)
},
@@ -45,7 +43,7 @@ export const generateCLICommands = ({
jsx: () => {
return (
<>
<span className="text-brand-600">supabase</span> secrets list
<span className="text-brand">supabase</span> secrets list
</>
)
},
@@ -57,7 +55,7 @@ export const generateCLICommands = ({
jsx: () => {
return (
<>
<span className="text-brand-600">supabase</span> secrets set NAME1=VALUE1 NAME2=VALUE2
<span className="text-brand">supabase</span> secrets set NAME1=VALUE1 NAME2=VALUE2
</>
)
},
@@ -69,7 +67,7 @@ export const generateCLICommands = ({
jsx: () => {
return (
<>
<span className="text-brand-600">supabase</span> secrets unset NAME1 NAME2
<span className="text-brand">supabase</span> secrets unset NAME1 NAME2
</>
)
},
@@ -86,7 +84,7 @@ export const generateCLICommands = ({
jsx: () => {
return (
<>
<span className="text-brand-600">curl</span> -L -X POST '{functionUrl}'{' '}
<span className="text-brand">curl</span> -L -X POST '{functionUrl}'{' '}
{selectedFunction?.verify_jwt
? `-H
'Authorization: Bearer [YOUR ANON KEY]' `
@@ -73,9 +73,11 @@ export const EdgeFunctionsListItem = ({ function: item }: EdgeFunctionsListItemP
</div>
</TableCell>
<TableCell className="hidden 2xl:table-cell whitespace-nowrap">
<p className="text-foreground-light">
{dayjs(item.created_at).format('DD MMM, YYYY HH:mm')}
</p>
<TimestampInfo
className="text-sm text-foreground-light whitespace-nowrap"
utcTimestamp={item.created_at}
label={dayjs(item.created_at).fromNow()}
/>
</TableCell>
<TableCell className="lg:table-cell">
<TimestampInfo
@@ -0,0 +1,30 @@
import { describe, expect, it } from 'vitest'
import { buildEdgeFunctionHeaderAddActions } from './httpHeaderAddActions'
describe('buildEdgeFunctionHeaderAddActions', () => {
it('includes the apikey header when requested', () => {
const [authAction] = buildEdgeFunctionHeaderAddActions({
apiKey: 'secret-key',
includeApiKeyHeader: true,
createRow: (name, value) => ({ name, value }),
})
expect(authAction.createRows()).toEqual([
{ name: 'Authorization', value: 'Bearer secret-key' },
{ name: 'apikey', value: 'secret-key' },
])
})
it('omits the apikey header when not requested', () => {
const [authAction] = buildEdgeFunctionHeaderAddActions({
apiKey: 'service-key',
includeApiKeyHeader: false,
createRow: (name, value) => ({ name, value }),
})
expect(authAction.createRows()).toEqual([
{ name: 'Authorization', value: 'Bearer service-key' },
])
})
})
@@ -0,0 +1,30 @@
import type { KeyValueFieldArrayAction } from 'ui-patterns/form/KeyValueFieldArray/KeyValueFieldArray'
interface BuildEdgeFunctionHeaderAddActionsParams<TRow> {
apiKey: string
includeApiKeyHeader?: boolean
createRow: (name: string, value: string) => TRow
}
export const buildEdgeFunctionHeaderAddActions = <TRow>({
apiKey,
includeApiKeyHeader = false,
createRow,
}: BuildEdgeFunctionHeaderAddActionsParams<TRow>): KeyValueFieldArrayAction<TRow>[] => [
{
key: 'add-auth-header',
label: 'Add auth header with secret key',
description: 'Required if your edge function enforces JWT verification',
createRows: () => [
createRow('Authorization', `Bearer ${apiKey}`),
...(includeApiKeyHeader ? [createRow('apikey', apiKey)] : []),
],
},
{
key: 'add-source-header',
label: 'Add custom source header',
description: 'Useful to verify that the edge function was triggered from this webhook',
createRows: () => createRow('x-supabase-webhook-source', '[Use a secret value]'),
separatorAbove: true,
},
]
@@ -3,15 +3,16 @@ import { CreateCronJobSheet } from 'components/interfaces/Integrations/CronJobs/
import { CronJob } from 'data/database-cron-jobs/database-cron-jobs-infinite-query'
import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query'
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
import { useInfiniteScroll } from 'hooks/misc/useInfiniteScroll'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { cleanPointerEventsNoneOnBody, isAtBottom } from 'lib/helpers'
import { cleanPointerEventsNoneOnBody } from 'lib/helpers'
import { createNavigationHandler } from 'lib/navigation'
import { isGreaterThanOrEqual } from 'lib/semver'
import { Loader2 } from 'lucide-react'
import { useRouter } from 'next/router'
import { parseAsBoolean, parseAsString, useQueryState } from 'nuqs'
import { MouseEvent, UIEvent, useEffect, useMemo, useRef, useState } from 'react'
import { MouseEvent, useEffect, useMemo, useState } from 'react'
import { toast } from 'sonner'
import { LoadingLine } from 'ui'
@@ -92,24 +93,12 @@ export const CronjobsTab = () => {
[org?.slug, ref, sendEvent, setCronJobForEditing, setCronJobForDeletion]
)
const xScroll = useRef<number>(0)
const handleScroll = (event: UIEvent<HTMLDivElement>) => {
const isScrollingHorizontally = xScroll.current !== event.currentTarget.scrollLeft
xScroll.current = event.currentTarget.scrollLeft
if (
grid.isLoading ||
grid.isFetchingNextPage ||
isScrollingHorizontally ||
!isAtBottom(event) ||
!grid.hasNextPage
) {
return
}
grid.fetchNextPage()
}
const handleScroll = useInfiniteScroll({
isLoading: grid.isLoading,
isFetchingNextPage: grid.isFetchingNextPage,
hasNextPage: grid.hasNextPage,
fetchNextPage: grid.fetchNextPage,
})
const onOpenCreateJobSheet = () => {
sendEvent({
@@ -1,37 +1,20 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { ChevronDown, Plus, Trash } from 'lucide-react'
import { useFieldArray } from 'react-hook-form'
import { useFormContext } from 'react-hook-form'
import { useParams } from 'common'
import { getKeys, useAPIKeysQuery } from 'data/api-keys/api-keys-query'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import {
Button,
cn,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
FormControl_Shadcn_,
FormField_Shadcn_,
FormItem_Shadcn_,
FormLabel_Shadcn_,
FormMessage_Shadcn_,
Input_Shadcn_,
SheetSection,
} from 'ui'
import { FormLabel_Shadcn_, SheetSection } from 'ui'
import { KeyValueFieldArray } from 'ui-patterns/form/KeyValueFieldArray/KeyValueFieldArray'
import { CreateCronJobForm } from './CreateCronJobSheet/CreateCronJobSheet.constants'
import { buildEdgeFunctionHeaderAddActions } from '@/components/interfaces/Functions/httpHeaderAddActions'
interface HTTPHeaderFieldsSectionProps {
variant: 'edge_function' | 'http_request'
}
export const HTTPHeaderFieldsSection = ({ variant }: HTTPHeaderFieldsSectionProps) => {
// gets the fields through form context
const { fields, append, remove } = useFieldArray<CreateCronJobForm>({
name: 'values.httpHeaders',
})
const form = useFormContext<CreateCronJobForm>()
const { ref } = useParams()
const { can: canReadAPIKeys } = useAsyncCheckPermissions(PermissionAction.SECRETS_READ, '*')
@@ -42,114 +25,29 @@ export const HTTPHeaderFieldsSection = ({ variant }: HTTPHeaderFieldsSectionProp
const { serviceKey, secretKey } = getKeys(apiKeys)
const apiKey = secretKey?.api_key ?? serviceKey?.api_key ?? '[YOUR API KEY]'
const addActions =
variant === 'edge_function'
? buildEdgeFunctionHeaderAddActions({
apiKey,
includeApiKeyHeader: serviceKey?.type === 'secret',
createRow: (name: string, value: string) => ({ name, value }),
})
: []
return (
<SheetSection>
<FormLabel_Shadcn_>HTTP Headers</FormLabel_Shadcn_>
<div className="space-y-3 mt-1">
{fields.map((field, index) => (
<div key={field.id} className="flex items-center space-x-2">
<FormField_Shadcn_
name={`values.httpHeaders.${index}.name`}
render={({ field }) => (
<FormItem_Shadcn_ className="flex-1">
<FormControl_Shadcn_>
<Input_Shadcn_
{...field}
size="small"
className="w-full"
placeholder="Header name"
/>
</FormControl_Shadcn_>
<FormMessage_Shadcn_ />
</FormItem_Shadcn_>
)}
/>
<FormField_Shadcn_
name={`values.httpHeaders.${index}.value`}
render={({ field }) => (
<FormItem_Shadcn_ className="flex-1">
<FormControl_Shadcn_>
<Input_Shadcn_
{...field}
value={field.value}
size="small"
className="w-full"
placeholder="Header value"
/>
</FormControl_Shadcn_>
<FormMessage_Shadcn_ />
</FormItem_Shadcn_>
)}
/>
<Button
type="default"
icon={<Trash size={12} />}
onClick={() => remove(index)}
className="h-[34px] w-[34px]"
/>
</div>
))}
<div className="flex items-center">
<Button
type="default"
size="tiny"
icon={<Plus />}
className={cn(variant === 'edge_function' && 'rounded-r-none px-3 border-r-0')}
onClick={() => append({ name: '', value: '' })}
>
Add a new header
</Button>
{variant === 'edge_function' && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button type="default" className="rounded-l-none px-[4px] py-[5px]">
<ChevronDown size={14} />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" side="bottom">
<DropdownMenuItem
key="add-auth-header"
onClick={() => {
append({
name: 'Authorization',
value: `Bearer ${apiKey}`,
})
if (serviceKey?.type === 'secret') {
append({ name: 'apikey', value: apiKey })
}
}}
>
<div className="space-y-1">
<p className="block text-foreground">Add auth header with secret key</p>
<p className="text-foreground-light">
Required if your edge function enforces JWT verification
</p>
</div>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
key="add-source-header"
onClick={() =>
append({
name: 'x-supabase-webhook-source',
value: `[Use a secret value]`,
})
}
>
<div className="space-y-1">
<p className="block text-foreground">Add custom source header</p>
<p className="text-foreground-light">
Useful to verify that the edge function was triggered from this webhook
</p>
</div>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
</div>
<KeyValueFieldArray
control={form.control}
name="values.httpHeaders"
keyFieldName="name"
valueFieldName="value"
createEmptyRow={() => ({ name: '', value: '' })}
keyPlaceholder="Header name"
valuePlaceholder="Header value"
addLabel="Add a new header"
addActions={addActions}
/>
</SheetSection>
)
}
@@ -1,17 +1,18 @@
import dayjs from 'dayjs'
import { CircleCheck, CircleX, Loader } from 'lucide-react'
import { UIEvent, useCallback, useMemo } from 'react'
import DataGrid, { Column, Row } from 'react-data-grid'
import { useParams } from 'common'
import {
CronJobRun,
useCronJobRunsInfiniteQuery,
} from 'data/database-cron-jobs/database-cron-jobs-runs-infinite-query'
import dayjs from 'dayjs'
import { useInfiniteScroll } from 'hooks/misc/useInfiniteScroll'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { CircleCheck, CircleX, Loader } from 'lucide-react'
import { useMemo } from 'react'
import DataGrid, { Column, Row } from 'react-data-grid'
import { cn, CodeBlock, LoadingLine, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
import { TimestampInfo } from 'ui-patterns'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import { calculateDuration, formatDate } from './CronJobs.utils'
import CronJobsEmptyState from './CronJobsEmptyState'
@@ -146,10 +147,6 @@ const columns = cronJobColumns.map((col) => {
return result
})
function isAtBottom({ currentTarget }: UIEvent<HTMLDivElement>): boolean {
return currentTarget.scrollTop + 10 >= currentTarget.scrollHeight - currentTarget.clientHeight
}
export const PreviousRunsTab = () => {
const { childId } = useParams()
const { data: project } = useSelectedProjectQuery()
@@ -160,6 +157,8 @@ export const PreviousRunsTab = () => {
data,
isPending: isLoadingCronJobRuns,
isFetching,
isFetchingNextPage,
hasNextPage,
fetchNextPage,
} = useCronJobRunsInfiniteQuery(
{
@@ -172,15 +171,12 @@ export const PreviousRunsTab = () => {
const cronJobRuns = useMemo(() => data?.pages.flatMap((p) => p) || [], [data?.pages])
const handleScroll = useCallback(
(event: UIEvent<HTMLDivElement>) => {
if (isLoadingCronJobRuns || !isAtBottom(event)) return
// the cancelRefetch is to prevent the query from being refetched when the user scrolls back up and down again,
// resulting in multiple fetchNextPage calls
fetchNextPage({ cancelRefetch: false })
},
[fetchNextPage, isLoadingCronJobRuns]
)
const handleScroll = useInfiniteScroll({
isLoading: isLoadingCronJobRuns,
isFetchingNextPage,
hasNextPage,
fetchNextPage,
})
return (
<div className="h-full flex flex-col">
@@ -36,8 +36,6 @@ export const AddNewSecretModal = () => {
setSubmitting(true)
try {
setSubmitting(true)
await addSecret({
projectRef: project.ref,
connectionString: project?.connectionString,
@@ -83,6 +81,7 @@ export const AddNewSecretModal = () => {
<div className="mr-1">
<Button
type="default"
className="w-7"
icon={showSecretValue ? <EyeOff /> : <Eye />}
onClick={() => setShowSecretValue(!showSecretValue)}
/>
@@ -68,7 +68,7 @@ export const DeleteSecretModal = () => {
<div className="space-y-1">
<p className="text-sm">{selectedSecret?.description}</p>
<p className="text-sm text-foreground-light">
ID: <span className="font-mono">{selectedSecret?.id}</span>
ID: <code className="text-code-inline">{selectedSecret?.id}</code>
</p>
</div>
</Modal.Content>
@@ -186,7 +186,7 @@ export const EditSecretModal = () => {
aria-label={
showSecretValue ? `Hide secret value` : `Show secret value`
}
className="absolute right-2 top-1 px-3 py-2"
className="absolute right-1 top-1 w-7"
icon={showSecretValue ? <EyeOff /> : <Eye />}
onClick={() => setShowSecretValue(!showSecretValue)}
/>
@@ -25,7 +25,7 @@ import {
BillingCustomerDataForm,
type BillingCustomerDataFormValues,
} from './BillingCustomerDataForm'
import { TAX_IDS } from './TaxID.constants'
import { resolveStoredTaxId } from './TaxID.utils'
import { useBillingCustomerDataForm } from './useBillingCustomerDataForm'
export const BillingCustomerData = () => {
@@ -66,9 +66,8 @@ export const BillingCustomerData = () => {
tax_id_type: taxId?.type,
tax_id_value: taxId?.value,
tax_id_name: taxId
? TAX_IDS.find(
(option) => option.type === taxId.type && option.countryIso2 === taxId.country
)?.name || ''
? (resolveStoredTaxId(taxId.type, taxId.country, customerProfile?.address?.country)?.name ??
'')
: '',
}),
[customerProfile, taxId]
Loaded 100 of 279 files, more files were not shown because too many files have changed in this diff. Show more