Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions frontend/snapshots.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6987,9 +6987,9 @@ snapshots:
scenes-app-settings-environment--settings-environment-details--light:
hash: v1.k794b7964.d4b5e0030757796876fa680f389ca76201f098f3b1b65eb303ca73b5e1be6ae4.nm2ebJO-70lgkubc-wOB4EFtPnO-885kq4I93mt0TNs
scenes-app-settings-environment--settings-environment-error-tracking--dark:
hash: v1.k794b7964.8c31cf4377cc4aa217ef13e05c56ccfa53994f780b73471c5d0982de4fa78d64.347ACdUg244bCzms7KAIMTpLv1WjkTahcO-uJXTkiyg
hash: v1.k794b7964.d9d299561ca437a5157edf37c4758a22d0d3db4844b78c5b23d2ad57fefeed46.1WzOoH8UOJjf6a6xfjBQfArtLbWmlU9q2VgRKExoisc
scenes-app-settings-environment--settings-environment-error-tracking--light:
hash: v1.k794b7964.26a597269ab5fffa13b4ee537a28a9dbb6dea5b74d2ba7c476792d89fea18f2e.TkgSHX0A7P-GqH7EyapdL8uq40FkIW2sq0tdL5EouTc
hash: v1.k794b7964.98d311695dbaf5a8fe7e533170c3194f4a16a3221b62bd50a7a0d52de7ffb87a.nL8nCyaLMIlm4UNM11v_EYEgfHJwOgT1yk_ar2DduhM
scenes-app-settings-environment--settings-environment-error-tracking-configuration--dark:
hash: v1.k794b7964.56ec7a0f848a210db0b28301b8de7a858daa47b4075114d92e49dcc314e53771.eBsNSzM5P7mAshc2EMJhB6A8K6MuESVzMaT_zktP0jI
scenes-app-settings-environment--settings-environment-error-tracking-configuration--light:
Expand All @@ -7003,9 +7003,9 @@ snapshots:
scenes-app-settings-environment--settings-environment-heatmaps--light:
hash: v1.k794b7964.05c858527b6a3deecc54799e4d4fad6c2b88ff83a26dc0baadff3448bf6635b4.u3Xak3I8OzaYGlO_KlLOvHMh7sWvmjOVDrKURMJBQLs
scenes-app-settings-environment--settings-environment-integrations--dark:
hash: v1.k794b7964.81a9ac1dae6f0520d24618ca1164d13febe35d137184e985a344dcd1632e0cec.fQFE2wr9G3dtsonE4swvWZ_5-LveeCtXd6lkJAqR2pg
hash: v1.k794b7964.c702b588bdda7c56430d8adf4a896345977abcf0a562e0e1bddf88238496770b.B_JN44u0kV7No27lpvJOOTBN7urhc6zcJ6r3kTOLFJk
scenes-app-settings-environment--settings-environment-integrations--light:
hash: v1.k794b7964.1934174417b65042ff2e3e967e833f604198ee713c7f7711f67b57646d9326a1.s9GyCmxzjPtnJ5aQsAbXkjY0WFJxqavzA2JTDDgFi34
hash: v1.k794b7964.f249ac0b4d1984c3d0259394fef46e89186d039a4089c7d2acb19922e1d939d5.T0FKR0TfOB2Flt5kqA4HboFgQlUHjdI92AdocREcaoQ
scenes-app-settings-environment--settings-environment-marketing-analytics--dark:
hash: v1.k794b7964.72d483d5eeffbd82927b8b748abb1f6a624ed157f86db39b7c231b26e1c0a574.DdQZfV_I433UB3bh4bmxnivledUaDRg0EVOIWqWfTOQ
scenes-app-settings-environment--settings-environment-marketing-analytics--light:
Expand All @@ -7023,9 +7023,9 @@ snapshots:
scenes-app-settings-environment--settings-environment-product-analytics--light:
hash: v1.k794b7964.70bf7959ecd4f39a1ac0d069beec0f6cdaf24d9afaee70e70b4cf75619f56de5.xQCvIAWzmRCUGafz6moNuItTspGJA6dwjKLb3mE16Uk
scenes-app-settings-environment--settings-environment-replay--dark:
hash: v1.k794b7964.420f87c12f0437816ce82f7e13a45851e78a6e84198cbd81262c964a90bdc47e.CaLkZAptUT5gtkAbv8PMsSfuCUSfKnhks4ojIwUks0E
hash: v1.k794b7964.21bb720ce05c578a8845a479d05d784ba1d53ddcb95c69394affbf938b1a7965.9i1A3a_KRw2QEMXpVyo4pvn14VQVGnQmsJK-aVTCZEY
scenes-app-settings-environment--settings-environment-replay--light:
hash: v1.k794b7964.7eff7d4f78ea0ebea1712c75a344af72769a1370cb89df2207419498ec5ca613.BYQaF3dooPNLprvFYODHlQwcc6VmfgnZ81Td6MzcOd0
hash: v1.k794b7964.e85b294e4ef70bcdac34028f9695d0141f1d3048df64dffc4efe44db0d5a3bad.NNpko46SP0bM093ww1OUryXTQvWWrrprlf4DJTjRrkY
scenes-app-settings-environment--settings-environment-revenue-analytics--dark:
hash: v1.k794b7964.1b59813d53eb3cf7f0505a250ff6dcac0c25785011679e74420d389dabe25582.Gn5HB_rvDNmwFNIlRts1ZZ17hRhFJ88yILUevtuwy0g
scenes-app-settings-environment--settings-environment-revenue-analytics--light:
Expand Down Expand Up @@ -7087,17 +7087,17 @@ snapshots:
scenes-app-settings-project--settings-project-details--light:
hash: v1.k794b7964.92d131dadd962625b5dfdd78a2cb9c42017b230b1c39f9ae1b293e8f6e9fe788.ufkrkTmgF764iFDi-uaHriqBvleJci3OVpjBAwJxiUw
scenes-app-settings-project--settings-project-integrations--dark:
hash: v1.k794b7964.81a9ac1dae6f0520d24618ca1164d13febe35d137184e985a344dcd1632e0cec.oySHN778JskP5Yz0hvSlx4gjJijO83k8OJLXyCNqb1w
hash: v1.k794b7964.c702b588bdda7c56430d8adf4a896345977abcf0a562e0e1bddf88238496770b.1kRxkbRjHvzKooz7gjEbDDb3eVs_NFoQLI7EJGWU7d8
scenes-app-settings-project--settings-project-integrations--light:
hash: v1.k794b7964.1934174417b65042ff2e3e967e833f604198ee713c7f7711f67b57646d9326a1.DxQZ3xs0WrHP48Y9r13SVaHcJsN4h1KJVK-NSPUSXpg
hash: v1.k794b7964.f249ac0b4d1984c3d0259394fef46e89186d039a4089c7d2acb19922e1d939d5.jJ02CU9zcmLg0Itz38r89UxL3MY_8Rnd-KddWUMrkP8
scenes-app-settings-project--settings-project-product-analytics--dark:
hash: v1.k794b7964.925784616ac6ad5cb556351ede1916440226c9089930265fc96df7e14863046c.6iXpmU_M40nsD_JokXYH4Ey-T5YaQsEB1jGl35jRBgo
scenes-app-settings-project--settings-project-product-analytics--light:
hash: v1.k794b7964.527360edf55b268b83ba976288336721af6da9af6088605669a4395b5a9b7a20.qnXEVYP868iG2Q0bj-z9_tORjlspkGdyJEMj5GDuG-8
scenes-app-settings-project--settings-project-replay--dark:
hash: v1.k794b7964.420f87c12f0437816ce82f7e13a45851e78a6e84198cbd81262c964a90bdc47e.MbG5FcxBuIFHRdrwoHEbyKJb_Pz8doVuDwvq1AaJeXM
hash: v1.k794b7964.236164910b062612c43d08adf4badb434a9040029b3863f00b2dd3e42b7dd6ea.MA7G3gr8yO98zhL7mvkJH2peqZi1qKQ4oi95zldIjD0
scenes-app-settings-project--settings-project-replay--light:
hash: v1.k794b7964.7eff7d4f78ea0ebea1712c75a344af72769a1370cb89df2207419498ec5ca613.MmWu9UBGKmS-G4nQoVPXkW1Po6RDl327aG2XZYgKm-c
hash: v1.k794b7964.e85b294e4ef70bcdac34028f9695d0141f1d3048df64dffc4efe44db0d5a3bad._O2pBTlPs63Agtc0ofCCJVnt1KyJOezEe3-llnsuGQE
scenes-app-settings-project--settings-project-surveys--dark:
hash: v1.k794b7964.116b8618e7355d13ff8ddc83c5095b98edd1bca44d8326d7a05c2922e967b0d8.mYqdrV9UEmN1USMnHw5MMm3DWaOk8_s_BNmFP-aRMFU
scenes-app-settings-project--settings-project-surveys--light:
Expand Down
51 changes: 45 additions & 6 deletions frontend/src/lib/integrations/integrationsLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,15 @@ import { urls } from 'scenes/urls'
import { EmailIntegrationDomainGroupedType, IntegrationKind, IntegrationType } from '~/types'

