From 26c830d6910e9a26444483f825fb8bb8631ac962 Mon Sep 17 00:00:00 2001 From: Jeremy Davis Date: Fri, 21 Jan 2022 17:48:32 +0100 Subject: [PATCH] SONAR-15951 Reintroduce portfolio breakdown --- .../components/extensions/exposeLibraries.ts | 4 + .../js/app/components/nav/component/Menu.tsx | 18 +- .../nav/component/__tests__/Menu-test.tsx | 34 +- .../__snapshots__/Menu-test.tsx.snap | 562 ++++++++++++------ .../js/apps/code/components/Breadcrumbs.tsx | 1 + .../js/apps/code/components/Component.tsx | 3 + .../js/apps/code/components/ComponentName.tsx | 24 +- .../js/apps/code/components/Components.tsx | 1 + .../components/__tests__/Component-test.tsx | 1 + .../__tests__/ComponentName-test.tsx | 18 + .../__snapshots__/Component-test.tsx.snap | 134 +++++ .../__snapshots__/ComponentName-test.tsx.snap | 85 +++ .../__snapshots__/Components-test.tsx.snap | 2 + .../resources/org/sonar/l10n/core.properties | 1 + 14 files changed, 674 insertions(+), 214 deletions(-) diff --git a/server/sonar-web/src/main/js/app/components/extensions/exposeLibraries.ts b/server/sonar-web/src/main/js/app/components/extensions/exposeLibraries.ts index 520549a347d..d85617d3fa7 100644 --- a/server/sonar-web/src/main/js/app/components/extensions/exposeLibraries.ts +++ b/server/sonar-web/src/main/js/app/components/extensions/exposeLibraries.ts @@ -51,6 +51,7 @@ import AlertSuccessIcon from '../../../components/icons/AlertSuccessIcon'; import AlertWarnIcon from '../../../components/icons/AlertWarnIcon'; import BranchIcon from '../../../components/icons/BranchIcon'; import BranchLikeIcon from '../../../components/icons/BranchLikeIcon'; +import BulletListIcon from '../../../components/icons/BulletListIcon'; import CheckIcon from '../../../components/icons/CheckIcon'; import ClearIcon from '../../../components/icons/ClearIcon'; import DetachIcon from '../../../components/icons/DetachIcon'; @@ -106,6 +107,7 @@ import { renderSonarSourceSecurityCategory } from '../../../helpers/security-standard'; import { + getCodeUrl, getComponentDrilldownUrl, getComponentIssuesUrl, getComponentSecurityHotspotsUrl, @@ -155,6 +157,7 @@ const exposeLibraries = () => { renderOwaspTop10Category, renderSansTop25Category, renderSonarSourceSecurityCategory, + getCodeUrl, getComponentDrilldownUrl, getComponentIssuesUrl, getComponentSecurityHotspotsUrl, @@ -192,6 +195,7 @@ const exposeLibraries = () => { AlertWarnIcon, BranchIcon: BranchLikeIcon, BoxedTabs, + BulletListIcon, Button, Checkbox, CheckIcon, diff --git a/server/sonar-web/src/main/js/app/components/nav/component/Menu.tsx b/server/sonar-web/src/main/js/app/components/nav/component/Menu.tsx index 11e68b702e1..4cb44009b46 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/Menu.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/Menu.tsx @@ -188,15 +188,22 @@ export class Menu extends React.PureComponent { ); }; + renderBreakdownLink = () => { + return this.isPortfolio() && this.isGovernanceEnabled() + ? this.renderMenuLink({ + label: translate('portfolio_breakdown.page'), + to: { pathname: '/code' } + }) + : null; + }; + renderCodeLink = () => { - const isPortfolio = this.isPortfolio(); - const isApplication = this.isApplication(); - const label = - isPortfolio || isApplication ? translate('view_projects.page') : translate('code.page'); - if (this.isDeveloper()) { + if (this.isPortfolio() || this.isDeveloper()) { return null; } + const label = this.isApplication() ? translate('view_projects.page') : translate('code.page'); + return this.renderMenuLink({ label, to: { pathname: '/code' } }); }; @@ -627,6 +634,7 @@ export class Menu extends React.PureComponent {
{this.renderDashboardLink()} + {this.renderBreakdownLink()} {this.renderIssuesLink()} {this.renderSecurityHotspotsLink()} {this.renderSecurityReports()} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/Menu-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/Menu-test.tsx index 26fe69a3388..5f40441b64b 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/Menu-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/Menu-test.tsx @@ -120,24 +120,22 @@ it('should work for pull requests', () => { ); }); -it('should work for all qualifiers', () => { - [ - ComponentQualifier.Project, - ComponentQualifier.Portfolio, - ComponentQualifier.SubPortfolio, - ComponentQualifier.Application - ].forEach(checkWithQualifier); - expect.assertions(4); - - function checkWithQualifier(qualifier: string) { - const component = { - ...baseComponent, - canBrowseAllChildProjects: true, - configuration: { showSettings: true }, - qualifier - }; - expect(shallowRender({ component })).toMatchSnapshot(); - } +it.each([ + [ComponentQualifier.Project, false], + [ComponentQualifier.Portfolio, false], + [ComponentQualifier.Portfolio, true], + [ComponentQualifier.SubPortfolio, false], + [ComponentQualifier.SubPortfolio, true], + [ComponentQualifier.Application, false] +])('should work for qualifier: %s, %s', (qualifier, enableGovernance) => { + const component = { + ...baseComponent, + canBrowseAllChildProjects: true, + configuration: { showSettings: true }, + extensions: enableGovernance ? [{ key: 'governance/', name: 'governance' }] : [], + qualifier + }; + expect(shallowRender({ component })).toMatchSnapshot(); }); it('should disable links if no analysis has been done', () => { diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/Menu-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/Menu-test.tsx.snap index 53cd6c07654..e97e4aa5b53 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/Menu-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/Menu-test.tsx.snap @@ -725,7 +725,7 @@ exports[`should work for a branch 2`] = `
`; -exports[`should work for all qualifiers 1`] = ` +exports[`should work for pull requests 1`] = `
@@ -740,6 +740,7 @@ exports[`should work for all qualifiers 1`] = ` "pathname": "/dashboard", "query": Object { "id": "foo", + "pullRequest": "1001", }, } } @@ -757,6 +758,7 @@ exports[`should work for all qualifiers 1`] = ` "pathname": "/project/issues", "query": Object { "id": "foo", + "pullRequest": "1001", "resolved": "false", }, } @@ -775,6 +777,7 @@ exports[`should work for all qualifiers 1`] = ` "pathname": "/security_hotspots", "query": Object { "id": "foo", + "pullRequest": "1001", }, } } @@ -792,6 +795,7 @@ exports[`should work for all qualifiers 1`] = ` "pathname": "/component_measures", "query": Object { "id": "foo", + "pullRequest": "1001", }, } } @@ -809,6 +813,7 @@ exports[`should work for all qualifiers 1`] = ` "pathname": "/code", "query": Object { "id": "foo", + "pullRequest": "1001", }, } } @@ -816,27 +821,8 @@ exports[`should work for all qualifiers 1`] = ` code.page -
  • - - project_activity.page - -
  • - - - project_settings.page - - -
  • - - project_branch_pull_request.page - -
  • -
  • - - project_baseline.page + ComponentFoo
  • + + } + tagName="li" + > + +
    +
    + +
    +`; + +exports[`should work for pull requests 2`] = ` +
    + +
  • + + overview.page + +
  • +
  • + + issues.page + +
  • +
  • + + layout.security_hotspots + +
  • +
  • + + layout.measures + +
  • +
  • + + code.page + +
  • +
  • - project_dump.page + ComponentFoo
  • + + } + tagName="li" + > + +
    +
    + +
    +`; + +exports[`should work for qualifier: APP, false 1`] = ` +
    + +
  • + + overview.page + +
  • +
  • + + issues.page + +
  • +
  • + + layout.security_hotspots + +
  • +
  • + + layout.measures + +
  • +
  • + + view_projects.page + +
  • +
  • + + project_activity.page + +
  • +
    + +
  • - webhooks.page + application_console.page
  • @@ -959,14 +1155,14 @@ exports[`should work for all qualifiers 1`] = ` - project.info.title + application.info.title
  • `; -exports[`should work for all qualifiers 2`] = ` +exports[`should work for qualifier: SVW, false 1`] = `
    @@ -1013,16 +1209,26 @@ exports[`should work for all qualifiers 2`] = ` style={Object {}} to={ Object { - "pathname": "/code", + "pathname": "/project/activity", "query": Object { "id": "foo", }, } } > - view_projects.page + project_activity.page + + +
    +`; + +exports[`should work for qualifier: SVW, true 1`] = ` +
    +
  • - project_activity.page + overview.page
  • -
    - - -
  • - - deletion.page - -
  • - - } - tagName="li" - > - -
    -
    -
    -`; - -exports[`should work for all qualifiers 3`] = ` -
    -
  • - issues.page + portfolio_breakdown.page
  • @@ -1105,14 +1270,15 @@ exports[`should work for all qualifiers 3`] = ` style={Object {}} to={ Object { - "pathname": "/component_measures", + "pathname": "/project/issues", "query": Object { "id": "foo", + "resolved": "false", }, } } > - layout.measures + issues.page
  • @@ -1122,14 +1288,14 @@ exports[`should work for all qualifiers 3`] = ` style={Object {}} to={ Object { - "pathname": "/code", + "pathname": "/component_measures", "query": Object { "id": "foo", }, } } > - view_projects.page + layout.measures
  • @@ -1154,7 +1320,7 @@ exports[`should work for all qualifiers 3`] = `
  • `; -exports[`should work for all qualifiers 4`] = ` +exports[`should work for qualifier: TRK, false 1`] = `
    @@ -1242,7 +1408,7 @@ exports[`should work for all qualifiers 4`] = ` } } > - view_projects.page + code.page
  • @@ -1277,14 +1443,82 @@ exports[`should work for all qualifiers 4`] = ` style={Object {}} to={ Object { - "pathname": "/application/console", + "pathname": "/project/settings", "query": Object { "id": "foo", }, } } > - application_console.page + project_settings.page + +
  • +
  • + + project_branch_pull_request.page + +
  • +
  • + + project_baseline.page + +
  • +
  • + + project_dump.page + +
  • +
  • + + webhooks.page
  • @@ -1320,36 +1554,18 @@ exports[`should work for all qualifiers 4`] = ` - application.info.title + project.info.title
  • `; -exports[`should work for pull requests 1`] = ` +exports[`should work for qualifier: VW, false 1`] = `
    -
  • - - overview.page - -
  • -
  • - - layout.security_hotspots - -
  • - code.page + project_activity.page
  • +
    + - ComponentFoo + deletion.page @@ -1455,11 +1650,10 @@ exports[`should work for pull requests 1`] = ` -
    `; -exports[`should work for pull requests 2`] = ` +exports[`should work for qualifier: VW, true 1`] = `
    @@ -1471,10 +1665,9 @@ exports[`should work for pull requests 2`] = ` style={Object {}} to={ Object { - "pathname": "/dashboard", + "pathname": "/portfolio", "query": Object { "id": "foo", - "pullRequest": "1001", }, } } @@ -1489,16 +1682,14 @@ exports[`should work for pull requests 2`] = ` style={Object {}} to={ Object { - "pathname": "/project/issues", + "pathname": "/code", "query": Object { "id": "foo", - "pullRequest": "1001", - "resolved": "false", }, } } > - issues.page + portfolio_breakdown.page
  • @@ -1508,15 +1699,15 @@ exports[`should work for pull requests 2`] = ` style={Object {}} to={ Object { - "pathname": "/security_hotspots", + "pathname": "/project/issues", "query": Object { "id": "foo", - "pullRequest": "1001", + "resolved": "false", }, } } > - layout.security_hotspots + issues.page
  • @@ -1529,7 +1720,6 @@ exports[`should work for pull requests 2`] = ` "pathname": "/component_measures", "query": Object { "id": "foo", - "pullRequest": "1001", }, } } @@ -1544,19 +1734,20 @@ exports[`should work for pull requests 2`] = ` style={Object {}} to={ Object { - "pathname": "/code", + "pathname": "/project/activity", "query": Object { "id": "foo", - "pullRequest": "1001", }, } } > - code.page + project_activity.page
  • + + - ComponentFoo + deletion.page @@ -1587,7 +1776,6 @@ exports[`should work for pull requests 2`] = ` -
    `; diff --git a/server/sonar-web/src/main/js/apps/code/components/Breadcrumbs.tsx b/server/sonar-web/src/main/js/apps/code/components/Breadcrumbs.tsx index 5b77127f21c..782af6e1793 100644 --- a/server/sonar-web/src/main/js/apps/code/components/Breadcrumbs.tsx +++ b/server/sonar-web/src/main/js/apps/code/components/Breadcrumbs.tsx @@ -37,6 +37,7 @@ export default function Breadcrumbs({ branchLike, breadcrumbs, rootComponent }: canBrowse={index < breadcrumbs.length - 1} component={component} rootComponent={rootComponent} + unclickable={true} /> ))} diff --git a/server/sonar-web/src/main/js/apps/code/components/Component.tsx b/server/sonar-web/src/main/js/apps/code/components/Component.tsx index de6abf0e4ff..61e9f2d02a9 100644 --- a/server/sonar-web/src/main/js/apps/code/components/Component.tsx +++ b/server/sonar-web/src/main/js/apps/code/components/Component.tsx @@ -34,6 +34,7 @@ interface Props { canBrowse?: boolean; component: T.ComponentMeasure; hasBaseComponent: boolean; + isBaseComponent?: boolean; metrics: T.Metric[]; previous?: T.ComponentMeasure; rootComponent: T.ComponentMeasure; @@ -48,6 +49,7 @@ export class Component extends React.PureComponent { canBrowse = false, component, hasBaseComponent, + isBaseComponent = false, metrics, previous, rootComponent, @@ -87,6 +89,7 @@ export class Component extends React.PureComponent { component={component} previous={previous} rootComponent={rootComponent} + unclickable={isBaseComponent} /> diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentName.tsx b/server/sonar-web/src/main/js/apps/code/components/ComponentName.tsx index 0d60199053d..cd67da0d00e 100644 --- a/server/sonar-web/src/main/js/apps/code/components/ComponentName.tsx +++ b/server/sonar-web/src/main/js/apps/code/components/ComponentName.tsx @@ -24,9 +24,14 @@ import BranchIcon from '../../../components/icons/BranchIcon'; import QualifierIcon from '../../../components/icons/QualifierIcon'; import { getBranchLikeQuery } from '../../../helpers/branch-like'; import { translate } from '../../../helpers/l10n'; -import { getProjectUrl } from '../../../helpers/urls'; +import { getComponentOverviewUrl } from '../../../helpers/urls'; import { BranchLike } from '../../../types/branch-like'; -import { ComponentQualifier } from '../../../types/component'; +import { + ComponentQualifier, + isApplication, + isPortfolioLike, + isProject +} from '../../../types/component'; export function getTooltip(component: T.ComponentMeasure) { const isFile = component.qualifier === 'FIL' || component.qualifier === 'UTS'; @@ -59,11 +64,13 @@ export interface Props { component: T.ComponentMeasure; previous?: T.ComponentMeasure; rootComponent: T.ComponentMeasure; + unclickable?: boolean; } export default function ComponentName({ branchLike, component, + unclickable = false, rootComponent, previous, canBrowse = false @@ -84,14 +91,23 @@ export default function ComponentName({ let inner = null; - if (component.refKey && component.qualifier !== ComponentQualifier.SubPortfolio) { + if ( + !unclickable && + (isPortfolioLike(component.qualifier) || + isApplication(component.qualifier) || + isProject(component.qualifier)) + ) { const branch = [ComponentQualifier.Application, ComponentQualifier.Portfolio].includes( rootComponent.qualifier as ComponentQualifier ) ? component.branch : undefined; inner = ( - + {name} ); diff --git a/server/sonar-web/src/main/js/apps/code/components/Components.tsx b/server/sonar-web/src/main/js/apps/code/components/Components.tsx index 2d803f21daf..0946b2275b9 100644 --- a/server/sonar-web/src/main/js/apps/code/components/Components.tsx +++ b/server/sonar-web/src/main/js/apps/code/components/Components.tsx @@ -62,6 +62,7 @@ export class Components extends React.PureComponent { canBePinned={canBePinned} component={baseComponent} hasBaseComponent={false} + isBaseComponent={true} key={baseComponent.key} metrics={this.props.metrics} rootComponent={rootComponent} diff --git a/server/sonar-web/src/main/js/apps/code/components/__tests__/Component-test.tsx b/server/sonar-web/src/main/js/apps/code/components/__tests__/Component-test.tsx index 1bdd69d6c41..6d857a8a55a 100644 --- a/server/sonar-web/src/main/js/apps/code/components/__tests__/Component-test.tsx +++ b/server/sonar-web/src/main/js/apps/code/components/__tests__/Component-test.tsx @@ -26,6 +26,7 @@ import { Component } from '../Component'; it('should render correctly', () => { expect(shallowRender()).toMatchSnapshot(); expect(shallowRender({ hasBaseComponent: true })).toMatchSnapshot('with base component'); + expect(shallowRender({ isBaseComponent: true })).toMatchSnapshot('is base component'); }); it('should render correctly for a file', () => { diff --git a/server/sonar-web/src/main/js/apps/code/components/__tests__/ComponentName-test.tsx b/server/sonar-web/src/main/js/apps/code/components/__tests__/ComponentName-test.tsx index 17d0976813f..33086dfc493 100644 --- a/server/sonar-web/src/main/js/apps/code/components/__tests__/ComponentName-test.tsx +++ b/server/sonar-web/src/main/js/apps/code/components/__tests__/ComponentName-test.tsx @@ -105,6 +105,24 @@ describe('#ComponentName', () => { }) ).toMatchSnapshot(); }); + + it.each([ + [ComponentQualifier.Application, 'refKey'], + [ComponentQualifier.Portfolio, 'refKey'], + [ComponentQualifier.SubPortfolio, 'refKey'], + [ComponentQualifier.Project, 'refKey'], + [ComponentQualifier.Project, undefined] + ])('should render breadcrumb correctly for %s', (qualifier, refKey) => { + expect( + shallowRender({ + component: mockComponentMeasure(false, { + refKey, + qualifier + }), + unclickable: true + }) + ).toMatchSnapshot(); + }); }); function shallowRender(props: Partial = {}) { diff --git a/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/Component-test.tsx.snap b/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/Component-test.tsx.snap index 2d8e79a5529..d1e4c539e92 100644 --- a/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/Component-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/Component-test.tsx.snap @@ -49,6 +49,7 @@ exports[`should render correctly 1`] = ` "qualifier": "TRK", } } + unclickable={false} /> @@ -185,6 +186,7 @@ exports[`should render correctly for a file 1`] = ` "qualifier": "TRK", } } + unclickable={false} /> @@ -262,6 +264,137 @@ exports[`should render correctly for a file 1`] = ` `; +exports[`should render correctly: is base component 1`] = ` + + + + +
    + +
    + + +
    + +
    + + +
    + +
    + + + +`; + exports[`should render correctly: with base component 1`] = ` diff --git a/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/ComponentName-test.tsx.snap b/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/ComponentName-test.tsx.snap index fe5e63e44a1..d045ac05387 100644 --- a/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/ComponentName-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/ComponentName-test.tsx.snap @@ -1,5 +1,90 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`#ComponentName should render breadcrumb correctly for APP 1`] = ` + + + + + Foo + + +`; + +exports[`#ComponentName should render breadcrumb correctly for SVW 1`] = ` + + + + + Foo + + +`; + +exports[`#ComponentName should render breadcrumb correctly for TRK 1`] = ` + + + + + Foo + + +`; + +exports[`#ComponentName should render breadcrumb correctly for TRK 2`] = ` + + + + + Foo + + +`; + +exports[`#ComponentName should render breadcrumb correctly for VW 1`] = ` + + + + + Foo + + +`; + exports[`#ComponentName should render correctly for dirs 1`] = `