diff --git a/plugins/github-deployments/src/api/index.ts b/plugins/github-deployments/src/api/index.ts index f239af9ef8..31e3defa47 100644 --- a/plugins/github-deployments/src/api/index.ts +++ b/plugins/github-deployments/src/api/index.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { parseLocationReference } from '@backstage/catalog-model'; import { createApiRef, OAuthApi } from '@backstage/core'; import { InputError } from '@backstage/errors'; import { ScmIntegrationRegistry } from '@backstage/integration'; @@ -20,25 +21,29 @@ import { graphql } from '@octokit/graphql'; const getBaseUrl = ( scmIntegrationsApi: ScmIntegrationRegistry, - host?: string, + location?: string, ): string => { - if (!host) { + if (!location) { return 'https://api.github.com'; } - const integrationConfig = scmIntegrationsApi.github.byHost(host); + const config = scmIntegrationsApi.github.byUrl( + parseLocationReference(location).target, + ); - if (!integrationConfig) { + if (!config) { throw new InputError( - `No matching GitHub integration configuration for host ${host}, please check your integrations config`, + `No matching GitHub integration configuration for location ${location}, please check your integrations config`, ); } - if (!integrationConfig.config.apiBaseUrl) { - throw new InputError(`No apiBaseUrl available for host ${host}`); + if (!config.config.apiBaseUrl) { + throw new InputError( + `No apiBaseUrl available for location ${location}, please check your integrations config`, + ); } - return integrationConfig.config.apiBaseUrl; + return config?.config.apiBaseUrl; }; export type GithubDeployment = { @@ -51,13 +56,17 @@ export type GithubDeployment = { }; }; +type QueryOptions = { + owner: string; + repo: string; + last: number; +}; + export interface GithubDeploymentsApi { - listDeployments(options: { - host?: string; - owner: string; - repo: string; - last: number; - }): Promise; + listDeployments( + options: QueryOptions, + location?: string, + ): Promise; } export const githubDeploymentsApiRef = createApiRef({ @@ -105,13 +114,11 @@ export class GithubDeploymentsApiClient implements GithubDeploymentsApi { this.scmIntegrationsApi = options.scmIntegrationsApi; } - async listDeployments(options: { - owner: string; - repo: string; - last: number; - host?: string; - }): Promise { - const baseUrl = getBaseUrl(this.scmIntegrationsApi, options.host); + async listDeployments( + options: QueryOptions, + location?: string, + ): Promise { + const baseUrl = getBaseUrl(this.scmIntegrationsApi, location); const token = await this.githubAuthApi.getAccessToken(['repo']); const graphQLWithAuth = graphql.defaults({ diff --git a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx index d7bc0dfcc8..040a9b6571 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx @@ -40,10 +40,13 @@ import { import { setupServer } from 'msw/node'; import { graphql } from 'msw'; import { ScmIntegrations } from '@backstage/integration'; +import { Entity } from '@backstage/catalog-model'; + +let entity: { entity: Entity }; jest.mock('@backstage/plugin-catalog-react', () => ({ useEntity: () => { - return entityStub; + return entity; }, })); @@ -53,15 +56,24 @@ const configApi: ConfigApi = new ConfigReader({ integrations: { github: [ { - host: 'missing-api-base-url.com', + host: 'my-github-1.com', + apiBaseUrl: 'https://api.my-github-1.com', }, { - host: 'my-github.com', - apiBaseUrl: 'https://api.my-github.com', + host: 'my-github-2.com', + apiBaseUrl: 'https://api.my-github-2.com', + }, + { + host: 'my-github-3.com', }, ], }, }); + +const GRAPHQL_GITHUB_API = graphql.link('https://api.github.com/graphql'); +const GRAPHQL_CUSTOM_API_1 = graphql.link('https://api.github-1.com/graphql'); +const GRAPHQL_CUSTOM_API_2 = graphql.link('https://api.github-2.com/graphql'); + const scmIntegrationsApi = ScmIntegrations.fromConfig(configApi); const githubAuthApi: OAuthApi = { getAccessToken: async _ => 'access_token', @@ -76,11 +88,36 @@ const apis = ApiRegistry.from([ ], ]); +const assertFetchedData = async () => { + const rendered = await renderInTestApp( + + + , + ); + + expect(await rendered.findByText('GitHub Deployments')).toBeInTheDocument(); + + expect(await rendered.findByText('active')).toBeInTheDocument(); + expect(await rendered.findByText('prd')).toBeInTheDocument(); + expect(await rendered.findByText('12345')).toHaveAttribute( + 'href', + 'https://exampleapi.com/123456789', + ); + + expect(await rendered.findByText('pending')).toBeInTheDocument(); + expect(await rendered.findByText('lab')).toBeInTheDocument(); + expect(await rendered.findByText('54321')).toHaveAttribute( + 'href', + 'https://exampleapi.com/543212345', + ); +}; + describe('github-deployments', () => { const worker = setupServer(); msw.setupDefaultHandlers(worker); beforeEach(() => { + worker.resetHandlers(); jest.resetAllMocks(); }); @@ -91,40 +128,27 @@ describe('github-deployments', () => { }); describe('GithubDeploymentsCard', () => { - it('should display fetched data', async () => { + beforeEach(() => { + entity = entityStub; + entity.entity.metadata.annotations = { + 'github.com/project-slug': 'org/repo', + }; + }); + + it('displays fetched data using default github api', async () => { worker.use( - graphql.query('deployments', (_, res, ctx) => + GRAPHQL_GITHUB_API.query('deployments', (_, res, ctx) => res(ctx.data(responseStub)), ), ); - const rendered = await renderInTestApp( - - - , - ); - - expect( - await rendered.findByText('GitHub Deployments'), - ).toBeInTheDocument(); - expect(await rendered.findByText('active')).toBeInTheDocument(); - expect(await rendered.findByText('prd')).toBeInTheDocument(); - expect(await rendered.findByText('12345')).toHaveAttribute( - 'href', - 'https://exampleapi.com/123456789', - ); - - expect(await rendered.findByText('pending')).toBeInTheDocument(); - expect(await rendered.findByText('lab')).toBeInTheDocument(); - expect(await rendered.findByText('54321')).toHaveAttribute( - 'href', - 'https://exampleapi.com/543212345', - ); + await assertFetchedData(); + expect.assertions(7); }); it('should display empty state when no data', async () => { worker.use( - graphql.query('deployments', (_, res, ctx) => + GRAPHQL_GITHUB_API.query('deployments', (_, res, ctx) => res(ctx.data(noDataResponseStub)), ), ); @@ -145,7 +169,7 @@ describe('github-deployments', () => { it('should shows new data on reload', async () => { worker.use( - graphql.query('deployments', (_, res, ctx) => + GRAPHQL_GITHUB_API.query('deployments', (_, res, ctx) => res(ctx.data(responseStub)), ), ); @@ -163,7 +187,7 @@ describe('github-deployments', () => { worker.resetHandlers(); worker.use( - graphql.query('deployments', (_, res, ctx) => + GRAPHQL_GITHUB_API.query('deployments', (_, res, ctx) => res(ctx.data(refreshedResponseStub)), ), ); @@ -177,44 +201,102 @@ describe('github-deployments', () => { expect(await rendered.findByText('failure')).toBeInTheDocument(); }); - it('shows error when host does not exist', async () => { - worker.use( - graphql.query('deployments', (_, res, ctx) => - res(ctx.data(responseStub)), - ), - ); + describe('entity with source location', () => { + beforeEach(() => { + entity = entityStub; + entity.entity.metadata.annotations = { + 'github.com/project-slug': 'org/repo', + 'backstage.io/source-location': + 'url:https://my-github-1.com/org/repo', + }; + }); - const rendered = await renderInTestApp( - - - , - ); + it('should fetch data using custom api', async () => { + const customAPI = graphql.link('https://api.my-github-1.com/graphql'); - expect( - await rendered.findByText( - 'Warning: No matching GitHub integration configuration for host unknown-host.com, please check your integrations config', - ), - ).toBeInTheDocument(); + worker.use( + customAPI.query('deployments', (_, res, ctx) => + res(ctx.data(responseStub)), + ), + ); + + await assertFetchedData(); + expect.assertions(7); + }); }); - it('shows error when baseApiURL does not exist for host', async () => { - worker.use( - graphql.query('deployments', (_, res, ctx) => - res(ctx.data(responseStub)), - ), - ); + describe('entity with managed by location', () => { + beforeEach(() => { + entity = entityStub; + entity.entity.metadata.annotations = { + 'github.com/project-slug': 'org/repo', + 'backstage.io/managed-by-location': + 'url:https://my-github-2.com/org/repo', + }; + }); - const rendered = await renderInTestApp( - - - , - ); + it('should fetch data using custom api', async () => { + const customAPI = graphql.link('https://api.my-github-2.com/graphql'); - expect( - await rendered.findByText( - 'Warning: No apiBaseUrl available for host missing-api-base-url.com', - ), - ).toBeInTheDocument(); + worker.use( + customAPI.query('deployments', (_, res, ctx) => + res(ctx.data(responseStub)), + ), + ); + + await assertFetchedData(); + expect.assertions(7); + }); + }); + + describe('entity with location without baseApiURL', () => { + beforeEach(() => { + entity = entityStub; + entity.entity.metadata.annotations = { + 'github.com/project-slug': 'org/repo', + 'backstage.io/managed-by-location': + 'url:https://my-github-3.com/org/repo', + }; + }); + + it('shows error when no matching github host', async () => { + const rendered = await renderInTestApp( + + + , + ); + + expect( + await rendered.findByText( + 'Warning: No apiBaseUrl available for location url:https://my-github-3.com/org/repo, please check your integrations config', + ), + ).toBeInTheDocument(); + }); + }); + + describe('entity with location without matching host', () => { + beforeEach(() => { + entity = entityStub; + entity.entity.metadata.annotations = { + 'github.com/project-slug': 'org/repo', + 'backstage.io/managed-by-location': + 'url:https://my-github-unknown.com/org/repo', + }; + }); + + it('shows error when no matching github host', async () => { + const rendered = await renderInTestApp( + + + , + ); + + expect( + await rendered.findByText( + 'Warning: No matching GitHub integration configuration for location url:https://my-github-unknown.com/org/repo, please check your integrations config', + ), + ).toBeInTheDocument(); + }); }); }); }); diff --git a/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx b/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx index 691c63fcc3..f7ba6293ee 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx @@ -27,21 +27,25 @@ import { isGithubDeploymentsAvailable, } from '../Router'; import GithubDeploymentsTable from './GithubDeploymentsTable/GithubDeploymentsTable'; +import { + LOCATION_ANNOTATION, + SOURCE_LOCATION_ANNOTATION, +} from '@backstage/catalog-model'; const GithubDeploymentsComponent = ({ - host, projectSlug, last, + location, }: { - host?: string; projectSlug: string; last: number; + location?: string; }) => { const api = useApi(githubDeploymentsApiRef); const [owner, repo] = projectSlug.split('/'); const { loading, value, error, retry: reload } = useAsyncRetry( - async () => await api.listDeployments({ host, owner, repo, last }), + async () => await api.listDeployments({ owner, repo, last }, location), ); if (error) { @@ -57,14 +61,11 @@ const GithubDeploymentsComponent = ({ ); }; -export const GithubDeploymentsCard = ({ - last, - host, -}: { - last?: number; - host?: string; -}) => { +export const GithubDeploymentsCard = ({ last }: { last?: number }) => { const { entity } = useEntity(); + const location = + entity?.metadata.annotations?.[SOURCE_LOCATION_ANNOTATION] || + entity?.metadata.annotations?.[LOCATION_ANNOTATION]; return !isGithubDeploymentsAvailable(entity) ? ( @@ -74,7 +75,7 @@ export const GithubDeploymentsCard = ({ entity?.metadata.annotations?.[GITHUB_PROJECT_SLUG_ANNOTATION] || '' } last={last || 10} - host={host} + location={location} /> ); }; diff --git a/plugins/github-deployments/src/mocks/mocks.ts b/plugins/github-deployments/src/mocks/mocks.ts index f636a08558..9e10e6c289 100644 --- a/plugins/github-deployments/src/mocks/mocks.ts +++ b/plugins/github-deployments/src/mocks/mocks.ts @@ -13,15 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { Entity } from '@backstage/catalog-model'; import { QueryResponse } from '../api'; -export const entityStub = { +export const entityStub: { entity: Entity } = { entity: { metadata: { namespace: 'default', - annotations: { - 'github.com/project-slug': 'org/repo', - }, + annotations: {}, name: 'sample-service', description: 'Sample service', uid: 'g0h33dd9-56h7-835b-b63v-7x5da3j64851',