more project link stuff and integration panel UI changes. Side panel added for project linking idea

This commit is contained in:
Jonathan Summers-Muir committed 2023-06-19 03:46:08 +08:00
1 parent 0343189017
commit bb47aeff34
11 files changed
+351 -47

No files matched your search

@@ -1,12 +1,11 @@
import { useParams } from 'common'
import { Markdown } from 'components/interfaces/Markdown'
import { Integration, IntegrationProjectConnection } from 'data/integrations/integrations-query'
import dayjs from 'dayjs'
import { useStore } from 'hooks'
import Image from 'next/image'
import { useRouter } from 'next/router'
import React from 'react'
import { Project } from 'types'
import { Badge, Button, IconArrowRight, IconGitHub, IconSquare } from 'ui'
import { cn } from 'ui/src/utils/cn'
@@ -14,7 +13,6 @@ const ICON_STROKE_WIDTH = 2
const ICON_SIZE = 14
export interface IntegrationInstallationProps extends React.HTMLAttributes<HTMLLIElement> {
title: string
// orgName: string
connection: Integration
}
@@ -40,11 +38,9 @@ const HandleIcon = ({ type }: { type: HandleIconType }) => {
</svg>
)
// <IconTriangle strokeWidth={ICON_STROKE_WIDTH} size={ICON_SIZE} />
break
case 'Supabase':
return <img src="/img/supabase-logo.svg" alt="Supabase" className="w-3.5"></img>
// <IconCloudLightning strokeWidth={ICON_STROKE_WIDTH} size={ICON_SIZE} />
break
default:
@@ -118,10 +114,11 @@ const IntegrationInstallation = React.forwardRef<HTMLLIElement, IntegrationInsta
export interface IntegrationConnectionProps extends React.HTMLAttributes<HTMLLIElement> {
connection: IntegrationProjectConnection
type: Integration['integration']['name']
actions: React.ReactNode
}
const IntegrationConnection = React.forwardRef<HTMLLIElement, IntegrationConnectionProps>(
({ className, connection, type, ...props }, ref) => {
({ className, connection, type, actions, ...props }, ref) => {
const { app } = useStore()
const { projects } = app
@@ -163,9 +160,7 @@ const IntegrationConnection = React.forwardRef<HTMLLIElement, IntegrationConnect
</div>
</div>
<div>
<Button type="default">Disconnect</Button>
</div>
<div>{actions}</div>
</div>
</li>
)
@@ -216,8 +211,6 @@ const EmptyIntegrationConnection = React.forwardRef<
>
<div className="absolute w-8 rounded-bl-full border-b border-l h-10 -left-px"></div>
<div
{...props}
ref={ref}
className={cn(
'w-full',
'border border-dashed',
@@ -240,10 +233,6 @@ const IntegrationConnectionHeader = React.forwardRef<HTMLDivElement, Integration
({ className, name, markdown = '', ...props }, ref) => {
return (
<div {...props} ref={ref} className={cn('border-l ml-6 pl-8 pt-6 pb-3', className)}>
{/* <h3>{props.title}</h3>
<p className="text-scale-1100 text-sm">
Repository connections for <span className="capitalize">{name?.toLowerCase()}</span>
</p> */}
<Markdown content={markdown} className="" />
</div>
)
@@ -2,8 +2,7 @@ import { databaseIcon, vercelIcon } from 'components/to-be-cleaned/ListIcons'
import { useIntegrationConnectionsCreateMutation } from 'data/integrations/integration-connections-create-mutation'
import { VercelProjectsResponse } from 'data/integrations/integrations-vercel-projects-query'
import { useState } from 'react'
import { TRUE } from 'sass'
import { Button, IconArrowRight, IconPlus, IconX, Listbox } from 'ui'
import { Button, IconArrowRight, IconPlus, IconX, Listbox, Select } from 'ui'
interface Project {
id: string
@@ -18,6 +17,7 @@ export interface ProjectLinkerProps {
foreignProjects: VercelProjectsResponse[]
supabaseProjects: Project[]
onCreateConnections?: () => void
installedConnections?: any[]
}
const UNDEFINED_SELECT_VALUE = 'undefined'
@@ -28,20 +28,34 @@ const ProjectLinker = ({
foreignProjects,
supabaseProjects,
onCreateConnections: _onCreateConnections,
installedConnections,
}: ProjectLinkerProps) => {
const [connections, setConnections] = useState([
{
foreignProjectId: UNDEFINED_SELECT_VALUE,
supabaseProjectId: UNDEFINED_SELECT_VALUE,
integrationId: INTEGRATION_INTERNAL_ID,
metadata: UNDEFINED_METADATA_VALUE,
},
])
const [connections, setConnections] = useState(
installedConnections && installedConnections.length > 0
? installedConnections?.map((connection) => {
return {
foreignProjectId: connection?.foreign_project_id,
supabaseProjectId: connection?.supabase_project_id,
integrationId: INTEGRATION_INTERNAL_ID,
metadata: UNDEFINED_METADATA_VALUE,
}
})
: [
{
foreignProjectId: UNDEFINED_SELECT_VALUE,
supabaseProjectId: UNDEFINED_SELECT_VALUE,
integrationId: INTEGRATION_INTERNAL_ID,
metadata: UNDEFINED_METADATA_VALUE,
},
]
)
console.log('installedConnections', installedConnections)
console.log('connections', connections)
console.log('foreignProjects', foreignProjects)
console.log('supabaseProjects', supabaseProjects)
function onSetForeignProjectId(idx: number, id: string) {
console.log(id)
console.log(foreignProjects)
const newConnections = [...connections]
const projectDetails = foreignProjects.filter((x) => x.id === id)[0]
newConnections[idx].metadata = { ...projectDetails }
@@ -86,13 +100,14 @@ const ProjectLinker = ({
})
function onCreateConnections() {
console.log('connections', connections)
createConnections({
organizationIntegrationId,
connections,
})
}
console.log(connections)
return (
<div className="flex flex-col gap-4">
<ul>
@@ -159,29 +174,27 @@ const ProjectLinkerItem = ({
removeConnection,
canRemove,
}: ProjectLinkerItemProps) => {
console.log(supabaseProjectId)
return (
<li className="py-2">
<div className="relative flex w-full space-x-2">
<div className="w-1/2 flex-grow">
<Listbox
value={supabaseProjectId ?? UNDEFINED_SELECT_VALUE}
onChange={setSupabaseProjectId}
>
<Listbox.Option value={UNDEFINED_SELECT_VALUE} label="Choose a project" disabled>
<Select value={supabaseProjectId} onChange={(e) => setSupabaseProjectId(e.target.value)}>
<Select.Option value={UNDEFINED_SELECT_VALUE} label="Choose a project" disabled>
Choose a project
</Listbox.Option>
</Select.Option>
{supabaseProjects.map((project) => (
<Listbox.Option
<Select.Option
key={project.id}
value={project.id}
label={project.name}
addOnBefore={() => databaseIcon}
// label={project.name}
// addOnBefore={() => databaseIcon}
>
{project.name}
</Listbox.Option>
</Select.Option>
))}
</Listbox>
</Select>
</div>
<div className="flex flex-shrink items-center">
<IconArrowRight className="text-scale-1000" />
@@ -1,3 +1,4 @@
import { useParams } from 'common'
import {
EmptyIntegrationConnection,
IntegrationConnection,
@@ -11,11 +12,17 @@ import {
ScaffoldSectionContent,
ScaffoldSectionDetail,
} from 'components/layouts/Scaffold'
import { Integration as TIntegration } from 'data/integrations/integrations-query'
import { useIntegrationConnectionsCreateMutation } from 'data/integrations/integration-connections-create-mutation'
import {
Integration as TIntegration,
useIntegrationsQuery,
} from 'data/integrations/integrations-query'
import { useIntegrationsVercelInstalledConnectionDeleteMutation } from 'data/integrations/integrations-vercel-installed-connection-delete-mutation'
import { pluralize } from 'lib/helpers'
import { EMPTY_ARR } from 'lib/void'
import { useMemo } from 'react'
import { useGithubConnectionConfigPanelSnapshot } from 'state/github-connection-config-panel'
import { Button, Dropdown, IconChevronDown, IconRefreshCcw, IconTrash } from 'ui'
export interface IntegrationProps {
title: string
@@ -35,6 +42,11 @@ const Integration = ({
integrations = EMPTY_ARR,
}: IntegrationProps) => {
const githubConnectionConfigPanelShotshot = useGithubConnectionConfigPanelSnapshot()
const deleteMutation = useIntegrationsVercelInstalledConnectionDeleteMutation({
onSuccess: (data, variables, context) => {
// Handle the success case
},
})
const ConnectionHeading = ({ integration }: { integration: TIntegration }) => {
return (
@@ -49,6 +61,16 @@ Repository connections for ${title?.toLowerCase()}
)
}
// Call the mutation when needed
const handleDelete = (organizationIntegrationId: string, projectIntegrationId: string) => {
const variables = {
organization_integration_id: organizationIntegrationId,
id: projectIntegrationId,
}
deleteMutation.mutate(variables)
}
return (
<>
<ScaffoldContainer>
@@ -84,6 +106,42 @@ Repository connections for ${title?.toLowerCase()}
key={i}
connection={connection}
type={integration.integration.name}
actions={
<Dropdown
side="bottom"
align="end"
size="large"
overlay={
<>
<Dropdown.Item
icon={
<div>
<IconRefreshCcw size={14} />
</div>
}
>
<div>Refresh Enviroment Variables</div>
<div className="text-scale-900 text-xs">
Reapply env vars in vercel.
</div>
</Dropdown.Item>
<Dropdown.Separator />
<Dropdown.Item
icon={<IconTrash size={14} />}
onSelect={() =>
handleDelete(integration.id, connection.id)
}
>
Delete
</Dropdown.Item>
</>
}
>
<Button asChild iconRight={<IconChevronDown />} type="default">
<span>Manage</span>
</Button>
</Dropdown>
}
/>
))}
</ul>
@@ -3,8 +3,10 @@ import { ScaffoldDivider } from 'components/layouts/Scaffold'
import { useIntegrationsQuery } from 'data/integrations/integrations-query'
import { useStore } from 'hooks'
import { observer } from 'mobx-react-lite'
import GitHubRepoSelection from './GitHubRepoSelection'
import GitHubRepoSelection from './SidePanelGitHubRepoSelection'
import Integration from './Integration'
import SidePanelGitHubRepoSelection from './SidePanelGitHubRepoSelection'
import SidePanelVercelProjectLinker from './SidePanelVercelProjectLinker'
const IntegrationSettings = () => {
const { ui } = useStore()
@@ -68,7 +70,8 @@ You can change the scope of the access for Supabase by configuring here.
`}
integrations={githubIntegrations}
/>
<GitHubRepoSelection />
{/* <SidePanelGitHubRepoSelection /> */}
<SidePanelVercelProjectLinker />
</>
)
}
@@ -0,0 +1,108 @@
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 { useStore } from 'hooks'
import { observer } from 'mobx-react-lite'
import { useMemo, useState } from 'react'
import { useGithubConnectionConfigPanelSnapshot } from 'state/github-connection-config-panel'
import { SidePanel } from 'ui'
import { useVercelProjectsQuery } from 'data/integrations/integrations-vercel-projects-query'
import { useProjectsQuery } from 'data/projects/projects-query'
import { EMPTY_ARR } from 'lib/void'
import { Organization } from 'types'
import { useVercelProjectConnectionsQuery } from 'data/integrations/integrations-vercel-installed-connections-query'
const SidePanelVercelProjectLinker = () => {
const { ui } = useStore()
const { slug } = useParams()
const { data } = useIntegrationsQuery({ orgSlug: slug })
const vercelIntegrations = data?.filter((integration) => integration.integration.id === 1) // vercel
const selectedOrg = ui.selectedOrganization
const [organizationIntegrationId, setOrganizationIntegrationId] = useState<string>(
'880cb835-54b4-415b-8178-6f7920fdc40d'
)
const githubConnectionConfigPanelSnapshot = useGithubConnectionConfigPanelSnapshot()
const { data: supabaseProjectsData } = useProjectsQuery({
enabled: organizationIntegrationId !== null,
})
const supabaseProjects = useMemo(
() =>
supabaseProjectsData
?.filter((project) => project.organization_id === selectedOrg?.id)
.map((project) => ({ id: project.id.toString(), name: project.name })) ?? EMPTY_ARR,
[selectedOrg?.id, supabaseProjectsData]
)
const { data: vercelProjectConnectionsData } = useVercelProjectConnectionsQuery(
{
organization_integration_id: organizationIntegrationId,
},
{ enabled: organizationIntegrationId !== null }
)
const vercelProjectConnections = useMemo(
() => vercelProjectConnectionsData ?? EMPTY_ARR,
[vercelProjectConnectionsData]
)
const { data: vercelProjectsData } = useVercelProjectsQuery(
{
orgId: selectedOrg?.id,
orgSlug: selectedOrg?.slug,
},
{ enabled: organizationIntegrationId !== null }
)
console.log('vercelProjectsData', vercelProjectsData)
const vercelProjects = useMemo(() => vercelProjectsData ?? EMPTY_ARR, [vercelProjectsData])
return (
<SidePanel
header={'Add GitHub repository'}
size="large"
visible={githubConnectionConfigPanelSnapshot.visible}
onCancel={() => githubConnectionConfigPanelSnapshot.setVisible(false)}
>
<div className="my-10 flex flex-col gap-6">
<SidePanel.Content>
<Markdown
content={`
### Choose repository to connect to
Check the details below before proceeding
`}
/>
</SidePanel.Content>
<SidePanel.Content className="flex flex-col gap-2">
<ProjectLinker
organizationIntegrationId={'880cb835-54b4-415b-8178-6f7920fdc40d'}
foreignProjects={vercelProjects}
supabaseProjects={supabaseProjects}
onCreateConnections={() => {
// if (next) {
// window.location.href = next
// }
}}
installedConnections={vercelProjectConnections}
/>
</SidePanel.Content>
<SidePanel.Content>
<ul>
<li className="border px-10">
{/* <IntegrationConnectionOption connection={githubIntegrations[0]} /> */}
</li>
</ul>
</SidePanel.Content>
</div>
</SidePanel>
)
}
export default observer(SidePanelVercelProjectLinker)
+2 -2
View File
@@ -1,7 +1,7 @@
import React from 'react'
import { cn } from 'ui/src/utils/cn'
const maxWdithClasses = 'mx-auto w-full max-w-[1600px]'
const maxWidthClasses = 'mx-auto w-full max-w-[1600px]'
const paddingClasses = 'px-6 lg:px-14 xl:px-28 2xl:px-32'
// lg: pt - 8
@@ -23,7 +23,7 @@ const ScaffoldTitle = React.forwardRef<
const ScaffoldContainer = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => {
return <div ref={ref} {...props} className={cn(maxWdithClasses, paddingClasses, className)} />
return <div ref={ref} {...props} className={cn(maxWidthClasses, paddingClasses, className)} />
}
)
@@ -16,7 +16,7 @@ export async function createIntegrationConnections({
organizationIntegrationId,
connections,
}: IntegrationConnectionsCreateVariables) {
const response = await post(`${API_URL}/integrations/connections`, {
const response = await post(`${API_URL}/integrations/vercel/connections`, {
organizationIntegrationId,
connections,
})
@@ -0,0 +1,41 @@
import { useMutation, UseMutationOptions } from '@tanstack/react-query'
import { delete_ } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { UserContent } from 'types'
type DeleteVariables = { organization_integration_id: string; id: string }
export async function deleteConnection(
{ organization_integration_id, id }: DeleteVariables,
signal?: AbortSignal
) {
if (!organization_integration_id) {
throw new Error('organization_integration_id is required')
}
const response = await delete_<UserContent>(
`${API_URL}/integrations/vercel/connections/${id}`,
{ organization_integration_id },
{ signal }
)
if (response.error) throw response.error
return response
}
type DeleteContentData = Awaited<ReturnType<typeof deleteConnection>>
export const useIntegrationsVercelInstalledConnectionDeleteMutation = ({
onSuccess,
...options
}: Omit<UseMutationOptions<DeleteContentData, unknown, DeleteVariables>, 'mutationFn'> = {}) => {
return useMutation<DeleteContentData, unknown, DeleteVariables>(
(args) => deleteConnection(args),
{
async onSuccess(data, variables, context) {
await onSuccess?.(data, variables, context)
},
...options,
}
)
}
@@ -0,0 +1,90 @@
import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query'
import { get } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { useCallback } from 'react'
import { integrationKeys } from './keys'
export type VercelProjectsVariables = {
organization_integration_id?: string
}
export type VercelProjectsResponse = {
id: string
name: string
metadata: {
id: string
name: string
framework: string
link: string
}
}
export async function getVercelInstalledConnections(
{ organization_integration_id }: VercelProjectsVariables,
signal?: AbortSignal
) {
if (!organization_integration_id) {
throw new Error('organization_integration_id is required')
}
const response = await get(
`${API_URL}/integrations/vercel/connections/${organization_integration_id}`,
{
signal,
}
)
if (response.error) {
throw response.error
}
return response as VercelProjectsResponse[]
}
export type VercelProjectsData = Awaited<ReturnType<typeof getVercelInstalledConnections>>
export type VercelProjectsError = unknown
export const useVercelProjectConnectionsQuery = <TData = VercelProjectsData>(
{ organization_integration_id }: VercelProjectsVariables,
{
enabled = true,
...options
}: UseQueryOptions<VercelProjectsData, VercelProjectsError, TData> = {}
) =>
useQuery<VercelProjectsData, VercelProjectsError, TData>(
integrationKeys.vercelProjectList(organization_integration_id),
({ signal }) => getVercelInstalledConnections({ organization_integration_id }, signal),
{
enabled:
enabled &&
typeof organization_integration_id !== 'undefined' &&
typeof organization_integration_id !== 'undefined',
...options,
}
)
/**
* useVercelProjectsPrefetch is used for prefetching data. For example, starting a query loading before a page is navigated to.
*
* @example
* const prefetch = useVercelProjectsPrefetch({ orgSlug })
*
* return (
* <Link onMouseEnter={() => prefetch()}>
* Start loading on hover
* </Link>
* )
*/
export const useVercelProjectsPrefetch = ({
organization_integration_id,
}: VercelProjectsVariables) => {
const client = useQueryClient()
return useCallback(() => {
if (organization_integration_id) {
client.prefetchQuery(
integrationKeys.vercelConnectionsList(organization_integration_id),
({ signal }) => getVercelInstalledConnections({ organization_integration_id }, signal)
)
}
}, [organization_integration_id])
}
+2
View File
@@ -2,4 +2,6 @@ export const integrationKeys = {
list: (orgSlug: string | undefined) => ['organizations', orgSlug, 'integrations'] as const,
vercelProjectList: (orgSlug: string | undefined) =>
['organizations', orgSlug, 'vercel-projects'] as const,
vercelConnectionsList: (organization_integration_id: string | undefined) =>
['organizations', organization_integration_id, 'vercel-connections'] as const,
}