diff --git a/.github/workflows/search.yml b/.github/workflows/search.yml index 7baa26f4981..c12c3ddd3e2 100644 --- a/.github/workflows/search.yml +++ b/.github/workflows/search.yml @@ -46,6 +46,7 @@ jobs: apps/docs apps/www/.env.local.example examples + packages supabase - uses: pnpm/action-setup@v4 diff --git a/apps/design-system/README.md b/apps/design-system/README.md index c4033664f80..47f009cebea 100644 --- a/apps/design-system/README.md +++ b/apps/design-system/README.md @@ -20,6 +20,16 @@ You can start editing the page by modifying `app/page.tsx`. The page auto-update This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. +### Watching for MDX changes + +If you would like to watch for changes to MDX files with hot reload, you can run the following command in a separate Terminal shell: + +``` +pnpm content:dev +``` + +This runs Contentlayer concurrently and watches for any changes. + ## Learn More To learn more about Next.js, take a look at the following resources: diff --git a/apps/design-system/package.json b/apps/design-system/package.json index 7a262a1a1a0..e6acd2763ef 100644 --- a/apps/design-system/package.json +++ b/apps/design-system/package.json @@ -10,6 +10,7 @@ "build:registry": "tsx --tsconfig ./tsconfig.scripts.json ./scripts/build-registry.mts && prettier --log-level silent --write \"registry/**/*.{ts,tsx,mdx}\" --cache", "start": "next start", "lint": "next lint", + "content:dev": "contentlayer2 dev", "content:build": "contentlayer2 build", "clean": "rimraf node_modules .next .turbo", "typecheck": "contentlayer2 build && tsc --noEmit -p tsconfig.json" diff --git a/apps/docs/app/page.tsx b/apps/docs/app/page.tsx index f6d2be258b8..85f11423335 100644 --- a/apps/docs/app/page.tsx +++ b/apps/docs/app/page.tsx @@ -3,6 +3,7 @@ import Link from 'next/link' import { cn, IconBackground, TextLink } from 'ui' import { IconPanel } from 'ui-patterns/IconPanel' +import { isFeatureEnabled } from 'common' import MenuIconPicker from '~/components/Navigation/NavigationMenu/MenuIconPicker' import { MIGRATION_PAGES } from '~/components/Navigation/NavigationMenu/NavigationMenu.constants' import { GlassPanelWithIconPicker } from '~/features/ui/GlassPanelWithIconPicker' @@ -10,6 +11,14 @@ import { IconPanelWithIconPicker } from '~/features/ui/IconPanelWithIconPicker' import HomeLayout from '~/layouts/HomeLayout' import { BASE_PATH } from '~/lib/constants' +const { sdkCsharp, sdkDart, sdkKotlin, sdkPython, sdkSwift } = isFeatureEnabled([ + 'sdk:csharp', + 'sdk:dart', + 'sdk:kotlin', + 'sdk:python', + 'sdk:swift', +]) + const generateMetadata = async (_, parent: ResolvingMetadata): Promise => { const parentAlternates = (await parent).alternates @@ -119,31 +128,37 @@ const clientLibraries = [ title: 'Javascript', icon: 'reference-javascript', href: '/reference/javascript/introduction', + enabled: true, }, { title: 'Flutter', icon: 'reference-dart', href: '/reference/dart/introduction', + enabled: sdkDart, }, { title: 'Python', icon: 'reference-python', href: '/reference/python/introduction', + enabled: sdkPython, }, { title: 'C#', icon: 'reference-csharp', href: '/reference/csharp/introduction', + enabled: sdkCsharp, }, { title: 'Swift', icon: 'reference-swift', href: '/reference/swift/introduction', + enabled: sdkSwift, }, { title: 'Kotlin', icon: 'reference-kotlin', href: '/reference/kotlin/introduction', + enabled: sdkKotlin, }, ] @@ -224,18 +239,21 @@ const HomePage = () => (
- {clientLibraries.map((library) => { - return ( - - - - ) - })} + {clientLibraries + .filter((library) => library.enabled) + + .map((library) => { + return ( + + + + ) + })}
diff --git a/apps/docs/components/GuidesTableOfContents.tsx b/apps/docs/components/GuidesTableOfContents.tsx index 9d780a46be9..ebb1fd4158f 100644 --- a/apps/docs/components/GuidesTableOfContents.tsx +++ b/apps/docs/components/GuidesTableOfContents.tsx @@ -1,6 +1,7 @@ 'use client' import { usePathname } from 'next/navigation' +import { isFeatureEnabled } from 'common' import { cn } from 'ui' import { ExpandableVideo } from 'ui-patterns/ExpandableVideo' import { Toc, TOCItems, TOCScrollArea } from 'ui-patterns/Toc' @@ -18,6 +19,8 @@ const GuidesTableOfContents = ({ className, video }: { className?: string; video const pathname = usePathname() const { toc } = useTocAnchors() + const showFeedback = isFeatureEnabled('feedback:docs') + const tocVideoPreview = `https://img.youtube.com/vi/${video}/0.jpg` return ( @@ -28,9 +31,11 @@ const GuidesTableOfContents = ({ className, video }: { className?: string; video )} -
- -
+ {showFeedback && ( +
+ +
+ )} {toc.length !== 0 && (

diff --git a/apps/docs/components/HomePageCover.tsx b/apps/docs/components/HomePageCover.tsx index 25bd3b32a48..591c630023e 100644 --- a/apps/docs/components/HomePageCover.tsx +++ b/apps/docs/components/HomePageCover.tsx @@ -3,12 +3,17 @@ import { ChevronRight, Play, Sparkles } from 'lucide-react' import Link from 'next/link' -import { useBreakpoint } from 'common' +import { isFeatureEnabled, useBreakpoint } from 'common' import { cn, IconBackground } from 'ui' import { IconPanel } from 'ui-patterns/IconPanel' import DocsCoverLogo from './DocsCoverLogo' +const { sdkDart: sdkDartEnabled, sdkKotlin: sdkKotlinEnabled } = isFeatureEnabled([ + 'sdk:dart', + 'sdk:kotlin', +]) + function AiPrompt({ className }: { className?: string }) { return ( { tooltip: 'Flutter', icon: '/docs/img/icons/flutter-icon', href: '/guides/getting-started/quickstarts/flutter', + enabled: sdkDartEnabled, }, { tooltip: 'Android Kotlin', icon: '/docs/img/icons/kotlin-icon', href: '/guides/getting-started/quickstarts/kotlin', + enabled: sdkKotlinEnabled, }, { tooltip: 'SvelteKit', @@ -109,16 +116,18 @@ const HomePageCover = (props) => {
- {frameworks.map((framework, i) => ( - - - - ))} + {frameworks + .filter((framework) => framework.enabled !== false) + .map((framework, i) => ( + + + + ))}
diff --git a/apps/docs/components/Navigation/Navigation.types.ts b/apps/docs/components/Navigation/Navigation.types.ts index ce2c2dec841..7918a38e68e 100644 --- a/apps/docs/components/Navigation/Navigation.types.ts +++ b/apps/docs/components/Navigation/Navigation.types.ts @@ -11,6 +11,7 @@ export interface NavMenuSection { name: string url?: `/${string}` | `https://${string}` items: Partial[] + enabled?: boolean } type MenuItem = { @@ -20,6 +21,7 @@ type MenuItem = { level?: string hasLightIcon?: boolean community?: boolean + enabled?: boolean } export type DropdownMenuItem = MenuItem & { diff --git a/apps/docs/components/Navigation/NavigationMenu/GlobalNavigationMenu.tsx b/apps/docs/components/Navigation/NavigationMenu/GlobalNavigationMenu.tsx index ce620239cf0..8b57fd7c29e 100644 --- a/apps/docs/components/Navigation/NavigationMenu/GlobalNavigationMenu.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/GlobalNavigationMenu.tsx @@ -22,7 +22,7 @@ import { GLOBAL_MENU_ITEMS } from './NavigationMenu.constants' /** * Get TopNav active label based on current pathname */ -export const useActiveMenuLabel = (GLOBAL_MENU_ITEMS) => { +export const useActiveMenuLabel = (menu: typeof GLOBAL_MENU_ITEMS) => { const pathname = usePathname() const [activeLabel, setActiveLabel] = useState('') @@ -32,8 +32,10 @@ export const useActiveMenuLabel = (GLOBAL_MENU_ITEMS) => { return setActiveLabel('Home') } - for (let index = 0; index < GLOBAL_MENU_ITEMS.length; index++) { - const section = GLOBAL_MENU_ITEMS[index] + for (let index = 0; index < menu.length; index++) { + const section = menu[index] + if (section[0].enabled === false) continue + // check if first level menu items match beginning of url if (section[0].href?.startsWith(pathname)) { return setActiveLabel(section[0].label) @@ -41,13 +43,15 @@ export const useActiveMenuLabel = (GLOBAL_MENU_ITEMS) => { // check if second level menu items match beginning of url if (section[0].menuItems) { section[0].menuItems.map((menuItemGroup) => - menuItemGroup.map( - (menuItem) => menuItem.href?.startsWith(pathname) && setActiveLabel(section[0].label) - ) + menuItemGroup + .filter((menuItem) => menuItem.enabled !== false) + .map( + (menuItem) => menuItem.href?.startsWith(pathname) && setActiveLabel(section[0].label) + ) ) } } - }, [pathname]) + }, [pathname, menu]) return activeLabel } @@ -67,65 +71,14 @@ const GlobalNavigationMenu: FC = () => { viewportClassName="mt-0 max-w-screen overflow-hidden border-0 rounded-none mt-1.5 rounded-md !border-x" > - {GLOBAL_MENU_ITEMS.map((section, sectionIdx) => - section[0].menuItems ? ( - - section[0].enabled !== false).map( + (section, sectionIdx) => + section[0].menuItems ? ( + - {section[0].label === 'Home' ? ( - - ) : ( - section[0].label - )} - - -
- {section[0].menuItems?.map((menuItem, menuItemIndex) => ( - - {menuItemIndex !== 0 && } - {menuItem.map((item, itemIdx) => - !item.href ? ( -
- {item.label} -
- ) : ( - - - - ) - )} -
- ))} -
-
-
- ) : ( - - - { ) : ( section[0].label )} - - - - ) + + +
+ {section[0].menuItems?.map((menuItem, menuItemIndex) => ( + + {menuItemIndex !== 0 && } + {menuItem + .filter((item) => item.enabled !== false) + .map((item, itemIdx) => + !item.href ? ( +
+ {item.label} +
+ ) : ( + + + + ) + )} +
+ ))} +
+
+ + ) : ( + + + + {section[0].label === 'Home' ? ( + + ) : ( + section[0].label + )} + + + + ) )}
diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index 87102c911f3..ea41b02c326 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts @@ -1,9 +1,32 @@ import type { ComponentProps } from 'react' +import { isFeatureEnabled } from 'common' import type { IconPanel } from 'ui-patterns/IconPanel' - import type { GlobalMenuItems, NavMenuConstant, NavMenuSection } from '../Navigation.types' +const { + authenticationShowProviders: allAuthProvidersEnabled, + billingAll: billingEnabled, + 'docsSelf-hosting': selfHostingEnabled, + sdkCsharp: sdkCsharpEnabled, + sdkDart: sdkDartEnabled, + sdkKotlin: sdkKotlinEnabled, + sdkPython: sdkPythonEnabled, + sdkSwift: sdkSwiftEnabled, +} = isFeatureEnabled([ + 'authentication:show_providers', + 'billing:all', + 'docs:self-hosting', + 'sdk:csharp', + 'sdk:dart', + 'sdk:kotlin', + 'sdk:python', + 'sdk:swift', +]) + +const jsOnly = + !sdkCsharpEnabled && !sdkDartEnabled && !sdkKotlinEnabled && !sdkPythonEnabled && !sdkSwiftEnabled + export const GLOBAL_MENU_ITEMS: GlobalMenuItems = [ [ { @@ -21,31 +44,31 @@ export const GLOBAL_MENU_ITEMS: GlobalMenuItems = [ { label: 'Database', icon: 'database', - href: '/guides/database/overview', + href: '/guides/database/overview' as `/${string}`, level: 'database', }, { label: 'Auth', icon: 'auth', - href: '/guides/auth', + href: '/guides/auth' as `/${string}`, level: 'auth', }, { label: 'Storage', icon: 'storage', - href: '/guides/storage', + href: '/guides/storage' as `/${string}`, level: 'storage', }, { label: 'Edge Functions', icon: 'edge-functions', - href: '/guides/functions', + href: '/guides/functions' as `/${string}`, level: 'functions', }, { label: 'Realtime', icon: 'realtime', - href: '/guides/realtime', + href: '/guides/realtime' as `/${string}`, level: 'realtime', }, ], @@ -54,19 +77,19 @@ export const GLOBAL_MENU_ITEMS: GlobalMenuItems = [ { label: 'AI & Vectors', icon: 'ai', - href: '/guides/ai', + href: '/guides/ai' as `/${string}`, level: 'ai', }, { label: 'Cron', icon: 'cron', - href: '/guides/cron', + href: '/guides/cron' as `/${string}`, level: 'cron', }, { label: 'Queues', icon: 'queues', - href: '/guides/queues', + href: '/guides/queues' as `/${string}`, level: 'queues', }, ], @@ -81,26 +104,27 @@ export const GLOBAL_MENU_ITEMS: GlobalMenuItems = [ { label: 'Local Development & CLI', icon: 'dev-cli', - href: '/guides/local-development', + href: '/guides/local-development' as `/${string}`, level: 'local_development', }, { label: 'Deployment', icon: 'deployment', - href: '/guides/deployment', + href: '/guides/deployment' as `/${string}`, level: 'deployment', }, { label: 'Self-Hosting', icon: 'self-hosting', - href: '/guides/self-hosting', + href: '/guides/self-hosting' as `/${string}`, level: 'self_hosting', + enabled: selfHostingEnabled, }, { label: 'Integrations', icon: 'integrations', hasLightIcon: true, - href: '/guides/integrations', + href: '/guides/integrations' as `/${string}`, level: 'integrations', }, ], @@ -115,25 +139,26 @@ export const GLOBAL_MENU_ITEMS: GlobalMenuItems = [ { label: 'Platform Management', icon: 'platform', - href: '/guides/platform', + href: '/guides/platform' as `/${string}`, level: 'platform', }, { label: 'Security & Compliance', icon: 'security', - href: '/guides/security', + href: '/guides/security' as `/${string}`, level: 'security', }, { label: 'Telemetry', icon: 'telemetry', - href: '/guides/telemetry', + href: '/guides/telemetry' as `/${string}`, level: 'telemetry', }, { label: 'Troubleshooting', icon: 'troubleshooting', - href: '/guides/troubleshooting', + href: '/guides/troubleshooting' as `/${string}`, + level: 'troubleshooting', }, ], ], @@ -150,53 +175,58 @@ export const GLOBAL_MENU_ITEMS: GlobalMenuItems = [ { label: 'JavaScript', icon: 'reference-javascript', - href: '/reference/javascript', + href: '/reference/javascript' as `/${string}`, level: 'reference_javascript', }, { label: 'Flutter', icon: 'reference-dart', - href: '/reference/dart', + href: '/reference/dart' as `/${string}`, level: 'reference_dart', + enabled: sdkDartEnabled, }, { label: 'Swift', icon: 'reference-swift', - href: '/reference/swift', + href: '/reference/swift' as `/${string}`, level: 'reference_swift', + enabled: sdkSwiftEnabled, }, { label: 'Python', icon: 'reference-python', - href: '/reference/python', + href: '/reference/python' as `/${string}`, level: 'reference_python', + enabled: sdkPythonEnabled, }, { label: 'C#', icon: 'reference-csharp', - href: '/reference/csharp', + href: '/reference/csharp' as `/${string}`, level: 'reference_csharp', community: true, + enabled: sdkCsharpEnabled, }, { label: 'Kotlin', icon: 'reference-kotlin', - href: '/reference/kotlin', + href: '/reference/kotlin' as `/${string}`, level: 'reference_kotlin', community: true, + enabled: sdkKotlinEnabled, }, ], [ { label: 'CLI Commands', icon: 'reference-cli', - href: '/reference/cli/introduction', + href: '/reference/cli/introduction' as `/${string}`, level: 'reference_javascript', }, { label: 'Management API', icon: 'reference-api', - href: '/reference/api/introduction', + href: '/reference/api/introduction' as `/${string}`, level: 'reference_javascript', }, ], @@ -205,13 +235,13 @@ export const GLOBAL_MENU_ITEMS: GlobalMenuItems = [ { label: 'REST', icon: 'rest', - href: '/guides/api', + href: '/guides/api' as `/${string}`, level: 'api', }, { label: 'GraphQL', icon: 'graphql', - href: '/guides/graphql', + href: '/guides/graphql' as `/${string}`, level: 'graphql', }, ], @@ -226,14 +256,14 @@ export const GLOBAL_MENU_ITEMS: GlobalMenuItems = [ { label: 'Glossary', icon: 'resources', - href: '/guides/resources/glossary', + href: '/guides/resources/glossary' as `/${string}`, level: 'resources', }, { label: 'Changelog', icon: 'changelog', hasLightIcon: true, - href: 'https://supabase.com/changelog', + href: 'https://supabase.com/changelog' as `/${string}`, level: 'changelog', }, { @@ -267,28 +297,40 @@ export const gettingstarted: NavMenuConstant = { { name: 'Nuxt', url: '/guides/getting-started/quickstarts/nuxtjs' }, { name: 'Vue', url: '/guides/getting-started/quickstarts/vue' }, { name: 'Hono', url: '/guides/getting-started/quickstarts/hono' }, - { name: 'Flutter', url: '/guides/getting-started/quickstarts/flutter' }, - { name: 'iOS SwiftUI', url: '/guides/getting-started/quickstarts/ios-swiftui' }, + { + name: 'Flutter', + url: '/guides/getting-started/quickstarts/flutter', + enabled: sdkDartEnabled, + }, + { + name: 'iOS SwiftUI', + url: '/guides/getting-started/quickstarts/ios-swiftui', + enabled: sdkSwiftEnabled, + }, { name: 'Android Kotlin', - url: '/guides/getting-started/quickstarts/kotlin', + url: '/guides/getting-started/quickstarts/kotlin' as `/${string}`, + enabled: sdkKotlinEnabled, }, { name: 'SvelteKit', - url: '/guides/getting-started/quickstarts/sveltekit', + url: '/guides/getting-started/quickstarts/sveltekit' as `/${string}`, }, { name: 'Laravel PHP', - url: '/guides/getting-started/quickstarts/laravel', + url: '/guides/getting-started/quickstarts/laravel' as `/${string}`, + enabled: !jsOnly, }, { name: 'Ruby on Rails', - url: '/guides/getting-started/quickstarts/ruby-on-rails', + url: '/guides/getting-started/quickstarts/ruby-on-rails' as `/${string}`, + enabled: !jsOnly, }, + { name: 'SolidJS', url: '/guides/getting-started/quickstarts/solidjs' }, { name: 'RedwoodJS', - url: '/guides/getting-started/quickstarts/redwoodjs', + url: '/guides/getting-started/quickstarts/redwoodjs' as `/${string}`, }, { name: 'refine', url: '/guides/getting-started/quickstarts/refine' }, ], @@ -298,43 +340,43 @@ export const gettingstarted: NavMenuConstant = { items: [ { name: 'Next.js', - url: '/guides/getting-started/tutorials/with-nextjs', + url: '/guides/getting-started/tutorials/with-nextjs' as `/${string}`, }, { name: 'React', - url: '/guides/getting-started/tutorials/with-react', + url: '/guides/getting-started/tutorials/with-react' as `/${string}`, }, { name: 'Vue 3', - url: '/guides/getting-started/tutorials/with-vue-3', + url: '/guides/getting-started/tutorials/with-vue-3' as `/${string}`, }, { name: 'Nuxt 3', - url: '/guides/getting-started/tutorials/with-nuxt-3', + url: '/guides/getting-started/tutorials/with-nuxt-3' as `/${string}`, }, { name: 'Angular', - url: '/guides/getting-started/tutorials/with-angular', + url: '/guides/getting-started/tutorials/with-angular' as `/${string}`, }, { name: 'RedwoodJS', - url: '/guides/getting-started/tutorials/with-redwoodjs', + url: '/guides/getting-started/tutorials/with-redwoodjs' as `/${string}`, }, { name: 'SolidJS', - url: '/guides/getting-started/tutorials/with-solidjs', + url: '/guides/getting-started/tutorials/with-solidjs' as `/${string}`, }, { name: 'Svelte', - url: '/guides/getting-started/tutorials/with-svelte', + url: '/guides/getting-started/tutorials/with-svelte' as `/${string}`, }, { name: 'SvelteKit', - url: '/guides/getting-started/tutorials/with-sveltekit', + url: '/guides/getting-started/tutorials/with-sveltekit' as `/${string}`, }, { name: 'refine', - url: '/guides/getting-started/tutorials/with-refine', + url: '/guides/getting-started/tutorials/with-refine' as `/${string}`, }, ], }, @@ -343,31 +385,36 @@ export const gettingstarted: NavMenuConstant = { items: [ { name: 'Flutter', - url: '/guides/getting-started/tutorials/with-flutter', + url: '/guides/getting-started/tutorials/with-flutter' as `/${string}`, + enabled: sdkDartEnabled, }, + { name: 'Expo React Native', - url: '/guides/getting-started/tutorials/with-expo-react-native', + url: '/guides/getting-started/tutorials/with-expo-react-native' as `/${string}`, }, { name: 'Android Kotlin', - url: '/guides/getting-started/tutorials/with-kotlin', + url: '/guides/getting-started/tutorials/with-kotlin' as `/${string}`, + enabled: sdkKotlinEnabled, }, + { name: 'Ionic React', - url: '/guides/getting-started/tutorials/with-ionic-react', + url: '/guides/getting-started/tutorials/with-ionic-react' as `/${string}`, }, { name: 'Ionic Vue', - url: '/guides/getting-started/tutorials/with-ionic-vue', + url: '/guides/getting-started/tutorials/with-ionic-vue' as `/${string}`, }, { name: 'Ionic Angular', - url: '/guides/getting-started/tutorials/with-ionic-angular', + url: '/guides/getting-started/tutorials/with-ionic-angular' as `/${string}`, }, { name: 'Swift', - url: '/guides/getting-started/tutorials/with-swift', + url: '/guides/getting-started/tutorials/with-swift' as `/${string}`, + enabled: sdkSwiftEnabled, }, ], }, @@ -377,11 +424,11 @@ export const gettingstarted: NavMenuConstant = { items: [ { name: 'Prompts', - url: '/guides/getting-started/ai-prompts', + url: '/guides/getting-started/ai-prompts' as `/${string}`, }, { name: 'Model context protocol (MCP)', - url: '/guides/getting-started/mcp', + url: '/guides/getting-started/mcp' as `/${string}`, }, ], }, @@ -561,12 +608,12 @@ export const auth = { items: [ { name: 'Next.js', - url: '/guides/auth/quickstarts/nextjs', + url: '/guides/auth/quickstarts/nextjs' as `/${string}`, }, { name: 'React', url: '/guides/auth/quickstarts/react', items: [] }, { name: 'React Native', - url: '/guides/auth/quickstarts/react-native', + url: '/guides/auth/quickstarts/react-native' as `/${string}`, }, ], }, @@ -596,46 +643,68 @@ export const auth = { { name: 'Next.js guide', url: '/guides/auth/server-side/nextjs' }, { name: 'SvelteKit guide', - url: '/guides/auth/server-side/sveltekit', + url: '/guides/auth/server-side/sveltekit' as `/${string}`, }, { name: 'Creating a client', url: '/guides/auth/server-side/creating-a-client' }, { name: 'Migrating from Auth Helpers', - url: '/guides/auth/server-side/migrating-to-ssr-from-auth-helpers', + url: '/guides/auth/server-side/migrating-to-ssr-from-auth-helpers' as `/${string}`, }, { name: 'Advanced guide', - url: '/guides/auth/server-side/advanced-guide', + url: '/guides/auth/server-side/advanced-guide' as `/${string}`, }, ], }, - { name: 'Password-based', url: '/guides/auth/passwords' }, - { name: 'Email (Magic Link or OTP)', url: '/guides/auth/auth-email-passwordless' }, + { + name: 'Password-based', + url: '/guides/auth/passwords', + enabled: allAuthProvidersEnabled, + }, + { + name: 'Email (Magic Link or OTP)', + url: '/guides/auth/auth-email-passwordless', + enabled: allAuthProvidersEnabled, + }, { name: 'Phone Login', - url: '/guides/auth/phone-login', + url: '/guides/auth/phone-login' as `/${string}`, + enabled: allAuthProvidersEnabled, }, + { name: 'Social Login (OAuth)', url: '/guides/auth/social-login', items: [...SocialLoginItems], + enabled: allAuthProvidersEnabled, }, + { name: 'Enterprise SSO', url: '/guides/auth/enterprise-sso', + enabled: allAuthProvidersEnabled, items: [ { name: 'SAML 2.0', - url: '/guides/auth/enterprise-sso/auth-sso-saml', + url: '/guides/auth/enterprise-sso/auth-sso-saml' as `/${string}`, }, ], }, - { name: 'Anonymous Sign-Ins', url: '/guides/auth/auth-anonymous' }, - { name: 'Web3 (Sign in with Solana)', url: '/guides/auth/auth-web3' }, + + { + name: 'Anonymous Sign-Ins', + url: '/guides/auth/auth-anonymous', + enabled: allAuthProvidersEnabled, + }, + { + name: 'Web3 (Sign in with Solana)', + url: '/guides/auth/auth-web3', + enabled: allAuthProvidersEnabled, + }, { name: 'Mobile Deep Linking', url: '/guides/auth/native-mobile-deep-linking' }, { name: 'Identity Linking', - url: '/guides/auth/auth-identity-linking', + url: '/guides/auth/auth-identity-linking' as `/${string}`, }, { name: 'Multi-Factor Authentication', @@ -647,7 +716,7 @@ export const auth = { }, { name: 'Signout', - url: '/guides/auth/signout', + url: '/guides/auth/signout' as `/${string}`, }, ], }, @@ -660,6 +729,7 @@ export const auth = { }, { name: 'Third-party auth', + enabled: allAuthProvidersEnabled, items: [ { name: 'Overview', url: '/guides/auth/third-party/overview' }, { name: 'Clerk', url: '/guides/auth/third-party/clerk' }, @@ -674,12 +744,12 @@ export const auth = { items: [ { name: 'General Configuration', - url: '/guides/auth/general-configuration', + url: '/guides/auth/general-configuration' as `/${string}`, }, { name: 'Email Templates', url: '/guides/auth/auth-email-templates' }, { name: 'Redirect URLs', - url: '/guides/auth/redirect-urls', + url: '/guides/auth/redirect-urls' as `/${string}`, }, { name: 'Auth Hooks', @@ -687,27 +757,27 @@ export const auth = { items: [ { name: 'Custom access token hook', - url: '/guides/auth/auth-hooks/custom-access-token-hook', + url: '/guides/auth/auth-hooks/custom-access-token-hook' as `/${string}`, }, { name: 'Send SMS hook', - url: '/guides/auth/auth-hooks/send-sms-hook', + url: '/guides/auth/auth-hooks/send-sms-hook' as `/${string}`, }, { name: 'Send email hook', - url: '/guides/auth/auth-hooks/send-email-hook', + url: '/guides/auth/auth-hooks/send-email-hook' as `/${string}`, }, { name: 'MFA verification hook', - url: '/guides/auth/auth-hooks/mfa-verification-hook', + url: '/guides/auth/auth-hooks/mfa-verification-hook' as `/${string}`, }, { name: 'Password verification hook', - url: '/guides/auth/auth-hooks/password-verification-hook', + url: '/guides/auth/auth-hooks/password-verification-hook' as `/${string}`, }, { name: 'Before User Created hook', - url: '/guides/auth/auth-hooks/before-user-created-hook', + url: '/guides/auth/auth-hooks/before-user-created-hook' as `/${string}`, }, ], }, @@ -730,22 +800,23 @@ export const auth = { { name: 'Row Level Security', url: '/guides/database/postgres/row-level-security' }, { name: 'Column Level Security', - url: '/guides/database/postgres/column-level-security', + url: '/guides/database/postgres/column-level-security' as `/${string}`, }, { name: 'Custom Claims & RBAC', - url: '/guides/database/postgres/custom-claims-and-role-based-access-control-rbac', + url: '/guides/database/postgres/custom-claims-and-role-based-access-control-rbac' as `/${string}`, }, ], }, { name: 'Auth UI', url: undefined, + enabled: allAuthProvidersEnabled, items: [ { name: 'Auth UI (Deprecated)', url: '/guides/auth/auth-helpers/auth-ui' }, { name: 'Flutter Auth UI', - url: '/guides/auth/auth-helpers/flutter-auth-ui', + url: '/guides/auth/auth-helpers/flutter-auth-ui' as `/${string}`, }, ], }, @@ -762,7 +833,7 @@ const ormQuickstarts: NavMenuSection = { items: [ { name: 'Prisma troubleshooting', - url: '/guides/database/prisma/prisma-troubleshooting', + url: '/guides/database/prisma/prisma-troubleshooting' as `/${string}`, }, ], }, @@ -816,7 +887,7 @@ export const database: NavMenuConstant = { items: [ { name: 'Connecting to your database', - url: '/guides/database/connecting-to-postgres', + url: '/guides/database/connecting-to-postgres' as `/${string}`, }, { name: 'Importing data', url: '/guides/database/import-data' }, { name: 'Securing your data', url: '/guides/database/secure-data' }, @@ -828,16 +899,16 @@ export const database: NavMenuConstant = { items: [ { name: 'Managing tables, views, and data', - url: '/guides/database/tables', + url: '/guides/database/tables' as `/${string}`, }, { name: 'Working with arrays', - url: '/guides/database/arrays', + url: '/guides/database/arrays' as `/${string}`, }, { name: 'Managing indexes', url: '/guides/database/postgres/indexes' }, { name: 'Querying joins and nested tables', - url: '/guides/database/joins-and-nesting', + url: '/guides/database/joins-and-nesting' as `/${string}`, }, { name: 'JSON and unstructured data', url: '/guides/database/json' }, ], @@ -848,32 +919,32 @@ export const database: NavMenuConstant = { items: [ { name: 'Implementing cascade deletes', - url: '/guides/database/postgres/cascade-deletes', + url: '/guides/database/postgres/cascade-deletes' as `/${string}`, }, { name: 'Managing enums', url: '/guides/database/postgres/enums' }, { name: 'Managing database functions', - url: '/guides/database/functions', + url: '/guides/database/functions' as `/${string}`, }, { name: 'Managing database triggers', - url: '/guides/database/postgres/triggers', + url: '/guides/database/postgres/triggers' as `/${string}`, }, { name: 'Managing database webhooks', - url: '/guides/database/webhooks', + url: '/guides/database/webhooks' as `/${string}`, }, { name: 'Using Full Text Search', - url: '/guides/database/full-text-search', + url: '/guides/database/full-text-search' as `/${string}`, }, { name: 'Partitioning your tables', - url: '/guides/database/partitions', + url: '/guides/database/partitions' as `/${string}`, }, { name: 'Managing connections', - url: '/guides/database/connection-management', + url: '/guides/database/connection-management' as `/${string}`, }, ], }, @@ -883,7 +954,7 @@ export const database: NavMenuConstant = { items: [ { name: 'Overview', - url: '/guides/database/orioledb', + url: '/guides/database/orioledb' as `/${string}`, }, ], }, @@ -893,32 +964,32 @@ export const database: NavMenuConstant = { items: [ { name: 'Row Level Security', - url: '/guides/database/postgres/row-level-security', + url: '/guides/database/postgres/row-level-security' as `/${string}`, }, { name: 'Column Level Security', - url: '/guides/database/postgres/column-level-security', + url: '/guides/database/postgres/column-level-security' as `/${string}`, }, { name: 'Hardening the Data API', - url: '/guides/database/hardening-data-api', + url: '/guides/database/hardening-data-api' as `/${string}`, }, { name: 'Custom Claims & RBAC', - url: '/guides/database/postgres/custom-claims-and-role-based-access-control-rbac', + url: '/guides/database/postgres/custom-claims-and-role-based-access-control-rbac' as `/${string}`, }, { name: 'Managing Postgres Roles', - url: '/guides/database/postgres/roles', + url: '/guides/database/postgres/roles' as `/${string}`, }, { name: 'Using Custom Postgres Roles', - url: '/guides/storage/schema/custom-roles', + url: '/guides/storage/schema/custom-roles' as `/${string}`, }, { name: 'Managing secrets with Vault', url: '/guides/database/vault' }, { name: 'Superuser Access and Unsupported Operations', - url: '/guides/database/postgres/roles-superuser', + url: '/guides/database/postgres/roles-superuser' as `/${string}`, }, ], }, @@ -928,20 +999,20 @@ export const database: NavMenuConstant = { items: [ { name: 'Database configuration', - url: '/guides/database/postgres/configuration', + url: '/guides/database/postgres/configuration' as `/${string}`, }, { name: 'Query optimization', - url: '/guides/database/query-optimization', + url: '/guides/database/query-optimization' as `/${string}`, }, { name: 'Database Advisors', - url: '/guides/database/database-advisors', + url: '/guides/database/database-advisors' as `/${string}`, }, { name: 'Testing your database', url: '/guides/database/testing' }, { name: 'Customizing Postgres config', - url: '/guides/database/custom-postgres-config', + url: '/guides/database/custom-postgres-config' as `/${string}`, }, ], }, @@ -951,23 +1022,23 @@ export const database: NavMenuConstant = { items: [ { name: 'Timeouts', - url: '/guides/database/postgres/timeouts', + url: '/guides/database/postgres/timeouts' as `/${string}`, }, { name: 'Debugging and monitoring', - url: '/guides/database/inspect', + url: '/guides/database/inspect' as `/${string}`, }, { name: 'Debugging performance issues', - url: '/guides/database/debugging-performance', + url: '/guides/database/debugging-performance' as `/${string}`, }, { name: 'Supavisor', - url: '/guides/database/supavisor', + url: '/guides/database/supavisor' as `/${string}`, }, { name: 'Troubleshooting', - url: '/guides/database/troubleshooting', + url: '/guides/database/troubleshooting' as `/${string}`, }, ], }, @@ -980,11 +1051,11 @@ export const database: NavMenuConstant = { { name: 'Overview', url: '/guides/database/replication' }, { name: 'Setting up replication', - url: '/guides/database/replication/setting-up-replication', + url: '/guides/database/replication/setting-up-replication' as `/${string}`, }, { name: 'Monitoring replication', - url: '/guides/database/replication/monitoring-replication', + url: '/guides/database/replication/monitoring-replication' as `/${string}`, }, { name: 'FAQ', url: '/guides/database/replication/faq' }, ], @@ -996,107 +1067,107 @@ export const database: NavMenuConstant = { { name: 'Overview', url: '/guides/database/extensions' }, { name: 'HypoPG: Hypothetical indexes', - url: '/guides/database/extensions/hypopg', + url: '/guides/database/extensions/hypopg' as `/${string}`, }, { name: 'plv8 (deprecated)', - url: '/guides/database/extensions/plv8', + url: '/guides/database/extensions/plv8' as `/${string}`, }, { name: 'http: RESTful Client', - url: '/guides/database/extensions/http', + url: '/guides/database/extensions/http' as `/${string}`, }, { name: 'index_advisor: Query optimization', - url: '/guides/database/extensions/index_advisor', + url: '/guides/database/extensions/index_advisor' as `/${string}`, }, { name: 'PGAudit: Postgres Auditing', - url: '/guides/database/extensions/pgaudit', + url: '/guides/database/extensions/pgaudit' as `/${string}`, }, { name: 'pgjwt (deprecated)', - url: '/guides/database/extensions/pgjwt', + url: '/guides/database/extensions/pgjwt' as `/${string}`, }, { name: 'PGroonga: Multilingual Full Text Search', - url: '/guides/database/extensions/pgroonga', + url: '/guides/database/extensions/pgroonga' as `/${string}`, }, { name: 'pgRouting: Geospatial Routing', - url: '/guides/database/extensions/pgrouting', + url: '/guides/database/extensions/pgrouting' as `/${string}`, }, { name: 'pg_cron: Schedule Recurring Jobs', - url: '/guides/database/extensions/pg_cron', + url: '/guides/database/extensions/pg_cron' as `/${string}`, }, { name: 'pg_graphql: GraphQL Support', - url: '/guides/database/extensions/pg_graphql', + url: '/guides/database/extensions/pg_graphql' as `/${string}`, }, { name: 'pg_hashids: Short UIDs', - url: '/guides/database/extensions/pg_hashids', + url: '/guides/database/extensions/pg_hashids' as `/${string}`, }, { name: 'pg_jsonschema: JSON Schema Validation', - url: '/guides/database/extensions/pg_jsonschema', + url: '/guides/database/extensions/pg_jsonschema' as `/${string}`, }, { name: 'pg_net: Async Networking', - url: '/guides/database/extensions/pg_net', + url: '/guides/database/extensions/pg_net' as `/${string}`, }, { name: 'pg_plan_filter: Restrict Total Cost', - url: '/guides/database/extensions/pg_plan_filter', + url: '/guides/database/extensions/pg_plan_filter' as `/${string}`, }, { name: 'postgres_fdw: query data from an external Postgres server', - url: '/guides/database/extensions/postgres_fdw', + url: '/guides/database/extensions/postgres_fdw' as `/${string}`, }, { name: 'pgvector: Embeddings and vector similarity', - url: '/guides/database/extensions/pgvector', + url: '/guides/database/extensions/pgvector' as `/${string}`, }, { name: 'pg_stat_statements: SQL Planning and Execution Statistics', - url: '/guides/database/extensions/pg_stat_statements', + url: '/guides/database/extensions/pg_stat_statements' as `/${string}`, }, { name: 'pg_repack: Storage Optimization', - url: '/guides/database/extensions/pg_repack', + url: '/guides/database/extensions/pg_repack' as `/${string}`, }, { name: 'PostGIS: Geo queries', - url: '/guides/database/extensions/postgis', + url: '/guides/database/extensions/postgis' as `/${string}`, }, { name: 'pgmq: Queues', - url: '/guides/database/extensions/pgmq', + url: '/guides/database/extensions/pgmq' as `/${string}`, }, { name: 'pgsodium (pending deprecation): Encryption Features', - url: '/guides/database/extensions/pgsodium', + url: '/guides/database/extensions/pgsodium' as `/${string}`, }, { name: 'pgTAP: Unit Testing', - url: '/guides/database/extensions/pgtap', + url: '/guides/database/extensions/pgtap' as `/${string}`, }, { name: 'plpgsql_check: PL/pgSQL Linter', - url: '/guides/database/extensions/plpgsql_check', + url: '/guides/database/extensions/plpgsql_check' as `/${string}`, }, { name: 'timescaledb (deprecated)', - url: '/guides/database/extensions/timescaledb', + url: '/guides/database/extensions/timescaledb' as `/${string}`, }, { name: 'uuid-ossp: Unique Identifiers', - url: '/guides/database/extensions/uuid-ossp', + url: '/guides/database/extensions/uuid-ossp' as `/${string}`, }, { name: 'RUM: inverted index for full-text search', - url: '/guides/database/extensions/rum', + url: '/guides/database/extensions/rum' as `/${string}`, }, ], }, @@ -1106,75 +1177,75 @@ export const database: NavMenuConstant = { items: [ { name: 'Overview', - url: '/guides/database/extensions/wrappers/overview', + url: '/guides/database/extensions/wrappers/overview' as `/${string}`, }, { name: 'Connecting to Auth0', - url: '/guides/database/extensions/wrappers/auth0', + url: '/guides/database/extensions/wrappers/auth0' as `/${string}`, }, { name: 'Connecting to Airtable', - url: '/guides/database/extensions/wrappers/airtable', + url: '/guides/database/extensions/wrappers/airtable' as `/${string}`, }, { name: 'Connecting to AWS Cognito', - url: '/guides/database/extensions/wrappers/cognito', + url: '/guides/database/extensions/wrappers/cognito' as `/${string}`, }, { name: 'Connecting to AWS S3', - url: '/guides/database/extensions/wrappers/s3', + url: '/guides/database/extensions/wrappers/s3' as `/${string}`, }, { name: 'Connecting to BigQuery', - url: '/guides/database/extensions/wrappers/bigquery', + url: '/guides/database/extensions/wrappers/bigquery' as `/${string}`, }, { name: 'Connecting to Clerk', - url: '/guides/database/extensions/wrappers/clerk', + url: '/guides/database/extensions/wrappers/clerk' as `/${string}`, }, { name: 'Connecting to ClickHouse', - url: '/guides/database/extensions/wrappers/clickhouse', + url: '/guides/database/extensions/wrappers/clickhouse' as `/${string}`, }, { name: 'Connecting to DuckDB', - url: '/guides/database/extensions/wrappers/duckdb', + url: '/guides/database/extensions/wrappers/duckdb' as `/${string}`, }, { name: 'Connecting to Firebase', - url: '/guides/database/extensions/wrappers/firebase', + url: '/guides/database/extensions/wrappers/firebase' as `/${string}`, }, { name: 'Connecting to Iceberg', - url: '/guides/database/extensions/wrappers/iceberg', + url: '/guides/database/extensions/wrappers/iceberg' as `/${string}`, }, { name: 'Connecting to Logflare', - url: '/guides/database/extensions/wrappers/logflare', + url: '/guides/database/extensions/wrappers/logflare' as `/${string}`, }, { name: 'Connecting to MSSQL', - url: '/guides/database/extensions/wrappers/mssql', + url: '/guides/database/extensions/wrappers/mssql' as `/${string}`, }, { name: 'Connecting to Notion', - url: '/guides/database/extensions/wrappers/notion', + url: '/guides/database/extensions/wrappers/notion' as `/${string}`, }, { name: 'Connecting to Paddle', - url: '/guides/database/extensions/wrappers/paddle', + url: '/guides/database/extensions/wrappers/paddle' as `/${string}`, }, { name: 'Connecting to Redis', - url: '/guides/database/extensions/wrappers/redis', + url: '/guides/database/extensions/wrappers/redis' as `/${string}`, }, { name: 'Connecting to Snowflake', - url: '/guides/database/extensions/wrappers/snowflake', + url: '/guides/database/extensions/wrappers/snowflake' as `/${string}`, }, { name: 'Connecting to Stripe', - url: '/guides/database/extensions/wrappers/stripe', + url: '/guides/database/extensions/wrappers/stripe' as `/${string}`, }, ], }, @@ -1184,19 +1255,19 @@ export const database: NavMenuConstant = { items: [ { name: 'Drop All Tables in Schema', - url: '/guides/database/postgres/dropping-all-tables-in-schema', + url: '/guides/database/postgres/dropping-all-tables-in-schema' as `/${string}`, }, { name: 'Select First Row per Group', - url: '/guides/database/postgres/first-row-in-group', + url: '/guides/database/postgres/first-row-in-group' as `/${string}`, }, { name: 'Print PostgreSQL Version', - url: '/guides/database/postgres/which-version-of-postgres', + url: '/guides/database/postgres/which-version-of-postgres' as `/${string}`, }, { name: 'Replicating from Supabase to External Postgres', - url: '/guides/database/postgres/setup-replication-external', + url: '/guides/database/postgres/setup-replication-external' as `/${string}`, }, ], }, @@ -1284,35 +1355,35 @@ export const api: NavMenuConstant = { items: [ { name: 'Managing tables, views, and data', - url: '/guides/database/tables', + url: '/guides/database/tables' as `/${string}`, }, { name: 'Querying joins and nested tables', - url: '/guides/database/joins-and-nesting', + url: '/guides/database/joins-and-nesting' as `/${string}`, }, { name: 'JSON and unstructured data', - url: '/guides/database/json', + url: '/guides/database/json' as `/${string}`, }, { name: 'Managing database functions', - url: '/guides/database/functions', + url: '/guides/database/functions' as `/${string}`, }, { name: 'Using full-text search', - url: '/guides/database/full-text-search', + url: '/guides/database/full-text-search' as `/${string}`, }, { name: 'Debugging performance issues', - url: '/guides/database/debugging-performance', + url: '/guides/database/debugging-performance' as `/${string}`, }, { name: 'Using custom schemas', - url: '/guides/api/using-custom-schemas', + url: '/guides/api/using-custom-schemas' as `/${string}`, }, { name: 'Converting from SQL to JavaScript API', - url: '/guides/api/sql-to-api', + url: '/guides/api/sql-to-api' as `/${string}`, }, ], }, @@ -1359,15 +1430,15 @@ export const functions: NavMenuConstant = { items: [ { name: 'Quickstart (Dashboard)', - url: '/guides/functions/quickstart-dashboard', + url: '/guides/functions/quickstart-dashboard' as `/${string}`, }, { name: 'Quickstart (CLI)', - url: '/guides/functions/quickstart', + url: '/guides/functions/quickstart' as `/${string}`, }, { name: 'Development Environment', - url: '/guides/functions/development-environment', + url: '/guides/functions/development-environment' as `/${string}`, }, ], }, @@ -1388,7 +1459,7 @@ export const functions: NavMenuConstant = { { name: 'Routing', url: '/guides/functions/routing' }, { name: 'Deploy to Production', - url: '/guides/functions/deploy', + url: '/guides/functions/deploy' as `/${string}`, }, ], }, @@ -1398,19 +1469,19 @@ export const functions: NavMenuConstant = { items: [ { name: 'Local Debugging with DevTools', - url: '/guides/functions/debugging-tools', + url: '/guides/functions/debugging-tools' as `/${string}`, }, { name: 'Testing your Functions', - url: '/guides/functions/unit-test', + url: '/guides/functions/unit-test' as `/${string}`, }, { name: 'Logging', - url: '/guides/functions/logging', + url: '/guides/functions/logging' as `/${string}`, }, { name: 'Troubleshooting', - url: '/guides/functions/troubleshooting', + url: '/guides/functions/troubleshooting' as `/${string}`, }, ], }, @@ -1420,19 +1491,20 @@ export const functions: NavMenuConstant = { items: [ { name: 'Regional invocations', - url: '/guides/functions/regional-invocation', + url: '/guides/functions/regional-invocation' as `/${string}`, }, { name: 'Status codes', - url: '/guides/functions/status-codes', + url: '/guides/functions/status-codes' as `/${string}`, }, { name: 'Limits', - url: '/guides/functions/limits', + url: '/guides/functions/limits' as `/${string}`, }, { name: 'Pricing', - url: '/guides/functions/pricing', + url: '/guides/functions/pricing' as `/${string}`, + enabled: billingEnabled, }, ], }, @@ -1463,63 +1535,63 @@ export const functions: NavMenuConstant = { items: [ { name: 'Auth Send Email Hook', - url: '/guides/functions/examples/auth-send-email-hook-react-email-resend', + url: '/guides/functions/examples/auth-send-email-hook-react-email-resend' as `/${string}`, }, { name: 'CORS support for invoking from the browser', - url: '/guides/functions/cors', + url: '/guides/functions/cors' as `/${string}`, }, { name: 'Scheduling Functions', - url: '/guides/functions/schedule-functions', + url: '/guides/functions/schedule-functions' as `/${string}`, }, { name: 'Sending Push Notifications', - url: '/guides/functions/examples/push-notifications', + url: '/guides/functions/examples/push-notifications' as `/${string}`, }, { name: 'Generating AI images', - url: '/guides/functions/examples/amazon-bedrock-image-generator', + url: '/guides/functions/examples/amazon-bedrock-image-generator' as `/${string}`, }, { name: 'Generating OG images ', - url: '/guides/functions/examples/og-image', + url: '/guides/functions/examples/og-image' as `/${string}`, }, { name: 'Semantic AI Search', - url: '/guides/functions/examples/semantic-search', + url: '/guides/functions/examples/semantic-search' as `/${string}`, }, { name: 'CAPTCHA support with Cloudflare Turnstile', - url: '/guides/functions/examples/cloudflare-turnstile', + url: '/guides/functions/examples/cloudflare-turnstile' as `/${string}`, }, { name: 'Building a Discord Bot', - url: '/guides/functions/examples/discord-bot', + url: '/guides/functions/examples/discord-bot' as `/${string}`, }, { name: 'Building a Telegram Bot', - url: '/guides/functions/examples/telegram-bot', + url: '/guides/functions/examples/telegram-bot' as `/${string}`, }, { name: 'Handling Stripe Webhooks ', - url: '/guides/functions/examples/stripe-webhooks', + url: '/guides/functions/examples/stripe-webhooks' as `/${string}`, }, { name: 'Rate-limiting with Redis', - url: '/guides/functions/examples/rate-limiting', + url: '/guides/functions/examples/rate-limiting' as `/${string}`, }, { name: 'Taking Screenshots with Puppeteer', - url: '/guides/functions/examples/screenshots', + url: '/guides/functions/examples/screenshots' as `/${string}`, }, { name: 'Slack Bot responding to mentions', - url: '/guides/functions/examples/slack-bot-mention', + url: '/guides/functions/examples/slack-bot-mention' as `/${string}`, }, { name: 'Image Transformation & Optimization', - url: '/guides/functions/examples/image-manipulation', + url: '/guides/functions/examples/image-manipulation' as `/${string}`, }, ], }, @@ -1530,40 +1602,40 @@ export const functions: NavMenuConstant = { { name: 'Dart Edge on Supabase', url: '/guides/functions/dart-edge' }, { name: 'Browserless.io', - url: '/guides/functions/examples/screenshots', + url: '/guides/functions/examples/screenshots' as `/${string}`, }, { name: 'Hugging Face', - url: '/guides/ai/examples/huggingface-image-captioning', + url: '/guides/ai/examples/huggingface-image-captioning' as `/${string}`, }, { name: 'Monitoring with Sentry', - url: '/guides/functions/examples/sentry-monitoring', + url: '/guides/functions/examples/sentry-monitoring' as `/${string}`, }, { name: 'OpenAI API', url: '/guides/ai/examples/openai' }, { name: 'React Email', - url: '/guides/functions/examples/auth-send-email-hook-react-email-resend', + url: '/guides/functions/examples/auth-send-email-hook-react-email-resend' as `/${string}`, }, { name: 'Sending Emails with Resend', - url: '/guides/functions/examples/send-emails', + url: '/guides/functions/examples/send-emails' as `/${string}`, }, { name: 'Upstash Redis', - url: '/guides/functions/examples/upstash-redis', + url: '/guides/functions/examples/upstash-redis' as `/${string}`, }, { name: 'Type-Safe SQL with Kysely', - url: '/guides/functions/kysely-postgres', + url: '/guides/functions/kysely-postgres' as `/${string}`, }, { name: 'Text To Speech with ElevenLabs', - url: '/guides/functions/examples/elevenlabs-generate-speech-stream', + url: '/guides/functions/examples/elevenlabs-generate-speech-stream' as `/${string}`, }, { name: 'Speech Transcription with ElevenLabs', - url: '/guides/functions/examples/elevenlabs-transcribe-speech', + url: '/guides/functions/examples/elevenlabs-transcribe-speech' as `/${string}`, }, ], }, @@ -1591,7 +1663,7 @@ export const realtime: NavMenuConstant = { { name: 'Presence', url: '/guides/realtime/presence' }, { name: 'Postgres Changes', - url: '/guides/realtime/postgres-changes', + url: '/guides/realtime/postgres-changes' as `/${string}`, }, { name: 'Settings', url: '/guides/realtime/settings' }, ], @@ -1607,19 +1679,19 @@ export const realtime: NavMenuConstant = { items: [ { name: 'Subscribing to Database Changes', - url: '/guides/realtime/subscribing-to-database-changes', + url: '/guides/realtime/subscribing-to-database-changes' as `/${string}`, }, { name: 'Using Realtime with Next.js', - url: '/guides/realtime/realtime-with-nextjs', + url: '/guides/realtime/realtime-with-nextjs' as `/${string}`, }, { name: 'Using Realtime Presence with Flutter', - url: '/guides/realtime/realtime-user-presence', + url: '/guides/realtime/realtime-user-presence' as `/${string}`, }, { name: 'Listening to Postgres Changes with Flutter', - url: '/guides/realtime/realtime-listening-flutter', + url: '/guides/realtime/realtime-listening-flutter' as `/${string}`, }, ], }, @@ -1628,7 +1700,11 @@ export const realtime: NavMenuConstant = { url: undefined, items: [ { name: 'Quotas', url: '/guides/realtime/quotas' }, - { name: 'Pricing', url: '/guides/realtime/pricing' }, + { + name: 'Pricing', + url: '/guides/realtime/pricing' as `/${string}`, + enabled: billingEnabled, + }, { name: 'Architecture', url: '/guides/realtime/architecture' }, { name: 'Protocol', url: '/guides/realtime/protocol', items: [] }, { name: 'Benchmarks', url: '/guides/realtime/benchmarks' }, @@ -1659,7 +1735,7 @@ export const storage: NavMenuConstant = { { name: 'Fundamentals', url: '/guides/storage/buckets/fundamentals' }, { name: 'Creating Buckets', - url: '/guides/storage/buckets/creating-buckets', + url: '/guides/storage/buckets/creating-buckets' as `/${string}`, }, ], }, @@ -1669,11 +1745,11 @@ export const storage: NavMenuConstant = { items: [ { name: 'Ownership', - url: '/guides/storage/security/ownership', + url: '/guides/storage/security/ownership' as `/${string}`, }, { name: 'Access Control', - url: '/guides/storage/security/access-control', + url: '/guides/storage/security/access-control' as `/${string}`, }, ], }, @@ -1683,15 +1759,15 @@ export const storage: NavMenuConstant = { items: [ { name: 'Standard Uploads', - url: '/guides/storage/uploads/standard-uploads', + url: '/guides/storage/uploads/standard-uploads' as `/${string}`, }, { name: 'Resumable Uploads', - url: '/guides/storage/uploads/resumable-uploads', + url: '/guides/storage/uploads/resumable-uploads' as `/${string}`, }, { name: 'S3 Uploads', - url: '/guides/storage/uploads/s3-uploads', + url: '/guides/storage/uploads/s3-uploads' as `/${string}`, }, { name: 'Limits', url: '/guides/storage/uploads/file-limits' }, ], @@ -1703,11 +1779,11 @@ export const storage: NavMenuConstant = { { name: 'Serving assets', url: '/guides/storage/serving/downloads' }, { name: 'Image Transformations', - url: '/guides/storage/serving/image-transformations', + url: '/guides/storage/serving/image-transformations' as `/${string}`, }, { name: 'Bandwidth & Storage Egress', - url: '/guides/storage/serving/bandwidth', + url: '/guides/storage/serving/bandwidth' as `/${string}`, }, ], }, @@ -1717,7 +1793,11 @@ export const storage: NavMenuConstant = { items: [ { name: 'Copy / Move Objects', url: '/guides/storage/management/copy-move-objects' }, { name: 'Delete Objects', url: '/guides/storage/management/delete-objects' }, - { name: 'Pricing', url: '/guides/storage/management/pricing' }, + { + name: 'Pricing', + url: '/guides/storage/management/pricing' as `/${string}`, + enabled: billingEnabled, + }, ], }, { @@ -1735,15 +1815,15 @@ export const storage: NavMenuConstant = { { name: 'Introduction', url: '/guides/storage/analytics/introduction' }, { name: 'Creating Analytics Buckets', - url: '/guides/storage/analytics/creating-analytics-buckets', + url: '/guides/storage/analytics/creating-analytics-buckets' as `/${string}`, }, { name: 'Connecting to Analytics Buckets', - url: '/guides/storage/analytics/connecting-to-analytics-bucket', + url: '/guides/storage/analytics/connecting-to-analytics-bucket' as `/${string}`, }, { name: 'Limits', - url: '/guides/storage/analytics/limits', + url: '/guides/storage/analytics/limits' as `/${string}`, }, ], }, @@ -1772,7 +1852,7 @@ export const storage: NavMenuConstant = { { name: 'Database Design', url: '/guides/storage/schema/design' }, { name: 'Helper Functions', - url: '/guides/storage/schema/helper-functions', + url: '/guides/storage/schema/helper-functions' as `/${string}`, }, { name: 'Custom Roles', url: '/guides/storage/schema/custom-roles' }, ], @@ -1819,20 +1899,20 @@ export const ai = { }, { name: 'Automatic embeddings', - url: '/guides/ai/automatic-embeddings', + url: '/guides/ai/automatic-embeddings' as `/${string}`, }, { name: 'Engineering for scale', - url: '/guides/ai/engineering-for-scale', + url: '/guides/ai/engineering-for-scale' as `/${string}`, }, { name: 'Choosing Compute Add-on', - url: '/guides/ai/choosing-compute-addon', + url: '/guides/ai/choosing-compute-addon' as `/${string}`, }, { name: 'Going to Production', url: '/guides/ai/going-to-prod' }, { name: 'RAG with Permissions', - url: '/guides/ai/rag-with-permissions', + url: '/guides/ai/rag-with-permissions' as `/${string}`, }, ], }, @@ -1851,25 +1931,25 @@ export const ai = { items: [ { name: 'OpenAI completions using Edge Functions', - url: '/guides/ai/examples/openai', + url: '/guides/ai/examples/openai' as `/${string}`, }, { name: 'Generate image captions using Hugging Face', - url: '/guides/ai/examples/huggingface-image-captioning', + url: '/guides/ai/examples/huggingface-image-captioning' as `/${string}`, }, { name: 'Generate Embeddings', - url: '/guides/ai/quickstarts/generate-text-embeddings', + url: '/guides/ai/quickstarts/generate-text-embeddings' as `/${string}`, }, { name: 'Adding generative Q&A to your documentation', - url: '/guides/ai/examples/headless-vector-search', + url: '/guides/ai/examples/headless-vector-search' as `/${string}`, }, { name: 'Adding generative Q&A to your Next.js site', - url: '/guides/ai/examples/nextjs-vector-search', + url: '/guides/ai/examples/nextjs-vector-search' as `/${string}`, }, ], }, @@ -1890,32 +1970,32 @@ export const ai = { items: [ { name: 'Developing locally with Vecs', - url: '/guides/ai/vecs-python-client', + url: '/guides/ai/vecs-python-client' as `/${string}`, }, { name: 'Creating and managing collections', - url: '/guides/ai/quickstarts/hello-world', + url: '/guides/ai/quickstarts/hello-world' as `/${string}`, }, { name: 'Text Deduplication', - url: '/guides/ai/quickstarts/text-deduplication', + url: '/guides/ai/quickstarts/text-deduplication' as `/${string}`, }, { name: 'Face similarity search', - url: '/guides/ai/quickstarts/face-similarity', + url: '/guides/ai/quickstarts/face-similarity' as `/${string}`, }, { name: 'Image search with OpenAI CLIP', - url: '/guides/ai/examples/image-search-openai-clip', + url: '/guides/ai/examples/image-search-openai-clip' as `/${string}`, }, { name: 'Semantic search with Amazon Titan', - url: '/guides/ai/examples/semantic-image-search-amazon-titan', + url: '/guides/ai/examples/semantic-image-search-amazon-titan' as `/${string}`, }, { name: 'Building ChatGPT Plugins', - url: '/guides/ai/examples/building-chatgpt-plugins', + url: '/guides/ai/examples/building-chatgpt-plugins' as `/${string}`, }, ], }, @@ -1925,31 +2005,31 @@ export const ai = { items: [ { name: 'LangChain', - url: '/guides/ai/langchain', + url: '/guides/ai/langchain' as `/${string}`, }, { name: 'Hugging Face', - url: '/guides/ai/hugging-face', + url: '/guides/ai/hugging-face' as `/${string}`, }, { name: 'Google Colab', - url: '/guides/ai/google-colab', + url: '/guides/ai/google-colab' as `/${string}`, }, { name: 'LlamaIndex', - url: '/guides/ai/integrations/llamaindex', + url: '/guides/ai/integrations/llamaindex' as `/${string}`, }, { name: 'Roboflow', - url: '/guides/ai/integrations/roboflow', + url: '/guides/ai/integrations/roboflow' as `/${string}`, }, { name: 'Amazon Bedrock', - url: '/guides/ai/integrations/amazon-bedrock', + url: '/guides/ai/integrations/amazon-bedrock' as `/${string}`, }, { name: 'Mixpeek', - url: '/guides/ai/examples/mixpeek-video-search', + url: '/guides/ai/examples/mixpeek-video-search' as `/${string}`, }, ], }, @@ -1978,23 +2058,23 @@ export const local_development: NavMenuConstant = { { name: 'Getting started', url: '/guides/local-development/overview' }, { name: 'Declarative database schemas', - url: '/guides/local-development/declarative-database-schemas', + url: '/guides/local-development/declarative-database-schemas' as `/${string}`, }, { name: 'Seeding your database', - url: '/guides/local-development/seeding-your-database', + url: '/guides/local-development/seeding-your-database' as `/${string}`, }, { name: 'Managing config and secrets', - url: '/guides/local-development/managing-config', + url: '/guides/local-development/managing-config' as `/${string}`, }, { name: 'Restoring downloaded backup', - url: '/guides/local-development/restoring-downloaded-backup', + url: '/guides/local-development/restoring-downloaded-backup' as `/${string}`, }, { name: 'Customizing email templates', - url: '/guides/local-development/customizing-email-templates', + url: '/guides/local-development/customizing-email-templates' as `/${string}`, }, ], }, @@ -2005,12 +2085,12 @@ export const local_development: NavMenuConstant = { { name: 'Getting started', url: '/guides/local-development/testing/overview' }, { name: 'pgTAP advanced guide', - url: '/guides/local-development/testing/pgtap-extended', + url: '/guides/local-development/testing/pgtap-extended' as `/${string}`, }, { name: 'Database testing', url: '/guides/database/testing' }, { name: 'RLS policies testing', - url: '/guides/database/extensions/pgtap#testing-rls-policies', + url: '/guides/database/extensions/pgtap#testing-rls-policies' as `/${string}`, }, ], }, @@ -2120,7 +2200,7 @@ export const security: NavMenuConstant = { { name: 'Production Checklist', url: '/guides/deployment/going-into-prod' }, { name: 'Shared Responsibility Model', - url: '/guides/deployment/shared-responsibility-model', + url: '/guides/deployment/shared-responsibility-model' as `/${string}`, }, { name: 'Row Level Security', url: '/guides/database/postgres/row-level-security' }, { name: 'Hardening the Data API', url: '/guides/database/hardening-data-api' }, @@ -2155,11 +2235,11 @@ export const platform: NavMenuConstant = { items: [ { name: 'Restore Dashboard backup', - url: '/guides/platform/migrating-within-supabase/dashboard-restore', + url: '/guides/platform/migrating-within-supabase/dashboard-restore' as `/${string}`, }, { name: 'Backup and restore using CLI', - url: '/guides/platform/migrating-within-supabase/backup-restore', + url: '/guides/platform/migrating-within-supabase/backup-restore' as `/${string}`, }, ], }, @@ -2176,7 +2256,7 @@ export const platform: NavMenuConstant = { items: [ { name: 'Access Control', - url: '/guides/platform/access-control', + url: '/guides/platform/access-control' as `/${string}`, }, { name: 'Multi-factor Authentication', @@ -2184,13 +2264,13 @@ export const platform: NavMenuConstant = { items: [ { name: 'Enforce MFA on organization', - url: '/guides/platform/mfa/org-mfa-enforcement', + url: '/guides/platform/mfa/org-mfa-enforcement' as `/${string}`, }, ], }, { name: 'Transfer Project', - url: '/guides/platform/project-transfer', + url: '/guides/platform/project-transfer' as `/${string}`, }, { name: 'Restore to a new project', @@ -2203,7 +2283,7 @@ export const platform: NavMenuConstant = { { name: 'SSO with Azure AD', url: '/guides/platform/sso/azure' }, { name: 'SSO with Google Workspace', - url: '/guides/platform/sso/gsuite', + url: '/guides/platform/sso/gsuite' as `/${string}`, }, { name: 'SSO with Okta', url: '/guides/platform/sso/okta' }, ], @@ -2214,137 +2294,143 @@ export const platform: NavMenuConstant = { name: 'Platform Configuration', url: undefined, items: [ - { name: 'Regions', url: '/guides/platform/regions' }, - { name: 'Compute and Disk', url: '/guides/platform/compute-and-disk' }, - { name: 'Database Size', url: '/guides/platform/database-size' }, - { name: 'HIPAA Projects', url: '/guides/platform/hipaa-projects' }, + { name: 'Regions', url: '/guides/platform/regions' as `/${string}` }, + { name: 'Compute and Disk', url: '/guides/platform/compute-and-disk' as `/${string}` }, + { name: 'Database Size', url: '/guides/platform/database-size' as `/${string}` }, + { name: 'HIPAA Projects', url: '/guides/platform/hipaa-projects' as `/${string}` }, { name: 'Network Restrictions', - url: '/guides/platform/network-restrictions', + url: '/guides/platform/network-restrictions' as `/${string}`, }, - { name: 'Performance Tuning', url: '/guides/platform/performance' }, - { name: 'SSL Enforcement', url: '/guides/platform/ssl-enforcement' }, - { name: 'Default Platform Permissions', url: '/guides/platform/permissions' }, - { name: 'PrivateLink', url: '/guides/platform/privatelink' }, + { name: 'Performance Tuning', url: '/guides/platform/performance' as `/${string}` }, + { name: 'SSL Enforcement', url: '/guides/platform/ssl-enforcement' as `/${string}` }, + { + name: 'Default Platform Permissions', + url: '/guides/platform/permissions' as `/${string}`, + }, + { name: 'PrivateLink', url: '/guides/platform/privatelink' as `/${string}` }, ], }, { name: 'Billing', url: undefined, + enabled: billingEnabled, items: [ { name: 'About billing on Supabase', - url: '/guides/platform/billing-on-supabase', + url: '/guides/platform/billing-on-supabase' as `/${string}`, }, { name: 'Get set up for billing', - url: '/guides/platform/get-set-up-for-billing', + url: '/guides/platform/get-set-up-for-billing' as `/${string}`, }, { name: 'Manage your subscription', - url: '/guides/platform/manage-your-subscription', + url: '/guides/platform/manage-your-subscription' as `/${string}`, }, { name: 'Manage your usage', - url: '/guides/platform/manage-your-usage', + url: '/guides/platform/manage-your-usage' as `/${string}`, items: [ + { name: 'Usage limits', url: '/guides/platform/usage-limits' as `/${string}` }, + { name: 'Overages', url: '/guides/platform/overages' as `/${string}` }, { name: 'Compute', - url: '/guides/platform/manage-your-usage/compute', + url: '/guides/platform/manage-your-usage/compute' as `/${string}`, }, { name: 'Egress', - url: '/guides/platform/manage-your-usage/egress', + url: '/guides/platform/manage-your-usage/egress' as `/${string}`, }, { name: 'Disk Size', - url: '/guides/platform/manage-your-usage/disk-size', + url: '/guides/platform/manage-your-usage/disk-size' as `/${string}`, }, { name: 'Disk Throughput', - url: '/guides/platform/manage-your-usage/disk-throughput', + url: '/guides/platform/manage-your-usage/disk-throughput' as `/${string}`, }, { name: 'Disk IOPS', - url: '/guides/platform/manage-your-usage/disk-iops', + url: '/guides/platform/manage-your-usage/disk-iops' as `/${string}`, }, { name: 'Monthly Active Users', - url: '/guides/platform/manage-your-usage/monthly-active-users', + url: '/guides/platform/manage-your-usage/monthly-active-users' as `/${string}`, }, { name: 'Monthly Active Third-Party Users', - url: '/guides/platform/manage-your-usage/monthly-active-users-third-party', + url: '/guides/platform/manage-your-usage/monthly-active-users-third-party' as `/${string}`, }, { name: 'Monthly Active SSO Users', - url: '/guides/platform/manage-your-usage/monthly-active-users-sso', + url: '/guides/platform/manage-your-usage/monthly-active-users-sso' as `/${string}`, }, { name: 'Storage Size', - url: '/guides/platform/manage-your-usage/storage-size', + url: '/guides/platform/manage-your-usage/storage-size' as `/${string}`, }, { name: 'Storage Image Transformations', - url: '/guides/platform/manage-your-usage/storage-image-transformations', + url: '/guides/platform/manage-your-usage/storage-image-transformations' as `/${string}`, }, { name: 'Edge Function Invocations', - url: '/guides/platform/manage-your-usage/edge-function-invocations', + url: '/guides/platform/manage-your-usage/edge-function-invocations' as `/${string}`, }, { name: 'Realtime Messages', - url: '/guides/platform/manage-your-usage/realtime-messages', + url: '/guides/platform/manage-your-usage/realtime-messages' as `/${string}`, }, { name: 'Realtime Peak Connections', - url: '/guides/platform/manage-your-usage/realtime-peak-connections', + url: '/guides/platform/manage-your-usage/realtime-peak-connections' as `/${string}`, }, { name: 'Custom Domains', - url: '/guides/platform/manage-your-usage/custom-domains', + url: '/guides/platform/manage-your-usage/custom-domains' as `/${string}`, }, { name: 'Point-in-Time Recovery', - url: '/guides/platform/manage-your-usage/point-in-time-recovery', + url: '/guides/platform/manage-your-usage/point-in-time-recovery' as `/${string}`, }, { name: 'IPv4', - url: '/guides/platform/manage-your-usage/ipv4', + url: '/guides/platform/manage-your-usage/ipv4' as `/${string}`, }, { name: 'MFA Phone', - url: '/guides/platform/manage-your-usage/advanced-mfa-phone', + url: '/guides/platform/manage-your-usage/advanced-mfa-phone' as `/${string}`, }, { name: 'Read Replicas', - url: '/guides/platform/manage-your-usage/read-replicas', + url: '/guides/platform/manage-your-usage/read-replicas' as `/${string}`, }, { name: 'Branching', - url: '/guides/platform/manage-your-usage/branching', + url: '/guides/platform/manage-your-usage/branching' as `/${string}`, }, { name: 'Log Drains', - url: '/guides/platform/manage-your-usage/log-drains', + url: '/guides/platform/manage-your-usage/log-drains' as `/${string}`, }, ], }, { name: 'Your monthly invoice', - url: '/guides/platform/your-monthly-invoice', + url: '/guides/platform/your-monthly-invoice' as `/${string}`, }, { name: 'Control your costs', - url: '/guides/platform/cost-control', + url: '/guides/platform/cost-control' as `/${string}`, }, { name: 'Credits', - url: '/guides/platform/credits', + url: '/guides/platform/credits' as `/${string}`, }, { name: 'Billing FAQ', - url: '/guides/platform/billing-faq', + url: '/guides/platform/billing-faq' as `/${string}`, }, ], }, @@ -2363,27 +2449,27 @@ export const telemetry: NavMenuConstant = { items: [ { name: 'Logging', - url: '/guides/telemetry/logs', + url: '/guides/telemetry/logs' as `/${string}`, }, { name: 'Advanced log filtering', - url: '/guides/telemetry/advanced-log-filtering', + url: '/guides/telemetry/advanced-log-filtering' as `/${string}`, }, { name: 'Log drains', - url: '/guides/telemetry/log-drains', + url: '/guides/telemetry/log-drains' as `/${string}`, }, { name: 'Reports', - url: '/guides/telemetry/reports', + url: '/guides/telemetry/reports' as `/${string}`, }, { name: 'Metrics', - url: '/guides/telemetry/metrics', + url: '/guides/telemetry/metrics' as `/${string}`, }, { name: 'Sentry integration', - url: '/guides/telemetry/sentry-monitoring', + url: '/guides/telemetry/sentry-monitoring' as `/${string}`, }, ], }, @@ -2416,7 +2502,7 @@ export const self_hosting: NavMenuConstant = { items: [ { name: 'Reference', - url: '/reference/self-hosting-storage/introduction', + url: '/reference/self-hosting-storage/introduction' as `/${string}`, }, { name: 'Configuration', url: '/guides/self-hosting/storage/config' }, ], @@ -2426,7 +2512,7 @@ export const self_hosting: NavMenuConstant = { items: [ { name: 'Reference', - url: '/reference/self-hosting-realtime/introduction', + url: '/reference/self-hosting-realtime/introduction' as `/${string}`, }, { name: 'Configuration', url: '/guides/self-hosting/realtime/config' }, ], @@ -2480,12 +2566,12 @@ export const deployment: NavMenuConstant = { { name: 'Branching via GitHub', url: '/guides/deployment/branching/github-integration' }, { name: 'Branching via dashboard', - url: '/guides/deployment/branching/dashboard', + url: '/guides/deployment/branching/dashboard' as `/${string}`, }, { name: 'Working with branches', - url: '/guides/deployment/branching/working-with-branches', + url: '/guides/deployment/branching/working-with-branches' as `/${string}`, }, { name: 'Configuration', url: '/guides/deployment/branching/configuration' }, { name: 'Integrations', url: '/guides/deployment/branching/integrations' }, @@ -2506,7 +2592,7 @@ export const deployment: NavMenuConstant = { items: [ { name: 'Shared responsibility model', - url: '/guides/deployment/shared-responsibility-model', + url: '/guides/deployment/shared-responsibility-model' as `/${string}`, }, { name: 'Maturity model', url: '/guides/deployment/maturity-model' }, { name: 'Production checklist', url: '/guides/deployment/going-into-prod' }, @@ -2518,7 +2604,7 @@ export const deployment: NavMenuConstant = { items: [ { name: 'Generate types from your database', - url: '/guides/deployment/ci/generating-types', + url: '/guides/deployment/ci/generating-types' as `/${string}`, }, { name: 'Automated testing', url: '/guides/deployment/ci/testing' }, { name: 'Back up your database', url: '/guides/deployment/ci/backups' }, @@ -2554,7 +2640,7 @@ export const integrations: NavMenuConstant = { items: [ { name: 'OAuth scopes', - url: '/guides/integrations/build-a-supabase-integration/oauth-scopes', + url: '/guides/integrations/build-a-supabase-integration/oauth-scopes' as `/${string}`, }, ], }, @@ -2575,39 +2661,44 @@ export const reference = { name: 'supabase-js', url: '/reference/javascript/start', level: 'reference_javascript', - icon: '/img/icons/menu/reference-javascript', + icon: '/img/icons/menu/reference-javascript' as `/${string}`, }, { name: 'supabase-dart', url: '/reference/dart/start', level: 'reference_dart', - icon: '/img/icons/menu/reference-dart', + icon: '/img/icons/menu/reference-dart' as `/${string}`, + enabled: sdkDartEnabled, }, { name: 'supabase-csharp', url: '/reference/csharp/start', level: 'reference_csharp', - icon: '/img/icons/menu/reference-csharp', + icon: '/img/icons/menu/reference-csharp' as `/${string}`, + enabled: sdkCsharpEnabled, }, { name: 'supbase-python', url: '/reference/python/start', level: 'reference_python', - icon: '/img/icons/menu/reference-python', + icon: '/img/icons/menu/reference-python' as `/${string}`, + enabled: sdkPythonEnabled, }, { name: 'supbase-swift', url: '/reference/swift/start', level: 'reference_swift', items: [], - icon: '/img/icons/menu/reference-swift', + icon: '/img/icons/menu/reference-swift' as `/${string}`, + enabled: sdkSwiftEnabled, }, { name: 'supabase-kt', url: '/reference/kotlin/start', level: 'reference_kotlin', items: [], - icon: '/img/icons/menu/reference-kotlin', + icon: '/img/icons/menu/reference-kotlin' as `/${string}`, + enabled: sdkKotlinEnabled, }, ], }, @@ -2617,12 +2708,12 @@ export const reference = { { name: 'Supabase CLI', url: '/reference/cli/start', - icon: '/img/icons/menu/reference-cli', + icon: '/img/icons/menu/reference-cli' as `/${string}`, }, { name: 'Management API', url: '/reference/javascript', - icon: '/img/icons/menu/reference-api', + icon: '/img/icons/menu/reference-api' as `/${string}`, }, ], }, @@ -2651,6 +2742,7 @@ export const reference_javascript_v2 = { }, } +// TODO: How to? export const reference_dart_v1 = { icon: 'reference-dart', title: 'Flutter', @@ -2829,6 +2921,7 @@ export const references = [ description: 'something about the reference', icon: '/docs/img/icons/python-icon.svg', url: '/reference/python/start', + enabled: sdkPythonEnabled, }, { label: 'supabase-dart', @@ -2836,6 +2929,7 @@ export const references = [ description: 'something about the reference', icon: '/docs/img/icons/dart-icon.svg', url: '/reference/dart/start', + enabled: sdkDartEnabled, }, { label: 'supabase-csharp', @@ -2843,6 +2937,7 @@ export const references = [ description: 'something about the reference', icon: '/docs/img/icons/c-sharp-icon.svg', url: '/reference/csharp/start', + enabled: sdkCsharpEnabled, }, { label: 'supabase-swift', @@ -2850,6 +2945,7 @@ export const references = [ description: 'something about the reference', icon: '/docs/img/icons/swift-icon.svg', url: '/reference/swift/start', + enabled: sdkSwiftEnabled, }, { label: 'supabase-kt', @@ -2857,6 +2953,7 @@ export const references = [ description: 'something about the reference', icon: '/docs/img/icons/kotlin-icon.svg', url: '/reference/kotlin/start', + enabled: sdkKotlinEnabled, }, ], }, diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideListItems.tsx b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideListItems.tsx index cc6c052a369..25fb97f180c 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideListItems.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideListItems.tsx @@ -94,23 +94,25 @@ const ContentAccordionLink = React.memo(function ContentAccordionLink(props: any {props.subItem.items && props.subItem.items.length > 0 && ( - {props.subItem.items.map((subSubItem) => { - return ( -
  • - - {subSubItem.name} - -
  • - ) - })} + {props.subItem.items + .filter((subItem) => subItem.enabled !== false) + .map((subSubItem) => { + return ( +
  • + + {subSubItem.name} + +
  • + ) + })}
    )} @@ -153,28 +155,32 @@ const Content = (props) => { - {menu.items.map((x) => { - return ( -
    - {x.items && x.items.length > 0 ? ( -
    - {x.items.map((subItem, subItemIndex) => { - return ( - - ) - })} -
    - ) : x.url ? ( - - ) : null} -
    - ) - })} + {menu.items + .filter((item) => item.enabled !== false) + .map((x) => { + return ( +
    + {x.items && x.items.length > 0 ? ( +
    + {x.items + .filter((item) => item.enabled !== false) + .map((subItem, subItemIndex) => { + return ( + + ) + })} +
    + ) : x.url ? ( + + ) : null} +
    + ) + })} ) } diff --git a/apps/docs/content/_partials/billing/pricing/pricing_custom_domains.mdx b/apps/docs/content/_partials/billing/pricing/pricing_custom_domains.mdx new file mode 100644 index 00000000000..3e5d3b686d5 --- /dev/null +++ b/apps/docs/content/_partials/billing/pricing/pricing_custom_domains.mdx @@ -0,0 +1,3 @@ +## Pricing + +For a detailed breakdown of how charges are calculated, refer to [Manage Custom Domain usage](/docs/guides/platform/manage-your-usage/custom-domains). diff --git a/apps/docs/content/_partials/billing/pricing/pricing_ipv4.mdx b/apps/docs/content/_partials/billing/pricing/pricing_ipv4.mdx new file mode 100644 index 00000000000..e07ea856d3d --- /dev/null +++ b/apps/docs/content/_partials/billing/pricing/pricing_ipv4.mdx @@ -0,0 +1,3 @@ +## Pricing + +For a detailed breakdown of how charges are calculated, refer to [Manage IPv4 usage](/docs/guides/platform/manage-your-usage/ipv4). diff --git a/apps/docs/content/_partials/billing/pricing/pricing_mau_sso.mdx b/apps/docs/content/_partials/billing/pricing/pricing_mau_sso.mdx index bf5d2628bd6..8ba7741e9b9 100644 --- a/apps/docs/content/_partials/billing/pricing/pricing_mau_sso.mdx +++ b/apps/docs/content/_partials/billing/pricing/pricing_mau_sso.mdx @@ -1,2 +1,6 @@ +## Pricing + per SSO MAU. You are only charged for usage exceeding your subscription plan's quota. + +For a detailed breakdown of how charges are calculated, refer to [Manage Monthly Active SSO Users usage](/docs/guides/platform/manage-your-usage/monthly-active-users-sso). diff --git a/apps/docs/content/_partials/billing/pricing/pricing_mau_third_party.mdx b/apps/docs/content/_partials/billing/pricing/pricing_mau_third_party.mdx index aa7576751bd..cbfccef3877 100644 --- a/apps/docs/content/_partials/billing/pricing/pricing_mau_third_party.mdx +++ b/apps/docs/content/_partials/billing/pricing/pricing_mau_third_party.mdx @@ -1,2 +1,6 @@ +## Pricing + per Third-Party MAU. You are only charged for usage exceeding your subscription plan's quota. + +For a detailed breakdown of how charges are calculated, refer to [Manage Monthly Active Third-Party Users usage](/docs/guides/platform/manage-your-usage/monthly-active-users-third-party). diff --git a/apps/docs/content/_partials/billing/pricing/pricing_mfa_phone.mdx b/apps/docs/content/_partials/billing/pricing/pricing_mfa_phone.mdx index 5635b906706..6708ec7a9d0 100644 --- a/apps/docs/content/_partials/billing/pricing/pricing_mfa_phone.mdx +++ b/apps/docs/content/_partials/billing/pricing/pricing_mfa_phone.mdx @@ -1,3 +1,5 @@ +## Pricing + per hour ( per month) for the first project. per hour ( per month) for every additional project. @@ -6,3 +8,5 @@ hour ( per month) for every additional project. | Pro | | | | | Team | | | | | Enterprise | Custom | Custom | Custom | + +For a detailed breakdown of how charges are calculated, refer to [Manage Advanced MFA Phone usage](/docs/guides/platform/manage-your-usage/advanced-mfa-phone). diff --git a/apps/docs/content/_partials/billing/pricing/pricing_pitr.mdx b/apps/docs/content/_partials/billing/pricing/pricing_pitr.mdx index 36b2c8801f1..98559804a69 100644 --- a/apps/docs/content/_partials/billing/pricing/pricing_pitr.mdx +++ b/apps/docs/content/_partials/billing/pricing/pricing_pitr.mdx @@ -1,3 +1,5 @@ +### Pricing + Pricing depends on the recovery retention period, which determines how many days back you can restore data to any chosen point of up to seconds in granularity. | Recovery Retention Period in Days | Hourly Price USD | Monthly Price USD | @@ -5,3 +7,5 @@ Pricing depends on the recovery retention period, which determines how many days | 7 | | | | 14 | | | | 28 | | | + +For a detailed breakdown of how charges are calculated, refer to [Manage Point-in-Time Recovery usage](/docs/guides/platform/manage-your-usage/point-in-time-recovery). diff --git a/apps/docs/content/_partials/billing/pricing/pricing_storage_image_transformations.mdx b/apps/docs/content/_partials/billing/pricing/pricing_storage_image_transformations.mdx index 4c4d4d429db..f8d93e45ab6 100644 --- a/apps/docs/content/_partials/billing/pricing/pricing_storage_image_transformations.mdx +++ b/apps/docs/content/_partials/billing/pricing/pricing_storage_image_transformations.mdx @@ -1,3 +1,5 @@ +## Pricing + per 1,000 origin images. You are only charged for usage exceeding your subscription plan's quota. @@ -12,3 +14,5 @@ The count resets at the start of each billing cycle. | Pro | 100 | per 1,000 origin images | | Team | 100 | per 1,000 origin images | | Enterprise | Custom | Custom | + +For a detailed breakdown of how charges are calculated, refer to [Manage Storage Image Transformations usage](/docs/guides/platform/manage-your-usage/storage-image-transformations). diff --git a/apps/docs/content/_partials/providers.mdx b/apps/docs/content/_partials/providers.mdx new file mode 100644 index 00000000000..4f4fe7f2def --- /dev/null +++ b/apps/docs/content/_partials/providers.mdx @@ -0,0 +1,11 @@ +## Providers + +Supabase Auth works with many popular Auth methods, including Social and Phone Auth using third-party providers. See the following sections for a list of supported third-party providers. + +### Social Auth + + + +### Phone Auth + + diff --git a/apps/docs/content/guides/api/quickstart.mdx b/apps/docs/content/guides/api/quickstart.mdx index 7ba5b9b8809..432838a2aa8 100644 --- a/apps/docs/content/guides/api/quickstart.mdx +++ b/apps/docs/content/guides/api/quickstart.mdx @@ -156,6 +156,7 @@ const { data, error } = await supabase.from('todos').select() ``` +<$Show if="sdk:dart"> ```dart @@ -163,6 +164,8 @@ final data = await supabase.from('todos').select('*'); ``` + +<$Show if="sdk:python"> ```python @@ -170,6 +173,8 @@ response = supabase.table('todos').select("*").execute() ``` + +<$Show if="sdk:swift"> ```swift @@ -177,4 +182,5 @@ let response = try await supabase.from("todos").select() ``` + diff --git a/apps/docs/content/guides/api/using-custom-schemas.mdx b/apps/docs/content/guides/api/using-custom-schemas.mdx index 5f9fe4770d5..55af9da01d2 100644 --- a/apps/docs/content/guides/api/using-custom-schemas.mdx +++ b/apps/docs/content/guides/api/using-custom-schemas.mdx @@ -57,8 +57,8 @@ const { data: todos, error } = await supabase.schema('myschema').from('todos').s ``` +<$Show if="sdk:dart"> - ```dart // Initialize the Flutter client await Supabase.initialize( @@ -73,9 +73,10 @@ final data = await supabase.from('todos').select(); // You can also change the target schema on a per-query basis final data = await supabase.schema('myschema').from('todos').select(); -``` +```` + ```bash @@ -94,7 +95,7 @@ curl -X POST '/rest/v1/todos' \ -H "Content-Type: application/json" \ -H "Content-Profile: myschema" \ -d '{"column_name": "value"}' -``` +```` diff --git a/apps/docs/content/guides/auth.mdx b/apps/docs/content/guides/auth.mdx index 213d1b91a1c..849e30219a0 100644 --- a/apps/docs/content/guides/auth.mdx +++ b/apps/docs/content/guides/auth.mdx @@ -27,17 +27,9 @@ Auth uses your project's Postgres database under the hood, storing user data and Auth also enables access control to your database's automatically generated [REST API](/docs/guides/api). When using Supabase SDKs, your data requests are automatically sent with the user's Auth Token. The Auth Token scopes database access on a row-by-row level when used along with [RLS policies](/docs/guides/database/postgres/row-level-security). -## Providers - -Supabase Auth works with many popular Auth methods, including Social and Phone Auth using third-party providers. See the following sections for a list of supported third-party providers. - -### Social Auth - - - -### Phone Auth - - +<$Show if="authentication:show_providers"> +<$Partial path="providers.mdx" /> + ## Pricing diff --git a/apps/docs/content/guides/auth/auth-anonymous.mdx b/apps/docs/content/guides/auth/auth-anonymous.mdx index 30b6af58592..f6c16268b0f 100644 --- a/apps/docs/content/guides/auth/auth-anonymous.mdx +++ b/apps/docs/content/guides/auth/auth-anonymous.mdx @@ -64,6 +64,7 @@ const { data, error } = await supabase.auth.signInAnonymously() ``` +<$Show if="sdk:dart"> Call the [`signInAnonymously()`](/docs/reference/dart/auth-signinanonymously) method: @@ -73,6 +74,8 @@ await supabase.auth.signInAnonymously(); ``` + +<$Show if="sdk:swift"> Call the [`signInAnonymously()`](/docs/reference/swift/auth-signinanonymously) method: @@ -82,6 +85,8 @@ let session = try await supabase.auth.signInAnonymously() ``` + +<$Show if="sdk:kotlin"> Call the [`signInAnonymously()`](/docs/reference/kotlin/auth-signinanonymously) method: @@ -91,6 +96,8 @@ supabase.auth.signInAnonymously() ``` + +<$Show if="sdk:python"> Call the [`sign_in_anonymously()`](/docs/reference/python/auth-signinanonymously) method: @@ -100,6 +107,7 @@ response = supabase.auth.sign_in_anonymously() ``` + ## Convert an anonymous user to a permanent user @@ -138,6 +146,7 @@ const { data: updatePasswordData, error: updatePasswordError } = await supabase. ``` +<$Show if="sdk:dart"> You can use the [`updateUser()`](/docs/reference/dart/auth-updateuser) method to link an email or phone identity to the anonymous user. @@ -147,6 +156,8 @@ await supabase.auth.updateUser(UserAttributes(email: 'valid.email@supabase.io')) ``` + +<$Show if="sdk:swift"> You can use the [`updateUser()`](/docs/reference/swift/auth-updateuser) method to link an email or phone identity to the anonymous user. @@ -158,6 +169,8 @@ try await supabase.auth.updateUser( ``` + +<$Show if="sdk:kotlin"> You can use the [`updateUser()`](/docs/reference/kotlin/auth-updateuser) method to link an email or phone identity to the anonymous user. @@ -169,6 +182,8 @@ supabase.auth.updateUser { ``` + +<$Show if="sdk:python"> You can use the [`update_user()`](/docs/reference/python/auth-updateuser) method to link an email or phone identity to the anonymous user. To add a password for the anonymous user, the user's email or phone number needs to be verified first. @@ -188,6 +203,7 @@ response = supabase.auth.update_user({ ``` + ### Link an OAuth identity @@ -212,6 +228,7 @@ const { data, error } = await supabase.auth.linkIdentity({ provider: 'google' }) ``` +<$Show if="sdk:dart"> You can use the [`linkIdentity()`](/docs/reference/dart/auth-linkidentity) method to link an OAuth identity to the anonymous user. @@ -221,6 +238,8 @@ await supabase.auth.linkIdentity(OAuthProvider.google); ``` + +<$Show if="sdk:swift"> You can use the [`linkIdentity()`](/docs/reference/swift/auth-linkidentity) method to link an OAuth identity to the anonymous user. @@ -230,6 +249,8 @@ try await supabase.auth.linkIdentity(provider: .google) ``` + +<$Show if="sdk:kotlin"> You can use the [`linkIdentity()`](/docs/reference/kotlin/auth-linkidentity) method to link an OAuth identity to the anonymous user. @@ -239,6 +260,8 @@ supabase.auth.linkIdentity(Google) ``` + +<$Show if="sdk:python"> You can use the [`link_identity()`](/docs/reference/python/auth-linkidentity) method to link an OAuth identity to the anonymous user. @@ -248,6 +271,7 @@ response = supabase.auth.link_identity({'provider': 'google'}) ``` + ## Access control diff --git a/apps/docs/content/guides/auth/auth-email-passwordless.mdx b/apps/docs/content/guides/auth/auth-email-passwordless.mdx index c6f2a6af289..ee2fc051a05 100644 --- a/apps/docs/content/guides/auth/auth-email-passwordless.mdx +++ b/apps/docs/content/guides/auth/auth-email-passwordless.mdx @@ -81,6 +81,7 @@ const { error } = await supabase.auth.signInWithOtp({ Read the [Deep Linking Documentation](/docs/guides/auth/native-mobile-deep-linking) to learn how to handle deep linking. +<$Show if="sdk:dart"> ```dart @@ -90,6 +91,8 @@ Future signInWithEmail() async { ``` + +<$Show if="sdk:swift"> ```swift @@ -102,6 +105,8 @@ try await supabase.auth.signInWithOTP( ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -113,6 +118,8 @@ suspend fun signInWithEmail() { ``` + +<$Show if="sdk:python"> ```python @@ -127,6 +134,7 @@ response = supabase.auth.sign_in_with_otp({ ``` + {/* supa-mdx-lint-disable-next-line Rule004ExcludeWords */} @@ -207,6 +215,7 @@ const { data, error } = await supabase.auth.signInWithOtp({ ``` +<$Show if="sdk:dart"> ```dart @@ -216,6 +225,8 @@ Future signInWithEmailOtp() async { ``` + +<$Show if="sdk:swift"> ```swift @@ -227,6 +238,8 @@ try await supabase.auth.signInWithOTP( ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -238,6 +251,8 @@ suspend fun signInWithEmailOtp() { ``` + +<$Show if="sdk:python"> ```python @@ -251,6 +266,7 @@ response = supabase.auth.sign_in_with_otp({ ``` + If the request is successful, you receive a response with `error: null` and a `data` object where both `user` and `session` are null. Let the user know to check their email inbox. @@ -296,6 +312,7 @@ const { ``` +<$Show if="sdk:swift"> ```swift @@ -307,6 +324,8 @@ try await supabase.auth.verifyOTP( ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -314,6 +333,8 @@ supabase.auth.verifyEmailOtp(type = OtpType.Email.EMAIL, email = "email", token ``` + +<$Show if="sdk:python"> ```python @@ -325,6 +346,7 @@ response = supabase.auth.verify_otp({ ``` + If successful, the user is now logged in, and you receive a valid session that looks like: diff --git a/apps/docs/content/guides/auth/auth-identity-linking.mdx b/apps/docs/content/guides/auth/auth-identity-linking.mdx index 43198263a2c..8ccdb45aae4 100644 --- a/apps/docs/content/guides/auth/auth-identity-linking.mdx +++ b/apps/docs/content/guides/auth/auth-identity-linking.mdx @@ -42,6 +42,7 @@ const { data, error } = await supabase.auth.linkIdentity({ provider: 'google' }) ``` +<$Show if="sdk:dart"> Supabase Auth allows a user to initiate identity linking with a different email address when they are logged in. To link an OAuth identity to the user, call [`linkIdentity()`](/docs/reference/dart/auth-linkidentity): @@ -51,6 +52,8 @@ await supabase.auth.linkIdentity(OAuthProvider.google); ``` + +<$Show if="sdk:swift"> Supabase Auth allows a user to initiate identity linking with a different email address when they are logged in. To link an OAuth identity to the user, call [`linkIdentity()`](/docs/reference/swift/auth-linkidentity): @@ -60,6 +63,8 @@ try await supabase.auth.linkIdentity(provider: .google) ``` + +<$Show if="sdk:kotlin"> Supabase Auth allows a user to initiate identity linking with a different email address when they are logged in. To link an OAuth identity to the user, call [`linkIdentity()`](/docs/reference/kotlin/auth-linkidentity): @@ -69,6 +74,8 @@ supabase.auth.linkIdentity(Google) ``` + +<$Show if="sdk:python"> Supabase Auth allows a user to initiate identity linking with a different email address when they are logged in. To link an OAuth identity to the user, call [`link_identity()`](/docs/reference/python/auth-linkidentity): @@ -78,6 +85,7 @@ response = supabase.auth.link_identity({'provider': 'google'}) ``` + In the example above, the user will be redirected to Google to complete the OAuth2.0 flow. Once the OAuth2.0 flow has completed successfully, the user will be redirected back to the application and the Google identity will be linked to the user. You can enable manual linking from your project's authentication [configuration options](/dashboard/project/_/settings/auth) or by setting the environment variable `GOTRUE_SECURITY_MANUAL_LINKING_ENABLED: true` when self-hosting. @@ -115,6 +123,7 @@ if (!identitiesError) { ``` +<$Show if="sdk:dart"> You can use [`getUserIdentities()`](/docs/reference/dart/auth-getuseridentities) to fetch all the identities linked to a user. Then, call [`unlinkIdentity()`](/docs/reference/dart/auth-unlinkidentity) to unlink the identity. The user needs to be logged in and have at least 2 linked identities in order to unlink an existing identity. @@ -132,6 +141,8 @@ await supabase.auth.unlinkIdentity(googleIdentity); ``` + +<$Show if="sdk:swift"> You can use [`getUserIdentities()`](/docs/reference/swift/auth-getuseridentities) to fetch all the identities linked to a user. Then, call [`unlinkIdentity()`](/docs/reference/swift/auth-unlinkidentity) to unlink the identity. The user needs to be logged in and have at least 2 linked identities in order to unlink an existing identity. @@ -148,6 +159,8 @@ try await supabase.auth.unlinkIdentity(googleIdentity) ``` + +<$Show if="sdk:kotlin"> You can use [`currentIdentitiesOrNull()`](/docs/reference/kotlin/auth-getuseridentities) to get all the identities linked to a user. Then, call [`unlinkIdentity()`](/docs/reference/kotlin/auth-unlinkidentity) to unlink the identity. The user needs to be logged in and have at least 2 linked identities in order to unlink an existing identity. @@ -164,6 +177,8 @@ supabase.auth.unlinkIdentity(googleIdentity.identityId!!) ``` + +<$Show if="sdk:python"> You can use [`get_user_identities()`](/docs/reference/python/auth-getuseridentities) to fetch all the identities linked to a user. Then, call [`unlink_identity()`](/docs/reference/python/auth-unlinkidentity) to unlink the identity. The user needs to be logged in and have at least 2 linked identities in order to unlink an existing identity. @@ -181,6 +196,7 @@ if google_identity: ``` + ## Frequently asked questions diff --git a/apps/docs/content/guides/auth/auth-mfa/phone.mdx b/apps/docs/content/guides/auth/auth-mfa/phone.mdx index 35292146ba8..f29187d7227 100644 --- a/apps/docs/content/guides/auth/auth-mfa/phone.mdx +++ b/apps/docs/content/guides/auth/auth-mfa/phone.mdx @@ -310,8 +310,6 @@ Each code is valid for up to 5 minutes, after which a new one can be sent. Succe Be aware that Phone MFA is vulnerable to SIM swap attacks where an attacker will call a mobile provider and ask to port the target's phone number to a new SIM card and then use the said SIM card to intercept an MFA code. Evaluate the your application's tolerance for such an attack. You can read more about SIM swapping attacks [here](https://en.wikipedia.org/wiki/SIM_swap_scam) -## Pricing - +<$Show if="billing:all"> <$Partial path="billing/pricing/pricing_mfa_phone.mdx" /> - -For a detailed breakdown of how charges are calculated, refer to [Manage Advanced MFA Phone usage](/docs/guides/platform/manage-your-usage/advanced-mfa-phone). + diff --git a/apps/docs/content/guides/auth/debugging/error-codes.mdx b/apps/docs/content/guides/auth/debugging/error-codes.mdx index 43dd3a762d2..9e0b429fcfc 100644 --- a/apps/docs/content/guides/auth/debugging/error-codes.mdx +++ b/apps/docs/content/guides/auth/debugging/error-codes.mdx @@ -36,6 +36,7 @@ Error objects are split in a few classes: Errors originating from the server API classed as `AuthApiError` always have a `code` property that can be used to identify the error returned by the server. The `status` property is also present, encoding the HTTP status code received in the response. +<$Show if="sdk:dart"> All errors originating from the `supabase.auth` namespace of the client library will be wrapped by the `AuthException` class. @@ -44,12 +45,16 @@ Error objects are split in a few classes. `AuthApiException` is an exception whi Errors originating from the server API classed as `AuthApiException` always have a `code` property that can be used to identify the error returned by the server. The `statusCode` property is also present, encoding the HTTP status code received in the response. + +<$Show if="sdk:swift"> All errors originating from the `supabase.auth` namespace of the client library will be a case of the `AuthError` enum. The `api(message:errorCode:underlyingData:underlyingResponse:)` case is a special case for errors which originates from the Supabase Auth API, this error always have an `errorCode` property that can be used to identify the error returned by the server. + +<$Show if="sdk:python"> All errors originating from the `supabase.auth` namespace of the client library will be wrapped by the `AuthError` class. @@ -58,6 +63,8 @@ Error objects are split in a few classes. `AuthApiError` is an error which origi Errors originating from the server API classed as `AuthApiError` always have a `code` property that can be used to identify the error returned by the server. The `status` property is also present, encoding the HTTP status code received in the response. + +<$Show if="sdk:kotlin"> All exceptions originating from the `supabase.auth` namespace of the Kotlin client library will be a subclass of `RestException`. @@ -70,6 +77,7 @@ Rest exceptions are split into a few classes: All instances and subclasses of a `AuthRestException` have a `errorCode` property that can be used to identify the error returned by the server. + diff --git a/apps/docs/content/guides/auth/enterprise-sso/auth-sso-saml.mdx b/apps/docs/content/guides/auth/enterprise-sso/auth-sso-saml.mdx index 77144658e12..653b7a4c6e7 100644 --- a/apps/docs/content/guides/auth/enterprise-sso/auth-sso-saml.mdx +++ b/apps/docs/content/guides/auth/enterprise-sso/auth-sso-saml.mdx @@ -206,6 +206,7 @@ supabase.auth.signInWithSSO({ Calling [`signInWithSSO`](/docs/reference/javascript/auth-signinwithsso) starts the sign-in process using the identity provider registered for the `company.com` domain name. It is not required that identity providers be assigned one or multiple domain names, in which case you can use the provider's unique ID instead. +<$Show if="sdk:dart"> ```dart @@ -217,6 +218,8 @@ await supabase.auth.signInWithSSO( Calling [`signInWithSSO`](/docs/reference/dart/auth-signinwithsso) starts the sign-in process using the identity provider registered for the `company.com` domain name. It is not required that identity providers be assigned one or multiple domain names, in which case you can use the provider's unique ID instead. + +<$Show if="sdk:swift"> ```swift @@ -228,6 +231,8 @@ try await supabase.auth.signInWithSSO( Calling [`signInWithSSO`](/docs/reference/swift/auth-signinwithsso) starts the sign-in process using the identity provider registered for the `company.com` domain name. It is not required that identity providers be assigned one or multiple domain names, in which case you can use the provider's unique ID instead. + +<$Show if="sdk:kotlin"> ```kotlin @@ -239,6 +244,7 @@ supabase.auth.signInWith(SSO) { Calling [`signInWith(SSO)`](/docs/reference/kotlin/auth-signinwithsso) starts the sign-in process using the identity provider registered for the `company.com` domain name. It is not required that identity providers be assigned one or multiple domain names, in which case you can use the provider's unique ID instead. + ### Understanding attribute mappings @@ -384,11 +390,9 @@ supabase sso show --project-ref You can use the `-o json` flag to output the information as JSON, should you need to. Other formats may be supported, use `--help` to see all available options. -## Pricing - +<$Show if="billing:all"> <$Partial path="billing/pricing/pricing_mau_sso.mdx" /> - -For a detailed breakdown of how charges are calculated, refer to [Manage Monthly Active SSO Users usage](/docs/guides/platform/manage-your-usage/monthly-active-users-sso). + ## Frequently asked questions diff --git a/apps/docs/content/guides/auth/jwts.mdx b/apps/docs/content/guides/auth/jwts.mdx index cc98b5825f2..bbdf43a7738 100644 --- a/apps/docs/content/guides/auth/jwts.mdx +++ b/apps/docs/content/guides/auth/jwts.mdx @@ -106,6 +106,8 @@ const supabase = createClient( +<$Show if="sdk:dart"> + ```dart @@ -120,6 +122,9 @@ await Supabase.initialize( ``` + + +<$Show if="sdk:swift"> @@ -140,6 +145,9 @@ let supabase = SupabaseClient( ``` + + +<$Show if="sdk:kotlin"> @@ -155,6 +163,7 @@ val supabase = createSupabaseClient( ``` + diff --git a/apps/docs/content/guides/auth/managing-user-data.mdx b/apps/docs/content/guides/auth/managing-user-data.mdx index 150c6069ab2..625f6434d5d 100644 --- a/apps/docs/content/guides/auth/managing-user-data.mdx +++ b/apps/docs/content/guides/auth/managing-user-data.mdx @@ -85,6 +85,7 @@ const { data, error } = await supabase.auth.signUp({ ``` +<$Show if="sdk:dart"> ```dart @@ -99,6 +100,8 @@ final res = await supabase.auth.signUp( ``` + +<$Show if="sdk:swift"> ```swift @@ -113,6 +116,8 @@ try await supabase.auth.signUp( ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -127,6 +132,7 @@ val data = supabase.auth.signUpWith(Email) { ``` + User metadata is stored on the `raw_user_meta_data` column of the `auth.users` table. To view the metadata: @@ -152,6 +158,7 @@ let metadata = user?.user_metadata ``` +<$Show if="sdk:dart"> ```dart @@ -160,6 +167,8 @@ final Map? metadata = user?.userMetadata; ``` + +<$Show if="sdk:swift"> ```swift @@ -168,6 +177,8 @@ let metadata = user.userMetadata ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -178,6 +189,7 @@ val metadata = user?.userMetadata ``` + ## Deleting users diff --git a/apps/docs/content/guides/auth/native-mobile-deep-linking.mdx b/apps/docs/content/guides/auth/native-mobile-deep-linking.mdx index 28058e5fe40..c8473e7dc08 100644 --- a/apps/docs/content/guides/auth/native-mobile-deep-linking.mdx +++ b/apps/docs/content/guides/auth/native-mobile-deep-linking.mdx @@ -304,6 +304,8 @@ With Deep Linking, you can configure this redirect to open a specific page. This +<$Show if="sdk:swift"> + ### Deep link config @@ -342,6 +344,8 @@ With Deep Linking, you can configure this redirect to open a specific page. This ``` + + <$Show if="sdk:kotlin"> ### Deep link config @@ -396,4 +400,5 @@ With Deep Linking, you can configure this redirect to open a specific page. This The user will now be authenticated when your app receives a valid deep link! + diff --git a/apps/docs/content/guides/auth/passwords.mdx b/apps/docs/content/guides/auth/passwords.mdx index 218d675e6da..2ff8353c077 100644 --- a/apps/docs/content/guides/auth/passwords.mdx +++ b/apps/docs/content/guides/auth/passwords.mdx @@ -72,6 +72,7 @@ async function signUpNewUser() { ``` +<$Show if="sdk:dart"> To sign up the user, call [signUp()](/docs/reference/dart/auth-signup) with their email address and password: @@ -86,6 +87,8 @@ Future signUpNewUser() async { ``` + +<$Show if="sdk:swift"> To sign up the user, call [signUp()](/docs/reference/swift/auth-signup) with their email address and password. @@ -103,6 +106,8 @@ let response = try await supabase.auth.signUp( ``` + +<$Show if="sdk:kotlin"> To sign up the user, call [signUpWith(Email)](/docs/reference/kotlin/auth-signup) with their email address and password: @@ -117,6 +122,8 @@ suspend fun signUpNewUser() { ``` + +<$Show if="sdk:python"> To sign up the user, call [signUp()](/docs/reference/python/auth-signup) with their email address and password. @@ -136,6 +143,7 @@ data = await supabase.auth.sign_up({ ``` + @@ -409,6 +417,7 @@ async function signUpNewUser() { ``` +<$Show if="sdk:dart"> To sign up the user, call [signUp()](/docs/reference/dart/auth-signup) with their email address and password: @@ -423,6 +432,8 @@ Future signUpNewUser() async { ``` + +<$Show if="sdk:swift"> To sign up the user, call [signUp()](/docs/reference/swift/auth-signup) with their email address and password: @@ -435,6 +446,8 @@ let response = try await supabase.auth.signUp( ``` + +<$Show if="sdk:kotlin"> To sign up the user, call [signUpWith(Email)](/docs/reference/kotlin/auth-signup) with their email address and password: @@ -449,6 +462,8 @@ suspend fun signUpNewUser() { ``` + +<$Show if="sdk:python"> To sign up the user, call [signUp()](/docs/reference/python/auth-signup) with their email address and password: @@ -461,6 +476,7 @@ data = supabase.auth.sign_up({ ``` + @@ -493,6 +509,7 @@ async function signInWithEmail() { ``` +<$Show if="sdk:dart"> When your user signs in, call [`signInWithPassword()`](/docs/reference/dart/auth-signinwithpassword) with their email address and password: @@ -507,6 +524,8 @@ Future signInWithEmail() async { ``` + +<$Show if="sdk:swift"> When your user signs in, call [signIn(email:password:)](/docs/reference/swift/auth-signinwithpassword) with their email address and password: @@ -519,6 +538,8 @@ try await supabase.auth.signIn( ``` + +<$Show if="sdk:kotlin"> When your user signs in, call [signInWith(Email)](/docs/reference/kotlin/auth-signinwithpassword) with their email address and password: @@ -533,6 +554,8 @@ suspend fun signInWithEmail() { ``` + +<$Show if="sdk:python"> When your user signs in, call [sign_in_with_password()](/docs/reference/python/auth-signinwithpassword) with their email address and password: @@ -545,6 +568,7 @@ data = client.auth.sign_in_with_password({ ``` + ### Resetting a password @@ -583,6 +607,7 @@ await supabase.auth.resetPasswordForEmail('valid.email@supabase.io', { ``` +<$Show if="sdk:swift"> ```swift @@ -593,6 +618,8 @@ try await supabase.auth.resetPasswordForEmail( ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -605,6 +632,8 @@ supabase.auth.resetPasswordForEmail( If you are on one of the Kotlin targets that have built-in support for redirect URL handling, such as Android, see [OAuth and OTP link verification](https://supabase.com/docs/reference/kotlin/initializing). + +<$Show if="sdk:python"> ```python @@ -615,6 +644,7 @@ client.auth.reset_password_email( ``` + #### Step 2: Create a change password page @@ -880,6 +910,7 @@ async function resetPassword() { ``` +<$Show if="sdk:swift"> ```swift @@ -887,6 +918,8 @@ try await supabase.auth.resetPasswordForEmail("valid.email@supabase.io") ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -896,6 +929,8 @@ supabase.gotrue.sendRecoveryEmail( ``` + +<$Show if="sdk:python"> ```python @@ -903,6 +938,7 @@ supabase.auth.reset_password_email('valid.email@supabase.io') ``` + Once you have a session, collect the user's new password and call `updateUser` to update their password. @@ -929,6 +965,7 @@ await supabase.auth.updateUser({ password: 'new_password' }) ``` +<$Show if="sdk:swift"> ```swift @@ -936,6 +973,8 @@ try await supabase.auth.updateUser(user: UserAttributes(password: newPassword)) ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -945,6 +984,8 @@ supabase.auth.updateUser { ``` + +<$Show if="sdk:python"> ```python @@ -952,6 +993,7 @@ supabase.auth.update_user({'password': 'new_password'}) ``` + ### Email sending @@ -1022,6 +1064,7 @@ const { data, error } = await supabase.auth.signUp({ ``` +<$Show if="sdk:swift"> ```swift @@ -1032,6 +1075,8 @@ try await supabase.auth.signUp( ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -1042,6 +1087,8 @@ supabase.auth.signUpWith(Phone) { ``` + +<$Show if="sdk:python"> ```python @@ -1052,6 +1099,7 @@ supabase.auth.sign_up({ ``` + ```bash @@ -1096,6 +1144,7 @@ const { ``` +<$Show if="sdk:swift"> You should present a form to the user so they can input the 6 digit pin, then send it along with the phone number to `verifyOTP`: @@ -1109,6 +1158,8 @@ try await supabase.auth.verifyOTP( ``` + +<$Show if="sdk:kotlin"> You should present a form to the user so they can input the 6 digit pin, then send it along with the phone number to `verifyPhoneOtp`: @@ -1122,6 +1173,8 @@ supabase.auth.verifyPhoneOtp( ``` + +<$Show if="sdk:python"> You should present a form to the user so they can input the 6 digit pin, then send it along with the phone number to `verify_otp`: @@ -1135,6 +1188,7 @@ supabase.auth.verify_otp({ ``` + ```bash @@ -1176,6 +1230,7 @@ const { data, error } = await supabase.auth.signInWithPassword({ ``` +<$Show if="sdk:swift"> ```swift @@ -1186,6 +1241,8 @@ try await supabase.auth.signIn( ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -1196,6 +1253,8 @@ supabase.auth.signInWith(Phone) { ``` + +<$Show if="sdk:python"> ```python @@ -1206,6 +1265,7 @@ supabase.auth.sign_in_with_password({ ``` + ```bash diff --git a/apps/docs/content/guides/auth/phone-login.mdx b/apps/docs/content/guides/auth/phone-login.mdx index 5f6279b5a13..fab354ef5a2 100644 --- a/apps/docs/content/guides/auth/phone-login.mdx +++ b/apps/docs/content/guides/auth/phone-login.mdx @@ -57,6 +57,7 @@ const { data, error } = await supabase.auth.signInWithOtp({ ``` +<$Show if="sdk:swift"> ```swift @@ -66,6 +67,8 @@ try await supabase.auth.signInWithOTP( ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -75,6 +78,8 @@ supabase.auth.signInWith(OTP) { ``` + +<$Show if="sdk:python"> ```python @@ -84,6 +89,7 @@ response = supabase.auth.sign_in_with_otp({ ``` + ```bash @@ -131,6 +137,7 @@ const { ``` +<$Show if="sdk:swift"> You should present a form to the user so they can input the 6 digit pin, then send it along with the phone number to `verifyOTP`: @@ -144,6 +151,8 @@ try await supabase.auth.verifyOTP( ``` + +<$Show if="sdk:kotlin"> You should present a form to the user so they can input the 6 digit pin, then send it along with the phone number to `verifyPhoneOtp`: @@ -157,6 +166,8 @@ supabase.auth.verifyPhoneOtp( ``` + +<$Show if="sdk:python"> You should present a form to the user so they can input the 6 digit pin, then send it along with the phone number to `verify_otp`: @@ -170,6 +181,7 @@ response = supabase.auth.verify_otp({ ``` + ```bash @@ -223,6 +235,7 @@ const { data, error } = await supabase.auth.updateUser({ ``` +<$Show if="sdk:swift"> ```swift @@ -234,6 +247,8 @@ try await supabase.auth.updateUser( ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -243,6 +258,8 @@ supabase.auth.updateUser { ``` + +<$Show if="sdk:python"> ```python @@ -252,6 +269,7 @@ response = supabase.auth.update_user({ ``` + The user receives an SMS with a 6-digit pin that you must [verify](#verifying-a-phone-otp) within 60 seconds. diff --git a/apps/docs/content/guides/auth/signout.mdx b/apps/docs/content/guides/auth/signout.mdx index 66cbd826f11..932c0bb9b71 100644 --- a/apps/docs/content/guides/auth/signout.mdx +++ b/apps/docs/content/guides/auth/signout.mdx @@ -30,6 +30,7 @@ async function signOut() { ``` +<$Show if="sdk:dart"> ```dart @@ -39,6 +40,8 @@ Future signOut() async { ``` + +<$Show if="sdk:swift"> ```swift @@ -46,6 +49,8 @@ try await supabase.auth.signOut() ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -55,6 +60,8 @@ suspend fun logout() { ``` + +<$Show if="sdk:python"> ```python @@ -62,6 +69,7 @@ supabase.auth.sign_out() ``` + ## Sign out and scopes @@ -99,6 +107,7 @@ await supabase.auth.signOut({ scope: 'local' }) ``` +<$Show if="sdk:dart"> ```dart @@ -110,6 +119,8 @@ await supabase.auth.signOut(scope: SignOutScope.global); ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -121,6 +132,7 @@ supabase.auth.signOut(SignOutScope.GLOBAL) ``` + Upon sign out, all refresh tokens and potentially other database objects related to the affected sessions are destroyed and the client library removes the session stored in the local storage medium. diff --git a/apps/docs/content/guides/auth/social-login/auth-apple.mdx b/apps/docs/content/guides/auth/social-login/auth-apple.mdx index 98d33c6856c..53c5927a333 100644 --- a/apps/docs/content/guides/auth/social-login/auth-apple.mdx +++ b/apps/docs/content/guides/auth/social-login/auth-apple.mdx @@ -320,6 +320,8 @@ curl -X PATCH "https://api.supabase.com/v1/projects/$PROJECT_REF/config/auth" \ +<$Show if="sdk:swift"> + ## Using native sign in with Apple in Swift @@ -378,6 +380,9 @@ curl -X PATCH "https://api.supabase.com/v1/projects/$PROJECT_REF/config/auth" \ + + +<$Show if="sdk:kotlin"> @@ -419,4 +424,5 @@ curl -X PATCH "https://api.supabase.com/v1/projects/$PROJECT_REF/config/auth" \ ``` + diff --git a/apps/docs/content/guides/auth/social-login/auth-azure.mdx b/apps/docs/content/guides/auth/social-login/auth-azure.mdx index c9176e917ee..9967206d72d 100644 --- a/apps/docs/content/guides/auth/social-login/auth-azure.mdx +++ b/apps/docs/content/guides/auth/social-login/auth-azure.mdx @@ -180,6 +180,7 @@ Future signInWithAzure() async { ``` +<$Show if="sdk:kotlin"> When your user signs in, call [signInWith(Provider)](/docs/reference/kotlin/auth-signinwithoauth) with `Azure` as the `Provider`: @@ -193,6 +194,7 @@ suspend fun signInWithAzure() { ``` + <$Partial path="oauth_pkce_flow.mdx" /> @@ -230,6 +232,7 @@ Future signOut() async { ``` +<$Show if="sdk:kotlin"> When your user signs out, call [signOut()](/docs/reference/kotlin/auth-signout) to remove them from the browser session and any objects from localStorage: @@ -241,6 +244,7 @@ suspend fun signOut() { ``` + ## Obtain the provider refresh token @@ -284,6 +288,7 @@ Future signInWithAzure() async { ``` +<$Show if="sdk:kotlin"> ```kotlin @@ -295,6 +300,7 @@ suspend fun signInWithAzure() { ``` + ## Resources diff --git a/apps/docs/content/guides/auth/social-login/auth-bitbucket.mdx b/apps/docs/content/guides/auth/social-login/auth-bitbucket.mdx index 24eb3432ac0..d41f1673825 100644 --- a/apps/docs/content/guides/auth/social-login/auth-bitbucket.mdx +++ b/apps/docs/content/guides/auth/social-login/auth-bitbucket.mdx @@ -91,6 +91,7 @@ Future signInWithBitbucket() async { ``` +<$Show if="sdk:kotlin"> When your user signs in, call [signInWith(Provider)](/docs/reference/kotlin/auth-signinwithoauth) with `Bitbucket` as the `Provider`: @@ -102,6 +103,7 @@ suspend fun signInWithBitbucket() { ``` + <$Partial path="oauth_pkce_flow.mdx" /> @@ -142,6 +144,7 @@ Future signOut() async { ``` +<$Show if="sdk:kotlin"> When your user signs out, call [signOut()](/docs/reference/kotlin/auth-signout) to remove them from the browser session and any objects from localStorage: @@ -153,6 +156,7 @@ suspend fun signOut() { ``` + ## Resources diff --git a/apps/docs/content/guides/auth/social-login/auth-discord.mdx b/apps/docs/content/guides/auth/social-login/auth-discord.mdx index 927854fdef2..6e6fdc7a702 100644 --- a/apps/docs/content/guides/auth/social-login/auth-discord.mdx +++ b/apps/docs/content/guides/auth/social-login/auth-discord.mdx @@ -108,6 +108,7 @@ Future signInWithDiscord() async { ``` +<$Show if="sdk:kotlin"> When your user signs in, call [signInWith(Provider)](/docs/reference/kotlin/auth-signinwithoauth) with `Discord` as the `Provider`: @@ -119,6 +120,7 @@ suspend fun signInWithDiscord() { ``` + <$Partial path="oauth_pkce_flow.mdx" /> @@ -161,6 +163,7 @@ Future signOut() async { ``` +<$Show if="sdk:kotlin"> When your user signs out, call [signOut()](/docs/reference/kotlin/auth-signout) to remove them from the browser session and any objects from localStorage: @@ -172,6 +175,7 @@ suspend fun signOut() { ``` + ## Resources diff --git a/apps/docs/content/guides/auth/social-login/auth-facebook.mdx b/apps/docs/content/guides/auth/social-login/auth-facebook.mdx index 5412fefbec7..5ae0b7271b1 100644 --- a/apps/docs/content/guides/auth/social-login/auth-facebook.mdx +++ b/apps/docs/content/guides/auth/social-login/auth-facebook.mdx @@ -171,6 +171,7 @@ Make sure to configure your Facebook app properly and add the required permissio +<$Show if="sdk:swift"> When your user signs in, call [`signInWithOAuth()`](/docs/reference/swift/auth-signinwithoauth) with `facebook` as the `provider`: @@ -191,6 +192,8 @@ func signInWithFacebook() async throws { ``` + +<$Show if="sdk:kotlin"> When your user signs in, call [signInWith(Provider)](/docs/reference/kotlin/auth-signinwithoauth) with `Facebook` as the `Provider`: @@ -202,6 +205,7 @@ suspend fun signInWithFacebook() { ``` + <$Partial path="oauth_pkce_flow.mdx" /> @@ -239,6 +243,7 @@ Future signOut() async { ``` +<$Show if="sdk:swift"> When your user signs out, call [signOut()](/docs/reference/swift/auth-signout) to remove them from the browser session and any objects from localStorage: @@ -250,6 +255,8 @@ func signOut() async throws { ``` + +<$Show if="sdk:kotlin"> When your user signs out, call [signOut()](/docs/reference/kotlin/auth-signout) to remove them from the browser session and any objects from localStorage: @@ -261,6 +268,7 @@ suspend fun signOut() { ``` + Now, you should be able to login with Facebook and alert you to `Submit for Login Review` when users try to sign into your app. Follow the instructions there to make your app go live for full features and products. diff --git a/apps/docs/content/guides/auth/social-login/auth-figma.mdx b/apps/docs/content/guides/auth/social-login/auth-figma.mdx index 6fd11bdc0c9..f934234e9c8 100644 --- a/apps/docs/content/guides/auth/social-login/auth-figma.mdx +++ b/apps/docs/content/guides/auth/social-login/auth-figma.mdx @@ -88,6 +88,7 @@ Future signInWithFigma() async { ``` +<$Show if="sdk:kotlin"> When your user signs in, call [signInWith(Provider)](/docs/reference/kotlin/auth-signinwithoauth) with `Figma` as the `Provider`: @@ -99,6 +100,7 @@ suspend fun signInWithFigma() { ``` + <$Partial path="oauth_pkce_flow.mdx" /> @@ -136,6 +138,7 @@ Future signOut() async { ``` +<$Show if="sdk:kotlin"> When your user signs out, call [signOut()](/docs/reference/kotlin/auth-signout) to remove them from the browser session and any objects from localStorage: @@ -147,6 +150,7 @@ suspend fun signOut() { ``` + ## Resources diff --git a/apps/docs/content/guides/auth/social-login/auth-github.mdx b/apps/docs/content/guides/auth/social-login/auth-github.mdx index a23a426d2f0..2fdb1896505 100644 --- a/apps/docs/content/guides/auth/social-login/auth-github.mdx +++ b/apps/docs/content/guides/auth/social-login/auth-github.mdx @@ -103,6 +103,7 @@ Future signInWithGithub() async { ``` +<$Show if="sdk:swift"> When your user signs in, call [`signInWithOAuth`](/docs/reference/swift/auth-signinwithoauth) with `.github` as the `Provider`: @@ -117,6 +118,8 @@ func signInWithGithub() async throws { ``` + +<$Show if="sdk:kotlin"> When your user signs in, call [signInWith(Provider)](/docs/reference/kotlin/auth-signinwithoauth) with [`Github`](https://github.com/supabase-community/supabase-kt/blob/master/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/providers/Providers.kt#L16-L20) as the `Provider`: @@ -128,6 +131,7 @@ suspend fun signInWithGithub() { ``` + <$Partial path="oauth_pkce_flow.mdx" /> @@ -168,6 +172,7 @@ Future signOut() async { ``` +<$Show if="sdk:kotlin"> When your user signs out, call [signOut()](/docs/reference/kotlin/auth-signout) to remove them from the browser session and any objects from localStorage: @@ -179,6 +184,7 @@ suspend fun signOut() { ``` + ## Resources diff --git a/apps/docs/content/guides/auth/social-login/auth-gitlab.mdx b/apps/docs/content/guides/auth/social-login/auth-gitlab.mdx index 56df4e93f72..91c74107c49 100644 --- a/apps/docs/content/guides/auth/social-login/auth-gitlab.mdx +++ b/apps/docs/content/guides/auth/social-login/auth-gitlab.mdx @@ -88,6 +88,7 @@ Future signInWithGitLab() async { ``` +<$Show if="sdk:kotlin"> When your user signs in, call [signInWith(Provider)](/docs/reference/kotlin/auth-signinwithoauth) with `Gitlab` as the `Provider`: @@ -99,6 +100,7 @@ suspend fun signInWithGitLab() { ``` + <$Partial path="oauth_pkce_flow.mdx" /> @@ -139,6 +141,7 @@ Future signOut() async { ``` +<$Show if="sdk:kotlin"> When your user signs out, call [signOut()](/docs/reference/kotlin/auth-signout) to remove them from the browser session and any objects from localStorage: @@ -150,6 +153,7 @@ suspend fun signOut() { ``` + ## Resources diff --git a/apps/docs/content/guides/auth/social-login/auth-google.mdx b/apps/docs/content/guides/auth/social-login/auth-google.mdx index be691bad7f7..42be9b5d23c 100644 --- a/apps/docs/content/guides/auth/social-login/auth-google.mdx +++ b/apps/docs/content/guides/auth/social-login/auth-google.mdx @@ -145,6 +145,8 @@ To use Google's pre-built signin buttons: +<$Show if="sdk:swift"> + Google sign-in with Supabase is done through the [`GoogleSignIn-iOS`](https://github.com/google/GoogleSignIn-iOS) package. @@ -189,6 +191,7 @@ To use Google's pre-built signin buttons: 3. Add web client ID and iOS client ID from step 1 in the [Google provider on the Supabase Dashboard](https://supabase.com/dashboard/project/_/auth/providers), under _Client IDs_, separated by a comma. Enable the `Skip nonce check` option. + diff --git a/apps/docs/content/guides/auth/social-login/auth-kakao.mdx b/apps/docs/content/guides/auth/social-login/auth-kakao.mdx index 950c50f67bb..97da18051a7 100644 --- a/apps/docs/content/guides/auth/social-login/auth-kakao.mdx +++ b/apps/docs/content/guides/auth/social-login/auth-kakao.mdx @@ -112,6 +112,7 @@ Future signInWithKakao() async { ``` +<$Show if="sdk:kotlin"> When your user signs in, call [signInWith(Provider)](/docs/reference/kotlin/auth-signinwithoauth) with `Kakao` as the `Provider`: @@ -123,6 +124,7 @@ suspend fun signInWithKakao() { ``` + <$Partial path="oauth_pkce_flow.mdx" /> @@ -156,6 +158,7 @@ Future signOut() async { ``` +<$Show if="sdk:kotlin"> When your user signs out, call [signOut()](/docs/reference/kotlin/auth-signout) to remove them from the browser session and any objects from localStorage: @@ -167,6 +170,7 @@ suspend fun signOut() { ``` + ## Using Kakao Login JS SDK diff --git a/apps/docs/content/guides/auth/social-login/auth-keycloak.mdx b/apps/docs/content/guides/auth/social-login/auth-keycloak.mdx index 73ec7c240af..df911c2794f 100644 --- a/apps/docs/content/guides/auth/social-login/auth-keycloak.mdx +++ b/apps/docs/content/guides/auth/social-login/auth-keycloak.mdx @@ -106,6 +106,7 @@ Future signInWithKeycloak() async { ``` +<$Show if="sdk:kotlin"> When your user signs in, call [signInWith(Provider)](/docs/reference/kotlin/auth-signinwithoauth) with `Keycloak` as the `Provider`: @@ -119,6 +120,7 @@ suspend fun signInWithKeycloak() { ``` + <$Partial path="oauth_pkce_flow.mdx" /> @@ -152,6 +154,7 @@ Future signOut() async { ``` +<$Show if="sdk:kotlin"> When your user signs out, call [signOut()](/docs/reference/kotlin/auth-signout) to remove them from the browser session and any objects from localStorage: @@ -167,6 +170,7 @@ suspend fun signOut() { ``` + ## Resources diff --git a/apps/docs/content/guides/auth/social-login/auth-linkedin.mdx b/apps/docs/content/guides/auth/social-login/auth-linkedin.mdx index 8a04410f563..beee52a0ba9 100644 --- a/apps/docs/content/guides/auth/social-login/auth-linkedin.mdx +++ b/apps/docs/content/guides/auth/social-login/auth-linkedin.mdx @@ -108,6 +108,7 @@ Future signInWithLinkedIn() async { ``` +<$Show if="sdk:kotlin"> When your user signs in, call [signInWith(Provider)](/docs/reference/kotlin/auth-signinwithoauth) with `LinkedIn` as the `Provider`: @@ -119,6 +120,7 @@ suspend fun signInWithKaLinkedIn() { ``` + <$Partial path="oauth_pkce_flow.mdx" /> @@ -156,6 +158,7 @@ Future signOut() async { ``` +<$Show if="sdk:kotlin"> When your user signs out, call [signOut()](/docs/reference/kotlin/auth-signout) to remove them from the browser session and any objects from localStorage: @@ -167,6 +170,7 @@ suspend fun signOut() { ``` + ## LinkedIn Open ID Connect (OIDC) diff --git a/apps/docs/content/guides/auth/social-login/auth-notion.mdx b/apps/docs/content/guides/auth/social-login/auth-notion.mdx index 4b3b115b9dc..6a4db97cbe7 100644 --- a/apps/docs/content/guides/auth/social-login/auth-notion.mdx +++ b/apps/docs/content/guides/auth/social-login/auth-notion.mdx @@ -88,6 +88,7 @@ Future signInWithNotion() async { ``` +<$Show if="sdk:kotlin"> When your user signs in, call [signInWith(Provider)](/docs/reference/kotlin/auth-signinwithoauth) with `Notion` as the `Provider`: @@ -99,6 +100,7 @@ suspend fun signInWithNotion() { ``` + <$Partial path="oauth_pkce_flow.mdx" /> @@ -136,6 +138,7 @@ Future signOut() async { ``` +<$Show if="sdk:kotlin"> When your user signs out, call [signOut()](/docs/reference/kotlin/auth-signout) to remove them from the browser session and any objects from localStorage: @@ -147,6 +150,7 @@ suspend fun signOut() { ``` + ## Resources diff --git a/apps/docs/content/guides/auth/social-login/auth-slack.mdx b/apps/docs/content/guides/auth/social-login/auth-slack.mdx index e1715289ba5..172eed7207d 100644 --- a/apps/docs/content/guides/auth/social-login/auth-slack.mdx +++ b/apps/docs/content/guides/auth/social-login/auth-slack.mdx @@ -124,6 +124,7 @@ Future signInWithSlack() async { ``` +<$Show if="sdk:kotlin"> When your user signs in, call [signInWith(Provider)](/docs/reference/kotlin/auth-signinwithoauth) with `SlackOIDC` as the `Provider`: @@ -135,6 +136,7 @@ suspend fun signInWithSlack() { ``` + <$Partial path="oauth_pkce_flow.mdx" /> @@ -172,6 +174,7 @@ Future signOut() async { ``` +<$Show if="sdk:kotlin"> When your user signs out, call [signOut()](/docs/reference/kotlin/auth-signout) to remove them from the browser session and any objects from localStorage: @@ -183,6 +186,7 @@ suspend fun signOut() { ``` + ## Resources diff --git a/apps/docs/content/guides/auth/social-login/auth-spotify.mdx b/apps/docs/content/guides/auth/social-login/auth-spotify.mdx index 6c74d83480c..57cd5f5f6a7 100644 --- a/apps/docs/content/guides/auth/social-login/auth-spotify.mdx +++ b/apps/docs/content/guides/auth/social-login/auth-spotify.mdx @@ -118,6 +118,7 @@ Future signInWithSpotify() async { ``` +<$Show if="sdk:kotlin"> When your user signs in, call [signInWith(Provider)](/docs/reference/kotlin/auth-signinwithoauth) with `Spotify` as the `Provider`: @@ -129,6 +130,7 @@ suspend fun signInWithSpotify() { ``` + <$Partial path="oauth_pkce_flow.mdx" /> @@ -166,6 +168,7 @@ Future signOut() async { ``` +<$Show if="sdk:kotlin"> When your user signs out, call [signOut()](/docs/reference/kotlin/auth-signout) to remove them from the browser session and any objects from localStorage: @@ -177,6 +180,7 @@ suspend fun signOut() { ``` + ## Resources diff --git a/apps/docs/content/guides/auth/social-login/auth-twitch.mdx b/apps/docs/content/guides/auth/social-login/auth-twitch.mdx index ad356b654af..3a4a2b8e978 100644 --- a/apps/docs/content/guides/auth/social-login/auth-twitch.mdx +++ b/apps/docs/content/guides/auth/social-login/auth-twitch.mdx @@ -103,6 +103,7 @@ Future signInWithTwitch() async { ``` +<$Show if="sdk:kotlin"> When your user signs in, call [signInWith(Provider)](/docs/reference/kotlin/auth-signinwithoauth) with `Twitch` as the `Provider`: @@ -114,6 +115,7 @@ suspend fun signInWithTwitch() { ``` + <$Partial path="oauth_pkce_flow.mdx" /> @@ -151,6 +153,7 @@ Future signOut() async { ``` +<$Show if="sdk:kotlin"> When your user signs out, call [signOut()](/docs/reference/kotlin/auth-signout) to remove them from the browser session and any objects from localStorage: @@ -162,6 +165,7 @@ suspend fun signOut() { ``` + ## Resources diff --git a/apps/docs/content/guides/auth/social-login/auth-twitter.mdx b/apps/docs/content/guides/auth/social-login/auth-twitter.mdx index b78996ceba3..6308bcc40d5 100644 --- a/apps/docs/content/guides/auth/social-login/auth-twitter.mdx +++ b/apps/docs/content/guides/auth/social-login/auth-twitter.mdx @@ -115,6 +115,7 @@ Future signInWithTwitter() async { ``` +<$Show if="sdk:kotlin"> When your user signs in, call [signInWith(Provider)](/docs/reference/kotlin/auth-signinwithoauth) with `Twitter` as the `Provider`: @@ -126,6 +127,7 @@ suspend fun signInWithTwitter() { ``` + <$Partial path="oauth_pkce_flow.mdx" /> @@ -166,6 +168,7 @@ Future signOut() async { ``` +<$Show if="sdk:kotlin"> When your user signs out, call [signOut()](/docs/reference/kotlin/auth-signout) to remove them from the browser session and any objects from localStorage: @@ -177,6 +180,7 @@ suspend fun signOut() { ``` + ## Resources diff --git a/apps/docs/content/guides/auth/social-login/auth-zoom.mdx b/apps/docs/content/guides/auth/social-login/auth-zoom.mdx index 71bbe5d6f96..b1714a6f58d 100644 --- a/apps/docs/content/guides/auth/social-login/auth-zoom.mdx +++ b/apps/docs/content/guides/auth/social-login/auth-zoom.mdx @@ -119,6 +119,7 @@ Future signInWithZoom() async { ``` +<$Show if="sdk:kotlin"> When your user signs in, call [signInWith(Provider)](/docs/reference/kotlin/auth-signinwithoauth) with `Zoom` as the `Provider`: @@ -130,6 +131,7 @@ suspend fun signInWithZoom() { ``` + <$Partial path="oauth_pkce_flow.mdx" /> @@ -167,6 +169,7 @@ Future signOut() async { ``` +<$Show if="sdk:kotlin"> When your user signs out, call [signOut()](/docs/reference/kotlin/auth-signout) to remove them from the browser session and any objects from localStorage: @@ -178,6 +181,7 @@ suspend fun signOut() { ``` + ## Resources diff --git a/apps/docs/content/guides/auth/third-party/auth0.mdx b/apps/docs/content/guides/auth/third-party/auth0.mdx index 76cf04be643..634378b437a 100644 --- a/apps/docs/content/guides/auth/third-party/auth0.mdx +++ b/apps/docs/content/guides/auth/third-party/auth0.mdx @@ -49,6 +49,8 @@ const supabase = createClient( +<$Show if="sdk:swift"> + ```swift @@ -73,6 +75,9 @@ let supabase = SupabaseClient( ``` + + +<$Show if="sdk:dart"> @@ -96,6 +101,9 @@ Future main() async { ``` + + +<$Show if="sdk:kotlin"> @@ -114,6 +122,7 @@ val supabase = createSupabaseClient( ``` + diff --git a/apps/docs/content/guides/auth/third-party/aws-cognito.mdx b/apps/docs/content/guides/auth/third-party/aws-cognito.mdx index d2ced1133a8..72e99ecf8a8 100644 --- a/apps/docs/content/guides/auth/third-party/aws-cognito.mdx +++ b/apps/docs/content/guides/auth/third-party/aws-cognito.mdx @@ -43,6 +43,8 @@ Hub.listen('auth', () => { +<$Show if="sdk:swift"> + ```swift @@ -72,6 +74,9 @@ let supabase = SupabaseClient( ``` + + +<$Show if="sdk:dart"> @@ -96,6 +101,9 @@ Future main() async { ``` + + +<$Show if="sdk:kotlin"> @@ -128,6 +136,7 @@ suspend fun getAccessToken(): String? { ``` + diff --git a/apps/docs/content/guides/auth/third-party/clerk.mdx b/apps/docs/content/guides/auth/third-party/clerk.mdx index f860db26d15..f7a809a7765 100644 --- a/apps/docs/content/guides/auth/third-party/clerk.mdx +++ b/apps/docs/content/guides/auth/third-party/clerk.mdx @@ -45,6 +45,8 @@ hideElidedLines={true} +<$Show if="sdk:dart"> + ```dart @@ -63,6 +65,9 @@ await Supabase.initialize( ``` + + +<$Show if="sdk:swift"> @@ -84,6 +89,7 @@ let supabase = SupabaseClient( ``` + diff --git a/apps/docs/content/guides/auth/third-party/firebase-auth.mdx b/apps/docs/content/guides/auth/third-party/firebase-auth.mdx index 438141cd5a0..a9a2b9246c0 100644 --- a/apps/docs/content/guides/auth/third-party/firebase-auth.mdx +++ b/apps/docs/content/guides/auth/third-party/firebase-auth.mdx @@ -40,6 +40,8 @@ Make sure the all users in your application have the `role: 'authenticated'` [cu +<$Show if="sdk:dart"> + Creating a client for the Web is as easy as passing the `accessToken` async function. This function should [return the Firebase Auth JWT of the current user](https://firebase.google.com/docs/auth/admin/verify-id-tokens) (or null if no such user) is found. @@ -59,6 +61,9 @@ await Supabase.initialize( Make sure the all users in your application have the `role: 'authenticated'` [custom claim](https://firebase.google.com/docs/auth/admin/custom-claims) set. If you're using the `onCreate` Cloud Function to add this custom claim to newly signed up users, you will need to call `getIdToken(/* forceRefresh */ true)` immediately after sign up as the `onCreate` function does not run synchronously. + + +<$Show if="sdk:swift"> @@ -86,6 +91,9 @@ let supabase = SupabaseClient( ``` + + +<$Show if="sdk:kotlin"> @@ -106,6 +114,9 @@ val supabase = createSupabaseClient( ``` + + +<$Show if="sdk:kotlin"> @@ -126,6 +137,7 @@ val supabase = createSupabaseClient( ``` + diff --git a/apps/docs/content/guides/auth/third-party/overview.mdx b/apps/docs/content/guides/auth/third-party/overview.mdx index 77d950275da..e110ec0e003 100644 --- a/apps/docs/content/guides/auth/third-party/overview.mdx +++ b/apps/docs/content/guides/auth/third-party/overview.mdx @@ -30,8 +30,6 @@ There are some limitations you should be aware of when using third-party authent 2. The JWT signing keys from the third-party provider are stored in the configuration of your project, and are checked for changes periodically. If you are rotating your keys (when supported) allow up to 30 minutes for the change to be picked up. 3. It is not possible to disable Supabase Auth at this time. -## Pricing - +<$Show if="billing:all"> <$Partial path="billing/pricing/pricing_mau_third_party.mdx" /> - -For a detailed breakdown of how charges are calculated, refer to [Manage Monthly Active Third-Party Users usage](/docs/guides/platform/manage-your-usage/monthly-active-users-third-party). + diff --git a/apps/docs/content/guides/database/arrays.mdx b/apps/docs/content/guides/database/arrays.mdx index 0faad66a9dd..4d4aa9dfbec 100644 --- a/apps/docs/content/guides/database/arrays.mdx +++ b/apps/docs/content/guides/database/arrays.mdx @@ -73,6 +73,7 @@ const { data, error } = await supabase ``` +<$Show if="sdk:swift"> Insert a record from the Swift client: @@ -97,6 +98,8 @@ try await supabase ``` + +<$Show if="sdk:python"> Insert a record from the Python client: @@ -114,6 +117,7 @@ supabase.from_('arraytest').insert( ``` + ## View the results @@ -204,6 +208,7 @@ returns: ``` +<$Show if="sdk:swift"> This returns the entire array field: @@ -228,6 +233,7 @@ returns: ``` + ## Resources diff --git a/apps/docs/content/guides/database/extensions/plv8.mdx b/apps/docs/content/guides/database/extensions/plv8.mdx index eb497655507..89e1a8a51b9 100644 --- a/apps/docs/content/guides/database/extensions/plv8.mdx +++ b/apps/docs/content/guides/database/extensions/plv8.mdx @@ -90,6 +90,7 @@ const { data, error } = supabase.rpc('function_name') ``` +<$Show if="sdk:kotlin"> ```kotlin @@ -97,6 +98,7 @@ val data = supabase.postgrest.rpc("function_name") ``` + ## Examples diff --git a/apps/docs/content/guides/database/extensions/postgis.mdx b/apps/docs/content/guides/database/extensions/postgis.mdx index c338c1ac912..fbdde09bdb5 100644 --- a/apps/docs/content/guides/database/extensions/postgis.mdx +++ b/apps/docs/content/guides/database/extensions/postgis.mdx @@ -128,6 +128,7 @@ const { error } = await supabase.from('restaurants').insert([ ``` +<$Show if="sdk:dart"> ```dart @@ -148,6 +149,8 @@ await supabase.from('restaurants').insert([ ``` + +<$Show if="sdk:swift"> ```swift @@ -168,6 +171,8 @@ try await supabase.from("restaurants") ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -187,6 +192,7 @@ val data = supabase.from("restaurants").insert(listOf( ``` + Notice the order in which you pass the latitude and longitude. Longitude comes first, and is because longitude represents the x-axis of the location. Another thing to watch for is when inserting data from the client library, there is no comma between the two values, just a single space. @@ -241,6 +247,7 @@ const { data, error } = await supabase.rpc('nearby_restaurants', { ``` +<$Show if="sdk:dart"> ```dart @@ -251,6 +258,8 @@ final data = await supabase.rpc('nearby_restaurants',params: { ``` + +<$Show if="sdk:swift"> ```swift @@ -279,6 +288,8 @@ let response: Response = try await supabase.rpc( ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -292,6 +303,7 @@ val data = supabase.postgrest.rpc( ``` + ```json @@ -367,6 +379,7 @@ const { data, error } = await supabase.rpc('restaurants_in_view', { ``` +<$Show if="sdk:dart"> ```dart @@ -379,6 +392,8 @@ final data = await supabase.rpc('restaurants_in_view', params: { ``` + +<$Show if="sdk:swift"> ```swift @@ -403,6 +418,8 @@ let response: Response = try await supabase.rpc( ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -418,6 +435,7 @@ val data = supabase.postgrest.rpc( ``` + ```json diff --git a/apps/docs/content/guides/database/full-text-search.mdx b/apps/docs/content/guides/database/full-text-search.mdx index e2f21d5a484..27a08579f17 100644 --- a/apps/docs/content/guides/database/full-text-search.mdx +++ b/apps/docs/content/guides/database/full-text-search.mdx @@ -129,6 +129,7 @@ const { data, error } = await supabase.from('books').select().eq('title', 'Harry ``` +<$Show if="sdk:dart"> ```dart @@ -139,6 +140,8 @@ final result = await client ``` + +<$Show if="sdk:swift"> ```swift @@ -149,6 +152,8 @@ let response = try await supabase.from("books") ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -160,6 +165,8 @@ val data = supabase.from("books").select { ``` + +<$Show if="sdk:python"> ```python @@ -167,6 +174,7 @@ data = supabase.from_('books').select().eq('title', 'Harry').execute() ``` + The equality symbol above (`=`) is very "strict" on what it matches. In a full text search context, we might want to find all "Harry Potter" books and so we can rewrite the @@ -195,6 +203,7 @@ const { data, error } = await supabase.from('books').select().textSearch('title' ``` +<$Show if="sdk:dart"> ```dart @@ -205,6 +214,8 @@ final result = await client ``` + +<$Show if="sdk:swift"> ```swift @@ -214,6 +225,8 @@ let response = try await supabase.from("books") ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -225,6 +238,7 @@ val data = supabase.from("books").select { ``` + ## Basic full text queries @@ -260,6 +274,7 @@ const { data, error } = await supabase.from('books').select().textSearch('descri ``` +<$Show if="sdk:dart"> ```dart @@ -270,6 +285,8 @@ final result = await client ``` + +<$Show if="sdk:swift"> ```swift @@ -280,6 +297,8 @@ let response = await client.from("books") ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -291,6 +310,8 @@ val data = supabase.from("books").select { ``` + +<$Show if="sdk:python"> ```python @@ -298,6 +319,7 @@ data = supabase.from_('books').select().text_search('description', "'big'").exec ``` + @@ -355,6 +377,7 @@ const { data, error } = await supabase ``` +<$Show if="sdk:dart"> ```sql @@ -371,6 +394,8 @@ final result = await client ``` + +<$Show if="sdk:swift"> ```sql @@ -388,6 +413,8 @@ let response = try await client ``` + +<$Show if="sdk:kotlin"> ```sql @@ -405,6 +432,8 @@ val data = supabase.from("books").select { ``` + +<$Show if="sdk:python"> ```sql @@ -418,6 +447,7 @@ data = supabase.from_('books').select().text_search('title_description', "little ``` + {/* supa-mdx-lint-disable Rule003Spelling */} @@ -466,6 +496,7 @@ const { data, error } = await supabase ``` +<$Show if="sdk:dart"> ```dart @@ -476,6 +507,8 @@ final result = await client ``` + +<$Show if="sdk:swift"> ```swift @@ -487,6 +520,8 @@ let response = try await client ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -498,6 +533,8 @@ val data = supabase.from("books").select { ``` + +<$Show if="sdk:python"> ```python @@ -505,6 +542,7 @@ data = supabase.from_('books').select().text_search('description', "'little' & ' ``` + {/* supa-mdx-lint-disable Rule003Spelling */} @@ -552,6 +590,7 @@ const { data, error } = await supabase ``` +<$Show if="sdk:dart"> ```dart @@ -562,6 +601,8 @@ final result = await client ``` + +<$Show if="sdk:swift"> ```swift @@ -573,6 +614,8 @@ let response = try await client ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -584,6 +627,8 @@ val data = supabase.from("books").select { ``` + +<$Show if="sdk:python"> ```python @@ -591,6 +636,7 @@ response = client.from_('books').select().text_search('description', "'little' | ``` + {/* supa-mdx-lint-disable Rule003Spelling */} @@ -652,6 +698,7 @@ const { data, error } = await supabase.rpc('search_books_by_title_prefix', { pre ``` +<$Show if="sdk:dart"> ```dart @@ -659,6 +706,8 @@ final data = await supabase.rpc('search_books_by_title_prefix', params: { 'prefi ``` + +<$Show if="sdk:swift"> ```swift @@ -670,6 +719,8 @@ let response = try await supabase.rpc( ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -678,6 +729,8 @@ val result = supabase.postgrest.rpc("search_books_by_title_prefix", rpcParams) ``` + +<$Show if="sdk:python"> ```python @@ -685,6 +738,7 @@ data = client.rpc('search_books_by_title_prefix', { 'prefix': 'Lit' }).execute() ``` + This function takes a prefix parameter and returns all books where the title contains a word starting with that prefix. The `:*` operator is used to denote a prefix match in the `to_tsquery()` function. @@ -777,6 +831,7 @@ const { data, error } = await supabase.from('books').select().textSearch('fts', ``` +<$Show if="sdk:dart"> ```dart @@ -787,6 +842,8 @@ final result = await client ``` + +<$Show if="sdk:swift"> ```swift @@ -798,6 +855,8 @@ let response = try await client ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -809,6 +868,8 @@ val data = supabase.from("books").select { ``` + +<$Show if="sdk:python"> ```python @@ -816,6 +877,7 @@ data = client.from_('books').select().text_search('fts', "'little' & 'big'").exe ``` + {/* supa-mdx-lint-disable Rule003Spelling */} @@ -868,6 +930,7 @@ const { data, error } = await supabase ``` +<$Show if="sdk:dart"> ```dart @@ -878,6 +941,8 @@ final result = await client ``` + +<$Show if="sdk:swift"> ```swift @@ -889,6 +954,8 @@ let response = try await client ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -900,6 +967,8 @@ val data = supabase.from("books").select { ``` + +<$Show if="sdk:python"> ```python @@ -907,6 +976,7 @@ data = client.from_('books').select().text_search('description', "'big' <-> 'dre ``` + We can also use the `<->` to find words within a certain distance of each other. For example to find `year` and `school` within 2 words of each other: @@ -940,6 +1010,7 @@ const { data, error } = await supabase ``` +<$Show if="sdk:dart"> ```dart @@ -950,6 +1021,8 @@ final result = await client ``` + +<$Show if="sdk:swift"> ```swift @@ -961,6 +1034,8 @@ let response = try await supabase ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -972,6 +1047,8 @@ val data = supabase.from("books").select { ``` + +<$Show if="sdk:python"> ```python @@ -979,6 +1056,7 @@ data = client.from_('books').select().text_search('description', "'year' <2> 'sc ``` + ### Negation: `!` [#negation] @@ -1015,6 +1093,7 @@ const { data, error } = await supabase ``` +<$Show if="sdk:dart"> ```dart @@ -1025,6 +1104,8 @@ final result = await client ``` + +<$Show if="sdk:swift"> ```swift @@ -1036,6 +1117,8 @@ let response = try await client ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -1047,6 +1130,8 @@ val data = supabase.from("books").select { ``` + +<$Show if="sdk:python"> ```python @@ -1054,6 +1139,7 @@ data = client.from_('books').select().text_search('description', "'big' & !'litt ``` + ## Resources diff --git a/apps/docs/content/guides/database/functions.mdx b/apps/docs/content/guides/database/functions.mdx index 8186c100eb4..3a6cac523b4 100644 --- a/apps/docs/content/guides/database/functions.mdx +++ b/apps/docs/content/guides/database/functions.mdx @@ -91,6 +91,7 @@ const { data, error } = await supabase.rpc('hello_world') Reference: [`rpc()`](../../reference/javascript/rpc) +<$Show if="sdk:dart"> ```dart @@ -101,6 +102,8 @@ final data = await supabase Reference: [`rpc()`](../../reference/dart/rpc) + +<$Show if="sdk:swift"> ```swift @@ -110,6 +113,8 @@ try await supabase.rpc("hello_world").execute() Reference: [`rpc()`](../../reference/swift/rpc) + +<$Show if="sdk:kotlin"> ```kotlin @@ -119,6 +124,8 @@ val data = supabase.postgrest.rpc("hello_world") Reference: [`rpc()`](../../reference/kotlin/rpc) + +<$Show if="sdk:python"> ```python @@ -128,6 +135,7 @@ data = supabase.rpc('hello_world').execute() Reference: [`rpc()`](../../reference/python/rpc) + ## Returning data sets @@ -236,6 +244,7 @@ const { data, error } = supabase.rpc('get_planets').eq('id', 1) ``` +<$Show if="sdk:dart"> ```dart @@ -245,6 +254,8 @@ final data = await supabase ``` + +<$Show if="sdk:swift"> ```swift @@ -252,6 +263,8 @@ let response = try await supabase.rpc("get_planets").eq("id", value: 1).execute( ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -263,6 +276,8 @@ val data = supabase.postgrest.rpc("get_planets") { ``` + +<$Show if="sdk:python"> ```python @@ -270,6 +285,7 @@ data = supabase.rpc('get_planets').eq('id', 1).execute() ``` + ## Passing parameters @@ -316,6 +332,7 @@ const { data, error } = await supabase.rpc('add_planet', { name: 'Jakku' }) ``` +<$Show if="sdk:dart"> ```dart @@ -324,6 +341,8 @@ final data = await supabase ``` + +<$Show if="sdk:swift"> Using `Encodable` type: @@ -351,6 +370,8 @@ try await supabase.rpc( ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -363,6 +384,8 @@ val data = supabase.postgrest.rpc( ``` + +<$Show if="sdk:python"> ```python @@ -370,6 +393,7 @@ data = supabase.rpc('add_planet', params={'name': 'Jakku'}).execute() ``` + ## Suggestions diff --git a/apps/docs/content/guides/database/joins-and-nesting.mdx b/apps/docs/content/guides/database/joins-and-nesting.mdx index 8921f0e568d..92f2a4a960f 100644 --- a/apps/docs/content/guides/database/joins-and-nesting.mdx +++ b/apps/docs/content/guides/database/joins-and-nesting.mdx @@ -129,6 +129,7 @@ const sectionsWithInstruments: SectionsWithInstruments = data ``` +<$Show if="sdk:dart"> ```dart @@ -136,6 +137,8 @@ final data = await supabase.from('orchestral_sections').select('id, name, instru ``` + +<$Show if="sdk:swift"> ```swift @@ -158,6 +161,8 @@ let orchestralSections: [OrchestralSection] = try await supabase ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -165,6 +170,8 @@ val data = supabase.from("orchestral_sections").select(Columns.raw("id, name, in ``` + +<$Show if="sdk:python"> ```python @@ -172,6 +179,7 @@ data = supabase.from_('orchestral_sections').select('id, name, instruments(id, n ``` + ```javascript @@ -245,6 +253,7 @@ const { data, error } = await supabase.from('teams').select(` ``` +<$Show if="sdk:dart"> ```dart @@ -252,6 +261,8 @@ final data = await supabase.from('teams').select('id, team_name, users(id, name) ``` + +<$Show if="sdk:swift"> ```swift @@ -284,6 +295,8 @@ let teams [Team] = try await supabase ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -291,6 +304,8 @@ val data = supabase.from("teams").select(Columns.raw("id, team_name, users(id, n ``` + +<$Show if="sdk:python"> ```python @@ -298,6 +313,7 @@ data = supabase.from_('teams').select('id, team_name, users(id, name)').execute( ``` + ````javascript @@ -396,6 +412,7 @@ const { data, error } = await supabase.from('shifts').select( ``` +<$Show if="sdk:dart"> ```dart @@ -415,6 +432,8 @@ end_scan:scans!scan_id_end ( ``` + +<$Show if="sdk:swift"> ```swift @@ -466,6 +485,9 @@ let shifts: [Shift] = try await supabase ``` + + +<$Show if="sdk:kotlin"> @@ -486,6 +508,8 @@ end_scan:scans!scan_id_end ( ``` + +<$Show if="sdk:python"> ```python @@ -505,6 +529,7 @@ data = supabase.from_('shifts').select(""" ``` + ```javascript diff --git a/apps/docs/content/guides/database/json.mdx b/apps/docs/content/guides/database/json.mdx index 1619c015cb6..e062dd8fa01 100644 --- a/apps/docs/content/guides/database/json.mdx +++ b/apps/docs/content/guides/database/json.mdx @@ -183,6 +183,7 @@ const { data, error } = await supabase.from('books').insert([ ``` +<$Show if="sdk:dart"> ```dart @@ -237,6 +238,8 @@ await supabase.from('books').insert([ ``` + +<$Show if="sdk:swift"> Supabase Swift provides a convenience `AnyJSON` type. @@ -301,6 +304,8 @@ try await supabase.from("books") ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -330,6 +335,8 @@ val data = supabase.from("books").insert(listOf( ``` + +<$Show if="sdk:python"> ```python @@ -384,6 +391,7 @@ supabase.from_('books').insert([ ``` + ## Query JSON data @@ -425,6 +433,7 @@ const { data, error } = await supabase.from('books').select(` ``` +<$Show if="sdk:swift"> ```swift @@ -443,6 +452,8 @@ try await supabase ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -456,6 +467,8 @@ val data = supabase.from("books").select(Columns.raw(""" ``` + +<$Show if="sdk:python"> ```python @@ -470,6 +483,7 @@ data = supabase.from_('books').select(""" ``` + | title | description | price | low_age | high_age | diff --git a/apps/docs/content/guides/database/tables.mdx b/apps/docs/content/guides/database/tables.mdx index 547d61ebb17..ad9db5e13a0 100644 --- a/apps/docs/content/guides/database/tables.mdx +++ b/apps/docs/content/guides/database/tables.mdx @@ -226,6 +226,7 @@ const { data, error } = await supabase.from('movies').insert([ ``` +<$Show if="sdk:dart"> ```dart @@ -241,6 +242,8 @@ await supabase ``` + +<$Show if="sdk:swift"> ```swift @@ -263,6 +266,8 @@ try await supabase.from("movies") ``` + +<$Show if="sdk:python"> ```python @@ -279,6 +284,8 @@ client.from_("movies").insert([ ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -299,6 +306,7 @@ supabase ``` + ### Bulk data loading diff --git a/apps/docs/content/guides/getting-started.mdx b/apps/docs/content/guides/getting-started.mdx index 3e50b8ff57b..9024dc736f4 100644 --- a/apps/docs/content/guides/getting-started.mdx +++ b/apps/docs/content/guides/getting-started.mdx @@ -101,6 +101,7 @@ hideToc: true description: 'Learn how to create a Supabase project, add some sample data to your database, and query the data from a React app.', icon: '/docs/img/icons/react-icon', + enabled: true, }, { title: 'Next.js', @@ -109,6 +110,7 @@ hideToc: true 'Learn how to create a Supabase project, add some sample data to your database, and query the data from a Next.js app.', icon: '/docs/img/icons/nextjs-icon', hasLightIcon: true, + enabled: true, }, { title: 'Nuxt', @@ -116,6 +118,7 @@ hideToc: true description: 'Learn how to create a Supabase project, add some sample data to your database, and query the data from a Nuxt app.', icon: '/docs/img/icons/nuxt-icon', + enabled: true, }, { title: 'Hono', @@ -123,6 +126,7 @@ hideToc: true description: 'Learn how to create a Supabase project, add some sample data to your database, secure it with auth, and query the data from a Hono app.', icon: '/docs/img/icons/hono-icon', + enabled: true, }, { title: 'RedwoodJS', @@ -130,6 +134,7 @@ hideToc: true description: 'Learn how to create a Supabase project, add some sample data to your database using Prisma migration and seeds, and query the data from a RedwoodJS app.', icon: '/docs/img/icons/redwood-icon', + enabled: true, }, { title: 'Flutter', @@ -137,6 +142,7 @@ hideToc: true description: 'Learn how to create a Supabase project, add some sample data to your database, and query the data from a Flutter app.', icon: '/docs/img/icons/flutter-icon', + enabled: isFeatureEnabled('sdk:dart'), }, { title: 'iOS SwiftUI', @@ -144,6 +150,7 @@ hideToc: true description: 'Learn how to create a Supabase project, add some sample data to your database, and query the data from an iOS app.', icon: '/docs/img/icons/swift-icon', + enabled: isFeatureEnabled('sdk:swift'), }, { title: 'Android Kotlin', @@ -151,6 +158,7 @@ hideToc: true description: 'Learn how to create a Supabase project, add some sample data to your database, and query the data from an Android Kotlin app.', icon: '/docs/img/icons/kotlin-icon', + enabled: isFeatureEnabled('sdk:kotlin'), }, { title: 'SvelteKit', @@ -158,6 +166,7 @@ hideToc: true description: 'Learn how to create a Supabase project, add some sample data to your database, and query the data from a SvelteKit app.', icon: '/docs/img/icons/svelte-icon', + enabled: true, }, { title: 'SolidJS', @@ -165,6 +174,7 @@ hideToc: true description: 'Learn how to create a Supabase project, add some sample data to your database, and query the data from a SolidJS app.', icon: '/docs/img/icons/solidjs-icon', + enabled: true, }, { title: 'Vue', @@ -172,6 +182,7 @@ hideToc: true description: 'Learn how to create a Supabase project, add some sample data to your database, and query the data from a Vue app.', icon: '/docs/img/icons/vuejs-icon', + enabled: true, }, { title: 'refine', @@ -179,22 +190,25 @@ hideToc: true description: 'Learn how to create a Supabase project, add some sample data to your database, and query the data from a refine app.', icon: '/docs/img/icons/refine-icon', + enabled: true, }, - ].map((item) => { - return ( - - - {item.description} - - - ) - })} + ] + .filter((item) => item.enabled !== false) + .map((item) => { + return ( + + + {item.description} + + + ) + })} ### Web app demos @@ -208,63 +222,63 @@ hideToc: true description: 'Learn how to build a user management app with Next.js and Supabase Database, Auth, and Storage functionality.', icon: '/docs/img/icons/nextjs-icon', - hasLightIcon: true + hasLightIcon: true, }, { title: 'React', href: '/guides/getting-started/tutorials/with-react', description: 'Learn how to build a user management app with React and Supabase Database, Auth, and Storage functionality.', - icon: '/docs/img/icons/react-icon' + icon: '/docs/img/icons/react-icon', }, { title: 'Vue 3', href: '/guides/getting-started/tutorials/with-vue-3', description: 'Learn how to build a user management app with Vue 3 and Supabase Database, Auth, and Storage functionality.', - icon: '/docs/img/icons/vuejs-icon' + icon: '/docs/img/icons/vuejs-icon', }, { title: 'Nuxt 3', href: '/guides/getting-started/tutorials/with-nuxt-3', description: 'Learn how to build a user management app with Nuxt 3 and Supabase Database, Auth, and Storage functionality.', - icon: '/docs/img/icons/nuxt-icon' + icon: '/docs/img/icons/nuxt-icon', }, { title: 'Angular', href: '/guides/getting-started/tutorials/with-angular', description: 'Learn how to build a user management app with Angular and Supabase Database, Auth, and Storage functionality.', - icon: '/docs/img/icons/angular-icon' + icon: '/docs/img/icons/angular-icon', }, { title: 'RedwoodJS', href: '/guides/getting-started/tutorials/with-redwoodjs', description: 'Learn how to build a user management app with RedwoodJS and Supabase Database, Auth, and Storage functionality.', - icon: '/docs/img/icons/redwood-icon' + icon: '/docs/img/icons/redwood-icon', }, { title: 'Svelte', href: '/guides/getting-started/tutorials/with-svelte', description: 'Learn how to build a user management app with Svelte and Supabase Database, Auth, and Storage functionality.', - icon: '/docs/img/icons/svelte-icon' + icon: '/docs/img/icons/svelte-icon', }, { title: 'SvelteKit', href: '/guides/getting-started/tutorials/with-sveltekit', description: 'Learn how to build a user management app with SvelteKit and Supabase Database, Auth, and Storage functionality.', - icon: '/docs/img/icons/svelte-icon' + icon: '/docs/img/icons/svelte-icon', }, { title: 'refine', href: '/guides/getting-started/tutorials/with-refine', description: 'Learn how to build a user management app with refine and Supabase Database, Auth, and Storage functionality.', - icon: '/docs/img/icons/refine-icon' + icon: '/docs/img/icons/refine-icon', } ] .map((item) => { @@ -295,7 +309,8 @@ hideToc: true href: '/guides/getting-started/tutorials/with-flutter', description: 'Learn how to build a user management app with Flutter and Supabase Database, Auth, and Storage functionality.', - icon: '/docs/img/icons/flutter-icon' + icon: '/docs/img/icons/flutter-icon', + enabled: isFeatureEnabled('sdk:dart') }, { title: 'Expo React Native', @@ -303,44 +318,52 @@ hideToc: true description: 'Learn how to build a user management app with Expo React Native and Supabase Database, Auth, and Storage functionality.', icon: '/docs/img/icons/expo-icon', - hasLightIcon: true + hasLightIcon: true, + enabled: true }, { title: 'Android Kotlin', href: '/guides/getting-started/tutorials/with-kotlin', description: 'Learn how to build a product management app with Android and Supabase Database, Auth, and Storage functionality.', - icon: '/docs/img/icons/kotlin-icon' + icon: '/docs/img/icons/kotlin-icon', + enabled: isFeatureEnabled('sdk:kotlin') }, { title: 'iOS Swift', href: '/guides/getting-started/tutorials/with-swift', description: 'Learn how to build a user management app with iOS and Supabase Database, Auth, and Storage functionality.', - icon: '/docs/img/icons/swift-icon' + icon: '/docs/img/icons/swift-icon', + enabled: isFeatureEnabled('sdk:swift') }, { title: 'Ionic React', href: '/guides/getting-started/tutorials/with-ionic-react', description: 'Learn how to build a user management app with Ionic React and Supabase Database, Auth, and Storage functionality.', - icon: '/docs/img/icons/ionic-icon' + icon: '/docs/img/icons/ionic-icon', + enabled: true }, { title: 'Ionic Vue', href: '/guides/getting-started/tutorials/with-ionic-vue', description: 'Learn how to build a user management app with Ionic Vue and Supabase Database, Auth, and Storage functionality.', - icon: '/docs/img/icons/ionic-icon' + icon: '/docs/img/icons/ionic-icon', + enabled: true }, { title: 'Ionic Angular', href: '/guides/getting-started/tutorials/with-ionic-angular', description: 'Learn how to build a user management app with Ionic Angular and Supabase Database, Auth, and Storage functionality.', - icon: '/docs/img/icons/ionic-icon' + icon: '/docs/img/icons/ionic-icon', + enabled: true } - ].map((item) => { + ] +.filter((item) => item.enabled !== false) +.map((item) => { return ( - In `App.jsx`, add a `getInstruments` function to fetch the data and display the query result to the page using a Supabase client. + Replace the contents of `App.jsx` to add a `getInstruments` function to fetch the data and display the query result to the page using a Supabase client. @@ -120,7 +120,7 @@ hideToc: true - Start the app, go to http://localhost:5173 in a browser, and open the browser console and you should see the list of instruments. + Run the development server, go to http://localhost:5173 in a browser and you should see the list of instruments. diff --git a/apps/docs/content/guides/platform/backups.mdx b/apps/docs/content/guides/platform/backups.mdx index 27ad8b94a2d..ddc201410f7 100644 --- a/apps/docs/content/guides/platform/backups.mdx +++ b/apps/docs/content/guides/platform/backups.mdx @@ -163,11 +163,9 @@ A date and time picker will be provided upon pressing the `Start a restore` butt After locking in the desired point in time to recover to, The Dashboard will then prompt for a review and confirmation before proceeding with the restoration. The project will be inaccessible following this. As such, do ensure to allot for downtime beforehand. This is dependent on the size of the database. The larger it is, the longer the downtime will be. Once the confirmation has been given, the latest physical backup available is downloaded to the project and the database is partially restored. WAL files generated after this physical backup up to the specified point-in-time are then downloaded. The underlying records of transactions in these files are replayed against the database to complete the restoration. The Dashboard will display a notification once the restoration completes. -### Pricing - +<$Show if="billing:all"> <$Partial path="billing/pricing/pricing_pitr.mdx" /> - -For a detailed breakdown of how charges are calculated, refer to [Manage Point-in-Time Recovery usage](/docs/guides/platform/manage-your-usage/point-in-time-recovery). + ## Restore to a new project diff --git a/apps/docs/content/guides/platform/credits.mdx b/apps/docs/content/guides/platform/credits.mdx index 625cfc8bd79..b7d765b0230 100644 --- a/apps/docs/content/guides/platform/credits.mdx +++ b/apps/docs/content/guides/platform/credits.mdx @@ -35,7 +35,7 @@ You may want to consider this option to avoid issues with recurring payments, ga -If you are interested in larger (> ,000) credit packages, [reach out](https://supabase.com/dashboard/support/new?subject=I%20would%20like%20to%20inquire%20about%20larger%20credit%20packages&category=Sales). +If you are interested in larger (> ) credit packages, [reach out](https://supabase.com/dashboard/support/new?subject=I%20would%20like%20to%20inquire%20about%20larger%20credit%20packages&category=Sales). diff --git a/apps/docs/content/guides/platform/custom-domains.mdx b/apps/docs/content/guides/platform/custom-domains.mdx index e4d4a5e8847..05b4f37e013 100644 --- a/apps/docs/content/guides/platform/custom-domains.mdx +++ b/apps/docs/content/guides/platform/custom-domains.mdx @@ -217,6 +217,6 @@ Use the [`vanity-subdomains delete`](/docs/reference/cli/supabase-vanity-subdoma supabase vanity-subdomains delete --project-ref abcdefghijklmnopqrst --experimental ``` -## Pricing - -For a detailed breakdown of how charges are calculated, refer to [Manage Custom Domain usage](/docs/guides/platform/manage-your-usage/custom-domains). +<$Show if="billing:all"> +<$Partial path="billing/pricing/pricing_custom_domains.mdx" /> + diff --git a/apps/docs/content/guides/platform/ipv4-address.mdx b/apps/docs/content/guides/platform/ipv4-address.mdx index 13c0b01abe7..4e1afd14e94 100644 --- a/apps/docs/content/guides/platform/ipv4-address.mdx +++ b/apps/docs/content/guides/platform/ipv4-address.mdx @@ -126,6 +126,6 @@ Always uses an IPv4 address postgresql://postgres.ajrbwkcuthywfddihrmflo:[YOUR-PASSWORD]@aws-0-us-east-1.pooler.supabase.com:5432/postgres ``` -## Pricing - -For a detailed breakdown of how charges are calculated, refer to [Manage IPv4 usage](/docs/guides/platform/manage-your-usage/ipv4). +<$Show if="billing:all"> +<$Partial path="billing/pricing/pricing_ipv4.mdx" /> + diff --git a/apps/docs/content/guides/realtime/authorization.mdx b/apps/docs/content/guides/realtime/authorization.mdx index 9fb4f3aa8aa..1ba5a3d45f3 100644 --- a/apps/docs/content/guides/realtime/authorization.mdx +++ b/apps/docs/content/guides/realtime/authorization.mdx @@ -171,6 +171,7 @@ Then, to join a topic with RLS enabled, instantiate the Channel with the `privat ``` + <$Show if="sdk:dart"> ```dart final channel = supabase.channel( @@ -190,6 +191,8 @@ Then, to join a topic with RLS enabled, instantiate the Channel with the `privat ``` + + <$Show if="sdk:swift"> ```swift let channel = supabase.channel("room-1") { @@ -207,6 +210,8 @@ Then, to join a topic with RLS enabled, instantiate the Channel with the `privat ``` + + <$Show if="sdk:kotlin"> ```kotlin val channel = supabase.channel("room-1") { @@ -220,6 +225,8 @@ Then, to join a topic with RLS enabled, instantiate the Channel with the `privat ``` + + <$Show if="sdk:python"> ```py channel = realtime.channel( @@ -238,6 +245,7 @@ Then, to join a topic with RLS enabled, instantiate the Channel with the `privat ``` + #### Allow a user to send a Broadcast message diff --git a/apps/docs/content/guides/realtime/broadcast.mdx b/apps/docs/content/guides/realtime/broadcast.mdx index 18bce679967..93791899048 100644 --- a/apps/docs/content/guides/realtime/broadcast.mdx +++ b/apps/docs/content/guides/realtime/broadcast.mdx @@ -33,6 +33,7 @@ Go to your Supabase project's [API Settings](https://supabase.com/dashboard/proj ``` + <$Show if="sdk:dart"> ```dart @@ -50,6 +51,8 @@ Go to your Supabase project's [API Settings](https://supabase.com/dashboard/proj ``` + + <$Show if="sdk:swift"> ```swift @@ -62,6 +65,8 @@ Go to your Supabase project's [API Settings](https://supabase.com/dashboard/proj ``` + + <$Show if="sdk:kotlin"> ```kotlin @@ -73,6 +78,8 @@ Go to your Supabase project's [API Settings](https://supabase.com/dashboard/proj ``` + + <$Show if="sdk:python"> ```python @@ -88,6 +95,7 @@ Go to your Supabase project's [API Settings](https://supabase.com/dashboard/proj ``` + ### Receiving Broadcast messages @@ -129,6 +137,7 @@ You can provide a callback for the `broadcast` channel to receive messages. This ``` + <$Show if="sdk:dart"> {/* prettier-ignore */} @@ -150,6 +159,8 @@ You can provide a callback for the `broadcast` channel to receive messages. This ``` + + <$Show if="sdk:swift"> ```swift @@ -166,6 +177,8 @@ You can provide a callback for the `broadcast` channel to receive messages. This ``` + + <$Show if="sdk:kotlin"> {/* prettier-ignore */} @@ -182,6 +195,8 @@ You can provide a callback for the `broadcast` channel to receive messages. This ``` + + <$Show if="sdk:python"> @@ -205,6 +220,7 @@ You can provide a callback for the `broadcast` channel to receive messages. This ``` + ## Send messages @@ -260,6 +276,8 @@ You can use the Supabase client libraries to send Broadcast messages. +<$Show if="sdk:dart"> + {/* prettier-ignore */} @@ -287,6 +305,8 @@ You can use the Supabase client libraries to send Broadcast messages. ``` + + <$Show if="sdk:swift"> {/* prettier-ignore */} @@ -307,6 +327,8 @@ You can use the Supabase client libraries to send Broadcast messages. ``` + + <$Show if="sdk:kotlin"> ```kotlin val myChannel = supabase.channel("test-channel") { @@ -329,6 +351,9 @@ You can use the Supabase client libraries to send Broadcast messages. ``` + + +<$Show if="sdk:python"> @@ -356,6 +381,7 @@ You can use the Supabase client libraries to send Broadcast messages. ``` + {/* supa-mdx-lint-disable-next-line Rule001HeadingCase */} ### Broadcast from the Database @@ -484,6 +510,7 @@ You can pass configuration options while initializing the Supabase Client. ``` + <$Show if="sdk:dart"> By default, broadcast messages are only sent to other clients. You can broadcast messages back to the sender by setting Broadcast's `self` parameter to `true`. @@ -512,6 +539,8 @@ You can pass configuration options while initializing the Supabase Client. ``` + + <$Show if="sdk:swift"> By default, broadcast messages are only sent to other clients. You can broadcast messages back to the sender by setting Broadcast's `receiveOwnBroadcasts` parameter to `true`. @@ -534,6 +563,8 @@ You can pass configuration options while initializing the Supabase Client. ``` + + <$Show if="sdk:kotlin"> By default, broadcast messages are only sent to other clients. You can broadcast messages back to the sender by setting Broadcast's `receiveOwnBroadcasts` parameter to `true`. @@ -562,6 +593,8 @@ You can pass configuration options while initializing the Supabase Client. ``` + + <$Show if="sdk:python"> @@ -595,6 +628,7 @@ You can pass configuration options while initializing the Supabase Client. ``` + ### Acknowledge messages @@ -636,6 +670,7 @@ You can pass configuration options while initializing the Supabase Client. ``` + <$Show if="sdk:dart"> ```dart @@ -659,6 +694,8 @@ You can pass configuration options while initializing the Supabase Client. ``` + + <$Show if="sdk:swift"> You can confirm that Realtime received your message by setting Broadcast's `acknowledgeBroadcasts` config to `true`. @@ -674,6 +711,8 @@ You can pass configuration options while initializing the Supabase Client. ``` + + <$Show if="sdk:kotlin"> By default, broadcast messages are only sent to other clients. You can broadcast messages back to the sender by setting Broadcast's `acknowledgeBroadcasts` parameter to `true`. @@ -691,9 +730,12 @@ You can pass configuration options while initializing the Supabase Client. ``` + + <$Show if="sdk:python"> Unsupported in Python yet. + Use this to guarantee that the server has received the message before resolving `channelD.send`'s promise. If the `ack` config is not set to `true` when creating the channel, the promise returned by `channelD.send` will resolve immediately. @@ -736,6 +778,7 @@ You can also send a Broadcast message by making an HTTP request to Realtime serv ``` + <$Show if="sdk:dart"> ```dart // No need to subscribe to channel @@ -751,6 +794,8 @@ You can also send a Broadcast message by making an HTTP request to Realtime serv ``` + + <$Show if="sdk:swift"> ```swift let myChannel = await supabase.channel("room-2") { @@ -763,6 +808,8 @@ You can also send a Broadcast message by making an HTTP request to Realtime serv ``` + + <$Show if="sdk:kotlin"> ```kotlin val myChannel = supabase.channel("room-2") { @@ -779,9 +826,12 @@ You can also send a Broadcast message by making an HTTP request to Realtime serv ``` + + <$Show if="sdk:python"> Unsupported in Python yet. + ## Trigger broadcast messages from your database diff --git a/apps/docs/content/guides/realtime/postgres-changes.mdx b/apps/docs/content/guides/realtime/postgres-changes.mdx index 993b6935fd6..d2b35ac32e6 100644 --- a/apps/docs/content/guides/realtime/postgres-changes.mdx +++ b/apps/docs/content/guides/realtime/postgres-changes.mdx @@ -238,6 +238,7 @@ const changes = supabase ``` +<$Show if="sdk:dart"> ```dart @@ -252,6 +253,8 @@ supabase ``` + +<$Show if="sdk:swift"> ```swift @@ -272,6 +275,8 @@ for await change in changes { ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -293,6 +298,8 @@ myChannel.subscribe() ``` + +<$Show if="sdk:python"> ```python @@ -305,6 +312,7 @@ changes = supabase.channel('schema-db-changes').on_postgres_changes( ``` + The channel name can be any string except 'realtime'. @@ -337,6 +345,7 @@ const changes = supabase ``` +<$Show if="sdk:dart"> ```dart @@ -350,6 +359,8 @@ final changes = supabase ``` + +<$Show if="sdk:swift"> Use `InsertAction.self` as type to listen only to database `INSERT`s: @@ -367,6 +378,8 @@ for await change in changes { ``` + +<$Show if="sdk:kotlin"> Use `PostgresAction.Insert` as type to listen only to database `INSERT`s: @@ -386,6 +399,8 @@ myChannel.subscribe() ``` + +<$Show if="sdk:python"> ```python @@ -398,6 +413,7 @@ changes = supabase.channel('schema-db-changes').on_postgres_changes( ``` + The channel name can be any string except 'realtime'. @@ -430,6 +446,7 @@ const changes = supabase ``` +<$Show if="sdk:dart"> ```dart @@ -443,6 +460,8 @@ supabase ``` + +<$Show if="sdk:swift"> Use `UpdateAction.self` as type to listen only to database `UPDATE`s: @@ -460,6 +479,8 @@ for await change in changes { ``` + +<$Show if="sdk:kotlin"> Use `PostgresAction.Update` as type to listen only to database `UPDATE`s: @@ -479,6 +500,8 @@ myChannel.subscribe() ``` + +<$Show if="sdk:python"> ```python @@ -491,6 +514,7 @@ changes = supabase.channel('schema-db-changes').on_postgres_changes( ``` + The channel name can be any string except 'realtime'. @@ -523,6 +547,7 @@ const changes = supabase ``` +<$Show if="sdk:dart"> ```dart @@ -536,6 +561,8 @@ supabase ``` + +<$Show if="sdk:swift"> Use `DeleteAction.self` as type to listen only to database `DELETE`s: @@ -553,6 +580,8 @@ for await change in changes { ``` + +<$Show if="sdk:kotlin"> Use `PostgresAction.Delete` as type to listen only to database `DELETE`s: @@ -572,6 +601,8 @@ myChannel.subscribe() ``` + +<$Show if="sdk:python"> ```python @@ -584,6 +615,7 @@ changes = supabase.channel('schema-db-changes').on_postgres_changes( ``` + The channel name can be any string except 'realtime'. @@ -617,6 +649,7 @@ const changes = supabase ``` +<$Show if="sdk:dart"> ```dart @@ -631,6 +664,8 @@ supabase ``` + +<$Show if="sdk:swfit"> ```swift @@ -651,6 +686,8 @@ for await change in changes { ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -670,6 +707,8 @@ myChannel.subscribe() ``` + +<$Show if="sdk:python"> ```python @@ -683,6 +722,7 @@ changes = supabase.channel('db-changes').on_postgres_changes( ``` + The channel name can be any string except 'realtime'. @@ -725,6 +765,7 @@ const channel = supabase ``` +<$Show if="sdk:dart"> ```dart @@ -744,6 +785,8 @@ supabase ``` + +<$Show if="sdk:swift"> ```swift @@ -756,6 +799,8 @@ await myChannel.subscribe() ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -770,6 +815,8 @@ myChannel.subscribe() ``` + +<$Show if="sdk:python"> ```python @@ -787,6 +834,7 @@ changes = supabase.channel('db-changes').on_postgres_changes( ``` + ### Filtering for specific changes @@ -819,6 +867,7 @@ const changes = supabase ``` +<$Show if="sdk:dart"> ```dart @@ -838,6 +887,8 @@ const changes = supabase ``` + +<$Show if="sdk:swift"> ```swift @@ -858,6 +909,8 @@ for await change in changes { ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -878,6 +931,8 @@ myChannel.subscribe() ``` + +<$Show if="sdk:python"> ```python @@ -892,6 +947,7 @@ changes = supabase.channel('db-changes').on_postgres_changes( ``` + ## Available filters @@ -928,6 +984,7 @@ const channel = supabase ``` +<$Show if="sdk:dart"> ```dart @@ -947,6 +1004,8 @@ supabase ``` + +<$Show if="sdk:swift"> ```swift @@ -967,6 +1026,8 @@ for await change in changes { ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -987,6 +1048,8 @@ myChannel.subscribe() ``` + +<$Show if="sdk:python"> ```python @@ -1001,6 +1064,7 @@ changes = supabase.channel('db-changes').on_postgres_changes( ``` + This filter uses Postgres's `=` filter. @@ -1035,6 +1099,7 @@ const channel = supabase ``` +<$Show if="sdk:dart"> ```dart @@ -1054,6 +1119,8 @@ supabase ``` + +<$Show if="sdk:swift"> ```swift @@ -1074,6 +1141,8 @@ for await change in changes { ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -1095,6 +1164,8 @@ myChannel.join() ``` + +<$Show if="sdk:python"> ```python @@ -1109,6 +1180,7 @@ changes = supabase.channel('db-changes').on_postgres_changes( ``` + This filter uses Postgres's `!=` filter. @@ -1143,6 +1215,7 @@ const channel = supabase ``` +<$Show if="sdk:dart"> ```dart @@ -1162,6 +1235,8 @@ supabase ``` + +<$Show if="sdk:swift"> ```swift @@ -1182,6 +1257,8 @@ for await change in changes { ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -1202,6 +1279,8 @@ myChannel.subscribe() ``` + +<$Show if="sdk:python"> ```python @@ -1216,6 +1295,7 @@ changes = supabase.channel('db-changes').on_postgres_changes( ``` + This filter uses Postgres's `<` filter, so it works for non-numeric types. Make sure to check the expected behavior of the compared data's type. @@ -1250,6 +1330,7 @@ const channel = supabase ``` +<$Show if="sdk:dart"> ```dart @@ -1269,6 +1350,8 @@ supabase ``` + +<$Show if="sdk:swift"> ```swift @@ -1289,6 +1372,8 @@ for await change in changes { ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -1309,6 +1394,8 @@ myChannel.subscribe() ``` + +<$Show if="sdk:python"> ```python @@ -1323,6 +1410,7 @@ changes = supabase.channel('db-changes').on_postgres_changes( ``` + This filter uses Postgres' `<=` filter, so it works for non-numeric types. Make sure to check the expected behavior of the compared data's type. @@ -1357,6 +1445,7 @@ const channel = supabase ``` +<$Show if="sdk:dart"> ```dart @@ -1376,6 +1465,8 @@ supabase ``` + +<$Show if="sdk:swift"> ```swift @@ -1396,6 +1487,8 @@ for await change in changes { ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -1416,6 +1509,8 @@ myChannel.subscribe() ``` + +<$Show if="sdk:python"> ```python @@ -1430,6 +1525,7 @@ changes = supabase.channel('db-changes').on_postgres_changes( ``` + This filter uses Postgres's `>` filter, so it works for non-numeric types. Make sure to check the expected behavior of the compared data's type. @@ -1464,6 +1560,7 @@ const channel = supabase ``` +<$Show if="sdk:dart"> ```dart @@ -1483,6 +1580,8 @@ supabase ``` + +<$Show if="sdk:swift"> ```swift @@ -1503,6 +1602,8 @@ for await change in changes { ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -1523,6 +1624,8 @@ myChannel.subscribe() ``` + +<$Show if="sdk:python"> ```python @@ -1537,6 +1640,7 @@ changes = supabase.channel('db-changes').on_postgres_changes( ``` + This filter uses Postgres's `>=` filter, so it works for non-numeric types. Make sure to check the expected behavior of the compared data's type. @@ -1571,6 +1675,7 @@ const channel = supabase ``` +<$Show if="sdk:dart"> ```dart @@ -1590,6 +1695,8 @@ supabase ``` + +<$Show if="sdk:swift"> ```swift @@ -1610,6 +1717,8 @@ for await change in changes { ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -1630,6 +1739,8 @@ myChannel.subscribe() ``` + +<$Show if="sdk:python"> ```python @@ -1644,6 +1755,7 @@ changes = supabase.channel('db-changes').on_postgres_changes( ``` + This filter uses Postgres's `= ANY`. Realtime allows a maximum of 100 values for this filter. @@ -1724,6 +1836,7 @@ const channel = supabase ``` +<$Show if="sdk:dart"> ```dart @@ -1746,6 +1859,8 @@ supabase ``` + +<$Show if="sdk:swift"> ```swift @@ -1768,6 +1883,8 @@ for await change in changes { ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -1793,6 +1910,8 @@ myChannel.subscribe() ``` + +<$Show if="sdk:python"> ```python @@ -1809,6 +1928,7 @@ changes = supabase.channel('db-changes').on_postgres_changes( ``` + ### Refreshed tokens @@ -1833,6 +1953,7 @@ supabase.realtime.setAuth('fresh-token') ``` +<$Show if="sdk:dart"> ```dart @@ -1840,6 +1961,8 @@ supabase.realtime.setAuth('fresh-token'); ``` + +<$Show if="sdk:swift"> ```swift @@ -1847,6 +1970,8 @@ await supabase.realtime.setAuth("fresh-token") ``` + +<$Show if="sdk:kotlin"> In Kotlin, you have to update the token manually per channel: @@ -1856,6 +1981,8 @@ myChannel.updateAuth("fresh-token") ``` + +<$Show if="sdk:python"> ```python @@ -1863,6 +1990,7 @@ supabase.realtime.set_auth('fresh-token') ``` + ## Limitations diff --git a/apps/docs/content/guides/realtime/presence.mdx b/apps/docs/content/guides/realtime/presence.mdx index 2c599015f08..86b474cc665 100644 --- a/apps/docs/content/guides/realtime/presence.mdx +++ b/apps/docs/content/guides/realtime/presence.mdx @@ -33,6 +33,7 @@ const supabase = createClient(SUPABASE_URL, SUPABASE_KEY) ``` +<$Show if="sdk:dart"> ```dart @@ -49,6 +50,8 @@ final supabase = Supabase.instance.client; ``` + +<$Show if="sdk:swift"> ```swift @@ -60,6 +63,8 @@ let realtime = supabase.realtime ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -71,6 +76,8 @@ val supabase = createSupabaseClient(supabaseUrl, supabaseKey) { ``` + +<$Show if="sdk:python"> ```python @@ -83,6 +90,7 @@ supabase = create_client(SUPABASE_URL, SUPABASE_KEY) ``` + ### Sync and track state @@ -120,6 +128,7 @@ roomOne ``` +<$Show if="sdk:dart"> ```dart @@ -138,6 +147,8 @@ roomOne.onPresenceSync((_) { ``` + +<$Show if="sdk:swift"> Listen to the presence change stream, emitting a new `PresenceAction` whenever someone joins or leaves: @@ -155,6 +166,8 @@ for await presence in presenceStream { ``` + +<$Show if="sdk:kotlin"> Listen to the presence change flow, emitting new a new `PresenceAction` whenever someone joins or leaves: @@ -173,6 +186,8 @@ roomOne.subscribe() ``` + +<$Show if="sdk:python"> Listen to the `sync`, `join`, and `leave` events triggered whenever any client joins or leaves the channel or changes their slice of state: @@ -188,6 +203,7 @@ room_one ``` + ### Sending state @@ -225,6 +241,7 @@ roomOne.subscribe(async (status) => { ``` +<$Show if="sdk:dart"> ```dart @@ -244,6 +261,8 @@ roomOne.subscribe((status, error) async { ``` + +<$Show if="sdk:swift"> ```swift @@ -269,6 +288,8 @@ await roomOne.track( ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -285,6 +306,8 @@ roomOne.track(userStatus) ``` + +<$Show if="sdk:python"> ```python @@ -305,6 +328,7 @@ room_one.subscribe(on_subscribe) ``` + A client will receive state from any other client that is subscribed to the same topic (in this case `room_01`). It will also automatically trigger its own `sync` and `join` event handlers. @@ -337,6 +361,7 @@ untrackPresence() ``` +<$Show if="sdk:dart"> ```dart @@ -351,6 +376,8 @@ untrackPresence(); ``` + +<$Show if="sdk:swift"> ```swift @@ -358,6 +385,8 @@ await roomOne.untrack() ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -369,6 +398,8 @@ untrackPresence() ``` + +<$Show if="sdk:python"> ```python @@ -376,6 +407,7 @@ room_one.untrack() ``` + ## Presence options @@ -409,6 +441,7 @@ const channelC = supabase.channel('test', { ``` +<$Show if="sdk:dart"> ```dart @@ -419,6 +452,8 @@ final channelC = supabase.channel( ``` + +<$Show if="sdk:swift"> ```swift @@ -428,6 +463,8 @@ let channelC = await supabase.channel("test") { ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -439,6 +476,8 @@ val channelC = supabase.channel("test") { ``` + +<$Show if="sdk:python"> ```python @@ -452,4 +491,5 @@ channel_c = supabase.channel('test', { ``` + diff --git a/apps/docs/content/guides/storage/buckets/creating-buckets.mdx b/apps/docs/content/guides/storage/buckets/creating-buckets.mdx index b466ec48cbf..65ad5398ba3 100644 --- a/apps/docs/content/guides/storage/buckets/creating-buckets.mdx +++ b/apps/docs/content/guides/storage/buckets/creating-buckets.mdx @@ -52,6 +52,7 @@ values ``` +<$Show if="sdk:dart"> ```dart @@ -67,6 +68,8 @@ void main() async { [Reference.](https://pub.dev/documentation/storage_client/latest/storage_client/SupabaseStorageClient/createBucket.html) + +<$Show if="sdk:swift"> ```swift @@ -79,6 +82,8 @@ try await supabase.storage.createBucket( [Reference.](/docs/reference/swift/storage-createbucket) + +<$Show if="sdk:python"> ```python @@ -91,6 +96,7 @@ supabase.storage.create_bucket( [Reference.](/docs/reference/python/storage-createbucket) + ## Restricting uploads diff --git a/apps/docs/content/guides/storage/quickstart.mdx b/apps/docs/content/guides/storage/quickstart.mdx index ee1f9e06f8c..2ea0e5b8ff8 100644 --- a/apps/docs/content/guides/storage/quickstart.mdx +++ b/apps/docs/content/guides/storage/quickstart.mdx @@ -72,6 +72,7 @@ const { data, error } = await supabase.storage.createBucket('avatars') [Reference.](/docs/reference/javascript/storage-createbucket) +<$Show if="sdk:dart"> ```dart @@ -87,6 +88,8 @@ void main() async { [Reference.](https://pub.dev/documentation/storage_client/latest/storage_client/SupabaseStorageClient/createBucket.html) + +<$Show if="sdk:swift"> ```swift @@ -96,6 +99,8 @@ try await supabase.storage.createBucket("avatars") [Reference.](/docs/reference/swift/storage-createbucket) + +<$Show if="sdk:python"> ```python @@ -105,6 +110,7 @@ response = supabase.storage.create_bucket('avatars') [Reference.](/docs/reference/python/storage-createbucket) + ## Upload a file @@ -138,6 +144,7 @@ const { data, error } = await supabase.storage [Reference.](/docs/reference/javascript/storage-from-upload) +<$Show if="sdk:dart"> ```dart @@ -157,6 +164,7 @@ void main() async { [Reference.](https://pub.dev/documentation/storage_client/latest/storage_client/SupabaseStorageClient/createBucket.html) + ## Download a file @@ -189,6 +197,7 @@ const { data, error } = await supabase.storage.from('avatars').download('public/ [Reference.](/docs/reference/javascript/storage-from-download) +<$Show if="sdk:dart"> ```dart @@ -205,6 +214,8 @@ void main() async { [Reference.](/docs/reference/dart/storage-from-download) + +<$Show if="sdk:swift"> ```swift @@ -214,6 +225,8 @@ let response = try await supabase.storage.from("avatars").download(path: "public [Reference.](/docs/reference/python/storage-from-download) + +<$Show if="sdk:python"> ```python @@ -223,6 +236,7 @@ response = supabase.storage.from_('avatars').download('public/avatar1.png') [Reference.](/docs/reference/python/storage-from-download) + ## Add security rules diff --git a/apps/docs/content/guides/storage/serving/image-transformations.mdx b/apps/docs/content/guides/storage/serving/image-transformations.mdx index 952d6a79b11..6d79231daad 100644 --- a/apps/docs/content/guides/storage/serving/image-transformations.mdx +++ b/apps/docs/content/guides/storage/serving/image-transformations.mdx @@ -42,6 +42,7 @@ supabase.storage.from('bucket').getPublicUrl('image.jpg', { ``` +<$Show if="sdk:dart"> ```dart @@ -55,6 +56,8 @@ final url = supabase.storage.from('bucket').getPublicUrl( ``` + +<$Show if="sdk:swift"> ```swift @@ -66,6 +69,8 @@ let url = try await supabase.storage.from("bucket") ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -75,6 +80,8 @@ val url = supabase.storage.from("bucket").publicRenderUrl("image.jpg") { ``` + +<$Show if="sdk:python"> ```python @@ -90,6 +97,7 @@ url = supabase.storage.from_('avatars').get_public_url( ``` + An example URL could look like this: @@ -125,6 +133,7 @@ supabase.storage.from('bucket').createSignedUrl('image.jpg', 60000, { ``` +<$Show if="sdk:dart"> ```dart @@ -139,6 +148,8 @@ final url = await supabase.storage.from('bucket').createSignedUrl( ``` + +<$Show if="sdk:swift"> ```swift @@ -154,6 +165,8 @@ let url = try await supabase.storage.from("bucket") ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -163,6 +176,7 @@ val url = supabase.storage.from("bucket").createSignedUrl("image.jpg", 60.second ``` + The transformation options are embedded into the token attached to the URL — they cannot be changed once signed. @@ -194,6 +208,7 @@ supabase.storage.from('bucket').download('image.jpg', { ``` +<$Show if="sdk:dart"> ```dart @@ -207,6 +222,8 @@ final data = await supabase.storage.from('bucket').download( ``` + +<$Show if="sdk:swift"> ```swift @@ -221,6 +238,8 @@ let data = try await supabase.storage.from("bucket") ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -240,6 +259,8 @@ supabase.storage.from("bucket").downloadAuthenticatedTo("image.jpg", file) { ``` + +<$Show if="sdk:python"> ```python @@ -253,6 +274,7 @@ response = supabase.storage.from_('bucket').download( ``` + ## Automatic image optimization (WebP) @@ -295,6 +317,7 @@ await supabase.storage.from('bucket').download('image.jpeg', { ``` +<$Show if="sdk:dart"> ```dart @@ -309,6 +332,8 @@ final data = await supabase.storage.from('bucket').download( ``` + +<$Show if="sdk:swift"> ```swift @@ -324,6 +349,8 @@ let data = try await supabase.storage.from("bucket") ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -345,6 +372,8 @@ supabase.storage.from("bucket").downloadAuthenticatedTo("image.jpg", file) { ``` + +<$Show if="sdk:python"> ```python @@ -361,6 +390,7 @@ response = supabase.storage.from_('bucket').download( ``` + ## Next.js loader @@ -430,6 +460,7 @@ supabase.storage.from('bucket').download('image.jpg', { ``` +<$Show if="sdk:dart"> ```dart @@ -442,6 +473,8 @@ final data = await supabase.storage.from('bucket').download( ``` + +<$Show if="sdk:swift"> ```swift @@ -455,6 +488,8 @@ let data = try await supabase.storage.from("bucket") ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -474,6 +509,8 @@ supabase.storage["bucket"].downloadAuthenticatedTo("image.jpg", file) { ``` + +<$Show if="sdk:python"> ```python @@ -488,6 +525,7 @@ response = supabase.storage.from_('bucket').download( ``` + ### Resizing @@ -532,6 +570,7 @@ supabase.storage.from('bucket').download('image.jpg', { ``` +<$Show if="sdk:dart"> ```dart @@ -546,6 +585,8 @@ final data = supabase.storage.from('bucket').download( ``` + +<$Show if="sdk:swift"> ```swift @@ -561,6 +602,8 @@ let data = try await supabase.storage.from("bucket") ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -582,6 +625,8 @@ supabase.storage.from("bucket").downloadAuthenticatedTo("image.jpg", file) { ``` + +<$Show if="sdk:python"> ```python @@ -598,6 +643,7 @@ response = supabase.storage.from_('bucket').download( ``` + ### Limits @@ -621,11 +667,9 @@ response = supabase.storage.from_('bucket').download( | BMP | `bmp` | ☑️ | ☑️ | | TIFF | `tiff` | ☑️ | ☑️ | -## Pricing - +<$Show if="billing:all"> <$Partial path="billing/pricing/pricing_storage_image_transformations.mdx" /> - -For a detailed breakdown of how charges are calculated, refer to [Manage Storage Image Transformations usage](/docs/guides/platform/manage-your-usage/storage-image-transformations). + ## Self hosting diff --git a/apps/docs/content/guides/storage/uploads/resumable-uploads.mdx b/apps/docs/content/guides/storage/uploads/resumable-uploads.mdx index 99fec0b0979..2a1db32286c 100644 --- a/apps/docs/content/guides/storage/uploads/resumable-uploads.mdx +++ b/apps/docs/content/guides/storage/uploads/resumable-uploads.mdx @@ -191,6 +191,8 @@ Instead of `https://project-id.supabase.co` use `https://project-id.storage.supa +<$Show if="sdk:kotlin"> + Kotlin supports resumable uploads natively for all targets: @@ -222,6 +224,9 @@ Instead of `https://project-id.supabase.co` use `https://project-id.storage.supa ``` + + +<$Show if="sdk:python"> @@ -269,6 +274,7 @@ Instead of `https://project-id.supabase.co` use `https://project-id.storage.supa ``` + ### Upload URL diff --git a/apps/docs/content/guides/storage/uploads/standard-uploads.mdx b/apps/docs/content/guides/storage/uploads/standard-uploads.mdx index fdde4e31fb7..2e7a2562a90 100644 --- a/apps/docs/content/guides/storage/uploads/standard-uploads.mdx +++ b/apps/docs/content/guides/storage/uploads/standard-uploads.mdx @@ -48,6 +48,7 @@ async function uploadFile(file) { ``` +<$Show if="sdk:dart"> ```dart @@ -58,6 +59,8 @@ Future uploadFile(File file) async { ``` + +<$Show if="sdk:swift"> ```swift @@ -70,6 +73,8 @@ try await supabase.storage.from("bucket_name").upload(path: "file_path", file: f ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -80,6 +85,8 @@ supabase.storage.from("bucket_name").upload("file_path", file) ``` + +<$Show if="sdk:python"> ```python @@ -87,6 +94,7 @@ response = supabase.storage.from_('bucket_name').upload('file_path', file) ``` + ## Overwriting files @@ -117,6 +125,7 @@ await supabase.storage.from('bucket_name').upload('file_path', file, { ``` +<$Show if="sdk:dart"> ```dart @@ -128,6 +137,8 @@ await supabase.storage.from('bucket_name').upload( ``` + +<$Show if="sdk:swift"> ```swift @@ -147,6 +158,8 @@ try await supabase.storage.from("bucket_name") ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -156,6 +169,8 @@ supabase.storage.from("bucket_name").upload("file_path", bytes) { ``` + +<$Show if="sdk:python"> ```python @@ -165,6 +180,7 @@ response = supabase.storage.from_('bucket_name').upload('file_path', file, { ``` + We do advise against overwriting files when possible, as our Content Delivery Network will take sometime to propagate the changes to all the edge nodes leading to stale content. @@ -197,6 +213,7 @@ await supabase.storage.from('bucket_name').upload('file_path', file, { ``` +<$Show if="sdk:dart"> ```dart @@ -208,6 +225,8 @@ await supabase.storage.from('bucket_name').upload( ``` + +<$Show if="sdk:swift"> ```swift @@ -227,6 +246,8 @@ try await supabase.storage.from("bucket_name") ``` + +<$Show if="sdk:kotlin"> ```kotlin @@ -236,6 +257,8 @@ supabase.storage.from("bucket_name").upload("file_path", bytes) { ``` + +<$Show if="sdk:python"> ```python @@ -245,6 +268,7 @@ response = supabase.storage.from_('bucket_name').upload('file_path', file, { ``` + ## Concurrency diff --git a/apps/docs/features/directives/Partial.ts b/apps/docs/features/directives/Partial.ts index eec3b109ebf..888071f2a70 100644 --- a/apps/docs/features/directives/Partial.ts +++ b/apps/docs/features/directives/Partial.ts @@ -65,6 +65,7 @@ function toFilePath(node: MdxJsxFlowElement) { if (typeof path !== 'string' || !isMdFile(path)) { throw new Error('Invalid $Partial path: path must end with .mdx or .md') } + const filePath = join(PARTIALS_DIRECTORY, path) if (!filePath.startsWith(PARTIALS_DIRECTORY)) { throw new Error(`Invalid $Partial path: Path must be inside ${PARTIALS_DIRECTORY}`) diff --git a/apps/docs/features/directives/Show.test.ts b/apps/docs/features/directives/Show.test.ts new file mode 100644 index 00000000000..cbe3024a114 --- /dev/null +++ b/apps/docs/features/directives/Show.test.ts @@ -0,0 +1,199 @@ +import { mdxToMarkdown } from 'mdast-util-mdx' +import { toMarkdown } from 'mdast-util-to-markdown' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +import { type Feature } from 'common' +import { showRemark } from './Show' +import { fromDocsMarkdown } from './utils.server' + +// Mock the isFeatureEnabled function from common package +vi.mock('common/enabled-features', () => ({ + isFeatureEnabled: vi.fn(), +})) + +const { isFeatureEnabled } = await import('common/enabled-features') + +describe('$Show', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + it('should keep children and remove $Show wrapper when feature is enabled', async () => { + vi.mocked(isFeatureEnabled).mockReturnValue(true) + + const markdown = ` +# Test content + +<$Show if="test-feature"> +This content should be visible when feature is enabled. + +## A nested heading + +Some more content. + + +Content after the show block. +`.trim() + + const mdast = fromDocsMarkdown(markdown) + const transformed = showRemark()(mdast) + const output = toMarkdown(transformed, { extensions: [mdxToMarkdown()] }) + + const expected = ` +# Test content + +This content should be visible when feature is enabled. + +## A nested heading + +Some more content. + +Content after the show block. +`.trimStart() + + expect(output).toEqual(expected) + expect(isFeatureEnabled).toHaveBeenCalledWith('test-feature') + }) + + it('should remove entire $Show block and children when feature is disabled', async () => { + vi.mocked(isFeatureEnabled).mockReturnValue(false) + + const markdown = ` +# Test content + +<$Show if="disabled-feature"> +This content should NOT be visible when feature is disabled. + +## This heading should also be removed + +Some more content that should be hidden. + + +Content after the show block should remain. +`.trim() + + const mdast = fromDocsMarkdown(markdown) + const transformed = showRemark()(mdast) + const output = toMarkdown(transformed, { extensions: [mdxToMarkdown()] }) + + const expected = ` +# Test content + +Content after the show block should remain. +`.trimStart() + + expect(output).toEqual(expected) + expect(isFeatureEnabled).toHaveBeenCalledWith('disabled-feature') + }) + + it('should handle multiple $Show blocks with different feature flags', async () => { + vi.mocked(isFeatureEnabled).mockImplementation((feature) => { + if (feature === ('enabled-feature' as Feature)) return true + return false + }) + + const markdown = ` +# Test content + +<$Show if="enabled-feature"> +This should be visible. + + +<$Show if="disabled-feature"> +This should be hidden. + + +<$Show if="another-disabled-feature"> +This should also be hidden. + + +Final content. +`.trim() + + const mdast = fromDocsMarkdown(markdown) + const transformed = showRemark()(mdast) + const output = toMarkdown(transformed, { extensions: [mdxToMarkdown()] }) + + const expected = ` +# Test content + +This should be visible. + +Final content. +`.trimStart() + + expect(output).toEqual(expected) + expect(isFeatureEnabled).toHaveBeenCalledWith('enabled-feature') + expect(isFeatureEnabled).toHaveBeenCalledWith('disabled-feature') + expect(isFeatureEnabled).toHaveBeenCalledWith('another-disabled-feature') + }) + + it('should handle nested $Show blocks correctly', async () => { + vi.mocked(isFeatureEnabled).mockImplementation((feature) => { + if (feature === ('outer-feature' as Feature)) return true + return false + }) + + const markdown = ` +# Test content + +<$Show if="outer-feature"> +Outer content visible. + +<$Show if="inner-feature"> +Inner content should be hidden. + + +More outer content. + +`.trim() + + const mdast = fromDocsMarkdown(markdown) + const transformed = showRemark()(mdast) + const output = toMarkdown(transformed, { extensions: [mdxToMarkdown()] }) + + const expected = ` +# Test content + +Outer content visible. + +More outer content. +`.trimStart() + + expect(output).toEqual(expected) + expect(isFeatureEnabled).toHaveBeenCalledWith('outer-feature') + expect(isFeatureEnabled).toHaveBeenCalledWith('inner-feature') + }) + + it('should throw error when "if" attribute is missing', () => { + const markdown = ` +<$Show> +Content without if attribute. + +`.trim() + + const mdast = fromDocsMarkdown(markdown) + + expect(() => { + showRemark()(mdast) + }).toThrow('$Show directive requires a string value for the "if" attribute') + }) + + it('should throw error when "if" attribute is not a string', () => { + const markdown = ` +<$Show if={true}> +Content with non-string if attribute. + +`.trim() + + const mdast = fromDocsMarkdown(markdown) + + expect(() => { + showRemark()(mdast) + }).toThrow('$Show directive requires a string value for the "if" attribute') + }) +}) diff --git a/apps/docs/features/directives/Show.ts b/apps/docs/features/directives/Show.ts new file mode 100644 index 00000000000..94401222466 --- /dev/null +++ b/apps/docs/features/directives/Show.ts @@ -0,0 +1,72 @@ +/** + * The Show directive supports conditional rendering of content based on feature flags. + * It uses the isFeatureEnabled function from the common package to determine whether + * the content should be displayed. + * + * If the feature is enabled, the <$Show> tag is stripped but its children remain. + * If the feature is disabled, both the <$Show> tag and its children are removed. + * + * ## Examples + * + * ### Basic usage + * + * ```mdx + * <$Show if="feature-name"> + * This content will only show if the feature is enabled. + * + * ``` + */ + +import { type Root } from 'mdast' +import type { MdxJsxFlowElement } from 'mdast-util-mdx-jsx' +import { type Parent } from 'unist' +import { visitParents } from 'unist-util-visit-parents' + +import { isFeatureEnabled, type Feature } from 'common/enabled-features' +import { getAttributeValue } from './utils.server' + +export function showRemark() { + return function transform(tree: Root) { + const nodesToProcess = [] as Array<{ + node: MdxJsxFlowElement + parent: Parent + shouldShow: boolean + }> + + // Collect all $Show nodes and determine their visibility + visitParents(tree, 'mdxJsxFlowElement', (node: MdxJsxFlowElement, ancestors) => { + if (node.name !== '$Show') return + + const parent = ancestors[ancestors.length - 1] + const featureName = getAttributeValue(node, 'if') + + if (typeof featureName !== 'string') { + throw new Error('$Show directive requires a string value for the "if" attribute') + } + + const shouldShow = isFeatureEnabled(featureName as Feature) + + nodesToProcess.push({ + node, + parent, + shouldShow, + }) + }) + + // Process nodes in reverse order to avoid index shifting issues + for (let i = nodesToProcess.length - 1; i >= 0; i--) { + const { node, parent, shouldShow } = nodesToProcess[i] + const nodeIndex = parent.children.indexOf(node) + + if (shouldShow) { + // Feature is enabled: remove the $Show wrapper but keep children + parent.children.splice(nodeIndex, 1, ...node.children) + } else { + // Feature is disabled: remove the entire $Show element and its children + parent.children.splice(nodeIndex, 1) + } + } + + return tree + } +} diff --git a/apps/docs/features/directives/utils.ts b/apps/docs/features/directives/utils.ts index 0138a0b9e66..225b54bfb01 100644 --- a/apps/docs/features/directives/utils.ts +++ b/apps/docs/features/directives/utils.ts @@ -10,6 +10,7 @@ import { codeSampleRemark } from './CodeSample' import { codeTabsRemark } from './CodeTabs' import { fromDocsMarkdown } from './utils.server' import { partialsRemark } from './Partial' +import { showRemark } from './Show' type Transformer = (ast: Root) => Root | Promise @@ -27,6 +28,7 @@ export async function preprocessMdx(mdx: string, transformers: Transformer[]) export function preprocessMdxWithDefaults(mdx: string) { return preprocessMdx(mdx, [ + showRemark(), remarkMkDocsAdmonition(), remarkPyMdownTabs(), partialsRemark(), diff --git a/apps/docs/features/docs/MdxBase.tsx b/apps/docs/features/docs/MdxBase.tsx index 8e77bf230e4..672a49b04ab 100644 --- a/apps/docs/features/docs/MdxBase.tsx +++ b/apps/docs/features/docs/MdxBase.tsx @@ -4,6 +4,7 @@ import rehypeKatex from 'rehype-katex' import remarkGfm from 'remark-gfm' import remarkMath from 'remark-math' +import { isFeatureEnabled } from 'common' import { preprocessMdxWithDefaults } from '~/features/directives/utils' import { components } from '~/features/docs/MdxBase.shared' import { SerializeOptions } from '~/types/next-mdx-remote-serialize' @@ -39,6 +40,7 @@ const MDXRemoteBase = async ({ } = mdxOptions const finalOptions = { + scope: { isFeatureEnabled }, ...mdxOptions, ...otherOptions, mdxOptions: { diff --git a/apps/docs/public/humans.txt b/apps/docs/public/humans.txt index 6d07b1a9a31..b5041267044 100644 --- a/apps/docs/public/humans.txt +++ b/apps/docs/public/humans.txt @@ -31,6 +31,7 @@ Chris Martin Chris Stockton Chris Ward Craig Cannon +Cuong Do Danny White Darren Cunningham Dave Wilson @@ -111,6 +112,7 @@ Riccardo Busetti Rodrigo Mansueli Ronan Lehane Rory Wilding +Sam Meech-Ward Sam Rose Sean Oliver Sergio Cioban Filho diff --git a/apps/docs/spec/common-api-sections.json b/apps/docs/spec/common-api-sections.json index 733b6ffb814..0e81bfb4e94 100644 --- a/apps/docs/spec/common-api-sections.json +++ b/apps/docs/spec/common-api-sections.json @@ -196,12 +196,6 @@ "slug": "v1-apply-a-migration", "type": "operation" }, - { - "id": "v1-create-restore-point", - "title": "Create restore point", - "slug": "v1-create-restore-point", - "type": "operation" - }, { "id": "v1-disable-readonly-mode-temporarily", "title": "Disable readonly mode temporarily", @@ -256,12 +250,6 @@ "slug": "v1-get-readonly-mode-status", "type": "operation" }, - { - "id": "v1-get-restore-point", - "title": "Get restore point", - "slug": "v1-get-restore-point", - "type": "operation" - }, { "id": "v1-get-ssl-enforcement-config", "title": "Get ssl enforcement config", @@ -310,12 +298,6 @@ "slug": "v1-setup-a-read-replica", "type": "operation" }, - { - "id": "v1-undo", - "title": "Undo", - "slug": "v1-undo", - "type": "operation" - }, { "id": "v1-update-pooler-config", "title": "Update pooler config", @@ -538,12 +520,6 @@ "slug": "v1-exchange-oauth-token", "type": "operation" }, - { - "id": "v1-oauth-authorize-project-claim", - "title": "Oauth authorize project claim", - "slug": "v1-oauth-authorize-project-claim", - "type": "operation" - }, { "id": "v1-revoke-token", "title": "Revoke token", @@ -556,12 +532,6 @@ "type": "category", "title": "Organizations", "items": [ - { - "id": "v1-claim-project-for-organization", - "title": "Claim project for organization", - "slug": "v1-claim-project-for-organization", - "type": "operation" - }, { "id": "v1-create-an-organization", "title": "Create an organization", @@ -574,12 +544,6 @@ "slug": "v1-get-an-organization", "type": "operation" }, - { - "id": "v1-get-organization-project-claim", - "title": "Get organization project claim", - "slug": "v1-get-organization-project-claim", - "type": "operation" - }, { "id": "v1-list-all-organizations", "title": "List all organizations", @@ -610,12 +574,6 @@ "slug": "v1-create-a-project", "type": "operation" }, - { - "id": "v1-create-project-claim-token", - "title": "Create project claim token", - "slug": "v1-create-project-claim-token", - "type": "operation" - }, { "id": "v1-delete-a-project", "title": "Delete a project", @@ -628,12 +586,6 @@ "slug": "v1-delete-network-bans", "type": "operation" }, - { - "id": "v1-delete-project-claim-token", - "title": "Delete project claim token", - "slug": "v1-delete-project-claim-token", - "type": "operation" - }, { "id": "v1-get-network-restrictions", "title": "Get network restrictions", @@ -658,12 +610,6 @@ "slug": "v1-get-project", "type": "operation" }, - { - "id": "v1-get-project-claim-token", - "title": "Get project claim token", - "slug": "v1-get-project-claim-token", - "type": "operation" - }, { "id": "v1-get-services-health", "title": "Get services health", diff --git a/apps/docs/spec/sections/generateMgmtApiSections.cts b/apps/docs/spec/sections/generateMgmtApiSections.cts index 580acb24866..c235b7eccf2 100644 --- a/apps/docs/spec/sections/generateMgmtApiSections.cts +++ b/apps/docs/spec/sections/generateMgmtApiSections.cts @@ -41,6 +41,12 @@ function extractSectionsFromOpenApi(filePath, outputPath) { for (const route in openApiJson.paths) { const methods = openApiJson.paths[route] for (const method in methods) { + // We are using `x-internal` to hide endpoints from the docs, + // but still have them included in the spec so they generate types and can be used. + if (methods[method]['x-internal']) { + continue + } + const tag = methods[method].tags?.[0] const operationId = methods[method].operationId // If operationId is not in the form of a slug ignore it. diff --git a/apps/docs/spec/supabase_dart_v2.yml b/apps/docs/spec/supabase_dart_v2.yml index 2a1dd1b2d4e..6219bea93b7 100644 --- a/apps/docs/spec/supabase_dart_v2.yml +++ b/apps/docs/spec/supabase_dart_v2.yml @@ -1341,7 +1341,7 @@ functions: isSpotlight: true code: | ```dart - // retrieve all identites linked to a user + // retrieve all identities linked to a user final identities = await supabase.auth.getUserIdentities(); // find the google identity diff --git a/apps/docs/spec/supabase_js_v2.yml b/apps/docs/spec/supabase_js_v2.yml index dcaf0903309..2e6c387f8d8 100644 --- a/apps/docs/spec/supabase_js_v2.yml +++ b/apps/docs/spec/supabase_js_v2.yml @@ -1625,7 +1625,7 @@ functions: isSpotlight: true code: | ```js - // retrieve all identites linked to a user + // retrieve all identities linked to a user const identities = await supabase.auth.getUserIdentities() // find the google identity diff --git a/apps/docs/spec/supabase_py_v2.yml b/apps/docs/spec/supabase_py_v2.yml index 661cf1e884e..31f21f04011 100644 --- a/apps/docs/spec/supabase_py_v2.yml +++ b/apps/docs/spec/supabase_py_v2.yml @@ -1550,7 +1550,7 @@ functions: isSpotlight: true code: | ```python - # retrieve all identites linked to a user + # retrieve all identities linked to a user response = supabase.auth.get_user_identities() # find the google identity diff --git a/apps/docs/spec/supabase_swift_v2.yml b/apps/docs/spec/supabase_swift_v2.yml index 37e8587aabd..9970a629f12 100644 --- a/apps/docs/spec/supabase_swift_v2.yml +++ b/apps/docs/spec/supabase_swift_v2.yml @@ -944,7 +944,7 @@ functions: isSpotlight: true code: | ```swift - // retrieve all identites linked to a user + // retrieve all identities linked to a user let identities = try await supabase.auth.userIdentities() // find the google identity diff --git a/apps/studio/.env b/apps/studio/.env index addb3f02120..aa475c4b298 100644 --- a/apps/studio/.env +++ b/apps/studio/.env @@ -21,7 +21,7 @@ NEXT_PUBLIC_GOTRUE_URL=$SUPABASE_PUBLIC_URL/auth/v1 NEXT_PUBLIC_HCAPTCHA_SITE_KEY=10000000-ffff-ffff-ffff-000000000001 # CmdK / AI -NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InhndWloeHV6cWlid3hqbmlteGV2Iiwicm9sZSI6ImFub24iLCJpYXQiOjE2NzUwOTQ4MzUsImV4cCI6MTk5MDY3MDgzNX0.0PMlOxtKL4O9GGZuAP_Xl4f-Tut1qOnW4bNEmAtoB8w +NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InhndWloeHV6cWlid3hqbmlteGV2Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3MTgzNzc1MTgsImV4cCI6MjAzMzk1MzUxOH0.aIqjQ9V7djMxYit-DT1fYNV_VWMHSqldh_18XfX2_BE NEXT_PUBLIC_SUPABASE_URL=https://xguihxuzqibwxjnimxev.supabase.co DOCKER_SOCKET_LOCATION=/var/run/docker.sock diff --git a/apps/studio/components/grid/SupabaseGrid.tsx b/apps/studio/components/grid/SupabaseGrid.tsx index 3129c172d0e..3bf86dd9ee2 100644 --- a/apps/studio/components/grid/SupabaseGrid.tsx +++ b/apps/studio/components/grid/SupabaseGrid.tsx @@ -13,9 +13,9 @@ import { useTableEditorStateSnapshot } from 'state/table-editor' import { useTableEditorTableStateSnapshot } from 'state/table-editor-table' import { Shortcuts } from './components/common/Shortcuts' -import Footer from './components/footer/Footer' +import { Footer } from './components/footer/Footer' import { Grid } from './components/grid/Grid' -import Header, { HeaderProps } from './components/header/Header' +import { Header, HeaderProps } from './components/header/Header' import { RowContextMenu } from './components/menu' import { GridProps } from './types' @@ -84,7 +84,7 @@ export const SupabaseGrid = ({ return (
    -
    +
    {children || ( <> @@ -99,7 +99,7 @@ export const SupabaseGrid = ({ filters={filters} onApplyFilters={onApplyFilters} /> -
    +
    )} diff --git a/apps/studio/components/grid/components/footer/Footer.tsx b/apps/studio/components/grid/components/footer/Footer.tsx index f4680d0acac..a6281989b9c 100644 --- a/apps/studio/components/grid/components/footer/Footer.tsx +++ b/apps/studio/components/grid/components/footer/Footer.tsx @@ -5,14 +5,9 @@ import { useTableEditorQuery } from 'data/table-editor/table-editor-query' import { isTableLike, isViewLike } from 'data/table-editor/table-editor-types' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { useUrlState } from 'hooks/ui/useUrlState' -import RefreshButton from '../header/RefreshButton' import { Pagination } from './pagination' -export interface FooterProps { - isRefetching?: boolean -} - -const Footer = ({ isRefetching }: FooterProps) => { +export const Footer = () => { const { id: _id } = useParams() const id = _id ? Number(_id) : undefined const { data: project } = useSelectedProjectQuery() @@ -41,10 +36,6 @@ const Footer = ({ isRefetching }: FooterProps) => { {selectedView === 'data' && }
    - {entity && selectedView === 'data' && ( - - )} - {(isViewSelected || isTableSelected) && ( { ) } - -export default Footer diff --git a/apps/studio/components/grid/components/header/Header.tsx b/apps/studio/components/grid/components/header/Header.tsx index fde164a247c..821a359c06d 100644 --- a/apps/studio/components/grid/components/header/Header.tsx +++ b/apps/studio/components/grid/components/header/Header.tsx @@ -8,7 +8,7 @@ import { toast } from 'sonner' import { useParams } from 'common' import { useTableFilter } from 'components/grid/hooks/useTableFilter' import { useTableSort } from 'components/grid/hooks/useTableSort' -import GridHeaderActions from 'components/interfaces/TableGridEditor/GridHeaderActions' +import { GridHeaderActions } from 'components/interfaces/TableGridEditor/GridHeaderActions' import { formatTableRowsToSQL } from 'components/interfaces/TableGridEditor/TableEntity.utils' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { useTableRowsCountQuery } from 'data/table-rows/table-rows-count-query' @@ -56,9 +56,10 @@ export const MAX_EXPORT_ROW_COUNT_MESSAGE = ( export type HeaderProps = { customHeader: ReactNode + isRefetching: boolean } -const Header = ({ customHeader }: HeaderProps) => { +export const Header = ({ customHeader, isRefetching }: HeaderProps) => { const snap = useTableEditorTableStateSnapshot() return ( @@ -71,14 +72,12 @@ const Header = ({ customHeader }: HeaderProps) => { ) : ( )} - +
    ) } -export default Header - const DefaultHeader = () => { const { ref: projectRef } = useParams() const { data: org } = useSelectedOrganizationQuery() diff --git a/apps/studio/components/grid/components/header/RefreshButton.tsx b/apps/studio/components/grid/components/header/RefreshButton.tsx index 1020e419b02..91c611c8c9a 100644 --- a/apps/studio/components/grid/components/header/RefreshButton.tsx +++ b/apps/studio/components/grid/components/header/RefreshButton.tsx @@ -2,15 +2,15 @@ import { useQueryClient } from '@tanstack/react-query' import { RefreshCw } from 'lucide-react' import { useParams } from 'common' +import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { tableRowKeys } from 'data/table-rows/keys' -import { Button } from 'ui' export type RefreshButtonProps = { tableId?: number isRefetching?: boolean } -const RefreshButton = ({ tableId, isRefetching }: RefreshButtonProps) => { +export const RefreshButton = ({ tableId, isRefetching }: RefreshButtonProps) => { const { ref } = useParams() const queryClient = useQueryClient() const queryKey = tableRowKeys.tableRowsAndCount(ref, tableId) @@ -20,14 +20,18 @@ const RefreshButton = ({ tableId, isRefetching }: RefreshButtonProps) => { } return ( - + className="w-7 h-7 p-0" + tooltip={{ + content: { + side: 'bottom', + text: 'Refresh table data', + }, + }} + /> ) } -export default RefreshButton diff --git a/apps/studio/components/interfaces/Auth/Users/UsersV2.tsx b/apps/studio/components/interfaces/Auth/Users/UsersV2.tsx index 0ead1d0a200..49c683f70b2 100644 --- a/apps/studio/components/interfaces/Auth/Users/UsersV2.tsx +++ b/apps/studio/components/interfaces/Auth/Users/UsersV2.tsx @@ -8,7 +8,7 @@ import { toast } from 'sonner' import { LOCAL_STORAGE_KEYS, useParams } from 'common' import { useIsAPIDocsSidePanelEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' import AlertError from 'components/ui/AlertError' -import APIDocsButton from 'components/ui/APIDocsButton' +import { APIDocsButton } from 'components/ui/APIDocsButton' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { FilterPopover } from 'components/ui/FilterPopover' import { FormHeader } from 'components/ui/Forms/FormHeader' diff --git a/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx b/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx index 7bfc6942c37..9ffa0387be7 100644 --- a/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx +++ b/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx @@ -1,6 +1,6 @@ import { zodResolver } from '@hookform/resolvers/zod' import { useQueryClient } from '@tanstack/react-query' -import { DollarSign, GitMerge, Github, Loader2 } from 'lucide-react' +import { DatabaseZap, DollarSign, GitMerge, Github, Loader2 } from 'lucide-react' import Image from 'next/image' import Link from 'next/link' import { useRouter } from 'next/router' @@ -21,6 +21,7 @@ import { useBranchCreateMutation } from 'data/branches/branch-create-mutation' import { useBranchesQuery } from 'data/branches/branches-query' import { useCheckGithubBranchValidity } from 'data/integrations/github-branch-check-query' import { useGitHubConnectionsQuery } from 'data/integrations/github-connections-query' +import { useCloneBackupsQuery } from 'data/projects/clone-query' import { projectKeys } from 'data/projects/keys' import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' @@ -45,6 +46,9 @@ import { Input_Shadcn_, Label_Shadcn_ as Label, Switch, + Tooltip, + TooltipContent, + TooltipTrigger, cn, } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' @@ -59,6 +63,9 @@ export const CreateBranchModal = () => { const gitlessBranching = useIsBranching2Enabled() const allowDataBranching = useFlag('allowDataBranching') + // [Joshen] This is meant to be short lived while we're figuring out how to control + // requests to this endpoint. Kill switch in case we need to stop the requests + const disableBackupsCheck = useFlag('disableBackupsCheckInCreatebranchmodal') const isProPlanAndUp = selectedOrg?.plan?.id !== 'free' const promptProPlanUpgrade = IS_PLATFORM && !isProPlanAndUp @@ -85,6 +92,17 @@ export const CreateBranchModal = () => { useCheckGithubBranchValidity({ onError: () => {}, }) + const { data: cloneBackups, error: cloneBackupsError } = useCloneBackupsQuery( + { projectRef }, + { + // [Joshen] Only trigger this request when the modal is opened + enabled: showCreateBranchModal && !disableBackupsCheck, + } + ) + const targetVolumeSizeGb = cloneBackups?.target_volume_size_gb ?? 0 + const noPhysicalBackups = cloneBackupsError?.message.startsWith( + 'Physical backups need to be enabled' + ) const { mutate: sendEvent } = useSendEventMutation() @@ -172,6 +190,7 @@ export const CreateBranchModal = () => { resolver: zodResolver(FormSchema), defaultValues: { branchName: '', gitBranchName: '', withData: false }, }) + const withData = form.watch('withData') const canSubmit = !isCreating && !isChecking const isDisabled = @@ -344,9 +363,22 @@ export const CreateBranchModal = () => { layout="flex-row-reverse" description="Clone production data into this branch" > - - - + + + + + + + {!disableBackupsCheck && noPhysicalBackups && ( + + PITR is required for the project to clone data into the branch + + )} + )} /> @@ -362,6 +394,30 @@ export const CreateBranchModal = () => { promptProPlanUpgrade && 'opacity-25 pointer-events-none' )} > + {withData && ( +
    +
    +
    + +
    +
    +
    +

    + Data branch takes longer time to create +

    +

    + Since your target database volume size is{' '} + {targetVolumeSizeGb} GB, creating a + data branch is estimated to take around{' '} + + {Math.round((720 / 21000) * targetVolumeSizeGb) + 3} minutes + + . +

    +
    +
    + )} + {githubConnection && (
    diff --git a/apps/studio/components/interfaces/Database/Extensions/EnableExtensionModal.tsx b/apps/studio/components/interfaces/Database/Extensions/EnableExtensionModal.tsx index 6a0595903cb..91414881c7f 100644 --- a/apps/studio/components/interfaces/Database/Extensions/EnableExtensionModal.tsx +++ b/apps/studio/components/interfaces/Database/Extensions/EnableExtensionModal.tsx @@ -9,6 +9,7 @@ import { useDatabaseExtensionEnableMutation } from 'data/database-extensions/dat import { useSchemasQuery } from 'data/database/schemas-query' import { executeSql } from 'data/sql/execute-sql-query' import { useIsOrioleDb, useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' +import { useProtectedSchemas } from 'hooks/useProtectedSchemas' import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, @@ -43,6 +44,7 @@ const EnableExtensionModal = ({ visible, extension, onCancel }: EnableExtensionM }, { enabled: visible } ) + const { data: protectedSchemas } = useProtectedSchemas({ excludeSchemas: ['extensions'] }) const { mutate: enableExtension, isLoading: isEnabling } = useDatabaseExtensionEnableMutation({ onSuccess: () => { toast.success(`Extension "${extension.name}" is now enabled`) @@ -180,19 +182,26 @@ const EnableExtensionModal = ({ visible, extension, onCancel }: EnableExtensionM Create a new schema "{extension.name}" - {schemas?.map((schema) => { - return ( - } - > - {schema.name} - + {schemas + ?.filter( + (schema) => + !protectedSchemas.some( + (protectedSchema) => protectedSchema.name === schema.name + ) ) - })} + .map((schema) => { + return ( + } + > + {schema.name} + + ) + })} )} diff --git a/apps/studio/components/interfaces/Database/Extensions/ExtensionRow.tsx b/apps/studio/components/interfaces/Database/Extensions/ExtensionRow.tsx index 17e78b1368f..45e3985ef83 100644 --- a/apps/studio/components/interfaces/Database/Extensions/ExtensionRow.tsx +++ b/apps/studio/components/interfaces/Database/Extensions/ExtensionRow.tsx @@ -193,6 +193,7 @@ export const ExtensionRow = ({ extension }: ExtensionRowProps) => { confirmLabel="Disable" variant="destructive" confirmLabelLoading="Disabling" + loading={isDisabling} onCancel={() => setIsDisableModalOpen(false)} onConfirm={() => onConfirmDisable()} > diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationPanel.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationPanel.tsx index 68199c42788..2d4aea160f4 100644 --- a/apps/studio/components/interfaces/Database/Replication/DestinationPanel.tsx +++ b/apps/studio/components/interfaces/Database/Replication/DestinationPanel.tsx @@ -74,6 +74,7 @@ interface DestinationPanelProps { destinationId: number pipelineId?: number enabled: boolean + statusName?: string } } @@ -178,7 +179,8 @@ export const DestinationPanel = ({ sourceId, }) // Set request status only right before starting, then fire and close - const snapshot = existingDestination.enabled ? 'started' : 'stopped' + const snapshot = + existingDestination.statusName ?? (existingDestination.enabled ? 'started' : 'stopped') if (existingDestination.enabled) { setRequestStatus( existingDestination.pipelineId, @@ -244,6 +246,13 @@ export const DestinationPanel = ({ } }, [destinationData, pipelineData, editMode, defaultValues, form]) + // Ensure the form always reflects the freshest data whenever the panel opens + useEffect(() => { + if (visible) { + form.reset(defaultValues) + } + }, [visible, defaultValues, form]) + return sourceId ? ( <> diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationRow.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationRow.tsx index 16437c30b5b..28da065f287 100644 --- a/apps/studio/components/interfaces/Database/Replication/DestinationRow.tsx +++ b/apps/studio/components/interfaces/Database/Replication/DestinationRow.tsx @@ -125,7 +125,20 @@ export const DestinationRow = ({ )} {isPipelineSuccess && ( - {isPipelineLoading ? : destinationName} + + {isPipelineLoading ? ( + + ) : pipeline?.id ? ( + + + {destinationName} + + Pipeline ID: {pipeline.id} + + ) : ( + destinationName + )} + {isPipelineLoading ? : type} {isPipelineLoading || !pipeline ? ( @@ -138,6 +151,7 @@ export const DestinationRow = ({ isError={isPipelineStatusError} isSuccess={isPipelineStatusSuccess} requestStatus={requestStatus} + pipelineId={pipeline?.id} /> )} @@ -195,7 +209,9 @@ export const DestinationRow = ({ sourceId, destinationId: destinationId, pipelineId: pipeline?.id, - enabled: statusName === PipelineStatusName.STARTED, + enabled: + statusName === PipelineStatusName.STARTED || statusName === PipelineStatusName.FAILED, + statusName, }} /> diff --git a/apps/studio/components/interfaces/Database/Replication/Pipeline.utils.ts b/apps/studio/components/interfaces/Database/Replication/Pipeline.utils.ts index eb69b88e689..8a4733175b7 100644 --- a/apps/studio/components/interfaces/Database/Replication/Pipeline.utils.ts +++ b/apps/studio/components/interfaces/Database/Replication/Pipeline.utils.ts @@ -1,5 +1,6 @@ import { ReplicationPipelineStatusData } from 'data/replication/pipeline-status-query' import { PipelineStatusRequestStatus } from 'state/replication-pipeline-request-status' +import { PipelineStatusName } from './PipelineStatus' export const PIPELINE_ERROR_MESSAGES = { RETRIEVE_PIPELINE: 'Failed to retrieve pipeline information', @@ -24,7 +25,7 @@ export const getStatusName = ( export const PIPELINE_ENABLE_ALLOWED_FROM = ['stopped'] as const export const PIPELINE_DISABLE_ALLOWED_FROM = ['started', 'failed'] as const -export const PIPELINE_ACTIONABLE_STATES = ['failed', 'started', 'stopped'] as const +export const PIPELINE_ACTIONABLE_STATES = ['failed', 'started', 'stopped'] as PipelineStatusName[] const PIPELINE_STATE_MESSAGES = { enabling: { diff --git a/apps/studio/components/interfaces/Database/Replication/PipelineStatus.tsx b/apps/studio/components/interfaces/Database/Replication/PipelineStatus.tsx index 14e8b9a9969..e0804c42dfa 100644 --- a/apps/studio/components/interfaces/Database/Replication/PipelineStatus.tsx +++ b/apps/studio/components/interfaces/Database/Replication/PipelineStatus.tsx @@ -1,7 +1,8 @@ +import { AlertTriangle, Loader2 } from 'lucide-react' + import { useParams } from 'common' import { InlineLink } from 'components/ui/InlineLink' import { ReplicationPipelineStatusData } from 'data/replication/pipeline-status-query' -import { AlertTriangle, Loader2 } from 'lucide-react' import { PipelineStatusRequestStatus } from 'state/replication-pipeline-request-status' import { ResponseError } from 'types' import { cn, Tooltip, TooltipContent, TooltipTrigger, WarningIcon } from 'ui' @@ -23,6 +24,7 @@ interface PipelineStatusProps { isError: boolean isSuccess: boolean requestStatus?: PipelineStatusRequestStatus + pipelineId?: number } export const PipelineStatus = ({ @@ -32,6 +34,7 @@ export const PipelineStatus = ({ isError, isSuccess, requestStatus, + pipelineId, }: PipelineStatusProps) => { const { ref } = useParams() @@ -129,6 +132,12 @@ export const PipelineStatus = ({ const statusConfig = getStatusConfig() + const pipelineLogsUrl = pipelineId + ? `/project/${ref}/logs/etl-replication-logs?f=${encodeURIComponent( + JSON.stringify({ pipeline_id: pipelineId }) + )}` + : `/project/${ref}/logs/etl-replication-logs` + return ( <> {isLoading && } @@ -155,9 +164,7 @@ export const PipelineStatus = ({ {['unknown', 'failed'].includes(pipelineStatus?.name ?? '') && ( <> {' '} - Check the{' '} - logs for - more information. + Check the logs for more information. )} diff --git a/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus.tsx b/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus.tsx index c3e4ca8117e..0bf7a556003 100644 --- a/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus.tsx +++ b/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus.tsx @@ -1,4 +1,14 @@ -import { Activity, ChevronLeft, ExternalLink, Search, X } from 'lucide-react' +import { + Activity, + Ban, + ChevronLeft, + ExternalLink, + Pause, + Play, + RotateCcw, + Search, + X, +} from 'lucide-react' import Link from 'next/link' import { useEffect, useState } from 'react' import { toast } from 'sonner' @@ -22,12 +32,10 @@ import { Input } from 'ui-patterns/DataInputs/Input' import { ErroredTableDetails } from './ErroredTableDetails' import { PIPELINE_ACTIONABLE_STATES, - PIPELINE_DISABLE_ALLOWED_FROM, - PIPELINE_ENABLE_ALLOWED_FROM, PIPELINE_ERROR_MESSAGES, getStatusName, } from './Pipeline.utils' -import { PipelineStatus } from './PipelineStatus' +import { PipelineStatus, PipelineStatusName } from './PipelineStatus' import { STATUS_REFRESH_FREQUENCY_MS } from './Replication.constants' import { TableState } from './ReplicationPipelineStatus.types' import { getDisabledStateConfig, getStatusConfig } from './ReplicationPipelineStatus.utils' @@ -104,21 +112,44 @@ export const ReplicationPipelineStatus = () => { requestStatus === PipelineStatusRequestStatus.RestartRequested const showDisabledState = !isPipelineRunning || isEnablingDisabling - const onTogglePipeline = async () => { - if (!projectRef) { - return console.error('Project ref is required') - } - if (!pipeline) { - return toast.error(PIPELINE_ERROR_MESSAGES.NO_PIPELINE_FOUND) - } + const logsUrl = `/project/${projectRef}/logs/etl-replication-logs${ + pipelineId ? `?f=${encodeURIComponent(JSON.stringify({ pipeline_id: pipelineId }))}` : '' + }` + + const label = + statusName === 'stopped' + ? 'Start' + : statusName === 'started' + ? 'Stop' + : statusName === 'failed' + ? 'Restart' + : 'Action unavailable' + + const icon = + statusName === 'stopped' ? ( + + ) : statusName === 'started' ? ( + + ) : statusName === 'failed' ? ( + + ) : ( + + ) + + const onPrimaryAction = async () => { + if (!projectRef) return console.error('Project ref is required') + if (!pipeline) return toast.error(PIPELINE_ERROR_MESSAGES.NO_PIPELINE_FOUND) try { - if (PIPELINE_ENABLE_ALLOWED_FROM.includes(statusName as any)) { + if (statusName === 'stopped') { setRequestStatus(pipeline.id, PipelineStatusRequestStatus.StartRequested, statusName) await startPipeline({ projectRef, pipelineId: pipeline.id }) - } else if (PIPELINE_DISABLE_ALLOWED_FROM.includes(statusName as any)) { + } else if (statusName === 'started') { setRequestStatus(pipeline.id, PipelineStatusRequestStatus.StopRequested, statusName) await stopPipeline({ projectRef, pipelineId: pipeline.id }) + } else if (statusName === 'failed') { + setRequestStatus(pipeline.id, PipelineStatusRequestStatus.RestartRequested, statusName) + await startPipeline({ projectRef, pipelineId: pipeline.id }) } } catch (error) { toast.error(PIPELINE_ERROR_MESSAGES.ENABLE_DESTINATION) @@ -136,57 +167,57 @@ export const ReplicationPipelineStatus = () => { -
    -
    -

    {destinationName || 'Pipeline'}

    - -
    +
    +

    {destinationName || 'Pipeline'}

    +
    -
    - - setFilterString(e.target.value)} - actions={ - filterString.length > 0 - ? [ - setFilterString('')} - />, - ] - : undefined - } - /> -
    + } + className="pl-7 h-[26px] text-xs" + placeholder="Search for tables" + value={filterString} + disabled={isPipelineError} + onChange={(e) => setFilterString(e.target.value)} + actions={ + filterString.length > 0 + ? [ + setFilterString('')} + />, + ] + : undefined + } + /> + +
    @@ -231,6 +262,7 @@ export const ReplicationPipelineStatus = () => { )}
    + {/* [Joshen] Should update to use new Table components next time */} Table, diff --git a/apps/studio/components/interfaces/Database/Replication/RowMenu.tsx b/apps/studio/components/interfaces/Database/Replication/RowMenu.tsx index 7e083058ed5..203aa40bae6 100644 --- a/apps/studio/components/interfaces/Database/Replication/RowMenu.tsx +++ b/apps/studio/components/interfaces/Database/Replication/RowMenu.tsx @@ -1,8 +1,9 @@ -import { Edit, MoreVertical, Pause, Play, Trash } from 'lucide-react' +import { Edit, MoreVertical, Pause, Play, RotateCcw, Trash } from 'lucide-react' import { toast } from 'sonner' import { useParams } from 'common' import AlertError from 'components/ui/AlertError' +import { ReplicationPipelineStatusData } from 'data/replication/pipeline-status-query' import { Pipeline } from 'data/replication/pipelines-query' import { useStartPipelineMutation } from 'data/replication/start-pipeline-mutation' import { useStopPipelineMutation } from 'data/replication/stop-pipeline-mutation' @@ -21,6 +22,7 @@ import { } from 'ui' import ShimmeringLoader from 'ui-patterns/ShimmeringLoader' import { + PIPELINE_ACTIONABLE_STATES, PIPELINE_DISABLE_ALLOWED_FROM, PIPELINE_ENABLE_ALLOWED_FROM, PIPELINE_ERROR_MESSAGES, @@ -30,7 +32,7 @@ import { PipelineStatusName } from './PipelineStatus' interface RowMenuProps { pipeline: Pipeline | undefined - pipelineStatus: any + pipelineStatus?: ReplicationPipelineStatusData['status'] error: ResponseError | null isLoading: boolean isError: boolean @@ -48,21 +50,42 @@ export const RowMenu = ({ onDeleteClick, }: RowMenuProps) => { const { ref: projectRef } = useParams() - const statusName = getStatusName(pipelineStatus) - const pipelineEnabled = statusName !== PipelineStatusName.STOPPED const { mutateAsync: startPipeline } = useStartPipelineMutation() const { mutateAsync: stopPipeline } = useStopPipelineMutation() - const { setRequestStatus: setGlobalRequestStatus } = usePipelineRequestStatus() + const { getRequestStatus, setRequestStatus: setGlobalRequestStatus } = usePipelineRequestStatus() + const requestStatus = pipeline?.id + ? getRequestStatus(pipeline.id) + : PipelineStatusRequestStatus.None + + const hasPipelineAction = + requestStatus === PipelineStatusRequestStatus.None && + [PipelineStatusName.STOPPED, PipelineStatusName.STARTED, PipelineStatusName.FAILED].includes( + statusName as PipelineStatusName + ) + + const pipelineActionIcon = + statusName === PipelineStatusName.STOPPED ? ( + + ) : statusName === PipelineStatusName.STARTED ? ( + + ) : statusName === PipelineStatusName.FAILED ? ( + + ) : null + + const pipelineActionLabel = + statusName === PipelineStatusName.STOPPED + ? 'Start pipeline' + : statusName === PipelineStatusName.STARTED + ? 'Stop pipeline' + : statusName === PipelineStatusName.FAILED + ? 'Restart pipeline' + : null const onEnablePipeline = async () => { - if (!projectRef) { - return console.error('Project ref is required') - } - if (!pipeline) { - return toast.error(PIPELINE_ERROR_MESSAGES.NO_PIPELINE_FOUND) - } + if (!projectRef) return console.error('Project ref is required') + if (!pipeline) return toast.error(PIPELINE_ERROR_MESSAGES.NO_PIPELINE_FOUND) try { // Only show 'enabling' when transitioning from allowed states @@ -77,14 +100,8 @@ export const RowMenu = ({ } const onDisablePipeline = async () => { - if (!projectRef) { - console.error('Project ref is required') - return - } - if (!pipeline) { - toast.error(PIPELINE_ERROR_MESSAGES.NO_PIPELINE_FOUND) - return - } + if (!projectRef) return console.error('Project ref is required') + if (!pipeline) return toast.error(PIPELINE_ERROR_MESSAGES.NO_PIPELINE_FOUND) try { // Only show 'disabling' when transitioning from allowed states @@ -98,9 +115,23 @@ export const RowMenu = ({ } } + const onRestartPipeline = async () => { + if (!projectRef) return console.error('Project ref is required') + if (!pipeline) return toast.error(PIPELINE_ERROR_MESSAGES.NO_PIPELINE_FOUND) + + try { + setGlobalRequestStatus(pipeline.id, PipelineStatusRequestStatus.RestartRequested, statusName) + await startPipeline({ projectRef, pipelineId: pipeline.id }) + } catch (error) { + setGlobalRequestStatus(pipeline.id, PipelineStatusRequestStatus.None) + toast.error(PIPELINE_ERROR_MESSAGES.ENABLE_DESTINATION) + } + } + return (
    {isLoading && } + {isError && ( )} @@ -109,19 +140,30 @@ export const RowMenu = ({ + + + {services.map((service) => ( + +
    + +
    +

    {service.name}

    +

    + +

    +
    +
    +
    + View logs + +
    + + ))} + {allServicesOperational ? null : ( + <> + +
    +
    + +
    + Recently restored projects can take up to 5 minutes to become fully operational. +
    + + )} +
    + + ) +} diff --git a/apps/studio/components/interfaces/HomeNew/SortableSection.tsx b/apps/studio/components/interfaces/HomeNew/SortableSection.tsx new file mode 100644 index 00000000000..78704e042da --- /dev/null +++ b/apps/studio/components/interfaces/HomeNew/SortableSection.tsx @@ -0,0 +1,36 @@ +import { useSortable } from '@dnd-kit/sortable' +import { GripVertical } from 'lucide-react' +import type { CSSProperties, ReactNode } from 'react' +import { cn } from 'ui' + +type SortableSectionProps = { + id: string + children: ReactNode +} + +export const SortableSection = ({ id, children }: SortableSectionProps) => { + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ + id, + }) + + const style: CSSProperties = { + transform: transform + ? `translate3d(${Math.round(transform.x)}px, ${Math.round(transform.y)}px, 0)` + : undefined, + transition, + } + + return ( +
    + +
    {children}
    +
    + ) +} diff --git a/apps/studio/components/interfaces/HomeNew/TopSection.tsx b/apps/studio/components/interfaces/HomeNew/TopSection.tsx new file mode 100644 index 00000000000..56c026709eb --- /dev/null +++ b/apps/studio/components/interfaces/HomeNew/TopSection.tsx @@ -0,0 +1,94 @@ +import Link from 'next/link' + +import { ActivityStats } from 'components/interfaces/HomeNew/ActivityStats' +import { ProjectPausedState } from 'components/layouts/ProjectLayout/PausedState/ProjectPausedState' +import { ComputeBadgeWrapper } from 'components/ui/ComputeBadgeWrapper' +import { InlineLink } from 'components/ui/InlineLink' +import { ProjectUpgradeFailedBanner } from 'components/ui/ProjectUpgradeFailedBanner' +import { ReactFlowProvider } from 'reactflow' +import { Badge, cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui' +import { InstanceConfiguration } from '../Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration' + +interface TopSectionProps { + projectName: string + isMainBranch?: boolean + parentProject?: { ref?: string; name?: string } | null + isOrioleDb?: boolean + project: any + organization: any + projectRef?: string + isPaused: boolean +} + +export const TopSection = ({ + projectName, + isMainBranch, + parentProject, + isOrioleDb, + project, + organization, + isPaused, +}: TopSectionProps) => { + return ( +
    +
    +
    +
    +
    + {!isMainBranch && ( + + {parentProject?.name} + + )} +

    {projectName}

    +
    +
    + {isOrioleDb && ( + + + OrioleDB + + + This project is using Postgres with OrioleDB which is currently in preview and + not suitable for production workloads. View our{' '} + + documentation + {' '} + for all limitations. + + + )} + +
    +
    +
    + +
    +
    +
    +
    + + + +
    +
    +
    + + {isPaused && } +
    + ) +} diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerData.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerData.tsx index 54d50a87d77..c52f590d526 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerData.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerData.tsx @@ -61,6 +61,7 @@ export const BillingCustomerData = () => { line1: customerProfile?.address?.line1, line2: customerProfile?.address?.line2 ?? undefined, postal_code: customerProfile?.address?.postal_code ?? undefined, + state: customerProfile?.address?.state ?? undefined, billing_name: customerProfile?.billing_name, tax_id_type: taxId?.type, tax_id_value: taxId?.value, diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerDataForm.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerDataForm.tsx index 98e84f71649..7440ff506fb 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerDataForm.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerDataForm.tsx @@ -32,39 +32,18 @@ interface BillingCustomerDataFormProps { } // Define the expected form values structure and validation schema -export const BillingCustomerDataSchema = z - .object({ - billing_name: z.string().min(3, 'Name must be at least 3 letters long'), - line1: z.string().optional(), - line2: z.string().optional(), - city: z.string().optional(), - state: z.string().optional(), - postal_code: z.string().optional(), - country: z.string().optional(), - tax_id_type: z.string(), - tax_id_value: z.string(), - tax_id_name: z.string(), - }) - .refine( - (data) => { - // its fine to just set the name, but once any other field is set, requires full address - const hasAnyField = data.line1 || data.line2 || data.city || data.state || data.postal_code - // If any field has value, country and line1 must have values. - return !hasAnyField || (!!data.country && !!data.line1) - }, - { - message: 'Country and Address line 1 are required if any other field is provided.', - path: ['line1'], - } - ) - .refine((data) => !(!!data.line1 && !data.country), { - message: 'Please select a country', - path: ['country'], - }) - .refine((data) => !(!!data.country && !data.line1), { - message: 'Please provide an address line 1', - path: ['line1'], - }) +export const BillingCustomerDataSchema = z.object({ + billing_name: z.string().min(3, 'Name must be at least 3 letters long'), + line1: z.string().trim().min(3, 'Address line 1 is required'), + line2: z.string().optional(), + city: z.string().trim().min(2, 'City is required'), + state: z.string().trim(), + postal_code: z.string().trim().min(1, 'Postal code is required'), + country: z.string().trim().min(1, 'Country is required'), + tax_id_type: z.string(), + tax_id_value: z.string(), + tax_id_name: z.string(), +}) export type BillingCustomerDataFormValues = z.infer diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PaymentMethodSelection.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PaymentMethodSelection.tsx index 114ed71e19f..8f60d2a372f 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PaymentMethodSelection.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PaymentMethodSelection.tsx @@ -63,8 +63,6 @@ const PaymentMethodSelection = forwardRef(function PaymentMethodSelection( }) const { data: taxId, isLoading: isCustomerTaxIdLoading } = useOrganizationTaxIdQuery({ slug }) - const hidePaymentMethodsWithoutAddress = useFlag('hidePaymentMethodsWithoutAddress') - const { data: allPaymentMethods, isLoading } = useOrganizationPaymentMethodsQuery({ slug }) const paymentMethods = useMemo(() => { @@ -74,18 +72,16 @@ const PaymentMethodSelection = forwardRef(function PaymentMethodSelection( defaultPaymentMethodId: null, } - const filtered = allPaymentMethods.data.filter( - (pm) => !hidePaymentMethodsWithoutAddress || pm.has_address - ) return { - data: filtered, + // force customer to put down address via payment method creation flow if they don't have an address set + data: customerProfile?.address == null ? [] : allPaymentMethods.data, defaultPaymentMethodId: allPaymentMethods.data.some( (pm) => pm.id === allPaymentMethods.defaultPaymentMethodId ) ? allPaymentMethods.defaultPaymentMethodId : null, } - }, [allPaymentMethods]) + }, [allPaymentMethods, customerProfile]) const captchaRefCallback = useCallback((node: any) => { setCaptchaRef(node) @@ -223,7 +219,7 @@ const PaymentMethodSelection = forwardRef(function PaymentMethodSelection( />
    - {isLoading ? ( + {isLoading || isCustomerProfileLoading ? (

    Retrieving payment methods

    diff --git a/apps/studio/components/interfaces/Organization/TeamSettings/UpdateRolesPanel/UpdateRolesConfirmationModal.tsx b/apps/studio/components/interfaces/Organization/TeamSettings/UpdateRolesPanel/UpdateRolesConfirmationModal.tsx index 1b7a0a482de..1ba27891feb 100644 --- a/apps/studio/components/interfaces/Organization/TeamSettings/UpdateRolesPanel/UpdateRolesConfirmationModal.tsx +++ b/apps/studio/components/interfaces/Organization/TeamSettings/UpdateRolesPanel/UpdateRolesConfirmationModal.tsx @@ -76,7 +76,22 @@ export const UpdateRolesConfirmationModal = ({ .map((id) => { return [...org_scoped_roles, ...project_scoped_roles].find((r) => r.id === id) }) + .map((x) => { + // [Joshen] This is merely a patch to handle a issue on the BE whereby for a project-scoped member, + // if one of the projects that the member is deleted, the roles isn't cleaned up on the BE + // Hence adding an FE patch here for dashboard to self-remediate by omitting any project IDs from the role + // which no longer exists in the organization projects list + if (!!x?.project_ids) { + return { + ...x, + project_ids: x.project_ids.filter((id) => orgProjects.some((p) => id === p.id)), + } + } else { + return x + } + }) .filter(Boolean) as OrganizationRole[] + const isChangeWithinOrgScope = projectsRoleConfiguration.length === 1 && projectsRoleConfiguration[0].ref === undefined diff --git a/apps/studio/components/interfaces/Organization/TeamSettings/UpdateRolesPanel/UpdateRolesPanel.utils.ts b/apps/studio/components/interfaces/Organization/TeamSettings/UpdateRolesPanel/UpdateRolesPanel.utils.ts index 89104c2c2d5..bc3dfb71627 100644 --- a/apps/studio/components/interfaces/Organization/TeamSettings/UpdateRolesPanel/UpdateRolesPanel.utils.ts +++ b/apps/studio/components/interfaces/Organization/TeamSettings/UpdateRolesPanel/UpdateRolesPanel.utils.ts @@ -41,7 +41,17 @@ export const formatMemberRoleToProjectRoleConfiguration = ( } }) .filter(Boolean) - .flat() as ProjectRoleConfiguration[] + .flat() + .filter((p) => { + // [Joshen] Validate only for project scoped roles + // This filters out project scoped roles for projects that the user doesn't have access to + // (e.g if the project was deleted) + if ('baseRoleId' in p!) { + return p.ref !== undefined + } else { + return p + } + }) as ProjectRoleConfiguration[] return roleConfiguration } diff --git a/apps/studio/components/interfaces/QueryPerformance/QueryDetail.tsx b/apps/studio/components/interfaces/QueryPerformance/QueryDetail.tsx index d22830df4dd..9f1531829c5 100644 --- a/apps/studio/components/interfaces/QueryPerformance/QueryDetail.tsx +++ b/apps/studio/components/interfaces/QueryPerformance/QueryDetail.tsx @@ -1,15 +1,9 @@ import { Lightbulb } from 'lucide-react' import { useEffect, useState } from 'react' +import dynamic from 'next/dynamic' import { formatSql } from 'lib/formatSql' -import { - AlertDescription_Shadcn_, - AlertTitle_Shadcn_, - Alert_Shadcn_, - Button, - CodeBlock, - cn, -} from 'ui' +import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button, cn } from 'ui' import { QueryPanelContainer, QueryPanelSection } from './QueryPanel' import { QUERY_PERFORMANCE_REPORTS, @@ -22,6 +16,14 @@ interface QueryDetailProps { onClickViewSuggestion: () => void } +// Load SqlMonacoBlock (monaco editor) client-side only (does not behave well server-side) +const SqlMonacoBlock = dynamic( + () => import('./SqlMonacoBlock').then(({ SqlMonacoBlock }) => SqlMonacoBlock), + { + ssr: false, + } +) + export const QueryDetail = ({ reportType, selectedRow, @@ -43,16 +45,7 @@ export const QueryDetail = ({

    Query pattern

    - code]:m-0 [&>code>span]:flex [&>code>span]:flex-wrap' - )} - /> + {isLinterWarning && ( } +// Load the monaco editor client-side only (does not behave well server-side) +const Editor = dynamic(() => import('@monaco-editor/react').then(({ Editor }) => Editor), { + ssr: false, +}) + export const QueryPerformanceGrid = ({ queryPerformanceQuery }: QueryPerformanceGridProps) => { const router = useRouter() const gridRef = useRef(null) @@ -74,7 +80,7 @@ export const QueryPerformanceGrid = ({ queryPerformanceQuery }: QueryPerformance const value = props.row?.[col.id] if (col.id === 'query') { return ( -
    +
    {hasIndexRecommendations(props.row.index_advisor_result, true) && ( )} -
    {value}
    + +
    + ) + } + + if (col.id === 'rolname') { + return ( +
    +

    {value || 'n/a'}

    +
    + ) + } + + if (col.id === 'prop_total_time') { + return ( +
    +

    {value || 'n/a'}

    ) } @@ -154,6 +207,43 @@ export const QueryPerformanceGrid = ({ queryPerformanceQuery }: QueryPerformance setSelectedRow(undefined) }, [preset, search, roles, urlSort, order]) + const handleKeyDown = useCallback( + (event: KeyboardEvent) => { + if (!reportData.length || selectedRow === undefined) return + + if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') return + + // stop default RDG behavior (which moves focus to header when selectedRow is 0) + event.stopPropagation() + + let nextIndex = selectedRow + if (event.key === 'ArrowUp' && selectedRow > 0) { + nextIndex = selectedRow - 1 + } else if (event.key === 'ArrowDown' && selectedRow < reportData.length - 1) { + nextIndex = selectedRow + 1 + } + + if (nextIndex !== selectedRow) { + setSelectedRow(nextIndex) + gridRef.current?.scrollToCell({ idx: 0, rowIdx: nextIndex }) + + const rowQuery = reportData[nextIndex]?.query ?? '' + if (!rowQuery.trim().toLowerCase().startsWith('select')) { + setView('details') + } + } + }, + [reportData, selectedRow] + ) + + useEffect(() => { + // run before RDG to prevent header focus (the third param: true) + window.addEventListener('keydown', handleKeyDown, true) + return () => { + window.removeEventListener('keydown', handleKeyDown, true) + } + }, [handleKeyDown]) + return ( { + const [copied, setCopied] = useState(false) + + const content = useMemo(() => value ?? '', [value]) + + const handleCopy = () => { + setCopied(true) + setTimeout(() => setCopied(false), 1000) + } + + return ( +
    + + + {!hideCopy && ( +
    + + + +
    + )} +
    + ) +} diff --git a/apps/studio/components/interfaces/Reports/Reports.constants.ts b/apps/studio/components/interfaces/Reports/Reports.constants.ts index 69edacca9cb..96b3b326d3b 100644 --- a/apps/studio/components/interfaces/Reports/Reports.constants.ts +++ b/apps/studio/components/interfaces/Reports/Reports.constants.ts @@ -406,6 +406,7 @@ select statements.query, statements.calls, statements.total_exec_time + statements.total_plan_time as total_time, + statements.mean_exec_time + statements.mean_plan_time as mean_time, to_char(((statements.total_exec_time + statements.total_plan_time)/sum(statements.total_exec_time + statements.total_plan_time) OVER()) * 100, 'FM90D0') || '%' AS prop_total_time${ runIndexAdvisor ? `, @@ -531,3 +532,62 @@ export const DEPRECATED_REPORTS = [ 'total_storage_patch_requests', 'total_options_requests', ] + +export const EDGE_FUNCTION_REGIONS = [ + { + key: 'ap-northeast-1', + label: 'Tokyo', + }, + { + key: 'ap-northeast-2', + label: 'Seoul', + }, + { + key: 'ap-south-1', + label: 'Mumbai', + }, + { + key: 'ap-southeast-1', + label: 'Singapore', + }, + { + key: 'ap-southeast-2', + label: 'Sydney', + }, + { + key: 'ca-central-1', + label: 'Canada Central', + }, + { + key: 'us-east-1', + label: 'N. Virginia', + }, + { + key: 'us-west-1', + label: 'N. California', + }, + { + key: 'us-west-2', + label: 'Oregon', + }, + { + key: 'eu-central-1', + label: 'Frankfurt', + }, + { + key: 'eu-west-1', + label: 'Ireland', + }, + { + key: 'eu-west-2', + label: 'London', + }, + { + key: 'eu-west-3', + label: 'Paris', + }, + { + key: 'sa-east-1', + label: 'São Paulo', + }, +] as const diff --git a/apps/studio/components/interfaces/Reports/v2/ReportChartV2.tsx b/apps/studio/components/interfaces/Reports/v2/ReportChartV2.tsx index 36538a71681..c021a855ac6 100644 --- a/apps/studio/components/interfaces/Reports/v2/ReportChartV2.tsx +++ b/apps/studio/components/interfaces/Reports/v2/ReportChartV2.tsx @@ -1,6 +1,6 @@ import { useQuery } from '@tanstack/react-query' import { Loader2 } from 'lucide-react' -import { useState } from 'react' +import { useEffect, useState } from 'react' import { ComposedChart } from 'components/ui/Charts/ComposedChart' import type { AnalyticsInterval } from 'data/analytics/constants' @@ -10,7 +10,6 @@ import { useCurrentOrgPlan } from 'hooks/misc/useCurrentOrgPlan' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { Card, CardContent, cn } from 'ui' import { ReportChartUpsell } from './ReportChartUpsell' - export interface ReportChartV2Props { report: ReportConfig projectRef: string @@ -18,10 +17,9 @@ export interface ReportChartV2Props { endDate: string interval: AnalyticsInterval updateDateRange: (from: string, to: string) => void - functionIds?: string[] - edgeFnIdToName?: (id: string) => string | undefined className?: string syncId?: string + filters?: any } export const ReportChartV2 = ({ @@ -31,10 +29,9 @@ export const ReportChartV2 = ({ endDate, interval, updateDateRange, - functionIds, - edgeFnIdToName, className, syncId, + filters, }: ReportChartV2Props) => { const { data: org } = useSelectedOrganizationQuery() const { plan: orgPlan } = useCurrentOrgPlan() @@ -43,27 +40,27 @@ export const ReportChartV2 = ({ const isAvailable = report.availableIn === undefined || (orgPlanId && report.availableIn.includes(orgPlanId)) - const canFetch = orgPlanId !== undefined + const canFetch = orgPlanId !== undefined && isAvailable const { data: queryResult, isLoading: isLoadingChart, error, + isFetching, } = useQuery( - ['projects', projectRef, 'report-v2', report.id, { startDate, endDate, interval, functionIds }], + [ + 'projects', + projectRef, + 'report-v2', + { reportId: report.id, startDate, endDate, interval, filters }, + ], async () => { - return await report.dataProvider( - projectRef, - startDate, - endDate, - interval, - functionIds, - edgeFnIdToName - ) + return await report.dataProvider(projectRef, startDate, endDate, interval, filters) }, { enabled: Boolean(projectRef && canFetch && isAvailable), refetchOnWindowFocus: false, + staleTime: 0, } ) @@ -86,7 +83,7 @@ export const ReportChartV2 = ({ const [chartStyle, setChartStyle] = useState(report.defaultChartStyle) - if (!isAvailable && !isLoadingChart) { + if (!isAvailable) { return } @@ -95,12 +92,17 @@ export const ReportChartV2 = ({ return ( - + {isLoadingChart ? ( ) : showEmptyState ? (

    - No data available for the selected time range + No data available for the selected time range and filters

    ) : isErrorState ? (

    diff --git a/apps/studio/components/interfaces/Reports/v2/ReportsNumericFilter.tsx b/apps/studio/components/interfaces/Reports/v2/ReportsNumericFilter.tsx new file mode 100644 index 00000000000..cb3765e1c94 --- /dev/null +++ b/apps/studio/components/interfaces/Reports/v2/ReportsNumericFilter.tsx @@ -0,0 +1,201 @@ +import { ChevronDown } from 'lucide-react' +import { useEffect, useState } from 'react' + +import { Label } from '@ui/components/shadcn/ui/label' +import { Popover, PopoverContent, PopoverTrigger } from '@ui/components/shadcn/ui/popover' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@ui/components/shadcn/ui/select' +import { Button, cn } from 'ui' +import { Input } from 'ui-patterns/DataInputs/Input' +import { z } from 'zod' + +const OPERATOR_LABELS = { + '=': 'Equals', + '>=': 'Greater than or equal to', + '<=': 'Less than or equal to', + '>': 'Greater than', + '<': 'Less than', + '!=': 'Not equal to', +} satisfies Record + +const comparisonOperatorSchema = z.enum(['=', '>=', '<=', '>', '<', '!=']) +export type ComparisonOperator = z.infer + +export const numericFilterSchema = z.object({ + operator: comparisonOperatorSchema, + value: z.number(), +}) +export type NumericFilter = z.infer + +interface ReportsNumericFilterProps { + label: string + value: NumericFilter | null + onChange: (value: NumericFilter | null) => void + operators?: ComparisonOperator[] + defaultOperator?: ComparisonOperator + placeholder?: string + min?: number + max?: number + step?: number + isLoading?: boolean + className?: string +} + +export const ReportsNumericFilter = ({ + label, + value, + onChange, + operators = ['=', '>=', '<=', '>', '<', '!='], + defaultOperator = '=', + placeholder = 'Enter value', + min, + max, + step = 1, + isLoading = false, + className, +}: ReportsNumericFilterProps) => { + const [open, setOpen] = useState(false) + const [tempValue, setTempValue] = useState(value) + + const isActive = value !== null + + useEffect(() => { + if (!open) { + setTempValue(value) + } + }, [open, value]) + + const handleApply = () => { + onChange(tempValue) + setOpen(false) + } + + const getDisplayValue = () => { + if (value) { + return `${value.operator} ${value.value}` + } + return '' + } + + const handleOperatorChange = (operator: ComparisonOperator) => { + setTempValue({ + operator, + value: tempValue?.value ?? 0, + }) + } + + const handleValueChange = (inputValue: string) => { + if (inputValue === '') { + setTempValue(null) + } else { + const numericValue = parseFloat(inputValue) + if (!isNaN(numericValue)) { + setTempValue({ + operator: tempValue?.operator ?? defaultOperator, + value: numericValue, + }) + } + } + } + + const handleClearAll = () => { + setTempValue(null) + } + + return ( + + + + + +
    { + e.preventDefault() + handleApply() + }} + className="px-3 py-3 flex flex-col gap-y-3" + > +

    + + +
    + +
    + + handleValueChange(e.target.value)} + min={min} + max={max} + step={step} + /> + {(min !== undefined || max !== undefined) && ( +

    + {min !== undefined && max !== undefined + ? `Range: ${min} - ${max}` + : min !== undefined + ? `Min: ${min}` + : `Max: ${max}`} +

    + )} +
    + + +
    + + +
    + + + ) +} diff --git a/apps/studio/components/interfaces/Reports/v2/ReportsSelectFilter.tsx b/apps/studio/components/interfaces/Reports/v2/ReportsSelectFilter.tsx new file mode 100644 index 00000000000..e38480d0359 --- /dev/null +++ b/apps/studio/components/interfaces/Reports/v2/ReportsSelectFilter.tsx @@ -0,0 +1,147 @@ +import { ChevronDown } from 'lucide-react' +import { useEffect, useState } from 'react' + +import { Checkbox } from '@ui/components/shadcn/ui/checkbox' +import { Label } from '@ui/components/shadcn/ui/label' +import { Popover, PopoverContent, PopoverTrigger } from '@ui/components/shadcn/ui/popover' +import { + Button, + cn, + Command_Shadcn_ as Command, + CommandInput_Shadcn_ as CommandInput, + CommandItem_Shadcn_, +} from 'ui' +import { CommandList_Shadcn_ as CommandList, CommandEmpty_Shadcn_ as CommandEmpty } from 'ui' +import { z } from 'zod' +import { CommandGroup } from '@ui/components/shadcn/ui/command' + +export interface ReportSelectOption { + label: React.ReactNode + value: string + description?: string +} + +export const selectFilterSchema = z.array(z.string()) +export type SelectFilters = z.infer + +interface ReportsSelectFilterProps { + label: string + options: ReportSelectOption[] + value: SelectFilters + onChange: (value: SelectFilters) => void + isLoading?: boolean + className?: string + showSearch?: boolean +} + +export const ReportsSelectFilter = ({ + label, + options, + value, + onChange, + isLoading = false, + className, + showSearch = false, +}: ReportsSelectFilterProps) => { + const [open, setOpen] = useState(false) + const [tempValue, setTempValue] = useState(value) + + const isActive = tempValue.length > 0 + + useEffect(() => { + if (!open) { + setTempValue(value) + } + }, [open, value]) + + const handleApply = () => { + onChange(tempValue) + setOpen(false) + } + + const handleClearAll = () => { + setTempValue([]) + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault() + handleApply() + } + } + + return ( + + + + + + + {showSearch && } + + No options found. + + {options.map((option) => ( + + + + ))} + + + + +
    + + +
    +
    +
    + ) +} diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.tsx b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.tsx index ea3cee853fc..f74301cc8c6 100644 --- a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.tsx +++ b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.tsx @@ -1,12 +1,13 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' -import { useParams } from 'common' import { partition } from 'lodash' import { ChevronDown, Globe2, Loader2, Network } from 'lucide-react' import { useTheme } from 'next-themes' +import Link from 'next/link' import { useEffect, useMemo, useRef, useState } from 'react' import ReactFlow, { Background, Edge, ReactFlowProvider, useReactFlow } from 'reactflow' import 'reactflow/dist/style.css' +import { useParams } from 'common' import AlertError from 'components/ui/AlertError' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { useLoadBalancersQuery } from 'data/read-replicas/load-balancers-query' @@ -18,7 +19,6 @@ import { import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useIsOrioleDb, useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { timeout } from 'lib/helpers' -import Link from 'next/link' import { type AWS_REGIONS_KEYS } from 'shared-data' import { Button, @@ -40,7 +40,11 @@ import MapView from './MapView' import { RestartReplicaConfirmationModal } from './RestartReplicaConfirmationModal' import { useShowNewReplicaPanel } from './use-show-new-replica' -const InstanceConfigurationUI = () => { +interface InstanceConfigurationUIProps { + diagramOnly?: boolean +} + +const InstanceConfigurationUI = ({ diagramOnly = false }: InstanceConfigurationUIProps) => { const reactFlow = useReactFlow() const isOrioleDb = useIsOrioleDb() const { resolvedTheme } = useTheme() @@ -210,9 +214,9 @@ const InstanceConfigurationUI = () => { }, [isSuccessReplicas, isSuccessLoadBalancers, nodes, edges, view]) return ( -
    +
    @@ -224,70 +228,72 @@ const InstanceConfigurationUI = () => { {isError && } {isSuccessReplicas && !isLoadingProject && ( <> -
    -
    - 0 ? 'rounded-r-none' : '')} - onClick={() => setShowNewReplicaPanel(true)} - tooltip={{ - content: { - side: 'bottom', - text: !canManageReplicas - ? 'You need additional permissions to deploy replicas' - : isOrioleDb - ? 'Read replicas are not supported with OrioleDB' - : undefined, - }, - }} - > - Deploy a new replica - - {replicas.length > 0 && ( - - -
    + {project?.cloud_provider === 'AWS' && ( +
    +
    )}
    - {project?.cloud_provider === 'AWS' && ( -
    -
    - )} -
    + )} {view === 'flow' ? ( { )}
    - setRefetchInterval(5000)} - onClose={() => { - setNewReplicaRegion(undefined) - setShowNewReplicaPanel(false) - }} - /> + {!diagramOnly && ( + <> + setRefetchInterval(5000)} + onClose={() => { + setNewReplicaRegion(undefined) + setShowNewReplicaPanel(false) + }} + /> - setRefetchInterval(5000)} - onCancel={() => setSelectedReplicaToDrop(undefined)} - /> + setRefetchInterval(5000)} + onCancel={() => setSelectedReplicaToDrop(undefined)} + /> - setRefetchInterval(5000)} - onCancel={() => setShowDeleteAllModal(false)} - /> + setRefetchInterval(5000)} + onCancel={() => setShowDeleteAllModal(false)} + /> - setRefetchInterval(5000)} - onCancel={() => setSelectedReplicaToRestart(undefined)} - /> + setRefetchInterval(5000)} + onCancel={() => setSelectedReplicaToRestart(undefined)} + /> + + )}
    ) } -const InstanceConfiguration = () => { +interface InstanceConfigurationProps { + diagramOnly?: boolean +} + +export const InstanceConfiguration = ({ diagramOnly = false }: InstanceConfigurationProps) => { return ( - + ) } - -export default InstanceConfiguration diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureInfo.tsx b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureInfo.tsx index c2e375ade9b..88c9a63eba5 100644 --- a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureInfo.tsx +++ b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureInfo.tsx @@ -24,7 +24,7 @@ import { TooltipTrigger, } from 'ui' import { ProjectUpgradeAlert } from '../General/Infrastructure/ProjectUpgradeAlert' -import InstanceConfiguration from './InfrastructureConfiguration/InstanceConfiguration' +import { InstanceConfiguration } from './InfrastructureConfiguration/InstanceConfiguration' import { ObjectsToBeDroppedWarning, ReadReplicasWarning, diff --git a/apps/studio/components/interfaces/Settings/Logs/Logs.DatePickers.tsx b/apps/studio/components/interfaces/Settings/Logs/Logs.DatePickers.tsx index b81e86ba18d..4200143336d 100644 --- a/apps/studio/components/interfaces/Settings/Logs/Logs.DatePickers.tsx +++ b/apps/studio/components/interfaces/Settings/Logs/Logs.DatePickers.tsx @@ -29,13 +29,14 @@ export type DatePickerValue = { text?: string } -interface Props { +interface LogsDatePickerProps { value: DatePickerValue helpers: DatetimeHelper[] onSubmit: (value: DatePickerValue) => void buttonTriggerProps?: ButtonProps popoverContentProps?: typeof PopoverContent_Shadcn_ hideWarnings?: boolean + align?: 'start' | 'end' | 'center' } export const LogsDatePicker = ({ @@ -45,7 +46,8 @@ export const LogsDatePicker = ({ buttonTriggerProps, popoverContentProps, hideWarnings, -}: PropsWithChildren) => { + align = 'end', +}: PropsWithChildren) => { const [open, setOpen] = useState(false) const todayButtonRef = useRef(null) @@ -253,7 +255,7 @@ export const LogsDatePicker = ({ diff --git a/apps/studio/components/interfaces/Settings/Logs/Logs.constants.ts b/apps/studio/components/interfaces/Settings/Logs/Logs.constants.ts index 6431378f378..9a6ae741a99 100644 --- a/apps/studio/components/interfaces/Settings/Logs/Logs.constants.ts +++ b/apps/studio/components/interfaces/Settings/Logs/Logs.constants.ts @@ -357,6 +357,10 @@ export const SQL_FILTER_TEMPLATES: any = { pg_cron_logs: { ..._SQL_FILTER_COMMON, }, + etl_replication_logs: { + ..._SQL_FILTER_COMMON, + pipeline_id: (value: string | number) => `pipeline_id = ${value}`, + }, } export enum LogsTableName { diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/ColumnContextMenu.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/ColumnContextMenu.tsx index ea17e28b8eb..d0acf14b033 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/ColumnContextMenu.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/ColumnContextMenu.tsx @@ -3,7 +3,7 @@ import { Item, Menu, Separator, Submenu } from 'react-contexify' import 'react-contexify/dist/ReactContexify.css' import { PermissionAction } from '@supabase/shared-types/out/constants' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { ChevronRight, ChevronsDown, ChevronsUp, Clipboard, Eye, FolderPlus } from 'lucide-react' import { useStorageExplorerStateSnapshot } from 'state/storage-explorer' import { @@ -17,18 +17,21 @@ interface ColumnContextMenuProps { id: string } -const ColumnContextMenu = ({ id = '' }: ColumnContextMenuProps) => { - const canUpdateFiles = useCheckPermissions(PermissionAction.STORAGE_WRITE, '*') +export const ColumnContextMenu = ({ id = '' }: ColumnContextMenuProps) => { const { columns, selectedItems, setSelectedItems, + setView, setSortBy, setSortByOrder, addNewFolderPlaceholder, } = useStorageExplorerStateSnapshot() - const snap = useStorageExplorerStateSnapshot() + const { can: canUpdateFiles } = useAsyncCheckProjectPermissions( + PermissionAction.STORAGE_WRITE, + '*' + ) const onSelectCreateFolder = (columnIndex = -1) => { addNewFolderPlaceholder(columnIndex) @@ -80,10 +83,10 @@ const ColumnContextMenu = ({ id = '' }: ColumnContextMenuProps) => { } arrow={} > - snap.setView(STORAGE_VIEWS.COLUMNS)}> + setView(STORAGE_VIEWS.COLUMNS)}> As columns - snap.setView(STORAGE_VIEWS.LIST)}> + setView(STORAGE_VIEWS.LIST)}> As list @@ -128,5 +131,3 @@ const ColumnContextMenu = ({ id = '' }: ColumnContextMenuProps) => { ) } - -export default ColumnContextMenu diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/CustomDragLayer.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/CustomDragLayer.tsx index cd848444384..5722c6fede7 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/CustomDragLayer.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/CustomDragLayer.tsx @@ -1,7 +1,7 @@ -import { useDragLayer } from 'react-dnd' import { useEffect, useState } from 'react' -import { StorageItemWithColumn } from '../Storage.types' +import { useDragLayer } from 'react-dnd' import { Checkbox, cn } from 'ui' +import { StorageItemWithColumn } from '../Storage.types' interface ItemPosition { item: StorageItemWithColumn @@ -52,7 +52,7 @@ const DragPreview = ({ itemPositions }: { itemPositions: ItemPosition[] }) => { ) } -const CustomDragLayer = () => { +export const CustomDragLayer = () => { const { isDragging, item, currentOffset, initialOffset, differenceFromInitialOffset } = useDragLayer((monitor) => ({ item: monitor.getItem(), @@ -126,5 +126,3 @@ const CustomDragLayer = () => {
    ) } - -export default CustomDragLayer diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/CustomExpiryModal.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/CustomExpiryModal.tsx index 1e56e753cd8..b33474ac03d 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/CustomExpiryModal.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/CustomExpiryModal.tsx @@ -12,7 +12,7 @@ const unitMap = { years: 3600 * 24 * 365, } -const CustomExpiryModal = () => { +export const CustomExpiryModal = () => { const { onCopyUrl } = useCopyUrl() const snap = useStorageExplorerStateSnapshot() const { selectedFileCustomExpiry, setSelectedFileCustomExpiry } = snap @@ -100,5 +100,3 @@ const CustomExpiryModal = () => { ) } - -export default CustomExpiryModal diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorer.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorer.tsx index f0beec5482b..5d99d9b5c18 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorer.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorer.tsx @@ -7,11 +7,11 @@ import { useStorageExplorerStateSnapshot } from 'state/storage-explorer' import { cn } from 'ui' import { CONTEXT_MENU_KEYS, STORAGE_VIEWS } from '../Storage.constants' import type { StorageColumn, StorageItemWithColumn } from '../Storage.types' -import ColumnContextMenu from './ColumnContextMenu' -import CustomDragLayer from './CustomDragLayer' -import FileExplorerColumn from './FileExplorerColumn' -import FolderContextMenu from './FolderContextMenu' -import ItemContextMenu from './ItemContextMenu' +import { ColumnContextMenu } from './ColumnContextMenu' +import { CustomDragLayer } from './CustomDragLayer' +import { FileExplorerColumn } from './FileExplorerColumn' +import { FolderContextMenu } from './FolderContextMenu' +import { ItemContextMenu } from './ItemContextMenu' export interface FileExplorerProps { columns: StorageColumn[] @@ -23,7 +23,7 @@ export interface FileExplorerProps { onColumnLoadMore: (index: number, column: StorageColumn) => void } -const FileExplorer = ({ +export const FileExplorer = ({ columns = [], selectedItems = [], itemSearchString, @@ -96,5 +96,3 @@ const FileExplorer = ({ ) } - -export default FileExplorer diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerColumn.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerColumn.tsx index d656d81afba..c9d065e6181 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerColumn.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerColumn.tsx @@ -9,7 +9,7 @@ import { toast } from 'sonner' import InfiniteList from 'components/ui/InfiniteList' import ShimmeringLoader from 'components/ui/ShimmeringLoader' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { BASE_PATH } from 'lib/constants' import { formatBytes } from 'lib/helpers' import { useStorageExplorerStateSnapshot } from 'state/storage-explorer' @@ -21,7 +21,7 @@ import { STORAGE_VIEWS, } from '../Storage.constants' import type { StorageColumn, StorageItemWithColumn } from '../Storage.types' -import FileExplorerRow from './FileExplorerRow' +import { FileExplorerRow } from './FileExplorerRow' const DragOverOverlay = ({ isOpen, onDragLeave, onDrop, folderIsEmpty }: any) => { return ( @@ -69,7 +69,7 @@ export interface FileExplorerColumnProps { onColumnLoadMore: (index: number, column: StorageColumn) => void } -const FileExplorerColumn = ({ +export const FileExplorerColumn = ({ index = 0, column, fullWidth = false, @@ -84,7 +84,10 @@ const FileExplorerColumn = ({ const fileExplorerColumnRef = useRef(null) const snap = useStorageExplorerStateSnapshot() - const canUpdateStorage = useCheckPermissions(PermissionAction.STORAGE_WRITE, '*') + const { can: canUpdateStorage } = useAsyncCheckProjectPermissions( + PermissionAction.STORAGE_WRITE, + '*' + ) // Helper function to get the path for this column const getColumnPath = () => { @@ -430,5 +433,3 @@ const FileExplorerColumn = ({
    ) } - -export default FileExplorerColumn diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerHeader.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerHeader.tsx index 915edffb0c1..0d080f6c90f 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerHeader.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerHeader.tsx @@ -1,11 +1,5 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { compact, debounce, isEqual, noop } from 'lodash' -import { useCallback, useEffect, useRef, useState } from 'react' - -import { useIsAPIDocsSidePanelEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' -import APIDocsButton from 'components/ui/APIDocsButton' -import { ButtonTooltip } from 'components/ui/ButtonTooltip' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { Check, ChevronLeft, @@ -20,6 +14,12 @@ import { Upload, X, } from 'lucide-react' +import { useCallback, useEffect, useRef, useState } from 'react' + +import { useIsAPIDocsSidePanelEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' +import { APIDocsButton } from 'components/ui/APIDocsButton' +import { ButtonTooltip } from 'components/ui/ButtonTooltip' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useStorageExplorerStateSnapshot } from 'state/storage-explorer' import { Button, @@ -137,10 +137,10 @@ const HeaderBreadcrumbs = ({ interface FileExplorerHeader { itemSearchString: string setItemSearchString: (value: string) => void - onFilesUpload: (event: any, columnIndex: number) => void + onFilesUpload: (event: any, columnIndex?: number) => void } -const FileExplorerHeader = ({ +export const FileExplorerHeader = ({ itemSearchString = '', setItemSearchString = noop, onFilesUpload = noop, @@ -179,7 +179,10 @@ const FileExplorerHeader = ({ const breadcrumbs = columns.map((column) => column.name) const backDisabled = columns.length <= 1 - const canUpdateStorage = useCheckPermissions(PermissionAction.STORAGE_WRITE, '*') + const { can: canUpdateStorage } = useAsyncCheckProjectPermissions( + PermissionAction.STORAGE_WRITE, + '*' + ) useEffect(() => { if (itemSearchString) setSearchString(itemSearchString) @@ -423,7 +426,6 @@ const FileExplorerHeader = ({
    - {/* @ts-ignore */}
    ) } - -export default FileExplorerHeader diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerHeaderSelection.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerHeaderSelection.tsx index 5dea522960d..9b4559c5b5b 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerHeaderSelection.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerHeaderSelection.tsx @@ -3,14 +3,17 @@ import { Download, Move, Trash2, X } from 'lucide-react' import { useParams } from 'common' import { ButtonTooltip } from 'components/ui/ButtonTooltip' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useStorageExplorerStateSnapshot } from 'state/storage-explorer' import { Button } from 'ui' import { downloadFile } from './StorageExplorer.utils' -const FileExplorerHeaderSelection = () => { +export const FileExplorerHeaderSelection = () => { const { ref: projectRef, bucketId } = useParams() - const canUpdateFiles = useCheckPermissions(PermissionAction.STORAGE_WRITE, '*') + const { can: canUpdateFiles } = useAsyncCheckProjectPermissions( + PermissionAction.STORAGE_WRITE, + '*' + ) const { selectedItems, @@ -80,5 +83,3 @@ const FileExplorerHeaderSelection = () => {
    ) } - -export default FileExplorerHeaderSelection diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerRow.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerRow.tsx index eab5b537ed9..f52851fc717 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerRow.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerRow.tsx @@ -21,7 +21,7 @@ import SVG from 'react-inlinesvg' import { useParams } from 'common' import type { ItemRenderer } from 'components/ui/InfiniteList' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { BASE_PATH } from 'lib/constants' import { formatBytes } from 'lib/helpers' import { useStorageExplorerStateSnapshot } from 'state/storage-explorer' @@ -49,7 +49,7 @@ import { URL_EXPIRY_DURATION, } from '../Storage.constants' import { StorageItem, StorageItemWithColumn } from '../Storage.types' -import FileExplorerRowEditing from './FileExplorerRowEditing' +import { FileExplorerRowEditing } from './FileExplorerRowEditing' import { copyPathToFolder, downloadFile } from './StorageExplorer.utils' import { useCopyUrl } from './useCopyUrl' @@ -100,13 +100,13 @@ export const RowIcon = ({ return } -export interface FileExplorerRowProps { +interface FileExplorerRowProps { view: STORAGE_VIEWS columnIndex: number selectedItems: StorageItemWithColumn[] } -const FileExplorerRow: ItemRenderer = ({ +export const FileExplorerRow: ItemRenderer = ({ index: itemIndex, item, view = STORAGE_VIEWS.COLUMNS, @@ -150,7 +150,10 @@ const FileExplorerRow: ItemRenderer = ({ const isOpened = openedFolders.length > columnIndex ? openedFolders[columnIndex].name === item.name : false const isPreviewed = !isEmpty(selectedFilePreview) && isEqual(selectedFilePreview?.id, item.id) - const canUpdateFiles = useCheckPermissions(PermissionAction.STORAGE_WRITE, '*') + const { can: canUpdateFiles } = useAsyncCheckProjectPermissions( + PermissionAction.STORAGE_WRITE, + '*' + ) // Check if this folder is currently being moved const isBeingMoved = @@ -799,5 +802,3 @@ const FileExplorerRow: ItemRenderer = ({
    ) } - -export default FileExplorerRow diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerRowEditing.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerRowEditing.tsx index 415e2b89cdb..2ee1a7f02c3 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerRowEditing.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerRowEditing.tsx @@ -12,7 +12,11 @@ export interface FileExplorerRowEditingProps { columnIndex: number } -const FileExplorerRowEditing = ({ item, view, columnIndex }: FileExplorerRowEditingProps) => { +export const FileExplorerRowEditing = ({ + item, + view, + columnIndex, +}: FileExplorerRowEditingProps) => { const { renameFile, renameFolder, addNewFolder, updateRowStatus } = useStorageExplorerStateSnapshot() @@ -112,5 +116,3 @@ const FileExplorerRowEditing = ({ item, view, columnIndex }: FileExplorerRowEdit
    ) } - -export default FileExplorerRowEditing diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/FolderContextMenu.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/FolderContextMenu.tsx index d4b2dafff54..1242a841a7b 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/FolderContextMenu.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/FolderContextMenu.tsx @@ -3,7 +3,7 @@ import { Clipboard, Download, Edit, Trash2 } from 'lucide-react' import { Item, Menu, Separator } from 'react-contexify' import 'react-contexify/dist/ReactContexify.css' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useStorageExplorerStateSnapshot } from 'state/storage-explorer' import { copyPathToFolder } from './StorageExplorer.utils' @@ -11,10 +11,13 @@ interface FolderContextMenuProps { id: string } -const FolderContextMenu = ({ id = '' }: FolderContextMenuProps) => { +export const FolderContextMenu = ({ id = '' }: FolderContextMenuProps) => { const { openedFolders, downloadFolder, setSelectedItemToRename, setSelectedItemsToDelete } = useStorageExplorerStateSnapshot() - const canUpdateFiles = useCheckPermissions(PermissionAction.STORAGE_WRITE, '*') + const { can: canUpdateFiles } = useAsyncCheckProjectPermissions( + PermissionAction.STORAGE_WRITE, + '*' + ) return ( @@ -42,5 +45,3 @@ const FolderContextMenu = ({ id = '' }: FolderContextMenuProps) => { ) } - -export default FolderContextMenu diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/ItemContextMenu.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/ItemContextMenu.tsx index 4ce816e86aa..0712e4b6578 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/ItemContextMenu.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/ItemContextMenu.tsx @@ -4,7 +4,7 @@ import { Item, Menu, Separator, Submenu } from 'react-contexify' import 'react-contexify/dist/ReactContexify.css' import { useParams } from 'common' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useStorageExplorerStateSnapshot } from 'state/storage-explorer' import { URL_EXPIRY_DURATION } from '../Storage.constants' import { StorageItemWithColumn } from '../Storage.types' @@ -15,7 +15,7 @@ interface ItemContextMenuProps { id: string } -const ItemContextMenu = ({ id = '' }: ItemContextMenuProps) => { +export const ItemContextMenu = ({ id = '' }: ItemContextMenuProps) => { const { ref: projectRef, bucketId } = useParams() const snap = useStorageExplorerStateSnapshot() const { setSelectedFileCustomExpiry } = snap @@ -28,7 +28,10 @@ const ItemContextMenu = ({ id = '' }: ItemContextMenuProps) => { } = useStorageExplorerStateSnapshot() const { onCopyUrl } = useCopyUrl() const isPublic = selectedBucket.public - const canUpdateFiles = useCheckPermissions(PermissionAction.STORAGE_WRITE, '*') + const { can: canUpdateFiles } = useAsyncCheckProjectPermissions( + PermissionAction.STORAGE_WRITE, + '*' + ) const onHandleClick = async (event: any, item: StorageItemWithColumn, expiresIn?: number) => { if (item.isCorrupted) return @@ -106,5 +109,3 @@ const ItemContextMenu = ({ id = '' }: ItemContextMenuProps) => { ) } - -export default ItemContextMenu diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/MoveItemsModal.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/MoveItemsModal.tsx index 9ec26ea001e..8da76072e42 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/MoveItemsModal.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/MoveItemsModal.tsx @@ -12,7 +12,7 @@ interface MoveItemsModalProps { onSelectMove: (path: string) => void } -const MoveItemsModal = ({ +export const MoveItemsModal = ({ bucketName = '', visible = false, selectedItemsToMove = [], @@ -87,5 +87,3 @@ const MoveItemsModal = ({ ) } - -export default MoveItemsModal diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/PreviewPane.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/PreviewPane.tsx index a7f39873b67..44adc6c0b30 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/PreviewPane.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/PreviewPane.tsx @@ -6,7 +6,7 @@ import SVG from 'react-inlinesvg' import { useParams } from 'common' import { ButtonTooltip } from 'components/ui/ButtonTooltip' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { BASE_PATH } from 'lib/constants' import { formatBytes } from 'lib/helpers' import { useStorageExplorerStateSnapshot } from 'state/storage-explorer' @@ -115,7 +115,7 @@ const PreviewFile = ({ item }: { item: StorageItem }) => { ) } -const PreviewPane = () => { +export const PreviewPane = () => { const { ref: projectRef, bucketId } = useParams() const { @@ -127,7 +127,10 @@ const PreviewPane = () => { } = useStorageExplorerStateSnapshot() const { onCopyUrl } = useCopyUrl() - const canUpdateFiles = useCheckPermissions(PermissionAction.STORAGE_WRITE, '*') + const { can: canUpdateFiles } = useAsyncCheckProjectPermissions( + PermissionAction.STORAGE_WRITE, + '*' + ) if (!file) return null @@ -139,148 +142,144 @@ const PreviewPane = () => { const updatedAt = file.updated_at ? new Date(file.updated_at).toLocaleString() : 'Unknown' return ( - <> - -
    - {/* Preview Header */} -
    - setSelectedFilePreview(undefined)} - /> -
    + +
    + {/* Preview Header */} +
    + setSelectedFilePreview(undefined)} + /> +
    - {/* Preview Thumbnail*/} -
    -
    - -
    -
    - -
    - {/* Preview Information */} -
    -
    {file.name}
    - {file.isCorrupted && ( -
    - -

    - File is corrupted, please delete and reupload this file again -

    -
    - )} - {mimeType && ( -

    - {mimeType} - {size && - {size}} -

    - )} -
    - - {/* Preview Metadata */} -
    -
    - -

    {createdAt}

    -
    -
    - -

    {updatedAt}

    -
    -
    - - {/* Actions */} -
    - - {selectedBucket.public ? ( - - ) : ( - - - - - - onCopyUrl(file.name, URL_EXPIRY_DURATION.WEEK)} - > - Expire in 1 week - - onCopyUrl(file.name, URL_EXPIRY_DURATION.MONTH)} - > - Expire in 1 month - - onCopyUrl(file.name, URL_EXPIRY_DURATION.YEAR)} - > - Expire in 1 year - - setSelectedFileCustomExpiry(file)} - > - Custom expiry - - - - )} -
    - } - onClick={() => setSelectedItemsToDelete([file])} - tooltip={{ - content: { - side: 'bottom', - text: !canUpdateFiles - ? 'You need additional permissions to delete this file' - : undefined, - }, - }} - > - Delete file - + {/* Preview Thumbnail*/} +
    +
    +
    - - + +
    + {/* Preview Information */} +
    +
    {file.name}
    + {file.isCorrupted && ( +
    + +

    + File is corrupted, please delete and reupload this file again +

    +
    + )} + {mimeType && ( +

    + {mimeType} + {size && - {size}} +

    + )} +
    + + {/* Preview Metadata */} +
    +
    + +

    {createdAt}

    +
    +
    + +

    {updatedAt}

    +
    +
    + + {/* Actions */} +
    + + {selectedBucket.public ? ( + + ) : ( + + + + + + onCopyUrl(file.name, URL_EXPIRY_DURATION.WEEK)} + > + Expire in 1 week + + onCopyUrl(file.name, URL_EXPIRY_DURATION.MONTH)} + > + Expire in 1 month + + onCopyUrl(file.name, URL_EXPIRY_DURATION.YEAR)} + > + Expire in 1 year + + setSelectedFileCustomExpiry(file)} + > + Custom expiry + + + + )} +
    + } + onClick={() => setSelectedItemsToDelete([file])} + tooltip={{ + content: { + side: 'bottom', + text: !canUpdateFiles + ? 'You need additional permissions to delete this file' + : undefined, + }, + }} + > + Delete file + +
    +
    + ) } - -export default PreviewPane diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/StorageExplorer.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/StorageExplorer.tsx index e352dc2b9f7..d3616e59a65 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/StorageExplorer.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/StorageExplorer.tsx @@ -8,12 +8,12 @@ import { IS_PLATFORM } from 'lib/constants' import { useStorageExplorerStateSnapshot } from 'state/storage-explorer' import { STORAGE_ROW_TYPES, STORAGE_VIEWS } from '../Storage.constants' import { ConfirmDeleteModal } from './ConfirmDeleteModal' -import CustomExpiryModal from './CustomExpiryModal' -import FileExplorer from './FileExplorer' -import FileExplorerHeader from './FileExplorerHeader' -import FileExplorerHeaderSelection from './FileExplorerHeaderSelection' -import MoveItemsModal from './MoveItemsModal' -import PreviewPane from './PreviewPane' +import { CustomExpiryModal } from './CustomExpiryModal' +import { FileExplorer } from './FileExplorer' +import { FileExplorerHeader } from './FileExplorerHeader' +import { FileExplorerHeaderSelection } from './FileExplorerHeaderSelection' +import { MoveItemsModal } from './MoveItemsModal' +import { PreviewPane } from './PreviewPane' interface StorageExplorerProps { bucket: Bucket @@ -126,7 +126,7 @@ export const StorageExplorer = ({ bucket }: StorageExplorerProps) => { /** File manipulation methods */ - const onFilesUpload = async (event: any, columnIndex = -1) => { + const onFilesUpload = async (event: any, columnIndex: number = -1) => { event.persist() const items = event.target.files || event.dataTransfer.items const isDrop = !isEmpty(get(event, ['dataTransfer', 'items'], [])) diff --git a/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePoliciesBucketRow.tsx b/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePoliciesBucketRow.tsx index 1dffed34246..dc71182c82f 100644 --- a/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePoliciesBucketRow.tsx +++ b/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePoliciesBucketRow.tsx @@ -23,7 +23,7 @@ interface StoragePoliciesBucketRowProps { label: string bucket?: Bucket policies: PostgresPolicy[] - onSelectPolicyAdd: (bucketName: string, table: string) => void + onSelectPolicyAdd: (bucketName: string | undefined, table: string) => void onSelectPolicyEdit: (policy: PostgresPolicy, bucketName: string, table: string) => void onSelectPolicyDelete: (policy: PostgresPolicy) => void } @@ -45,11 +45,9 @@ export const StoragePoliciesBucketRow = ({ {label} {bucket?.public && Public}
    - {!!bucket && ( - - )} + {policies.length === 0 ? ( diff --git a/apps/studio/components/interfaces/Storage/StorageSettings/CreateCredentialModal.tsx b/apps/studio/components/interfaces/Storage/StorageSettings/CreateCredentialModal.tsx index 8d331ac886d..cb5b6de2ab9 100644 --- a/apps/studio/components/interfaces/Storage/StorageSettings/CreateCredentialModal.tsx +++ b/apps/studio/components/interfaces/Storage/StorageSettings/CreateCredentialModal.tsx @@ -9,7 +9,7 @@ import { useParams } from 'common' import { useIsProjectActive } from 'components/layouts/ProjectLayout/ProjectContext' import { useProjectStorageConfigQuery } from 'data/config/project-storage-config-query' import { useS3AccessKeyCreateMutation } from 'data/storage/s3-access-key-create-mutation' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { Button, Dialog, @@ -39,7 +39,10 @@ export const CreateCredentialModal = ({ visible, onOpenChange }: CreateCredentia const isProjectActive = useIsProjectActive() const [showSuccess, setShowSuccess] = useState(false) - const canCreateCredentials = useCheckPermissions(PermissionAction.STORAGE_ADMIN_WRITE, '*') + const { can: canCreateCredentials } = useAsyncCheckProjectPermissions( + PermissionAction.STORAGE_ADMIN_WRITE, + '*' + ) const { data: config } = useProjectStorageConfigQuery({ projectRef }) const isS3ConnectionEnabled = config?.features.s3Protocol.enabled diff --git a/apps/studio/components/interfaces/Storage/StorageSettings/S3Connection.tsx b/apps/studio/components/interfaces/Storage/StorageSettings/S3Connection.tsx index 85085e00e88..0057b4e751d 100644 --- a/apps/studio/components/interfaces/Storage/StorageSettings/S3Connection.tsx +++ b/apps/studio/components/interfaces/Storage/StorageSettings/S3Connection.tsx @@ -22,7 +22,7 @@ import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query import { useProjectStorageConfigQuery } from 'data/config/project-storage-config-query' import { useProjectStorageConfigUpdateUpdateMutation } from 'data/config/project-storage-config-update-mutation' import { useStorageCredentialsQuery } from 'data/storage/s3-access-key-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { AlertDescription_Shadcn_, @@ -54,8 +54,12 @@ export const S3Connection = () => { const [openDeleteDialog, setOpenDeleteDialog] = useState(false) const [deleteCred, setDeleteCred] = useState<{ id: string; description: string }>() - const canReadS3Credentials = useCheckPermissions(PermissionAction.STORAGE_ADMIN_READ, '*') - const canUpdateStorageSettings = useCheckPermissions(PermissionAction.STORAGE_ADMIN_WRITE, '*') + const { can: canReadS3Credentials, isLoading: isLoadingPermissions } = + useAsyncCheckProjectPermissions(PermissionAction.STORAGE_ADMIN_READ, '*') + const { can: canUpdateStorageSettings } = useAsyncCheckProjectPermissions( + PermissionAction.STORAGE_ADMIN_WRITE, + '*' + ) const { data: settings } = useProjectSettingsV2Query({ projectRef }) const { @@ -170,7 +174,7 @@ export const S3Connection = () => {
    - {!canUpdateStorageSettings && ( + {!isLoadingPermissions && !canUpdateStorageSettings && (

    You need additional permissions to update storage settings @@ -216,6 +220,7 @@ export const S3Connection = () => { +

    @@ -227,10 +232,10 @@ export const S3Connection = () => {
    - {!canReadS3Credentials ? ( - - ) : projectIsLoading ? ( + {projectIsLoading || isLoadingPermissions ? ( + ) : !canReadS3Credentials ? ( + ) : !isProjectActive ? ( diff --git a/apps/studio/components/interfaces/Storage/StorageSettings/StorageCredItem.tsx b/apps/studio/components/interfaces/Storage/StorageSettings/StorageCredItem.tsx index d1dd01b9b1b..7eb0717b6ff 100644 --- a/apps/studio/components/interfaces/Storage/StorageSettings/StorageCredItem.tsx +++ b/apps/studio/components/interfaces/Storage/StorageSettings/StorageCredItem.tsx @@ -3,7 +3,7 @@ import { differenceInDays } from 'date-fns' import { MoreVertical, TrashIcon } from 'lucide-react' import CopyButton from 'components/ui/CopyButton' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { Button, DropdownMenu, @@ -25,7 +25,10 @@ export const StorageCredItem = ({ access_key: string onDeleteClick: (id: string) => void }) => { - const canRemoveAccessKey = useCheckPermissions(PermissionAction.STORAGE_ADMIN_WRITE, '*') + const { can: canRemoveAccessKey } = useAsyncCheckProjectPermissions( + PermissionAction.STORAGE_ADMIN_WRITE, + '*' + ) function daysSince(date: string) { const now = new Date() diff --git a/apps/studio/components/interfaces/Storage/__tests__/EmptyBucketModal.test.tsx b/apps/studio/components/interfaces/Storage/__tests__/EmptyBucketModal.test.tsx index a9b79f4daa3..dd76310edc5 100644 --- a/apps/studio/components/interfaces/Storage/__tests__/EmptyBucketModal.test.tsx +++ b/apps/studio/components/interfaces/Storage/__tests__/EmptyBucketModal.test.tsx @@ -69,7 +69,7 @@ describe(`EmptyBucketModal`, () => { path: `/platform/storage/:ref/buckets/:id/empty`, }) // Called by useStorageExplorerStateSnapshot but seems - // to be unnecessary for succesful test? + // to be unnecessary for successful test? // // useProjectSettingsV2Query -> ProjectSettings // GET /platform/projects/:ref/settings diff --git a/apps/studio/components/interfaces/TableGridEditor/GridHeaderActions.tsx b/apps/studio/components/interfaces/TableGridEditor/GridHeaderActions.tsx index c4dab21d894..0322cba739a 100644 --- a/apps/studio/components/interfaces/TableGridEditor/GridHeaderActions.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/GridHeaderActions.tsx @@ -5,8 +5,9 @@ import { useState } from 'react' import { toast } from 'sonner' import { useParams } from 'common' +import { RefreshButton } from 'components/grid/components/header/RefreshButton' import { getEntityLintDetails } from 'components/interfaces/TableGridEditor/TableEntity.utils' -import APIDocsButton from 'components/ui/APIDocsButton' +import { APIDocsButton } from 'components/ui/APIDocsButton' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { useDatabasePoliciesQuery } from 'data/database-policies/database-policies-query' import { useDatabasePublicationsQuery } from 'data/database-publications/database-publications-query' @@ -45,9 +46,10 @@ import ViewEntityAutofixSecurityModal from './ViewEntityAutofixSecurityModal' export interface GridHeaderActionsProps { table: Entity + isRefetching: boolean } -const GridHeaderActions = ({ table }: GridHeaderActionsProps) => { +export const GridHeaderActions = ({ table, isRefetching }: GridHeaderActionsProps) => { const { ref } = useParams() const { data: project } = useSelectedProjectQuery() const { data: org } = useSelectedOrganizationQuery() @@ -277,13 +279,13 @@ const GridHeaderActions = ({ table }: GridHeaderActionsProps) => { -

    +

    Row Level Security (RLS) -

    -
    + +

    You can restrict and control who can read, write and update data in this table using Row Level Security. @@ -293,14 +295,13 @@ const GridHeaderActions = ({ table }: GridHeaderActionsProps) => { table.

    {!isSchemaLocked && ( -
    - -
    + )}
    @@ -443,8 +444,9 @@ const GridHeaderActions = ({ table }: GridHeaderActionsProps) => { {isTable && realtimeEnabled && ( - + {!isRealtimeEnabled && 'Enable Realtime'} + )} + {doesHaveAutoGeneratedAPIDocs && } + +
    )} {
    ) } - -export default GridHeaderActions diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ForeignKeySelector/ForeignKeySelector.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ForeignKeySelector/ForeignKeySelector.tsx index 9659674e3fc..91496959b01 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ForeignKeySelector/ForeignKeySelector.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ForeignKeySelector/ForeignKeySelector.tsx @@ -164,13 +164,7 @@ export const ForeignKeySelector = ({ // [Joshen] Doing this way so that its more readable // If either source or target not selected yet, thats okay - if (source === '' || target === '') { - return typeErrors.push(undefined) - } - - if (sourceColumn?.isNewColumn && targetType !== '') { - return typeNotice.push({ sourceType, targetType }) - } + if (source === '' || target === '') return // If source and target are in the same type of data types, thats okay if ( @@ -178,13 +172,14 @@ export const ForeignKeySelector = ({ (TEXT_TYPES.includes(sourceType) && TEXT_TYPES.includes(targetType)) || (TEXT_TYPES.includes(sourceType) && TEXT_TYPES.includes(targetType)) || (sourceType === 'uuid' && targetType === 'uuid') - ) { - return typeErrors.push(undefined) - } + ) + return // Otherwise just check if the format is equal to each other - if (sourceType === targetType) { - return typeErrors.push(undefined) + if (sourceType === targetType) return + + if (sourceColumn?.isNewColumn && targetType !== '') { + return typeNotice.push({ sourceType, targetType }) } typeErrors.push({ sourceType, targetType }) @@ -419,7 +414,7 @@ export const ForeignKeySelector = ({
  • {fk.columns[idx]?.source}{' '} - {x.targetType} + {x.targetType}
  • ) diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/ForeignRowSelector/ForeignRowSelector.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/ForeignRowSelector/ForeignRowSelector.tsx index 9a2a6ac1c87..e815f7c3873 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/ForeignRowSelector/ForeignRowSelector.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/ForeignRowSelector/ForeignRowSelector.tsx @@ -4,7 +4,7 @@ import { DndProvider } from 'react-dnd' import { HTML5Backend } from 'react-dnd-html5-backend' import { useParams } from 'common' -import RefreshButton from 'components/grid/components/header/RefreshButton' +import { RefreshButton } from 'components/grid/components/header/RefreshButton' import { FilterPopoverPrimitive } from 'components/grid/components/header/filter/FilterPopoverPrimitive' import { SortPopoverPrimitive } from 'components/grid/components/header/sort/SortPopoverPrimitive' import type { Filter, Sort } from 'components/grid/types' diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetImport.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetImport.tsx index c9e2c748059..9eee2b10105 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetImport.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetImport.tsx @@ -109,9 +109,8 @@ const SpreadsheetImport = ({ const [file] = event.target.files || event.dataTransfer.files if (file && !flagInvalidFileImport(file)) { await processFile(file) - } else { - event.target.value = '' } + event.target.value = '' }, [processFile] ) diff --git a/apps/studio/components/interfaces/TableGridEditor/TableDefinition.tsx b/apps/studio/components/interfaces/TableGridEditor/TableDefinition.tsx index 45e81d6abf8..f424dc65ba4 100644 --- a/apps/studio/components/interfaces/TableGridEditor/TableDefinition.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/TableDefinition.tsx @@ -4,7 +4,7 @@ import Link from 'next/link' import { useMemo, useRef } from 'react' import { useParams } from 'common' -import Footer from 'components/grid/components/footer/Footer' +import { Footer } from 'components/grid/components/footer/Footer' import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useTableDefinitionQuery } from 'data/database/table-definition-query' import { useViewDefinitionQuery } from 'data/database/view-definition-query' @@ -24,7 +24,7 @@ export interface TableDefinitionProps { entity?: Entity } -const TableDefinition = ({ entity }: TableDefinitionProps) => { +export const TableDefinition = ({ entity }: TableDefinitionProps) => { const { ref } = useParams() const editorRef = useRef(null) const monacoRef = useRef(null) @@ -132,5 +132,3 @@ const TableDefinition = ({ entity }: TableDefinitionProps) => { ) } - -export default TableDefinition diff --git a/apps/studio/components/interfaces/TableGridEditor/TableEntity.utils.ts b/apps/studio/components/interfaces/TableGridEditor/TableEntity.utils.ts index 41afdff8602..7d208f3b351 100644 --- a/apps/studio/components/interfaces/TableGridEditor/TableEntity.utils.ts +++ b/apps/studio/components/interfaces/TableGridEditor/TableEntity.utils.ts @@ -39,12 +39,21 @@ export const formatTableRowsToSQL = (table: SupaTable, rows: any[]) => { // We only check for NULL, array and JSON types, everything else we stringify // given that Postgres can implicitly cast the right type based on the column type + // For string types, we need to deal with escaping single quotes + const stringFormats = ['text', 'varchar'] + if (val === null) { return 'null' } else if (dataType === 'ARRAY') { return `'${JSON.stringify(val).replace('[', '{').replace(/.$/, '}')}'` } else if (format?.includes('json')) { return `${JSON.stringify(val).replace(/\\"/g, '"').replace(/'/g, "''").replace('"', "'").replace(/.$/, "'")}` + } else if ( + typeof format === 'string' && + typeof val === 'string' && + stringFormats.includes(format) + ) { + return `'${val.replaceAll("'", "''")}'` } else { return `'${val}'` } diff --git a/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx b/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx index d1bff23294c..f0c1904650a 100644 --- a/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx @@ -22,7 +22,7 @@ import { Button } from 'ui' import { Admonition, GenericSkeletonLoader } from 'ui-patterns' import DeleteConfirmationDialogs from './DeleteConfirmationDialogs' import SidePanelEditor from './SidePanelEditor/SidePanelEditor' -import TableDefinition from './TableDefinition' +import { TableDefinition } from './TableDefinition' export interface TableGridEditorProps { isLoadingSelectedTable?: boolean diff --git a/apps/studio/components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout.tsx b/apps/studio/components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout.tsx index ae035f65138..ef94be5b6d2 100644 --- a/apps/studio/components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout.tsx +++ b/apps/studio/components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout.tsx @@ -9,7 +9,7 @@ import { useParams } from 'common' import { useIsAPIDocsSidePanelEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' import { EdgeFunctionTesterSheet } from 'components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionTesterSheet' import { PageLayout } from 'components/layouts/PageLayout/PageLayout' -import APIDocsButton from 'components/ui/APIDocsButton' +import { APIDocsButton } from 'components/ui/APIDocsButton' import { DocsButton } from 'components/ui/DocsButton' import NoPermission from 'components/ui/NoPermission' import { useEdgeFunctionBodyQuery } from 'data/edge-functions/edge-function-body-query' diff --git a/apps/studio/components/layouts/TableEditorLayout/EntityListItem.tsx b/apps/studio/components/layouts/TableEditorLayout/EntityListItem.tsx index f835be98f68..84dff45cae3 100644 --- a/apps/studio/components/layouts/TableEditorLayout/EntityListItem.tsx +++ b/apps/studio/components/layouts/TableEditorLayout/EntityListItem.tsx @@ -210,11 +210,13 @@ const EntityListItem: ItemRenderer = ({ connectionString: project?.connectionString, table: supaTable, }) + const formattedRows = rows.map((row) => { - const formattedRow = row - Object.keys(row).map((column) => { - if (typeof row[column] === 'object' && row[column] !== null) - formattedRow[column] = JSON.stringify(formattedRow[column]) + const formattedRow = { ...row } + Object.keys(row).forEach((column) => { + if (typeof row[column] === 'object' && row[column] !== null) { + formattedRow[column] = JSON.stringify(row[column]) + } }) return formattedRow }) diff --git a/apps/studio/components/ui/APIDocsButton.tsx b/apps/studio/components/ui/APIDocsButton.tsx index dcc0a5b2b3a..4a2f4971e49 100644 --- a/apps/studio/components/ui/APIDocsButton.tsx +++ b/apps/studio/components/ui/APIDocsButton.tsx @@ -1,27 +1,30 @@ -import { Code } from 'lucide-react' +import { BookOpenText } from 'lucide-react' import { useAppStateSnapshot } from 'state/app-state' -import { Button } from 'ui' +import { ButtonTooltip } from './ButtonTooltip' interface APIDocsButtonProps { section?: string[] } -const APIDocsButton = ({ section }: APIDocsButtonProps) => { +export const APIDocsButton = ({ section }: APIDocsButtonProps) => { const snap = useAppStateSnapshot() return ( - + icon={} + className="h-7 w-7" + tooltip={{ + content: { + side: 'bottom', + text: 'API Docs', + }, + }} + /> ) } - -export default APIDocsButton diff --git a/apps/studio/components/ui/Charts/ReportSettings.tsx b/apps/studio/components/ui/Charts/ReportSettings.tsx index df6511047b3..5fc052c620b 100644 --- a/apps/studio/components/ui/Charts/ReportSettings.tsx +++ b/apps/studio/components/ui/Charts/ReportSettings.tsx @@ -1,9 +1,15 @@ +import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { Settings } from 'lucide-react' import { useState } from 'react' -import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger, Label_Shadcn_, Switch } from 'ui' -import { ButtonTooltip } from 'components/ui/ButtonTooltip' +import { + cn, + DropdownMenu, + DropdownMenuContent, + DropdownMenuTrigger, + Label_Shadcn_, + Switch, +} from 'ui' import { useChartHoverState } from './useChartHoverState' -import { cn } from 'ui' interface ReportSettingsProps { chartId: string @@ -23,7 +29,7 @@ export const ReportSettings = ({ chartId }: ReportSettingsProps) => { tooltip={{ content: { side: 'bottom', text: 'Report settings' } }} />
    - +
    diff --git a/apps/studio/components/ui/Charts/StackedBarChart.tsx b/apps/studio/components/ui/Charts/StackedBarChart.tsx index ac12386f3b2..4d362d9b25d 100644 --- a/apps/studio/components/ui/Charts/StackedBarChart.tsx +++ b/apps/studio/components/ui/Charts/StackedBarChart.tsx @@ -80,7 +80,17 @@ const StackedBarChart: React.FC = ({ const resolvedHighlightedValue = focusDataIndex !== null ? data[focusDataIndex]?.[yAxisKey] : highlightedValue - if (!data || data.length === 0) return + if (!data || data.length === 0) { + return ( + + ) + } + const stackColorScales = genStackColorScales(stackColors) return (
    diff --git a/apps/studio/components/ui/Charts/useChartHoverState.test.tsx b/apps/studio/components/ui/Charts/useChartHoverState.test.tsx index aac7c8c8b21..7d0d7b1f0fa 100644 --- a/apps/studio/components/ui/Charts/useChartHoverState.test.tsx +++ b/apps/studio/components/ui/Charts/useChartHoverState.test.tsx @@ -43,8 +43,8 @@ describe('useChartHoverState', () => { const { result } = renderHook(() => useChartHoverState('chart1')) expect(result.current.hoveredIndex).toBe(null) - expect(result.current.syncHover).toBe(false) - expect(result.current.syncTooltip).toBe(false) + expect(result.current.syncHover).toBe(true) + expect(result.current.syncTooltip).toBe(true) expect(result.current.hoveredChart).toBe(null) expect(result.current.isHovered).toBe(false) expect(result.current.isCurrentChart).toBe(false) @@ -76,8 +76,8 @@ describe('useChartHoverState', () => { const { result } = renderHook(() => useChartHoverStateWithCorrupted('chart1')) - expect(result.current.syncHover).toBe(false) - expect(result.current.syncTooltip).toBe(false) + expect(result.current.syncHover).toBe(true) + expect(result.current.syncTooltip).toBe(true) expect(consoleWarnSpy).toHaveBeenCalled() }) }) @@ -301,7 +301,7 @@ describe('useChartHoverState', () => { const initialSyncHover = result.current.syncHover act(() => { - result.current.setSyncHover(false) + result.current.setSyncHover(initialSyncHover) }) expect(result.current.syncHover).toBe(initialSyncHover) diff --git a/apps/studio/components/ui/Charts/useChartHoverState.tsx b/apps/studio/components/ui/Charts/useChartHoverState.tsx index 0bbb2b6160f..672706d4a5a 100644 --- a/apps/studio/components/ui/Charts/useChartHoverState.tsx +++ b/apps/studio/components/ui/Charts/useChartHoverState.tsx @@ -14,8 +14,8 @@ const CHART_TOOLTIP_SYNC_STORAGE_KEY = 'supabase-chart-tooltip-sync-enabled' let globalState: ChartHoverState = { hoveredIndex: null, hoveredChart: null, - syncHover: false, - syncTooltip: false, + syncHover: true, + syncTooltip: true, } // Subscribers for state changes diff --git a/apps/studio/csp.js b/apps/studio/csp.js index 3175bd5096b..5c94f4093e1 100644 --- a/apps/studio/csp.js +++ b/apps/studio/csp.js @@ -33,6 +33,10 @@ const isDevOrStaging = const NIMBUS_STAGING_PROJECTS_URL = 'https://*.nmb-proj.com' const NIMBUS_STAGING_PROJECTS_URL_WS = 'wss://*.nmb-proj.com' + +const NIMBUS_PROD_PROJECTS_URL = process.env.NIMBUS_PROD_PROJECTS_URL || '' +const NIMBUS_PROD_PROJECTS_URL_WS = process.env.NIMBUS_PROD_PROJECTS_URL_WS || '' + const SUPABASE_STAGING_PROJECTS_URL = 'https://*.supabase.red' const SUPABASE_STAGING_PROJECTS_URL_WS = 'wss://*.supabase.red' const SUPABASE_COM_URL = 'https://supabase.com' @@ -98,6 +102,7 @@ module.exports.getCSP = function getCSP() { STAPE_URL, GOOGLE_MAPS_API_URL, POSTHOG_URL, + ...(!!NIMBUS_PROD_PROJECTS_URL ? [NIMBUS_PROD_PROJECTS_URL, NIMBUS_PROD_PROJECTS_URL_WS] : []), ] const SCRIPT_SRC_URLS = [ CLOUDFLARE_CDN_URL, @@ -117,6 +122,7 @@ module.exports.getCSP = function getCSP() { SUPABASE_ASSETS_URL, USERCENTRICS_APP_URL, STAPE_URL, + ...(!!NIMBUS_PROD_PROJECTS_URL ? [NIMBUS_PROD_PROJECTS_URL, NIMBUS_PROD_PROJECTS_URL_WS] : []), ] const STYLE_SRC_URLS = [CLOUDFLARE_CDN_URL, SUPABASE_ASSETS_URL] const FONT_SRC_URLS = [CLOUDFLARE_CDN_URL, SUPABASE_ASSETS_URL] diff --git a/apps/studio/data/analytics/functions-combined-stats-query.ts b/apps/studio/data/analytics/functions-combined-stats-query.ts new file mode 100644 index 00000000000..edb4c095162 --- /dev/null +++ b/apps/studio/data/analytics/functions-combined-stats-query.ts @@ -0,0 +1,70 @@ +import { useQuery, UseQueryOptions } from '@tanstack/react-query' +import { operations } from 'api-types' +import { get, handleError } from 'data/fetchers' +import { analyticsKeys } from './keys' + +export type FunctionsCombinedStatsVariables = { + projectRef?: string + functionId?: string + interval?: operations['FunctionsLogsController_getCombinedStats']['parameters']['query']['interval'] +} + +export type FunctionsCombinedStatsResponse = any + +export async function getFunctionsCombinedStats( + { projectRef, functionId, interval }: FunctionsCombinedStatsVariables, + signal?: AbortSignal +) { + if (!projectRef) { + throw new Error('projectRef is required') + } + if (!functionId) { + throw new Error('functionId is required') + } + if (!interval) { + throw new Error('interval is required') + } + + const { data, error } = await get( + '/platform/projects/{ref}/analytics/endpoints/functions.combined-stats', + { + params: { + path: { + ref: projectRef, + }, + query: { + function_id: functionId, + interval, + }, + }, + signal, + } + ) + + if (error) handleError(error) + + return data +} + +export type FunctionsCombinedStatsData = Awaited> +export type FunctionsCombinedStatsError = unknown + +export const useFunctionsCombinedStatsQuery = ( + { projectRef, functionId, interval }: FunctionsCombinedStatsVariables, + { + enabled = true, + ...options + }: UseQueryOptions = {} +) => + useQuery( + analyticsKeys.functionsCombinedStats(projectRef, { functionId, interval }), + ({ signal }) => getFunctionsCombinedStats({ projectRef, functionId, interval }, signal), + { + enabled: + enabled && + typeof projectRef !== 'undefined' && + typeof functionId !== 'undefined' && + typeof interval !== 'undefined', + ...options, + } + ) diff --git a/apps/studio/data/analytics/keys.ts b/apps/studio/data/analytics/keys.ts index fa77be8e2c4..17e8d7ce14c 100644 --- a/apps/studio/data/analytics/keys.ts +++ b/apps/studio/data/analytics/keys.ts @@ -1,5 +1,24 @@ export const analyticsKeys = { // logs/reports endpoints + functionsCombinedStats: ( + projectRef: string | undefined, + { + interval, + functionId, + }: { + functionId: string | undefined + interval: string | undefined + } + ) => + [ + 'projects', + projectRef, + 'functions-combined-stats', + { + interval, + functionId, + }, + ] as const, functionsInvStats: ( projectRef: string | undefined, { diff --git a/apps/studio/data/replication/update-destination-pipeline-mutation.ts b/apps/studio/data/replication/update-destination-pipeline-mutation.ts index 2c04b48db4c..c96719e1df3 100644 --- a/apps/studio/data/replication/update-destination-pipeline-mutation.ts +++ b/apps/studio/data/replication/update-destination-pipeline-mutation.ts @@ -97,11 +97,15 @@ export const useUpdateDestinationPipelineMutation = ({ (vars) => updateDestinationPipeline(vars), { async onSuccess(data, variables, context) { - const { projectRef } = variables + const { projectRef, destinationId, pipelineId } = variables await Promise.all([ + // Invalidate lists queryClient.invalidateQueries(replicationKeys.destinations(projectRef)), queryClient.invalidateQueries(replicationKeys.pipelines(projectRef)), + // Invalidate item-level caches used by the editor panel + queryClient.invalidateQueries(replicationKeys.destinationById(projectRef, destinationId)), + queryClient.invalidateQueries(replicationKeys.pipelineById(projectRef, pipelineId)), ]) await onSuccess?.(data, variables, context) diff --git a/apps/studio/data/reports/v2/edge-functions.config.ts b/apps/studio/data/reports/v2/edge-functions.config.ts index 2e65c925232..64d7307f1ce 100644 --- a/apps/studio/data/reports/v2/edge-functions.config.ts +++ b/apps/studio/data/reports/v2/edge-functions.config.ts @@ -12,11 +12,51 @@ import { } from 'data/reports/report.utils' import { getHttpStatusCodeInfo } from 'lib/http-status-codes' import { ReportConfig } from './reports.types' +import { NumericFilter } from 'components/interfaces/Reports/v2/ReportsNumericFilter' +import { SelectFilters } from 'components/interfaces/Reports/v2/ReportsSelectFilter' -const METRIC_SQL: Record string> = - { - TotalInvocations: (interval, functionIds) => { - return ` +type EdgeFunctionReportFilters = { + status_code: NumericFilter | null + region: SelectFilters + execution_time: NumericFilter | null + functions: SelectFilters +} + +export function filterToWhereClause(filters?: EdgeFunctionReportFilters): string { + const whereClauses: string[] = [] + + if (filters?.functions && filters.functions.length > 0) { + whereClauses.push(`function_id IN (${filters.functions.map((id) => `'${id}'`).join(',')})`) + } + + if (filters?.status_code) { + whereClauses.push( + `response.status_code ${filters.status_code.operator} ${filters.status_code.value}` + ) + } + + if (filters?.region && filters.region.length > 0) { + whereClauses.push( + `h.x_sb_edge_region IN (${filters.region.map((region) => `'${region}'`).join(',')})` + ) + } + + if (filters?.execution_time) { + whereClauses.push( + `m.execution_time_ms ${filters.execution_time.operator} ${filters.execution_time.value}` + ) + } + + return whereClauses.length > 0 ? `WHERE ${whereClauses.join(' AND ')}` : '' +} + +const METRIC_SQL: Record< + string, + (interval: AnalyticsInterval, filters?: EdgeFunctionReportFilters) => string +> = { + TotalInvocations: (interval, filters) => { + const whereClause = filterToWhereClause(filters) + return ` --edgefn-report-invocations select timestamp_trunc(timestamp, ${analyticsIntervalToGranularity(interval)}) as timestamp, @@ -27,20 +67,18 @@ from CROSS JOIN UNNEST(metadata) AS m CROSS JOIN UNNEST(m.request) AS request CROSS JOIN UNNEST(m.response) AS response - ${ - functionIds && functionIds.length > 0 - ? `WHERE function_id IN (${functionIds.map((id) => `'${id}'`).join(',')})` - : '' - } + CROSS JOIN UNNEST(response.headers) AS h + ${whereClause} group by timestamp, function_id order by timestamp desc; ` - }, - ExecutionStatusCodes: (interval, functionIds) => { - return ` + }, + ExecutionStatusCodes: (interval, filters) => { + const whereClause = filterToWhereClause(filters) + return ` --edgefn-report-execution-status-codes select timestamp_trunc(timestamp, ${analyticsIntervalToGranularity(interval)}) as timestamp, @@ -50,21 +88,24 @@ from function_edge_logs cross join unnest(metadata) as m cross join unnest(m.response) as response - ${ - functionIds && functionIds.length > 0 - ? `where function_id in (${functionIds.map((id) => `'${id}'`).join(',')})` - : '' - } + cross join unnest(response.headers) as h + ${whereClause} group by timestamp, status_code order by timestamp desc ` - }, - InvocationsByRegion: (interval, functionIds) => { - const granularity = analyticsIntervalToGranularity(interval) - return ` + }, + InvocationsByRegion: (interval, filters) => { + const granularity = analyticsIntervalToGranularity(interval) + const whereClause = filterToWhereClause(filters) + const hasWhere = whereClause.includes('WHERE') + const regionCondition = hasWhere + ? 'AND h.x_sb_edge_region is not null' + : 'WHERE h.x_sb_edge_region is not null' + + return ` --edgefn-report-invocations-by-region select timestamp_trunc(timestamp, ${granularity}) as timestamp, @@ -75,41 +116,40 @@ from cross join unnest(metadata) as m cross join unnest(m.response) as r cross join unnest(r.headers) as h - where h.x_sb_edge_region is not null - ${ - functionIds && functionIds.length > 0 - ? `and function_id IN (${functionIds.map((id) => `'${id}'`).join(',')})` - : '' - } + ${whereClause} + ${regionCondition} group by timestamp, region order by timestamp desc ` - }, - ExecutionTime: (interval, functionIds) => { - const granularity = analyticsIntervalToGranularity(interval) - const hasFunctions = functionIds && functionIds.length > 0 - return ` + }, + ExecutionTime: (interval, filters) => { + const granularity = analyticsIntervalToGranularity(interval) + const whereClause = filterToWhereClause(filters) + + return ` --edgefn-report-execution-time select timestamp_trunc(timestamp, ${granularity}) as timestamp, - ${hasFunctions ? 'function_id,' : ''} + function_id, avg(m.execution_time_ms) as avg_execution_time from function_edge_logs cross join unnest(metadata) as m cross join unnest(m.request) as request - ${hasFunctions ? `where function_id IN (${functionIds.map((id) => `'${id}'`).join(',')})` : ''} + cross join unnest(m.response) as response + cross join unnest(response.headers) as h + ${whereClause} group by - timestamp - ${hasFunctions ? ', function_id' : ''} + timestamp, + function_id order by timestamp desc ` - }, - } + }, +} async function runQuery(projectRef: string, sql: string, startDate: string, endDate: string) { const { data, error } = await get(`/platform/projects/{ref}/analytics/endpoints/logs.all`, { @@ -216,10 +256,8 @@ export const edgeFunctionReports = ({ startDate: string endDate: string interval: AnalyticsInterval - filters: { - functionIds?: string[] - } -}): ReportConfig[] => [ + filters: EdgeFunctionReportFilters +}): ReportConfig[] => [ { id: 'total-invocations', label: 'Total Edge Function Invocations', @@ -233,7 +271,7 @@ export const edgeFunctionReports = ({ titleTooltip: 'The total number of edge function invocations over time.', availableIn: ['free', 'pro', 'team', 'enterprise'], dataProvider: async () => { - const sql = METRIC_SQL.TotalInvocations(interval, filters.functionIds) + const sql = METRIC_SQL.TotalInvocations(interval, filters) const response = await runQuery(projectRef, sql, startDate, endDate) if (!response?.result) return { data: [] } @@ -264,14 +302,8 @@ export const edgeFunctionReports = ({ defaultChartStyle: 'line', titleTooltip: 'The total number of edge function executions by status code.', availableIn: ['free', 'pro', 'team', 'enterprise'], - dataProvider: async ( - projectRef: string, - startDate: string, - endDate: string, - interval: AnalyticsInterval, - functionIds?: string[] - ) => { - const sql = METRIC_SQL.ExecutionStatusCodes(interval, functionIds) + dataProvider: async () => { + const sql = METRIC_SQL.ExecutionStatusCodes(interval, filters) const rawData = await runQuery(projectRef, sql, startDate, endDate) if (!rawData?.result) return { data: [] } @@ -308,7 +340,7 @@ export const edgeFunctionReports = ({ }, format: (value: unknown) => `${Number(value).toFixed(0)}ms`, dataProvider: async () => { - const sql = METRIC_SQL.ExecutionTime(interval, filters.functionIds) + const sql = METRIC_SQL.ExecutionTime(interval, filters) const rawData = await runQuery(projectRef, sql, startDate, endDate) if (!rawData?.result) return { data: [] } @@ -365,7 +397,7 @@ export const edgeFunctionReports = ({ titleTooltip: 'The total number of edge function invocations by region.', availableIn: ['pro', 'team', 'enterprise'], dataProvider: async () => { - const sql = METRIC_SQL.InvocationsByRegion(interval, filters.functionIds) + const sql = METRIC_SQL.InvocationsByRegion(interval, filters) const rawData = await runQuery(projectRef, sql, startDate, endDate) const data = rawData.result?.map((point: any) => ({ ...point, diff --git a/apps/studio/data/reports/v2/edge-functions.test.tsx b/apps/studio/data/reports/v2/edge-functions.test.tsx index e1217d7b7a0..d02f656d39c 100644 --- a/apps/studio/data/reports/v2/edge-functions.test.tsx +++ b/apps/studio/data/reports/v2/edge-functions.test.tsx @@ -5,8 +5,16 @@ import { transformStatusCodeData, transformInvocationData, aggregateInvocationsByTimestamp, + filterToWhereClause, } from './edge-functions.config' +const defaultFilters = { + status_code: null, + region: [], + execution_time: null, + functions: [], +} + describe('extractStatusCodesFromData', () => { it('should extract and sort unique status codes from the data', () => { const data = [ @@ -249,3 +257,172 @@ describe('aggregateInvocationsByTimestamp', () => { ]) }) }) + +describe('filterToWhereClause', () => { + it('should return empty string when no filters are provided', () => { + const result = filterToWhereClause() + expect(result).toBe('') + }) + + it('should return empty string when filters object is empty', () => { + const result = filterToWhereClause(defaultFilters) + expect(result).toBe('') + }) + + it('should generate WHERE clause for functions filter', () => { + const filters = { + ...defaultFilters, + functions: ['func1', 'func2'], + } + const result = filterToWhereClause(filters) + expect(result).toBe("WHERE function_id IN ('func1','func2')") + }) + + it('should generate WHERE clause for status_code filter', () => { + const filters = { + ...defaultFilters, + status_code: { + operator: '>=' as const, + value: 400, + }, + } + const result = filterToWhereClause(filters) + expect(result).toBe('WHERE response.status_code >= 400') + }) + + it('should generate WHERE clause for region filter', () => { + const filters = { + ...defaultFilters, + region: ['us-east-1', 'eu-west-1'], + } + const result = filterToWhereClause(filters) + expect(result).toBe("WHERE h.x_sb_edge_region IN ('us-east-1','eu-west-1')") + }) + + it('should generate WHERE clause for execution_time filter', () => { + const filters = { + ...defaultFilters, + execution_time: { + operator: '<' as const, + value: 1000, + }, + } + const result = filterToWhereClause(filters) + expect(result).toBe('WHERE m.execution_time_ms < 1000') + }) + + it('should combine multiple filters with AND', () => { + const filters = { + functions: ['func1'], + status_code: { + operator: '=' as const, + value: 200, + }, + region: ['us-east-1'], + execution_time: { + operator: '<=' as const, + value: 500, + }, + } + const result = filterToWhereClause(filters) + expect(result).toBe( + "WHERE function_id IN ('func1') AND response.status_code = 200 AND h.x_sb_edge_region IN ('us-east-1') AND m.execution_time_ms <= 500" + ) + }) + + it('should handle functions filter with no selected functions', () => { + const filters = { + ...defaultFilters, + functions: [], + } + const result = filterToWhereClause(filters) + expect(result).toBe('') + }) + + it('should handle region filter with no selected regions', () => { + const filters = { + ...defaultFilters, + region: [], + } + const result = filterToWhereClause(filters) + expect(result).toBe('') + }) + + it('should handle single function selection', () => { + const filters = { + ...defaultFilters, + functions: ['single-func'], + } + const result = filterToWhereClause(filters) + expect(result).toBe("WHERE function_id IN ('single-func')") + }) + + it('should handle single region selection', () => { + const filters = { + ...defaultFilters, + region: ['single-region'], + } + const result = filterToWhereClause(filters) + expect(result).toBe("WHERE h.x_sb_edge_region IN ('single-region')") + }) + + it('should handle all comparison operators for status_code', () => { + const operators = ['=', '!=', '>', '>=', '<', '<='] as const + + operators.forEach((operator) => { + const filters = { + ...defaultFilters, + status_code: { + operator, + value: 200, + }, + } + const result = filterToWhereClause(filters) + expect(result).toBe(`WHERE response.status_code ${operator} 200`) + }) + }) + + it('should handle all comparison operators for execution_time', () => { + const operators = ['=', '!=', '>', '>=', '<', '<='] as const + + operators.forEach((operator) => { + const filters = { + ...defaultFilters, + execution_time: { + operator, + value: 100, + }, + } + const result = filterToWhereClause(filters) + expect(result).toBe(`WHERE m.execution_time_ms ${operator} 100`) + }) + }) + + it('should handle numeric values correctly', () => { + const filters = { + ...defaultFilters, + status_code: { + operator: '>' as const, + value: 0, + }, + execution_time: { + operator: '>=' as const, + value: 1.5, + }, + } + const result = filterToWhereClause(filters) + expect(result).toBe('WHERE response.status_code > 0 AND m.execution_time_ms >= 1.5') + }) + + it('should handle special characters in function IDs and regions', () => { + const filters = { + ...defaultFilters, + functions: ['func-with-dash', 'func_with_underscore', 'func.with.dots'], + region: ['region-with-dash', 'region_with_underscore'], + } + const result = filterToWhereClause(filters) + expect(result).toBe( + "WHERE function_id IN ('func-with-dash','func_with_underscore','func.with.dots') AND h.x_sb_edge_region IN ('region-with-dash','region_with_underscore')" + ) + }) +}) diff --git a/apps/studio/data/reports/v2/reports.types.ts b/apps/studio/data/reports/v2/reports.types.ts index 5edd0b23791..cfa86deab93 100644 --- a/apps/studio/data/reports/v2/reports.types.ts +++ b/apps/studio/data/reports/v2/reports.types.ts @@ -1,19 +1,13 @@ import { AnalyticsInterval } from 'data/analytics/constants' import { YAxisProps } from 'recharts' -type ReportDataProviderFilter = { - functionIds?: string[] -} - -export interface ReportDataProvider { +export interface ReportDataProvider { ( projectRef: string, startDate: string, endDate: string, interval: AnalyticsInterval, - functionIds?: string[], - edgeFnIdToName?: (id: string) => string | undefined, - filters?: ReportDataProviderFilter[] + filters?: FiltersType ): Promise<{ data: any attributes?: { @@ -25,10 +19,10 @@ export interface ReportDataProvider { }> // [jordi] would be cool to have a type that forces data keys to match the attributes } -export interface ReportConfig { +export interface ReportConfig { id: string label: string - dataProvider: ReportDataProvider + dataProvider: ReportDataProvider valuePrecision: number hide: boolean showTooltip: boolean diff --git a/apps/studio/middleware.ts b/apps/studio/middleware.ts index 0e34bbb48f2..5327cb589e3 100644 --- a/apps/studio/middleware.ts +++ b/apps/studio/middleware.ts @@ -13,6 +13,7 @@ const HOSTED_SUPPORTED_API_URLS = [ '/ai/sql/title-v2', '/ai/onboarding/design', '/ai/feedback/classify', + '/ai/docs', '/get-ip-address', '/get-utc-time', '/get-deployment-commit', diff --git a/apps/studio/pages/api/ai/docs.ts b/apps/studio/pages/api/ai/docs.ts new file mode 100644 index 00000000000..21aaf2da7fa --- /dev/null +++ b/apps/studio/pages/api/ai/docs.ts @@ -0,0 +1,153 @@ +import { SupabaseClient } from '@supabase/supabase-js' +import { ApplicationError, UserError, clippy } from 'ai-commands/edge' +import { NextRequest } from 'next/server' +import OpenAI from 'openai' + +export const config = { + runtime: 'edge', + /* To avoid OpenAI errors, restrict to the Vercel Edge Function regions that + overlap with the OpenAI API regions. + + Reference for Vercel regions: https://vercel.com/docs/edge-network/regions#region-list + Reference for OpenAI regions: https://help.openai.com/en/articles/5347006-openai-api-supported-countries-and-territories + */ + regions: [ + 'arn1', + 'bom1', + 'cdg1', + 'cle1', + 'cpt1', + 'dub1', + 'fra1', + 'gru1', + 'hnd1', + 'iad1', + 'icn1', + 'kix1', + 'lhr1', + 'pdx1', + 'sfo1', + 'sin1', + 'syd1', + ], +} + +const openAiKey = process.env.OPENAI_API_KEY +const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL +const supabaseServiceKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY + +export default async function handler(req: NextRequest) { + console.log('AI Docs request received', supabaseUrl, supabaseServiceKey) + if (!openAiKey) { + return new Response( + JSON.stringify({ + error: 'No OPENAI_API_KEY set. Create this environment variable to use AI features.', + }), + { + status: 500, + headers: { 'Content-Type': 'application/json' }, + } + ) + } + + if (!supabaseUrl) { + return new Response( + JSON.stringify({ + error: + 'No NEXT_PUBLIC_SUPABASE_URL set. Create this environment variable to use AI features.', + }), + { + status: 500, + headers: { 'Content-Type': 'application/json' }, + } + ) + } + + if (!supabaseServiceKey) { + return new Response( + JSON.stringify({ + error: + 'No NEXT_PUBLIC_SUPABASE_ANON_KEY set. Create this environment variable to use AI features.', + }), + { + status: 500, + headers: { 'Content-Type': 'application/json' }, + } + ) + } + + const { method } = req + + switch (method) { + case 'POST': + return handlePost(req) + default: + return new Response( + JSON.stringify({ data: null, error: { message: `Method ${method} Not Allowed` } }), + { + status: 405, + headers: { 'Content-Type': 'application/json', Allow: 'POST' }, + } + ) + } +} + +async function handlePost(request: NextRequest) { + const openai = new OpenAI({ apiKey: openAiKey }) + + const body = await (request.json() as Promise<{ + messages: { content: string; role: 'user' | 'assistant' }[] + }>) + + const { messages } = body + + if (!messages) { + throw new UserError('Missing messages in request data') + } + + const supabaseClient = new SupabaseClient(supabaseUrl!, supabaseServiceKey!) + + try { + const response = await clippy(openai, supabaseClient, messages) + + // Proxy the streamed SSE response from OpenAI + return new Response(response.body, { + headers: { + 'Content-Type': 'text/event-stream', + }, + }) + } catch (error: unknown) { + console.error(error) + if (error instanceof UserError) { + return new Response( + JSON.stringify({ + error: error.message, + data: error.data, + }), + { + status: 400, + headers: { 'Content-Type': 'application/json' }, + } + ) + } else if (error instanceof ApplicationError) { + // Print out application errors with their additional data + console.error(`${error.message}: ${JSON.stringify(error.data)}`) + } else { + // Print out unexpected errors as is to help with debugging + console.error(error) + } + + console.log('Returning generic 500 ApplicationError to client') + + // TODO: include more response info in debug environments + return new Response( + JSON.stringify({ + error: 'There was an error processing your request', + }), + { + status: 500, + headers: { 'Content-Type': 'application/json' }, + } + ) + } +} diff --git a/apps/studio/pages/api/v1/projects/[ref]/database/migrations.ts b/apps/studio/pages/api/v1/projects/[ref]/database/migrations.ts new file mode 100644 index 00000000000..22d43e313b6 --- /dev/null +++ b/apps/studio/pages/api/v1/projects/[ref]/database/migrations.ts @@ -0,0 +1,100 @@ +import { NextApiRequest, NextApiResponse } from 'next' + +import { fetchPost } from 'data/fetchers' +import { constructHeaders } from 'lib/api/apiHelpers' +import apiWrapper from 'lib/api/apiWrapper' +import { PG_META_URL } from 'lib/constants' +import { makeRandomString } from 'lib/helpers' + +export default (req: NextApiRequest, res: NextApiResponse) => + apiWrapper(req, res, handler, { withAuth: true }) + +async function handler(req: NextApiRequest, res: NextApiResponse) { + const { method } = req + + switch (method) { + case 'GET': + return handleGetAll(req, res) + case 'POST': + return handlePost(req, res) + default: + res.setHeader('Allow', ['GET', 'POST']) + res.status(405).json({ error: { message: `Method ${method} Not Allowed` } }) + } +} +const listMigrationVersions = + 'select version, name from supabase_migrations.schema_migrations order by version' + +const handleGetAll = async (req: NextApiRequest, res: NextApiResponse) => { + const headers = constructHeaders(req.headers) + + const response = await fetchPost( + `${PG_META_URL}/query`, + { query: listMigrationVersions }, + { headers } + ) + + if (response.error) { + const { code, message } = response.error + return res.status(code).json({ message }) + } else { + return res.status(200).json(response) + } +} + +export const initialiseHistoryTable = `begin; + +create schema if not exists supabase_migrations; +create table if not exists supabase_migrations.schema_migrations (version text not null primary key); +alter table supabase_migrations.schema_migrations add column if not exists statements text[]; +alter table supabase_migrations.schema_migrations add column if not exists name text; + +commit;` + +export function applyAndTrackMigrations(query: string, name?: string) { + // Escapes literals using postgres dollar quoted string + const dollar = `$${makeRandomString(20)}$` + const quote = (s?: string) => (s ? dollar + s + dollar : `''`) + return `begin; + +-- apply sql from post body +${query}; + +-- track statements in history table +insert into supabase_migrations.schema_migrations (version, name, statements) +values ( + to_char(current_timestamp, 'YYYYMMDDHHMISS'), + ${quote(name)}, + array[${quote(query)}] +); + +commit;` +} + +const handlePost = async (req: NextApiRequest, res: NextApiResponse) => { + const headers = constructHeaders(req.headers) + + const { error } = await fetchPost( + `${PG_META_URL}/query`, + { query: initialiseHistoryTable }, + { headers } + ) + if (error) { + const { code, message } = error + return res.status(code).json({ message, formattedError: message }) + } + + const { query, name } = req.body + const response = await fetchPost( + `${PG_META_URL}/query`, + { query: applyAndTrackMigrations(query, name) }, + { headers } + ) + + if (response.error) { + const { code, message } = response.error + return res.status(code).json({ message, formattedError: message }) + } else { + return res.status(200).json(response) + } +} diff --git a/apps/studio/pages/project/[ref]/functions/[functionSlug]/index.tsx b/apps/studio/pages/project/[ref]/functions/[functionSlug]/index.tsx index dc375da3456..dc9cd65fb22 100644 --- a/apps/studio/pages/project/[ref]/functions/[functionSlug]/index.tsx +++ b/apps/studio/pages/project/[ref]/functions/[functionSlug]/index.tsx @@ -1,6 +1,7 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { useParams } from 'common' import dayjs, { Dayjs } from 'dayjs' +import maxBy from 'lodash/maxBy' import meanBy from 'lodash/meanBy' import sumBy from 'lodash/sumBy' import { useRouter } from 'next/router' @@ -13,13 +14,9 @@ import AreaChart from 'components/ui/Charts/AreaChart' import StackedBarChart from 'components/ui/Charts/StackedBarChart' import NoPermission from 'components/ui/NoPermission' import { - FunctionsReqStatsVariables, - useFunctionsReqStatsQuery, -} from 'data/analytics/functions-req-stats-query' -import { - FunctionsResourceUsageVariables, - useFunctionsResourceUsageQuery, -} from 'data/analytics/functions-resource-usage-query' + FunctionsCombinedStatsVariables, + useFunctionsCombinedStatsQuery, +} from 'data/analytics/functions-combined-stats-query' import { useEdgeFunctionQuery } from 'data/edge-functions/edge-function-query' import { useFillTimeseriesSorted } from 'hooks/analytics/useFillTimeseriesSorted' import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' @@ -31,6 +28,7 @@ import { Button, WarningIcon, } from 'ui' +import { useFlag } from 'common' const CHART_INTERVALS: ChartIntervals[] = [ { @@ -47,6 +45,13 @@ const CHART_INTERVALS: ChartIntervals[] = [ startUnit: 'hour', format: 'MMM D, h:mma', }, + { + key: '3hr', + label: '3 hours', + startValue: 3, + startUnit: 'hour', + format: 'MMM D, h:mma', + }, { key: '1day', label: '1 day', @@ -54,18 +59,12 @@ const CHART_INTERVALS: ChartIntervals[] = [ startUnit: 'hour', format: 'MMM D, h:mma', }, - // { - // key: '7day', - // label: '7 days', - // startValue: 7, - // startUnit: 'day', - // format: 'MMM D', - // }, ] const PageLayout: NextPageWithLayout = () => { const router = useRouter() const { ref: projectRef, functionSlug } = useParams() + const newChartsEnabled = useFlag('newEdgeFunctionOverviewCharts') const [interval, setInterval] = useState('15min') const selectedInterval = CHART_INTERVALS.find((i) => i.key === interval) || CHART_INTERVALS[1] const { data: selectedFunction } = useEdgeFunctionQuery({ @@ -73,26 +72,16 @@ const PageLayout: NextPageWithLayout = () => { slug: functionSlug, }) const id = selectedFunction?.id - const reqStatsResult = useFunctionsReqStatsQuery({ + const combinedStatsResults = useFunctionsCombinedStatsQuery({ projectRef, functionId: id, - interval: selectedInterval.key as FunctionsReqStatsVariables['interval'], + interval: selectedInterval.key as FunctionsCombinedStatsVariables['interval'], }) - const resourceUsageResult = useFunctionsResourceUsageQuery({ - projectRef, - functionId: id, - interval: selectedInterval.key as FunctionsResourceUsageVariables['interval'], - }) - - const reqStatsData = useMemo(() => { - const result = reqStatsResult.data?.result + const combinedStatsData = useMemo(() => { + const result = combinedStatsResults.data?.result return result || [] - }, [reqStatsResult.data]) - - const resourceUsageData = useMemo(() => { - return resourceUsageResult.data?.result || [] - }, [resourceUsageResult.data]) + }, [combinedStatsResults.data]) const [startDate, endDate]: [Dayjs, Dayjs] = useMemo(() => { const start = dayjs() @@ -104,39 +93,30 @@ const PageLayout: NextPageWithLayout = () => { }, [selectedInterval]) const { - data: execTimeChartData, - error: execTimeError, - isError: isErrorExecTime, + data: combinedStatsChartData, + error: combinedStatsError, + isError: isErrorCombinedStats, } = useFillTimeseriesSorted( - reqStatsData, + combinedStatsData, 'timestamp', - ['avg_execution_time'], - 0, - startDate.toISOString(), - endDate.toISOString() - ) - - const { - data: invocationsChartData, - error: invocationsError, - isError: isErrorInvocations, - } = useFillTimeseriesSorted( - reqStatsData, - 'timestamp', - ['count', 'success_count', 'redirect_count', 'client_err_count', 'server_err_count'], - 0, - startDate.toISOString(), - endDate.toISOString() - ) - - const { - data: resourceUsageChartData, - error: resourceUsageError, - isError: isErrorResourceUsage, - } = useFillTimeseriesSorted( - resourceUsageData, - 'timestamp', - ['avg_cpu_time_used', 'avg_memory_used'], + [ + 'requests_count', + 'log_count', + 'log_info_count', + 'log_warn_count', + 'log_error_count', + 'success_count', + 'redirect_count', + 'client_err_count', + 'server_err_count', + 'avg_cpu_time_used', + 'avg_memory_used', + 'avg_execution_time', + 'max_execution_time', + 'avg_heap_memory_used', + 'avg_external_memory_used', + 'max_cpu_time_used', + ], 0, startDate.toISOString(), endDate.toISOString() @@ -151,7 +131,7 @@ const PageLayout: NextPageWithLayout = () => { } return ( -
    +
    @@ -188,45 +168,65 @@ const PageLayout: NextPageWithLayout = () => { { - return isErrorExecTime ? ( + return isErrorCombinedStats ? ( Failed to reterieve execution time - {execTimeError.message} + + {combinedStatsError.message} + ) : ( - +
    + + {newChartsEnabled && ( + + )} +
    ) }} /> { - if (isErrorInvocations) { + if (isErrorCombinedStats) { return ( Failed to reterieve invocations - {invocationsError.message} + {combinedStatsError.message} ) } else { - const data = props.data + const requestData = props.data .map((d: any) => [ { status: '2xx', @@ -251,22 +251,63 @@ const PageLayout: NextPageWithLayout = () => { ]) .flat() + const logsData = props.data + .map((d: any) => [ + { + status: 'error', + count: d.log_error_count, + timestamp: d.timestamp, + }, + { + status: 'info', + count: d.log_info_count, + timestamp: d.timestamp, + }, + { + status: 'warn', + count: d.log_warn_count, + timestamp: d.timestamp, + }, + ]) + .flat() + return ( - { - router.push( - `/project/${projectRef}/functions/${functionSlug}/invocations?its=${startDate.toISOString()}` - ) - }} - /> +
    + { + router.push( + `/project/${projectRef}/functions/${functionSlug}/invocations?its=${startDate.toISOString()}` + ) + }} + /> + {newChartsEnabled && ( + { + router.push( + `/project/${projectRef}/functions/${functionSlug}/logs?its=${startDate.toISOString()}` + ) + }} + /> + )} +
    ) } }} @@ -274,54 +315,105 @@ const PageLayout: NextPageWithLayout = () => { { - return isErrorResourceUsage ? ( + return isErrorCombinedStats ? ( Failed to retrieve CPU time - {resourceUsageError.message} + {combinedStatsError.message} ) : ( - +
    + + {newChartsEnabled && ( + + )} +
    ) }} /> { - return isErrorResourceUsage ? ( - - - Failed to retrieve memory usage - - {resourceUsageError.message} - - - ) : ( - + if (isErrorCombinedStats) { + return ( + + + Failed to retrieve memory usage + + {combinedStatsError.message} + + + ) + } + + const memoryData = props.data + .map((d: any) => [ + { + type: 'heap', + count: d.avg_heap_memory_used, + timestamp: d.timestamp, + }, + { + type: 'external', + count: d.avg_external_memory_used, + timestamp: d.timestamp, + }, + ]) + .flat() + + return ( +
    + + {newChartsEnabled && ( + + )} +
    ) }} /> diff --git a/apps/studio/pages/project/[ref]/index.tsx b/apps/studio/pages/project/[ref]/index.tsx index 567ecaa1800..b786f450fe3 100644 --- a/apps/studio/pages/project/[ref]/index.tsx +++ b/apps/studio/pages/project/[ref]/index.tsx @@ -1,308 +1,22 @@ -import dayjs from 'dayjs' -import Link from 'next/link' -import { useEffect, useMemo, useRef } from 'react' - -import { useParams } from 'common' -import { ClientLibrary } from 'components/interfaces/Home' -import { AdvisorWidget } from 'components/interfaces/Home/AdvisorWidget' -import { ExampleProject } from 'components/interfaces/Home/ExampleProject' -import { CLIENT_LIBRARIES, EXAMPLE_PROJECTS } from 'components/interfaces/Home/Home.constants' -import { NewProjectPanel } from 'components/interfaces/Home/NewProjectPanel/NewProjectPanel' -import { ProjectUsageSection } from 'components/interfaces/Home/ProjectUsageSection' -import { ServiceStatus } from 'components/interfaces/Home/ServiceStatus' +import { useFlag } from 'common' +import { Home } from 'components/interfaces/Home/Home' +import { HomeV2 } from 'components/interfaces/HomeNew/Home' import DefaultLayout from 'components/layouts/DefaultLayout' -import { ProjectPausedState } from 'components/layouts/ProjectLayout/PausedState/ProjectPausedState' import { ProjectLayoutWithAuth } from 'components/layouts/ProjectLayout/ProjectLayout' -import { ComputeBadgeWrapper } from 'components/ui/ComputeBadgeWrapper' -import { InlineLink } from 'components/ui/InlineLink' -import { ProjectUpgradeFailedBanner } from 'components/ui/ProjectUpgradeFailedBanner' -import { useBranchesQuery } from 'data/branches/branches-query' -import { useEdgeFunctionsQuery } from 'data/edge-functions/edge-functions-query' -import { useReadReplicasQuery } from 'data/read-replicas/replicas-query' -import { useTablesQuery } from 'data/tables/tables-query' -import { useCustomContent } from 'hooks/custom-content/useCustomContent' -import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' -import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' -import { - useIsOrioleDb, - useProjectByRefQuery, - useSelectedProjectQuery, -} from 'hooks/misc/useSelectedProject' -import { IS_PLATFORM, PROJECT_STATUS } from 'lib/constants' -import { useAppStateSnapshot } from 'state/app-state' import type { NextPageWithLayout } from 'types' -import { - Badge, - cn, - Tabs_Shadcn_, - TabsContent_Shadcn_, - TabsList_Shadcn_, - TabsTrigger_Shadcn_, - Tooltip, - TooltipContent, - TooltipTrigger, -} from 'ui' -import ShimmeringLoader from 'ui-patterns/ShimmeringLoader' -const Home: NextPageWithLayout = () => { - const { data: project } = useSelectedProjectQuery() - const { data: organization } = useSelectedOrganizationQuery() - const { data: parentProject } = useProjectByRefQuery(project?.parent_project_ref) - const isOrioleDb = useIsOrioleDb() - const snap = useAppStateSnapshot() - const { ref, enableBranching } = useParams() - - const { projectHomepageExampleProjects } = useCustomContent(['project_homepage:example_projects']) - - const { - projectHomepageShowAllClientLibraries: showAllClientLibraries, - projectHomepageShowInstanceSize: showInstanceSize, - projectHomepageShowExamples: showExamples, - } = useIsFeatureEnabled([ - 'project_homepage:show_all_client_libraries', - 'project_homepage:show_instance_size', - 'project_homepage:show_examples', - ]) - - const clientLibraries = useMemo(() => { - if (showAllClientLibraries) { - return CLIENT_LIBRARIES - } - return CLIENT_LIBRARIES.filter((library) => library.language === 'JavaScript') - }, [showAllClientLibraries]) - - const hasShownEnableBranchingModalRef = useRef(false) - const isPaused = project?.status === PROJECT_STATUS.INACTIVE - const isNewProject = dayjs(project?.inserted_at).isAfter(dayjs().subtract(2, 'day')) - - useEffect(() => { - if (enableBranching && !hasShownEnableBranchingModalRef.current) { - hasShownEnableBranchingModalRef.current = true - snap.setShowCreateBranchModal(true) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [enableBranching]) - - const { data: tablesData, isLoading: isLoadingTables } = useTablesQuery({ - projectRef: project?.ref, - connectionString: project?.connectionString, - schema: 'public', - }) - const { data: functionsData, isLoading: isLoadingFunctions } = useEdgeFunctionsQuery({ - projectRef: project?.ref, - }) - const { data: replicasData, isLoading: isLoadingReplicas } = useReadReplicasQuery({ - projectRef: project?.ref, - }) - - const { data: branches } = useBranchesQuery({ - projectRef: project?.parent_project_ref ?? project?.ref, - }) - - const mainBranch = branches?.find((branch) => branch.is_default) - const currentBranch = branches?.find((branch) => branch.project_ref === project?.ref) - const isMainBranch = currentBranch?.name === mainBranch?.name - let projectName = 'Welcome to your project' - - if (currentBranch && !isMainBranch) { - projectName = currentBranch?.name - } else if (project?.name) { - projectName = project?.name +const HomePage: NextPageWithLayout = () => { + const isHomeNew = useFlag('homeNew') + if (isHomeNew) { + return } - - const tablesCount = Math.max(0, tablesData?.length ?? 0) - const functionsCount = Math.max(0, functionsData?.length ?? 0) - // [Joshen] JFYI minus 1 as the replicas endpoint returns the primary DB minimally - const replicasCount = Math.max(0, (replicasData?.length ?? 1) - 1) - - return ( -
    -
    -
    -
    -
    -
    - {!isMainBranch && ( - - {parentProject?.name} - - )} -

    {projectName}

    -
    -
    - {isOrioleDb && ( - - - OrioleDB - - - This project is using Postgres with OrioleDB which is currently in preview and - not suitable for production workloads. View our{' '} - - documentation - {' '} - for all limitations. - - - )} - {showInstanceSize && ( - - )} -
    -
    -
    - {project?.status === PROJECT_STATUS.ACTIVE_HEALTHY && ( -
    -
    - - Tables - - - {isLoadingTables ? ( - - ) : ( -

    {tablesCount}

    - )} -
    - - {IS_PLATFORM && ( -
    - - Functions - - {isLoadingFunctions ? ( - - ) : ( -

    {functionsCount}

    - )} -
    - )} - - {IS_PLATFORM && ( -
    - - Replicas - - {isLoadingReplicas ? ( - - ) : ( -

    {replicasCount}

    - )} -
    - )} -
    - )} - {IS_PLATFORM && project?.status === PROJECT_STATUS.ACTIVE_HEALTHY && ( -
    - -
    - )} -
    -
    - - {isPaused && } -
    -
    - - {!isPaused && ( - <> -
    -
    - {IS_PLATFORM && project?.status !== PROJECT_STATUS.INACTIVE && ( - <>{isNewProject ? : } - )} - {!isNewProject && project?.status !== PROJECT_STATUS.INACTIVE && } -
    -
    - -
    -
    - {project?.status !== PROJECT_STATUS.INACTIVE && ( - <> -
    -

    Client libraries

    -
    - {clientLibraries.map((library) => ( - - ))} -
    -
    - {showExamples && ( -
    -

    Example projects

    - {!!projectHomepageExampleProjects ? ( -
    - {projectHomepageExampleProjects - .sort((a, b) => a.title.localeCompare(b.title)) - .map((project) => ( - - ))} -
    - ) : ( -
    - - - App Frameworks - - Mobile Frameworks - - - -
    - {EXAMPLE_PROJECTS.filter((project) => project.type === 'app') - .sort((a, b) => a.title.localeCompare(b.title)) - .map((project) => ( - - ))} -
    -
    - -
    - {EXAMPLE_PROJECTS.filter((project) => project.type === 'mobile') - .sort((a, b) => a.title.localeCompare(b.title)) - .map((project) => ( - - ))} -
    -
    -
    -
    - )} -
    - )} - - )} -
    -
    - - )} -
    - ) + return } -Home.getLayout = (page) => ( +HomePage.getLayout = (page) => ( {page} ) -export default Home +export default HomePage diff --git a/apps/studio/pages/project/[ref]/reports/edge-functions.tsx b/apps/studio/pages/project/[ref]/reports/edge-functions.tsx index 16b4705e043..3cbcc7e27b2 100644 --- a/apps/studio/pages/project/[ref]/reports/edge-functions.tsx +++ b/apps/studio/pages/project/[ref]/reports/edge-functions.tsx @@ -1,19 +1,25 @@ import { useQueryClient } from '@tanstack/react-query' import { useParams } from 'common' import dayjs from 'dayjs' -import { ArrowRight, ChevronDown, RefreshCw } from 'lucide-react' -import { useEffect, useMemo, useState } from 'react' +import { ArrowRight, RefreshCw } from 'lucide-react' +import { useMemo, useState } from 'react' -import { Label } from '@ui/components/shadcn/ui/label' -import { ReportChartV2 } from 'components/interfaces/Reports/v2/ReportChartV2' import ReportHeader from 'components/interfaces/Reports/ReportHeader' import ReportPadding from 'components/interfaces/Reports/ReportPadding' import ReportStickyNav from 'components/interfaces/Reports/ReportStickyNav' +import { ReportChartV2 } from 'components/interfaces/Reports/v2/ReportChartV2' +import { + ReportsNumericFilter, + numericFilterSchema, +} from 'components/interfaces/Reports/v2/ReportsNumericFilter' +import { + ReportsSelectFilter, + selectFilterSchema, +} from 'components/interfaces/Reports/v2/ReportsSelectFilter' import { LogsDatePicker } from 'components/interfaces/Settings/Logs/Logs.DatePickers' import DefaultLayout from 'components/layouts/DefaultLayout' import ReportsLayout from 'components/layouts/ReportsLayout/ReportsLayout' import { ButtonTooltip } from 'components/ui/ButtonTooltip' -import { Button, Checkbox, DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from 'ui' import { useChartHoverState } from 'components/ui/Charts/useChartHoverState' import { useEdgeFunctionsQuery } from 'data/edge-functions/edge-functions-query' @@ -23,8 +29,11 @@ import { REPORT_DATERANGE_HELPER_LABELS } from 'components/interfaces/Reports/Re import UpgradePrompt from 'components/interfaces/Settings/Logs/UpgradePrompt' import { useReportDateRange } from 'hooks/misc/useReportDateRange' -import type { NextPageWithLayout } from 'types' +import { EDGE_FUNCTION_REGIONS } from 'components/interfaces/Reports/Reports.constants' import { ReportSettings } from 'components/ui/Charts/ReportSettings' +import { BASE_PATH } from 'lib/constants' +import type { NextPageWithLayout } from 'types' +import { useQueryState, parseAsJson } from 'nuqs' const EdgeFunctionsReportV2: NextPageWithLayout = () => { return ( @@ -44,22 +53,32 @@ export default EdgeFunctionsReportV2 const EdgeFunctionsUsage = () => { const { ref } = useParams() - const { data: functions, isLoading: isLoadingFunctions } = useEdgeFunctionsQuery({ + const { data: functions } = useEdgeFunctionsQuery({ projectRef: ref, }) const chartSyncId = `edge-functions-${ref}` useChartHoverState(chartSyncId) - const [isOpen, setIsOpen] = useState(false) - const [functionIds, setFunctionIds] = useState([]) - const [tempFunctionIds, setTempFunctionIds] = useState(functionIds) + // Filters + const [statusCodeFilter, setStatusCodeFilter] = useQueryState( + 'status_code', + parseAsJson(numericFilterSchema.parse) + ) - useEffect(() => { - if (isOpen) { - setTempFunctionIds(functionIds) - } - }, [isOpen, functionIds]) + const [regionFilter, setRegionFilter] = useQueryState( + 'region', + parseAsJson(selectFilterSchema.parse) + ) + const [executionTimeFilter, setExecutionTimeFilter] = useQueryState( + 'execution_time', + parseAsJson(numericFilterSchema.parse) + ) + + const [functionFilter, setFunctionFilter] = useQueryState( + 'functions', + parseAsJson(selectFilterSchema.parse) + ) const { selectedDateRange, @@ -82,16 +101,27 @@ const EdgeFunctionsUsage = () => { endDate: selectedDateRange?.period_end?.date ?? '', interval: selectedDateRange?.interval ?? 'minute', filters: { - functionIds, + functions: functionFilter ?? [], + status_code: statusCodeFilter, + region: regionFilter ?? [], + execution_time: executionTimeFilter, }, }) - }, [ref, functions, selectedDateRange, functionIds]) + }, [ + ref, + functions, + selectedDateRange, + functionFilter, + statusCodeFilter, + regionFilter, + executionTimeFilter, + ]) const onRefreshReport = async () => { if (!selectedDateRange) return setIsRefreshing(true) - queryClient.invalidateQueries(['report-v2']) + queryClient.invalidateQueries({ queryKey: ['projects', ref, 'report-v2'] }) setTimeout(() => setIsRefreshing(false), 1000) } @@ -110,12 +140,14 @@ const EdgeFunctionsUsage = () => { tooltip={{ content: { side: 'bottom', text: 'Refresh report' } }} onClick={onRefreshReport} /> + { description="Report data can be stored for a maximum of 3 months depending on the plan that your project is on." source="edgeFunctionsReportDateRange" /> + {selectedDateRange && (

    @@ -138,92 +171,67 @@ const EdgeFunctionsUsage = () => {

    )}
    -
    - - - - - -
    -
    } > -
    +
    {selectedDateRange && reportConfig .filter((report) => !report.hide) @@ -236,8 +244,13 @@ const EdgeFunctionsUsage = () => { startDate={selectedDateRange?.period_start?.date} endDate={selectedDateRange?.period_end?.date} updateDateRange={updateDateRange} - functionIds={functionIds} syncId={chartSyncId} + filters={{ + functions: functionFilter, + status_code: statusCodeFilter, + region: regionFilter, + execution_time: executionTimeFilter, + }} /> ))}
    diff --git a/apps/studio/public/img/regions/us-west-2.svg b/apps/studio/public/img/regions/us-west-2.svg new file mode 100644 index 00000000000..3189d8e2dc3 --- /dev/null +++ b/apps/studio/public/img/regions/us-west-2.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/apps/www/_blog/2025-04-01-supabase-edge-functions-deploy-dashboard-deno-2-1.mdx b/apps/www/_blog/2025-04-01-supabase-edge-functions-deploy-dashboard-deno-2-1.mdx index d941752723b..5438cb6b064 100644 --- a/apps/www/_blog/2025-04-01-supabase-edge-functions-deploy-dashboard-deno-2-1.mdx +++ b/apps/www/_blog/2025-04-01-supabase-edge-functions-deploy-dashboard-deno-2-1.mdx @@ -105,18 +105,12 @@ The ability to deploy without Docker in both the Edge Functions editor and Supab You can check [the Changelog announcement](https://github.com/orgs/supabase/discussions/33720) for more details and official references to these API endpoints. -## Deno 2.1 Preview +## Deno 2.1 support Last, but not least, we have added Deno 2.1 support for Supabase Edge Runtime. With Deno 2.1, you can use built-in Deno commands to scaffold a new project, manage dependencies, run tests, and lints. Check [our guide on how to start using Deno 2.1](https://github.com/orgs/supabase/discussions/34054) tooling for your Edge Functions. - - -Note that the Supabase hosted platform is still using Deno 1.45. In the coming weeks, we will provide more details on deploying Deno 2.1 projects in the hosted platform. - - - ## Conclusion These changes to Supabase Edge Functions make it easier and more accessible for all developers to build powerful functionality into their applications. diff --git a/apps/www/_blog/2025-07-18-persistent-storage-for-faster-edge-functions.mdx b/apps/www/_blog/2025-07-18-persistent-storage-for-faster-edge-functions.mdx index 6f2e3514335..239626d91bb 100644 --- a/apps/www/_blog/2025-07-18-persistent-storage-for-faster-edge-functions.mdx +++ b/apps/www/_blog/2025-07-18-persistent-storage-for-faster-edge-functions.mdx @@ -148,6 +148,6 @@ Deno.serve(() => { }) ``` -## Try it on Preview Today +## How to try -These changes will be rolled out along with the Deno 2 upgrade to all clusters within the next 2 weeks. Meanwhile, you can use the Preview cluster if you'd like to try them out today. Please see [this guide](https://github.com/orgs/supabase/discussions/36814) on how to test your functions in Preview cluster. +These changes are already deployed and available to use on all regions. diff --git a/apps/www/app/api-v2/submit-form-apply-to-supasquad/route.tsx b/apps/www/app/api-v2/submit-form-apply-to-supasquad/route.tsx new file mode 100644 index 00000000000..9de5a34550f --- /dev/null +++ b/apps/www/app/api-v2/submit-form-apply-to-supasquad/route.tsx @@ -0,0 +1,294 @@ +import * as Sentry from '@sentry/nextjs' +import z from 'zod' + +import { CustomerioAppClient, CustomerioTrackClient } from '~/lib/customerio' +import { insertPageInDatabase } from '~/lib/notion' + +// Using a separate Sentry client for community following this guide: +// https://docs.sentry.io/platforms/javascript/best-practices/multiple-sentry-instances/ +const integrations = Sentry.getDefaultIntegrations({}).filter((defaultIntegration) => { + return !['BrowserApiErrors', 'Breadcrumbs', 'GlobalHandlers'].includes(defaultIntegration.name) +}) + +const sentryCommunityClient = new Sentry.NodeClient({ + dsn: process.env.SENTRY_DSN_COMMUNITY, + transport: Sentry.makeNodeTransport, + stackParser: Sentry.defaultStackParser, + integrations: [...integrations], +}) + +const sentryCommunity = new Sentry.Scope() +sentryCommunity.setClient(sentryCommunityClient) + +const captureSentryCommunityException = (error: any) => { + if (process.env.SENTRY_DSN_COMMUNITY) { + sentryCommunity.captureException(error) + } +} + +const NOTION_API_KEY = process.env.NOTION_SUPASQUAD_API_KEY +const NOTION_DB_ID = process.env.NOTION_SUPASQUAD_APPLICATIONS_DB_ID + +const applicationSchema = z.object({ + first_name: z.string().min(1, 'First name is required'), + last_name: z.string().min(1, 'Last name is required'), + email: z.string().email('Please enter a valid email address'), + tracks: z + .array( + z.object({ + heading: z.string(), + description: z.string(), + }) + ) + .min(1, 'Select at least 1 track'), + areas_of_interest: z.array(z.string()).min(1, 'Select at least 1 area of interest'), + why_you_want_to_join: z.string().min(1, 'This is required'), + monthly_commitment: z.string().optional(), + languages_spoken: z.array(z.string()).min(1, 'Select at least 1 language'), + skills: z.string().optional(), + city: z.string().min(1, 'Specify your city'), + country: z.string().min(1, 'Specify your country'), + github: z.string().optional(), + twitter: z.string().optional(), +}) + +const corsHeaders = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', + 'Access-Control-Allow-Methods': 'POST,OPTIONS', +} + +export async function OPTIONS() { + return new Response('ok', { headers: corsHeaders }) +} + +function truncateRichText(s: string, max = 1900) { + if (!s) return '' + return s.length > max ? s.slice(0, max) + '…' : s +} + +function asMultiSelect(values: string[]) { + return values.map((v) => ({ name: v })) +} + +function normalizeTrack(t: { heading: string; description: string } | string) { + // Handle both old string format and new object format + const trackName = typeof t === 'string' ? t : t.heading + if (trackName === 'Builder/Maintainer') return 'Builder / Maintainer' + return trackName +} + +export async function POST(req: Request) { + if (!NOTION_API_KEY || !NOTION_DB_ID) { + captureSentryCommunityException(new Error('Server misconfigured: missing Notion credentials')) + return new Response( + JSON.stringify({ message: 'Server misconfigured: missing Notion credentials' }), + { + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + status: 500, + } + ) + } + + let body: unknown + try { + body = await req.json() + } catch (error: any) { + captureSentryCommunityException(new Error('Unable to parse JSON:`')) + return new Response(JSON.stringify({ message: 'Invalid JSON' }), { + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + status: 400, + }) + } + + const parsed = applicationSchema.safeParse(body) + if (!parsed.success) { + return new Response(JSON.stringify({ message: parsed.error.flatten() }), { + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + status: 422, + }) + } + + const data = parsed.data + + try { + const notionProps = getNotionPageProps(data) + const notionPageId = await insertPageInDatabase(NOTION_DB_ID, NOTION_API_KEY, notionProps) + + await savePersonAndEventInCustomerIO({ + ...data, + tracks: data.tracks.map(normalizeTrack), + notion_page_id: notionPageId, + source_url: req.headers.get('origin'), + }) + + return new Response(JSON.stringify({ message: 'Submission successful', id: notionPageId }), { + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + status: 201, + }) + } catch (err: any) { + console.error(err) + captureSentryCommunityException(err) + return new Response( + JSON.stringify({ message: 'Error sending your application', error: err?.message }), + { + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + status: 502, + } + ) + } +} + +const savePersonAndEventInCustomerIO = async (data: any) => { + const customerioSiteId = process.env.CUSTOMERIO_SITE_ID + const customerioApiKey = process.env.CUSTOMERIO_API_KEY + + if (customerioSiteId && customerioApiKey) { + try { + const customerioClient = new CustomerioTrackClient(customerioSiteId, customerioApiKey) + + // Create or update profile in Customer.io + // Note: only include personal information + // not application specific responses + await customerioClient.createOrUpdateProfile(data.email, { + first_name: data.first_name, + last_name: data.last_name, + email: data.email, + city: data.city, + country: data.country, + github: data.github, + twitter: data.twitter, + }) + + // Track the supasquad_application_form_submitted event + // This includes application specific responses + const customerioEvent = { + userId: data.email, + type: 'track' as const, + event: 'supasquad_application_form_submitted', + properties: { + ...data, + event_type: 'supasquad_application_form_submitted', + source: 'supasquad_application_form', + submitted_at: new Date().toISOString(), + }, + timestamp: customerioClient.isoToUnixTimestamp(new Date().toISOString()), + } + + await customerioClient.trackEvent(data.email, customerioEvent) + + await sendConfirmationEmail({ + email: data.email, + first_name: data.first_name, + last_name: data.last_name, + }) + } catch (error) { + console.error('Customer.io Track API integration failed:', error) + } + } +} + +const sendConfirmationEmail = async (emailData: { + email: string + first_name: string + last_name: string +}) => { + const customerioApiKey = process.env.CUSTOMERIO_APP_API_KEY + + if (customerioApiKey) { + const customerioAppClient = new CustomerioAppClient(customerioApiKey) + + try { + const emailRequest = { + transactional_message_id: 9, + to: emailData.email, + identifiers: { + email: emailData.email, + }, + message_data: { + first_name: emailData.first_name, + last_name: emailData.last_name, + }, + send_at: customerioAppClient.isoToUnixTimestamp( + new Date(Date.now() + 60 * 1000).toISOString() + ), // Schedule to send after 1 minute + } + + await customerioAppClient.sendTransactionalEmail(emailRequest) + } catch (error) { + throw new Error(`Failed to send confirmation email: ${error}`) + } + } else { + console.warn('Customer.io App API key is not set') + } +} + +const getNotionPageProps = (data: any) => { + const fullName = + `${data.first_name?.trim() || ''} ${data.last_name?.trim() || ''}`.trim() || 'Unnamed' + + const props: Record = { + Name: { + title: [{ type: 'text', text: { content: fullName } }], + }, + 'First name': { + rich_text: [{ type: 'text', text: { content: data.first_name || '' } }], + }, + 'Last name': { + rich_text: [{ type: 'text', text: { content: data.last_name || '' } }], + }, + Email: { email: data.email }, + 'What track would you like to be considered for?': { + multi_select: asMultiSelect(data.tracks.map(normalizeTrack)), + }, + 'Product areas of interest': { + multi_select: asMultiSelect(data.areas_of_interest), + }, + 'Languages spoken': { + multi_select: asMultiSelect(data.languages_spoken), + }, + 'Date submitted': { + date: { start: new Date().toISOString().split('T')[0] }, + }, + Country: { + select: { name: data.country }, + }, + City: { + rich_text: [{ type: 'text', text: { content: truncateRichText(data.city, 120) } }], + }, + Location: { + rich_text: [ + { type: 'text', text: { content: truncateRichText(data.city + ', ' + data.country, 120) } }, + ], + }, + } + if (data.monthly_commitment) { + props['Monthly commitment'] = { + rich_text: [{ type: 'text', text: { content: truncateRichText(data.monthly_commitment) } }], + } + } + if (data.skills) { + props['Skills (frameworks, tools, languages)'] = { + rich_text: [{ type: 'text', text: { content: truncateRichText(data.skills) } }], + } + } + if (data.why_you_want_to_join) { + props['Why do you want to join the program'] = { + rich_text: [ + { type: 'text', text: { content: truncateRichText(data.why_you_want_to_join, 1800) } }, + ], + } + } + if (data.github) { + props['GitHub Profile'] = { + rich_text: [{ type: 'text', text: { content: data.github } }], + } + } + if (data.twitter) { + props['Twitter handle'] = { + rich_text: [{ type: 'text', text: { content: data.twitter } }], + } + } + + return props +} diff --git a/apps/www/components/Forms/ApplyToSupaSquadForm.tsx b/apps/www/components/Forms/ApplyToSupaSquadForm.tsx new file mode 100644 index 00000000000..017d247283e --- /dev/null +++ b/apps/www/components/Forms/ApplyToSupaSquadForm.tsx @@ -0,0 +1,698 @@ +import { FC, useEffect, useState, memo } from 'react' +import { AlertCircle, CheckCircle2 } from 'lucide-react' +import * as z from 'zod' +import { useForm } from 'react-hook-form' +import { zodResolver } from '@hookform/resolvers/zod' +import { + Button, + Form_Shadcn_, + FormField_Shadcn_, + FormLabel_Shadcn_, + FormControl_Shadcn_, + FormItem_Shadcn_, + Input_Shadcn_, + FormMessage_Shadcn_, + Separator, + TextArea_Shadcn_, + FormDescription_Shadcn_, +} from 'ui' +import { Alert, AlertTitle, AlertDescription } from 'ui/src/components/shadcn/ui/alert' +import { + AlertDialog, + AlertDialogAction, + AlertDialogContent, + AlertDialogDescription, + AlertDialogTitle, +} from 'ui/src/components/shadcn/ui/alert-dialog' +import { + MultiSelector, + MultiSelectorContent, + MultiSelectorList, + MultiSelectorTrigger, + MultiSelectorItem, +} from 'ui-patterns/multi-select' +import { CountrySelector } from '../Supasquad/CountrySelector' + +interface FormItem_Shadcn_ { + type: 'text' | 'textarea' + label: string + placeholder: string + required: boolean + className?: string + component: typeof TextArea_Shadcn_ | typeof Input_Shadcn_ +} + +interface Track { + heading: string + description: string +} + +interface Props { + className?: string +} + +const tracks: Track[] = [ + { + heading: 'Advocate', + description: 'Help spread word on socials answer community questions', + }, + { + heading: 'Helper', + description: 'Answer questions and improve docs across our platforms', + }, + { + heading: 'Builder/Maintainer', + description: 'Contribute to client libraries, manage issues, fix bugs', + }, + { + heading: 'Moderator', + description: 'Ensure our social spaces remain productive and helpful', + }, +] + +const productAreasOfInterest: string[] = [ + 'Auth', + 'Branching', + 'Client libraries', + 'Database / Postgres', + 'Dashboard', + 'CLI', + 'Edge Functions', + 'Integrations', + 'Realtime', + 'Storage', + 'Vectors / AI', + 'Other', +] + +const languagesSpoken: string[] = [ + 'English', + 'Spanish', + 'Portuguese', + 'French', + 'German', + 'Italian', + 'Russian', + 'Arabic', + 'Hindi', + 'Mandarin Chinese', + 'Japanese', + 'Turkish', + 'Indonesian', + 'Thai', + 'Vietnamese', + 'Bengali', + 'Urdu', + 'Polish', + 'Dutch', + 'Other', +] + +const applicationSchema = z.object({ + first_name: z.string().min(1, 'First name is required'), + last_name: z.string().min(1, 'Last name is required'), + email: z.string().email('Please enter a valid email address'), + tracks: z + .array( + z.object({ + heading: z.string(), + description: z.string(), + }) + ) + .min(1, 'Select at least 1 track'), + areas_of_interest: z.array(z.string()).min(1, 'Select at least 1 area of interest'), + why_you_want_to_join: z.string().min(1, 'This is required'), + monthly_commitment: z.string().optional(), + languages_spoken: z.array(z.string()).min(1, 'Select at least 1 language'), + skills: z.string().optional(), + city: z.string().min(1, 'Specify your city'), + country: z.string().min(1, 'Specify your country'), + github: z.string().optional(), + twitter: z.string().optional(), +}) + +type ApplicationFormData = z.infer + +const headerContent = { + title: 'Apply to join SupaSquad', + description: + 'Join our community of passionate contributors and help shape the future of Supabase. Fill out the form below to apply.', +} + +const FormContent = memo(function FormContent({ + form, + errors, + onSubmit, + honeypot, + setHoneypot, + isMobile, + isSubmitted, + handleCancel, + isSubmitting, +}: { + form: ReturnType> + errors: { [key: string]: string } + onSubmit: (data: ApplicationFormData) => Promise + honeypot: string + setHoneypot: React.Dispatch> + isMobile: boolean + isSubmitted: boolean + handleCancel: () => void + isSubmitting: boolean +}) { + return ( +
    + +
    +
    + ( + + First Name * + + + + + + + )} + /> + + ( + + Last Name * + +
    + +
    +
    + + +
    + )} + /> +
    + + ( + + Email Address * + +
    + +
    +
    + +
    + )} + /> + + + +
    +

    Interests and skills

    + ( + + + Why do you want to join the program? * + + + What do you have to contribute? What would you like to get out of it? + + +
    + +
    +
    + +
    + )} + /> + + ( + + + What track would you like to be considered for? * + + + See longer descriptions of the 4 options above + + +
    + { + // Convert selected headings back to track objects + const selectedTracks = values.map( + (heading) => tracks.find((track) => track.heading === heading)! + ) + field.onChange(selectedTracks) + }} + values={field.value.map((track) => track.heading)} + size="small" + > + + + + {tracks.map((item) => ( + +
    +
    {item.heading}
    +
    + {item.description} +
    +
    +
    + ))} +
    +
    +
    +
    +
    + + +
    + )} + /> + + ( + + + Product Areas of Interest * + + + What specific areas would you like to help with? Leave blank if you're not sure. + + +
    + + + + + {productAreasOfInterest.map((item) => ( + + {item} + + ))} + + + +
    +
    + +
    + )} + /> + + ( + + + Skills (frameworks, tools, programming languages) + + + Know Postgres really well? React? Python? Rust? Terraform? Add it here! + + +
    + +
    +
    + +
    + )} + /> +
    + + + +
    +

    Location and Availability

    + +
    + ( + + Country * + +
    + {/* */} + +
    +
    + +
    + )} + /> + + ( + + City * + +
    + +
    +
    + +
    + )} + /> +
    + + ( + + + Monthly Commitment + + + How many hours can you commit per month? If not sure, leave blank. + + +
    + +
    +
    + +
    + )} + /> + + ( + + + Languages spoken * + + + What languages do you speak? + + +
    + + + + + {languagesSpoken.map((item) => ( + + {item} + + ))} + + + +
    +
    + +
    + )} + /> +
    + + + +
    +

    Social Links

    + +
    + ( + + GitHub + +
    + +
    +
    + +
    + )} + /> + + ( + + Twitter + +
    + +
    +
    + +
    + )} + /> +
    +
    + + {!isSubmitted && ( +
    + + +
    + )} + + {/* Spam prevention */} + ) => setHoneypot(e.target.value)} + className="hidden" + tabIndex={-1} + autoComplete="off" + aria-hidden="true" + /> + +
    + + {Object.values(errors).length > 0 && ( + + + {Object.values(errors).join('\n')} + + )} +
    + ) +}) + +const ApplyToSupaSquadForm: FC = ({ className }) => { + const [honeypot, setHoneypot] = useState('') // field to prevent spam + const [errors, setErrors] = useState<{ [key: string]: string }>({}) + const [isSubmitting, setIsSubmitting] = useState(false) + const [isSubmitted, setIsSubmitted] = useState(false) + const [success, setSuccess] = useState(null) + const [showConfirmation, setShowConfirmation] = useState(false) + const [startTime, setStartTime] = useState(0) + + const form = useForm({ + resolver: zodResolver(applicationSchema), + defaultValues: { + first_name: '', + last_name: '', + email: '', + tracks: [], + areas_of_interest: [], + skills: '', + why_you_want_to_join: '', + city: '', + country: '', + monthly_commitment: '', + languages_spoken: [], + github: '', + twitter: '', + }, + mode: 'onBlur', + reValidateMode: 'onBlur', + }) + + const handleCancel = () => { + form.reset() + setIsSubmitted(false) + } + + const handleConfirmationClose = () => { + form.reset() + setShowConfirmation(false) + setIsSubmitted(false) + setErrors({}) + } + + const onSubmit = async (data: ApplicationFormData) => { + const currentTime = Date.now() + const timeElapsed = (currentTime - startTime) / 1000 + + // Spam prevention: Reject form if submitted too quickly (less than 3 seconds) + if (timeElapsed < 3) { + setErrors({ general: 'Submission too fast. Please fill the form correctly.' }) + return + } + + setIsSubmitting(true) + setSuccess(null) + + try { + const response = await fetch('/api-v2/submit-form-apply-to-supasquad', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }) + + if (response.ok) { + setSuccess('Thank you for your submission!') + setShowConfirmation(true) + } else { + const errorData = await response.json() + setErrors({ general: `Submission failed: ${errorData.message}` }) + } + } catch (error) { + setErrors({ general: 'An unexpected error occurred. Please try again.' }) + } finally { + setIsSubmitting(false) + } + } + + useEffect(() => { + setStartTime(Date.now()) + }, []) + + return ( + <> +
    +
    +

    {headerContent.title}

    +

    {headerContent.description}

    +
    + + + + +
    + + {/* Confirmation AlertDialog Overlay */} + {}}> + + Application Submitted + + Your application has been successfully submitted. Please check your email for + confirmation. + +
    +
    + +
    +
    +

    Application Submitted!

    +

    + Thank you for your submission. Please check your email for a confirmation link to + complete your application. +

    +
    + + Got it, thanks! + +
    +
    +
    + + ) +} + +export default ApplyToSupaSquadForm diff --git a/apps/www/components/Sections/TwitterSocialProof.tsx b/apps/www/components/Sections/TwitterSocialProof.tsx index ec9d6a33eec..1c436bdc959 100644 --- a/apps/www/components/Sections/TwitterSocialProof.tsx +++ b/apps/www/components/Sections/TwitterSocialProof.tsx @@ -4,7 +4,7 @@ import { cn } from 'ui' import { TweetCard } from 'ui-patterns/TweetCard' import { range } from 'lib/helpers' -import Tweets from '~/data/tweets/Tweets.json' +import tweets from 'shared-data/tweets' import { useBreakpoint } from 'common' import React from 'react' @@ -16,7 +16,7 @@ const TwitterSocialProof: React.FC = ({ className }) => { const { basePath } = useRouter() const isSm = useBreakpoint() const isMd = useBreakpoint(1024) - const tweets = Tweets.slice(0, 18) + const tweetsData = tweets.slice(0, 18) return ( <> @@ -39,7 +39,7 @@ const TwitterSocialProof: React.FC = ({ className }) => { 'will-change-transform transition-transform' )} > - {tweets.slice(0, isSm ? 9 : isMd ? 12 : 18).map((tweet: any, i: number) => ( + {tweetsData.slice(0, isSm ? 9 : isMd ? 12 : 18).map((tweet: any, i: number) => ( = ({ feature }) => { /> ) : ( - + ))}
    diff --git a/apps/www/components/Supasquad/ApplicationFormSection.tsx b/apps/www/components/Supasquad/ApplicationFormSection.tsx new file mode 100644 index 00000000000..c8103d6b9ad --- /dev/null +++ b/apps/www/components/Supasquad/ApplicationFormSection.tsx @@ -0,0 +1,40 @@ +import React from 'react' +import { cn } from 'ui' +import SectionContainer from '~/components/Layouts/SectionContainer' +import ApplyToSupaSquadForm from '~/components/Forms/ApplyToSupaSquadForm' + +interface Props { + id: string + title: string | React.ReactNode + subtitle?: string + cta: { + label: string + icon?: React.ReactNode + } + image?: { + dark: string + light: string + } + className?: string +} + +const ApplicationFormSection = ({ id, title, subtitle, cta, className }: Props) => { + return ( + +
    +
    +

    {title}

    + {subtitle &&

    {subtitle}

    } + +
    +
    + +
    +
    + ) +} + +export default ApplicationFormSection diff --git a/apps/www/components/Supasquad/CountrySelector.tsx b/apps/www/components/Supasquad/CountrySelector.tsx new file mode 100644 index 00000000000..b415cfd4615 --- /dev/null +++ b/apps/www/components/Supasquad/CountrySelector.tsx @@ -0,0 +1,192 @@ +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from 'ui/src/components/shadcn/ui/select' + +// Country data with flags and names (alphabetically sorted) +const countries = [ + { code: 'AF', name: 'Afghanistan', flag: '🇦🇫' }, + { code: 'AL', name: 'Albania', flag: '🇦🇱' }, + { code: 'DZ', name: 'Algeria', flag: '🇩🇿' }, + { code: 'AO', name: 'Angola', flag: '🇦🇴' }, + { code: 'AR', name: 'Argentina', flag: '🇦🇷' }, + { code: 'AM', name: 'Armenia', flag: '🇦🇲' }, + { code: 'AU', name: 'Australia', flag: '🇦🇺' }, + { code: 'AT', name: 'Austria', flag: '🇦🇹' }, + { code: 'AZ', name: 'Azerbaijan', flag: '🇦🇿' }, + { code: 'BH', name: 'Bahrain', flag: '🇧🇭' }, + { code: 'BD', name: 'Bangladesh', flag: '🇧🇩' }, + { code: 'BY', name: 'Belarus', flag: '🇧🇾' }, + { code: 'BE', name: 'Belgium', flag: '🇧🇪' }, + { code: 'BJ', name: 'Benin', flag: '🇧🇯' }, + { code: 'BT', name: 'Bhutan', flag: '🇧🇹' }, + { code: 'BO', name: 'Bolivia', flag: '🇧🇴' }, + { code: 'BW', name: 'Botswana', flag: '🇧🇼' }, + { code: 'BR', name: 'Brazil', flag: '🇧🇷' }, + { code: 'BF', name: 'Burkina Faso', flag: '🇧🇫' }, + { code: 'BI', name: 'Burundi', flag: '🇧🇮' }, + { code: 'BG', name: 'Bulgaria', flag: '🇧🇬' }, + { code: 'CA', name: 'Canada', flag: '🇨🇦' }, + { code: 'CV', name: 'Cape Verde', flag: '🇨🇻' }, + { code: 'CF', name: 'Central African Republic', flag: '🇨🇫' }, + { code: 'TD', name: 'Chad', flag: '🇹🇩' }, + { code: 'CL', name: 'Chile', flag: '🇨🇱' }, + { code: 'CN', name: 'China', flag: '🇨🇳' }, + { code: 'CO', name: 'Colombia', flag: '🇨🇴' }, + { code: 'KM', name: 'Comoros', flag: '🇰🇲' }, + { code: 'CG', name: 'Republic of Congo', flag: '🇨🇬' }, + { code: 'CD', name: 'DR Congo', flag: '🇨🇩' }, + { code: 'HR', name: 'Croatia', flag: '🇭🇷' }, + { code: 'CY', name: 'Cyprus', flag: '🇨🇾' }, + { code: 'CZ', name: 'Czech Republic', flag: '🇨🇿' }, + { code: 'DK', name: 'Denmark', flag: '🇩🇰' }, + { code: 'DJ', name: 'Djibouti', flag: '🇩🇯' }, + { code: 'EC', name: 'Ecuador', flag: '🇪🇨' }, + { code: 'EG', name: 'Egypt', flag: '🇪🇬' }, + { code: 'ER', name: 'Eritrea', flag: '🇪🇷' }, + { code: 'EE', name: 'Estonia', flag: '🇪🇪' }, + { code: 'ET', name: 'Ethiopia', flag: '🇪🇹' }, + { code: 'FK', name: 'Falkland Islands', flag: '🇫🇰' }, + { code: 'FJ', name: 'Fiji', flag: '🇫🇯' }, + { code: 'FI', name: 'Finland', flag: '🇫🇮' }, + { code: 'FR', name: 'France', flag: '🇫🇷' }, + { code: 'PF', name: 'French Polynesia', flag: '🇵🇫' }, + { code: 'GF', name: 'French Guiana', flag: '🇬🇫' }, + { code: 'GA', name: 'Gabon', flag: '🇬🇦' }, + { code: 'GM', name: 'Gambia', flag: '🇬🇲' }, + { code: 'GE', name: 'Georgia', flag: '🇬🇪' }, + { code: 'DE', name: 'Germany', flag: '🇩🇪' }, + { code: 'GH', name: 'Ghana', flag: '🇬🇭' }, + { code: 'GR', name: 'Greece', flag: '🇬🇷' }, + { code: 'GN', name: 'Guinea', flag: '🇬🇳' }, + { code: 'GW', name: 'Guinea-Bissau', flag: '🇬🇼' }, + { code: 'GY', name: 'Guyana', flag: '🇬🇾' }, + { code: 'HK', name: 'Hong Kong', flag: '🇭🇰' }, + { code: 'HU', name: 'Hungary', flag: '🇭🇺' }, + { code: 'IN', name: 'India', flag: '🇮🇳' }, + { code: 'ID', name: 'Indonesia', flag: '🇮🇩' }, + { code: 'IQ', name: 'Iraq', flag: '🇮🇶' }, + { code: 'IE', name: 'Ireland', flag: '🇮🇪' }, + { code: 'IL', name: 'Israel', flag: '🇮🇱' }, + { code: 'IT', name: 'Italy', flag: '🇮🇹' }, + { code: 'CI', name: 'Ivory Coast', flag: '🇨🇮' }, + { code: 'JP', name: 'Japan', flag: '🇯🇵' }, + { code: 'JO', name: 'Jordan', flag: '🇯🇴' }, + { code: 'KZ', name: 'Kazakhstan', flag: '🇰🇿' }, + { code: 'KE', name: 'Kenya', flag: '🇰🇪' }, + { code: 'KH', name: 'Cambodia', flag: '🇰🇭' }, + { code: 'KW', name: 'Kuwait', flag: '🇰🇼' }, + { code: 'LA', name: 'Laos', flag: '🇱🇦' }, + { code: 'LV', name: 'Latvia', flag: '🇱🇻' }, + { code: 'LB', name: 'Lebanon', flag: '🇱🇧' }, + { code: 'LS', name: 'Lesotho', flag: '🇱🇸' }, + { code: 'LR', name: 'Liberia', flag: '🇱🇷' }, + { code: 'LY', name: 'Libya', flag: '🇱🇾' }, + { code: 'LT', name: 'Lithuania', flag: '🇱🇹' }, + { code: 'MG', name: 'Madagascar', flag: '🇲🇬' }, + { code: 'MW', name: 'Malawi', flag: '🇲🇼' }, + { code: 'MY', name: 'Malaysia', flag: '🇲🇾' }, + { code: 'MV', name: 'Maldives', flag: '🇲🇻' }, + { code: 'ML', name: 'Mali', flag: '🇲🇱' }, + { code: 'MT', name: 'Malta', flag: '🇲🇹' }, + { code: 'MR', name: 'Mauritania', flag: '🇲🇷' }, + { code: 'MU', name: 'Mauritius', flag: '🇲🇺' }, + { code: 'MX', name: 'Mexico', flag: '🇲🇽' }, + { code: 'MD', name: 'Moldova', flag: '🇲🇩' }, + { code: 'MN', name: 'Mongolia', flag: '🇲🇳' }, + { code: 'MM', name: 'Myanmar', flag: '🇲🇲' }, + { code: 'MZ', name: 'Mozambique', flag: '🇲🇿' }, + { code: 'NA', name: 'Namibia', flag: '🇳🇦' }, + { code: 'NC', name: 'New Caledonia', flag: '🇳🇨' }, + { code: 'NP', name: 'Nepal', flag: '🇳🇵' }, + { code: 'NL', name: 'Netherlands', flag: '🇳🇱' }, + { code: 'NZ', name: 'New Zealand', flag: '🇳🇿' }, + { code: 'NE', name: 'Niger', flag: '🇳🇪' }, + { code: 'NG', name: 'Nigeria', flag: '🇳🇬' }, + { code: 'KP', name: 'North Korea', flag: '🇰🇵' }, + { code: 'NO', name: 'Norway', flag: '🇳🇴' }, + { code: 'OM', name: 'Oman', flag: '🇴🇲' }, + { code: 'PK', name: 'Pakistan', flag: '🇵🇰' }, + { code: 'PG', name: 'Papua New Guinea', flag: '🇵🇬' }, + { code: 'PY', name: 'Paraguay', flag: '🇵🇾' }, + { code: 'PE', name: 'Peru', flag: '🇵🇪' }, + { code: 'PH', name: 'Philippines', flag: '🇵🇭' }, + { code: 'PL', name: 'Poland', flag: '🇵🇱' }, + { code: 'PT', name: 'Portugal', flag: '🇵🇹' }, + { code: 'QA', name: 'Qatar', flag: '🇶🇦' }, + { code: 'RO', name: 'Romania', flag: '🇷🇴' }, + { code: 'RU', name: 'Russia', flag: '🇷🇺' }, + { code: 'RW', name: 'Rwanda', flag: '🇷🇼' }, + { code: 'SA', name: 'Saudi Arabia', flag: '🇸🇦' }, + { code: 'SC', name: 'Seychelles', flag: '🇸🇨' }, + { code: 'SG', name: 'Singapore', flag: '🇸🇬' }, + { code: 'SK', name: 'Slovakia', flag: '🇸🇰' }, + { code: 'SI', name: 'Slovenia', flag: '🇸🇮' }, + { code: 'SB', name: 'Solomon Islands', flag: '🇸🇧' }, + { code: 'SO', name: 'Somalia', flag: '🇸🇴' }, + { code: 'ZA', name: 'South Africa', flag: '🇿🇦' }, + { code: 'KR', name: 'South Korea', flag: '🇰🇷' }, + { code: 'SS', name: 'South Sudan', flag: '🇸🇸' }, + { code: 'ES', name: 'Spain', flag: '🇪🇸' }, + { code: 'LK', name: 'Sri Lanka', flag: '🇱🇰' }, + { code: 'SR', name: 'Suriname', flag: '🇸🇷' }, + { code: 'SE', name: 'Sweden', flag: '🇸🇪' }, + { code: 'CH', name: 'Switzerland', flag: '🇨🇭' }, + { code: 'SY', name: 'Syria', flag: '🇸🇾' }, + { code: 'SZ', name: 'Eswatini', flag: '🇸🇿' }, + { code: 'TW', name: 'Taiwan', flag: '🇹🇼' }, + { code: 'TZ', name: 'Tanzania', flag: '🇹🇿' }, + { code: 'TH', name: 'Thailand', flag: '🇹🇭' }, + { code: 'TL', name: 'Timor-Leste', flag: '🇹🇱' }, + { code: 'TG', name: 'Togo', flag: '🇹🇬' }, + { code: 'TR', name: 'Turkey', flag: '🇹🇷' }, + { code: 'UG', name: 'Uganda', flag: '🇺🇬' }, + { code: 'UA', name: 'Ukraine', flag: '🇺🇦' }, + { code: 'AE', name: 'United Arab Emirates', flag: '🇦🇪' }, + { code: 'GB', name: 'United Kingdom', flag: '🇬🇧' }, + { code: 'US', name: 'United States', flag: '🇺🇸' }, + { code: 'UY', name: 'Uruguay', flag: '🇺🇾' }, + { code: 'VU', name: 'Vanuatu', flag: '🇻🇺' }, + { code: 'VE', name: 'Venezuela', flag: '🇻🇪' }, + { code: 'VN', name: 'Vietnam', flag: '🇻🇳' }, + { code: 'ZM', name: 'Zambia', flag: '🇿🇲' }, + { code: 'ZW', name: 'Zimbabwe', flag: '🇿🇼' }, + { code: 'Other', name: 'Other', flag: '🌍' }, +] + +interface CountrySelectorProps { + value: string + onValueChange: (value: string) => void + placeholder?: string +} + +export function CountrySelector({ + value, + onValueChange, + placeholder = 'Select your country', +}: CountrySelectorProps) { + return ( + + ) +} diff --git a/apps/www/components/Supasquad/CtaSection.tsx b/apps/www/components/Supasquad/CtaSection.tsx new file mode 100644 index 00000000000..a73aa95a006 --- /dev/null +++ b/apps/www/components/Supasquad/CtaSection.tsx @@ -0,0 +1,57 @@ +import Link from 'next/link' +import React from 'react' +import { Button, cn, Image } from 'ui' +import SectionContainer from '~/components/Layouts/SectionContainer' + +interface Props { + id: string + title: string | React.ReactNode + subtitle?: string + primaryCta: { + label: string + url: string + target?: string + icon?: React.ReactNode + } + secondaryCta?: { + label: string + url: string + } + image?: { + dark: string + light: string + } + className?: string +} + +const CtaSection = ({ id, title, subtitle, primaryCta, secondaryCta, className }: Props) => { + return ( + +
    +
    +

    {title}

    + {subtitle &&

    {subtitle}

    } +
    + + {secondaryCta && ( + + )} +
    +
    +
    + +
    +
    + ) +} + +export default CtaSection diff --git a/apps/www/components/Supasquad/FeatureIcon.tsx b/apps/www/components/Supasquad/FeatureIcon.tsx new file mode 100644 index 00000000000..a312031c38e --- /dev/null +++ b/apps/www/components/Supasquad/FeatureIcon.tsx @@ -0,0 +1,81 @@ +import React, { FC } from 'react' + +import { cn } from 'ui' +import type { Feature } from '~/data/open-source/contributing/supasquad.utils' +import { + Award, + Zap, + MessageSquare, + DollarSign, + Gift, + TrendingUp, + Heart, + LifeBuoy, + Wrench, + Shield, +} from 'lucide-react' + +const ICONS = { + award: Award, + zap: Zap, + 'message-square': MessageSquare, + 'dollar-sign': DollarSign, + gift: Gift, + 'trending-up': TrendingUp, + heart: Heart, + 'life-buoy': LifeBuoy, + wrench: Wrench, + shield: Shield, +} as const + +type IconName = keyof typeof ICONS + +// Type guard to check if a string is a valid icon name +function isValidIconName(icon: string): icon is IconName { + return icon in ICONS +} + +interface FeatureIconProps { + icon: Feature['icon'] + iconNoStroke: Feature['iconNoStroke'] + strokeWidth?: number +} + +const FeatureIcon: FC = ({ icon, iconNoStroke, strokeWidth = 1.5 }) => { + const Icon = icon && isValidIconName(icon) ? ICONS[icon] : null + const iconSize = 7 + const iconWidth = `w-${iconSize}` + const iconHeight = `h-${iconSize}` + + return ( + Icon && + (typeof Icon === 'string' ? ( + + + + ) : ( + + )) + ) +} + +export default FeatureIcon diff --git a/apps/www/components/Supasquad/FeaturesSection.tsx b/apps/www/components/Supasquad/FeaturesSection.tsx new file mode 100644 index 00000000000..bc2cc753190 --- /dev/null +++ b/apps/www/components/Supasquad/FeaturesSection.tsx @@ -0,0 +1,52 @@ +import React, { FC } from 'react' +import dynamic from 'next/dynamic' +import { cn } from 'ui' +import SectionContainer from '~/components/Layouts/SectionContainer' +import type { + Feature, + FeaturesSection as FeaturesSectionType, +} from '~/data/open-source/contributing/supasquad.utils' + +const FeatureIcon = dynamic(() => import('~/components/Supasquad/FeatureIcon'), { ssr: false }) + +const FeaturesSection = (props: FeaturesSectionType) => { + return ( + +
    + {props.label} +

    {props.heading}

    + {props.subheading &&

    {props.subheading}

    } +
    +
      + {props.features?.map((feature: Feature, index: number) => ( + + ))} +
    +
    + ) +} + +interface FeatureItemProps { + feature: Feature +} + +const FeatureItem: FC = ({ feature }) => { + return ( +
  • + +
    + +
    +

    {feature.heading}

    +

    {feature.subheading}

    + {/* */} +
  • + ) +} + +export default FeaturesSection diff --git a/apps/www/components/Supasquad/PerfectTiming.tsx b/apps/www/components/Supasquad/PerfectTiming.tsx new file mode 100644 index 00000000000..beea6adf4da --- /dev/null +++ b/apps/www/components/Supasquad/PerfectTiming.tsx @@ -0,0 +1,110 @@ +import React, { FC } from 'react' +import { cn, Badge } from 'ui' +import SectionContainer from '~/components/Layouts/SectionContainer' + +export interface PerfectTimingProps { + id: string + heading: string | JSX.Element + subheading: string | JSX.Element + highlights: Highlight[] +} + +type Highlight = { + heading: string + subheading: string +} + +const PerfectTiming = (props: PerfectTimingProps) => { + return ( + +
    +
    + {/* {props.label} */} +

    {props.heading}

    +

    {props.subheading}

    +
    +
    + {props.highlights.map((highlight) => ( + + ))} +
    +
    +
    + + + + + + + + + + + +
    +
    + + ) +} + +const GraphLabel: FC<{ className?: string }> = ({ className }) => ( +
    +
    + Users +
    + 230,550 + + +13.4% + +
    +
    +
    +
    +) + +interface HighlightItemProps { + highlight: Highlight +} + +const HighlightItem: FC = ({ highlight }) => { + return ( +
  • + {highlight.heading} +

    {highlight.subheading}

    +
  • + ) +} + +export default PerfectTiming diff --git a/apps/www/components/Supasquad/Quotes.tsx b/apps/www/components/Supasquad/Quotes.tsx new file mode 100644 index 00000000000..a278ca2971c --- /dev/null +++ b/apps/www/components/Supasquad/Quotes.tsx @@ -0,0 +1,90 @@ +import 'swiper/css' + +import Link from 'next/link' +import { FC } from 'react' +import { Swiper, SwiperSlide } from 'swiper/react' + +import SectionContainer from '~/components/Layouts/SectionContainer' +import Panel from '~/components/Panel' + +import type { Quote, Quotes } from '~/data/solutions/solutions.utils' +import Image from 'next/image' + +const Quotes: FC = (props) => ( +
    +
    + +
      + {props.items?.map((quote: Quote) => ( +
    • + +
    • + ))} +
    +
    + + {props.items?.map((quote: Quote, i: number) => ( + + + + ))} + +
    +
    +
    +
    +) + +const QuoteCard: FC = ({ quote, author, avatar, authorTitle }) => { + return ( + +
    + {quote} +
    + +
    + +
    + {author} + {authorTitle && ( + + {authorTitle} + + )} +
    +
    +
    + ) +} + +export default Quotes diff --git a/apps/www/components/SurveyResults/DecorativeProgressBar.tsx b/apps/www/components/SurveyResults/DecorativeProgressBar.tsx new file mode 100644 index 00000000000..ef691aaa401 --- /dev/null +++ b/apps/www/components/SurveyResults/DecorativeProgressBar.tsx @@ -0,0 +1,70 @@ +interface DecorativeProgressBarProps { + /** Whether to reverse the animation direction */ + reverse?: boolean + /** Whether to align to start or end */ + align?: 'start' | 'end' +} + +export function DecorativeProgressBar({ + reverse = false, + align = 'start', +}: DecorativeProgressBarProps) { + // Define the three progress bar configurations + const progressBars = [ + { + height: 'h-4', + bgColor: 'bg-foreground-muted/80', + fgColor: 'bg-brand', + animationDelay: '0s', + }, + { + height: 'h-8', + bgColor: 'bg-foreground-muted/50', + fgColor: 'bg-brand-500', + animationDelay: '0.3s', + }, + { + height: 'h-12', + bgColor: 'bg-foreground-muted/20', + fgColor: 'bg-brand-300', + animationDelay: '0.6s', + }, + ] + + return ( +