deploy button flow started

This commit is contained in:
Jonathan Summers-Muir committed 2023-06-23 16:07:48 +08:00
1 parent 6c31d28f05
commit beb5d10b61
21 files changed
+1003 -278

No files matched your search

+32 -1
View File
@@ -1,15 +1,46 @@
import { useRouter } from 'next/router'
import { useMemo } from 'react'
/**
* Helper to convert kebab case to camel case
*/
function convertToCamelCase(key: string): string {
if (!key.includes('-')) {
return key
}
const parts = key.split('-')
const capitalizedParts = parts.map((part, index) => {
if (index === 0) {
return part
}
return part.charAt(0).toUpperCase() + part.slice(1)
})
return capitalizedParts.join('')
}
export function useParams(): {
[k: string]: string | undefined
} {
const { query } = useRouter()
const modifiedQuery = {
...query,
}
// Convert kebab case keys to camel case
Object.keys(modifiedQuery).forEach((key) => {
const modifiedKey = convertToCamelCase(key)
if (modifiedKey !== key) {
modifiedQuery[modifiedKey] = modifiedQuery[key]
delete modifiedQuery[key]
}
})
return useMemo(
() =>
Object.fromEntries(
Object.entries(query).map(([key, value]) => {
Object.entries(modifiedQuery).map(([key, value]) => {
if (Array.isArray(value)) {
return [key, value[0]]
} else {
@@ -147,7 +147,6 @@ const PITRSelection = ({}) => {
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger asChild>
<Button
asChild
type="warning"
disabled={isSelectedOutOfRange || !selectedDate}
onClick={() => setShowConfirmation(true)}
@@ -1,5 +1,8 @@
import { Markdown } from 'components/interfaces/Markdown'
import { Integration, IntegrationProjectConnection } from 'data/integrations/integrations-query'
import {
Integration,
IntegrationProjectConnection,
} from 'data/integrations/integrations-query-org-only'
import dayjs from 'dayjs'
import { useStore } from 'hooks'
import { BASE_PATH } from 'lib/constants'
@@ -2,12 +2,12 @@ import { ENV_VAR_RAW_KEYS } from 'components/interfaces/Integrations/Integration
import { Markdown } from 'components/interfaces/Markdown'
import { vercelIcon } from 'components/to-be-cleaned/ListIcons'
import { useIntegrationConnectionsCreateMutation } from 'data/integrations/integration-connections-create-mutation'
import { Integration, IntegrationProjectConnection } from 'data/integrations/integrations-query'
import { IntegrationProjectConnection } from 'data/integrations/integrations.types'
import { VercelProjectsResponse } from 'data/integrations/integrations-vercel-projects-query'
import { useStore } from 'hooks'
import { BASE_PATH } from 'lib/constants'
import { useState } from 'react'
import { Button, IconArrowRight, IconPlus, IconX, Input, Listbox, Select, cn } from 'ui'
import { Button, Listbox, cn } from 'ui'
interface Project {
id: string
@@ -16,10 +16,10 @@ interface Project {
}
// to do: move this somewhere
const INTEGRATION_INTERNAL_ID = 1
// const INTEGRATION_INTERNAL_ID = 1
export interface ProjectLinkerProps {
organizationIntegrationId: string
organizationIntegrationId: string | undefined
foreignProjects: VercelProjectsResponse[]
supabaseProjects: Project[]
onCreateConnections?: () => void
@@ -27,7 +27,6 @@ export interface ProjectLinkerProps {
}
const UNDEFINED_SELECT_VALUE = 'undefined'
const UNDEFINED_METADATA_VALUE = {}
const ProjectLinker = ({
organizationIntegrationId,
@@ -41,26 +40,6 @@ const ProjectLinker = ({
const [supabaseProjectRef, setSupabaseProjectRef] = useState(UNDEFINED_SELECT_VALUE)
const [vercelProjectId, setVercelProjectId] = useState(UNDEFINED_SELECT_VALUE)
// function addConnection() {
// setConnections([
// ...connections,
// {
// foreign_project_id: UNDEFINED_SELECT_VALUE,
// supabase_project_id: UNDEFINED_SELECT_VALUE,
// integrationId: INTEGRATION_INTERNAL_ID,
// metadata: UNDEFINED_METADATA_VALUE,
// },
// ])
// }
// function removeConnection(idx: number) {
// const newConnections = [...connections]
// newConnections.splice(idx, 1)
// setConnections(newConnections)
// }
// console.log('installed connections', installedConnections)
const { mutate: createConnections, isLoading } = useIntegrationConnectionsCreateMutation({
onSuccess() {
_onCreateConnections?.()
@@ -70,12 +49,17 @@ const ProjectLinker = ({
function onCreateConnections() {
const projectDetails = foreignProjects.filter((x) => x.id === vercelProjectId)[0]
if (!organizationIntegrationId) {
console.error('No integration ID set')
return
}
createConnections({
organizationIntegrationId,
connection: {
foreign_project_id: vercelProjectId,
supabase_project_ref: supabaseProjectRef,
integration_id: INTEGRATION_INTERNAL_ID,
// integration_id: INTEGRATION_INTERNAL_ID,
metadata: {
...projectDetails,
supabaseConfig: {
@@ -85,7 +69,7 @@ const ProjectLinker = ({
},
},
},
orgId: ui.selectedOrganization?.id,
orgSlug: ui.selectedOrganization?.slug,
})
}
@@ -118,9 +102,9 @@ const ProjectLinker = ({
return (
<div className="flex flex-col gap-4">
<div className="relative border rounded- lg p-12 bg border-body">
<div className="relative border rounded-lg p-12 bg shadow-inner">
<div
className="absolute inset-0 bg-grid-white/5 [mask-image:linear-gradient(0deg,#fff,rgba(255,255,255,0.6))] dark:bg-grid-slate-700/25 dark:[mask-image:linear-gradient(0deg,rgba(255,255,255,0.1),rgba(255,255,255,0.5))]"
className="absolute inset-0 bg-grid-black/5 [mask-image:linear-gradient(0deg,#fff,rgba(255,255,255,0.6))] dark:bg-grid-white/5 dark:[mask-image:linear-gradient(0deg,rgba(255,255,255,0.1),rgba(255,255,255,0.5))]"
style={{ backgroundPosition: '10px 10px' }}
></div>
<div className="flex gap-0 w-full relative">
@@ -223,20 +207,6 @@ const ProjectLinker = ({
Connect project
</Button>
</div>
{/* <div>
<p className="text-scale-900 text-sm mb-3">
The following enviroment variables will be added:
</p>
<p>
{ENV_VAR_RAW_KEYS.map((x, idx) => {
return (
<span key={idx} className="text-scale-1100 text-xs font-mono">
{x},{' '}
</span>
)
})}
</p>
</div> */}
<Markdown
content={`
The following enviroment variables will be added:
@@ -11,7 +11,7 @@ import {
ScaffoldSectionContent,
ScaffoldSectionDetail,
} from 'components/layouts/Scaffold'
import { Integration as TIntegration } from 'data/integrations/integrations-query'
import { Integration as TIntegration } from 'data/integrations/integrations-query-org-only'
import { useIntegrationsVercelInstalledConnectionDeleteMutation } from 'data/integrations/integrations-vercel-installed-connection-delete-mutation'
import { useStore } from 'hooks'
import { BASE_PATH } from 'lib/constants'
@@ -11,7 +11,7 @@ import {
ScaffoldSectionContent,
ScaffoldSectionDetail,
} from 'components/layouts/Scaffold'
import { Integration as TIntegration } from 'data/integrations/integrations-query'
import { Integration as TIntegration } from 'data/integrations/integrations-query-org-only'
import { pluralize } from 'lib/helpers'
import { EMPTY_ARR } from 'lib/void'
import { useMemo } from 'react'
@@ -1,5 +1,5 @@
import { ScaffoldDivider } from 'components/layouts/Scaffold'
import { useIntegrationsQuery } from 'data/integrations/integrations-query'
import { useOrgIntegrationsQuery } from 'data/integrations/integrations-query-org-only'
import { useStore } from 'hooks'
import { observer } from 'mobx-react-lite'
import Integration from './Integration'
@@ -7,7 +7,7 @@ import SidePanelVercelProjectLinker from './SidePanelVercelProjectLinker'
const IntegrationSettings = () => {
const { ui } = useStore()
const { data } = useIntegrationsQuery({ orgSlug: ui.selectedOrganization?.slug })
const { data } = useOrgIntegrationsQuery({ orgSlug: ui.selectedOrganization?.slug })
const org = ui.selectedOrganization
@@ -1,6 +1,6 @@
import { useParams } from 'common'
import { Markdown } from 'components/interfaces/Markdown'
import { useIntegrationsQuery } from 'data/integrations/integrations-query'
import { useIntegrationsQuery } from 'data/integrations/integrations-query-org-only'
import { useStore } from 'hooks'
import { observer } from 'mobx-react-lite'
import { useGithubConnectionConfigPanelSnapshot } from 'state/github-connection-config-panel'
@@ -1,6 +1,7 @@
import { useParams } from 'common'
import ProjectLinker from 'components/interfaces/Integrations/ProjectLinker'
import { Markdown } from 'components/interfaces/Markdown'
import { useIntegrationsQuery } from 'data/integrations/integrations-query'
import { useOrgIntegrationsQuery } from 'data/integrations/integrations-query-org-only'
import { useVercelProjectConnectionsQuery } from 'data/integrations/integrations-vercel-installed-connections-query'
import { useVercelProjectsQuery } from 'data/integrations/integrations-vercel-projects-query'
import { useProjectsQuery } from 'data/projects/projects-query'
@@ -13,10 +14,11 @@ import { SidePanel } from 'ui'
const SidePanelVercelProjectLinker = () => {
const { ui } = useStore()
const selectedOrg = ui.selectedOrganization
const { data: integrationData } = useIntegrationsQuery({
orgId: ui.selectedOrganization?.id,
const { data: integrationData } = useOrgIntegrationsQuery({
orgSlug: ui.selectedOrganization?.slug,
})
const vercelIntegrations = integrationData?.filter(
(integration) => integration.integration.name === 'Vercel'
+67 -6
View File
@@ -1,19 +1,41 @@
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
import { useIntegrationsQuery } from 'data/integrations/integrations-query'
import { IntegrationName } from 'data/integrations/integrations.types'
import {
OrganizationsResponse,
useOrganizationsQuery,
} from 'data/organizations/organizations-query'
import { useStore } from 'hooks'
import { useState } from 'react'
import { Organization } from 'types'
import { Listbox } from 'ui'
import { Badge, IconHexagon, Listbox } from 'ui'
export interface OrganizationPickerProps {
label?: string
onSelectedOrgChange?: (org: Organization) => void
integrationName: IntegrationName
}
const OrganizationPicker = ({
label = 'Choose an organization',
onSelectedOrgChange,
integrationName,
}: OrganizationPickerProps) => {
const { ui } = useStore()
const [selectedOrg, setSelectedOrg] = useState<Organization | null>(null)
/**
* array of integrations installed on all
*/
const { data: integrationData, isLoading: integrationDataLoading } = useIntegrationsQuery({})
console.log('integrationData', integrationData)
/**
* filter integrations to match integrationName
*/
const vercelIntegrationsInstalled = integrationData?.filter(
(integration) => integration.integration.name === 'Vercel'
)
const { data, isLoading } = useOrganizationsQuery({
onSuccess(organizations) {
const firstOrg = organizations?.[0]
@@ -33,7 +55,7 @@ const OrganizationPicker = ({
}
}
if (isLoading) {
if (isLoading || integrationDataLoading) {
return (
<Listbox label={label} value="loading" disabled>
<Listbox.Option key="loading" value="loading" label="Loading...">
@@ -43,12 +65,51 @@ const OrganizationPicker = ({
)
}
/**
* Organization type with `installationInstalled` added
*/
interface OrganizationsResponseWithInstalledData extends Organization {
installationInstalled?: boolean
}
/**
* A flat array of org slugs that have integration installed
*/
const flatInstalledConnectionsIds =
integrationData && integrationData.length > 0
? integrationData?.map((x) => x.organization.slug)
: []
/**
* Organizations with extra `installationInstalled` attribute
* Used to show label/badge and allow/disallow installing
*/
const organizationsWithInstalledData: OrganizationsResponseWithInstalledData[] = data
? data.map((org) => {
return {
...org,
installationInstalled: !flatInstalledConnectionsIds.includes(org.slug) ? true : false,
}
})
: []
return (
<Listbox label={label} value={selectedOrg?.slug} onChange={_onSelectedOrgChange}>
{data?.map((org) => {
return (
<Listbox.Option key={org.id} value={org.slug} label={org.name}>
{organizationsWithInstalledData?.map((org) => {
const label = (
<div className="flex gap-3 items-center">
{org.name}
{org.installationInstalled && <Badge color="scale">Integration Installed</Badge>}
</div>
)
return (
<Listbox.Option
key={org.id}
value={org.slug}
label={label}
addOnBefore={({ active, selected }: any) => <IconHexagon />}
>
{label}
</Listbox.Option>
)
})}
@@ -8,10 +8,9 @@ export type IntegrationConnectionsCreateVariables = {
connection: {
foreign_project_id: string
supabase_project_ref: string
integration_id: number
metadata: any
}
orgId: number | undefined
orgSlug: string | undefined
}
export async function createIntegrationConnections({
@@ -49,7 +48,13 @@ export const useIntegrationConnectionsCreateMutation = ({
IntegrationConnectionsCreateVariables
>((vars) => createIntegrationConnections(vars), {
async onSuccess(data, variables, context) {
await Promise.all([queryClient.invalidateQueries(integrationKeys.list(variables.orgId))])
await Promise.all([
queryClient.invalidateQueries(integrationKeys.integrationsList()),
queryClient.invalidateQueries(integrationKeys.integrationsListWithOrg(variables.orgSlug)),
queryClient.invalidateQueries(
integrationKeys.vercelProjectList(variables.organizationIntegrationId)
),
])
await onSuccess?.(data, variables, context)
},
...options,
@@ -0,0 +1,35 @@
import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query'
import { get } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { integrationKeys } from './keys'
import { IntegrationsVariables, Integration } from './integrations.types'
export type IntegrationsResponse = Integration[]
export async function getIntegrations({ orgSlug }: IntegrationsVariables, signal?: AbortSignal) {
if (!orgSlug) {
throw new Error('orgSlug is required')
}
const response = await get(`${API_URL}/integrations/${orgSlug}?expand=true`, {
signal,
})
if (response.error) {
throw response.error
}
return response as IntegrationsResponse
}
export type IntegrationsData = Awaited<ReturnType<typeof getIntegrations>>
export type ProjectIntegrationConnectionsData = Awaited<ReturnType<typeof getIntegrations>>
export type IntegrationsError = unknown
export const useOrgIntegrationsQuery = <TData = IntegrationsData>(
{ orgSlug }: IntegrationsVariables,
{ enabled = true, ...options }: UseQueryOptions<IntegrationsData, IntegrationsError, TData> = {}
) =>
useQuery<IntegrationsData, IntegrationsError, TData>(
integrationKeys.integrationsListWithOrg(orgSlug),
({ signal }) => getIntegrations({ orgSlug }, signal),
{ enabled: enabled && typeof orgSlug !== 'undefined', ...options }
)
+8 -188
View File
@@ -2,190 +2,12 @@ import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query
import { get } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { integrationKeys } from './keys'
export type VercelFramework =
| (
| 'blitzjs'
| 'nextjs'
| 'gatsby'
| 'remix'
| 'astro'
| 'hexo'
| 'eleventy'
| 'docusaurus-2'
| 'docusaurus'
| 'preact'
| 'solidstart'
| 'dojo'
| 'ember'
| 'vue'
| 'scully'
| 'ionic-angular'
| 'angular'
| 'polymer'
| 'svelte'
| 'sveltekit'
| 'sveltekit-1'
| 'ionic-react'
| 'create-react-app'
| 'gridsome'
| 'umijs'
| 'sapper'
| 'saber'
| 'stencil'
| 'nuxtjs'
| 'redwoodjs'
| 'hugo'
| 'jekyll'
| 'brunch'
| 'middleman'
| 'zola'
| 'hydrogen'
| 'vite'
| 'vitepress'
| 'vuepress'
| 'parcel'
| 'sanity'
| 'storybook'
)
| null
export type VercelGitLink =
| {
/**
* GitHub link
*/
org?: string
repo?: string
repoId?: number
type?: 'github'
createdAt?: number
deployHooks: {
createdAt?: number
id: string
name: string
ref: string
url: string
}[]
gitCredentialId?: string
updatedAt?: number
sourceless?: boolean
productionBranch?: string
}
| {
/**
* GitLab link
*/
projectId?: string
projectName?: string
projectNameWithNamespace?: string
projectNamespace?: string
projectUrl?: string
type?: 'gitlab'
createdAt?: number
deployHooks: {
createdAt?: number
id: string
name: string
ref: string
url: string
}[]
gitCredentialId?: string
updatedAt?: number
sourceless?: boolean
productionBranch?: string
}
| {
/**
* Bitbucket link
*/
name?: string
slug?: string
owner?: string
type?: 'bitbucket'
uuid?: string
workspaceUuid?: string
createdAt?: number
deployHooks: {
createdAt?: number
id: string
name: string
ref: string
url: string
}[]
gitCredentialId?: string
updatedAt?: number
sourceless?: boolean
productionBranch?: string
}
type Imetadata = {
metadata: {
id: string
supabaseConfig: {
projectEnvVars: {
write: boolean
}
}
link?: VercelGitLink
name: string
framework: VercelFramework
}
}
export type IntegrationProjectConnection = {
id: string
created_at: string
updated_at: string
added_by: addedBy
supabase_project_id: string
foreign_project_id: string
organization_integration_id: string
metadata: Imetadata
}
export type IntegrationsVariables = {
orgSlug?: string
}
export type IntegrationProjectConnectionPayload = {
foreignProjectId: string
supabaseProjectId: string
integrationId: string
metadata: Imetadata
}
type userDetails = {
username: string
id: string
primary_email: string
}
type addedBy = userDetails
type updatedBy = userDetails
export type Integration = {
id: string
integration: {
id: string
name: 'Vercel' | 'Netlify' | 'GitHub'
}
added_by: addedBy
created_at: string
updated_at: string
connections: IntegrationProjectConnection[] | []
metadata?: {
vercelTeam?: string
gitHubConnectionOwner?: string
}
}
import { IntegrationsVariables, Integration } from './integrations.types'
export type IntegrationsResponse = Integration[]
export async function getIntegrations({ orgSlug }: IntegrationsVariables, signal?: AbortSignal) {
if (!orgSlug) {
throw new Error('orgSlug is required')
}
const response = await get(`${API_URL}/integrations/${orgSlug}`, {
export async function getIntegrations({}: {}, signal?: AbortSignal) {
const response = await get(`${API_URL}/integrations`, {
signal,
})
if (response.error) {
@@ -196,17 +18,15 @@ export async function getIntegrations({ orgSlug }: IntegrationsVariables, signal
}
export type IntegrationsData = Awaited<ReturnType<typeof getIntegrations>>
export type ProjectIntegrationConnectionsData = Awaited<
ReturnType<typeof getProjectIntegrationConnections>
>
export type ProjectIntegrationConnectionsData = Awaited<ReturnType<typeof getIntegrations>>
export type IntegrationsError = unknown
export const useIntegrationsQuery = <TData = IntegrationsData>(
{ orgSlug }: IntegrationsVariables,
{}: {},
{ enabled = true, ...options }: UseQueryOptions<IntegrationsData, IntegrationsError, TData> = {}
) =>
useQuery<IntegrationsData, IntegrationsError, TData>(
integrationKeys.list(orgSlug),
({ signal }) => getIntegrations({ orgSlug }, signal),
{ enabled: enabled && typeof orgSlug !== 'undefined', ...options }
integrationKeys.integrationsList(),
({ signal }) => getIntegrations({}, signal),
{ enabled: enabled, ...options }
)
@@ -3,12 +3,10 @@ import { get } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { useCallback } from 'react'
import { integrationKeys } from './keys'
import { VercelFramework } from './integrations-query'
import { VercelFramework } from './integrations.types'
export type VercelProjectsVariables = {
orgId?: number
orgSlug?: string
organization_integration_id?: string
organization_integration_id: string | undefined
}
export type VercelProjectsResponse = {
@@ -24,11 +22,11 @@ export type VercelProjectsResponse = {
}
export async function getVercelProjects(
{ orgId, organization_integration_id }: VercelProjectsVariables,
{ organization_integration_id }: VercelProjectsVariables,
signal?: AbortSignal
) {
if (!organization_integration_id) {
throw new Error('orgId is required')
throw new Error('organization_integration_id is required')
}
const response = await get(
@@ -48,17 +46,17 @@ export type VercelProjectsData = Awaited<ReturnType<typeof getVercelProjects>>
export type VercelProjectsError = unknown
export const useVercelProjectsQuery = <TData = VercelProjectsData>(
{ orgSlug, orgId, organization_integration_id }: VercelProjectsVariables,
{ organization_integration_id }: VercelProjectsVariables,
{
enabled = true,
...options
}: UseQueryOptions<VercelProjectsData, VercelProjectsError, TData> = {}
) =>
useQuery<VercelProjectsData, VercelProjectsError, TData>(
integrationKeys.vercelProjectList(orgSlug),
({ signal }) => getVercelProjects({ orgSlug, orgId, organization_integration_id }, signal),
integrationKeys.vercelProjectList(organization_integration_id),
({ signal }) => getVercelProjects({ organization_integration_id }, signal),
{
enabled: enabled && typeof orgSlug !== 'undefined' && typeof orgId !== 'undefined',
enabled: enabled && typeof organization_integration_id !== 'undefined',
...options,
}
)
@@ -76,17 +74,18 @@ export const useVercelProjectsQuery = <TData = VercelProjectsData>(
* )
*/
export const useVercelProjectsPrefetch = ({
orgSlug,
orgId,
// orgSlug,
// orgId,
organization_integration_id,
}: VercelProjectsVariables) => {
const client = useQueryClient()
return useCallback(() => {
if (orgSlug) {
client.prefetchQuery(integrationKeys.vercelProjectList(orgSlug), ({ signal }) =>
getVercelProjects({ orgSlug, orgId, organization_integration_id }, signal)
if (organization_integration_id) {
client.prefetchQuery(
integrationKeys.vercelProjectList(organization_integration_id),
({ signal }) => getVercelProjects({ organization_integration_id }, signal)
)
}
}, [orgSlug])
}, [organization_integration_id])
}
@@ -0,0 +1,178 @@
export type VercelFramework =
| (
| 'blitzjs'
| 'nextjs'
| 'gatsby'
| 'remix'
| 'astro'
| 'hexo'
| 'eleventy'
| 'docusaurus-2'
| 'docusaurus'
| 'preact'
| 'solidstart'
| 'dojo'
| 'ember'
| 'vue'
| 'scully'
| 'ionic-angular'
| 'angular'
| 'polymer'
| 'svelte'
| 'sveltekit'
| 'sveltekit-1'
| 'ionic-react'
| 'create-react-app'
| 'gridsome'
| 'umijs'
| 'sapper'
| 'saber'
| 'stencil'
| 'nuxtjs'
| 'redwoodjs'
| 'hugo'
| 'jekyll'
| 'brunch'
| 'middleman'
| 'zola'
| 'hydrogen'
| 'vite'
| 'vitepress'
| 'vuepress'
| 'parcel'
| 'sanity'
| 'storybook'
)
| null
export type VercelGitLink =
| {
/**
* GitHub link
*/
org?: string
repo?: string
repoId?: number
type?: 'github'
createdAt?: number
deployHooks: {
createdAt?: number
id: string
name: string
ref: string
url: string
}[]
gitCredentialId?: string
updatedAt?: number
sourceless?: boolean
productionBranch?: string
}
| {
/**
* GitLab link
*/
projectId?: string
projectName?: string
projectNameWithNamespace?: string
projectNamespace?: string
projectUrl?: string
type?: 'gitlab'
createdAt?: number
deployHooks: {
createdAt?: number
id: string
name: string
ref: string
url: string
}[]
gitCredentialId?: string
updatedAt?: number
sourceless?: boolean
productionBranch?: string
}
| {
/**
* Bitbucket link
*/
name?: string
slug?: string
owner?: string
type?: 'bitbucket'
uuid?: string
workspaceUuid?: string
createdAt?: number
deployHooks: {
createdAt?: number
id: string
name: string
ref: string
url: string
}[]
gitCredentialId?: string
updatedAt?: number
sourceless?: boolean
productionBranch?: string
}
export type Imetadata = {
metadata: {
id: string
supabaseConfig: {
projectEnvVars: {
write: boolean
}
}
link?: VercelGitLink
name: string
framework: VercelFramework
}
}
export type IntegrationProjectConnection = {
id: string
created_at: string
updated_at: string
added_by: addedBy
supabase_project_id: string
foreign_project_id: string
organization_integration_id: string
metadata: Imetadata
}
export type IntegrationsVariables = {
orgSlug?: string
}
export type IntegrationProjectConnectionPayload = {
foreignProjectId: string
supabaseProjectId: string
integrationId: string
metadata: Imetadata
}
export type userDetails = {
username: string
id: string
primary_email: string
}
type addedBy = userDetails
type updatedBy = userDetails
export type IntegrationName = 'Vercel' | 'Netlify' | 'GitHub'
export type Integration = {
id: string
integration: {
id: string
name: 'Vercel' | 'Netlify' | 'GitHub'
}
added_by: addedBy
created_at: string
updated_at: string
connections: IntegrationProjectConnection[] | []
organization: {
slug: string
}
metadata?: {
vercelTeam?: string
gitHubConnectionOwner?: string
}
}
+3 -1
View File
@@ -1,5 +1,7 @@
export const integrationKeys = {
list: (orgSlug: string | undefined) => ['organizations', orgSlug, 'integrations'] as const,
integrationsListWithOrg: (orgSlug: string | undefined) =>
['organizations', orgSlug, 'integrations'] as const,
integrationsList: () => ['organizations', 'integrations'] as const,
vercelProjectList: (organization_integration_id: string | undefined) =>
['organizations', organization_integration_id, 'vercel-projects'] as const,
vercelConnectionsList: (organization_integration_id: string | undefined) =>
@@ -1,6 +1,7 @@
import { useMutation, UseMutationOptions } from '@tanstack/react-query'
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
import { post } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { integrationKeys } from './keys'
export type VercelIntegrationCreateVariables = {
code: string
@@ -46,11 +47,18 @@ export const useVercelIntegrationCreateMutation = ({
UseMutationOptions<VercelIntegrationCreateData, unknown, VercelIntegrationCreateVariables>,
'mutationFn'
> = {}) => {
const queryClient = useQueryClient()
console.log('mutating')
return useMutation<VercelIntegrationCreateData, unknown, VercelIntegrationCreateVariables>(
(vars) => createVercelIntegration(vars),
{
async onSuccess(data, variables, context) {
console.log('data', data, variables)
await Promise.all([
queryClient.invalidateQueries(integrationKeys.integrationsList()),
queryClient.invalidateQueries(integrationKeys.integrationsListWithOrg(variables.orgSlug)),
queryClient.invalidateQueries(integrationKeys.vercelProjectList(data.id)),
])
await onSuccess?.(data, variables, context)
},
...options,
+24 -9
View File
@@ -1,25 +1,36 @@
import { useMemo, useState } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { useParams } from 'common'
import ProjectLinker from 'components/interfaces/Integrations/ProjectLinker'
import { Markdown } from 'components/interfaces/Markdown'
import IntegrationWindowLayout from 'components/layouts/IntegrationWindowLayout'
import { ScaffoldContainer, ScaffoldDivider } from 'components/layouts/Scaffold'
import OrganizationPicker from 'components/ui/OrganizationPicker'
import { useVercelIntegrationCreateMutation } from 'data/integrations/vercel-integration-create-mutation'
import { useIntegrationsQuery } from 'data/integrations/integrations-query'
import { useVercelProjectsQuery } from 'data/integrations/integrations-vercel-projects-query'
import { useVercelIntegrationCreateMutation } from 'data/integrations/vercel-integration-create-mutation'
import { useProjectsQuery } from 'data/projects/projects-query'
import { useStore } from 'hooks'
import { EMPTY_ARR } from 'lib/void'
import { NextPageWithLayout, Organization } from 'types'
import { Alert, Button, IconBook, IconLifeBuoy, Input, LoadingLine, cn } from 'ui'
import IntegrationWindowLayout from 'components/layouts/IntegrationWindowLayout'
import { ScaffoldContainer, ScaffoldDivider, ScaffoldSection } from 'components/layouts/Scaffold'
import { Markdown } from 'components/interfaces/Markdown'
import { BASE_PATH } from 'lib/constants'
import { ENV_VAR_RAW_KEYS } from 'components/interfaces/Integrations/Integrations-Vercel.constants'
import { Alert, Button, IconBook, IconLifeBuoy, LoadingLine } from 'ui'
import { useRouter } from 'next/router'
const VercelIntegration: NextPageWithLayout = () => {
const router = useRouter()
useEffect(() => {
router.push({ pathname: '/integrations/vercel/install', query: router.query })
}, [router])
/**
*
* remove everything here
*/
const { ui } = useStore()
const { code, configurationId, next, teamId, source } = useParams()
const { code, configurationId, next, teamId, source, externalId } = useParams()
const params = useParams()
const [selectedOrg, setSelectedOrg] = useState<Organization | null>(null)
const [organizationIntegrationId, setOrganizationIntegrationId] = useState<string | null>(null)
@@ -29,6 +40,9 @@ const VercelIntegration: NextPageWithLayout = () => {
},
})
console.log('params', params)
console.log('externalId', externalId)
function onInstall() {
const orgSlug = selectedOrg?.slug
if (!orgSlug) {
@@ -65,6 +79,7 @@ const VercelIntegration: NextPageWithLayout = () => {
const { data: supabaseProjectsData } = useProjectsQuery({
enabled: organizationIntegrationId !== null,
})
const supabaseProjects = useMemo(
() =>
supabaseProjectsData
@@ -0,0 +1,334 @@
import { useParams } from 'common'
import { Markdown } from 'components/interfaces/Markdown'
import IntegrationWindowLayout from 'components/layouts/IntegrationWindowLayout'
import { ScaffoldContainer, ScaffoldDivider } from 'components/layouts/Scaffold'
import PasswordStrengthBar from 'components/ui/PasswordStrengthBar'
import { useIntegrationConnectionsCreateMutation } from 'data/integrations/integration-connections-create-mutation'
import { useIntegrationsQuery } from 'data/integrations/integrations-query'
import { useVercelProjectsQuery } from 'data/integrations/integrations-vercel-projects-query'
import { Integration } from 'data/integrations/integrations.types'
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
import generator from 'generate-password'
import { useStore } from 'hooks'
import { post } from 'lib/common/fetch'
import {
API_URL,
AWS_REGIONS,
DEFAULT_MINIMUM_PASSWORD_STRENGTH,
PRICING_TIER_PRODUCT_IDS,
PROVIDERS,
} from 'lib/constants'
import { passwordStrength } from 'lib/helpers'
import { debounce } from 'lodash'
import { useRouter } from 'next/router'
import { ChangeEvent, useRef, useState } from 'react'
import { NextPageWithLayout } from 'types'
import { Alert, Button, IconBook, IconLifeBuoy, Input, Listbox, LoadingLine } from 'ui'
const VercelIntegration: NextPageWithLayout = () => {
const [loading, setLoading] = useState<boolean>(false)
return (
<>
<main className="overflow-auto flex flex-col h-full">
<LoadingLine loading={loading} />
<>
<ScaffoldContainer className="max-w-md flex flex-col gap-6 grow py-8">
<h1 className="text-xl text-scale-1200">New project</h1>
<>
<Markdown content={`Choose the Supabase Organization you wish to install to`} />
<CreateProject setLoading={(e: boolean) => setLoading(e)} />
</>
</ScaffoldContainer>
<ScaffoldContainer className="flex flex-col gap-6 py-3">
<Alert withIcon variant="info" title="You can uninstall this Integration at any time.">
<Markdown
content={`You can remove this integration at any time either via Vercel or the Supabase dashboard.`}
/>
</Alert>
</ScaffoldContainer>
</>
<ScaffoldDivider />
</main>
<ScaffoldContainer className="bg-body flex flex-row gap-6 py-6 border-t">
<div className="flex items-center gap-2 text-xs text-scale-900">
<IconBook size={16} /> Docs
</div>
<div className="flex items-center gap-2 text-xs text-scale-900">
<IconLifeBuoy size={16} /> Support
</div>
</ScaffoldContainer>
</>
)
}
VercelIntegration.getLayout = (page) => <IntegrationWindowLayout>{page}</IntegrationWindowLayout>
const CreateProject = ({ setLoading }: { setLoading: (e: boolean) => void }) => {
const router = useRouter()
const { ui } = useStore()
const [projectName, setProjectName] = useState('')
const [dbPass, setDbPass] = useState('')
const [passwordStrengthMessage, setPasswordStrengthMessage] = useState('')
const [passwordStrengthScore, setPasswordStrengthScore] = useState(-1)
const [dbRegion, setDbRegion] = useState(PROVIDERS.AWS.default_region)
// const [loading, setLoading] = useState(false)
const delayedCheckPasswordStrength = useRef(
debounce((value: string) => checkPasswordStrength(value), 300)
).current
const { configurationId, next, currentProjectId: foreignProjectId } = useParams()
const { mutateAsync: createConnections, isLoading: isLoadingCreateConnections } =
useIntegrationConnectionsCreateMutation({})
const { data: organizationData, isLoading: isLoadingOrganizationsQuery } = useOrganizationsQuery()
const { slug } = router.query
console.log('slug')
const organization = organizationData?.find((x) => x.slug === slug)
/**
* array of integrations installed
*/
const { data: integrationData, isLoading: integrationDataLoading } = useIntegrationsQuery({})
/**
* the vercel integration installed for organization chosen
*/
const organizationIntegration: Integration | undefined = integrationData?.find(
(x) => x.organization.slug === slug
)
/**
* Vercel projects available for this integration
*/
const { data: vercelProjects } = useVercelProjectsQuery(
{
organization_integration_id: organizationIntegration?.id,
},
{ enabled: organizationIntegration !== undefined }
)
console.log('vercelProjects near top', vercelProjects)
const canSubmit =
projectName != '' &&
passwordStrengthScore >= DEFAULT_MINIMUM_PASSWORD_STRENGTH &&
dbRegion != undefined
function onProjectNameChange(e: ChangeEvent<HTMLInputElement>) {
e.target.value = e.target.value.replace(/\./g, '')
setProjectName(e.target.value)
}
function onDbPassChange(e: ChangeEvent<HTMLInputElement>) {
const value = e.target.value
setDbPass(value)
if (value == '') {
setPasswordStrengthScore(-1)
setPasswordStrengthMessage('')
} else delayedCheckPasswordStrength(value)
}
async function checkPasswordStrength(value: string) {
const { message, strength } = await passwordStrength(value)
setPasswordStrengthScore(strength)
setPasswordStrengthMessage(message)
}
function generateStrongPassword() {
const password = generator.generate({
length: 16,
numbers: true,
uppercase: true,
})
setDbPass(password)
delayedCheckPasswordStrength(password)
}
async function createSupabaseProject(dbSql?: string) {
const data = {
cloud_provider: PROVIDERS.AWS.id, // hardcoded for DB instances to be under AWS
org_id: organization?.id,
name: projectName,
db_pass: dbPass,
db_region: dbRegion,
// db_sql: dbSql || '',
db_pricing_tier_id: PRICING_TIER_PRODUCT_IDS.FREE,
// auth_site_url: _store.selectedVercelProjectUrl,
vercel_configuration_id: configurationId,
}
const project = await post(`${API_URL}/projects`, data)
return { ...project, db_host: `db.${project.ref}.supabase.co`, db_password: dbPass }
}
async function onCreateProject() {
setLoading(true)
if (!organizationIntegration) {
console.error('No organizationIntegration set')
return
}
if (!foreignProjectId) {
console.error('No foreignProjectId ID set')
return
}
if (!configurationId) {
console.error('No configurationId ID set')
return
}
try {
const response = await createSupabaseProject()
// dbSql
if (response.error) {
setLoading(false)
ui.setNotification({
category: 'error',
message: `Failed to create project: ${response.error.message}`,
})
return
}
const project = response
console.log('vercelProjects before projectDetails', vercelProjects)
const projectDetails = vercelProjects?.find((x) => x.id === foreignProjectId)
console.log('projectDetails', projectDetails)
console.log(project)
console.log('supabase_project_ref', project.ref)
// Introduce a wait of 10 seconds
try {
await new Promise((resolve) => setTimeout(resolve, 10000))
} catch (error) {
console.error('An error occurred during the delay:', error)
}
// Wrap the createConnections function call in a try-catch block
try {
await createConnections({
organizationIntegrationId: organizationIntegration?.id,
connection: {
foreign_project_id: foreignProjectId,
supabase_project_ref: project.ref,
metadata: {
...projectDetails,
supabaseConfig: {
projectEnvVars: {
write: true,
},
},
},
},
orgSlug: ui.selectedOrganization?.slug,
})
} catch (error) {
console.error('An error occurred during createConnections:', error)
}
// const query = new URLSearchParams(_store.queryParams).toString()
// router.push(`/vercel/complete?${query}`)
if (next) {
window.location.href = next
}
} catch (error) {
console.error('Error', error)
setLoading(false)
}
}
return (
<div className="">
<p className="mb-2">Project details for integration</p>
<div className="py-2">
<Input
autoFocus
id="projectName"
label="Project name"
type="text"
placeholder=""
descriptionText=""
value={projectName}
onChange={onProjectNameChange}
/>
</div>
<div className="py-2">
<Input
id="dbPass"
label="Database Password"
type="password"
placeholder="Type in a strong password"
value={dbPass}
copy={dbPass.length > 0}
onChange={onDbPassChange}
descriptionText={
<PasswordStrengthBar
passwordStrengthScore={passwordStrengthScore}
password={dbPass}
passwordStrengthMessage={passwordStrengthMessage}
generateStrongPassword={generateStrongPassword}
/>
}
/>
</div>
<div className="py-2 pb-4">
<div className="mt-1">
<Listbox
label="Region"
type="select"
value={dbRegion}
onChange={(region) => setDbRegion(region)}
descriptionText="Select a region close to your users for the best performance."
>
{Object.keys(AWS_REGIONS).map((option: string, i) => {
const label = Object.values(AWS_REGIONS)[i]
return (
<Listbox.Option
key={option}
label={label}
value={label}
addOnBefore={({ active, selected }: any) => (
<img
alt="region icon"
className="w-5 rounded-sm"
src={`${router.basePath}/img/regions/${Object.keys(AWS_REGIONS)[i]}.svg`}
/>
)}
>
<span className="text-scale-1200">{label}</span>
</Listbox.Option>
)
})}
</Listbox>
</div>
</div>
<div className="flex flex-row w-full justify-end">
<Button
size="medium"
className="self-end"
// disabled={isLoading}
// loading={isLoading}
onClick={onCreateProject}
>
Create Project
</Button>
</div>
{/* <Button disabled={loading || !canSubmit} loading={loading} onClick={onCreateProject}>
Create project
</Button> */}
</div>
)
}
export default VercelIntegration
@@ -0,0 +1,248 @@
import { useParams } from 'common'
import { Markdown } from 'components/interfaces/Markdown'
import IntegrationWindowLayout from 'components/layouts/IntegrationWindowLayout'
import { ScaffoldContainer, ScaffoldDivider } from 'components/layouts/Scaffold'
import { useIntegrationsQuery } from 'data/integrations/integrations-query'
import { IntegrationName } from 'data/integrations/integrations.types'
import { useVercelIntegrationCreateMutation } from 'data/integrations/vercel-integration-create-mutation'
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
import { useStore } from 'hooks'
import { useRouter } from 'next/router'
import { useState } from 'react'
import { useVercelIntegrationInstallationState } from 'state/vercel-integration-installation'
import { NextPageWithLayout, Organization } from 'types'
import { Alert, Badge, Button, IconBook, IconHexagon, IconLifeBuoy, Listbox, LoadingLine } from 'ui'
/**
* Organization type with `installationInstalled` added
*/
interface OrganizationsResponseWithInstalledData extends Organization {
installationInstalled?: boolean
}
const VercelIntegration: NextPageWithLayout = () => {
const router = useRouter()
const { ui } = useStore()
const { code, configurationId, next, teamId, source, externalId } = useParams()
const [selectedOrg, setSelectedOrg] = useState<Organization | null>(null)
const [organizationIntegrationId, setOrganizationIntegrationId] = useState<string | null>(null)
const snapshot = useVercelIntegrationInstallationState()
/**
* array of integrations installed on all
*/
const { data: integrationData, isLoading: integrationDataLoading } = useIntegrationsQuery({})
const { data, isLoading: isLoadingOrganizationsQuery } = useOrganizationsQuery({
onSuccess(organizations) {
const firstOrg = organizations?.[0]
if (firstOrg && selectedOrg === null) {
setSelectedOrg(firstOrg)
snapshot.setSelectedOrganizationSlug(firstOrg.slug)
}
},
})
/**
* A flat array of org slugs that have integration installed
*/
const flatInstalledConnectionsIds =
integrationData && integrationData.length > 0
? integrationData?.map((x) => x.organization.slug)
: []
/**
* Organizations with extra `installationInstalled` attribute
* Used to show label/badge and allow/disallow installing
*/
const organizationsWithInstalledData: OrganizationsResponseWithInstalledData[] = data
? data.map((org) => {
return {
...org,
installationInstalled: flatInstalledConnectionsIds.includes(org.slug) ? true : false,
}
})
: []
const { mutate, isLoading: isLoadingVercelIntegrationCreateMutation } =
useVercelIntegrationCreateMutation({
onSuccess({ id }) {
setOrganizationIntegrationId(id)
},
})
// console.log('params', params)
// console.log('externalId', externalId)
function onInstall() {
const orgSlug = selectedOrg?.slug
if (!orgSlug) {
return ui.setNotification({ category: 'error', message: 'Please select an organization' })
}
if (!code) {
return ui.setNotification({ category: 'error', message: 'Vercel code missing' })
}
if (!configurationId) {
return ui.setNotification({ category: 'error', message: 'Vercel Configuration ID missing' })
}
if (!source) {
return ui.setNotification({
category: 'error',
message: 'Vercel Configuration source missing',
})
}
const metadata = {}
if (!flatInstalledConnectionsIds.includes(orgSlug)) {
mutate({
code,
configurationId,
orgSlug,
metadata,
source,
teamId: teamId,
})
}
router.push({ pathname: `/integrations/vercel/${orgSlug}/new-project`, query: router.query })
}
const dataLoading = isLoadingVercelIntegrationCreateMutation || isLoadingOrganizationsQuery
return (
<>
<main className="overflow-auto flex flex-col h-full">
<LoadingLine loading={isLoadingVercelIntegrationCreateMutation} />
{organizationIntegrationId === null && (
<>
<ScaffoldContainer className="max-w-md flex flex-col gap-6 grow py-8">
<h1 className="text-xl text-scale-1200">Choose organization</h1>
<>
<Markdown content={`Choose the Supabase Organization you wish to install to`} />
<OrganizationPicker
integrationName="Vercel"
organizationsWithInstalledData={organizationsWithInstalledData}
onSelectedOrgChange={setSelectedOrg}
dataLoading={dataLoading}
/>
<div className="flex flex-row w-full justify-end">
<Button
size="medium"
className="self-end"
disabled={isLoadingVercelIntegrationCreateMutation}
loading={isLoadingVercelIntegrationCreateMutation}
onClick={onInstall}
>
Install integration
</Button>
</div>
</>
</ScaffoldContainer>
<ScaffoldContainer className="flex flex-col gap-6 py-3">
<Alert
withIcon
variant="info"
title="You can uninstall this Integration at any time."
>
<Markdown
content={`You can remove this integration at any time either via Vercel or the Supabase dashboard.`}
/>
</Alert>
</ScaffoldContainer>
</>
)}
<ScaffoldDivider />
</main>
<ScaffoldContainer className="bg-body flex flex-row gap-6 py-6 border-t">
<div className="flex items-center gap-2 text-xs text-scale-900">
<IconBook size={16} /> Docs
</div>
<div className="flex items-center gap-2 text-xs text-scale-900">
<IconLifeBuoy size={16} /> Support
</div>
</ScaffoldContainer>
</>
)
}
VercelIntegration.getLayout = (page) => <IntegrationWindowLayout>{page}</IntegrationWindowLayout>
export interface OrganizationPickerProps {
label?: string
onSelectedOrgChange?: (org: Organization) => void
integrationName: IntegrationName
dataLoading: boolean
organizationsWithInstalledData: OrganizationsResponseWithInstalledData[]
}
const OrganizationPicker = ({
label = 'Choose an organization',
onSelectedOrgChange,
integrationName,
dataLoading,
organizationsWithInstalledData,
}: OrganizationPickerProps) => {
const { ui } = useStore()
const [selectedOrg, setSelectedOrg] = useState<Organization | null>(null)
const { data, isLoading } = useOrganizationsQuery({
onSuccess(organizations) {
const firstOrg = organizations?.[0]
if (firstOrg && selectedOrg === null) {
setSelectedOrg(firstOrg)
onSelectedOrgChange?.(firstOrg)
}
},
})
function _onSelectedOrgChange(slug: string) {
const org = data?.find((org) => org.slug === slug)
if (org) {
setSelectedOrg(org)
onSelectedOrgChange?.(org)
}
}
if (dataLoading) {
return (
<Listbox label={label} value="loading" disabled>
<Listbox.Option key="loading" value="loading" label="Loading...">
Loading...
</Listbox.Option>
</Listbox>
)
}
return (
<Listbox label={label} value={selectedOrg?.slug} onChange={_onSelectedOrgChange}>
{organizationsWithInstalledData?.map((org) => {
const label = (
<div className="flex gap-3 items-center">
{org.name}
{org.installationInstalled && <Badge color="scale">Integration Installed</Badge>}
</div>
)
return (
<Listbox.Option
key={org.id}
value={org.slug}
// @ts-expect-error
label={label}
addOnBefore={({ active, selected }: any) => <IconHexagon />}
>
{label}
</Listbox.Option>
)
})}
</Listbox>
)
}
export default VercelIntegration
@@ -0,0 +1,15 @@
import { proxy, snapshot, useSnapshot } from 'valtio'
export const vercelIntegrationInstallationState = proxy({
selectedOrganizationSlug: '' as string,
setSelectedOrganizationSlug: (selectedOrganizationSlug: string) => {
vercelIntegrationInstallationState.selectedOrganizationSlug = selectedOrganizationSlug
},
})
export const getVercelIntegrationInstallationState = () =>
snapshot(vercelIntegrationInstallationState)
export const useVercelIntegrationInstallationState = (
options?: Parameters<typeof useSnapshot>[1]
) => useSnapshot(vercelIntegrationInstallationState, options)