diff --git a/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTabV2/FilesViewer.tsx b/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTabV2/FilesViewer.tsx
index 6ece9fab583..7ed57d47876 100644
--- a/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTabV2/FilesViewer.tsx
+++ b/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTabV2/FilesViewer.tsx
@@ -1,28 +1,43 @@
import { useState } from 'react'
-import { cn } from 'ui'
+import { cn, Dialog, DialogContent } from 'ui'
export const FilesViewer = ({ files }: { files: string[] }) => {
const [selected, setSelected] = useState(files[0])
+ const [showDialog, setShowDialog] = useState(false)
return (
-
-

- {files.length > 1 && (
-
- {files.map((x) => (
-
- ))}
-
- )}
-
+ <>
+
+
+
+ {files.length > 1 && (
+
+ {files.map((x) => (
+
+ ))}
+
+ )}
+
+
+ >
)
}
diff --git a/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTabV2/InstallIntegrationSheet/InstallOAuthIntegrationButton.tsx b/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTabV2/InstallIntegrationSheet/InstallOAuthIntegrationButton.tsx
new file mode 100644
index 00000000000..663288b0323
--- /dev/null
+++ b/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTabV2/InstallIntegrationSheet/InstallOAuthIntegrationButton.tsx
@@ -0,0 +1,81 @@
+import { useParams } from 'common'
+import { useMemo } from 'react'
+import { toast } from 'sonner'
+import { Button } from 'ui'
+
+import type { IntegrationDefinition } from '@/components/interfaces/Integrations/Landing/Integrations.constants'
+import { useAPIKeysQuery } from '@/data/api-keys/api-keys-query'
+import { useInstallOAuthIntegrationMutation } from '@/data/marketplace/install-oauth-integration-mutation'
+
+interface InstallOAuthIntegrationButtonProps {
+ integration: IntegrationDefinition
+}
+
+export function InstallOAuthIntegrationButton({ integration }: InstallOAuthIntegrationButtonProps) {
+ const { ref: projectRef } = useParams()
+
+ const { data: apiKeys, isLoading: isApiKeysLoading } = useAPIKeysQuery(
+ { projectRef, reveal: false },
+ { enabled: !!projectRef }
+ )
+
+ const { mutate: installOAuthIntegration, isPending: isInstalling } =
+ useInstallOAuthIntegrationMutation({
+ onSuccess: (data) => {
+ if ('redirectUrl' in data) {
+ if (!data.redirectUrl) {
+ toast.error('Failed to redirect because redirect URL is invalid')
+ return
+ }
+ window.location.href = data.redirectUrl
+ } else {
+ toast.error('Failed to start integration installation')
+ }
+ },
+ })
+
+ const isLoading =
+ integration.installIdentificationMethod === 'secret_key_prefix' && isApiKeysLoading
+
+ const isIntegrationInstalled = useMemo(() => {
+ if (!integration) return false
+
+ const prefix = integration.secretKeyPrefix
+
+ if (integration.installIdentificationMethod !== 'secret_key_prefix' || !prefix) return false
+ if (isApiKeysLoading || !apiKeys) return false
+
+ return apiKeys.some((k) => k.type === 'secret' && k.name.startsWith(prefix))
+ }, [apiKeys, integration, isApiKeysLoading])
+
+ const handleInstallClick = async () => {
+ if (!integration || !projectRef) return
+
+ if (integration.installUrlType === 'post') {
+ if (!integration.listingId) return toast.error('Listing ID is required')
+ installOAuthIntegration({ projectRef, id: integration.listingId })
+ } else {
+ window.location.href = integration.installUrl ?? '/'
+ }
+ }
+
+ return (
+ <>
+ {isIntegrationInstalled ? (
+
+ ) : (
+
+ )}
+ >
+ )
+}
diff --git a/apps/studio/components/interfaces/Integrations/Landing/Integrations.constants.tsx b/apps/studio/components/interfaces/Integrations/Landing/Integrations.constants.tsx
index 3e1a8d1d994..186d7e4bbd7 100644
--- a/apps/studio/components/interfaces/Integrations/Landing/Integrations.constants.tsx
+++ b/apps/studio/components/interfaces/Integrations/Landing/Integrations.constants.tsx
@@ -46,6 +46,10 @@ type IntegrationStep = {
description?: string
}
+type InstallUrlType = 'get' | 'post'
+
+type InstallIdentificationMethod = 'secret_key_prefix'
+
/**
* [Joshen] For marketplace, we probably need to revisit this definition
* What properties are obsolete, what properties we need from remote source
@@ -97,6 +101,13 @@ export type IntegrationDefinition = {
inputs?: IntegrationInputs
/** Purely visual, just to show what are the changes on the project from installing the integration */
steps?: IntegrationStep[]
+
+ /** These are for OAuth Integrations */
+ installUrl?: string | null
+ installUrlType?: InstallUrlType
+ installIdentificationMethod?: InstallIdentificationMethod
+ secretKeyPrefix?: string
+ listingId?: string
} & (
| { type: 'wrapper'; meta: WrapperMeta }
| { type: 'postgres_extension' | 'custom' | 'oauth' | 'template' }
diff --git a/apps/studio/components/interfaces/Integrations/Landing/useAvailableIntegrations.tsx b/apps/studio/components/interfaces/Integrations/Landing/useAvailableIntegrations.tsx
index 33bafdfae6a..f65a769b7c4 100644
--- a/apps/studio/components/interfaces/Integrations/Landing/useAvailableIntegrations.tsx
+++ b/apps/studio/components/interfaces/Integrations/Landing/useAvailableIntegrations.tsx
@@ -2,6 +2,7 @@ import { useQuery } from '@tanstack/react-query'
import { FeatureFlagContext, IS_PLATFORM, useFlag } from 'common'
import { Boxes } from 'lucide-react'
import dynamic from 'next/dynamic'
+import Image from 'next/image'
import { useContext, useMemo } from 'react'
import { cn } from 'ui'
@@ -10,6 +11,11 @@ import { marketplaceIntegrationsQueryOptions } from '@/data/marketplace/integrat
import { useCLIReleaseVersionQuery } from '@/data/misc/cli-release-version-query'
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
+const fullImageUrl = (imagePath: string) => {
+ const API_URL = process.env.NEXT_PUBLIC_MARKETPLACE_API_URL || ''
+ return `${API_URL}${imagePath}`
+}
+
/**
* [Joshen] Returns a combination of
* - Marketplace integrations retrieved remotely (Only if feature flag enabled)
@@ -33,69 +39,93 @@ export const useAvailableIntegrations = () => {
// [Joshen] Format marketplace integrations into existing ones for now
// Likely that we might need to change, but can look into separately
- const marketplaceIntegrations: IntegrationDefinition[] = (data ?? [])?.map((integration) => {
- const {
- id,
- type,
- categories,
- title: name,
- summary: description,
- documentation_url: docsUrl,
- url: siteUrl,
- content,
- files,
- } = integration
+ const marketplaceIntegrations: IntegrationDefinition[] = useMemo(
+ () =>
+ (data ?? [])?.map((integration) => {
+ const {
+ id: listingId,
+ slug,
+ categories,
+ title,
+ description,
+ documentation_url: docsUrl,
+ website_url: siteUrl,
+ installation_url: installUrl,
+ installation_url_type: installUrlType,
+ installation_identification_method: installMethod,
+ secret_key_prefix: secretKeyPrefix,
+ images,
+ content,
+ partner_name: authorName,
+ listing_logo: listingLogo,
+ } = integration
- const status = undefined
- const author = { name: '', websiteUrl: '' }
+ const status = undefined
+ const author = { name: authorName ?? '', websiteUrl: '' }
- return {
- id: id.toString(),
- name,
- status,
- type,
- categories: categories.map((x) => x.slug),
- content,
- files,
- description,
- docsUrl,
- siteUrl,
- author,
- requiredExtensions: [],
- icon: ({ className, ...props } = {}) => (
-
- ),
- navigation: [
- {
- route: 'overview',
- label: 'Overview',
- },
- ],
- navigate: ({ pageId = 'overview' }) => {
- switch (pageId) {
- case 'overview':
- return dynamic(
- () =>
- import('@/components/interfaces/Integrations/Integration/IntegrationOverviewTabV2/index').then(
- (mod) => mod.IntegrationOverviewTabV2
- ),
- {
- loading: Loading,
- }
- )
- case 'secrets':
- return dynamic(
- () =>
- import('../Vault/Secrets/SecretsManagement').then((mod) => mod.SecretsManagement),
- {
- loading: Loading,
- }
- )
+ return {
+ id: slug ?? '',
+ name: title ?? '',
+ status,
+ type: 'oauth' as const, // Currently marketplace only supports oauth apps
+ categories: Array.isArray(categories)
+ ? (categories as Array<{ slug: string }>).map((x) => x.slug)
+ : [],
+ content,
+ files: images?.map((image) => fullImageUrl(image)),
+ description,
+ docsUrl,
+ siteUrl,
+ installUrl,
+ installUrlType: installUrlType ?? undefined,
+ installIdentificationMethod: installMethod ?? undefined,
+ secretKeyPrefix: secretKeyPrefix ?? undefined,
+ listingId: listingId ?? undefined,
+ author,
+ requiredExtensions: [],
+ icon: ({ className, ...props } = {}) => (
+
+ {listingLogo ? (
+
+ ) : (
+
+ )}
+
+ ),
+ navigation: [
+ {
+ route: 'overview',
+ label: 'Overview',
+ },
+ ],
+ navigate: ({ pageId = 'overview' }) => {
+ switch (pageId) {
+ case 'overview':
+ return dynamic(
+ () =>
+ import('@/components/interfaces/Integrations/Integration/IntegrationOverviewTabV2/index').then(
+ (mod) => mod.IntegrationOverviewTabV2
+ ),
+ {
+ loading: Loading,
+ }
+ )
+ }
+ return null
+ },
}
- return null
- },
- }
- })
+ }),
+ [data]
+ )
// [Joshen] Existing integrations that are defined within studio
// Available integrations are all integrations that can be installed. If an integration can't be installed (needed
@@ -117,13 +147,14 @@ export const useAvailableIntegrations = () => {
})
}, [integrationsWrappers, isCLI])
- const availableIntegrations = useMemo(
- () => allIntegrations.sort((a, b) => a.name.localeCompare(b.name)),
- [allIntegrations]
- )
+ const dataWithMarketplace = useMemo(() => {
+ return [...marketplaceIntegrations, ...allIntegrations].sort((a, b) =>
+ a.name.localeCompare(b.name)
+ )
+ }, [marketplaceIntegrations, allIntegrations])
return {
- data: [...marketplaceIntegrations, ...availableIntegrations],
+ data: dataWithMarketplace,
error,
isPending,
isSuccess,
diff --git a/apps/studio/components/layouts/ProjectIntegrationsLayout.tsx b/apps/studio/components/layouts/ProjectIntegrationsLayout.tsx
index bc6c161d74a..fffae6d0743 100644
--- a/apps/studio/components/layouts/ProjectIntegrationsLayout.tsx
+++ b/apps/studio/components/layouts/ProjectIntegrationsLayout.tsx
@@ -85,8 +85,8 @@ const IntegrationCategoriesMenu = ({ page }: { page: string }) => {
items: [],
},
...categories.map((category) => ({
- name: category.title,
- key: category.slug,
+ name: category.name ?? '',
+ key: category.slug ?? '',
url: `/project/${ref}/integrations?category=${category.slug}`,
items: [],
})),
diff --git a/apps/studio/csp.ts b/apps/studio/csp.ts
index c3dd416efc7..9c6218254c2 100644
--- a/apps/studio/csp.ts
+++ b/apps/studio/csp.ts
@@ -5,6 +5,10 @@ const SUPABASE_URL = process.env.SUPABASE_URL ? new URL(process.env.SUPABASE_URL
const GOTRUE_URL = process.env.NEXT_PUBLIC_GOTRUE_URL
? new URL(process.env.NEXT_PUBLIC_GOTRUE_URL).origin
: ''
+const MARKETPLACE_API_URL = process.env.NEXT_PUBLIC_MARKETPLACE_API_URL
+ ? new URL(process.env.NEXT_PUBLIC_MARKETPLACE_API_URL).origin
+ : ''
+
const SUPABASE_PROJECTS_URL = 'https://*.supabase.co https://*.storage.supabase.co'
const SUPABASE_PROJECTS_URL_WS = 'wss://*.supabase.co'
@@ -85,6 +89,7 @@ export function getCSP() {
API_URL,
SUPABASE_URL,
GOTRUE_URL,
+ MARKETPLACE_API_URL,
SUPABASE_LOCAL_PROJECTS_URL_WS,
SUPABASE_PROJECTS_URL,
SUPABASE_PROJECTS_URL_WS,
@@ -130,6 +135,7 @@ export function getCSP() {
USERCENTRICS_APP_URL,
STAPE_URL,
USERCENTRICS_URLS,
+ MARKETPLACE_API_URL,
...(!!NIMBUS_PROD_PROJECTS_URL ? [NIMBUS_PROD_PROJECTS_URL, NIMBUS_PROD_PROJECTS_URL_WS] : []),
]
const STYLE_SRC_URLS = [CLOUDFLARE_CDN_URL, SUPABASE_ASSETS_URL]
diff --git a/apps/studio/data/marketplace/install-oauth-integration-mutation.ts b/apps/studio/data/marketplace/install-oauth-integration-mutation.ts
new file mode 100644
index 00000000000..30108618770
--- /dev/null
+++ b/apps/studio/data/marketplace/install-oauth-integration-mutation.ts
@@ -0,0 +1,52 @@
+import { useMutation } from '@tanstack/react-query'
+import { toast } from 'sonner'
+
+import { handleError, post } from '@/data/fetchers'
+import type { ResponseError, UseCustomMutationOptions } from '@/types'
+
+export type OAuthIntegrationInstallVariables = {
+ projectRef: string
+ id: string
+}
+
+export async function installOAuthIntegration({
+ projectRef,
+ id,
+}: OAuthIntegrationInstallVariables) {
+ const { data, error } = await post('/platform/integrations/partners/{ref}/{listing_id}', {
+ params: { path: { ref: projectRef, listing_id: id } },
+ })
+
+ if (error) handleError(error)
+ return data
+}
+
+type OAuthIntegrationInstallData = Awaited>
+
+export const useInstallOAuthIntegrationMutation = ({
+ onSuccess,
+ onError,
+ ...options
+}: Omit<
+ UseCustomMutationOptions<
+ OAuthIntegrationInstallData,
+ ResponseError,
+ OAuthIntegrationInstallVariables
+ >,
+ 'mutationFn'
+> = {}) => {
+ return useMutation({
+ mutationFn: (vars) => installOAuthIntegration(vars),
+ async onSuccess(data, variables, context) {
+ await onSuccess?.(data, variables, context)
+ },
+ async onError(data, variables, context) {
+ if (onError === undefined) {
+ toast.error(`Failed to start OAuth integration installation: ${data.message}`)
+ } else {
+ onError(data, variables, context)
+ }
+ },
+ ...options,
+ })
+}
diff --git a/apps/studio/data/marketplace/integration-categories-query.ts b/apps/studio/data/marketplace/integration-categories-query.ts
index e86439ae4a9..06a45287454 100644
--- a/apps/studio/data/marketplace/integration-categories-query.ts
+++ b/apps/studio/data/marketplace/integration-categories-query.ts
@@ -1,12 +1,12 @@
import { queryOptions } from '@tanstack/react-query'
+import { createMarketplaceClient } from 'common/marketplace-client'
import { marketplaceIntegrationsKeys } from './keys'
-import { createMarketplaceClient } from './marketplace-client'
import { handleError } from '@/data/fetchers'
async function getMarketplaceCategories() {
- const client = createMarketplaceClient()
- const { data, error } = await client.from('categories').select('*')
+ const marketplaceClient = createMarketplaceClient()
+ const { data, error } = await marketplaceClient.from('categories').select('*')
if (error) handleError(error)
return data ?? []
diff --git a/apps/studio/data/marketplace/integrations-query.ts b/apps/studio/data/marketplace/integrations-query.ts
index 6114ed024c1..51a7db647ac 100644
--- a/apps/studio/data/marketplace/integrations-query.ts
+++ b/apps/studio/data/marketplace/integrations-query.ts
@@ -1,14 +1,15 @@
import { queryOptions } from '@tanstack/react-query'
+import { createMarketplaceClient } from 'common/marketplace-client'
import { marketplaceIntegrationsKeys } from './keys'
-import { createMarketplaceClient } from './marketplace-client'
import { handleError } from '@/data/fetchers'
async function getMarketplaceIntegrations() {
- const client = createMarketplaceClient()
- const { data, error } = await client
- .from('items')
- .select('*, categories:category_items(...categories(slug, title))')
+ const marketplaceClient = createMarketplaceClient()
+ const { data, error } = await marketplaceClient
+ .from('listings')
+ .select('*')
+ .or('publish_location.eq.dashboard,publish_location.eq.both')
if (error) handleError(error)
return data ?? []
diff --git a/apps/studio/data/marketplace/marketplace.types.ts b/apps/studio/data/marketplace/marketplace.types.ts
deleted file mode 100644
index 06d17342560..00000000000
--- a/apps/studio/data/marketplace/marketplace.types.ts
+++ /dev/null
@@ -1,453 +0,0 @@
-export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[]
-
-export type Database = {
- // Allows to automatically instantiate createClient with right options
- // instead of createClient(URL, KEY)
- __InternalSupabase: {
- PostgrestVersion: '14.4'
- }
- graphql_public: {
- Tables: {
- [_ in never]: never
- }
- Views: {
- [_ in never]: never
- }
- Functions: {
- graphql: {
- Args: {
- extensions?: Json
- operationName?: string
- query?: string
- variables?: Json
- }
- Returns: Json
- }
- }
- Enums: {
- [_ in never]: never
- }
- CompositeTypes: {
- [_ in never]: never
- }
- }
- public: {
- Tables: {
- categories: {
- Row: {
- created_at: string
- description: string | null
- id: number
- slug: string
- title: string
- updated_at: string
- }
- Insert: {
- created_at?: string
- description?: string | null
- id?: never
- slug: string
- title: string
- updated_at?: string
- }
- Update: {
- created_at?: string
- description?: string | null
- id?: never
- slug?: string
- title?: string
- updated_at?: string
- }
- Relationships: []
- }
- category_items: {
- Row: {
- category_id: number
- created_at: string
- item_id: number
- }
- Insert: {
- category_id: number
- created_at?: string
- item_id: number
- }
- Update: {
- category_id?: number
- created_at?: string
- item_id?: number
- }
- Relationships: [
- {
- foreignKeyName: 'category_items_category_id_fkey'
- columns: ['category_id']
- isOneToOne: false
- referencedRelation: 'categories'
- referencedColumns: ['id']
- },
- {
- foreignKeyName: 'category_items_item_id_fkey'
- columns: ['item_id']
- isOneToOne: false
- referencedRelation: 'items'
- referencedColumns: ['id']
- },
- ]
- }
- item_reviews: {
- Row: {
- created_at: string
- featured: boolean
- item_id: number
- published_at: string | null
- review_notes: string | null
- reviewed_at: string | null
- reviewed_by: string | null
- status: Database['public']['Enums']['marketplace_review_status']
- updated_at: string
- }
- Insert: {
- created_at?: string
- featured?: boolean
- item_id: number
- published_at?: string | null
- review_notes?: string | null
- reviewed_at?: string | null
- reviewed_by?: string | null
- status?: Database['public']['Enums']['marketplace_review_status']
- updated_at?: string
- }
- Update: {
- created_at?: string
- featured?: boolean
- item_id?: number
- published_at?: string | null
- review_notes?: string | null
- reviewed_at?: string | null
- reviewed_by?: string | null
- status?: Database['public']['Enums']['marketplace_review_status']
- updated_at?: string
- }
- Relationships: [
- {
- foreignKeyName: 'item_reviews_item_id_fkey'
- columns: ['item_id']
- isOneToOne: true
- referencedRelation: 'items'
- referencedColumns: ['id']
- },
- ]
- }
- items: {
- Row: {
- content: string | null
- created_at: string
- documentation_url: string | null
- files: string[]
- id: number
- partner_id: number
- published: boolean
- registry_item_url: string | null
- slug: string
- submitted_by: string | null
- summary: string | null
- title: string
- type: Database['public']['Enums']['marketplace_item_type']
- updated_at: string
- url: string | null
- }
- Insert: {
- content?: string | null
- created_at?: string
- documentation_url?: string | null
- files?: string[]
- id?: never
- partner_id: number
- published?: boolean
- registry_item_url?: string | null
- slug: string
- submitted_by?: string | null
- summary?: string | null
- title: string
- type: Database['public']['Enums']['marketplace_item_type']
- updated_at?: string
- url?: string | null
- }
- Update: {
- content?: string | null
- created_at?: string
- documentation_url?: string | null
- files?: string[]
- id?: never
- partner_id?: number
- published?: boolean
- registry_item_url?: string | null
- slug?: string
- submitted_by?: string | null
- summary?: string | null
- title?: string
- type?: Database['public']['Enums']['marketplace_item_type']
- updated_at?: string
- url?: string | null
- }
- Relationships: [
- {
- foreignKeyName: 'items_partner_id_fkey'
- columns: ['partner_id']
- isOneToOne: false
- referencedRelation: 'partners'
- referencedColumns: ['id']
- },
- ]
- }
- partner_members: {
- Row: {
- created_at: string
- partner_id: number
- role: string
- user_id: string
- }
- Insert: {
- created_at?: string
- partner_id: number
- role?: string
- user_id: string
- }
- Update: {
- created_at?: string
- partner_id?: number
- role?: string
- user_id?: string
- }
- Relationships: [
- {
- foreignKeyName: 'partner_members_partner_id_fkey'
- columns: ['partner_id']
- isOneToOne: false
- referencedRelation: 'partners'
- referencedColumns: ['id']
- },
- ]
- }
- partners: {
- Row: {
- created_at: string
- created_by: string | null
- description: string | null
- id: number
- logo_url: string | null
- role: Database['public']['Enums']['marketplace_partner_role']
- slug: string
- title: string
- updated_at: string
- website: string | null
- }
- Insert: {
- created_at?: string
- created_by?: string | null
- description?: string | null
- id?: never
- logo_url?: string | null
- role?: Database['public']['Enums']['marketplace_partner_role']
- slug: string
- title: string
- updated_at?: string
- website?: string | null
- }
- Update: {
- created_at?: string
- created_by?: string | null
- description?: string | null
- id?: never
- logo_url?: string | null
- role?: Database['public']['Enums']['marketplace_partner_role']
- slug?: string
- title?: string
- updated_at?: string
- website?: string | null
- }
- Relationships: []
- }
- }
- Views: {
- [_ in never]: never
- }
- Functions: {
- add_partner_member: {
- Args: {
- target_email: string
- target_partner_id: number
- target_role?: string
- }
- Returns: {
- created_at: string
- partner_id: number
- role: string
- user_id: string
- }
- SetofOptions: {
- from: '*'
- to: 'partner_members'
- isOneToOne: true
- isSetofReturn: false
- }
- }
- before_user_created_hook: { Args: { event: Json }; Returns: Json }
- is_admin_member: { Args: never; Returns: boolean }
- is_partner_admin: {
- Args: { target_partner_id: number }
- Returns: boolean
- }
- is_partner_member: {
- Args: { target_partner_id: number }
- Returns: boolean
- }
- is_review_manager_member: { Args: never; Returns: boolean }
- is_reviewer_member: { Args: never; Returns: boolean }
- item_latest_review_is_approved: {
- Args: { target_item_id: number }
- Returns: boolean
- }
- storage_object_item_id: { Args: { object_name: string }; Returns: number }
- storage_object_partner_id: {
- Args: { object_name: string }
- Returns: number
- }
- }
- Enums: {
- marketplace_item_type: 'oauth' | 'template'
- marketplace_partner_role: 'partner' | 'reviewer' | 'admin'
- marketplace_review_status: 'draft' | 'pending_review' | 'approved' | 'rejected'
- }
- CompositeTypes: {
- [_ in never]: never
- }
- }
-}
-
-type DatabaseWithoutInternals = Omit
-
-type DefaultSchema = DatabaseWithoutInternals[Extract]
-
-export type Tables<
- DefaultSchemaTableNameOrOptions extends
- | keyof (DefaultSchema['Tables'] & DefaultSchema['Views'])
- | { schema: keyof DatabaseWithoutInternals },
- TableName extends DefaultSchemaTableNameOrOptions extends {
- schema: keyof DatabaseWithoutInternals
- }
- ? keyof (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Tables'] &
- DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Views'])
- : never = never,
-> = DefaultSchemaTableNameOrOptions extends {
- schema: keyof DatabaseWithoutInternals
-}
- ? (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Tables'] &
- DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Views'])[TableName] extends {
- Row: infer R
- }
- ? R
- : never
- : DefaultSchemaTableNameOrOptions extends keyof (DefaultSchema['Tables'] & DefaultSchema['Views'])
- ? (DefaultSchema['Tables'] & DefaultSchema['Views'])[DefaultSchemaTableNameOrOptions] extends {
- Row: infer R
- }
- ? R
- : never
- : never
-
-export type TablesInsert<
- DefaultSchemaTableNameOrOptions extends
- | keyof DefaultSchema['Tables']
- | { schema: keyof DatabaseWithoutInternals },
- TableName extends DefaultSchemaTableNameOrOptions extends {
- schema: keyof DatabaseWithoutInternals
- }
- ? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Tables']
- : never = never,
-> = DefaultSchemaTableNameOrOptions extends {
- schema: keyof DatabaseWithoutInternals
-}
- ? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Tables'][TableName] extends {
- Insert: infer I
- }
- ? I
- : never
- : DefaultSchemaTableNameOrOptions extends keyof DefaultSchema['Tables']
- ? DefaultSchema['Tables'][DefaultSchemaTableNameOrOptions] extends {
- Insert: infer I
- }
- ? I
- : never
- : never
-
-export type TablesUpdate<
- DefaultSchemaTableNameOrOptions extends
- | keyof DefaultSchema['Tables']
- | { schema: keyof DatabaseWithoutInternals },
- TableName extends DefaultSchemaTableNameOrOptions extends {
- schema: keyof DatabaseWithoutInternals
- }
- ? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Tables']
- : never = never,
-> = DefaultSchemaTableNameOrOptions extends {
- schema: keyof DatabaseWithoutInternals
-}
- ? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Tables'][TableName] extends {
- Update: infer U
- }
- ? U
- : never
- : DefaultSchemaTableNameOrOptions extends keyof DefaultSchema['Tables']
- ? DefaultSchema['Tables'][DefaultSchemaTableNameOrOptions] extends {
- Update: infer U
- }
- ? U
- : never
- : never
-
-export type Enums<
- DefaultSchemaEnumNameOrOptions extends
- | keyof DefaultSchema['Enums']
- | { schema: keyof DatabaseWithoutInternals },
- EnumName extends DefaultSchemaEnumNameOrOptions extends {
- schema: keyof DatabaseWithoutInternals
- }
- ? keyof DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions['schema']]['Enums']
- : never = never,
-> = DefaultSchemaEnumNameOrOptions extends {
- schema: keyof DatabaseWithoutInternals
-}
- ? DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions['schema']]['Enums'][EnumName]
- : DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema['Enums']
- ? DefaultSchema['Enums'][DefaultSchemaEnumNameOrOptions]
- : never
-
-export type CompositeTypes<
- PublicCompositeTypeNameOrOptions extends
- | keyof DefaultSchema['CompositeTypes']
- | { schema: keyof DatabaseWithoutInternals },
- CompositeTypeName extends PublicCompositeTypeNameOrOptions extends {
- schema: keyof DatabaseWithoutInternals
- }
- ? keyof DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions['schema']]['CompositeTypes']
- : never = never,
-> = PublicCompositeTypeNameOrOptions extends {
- schema: keyof DatabaseWithoutInternals
-}
- ? DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions['schema']]['CompositeTypes'][CompositeTypeName]
- : PublicCompositeTypeNameOrOptions extends keyof DefaultSchema['CompositeTypes']
- ? DefaultSchema['CompositeTypes'][PublicCompositeTypeNameOrOptions]
- : never
-
-export const Constants = {
- graphql_public: {
- Enums: {},
- },
- public: {
- Enums: {
- marketplace_item_type: ['oauth', 'template'],
- marketplace_partner_role: ['partner', 'reviewer', 'admin'],
- marketplace_review_status: ['draft', 'pending_review', 'approved', 'rejected'],
- },
- },
-} as const
diff --git a/apps/studio/next.config.ts b/apps/studio/next.config.ts
index c0ebf3c40e2..2f3b59c76cf 100644
--- a/apps/studio/next.config.ts
+++ b/apps/studio/next.config.ts
@@ -29,6 +29,17 @@ function getAssetPrefix() {
return `${SUPABASE_ASSETS_URL}/${process.env.SITE_NAME}/${process.env.VERCEL_GIT_COMMIT_SHA?.substring(0, 12) ?? 'unknown'}`
}
+const marketplaceApiUrl = process.env.NEXT_PUBLIC_MARKETPLACE_API_URL
+ ? new URL(process.env.NEXT_PUBLIC_MARKETPLACE_API_URL)
+ : null
+
+const marketplaceApiProtocol: 'http' | 'https' | null =
+ marketplaceApiUrl?.protocol === 'https:'
+ ? 'https'
+ : marketplaceApiUrl?.protocol === 'http:'
+ ? 'http'
+ : null
+
// Use `satisfies` instead of `: NextConfig` so TypeScript preserves narrow
// inferred types (e.g. async headers → Promise). This avoids TS2345 when
// wrapper functions (bundle-analyzer, sentry) resolve their `next` peer
@@ -576,6 +587,16 @@ const nextConfig = {
port: '',
pathname: '**',
},
+ ...(marketplaceApiUrl
+ ? [
+ {
+ ...(marketplaceApiProtocol ? { protocol: marketplaceApiProtocol } : {}),
+ hostname: marketplaceApiUrl.hostname,
+ port: marketplaceApiUrl.port,
+ pathname: '**',
+ },
+ ]
+ : []),
],
},
transpilePackages: [
diff --git a/apps/studio/pages/project/[ref]/integrations/[id]/[pageId]/index.tsx b/apps/studio/pages/project/[ref]/integrations/[id]/[pageId]/index.tsx
index eb49a03adce..36f9b7567fa 100644
--- a/apps/studio/pages/project/[ref]/integrations/[id]/[pageId]/index.tsx
+++ b/apps/studio/pages/project/[ref]/integrations/[id]/[pageId]/index.tsx
@@ -29,6 +29,7 @@ import {
import ShimmeringLoader, { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import { InstallIntegrationSheet } from '@/components/interfaces/Integrations/Integration/IntegrationOverviewTabV2/InstallIntegrationSheet/InstallIntegrationSheet'
+import { InstallOAuthIntegrationButton } from '@/components/interfaces/Integrations/Integration/IntegrationOverviewTabV2/InstallIntegrationSheet/InstallOAuthIntegrationButton'
import { useAvailableIntegrations } from '@/components/interfaces/Integrations/Landing/useAvailableIntegrations'
import { useInstalledIntegrations } from '@/components/interfaces/Integrations/Landing/useInstalledIntegrations'
import { DefaultLayout } from '@/components/layouts/DefaultLayout'
@@ -208,11 +209,7 @@ const IntegrationPage: NextPageWithLayout = () => {
{integration?.type === 'oauth' ? (
-
+
) : isMarketplaceEnabled && !!integration && !isInstalled ? (
) : isMarketplaceEnabled && isInstalled ? (
diff --git a/packages/api-types/types/platform.d.ts b/packages/api-types/types/platform.d.ts
index d262b76b518..49825a991c5 100644
--- a/packages/api-types/types/platform.d.ts
+++ b/packages/api-types/types/platform.d.ts
@@ -670,6 +670,23 @@ export interface paths {
patch?: never
trace?: never
}
+ '/platform/integrations/partners/{ref}/{listing_id}': {
+ parameters: {
+ query?: never
+ header?: never
+ path?: never
+ cookie?: never
+ }
+ get?: never
+ put?: never
+ /** Creates a partner integration and returns the redirect URL */
+ post: operations['PartnerIntegrationsController_createIntegration']
+ delete?: never
+ options?: never
+ head?: never
+ patch?: never
+ trace?: never
+ }
'/platform/integrations/private-link/{slug}': {
parameters: {
query?: never
@@ -7918,6 +7935,23 @@ export interface components {
organization_id: number
overdue_invoice_count: number
}
+ PartnerIntegrationsResponse: {
+ /**
+ * Format: date-time
+ * @description When the integration link expires (1 hour from creation). The user must begin the flow before this time.
+ */
+ expiresAt: string
+ /**
+ * Format: uuid
+ * @description Unique identifier for the integration record
+ */
+ integrationId: string
+ /**
+ * Format: uri
+ * @description URL to redirect the user's browser to
+ */
+ redirectUrl: string
+ }
PauseStatusResponse: {
can_restore: boolean
last_paused_on: string | null
@@ -13210,6 +13244,65 @@ export interface operations {
}
}
}
+ PartnerIntegrationsController_createIntegration: {
+ parameters: {
+ query?: never
+ header?: never
+ path: {
+ /** @description The id of the listing in the marketplace database */
+ listing_id: string
+ /** @description Supabase project ref */
+ ref: string
+ }
+ cookie?: never
+ }
+ requestBody?: never
+ responses: {
+ 201: {
+ headers: {
+ [name: string]: unknown
+ }
+ content: {
+ 'application/json': components['schemas']['PartnerIntegrationsResponse']
+ }
+ }
+ /** @description Unauthorized */
+ 401: {
+ headers: {
+ [name: string]: unknown
+ }
+ content?: never
+ }
+ /** @description Forbidden action */
+ 403: {
+ headers: {
+ [name: string]: unknown
+ }
+ content?: never
+ }
+ /** @description Project or listing not found */
+ 404: {
+ headers: {
+ [name: string]: unknown
+ }
+ content?: never
+ }
+ /** @description Rate limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown
+ }
+ content?: never
+ }
+ /** @description Failed to retrieve listing or project, or failed to create partner integration */
+ 500: {
+ headers: {
+ [name: string]: unknown
+ }
+ content?: never
+ }
+ }
+ }
PrivateLinkController_getPrivateLinkConfig: {
parameters: {
query?: never
diff --git a/apps/studio/data/marketplace/marketplace-client.ts b/packages/common/marketplace-client.ts
similarity index 100%
rename from apps/studio/data/marketplace/marketplace-client.ts
rename to packages/common/marketplace-client.ts
diff --git a/packages/common/marketplace.types.ts b/packages/common/marketplace.types.ts
new file mode 100644
index 00000000000..7799dce739a
--- /dev/null
+++ b/packages/common/marketplace.types.ts
@@ -0,0 +1,243 @@
+export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[]
+
+export type Database = {
+ public: {
+ Tables: {
+ v_secret_id: {
+ Row: {
+ create_secret: string | null
+ }
+ Insert: {
+ create_secret?: string | null
+ }
+ Update: {
+ create_secret?: string | null
+ }
+ Relationships: []
+ }
+ }
+ Views: {
+ categories: {
+ Row: {
+ description: string | null
+ id: string | null
+ name: string | null
+ slug: string | null
+ }
+ Insert: {
+ description?: string | null
+ id?: string | null
+ name?: string | null
+ slug?: string | null
+ }
+ Update: {
+ description?: string | null
+ id?: string | null
+ name?: string | null
+ slug?: string | null
+ }
+ Relationships: []
+ }
+ listings: {
+ Row: {
+ aud: string | null
+ categories: Json | null
+ content: string | null
+ description: string | null
+ documentation_url: string | null
+ featured: boolean | null
+ id: string | null
+ images: string[] | null
+ installation_identification_method: 'secret_key_prefix' | null
+ installation_url: string | null
+ installation_url_type: 'get' | 'post' | null
+ listing_logo: string | null
+ marketplace_url: string | null
+ partner_logo: string | null
+ partner_name: string | null
+ partner_slug: string | null
+ publish_location: 'marketplace' | 'dashboard' | 'both' | null
+ secret_key_prefix: string | null
+ slug: string | null
+ title: string | null
+ website_url: string | null
+ youtube_id: string | null
+ }
+ Relationships: []
+ }
+ partners: {
+ Row: {
+ country: string | null
+ description: string | null
+ id: string | null
+ logo: string | null
+ name: string | null
+ num_of_employees: number | null
+ slug: string | null
+ type: 'technology' | 'expert' | null
+ website: string | null
+ }
+ Insert: {
+ country?: string | null
+ description?: string | null
+ id?: string | null
+ logo?: never
+ name?: string | null
+ num_of_employees?: number | null
+ slug?: string | null
+ type?: 'technology' | 'expert' | null
+ website?: string | null
+ }
+ Update: {
+ country?: string | null
+ description?: string | null
+ id?: string | null
+ logo?: never
+ name?: string | null
+ num_of_employees?: number | null
+ slug?: string | null
+ type?: 'technology' | 'expert' | null
+ website?: string | null
+ }
+ Relationships: []
+ }
+ }
+ Functions: {
+ get_redirect_url: {
+ Args: {
+ p_listing_id: string
+ p_organization_slug: string
+ p_project_id: string
+ }
+ Returns: Json
+ }
+ }
+ Enums: {
+ [_ in never]: never
+ }
+ CompositeTypes: {
+ [_ in never]: never
+ }
+ }
+}
+
+type DatabaseWithoutInternals = Omit
+
+type DefaultSchema = DatabaseWithoutInternals[Extract]
+
+export type Tables<
+ DefaultSchemaTableNameOrOptions extends
+ | keyof (DefaultSchema['Tables'] & DefaultSchema['Views'])
+ | { schema: keyof DatabaseWithoutInternals },
+ TableName extends DefaultSchemaTableNameOrOptions extends {
+ schema: keyof DatabaseWithoutInternals
+ }
+ ? keyof (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Tables'] &
+ DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Views'])
+ : never = never,
+> = DefaultSchemaTableNameOrOptions extends {
+ schema: keyof DatabaseWithoutInternals
+}
+ ? (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Tables'] &
+ DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Views'])[TableName] extends {
+ Row: infer R
+ }
+ ? R
+ : never
+ : DefaultSchemaTableNameOrOptions extends keyof (DefaultSchema['Tables'] & DefaultSchema['Views'])
+ ? (DefaultSchema['Tables'] & DefaultSchema['Views'])[DefaultSchemaTableNameOrOptions] extends {
+ Row: infer R
+ }
+ ? R
+ : never
+ : never
+
+export type TablesInsert<
+ DefaultSchemaTableNameOrOptions extends
+ | keyof DefaultSchema['Tables']
+ | { schema: keyof DatabaseWithoutInternals },
+ TableName extends DefaultSchemaTableNameOrOptions extends {
+ schema: keyof DatabaseWithoutInternals
+ }
+ ? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Tables']
+ : never = never,
+> = DefaultSchemaTableNameOrOptions extends {
+ schema: keyof DatabaseWithoutInternals
+}
+ ? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Tables'][TableName] extends {
+ Insert: infer I
+ }
+ ? I
+ : never
+ : DefaultSchemaTableNameOrOptions extends keyof DefaultSchema['Tables']
+ ? DefaultSchema['Tables'][DefaultSchemaTableNameOrOptions] extends {
+ Insert: infer I
+ }
+ ? I
+ : never
+ : never
+
+export type TablesUpdate<
+ DefaultSchemaTableNameOrOptions extends
+ | keyof DefaultSchema['Tables']
+ | { schema: keyof DatabaseWithoutInternals },
+ TableName extends DefaultSchemaTableNameOrOptions extends {
+ schema: keyof DatabaseWithoutInternals
+ }
+ ? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Tables']
+ : never = never,
+> = DefaultSchemaTableNameOrOptions extends {
+ schema: keyof DatabaseWithoutInternals
+}
+ ? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Tables'][TableName] extends {
+ Update: infer U
+ }
+ ? U
+ : never
+ : DefaultSchemaTableNameOrOptions extends keyof DefaultSchema['Tables']
+ ? DefaultSchema['Tables'][DefaultSchemaTableNameOrOptions] extends {
+ Update: infer U
+ }
+ ? U
+ : never
+ : never
+
+export type Enums<
+ DefaultSchemaEnumNameOrOptions extends
+ | keyof DefaultSchema['Enums']
+ | { schema: keyof DatabaseWithoutInternals },
+ EnumName extends DefaultSchemaEnumNameOrOptions extends {
+ schema: keyof DatabaseWithoutInternals
+ }
+ ? keyof DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions['schema']]['Enums']
+ : never = never,
+> = DefaultSchemaEnumNameOrOptions extends {
+ schema: keyof DatabaseWithoutInternals
+}
+ ? DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions['schema']]['Enums'][EnumName]
+ : DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema['Enums']
+ ? DefaultSchema['Enums'][DefaultSchemaEnumNameOrOptions]
+ : never
+
+export type CompositeTypes<
+ PublicCompositeTypeNameOrOptions extends
+ | keyof DefaultSchema['CompositeTypes']
+ | { schema: keyof DatabaseWithoutInternals },
+ CompositeTypeName extends PublicCompositeTypeNameOrOptions extends {
+ schema: keyof DatabaseWithoutInternals
+ }
+ ? keyof DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions['schema']]['CompositeTypes']
+ : never = never,
+> = PublicCompositeTypeNameOrOptions extends {
+ schema: keyof DatabaseWithoutInternals
+}
+ ? DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions['schema']]['CompositeTypes'][CompositeTypeName]
+ : PublicCompositeTypeNameOrOptions extends keyof DefaultSchema['CompositeTypes']
+ ? DefaultSchema['CompositeTypes'][PublicCompositeTypeNameOrOptions]
+ : never
+
+export const Constants = {
+ public: {
+ Enums: {},
+ },
+} as const