mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
feat: Add possibility to remove and reauthorize GitHub connections (#40126)
This commit is contained in:
1 parent
04bac966a8
commit
d23f08e561
7 files changed
+233
-34
No files matched your search
@@ -1,10 +1,24 @@
|
||||
import { ChevronDown, RefreshCw, Unlink } from 'lucide-react'
|
||||
import Image from 'next/image'
|
||||
import { useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import Panel from 'components/ui/Panel'
|
||||
import { useGitHubAuthorizationDeleteMutation } from 'data/integrations/github-authorization-delete-mutation'
|
||||
import { useGitHubAuthorizationQuery } from 'data/integrations/github-authorization-query'
|
||||
import { BASE_PATH } from 'lib/constants'
|
||||
import { openInstallGitHubIntegrationWindow } from 'lib/github'
|
||||
import { Badge, Button, cn } from 'ui'
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
cn,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from 'ui'
|
||||
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
||||
import ShimmeringLoader from 'ui-patterns/ShimmeringLoader'
|
||||
|
||||
export const AccountConnections = () => {
|
||||
@@ -16,12 +30,30 @@ export const AccountConnections = () => {
|
||||
error,
|
||||
} = useGitHubAuthorizationQuery()
|
||||
|
||||
const [isRemoveModalOpen, setIsRemoveModalOpen] = useState(false)
|
||||
|
||||
const isConnected = gitHubAuthorization !== null
|
||||
|
||||
const { mutate: removeAuthorization, isLoading: isRemoving } =
|
||||
useGitHubAuthorizationDeleteMutation({
|
||||
onSuccess: () => {
|
||||
toast.success('GitHub authorization removed successfully')
|
||||
setIsRemoveModalOpen(false)
|
||||
},
|
||||
})
|
||||
|
||||
const handleConnect = () => {
|
||||
openInstallGitHubIntegrationWindow('authorize')
|
||||
}
|
||||
|
||||
const handleReauthenticate = () => {
|
||||
openInstallGitHubIntegrationWindow('authorize')
|
||||
}
|
||||
|
||||
const handleRemove = () => {
|
||||
removeAuthorization()
|
||||
}
|
||||
|
||||
return (
|
||||
<Panel
|
||||
className="mb-4 md:mb-8"
|
||||
@@ -63,9 +95,37 @@ export const AccountConnections = () => {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-x-1">
|
||||
<div className="flex items-center gap-x-2 ml-2">
|
||||
{isConnected ? (
|
||||
<Badge variant="success">Connected</Badge>
|
||||
<>
|
||||
<Badge variant="success">Connected</Badge>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button iconRight={<ChevronDown size={14} />} type="default">
|
||||
<span>Manage</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="bottom" align="end">
|
||||
<DropdownMenuItem
|
||||
className="space-x-2"
|
||||
onSelect={(event) => {
|
||||
event.preventDefault()
|
||||
handleReauthenticate()
|
||||
}}
|
||||
>
|
||||
<RefreshCw size={14} />
|
||||
<p>Re-authenticate</p>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="space-x-2"
|
||||
onSelect={() => setIsRemoveModalOpen(true)}
|
||||
>
|
||||
<Unlink size={14} />
|
||||
<p>Remove connection</p>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</>
|
||||
) : (
|
||||
<Button type="primary" onClick={handleConnect}>
|
||||
Connect
|
||||
@@ -74,6 +134,21 @@ export const AccountConnections = () => {
|
||||
</div>
|
||||
</Panel.Content>
|
||||
)}
|
||||
<ConfirmationModal
|
||||
variant="destructive"
|
||||
size="small"
|
||||
visible={isRemoveModalOpen}
|
||||
title="Confirm to remove GitHub authorization"
|
||||
confirmLabel="Remove connection"
|
||||
onCancel={() => setIsRemoveModalOpen(false)}
|
||||
onConfirm={handleRemove}
|
||||
loading={isRemoving}
|
||||
>
|
||||
<p className="text-sm text-foreground-light">
|
||||
Removing this authorization will disconnect your GitHub account from Supabase. You can
|
||||
reconnect at any time.
|
||||
</p>
|
||||
</ConfirmationModal>
|
||||
</Panel>
|
||||
)
|
||||
}
|
||||
+52
-26
@@ -1,6 +1,6 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { ChevronDown, Loader2, PlusIcon } from 'lucide-react'
|
||||
import { ChevronDown, Info, Loader2, PlusIcon, RefreshCw } from 'lucide-react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { toast } from 'sonner'
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
CommandInput_Shadcn_,
|
||||
CommandItem_Shadcn_,
|
||||
CommandList_Shadcn_,
|
||||
CommandSeparator_Shadcn_,
|
||||
Form_Shadcn_,
|
||||
FormControl_Shadcn_,
|
||||
FormField_Shadcn_,
|
||||
@@ -141,7 +142,7 @@ const GitHubIntegrationConnectionForm = ({
|
||||
|
||||
const githubRepos = useMemo(
|
||||
() =>
|
||||
githubReposData?.map((repo) => ({
|
||||
githubReposData?.repositories?.map((repo) => ({
|
||||
id: repo.id.toString(),
|
||||
name: repo.name,
|
||||
installation_id: repo.installation_id,
|
||||
@@ -150,6 +151,8 @@ const GitHubIntegrationConnectionForm = ({
|
||||
[githubReposData]
|
||||
)
|
||||
|
||||
const hasPartialResponseDueToSSO = githubReposData?.partial_response_due_to_sso ?? false
|
||||
|
||||
const prodBranch = existingBranches?.find((branch) => branch.is_default)
|
||||
|
||||
// Combined GitHub Settings Form
|
||||
@@ -474,30 +477,32 @@ const GitHubIntegrationConnectionForm = ({
|
||||
<CommandInput_Shadcn_ placeholder="Search repositories..." />
|
||||
<CommandList_Shadcn_ className="!max-h-[200px]">
|
||||
<CommandEmpty_Shadcn_>No repositories found.</CommandEmpty_Shadcn_>
|
||||
<CommandGroup_Shadcn_>
|
||||
{githubRepos.map((repo, i) => (
|
||||
<CommandItem_Shadcn_
|
||||
key={repo.id}
|
||||
value={`${repo.name.replaceAll('"', '')}-${i}`}
|
||||
className="flex gap-2 items-center"
|
||||
onSelect={() => {
|
||||
field.onChange(repo.id)
|
||||
setRepoComboboxOpen(false)
|
||||
githubSettingsForm.setValue(
|
||||
'branchName',
|
||||
repo.default_branch || 'main'
|
||||
)
|
||||
}}
|
||||
>
|
||||
<div className="bg-black shadow rounded p-1 w-5 h-5 flex justify-center items-center">
|
||||
{GITHUB_ICON}
|
||||
</div>
|
||||
<span className="truncate" title={repo.name}>
|
||||
{repo.name}
|
||||
</span>
|
||||
</CommandItem_Shadcn_>
|
||||
))}
|
||||
</CommandGroup_Shadcn_>
|
||||
{githubRepos.length > 0 ? (
|
||||
<CommandGroup_Shadcn_>
|
||||
{githubRepos.map((repo, i) => (
|
||||
<CommandItem_Shadcn_
|
||||
key={repo.id}
|
||||
value={`${repo.name.replaceAll('"', '')}-${i}`}
|
||||
className="flex gap-2 items-center"
|
||||
onSelect={() => {
|
||||
field.onChange(repo.id)
|
||||
setRepoComboboxOpen(false)
|
||||
githubSettingsForm.setValue(
|
||||
'branchName',
|
||||
repo.default_branch || 'main'
|
||||
)
|
||||
}}
|
||||
>
|
||||
<div className="bg-black shadow rounded p-1 w-5 h-5 flex justify-center items-center">
|
||||
{GITHUB_ICON}
|
||||
</div>
|
||||
<span className="truncate" title={repo.name}>
|
||||
{repo.name}
|
||||
</span>
|
||||
</CommandItem_Shadcn_>
|
||||
))}
|
||||
</CommandGroup_Shadcn_>
|
||||
) : null}
|
||||
<CommandGroup_Shadcn_>
|
||||
<CommandItem_Shadcn_
|
||||
className="flex gap-2 items-center cursor-pointer"
|
||||
@@ -512,6 +517,27 @@ const GitHubIntegrationConnectionForm = ({
|
||||
Add GitHub Repositories
|
||||
</CommandItem_Shadcn_>
|
||||
</CommandGroup_Shadcn_>
|
||||
{hasPartialResponseDueToSSO && (
|
||||
<>
|
||||
<CommandSeparator_Shadcn_ />
|
||||
<CommandGroup_Shadcn_>
|
||||
<CommandItem_Shadcn_
|
||||
className="flex gap-2 items-start cursor-pointer"
|
||||
onSelect={() => {
|
||||
openInstallGitHubIntegrationWindow(
|
||||
'authorize',
|
||||
refetchGitHubAuthorizationAndRepositories
|
||||
)
|
||||
}}
|
||||
>
|
||||
<RefreshCw size={16} className="mt-0.5 shrink-0" />
|
||||
<div className="text-xs text-foreground-light">
|
||||
Re-authorize GitHub with SSO to show all repositories
|
||||
</div>
|
||||
</CommandItem_Shadcn_>
|
||||
</CommandGroup_Shadcn_>
|
||||
</>
|
||||
)}
|
||||
</CommandList_Shadcn_>
|
||||
</Command_Shadcn_>
|
||||
</PopoverContent_Shadcn_>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { LOCAL_STORAGE_KEYS } from 'common'
|
||||
import { handleError, post } from 'data/fetchers'
|
||||
import type { ResponseError, UseCustomMutationOptions } from 'types'
|
||||
import { integrationKeys } from './keys'
|
||||
|
||||
export type GitHubAuthorizationCreateVariables = {
|
||||
code: string
|
||||
@@ -44,6 +45,7 @@ export const useGitHubAuthorizationCreateMutation = ({
|
||||
>,
|
||||
'mutationFn'
|
||||
> = {}) => {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation<
|
||||
GitHubAuthorizationCreateData,
|
||||
ResponseError,
|
||||
@@ -51,6 +53,14 @@ export const useGitHubAuthorizationCreateMutation = ({
|
||||
>({
|
||||
mutationFn: (vars) => createGitHubAuthorization(vars),
|
||||
async onSuccess(data, variables, context) {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: integrationKeys.githubAuthorization(),
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: integrationKeys.githubRepositoriesList(),
|
||||
}),
|
||||
])
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
async onError(data, variables, context) {
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { del, handleError } from 'data/fetchers'
|
||||
import type { ResponseError, UseCustomMutationOptions } from 'types'
|
||||
import { integrationKeys } from './keys'
|
||||
|
||||
export async function deleteGitHubAuthorization(signal?: AbortSignal) {
|
||||
const { data, error } = await del('/platform/integrations/github/authorization', { signal })
|
||||
|
||||
if (error) handleError(error)
|
||||
return data
|
||||
}
|
||||
|
||||
type GitHubAuthorizationDeleteData = Awaited<ReturnType<typeof deleteGitHubAuthorization>>
|
||||
|
||||
export const useGitHubAuthorizationDeleteMutation = ({
|
||||
onSuccess,
|
||||
onError,
|
||||
...options
|
||||
}: Omit<
|
||||
UseCustomMutationOptions<GitHubAuthorizationDeleteData, ResponseError, void>,
|
||||
'mutationFn'
|
||||
> = {}) => {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation<GitHubAuthorizationDeleteData, ResponseError, void>({
|
||||
mutationFn: () => deleteGitHubAuthorization(),
|
||||
async onSuccess(data, variables, context) {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: integrationKeys.githubAuthorization(),
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: integrationKeys.githubRepositoriesList(),
|
||||
}),
|
||||
])
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
async onError(data, variables, context) {
|
||||
if (onError === undefined) {
|
||||
toast.error(`Failed to remove GitHub authorization: ${data.message}`)
|
||||
} else {
|
||||
onError(data, variables, context)
|
||||
}
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
@@ -10,7 +10,7 @@ export async function getGitHubRepositories(signal?: AbortSignal) {
|
||||
})
|
||||
|
||||
if (error) handleError(error)
|
||||
return data.repositories
|
||||
return data
|
||||
}
|
||||
|
||||
export type GitHubRepositoriesData = Awaited<ReturnType<typeof getGitHubRepositories>>
|
||||
|
||||
@@ -50,7 +50,7 @@ export function openInstallGitHubIntegrationWindow(
|
||||
} else {
|
||||
const state = makeRandomString(32)
|
||||
localStorage.setItem(LOCAL_STORAGE_KEYS.GITHUB_AUTHORIZATION_STATE, state)
|
||||
windowUrl = `${GITHUB_INTEGRATION_AUTHORIZATION_URL}&state=${state}`
|
||||
windowUrl = `${GITHUB_INTEGRATION_AUTHORIZATION_URL}&state=${state}&prompt=select_account`
|
||||
}
|
||||
|
||||
const systemZoom = width / window.screen.availWidth
|
||||
|
||||
+42
-2
@@ -544,9 +544,16 @@ export interface paths {
|
||||
/** Get GitHub authorization */
|
||||
get: operations['GitHubAuthorizationsController_getGitHubAuthorization']
|
||||
put?: never
|
||||
/** Create GitHub authorization */
|
||||
/**
|
||||
* Upsert GitHub authorization
|
||||
* @description Creates or updates a GitHub authorization for the current user
|
||||
*/
|
||||
post: operations['GitHubAuthorizationsController_createGitHubAuthorization']
|
||||
delete?: never
|
||||
/**
|
||||
* Remove GitHub authorization
|
||||
* @description Removes the GitHub authorization for the current user
|
||||
*/
|
||||
delete: operations['GitHubAuthorizationsController_removeGitHubAuthorization']
|
||||
options?: never
|
||||
head?: never
|
||||
patch?: never
|
||||
@@ -6992,6 +6999,8 @@ export interface components {
|
||||
}[]
|
||||
}
|
||||
ListGitHubRepositoriesResponse: {
|
||||
/** @description The authorized user may not have access to all GitHub repositories in case they haven't gone through the authorization process with SSO yet. This field will be `true` if this is the case. The calling user must reauthorize their GitHub account with SSO to see all repositories. */
|
||||
partial_response_due_to_sso: boolean
|
||||
repositories: {
|
||||
default_branch: string
|
||||
id: number
|
||||
@@ -12130,6 +12139,37 @@ export interface operations {
|
||||
}
|
||||
}
|
||||
}
|
||||
GitHubAuthorizationsController_removeGitHubAuthorization: {
|
||||
parameters: {
|
||||
query?: never
|
||||
header?: never
|
||||
path?: never
|
||||
cookie?: never
|
||||
}
|
||||
requestBody?: never
|
||||
responses: {
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content?: never
|
||||
}
|
||||
/** @description There was no GitHub authorization attached to the user */
|
||||
404: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content?: never
|
||||
}
|
||||
/** @description Failed to remove GitHub authorization */
|
||||
500: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content?: never
|
||||
}
|
||||
}
|
||||
}
|
||||
GitHubBranchesController_listConnectionBranches: {
|
||||
parameters: {
|
||||
query?: {
|
||||
|
||||
Reference in new issue
Block a user