From b12198afc83191135ae2e1350355e4a1c5f1abe1 Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Tue, 18 Jul 2023 12:03:26 +0800 Subject: [PATCH 1/2] chore: vercel integration manual env sync --- .../interfaces/Integrations/ProjectLinker.tsx | 15 +- studio/data/api.d.ts | 311 ++++++++++++++++-- ...integration-connections-create-mutation.ts | 22 +- ...ns-vercel-connection-sync-envs-mutation.ts | 53 +++ .../integrations-vercel-projects-query.ts | 37 +-- .../vercel-integration-create-mutation.ts | 1 - .../[slug]/deploy-button/new-project.tsx | 9 +- .../[slug]/marketplace/choose-project.tsx | 2 +- 8 files changed, 395 insertions(+), 55 deletions(-) create mode 100644 studio/data/integrations/integrations-vercel-connection-sync-envs-mutation.ts diff --git a/studio/components/interfaces/Integrations/ProjectLinker.tsx b/studio/components/interfaces/Integrations/ProjectLinker.tsx index 0be4ac9d5d4..449b197d618 100644 --- a/studio/components/interfaces/Integrations/ProjectLinker.tsx +++ b/studio/components/interfaces/Integrations/ProjectLinker.tsx @@ -1,9 +1,11 @@ import { useState } from 'react' +import { toast } from 'react-hot-toast' import { ENV_VAR_RAW_KEYS } from 'components/interfaces/Integrations/Integrations-Vercel.constants' import { Markdown } from 'components/interfaces/Markdown' import { vercelIcon } from 'components/to-be-cleaned/ListIcons' import { useIntegrationConnectionsCreateMutation } from 'data/integrations/integration-connections-create-mutation' +import { useIntegrationsVercelConnectionSyncEnvsMutation } from 'data/integrations/integrations-vercel-connection-sync-envs-mutation' import { VercelProjectsResponse } from 'data/integrations/integrations-vercel-projects-query' import { IntegrationProjectConnection } from 'data/integrations/integrations.types' import { useSelectedOrganization } from 'hooks' @@ -40,12 +42,19 @@ const ProjectLinker = ({ const [supabaseProjectRef, setSupabaseProjectRef] = useState(UNDEFINED_SELECT_VALUE) const [vercelProjectId, setVercelProjectId] = useState(UNDEFINED_SELECT_VALUE) + const { mutateAsync: syncEnvs } = useIntegrationsVercelConnectionSyncEnvsMutation() const { mutate: createConnections, isLoading } = useIntegrationConnectionsCreateMutation({ - onSuccess() { + async onSuccess({ id }) { + try { + await syncEnvs({ connectionId: id }) + } catch (error: any) { + toast.error('Failed to sync environment variables: ', error.message) + } + + if (setLoading) setLoading(false) _onCreateConnections?.() }, - - onSettled() { + onError() { if (setLoading) setLoading(false) }, }) diff --git a/studio/data/api.d.ts b/studio/data/api.d.ts index 57c1537e7bf..b76fee25169 100644 --- a/studio/data/api.d.ts +++ b/studio/data/api.d.ts @@ -688,9 +688,13 @@ export interface paths { */ post: operations["VercelIntegrationController_createVercelIntegration"]; }; + "/platform/integrations/vercel/{organization_integration_id}": { + /** Removes Vercel organization integration with the given id */ + delete: operations["VercelIntegrationController_removeVercelIntegration"]; + }; "/platform/integrations/vercel/projects/{organization_integration_id}": { - /** Gets vercel projects for the given organization */ - get: operations["VercelProjectController_getProjects"]; + /** Gets vercel projects with the given organization integration id */ + get: operations["VercelProjectController_getVercelProjects"]; }; "/platform/integrations/vercel/connections/{organization_integration_id}": { /** Gets installed vercel project connections for the given organization integration */ @@ -700,6 +704,10 @@ export interface paths { /** Connects a Vercel project to a supabase project */ post: operations["VercelConnectionsController_createVercelConnection"]; }; + "/platform/integrations/vercel/connections/{connection_id}/sync-envs": { + /** Syncs supabase project envs with given connection id */ + post: operations["VercelConnectionsController_syncVercelConnectionEnvs"]; + }; "/platform/integrations/vercel/connections/{connection_id}": { /** Deletes vercel project connection */ delete: operations["VercelConnectionsController_deleteVercelConnection"]; @@ -776,6 +784,10 @@ export interface paths { /** Migrates org to org-level billing. */ post: operations["BillingMigrationController_migrateToOrgLevelBilling"]; }; + "/system/billing/migrate/org-level-billing-preview": { + /** Previews the migration of the organization to the new org level billing. */ + post: operations["BillingMigrationController_preview"]; + }; "/system/integrations/vercel/webhooks": { /** Processes Vercel event */ post: operations["VercelWebhooksController_processEvent"]; @@ -1235,6 +1247,23 @@ export interface paths { /** Deletes objects */ delete: operations["StorageObjectsController_deleteObjects"]; }; + "/v1/branch/{branch_id}": { + /** + * Get database branch config + * @description Fetches configurations of the specified database branch + */ + get: operations["BranchController_getBranchDetails"]; + /** + * Delete a database branch + * @description Deletes the specified database branch + */ + delete: operations["BranchController_deleteBranch"]; + /** + * Update database branch config + * @description Updates the configuration of the specified database branch + */ + patch: operations["BranchController_updateBranch"]; + }; "/v1/projects": { /** * List all projects @@ -1247,6 +1276,23 @@ export interface paths { "/v1/projects/{ref}/api-keys": { get: operations["ApiKeysController_getProjectApiKeys"]; }; + "/v1/projects/{ref}/branches": { + /** + * List all database branches + * @description Returns all database branches of the specified project. + */ + get: operations["BranchesController_getBranches"]; + /** + * Create a database branch + * @description Creates a database branch from the specified project. + */ + post: operations["BranchesController_createBranch"]; + /** + * Disables preview branching + * @description Disables preview branching for the specified project + */ + delete: operations["BranchesController_disableBranch"]; + }; "/v1/projects/{ref}/custom-hostname": { /** Gets project's custom hostname config */ get: operations["CustomHostnamesController_getCustomHostnameConfig"]; @@ -3026,6 +3072,15 @@ export interface components { postgrest: string; "supabase-postgres": string; }; + BranchResponse: { + id: string; + name: string; + project_ref: string; + is_default: boolean; + git_branch: string; + created_at: string; + updated_at: string; + }; ProjectDetailResponse: { cloud_provider: string; db_host: string; @@ -3046,6 +3101,7 @@ export interface components { volumeSizeGb?: number; maxDatabasePreprovisionGb?: number; lastDatabaseResizeAt?: string; + preview_branches: (components["schemas"]["BranchResponse"])[]; }; ProjectRefResponse: { id: number; @@ -3704,6 +3760,14 @@ export interface components { framework?: string | null; link?: components["schemas"]["VercelProjectLink"]; }; + GetVercelProjectsResponse: { + projects: (components["schemas"]["IntegrationVercelProject"])[]; + pagination: { + count?: number; + next?: number | null; + prev?: number | null; + }; + }; GetVercelConnections: { id: string; inserted_at: string; @@ -3716,13 +3780,18 @@ export interface components { IntegrationConnection: { foreign_project_id: string; supabase_project_ref: string; - integration_id: string; metadata: Record; }; CreateVercelConnectionsBody: { organization_integration_id: string; connection: components["schemas"]["IntegrationConnection"]; }; + CreateVercelConnectionResponse: { + id: string; + }; + DeleteVercelConnectionResponse: { + id: string; + }; FunctionResponse: { id: string; slug: string; @@ -3798,6 +3867,21 @@ export interface components { GetMetricsResponse: { metrics: (components["schemas"]["ProjectMetric"])[]; }; + BranchDetailResponse: { + db_port: number; + ref: string; + postgres_version: string; + /** @enum {string} */ + status: "ACTIVE_HEALTHY" | "ACTIVE_UNHEALTHY" | "COMING_UP" | "GOING_DOWN" | "INACTIVE" | "INIT_FAILED" | "REMOVED" | "RESTORING" | "UNKNOWN" | "UPGRADING" | "PAUSING"; + db_host: string; + db_user?: string; + db_pass?: string; + jwt_secret?: string; + }; + UpdateBranchBody: { + branch_name?: string; + git_branch?: string; + }; DatabaseResponse: { /** @description Database host */ host: string; @@ -3808,6 +3892,11 @@ export interface components { name: string; api_key: string; }; + CreateBranchBody: { + branch_name: string; + git_branch?: string; + region?: string; + }; UpdateCustomHostnameResponse: { /** @enum {string} */ status: "1_not_started" | "2_initiated" | "3_challenge_verified" | "4_origin_setup_completed" | "5_services_reconfigured"; @@ -4010,6 +4099,16 @@ export interface components { redirect_uris: (string)[]; icon?: string; }; + TokenDTO: { + /** @enum {string} */ + grant_type: "authorization_code" | "refresh_token"; + client_id: string; + client_secret: string; + code?: string; + code_verifier?: string; + redirect_uri?: string; + refresh_token?: string; + }; AuthorizationsApproveBody: { organization_id: string; }; @@ -8072,20 +8171,38 @@ export interface operations { 500: never; }; }; - /** Gets vercel projects for the given organization */ - VercelProjectController_getProjects: { + /** Removes Vercel organization integration with the given id */ + VercelIntegrationController_removeVercelIntegration: { parameters: { path: { organization_integration_id: string; }; }; + responses: { + 200: never; + /** @description Failed to remove Vercel organization integration with the given id */ + 500: never; + }; + }; + /** Gets vercel projects with the given organization integration id */ + VercelProjectController_getVercelProjects: { + parameters: { + query: { + search?: string; + from?: string; + limit: string; + }; + path: { + organization_integration_id: string; + }; + }; responses: { 200: { content: { - "application/json": (components["schemas"]["IntegrationVercelProject"])[]; + "application/json": components["schemas"]["GetVercelProjectsResponse"]; }; }; - /** @description Failed to get vercel projects for the given organization */ + /** @description Failed to get vercel projects with the given organization integration id */ 500: never; }; }; @@ -8114,11 +8231,28 @@ export interface operations { }; }; responses: { - 201: never; + 201: { + content: { + "application/json": components["schemas"]["CreateVercelConnectionResponse"]; + }; + }; /** @description Failed to create project connections */ 500: never; }; }; + /** Syncs supabase project envs with given connection id */ + VercelConnectionsController_syncVercelConnectionEnvs: { + parameters: { + path: { + connection_id: string; + }; + }; + responses: { + 201: never; + /** @description Failed to sync supabase project envs with given connection id */ + 500: never; + }; + }; /** Deletes vercel project connection */ VercelConnectionsController_deleteVercelConnection: { parameters: { @@ -8127,7 +8261,11 @@ export interface operations { }; }; responses: { - 200: never; + 200: { + content: { + "application/json": components["schemas"]["DeleteVercelConnectionResponse"]; + }; + }; /** @description Failed to delete vercel integration project connection */ 500: never; }; @@ -8462,6 +8600,19 @@ export interface operations { 500: never; }; }; + /** Previews the migration of the organization to the new org level billing. */ + BillingMigrationController_preview: { + requestBody: { + content: { + "application/json": components["schemas"]["MigrateToOrgLevelBillingBody"]; + }; + }; + responses: { + 201: never; + /** @description Failed to preview org billing organization */ + 500: never; + }; + }; /** Processes Vercel event */ VercelWebhooksController_processEvent: { parameters: { @@ -8517,6 +8668,70 @@ export interface operations { }; }; }; + /** + * Get database branch config + * @description Fetches configurations of the specified database branch + */ + BranchController_getBranchDetails: { + parameters: { + path: { + /** @description Branch ID */ + branch_id: string; + }; + }; + responses: { + 200: { + content: { + "application/json": components["schemas"]["BranchDetailResponse"]; + }; + }; + /** @description Failed to update database branch */ + 500: never; + }; + }; + /** + * Delete a database branch + * @description Deletes the specified database branch + */ + BranchController_deleteBranch: { + parameters: { + path: { + /** @description Branch ID */ + branch_id: string; + }; + }; + responses: { + 200: never; + /** @description Failed to delete database branch */ + 500: never; + }; + }; + /** + * Update database branch config + * @description Updates the configuration of the specified database branch + */ + BranchController_updateBranch: { + parameters: { + path: { + /** @description Branch ID */ + branch_id: string; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UpdateBranchBody"]; + }; + }; + responses: { + 200: { + content: { + "application/json": components["schemas"]["BranchResponse"]; + }; + }; + /** @description Failed to update database branch */ + 500: never; + }; + }; ApiKeysController_getProjectApiKeys: { parameters: { path: { @@ -8533,6 +8748,70 @@ export interface operations { 403: never; }; }; + /** + * List all database branches + * @description Returns all database branches of the specified project. + */ + BranchesController_getBranches: { + parameters: { + path: { + /** @description Project ref */ + ref: string; + }; + }; + responses: { + 200: { + content: { + "application/json": (components["schemas"]["BranchResponse"])[]; + }; + }; + /** @description Failed to retrieve database branches */ + 500: never; + }; + }; + /** + * Create a database branch + * @description Creates a database branch from the specified project. + */ + BranchesController_createBranch: { + parameters: { + path: { + /** @description Project ref */ + ref: string; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateBranchBody"]; + }; + }; + responses: { + 201: { + content: { + "application/json": components["schemas"]["BranchResponse"]; + }; + }; + /** @description Failed to create database branch */ + 500: never; + }; + }; + /** + * Disables preview branching + * @description Disables preview branching for the specified project + */ + BranchesController_disableBranch: { + parameters: { + path: { + /** @description Project ref */ + ref: string; + }; + }; + responses: { + 200: never; + /** @description Failed to disable preview branching */ + 500: never; + }; + }; /** Gets project's custom hostname config */ CustomHostnamesController_getCustomHostnameConfig: { parameters: { @@ -9467,7 +9746,7 @@ export interface operations { state?: string; response_mode?: string; code_challenge?: string; - code_challenge_method?: "plain" | "sha256"; + code_challenge_method?: "plain" | "sha256" | "S256"; }; }; responses: { @@ -9476,15 +9755,9 @@ export interface operations { }; /** Exchange auth code for user's access and refresh token */ OAuthController_token: { - parameters: { - query: { - grant_type: "authorization_code" | "refresh_token"; - client_id: string; - client_secret: string; - code?: string; - code_verifier?: string; - redirect_uri?: string; - refresh_token?: string; + requestBody: { + content: { + "application/x-www-form-urlencoded": components["schemas"]["TokenDTO"]; }; }; responses: { diff --git a/studio/data/integrations/integration-connections-create-mutation.ts b/studio/data/integrations/integration-connections-create-mutation.ts index 9431e94aafb..b72f4bb064e 100644 --- a/studio/data/integrations/integration-connections-create-mutation.ts +++ b/studio/data/integrations/integration-connections-create-mutation.ts @@ -1,6 +1,6 @@ import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' -import { post } from 'lib/common/fetch' -import { API_URL } from 'lib/constants' + +import { post } from 'data/fetchers' import { integrationKeys } from './keys' export type IntegrationConnectionsCreateVariables = { @@ -17,15 +17,21 @@ export async function createIntegrationConnections({ organizationIntegrationId, connection, }: IntegrationConnectionsCreateVariables) { - const response = await post(`${API_URL}/integrations/vercel/connections`, { - organization_integration_id: organizationIntegrationId, - connection, + const { data, error } = await post('/platform/integrations/vercel/connections', { + body: { + organization_integration_id: organizationIntegrationId, + connection: { + foreign_project_id: connection.foreign_project_id, + supabase_project_ref: connection.supabase_project_ref, + metadata: connection.metadata, + }, + }, }) - if (response.error) { - throw response.error + if (error) { + throw error } - return response + return data } type IntegrationConnectionsCreateData = Awaited> diff --git a/studio/data/integrations/integrations-vercel-connection-sync-envs-mutation.ts b/studio/data/integrations/integrations-vercel-connection-sync-envs-mutation.ts new file mode 100644 index 00000000000..357bc3b6438 --- /dev/null +++ b/studio/data/integrations/integrations-vercel-connection-sync-envs-mutation.ts @@ -0,0 +1,53 @@ +import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' +import { post } from 'data/fetchers' + +export type IntegrationsVercelConnectionSyncEnvsVariables = { + connectionId: string +} + +export async function syncEnvsIntegrationsVercelConnection({ + connectionId, +}: IntegrationsVercelConnectionSyncEnvsVariables) { + const { data, error } = await post( + '/platform/integrations/vercel/connections/{connection_id}/sync-envs', + { + params: { + path: { connection_id: connectionId }, + }, + } + ) + if (error) { + throw error + } + + return data +} + +type IntegrationsVercelConnectionSyncEnvsData = Awaited< + ReturnType +> + +export const useIntegrationsVercelConnectionSyncEnvsMutation = ({ + onSuccess, + ...options +}: Omit< + UseMutationOptions< + IntegrationsVercelConnectionSyncEnvsData, + unknown, + IntegrationsVercelConnectionSyncEnvsVariables + >, + 'mutationFn' +> = {}) => { + const queryClient = useQueryClient() + + return useMutation< + IntegrationsVercelConnectionSyncEnvsData, + unknown, + IntegrationsVercelConnectionSyncEnvsVariables + >((vars) => syncEnvsIntegrationsVercelConnection(vars), { + async onSuccess(data, variables, context) { + await onSuccess?.(data, variables, context) + }, + ...options, + }) +} diff --git a/studio/data/integrations/integrations-vercel-projects-query.ts b/studio/data/integrations/integrations-vercel-projects-query.ts index 5c6556c283f..e3c57f93006 100644 --- a/studio/data/integrations/integrations-vercel-projects-query.ts +++ b/studio/data/integrations/integrations-vercel-projects-query.ts @@ -1,26 +1,13 @@ import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query' -import { get } from 'lib/common/fetch' -import { API_URL } from 'lib/constants' +import { get } from 'data/fetchers' import { useCallback } from 'react' -import { integrationKeys } from './keys' import { VercelFramework } from './integrations.types' +import { integrationKeys } from './keys' export type VercelProjectsVariables = { organization_integration_id: string | undefined } -export type VercelProjectsResponse = { - id: string - name: string - framework: VercelFramework - metadata: { - id: string - name: string - framework: string - link: string - } -} - export async function getVercelProjects( { organization_integration_id }: VercelProjectsVariables, signal?: AbortSignal @@ -29,20 +16,30 @@ export async function getVercelProjects( throw new Error('organization_integration_id is required') } - const response = await get( - `${API_URL}/integrations/vercel/projects/${organization_integration_id}`, + const { data, error } = await get( + '/platform/integrations/vercel/projects/{organization_integration_id}', { + params: { + path: { organization_integration_id }, + query: { + // [Alaister]: setting a large limit here to avoid pagination + // until we have merged the new shadcn listbox which will support it + limit: '1000', + }, + }, signal, } ) - if (response.error) { - throw response.error + + if (error) { + throw error } - return response as VercelProjectsResponse[] + return data.projects } export type VercelProjectsData = Awaited> +export type VercelProjectsResponse = VercelProjectsData[0] export type VercelProjectsError = unknown export const useVercelProjectsQuery = ( diff --git a/studio/data/integrations/vercel-integration-create-mutation.ts b/studio/data/integrations/vercel-integration-create-mutation.ts index ae99a6e5785..53f1666e05d 100644 --- a/studio/data/integrations/vercel-integration-create-mutation.ts +++ b/studio/data/integrations/vercel-integration-create-mutation.ts @@ -47,7 +47,6 @@ export const useVercelIntegrationCreateMutation = ({ 'mutationFn' > = {}) => { const queryClient = useQueryClient() - console.log('mutating') return useMutation( (vars) => createVercelIntegration(vars), { diff --git a/studio/pages/integrations/vercel/[slug]/deploy-button/new-project.tsx b/studio/pages/integrations/vercel/[slug]/deploy-button/new-project.tsx index 7736f118c58..80bd6c8d3ef 100644 --- a/studio/pages/integrations/vercel/[slug]/deploy-button/new-project.tsx +++ b/studio/pages/integrations/vercel/[slug]/deploy-button/new-project.tsx @@ -10,6 +10,7 @@ import PasswordStrengthBar from 'components/ui/PasswordStrengthBar' import { useProjectApiQuery } from 'data/config/project-api-query' import { useIntegrationConnectionsCreateMutation } from 'data/integrations/integration-connections-create-mutation' import { useIntegrationsQuery } from 'data/integrations/integrations-query' +import { useIntegrationsVercelConnectionSyncEnvsMutation } from 'data/integrations/integrations-vercel-connection-sync-envs-mutation' import { useVercelProjectsQuery } from 'data/integrations/integrations-vercel-projects-query' import { Integration } from 'data/integrations/integrations.types' import { useOrganizationsQuery } from 'data/organizations/organizations-query' @@ -93,7 +94,8 @@ const CreateProject = ({ } = useParams() const { mutateAsync: createConnections, isLoading: isLoadingCreateConnections } = - useIntegrationConnectionsCreateMutation({}) + useIntegrationConnectionsCreateMutation() + const { mutateAsync: syncEnvs } = useIntegrationsVercelConnectionSyncEnvsMutation() const { data: organizationData, isLoading: isLoadingOrganizationsQuery } = useOrganizationsQuery() @@ -256,9 +258,8 @@ const CreateProject = ({ const projectDetails = vercelProjects?.find((x) => x.id === foreignProjectId) - // Wrap the createConnections function call in a try-catch block try { - await createConnections({ + const { id: connectionId } = await createConnections({ organizationIntegrationId: organizationIntegration?.id, connection: { foreign_project_id: foreignProjectId, @@ -274,6 +275,8 @@ const CreateProject = ({ }, orgSlug: selectedOrganization?.slug, }) + + await syncEnvs({ connectionId }) } catch (error) { console.error('An error occurred during createConnections:', error) return diff --git a/studio/pages/integrations/vercel/[slug]/marketplace/choose-project.tsx b/studio/pages/integrations/vercel/[slug]/marketplace/choose-project.tsx index 75a9ae1bece..5a44b916f90 100644 --- a/studio/pages/integrations/vercel/[slug]/marketplace/choose-project.tsx +++ b/studio/pages/integrations/vercel/[slug]/marketplace/choose-project.tsx @@ -70,7 +70,7 @@ const VercelIntegration: NextPageWithLayout = () => { From 320f59bf80d65ff532e5acd5bff7a4fc580db661 Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Tue, 18 Jul 2023 13:40:09 +0800 Subject: [PATCH 2/2] add skip button on project connections --- .../interfaces/Integrations/ProjectLinker.tsx | 15 ++++++++++++++- .../vercel/[slug]/marketplace/choose-project.tsx | 1 + 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/studio/components/interfaces/Integrations/ProjectLinker.tsx b/studio/components/interfaces/Integrations/ProjectLinker.tsx index 449b197d618..41cf88e40ed 100644 --- a/studio/components/interfaces/Integrations/ProjectLinker.tsx +++ b/studio/components/interfaces/Integrations/ProjectLinker.tsx @@ -25,6 +25,7 @@ export interface ProjectLinkerProps { onCreateConnections?: () => void installedConnections: IntegrationProjectConnection[] | undefined setLoading?: (x: boolean) => void + showSkip?: boolean } const UNDEFINED_SELECT_VALUE = 'undefined' @@ -36,6 +37,7 @@ const ProjectLinker = ({ onCreateConnections: _onCreateConnections, installedConnections = [], setLoading, + showSkip = false, }: ProjectLinkerProps) => { const selectedOrganization = useSelectedOrganization() @@ -207,7 +209,18 @@ const ProjectLinker = ({ -
+
+ {showSkip && ( + + )}