diff --git a/plugins/sonarqube-react/.eslintrc.js b/plugins/sonarqube-react/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/sonarqube-react/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/sonarqube-react/package.json b/plugins/sonarqube-react/package.json new file mode 100644 index 0000000000..99e794e6cd --- /dev/null +++ b/plugins/sonarqube-react/package.json @@ -0,0 +1,48 @@ +{ + "name": "@backstage/plugin-sonarqube-react", + "version": "0.0.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", + "alphaTypes": "dist/index.alpha.d.ts" + }, + "backstage": { + "role": "web-library" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/sonarqube-react" + }, + "keywords": [ + "backstage" + ], + "scripts": { + "build": "backstage-cli package build --experimental-type-build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean", + "start": "backstage-cli package start" + }, + "dependencies": { + "@backstage/catalog-model": "workspace:^", + "@backstage/core-plugin-api": "workspace:^" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, + "devDependencies": { + "@backstage/cli": "workspace:^" + }, + "files": [ + "dist", + "alpha" + ] +} diff --git a/plugins/sonarqube/src/api/SonarQubeApi.ts b/plugins/sonarqube-react/src/api/SonarQubeApi.ts similarity index 67% rename from plugins/sonarqube/src/api/SonarQubeApi.ts rename to plugins/sonarqube-react/src/api/SonarQubeApi.ts index 9dbb6331dd..232ca535a9 100644 --- a/plugins/sonarqube/src/api/SonarQubeApi.ts +++ b/plugins/sonarqube-react/src/api/SonarQubeApi.ts @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Backstage Authors + * Copyright 2022 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. @@ -14,9 +14,36 @@ * limitations under the License. */ -import { MetricKey, SonarUrlProcessorFunc } from './types'; import { createApiRef } from '@backstage/core-plugin-api'; +export type MetricKey = + // alert status + | 'alert_status' + + // bugs and rating (-> reliability) + | 'bugs' + | 'reliability_rating' + + // vulnerabilities and rating (-> security) + | 'vulnerabilities' + | 'security_rating' + + // code smells and rating (-> maintainability) + | 'code_smells' + | 'sqale_rating' + + // security hotspots + | 'security_hotspots_reviewed' + | 'security_review_rating' + + // coverage + | 'coverage' + + // duplicated lines + | 'duplicated_lines_density'; + +export type SonarUrlProcessorFunc = (identifier: string) => string; + /** * Define a type to make sure that all metrics are used */ @@ -37,6 +64,7 @@ export const sonarQubeApiRef = createApiRef({ id: 'plugin.sonarqube.service', }); +/** @alpha */ export type SonarQubeApi = { getFindingSummary(options: { componentKey?: string; diff --git a/plugins/sonarqube-react/src/api/index.ts b/plugins/sonarqube-react/src/api/index.ts new file mode 100644 index 0000000000..35ebdacabe --- /dev/null +++ b/plugins/sonarqube-react/src/api/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2022 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 { sonarQubeApiRef } from './SonarQubeApi'; +export type { + Metrics, + MetricKey, + SonarQubeApi, + FindingSummary, + SonarUrlProcessorFunc, +} from './SonarQubeApi'; diff --git a/plugins/sonarqube-react/src/components/index.ts b/plugins/sonarqube-react/src/components/index.ts new file mode 100644 index 0000000000..a000986306 --- /dev/null +++ b/plugins/sonarqube-react/src/components/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright 2022 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 { + isSonarQubeAvailable, + SONARQUBE_PROJECT_KEY_ANNOTATION, + SONARQUBE_PROJECT_INSTANCE_SEPARATOR, +} from './isSonarQubeAvailable'; diff --git a/plugins/sonarqube-react/src/components/isSonarQubeAvailable.test.ts b/plugins/sonarqube-react/src/components/isSonarQubeAvailable.test.ts new file mode 100644 index 0000000000..9053b1a347 --- /dev/null +++ b/plugins/sonarqube-react/src/components/isSonarQubeAvailable.test.ts @@ -0,0 +1,58 @@ +/* + * Copyright 2022 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 { Entity } from '@backstage/catalog-model'; +import { + isSonarQubeAvailable, + SONARQUBE_PROJECT_KEY_ANNOTATION, +} from './isSonarQubeAvailable'; + +const createDummyEntity = (sonarqubeAnnotationValue: string): Entity => { + return { + apiVersion: '', + kind: '', + metadata: { + name: 'dummy', + annotations: { + [SONARQUBE_PROJECT_KEY_ANNOTATION]: sonarqubeAnnotationValue, + }, + }, + }; +}; + +describe('isSonarQubeAvailable', () => { + it('returns true if sonarqube annotation defined', () => { + const entity = createDummyEntity('dummy'); + expect(isSonarQubeAvailable(entity)).toBe(true); + }); + + it('returns false if sonarqube annotation empty', () => { + const entity = createDummyEntity(''); + expect(isSonarQubeAvailable(entity)).toBe(false); + }); + + it('returns false if sonarqube annotation not defined', () => { + const entity = { + apiVersion: '', + kind: '', + metadata: { + name: 'dummy', + annotations: {}, + }, + }; + expect(isSonarQubeAvailable(entity)).toBe(false); + }); +}); diff --git a/plugins/sonarqube-react/src/components/isSonarQubeAvailable.ts b/plugins/sonarqube-react/src/components/isSonarQubeAvailable.ts new file mode 100644 index 0000000000..b8504809c3 --- /dev/null +++ b/plugins/sonarqube-react/src/components/isSonarQubeAvailable.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2022 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 { Entity } from '@backstage/catalog-model'; + +export const SONARQUBE_PROJECT_KEY_ANNOTATION = 'sonarqube.org/project-key'; +export const SONARQUBE_PROJECT_INSTANCE_SEPARATOR = '/'; + +/** @public */ +export const isSonarQubeAvailable = (entity: Entity) => + Boolean(entity.metadata.annotations?.[SONARQUBE_PROJECT_KEY_ANNOTATION]); diff --git a/plugins/sonarqube-react/src/hooks/index.ts b/plugins/sonarqube-react/src/hooks/index.ts new file mode 100644 index 0000000000..16892a5e3d --- /dev/null +++ b/plugins/sonarqube-react/src/hooks/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 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 { useProjectInfo } from './useProjectInfo'; diff --git a/plugins/sonarqube-react/src/hooks/useProjectInfo.test.ts b/plugins/sonarqube-react/src/hooks/useProjectInfo.test.ts new file mode 100644 index 0000000000..2babb4cd42 --- /dev/null +++ b/plugins/sonarqube-react/src/hooks/useProjectInfo.test.ts @@ -0,0 +1,108 @@ +/* + * Copyright 2022 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 { Entity } from '@backstage/catalog-model'; +import { + SONARQUBE_PROJECT_INSTANCE_SEPARATOR, + SONARQUBE_PROJECT_KEY_ANNOTATION, +} from '../components'; +import { useProjectInfo } from './useProjectInfo'; + +const createDummyEntity = (sonarqubeAnnotationValue: string): Entity => { + return { + apiVersion: '', + kind: '', + metadata: { + name: 'dummy', + annotations: { + [SONARQUBE_PROJECT_KEY_ANNOTATION]: sonarqubeAnnotationValue, + }, + }, + }; +}; + +describe('useProjectInfo', () => { + const DUMMY_INSTANCE = 'dummyInstance'; + const DUMMY_KEY = 'dummyKey'; + + it('parse annotation with key and instance', () => { + const entity = createDummyEntity( + DUMMY_INSTANCE + SONARQUBE_PROJECT_INSTANCE_SEPARATOR + DUMMY_KEY, + ); + expect(useProjectInfo(entity)).toEqual({ + projectInstance: DUMMY_INSTANCE, + projectKey: DUMMY_KEY, + }); + }); + + it('parse annotation with instance, tenant/project-key', () => { + const DUMMY_KEY_WITH_TENANT = 'dummy-tenant/dummyKey'; + const entity = createDummyEntity( + DUMMY_INSTANCE + + SONARQUBE_PROJECT_INSTANCE_SEPARATOR + + DUMMY_KEY_WITH_TENANT, + ); + expect(useProjectInfo(entity)).toEqual({ + projectInstance: DUMMY_INSTANCE, + projectKey: DUMMY_KEY_WITH_TENANT, + }); + }); + + it('parse annotation with instance, tenant:project-key', () => { + const DUMMY_KEY_WITH_TENANT = 'dummy-tenant:dummyKey'; + const entity = createDummyEntity( + DUMMY_INSTANCE + + SONARQUBE_PROJECT_INSTANCE_SEPARATOR + + DUMMY_KEY_WITH_TENANT, + ); + expect(useProjectInfo(entity)).toEqual({ + projectInstance: DUMMY_INSTANCE, + projectKey: DUMMY_KEY_WITH_TENANT, + }); + }); + + // compatibility with previous mono-instance sonarqube config + it('parse annotation with only key', () => { + const entity = createDummyEntity(DUMMY_KEY); + expect(useProjectInfo(entity)).toEqual({ + projectInstance: undefined, + projectKey: DUMMY_KEY, + }); + }); + + it('handle empty annotation', () => { + const entity = createDummyEntity(''); + expect(useProjectInfo(entity)).toEqual({ + projectInstance: undefined, + projectKey: undefined, + }); + }); + + it('handle non-existent annotation', () => { + const entity = { + apiVersion: '', + kind: '', + metadata: { + name: 'dummy', + annotations: {}, + }, + }; + expect(useProjectInfo(entity)).toEqual({ + projectInstance: undefined, + projectKey: undefined, + }); + }); +}); diff --git a/plugins/sonarqube-react/src/hooks/useProjectInfo.ts b/plugins/sonarqube-react/src/hooks/useProjectInfo.ts new file mode 100644 index 0000000000..61c8baed98 --- /dev/null +++ b/plugins/sonarqube-react/src/hooks/useProjectInfo.ts @@ -0,0 +1,59 @@ +/* + * Copyright 2022 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 { Entity } from '@backstage/catalog-model'; +import { + SONARQUBE_PROJECT_INSTANCE_SEPARATOR, + SONARQUBE_PROJECT_KEY_ANNOTATION, +} from '../components'; + +/** + * + * Try to parse sonarqube information from an entity. + * + * If part or all info are not found, they will default to undefined + * + * @alpha + * @param entity entity to find the sonarqube information from. + * @return a ProjectInfo properly populated. + */ +export const useProjectInfo = ( + entity: Entity, +): { + projectInstance: string | undefined; + projectKey: string | undefined; +} => { + let projectInstance = undefined; + let projectKey = undefined; + const annotation = + entity?.metadata.annotations?.[SONARQUBE_PROJECT_KEY_ANNOTATION]; + if (annotation) { + const instanceSeparatorIndex = annotation.indexOf( + SONARQUBE_PROJECT_INSTANCE_SEPARATOR, + ); + if (instanceSeparatorIndex > -1) { + // Examples: + // instanceA/projectA -> projectInstance = "instanceA" & projectKey = "projectA" + // instanceA/tenantA:projectA -> projectInstance = "instanceA" & projectKey = "tenantA:projectA" + // instanceA/tenantA/projectA -> projectInstance = "instanceA" & projectKey = "tenantA/projectA" + projectInstance = annotation.substring(0, instanceSeparatorIndex); + projectKey = annotation.substring(instanceSeparatorIndex + 1); + } else { + projectKey = annotation; + } + } + return { projectInstance, projectKey }; +}; diff --git a/plugins/sonarqube-react/src/index.ts b/plugins/sonarqube-react/src/index.ts new file mode 100644 index 0000000000..fe04a0459f --- /dev/null +++ b/plugins/sonarqube-react/src/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2022 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 * from './api'; +export * from './components'; +export * from './hooks';