import { makeAutoObservable, runInAction } from 'mobx' import { observer, useLocalObservable } from 'mobx-react-lite' import Link from 'next/link' import { useRouter } from 'next/router' import { ChangeEvent, createContext, useContext, useEffect, useState } from 'react' import { toast } from 'sonner' import VercelIntegrationLayout from 'components/layouts/VercelIntegrationLayout' import { createVercelEnv, fetchVercelProjectEnvs, fetchVercelProjects, prepareVercelEvns, } from 'components/to-be-cleaned/Integration/Vercel.utils' import { databaseIcon, vercelIcon } from 'components/to-be-cleaned/ListIcons' import { Loading } from 'components/ui/Loading' import { useProjectsQuery } from 'data/projects/projects-query' import { withAuth } from 'hooks/misc/withAuth' import { get } from 'lib/common/fetch' import { get as getV2 } from 'data/fetchers' import { API_URL } from 'lib/constants' import { INTEGRATION_ENVS_ALIAS, VERCEL_DEFAULT_EXTERNAL_ID, VERCEL_INTEGRATION_CONFIGS, } from 'lib/vercelConfigs' import type { Dictionary } from 'types' import { Button, Listbox, Select, Separator } from 'ui' import { PlusCircle, ChevronRight, X } from 'lucide-react' import { getAPIKeys } from 'data/config/project-settings-v2-query' interface IVercelIntegrationStore { code: string token: string teamId: string externalId: string configurationId: string next: string currentProjectId?: string selectedVercelProjectId?: string vercelProjects: Dictionary[] loading: boolean waitingIntegration: boolean projectLinks: { vercelProjectId?: string supabaseProjectRef?: string error?: string result?: { status: 'waiting' | 'success' | 'fail'; message?: string } }[] isDeployButtonFlow: boolean integrationComplete: boolean queryParams: Dictionary projectLinkRemaining: number vercelProjectsAvailable: Dictionary[] loadInitialData: () => void } class VercelIntegrationStore implements IVercelIntegrationStore { code: string = '' token: string = '' teamId: string = '' externalId: string = '' configurationId: string = '' next: string = '' currentProjectId?: string selectedVercelProjectId?: string vercelProjects: Dictionary[] = [] loading: boolean = true // @ts-ignore waitingIntegration: boolean projectLinks: { vercelProjectId?: string supabaseProjectRef?: string error?: string result?: { status: 'waiting' | 'success' | 'fail'; message?: string } }[] = [ { vercelProjectId: undefined, supabaseProjectRef: undefined, }, ] constructor() { makeAutoObservable(this) // this.globalStore = globalStore } get isDeployButtonFlow() { return !!this.currentProjectId } // @ts-ignore get queryParams() { return { token: this.token, teamId: this.teamId, 'external-id': this.externalId, configurationId: this.configurationId, vercelProjectId: this.selectedVercelProjectId, next: this.next, } } get projectLinkRemaining() { if (!this.vercelProjects) return 0 const temp = this.vercelProjects?.length - this.projectLinks.length return temp >= 0 ? temp : 0 } get vercelProjectsAvailable() { const existed = this.projectLinks.map((x) => x.vercelProjectId) const filtered = this.vercelProjects.filter((x) => existed.includes(x.id) == false) return filtered } get integrationComplete() { if (!this.waitingIntegration) return false const pendingItegrations = this.projectLinks.filter((x) => !x.result || !x.result?.message) return pendingItegrations.length == 0 } loadInitialData() { this.getQueryParams() this.getVercelAuthToken() } getQueryParams() { const params = new URLSearchParams(window.location.search) this.code = params.get('code') as string this.configurationId = params.get('configurationId') as string this.currentProjectId = params.get('currentProjectId') as string this.next = params.get('next') as string this.externalId = params.get('external-id') || VERCEL_DEFAULT_EXTERNAL_ID this.teamId = params.get('teamId') as string // double check and set VERCEL_DEFAULT_EXTERNAL_ID as default externalId if config invalid const config = VERCEL_INTEGRATION_CONFIGS.find((x) => x.id == this.externalId) if (!config) this.externalId = VERCEL_DEFAULT_EXTERNAL_ID } async getVercelAuthToken() { const query = new URLSearchParams({ code: this.code, }).toString() const response = await get(`${API_URL}/vercel/token?${query}`) if (!response.errror) { this.token = response.token // retrieve vercel projects after getting token await this.getVercelProjects() this.loading = false } else { toast.error(`Failed to retrieve Vercel token: ${response.error.message}`) this.loading = false } } async getVercelProjects() { const { data, error } = await fetchVercelProjects({ vercelTeamId: this.teamId, vercelToken: this.token, }) if (error) { toast.error(error) } else if (data) { this.vercelProjects = data // if currentProjectId available. Auto select it if (this.currentProjectId) { // @ts-ignore const found = data.find((x: { id: string }) => (x.id = this.currentProjectId)) if (found) this.selectedVercelProjectId = found.id } } } } const PageContext = createContext(undefined!) const VercelIntegration = () => { // @ts-ignore const _store: IVercelIntegrationStore = useLocalObservable(() => new VercelIntegrationStore()) const { data: projects } = useProjectsQuery() const isSupabaseProjectListEmpty = !projects || projects?.length === 0 useEffect(() => { _store.loadInitialData() }, []) return ( {_store.loading ? ( ) : _store.isDeployButtonFlow ? ( ) : isSupabaseProjectListEmpty ? ( ) : ( )} ) } export default withAuth(observer(VercelIntegration)) const Connecting = () => (

Connecting...

) const ProjectLinksEmptyState = () => (

You haven't created a Supabase project yet. Get started by creating a new Supabase project, then close this window and retry adding integration.

Start a new Supabase project
) const UNDEFINED_SELECT_VALUE = 'undefined' const IntegrationProject = observer(() => { const _store = useContext(PageContext) const router = useRouter() const [name, setName] = useState('') const [loading, setLoading] = useState(false) const [errorMsg, setErrorMsg] = useState() useEffect(() => { const config = VERCEL_INTEGRATION_CONFIGS.find((x) => x.id == _store.externalId) if (!config && !errorMsg) setErrorMsg('This integration is not supported right now') else setName(config?.name || _store.externalId) }, [_store.externalId]) function onClick() { if (!_store.selectedVercelProjectId) { setErrorMsg('Error: please select a vercel project to continue') return } if (!_store.token || _store.token == '') { setErrorMsg('Error: vercel auth token is invalid') return } setLoading(true) const query = new URLSearchParams(_store.queryParams).toString() router.push(`/vercel/setupOrg?${query}`) } function onVercelProjectChange(e: ChangeEvent) { setErrorMsg(undefined) _store.selectedVercelProjectId = e.target.value != UNDEFINED_SELECT_VALUE ? e.target.value : undefined } return (

{name}

{errorMsg &&

{errorMsg}

}
) }) const delayTimer = (ms: number) => new Promise((res) => setTimeout(res, ms)) const defaultVercelEnvs = [ { key: 'NEXT_PUBLIC_SUPABASE_URL', alias: INTEGRATION_ENVS_ALIAS.ENDPOINT, type: 'encrypted', }, { key: 'NEXT_PUBLIC_SUPABASE_ANON_KEY', alias: INTEGRATION_ENVS_ALIAS.ANONKEY, type: 'encrypted', }, { key: 'SUPABASE_SERVICE_ROLE_KEY', alias: INTEGRATION_ENVS_ALIAS.SERVICEKEY, type: 'encrypted', }, ] const ProjectLinks = observer(() => { const _store = useContext(PageContext) async function onSubmit() { if (validateInputs()) { runInAction(() => { _store.waitingIntegration = true }) for (let i = 0; i < _store.projectLinks.length; i++) { const item = _store.projectLinks[i] runInAction(() => { item.result = { status: 'waiting' } }) // pull vercel project env, check if supabase env is existed or not const { data: existedEnvs, error: fetchEnvsError }: any = await fetchVercelProjectEnvs({ id: item.vercelProjectId as string, vercelTeamId: _store.teamId, vercelToken: _store.token, }) if (fetchEnvsError) { console.error('envsError: ', fetchEnvsError) runInAction(() => { item.result = { status: 'fail', message: 'Error: Failed to validate Vercel project envs', } }) continue } const found = existedEnvs.find((x: any) => x.key.includes('SUPABASE')) if (!!found) { console.error('Existed Supabase env: ', found) runInAction(() => { item.result = { status: 'fail', message: 'Error: This Vercel project already contains Supabase envs', } }) continue } // If not, pull project detail info if (item.supabaseProjectRef === undefined) { console.error('Project ref is missing') runInAction(() => { item.result = { status: 'fail', message: 'Error: Failed to retrieve project ref', } }) continue } const { data: settings, error } = await getV2('/platform/projects/{ref}/settings', { params: { path: { ref: item.supabaseProjectRef } }, }) if (error) { console.error('project info error: ', error) runInAction(() => { item.result = { status: 'fail', message: 'Error: Failed to fetch Supabase project details', } }) continue } // Then create env for vercel project with supabase project const endpoint = `https://${settings.app_config?.endpoint ?? '-'}` const { anonKey, serviceKey } = getAPIKeys(settings) const vercelEnvs = prepareVercelEvns(defaultVercelEnvs, { endpoint, anon_key: anonKey?.api_key ?? '', service_key: serviceKey?.api_key ?? '', }) await Promise.allSettled( vercelEnvs.map(async (env: any) => { try { const data = await createVercelEnv({ ...env, vercelProjectId: item.vercelProjectId, vercelTeamId: _store.teamId, vercelToken: _store.token, }) return data } catch (err) { console.error(`Error: ${err}`) } }) ) runInAction(() => { item.result = { status: 'success', message: 'Projects linked successfully', } }) await delayTimer(1000) } } } function validateInputs() { let isValid = true for (let i = 0; i < _store.projectLinks.length; i++) { const item = _store.projectLinks[i] if (!item.supabaseProjectRef || !item.vercelProjectId) { isValid = false runInAction(() => { _store.projectLinks[i].error = 'Invalid selection. Please choose a Vercel project and a Supabase project to link.' }) } } return isValid } function onFinish() { window.location.href = _store.next } function displayButton() { if (!_store.integrationComplete) { return ( ) } if (_store.projectLinks.every(({ result: { status } }: any) => status === 'success')) { onFinish() } else { return } } return (

Link Vercel to Supabase

Choose which of your Vercel projects to link to your existing Supabase projects.

Vercel Projects

Supabase Projects

{displayButton()}
) }) const ProjectLinkList = observer(() => { const _store = useContext(PageContext) function addProjectLink() { runInAction(() => { _store.projectLinks.push({ vercelProjectId: undefined, supabaseProjectRef: undefined, }) }) } return ( <>
    {_store.projectLinks.map((x, idx) => ( ))}
{_store.projectLinkRemaining == 0 ? (

All Vercel projects for selected scope have been added

) : (

{_store.projectLinkRemaining} project(s) remaining

)}
) }) interface ProjectLinkItemProps { idx: number vercelProjectId?: string supabaseProjectRef?: string error?: string result?: { status: 'waiting' | 'success' | 'fail'; message?: string } } const ProjectLinkItem = observer( ({ idx, vercelProjectId, supabaseProjectRef, error, result }: ProjectLinkItemProps) => { const _store = useContext(PageContext) const selectedVercelProject = _store.vercelProjects.find((x) => x.id == vercelProjectId) const { data: projects } = useProjectsQuery() function onVercelProjectChange(e: string) { const value = e != UNDEFINED_SELECT_VALUE ? e : undefined runInAction(() => { _store.projectLinks[idx].vercelProjectId = value _store.projectLinks[idx].error = undefined }) } function onSupabaseProjectChange(e: string) { const value = e != UNDEFINED_SELECT_VALUE ? e : undefined runInAction(() => { _store.projectLinks[idx].supabaseProjectRef = value _store.projectLinks[idx].error = undefined }) } function onRemove() { runInAction(() => { const filtered = _store.projectLinks.filter((_, index) => index != idx) _store.projectLinks = filtered }) } return (
  • Choose a project {selectedVercelProject && ( vercelIcon} > {selectedVercelProject.name} )} {_store.vercelProjectsAvailable.map((x) => ( vercelIcon} > {x.name} ))}
    Choose a project {projects?.map((x: Dictionary) => ( databaseIcon} > {x.name} ))}
    {idx != 0 && (
    )}
    {error &&

    {error}

    } {_store.waitingIntegration && result && (

    {result?.message ?? 'Processing...'}

    )}
  • ) } )