From bb47aeff34904354b2c443ed4cce9c663ce9fcdd Mon Sep 17 00:00:00 2001 From: Jonathan Summers-Muir Date: Mon, 19 Jun 2023 03:46:08 +0800 Subject: [PATCH] more project link stuff and integration panel UI changes. Side panel added for project linking idea --- .../Integrations/IntegrationPanels.tsx | 21 +--- .../interfaces/Integrations/ProjectLinker.tsx | 63 ++++++---- .../IntegrationSettings/Integration.tsx | 60 +++++++++- .../IntegrationSettings.tsx | 7 +- ...n.tsx => SidePanelGitHubRepoSelection.tsx} | 0 .../SidePanelVercelProjectLinker.tsx | 108 ++++++++++++++++++ studio/components/layouts/Scaffold.tsx | 4 +- ...integration-connections-create-mutation.ts | 2 +- ...el-installed-connection-delete-mutation.ts | 41 +++++++ ...ions-vercel-installed-connections-query.ts | 90 +++++++++++++++ studio/data/integrations/keys.ts | 2 + 11 files changed, 351 insertions(+), 47 deletions(-) rename studio/components/interfaces/Organization/IntegrationSettings/{GitHubRepoSelection.tsx => SidePanelGitHubRepoSelection.tsx} (100%) create mode 100644 studio/components/interfaces/Organization/IntegrationSettings/SidePanelVercelProjectLinker.tsx create mode 100644 studio/data/integrations/integrations-vercel-installed-connection-delete-mutation.ts create mode 100644 studio/data/integrations/integrations-vercel-installed-connections-query.ts diff --git a/studio/components/interfaces/Integrations/IntegrationPanels.tsx b/studio/components/interfaces/Integrations/IntegrationPanels.tsx index 93c9d9ce8cf..46b9b59db37 100644 --- a/studio/components/interfaces/Integrations/IntegrationPanels.tsx +++ b/studio/components/interfaces/Integrations/IntegrationPanels.tsx @@ -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 { title: string - // orgName: string connection: Integration } @@ -40,11 +38,9 @@ const HandleIcon = ({ type }: { type: HandleIconType }) => { ) - // break case 'Supabase': return Supabase - // break default: @@ -118,10 +114,11 @@ const IntegrationInstallation = React.forwardRef { connection: IntegrationProjectConnection type: Integration['integration']['name'] + actions: React.ReactNode } const IntegrationConnection = React.forwardRef( - ({ 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 -
- -
+
{actions}
) @@ -216,8 +211,6 @@ const EmptyIntegrationConnection = React.forwardRef< >
{ return (
- {/*

{props.title}

-

- Repository connections for {name?.toLowerCase()} -

*/}
) diff --git a/studio/components/interfaces/Integrations/ProjectLinker.tsx b/studio/components/interfaces/Integrations/ProjectLinker.tsx index 9c694ead9ab..7488298a8a8 100644 --- a/studio/components/interfaces/Integrations/ProjectLinker.tsx +++ b/studio/components/interfaces/Integrations/ProjectLinker.tsx @@ -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 (
    @@ -159,29 +174,27 @@ const ProjectLinkerItem = ({ removeConnection, canRemove, }: ProjectLinkerItemProps) => { + console.log(supabaseProjectId) return (
  • - - +
    diff --git a/studio/components/interfaces/Organization/IntegrationSettings/Integration.tsx b/studio/components/interfaces/Organization/IntegrationSettings/Integration.tsx index 9be3d46e382..ecbd6751ea2 100644 --- a/studio/components/interfaces/Organization/IntegrationSettings/Integration.tsx +++ b/studio/components/interfaces/Organization/IntegrationSettings/Integration.tsx @@ -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 ( <> @@ -84,6 +106,42 @@ Repository connections for ${title?.toLowerCase()} key={i} connection={connection} type={integration.integration.name} + actions={ + + + +
    + } + > +
    Refresh Enviroment Variables
    +
    + Reapply env vars in vercel. +
    + + + } + onSelect={() => + handleDelete(integration.id, connection.id) + } + > + Delete + + + } + > + + + } /> ))}
