mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
deploy button flow started
This commit is contained in:
1 parent
6c31d28f05
commit
beb5d10b61
21 files changed
+1003
-278
No files matched your search
@@ -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'
|
||||
|
||||
+2
-2
@@ -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
-1
@@ -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'
|
||||
|
||||
+5
-3
@@ -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'
|
||||
|
||||
@@ -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 }
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
Reference in new issue
Block a user