import {
integrationsGithubAvailableInstallationsRetrieve,
integrationsGithubReposRetrieve,
integrationsRequestAccessCreate,
} from 'products/integrations/frontend/generated/api'
import type { GitHubRepoApi, IntegrationKindEnumApi } from 'products/integrations/frontend/generated/api.schemas'
import type {
GitHubAvailableInstallationApi,
GitHubRepoApi,
IntegrationKindEnumApi,
} from 'products/integrations/frontend/generated/api.schemas'
import { ChannelType } from 'products/workflows/frontend/Channels/MessageChannels'

import type { AvailableSetupTaskIdsEnumApi } from '../../generated/core/api.schemas'
Expand Down Expand Up @@ -86,6 +91,8 @@ export interface integrationsLogicValues {
| 'vercel'
)[]
) => IntegrationType[]
githubAvailableInstallations: GitHubAvailableInstallationApi[] | null
githubAvailableInstallationsLoading: boolean
githubIntegrations: IntegrationType[]
githubRepositories: Record<number, GitHubRepoApi[]>
githubRepositoriesLoading: boolean
Expand Down Expand Up @@ -162,7 +169,7 @@ export interface integrationsLogicActions {
| 'vercel'
searchParams: any
}
linkExistingGithubInstallation: () => any
linkExistingGithubInstallation: (installationId?: string) => string
linkExistingGithubInstallationFailure: (
error: string,
errorObject?: any
Expand All @@ -172,10 +179,10 @@ export interface integrationsLogicActions {
}
linkExistingGithubInstallationSuccess: (
linkedGithubInstallation: IntegrationType,
payload?: any
payload?: string
) => {
linkedGithubInstallation: IntegrationType
payload?: any
payload?: string
}
loadGitHubRepositories: (integrationId: number) => {
integrationId: number
Expand All @@ -199,6 +206,21 @@ export interface integrationsLogicActions {
integrationId: number
repositories: GitHubRepoApi[]
}
loadGithubAvailableInstallations: () => any
loadGithubAvailableInstallationsFailure: (
error: string,
errorObject?: any
) => {
error: string
errorObject?: any
}
loadGithubAvailableInstallationsSuccess: (
githubAvailableInstallations: GitHubAvailableInstallationApi[],
payload?: any
) => {
githubAvailableInstallations: GitHubAvailableInstallationApi[]
payload?: any
}
loadIntegrations: () => any
loadIntegrationsFailure: (
error: string,
Expand Down Expand Up @@ -814,9 +836,13 @@ export const integrationsLogic = kea<integrationsLogicType>([
// Reuse a GitHub App installation already connected to another project in the same
// org. A GitHub App installs once per org, so a second project can't reinstall; this
// links the existing install without the fragile GitHub setup redirect roundtrip.
linkExistingGithubInstallation: async () => {
// When the org has more than one installation the caller passes the chosen
// installationId, since the backend can't auto-resolve between them.
linkExistingGithubInstallation: async (installationId?: string) => {
try {
const integration = await api.integrations.githubLinkExisting({})
const integration = await api.integrations.githubLinkExisting(
installationId ? { installation_id: installationId } : {}
)
lemonToast.success('Linked the existing GitHub installation to this project.')
actions.loadIntegrations()
return integration
Expand All @@ -827,6 +853,19 @@ export const integrationsLogic = kea<integrationsLogicType>([
},
},
],
githubAvailableInstallations: [
null as GitHubAvailableInstallationApi[] | null,
{
// The org's other GitHub installations, so the UI can offer a picker when there's
// more than one, rather than failing the auto-resolve link as ambiguous.
loadGithubAvailableInstallations: async () => {
const response = await integrationsGithubAvailableInstallationsRetrieve(
String(values.currentProjectId)
)
return response.installations
},
Comment thread
Twixes marked this conversation as resolved.
},
],
Comment thread
Twixes marked this conversation as resolved.
accessRequest: [
null as IntegrationKind | null,
{
Expand Down
39 changes: 39 additions & 0 deletions frontend/src/scenes/integrations/components/Integrations.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import '@testing-library/jest-dom'

import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'

import type { GitHubAvailableInstallationApi } from 'products/integrations/frontend/generated/api.schemas'

import { GitHubInstallationLink } from './Integrations'

describe('GitHubInstallationLink', () => {
it('uses one menu trigger when multiple GitHub installations are available', async () => {
const user = userEvent.setup()
const onLink = jest.fn()
const installations: GitHubAvailableInstallationApi[] = [
{
installation_id: '101',
account_name: 'PostHog',
account_type: 'Organization',
source_team_id: 1,
},
{
installation_id: '202',
account_name: 'Hedgebox',
account_type: 'Organization',
source_team_id: 2,
},
]

render(<GitHubInstallationLink installations={installations} loading={false} onLink={onLink} />)

expect(screen.getAllByText('Link existing installation')).toHaveLength(1)
expect(screen.queryByText('PostHog')).not.toBeInTheDocument()

await user.click(screen.getByText('Link existing installation'))
await user.click(await screen.findByText('PostHog'))

expect(onLink).toHaveBeenCalledWith('101')
})
})
91 changes: 76 additions & 15 deletions frontend/src/scenes/integrations/components/Integrations.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import { useActions, useValues } from 'kea'
import { PropsWithChildren, useMemo, useState } from 'react'

import { IconChevronDown } from '@posthog/icons'
import { LemonButton } from '@posthog/lemon-ui'

import api from 'lib/api'
import { useOnMountEffect } from 'lib/hooks/useOnMountEffect'
import { integrationsLogic } from 'lib/integrations/integrationsLogic'
import { IntegrationView } from 'lib/integrations/IntegrationView'
import { LemonMenu } from 'lib/lemon-ui/LemonMenu'
Comment thread
Twixes marked this conversation as resolved.
import { GitLabSetupModal } from 'scenes/integrations/gitlab/GitLabSetupModal'
import { teamLogic } from 'scenes/teamLogic'
import { urls } from 'scenes/urls'

import { IntegrationKind, IntegrationType } from '~/types'

import type { GitHubAvailableInstallationApi } from 'products/integrations/frontend/generated/api.schemas'

export function GitLabIntegration(): JSX.Element {
const [isOpen, setIsOpen] = useState<boolean>(false)
return (
Expand All @@ -30,45 +35,101 @@ export function LinearIntegration({ next }: { next?: string }): JSX.Element {

export function GithubIntegration({ next }: { next?: string }): JSX.Element {
const { currentTeam } = useValues(teamLogic)
const { linkedGithubInstallationLoading } = useValues(integrationsLogic)
const { linkExistingGithubInstallation } = useActions(integrationsLogic)
const { linkedGithubInstallationLoading, githubAvailableInstallations } = useValues(integrationsLogic)
const { linkExistingGithubInstallation, loadGithubAvailableInstallations } = useActions(integrationsLogic)
const githubIntegrations = useIntegrations('github')

// integrationsLogic is a singleton mounted from dozens of unrelated surfaces, so this fetch
// hangs off the GitHub setup UI instead of the shared integrations load.
useOnMountEffect(() => {
loadGithubAvailableInstallations()
})

const settingsPath = next ?? urls.settings('environment-integrations')
const authorizationUrl = api.integrations.authorizeUrl({
next: currentTeam?.id ? urls.project(currentTeam.id, settingsPath) : settingsPath,
kind: 'github',
})

const installations = githubAvailableInstallations ?? []
const isConnected = githubIntegrations.length > 0
const canLinkExisting = !isConnected && installations.length > 0
const multipleInstallations = installations.length > 1

return (
<Integration kind="github">
<div className="flex flex-col gap-y-2">
<div className="flex gap-x-2">
<div className="flex flex-wrap gap-2">
{/* This leaves PostHog entirely, and a GitHub App installs at most once per
account, so GitHub offers install where it's missing and configure where it
isn't. Connecting is only a promise we can keep while this project has
nothing linked; past that the honest label is the destination. */}
<LemonButton type="secondary" disableClientSideRouting to={authorizationUrl}>
Connect organization
{isConnected ? 'Manage on GitHub' : 'Connect organization'}
</LemonButton>
{githubIntegrations.length === 0 && (
<LemonButton
type="secondary"
{canLinkExisting && (
<GitHubInstallationLink
installations={installations}
loading={linkedGithubInstallationLoading}
onClick={() => linkExistingGithubInstallation()}
>
Link existing installation
</LemonButton>
onLink={linkExistingGithubInstallation}
/>
)}
</div>
{githubIntegrations.length === 0 && (
{isConnected && (
<p className="text-secondary text-xs mb-0">
Already installed the PostHog GitHub App for another project in this organization? A GitHub App
installs once per organization, so use "Link existing installation" to connect it here instead
of reinstalling.
Install the PostHog app on another GitHub account, or change which repositories this
installation can see.
</p>
)}
{canLinkExisting && (
<p className="text-secondary text-xs mb-0">
{multipleInstallations
? 'Choose an existing GitHub installation to connect to this project.'
: 'A GitHub App installs once per organization. Link the installation you already have instead of reinstalling.'}
</p>
)}
</div>
</Integration>
)
}

export function GitHubInstallationLink({
installations,
loading,
onLink,
}: {
installations: GitHubAvailableInstallationApi[]
loading: boolean
onLink: (installationId?: string) => void
}): JSX.Element | null {
if (installations.length === 0) {
return null
}

if (installations.length === 1) {
return (
<LemonButton type="secondary" loading={loading} onClick={() => onLink()}>
Link existing installation
</LemonButton>
)
}

return (
<LemonMenu
items={installations.map((installation) => ({
key: installation.installation_id,
label: installation.account_name ?? `Installation ${installation.installation_id}`,
disabledReason: loading ? 'Linking an installation' : undefined,
onClick: () => onLink(installation.installation_id),
}))}
>
<LemonButton type="secondary" loading={loading} sideIcon={<IconChevronDown />}>
Link existing installation
</LemonButton>
</LemonMenu>
)
}

export function JiraIntegration({ next }: { next?: string }): JSX.Element {
return <OAuthIntegration kind="jira" connectText="Connect site" next={next} />
}
Expand Down
Loading
Loading