diff --git a/.changeset/good-turtles-judge.md b/.changeset/good-turtles-judge.md new file mode 100644 index 000000000..2e3e0d62d --- /dev/null +++ b/.changeset/good-turtles-judge.md @@ -0,0 +1,7 @@ +--- +'@roadiehq/backstage-plugin-github-pull-requests': patch +'@roadiehq/backstage-plugin-security-insights': patch +'@roadiehq/backstage-plugin-github-insights': patch +--- + +Modify login functionality to allow logging in using the Sign In button. diff --git a/packages/app/src/apis.ts b/packages/app/src/apis.ts index fa4977567..904556774 100644 --- a/packages/app/src/apis.ts +++ b/packages/app/src/apis.ts @@ -18,22 +18,66 @@ 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({ api: scmIntegrationsApiRef, 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/backend/src/plugins/auth.ts b/packages/backend/src/plugins/auth.ts index 03851610f..6c22cd74b 100644 --- a/packages/backend/src/plugins/auth.ts +++ b/packages/backend/src/plugins/auth.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createRouter } from '@backstage/plugin-auth-backend'; +import { createRouter, providers } from '@backstage/plugin-auth-backend'; import { Router } from 'express'; import { PluginEnvironment } from '../types'; @@ -30,5 +30,8 @@ export default async function createPlugin({ database, discovery, tokenManager, + providerFactories: { + ghes: providers.github.create(), + }, }); } diff --git a/plugins/frontend/backstage-plugin-github-insights/src/components/GitHubAuthorizationWrapper.tsx b/plugins/frontend/backstage-plugin-github-insights/src/components/GitHubAuthorizationWrapper.tsx new file mode 100644 index 000000000..b439479d9 --- /dev/null +++ b/plugins/frontend/backstage-plugin-github-insights/src/components/GitHubAuthorizationWrapper.tsx @@ -0,0 +1,100 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + scmAuthApiRef, + ScmAuthTokenResponse, +} from '@backstage/integration-react'; +import { useApi } from '@backstage/core-plugin-api'; +import React, { useCallback, useState } from 'react'; +import { Button, Grid, Tooltip, Typography } from '@material-ui/core'; +import { InfoCard } from '@backstage/core-components'; +import { useGithubLoggedIn } from '../hooks/useGithubLoggedIn'; + +export const GithubNotAuthorized = ({ + hostname = 'github.com', + validateCredentials, +}: { + hostname?: string; + validateCredentials: (credentials: ScmAuthTokenResponse) => void; +}) => { + const scmAuth = useApi(scmAuthApiRef); + + const signIn = useCallback(async () => { + const credentials = await scmAuth.getCredentials({ + url: `https://${hostname}/`, + additionalScope: { + customScopes: { + github: ['repo'], + }, + }, + }); + validateCredentials(credentials); + }, [scmAuth, hostname, validateCredentials]); + + return ( + + + + You are not logged into GitHub. You need to be signed in to see the + content of this card. + + + + + + + + + ); +}; + +export const GitHubAuthorizationWrapper = ({ + children, + title, + hostname, +}: { + children: React.ReactNode; + title: string; + hostname?: string; +}) => { + const isLoggedIn = useGithubLoggedIn(); + const [credentialsValidated, setCredentialsValidated] = useState(false); + const validateCredentials = useCallback( + (credentials: ScmAuthTokenResponse) => { + if (isLoggedIn) { + return; + } + + if (credentials.token) { + setCredentialsValidated(true); + } + }, + [isLoggedIn], + ); + return isLoggedIn || credentialsValidated ? ( + children + ) : ( + + + + ); +}; diff --git a/plugins/frontend/backstage-plugin-github-insights/src/components/Widgets/ComplianceCard/ComplianceCard.tsx b/plugins/frontend/backstage-plugin-github-insights/src/components/Widgets/ComplianceCard/ComplianceCard.tsx index d0a3562e8..c484b1d7f 100644 --- a/plugins/frontend/backstage-plugin-github-insights/src/components/Widgets/ComplianceCard/ComplianceCard.tsx +++ b/plugins/frontend/backstage-plugin-github-insights/src/components/Widgets/ComplianceCard/ComplianceCard.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. @@ -36,8 +36,10 @@ import { import WarningIcon from '@material-ui/icons/ErrorOutline'; import { styles as useStyles } from '../../utils/styles'; import { useEntity } from '@backstage/plugin-catalog-react'; +import { getHostname } from '../../utils/githubUtils'; +import { GitHubAuthorizationWrapper } from '../../GitHubAuthorizationWrapper'; -const ComplianceCard = () => { +const ComplianceCardContent = () => { const { entity } = useEntity(); const { branches, loading, error } = useProtectedBranches(entity); const { @@ -59,7 +61,7 @@ const ComplianceCard = () => { } else if (error || licenseError) { return ( - Error occured while fetching data for the compliance card:{' '} + Error occurred while fetching data for the compliance card:{' '} {error?.message} ); @@ -109,4 +111,15 @@ const ComplianceCard = () => { ); }; +const ComplianceCard = () => { + const { entity } = useEntity(); + const hostname = getHostname(entity); + + return ( + + + + ); +}; + export default ComplianceCard; diff --git a/plugins/frontend/backstage-plugin-github-insights/src/components/Widgets/ContributorsCard/ContributorsCard.tsx b/plugins/frontend/backstage-plugin-github-insights/src/components/Widgets/ContributorsCard/ContributorsCard.tsx index 96cdae39b..d6fcbd9d7 100644 --- a/plugins/frontend/backstage-plugin-github-insights/src/components/Widgets/ContributorsCard/ContributorsCard.tsx +++ b/plugins/frontend/backstage-plugin-github-insights/src/components/Widgets/ContributorsCard/ContributorsCard.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,22 +19,20 @@ import { makeStyles } from '@material-ui/core/styles'; import Alert from '@material-ui/lab/Alert'; import { InfoCard, - Progress, MissingAnnotationEmptyState, + Progress, } from '@backstage/core-components'; import ContributorsList from './components/ContributorsList'; import { useRequest } from '../../../hooks/useRequest'; import { useEntityGithubScmIntegration } from '../../../hooks/useEntityGithubScmIntegration'; import { useProjectEntity } from '../../../hooks/useProjectEntity'; import { - isGithubInsightsAvailable, GITHUB_INSIGHTS_ANNOTATION, + isGithubInsightsAvailable, } from '../../utils/isGithubInsightsAvailable'; import { useEntity } from '@backstage/plugin-catalog-react'; -import { - GithubNotAuthorized, - useGithubLoggedIn, -} from '../../../hooks/useGithubLoggedIn'; +import { getHostname } from '../../utils/githubUtils'; +import { GitHubAuthorizationWrapper } from '../../GitHubAuthorizationWrapper'; const useStyles = makeStyles(theme => ({ infoCard: { @@ -89,15 +87,13 @@ const ContributorsCardContent = () => { }; const ContributorsCard = () => { - const classes = useStyles(); - const isLoggedIn = useGithubLoggedIn(); + const { entity } = useEntity(); + const hostname = getHostname(entity); - return isLoggedIn ? ( - - ) : ( - - - + return ( + + + ); }; diff --git a/plugins/frontend/backstage-plugin-github-insights/src/components/Widgets/EnvironmentsCard/EnvironmentsCard.tsx b/plugins/frontend/backstage-plugin-github-insights/src/components/Widgets/EnvironmentsCard/EnvironmentsCard.tsx index a977fd5e1..587ce3780 100644 --- a/plugins/frontend/backstage-plugin-github-insights/src/components/Widgets/EnvironmentsCard/EnvironmentsCard.tsx +++ b/plugins/frontend/backstage-plugin-github-insights/src/components/Widgets/EnvironmentsCard/EnvironmentsCard.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. @@ -31,6 +31,8 @@ import { } from '../../utils/isGithubInsightsAvailable'; import { useEntity } from '@backstage/plugin-catalog-react'; import { styles as useStyles } from '../../utils/styles'; +import { getHostname } from '../../utils/githubUtils'; +import { GitHubAuthorizationWrapper } from '../../GitHubAuthorizationWrapper'; type Environment = { id: number; @@ -38,7 +40,7 @@ type Environment = { name: string; }; -const EnvironmentsCard = () => { +const EnvironmentsCardContent = () => { const classes = useStyles(); const { entity } = useEntity(); @@ -96,4 +98,15 @@ const EnvironmentsCard = () => { ); }; +const EnvironmentsCard = () => { + const { entity } = useEntity(); + const hostname = getHostname(entity); + + return ( + + + + ); +}; + export default EnvironmentsCard; diff --git a/plugins/frontend/backstage-plugin-github-insights/src/components/Widgets/LanguagesCard/LanguagesCard.tsx b/plugins/frontend/backstage-plugin-github-insights/src/components/Widgets/LanguagesCard/LanguagesCard.tsx index 52bd8f9b3..8f19b03a0 100644 --- a/plugins/frontend/backstage-plugin-github-insights/src/components/Widgets/LanguagesCard/LanguagesCard.tsx +++ b/plugins/frontend/backstage-plugin-github-insights/src/components/Widgets/LanguagesCard/LanguagesCard.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,25 +16,22 @@ import React from 'react'; import { Chip, makeStyles, Tooltip } from '@material-ui/core'; -// eslint-disable-next-line import Alert from '@material-ui/lab/Alert'; import { InfoCard, - Progress, MissingAnnotationEmptyState, + Progress, } from '@backstage/core-components'; import { useRequest } from '../../../hooks/useRequest'; import { colors } from './colors'; import { useProjectEntity } from '../../../hooks/useProjectEntity'; import { - GithubNotAuthorized, - useGithubLoggedIn, -} from '../../../hooks/useGithubLoggedIn'; -import { - isGithubInsightsAvailable, GITHUB_INSIGHTS_ANNOTATION, + isGithubInsightsAvailable, } from '../../utils/isGithubInsightsAvailable'; import { useEntity } from '@backstage/plugin-catalog-react'; +import { getHostname } from '../../utils/githubUtils'; +import { GitHubAuthorizationWrapper } from '../../GitHubAuthorizationWrapper'; const useStyles = makeStyles(theme => ({ infoCard: { @@ -159,15 +156,13 @@ const LanguagesCardContent = () => { }; const LanguagesCard = () => { - const classes = useStyles(); - const isLoggedIn = useGithubLoggedIn(); + const { entity } = useEntity(); + const hostname = getHostname(entity); - return isLoggedIn ? ( - - ) : ( - - - + return ( + + + ); }; 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 f1012261a..bcd6375fc 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 @@ -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,7 +14,7 @@ * limitations under the License. */ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback } from 'react'; import { Alert } from '@material-ui/lab'; import { MarkdownContent as RawMarkdownContent, @@ -23,14 +23,13 @@ import { import { ApiHolder, configApiRef, - useApi, useApiHolder, } from '@backstage/core-plugin-api'; 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'; +import { GitHubAuthorizationWrapper } from '../../GitHubAuthorizationWrapper'; const getGithubClient = (apiHolder: ApiHolder) => { let githubClient: GithubApi | undefined = apiHolder.get(githubApiRef); @@ -96,43 +95,6 @@ const GithubFileContent = (props: MarkdownContentProps) => { ); }; -const GithubNotAuthorized = ({ - hostname = 'github.com', -}: { - hostname?: string; -}) => { - const scmAuth = useApi(scmAuthApiRef); - return ( - - - - You are not logged into github. You need to be signed in to see the - content of this card. - - - - - - - - - ); -}; - /** * A component to render a markdown file from github * @@ -140,32 +102,10 @@ const GithubNotAuthorized = ({ */ const MarkdownContent = (props: MarkdownContentProps) => { const { hostname } = props; - const scmAuth = useApi(scmAuthApiRef); - const [isLoggedIn, setIsLoggedIn] = useState(false); - - const githubUrl = hostname ? `https://${hostname}` : 'https://github.com'; - - useEffect(() => { - const doLogin = async () => { - const credentials = await scmAuth.getCredentials({ - additionalScope: { - customScopes: { github: ['repo'] }, - }, - url: githubUrl, - optional: true, - }); - - if (credentials?.token) { - setIsLoggedIn(true); - } - }; - doLogin(); - }, [scmAuth, githubUrl]); - - return isLoggedIn ? ( - - ) : ( - + return ( + + + ); }; diff --git a/plugins/frontend/backstage-plugin-github-insights/src/components/Widgets/ReleasesCard/ReleasesCard.tsx b/plugins/frontend/backstage-plugin-github-insights/src/components/Widgets/ReleasesCard/ReleasesCard.tsx index 9d0d9f2b8..2c6a3aea5 100644 --- a/plugins/frontend/backstage-plugin-github-insights/src/components/Widgets/ReleasesCard/ReleasesCard.tsx +++ b/plugins/frontend/backstage-plugin-github-insights/src/components/Widgets/ReleasesCard/ReleasesCard.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,27 +15,25 @@ */ import React from 'react'; -import { Link, List, ListItem, Chip } from '@material-ui/core'; +import { Chip, Link, List, ListItem } from '@material-ui/core'; import LocalOfferOutlinedIcon from '@material-ui/icons/LocalOfferOutlined'; import Alert from '@material-ui/lab/Alert'; import { InfoCard, - Progress, MissingAnnotationEmptyState, + Progress, } from '@backstage/core-components'; import { useRequest } from '../../../hooks/useRequest'; import { useEntityGithubScmIntegration } from '../../../hooks/useEntityGithubScmIntegration'; import { useProjectEntity } from '../../../hooks/useProjectEntity'; import { - isGithubInsightsAvailable, GITHUB_INSIGHTS_ANNOTATION, + isGithubInsightsAvailable, } from '../../utils/isGithubInsightsAvailable'; import { useEntity } from '@backstage/plugin-catalog-react'; import { styles as useStyles } from '../../utils/styles'; -import { - GithubNotAuthorized, - useGithubLoggedIn, -} from '../../../hooks/useGithubLoggedIn'; +import { GitHubAuthorizationWrapper } from '../../GitHubAuthorizationWrapper'; +import { getHostname } from '../../utils/githubUtils'; type Release = { id: number; @@ -113,15 +111,13 @@ const ReleasesCardContent = () => { }; const ReleasesCard = () => { - const classes = useStyles(); - const isLoggedIn = useGithubLoggedIn(); + const { entity } = useEntity(); + const hostname = getHostname(entity); - return isLoggedIn ? ( - - ) : ( - - - + return ( + + + ); }; diff --git a/plugins/frontend/backstage-plugin-github-insights/src/components/utils/githubUtils.ts b/plugins/frontend/backstage-plugin-github-insights/src/components/utils/githubUtils.ts new file mode 100644 index 000000000..cf917e9bb --- /dev/null +++ b/plugins/frontend/backstage-plugin-github-insights/src/components/utils/githubUtils.ts @@ -0,0 +1,31 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +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/plugins/frontend/backstage-plugin-github-pull-requests/src/components/GitHubAuthorizationWrapper.tsx b/plugins/frontend/backstage-plugin-github-pull-requests/src/components/GitHubAuthorizationWrapper.tsx new file mode 100644 index 000000000..10536a185 --- /dev/null +++ b/plugins/frontend/backstage-plugin-github-pull-requests/src/components/GitHubAuthorizationWrapper.tsx @@ -0,0 +1,84 @@ +import { + scmAuthApiRef, + ScmAuthTokenResponse, +} from '@backstage/integration-react'; +import { useApi } from '@backstage/core-plugin-api'; +import React, { useCallback, useState } from 'react'; +import { Button, Grid, Tooltip, Typography } from '@material-ui/core'; +import { useGithubLoggedIn } from './useGithubLoggedIn'; +import { InfoCard } from '@backstage/core-components'; + +export const GithubNotAuthorized = ({ + hostname = 'github.com', + validateCredentials, +}: { + hostname?: string; + validateCredentials: (credentials: ScmAuthTokenResponse) => void; +}) => { + const scmAuth = useApi(scmAuthApiRef); + + const signIn = useCallback(async () => { + const credentials = await scmAuth.getCredentials({ + url: `https://${hostname}/`, + additionalScope: { + customScopes: { + github: ['repo'], + }, + }, + }); + validateCredentials(credentials); + }, [scmAuth, hostname, validateCredentials]); + + return ( + + + + You are not logged into GitHub. You need to be signed in to see the + content of this card. + + + + + + + + + ); +}; + +export const GitHubAuthorizationWrapper = ({ + children, + title, + hostname, +}: { + children: React.ReactNode; + title: string; + hostname?: string; +}) => { + const isLoggedIn = useGithubLoggedIn(); + const [credentialsValidated, setCredentialsValidated] = useState(false); + const validateCredentials = useCallback( + (credentials: ScmAuthTokenResponse) => { + if (isLoggedIn) { + return; + } + + if (credentials.token) { + setCredentialsValidated(true); + } + }, + [isLoggedIn], + ); + return isLoggedIn || credentialsValidated ? ( + children + ) : ( + + + + ); +}; 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 2a2b53374..05c20ce66 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 @@ -84,7 +84,19 @@ describe('', () => { render( wrapInTestApp( - + , @@ -93,7 +105,7 @@ describe('', () => { ); expect( - await screen.findByText('You are not logged into github.', { + await screen.findByText('You are not logged into GitHub.', { exact: false, }), ).toBeInTheDocument(); 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 6be0f5ec5..7e222f00f 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 @@ -1,4 +1,6 @@ /* + * 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. * You may obtain a copy of the License at @@ -11,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React from 'react'; import { MissingAnnotationEmptyState } from '@backstage/core-components'; import { @@ -18,15 +21,15 @@ import { SkeletonPullRequestsListView, } from '../PullRequestsListView'; import { useGithubSearchPullRequest } from '../useGithubSearchPullRequest'; -import { useGithubLoggedIn, GithubNotAuthorized } from '../useGithubLoggedIn'; import { - isGithubTeamSlugSet, GITHUB_PULL_REQUESTS_TEAM_ANNOTATION, + isGithubTeamSlugSet, } from '../../utils/isGithubSlugSet'; 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'; +import { GitHubAuthorizationWrapper } from '../GitHubAuthorizationWrapper'; export const getPullRequestsQueryForGroup = (entity: Entity) => { const githubTeamName = isGithubTeamSlugSet(entity); @@ -51,10 +54,6 @@ const PullRequestsCard = () => { export const Content = () => { const { entity } = useEntity(); const hostname = getHostname(entity); - const isLoggedIn = useGithubLoggedIn(hostname); - if (!isLoggedIn) { - return ; - } const githubTeamName = isGithubTeamSlugSet(entity); if (!githubTeamName || githubTeamName === '') { return ( @@ -63,10 +62,20 @@ export const Content = () => { /> ); } - if (isGroupEntity(entity)) { - return ; + if (!isGroupEntity(entity)) { + return ( + + This card can only be used on Group Entities + + ); } + return ( - This card can only be used on Group Entities + + + ); }; diff --git a/plugins/frontend/backstage-plugin-github-pull-requests/src/components/Home/RequestedReviewsCard/Content.tsx b/plugins/frontend/backstage-plugin-github-pull-requests/src/components/Home/RequestedReviewsCard/Content.tsx index 8f79d92c1..bd3bdba97 100644 --- a/plugins/frontend/backstage-plugin-github-pull-requests/src/components/Home/RequestedReviewsCard/Content.tsx +++ b/plugins/frontend/backstage-plugin-github-pull-requests/src/components/Home/RequestedReviewsCard/Content.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. @@ -20,11 +20,8 @@ import { SkeletonPullRequestsListView, } from '../../PullRequestsListView'; import { useGithubSearchPullRequest } from '../../useGithubSearchPullRequest'; -import { - useGithubLoggedIn, - GithubNotAuthorized, -} from '../../useGithubLoggedIn'; import Alert from '@material-ui/lab/Alert'; +import { GitHubAuthorizationWrapper } from '../../GitHubAuthorizationWrapper'; type RequestedReviewsCardProps = { query?: string; @@ -44,11 +41,9 @@ const RequestedReviewsContent = (props: RequestedReviewsCardProps) => { ); }; export const Content = (props: RequestedReviewsCardProps) => { - const isLoggedIn = useGithubLoggedIn(); - - return isLoggedIn ? ( - - ) : ( - + return ( + + + ); }; diff --git a/plugins/frontend/backstage-plugin-github-pull-requests/src/components/Home/YourOpenPullRequestsCard/Content.tsx b/plugins/frontend/backstage-plugin-github-pull-requests/src/components/Home/YourOpenPullRequestsCard/Content.tsx index a1aa52255..9f216bcad 100644 --- a/plugins/frontend/backstage-plugin-github-pull-requests/src/components/Home/YourOpenPullRequestsCard/Content.tsx +++ b/plugins/frontend/backstage-plugin-github-pull-requests/src/components/Home/YourOpenPullRequestsCard/Content.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. @@ -20,11 +20,8 @@ import { SkeletonPullRequestsListView, } from '../../PullRequestsListView'; import { useGithubSearchPullRequest } from '../../useGithubSearchPullRequest'; -import { - useGithubLoggedIn, - GithubNotAuthorized, -} from '../../useGithubLoggedIn'; import Alert from '@material-ui/lab/Alert'; +import { GitHubAuthorizationWrapper } from '../../GitHubAuthorizationWrapper'; type OpenPullRequestsCardProps = { query?: string; @@ -48,10 +45,9 @@ const OpenPullRequestsContent = (props: OpenPullRequestsCardProps) => { }; export const Content = (props: OpenPullRequestsCardProps) => { - const isLoggedIn = useGithubLoggedIn(); - return isLoggedIn ? ( - - ) : ( - + return ( + + + ); }; 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 e019e1aee..abb92426d 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 @@ -39,7 +39,8 @@ import { import { Entity } from '@backstage/catalog-model'; import { useEntity } from '@backstage/plugin-catalog-react'; import { TooltipContent } from './components/TooltipContent'; -import { GithubNotAuthorized, useGithubLoggedIn } from '../useGithubLoggedIn'; +import { GitHubAuthorizationWrapper } from '../GitHubAuthorizationWrapper'; +import { getHostname } from '../../utils/githubUtils'; const useStyles = makeStyles(theme => ({ infoCard: { @@ -129,9 +130,8 @@ const StatsCard = (props: Props) => { const PullRequestsStatsCard = (props: Props) => { const { entity } = useEntity(); + const hostname = getHostname(entity); const projectName = isGithubSlugSet(entity); - const isLoggedIn = useGithubLoggedIn(); - if (!projectName || projectName === '') { return ( { /> ); } - - return isLoggedIn ? ( - - ) : ( - - - + return ( + + + ); }; 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 73deb5da9..0b6ce0283 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 @@ -45,6 +45,8 @@ import { PullRequestState } from '../../types'; import { Entity } from '@backstage/catalog-model'; import { getStatusIconType } from '../Icons'; import { useEntity } from '@backstage/plugin-catalog-react'; +import { GitHubAuthorizationWrapper } from '../GitHubAuthorizationWrapper'; +import { getHostname } from '../../utils/githubUtils'; const generatedColumns: TableColumn[] = [ { @@ -322,6 +324,7 @@ const PullRequests = (__props: TableProps) => { export const PullRequestsTable = (__props: TableProps) => { const { entity } = useEntity(); + const hostname = getHostname(entity); const projectName = isGithubSlugSet(entity); if (!projectName || projectName === '') { return ( @@ -330,5 +333,9 @@ export const PullRequestsTable = (__props: TableProps) => { /> ); } - return ; + return ( + + + + ); }; 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 c013de8cd..3d6a12834 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,48 +1,9 @@ -import React, { useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; 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); - - return ( - - - - You are not logged into GitHub. You need to be signed in to see the - content of this card. - - - - - - - - - ); -}; +import { ScmAuthApi, scmAuthApiRef } from '@backstage/integration-react'; export const useGithubLoggedIn = (hostname: string = 'github.com') => { - const scmAuth = useApi(scmAuthApiRef); + const scmAuth: ScmAuthApi = useApi(scmAuthApiRef); const [isLoggedIn, setIsLoggedIn] = useState(false); useEffect(() => { diff --git a/plugins/frontend/backstage-plugin-security-insights/package.json b/plugins/frontend/backstage-plugin-security-insights/package.json index 7196e7892..5387bb5ba 100644 --- a/plugins/frontend/backstage-plugin-security-insights/package.json +++ b/plugins/frontend/backstage-plugin-security-insights/package.json @@ -56,6 +56,7 @@ "@material-ui/lab": "^4.0.0-alpha.45", "@octokit/graphql": "^5.0.0", "@octokit/rest": "^19.0.3", + "git-url-parse": "^16.0.0", "cross-fetch": "^3.1.4", "history": "^5.0.0", "luxon": "^3.0.0", diff --git a/plugins/frontend/backstage-plugin-security-insights/src/components/DependabotAlertsWidget/DependabotAlertsWidget.tsx b/plugins/frontend/backstage-plugin-security-insights/src/components/DependabotAlertsWidget/DependabotAlertsWidget.tsx index 7479c0065..a2caf5293 100644 --- a/plugins/frontend/backstage-plugin-security-insights/src/components/DependabotAlertsWidget/DependabotAlertsWidget.tsx +++ b/plugins/frontend/backstage-plugin-security-insights/src/components/DependabotAlertsWidget/DependabotAlertsWidget.tsx @@ -15,7 +15,6 @@ */ import React, { FC } from 'react'; -// eslint-disable-next-line import Alert from '@material-ui/lab/Alert'; import { Box, Grid, makeStyles, Theme, Typography } from '@material-ui/core'; import { graphql } from '@octokit/graphql'; @@ -25,8 +24,9 @@ import { useProjectEntity } from '../useProjectEntity'; import { useUrl } from '../useUrl'; import { useEntity } from '@backstage/plugin-catalog-react'; import { InfoCard, Progress } from '@backstage/core-components'; -import { GithubNotAuthorized, useGithubLoggedIn } from '../useGithubLoggedIn'; import { scmAuthApiRef } from '@backstage/integration-react'; +import { getHostname } from '../utils'; +import { GitHubAuthorizationWrapper } from '../GitHubAuthorizationWrapper'; const useStyles = makeStyles((theme: Theme) => ({ infoCard: { @@ -289,14 +289,12 @@ const DependabotAlertsWidgetContent = () => { }; export const DependabotAlertsWidget = () => { - const classes = useStyles(); - const isLoggedIn = useGithubLoggedIn(); + const { entity } = useEntity(); + const hostname = getHostname(entity); - return isLoggedIn ? ( - - ) : ( - - - + return ( + + + ); }; diff --git a/plugins/frontend/backstage-plugin-security-insights/src/components/GitHubAuthorizationWrapper.tsx b/plugins/frontend/backstage-plugin-security-insights/src/components/GitHubAuthorizationWrapper.tsx new file mode 100644 index 000000000..d5a386b38 --- /dev/null +++ b/plugins/frontend/backstage-plugin-security-insights/src/components/GitHubAuthorizationWrapper.tsx @@ -0,0 +1,100 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + scmAuthApiRef, + ScmAuthTokenResponse, +} from '@backstage/integration-react'; +import { useApi } from '@backstage/core-plugin-api'; +import React, { useCallback, useState } from 'react'; +import { Button, Grid, Tooltip, Typography } from '@material-ui/core'; +import { InfoCard } from '@backstage/core-components'; +import { useGithubLoggedIn } from './useGithubLoggedIn'; + +export const GithubNotAuthorized = ({ + hostname = 'github.com', + validateCredentials, +}: { + hostname?: string; + validateCredentials: (credentials: ScmAuthTokenResponse) => void; +}) => { + const scmAuth = useApi(scmAuthApiRef); + + const signIn = useCallback(async () => { + const credentials = await scmAuth.getCredentials({ + url: `https://${hostname}/`, + additionalScope: { + customScopes: { + github: ['repo'], + }, + }, + }); + validateCredentials(credentials); + }, [scmAuth, hostname, validateCredentials]); + + return ( + + + + You are not logged into GitHub. You need to be signed in to see the + content of this card. + + + + + + + + + ); +}; + +export const GitHubAuthorizationWrapper = ({ + children, + title, + hostname, +}: { + children: React.ReactNode; + title: string; + hostname?: string; +}) => { + const isLoggedIn = useGithubLoggedIn(); + const [credentialsValidated, setCredentialsValidated] = useState(false); + const validateCredentials = useCallback( + (credentials: ScmAuthTokenResponse) => { + if (isLoggedIn) { + return; + } + + if (credentials.token) { + setCredentialsValidated(true); + } + }, + [isLoggedIn], + ); + return isLoggedIn || credentialsValidated ? ( + children + ) : ( + + + + ); +}; diff --git a/plugins/frontend/backstage-plugin-security-insights/src/components/SecurityInsightsWidget/SecurityInsightsWidget.tsx b/plugins/frontend/backstage-plugin-security-insights/src/components/SecurityInsightsWidget/SecurityInsightsWidget.tsx index 4bec380fa..29330ab2e 100644 --- a/plugins/frontend/backstage-plugin-security-insights/src/components/SecurityInsightsWidget/SecurityInsightsWidget.tsx +++ b/plugins/frontend/backstage-plugin-security-insights/src/components/SecurityInsightsWidget/SecurityInsightsWidget.tsx @@ -27,7 +27,7 @@ import { useAsync } from 'react-use'; import { Octokit } from '@octokit/rest'; import { useProjectEntity } from '../useProjectEntity'; import { useUrl } from '../useUrl'; -import { getSeverityBadge } from '../utils'; +import { getHostname, getSeverityBadge } from '../utils'; import { IssuesCounterProps, SecurityInsight, @@ -35,8 +35,8 @@ import { SeverityLevels, } from '../../types'; import { useEntity } from '@backstage/plugin-catalog-react'; -import { GithubNotAuthorized, useGithubLoggedIn } from '../useGithubLoggedIn'; import { scmAuthApiRef } from '@backstage/integration-react'; +import { GitHubAuthorizationWrapper } from '../GitHubAuthorizationWrapper'; const useStyles = makeStyles(theme => ({ infoCard: { @@ -148,14 +148,12 @@ const SecurityInsightsWidgetContent = () => { }; export const SecurityInsightsWidget = () => { - const classes = useStyles(); - const isLoggedIn = useGithubLoggedIn(); + const { entity } = useEntity(); + const hostname = getHostname(entity); - return isLoggedIn ? ( - - ) : ( - - - + return ( + + + ); }; diff --git a/plugins/frontend/backstage-plugin-security-insights/src/components/utils.tsx b/plugins/frontend/backstage-plugin-security-insights/src/components/utils.tsx index 01d5ceb6f..6cd2aa9f3 100644 --- a/plugins/frontend/backstage-plugin-security-insights/src/components/utils.tsx +++ b/plugins/frontend/backstage-plugin-security-insights/src/components/utils.tsx @@ -3,6 +3,12 @@ import { Box } from '@material-ui/core'; import WarningRoundedIcon from '@material-ui/icons/WarningRounded'; import ErrorRoundedIcon from '@material-ui/icons/ErrorRounded'; import NoteRoundedIcon from '@material-ui/icons/NoteRounded'; +import { + ANNOTATION_LOCATION, + ANNOTATION_SOURCE_LOCATION, + Entity, +} from '@backstage/catalog-model'; +import gitUrlParse from 'git-url-parse'; export const getSeverityBadge = ( severityLevel: string, @@ -52,3 +58,13 @@ export const getSeverityBadge = ( return 'Unknown'; } }; + +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 3b7657f55..98b984698 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16186,6 +16186,11 @@ resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.2.tgz#5950e50960793055845e956c427fc2b0d70c5239" integrity sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw== +"@types/parse-path@^7.0.0": + version "7.0.3" + resolved "https://registry.yarnpkg.com/@types/parse-path/-/parse-path-7.0.3.tgz#cec2da2834ab58eb2eb579122d9a1fc13bd7ef36" + integrity sha512-LriObC2+KYZD3FzCrgWGv/qufdUy4eXrxcLgQMfYXgPbLIecKIsVBaQgUPmxSSLcjmYbDTQbMgr6qr6l/eb7Bg== + "@types/passport-oauth2@^1.4.11": version "1.4.15" resolved "https://registry.yarnpkg.com/@types/passport-oauth2/-/passport-oauth2-1.4.15.tgz#34f2684f53aad36e664cd01ca9879224229f47e7" @@ -23212,6 +23217,14 @@ git-up@^7.0.0: is-ssh "^1.4.0" parse-url "^8.1.0" +git-up@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/git-up/-/git-up-8.0.0.tgz#674d398f95c4f70b4193f3f3d87c73cf28c2bee1" + integrity sha512-uBI8Zdt1OZlrYfGcSVroLJKgyNNXlgusYFzHk614lTasz35yg2PVpL1RMy0LOO2dcvF9msYW3pRfUSmafZNrjg== + dependencies: + is-ssh "^1.4.0" + parse-url "^9.2.0" + git-url-parse@13.1.0: version "13.1.0" resolved "https://registry.yarnpkg.com/git-url-parse/-/git-url-parse-13.1.0.tgz#07e136b5baa08d59fabdf0e33170de425adf07b4" @@ -23233,6 +23246,13 @@ git-url-parse@^15.0.0: dependencies: git-up "^7.0.0" +git-url-parse@^16.0.0: + version "16.0.0" + resolved "https://registry.yarnpkg.com/git-url-parse/-/git-url-parse-16.0.0.tgz#04dcc54197ad9aa2c92795b32be541d217c11f70" + integrity sha512-Y8iAF0AmCaqXc6a5GYgPQW9ESbncNLOL+CeQAJRhmWUOmnPkKpBYeWYp4mFd3LA5j53CdGDdslzX12yEBVHQQg== + dependencies: + git-up "^8.0.0" + gitconfiglocal@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/gitconfiglocal/-/gitconfiglocal-1.0.0.tgz#41d045f3851a5ea88f03f24ca1c6178114464b9b" @@ -30158,6 +30178,14 @@ parse-url@^8.1.0: dependencies: parse-path "^7.0.0" +parse-url@^9.2.0: + version "9.2.0" + resolved "https://registry.yarnpkg.com/parse-url/-/parse-url-9.2.0.tgz#d75da32b3bbade66e4eb0763fb4851d27526b97b" + integrity sha512-bCgsFI+GeGWPAvAiUv63ZorMeif3/U0zaXABGJbOWt5OH2KCaPHF6S+0ok4aqM9RuIPGyZdx9tR9l13PsW4AYQ== + dependencies: + "@types/parse-path" "^7.0.0" + parse-path "^7.0.0" + parse5@6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"