@@ -22,6 +22,7 @@ import {
2222 FolderOpen ,
2323 GearSix ,
2424 GitBranch ,
25+ Plus ,
2526} from "@phosphor-icons/react" ;
2627import {
2728 AlertDialog ,
@@ -110,12 +111,11 @@ export function GitIntegrationStep({
110111 isLoading : githubUserIntegrationsLoading ,
111112 } = useUserGithubIntegrations ( ) ;
112113 const hasGitIntegration = githubUserIntegrations . length > 0 ;
113- const { repositories, isLoadingRepos , failedInstallationIds } =
114+ const { repositories, failedInstallationIds , reposByInstallationId } =
114115 useUserRepositoryIntegration ( ) ;
115- const githubIntegration = githubUserIntegrations [ 0 ] ?? null ;
116- const integrationIsStale =
117- ! ! githubIntegration &&
118- failedInstallationIds . includes ( githubIntegration . installation_id ) ;
116+ const anyIntegrationStale = githubUserIntegrations . some ( ( i ) =>
117+ failedInstallationIds . includes ( i . installation_id ) ,
118+ ) ;
119119
120120 const alternativeConnectedProjects = useMemo ( ( ) => {
121121 if ( hasGitIntegration ) return [ ] ;
@@ -136,13 +136,6 @@ export function GitIntegrationStep({
136136 ) ;
137137 } , [ alternativeConnectedProjects , selectedAlternativeId ] ) ;
138138
139- const repoSummary = useMemo ( ( ) => {
140- if ( repositories . length === 0 ) return null ;
141- const names = repositories . map ( ( r ) => r . split ( "/" ) . pop ( ) ?? r ) ;
142- if ( names . length <= 2 ) return names . join ( " and " ) ;
143- return `${ names [ 0 ] } , ${ names [ 1 ] } and ${ names . length - 2 } more` ;
144- } , [ repositories ] ) ;
145-
146139 const repoMatchesGitHub = useMemo ( ( ) => {
147140 if ( ! detectedRepo || repositories . length === 0 ) return false ;
148141 return repositories . some (
@@ -151,18 +144,23 @@ export function GitIntegrationStep({
151144 } , [ detectedRepo , repositories ] ) ;
152145
153146 const apiClient = useOptionalAuthenticatedClient ( ) ;
154- const [ disconnectConfirmOpen , setDisconnectConfirmOpen ] = useState ( false ) ;
147+ const [ disconnectTarget , setDisconnectTarget ] = useState < {
148+ installationId : string ;
149+ accountName : string ;
150+ } | null > ( null ) ;
151+ const [ reconnectingInstallationId , setReconnectingInstallationId ] = useState <
152+ string | null
153+ > ( null ) ;
155154 const disconnectMutation = useMutation ( {
156- mutationFn : async ( opts : { silent ?: boolean } = { } ) => {
157- const installationId = githubIntegration ?. installation_id ;
158- if ( ! apiClient || ! installationId ) {
159- throw new Error ( "No GitHub integration to disconnect" ) ;
155+ mutationFn : async ( opts : { installationId : string ; silent ?: boolean } ) => {
156+ if ( ! apiClient ) {
157+ throw new Error ( "Not authenticated" ) ;
160158 }
161- await apiClient . disconnectGithubUserIntegration ( installationId ) ;
159+ await apiClient . disconnectGithubUserIntegration ( opts . installationId ) ;
162160 return { silent : opts . silent ?? false } ;
163161 } ,
164162 onSuccess : ( { silent } ) => {
165- setDisconnectConfirmOpen ( false ) ;
163+ setDisconnectTarget ( null ) ;
166164 invalidateGithubQueries ( queryClient , selectedProjectId ) ;
167165 if ( ! silent ) toast . success ( "GitHub disconnected." ) ;
168166 } ,
@@ -344,7 +342,7 @@ export function GitIntegrationStep({
344342 { isLoading || githubUserIntegrationsLoading ? (
345343 < Skeleton className = "h-[16px] w-[80px]" />
346344 ) : hasGitIntegration ? (
347- integrationIsStale ? (
345+ anyIntegrationStale ? (
348346 < Text className = "text-(--amber-11) text-[13px]" >
349347 Reconnect needed
350348 </ Text >
@@ -356,72 +354,136 @@ export function GitIntegrationStep({
356354 className = "text-(--green-9)"
357355 />
358356 < Text className = "text-(--green-11) text-[13px]" >
359- Connected
357+ { githubUserIntegrations . length > 1
358+ ? `Connected (${ githubUserIntegrations . length } )`
359+ : "Connected" }
360360 </ Text >
361361 </ Flex >
362362 )
363363 ) : null }
364364 </ Flex >
365365 { hasGitIntegration ? (
366366 < Flex direction = "column" gap = "3" >
367- < Text
368- className = {
369- integrationIsStale
370- ? "text-(--amber-11) text-sm"
371- : "text-(--gray-11) text-sm"
372- }
373- >
374- { integrationIsStale
375- ? "We can't reach GitHub with your saved authorization. The app may have been uninstalled or the token revoked. Reconnect to restore access."
376- : isLoadingRepos
377- ? "Loading repositories..."
378- : repoSummary
379- ? `Access to ${ repoSummary } `
380- : "No repositories found. Check your GitHub app settings." }
381- </ Text >
382- < Flex align = "center" gap = "3" wrap = "wrap" >
383- { integrationIsStale && (
384- < Button
385- size = "1"
386- variant = "solid"
387- loading = {
388- disconnectMutation . isPending || isConnecting
389- }
390- onClick = { async ( ) => {
391- if ( ! githubIntegration ?. installation_id ) return ;
392- try {
393- await disconnectMutation . mutateAsync ( {
394- silent : true ,
395- } ) ;
396- } catch {
397- return ;
398- }
399- void handleConnectGitHub ( ) ;
400- } }
367+ { githubUserIntegrations . map ( ( integration ) => {
368+ const installationId = integration . installation_id ;
369+ const accountName =
370+ integration . account ?. name ?? "GitHub" ;
371+ const installRepos =
372+ reposByInstallationId [ installationId ] ;
373+ const isLoadingInstallRepos =
374+ installRepos === undefined ;
375+ const isStale =
376+ failedInstallationIds . includes ( installationId ) ;
377+ const isReconnecting =
378+ reconnectingInstallationId === installationId ;
379+ return (
380+ < Flex
381+ key = { integration . id }
382+ direction = "column"
383+ gap = "2"
384+ p = "3"
385+ className = "rounded-[8px] border border-(--gray-a3)"
401386 >
402- Reconnect
403- < ArrowSquareOut size = { 12 } />
404- </ Button >
405- ) }
406- < Button
407- size = "1"
408- variant = "soft"
409- color = "gray"
410- onClick = { ( ) => {
411- const id = githubIntegration ?. installation_id ;
412- const account = githubIntegration ?. account ;
413- const url = id
414- ? account ?. type === "Organization" &&
415- account . name
416- ? `https://github.com/organizations/${ account . name } /settings/installations/${ id } `
417- : `https://github.com/settings/installations/${ id } `
418- : "https://github.com/settings/installations" ;
419- trpcClient . os . openExternal . mutate ( { url } ) ;
420- } }
421- >
422- < GearSix size = { 12 } />
423- Settings
424- </ Button >
387+ < Flex
388+ align = "center"
389+ justify = "between"
390+ gap = "2"
391+ wrap = "wrap"
392+ >
393+ < Flex align = "center" gap = "2" >
394+ < Text className = "font-bold text-(--gray-12) text-sm" >
395+ { accountName }
396+ </ Text >
397+ < Text className = "text-(--gray-10) text-[12px]" >
398+ { integration . account ?. type ===
399+ "Organization"
400+ ? "org"
401+ : "personal" }
402+ </ Text >
403+ </ Flex >
404+ { isStale ? (
405+ < Text className = "text-(--amber-11) text-[12px]" >
406+ Reconnect needed
407+ </ Text >
408+ ) : (
409+ < Text className = "text-(--gray-10) text-[12px]" >
410+ { isLoadingInstallRepos
411+ ? "Loading…"
412+ : installRepos . length === 1
413+ ? "1 repo"
414+ : `${ installRepos . length } repos` }
415+ </ Text >
416+ ) }
417+ </ Flex >
418+ < Flex align = "center" gap = "3" wrap = "wrap" >
419+ { isStale && (
420+ < Button
421+ size = "1"
422+ variant = "solid"
423+ loading = { isReconnecting }
424+ disabled = {
425+ reconnectingInstallationId !== null &&
426+ ! isReconnecting
427+ }
428+ onClick = { async ( ) => {
429+ setReconnectingInstallationId (
430+ installationId ,
431+ ) ;
432+ try {
433+ await disconnectMutation . mutateAsync ( {
434+ installationId,
435+ silent : true ,
436+ } ) ;
437+ } catch {
438+ setReconnectingInstallationId ( null ) ;
439+ return ;
440+ }
441+ try {
442+ await handleConnectGitHub ( ) ;
443+ } finally {
444+ setReconnectingInstallationId ( null ) ;
445+ }
446+ } }
447+ >
448+ Reconnect
449+ < ArrowSquareOut size = { 12 } />
450+ </ Button >
451+ ) }
452+ < Button
453+ size = "1"
454+ variant = "soft"
455+ color = "gray"
456+ onClick = { ( ) => {
457+ const account = integration . account ;
458+ const url =
459+ account ?. type === "Organization" &&
460+ account . name
461+ ? `https://github.com/organizations/${ account . name } /settings/installations/${ installationId } `
462+ : `https://github.com/settings/installations/${ installationId } ` ;
463+ trpcClient . os . openExternal . mutate ( { url } ) ;
464+ } }
465+ >
466+ < GearSix size = { 12 } />
467+ Settings
468+ </ Button >
469+ < Button
470+ size = "1"
471+ variant = "soft"
472+ color = "red"
473+ onClick = { ( ) =>
474+ setDisconnectTarget ( {
475+ installationId,
476+ accountName,
477+ } )
478+ }
479+ >
480+ Disconnect
481+ </ Button >
482+ </ Flex >
483+ </ Flex >
484+ ) ;
485+ } ) }
486+ < Flex align = "center" gap = "3" wrap = "wrap" >
425487 < Button
426488 size = "1"
427489 variant = "soft"
@@ -441,11 +503,12 @@ export function GitIntegrationStep({
441503 < Button
442504 size = "1"
443505 variant = "ghost"
444- color = "red "
445- disabled = { ! githubIntegration ?. installation_id }
446- onClick = { ( ) => setDisconnectConfirmOpen ( true ) }
506+ color = "gray "
507+ onClick = { ( ) => void handleConnectGitHub ( ) }
508+ loading = { isConnecting }
447509 >
448- Disconnect
510+ < Plus size = { 12 } />
511+ Add another GitHub org
449512 </ Button >
450513 </ Flex >
451514 </ Flex >
@@ -609,15 +672,18 @@ export function GitIntegrationStep({
609672 </ StepActions >
610673
611674 < AlertDialog . Root
612- open = { disconnectConfirmOpen }
675+ open = { disconnectTarget !== null }
613676 onOpenChange = { ( next ) => {
614677 if ( ! next && ! disconnectMutation . isPending ) {
615- setDisconnectConfirmOpen ( false ) ;
678+ setDisconnectTarget ( null ) ;
616679 }
617680 } }
618681 >
619682 < AlertDialog . Content maxWidth = "450px" >
620- < AlertDialog . Title > Disconnect GitHub</ AlertDialog . Title >
683+ < AlertDialog . Title >
684+ Disconnect{ " " }
685+ { disconnectTarget ? disconnectTarget . accountName : "GitHub" }
686+ </ AlertDialog . Title >
621687 < AlertDialog . Description className = "text-sm" >
622688 This removes your personal GitHub authorization from PostHog. You
623689 can reconnect at any time. The GitHub App itself stays installed
@@ -637,7 +703,12 @@ export function GitIntegrationStep({
637703 < Button
638704 variant = "solid"
639705 color = "red"
640- onClick = { ( ) => disconnectMutation . mutate ( { } ) }
706+ onClick = { ( ) => {
707+ if ( ! disconnectTarget ) return ;
708+ disconnectMutation . mutate ( {
709+ installationId : disconnectTarget . installationId ,
710+ } ) ;
711+ } }
641712 disabled = { disconnectMutation . isPending }
642713 >
643714 { disconnectMutation . isPending ? < Spinner size = "1" /> : null }
0 commit comments