diff --git a/studio/components/interfaces/Organization/IntegrationSettings/IntegrationSettings.tsx b/studio/components/interfaces/Organization/IntegrationSettings/IntegrationSettings.tsx index 4e13f013ad0..7576bb5187c 100644 --- a/studio/components/interfaces/Organization/IntegrationSettings/IntegrationSettings.tsx +++ b/studio/components/interfaces/Organization/IntegrationSettings/IntegrationSettings.tsx @@ -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} /> - + {/* */} + ) } diff --git a/studio/components/interfaces/Organization/IntegrationSettings/GitHubRepoSelection.tsx b/studio/components/interfaces/Organization/IntegrationSettings/SidePanelGitHubRepoSelection.tsx similarity index 100% rename from studio/components/interfaces/Organization/IntegrationSettings/GitHubRepoSelection.tsx rename to studio/components/interfaces/Organization/IntegrationSettings/SidePanelGitHubRepoSelection.tsx diff --git a/studio/components/interfaces/Organization/IntegrationSettings/SidePanelVercelProjectLinker.tsx b/studio/components/interfaces/Organization/IntegrationSettings/SidePanelVercelProjectLinker.tsx new file mode 100644 index 00000000000..44132a299db --- /dev/null +++ b/studio/components/interfaces/Organization/IntegrationSettings/SidePanelVercelProjectLinker.tsx @@ -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( + '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 ( + githubConnectionConfigPanelSnapshot.setVisible(false)} + > +
+ + + + + { + // if (next) { + // window.location.href = next + // } + }} + installedConnections={vercelProjectConnections} + /> + + +
    +
  • + {/* */} +
  • +
+
+
+
+ ) +} + +export default observer(SidePanelVercelProjectLinker) diff --git a/studio/components/layouts/Scaffold.tsx b/studio/components/layouts/Scaffold.tsx index e69a4a44cd0..b3adc0714f7 100644 --- a/studio/components/layouts/Scaffold.tsx +++ b/studio/components/layouts/Scaffold.tsx @@ -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>( ({ className, ...props }, ref) => { - return
+ return
} ) diff --git a/studio/data/integrations/integration-connections-create-mutation.ts b/studio/data/integrations/integration-connections-create-mutation.ts index d60828ca565..6881b405fab 100644 --- a/studio/data/integrations/integration-connections-create-mutation.ts +++ b/studio/data/integrations/integration-connections-create-mutation.ts @@ -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, }) diff --git a/studio/data/integrations/integrations-vercel-installed-connection-delete-mutation.ts b/studio/data/integrations/integrations-vercel-installed-connection-delete-mutation.ts new file mode 100644 index 00000000000..5c9825553b4 --- /dev/null +++ b/studio/data/integrations/integrations-vercel-installed-connection-delete-mutation.ts @@ -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_( + `${API_URL}/integrations/vercel/connections/${id}`, + { organization_integration_id }, + { signal } + ) + + if (response.error) throw response.error + return response +} + +type DeleteContentData = Awaited> + +export const useIntegrationsVercelInstalledConnectionDeleteMutation = ({ + onSuccess, + ...options +}: Omit, 'mutationFn'> = {}) => { + return useMutation( + (args) => deleteConnection(args), + { + async onSuccess(data, variables, context) { + await onSuccess?.(data, variables, context) + }, + ...options, + } + ) +} diff --git a/studio/data/integrations/integrations-vercel-installed-connections-query.ts b/studio/data/integrations/integrations-vercel-installed-connections-query.ts new file mode 100644 index 00000000000..f4136c2f11d --- /dev/null +++ b/studio/data/integrations/integrations-vercel-installed-connections-query.ts @@ -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> +export type VercelProjectsError = unknown + +export const useVercelProjectConnectionsQuery = ( + { organization_integration_id }: VercelProjectsVariables, + { + enabled = true, + ...options + }: UseQueryOptions = {} +) => + useQuery( + 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 ( + * prefetch()}> + * Start loading on hover + * + * ) + */ +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]) +} diff --git a/studio/data/integrations/keys.ts b/studio/data/integrations/keys.ts index f317710e2b3..be833c0d307 100644 --- a/studio/data/integrations/keys.ts +++ b/studio/data/integrations/keys.ts @@ -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, }