diff --git a/client/web/BUILD.bazel b/client/web/BUILD.bazel index e535e4914c88..0ffba7255c5e 100644 --- a/client/web/BUILD.bazel +++ b/client/web/BUILD.bazel @@ -1712,6 +1712,7 @@ ts_project( "src/user/settings/RedirectToUserPage.tsx", "src/user/settings/RedirectToUserSettings.tsx", "src/user/settings/ScimAlert.tsx", + "src/user/settings/UserGitHubAppsArea.tsx", "src/user/settings/UserSettingsArea.tsx", "src/user/settings/UserSettingsSidebar.tsx", "src/user/settings/accessTokens/UserSettingsCreateAccessTokenCallbackPage.tsx", diff --git a/client/web/src/components/gitHubApps/GitHubAppPage.tsx b/client/web/src/components/gitHubApps/GitHubAppPage.tsx index b27fc7a4ee67..4cff985d037d 100644 --- a/client/web/src/components/gitHubApps/GitHubAppPage.tsx +++ b/client/web/src/components/gitHubApps/GitHubAppPage.tsx @@ -27,7 +27,12 @@ import { // eslint-disable-next-line no-restricted-imports import type { BreadcrumbItem } from '@sourcegraph/wildcard/src/components/PageHeader' -import { GitHubAppDomain, type GitHubAppByIDResult, type GitHubAppByIDVariables } from '../../graphql-operations' +import { + GitHubAppDomain, + type GitHubAppByIDResult, + type GitHubAppByIDVariables, + GitHubAppKind, +} from '../../graphql-operations' import { ExternalServiceNode } from '../externalServices/ExternalServiceNode' import { ConnectionList, ConnectionSummary, SummaryContainer } from '../FilteredConnection/ui' import { PageTitle } from '../PageTitle' @@ -44,6 +49,7 @@ interface Props extends TelemetryProps, TelemetryV2Props { * The parent breadcrumb item to show for this page in the header. */ headerParentBreadcrumb: BreadcrumbItem + userOwned: boolean /** An optional annotation to show in the page header. */ headerAnnotation?: React.ReactNode } @@ -53,15 +59,16 @@ export const GitHubAppPage: FC = ({ telemetryRecorder, headerParentBreadcrumb, headerAnnotation, + userOwned, }) => { const { appID } = useParams() const navigate = useNavigate() const [removeModalOpen, setRemoveModalOpen] = useState(false) useEffect(() => { - telemetryService.logPageView('SiteAdminGitHubApp') - telemetryRecorder.recordEvent('admin.GitHubApp', 'view') - }, [telemetryService, telemetryRecorder]) + telemetryService.logPageView(userOwned ? 'UserGitHubApp' : 'SiteAdminGitHubApp') + telemetryRecorder.recordEvent(userOwned ? 'user.GitHubApp' : 'admin.GitHubApp', 'view') + }, [telemetryService, telemetryRecorder, userOwned]) const [fetchError, setError] = useState() const { data, loading, error } = useQuery(GITHUB_APP_BY_ID_QUERY, { @@ -81,7 +88,9 @@ export const GitHubAppPage: FC = ({ const onAddInstallation = async (app: NonNullable): Promise => { try { - const req = await fetch(`/githubapp/state?id=${app?.id}&domain=${app?.domain}`) + const req = await fetch( + `/githubapp/state?id=${app?.id}&domain=${app?.domain}&kind=${GitHubAppKind.USER_CREDENTIAL}` + ) const state = await req.text() const trailingSlash = app.appURL.endsWith('/') ? '' : '/' window.location.assign(`${app.appURL}${trailingSlash}installations/new?state=${state}`) @@ -100,7 +109,7 @@ export const GitHubAppPage: FC = ({ {removeModalOpen && ( setRemoveModalOpen(false)} - afterDelete={() => navigate('/site-admin/github-apps')} + afterDelete={() => navigate(`/${userOwned ? 'user/settings' : 'site-admin'}/github-apps`)} app={app} /> )} diff --git a/client/web/src/components/gitHubApps/GitHubAppsPage.tsx b/client/web/src/components/gitHubApps/GitHubAppsPage.tsx index bbae037ca5de..6bf87ae68c0c 100644 --- a/client/web/src/components/gitHubApps/GitHubAppsPage.tsx +++ b/client/web/src/components/gitHubApps/GitHubAppsPage.tsx @@ -27,20 +27,21 @@ import styles from './GitHubAppsPage.module.scss' interface Props extends TelemetryV2Props { batchChangesEnabled: boolean + userOwned: boolean } -export const GitHubAppsPage: React.FC = ({ batchChangesEnabled, telemetryRecorder }) => { +export const GitHubAppsPage: React.FC = ({ batchChangesEnabled, telemetryRecorder, userOwned }) => { const { data, loading, error, refetch } = useQuery(GITHUB_APPS_QUERY, { variables: { - domain: GitHubAppDomain.REPOS, + domain: userOwned ? GitHubAppDomain.BATCHES : GitHubAppDomain.REPOS, }, }) const gitHubApps = useMemo(() => data?.gitHubApps?.nodes ?? [], [data]) useEffect(() => { - EVENT_LOGGER.logPageView('SiteAdminGitHubApps') - telemetryRecorder.recordEvent('admin.GitHubApps', 'view') - }, [telemetryRecorder]) + EVENT_LOGGER.logPageView(userOwned ? 'UserGitHubApps' : 'SiteAdminGitHubApps') + telemetryRecorder.recordEvent(userOwned ? 'user.GitHubApps' : 'admin.GitHubApps', 'view') + }, [telemetryRecorder, userOwned]) const location = useLocation() const success = new URLSearchParams(location.search).get('success') === 'true' @@ -63,11 +64,26 @@ export const GitHubAppsPage: React.FC = ({ batchChangesEnabled, telemetry className={classNames(styles.pageHeader, 'mb-3')} description={ <> - Create and connect a GitHub App to better manage GitHub code host connections.{' '} - - See how GitHub App configuration works. - - {batchChangesEnabled && ( + {userOwned ? ( + batchChangesEnabled ? ( + <>Use personal GitHub Apps to act on your behalf when running Batch Changes. + ) : ( + <> + Personal GitHub Apps are currently only used for Batch Changes, but this feature is + not enabled on your instance. + + ) + ) : ( + <> + Create and connect a GitHub App to better manage GitHub code host connections.{' '} + + See how GitHub App configuration works. + + + )} + {batchChangesEnabled && userOwned ? ( + <> To create a GitHub App to sign Batch Changes commits, ask your site admin. + ) : ( <> {' '} To create a GitHub App to sign Batch Changes commits, visit{' '} @@ -77,14 +93,18 @@ export const GitHubAppsPage: React.FC = ({ batchChangesEnabled, telemetry } actions={ - - Create GitHub App - + userOwned ? ( + <> + ) : ( + + Create GitHub App + + ) } /> diff --git a/client/web/src/site-admin/SiteAdminGitHubAppsArea.tsx b/client/web/src/site-admin/SiteAdminGitHubAppsArea.tsx index 83678c4611d3..928217f3362b 100644 --- a/client/web/src/site-admin/SiteAdminGitHubAppsArea.tsx +++ b/client/web/src/site-admin/SiteAdminGitHubAppsArea.tsx @@ -75,6 +75,7 @@ export const SiteAdminGitHubAppsArea: FC = props => { } /> @@ -98,6 +99,7 @@ export const SiteAdminGitHubAppsArea: FC = props => { } diff --git a/client/web/src/site-admin/routes.tsx b/client/web/src/site-admin/routes.tsx index 304b8a5b0e8e..88e41ee117ee 100644 --- a/client/web/src/site-admin/routes.tsx +++ b/client/web/src/site-admin/routes.tsx @@ -425,6 +425,7 @@ export const otherSiteAdminRoutes: readonly SiteAdminAreaRoute[] = [ headerAnnotation={} telemetryService={props.telemetryService} telemetryRecorder={props.platformContext.telemetryRecorder} + userOwned={false} /> ), condition: ({ batchChangesEnabled }) => batchChangesEnabled, diff --git a/client/web/src/user/settings/UserGitHubAppsArea.tsx b/client/web/src/user/settings/UserGitHubAppsArea.tsx new file mode 100644 index 000000000000..3a04c982652b --- /dev/null +++ b/client/web/src/user/settings/UserGitHubAppsArea.tsx @@ -0,0 +1,67 @@ +import type { FC } from 'react' + +import { Route, Routes } from 'react-router-dom' + +import { useQuery } from '@sourcegraph/http-client' +import type { AuthenticatedUser } from '@sourcegraph/shared/src/auth' +import type { PlatformContextProps } from '@sourcegraph/shared/src/platform/context' +import type { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService' +import { lazyComponent } from '@sourcegraph/shared/src/util/lazyComponent' +import { ErrorAlert, LoadingSpinner } from '@sourcegraph/wildcard' + +import { type SiteExternalServiceConfigResult, type SiteExternalServiceConfigVariables } from '../../graphql-operations' +import { SITE_EXTERNAL_SERVICE_CONFIG } from '../../site-admin/backend' + +const GitHubAppPage = lazyComponent(() => import('../../components/gitHubApps/GitHubAppPage'), 'GitHubAppPage') +const GitHubAppsPage = lazyComponent(() => import('../../components/gitHubApps/GitHubAppsPage'), 'GitHubAppsPage') + +interface Props extends TelemetryProps, PlatformContextProps { + authenticatedUser: AuthenticatedUser + batchChangesEnabled: boolean +} + +export const UserGitHubAppsArea: FC = props => { + const { data, error, loading } = useQuery( + SITE_EXTERNAL_SERVICE_CONFIG, + {} + ) + + if (error && !loading) { + return + } + + if (loading && !error) { + return + } + + if (!data) { + return null + } + + return ( + + + } + /> + + + } + /> + + ) +} diff --git a/client/web/src/user/settings/routes.tsx b/client/web/src/user/settings/routes.tsx index 37b1c8d49c4f..660d854ef861 100644 --- a/client/web/src/user/settings/routes.tsx +++ b/client/web/src/user/settings/routes.tsx @@ -107,6 +107,12 @@ export const userSettingsAreaRoutes: readonly UserSettingsAreaRoute[] = [ ), condition: shouldRenderBatchChangesPage, }, + { + path: 'github-apps/*', + render: lazyComponent(() => import('./UserGitHubAppsArea'), 'UserGitHubAppsArea'), + // GitHub Apps are currently only relevant for users who use them with batch changes. If they are used for other things too, you can remove this condition. + condition: shouldRenderBatchChangesPage, + }, ] interface UserSettingAreaIndexPageProps extends PlatformContextProps, SettingsCascadeProps, TelemetryProps { diff --git a/client/web/src/user/settings/sidebaritems.ts b/client/web/src/user/settings/sidebaritems.ts index a2c2629ba466..f588a865b255 100644 --- a/client/web/src/user/settings/sidebaritems.ts +++ b/client/web/src/user/settings/sidebaritems.ts @@ -61,4 +61,9 @@ export const userSettingsSideBarItems: UserSettingsSidebarItems = [ label: 'Event log', condition: ({ user: { viewerCanAdminister } }) => viewerCanAdminister, }, + { + to: '/github-apps', + label: 'GitHub Apps', + condition: ({ user: { viewerCanAdminister } }) => viewerCanAdminister, + }, ] diff --git a/cmd/frontend/internal/githubapp/httpapi.go b/cmd/frontend/internal/githubapp/httpapi.go index c7196a7febe7..234892a634c6 100644 --- a/cmd/frontend/internal/githubapp/httpapi.go +++ b/cmd/frontend/internal/githubapp/httpapi.go @@ -136,6 +136,7 @@ func (srv *gitHubAppServer) stateHandler(w http.ResponseWriter, r *http.Request) gqlID := r.URL.Query().Get("id") domain := r.URL.Query().Get("domain") baseURL := r.URL.Query().Get("baseURL") + kind := r.URL.Query().Get("kind") if gqlID == "" { // we marshal an empty `gitHubAppStateDetails` struct because we want the values saved in the cache // to always conform to the same structure. @@ -159,6 +160,7 @@ func (srv *gitHubAppServer) stateHandler(w http.ResponseWriter, r *http.Request) AppID: int(id64), Domain: domain, BaseURL: baseURL, + Kind: kind, }) if err != nil { http.Error(w, fmt.Sprintf("Unexpected error when marshalling state: %s", err.Error()), http.StatusInternalServerError)