From d23f08e561a47a06fc72ad6f2dc6e556efb40f3c Mon Sep 17 00:00:00 2001
From: Luca Forstner
Date: Wed, 5 Nov 2025 11:58:37 +0100
Subject: [PATCH 1/9] feat: Add possibility to remove and reauthorize GitHub
connections (#40126)
---
.../Preferences/AccountConnections.tsx | 81 ++++++++++++++++++-
.../GitHubIntegrationConnectionForm.tsx | 78 ++++++++++++------
.../github-authorization-create-mutation.ts | 12 ++-
.../github-authorization-delete-mutation.ts | 48 +++++++++++
.../integrations/github-repositories-query.ts | 2 +-
apps/studio/lib/github.ts | 2 +-
packages/api-types/types/platform.d.ts | 44 +++++++++-
7 files changed, 233 insertions(+), 34 deletions(-)
create mode 100644 apps/studio/data/integrations/github-authorization-delete-mutation.ts
diff --git a/apps/studio/components/interfaces/Account/Preferences/AccountConnections.tsx b/apps/studio/components/interfaces/Account/Preferences/AccountConnections.tsx
index a8907fa8b35fc..5d7d40521254d 100644
--- a/apps/studio/components/interfaces/Account/Preferences/AccountConnections.tsx
+++ b/apps/studio/components/interfaces/Account/Preferences/AccountConnections.tsx
@@ -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 (
{
-
+
{isConnected ? (
-
Connected
+ <>
+
Connected
+
+
+ } type="default">
+ Manage
+
+
+
+ {
+ event.preventDefault()
+ handleReauthenticate()
+ }}
+ >
+
+ Re-authenticate
+
+ setIsRemoveModalOpen(true)}
+ >
+
+ Remove connection
+
+
+
+ >
) : (
)}
+
setIsRemoveModalOpen(false)}
+ onConfirm={handleRemove}
+ loading={isRemoving}
+ >
+
+ Removing this authorization will disconnect your GitHub account from Supabase. You can
+ reconnect at any time.
+
+
)
}
diff --git a/apps/studio/components/interfaces/Settings/Integrations/GithubIntegration/GitHubIntegrationConnectionForm.tsx b/apps/studio/components/interfaces/Settings/Integrations/GithubIntegration/GitHubIntegrationConnectionForm.tsx
index 252894e8bbec0..28b64e069304f 100644
--- a/apps/studio/components/interfaces/Settings/Integrations/GithubIntegration/GitHubIntegrationConnectionForm.tsx
+++ b/apps/studio/components/interfaces/Settings/Integrations/GithubIntegration/GitHubIntegrationConnectionForm.tsx
@@ -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 = ({
No repositories found.
-
- {githubRepos.map((repo, i) => (
- {
- field.onChange(repo.id)
- setRepoComboboxOpen(false)
- githubSettingsForm.setValue(
- 'branchName',
- repo.default_branch || 'main'
- )
- }}
- >
-
- {GITHUB_ICON}
-
-
- {repo.name}
-
-
- ))}
-
+ {githubRepos.length > 0 ? (
+
+ {githubRepos.map((repo, i) => (
+ {
+ field.onChange(repo.id)
+ setRepoComboboxOpen(false)
+ githubSettingsForm.setValue(
+ 'branchName',
+ repo.default_branch || 'main'
+ )
+ }}
+ >
+
+ {GITHUB_ICON}
+
+
+ {repo.name}
+
+
+ ))}
+
+ ) : null}
+ {hasPartialResponseDueToSSO && (
+ <>
+
+
+ {
+ openInstallGitHubIntegrationWindow(
+ 'authorize',
+ refetchGitHubAuthorizationAndRepositories
+ )
+ }}
+ >
+
+
+ Re-authorize GitHub with SSO to show all repositories
+
+
+
+ >
+ )}
diff --git a/apps/studio/data/integrations/github-authorization-create-mutation.ts b/apps/studio/data/integrations/github-authorization-create-mutation.ts
index 3fd22101d1748..18b1efea59404 100644
--- a/apps/studio/data/integrations/github-authorization-create-mutation.ts
+++ b/apps/studio/data/integrations/github-authorization-create-mutation.ts
@@ -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) {
diff --git a/apps/studio/data/integrations/github-authorization-delete-mutation.ts b/apps/studio/data/integrations/github-authorization-delete-mutation.ts
new file mode 100644
index 0000000000000..355ec5fc4f3af
--- /dev/null
+++ b/apps/studio/data/integrations/github-authorization-delete-mutation.ts
@@ -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
>
+
+export const useGitHubAuthorizationDeleteMutation = ({
+ onSuccess,
+ onError,
+ ...options
+}: Omit<
+ UseCustomMutationOptions,
+ 'mutationFn'
+> = {}) => {
+ const queryClient = useQueryClient()
+ return useMutation({
+ 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,
+ })
+}
diff --git a/apps/studio/data/integrations/github-repositories-query.ts b/apps/studio/data/integrations/github-repositories-query.ts
index 9ac089e0a2b19..6d6739321ffe1 100644
--- a/apps/studio/data/integrations/github-repositories-query.ts
+++ b/apps/studio/data/integrations/github-repositories-query.ts
@@ -10,7 +10,7 @@ export async function getGitHubRepositories(signal?: AbortSignal) {
})
if (error) handleError(error)
- return data.repositories
+ return data
}
export type GitHubRepositoriesData = Awaited>
diff --git a/apps/studio/lib/github.ts b/apps/studio/lib/github.ts
index b78986c52e31b..e961960cf0ea4 100644
--- a/apps/studio/lib/github.ts
+++ b/apps/studio/lib/github.ts
@@ -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
diff --git a/packages/api-types/types/platform.d.ts b/packages/api-types/types/platform.d.ts
index 2a9186388b709..91a27a413eb9e 100644
--- a/packages/api-types/types/platform.d.ts
+++ b/packages/api-types/types/platform.d.ts
@@ -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?: {
From 5a0e1cb1ccf54194168ae51324e8c2ab53897dd7 Mon Sep 17 00:00:00 2001
From: "kemal.earth" <606977+kemaldotearth@users.noreply.github.com>
Date: Wed, 5 Nov 2025 11:19:50 +0000
Subject: [PATCH 2/9] feat(studio): log drains entry point from logs (#40095)
* fix: field reference button padding
* fix: remove overriding button style on field ref button
* fix: tidy up the asChild stuff on side panel trigger element
* fix: bunch of type errors on logs sidebar
Theres a temp any fix in there dont kill me frontend team :D
* feat: add to new logs sidebar too
* feat: add to download dropdown menus
* chore: alphabetical order on new logs dropdown
* chore: add target blank to sheet footer
* fix: gap between buttons on old logs toolbar
* feat: add a separator on old logs
* style: new logs top buttons correct types
---
.../LogDrainDestinationSheetForm.tsx | 1 +
.../Settings/Logs/LogsQueryPanel.tsx | 1 -
.../Settings/Logs/PreviewFilterPanel.tsx | 4 +--
.../components/DownloadLogsButton.tsx | 8 ++++-
.../layouts/LogsLayout/LogsSidebarMenuV2.tsx | 35 +++++++++++++++----
.../ui/DataTable/DataTableToolbar.tsx | 2 +-
.../ui/DataTable/DataTableViewOptions.tsx | 2 +-
.../components/ui/DataTable/FilterSideBar.tsx | 20 +++++++++++
.../components/ui/DataTable/RefreshButton.tsx | 2 +-
.../components/ui/DownloadResultsButton.tsx | 17 ++++++---
.../components/ui/Logs/LogsExplorerHeader.tsx | 2 +-
.../ui/src/components/SidePanel/SidePanel.tsx | 6 +---
12 files changed, 76 insertions(+), 24 deletions(-)
diff --git a/apps/studio/components/interfaces/LogDrains/LogDrainDestinationSheetForm.tsx b/apps/studio/components/interfaces/LogDrains/LogDrainDestinationSheetForm.tsx
index e6bcaafa92e78..b9c930d20ed92 100644
--- a/apps/studio/components/interfaces/LogDrains/LogDrainDestinationSheetForm.tsx
+++ b/apps/studio/components/interfaces/LogDrains/LogDrainDestinationSheetForm.tsx
@@ -612,6 +612,7 @@ export function LogDrainDestinationSheetForm({
See full pricing breakdown{' '}
here
diff --git a/apps/studio/components/interfaces/Settings/Logs/LogsQueryPanel.tsx b/apps/studio/components/interfaces/Settings/Logs/LogsQueryPanel.tsx
index 6106cf020814d..bb44df24c7402 100644
--- a/apps/studio/components/interfaces/Settings/Logs/LogsQueryPanel.tsx
+++ b/apps/studio/components/interfaces/Settings/Logs/LogsQueryPanel.tsx
@@ -206,7 +206,6 @@ const LogsQueryPanel = ({
hideFooter
triggerElement={