From 6e4add9a59e871697d3242b581ee05813535d304 Mon Sep 17 00:00:00 2001 From: Jussi Hallila Date: Thu, 9 Jan 2025 12:52:16 +0100 Subject: [PATCH] Update '@roadiehq/backstage-plugin-github-pull-requests' and '@roadiehq/backstage-plugin-github-insights' to use SCM auth instead of GitHub auth to allow multiple integrations to work at the same time. --- .changeset/breezy-days-refuse.md | 8 ++ packages/app/src/apis.ts | 47 +++++++- .../app/src/components/catalog/EntityPage.tsx | 8 +- packages/backend/src/plugins/auth.ts | 4 + .../package.json | 5 +- .../src/apis/GithubClient.ts | 41 ++++++- .../InsightsPage/InsightsPage.test.tsx | 30 ++--- .../ComplianceCard/ComplianceCard.test.tsx | 15 +-- .../ContributorsCard.test.tsx | 21 ++-- .../EnvironmentsCard.test.tsx | 11 +- .../LanguagesCard/LanguagesCard.test.tsx | 21 ++-- .../MarkdownContent/MarkdownContent.test.tsx | 41 ++----- .../MarkdownContent/MarkdownContent.tsx | 60 +++++++--- .../Widgets/ReadMeCard/ReadmeCard.test.tsx | 19 ++-- .../ReleasesCard/ReleasesCard.test.tsx | 19 ++-- .../src/hooks/useComplianceHooks.ts | 33 ++++-- .../src/hooks/useContributor.ts | 19 +++- .../src/hooks/useGithubLoggedIn.tsx | 53 +++++---- .../src/hooks/useRequest.ts | 17 ++- .../src/plugin.ts | 10 +- .../package.json | 9 +- .../src/api/GithubPullRequestsApi.ts | 31 ++--- .../src/api/GithubPullRequestsClient.ts | 106 ++++++++++++++---- .../GroupPullRequestsCard/Content.test.tsx | 49 ++++++-- .../GroupPullRequestsCard/Content.tsx | 9 +- .../RequestedReviewsCard/Content.test.tsx | 49 ++++++-- .../YourOpenPullRequestsCard/Content.test.tsx | 50 +++++++-- .../PullRequestListView.test.tsx | 51 ++++++--- .../PullRequestsListView.tsx | 2 +- .../PullRequestsPage/PullRequestsPage.tsx | 2 +- .../PullRequestsStatsCard.test.tsx | 46 ++++---- .../PullRequestsStatsCard.tsx | 10 +- .../PullRequestsTable.test.tsx | 40 ++++--- .../PullRequestsTable/PullRequestsTable.tsx | 22 ++-- .../src/components/useGithubLoggedIn.tsx | 56 +++++---- .../components/useGithubRepositoryData.tsx | 32 ++++-- .../components/useGithubSearchPullRequest.tsx | 54 ++------- .../src/components/usePullRequests.ts | 17 ++- .../components/usePullRequestsStatistics.ts | 24 ++-- .../src/mocks/mocks.ts | 4 +- .../scmIntegrationsApiMock.ts} | 24 ++-- .../src/plugin.ts | 10 +- .../src/utils/githubUtils.ts | 16 +++ yarn.lock | 61 +++++++++- 44 files changed, 819 insertions(+), 437 deletions(-) create mode 100644 .changeset/breezy-days-refuse.md rename plugins/frontend/backstage-plugin-github-pull-requests/src/{components/useBaseUrl.ts => mocks/scmIntegrationsApiMock.ts} (62%) create mode 100644 plugins/frontend/backstage-plugin-github-pull-requests/src/utils/githubUtils.ts diff --git a/.changeset/breezy-days-refuse.md b/.changeset/breezy-days-refuse.md new file mode 100644 index 000000000..5c6650ac6 --- /dev/null +++ b/.changeset/breezy-days-refuse.md @@ -0,0 +1,8 @@ +--- +'@roadiehq/backstage-plugin-github-pull-requests': major +'@roadiehq/backstage-plugin-github-insights': major +--- + +BREAKING: Needs SCM auth API to be configured in the application. + +Migrate to use SCM auth instead of direct GitHub to allow possibility to work with multiple GitHub integrations at once. diff --git a/packages/app/src/apis.ts b/packages/app/src/apis.ts index b9c6dd2ca..64bc1b7ea 100644 --- a/packages/app/src/apis.ts +++ b/packages/app/src/apis.ts @@ -18,14 +18,29 @@ import { ScmIntegrationsApi, scmIntegrationsApiRef, ScmAuth, + scmAuthApiRef, } from '@backstage/integration-react'; import { AnyApiFactory, + ApiRef, configApiRef, createApiFactory, + createApiRef, + discoveryApiRef, fetchApiRef, + githubAuthApiRef, + OAuthApi, + oauthRequestApiRef, + ProfileInfoApi, + SessionApi, } from '@backstage/core-plugin-api'; import fetch from 'cross-fetch'; +import { GithubAuth } from '@backstage/core-app-api'; + +const ghesAuthApiRef: ApiRef = + createApiRef({ + id: 'internal.auth.ghe', + }); export const apis: AnyApiFactory[] = [ createApiFactory({ @@ -33,7 +48,37 @@ export const apis: AnyApiFactory[] = [ deps: { configApi: configApiRef }, factory: ({ configApi }) => ScmIntegrationsApi.fromConfig(configApi), }), - ScmAuth.createDefaultApiFactory(), + createApiFactory({ + api: ghesAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + GithubAuth.create({ + configApi, + discoveryApi, + oauthRequestApi, + provider: { id: 'ghes', title: 'GitHub Enterprise', icon: () => null }, + defaultScopes: ['read:user'], + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: scmAuthApiRef, + deps: { + gheAuthApi: ghesAuthApiRef, + githubAuthApi: githubAuthApiRef, + }, + factory: ({ githubAuthApi, gheAuthApi }) => + ScmAuth.merge( + ScmAuth.forGithub(githubAuthApi), + ScmAuth.forGithub(gheAuthApi, { + host: 'ghes.enginehouse.io', + }), + ), + }), createApiFactory({ api: fetchApiRef, deps: {}, diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index 6fcda3f20..184fabf40 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -54,6 +54,7 @@ import { EntityTechdocsContent } from '@backstage/plugin-techdocs'; import { EntityGithubPullRequestsContent, EntityGithubPullRequestsOverviewCard, + EntityGithubPullRequestsTable, } from '@roadiehq/backstage-plugin-github-pull-requests'; import { EntityBitbucketPullRequestsContent } from '@roadiehq/backstage-plugin-bitbucket-pullrequest'; import { @@ -168,7 +169,6 @@ const overviewContent = ( - @@ -176,7 +176,6 @@ const overviewContent = ( - @@ -184,7 +183,6 @@ const overviewContent = ( - @@ -192,7 +190,6 @@ const overviewContent = ( - @@ -305,6 +302,9 @@ const overviewContent = ( + + + ); diff --git a/packages/backend/src/plugins/auth.ts b/packages/backend/src/plugins/auth.ts index 03851610f..0e1328eeb 100644 --- a/packages/backend/src/plugins/auth.ts +++ b/packages/backend/src/plugins/auth.ts @@ -16,6 +16,7 @@ import { createRouter } from '@backstage/plugin-auth-backend'; import { Router } from 'express'; import { PluginEnvironment } from '../types'; +import { providers } from '@backstage/plugin-auth-backend'; export default async function createPlugin({ logger, @@ -30,5 +31,8 @@ export default async function createPlugin({ database, discovery, tokenManager, + providerFactories: { + ghes: providers.github.create(), + }, }); } diff --git a/plugins/frontend/backstage-plugin-github-insights/package.json b/plugins/frontend/backstage-plugin-github-insights/package.json index cfed20934..e36dd11e7 100644 --- a/plugins/frontend/backstage-plugin-github-insights/package.json +++ b/plugins/frontend/backstage-plugin-github-insights/package.json @@ -48,6 +48,7 @@ "@backstage/catalog-model": "^1.7.1", "@backstage/core-components": "^0.16.1", "@backstage/core-plugin-api": "^1.10.1", + "@backstage/integration": "^1.16.0", "@backstage/integration-react": "^1.2.1", "@backstage/plugin-catalog-react": "^1.14.2", "@backstage/theme": "^0.6.2", @@ -60,8 +61,8 @@ "git-url-parse": "^14.0.0", "history": "^5.0.0", "immer": "9.0.7", - "zustand": "3.6.9", - "react-use": "^17.2.4" + "react-use": "^17.2.4", + "zustand": "3.6.9" }, "peerDependencies": { "react": "^16.13.1 || ^17.0.0 || ^18.0.0", diff --git a/plugins/frontend/backstage-plugin-github-insights/src/apis/GithubClient.ts b/plugins/frontend/backstage-plugin-github-insights/src/apis/GithubClient.ts index 4736996cb..4e8887f3e 100644 --- a/plugins/frontend/backstage-plugin-github-insights/src/apis/GithubClient.ts +++ b/plugins/frontend/backstage-plugin-github-insights/src/apis/GithubClient.ts @@ -15,10 +15,12 @@ */ import { GithubApi } from './GithubApi'; -import { OAuthApi } from '@backstage/core-plugin-api'; +import { ConfigApi } from '@backstage/core-plugin-api'; import { Octokit } from '@octokit/rest'; import parseGitUrl from 'git-url-parse'; import { MarkdownContentProps } from '../components/Widgets/MarkdownContent/types'; +import { readGithubIntegrationConfigs } from '@backstage/integration'; +import { ScmAuthApi } from '@backstage/integration-react'; const mimeTypeMap: Record = { svg: 'image/svg+xml', @@ -79,10 +81,29 @@ const combinePaths = (readmePath: string, relativePath: string): string => { }; export class GithubClient implements GithubApi { - private githubAuthApi: OAuthApi; + private readonly configApi: ConfigApi; + private readonly scmAuthApi: ScmAuthApi; - constructor(deps: { githubAuthApi: OAuthApi }) { - this.githubAuthApi = deps.githubAuthApi; + constructor(options: { configApi: ConfigApi; scmAuthApi: ScmAuthApi }) { + this.configApi = options.configApi; + this.scmAuthApi = options.scmAuthApi; + } + + private async getOctokit(hostname: string = 'github.com'): Promise { + const { token } = await this.scmAuthApi.getCredentials({ + url: `https://${hostname}/`, + additionalScope: { + customScopes: { + github: ['repo'], + }, + }, + }); + const configs = readGithubIntegrationConfigs( + this.configApi.getOptionalConfigArray('integrations.github') ?? [], + ); + const githubIntegrationConfig = configs.find(v => v.host === hostname); + const baseUrl = githubIntegrationConfig?.apiBaseUrl; + return new Octokit({ auth: token, baseUrl }); } async getContent(props: MarkdownContentProps): Promise<{ @@ -97,8 +118,16 @@ export class GithubClient implements GithubApi { branch, baseUrl = defaultBaseUrl, } = props; - const token = await this.githubAuthApi.getAccessToken(); - const octokit = new Octokit({ auth: token, baseUrl }); + + let hostname = baseUrl ?? 'github.com'; + try { + const u = new URL(hostname); + hostname = `${u.protocol}//${u.host}`; + } catch (e) { + // ignored + } + + const octokit = await this.getOctokit(hostname); let query = 'readme'; if (customReadmePath) { diff --git a/plugins/frontend/backstage-plugin-github-insights/src/components/InsightsPage/InsightsPage.test.tsx b/plugins/frontend/backstage-plugin-github-insights/src/components/InsightsPage/InsightsPage.test.tsx index a6dc5c08f..6f9848c5f 100644 --- a/plugins/frontend/backstage-plugin-github-insights/src/components/InsightsPage/InsightsPage.test.tsx +++ b/plugins/frontend/backstage-plugin-github-insights/src/components/InsightsPage/InsightsPage.test.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2021 Larder Software Limited + * Copyright 2025 Larder Software Limited * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,34 +19,24 @@ import { render } from '@testing-library/react'; import { InsightsPage } from './InsightsPage'; import { ThemeProvider } from '@material-ui/core'; import { lightTheme } from '@backstage/theme'; -import { wrapInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { TestApiProvider, wrapInTestApp } from '@backstage/test-utils'; import { EntityProvider } from '@backstage/plugin-catalog-react'; -import { AnyApiRef, githubAuthApiRef } from '@backstage/core-plugin-api'; +import { AnyApiRef } from '@backstage/core-plugin-api'; +import { ConfigReader } from '@backstage/core-app-api'; import { - GithubAuth, - OAuthRequestManager, - UrlPatternDiscovery, - ConfigReader, -} from '@backstage/core-app-api'; -import { - scmIntegrationsApiRef, + scmAuthApiRef, ScmIntegrationsApi, + scmIntegrationsApiRef, } from '@backstage/integration-react'; import { defaultIntegrationsConfig } from '../../mocks/scmIntegrationsApiMock'; -const oauthRequestApi = new OAuthRequestManager(); +const mockScmAuth = { + getCredentials: async () => ({ token: 'test-token' }), +}; const apis: [AnyApiRef, Partial][] = [ - [ - githubAuthApiRef, - GithubAuth.create({ - discoveryApi: UrlPatternDiscovery.compile( - 'http://example.com/{{pluginId}}', - ), - oauthRequestApi, - }), - ], + [scmAuthApiRef, mockScmAuth], [ scmIntegrationsApiRef, ScmIntegrationsApi.fromConfig( diff --git a/plugins/frontend/backstage-plugin-github-insights/src/components/Widgets/ComplianceCard/ComplianceCard.test.tsx b/plugins/frontend/backstage-plugin-github-insights/src/components/Widgets/ComplianceCard/ComplianceCard.test.tsx index d8c3b55ca..8a6b433fc 100644 --- a/plugins/frontend/backstage-plugin-github-insights/src/components/Widgets/ComplianceCard/ComplianceCard.test.tsx +++ b/plugins/frontend/backstage-plugin-github-insights/src/components/Widgets/ComplianceCard/ComplianceCard.test.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2021 Larder Software Limited + * Copyright 2025 Larder Software Limited * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,13 +16,13 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; -import { AnyApiRef, githubAuthApiRef } from '@backstage/core-plugin-api'; +import { AnyApiRef } from '@backstage/core-plugin-api'; import { ConfigReader } from '@backstage/core-app-api'; import { rest } from 'msw'; import { setupRequestMockHandlers, - wrapInTestApp, TestApiProvider, + wrapInTestApp, } from '@backstage/test-utils'; import { setupServer } from 'msw/node'; import { @@ -35,17 +35,18 @@ import { lightTheme } from '@backstage/theme'; import ComplianceCard from '.'; import { EntityProvider } from '@backstage/plugin-catalog-react'; import { - scmIntegrationsApiRef, + scmAuthApiRef, ScmIntegrationsApi, + scmIntegrationsApiRef, } from '@backstage/integration-react'; import { defaultIntegrationsConfig } from '../../../mocks/scmIntegrationsApiMock'; -const mockGithubAuth = { - getAccessToken: async (_: string[]) => 'test-token', +const mockScmAuth = { + getCredentials: async () => ({ token: 'test-token' }), }; const apis: [AnyApiRef, Partial][] = [ - [githubAuthApiRef, mockGithubAuth], + [scmAuthApiRef, mockScmAuth], [ scmIntegrationsApiRef, ScmIntegrationsApi.fromConfig( diff --git a/plugins/frontend/backstage-plugin-github-insights/src/components/Widgets/ContributorsCard/ContributorsCard.test.tsx b/plugins/frontend/backstage-plugin-github-insights/src/components/Widgets/ContributorsCard/ContributorsCard.test.tsx index bae0c90e7..3fb3c84e8 100644 --- a/plugins/frontend/backstage-plugin-github-insights/src/components/Widgets/ContributorsCard/ContributorsCard.test.tsx +++ b/plugins/frontend/backstage-plugin-github-insights/src/components/Widgets/ContributorsCard/ContributorsCard.test.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2021 Larder Software Limited + * Copyright 2025 Larder Software Limited * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,13 +16,13 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; -import { AnyApiRef, githubAuthApiRef } from '@backstage/core-plugin-api'; +import { AnyApiRef } from '@backstage/core-plugin-api'; import { ConfigReader } from '@backstage/core-app-api'; import { rest } from 'msw'; import { setupRequestMockHandlers, - wrapInTestApp, TestApiProvider, + wrapInTestApp, } from '@backstage/test-utils'; import { setupServer } from 'msw/node'; import { contributorsResponseMock, entityMock } from '../../../mocks/mocks'; @@ -31,23 +31,18 @@ import { lightTheme } from '@backstage/theme'; import { ContributorsCard } from '..'; import { EntityProvider } from '@backstage/plugin-catalog-react'; import { - scmIntegrationsApiRef, + scmAuthApiRef, ScmIntegrationsApi, + scmIntegrationsApiRef, } from '@backstage/integration-react'; import { defaultIntegrationsConfig } from '../../../mocks/scmIntegrationsApiMock'; -const mockGithubAuth = { - getAccessToken: async (_: string[]) => 'test-token', - sessionState$: jest.fn(() => ({ - subscribe: (fn: (a: string) => void) => { - fn('SignedIn'); - return { unsubscribe: jest.fn() }; - }, - })), +const mockScmAuth = { + getCredentials: async () => ({ token: 'test-token' }), }; const apis: [AnyApiRef, Partial][] = [ - [githubAuthApiRef, mockGithubAuth], + [scmAuthApiRef, mockScmAuth], [ scmIntegrationsApiRef, ScmIntegrationsApi.fromConfig( diff --git a/plugins/frontend/backstage-plugin-github-insights/src/components/Widgets/EnvironmentsCard/EnvironmentsCard.test.tsx b/plugins/frontend/backstage-plugin-github-insights/src/components/Widgets/EnvironmentsCard/EnvironmentsCard.test.tsx index 55f7aa41b..358a903c5 100644 --- a/plugins/frontend/backstage-plugin-github-insights/src/components/Widgets/EnvironmentsCard/EnvironmentsCard.test.tsx +++ b/plugins/frontend/backstage-plugin-github-insights/src/components/Widgets/EnvironmentsCard/EnvironmentsCard.test.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2021 Larder Software Limited + * Copyright 2025 Larder Software Limited * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ import React from 'react'; import { render, screen, waitFor } from '@testing-library/react'; -import { AnyApiRef, githubAuthApiRef } from '@backstage/core-plugin-api'; +import { AnyApiRef } from '@backstage/core-plugin-api'; import { ConfigReader } from '@backstage/core-app-api'; import { rest } from 'msw'; import { @@ -31,17 +31,18 @@ import { lightTheme } from '@backstage/theme'; import EnvironmentsCard from '.'; import { EntityProvider } from '@backstage/plugin-catalog-react'; import { + scmAuthApiRef, ScmIntegrationsApi, scmIntegrationsApiRef, } from '@backstage/integration-react'; import { defaultIntegrationsConfig } from '../../../mocks/scmIntegrationsApiMock'; -const mockGithubAuth = { - getAccessToken: async (_: string[]) => 'test-token', +const mockScmAuth = { + getCredentials: async () => ({ token: 'test-token' }), }; const apis: [AnyApiRef, Partial][] = [ - [githubAuthApiRef, mockGithubAuth], + [scmAuthApiRef, mockScmAuth], [ scmIntegrationsApiRef, ScmIntegrationsApi.fromConfig( diff --git a/plugins/frontend/backstage-plugin-github-insights/src/components/Widgets/LanguagesCard/LanguagesCard.test.tsx b/plugins/frontend/backstage-plugin-github-insights/src/components/Widgets/LanguagesCard/LanguagesCard.test.tsx index 84314d0ab..23245180e 100644 --- a/plugins/frontend/backstage-plugin-github-insights/src/components/Widgets/LanguagesCard/LanguagesCard.test.tsx +++ b/plugins/frontend/backstage-plugin-github-insights/src/components/Widgets/LanguagesCard/LanguagesCard.test.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2021 Larder Software Limited + * Copyright 2025 Larder Software Limited * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,13 +16,13 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; -import { AnyApiRef, githubAuthApiRef } from '@backstage/core-plugin-api'; +import { AnyApiRef } from '@backstage/core-plugin-api'; import { ConfigReader } from '@backstage/core-app-api'; import { rest } from 'msw'; import { setupRequestMockHandlers, - wrapInTestApp, TestApiProvider, + wrapInTestApp, } from '@backstage/test-utils'; import { setupServer } from 'msw/node'; import { entityMock, languagesResponseMock } from '../../../mocks/mocks'; @@ -31,23 +31,18 @@ import { lightTheme } from '@backstage/theme'; import { LanguagesCard } from '..'; import { EntityProvider } from '@backstage/plugin-catalog-react'; import { - scmIntegrationsApiRef, + scmAuthApiRef, ScmIntegrationsApi, + scmIntegrationsApiRef, } from '@backstage/integration-react'; import { defaultIntegrationsConfig } from '../../../mocks/scmIntegrationsApiMock'; -const mockGithubAuth = { - getAccessToken: async (_: string[]) => 'test-token', - sessionState$: jest.fn(() => ({ - subscribe: (fn: (a: string) => void) => { - fn('SignedIn'); - return { unsubscribe: jest.fn() }; - }, - })), +const mockScmAuth = { + getCredentials: async () => ({ token: 'test-token' }), }; const apis: [AnyApiRef, Partial][] = [ - [githubAuthApiRef, mockGithubAuth], + [scmAuthApiRef, mockScmAuth], [ scmIntegrationsApiRef, ScmIntegrationsApi.fromConfig( diff --git a/plugins/frontend/backstage-plugin-github-insights/src/components/Widgets/MarkdownContent/MarkdownContent.test.tsx b/plugins/frontend/backstage-plugin-github-insights/src/components/Widgets/MarkdownContent/MarkdownContent.test.tsx index 0836a565c..89fa00fc3 100644 --- a/plugins/frontend/backstage-plugin-github-insights/src/components/Widgets/MarkdownContent/MarkdownContent.test.tsx +++ b/plugins/frontend/backstage-plugin-github-insights/src/components/Widgets/MarkdownContent/MarkdownContent.test.tsx @@ -15,32 +15,20 @@ */ import React from 'react'; -import { - AnyApiRef, - errorApiRef, - githubAuthApiRef, -} from '@backstage/core-plugin-api'; +import { AnyApiRef, errorApiRef } from '@backstage/core-plugin-api'; import { TestApiProvider } from '@backstage/test-utils'; import { render, screen } from '@testing-library/react'; import MarkdownContent from './MarkdownContent'; import { GetContentProps, GetContentResponse, - githubApiRef, GithubApi, + githubApiRef, } from '../../../apis'; +import { scmAuthApiRef } from '@backstage/integration-react'; -const mockAccessToken = jest - .fn() - .mockImplementation(async (_: string[]) => 'test-token'); -const mockGithubAuth = { - getAccessToken: mockAccessToken, - sessionState$: jest.fn(() => ({ - subscribe: (fn: (a: string) => void) => { - fn('SignedIn'); - return { unsubscribe: jest.fn() }; - }, - })), +const mockScmAuth = { + getCredentials: async () => ({ token: 'test-token' }), }; const mockGithubApi: GithubApi = { @@ -80,25 +68,20 @@ const mockGithubApi: GithubApi = { }; const apis: [AnyApiRef, Partial][] = [ - [githubAuthApiRef, mockGithubAuth], + [scmAuthApiRef, mockScmAuth], [githubApiRef, mockGithubApi], [errorApiRef, jest.fn()], ]; describe('', () => { it('should render sign in page', async () => { - const mockGithubUnAuth = { - getAccessToken: async (_: string[]) => 'test-token', - sessionState$: jest.fn(() => ({ - subscribe: (fn: (a: string) => void) => { - fn('SignedOut'); - return { unsubscribe: jest.fn() }; - }, - })), - }; - const api: [AnyApiRef, Partial][] = [ - [githubAuthApiRef, mockGithubUnAuth], + [ + scmAuthApiRef, + { + getCredentials: async () => ({ token: undefined }), + }, + ], ]; render( diff --git a/plugins/frontend/backstage-plugin-github-insights/src/components/Widgets/MarkdownContent/MarkdownContent.tsx b/plugins/frontend/backstage-plugin-github-insights/src/components/Widgets/MarkdownContent/MarkdownContent.tsx index 9512a1bae..11149d152 100644 --- a/plugins/frontend/backstage-plugin-github-insights/src/components/Widgets/MarkdownContent/MarkdownContent.tsx +++ b/plugins/frontend/backstage-plugin-github-insights/src/components/Widgets/MarkdownContent/MarkdownContent.tsx @@ -22,8 +22,7 @@ import { } from '@backstage/core-components'; import { ApiHolder, - githubAuthApiRef, - SessionState, + configApiRef, useApi, useApiHolder, } from '@backstage/core-plugin-api'; @@ -31,18 +30,20 @@ import { MarkdownContentProps } from './types'; import { Button, Grid, Tooltip, Typography } from '@material-ui/core'; import useAsync from 'react-use/lib/useAsync'; import { GithubApi, githubApiRef, GithubClient } from '../../../apis'; +import { scmAuthApiRef } from '@backstage/integration-react'; const getGithubClient = (apiHolder: ApiHolder) => { let githubClient: GithubApi | undefined = apiHolder.get(githubApiRef); if (!githubClient) { - const auth = apiHolder.get(githubAuthApiRef); - if (auth) { - githubClient = new GithubClient({ githubAuthApi: auth }); + const configApi = apiHolder.get(configApiRef); + const scmAuthApi = apiHolder.get(scmAuthApiRef); + if (scmAuthApi && configApi) { + githubClient = new GithubClient({ configApi, scmAuthApi }); } } if (!githubClient) { throw new Error( - 'The MarkdownCard component Failed to get the github client', + 'The MarkdownCard component Failed to get the SCM auth client or SCM configuration', ); } return githubClient; @@ -95,8 +96,12 @@ const GithubFileContent = (props: MarkdownContentProps) => { ); }; -const GithubNotAuthorized = () => { - const githubApi = useApi(githubAuthApiRef); +const GithubNotAuthorized = ({ + hostname = 'github.com', +}: { + hostname?: string; +}) => { + const scmAuth = useApi(scmAuthApiRef); return ( @@ -111,7 +116,15 @@ const GithubNotAuthorized = () => { variant="outlined" color="primary" // Calling getAccessToken instead of a plain signIn because we are going to get the correct scopes right away. No need to second request - onClick={() => githubApi.getAccessToken('repo')} + onClick={() => + scmAuth.getCredentials({ + additionalScope: { + customScopes: { github: ['repo'] }, + }, + url: `https://${hostname}`, + optional: true, + }) + } > Sign in @@ -127,19 +140,34 @@ const GithubNotAuthorized = () => { * @public */ const MarkdownContent = (props: MarkdownContentProps) => { - const githubApi = useApi(githubAuthApiRef); + const { baseUrl } = props; + const scmAuth = useApi(scmAuthApiRef); const [isLoggedIn, setIsLoggedIn] = useState(false); + let githubUrl = baseUrl ?? 'https://github.com'; + try { + const u = new URL(githubUrl); + githubUrl = `${u.protocol}//${u.host}`; + } catch (e) { + // ignored + } + useEffect(() => { - const authSubscription = githubApi.sessionState$().subscribe(state => { - if (state === SessionState.SignedIn) { + const doLogin = async () => { + const credentials = await scmAuth.getCredentials({ + additionalScope: { + customScopes: { github: ['repo'] }, + }, + url: githubUrl, + optional: true, + }); + + if (credentials?.token) { setIsLoggedIn(true); } - }); - return () => { - authSubscription.unsubscribe(); }; - }, [githubApi]); + doLogin(); + }, [scmAuth, githubUrl]); return isLoggedIn ? ( diff --git a/plugins/frontend/backstage-plugin-github-insights/src/components/Widgets/ReadMeCard/ReadmeCard.test.tsx b/plugins/frontend/backstage-plugin-github-insights/src/components/Widgets/ReadMeCard/ReadmeCard.test.tsx index aa3daf5a8..70ea80d4e 100644 --- a/plugins/frontend/backstage-plugin-github-insights/src/components/Widgets/ReadMeCard/ReadmeCard.test.tsx +++ b/plugins/frontend/backstage-plugin-github-insights/src/components/Widgets/ReadMeCard/ReadmeCard.test.tsx @@ -16,17 +16,18 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; -import { AnyApiRef, githubAuthApiRef } from '@backstage/core-plugin-api'; +import { AnyApiRef } from '@backstage/core-plugin-api'; import { ConfigReader } from '@backstage/core-app-api'; -import { wrapInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { TestApiProvider, wrapInTestApp } from '@backstage/test-utils'; import { entityMock } from '../../../mocks/mocks'; import { ThemeProvider } from '@material-ui/core'; import { lightTheme } from '@backstage/theme'; import { ReadMeCard } from '..'; import { EntityProvider } from '@backstage/plugin-catalog-react'; import { - scmIntegrationsApiRef, + scmAuthApiRef, ScmIntegrationsApi, + scmIntegrationsApiRef, } from '@backstage/integration-react'; import { defaultIntegrationsConfig } from '../../../mocks/scmIntegrationsApiMock'; import { @@ -57,19 +58,13 @@ const mockGithubApi: GithubApi = { throw new Error(`${JSON.stringify(props)} NotFound`); }, }; -const mockGithubAuth = { - getAccessToken: async (_: string[]) => 'test-token', - sessionState$: jest.fn(() => ({ - subscribe: (fn: (a: string) => void) => { - fn('SignedIn'); - return { unsubscribe: jest.fn() }; - }, - })), +const mockScmAuth = { + getCredentials: async () => ({ token: 'test-token' }), }; const apis: [AnyApiRef, Partial][] = [ [githubApiRef, mockGithubApi], - [githubAuthApiRef, mockGithubAuth], + [scmAuthApiRef, mockScmAuth], [ scmIntegrationsApiRef, ScmIntegrationsApi.fromConfig( diff --git a/plugins/frontend/backstage-plugin-github-insights/src/components/Widgets/ReleasesCard/ReleasesCard.test.tsx b/plugins/frontend/backstage-plugin-github-insights/src/components/Widgets/ReleasesCard/ReleasesCard.test.tsx index 287c9f66e..24ad83c0d 100644 --- a/plugins/frontend/backstage-plugin-github-insights/src/components/Widgets/ReleasesCard/ReleasesCard.test.tsx +++ b/plugins/frontend/backstage-plugin-github-insights/src/components/Widgets/ReleasesCard/ReleasesCard.test.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2021 Larder Software Limited + * Copyright 2025 Larder Software Limited * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; -import { AnyApiRef, githubAuthApiRef } from '@backstage/core-plugin-api'; +import { AnyApiRef } from '@backstage/core-plugin-api'; import { ConfigReader } from '@backstage/core-app-api'; import { rest } from 'msw'; import { @@ -31,23 +31,18 @@ import { lightTheme } from '@backstage/theme'; import ReleasesCard from '.'; import { EntityProvider } from '@backstage/plugin-catalog-react'; import { - scmIntegrationsApiRef, + scmAuthApiRef, ScmIntegrationsApi, + scmIntegrationsApiRef, } from '@backstage/integration-react'; import { defaultIntegrationsConfig } from '../../../mocks/scmIntegrationsApiMock'; -const mockGithubAuth = { - getAccessToken: async (_: string[]) => 'test-token', - sessionState$: jest.fn(() => ({ - subscribe: (fn: (a: string) => void) => { - fn('SignedIn'); - return { unsubscribe: jest.fn() }; - }, - })), +const mockScmAuth = { + getCredentials: async () => ({ token: 'test-token' }), }; const apis: [AnyApiRef, Partial][] = [ - [githubAuthApiRef, mockGithubAuth], + [scmAuthApiRef, mockScmAuth], [ scmIntegrationsApiRef, ScmIntegrationsApi.fromConfig( diff --git a/plugins/frontend/backstage-plugin-github-insights/src/hooks/useComplianceHooks.ts b/plugins/frontend/backstage-plugin-github-insights/src/hooks/useComplianceHooks.ts index 5d6057b06..6c9c1ee76 100644 --- a/plugins/frontend/backstage-plugin-github-insights/src/hooks/useComplianceHooks.ts +++ b/plugins/frontend/backstage-plugin-github-insights/src/hooks/useComplianceHooks.ts @@ -1,5 +1,5 @@ /* - * Copyright 2021 Larder Software Limited + * Copyright 2025 Larder Software Limited * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,21 +14,22 @@ * limitations under the License. */ import { Entity } from '@backstage/catalog-model'; -import { useApi, githubAuthApiRef } from '@backstage/core-plugin-api'; +import { useApi } from '@backstage/core-plugin-api'; import { Octokit } from '@octokit/rest'; import { OctokitResponse } from '@octokit/types'; import { useAsync } from 'react-use'; import { useProjectEntity } from './useProjectEntity'; import { useEntityGithubScmIntegration } from './useEntityGithubScmIntegration'; import { useStore } from '../components/store'; +import { scmAuthApiRef } from '@backstage/integration-react'; export const NO_LICENSE_MSG = 'No license file found'; export const useProtectedBranches = ( entity: Entity, ): { branches?: []; error?: Error; loading: boolean } => { - const auth = useApi(githubAuthApiRef); - const { baseUrl } = useEntityGithubScmIntegration(entity); + const auth = useApi(scmAuthApiRef); + const { baseUrl, hostname } = useEntityGithubScmIntegration(entity); const { owner, repo } = useProjectEntity(entity); const { state: branches, setState: setBranches } = useStore( @@ -38,7 +39,15 @@ export const useProtectedBranches = ( const { value, loading, error } = useAsync(async (): Promise => { let result; try { - const token = await auth.getAccessToken(['repo']); + const { token } = await auth.getCredentials({ + url: `https://${hostname}/`, + additionalScope: { + customScopes: { + github: ['repo'], + }, + }, + }); + const octokit = new Octokit({ auth: token }); const response = await octokit.request( @@ -77,8 +86,8 @@ export const useProtectedBranches = ( export const useRepoLicence = ( entity: Entity, ): { license?: string; error?: Error; loading: boolean } => { - const auth = useApi(githubAuthApiRef); - const { baseUrl } = useEntityGithubScmIntegration(entity); + const auth = useApi(scmAuthApiRef); + const { baseUrl, hostname } = useEntityGithubScmIntegration(entity); const { owner, repo } = useProjectEntity(entity); const { state: license, setState: setLicense } = useStore( @@ -90,7 +99,15 @@ export const useRepoLicence = ( return license; } - const token = await auth.getAccessToken(['repo']); + const { token } = await auth.getCredentials({ + url: `https://${hostname}/`, + additionalScope: { + customScopes: { + github: ['repo'], + }, + }, + }); + const octokit = new Octokit({ auth: token }); let result; diff --git a/plugins/frontend/backstage-plugin-github-insights/src/hooks/useContributor.ts b/plugins/frontend/backstage-plugin-github-insights/src/hooks/useContributor.ts index 2ed18dc45..d1243fd7e 100644 --- a/plugins/frontend/backstage-plugin-github-insights/src/hooks/useContributor.ts +++ b/plugins/frontend/backstage-plugin-github-insights/src/hooks/useContributor.ts @@ -1,5 +1,5 @@ /* - * Copyright 2021 Larder Software Limited + * Copyright 2025 Larder Software Limited * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,18 +16,19 @@ import { useAsync } from 'react-use'; import { Octokit } from '@octokit/rest'; -import { useApi, githubAuthApiRef } from '@backstage/core-plugin-api'; +import { useApi } from '@backstage/core-plugin-api'; import { useEntityGithubScmIntegration } from './useEntityGithubScmIntegration'; import { ContributorData, GithubRequestState } from '../components/types'; import { useEntity } from '@backstage/plugin-catalog-react'; import { useStore } from '../components/store'; +import { scmAuthApiRef } from '@backstage/integration-react'; export const useContributor = ( username: string, ): { contributor?: ContributorData; error?: Error; loading: boolean } => { - const auth = useApi(githubAuthApiRef); + const auth = useApi(scmAuthApiRef); const { entity } = useEntity(); - const { baseUrl } = useEntityGithubScmIntegration(entity); + const { hostname, baseUrl } = useEntityGithubScmIntegration(entity); const { state: contributorData, setState: setContributorData } = useStore( state => state.contributor, @@ -38,7 +39,15 @@ export const useContributor = ( > => { let result; try { - const token = await auth.getAccessToken(['repo']); + const { token } = await auth.getCredentials({ + url: `https://${hostname}/`, + additionalScope: { + customScopes: { + github: ['repo'], + }, + }, + }); + const octokit = new Octokit({ auth: token }); const response = await octokit.request(`GET /users/${username}`, { diff --git a/plugins/frontend/backstage-plugin-github-insights/src/hooks/useGithubLoggedIn.tsx b/plugins/frontend/backstage-plugin-github-insights/src/hooks/useGithubLoggedIn.tsx index e7ff05f44..bb963fb07 100644 --- a/plugins/frontend/backstage-plugin-github-insights/src/hooks/useGithubLoggedIn.tsx +++ b/plugins/frontend/backstage-plugin-github-insights/src/hooks/useGithubLoggedIn.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2024 Larder Software Limited + * Copyright 2025 Larder Software Limited * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,15 +14,16 @@ * limitations under the License. */ import React, { useEffect, useState } from 'react'; -import { - useApi, - githubAuthApiRef, - SessionState, -} from '@backstage/core-plugin-api'; -import { Grid, Tooltip, Button, Typography } from '@material-ui/core'; +import { useApi } from '@backstage/core-plugin-api'; +import { Button, Grid, Tooltip, Typography } from '@material-ui/core'; +import { scmAuthApiRef } from '@backstage/integration-react'; -export const GithubNotAuthorized = () => { - const githubApi = useApi(githubAuthApiRef); +export const GithubNotAuthorized = ({ + hostname = 'github.com', +}: { + hostname?: string; +}) => { + const scmAuth = useApi(scmAuthApiRef); return ( @@ -37,7 +38,15 @@ export const GithubNotAuthorized = () => { variant="outlined" color="primary" // Calling getAccessToken instead of a plain signIn because we are going to get the correct scopes right away. No need to second request - onClick={() => githubApi.getAccessToken('repo')} + onClick={() => + scmAuth.getCredentials({ + additionalScope: { + customScopes: { github: ['repo'] }, + }, + url: `https://${hostname}`, + optional: true, + }) + } > Sign in @@ -47,21 +56,27 @@ export const GithubNotAuthorized = () => { ); }; -export const useGithubLoggedIn = () => { - const githubApi = useApi(githubAuthApiRef); +export const useGithubLoggedIn = (hostname: string = 'github.com') => { + const scmAuth = useApi(scmAuthApiRef); const [isLoggedIn, setIsLoggedIn] = useState(false); useEffect(() => { - githubApi.getAccessToken('repo', { optional: true }); - const authSubscription = githubApi.sessionState$().subscribe(state => { - if (state === SessionState.SignedIn) { + const doLogin = async () => { + const credentials = await scmAuth.getCredentials({ + additionalScope: { + customScopes: { github: ['repo'] }, + }, + url: `https://${hostname}`, + optional: true, + }); + + if (credentials?.token) { setIsLoggedIn(true); } - }); - return () => { - authSubscription.unsubscribe(); }; - }, [githubApi]); + + doLogin(); + }, [hostname, scmAuth]); return isLoggedIn; }; diff --git a/plugins/frontend/backstage-plugin-github-insights/src/hooks/useRequest.ts b/plugins/frontend/backstage-plugin-github-insights/src/hooks/useRequest.ts index ba47743cf..263b6f76d 100644 --- a/plugins/frontend/backstage-plugin-github-insights/src/hooks/useRequest.ts +++ b/plugins/frontend/backstage-plugin-github-insights/src/hooks/useRequest.ts @@ -17,10 +17,11 @@ import { useAsync } from 'react-use'; import { Octokit } from '@octokit/rest'; import { Entity } from '@backstage/catalog-model'; -import { useApi, githubAuthApiRef } from '@backstage/core-plugin-api'; +import { useApi } from '@backstage/core-plugin-api'; import { useProjectEntity } from './useProjectEntity'; import { useEntityGithubScmIntegration } from './useEntityGithubScmIntegration'; import { useStore } from '../components/store'; +import { scmAuthApiRef } from '@backstage/integration-react'; export const useRequest = ( entity: Entity, @@ -28,8 +29,8 @@ export const useRequest = ( perPage: number = 0, maxResults: number = 0, ) => { - const auth = useApi(githubAuthApiRef); - const { baseUrl } = useEntityGithubScmIntegration(entity); + const auth = useApi(scmAuthApiRef); + const { hostname, baseUrl } = useEntityGithubScmIntegration(entity); const { owner, repo } = useProjectEntity(entity); const { state: requestState, setState: setRequestState } = useStore( @@ -39,7 +40,15 @@ export const useRequest = ( const { value, loading, error } = useAsync(async (): Promise => { let result; try { - const token = await auth.getAccessToken(['repo']); + const { token } = await auth.getCredentials({ + url: `https://${hostname}/`, + additionalScope: { + customScopes: { + github: ['repo'], + }, + }, + }); + const octokit = new Octokit({ auth: token }); const response = await octokit.request( diff --git a/plugins/frontend/backstage-plugin-github-insights/src/plugin.ts b/plugins/frontend/backstage-plugin-github-insights/src/plugin.ts index 91a2254d8..c45885a94 100644 --- a/plugins/frontend/backstage-plugin-github-insights/src/plugin.ts +++ b/plugins/frontend/backstage-plugin-github-insights/src/plugin.ts @@ -1,5 +1,5 @@ /* - * Copyright 2021 Larder Software Limited + * Copyright 2025 Larder Software Limited * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,15 +15,15 @@ */ import { + configApiRef, createApiFactory, createComponentExtension, createPlugin, createRoutableExtension, - errorApiRef, - githubAuthApiRef, } from '@backstage/core-plugin-api'; import { rootRouteRef } from './routes'; import { githubApiRef, GithubClient } from './apis'; +import { scmAuthApiRef } from '@backstage/integration-react'; export const githubInsightsPlugin = createPlugin({ id: 'code-insights', @@ -31,8 +31,8 @@ export const githubInsightsPlugin = createPlugin({ createApiFactory({ api: githubApiRef, deps: { - githubAuthApi: githubAuthApiRef, - errorApi: errorApiRef, + configApi: configApiRef, + scmAuthApi: scmAuthApiRef, }, factory: deps => new GithubClient(deps), }), diff --git a/plugins/frontend/backstage-plugin-github-pull-requests/package.json b/plugins/frontend/backstage-plugin-github-pull-requests/package.json index 6f0f20845..7abf291b5 100644 --- a/plugins/frontend/backstage-plugin-github-pull-requests/package.json +++ b/plugins/frontend/backstage-plugin-github-pull-requests/package.json @@ -49,6 +49,8 @@ "@backstage/catalog-model": "^1.7.1", "@backstage/core-components": "^0.16.1", "@backstage/core-plugin-api": "^1.10.1", + "@backstage/integration": "^1.16.0", + "@backstage/integration-react": "^1.2.2", "@backstage/plugin-catalog-react": "^1.14.2", "@backstage/plugin-home-react": "^0.1.20", "@material-ui/core": "^4.12.2", @@ -57,6 +59,7 @@ "@octokit/rest": "^19.0.3", "@octokit/types": "^9.0.0", "@types/node-fetch": "^2.5.7", + "git-url-parse": "^14.0.0", "history": "^5.0.0", "lodash": "^4.17.21", "luxon": "^3.0.0", @@ -70,18 +73,18 @@ "react-router": "6.0.0-beta.0 || ^6.3.0" }, "devDependencies": { - "@types/luxon": "^3.0.0", "@backstage/cli": "^0.29.2", "@backstage/core-app-api": "^1.15.2", "@backstage/dev-utils": "^1.1.4", "@backstage/test-utils": "^1.7.2", + "@rollup/plugin-commonjs": "^24.0.1", + "@rollup/plugin-node-resolve": "^15.0.1", "@testing-library/jest-dom": "^6.0.0", "@testing-library/react": "^14.0.0", + "@types/luxon": "^3.0.0", "cross-fetch": "^3.1.4", "jest-environment-jsdom": "^29.2.1", "rollup-plugin-dts": "^5.2.0", - "@rollup/plugin-commonjs": "^24.0.1", - "@rollup/plugin-node-resolve": "^15.0.1", "rollup-plugin-esbuild": "^5.0.0" }, "files": [ diff --git a/plugins/frontend/backstage-plugin-github-pull-requests/src/api/GithubPullRequestsApi.ts b/plugins/frontend/backstage-plugin-github-pull-requests/src/api/GithubPullRequestsApi.ts index c7481626a..5a6748d48 100644 --- a/plugins/frontend/backstage-plugin-github-pull-requests/src/api/GithubPullRequestsApi.ts +++ b/plugins/frontend/backstage-plugin-github-pull-requests/src/api/GithubPullRequestsApi.ts @@ -16,9 +16,10 @@ import { createApiRef } from '@backstage/core-plugin-api'; import { - SearchPullRequestsResponseData, - GithubRepositoryData, GithubFirstCommitDate, + GithubRepositoryData, + GithubSearchPullRequestsDataItem, + SearchPullRequestsResponseData, } from '../types'; export const githubPullRequestsApiRef = createApiRef({ @@ -28,47 +29,49 @@ export const githubPullRequestsApiRef = createApiRef({ export type GithubPullRequestsApi = { listPullRequests: ({ search, - token, owner, repo, pageSize, page, branch, - baseUrl, + hostname, }: { search: string; - token: string; owner: string; repo: string; pageSize?: number; page?: number; branch?: string; - baseUrl: string | undefined; + hostname?: string; }) => Promise<{ pullRequestsData: SearchPullRequestsResponseData; }>; getRepositoryData: ({ - baseUrl, - token, + hostname, url, }: { - baseUrl: string | undefined; - token: string; + hostname?: string; url: string; }) => Promise; getCommitDetailsData({ - baseUrl, - token, + hostname, owner, repo, number, }: { - baseUrl: string | undefined; - token: string; + hostname?: string; owner: string; repo: string; number: number; }): Promise; + + searchPullRequest({ + query, + hostname, + }: { + query: string; + hostname?: string; + }): Promise; }; diff --git a/plugins/frontend/backstage-plugin-github-pull-requests/src/api/GithubPullRequestsClient.ts b/plugins/frontend/backstage-plugin-github-pull-requests/src/api/GithubPullRequestsClient.ts index 3af5b273b..6bbbfeb90 100644 --- a/plugins/frontend/backstage-plugin-github-pull-requests/src/api/GithubPullRequestsClient.ts +++ b/plugins/frontend/backstage-plugin-github-pull-requests/src/api/GithubPullRequestsClient.ts @@ -15,37 +15,64 @@ */ import { GithubPullRequestsApi } from './GithubPullRequestsApi'; +import { readGithubIntegrationConfigs } from '@backstage/integration'; import { Octokit } from '@octokit/rest'; import { SearchPullRequestsResponseData, GithubRepositoryData, GithubFirstCommitDate, + GetSearchPullRequestsResponseType, + GithubSearchPullRequestsDataItem, } from '../types'; +import { ConfigApi } from '@backstage/core-plugin-api'; +import { ScmAuthApi } from '@backstage/integration-react'; +import { DateTime } from 'luxon'; export class GithubPullRequestsClient implements GithubPullRequestsApi { + private readonly configApi: ConfigApi; + private readonly scmAuthApi: ScmAuthApi; + + constructor(options: { configApi: ConfigApi; scmAuthApi: ScmAuthApi }) { + this.configApi = options.configApi; + this.scmAuthApi = options.scmAuthApi; + } + + private async getOctokit(hostname: string = 'github.com'): Promise { + const { token } = await this.scmAuthApi.getCredentials({ + url: `https://${hostname}/`, + additionalScope: { + customScopes: { + github: ['repo'], + }, + }, + }); + const configs = readGithubIntegrationConfigs( + this.configApi.getOptionalConfigArray('integrations.github') ?? [], + ); + const githubIntegrationConfig = configs.find(v => v.host === hostname); + const baseUrl = githubIntegrationConfig?.apiBaseUrl; + return new Octokit({ auth: token, baseUrl }); + } + async listPullRequests({ search = '', - token, owner, repo, pageSize = 5, page, - baseUrl, + hostname, }: { search: string; - token: string; owner: string; repo: string; pageSize?: number; page?: number; - baseUrl: string | undefined; + hostname?: string; }): Promise<{ pullRequestsData: SearchPullRequestsResponseData; }> { - const pullRequestResponse = await new Octokit({ - auth: token, - ...(baseUrl && { baseUrl }), - }).search.issuesAndPullRequests({ + const octokit = await this.getOctokit(hostname); + const pullRequestResponse = await octokit.search.issuesAndPullRequests({ q: `${search} in:title type:pr repo:${owner}/${repo}`, per_page: pageSize, page, @@ -56,18 +83,14 @@ export class GithubPullRequestsClient implements GithubPullRequestsApi { }; } async getRepositoryData({ - baseUrl, - token, + hostname, url, }: { - baseUrl: string | undefined; - token: string; + hostname?: string; url: string; }): Promise { - const response = await new Octokit({ - auth: token, - ...(baseUrl && { baseUrl }), - }).request({ url: url }); + const octokit = await this.getOctokit(hostname); + const response = await octokit.request({ url: url }); return { htmlUrl: response.data.html_url, @@ -79,25 +102,60 @@ export class GithubPullRequestsClient implements GithubPullRequestsApi { } async getCommitDetailsData({ - baseUrl, - token, + hostname, owner, repo, number, }: { - baseUrl: string | undefined; - token: string; + hostname: string; owner: string; repo: string; number: number; }): Promise { - const { data: commits } = await new Octokit({ - auth: token, - ...(baseUrl && { baseUrl }), - }).pulls.listCommits({ owner: owner, repo: repo, pull_number: number }); + const octokit = await this.getOctokit(hostname); + const { data: commits } = await octokit.pulls.listCommits({ + owner: owner, + repo: repo, + pull_number: number, + }); const firstCommit = commits[0]; return { firstCommitDate: new Date(firstCommit.commit.author!.date!), }; } + + async searchPullRequest({ + query, + hostname, + }: { + query: string; + hostname?: string; + }): Promise { + const octokit = await this.getOctokit(hostname); + const pullRequestResponse: GetSearchPullRequestsResponseType = + await octokit.search.issuesAndPullRequests({ + q: query, + per_page: 100, + page: 1, + }); + return pullRequestResponse.data.items.map(pr => ({ + id: pr.id, + state: pr.state, + draft: pr.draft ?? false, + merged: pr.pull_request?.merged_at ?? undefined, + repositoryUrl: pr.repository_url, + pullRequest: { + htmlUrl: pr.pull_request?.html_url || undefined, + created_at: DateTime.fromISO(pr.created_at).toRelative() || undefined, + }, + title: pr.title, + number: pr.number, + user: { + login: pr.user?.login, + htmlUrl: pr.user?.html_url, + }, + comments: pr.comments, + htmlUrl: pr.html_url, + })); + } } diff --git a/plugins/frontend/backstage-plugin-github-pull-requests/src/components/GroupPullRequestsCard/Content.test.tsx b/plugins/frontend/backstage-plugin-github-pull-requests/src/components/GroupPullRequestsCard/Content.test.tsx index 754aab816..2a2b53374 100644 --- a/plugins/frontend/backstage-plugin-github-pull-requests/src/components/GroupPullRequestsCard/Content.test.tsx +++ b/plugins/frontend/backstage-plugin-github-pull-requests/src/components/GroupPullRequestsCard/Content.test.tsx @@ -1,4 +1,6 @@ /* + * Copyright 2021 Larder Software Limited + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -13,13 +15,13 @@ */ import React from 'react'; -import { AnyApiRef, githubAuthApiRef } from '@backstage/core-plugin-api'; +import { AnyApiRef, ConfigApi, configApiRef } from '@backstage/core-plugin-api'; import { - wrapInTestApp, setupRequestMockHandlers, TestApiProvider, + wrapInTestApp, } from '@backstage/test-utils'; -import { render, screen, cleanup } from '@testing-library/react'; +import { cleanup, render, screen } from '@testing-library/react'; import { setupServer } from 'msw/node'; import { handlers } from '../../mocks/handlers'; import { @@ -27,16 +29,40 @@ import { groupEntityMock, groupEntityMockWithSlug, } from '../../mocks/mocks'; -import { - SignedInMockGithubAuthState, - SignedOutMockGithubAuthState, -} from '../../mocks/githubAuthApi'; import { EntityProvider } from '@backstage/plugin-catalog-react'; import { Content } from './Content'; +import { ScmAuthApi, scmAuthApiRef } from '@backstage/integration-react'; +import { githubPullRequestsApiRef, GithubPullRequestsClient } from '../../api'; +import { ConfigReader } from '@backstage/core-app-api'; +import { defaultIntegrationsConfig } from '../../mocks/scmIntegrationsApiMock'; + +const mockScmAuth = { + getCredentials: async () => ({ token: 'test-token', headers: {} }), +} as ScmAuthApi; + +const config = { + getOptionalConfigArray(_: string) { + return [{ getOptionalString: (_s: string) => undefined }]; + }, +} as ConfigApi; const apis: [AnyApiRef, Partial][] = [ - [githubAuthApiRef, SignedInMockGithubAuthState], + [configApiRef, config], + [scmAuthApiRef, mockScmAuth], + [ + githubPullRequestsApiRef, + new GithubPullRequestsClient({ + configApi: ConfigReader.fromConfigs([ + { + context: 'unit-test', + data: defaultIntegrationsConfig, + }, + ]), + scmAuthApi: mockScmAuth, + }), + ], ]; + describe('', () => { const worker = setupServer(); setupRequestMockHandlers(worker); @@ -48,7 +74,12 @@ describe('', () => { }); it('should render sign in page', async () => { const api: [AnyApiRef, Partial][] = [ - [githubAuthApiRef, SignedOutMockGithubAuthState], + [ + scmAuthApiRef, + { + getCredentials: async () => ({ token: undefined, headers: {} }), + }, + ], ]; render( wrapInTestApp( diff --git a/plugins/frontend/backstage-plugin-github-pull-requests/src/components/GroupPullRequestsCard/Content.tsx b/plugins/frontend/backstage-plugin-github-pull-requests/src/components/GroupPullRequestsCard/Content.tsx index d0f9080ac..6be0f5ec5 100644 --- a/plugins/frontend/backstage-plugin-github-pull-requests/src/components/GroupPullRequestsCard/Content.tsx +++ b/plugins/frontend/backstage-plugin-github-pull-requests/src/components/GroupPullRequestsCard/Content.tsx @@ -26,6 +26,7 @@ import { import Alert from '@material-ui/lab/Alert'; import { Entity, isGroupEntity } from '@backstage/catalog-model'; import { useEntity } from '@backstage/plugin-catalog-react'; +import { getHostname } from '../../utils/githubUtils'; export const getPullRequestsQueryForGroup = (entity: Entity) => { const githubTeamName = isGithubTeamSlugSet(entity); @@ -35,8 +36,9 @@ export const getPullRequestsQueryForGroup = (entity: Entity) => { const PullRequestsCard = () => { const { entity } = useEntity(); + const hostname = getHostname(entity); const query = getPullRequestsQueryForGroup(entity); - const { loading, error, value } = useGithubSearchPullRequest(query); + const { loading, error, value } = useGithubSearchPullRequest(query, hostname); if (loading) return ; if (error) return {error.message}; @@ -48,9 +50,10 @@ const PullRequestsCard = () => { export const Content = () => { const { entity } = useEntity(); - const isLoggedIn = useGithubLoggedIn(); + const hostname = getHostname(entity); + const isLoggedIn = useGithubLoggedIn(hostname); if (!isLoggedIn) { - return ; + return ; } const githubTeamName = isGithubTeamSlugSet(entity); if (!githubTeamName || githubTeamName === '') { diff --git a/plugins/frontend/backstage-plugin-github-pull-requests/src/components/Home/RequestedReviewsCard/Content.test.tsx b/plugins/frontend/backstage-plugin-github-pull-requests/src/components/Home/RequestedReviewsCard/Content.test.tsx index 359436daf..cc440191d 100644 --- a/plugins/frontend/backstage-plugin-github-pull-requests/src/components/Home/RequestedReviewsCard/Content.test.tsx +++ b/plugins/frontend/backstage-plugin-github-pull-requests/src/components/Home/RequestedReviewsCard/Content.test.tsx @@ -15,25 +15,51 @@ */ import React from 'react'; -import { AnyApiRef, githubAuthApiRef } from '@backstage/core-plugin-api'; +import { AnyApiRef, ConfigApi, configApiRef } from '@backstage/core-plugin-api'; import { - wrapInTestApp, setupRequestMockHandlers, TestApiProvider, + wrapInTestApp, } from '@backstage/test-utils'; -import { render, screen, cleanup } from '@testing-library/react'; +import { cleanup, render, screen } from '@testing-library/react'; import { setupServer } from 'msw/node'; import { Content } from './Content'; import { handlers } from '../../../mocks/handlers'; - +import { ScmAuthApi, scmAuthApiRef } from '@backstage/integration-react'; import { - SignedInMockGithubAuthState, - SignedOutMockGithubAuthState, -} from '../../../mocks/githubAuthApi'; + githubPullRequestsApiRef, + GithubPullRequestsClient, +} from '../../../api'; +import { ConfigReader } from '@backstage/core-app-api'; +import { defaultIntegrationsConfig } from '../../../mocks/scmIntegrationsApiMock'; + +const mockScmAuth = { + getCredentials: async () => ({ token: 'test-token', headers: {} }), +} as ScmAuthApi; + +const config = { + getOptionalConfigArray(_: string) { + return [{ getOptionalString: (_s: string) => undefined }]; + }, +} as ConfigApi; const apis: [AnyApiRef, Partial][] = [ - [githubAuthApiRef, SignedInMockGithubAuthState], + [configApiRef, config], + [scmAuthApiRef, mockScmAuth], + [ + githubPullRequestsApiRef, + new GithubPullRequestsClient({ + configApi: ConfigReader.fromConfigs([ + { + context: 'unit-test', + data: defaultIntegrationsConfig, + }, + ]), + scmAuthApi: mockScmAuth, + }), + ], ]; + describe('', () => { const worker = setupServer(); setupRequestMockHandlers(worker); @@ -45,7 +71,12 @@ describe('', () => { }); it('should render sign in page', async () => { const api: [AnyApiRef, Partial][] = [ - [githubAuthApiRef, SignedOutMockGithubAuthState], + [ + scmAuthApiRef, + { + getCredentials: async () => ({ token: undefined, headers: {} }), + }, + ], ]; render( wrapInTestApp( diff --git a/plugins/frontend/backstage-plugin-github-pull-requests/src/components/Home/YourOpenPullRequestsCard/Content.test.tsx b/plugins/frontend/backstage-plugin-github-pull-requests/src/components/Home/YourOpenPullRequestsCard/Content.test.tsx index 70e46ddd8..88210433c 100644 --- a/plugins/frontend/backstage-plugin-github-pull-requests/src/components/Home/YourOpenPullRequestsCard/Content.test.tsx +++ b/plugins/frontend/backstage-plugin-github-pull-requests/src/components/Home/YourOpenPullRequestsCard/Content.test.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2021 Larder Software Limited + * Copyright 2025 Larder Software Limited * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,24 +15,51 @@ */ import React from 'react'; -import { AnyApiRef, githubAuthApiRef } from '@backstage/core-plugin-api'; +import { AnyApiRef, ConfigApi, configApiRef } from '@backstage/core-plugin-api'; import { - wrapInTestApp, setupRequestMockHandlers, TestApiProvider, + wrapInTestApp, } from '@backstage/test-utils'; -import { render, screen, cleanup } from '@testing-library/react'; +import { cleanup, render, screen } from '@testing-library/react'; import { setupServer } from 'msw/node'; import { Content } from './Content'; import { handlers } from '../../../mocks/handlers'; +import { ScmAuthApi, scmAuthApiRef } from '@backstage/integration-react'; import { - SignedInMockGithubAuthState, - SignedOutMockGithubAuthState, -} from '../../../mocks/githubAuthApi'; + githubPullRequestsApiRef, + GithubPullRequestsClient, +} from '../../../api'; +import { ConfigReader } from '@backstage/core-app-api'; +import { defaultIntegrationsConfig } from '../../../mocks/scmIntegrationsApiMock'; + +const mockScmAuth = { + getCredentials: async () => ({ token: 'test-token', headers: {} }), +} as ScmAuthApi; + +const config = { + getOptionalConfigArray(_: string) { + return [{ getOptionalString: (_s: string) => undefined }]; + }, +} as ConfigApi; const apis: [AnyApiRef, Partial][] = [ - [githubAuthApiRef, SignedInMockGithubAuthState], + [configApiRef, config], + [scmAuthApiRef, mockScmAuth], + [ + githubPullRequestsApiRef, + new GithubPullRequestsClient({ + configApi: ConfigReader.fromConfigs([ + { + context: 'unit-test', + data: defaultIntegrationsConfig, + }, + ]), + scmAuthApi: mockScmAuth, + }), + ], ]; + describe('', () => { const worker = setupServer(); setupRequestMockHandlers(worker); @@ -44,7 +71,12 @@ describe('', () => { }); it('should render sign in page', async () => { const api: [AnyApiRef, Partial][] = [ - [githubAuthApiRef, SignedOutMockGithubAuthState], + [ + scmAuthApiRef, + { + getCredentials: async () => ({ token: undefined, headers: {} }), + }, + ], ]; render( wrapInTestApp( diff --git a/plugins/frontend/backstage-plugin-github-pull-requests/src/components/PullRequestsListView/PullRequestListView.test.tsx b/plugins/frontend/backstage-plugin-github-pull-requests/src/components/PullRequestsListView/PullRequestListView.test.tsx index 803df78bf..ea27735d7 100644 --- a/plugins/frontend/backstage-plugin-github-pull-requests/src/components/PullRequestsListView/PullRequestListView.test.tsx +++ b/plugins/frontend/backstage-plugin-github-pull-requests/src/components/PullRequestsListView/PullRequestListView.test.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2021 Larder Software Limited + * Copyright 2025 Larder Software Limited * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,33 +15,46 @@ */ import React from 'react'; -import { render, screen, cleanup } from '@testing-library/react'; -import { - configApiRef, - githubAuthApiRef, - AnyApiRef, -} from '@backstage/core-plugin-api'; +import { cleanup, render, screen } from '@testing-library/react'; +import { AnyApiRef, ConfigApi, configApiRef } from '@backstage/core-plugin-api'; import { setupRequestMockHandlers, TestApiProvider, + wrapInTestApp, } from '@backstage/test-utils'; import { setupServer } from 'msw/node'; import { PullRequestsListView } from './PullRequestsListView'; import { handlers } from '../../mocks/handlers'; +import { ScmAuthApi, scmAuthApiRef } from '@backstage/integration-react'; +import { githubPullRequestsApiRef, GithubPullRequestsClient } from '../../api'; +import { ConfigReader } from '@backstage/core-app-api'; +import { defaultIntegrationsConfig } from '../../mocks/scmIntegrationsApiMock'; -const mockGithubAuth = { - getAccessToken: async (_: string[]) => 'test-token', -}; +const mockScmAuth = { + getCredentials: async () => ({ token: 'test-token', headers: {} }), +} as ScmAuthApi; const config = { - getOptionalConfigArray: (_: string) => [ - { getOptionalString: (_s: string) => undefined }, - ], -}; + getOptionalConfigArray(_: string) { + return [{ getOptionalString: (_s: string) => undefined }]; + }, +} as ConfigApi; const apis: [AnyApiRef, Partial][] = [ [configApiRef, config], - [githubAuthApiRef, mockGithubAuth], + [scmAuthApiRef, mockScmAuth], + [ + githubPullRequestsApiRef, + new GithubPullRequestsClient({ + configApi: ConfigReader.fromConfigs([ + { + context: 'unit-test', + data: defaultIntegrationsConfig, + }, + ]), + scmAuthApi: mockScmAuth, + }), + ], ]; describe('PullRequestsTable', () => { @@ -85,9 +98,11 @@ describe('PullRequestsTable', () => { }, ]; render( - - - , + wrapInTestApp( + + + , + ), ); expect(await screen.findByText('Test PR listed 1')).toBeInTheDocument(); diff --git a/plugins/frontend/backstage-plugin-github-pull-requests/src/components/PullRequestsListView/PullRequestsListView.tsx b/plugins/frontend/backstage-plugin-github-pull-requests/src/components/PullRequestsListView/PullRequestsListView.tsx index f8addb451..a12e015ae 100644 --- a/plugins/frontend/backstage-plugin-github-pull-requests/src/components/PullRequestsListView/PullRequestsListView.tsx +++ b/plugins/frontend/backstage-plugin-github-pull-requests/src/components/PullRequestsListView/PullRequestsListView.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2021 Larder Software Limited + * Copyright 2025 Larder Software Limited * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/frontend/backstage-plugin-github-pull-requests/src/components/PullRequestsPage/PullRequestsPage.tsx b/plugins/frontend/backstage-plugin-github-pull-requests/src/components/PullRequestsPage/PullRequestsPage.tsx index 083867d0d..297e3e320 100644 --- a/plugins/frontend/backstage-plugin-github-pull-requests/src/components/PullRequestsPage/PullRequestsPage.tsx +++ b/plugins/frontend/backstage-plugin-github-pull-requests/src/components/PullRequestsPage/PullRequestsPage.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2021 Larder Software Limited + * Copyright 2025 Larder Software Limited * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/frontend/backstage-plugin-github-pull-requests/src/components/PullRequestsStatsCard/PullRequestsStatsCard.test.tsx b/plugins/frontend/backstage-plugin-github-pull-requests/src/components/PullRequestsStatsCard/PullRequestsStatsCard.test.tsx index 58cb591bd..e3a1d8e66 100644 --- a/plugins/frontend/backstage-plugin-github-pull-requests/src/components/PullRequestsStatsCard/PullRequestsStatsCard.test.tsx +++ b/plugins/frontend/backstage-plugin-github-pull-requests/src/components/PullRequestsStatsCard/PullRequestsStatsCard.test.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2021 Larder Software Limited + * Copyright 2025 Larder Software Limited * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,7 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; -import { - configApiRef, - githubAuthApiRef, - AnyApiRef, -} from '@backstage/core-plugin-api'; +import { AnyApiRef, ConfigApi, configApiRef } from '@backstage/core-plugin-api'; import { setupRequestMockHandlers, TestApiProvider, @@ -32,27 +28,35 @@ import { entityMock } from '../../mocks/mocks'; import PullRequestsStatsCard from './PullRequestsStatsCard'; import { EntityProvider } from '@backstage/plugin-catalog-react'; import { handlers } from '../../mocks/handlers'; +import { ScmAuthApi, scmAuthApiRef } from '@backstage/integration-react'; +import { ConfigReader } from '@backstage/core-app-api'; +import { defaultIntegrationsConfig } from '../../mocks/scmIntegrationsApiMock'; -const mockGithubAuth = { - getAccessToken: async (_: string[]) => 'test-token', - sessionState$: jest.fn(() => ({ - subscribe: (fn: (a: string) => void) => { - fn('SignedIn'); - return { unsubscribe: jest.fn() }; - }, - })), -}; +const mockScmAuth = { + getCredentials: async () => ({ token: 'test-token', headers: {} }), +} as ScmAuthApi; const config = { - getOptionalConfigArray: (_: string) => [ - { getOptionalString: (_s: string) => undefined }, - ], -}; + getOptionalConfigArray(_: string) { + return [{ getOptionalString: (_s: string) => undefined }]; + }, +} as ConfigApi; const apis: [AnyApiRef, Partial][] = [ [configApiRef, config], - [githubAuthApiRef, mockGithubAuth], - [githubPullRequestsApiRef, new GithubPullRequestsClient()], + [scmAuthApiRef, mockScmAuth], + [ + githubPullRequestsApiRef, + new GithubPullRequestsClient({ + configApi: ConfigReader.fromConfigs([ + { + context: 'unit-test', + data: defaultIntegrationsConfig, + }, + ]), + scmAuthApi: mockScmAuth, + }), + ], ]; describe('PullRequestsCard', () => { diff --git a/plugins/frontend/backstage-plugin-github-pull-requests/src/components/PullRequestsStatsCard/PullRequestsStatsCard.tsx b/plugins/frontend/backstage-plugin-github-pull-requests/src/components/PullRequestsStatsCard/PullRequestsStatsCard.tsx index 8f05580d2..e019e1aee 100644 --- a/plugins/frontend/backstage-plugin-github-pull-requests/src/components/PullRequestsStatsCard/PullRequestsStatsCard.tsx +++ b/plugins/frontend/backstage-plugin-github-pull-requests/src/components/PullRequestsStatsCard/PullRequestsStatsCard.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2021 Larder Software Limited + * Copyright 2025 Larder Software Limited * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,12 +18,12 @@ import React, { useState } from 'react'; import { InfoCard, InfoCardVariants, - StructuredMetadataTable, MissingAnnotationEmptyState, + StructuredMetadataTable, } from '@backstage/core-components'; import { - isGithubSlugSet, GITHUB_PULL_REQUESTS_ANNOTATION, + isGithubSlugSet, } from '../../utils/isGithubSlugSet'; import { usePullRequestsStatistics } from '../usePullRequestsStatistics'; import { @@ -31,13 +31,13 @@ import { CircularProgress, FormControl, FormHelperText, + makeStyles, MenuItem, Select, - makeStyles, + Tooltip, } from '@material-ui/core'; import { Entity } from '@backstage/catalog-model'; import { useEntity } from '@backstage/plugin-catalog-react'; -import { Tooltip } from '@material-ui/core'; import { TooltipContent } from './components/TooltipContent'; import { GithubNotAuthorized, useGithubLoggedIn } from '../useGithubLoggedIn'; diff --git a/plugins/frontend/backstage-plugin-github-pull-requests/src/components/PullRequestsTable/PullRequestsTable.test.tsx b/plugins/frontend/backstage-plugin-github-pull-requests/src/components/PullRequestsTable/PullRequestsTable.test.tsx index f0892a153..8904e4b0b 100644 --- a/plugins/frontend/backstage-plugin-github-pull-requests/src/components/PullRequestsTable/PullRequestsTable.test.tsx +++ b/plugins/frontend/backstage-plugin-github-pull-requests/src/components/PullRequestsTable/PullRequestsTable.test.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2021 Larder Software Limited + * Copyright 2025 Larder Software Limited * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,7 @@ import React from 'react'; import { render } from '@testing-library/react'; -import { - configApiRef, - githubAuthApiRef, - AnyApiRef, -} from '@backstage/core-plugin-api'; +import { configApiRef, AnyApiRef, ConfigApi } from '@backstage/core-plugin-api'; import { rest } from 'msw'; import { setupRequestMockHandlers, @@ -32,21 +28,35 @@ import { GithubPullRequestsClient } from '../../api'; import { entityMock, openPullsRequestMock } from '../../mocks/mocks'; import { PullRequestsTable } from './PullRequestsTable'; import { EntityProvider } from '@backstage/plugin-catalog-react'; +import { ScmAuthApi, scmAuthApiRef } from '@backstage/integration-react'; +import { ConfigReader } from '@backstage/core-app-api'; +import { defaultIntegrationsConfig } from '../../mocks/scmIntegrationsApiMock'; -const mockGithubAuth = { - getAccessToken: async (_: string[]) => 'test-token', -}; +const mockScmAuth = { + getCredentials: async () => ({ token: 'test-token', headers: {} }), +} as ScmAuthApi; const config = { - getOptionalConfigArray: (_: string) => [ - { getOptionalString: (_s: string) => undefined }, - ], -}; + getOptionalConfigArray(_: string) { + return [{ getOptionalString: (_s: string) => undefined }]; + }, +} as ConfigApi; const apis: [AnyApiRef, Partial][] = [ [configApiRef, config], - [githubAuthApiRef, mockGithubAuth], - [githubPullRequestsApiRef, new GithubPullRequestsClient()], + [scmAuthApiRef, mockScmAuth], + [ + githubPullRequestsApiRef, + new GithubPullRequestsClient({ + configApi: ConfigReader.fromConfigs([ + { + context: 'unit-test', + data: defaultIntegrationsConfig, + }, + ]), + scmAuthApi: mockScmAuth, + }), + ], ]; describe('PullRequestsTable', () => { diff --git a/plugins/frontend/backstage-plugin-github-pull-requests/src/components/PullRequestsTable/PullRequestsTable.tsx b/plugins/frontend/backstage-plugin-github-pull-requests/src/components/PullRequestsTable/PullRequestsTable.tsx index 2ffb4907f..73deb5da9 100644 --- a/plugins/frontend/backstage-plugin-github-pull-requests/src/components/PullRequestsTable/PullRequestsTable.tsx +++ b/plugins/frontend/backstage-plugin-github-pull-requests/src/components/PullRequestsTable/PullRequestsTable.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2021 Larder Software Limited + * Copyright 2025 Larder Software Limited * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,33 +14,33 @@ * limitations under the License. */ -import React, { FC, useState, useRef } from 'react'; +import React, { FC, useRef, useState } from 'react'; import { debounce } from 'lodash'; import { - InputAdornment, - IconButton, - TextField, - Typography, Box, - ButtonGroup, Button, + ButtonGroup, + IconButton, + InputAdornment, + TextField, Tooltip, + Typography, } from '@material-ui/core'; import GitHubIcon from '@material-ui/icons/GitHub'; import ClearIcon from '@material-ui/icons/Clear'; import SearchIcon from '@material-ui/icons/Search'; import { - Table, - TableColumn, MarkdownContent, MissingAnnotationEmptyState, + Table, + TableColumn, } from '@backstage/core-components'; import { - isGithubSlugSet, GITHUB_PULL_REQUESTS_ANNOTATION, + isGithubSlugSet, } from '../../utils/isGithubSlugSet'; import { isRoadieBackstageDefaultFilterSet } from '../../utils/isRoadieBackstageDefaultFilterSet'; -import { usePullRequests, PullRequest } from '../usePullRequests'; +import { PullRequest, usePullRequests } from '../usePullRequests'; import { PullRequestState } from '../../types'; import { Entity } from '@backstage/catalog-model'; import { getStatusIconType } from '../Icons'; diff --git a/plugins/frontend/backstage-plugin-github-pull-requests/src/components/useGithubLoggedIn.tsx b/plugins/frontend/backstage-plugin-github-pull-requests/src/components/useGithubLoggedIn.tsx index 31eb6142a..c0f618f0c 100644 --- a/plugins/frontend/backstage-plugin-github-pull-requests/src/components/useGithubLoggedIn.tsx +++ b/plugins/frontend/backstage-plugin-github-pull-requests/src/components/useGithubLoggedIn.tsx @@ -1,18 +1,20 @@ import React, { useEffect, useState } from 'react'; -import { - useApi, - githubAuthApiRef, - SessionState, -} from '@backstage/core-plugin-api'; -import { Grid, Tooltip, Button, Typography } from '@material-ui/core'; +import { useApi } from '@backstage/core-plugin-api'; +import { scmAuthApiRef } from '@backstage/integration-react'; +import { Button, Grid, Tooltip, Typography } from '@material-ui/core'; + +export const GithubNotAuthorized = ({ + hostname = 'github.com', +}: { + hostname?: string; +}) => { + const scmAuth = useApi(scmAuthApiRef); -export const GithubNotAuthorized = () => { - const githubApi = useApi(githubAuthApiRef); return ( - You are not logged into github. You need to be signed in to see the + You are not logged into GitHub. You need to be signed in to see the content of this card. @@ -21,8 +23,16 @@ export const GithubNotAuthorized = () => { @@ -32,21 +42,27 @@ export const GithubNotAuthorized = () => { ); }; -export const useGithubLoggedIn = () => { - const githubApi = useApi(githubAuthApiRef); +export const useGithubLoggedIn = (hostname: string = 'github.com') => { + const scmAuth = useApi(scmAuthApiRef); const [isLoggedIn, setIsLoggedIn] = useState(false); useEffect(() => { - githubApi.getAccessToken('repo', { optional: true }); - const authSubscription = githubApi.sessionState$().subscribe(state => { - if (state === SessionState.SignedIn) { + const doLogin = async () => { + const credentials = await scmAuth.getCredentials({ + additionalScope: { + customScopes: { github: ['repo'] }, + }, + url: `https://${hostname}`, + optional: true, + }); + + if (credentials?.token) { setIsLoggedIn(true); } - }); - return () => { - authSubscription.unsubscribe(); }; - }, [githubApi]); + + doLogin(); + }, [hostname, scmAuth]); return isLoggedIn; }; diff --git a/plugins/frontend/backstage-plugin-github-pull-requests/src/components/useGithubRepositoryData.tsx b/plugins/frontend/backstage-plugin-github-pull-requests/src/components/useGithubRepositoryData.tsx index 7fbeefdcd..d22247916 100644 --- a/plugins/frontend/backstage-plugin-github-pull-requests/src/components/useGithubRepositoryData.tsx +++ b/plugins/frontend/backstage-plugin-github-pull-requests/src/components/useGithubRepositoryData.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2021 Larder Software Limited + * Copyright 2025 Larder Software Limited * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,23 +13,31 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { useApi, githubAuthApiRef } from '@backstage/core-plugin-api'; +import { useApi } from '@backstage/core-plugin-api'; import { useAsync } from 'react-use'; import { GithubRepositoryData } from '../types'; -import { useBaseUrl } from './useBaseUrl'; -import { GithubPullRequestsClient } from '../api'; +import { githubPullRequestsApiRef } from '../api'; export const useGithubRepositoryData = (url: string) => { - const githubAuthApi = useApi(githubAuthApiRef); - const baseUrl = useBaseUrl(); + const githubPullRequestsApi = useApi(githubPullRequestsApiRef); - return useAsync(async (): Promise => { - const token = await githubAuthApi.getAccessToken(['repo']); + let domain = ''; + try { + const hostname = new URL(url).hostname; + const parts = hostname.split('.'); + if (parts.length >= 2) { + domain = `${parts[parts.length - 2]}.${parts[parts.length - 1]}`; + } else { + throw new Error('Hostname is not valid for domain extraction'); + } + } catch (err) { + throw new Error('Invalid URL for extracting domain'); + } - return new GithubPullRequestsClient().getRepositoryData({ + return useAsync(async (): Promise => { + return githubPullRequestsApi.getRepositoryData({ url, - baseUrl, - token, + hostname: domain, }); - }, [githubAuthApi]); + }, [githubPullRequestsApi, domain]); }; diff --git a/plugins/frontend/backstage-plugin-github-pull-requests/src/components/useGithubSearchPullRequest.tsx b/plugins/frontend/backstage-plugin-github-pull-requests/src/components/useGithubSearchPullRequest.tsx index b80d4cc68..04b4a9512 100644 --- a/plugins/frontend/backstage-plugin-github-pull-requests/src/components/useGithubSearchPullRequest.tsx +++ b/plugins/frontend/backstage-plugin-github-pull-requests/src/components/useGithubSearchPullRequest.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2021 Larder Software Limited + * Copyright 2025 Larder Software Limited * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,50 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { useApi, githubAuthApiRef } from '@backstage/core-plugin-api'; -import { Octokit } from '@octokit/rest'; +import { useApi } from '@backstage/core-plugin-api'; import { useAsync } from 'react-use'; -import { - GetSearchPullRequestsResponseType, - GithubSearchPullRequestsDataItem, -} from '../types'; -import { useBaseUrl } from './useBaseUrl'; -import { DateTime } from 'luxon'; +import { GithubSearchPullRequestsDataItem } from '../types'; +import { githubPullRequestsApiRef } from '../api'; -export const useGithubSearchPullRequest = (query: string) => { - const githubAuthApi = useApi(githubAuthApiRef); - const baseUrl = useBaseUrl(); +export const useGithubSearchPullRequest = ( + query: string, + hostname?: string, +) => { + const githubPrsApi = useApi(githubPullRequestsApiRef); return useAsync(async (): Promise => { - const token = await githubAuthApi.getAccessToken(['repo']); - - const pullRequestResponse: GetSearchPullRequestsResponseType = - await new Octokit({ - auth: token, - ...(baseUrl && { baseUrl }), - }).search.issuesAndPullRequests({ - q: query, - per_page: 100, - page: 1, - }); - return pullRequestResponse.data.items.map(pr => ({ - id: pr.id, - state: pr.state, - draft: pr.draft ?? false, - merged: pr.pull_request?.merged_at ?? undefined, - repositoryUrl: pr.repository_url, - pullRequest: { - htmlUrl: pr.pull_request?.html_url || undefined, - created_at: DateTime.fromISO(pr.created_at).toRelative() || undefined, - }, - title: pr.title, - number: pr.number, - user: { - login: pr.user?.login, - htmlUrl: pr.user?.html_url, - }, - comments: pr.comments, - htmlUrl: pr.html_url, - })); - }, [githubAuthApi]); + return await githubPrsApi.searchPullRequest({ query, hostname }); + }, [githubPrsApi]); }; diff --git a/plugins/frontend/backstage-plugin-github-pull-requests/src/components/usePullRequests.ts b/plugins/frontend/backstage-plugin-github-pull-requests/src/components/usePullRequests.ts index d8d9434e5..bdd8bf624 100644 --- a/plugins/frontend/backstage-plugin-github-pull-requests/src/components/usePullRequests.ts +++ b/plugins/frontend/backstage-plugin-github-pull-requests/src/components/usePullRequests.ts @@ -1,5 +1,5 @@ /* - * Copyright 2021 Larder Software Limited + * Copyright 2025 Larder Software Limited * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,12 @@ import { useEffect, useState } from 'react'; import { useAsyncRetry } from 'react-use'; -import { githubPullRequestsApiRef } from '../api/GithubPullRequestsApi'; -import { useApi, githubAuthApiRef } from '@backstage/core-plugin-api'; +import { githubPullRequestsApiRef } from '../api'; +import { useApi } from '@backstage/core-plugin-api'; import { SearchPullRequestsResponseData } from '../types'; -import { useBaseUrl } from './useBaseUrl'; import { DateTime } from 'luxon'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import { getHostname } from '../utils/githubUtils'; export type PullRequest = { id: number; @@ -49,8 +50,8 @@ export function usePullRequests({ branch?: string; }) { const api = useApi(githubPullRequestsApiRef); - const auth = useApi(githubAuthApiRef); - const baseUrl = useBaseUrl(); + const { entity } = useEntity(); + const hostname = getHostname(entity); const [total, setTotal] = useState(0); const [totalResults, setTotalResults] = useState(0); const [page, setPage] = useState(0); @@ -70,7 +71,6 @@ export function usePullRequests({ retry, error, } = useAsyncRetry(async () => { - const token = await auth.getAccessToken(['repo']); if (!repo) { return []; } @@ -78,14 +78,13 @@ export function usePullRequests({ api // GitHub API pagination count starts from 1 .listPullRequests({ - token, search, owner, repo, pageSize, page: page + 1, branch, - baseUrl, + hostname, }) .then( ({ diff --git a/plugins/frontend/backstage-plugin-github-pull-requests/src/components/usePullRequestsStatistics.ts b/plugins/frontend/backstage-plugin-github-pull-requests/src/components/usePullRequestsStatistics.ts index 7b7c33ca5..271561041 100644 --- a/plugins/frontend/backstage-plugin-github-pull-requests/src/components/usePullRequestsStatistics.ts +++ b/plugins/frontend/backstage-plugin-github-pull-requests/src/components/usePullRequestsStatistics.ts @@ -1,5 +1,5 @@ /* - * Copyright 2021 Larder Software Limited + * Copyright 2025 Larder Software Limited * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,11 +15,12 @@ */ import { useAsync } from 'react-use'; -import { githubPullRequestsApiRef } from '../api/GithubPullRequestsApi'; -import { useApi, githubAuthApiRef } from '@backstage/core-plugin-api'; -import { useBaseUrl } from './useBaseUrl'; +import { githubPullRequestsApiRef } from '../api'; +import { useApi } from '@backstage/core-plugin-api'; import { PullRequestState, SearchPullRequestsResponseData } from '../types'; import { Duration } from 'luxon'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import { getHostname } from '../utils/githubUtils'; export type PullRequestStats = { avgTimeUntilMerge: string; @@ -109,15 +110,13 @@ export function usePullRequestsStatistics({ state: PullRequestState; }) { const api = useApi(githubPullRequestsApiRef); - const auth = useApi(githubAuthApiRef); - const baseUrl = useBaseUrl(); - + const { entity } = useEntity(); + const hostname = getHostname(entity); const { loading, value: statsData, error, } = useAsync(async (): Promise => { - const token = await auth.getAccessToken(['repo']); if (!repo) { return { avgTimeUntilMerge: 'Never', @@ -135,13 +134,12 @@ export function usePullRequestsStatistics({ pullRequestsData: SearchPullRequestsResponseData; } = await api.listPullRequests({ search: `state:${state}`, - token, + hostname, owner, repo, pageSize, page: 1, branch, - baseUrl, }); const botUsernames = [ @@ -158,12 +156,10 @@ export function usePullRequestsStatistics({ .map(async pr => { const repoData = await api.getRepositoryData({ url: pr.pull_request.url, - baseUrl, - token, + hostname, }); const commitDate = await api.getCommitDetailsData({ - baseUrl, - token, + hostname, owner, repo, number: pr.number, diff --git a/plugins/frontend/backstage-plugin-github-pull-requests/src/mocks/mocks.ts b/plugins/frontend/backstage-plugin-github-pull-requests/src/mocks/mocks.ts index d4b4152b1..02883c674 100644 --- a/plugins/frontend/backstage-plugin-github-pull-requests/src/mocks/mocks.ts +++ b/plugins/frontend/backstage-plugin-github-pull-requests/src/mocks/mocks.ts @@ -27,7 +27,7 @@ export const groupEntityMock = { namespace: 'default', annotations: { 'backstage.io/managed-by-location': - 'url:https://raw.githubusercontent.com/RoadieHQ/sample-service/main/admin-group.yaml', + 'url:https://github.com/org/repo/blob/master/catalog-info.yaml', }, name: 'roadie-backstage-admin', description: 'The Backstage Admins', @@ -55,7 +55,7 @@ export const groupEntityMockWithSlug = { namespace: 'default', annotations: { 'backstage.io/managed-by-location': - 'url:https://raw.githubusercontent.com/RoadieHQ/sample-service/main/admin-group.yaml', + 'url:https://github.com/org/repo/blob/master/catalog-info.yaml', 'github.com/team-slug': 'rroadie-backstage-admin', }, name: 'roadie-backstage-admin', diff --git a/plugins/frontend/backstage-plugin-github-pull-requests/src/components/useBaseUrl.ts b/plugins/frontend/backstage-plugin-github-pull-requests/src/mocks/scmIntegrationsApiMock.ts similarity index 62% rename from plugins/frontend/backstage-plugin-github-pull-requests/src/components/useBaseUrl.ts rename to plugins/frontend/backstage-plugin-github-pull-requests/src/mocks/scmIntegrationsApiMock.ts index bde1fd814..c26c19d25 100644 --- a/plugins/frontend/backstage-plugin-github-pull-requests/src/components/useBaseUrl.ts +++ b/plugins/frontend/backstage-plugin-github-pull-requests/src/mocks/scmIntegrationsApiMock.ts @@ -14,13 +14,19 @@ * limitations under the License. */ -import { useApi, configApiRef } from '@backstage/core-plugin-api'; - -export const useBaseUrl = () => { - const config = useApi(configApiRef); - const providerConfigs = - config.getOptionalConfigArray('integrations.github') ?? []; - const targetProviderConfig = providerConfigs[0]; - const baseUrl = targetProviderConfig?.getOptionalString('apiBaseUrl'); - return baseUrl; +export const defaultIntegrationsConfig = { + integrations: { + github: [ + { + host: 'fake', + apiBaseUrl: 'https://fake', + token: 'fake', + }, + { + host: 'github.com', + apiBaseUrl: 'https://api.github.com', + token: 'asdf', + }, + ], + }, }; diff --git a/plugins/frontend/backstage-plugin-github-pull-requests/src/plugin.ts b/plugins/frontend/backstage-plugin-github-pull-requests/src/plugin.ts index aa5a107b6..49ba993b7 100644 --- a/plugins/frontend/backstage-plugin-github-pull-requests/src/plugin.ts +++ b/plugins/frontend/backstage-plugin-github-pull-requests/src/plugin.ts @@ -20,10 +20,12 @@ import { createRouteRef, createRoutableExtension, createComponentExtension, + configApiRef, } from '@backstage/core-plugin-api'; import { createCardExtension } from '@backstage/plugin-home-react'; import { githubPullRequestsApiRef, GithubPullRequestsClient } from './api'; +import { scmAuthApiRef } from '@backstage/integration-react'; export const entityContentRouteRef = createRouteRef({ id: 'github-pull-requests', @@ -31,8 +33,14 @@ export const entityContentRouteRef = createRouteRef({ export const githubPullRequestsPlugin = createPlugin({ id: 'github-pull-requests', + apis: [ - createApiFactory(githubPullRequestsApiRef, new GithubPullRequestsClient()), + createApiFactory({ + api: githubPullRequestsApiRef, + deps: { configApi: configApiRef, scmAuthApi: scmAuthApiRef }, + factory: ({ configApi, scmAuthApi }) => + new GithubPullRequestsClient({ configApi, scmAuthApi }), + }), ], routes: { entityContent: entityContentRouteRef, diff --git a/plugins/frontend/backstage-plugin-github-pull-requests/src/utils/githubUtils.ts b/plugins/frontend/backstage-plugin-github-pull-requests/src/utils/githubUtils.ts new file mode 100644 index 000000000..d4791954e --- /dev/null +++ b/plugins/frontend/backstage-plugin-github-pull-requests/src/utils/githubUtils.ts @@ -0,0 +1,16 @@ +import { + ANNOTATION_LOCATION, + ANNOTATION_SOURCE_LOCATION, + Entity, +} from '@backstage/catalog-model'; +import gitUrlParse from 'git-url-parse'; + +export const getHostname = (entity: Entity) => { + const location = + entity?.metadata.annotations?.[ANNOTATION_SOURCE_LOCATION] ?? + entity?.metadata.annotations?.[ANNOTATION_LOCATION]; + + return location?.startsWith('url:') + ? gitUrlParse(location.slice(4)).resource + : undefined; +}; diff --git a/yarn.lock b/yarn.lock index 0abe3ab9f..790a63ff3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5816,6 +5816,15 @@ "@backstage/types" "^1.2.0" ms "^2.1.3" +"@backstage/config@^1.3.1": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@backstage/config/-/config-1.3.1.tgz#19669d1c7c923e6f612aa02446226e7f2f71ff0a" + integrity sha512-2WcfyfE06zqhVxuKRj6vrKXBk9iIqLuAMCcu4EZdyW6vmsiPo8t7Y54CB8ma1u3+GJf5hct9p/xiqIjeObyOFA== + dependencies: + "@backstage/errors" "^1.2.6" + "@backstage/types" "^1.2.0" + ms "^2.1.3" + "@backstage/core-app-api@^1.15.2": version "1.15.2" resolved "https://registry.yarnpkg.com/@backstage/core-app-api/-/core-app-api-1.15.2.tgz#a31aae0e7a1cfcb2eab163b75b4141a31cff1982" @@ -5952,6 +5961,17 @@ "@backstage/version-bridge" "^1.0.10" history "^5.0.0" +"@backstage/core-plugin-api@^1.10.2": + version "1.10.2" + resolved "https://registry.yarnpkg.com/@backstage/core-plugin-api/-/core-plugin-api-1.10.2.tgz#0cbf3a1e06fc1231bcab18e85140f161cb419b0f" + integrity sha512-uye1xq1dklizvsOyj8WLQ0/SJcVARKZkVzpRzD3CgKlaMl2Sj5dbxPeMeuu67mbTyBmxEF6QyEpwgNSosRdGBw== + dependencies: + "@backstage/config" "^1.3.1" + "@backstage/errors" "^1.2.6" + "@backstage/types" "^1.2.0" + "@backstage/version-bridge" "^1.0.10" + history "^5.0.0" + "@backstage/core-plugin-api@^1.9.2", "@backstage/core-plugin-api@^1.9.3": version "1.9.3" resolved "https://registry.yarnpkg.com/@backstage/core-plugin-api/-/core-plugin-api-1.9.3.tgz#66b4b7dc620823c66b123c8a2d6db088e2936027" @@ -5997,6 +6017,14 @@ "@backstage/types" "^1.2.0" serialize-error "^8.0.1" +"@backstage/errors@^1.2.6": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@backstage/errors/-/errors-1.2.6.tgz#843a73d03433089f0a3b8a4530a53be013c34c6c" + integrity sha512-Zmpet5DS5670RHXIFUY/fcJbUxRbS1D/l+Ai7V9k3qGQw7TfCrtu5hiGaBAi1EjjJ0xdA8L9Y3Smyc7nDWB/YQ== + dependencies: + "@backstage/types" "^1.2.0" + serialize-error "^8.0.1" + "@backstage/eslint-plugin@^0.1.10": version "0.1.10" resolved "https://registry.yarnpkg.com/@backstage/eslint-plugin/-/eslint-plugin-0.1.10.tgz#8f786ccc3c315dfe9b1cd3aa6d8435fd266a0574" @@ -6140,6 +6168,17 @@ "@material-ui/core" "^4.12.2" "@material-ui/icons" "^4.9.1" +"@backstage/integration-react@^1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@backstage/integration-react/-/integration-react-1.2.2.tgz#1683517baaca8bcfb20ff887bc07b60c6243402f" + integrity sha512-M/tk92/gzfRZIh6ver8oRqkl3E98qjGH5+zf6+8BvXGuC3F/TuLPNx2Ey83BPIJkf4qvOJYVyMAk/X6vGumAZQ== + dependencies: + "@backstage/config" "^1.3.1" + "@backstage/core-plugin-api" "^1.10.2" + "@backstage/integration" "^1.16.0" + "@material-ui/core" "^4.12.2" + "@material-ui/icons" "^4.9.1" + "@backstage/integration@^1.10.0", "@backstage/integration@^1.14.0": version "1.14.0" resolved "https://registry.yarnpkg.com/@backstage/integration/-/integration-1.14.0.tgz#a7b3542f3c0cbb1bf902dab864512f6a28718985" @@ -6185,6 +6224,22 @@ lodash "^4.17.21" luxon "^3.0.0" +"@backstage/integration@^1.16.0": + version "1.16.0" + resolved "https://registry.yarnpkg.com/@backstage/integration/-/integration-1.16.0.tgz#b9acb765e0522d679d211f2b3b55acfae4ba214c" + integrity sha512-YGWOQOC338b9OFGbXbZcnNAjIArUy1YSgUrOxo/L8cPkRhEnIJ7F67PkWl+0V0eSrRiC4Gq/DbE/gB0N/1M4oQ== + dependencies: + "@azure/identity" "^4.0.0" + "@azure/storage-blob" "^12.5.0" + "@backstage/config" "^1.3.1" + "@backstage/errors" "^1.2.6" + "@octokit/auth-app" "^4.0.0" + "@octokit/rest" "^19.0.3" + cross-fetch "^4.0.0" + git-url-parse "^15.0.0" + lodash "^4.17.21" + luxon "^3.0.0" + "@backstage/plugin-api-docs@^0.12.1": version "0.12.1" resolved "https://registry.yarnpkg.com/@backstage/plugin-api-docs/-/plugin-api-docs-0.12.1.tgz#54735ea2d18e431f551ba2d320a82697d46e8343" @@ -16291,9 +16346,9 @@ "@types/react" "*" "@types/react@*", "@types/react@^16.13.1 || ^17.0.0", "@types/react@^16.13.1 || ^17.0.0 || ^18.0.0", "@types/react@^18": - version "18.3.16" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.16.tgz#5326789125fac98b718d586ad157442ceb44ff28" - integrity sha512-oh8AMIC4Y2ciKufU8hnKgs+ufgbA/dhPTACaZPM86AbwX9QwnFtSoPWEeRUj8fge+v6kFt78BXcDhAU1SrrAsw== + version "18.3.18" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.18.tgz#9b382c4cd32e13e463f97df07c2ee3bbcd26904b" + integrity sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ== dependencies: "@types/prop-types" "*" csstype "^3.0.2"