From 2eafe0fcfefd9ce44d2abcd6be6fe1b6144f7032 Mon Sep 17 00:00:00 2001 From: Bolaji Olajide <25608335+BolajiOlajide@users.noreply.github.com> Date: Tue, 2 Jul 2024 14:56:17 +0100 Subject: [PATCH] Implement UI for Github Apps <> Batch Changes integration (#63597) Closes SRCH-660 Closes SRCH-661 This PR adds the UI flow for adding a GitHub app as a Batch Changes credential. The actual functionality to create the GitHub app and store it's information will be done in a follow up PR. https://github.com/sourcegraph/sourcegraph/assets/25608335/1089a363-070d-4fd8-8a03-af63cd378947 ## Test plan Manual testing. ## Changelog --- .vscode/settings.json | 4 +- .../gitHubApps/CreateGitHubAppPage.tsx | 120 ++++--- .../settings/AddCredentialModal.module.scss | 7 +- .../settings/AddCredentialModal.story.tsx | 22 +- .../batches/settings/AddCredentialModal.tsx | 298 ++++++++++++------ .../BatchChangesCreateGitHubAppPage.tsx | 79 ++++- .../BatchChangesSettingsArea.story.tsx | 7 +- .../settings/BatchChangesSettingsArea.tsx | 4 +- .../BatchChangesSiteConfigSettingsPage.tsx | 2 +- .../settings/CodeHostConnectionNode.story.tsx | 3 +- .../settings/CodeHostConnectionNode.tsx | 10 +- .../batches/settings/CodeHostConnections.tsx | 51 ++- client/web/src/featureFlags/featureFlags.ts | 1 + .../site-admin/SiteAdminGitHubAppsArea.tsx | 2 + client/web/src/site-admin/routes.tsx | 8 +- .../graphqlbackend/githubapps.graphql | 22 ++ 16 files changed, 454 insertions(+), 186 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 4c2bd4ca825..3cb0181d452 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -73,7 +73,5 @@ "editor.defaultFormatter": "esbenp.prettier-vscode" }, "cody.codebase": "github.com/sourcegraph/sourcegraph", - "rust-analyzer.linkedProjects": [ - "docker-images/syntax-highlighter/Cargo.toml" - ], + "rust-analyzer.linkedProjects": ["docker-images/syntax-highlighter/Cargo.toml"] } diff --git a/client/web/src/components/gitHubApps/CreateGitHubAppPage.tsx b/client/web/src/components/gitHubApps/CreateGitHubAppPage.tsx index 467e12610c5..c03ce76a507 100644 --- a/client/web/src/components/gitHubApps/CreateGitHubAppPage.tsx +++ b/client/web/src/components/gitHubApps/CreateGitHubAppPage.tsx @@ -1,23 +1,15 @@ import React, { type FC, useState, useCallback, useRef, useEffect } from 'react' +import classNames from 'classnames' import { noop } from 'lodash' +import { useNavigate } from 'react-router-dom' import type { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry' import { EVENT_LOGGER } from '@sourcegraph/shared/src/telemetry/web/eventLogger' -import { - Alert, - Container, - Button, - Input, - Label, - Text, - PageHeader, - ButtonLink, - Checkbox, - Link, -} from '@sourcegraph/wildcard' +import { Alert, Container, Button, Input, Label, Text, PageHeader, Checkbox, Link } from '@sourcegraph/wildcard' -import { GitHubAppDomain } from '../../graphql-operations' +import type { AuthenticatedUser } from '../../auth' +import type { GitHubAppDomain, GitHubAppKind } from '../../graphql-operations' import { PageTitle } from '../PageTitle' interface StateResponse { @@ -51,6 +43,8 @@ export interface CreateGitHubAppPageProps extends TelemetryV2Props { headerAnnotation?: React.ReactNode /** The domain the new GitHub App is meant to be used for in Sourcegraph. */ appDomain: GitHubAppDomain + /** The purpose of the GitHub App to be created. This is only applicable when the appDomain is BATCHES. */ + appKind: GitHubAppKind /** The name to use for the new GitHub App. Defaults to "Sourcegraph". */ defaultAppName?: string /* @@ -63,6 +57,13 @@ export interface CreateGitHubAppPageProps extends TelemetryV2Props { * or a string with an error message reason if not. */ validateURL?: (url: string) => true | string + /** The currently authenticated user */ + authenticatedUser: AuthenticatedUser + /** + * Whether or not the page is being rendered in a minimized mode. + * Minimized mode is when this component is rendered in a modal. + */ + minimizedMode?: boolean } /** @@ -79,7 +80,11 @@ export const CreateGitHubAppPage: FC = ({ baseURL, validateURL, telemetryRecorder, + appKind, + authenticatedUser, + minimizedMode, }) => { + const navigate = useNavigate() const ref = useRef(null) const formInput = useRef(null) const [name, setName] = useState(defaultAppName) @@ -143,24 +148,30 @@ export const CreateGitHubAppPage: FC = ({ const createState = useCallback(async () => { setError(undefined) try { - const response = await fetch( - `/githubapp/new-app-state?appName=${name}&webhookURN=${url}&domain=${appDomain}&baseURL=${url}` - ) + let appStateUrl = `/githubapp/new-app-state?appName=${encodeURIComponent( + name + )}&webhookURN=${url}&domain=${appDomain}&baseURL=${encodeURIComponent(url)}&kind=${appKind}` + if (authenticatedUser) { + appStateUrl = `${appStateUrl}&userID=${authenticatedUser.id}` + } + // We encode the name and url here so that special characters like `#` are interpreted as + // part of the URL and not the fragment. + const response = await fetch(appStateUrl) if (!response.ok) { if (response.body instanceof ReadableStream) { const error = await response.text() throw new Error(error) } } - const state = (await response.json()) as StateResponse + const jsonResponse = (await response.json()) as StateResponse let webhookURL: string | undefined - if (state.webhookUUID?.length) { - webhookURL = new URL(`/.api/webhooks/${state.webhookUUID}`, originURL).href + if (jsonResponse.webhookUUID?.length) { + webhookURL = new URL(`/.api/webhooks/${jsonResponse.webhookUUID}`, originURL).href } - if (!state.state?.length) { + if (!jsonResponse.state?.length) { throw new Error('Response from server missing state parameter') } - submitForm({ state: state.state, webhookURL, name }) + submitForm({ state: jsonResponse.state, webhookURL, name }) } catch (error_) { if (error_ instanceof Error) { setError(error_.message) @@ -170,7 +181,7 @@ export const CreateGitHubAppPage: FC = ({ setError('Unknown error occurred.') } } - }, [submitForm, name, appDomain, url, originURL]) + }, [submitForm, name, appDomain, url, originURL, appKind, authenticatedUser]) const handleNameChange = useCallback((event: React.ChangeEvent) => { setName(event.target.value) @@ -207,34 +218,38 @@ export const CreateGitHubAppPage: FC = ({ const handleOrgChange = useCallback((event: React.ChangeEvent) => setOrg(event.target.value), []) const toggleIsPublic = useCallback(() => setIsPublic(isPublic => !isPublic), []) - const cancelUrl = `/site-admin/${appDomain === GitHubAppDomain.BATCHES ? 'batch-changes' : 'github-apps'}` return ( <> - - - Register a GitHub App to better manage GitHub code host connections.{' '} - - See how GitHub App configuration works. - - - ) - } - annotation={headerAnnotation} - className="mb-3" - /> + {!minimizedMode && ( + <> + + + Register a GitHub App to better manage GitHub code host connections.{' '} + + See how GitHub App configuration works. + + + ) + } + annotation={headerAnnotation} + className="mb-3" + /> + + )} + {error && Error creating GitHub App: {error}} Provide the details for a new GitHub App with the form below. Once you click "Create GitHub App", - you will be routed to {baseURL || 'GitHub'} to create the App and choose which repositories to grant - it access to. Once created on {baseURL || 'GitHub'}, you'll be redirected back here to finish - connecting it to Sourcegraph. + you will be routed to {baseURL || 'GitHub'} to create the App and choose which + repositories to grant it access to. Once created on {baseURL || 'GitHub'}, you'll + be redirected back here to finish connecting it to Sourcegraph. -
+
- +
) diff --git a/client/web/src/enterprise/batches/settings/AddCredentialModal.module.scss b/client/web/src/enterprise/batches/settings/AddCredentialModal.module.scss index d67d41a03b5..2abc7c59ab5 100644 --- a/client/web/src/enterprise/batches/settings/AddCredentialModal.module.scss +++ b/client/web/src/enterprise/batches/settings/AddCredentialModal.module.scss @@ -1,5 +1,10 @@ .add-credential-modal { - &__modal-step-ruler { + &__container { + max-height: 40rem; + overflow-y: auto; + } + + &__step-ruler { height: 0.125rem; width: 100%; border-radius: 0.125rem; diff --git a/client/web/src/enterprise/batches/settings/AddCredentialModal.story.tsx b/client/web/src/enterprise/batches/settings/AddCredentialModal.story.tsx index a6344a5a8a7..709c9b29d7a 100644 --- a/client/web/src/enterprise/batches/settings/AddCredentialModal.story.tsx +++ b/client/web/src/enterprise/batches/settings/AddCredentialModal.story.tsx @@ -6,7 +6,7 @@ import { getDocumentNode } from '@sourcegraph/http-client' import { MockedTestProvider } from '@sourcegraph/shared/src/testing/apollo' import { WebStory } from '../../../components/WebStory' -import { ExternalServiceKind } from '../../../graphql-operations' +import { ExternalServiceKind, type UserAreaUserFields } from '../../../graphql-operations' import { AddCredentialModal } from './AddCredentialModal' import { CREATE_BATCH_CHANGES_CREDENTIAL } from './backend' @@ -24,6 +24,14 @@ const config: Meta = { }, } +const user = { + __typename: 'User', + id: '123', + username: 'alice', + avatarURL: null, + viewerCanAdminister: true, +} as UserAreaUserFields + export default config export const RequiresSSHstep1: StoryFn = args => ( @@ -54,7 +62,7 @@ export const RequiresSSHstep1: StoryFn = args => ( > ( {props => ( ( {props => ( ( {props => ( ( {props => ( ( {props => ( void afterCreate: () => void - userID: Scalars['ID'] | null + user: UserAreaUserFields | null externalServiceKind: ExternalServiceKind externalServiceURL: string requiresSSH: boolean @@ -77,10 +80,17 @@ const scopeRequirements: Record = { type Step = 'add-token' | 'get-ssh-key' -export const AddCredentialModal: React.FunctionComponent> = ({ +const AuthenticationStrategy = { + PERSONAL_ACCESS_TOKEN: 'PERSONAL_ACCESS_TOKEN', + GITHUB_APP: 'GITHUB_APP', +} as const + +type AuthenticationStrategyType = typeof AuthenticationStrategy[keyof typeof AuthenticationStrategy] + +export const AddCredentialModal: FC> = ({ onCancel, afterCreate, - userID, + user, externalServiceKind, externalServiceURL, requiresSSH, @@ -92,6 +102,10 @@ export const AddCredentialModal: React.FunctionComponent() const [username, setUsername] = useState('') const [step, setStep] = useState(initialStep) + const [authStrategy, setAuthStrategy] = useState( + AuthenticationStrategy.PERSONAL_ACCESS_TOKEN + ) + const [isGithubAppIntegrationEnabled] = useFeatureFlag('batches-github-app-integration') const onChangeCredential = useCallback>(event => { setCredential(event.target.value) @@ -110,7 +124,7 @@ export const AddCredentialModal: React.FunctionComponent -
+ +
+ {isGitHubKind && isGithubAppIntegrationEnabled && isTokenSection && ( + + )} {requiresSSH && (
@@ -164,8 +189,8 @@ export const AddCredentialModal: React.FunctionComponent
@@ -175,83 +200,32 @@ export const AddCredentialModal: React.FunctionComponent
)} {step === 'add-token' && ( - <> - {error && } -
-
- {requiresUsername && ( - <> - - - )} - - - - - Create a new {patLabel.toLocaleLowerCase()} - {' '} - {scopeRequirements[externalServiceKind]} - -
-
- - -
-
- + )} {step === 'get-ssh-key' && ( <> @@ -273,3 +247,149 @@ export const AddCredentialModal: React.FunctionComponent ) } + +const computeCredentialLabel = ( + externalServiceKind: ExternalServiceKind, + authStrategy: AuthenticationStrategyType +): string => { + if (externalServiceKind === ExternalServiceKind.PERFORCE) { + return 'Ticket' + } + + if (externalServiceKind === ExternalServiceKind.BITBUCKETCLOUD) { + return 'App password' + } + + if (externalServiceKind === ExternalServiceKind.GITHUB && authStrategy === AuthenticationStrategy.GITHUB_APP) { + return 'Create GitHub App' + } + + return 'Personal access token' +} + +interface AddTokenProps { + step: Step + error: unknown + onSubmit: React.FormEventHandler + requiresUsername: boolean + credential: string + username: string + onChangeUsername: React.ChangeEventHandler + onChangeCredential: React.ChangeEventHandler + externalServiceKind: ExternalServiceKind + requiresSSH: boolean + loading: boolean + onCancel: () => void + authStrategy: AuthenticationStrategyType + externalServiceURL: string + user: UserAreaUserFields | null +} + +const AddToken: FC = ({ + step, + error, + onSubmit, + requiresUsername, + credential, + username, + onChangeUsername, + onChangeCredential, + externalServiceKind, + requiresSSH, + loading, + onCancel, + authStrategy, + externalServiceURL, + user, +}) => { + const patLabel = computeCredentialLabel(externalServiceKind, authStrategy) + const isStrategyPAT = authStrategy === AuthenticationStrategy.PERSONAL_ACCESS_TOKEN + const kind = user ? GitHubAppKind.USER_CREDENTIAL : GitHubAppKind.SITE_CREDENTIAL + + if (step === 'add-token') { + return ( + <> + {error && } + {isStrategyPAT ? ( +
+
+ {requiresUsername && ( + <> + + + )} + + + + + Create a new {patLabel.toLocaleLowerCase()} + {' '} + {scopeRequirements[externalServiceKind]} + +
+
+ {isStrategyPAT && ( + <> + + + + )} +
+
+ ) : ( + + )} + + ) + } + + return null +} diff --git a/client/web/src/enterprise/batches/settings/BatchChangesCreateGitHubAppPage.tsx b/client/web/src/enterprise/batches/settings/BatchChangesCreateGitHubAppPage.tsx index 85f8570e7b5..bb5c875b039 100644 --- a/client/web/src/enterprise/batches/settings/BatchChangesCreateGitHubAppPage.tsx +++ b/client/web/src/enterprise/batches/settings/BatchChangesCreateGitHubAppPage.tsx @@ -1,12 +1,14 @@ -import { useCallback } from 'react' +import { useCallback, type FC } from 'react' +import { capitalize } from 'lodash' import { useLocation } from 'react-router-dom' +import type { AuthenticatedUser } from '@sourcegraph/shared/src/auth' import { noOpTelemetryRecorder } from '@sourcegraph/shared/src/telemetry' -import { FeedbackBadge, Link } from '@sourcegraph/wildcard' +import { Link, FeedbackBadge } from '@sourcegraph/wildcard' import { CreateGitHubAppPage } from '../../../components/gitHubApps/CreateGitHubAppPage' -import { GitHubAppDomain } from '../../../graphql-operations' +import { GitHubAppDomain, GitHubAppKind } from '../../../graphql-operations' import { useGlobalBatchChangesCodeHostConnection } from './backend' @@ -17,9 +19,22 @@ const DEFAULT_PERMISSIONS = { metadata: 'read', } -export const BatchChangesCreateGitHubAppPage: React.FunctionComponent = () => { +interface BatchChangesCreateGitHubAppPageProps { + authenticatedUser: AuthenticatedUser + minimizedMode?: boolean + kind: GitHubAppKind +} + +export const BatchChangesCreateGitHubAppPage: FC = ({ + minimizedMode, + kind, + authenticatedUser, +}) => { const location = useLocation() - const baseURL = new URLSearchParams(location.search).get('baseURL') + const searchParams = new URLSearchParams(location.search) + const baseURL = searchParams.get('baseURL') + + const isGitHubAppKindCredential = kind === GitHubAppKind.USER_CREDENTIAL || kind === GitHubAppKind.SITE_CREDENTIAL const { connection } = useGlobalBatchChangesCodeHostConnection() // validateURL compares a provided URL against the URLs of existing commit signing @@ -35,26 +50,48 @@ export const BatchChangesCreateGitHubAppPage: React.FunctionComponent = () => { // assume this call will succeed. const asURL = new URL(url) const isDuplicate = connection.nodes.some(node => { - const existingURL = node.commitSigningConfiguration?.baseURL + const existingURL = isGitHubAppKindCredential + ? node.externalServiceURL + : node.commitSigningConfiguration?.baseURL if (!existingURL) { return false } return new URL(existingURL).hostname === asURL.hostname }) - return isDuplicate ? 'A commit signing integration for the code host at this URL already exists.' : true + const errorMsg = `A ${ + isGitHubAppKindCredential ? 'GitHub app' : 'commit signing' + } integration for the code host at this URL already exists.` + return isDuplicate ? errorMsg : true }, - [connection] + [connection, isGitHubAppKindCredential] ) + const pageTitle = isGitHubAppKindCredential + ? `Create GitHub app for ${ + kind === GitHubAppKind.USER_CREDENTIAL ? authenticatedUser.username : 'Global' + } Batch Changes credential` + : 'Create GitHub app for commit signing' + const defaultAppName = computeAppName(kind, authenticatedUser?.username) + + // COMMIT SIGNING apps do not need permissions to create pull request, we duplicate the + // commit using the GraphQL request and the changeset is created with the PAT. + const permissions = { + ...DEFAULT_PERMISSIONS, + ...(isGitHubAppKindCredential ? { pull_requests: 'write' } : {}), + } return ( - Register a GitHub App to enable Sourcegraph to sign commits for Batch Change changesets on your - behalf. + Register a GitHub App to enable Sourcegraph{' '} + {isGitHubAppKindCredential ? 'create' : 'sign commits for'} Batch Change changesets on your behalf. + {/* TODO (@BolajiOlajide/@bahrmichael) update link here for credential github app */} See how GitHub App configuration works. @@ -62,10 +99,26 @@ export const BatchChangesCreateGitHubAppPage: React.FunctionComponent = () => { } headerAnnotation={} appDomain={GitHubAppDomain.BATCHES} - defaultAppName="Sourcegraph Commit Signing" + defaultAppName={defaultAppName} baseURL={baseURL?.length ? baseURL : undefined} validateURL={validateURL} telemetryRecorder={noOpTelemetryRecorder} /> ) } + +const computeAppName = (kind: GitHubAppKind, username?: string): string => { + switch (kind) { + case GitHubAppKind.COMMIT_SIGNING: { + return 'Sourcegraph Commit Signing' + } + + case GitHubAppKind.USER_CREDENTIAL: { + return `${capitalize(username)}'s Batch Changes GitHub App` + } + + default: { + return 'Batch Changes GitHub App' + } + } +} diff --git a/client/web/src/enterprise/batches/settings/BatchChangesSettingsArea.story.tsx b/client/web/src/enterprise/batches/settings/BatchChangesSettingsArea.story.tsx index f5ee26c14ed..880ea76bd96 100644 --- a/client/web/src/enterprise/batches/settings/BatchChangesSettingsArea.story.tsx +++ b/client/web/src/enterprise/batches/settings/BatchChangesSettingsArea.story.tsx @@ -9,6 +9,7 @@ import { type BatchChangesCredentialFields, ExternalServiceKind, type UserBatchChangesCodeHostsResult, + type UserAreaUserFields, } from '../../../graphql-operations' import { BATCH_CHANGES_SITE_CONFIGURATION } from '../backend' import { noRolloutWindowMockResult, rolloutWindowConfigMockResult } from '../mocks' @@ -133,7 +134,7 @@ export const Overview: StoryFn = () => ( }, ]} > - + )} @@ -214,7 +215,7 @@ export const ConfigAdded: StoryFn = () => ( }, ]} > - + )} @@ -297,7 +298,7 @@ export const RolloutWindowsConfigurationStory: StoryFn = () => ( }, ]} > - + )} diff --git a/client/web/src/enterprise/batches/settings/BatchChangesSettingsArea.tsx b/client/web/src/enterprise/batches/settings/BatchChangesSettingsArea.tsx index c06f88b3d82..c5c6aa35691 100644 --- a/client/web/src/enterprise/batches/settings/BatchChangesSettingsArea.tsx +++ b/client/web/src/enterprise/batches/settings/BatchChangesSettingsArea.tsx @@ -10,7 +10,7 @@ import { UserCommitSigningIntegrations } from './CommitSigningIntegrations' import { RolloutWindowsConfiguration } from './RolloutWindowsConfiguration' export interface BatchChangesSettingsAreaProps { - user: Pick + user: UserAreaUserFields } /** The page area for all batch changes settings. It's shown in the user settings sidebar. */ @@ -23,7 +23,7 @@ export const BatchChangesSettingsArea: React.FunctionComponent< Add access tokens to enable Batch Changes changeset creation on your code hosts.} - userID={props.user.id} + user={props.user} />
diff --git a/client/web/src/enterprise/batches/settings/BatchChangesSiteConfigSettingsPage.tsx b/client/web/src/enterprise/batches/settings/BatchChangesSiteConfigSettingsPage.tsx index aad21d33cda..9cf32b0ec4a 100644 --- a/client/web/src/enterprise/batches/settings/BatchChangesSiteConfigSettingsPage.tsx +++ b/client/web/src/enterprise/batches/settings/BatchChangesSiteConfigSettingsPage.tsx @@ -1,6 +1,6 @@ import React, { useEffect } from 'react' -import { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry' +import type { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry' import { PageHeader, Alert, Text } from '@sourcegraph/wildcard' import { PageTitle } from '../../../components/PageTitle' diff --git a/client/web/src/enterprise/batches/settings/CodeHostConnectionNode.story.tsx b/client/web/src/enterprise/batches/settings/CodeHostConnectionNode.story.tsx index eea8e4bdf46..507a0fa4b93 100644 --- a/client/web/src/enterprise/batches/settings/CodeHostConnectionNode.story.tsx +++ b/client/web/src/enterprise/batches/settings/CodeHostConnectionNode.story.tsx @@ -8,6 +8,7 @@ import { type BatchChangesCredentialFields, type CheckBatchChangesCredentialResult, ExternalServiceKind, + type UserAreaUserFields, } from '../../../graphql-operations' import { CHECK_BATCH_CHANGES_CREDENTIAL } from './backend' @@ -67,7 +68,7 @@ export const Overview: StoryFn = () => ( commitSigningConfiguration: null, }} refetchAll={() => {}} - userID="123" + user={{ id: '123' } as UserAreaUserFields} /> )} diff --git a/client/web/src/enterprise/batches/settings/CodeHostConnectionNode.tsx b/client/web/src/enterprise/batches/settings/CodeHostConnectionNode.tsx index 57cb60be904..8455dccbb19 100644 --- a/client/web/src/enterprise/batches/settings/CodeHostConnectionNode.tsx +++ b/client/web/src/enterprise/batches/settings/CodeHostConnectionNode.tsx @@ -13,7 +13,7 @@ import type { BatchChangesCodeHostFields, CheckBatchChangesCredentialResult, CheckBatchChangesCredentialVariables, - Scalars, + UserAreaUserFields, } from '../../../graphql-operations' import { AddCredentialModal } from './AddCredentialModal' @@ -27,7 +27,7 @@ import styles from './CodeHostConnectionNode.module.scss' export interface CodeHostConnectionNodeProps { node: BatchChangesCodeHostFields refetchAll: () => void - userID: Scalars['ID'] | null + user: UserAreaUserFields | null } type OpenModal = 'add' | 'view' | 'delete' @@ -35,7 +35,7 @@ type OpenModal = 'add' | 'view' | 'delete' export const CodeHostConnectionNode: React.FunctionComponent> = ({ node, refetchAll, - userID, + user, }) => { const [checkCredError, setCheckCredError] = useState() const ExternalServiceIcon = defaultExternalServices[node.externalServiceKind].icon @@ -75,7 +75,7 @@ export const CodeHostConnectionNode: React.FunctionComponent -> = props => ( - -) +> = props => export interface UserCodeHostConnectionsProps extends GlobalCodeHostConnectionsProps { - userID: Scalars['ID'] + user: UserAreaUserFields } export const UserCodeHostConnections: React.FunctionComponent< React.PropsWithChildren -> = props => +> = ({ user, headerLine }) => ( + +) interface CodeHostConnectionsProps extends GlobalCodeHostConnectionsProps { - userID: Scalars['ID'] | null + user: UserAreaUserFields | null connectionResult: UseShowMorePaginationResult< GlobalBatchChangesCodeHostsResult | UserBatchChangesCodeHostsResult, BatchChangesCodeHostFields @@ -49,11 +58,17 @@ interface CodeHostConnectionsProps extends GlobalCodeHostConnectionsProps { } const CodeHostConnections: React.FunctionComponent> = ({ - userID, + user, headerLine, connectionResult, }) => { const { loading, hasNextPage, fetchMore, connection, error, refetchAll } = connectionResult + const location = useLocation() + const kind = new URLSearchParams(location.search).get('kind') + const success = new URLSearchParams(location.search).get('success') === 'true' + const appName = new URLSearchParams(location.search).get('app_name') + const setupError = new URLSearchParams(location.search).get('error') + const shouldShowError = !success && setupError && kind !== GitHubAppKind.COMMIT_SIGNING return (

Code host tokens

@@ -61,13 +76,23 @@ const CodeHostConnections: React.FunctionComponent {error && } {loading && !connection && } + {success && ( + + GitHub App {appName?.length ? `"${appName}" ` : ''}successfully connected. + + )} + {shouldShowError && } {connection?.nodes?.map(node => ( ))} diff --git a/client/web/src/featureFlags/featureFlags.ts b/client/web/src/featureFlags/featureFlags.ts index 166135d5835..bd1a77e39f7 100644 --- a/client/web/src/featureFlags/featureFlags.ts +++ b/client/web/src/featureFlags/featureFlags.ts @@ -29,6 +29,7 @@ export const FEATURE_FLAGS = [ 'sourcegraph-operator-site-admin-hide-maintenance', 'sourcegraph-cloud-managed-feature-flags-warning-shown', 'ab-shortened-install-first-signup-flow-cody-2024-04', + 'batches-github-app-integration', ] as const export type FeatureFlagName = typeof FEATURE_FLAGS[number] diff --git a/client/web/src/site-admin/SiteAdminGitHubAppsArea.tsx b/client/web/src/site-admin/SiteAdminGitHubAppsArea.tsx index 1abe40dcb64..83678c4611d 100644 --- a/client/web/src/site-admin/SiteAdminGitHubAppsArea.tsx +++ b/client/web/src/site-admin/SiteAdminGitHubAppsArea.tsx @@ -11,6 +11,7 @@ import { LoadingSpinner, ErrorAlert } from '@sourcegraph/wildcard' import { GitHubAppDomain, + GitHubAppKind, type SiteExternalServiceConfigResult, type SiteExternalServiceConfigVariables, } from '../graphql-operations' @@ -82,6 +83,7 @@ export const SiteAdminGitHubAppsArea: FC = props => { path="new" element={ , + render: ({ authenticatedUser }) => ( + + ), condition: ({ batchChangesEnabled }) => batchChangesEnabled, }, { diff --git a/cmd/frontend/graphqlbackend/githubapps.graphql b/cmd/frontend/graphqlbackend/githubapps.graphql index 9cacf34f80b..1d73ad13588 100644 --- a/cmd/frontend/graphqlbackend/githubapps.graphql +++ b/cmd/frontend/graphqlbackend/githubapps.graphql @@ -41,6 +41,28 @@ enum GitHubAppDomain { BATCHES } +""" +GitHubAppKind enumerates the domains in which GitHub Apps can be used. +""" +enum GitHubAppKind { + """ + GitHub Apps that are configured for commit signing. + """ + COMMIT_SIGNING + """ + GitHub Apps that are configured for a user's batch changes credential. + """ + USER_CREDENTIAL + """ + GitHub Apps that are configured for repo syncing. + """ + REPO_SYNC + """ + GitHub Apps that are configured for a site's batch changes credential. + """ + SITE_CREDENTIAL +} + """ A list of GitHub Apps. """