From a971957d5023ad8f1bd5f033c0c955f6b73d74ad Mon Sep 17 00:00:00 2001 From: Pepijn Schildkamp Date: Fri, 16 Jul 2021 20:53:11 +0200 Subject: [PATCH 01/83] Add authHandler and login resolver for OAuth2 provider Signed-off-by: Pepijn Schildkamp --- plugins/auth-backend/src/providers/index.ts | 1 + .../src/providers/oauth2/index.ts | 2 +- .../src/providers/oauth2/provider.test.ts | 93 ++++++++ .../src/providers/oauth2/provider.ts | 202 ++++++++++++++---- 4 files changed, 255 insertions(+), 43 deletions(-) create mode 100644 plugins/auth-backend/src/providers/oauth2/provider.test.ts diff --git a/plugins/auth-backend/src/providers/index.ts b/plugins/auth-backend/src/providers/index.ts index 6b262dcf68..576c5011af 100644 --- a/plugins/auth-backend/src/providers/index.ts +++ b/plugins/auth-backend/src/providers/index.ts @@ -16,6 +16,7 @@ export * from './google'; export * from './microsoft'; +export * from './oauth2'; export { factories as defaultAuthProviderFactories } from './factories'; // Export the minimal interface required for implementing a diff --git a/plugins/auth-backend/src/providers/oauth2/index.ts b/plugins/auth-backend/src/providers/oauth2/index.ts index d68208a148..4e2509735d 100644 --- a/plugins/auth-backend/src/providers/oauth2/index.ts +++ b/plugins/auth-backend/src/providers/oauth2/index.ts @@ -14,5 +14,5 @@ * limitations under the License. */ -export { createOAuth2Provider } from './provider'; +export { createOAuth2Provider, oAuth2EmailSignInResolver } from './provider'; export type { OAuth2ProviderOptions } from './provider'; diff --git a/plugins/auth-backend/src/providers/oauth2/provider.test.ts b/plugins/auth-backend/src/providers/oauth2/provider.test.ts new file mode 100644 index 0000000000..773471c42f --- /dev/null +++ b/plugins/auth-backend/src/providers/oauth2/provider.test.ts @@ -0,0 +1,93 @@ +/* + * Copyright 2020 The Backstage Authors + * + * 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 { OAuth2AuthProvider } from './provider'; +import * as helpers from '../../lib/passport/PassportStrategyHelper'; +import { OAuthResult } from '../../lib/oauth'; +import { getVoidLogger } from '@backstage/backend-common'; +import { TokenIssuer } from '../../identity/types'; +import { CatalogIdentityClient } from '../../lib/catalog'; + +const mockFrameHandler = (jest.spyOn( + helpers, + 'executeFrameHandlerStrategy', +) as unknown) as jest.MockedFunction< + () => Promise<{ result: OAuthResult; privateInfo: any }> +>; + +describe('createOAuth2Provider', () => { + it('should auth', async () => { + const tokenIssuer = { + issueToken: jest.fn(), + listPublicKeys: jest.fn(), + }; + const catalogIdentityClient = { + findUser: jest.fn(), + }; + + const provider = new OAuth2AuthProvider({ + logger: getVoidLogger(), + catalogIdentityClient: (catalogIdentityClient as unknown) as CatalogIdentityClient, + tokenIssuer: (tokenIssuer as unknown) as TokenIssuer, + authHandler: async ({ fullProfile }) => ({ + profile: { + email: fullProfile.emails![0]!.value, + displayName: fullProfile.displayName, + picture: 'http://backstage.io/lols', + }, + }), + clientId: 'mock', + clientSecret: 'mock', + callbackUrl: 'mock', + authorizationUrl: 'mock', + tokenUrl: 'mock', + }); + + mockFrameHandler.mockResolvedValueOnce({ + result: { + fullProfile: { + emails: [{ value: 'conrad@example.com' }], + displayName: 'Conrad', + id: 'conrad', + provider: 'oAuth2', + }, + params: { + id_token: 'idToken', + scope: 'scope', + expires_in: 123, + }, + accessToken: 'accessToken', + }, + privateInfo: { + refreshToken: 'wacka', + }, + }); + const { response } = await provider.handler({} as any); + expect(response).toEqual({ + providerInfo: { + accessToken: 'accessToken', + expiresInSeconds: 123, + idToken: 'idToken', + scope: 'scope', + }, + profile: { + email: 'conrad@example.com', + displayName: 'Conrad', + picture: 'http://backstage.io/lols', + }, + }); + }); +}); diff --git a/plugins/auth-backend/src/providers/oauth2/provider.ts b/plugins/auth-backend/src/providers/oauth2/provider.ts index f174543a63..7e09487c32 100644 --- a/plugins/auth-backend/src/providers/oauth2/provider.ts +++ b/plugins/auth-backend/src/providers/oauth2/provider.ts @@ -18,15 +18,15 @@ import express from 'express'; import passport from 'passport'; import { Strategy as OAuth2Strategy } from 'passport-oauth2'; import { - OAuthAdapter, - OAuthProviderOptions, - OAuthHandlers, - OAuthResponse, - OAuthEnvironmentHandler, - OAuthStartRequest, encodeState, + OAuthAdapter, + OAuthEnvironmentHandler, + OAuthHandlers, + OAuthProviderOptions, OAuthRefreshRequest, + OAuthResponse, OAuthResult, + OAuthStartRequest, } from '../../lib/oauth'; import { executeFetchUserProfileStrategy, @@ -36,22 +36,46 @@ import { makeProfileInfo, PassportDoneCallback, } from '../../lib/passport'; -import { RedirectInfo, AuthProviderFactory } from '../types'; +import { + AuthHandler, + AuthProviderFactory, + RedirectInfo, + SignInResolver, +} from '../types'; +import { CatalogIdentityClient, getEntityClaims } from '../../lib/catalog'; +import { TokenIssuer } from '../../identity'; +import { Logger } from 'winston'; type PrivateInfo = { refreshToken: string; }; export type OAuth2AuthProviderOptions = OAuthProviderOptions & { + signInResolver?: SignInResolver; + authHandler: AuthHandler; + tokenIssuer: TokenIssuer; + catalogIdentityClient: CatalogIdentityClient; authorizationUrl: string; tokenUrl: string; scope?: string; + logger: Logger; }; export class OAuth2AuthProvider implements OAuthHandlers { private readonly _strategy: OAuth2Strategy; + private readonly signInResolver?: SignInResolver; + private readonly authHandler: AuthHandler; + private readonly tokenIssuer: TokenIssuer; + private readonly catalogIdentityClient: CatalogIdentityClient; + private readonly logger: Logger; constructor(options: OAuth2AuthProviderOptions) { + this.signInResolver = options.signInResolver; + this.authHandler = options.authHandler; + this.tokenIssuer = options.tokenIssuer; + this.catalogIdentityClient = options.catalogIdentityClient; + this.logger = options.logger; + this._strategy = new OAuth2Strategy( { clientID: options.clientId, @@ -102,18 +126,8 @@ export class OAuth2AuthProvider implements OAuthHandlers { PrivateInfo >(req, this._strategy); - const profile = makeProfileInfo(result.fullProfile, result.params.id_token); - return { - response: await this.populateIdentity({ - profile, - providerInfo: { - idToken: result.params.id_token, - accessToken: result.accessToken, - scope: result.params.scope, - expiresInSeconds: result.params.expires_in, - }, - }), + response: await this.handleResult(result), refreshToken: privateInfo.refreshToken, }; } @@ -130,46 +144,124 @@ export class OAuth2AuthProvider implements OAuthHandlers { refreshToken: updatedRefreshToken, } = refreshTokenResponse; - const rawProfile = await executeFetchUserProfileStrategy( + const fullProfile = await executeFetchUserProfileStrategy( this._strategy, accessToken, ); - const profile = makeProfileInfo(rawProfile, params.id_token); - return this.populateIdentity({ - providerInfo: { - accessToken, - refreshToken: updatedRefreshToken, - idToken: params.id_token, - expiresInSeconds: params.expires_in, - scope: params.scope, - }, - profile, + return this.handleResult({ + fullProfile, + params, + accessToken, + refreshToken: updatedRefreshToken, }); } - // Use this function to grab the user profile info from the token - // Then populate the profile with it - private async populateIdentity( - response: OAuthResponse, - ): Promise { - const { profile } = response; + private async handleResult(result: OAuthResult) { + const { profile } = await this.authHandler(result); - if (!profile.email) { - throw new Error('Profile does not contain an email'); + const response: OAuthResponse = { + providerInfo: { + idToken: result.params.id_token, + accessToken: result.accessToken, + scope: result.params.scope, + expiresInSeconds: result.params.expires_in, + }, + profile, + }; + + if (this.signInResolver) { + response.backstageIdentity = await this.signInResolver( + { + result, + profile, + }, + { + tokenIssuer: this.tokenIssuer, + catalogIdentityClient: this.catalogIdentityClient, + logger: this.logger, + }, + ); } - const id = profile.email.split('@')[0]; - return { ...response, backstageIdentity: { id } }; + return response; } } -export type OAuth2ProviderOptions = {}; +export const oAuth2EmailSignInResolver: SignInResolver = async ( + info, + ctx, +) => { + const { profile } = info; + + if (!profile.email) { + throw new Error('OAuth2 profile contained no email'); + } + + const entity = await ctx.catalogIdentityClient.findUser({ + annotations: { + 'oauth2/email': profile.email, + }, + }); + + const claims = getEntityClaims(entity); + const token = await ctx.tokenIssuer.issueToken({ claims }); + + return { id: entity.metadata.name, entity, token }; +}; + +export const oAuth2DefaultSignInResolver: SignInResolver = async ( + info, + ctx, +) => { + const { profile } = info; + + if (!profile.email) { + throw new Error('Profile contained no email'); + } + + let userId: string; + + try { + const entity = await ctx.catalogIdentityClient.findUser({ + annotations: { + 'oauth2/email': profile.email, + }, + }); + userId = entity.metadata.name; + } catch (error) { + ctx.logger.warn( + `Failed to look up user, ${error}, falling back to allowing login based on email pattern, this will probably break in the future`, + ); + userId = profile.email.split('@')[0]; + } + + const token = await ctx.tokenIssuer.issueToken({ + claims: { sub: userId, ent: [`user:default/${userId}`] }, + }); + + return { id: userId, token }; +}; + +export type OAuth2ProviderOptions = { + authHandler?: AuthHandler; + + signIn?: { + resolver?: SignInResolver; + }; +}; export const createOAuth2Provider = ( - _options?: OAuth2ProviderOptions, + options?: OAuth2ProviderOptions, ): AuthProviderFactory => { - return ({ providerId, globalConfig, config, tokenIssuer }) => + return ({ + providerId, + globalConfig, + config, + tokenIssuer, + catalogApi, + logger, + }) => OAuthEnvironmentHandler.mapConfig(config, envConfig => { const clientId = envConfig.getString('clientId'); const clientSecret = envConfig.getString('clientSecret'); @@ -178,13 +270,39 @@ export const createOAuth2Provider = ( const tokenUrl = envConfig.getString('tokenUrl'); const scope = envConfig.getOptionalString('scope'); + const catalogIdentityClient = new CatalogIdentityClient({ + catalogApi, + tokenIssuer, + }); + + const authHandler: AuthHandler = options?.authHandler + ? options.authHandler + : async ({ fullProfile, params }) => ({ + profile: makeProfileInfo(fullProfile, params.id_token), + }); + + const signInResolverFn = + options?.signIn?.resolver ?? oAuth2DefaultSignInResolver; + + const signInResolver: SignInResolver = info => + signInResolverFn(info, { + catalogIdentityClient, + tokenIssuer, + logger, + }); + const provider = new OAuth2AuthProvider({ clientId, clientSecret, + tokenIssuer, + catalogIdentityClient, callbackUrl, + signInResolver, + authHandler, authorizationUrl, tokenUrl, scope, + logger, }); return OAuthAdapter.fromConfig(globalConfig, provider, { From e9b1e2a9f40950b955a15c55109c7863701b95a5 Mon Sep 17 00:00:00 2001 From: Pepijn Schildkamp Date: Fri, 16 Jul 2021 21:04:55 +0200 Subject: [PATCH 02/83] Added changeset for oAuth2 provider refactoring Signed-off-by: Pepijn Schildkamp --- .changeset/strong-ravens-smoke.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/strong-ravens-smoke.md diff --git a/.changeset/strong-ravens-smoke.md b/.changeset/strong-ravens-smoke.md new file mode 100644 index 0000000000..8579e74d37 --- /dev/null +++ b/.changeset/strong-ravens-smoke.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': minor +--- + +Added signIn and authHandler resolver for oAuth2 provider From ce0260753273ef24fd9d43659b635457d4a453ce Mon Sep 17 00:00:00 2001 From: Aurelio Saraiva Date: Wed, 18 Aug 2021 18:22:37 -0300 Subject: [PATCH 03/83] added title Signed-off-by: Aurelio Saraiva --- .../examples/documented-component/catalog-info.yaml | 1 + plugins/techdocs/src/home/components/columns.tsx | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/plugins/techdocs-backend/examples/documented-component/catalog-info.yaml b/plugins/techdocs-backend/examples/documented-component/catalog-info.yaml index 125c18200d..e6d14025e1 100644 --- a/plugins/techdocs-backend/examples/documented-component/catalog-info.yaml +++ b/plugins/techdocs-backend/examples/documented-component/catalog-info.yaml @@ -2,6 +2,7 @@ apiVersion: backstage.io/v1alpha1 kind: Component metadata: name: documented-component + title: Documented Component description: A Service with TechDocs documentation annotations: backstage.io/techdocs-ref: dir:. diff --git a/plugins/techdocs/src/home/components/columns.tsx b/plugins/techdocs/src/home/components/columns.tsx index 5cdcc66c37..80dfd80a35 100644 --- a/plugins/techdocs/src/home/components/columns.tsx +++ b/plugins/techdocs/src/home/components/columns.tsx @@ -19,6 +19,10 @@ import { Link, SubvalueCell, TableColumn } from '@backstage/core-components'; import { EntityRefLinks } from '@backstage/plugin-catalog-react'; import { DocsTableRow } from './types'; +function formatTitle(entity): String { + return entity.metadata.title || entity.metadata.name; +} + export function createNameColumn(): TableColumn { return { title: 'Document', @@ -26,9 +30,7 @@ export function createNameColumn(): TableColumn { highlight: true, render: (row: DocsTableRow) => ( {row.entity.metadata.name} - } + value={{formatTitle(row.entity)}} subvalue={row.entity.metadata.description} /> ), From 60169a761d5c98821e4f1bf6488a3c2e30188778 Mon Sep 17 00:00:00 2001 From: Aurelio Saraiva Date: Wed, 18 Aug 2021 19:34:16 -0300 Subject: [PATCH 04/83] renamed function to customTitle Signed-off-by: Aurelio Saraiva --- plugins/techdocs/src/home/components/columns.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/techdocs/src/home/components/columns.tsx b/plugins/techdocs/src/home/components/columns.tsx index 80dfd80a35..b7ec8271a4 100644 --- a/plugins/techdocs/src/home/components/columns.tsx +++ b/plugins/techdocs/src/home/components/columns.tsx @@ -17,9 +17,10 @@ import React from 'react'; import { Link, SubvalueCell, TableColumn } from '@backstage/core-components'; import { EntityRefLinks } from '@backstage/plugin-catalog-react'; +import { Entity } from '@backstage/catalog-model'; import { DocsTableRow } from './types'; -function formatTitle(entity): String { +function customTitle(entity: Entity): String { return entity.metadata.title || entity.metadata.name; } @@ -30,7 +31,7 @@ export function createNameColumn(): TableColumn { highlight: true, render: (row: DocsTableRow) => ( {formatTitle(row.entity)}} + value={{customTitle(row.entity)}} subvalue={row.entity.metadata.description} /> ), From 4578d0708c528a22eed08d759113dcfb0bc6eb3e Mon Sep 17 00:00:00 2001 From: Aurelio Saraiva Date: Wed, 18 Aug 2021 19:49:37 -0300 Subject: [PATCH 05/83] updated docs with new property title Signed-off-by: Aurelio Saraiva --- docs/features/software-catalog/descriptor-format.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index fb17f4a434..f39454a797 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -231,6 +231,14 @@ follows. Example: `visits-tracking-service`, `CircleciBuildsDumpV2_avro_gcs` +### `title` [optional] + +The title of the entity. This is a friendly title to be used in the Backstage +interface. This is an optional property, if it doesn't exist the `name` property +will be used. + +Only available for Templates and Docs. + ### `namespace` [optional] The ID of a namespace that the entity belongs to. This is a string that follows From bda7fd72c7c1c8c1856f8f664b2fb605849d4a16 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Aug 2021 04:08:14 +0000 Subject: [PATCH 06/83] chore(deps): bump @rjsf/material-ui from 3.0.0 to 3.1.0 Bumps [@rjsf/material-ui](https://github.com/rjsf-team/react-jsonschema-form) from 3.0.0 to 3.1.0. - [Release notes](https://github.com/rjsf-team/react-jsonschema-form/releases) - [Commits](https://github.com/rjsf-team/react-jsonschema-form/compare/v3.0.0...v3.1.0) --- updated-dependencies: - dependency-name: "@rjsf/material-ui" dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 94b81f35ee..59deeb3b96 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5053,9 +5053,9 @@ react-is "^16.9.0" "@rjsf/material-ui@^3.0.0": - version "3.0.0" - resolved "https://registry.npmjs.org/@rjsf/material-ui/-/material-ui-3.0.0.tgz#69ece2cb549f0e860b5f89898db90edcc95b15ba" - integrity sha512-T2B8QnrDQphbFNxDz7baAa0zTd5TXJmO9soHBPTKKdniRbMEOQ19AJBbZkA3ED2XZa/xrUY/6XjERQLpNACddw== + version "3.1.0" + resolved "https://registry.npmjs.org/@rjsf/material-ui/-/material-ui-3.1.0.tgz#ed15fc75de1594f87cd8336b7f2ebc492a432d8b" + integrity sha512-kWz37spT5SOXkb8Axq4g4BzQjXRylQr6B7eFAg1NPhSK7KrJYwSMSsFJ9Ze2vEBxwEiR0Z0n/4puaamGuGFRBw== "@roadiehq/backstage-plugin-buildkite@^1.0.8": version "1.0.8" From 4e7c25ff66f858141eb10a992102ac58bfdfe83d Mon Sep 17 00:00:00 2001 From: Deepak Bhardwaj Date: Mon, 23 Aug 2021 19:03:54 +0530 Subject: [PATCH 07/83] Created Allure Plugin Signed-off-by: Deepak Bhardwaj --- app-config.yaml | 3 + packages/app/package.json | 1 + .../app/src/components/catalog/EntityPage.tsx | 5 ++ plugins/allure/.eslintrc.js | 3 + plugins/allure/README.md | 47 +++++++++++++ plugins/allure/package.json | 68 ++++++++++++++++++ plugins/allure/src/api/AllureApi.ts | 24 +++++++ plugins/allure/src/api/AllureApiClient.ts | 30 ++++++++ plugins/allure/src/api/index.ts | 17 +++++ .../AllureReportComponent.test.tsx | 44 ++++++++++++ .../AllureReportComponent.tsx | 70 +++++++++++++++++++ .../components/AllureReportComponent/index.ts | 16 +++++ .../src/components/useAllureProjectId.ts | 25 +++++++ plugins/allure/src/index.ts | 16 +++++ plugins/allure/src/plugin.test.ts | 22 ++++++ plugins/allure/src/plugin.ts | 53 ++++++++++++++ plugins/allure/src/setupTests.ts | 17 +++++ 17 files changed, 461 insertions(+) create mode 100644 plugins/allure/.eslintrc.js create mode 100644 plugins/allure/README.md create mode 100644 plugins/allure/package.json create mode 100644 plugins/allure/src/api/AllureApi.ts create mode 100644 plugins/allure/src/api/AllureApiClient.ts create mode 100644 plugins/allure/src/api/index.ts create mode 100644 plugins/allure/src/components/AllureReportComponent/AllureReportComponent.test.tsx create mode 100644 plugins/allure/src/components/AllureReportComponent/AllureReportComponent.tsx create mode 100644 plugins/allure/src/components/AllureReportComponent/index.ts create mode 100644 plugins/allure/src/components/useAllureProjectId.ts create mode 100644 plugins/allure/src/index.ts create mode 100644 plugins/allure/src/plugin.test.ts create mode 100644 plugins/allure/src/plugin.ts create mode 100644 plugins/allure/src/setupTests.ts diff --git a/app-config.yaml b/app-config.yaml index 9f365becef..c5c4a23bdd 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -133,6 +133,9 @@ kafka: brokers: - localhost:9092 +allure: + baseUrl: http://localhost:5050/allure-docker-service + integrations: github: - host: github.com diff --git a/packages/app/package.json b/packages/app/package.json index 78ebe32e25..a5407abe09 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -10,6 +10,7 @@ "@backstage/core-components": "^0.3.2", "@backstage/core-plugin-api": "^0.1.6", "@backstage/integration-react": "^0.1.7", + "@backstage/plugin-allure": "^0.1.0", "@backstage/plugin-api-docs": "^0.6.6", "@backstage/plugin-badges": "^0.2.7", "@backstage/plugin-catalog": "^0.6.12", diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index d84a7341bc..89bc39952f 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -110,6 +110,7 @@ import { } from '@roadiehq/backstage-plugin-travis-ci'; import { EntityCodeCoverageContent } from '@backstage/plugin-code-coverage'; import { EmptyState } from '@backstage/core-components'; +import { EntityAllureReportContent } from '@backstage/plugin-allure'; const EntityLayoutWrapper = (props: { children?: ReactNode }) => { const [badgesDialogOpen, setBadgesDialogOpen] = useState(false); @@ -356,6 +357,10 @@ const serviceEntityPage = ( + + + + ); diff --git a/plugins/allure/.eslintrc.js b/plugins/allure/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/plugins/allure/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/plugins/allure/README.md b/plugins/allure/README.md new file mode 100644 index 0000000000..d28fe913b1 --- /dev/null +++ b/plugins/allure/README.md @@ -0,0 +1,47 @@ +# Allure + +Welcome to the Backstage Allure plugin. This plugin add an entity service page to display Allure test reports related to the service. + +## Install + +Run the below command from the `app` package directory. + +```shell +yarn add @backstage/plugin-allure +``` + +alternatively, you can execute below command from the root directory of your Backstage app. + +```shell +yarn workspace app add @backstage/plugin-allure +``` + +## Configure + +### Configure Allure service + +Add below configuration in the `app-config.yaml`. + +```yaml +allure: + baseUrl: # Example: http://localhost:5050/allure-docker-service +``` + +### Setup entity service page + +Add `EntityAllureReportContent` in the `EntityPage.tsx` like below: + +```diff ++ import { EntityAllureReportContent } from '@backstage/plugin-allure'; + +... + +const serviceEntityPage = ( + + ... ++ ++ ++ + +); +``` diff --git a/plugins/allure/package.json b/plugins/allure/package.json new file mode 100644 index 0000000000..81a37ed996 --- /dev/null +++ b/plugins/allure/package.json @@ -0,0 +1,68 @@ +{ + "name": "@backstage/plugin-allure", + "version": "0.1.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "scripts": { + "build": "backstage-cli plugin:build", + "start": "backstage-cli plugin:serve", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "diff": "backstage-cli plugin:diff", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/catalog-model": "^0.9.0", + "@backstage/core-components": "^0.3.2", + "@backstage/core-plugin-api": "^0.1.6", + "@backstage/plugin-catalog-react": "^0.4.3", + "@backstage/theme": "^0.2.10", + "@material-ui/core": "^4.12.2", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.45", + "react": "^16.13.1", + "react-dom": "^16.13.1", + "react-router-dom": "6.0.0-beta.0", + "react-use": "^17.2.4" + }, + "devDependencies": { + "@backstage/cli": "^0.7.9", + "@backstage/core-app-api": "^0.1.9", + "@backstage/dev-utils": "^0.2.7", + "@backstage/test-utils": "^0.1.17", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^11.2.5", + "@testing-library/user-event": "^13.1.8", + "@types/jest": "^26.0.7", + "@types/node": "^14.14.32", + "cross-fetch": "^3.0.6", + "msw": "^0.29.0" + }, + "files": [ + "dist" + ], + "configSchema": { + "$schema": "https://backstage.io/schema/config-v1", + "title": "@backstage/allure", + "type": "object", + "properties": { + "allure": { + "type": "object", + "properties": { + "baseUrl": { + "type": "string", + "visibility": "frontend" + } + } + } + } + } +} diff --git a/plugins/allure/src/api/AllureApi.ts b/plugins/allure/src/api/AllureApi.ts new file mode 100644 index 0000000000..a926da03d0 --- /dev/null +++ b/plugins/allure/src/api/AllureApi.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2021 The Backstage Authors + * + * 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 { createApiRef } from '@backstage/core-plugin-api'; + +export type AllureApi = { + getReportUrl(projectId: string): Promise; +}; + +export const allureApiRef = createApiRef({ + id: 'allure-api', +}); diff --git a/plugins/allure/src/api/AllureApiClient.ts b/plugins/allure/src/api/AllureApiClient.ts new file mode 100644 index 0000000000..698713c837 --- /dev/null +++ b/plugins/allure/src/api/AllureApiClient.ts @@ -0,0 +1,30 @@ +/* + * Copyright 2021 The Backstage Authors + * + * 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 { ConfigApi } from '@backstage/core-plugin-api'; +import { AllureApi } from './AllureApi'; + +export class AllureApiClient implements AllureApi { + readonly configApi: ConfigApi; + + constructor(options: { configApi: ConfigApi }) { + this.configApi = options.configApi; + } + + async getReportUrl(projectId: string): Promise { + const baseUrl = this.configApi.getString('allure.baseUrl'); + return `${baseUrl}/projects/${projectId}/reports/latest/index.html`; + } +} diff --git a/plugins/allure/src/api/index.ts b/plugins/allure/src/api/index.ts new file mode 100644 index 0000000000..1c1adb1db2 --- /dev/null +++ b/plugins/allure/src/api/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 The Backstage Authors + * + * 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. + */ +export { allureApiRef } from './AllureApi'; +export { AllureApiClient } from './AllureApiClient'; diff --git a/plugins/allure/src/components/AllureReportComponent/AllureReportComponent.test.tsx b/plugins/allure/src/components/AllureReportComponent/AllureReportComponent.test.tsx new file mode 100644 index 0000000000..38fa4c0923 --- /dev/null +++ b/plugins/allure/src/components/AllureReportComponent/AllureReportComponent.test.tsx @@ -0,0 +1,44 @@ +/* + * Copyright 2021 The Backstage Authors + * + * 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 React from 'react'; +import { AllureReportComponent } from './AllureReportComponent'; +import { ThemeProvider } from '@material-ui/core'; +import { lightTheme } from '@backstage/theme'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { msw, renderInTestApp } from '@backstage/test-utils'; + +describe('ExampleComponent', () => { + const server = setupServer(); + // Enable sane handlers for network requests + msw.setupDefaultHandlers(server); + + // setup mock response + beforeEach(() => { + server.use( + rest.get('/*', (_, res, ctx) => res(ctx.status(200), ctx.json({}))), + ); + }); + + it('should render', async () => { + const rendered = await renderInTestApp( + + + , + ); + expect(rendered.getByText('Welcome to allure!')).toBeInTheDocument(); + }); +}); diff --git a/plugins/allure/src/components/AllureReportComponent/AllureReportComponent.tsx b/plugins/allure/src/components/AllureReportComponent/AllureReportComponent.tsx new file mode 100644 index 0000000000..c59284b3ef --- /dev/null +++ b/plugins/allure/src/components/AllureReportComponent/AllureReportComponent.tsx @@ -0,0 +1,70 @@ +/* + * Copyright 2021 The Backstage Authors + * + * 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 React from 'react'; +import { useApi } from '@backstage/core-plugin-api'; +import { allureApiRef } from '../../api'; +import { AllureApi } from '../../api/AllureApi'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import { + ALLURE_PROJECT_ID_ANNOTATION, + isAllureReportAvailable, + useAllureProjectId, +} from '../useAllureProjectId'; +import { + MissingAnnotationEmptyState, + Progress, +} from '@backstage/core-components'; +import { useAsync } from 'react-use'; +import { Entity } from '@backstage/catalog-model'; + +const AllureReport = (props: { entity: Entity }) => { + const allureApi = useApi(allureApiRef); + + const allureProjectId = useAllureProjectId(props.entity); + const { value, loading } = useAsync(async () => { + const url = await allureApi.getReportUrl(allureProjectId); + return url; + }); + + if (loading) { + return ; + } + return ( + <> + {!loading && ( +