diff --git a/.changeset/unlucky-pigs-end.md b/.changeset/unlucky-pigs-end.md new file mode 100644 index 0000000000..7088a92c48 --- /dev/null +++ b/.changeset/unlucky-pigs-end.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-sonarqube': minor +--- + +Fix sonarqube annotation parsing. Add content page for Sonarqube. +Removed the deprecated `plugin` export; please use `sonarQubePlugin` instead. diff --git a/plugins/sonarqube/api-report.md b/plugins/sonarqube/api-report.md index e0a51479a4..2edf80618e 100644 --- a/plugins/sonarqube/api-report.md +++ b/plugins/sonarqube/api-report.md @@ -21,9 +21,17 @@ export const EntitySonarQubeCard: (props: { duplicationRatings?: DuplicationRating[] | undefined; }) => JSX.Element; +// @public (undocumented) +export const EntitySonarQubeContentPage: ( + props: SonarQubeContentPageProps, +) => JSX.Element; + // @public (undocumented) export const isSonarQubeAvailable: (entity: Entity) => boolean; +// @public (undocumented) +export const SONARQUBE_PROJECT_KEY_ANNOTATION = 'sonarqube.org/project-key'; + // @public (undocumented) export const SonarQubeCard: (props: { variant?: InfoCardVariants; @@ -31,7 +39,11 @@ export const SonarQubeCard: (props: { }) => JSX.Element; // @public (undocumented) -const sonarQubePlugin: BackstagePlugin<{}, {}, {}>; -export { sonarQubePlugin as plugin }; -export { sonarQubePlugin }; +export type SonarQubeContentPageProps = { + title?: string; + supportTitle?: string; +}; + +// @public (undocumented) +export const sonarQubePlugin: BackstagePlugin<{}, {}, {}>; ``` diff --git a/plugins/sonarqube/src/components/SonarQubeContentPage/SonarQubeContentPage.test.tsx b/plugins/sonarqube/src/components/SonarQubeContentPage/SonarQubeContentPage.test.tsx new file mode 100644 index 0000000000..8e847d7c31 --- /dev/null +++ b/plugins/sonarqube/src/components/SonarQubeContentPage/SonarQubeContentPage.test.tsx @@ -0,0 +1,91 @@ +/* + * 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 { EntityProvider } from '@backstage/plugin-catalog-react'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { lightTheme } from '@backstage/theme'; +import { ThemeProvider } from '@material-ui/core'; +import React from 'react'; +import { + isSonarQubeAvailable, + SONARQUBE_PROJECT_KEY_ANNOTATION, +} from '../useProjectKey'; +import { SonarQubeApi, sonarQubeApiRef } from '../../api'; + +const Providers = ({ + annotation, + children, +}: { annotation: string } & React.PropsWithChildren): JSX.Element => ( + + + {children} + + +); + +describe('', () => { + beforeAll(() => { + jest.mock('@backstage/plugin-sonarqube', () => ({ + __esModule: true, + isSonarQubeAvailable, + EntitySonarQubeCard: () => { + return
; + }, + })); + }); + + it('renders EntitySonarQubeCard', async () => { + const { SonarQubeContentPage } = require('./SonarQubeContentPage'); + + const rendered = await renderInTestApp( + + + , + ); + expect(rendered.getByText('SonarQube Dashboard')).toBeInTheDocument(); + expect(rendered.getByText('No information to display')).toBeInTheDocument(); + expect( + rendered.getByText("There is no SonarQube project with key 'bar'."), + ).toBeInTheDocument(); + }, 15000); + + it('renders MissingAnnotationEmptyState if sonar annotation is missing', async () => { + const { SonarQubeContentPage } = require('./SonarQubeContentPage'); + + const rendered = await renderInTestApp( + + + , + ); + expect(rendered.getByText('Missing Annotation')).toBeInTheDocument(); + expect( + rendered.getAllByText(SONARQUBE_PROJECT_KEY_ANNOTATION, { exact: false }) + .length, + ).toBe(2); + }, 15000); +}); diff --git a/plugins/sonarqube/src/components/SonarQubeContentPage/SonarQubeContentPage.tsx b/plugins/sonarqube/src/components/SonarQubeContentPage/SonarQubeContentPage.tsx new file mode 100644 index 0000000000..33a145b746 --- /dev/null +++ b/plugins/sonarqube/src/components/SonarQubeContentPage/SonarQubeContentPage.tsx @@ -0,0 +1,53 @@ +/* + * 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 { + Content, + ContentHeader, + SupportButton, +} from '@backstage/core-components'; +import { MissingAnnotationEmptyState } from '@backstage/core-components'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import React from 'react'; +import { + isSonarQubeAvailable, + SONARQUBE_PROJECT_KEY_ANNOTATION, +} from '../useProjectKey'; +import { SonarQubeCard } from '../SonarQubeCard'; + +/** @public */ +export type SonarQubeContentPageProps = { + title?: string; + supportTitle?: string; +}; + +export const SonarQubeContentPage = (props: SonarQubeContentPageProps) => { + const { entity } = useEntity(); + const { title, supportTitle } = props; + + return isSonarQubeAvailable(entity) ? ( + + + {supportTitle && {supportTitle}} + + + + ) : ( + + ); +}; diff --git a/plugins/sonarqube/src/components/SonarQubeContentPage/index.ts b/plugins/sonarqube/src/components/SonarQubeContentPage/index.ts new file mode 100644 index 0000000000..59327e9a88 --- /dev/null +++ b/plugins/sonarqube/src/components/SonarQubeContentPage/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 { SonarQubeContentPage } from './SonarQubeContentPage'; +export type { SonarQubeContentPageProps } from './SonarQubeContentPage'; diff --git a/plugins/sonarqube/src/components/index.ts b/plugins/sonarqube/src/components/index.ts index 7e58100efa..8e1d93f85b 100644 --- a/plugins/sonarqube/src/components/index.ts +++ b/plugins/sonarqube/src/components/index.ts @@ -14,5 +14,10 @@ * limitations under the License. */ -export * from './SonarQubeCard'; -export { isSonarQubeAvailable } from './useProjectKey'; +export { SonarQubeCard } from './SonarQubeCard'; +export type { DuplicationRating } from './SonarQubeCard'; +export type { SonarQubeContentPageProps } from './SonarQubeContentPage'; +export { + isSonarQubeAvailable, + SONARQUBE_PROJECT_KEY_ANNOTATION, +} from './useProjectKey'; diff --git a/plugins/sonarqube/src/components/useProjectKey.test.ts b/plugins/sonarqube/src/components/useProjectKey.test.ts index 921e9455c0..8686ac82de 100644 --- a/plugins/sonarqube/src/components/useProjectKey.test.ts +++ b/plugins/sonarqube/src/components/useProjectKey.test.ts @@ -39,10 +39,12 @@ describe('isSonarQubeAvailable', () => { 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: '', @@ -59,6 +61,7 @@ describe('isSonarQubeAvailable', () => { 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, @@ -68,6 +71,33 @@ describe('useProjectInfo', () => { 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); @@ -76,6 +106,7 @@ describe('useProjectInfo', () => { projectKey: DUMMY_KEY, }); }); + it('handle empty annotation', () => { const entity = createDummyEntity(''); expect(useProjectInfo(entity)).toEqual({ @@ -83,6 +114,7 @@ describe('useProjectInfo', () => { projectKey: undefined, }); }); + it('handle non-existent annotation', () => { const entity = { apiVersion: '', diff --git a/plugins/sonarqube/src/components/useProjectKey.ts b/plugins/sonarqube/src/components/useProjectKey.ts index 59a572a6c4..20f7f03bb3 100644 --- a/plugins/sonarqube/src/components/useProjectKey.ts +++ b/plugins/sonarqube/src/components/useProjectKey.ts @@ -16,6 +16,7 @@ import { Entity } from '@backstage/catalog-model'; +/** @public */ export const SONARQUBE_PROJECT_KEY_ANNOTATION = 'sonarqube.org/project-key'; export const SONARQUBE_PROJECT_INSTANCE_SEPARATOR = '/'; @@ -42,11 +43,16 @@ export const useProjectInfo = ( const annotation = entity?.metadata.annotations?.[SONARQUBE_PROJECT_KEY_ANNOTATION]; if (annotation) { - if (annotation.indexOf(SONARQUBE_PROJECT_INSTANCE_SEPARATOR) > -1) { - [projectInstance, projectKey] = annotation.split( - SONARQUBE_PROJECT_INSTANCE_SEPARATOR, - 2, - ); + 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; } diff --git a/plugins/sonarqube/src/index.ts b/plugins/sonarqube/src/index.ts index 1e6f83aa49..30f6351d4d 100644 --- a/plugins/sonarqube/src/index.ts +++ b/plugins/sonarqube/src/index.ts @@ -22,8 +22,4 @@ */ export * from './components'; -export { - sonarQubePlugin, - sonarQubePlugin as plugin, - EntitySonarQubeCard, -} from './plugin'; +export * from './plugin'; diff --git a/plugins/sonarqube/src/plugin.ts b/plugins/sonarqube/src/plugin.ts index 0ea62de492..5eb5ea82f3 100644 --- a/plugins/sonarqube/src/plugin.ts +++ b/plugins/sonarqube/src/plugin.ts @@ -52,3 +52,16 @@ export const EntitySonarQubeCard = sonarQubePlugin.provide( }, }), ); + +/** @public */ +export const EntitySonarQubeContentPage = sonarQubePlugin.provide( + createComponentExtension({ + name: 'EntitySonarQubeContentPage', + component: { + lazy: () => + import('./components/SonarQubeContentPage').then( + m => m.SonarQubeContentPage, + ), + }, + }), +);