Skip to content

Commit d23f08e

Browse files
authored
feat: Add possibility to remove and reauthorize GitHub connections (supabase#40126)
1 parent 04bac96 commit d23f08e

File tree

7 files changed

+233
-34
lines changed

7 files changed

+233
-34
lines changed

apps/studio/components/interfaces/Account/Preferences/AccountConnections.tsx

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,24 @@
1+
import { ChevronDown, RefreshCw, Unlink } from 'lucide-react'
12
import Image from 'next/image'
3+
import { useState } from 'react'
4+
import { toast } from 'sonner'
25

36
import Panel from 'components/ui/Panel'
7+
import { useGitHubAuthorizationDeleteMutation } from 'data/integrations/github-authorization-delete-mutation'
48
import { useGitHubAuthorizationQuery } from 'data/integrations/github-authorization-query'
59
import { BASE_PATH } from 'lib/constants'
610
import { openInstallGitHubIntegrationWindow } from 'lib/github'
7-
import { Badge, Button, cn } from 'ui'
11+
import {
12+
Badge,
13+
Button,
14+
cn,
15+
DropdownMenu,
16+
DropdownMenuContent,
17+
DropdownMenuItem,
18+
DropdownMenuSeparator,
19+
DropdownMenuTrigger,
20+
} from 'ui'
21+
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
822
import ShimmeringLoader from 'ui-patterns/ShimmeringLoader'
923

1024
export const AccountConnections = () => {
@@ -16,12 +30,30 @@ export const AccountConnections = () => {
1630
error,
1731
} = useGitHubAuthorizationQuery()
1832

33+
const [isRemoveModalOpen, setIsRemoveModalOpen] = useState(false)
34+
1935
const isConnected = gitHubAuthorization !== null
2036

37+
const { mutate: removeAuthorization, isLoading: isRemoving } =
38+
useGitHubAuthorizationDeleteMutation({
39+
onSuccess: () => {
40+
toast.success('GitHub authorization removed successfully')
41+
setIsRemoveModalOpen(false)
42+
},
43+
})
44+
2145
const handleConnect = () => {
2246
openInstallGitHubIntegrationWindow('authorize')
2347
}
2448

49+
const handleReauthenticate = () => {
50+
openInstallGitHubIntegrationWindow('authorize')
51+
}
52+
53+
const handleRemove = () => {
54+
removeAuthorization()
55+
}
56+
2557
return (
2658
<Panel
2759
className="mb-4 md:mb-8"
@@ -63,9 +95,37 @@ export const AccountConnections = () => {
6395
</p>
6496
</div>
6597
</div>
66-
<div className="flex items-center gap-x-1">
98+
<div className="flex items-center gap-x-2 ml-2">
6799
{isConnected ? (
68-
<Badge variant="success">Connected</Badge>
100+
<>
101+
<Badge variant="success">Connected</Badge>
102+
<DropdownMenu>
103+
<DropdownMenuTrigger asChild>
104+
<Button iconRight={<ChevronDown size={14} />} type="default">
105+
<span>Manage</span>
106+
</Button>
107+
</DropdownMenuTrigger>
108+
<DropdownMenuContent side="bottom" align="end">
109+
<DropdownMenuItem
110+
className="space-x-2"
111+
onSelect={(event) => {
112+
event.preventDefault()
113+
handleReauthenticate()
114+
}}
115+
>
116+
<RefreshCw size={14} />
117+
<p>Re-authenticate</p>
118+
</DropdownMenuItem>
119+
<DropdownMenuItem
120+
className="space-x-2"
121+
onSelect={() => setIsRemoveModalOpen(true)}
122+
>
123+
<Unlink size={14} />
124+
<p>Remove connection</p>
125+
</DropdownMenuItem>
126+
</DropdownMenuContent>
127+
</DropdownMenu>
128+
</>
69129
) : (
70130
<Button type="primary" onClick={handleConnect}>
71131
Connect
@@ -74,6 +134,21 @@ export const AccountConnections = () => {
74134
</div>
75135
</Panel.Content>
76136
)}
137+
<ConfirmationModal
138+
variant="destructive"
139+
size="small"
140+
visible={isRemoveModalOpen}
141+
title="Confirm to remove GitHub authorization"
142+
confirmLabel="Remove connection"
143+
onCancel={() => setIsRemoveModalOpen(false)}
144+
onConfirm={handleRemove}
145+
loading={isRemoving}
146+
>
147+
<p className="text-sm text-foreground-light">
148+
Removing this authorization will disconnect your GitHub account from Supabase. You can
149+
reconnect at any time.
150+
</p>
151+
</ConfirmationModal>
77152
</Panel>
78153
)
79154
}

apps/studio/components/interfaces/Settings/Integrations/GithubIntegration/GitHubIntegrationConnectionForm.tsx

Lines changed: 52 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { zodResolver } from '@hookform/resolvers/zod'
22
import { PermissionAction } from '@supabase/shared-types/out/constants'
3-
import { ChevronDown, Loader2, PlusIcon } from 'lucide-react'
3+
import { ChevronDown, Info, Loader2, PlusIcon, RefreshCw } from 'lucide-react'
44
import { useEffect, useMemo, useState } from 'react'
55
import { useForm } from 'react-hook-form'
66
import { toast } from 'sonner'
@@ -33,6 +33,7 @@ import {
3333
CommandInput_Shadcn_,
3434
CommandItem_Shadcn_,
3535
CommandList_Shadcn_,
36+
CommandSeparator_Shadcn_,
3637
Form_Shadcn_,
3738
FormControl_Shadcn_,
3839
FormField_Shadcn_,
@@ -141,7 +142,7 @@ const GitHubIntegrationConnectionForm = ({
141142

142143
const githubRepos = useMemo(
143144
() =>
144-
githubReposData?.map((repo) => ({
145+
githubReposData?.repositories?.map((repo) => ({
145146
id: repo.id.toString(),
146147
name: repo.name,
147148
installation_id: repo.installation_id,
@@ -150,6 +151,8 @@ const GitHubIntegrationConnectionForm = ({
150151
[githubReposData]
151152
)
152153

154+
const hasPartialResponseDueToSSO = githubReposData?.partial_response_due_to_sso ?? false
155+
153156
const prodBranch = existingBranches?.find((branch) => branch.is_default)
154157

155158
// Combined GitHub Settings Form
@@ -474,30 +477,32 @@ const GitHubIntegrationConnectionForm = ({
474477
<CommandInput_Shadcn_ placeholder="Search repositories..." />
475478
<CommandList_Shadcn_ className="!max-h-[200px]">
476479
<CommandEmpty_Shadcn_>No repositories found.</CommandEmpty_Shadcn_>
477-
<CommandGroup_Shadcn_>
478-
{githubRepos.map((repo, i) => (
479-
<CommandItem_Shadcn_
480-
key={repo.id}
481-
value={`${repo.name.replaceAll('"', '')}-${i}`}
482-
className="flex gap-2 items-center"
483-
onSelect={() => {
484-
field.onChange(repo.id)
485-
setRepoComboboxOpen(false)
486-
githubSettingsForm.setValue(
487-
'branchName',
488-
repo.default_branch || 'main'
489-
)
490-
}}
491-
>
492-
<div className="bg-black shadow rounded p-1 w-5 h-5 flex justify-center items-center">
493-
{GITHUB_ICON}
494-
</div>
495-
<span className="truncate" title={repo.name}>
496-
{repo.name}
497-
</span>
498-
</CommandItem_Shadcn_>
499-
))}
500-
</CommandGroup_Shadcn_>
480+
{githubRepos.length > 0 ? (
481+
<CommandGroup_Shadcn_>
482+
{githubRepos.map((repo, i) => (
483+
<CommandItem_Shadcn_
484+
key={repo.id}
485+
value={`${repo.name.replaceAll('"', '')}-${i}`}
486+
className="flex gap-2 items-center"
487+
onSelect={() => {
488+
field.onChange(repo.id)
489+
setRepoComboboxOpen(false)
490+
githubSettingsForm.setValue(
491+
'branchName',
492+
repo.default_branch || 'main'
493+
)
494+
}}
495+
>
496+
<div className="bg-black shadow rounded p-1 w-5 h-5 flex justify-center items-center">
497+
{GITHUB_ICON}
498+
</div>
499+
<span className="truncate" title={repo.name}>
500+
{repo.name}
501+
</span>
502+
</CommandItem_Shadcn_>
503+
))}
504+
</CommandGroup_Shadcn_>
505+
) : null}
501506
<CommandGroup_Shadcn_>
502507
<CommandItem_Shadcn_
503508
className="flex gap-2 items-center cursor-pointer"
@@ -512,6 +517,27 @@ const GitHubIntegrationConnectionForm = ({
512517
Add GitHub Repositories
513518
</CommandItem_Shadcn_>
514519
</CommandGroup_Shadcn_>
520+
{hasPartialResponseDueToSSO && (
521+
<>
522+
<CommandSeparator_Shadcn_ />
523+
<CommandGroup_Shadcn_>
524+
<CommandItem_Shadcn_
525+
className="flex gap-2 items-start cursor-pointer"
526+
onSelect={() => {
527+
openInstallGitHubIntegrationWindow(
528+
'authorize',
529+
refetchGitHubAuthorizationAndRepositories
530+
)
531+
}}
532+
>
533+
<RefreshCw size={16} className="mt-0.5 shrink-0" />
534+
<div className="text-xs text-foreground-light">
535+
Re-authorize GitHub with SSO to show all repositories
536+
</div>
537+
</CommandItem_Shadcn_>
538+
</CommandGroup_Shadcn_>
539+
</>
540+
)}
515541
</CommandList_Shadcn_>
516542
</Command_Shadcn_>
517543
</PopoverContent_Shadcn_>

apps/studio/data/integrations/github-authorization-create-mutation.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { useMutation } from '@tanstack/react-query'
1+
import { useMutation, useQueryClient } from '@tanstack/react-query'
22
import { toast } from 'sonner'
33

44
import { LOCAL_STORAGE_KEYS } from 'common'
55
import { handleError, post } from 'data/fetchers'
66
import type { ResponseError, UseCustomMutationOptions } from 'types'
7+
import { integrationKeys } from './keys'
78

89
export type GitHubAuthorizationCreateVariables = {
910
code: string
@@ -44,13 +45,22 @@ export const useGitHubAuthorizationCreateMutation = ({
4445
>,
4546
'mutationFn'
4647
> = {}) => {
48+
const queryClient = useQueryClient()
4749
return useMutation<
4850
GitHubAuthorizationCreateData,
4951
ResponseError,
5052
GitHubAuthorizationCreateVariables
5153
>({
5254
mutationFn: (vars) => createGitHubAuthorization(vars),
5355
async onSuccess(data, variables, context) {
56+
await Promise.all([
57+
queryClient.invalidateQueries({
58+
queryKey: integrationKeys.githubAuthorization(),
59+
}),
60+
queryClient.invalidateQueries({
61+
queryKey: integrationKeys.githubRepositoriesList(),
62+
}),
63+
])
5464
await onSuccess?.(data, variables, context)
5565
},
5666
async onError(data, variables, context) {
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { useMutation, useQueryClient } from '@tanstack/react-query'
2+
import { toast } from 'sonner'
3+
4+
import { del, handleError } from 'data/fetchers'
5+
import type { ResponseError, UseCustomMutationOptions } from 'types'
6+
import { integrationKeys } from './keys'
7+
8+
export async function deleteGitHubAuthorization(signal?: AbortSignal) {
9+
const { data, error } = await del('/platform/integrations/github/authorization', { signal })
10+
11+
if (error) handleError(error)
12+
return data
13+
}
14+
15+
type GitHubAuthorizationDeleteData = Awaited<ReturnType<typeof deleteGitHubAuthorization>>
16+
17+
export const useGitHubAuthorizationDeleteMutation = ({
18+
onSuccess,
19+
onError,
20+
...options
21+
}: Omit<
22+
UseCustomMutationOptions<GitHubAuthorizationDeleteData, ResponseError, void>,
23+
'mutationFn'
24+
> = {}) => {
25+
const queryClient = useQueryClient()
26+
return useMutation<GitHubAuthorizationDeleteData, ResponseError, void>({
27+
mutationFn: () => deleteGitHubAuthorization(),
28+
async onSuccess(data, variables, context) {
29+
await Promise.all([
30+
queryClient.invalidateQueries({
31+
queryKey: integrationKeys.githubAuthorization(),
32+
}),
33+
queryClient.invalidateQueries({
34+
queryKey: integrationKeys.githubRepositoriesList(),
35+
}),
36+
])
37+
await onSuccess?.(data, variables, context)
38+
},
39+
async onError(data, variables, context) {
40+
if (onError === undefined) {
41+
toast.error(`Failed to remove GitHub authorization: ${data.message}`)
42+
} else {
43+
onError(data, variables, context)
44+
}
45+
},
46+
...options,
47+
})
48+
}

apps/studio/data/integrations/github-repositories-query.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export async function getGitHubRepositories(signal?: AbortSignal) {
1010
})
1111

1212
if (error) handleError(error)
13-
return data.repositories
13+
return data
1414
}
1515

1616
export type GitHubRepositoriesData = Awaited<ReturnType<typeof getGitHubRepositories>>

apps/studio/lib/github.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export function openInstallGitHubIntegrationWindow(
5050
} else {
5151
const state = makeRandomString(32)
5252
localStorage.setItem(LOCAL_STORAGE_KEYS.GITHUB_AUTHORIZATION_STATE, state)
53-
windowUrl = `${GITHUB_INTEGRATION_AUTHORIZATION_URL}&state=${state}`
53+
windowUrl = `${GITHUB_INTEGRATION_AUTHORIZATION_URL}&state=${state}&prompt=select_account`
5454
}
5555

5656
const systemZoom = width / window.screen.availWidth

0 commit comments

Comments
 (0)