From b994ce6100ae6664b1b08aafbaf763cefdf582cb Mon Sep 17 00:00:00 2001 From: Wouter Admiraal Date: Tue, 28 Apr 2020 15:47:44 +0200 Subject: [PATCH] SONAR-13339 Disable component menu links when no analysis has been run yet --- .../components/nav/component/ComponentNav.tsx | 3 + .../js/app/components/nav/component/Menu.css | 7 + .../js/app/components/nav/component/Menu.tsx | 346 +++++++------ .../nav/component/__tests__/Menu-test.tsx | 25 +- .../__snapshots__/ComponentNav-test.tsx.snap | 3 + .../__snapshots__/Menu-test.tsx.snap | 477 ++++++++++-------- .../resources/org/sonar/l10n/core.properties | 1 + 7 files changed, 486 insertions(+), 376 deletions(-) diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx index 29f8c596785..ceeae0dbbf6 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx @@ -104,7 +104,10 @@ export default function ComponentNav(props: Props) { setDisplayProjectInfo(!displayProjectInfo)} /> li > a.disabled-link, +.navbar-tabs > li > a.disabled-link:hover { + color: var(--gray71); + cursor: default; + border-bottom-color: transparent; +} 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 c1aea4a7bfa..b217d44e23c 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 @@ -19,8 +19,9 @@ */ import * as classNames from 'classnames'; import * as React from 'react'; -import { Link } from 'react-router'; +import { Link, LinkProps } from 'react-router'; import Dropdown from 'sonar-ui-common/components/controls/Dropdown'; +import Tooltip from 'sonar-ui-common/components/controls/Tooltip'; import BulletListIcon from 'sonar-ui-common/components/icons/BulletListIcon'; import DropdownIcon from 'sonar-ui-common/components/icons/DropdownIcon'; import NavBarTabs from 'sonar-ui-common/components/ui/NavBarTabs'; @@ -28,7 +29,7 @@ import { hasMessage, translate } from 'sonar-ui-common/helpers/l10n'; import { withAppState } from '../../../../components/hoc/withAppState'; import { getBranchLikeQuery, isMainBranch, isPullRequest } from '../../../../helpers/branch-like'; import { isSonarCloud } from '../../../../helpers/system'; -import { BranchLike } from '../../../../types/branch-like'; +import { BranchLike, BranchParameters } from '../../../../types/branch-like'; import { ComponentQualifier } from '../../../../types/component'; import './Menu.css'; @@ -52,66 +53,101 @@ const SETTINGS_URLS = [ interface Props { appState: Pick; branchLike: BranchLike | undefined; + branchLikes: BranchLike[] | undefined; component: T.Component; + isInProgress?: boolean; + isPending?: boolean; onToggleProjectInfo: () => void; } +type Query = BranchParameters & { id: string }; + +function MenuLink({ + hasAnalysis, + label, + ...props +}: LinkProps & { hasAnalysis: boolean; label: React.ReactNode }) { + return hasAnalysis ? ( + {label} + ) : ( + + + {label} + + + ); +} + export class Menu extends React.PureComponent { - isProject() { + hasAnalysis = () => { + const { branchLikes = [], component, isInProgress, isPending } = this.props; + const hasBranches = branchLikes.length > 1; + return hasBranches || isInProgress || isPending || component.analysisDate !== undefined; + }; + + isProject = () => { return this.props.component.qualifier === ComponentQualifier.Project; - } + }; - isDeveloper() { + isDeveloper = () => { return this.props.component.qualifier === ComponentQualifier.Developper; - } + }; - isPortfolio() { + isPortfolio = () => { const { qualifier } = this.props.component; return ( qualifier === ComponentQualifier.Portfolio || qualifier === ComponentQualifier.SubPortfolio ); - } + }; - isApplication() { + isApplication = () => { return this.props.component.qualifier === ComponentQualifier.Application; - } + }; - getConfiguration() { + getConfiguration = () => { return this.props.component.configuration || {}; - } + }; - getQuery = () => { + getQuery = (): Query => { return { id: this.props.component.key, ...getBranchLikeQuery(this.props.branchLike) }; }; - renderDashboardLink() { - const pathname = this.isPortfolio() ? '/portfolio' : '/dashboard'; + renderDashboardLink = (query: Query, isPortfolio: boolean) => { + const pathname = isPortfolio ? '/portfolio' : '/dashboard'; return (
  • - + {translate('overview.page')}
  • ); - } + }; - renderCodeLink() { + renderCodeLink = ( + hasAnalysis: boolean, + query: Query, + isApplication: boolean, + isPortfolio: boolean + ) => { if (this.isDeveloper()) { return null; } return (
  • - - {this.isPortfolio() || this.isApplication() - ? translate('view_projects.page') - : translate('code.page')} - +
  • ); - } + }; - renderActivityLink() { + renderActivityLink = (hasAnalysis: boolean, query: Query) => { const { branchLike } = this.props; if (isPullRequest(branchLike)) { @@ -120,54 +156,58 @@ export class Menu extends React.PureComponent { return (
  • - - {translate('project_activity.page')} - + hasAnalysis={hasAnalysis} + label={translate('project_activity.page')} + to={{ pathname: '/project/activity', query }} + />
  • ); - } + }; - renderIssuesLink() { + renderIssuesLink = (hasAnalysis: boolean, query: Query) => { return (
  • - - {translate('issues.page')} - + hasAnalysis={hasAnalysis} + label={translate('issues.page')} + to={{ pathname: '/project/issues', query: { ...query, resolved: 'false' } }} + />
  • ); - } + }; - renderComponentMeasuresLink() { + renderComponentMeasuresLink = (hasAnalysis: boolean, query: Query) => { return (
  • - - {translate('layout.measures')} - + hasAnalysis={hasAnalysis} + label={translate('layout.measures')} + to={{ pathname: '/component_measures', query }} + />
  • ); - } + }; - renderSecurityHotspotsLink() { + renderSecurityHotspotsLink = (hasAnalysis: boolean, query: Query, isPortfolio: boolean) => { return ( - !this.isPortfolio() && ( + !isPortfolio && (
  • - - {translate('layout.security_hotspots')} - + hasAnalysis={hasAnalysis} + label={translate('layout.security_hotspots')} + to={{ pathname: '/security_hotspots', query }} + />
  • ) ); - } + }; - renderSecurityReports() { + renderSecurityReports = (hasAnalysis: boolean, query: Query) => { const { branchLike, component } = this.props; const { extensions = [] } = component; @@ -185,19 +225,25 @@ export class Menu extends React.PureComponent { return (
  • - - {translate('layout.security_reports')} - + query + }} + />
  • ); - } + }; - renderAdministration() { + renderAdministration = ( + query: Query, + isProject: boolean, + isApplication: boolean, + isPortfolio: boolean + ) => { const { branchLike, component } = this.props; if (!this.getConfiguration().showSettings || isPullRequest(branchLike)) { @@ -206,7 +252,7 @@ export class Menu extends React.PureComponent { const isSettingsActive = SETTINGS_URLS.some(url => window.location.href.indexOf(url) !== -1); - const adminLinks = this.renderAdministrationLinks(); + const adminLinks = this.renderAdministrationLinks(query, isProject, isApplication, isPortfolio); if (!adminLinks.some(link => link != null)) { return null; } @@ -232,33 +278,38 @@ export class Menu extends React.PureComponent { )} ); - } + }; - renderAdministrationLinks() { + renderAdministrationLinks = ( + query: Query, + isProject: boolean, + isApplication: boolean, + isPortfolio: boolean + ) => { return [ - this.renderSettingsLink(), - this.renderBranchesLink(), - this.renderBaselineLink(), - this.renderProfilesLink(), - this.renderQualityGateLink(), - this.renderCustomMeasuresLink(), - this.renderLinksLink(), - this.renderPermissionsLink(), - this.renderBackgroundTasksLink(), - this.renderUpdateKeyLink(), - this.renderWebhooksLink(), - ...this.renderAdminExtensions(), - this.renderDeletionLink() + this.renderSettingsLink(query, isApplication, isPortfolio), + this.renderBranchesLink(query, isProject), + this.renderBaselineLink(query, isApplication, isPortfolio), + this.renderProfilesLink(query), + this.renderQualityGateLink(query), + this.renderCustomMeasuresLink(query), + this.renderLinksLink(query), + this.renderPermissionsLink(query), + this.renderBackgroundTasksLink(query), + this.renderUpdateKeyLink(query), + this.renderWebhooksLink(query, isProject), + ...this.renderAdminExtensions(query), + this.renderDeletionLink(query) ]; - } + }; - renderProjectInformationButton() { + renderProjectInformationButton = (isProject: boolean, isApplication: boolean) => { if (isPullRequest(this.props.branchLike)) { return null; } return ( - (this.isProject() || this.isApplication()) && ( + (isProject || isApplication) && (
  • { role="button" tabIndex={0}> - {translate(this.isProject() ? 'project' : 'application', 'info.title')} + {translate(isProject ? 'project' : 'application', 'info.title')}
  • ) ); - } + }; - renderSettingsLink() { - if (!this.getConfiguration().showSettings || this.isApplication() || this.isPortfolio()) { + renderSettingsLink = (query: Query, isApplication: boolean, isPortfolio: boolean) => { + if (!this.getConfiguration().showSettings || isApplication || isPortfolio) { return null; } return (
  • - + {translate('project_settings.page')}
  • ); - } + }; - renderBranchesLink() { + renderBranchesLink = (query: Query, isProject: boolean) => { if ( !this.props.appState.branchesEnabled || - !this.isProject() || + !isProject || !this.getConfiguration().showSettings ) { return null; @@ -303,145 +352,131 @@ export class Menu extends React.PureComponent { return (
  • - + {translate('project_branch_pull_request.page')}
  • ); - } + }; - renderBaselineLink() { - if (!this.getConfiguration().showSettings || this.isApplication() || this.isPortfolio()) { + renderBaselineLink = (query: Query, isApplication: boolean, isPortfolio: boolean) => { + if (!this.getConfiguration().showSettings || isApplication || isPortfolio) { return null; } return (
  • - + {translate('project_baseline.page')}
  • ); - } + }; - renderProfilesLink() { + renderProfilesLink = (query: Query) => { if (!this.getConfiguration().showQualityProfiles) { return null; } return (
  • - + {translate('project_quality_profiles.page')}
  • ); - } + }; - renderQualityGateLink() { + renderQualityGateLink = (query: Query) => { if (!this.getConfiguration().showQualityGates) { return null; } return (
  • - + {translate('project_quality_gate.page')}
  • ); - } + }; - renderCustomMeasuresLink() { + renderCustomMeasuresLink = (query: Query) => { if (isSonarCloud() || !this.getConfiguration().showManualMeasures) { return null; } return (
  • - + {translate('custom_measures.page')}
  • ); - } + }; - renderLinksLink() { + renderLinksLink = (query: Query) => { if (!this.getConfiguration().showLinks) { return null; } return (
  • - + {translate('project_links.page')}
  • ); - } + }; - renderPermissionsLink() { + renderPermissionsLink = (query: Query) => { if (!this.getConfiguration().showPermissions) { return null; } return (
  • - + {translate('permissions.page')}
  • ); - } + }; - renderBackgroundTasksLink() { + renderBackgroundTasksLink = (query: Query) => { if (!this.getConfiguration().showBackgroundTasks) { return null; } return (
  • - + {translate('background_tasks.page')}
  • ); - } + }; - renderUpdateKeyLink() { + renderUpdateKeyLink = (query: Query) => { if (!this.getConfiguration().showUpdateKey) { return null; } return (
  • - + {translate('update_key.page')}
  • ); - } + }; - renderWebhooksLink() { - if (!this.getConfiguration().showSettings || !this.isProject()) { + renderWebhooksLink = (query: Query, isProject: boolean) => { + if (!this.getConfiguration().showSettings || !isProject) { return null; } return (
  • - + {translate('webhooks.page')}
  • ); - } + }; - renderDeletionLink() { + renderDeletionLink = (query: Query) => { const { qualifier } = this.props.component; if (!this.getConfiguration().showSettings) { @@ -460,18 +495,16 @@ export class Menu extends React.PureComponent { return (
  • - + {translate('deletion.page')}
  • ); - } + }; - renderExtension = ({ key, name }: T.Extension, isAdmin: boolean) => { + renderExtension = ({ key, name }: T.Extension, isAdmin: boolean, baseQuery: Query) => { const pathname = isAdmin ? `/project/admin/extension/${key}` : `/project/extension/${key}`; - const query = { ...this.getQuery(), qualifier: this.props.component.qualifier }; + const query = { ...baseQuery, qualifier: this.props.component.qualifier }; return (
  • @@ -481,15 +514,15 @@ export class Menu extends React.PureComponent { ); }; - renderAdminExtensions() { + renderAdminExtensions = (query: Query) => { if (this.props.branchLike && !isMainBranch(this.props.branchLike)) { return []; } const extensions = this.getConfiguration().extensions || []; - return extensions.map(e => this.renderExtension(e, true)); - } + return extensions.map(e => this.renderExtension(e, true, query)); + }; - renderExtensions() { + renderExtensions = (query: Query) => { const extensions = this.props.component.extensions || []; const withoutSecurityExtension = extensions.filter( extension => !extension.key.startsWith('securityreport/') @@ -506,7 +539,7 @@ export class Menu extends React.PureComponent { data-test="extensions" overlay={
      - {withoutSecurityExtension.map(e => this.renderExtension(e, false))} + {withoutSecurityExtension.map(e => this.renderExtension(e, false, query))}
    } tagName="li"> @@ -524,24 +557,29 @@ export class Menu extends React.PureComponent { )} ); - } + }; render() { + const isProject = this.isProject(); + const isApplication = this.isApplication(); + const isPortfolio = this.isPortfolio(); + const hasAnalysis = this.hasAnalysis(); + const query = this.getQuery(); return (
    - {this.renderDashboardLink()} - {this.renderIssuesLink()} - {this.renderSecurityHotspotsLink()} - {this.renderSecurityReports()} - {this.renderComponentMeasuresLink()} - {this.renderCodeLink()} - {this.renderActivityLink()} - {this.renderExtensions()} + {this.renderDashboardLink(query, isPortfolio)} + {this.renderIssuesLink(hasAnalysis, query)} + {this.renderSecurityHotspotsLink(hasAnalysis, query, isPortfolio)} + {this.renderSecurityReports(hasAnalysis, query)} + {this.renderComponentMeasuresLink(hasAnalysis, query)} + {this.renderCodeLink(hasAnalysis, query, isApplication, isPortfolio)} + {this.renderActivityLink(hasAnalysis, query)} + {this.renderExtensions(query)} - {this.renderAdministration()} - {this.renderProjectInformationButton()} + {this.renderAdministration(query, isProject, isApplication, isPortfolio)} + {this.renderProjectInformationButton(isProject, isApplication)}
    ); 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 7f825a0c9ef..041f3c3d8fa 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 @@ -25,18 +25,17 @@ import { mockMainBranch, mockPullRequest } from '../../../../../helpers/mocks/branch-like'; +import { mockComponent } from '../../../../../helpers/testMocks'; import { ComponentQualifier } from '../../../../../types/component'; import { Menu } from '../Menu'; const mainBranch = mockMainBranch(); -const baseComponent = { - breadcrumbs: [], +const baseComponent = mockComponent({ + analysisDate: '2019-12-01', key: 'foo', - name: 'foo', - organization: 'org', - qualifier: 'TRK' -}; + name: 'foo' +}); it('should work with extensions', () => { const component = { @@ -137,12 +136,26 @@ it('should work for all qualifiers', () => { } }); +it('should disable links if no analysis has been done', () => { + expect( + shallowRender({ + component: { + ...baseComponent, + analysisDate: undefined + } + }) + ).toMatchSnapshot(); +}); + function shallowRender(props: Partial) { return shallow( diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap index e4bd6ee6c20..e311796c90b 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap @@ -67,6 +67,7 @@ exports[`renders 1`] = ` /> + +
  • + + overview.page + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
    + +
  • + + + project.info.title + +
  • +
    + +`; + exports[`should render correctly for security extensions 1`] = `
  • - - issues.page - + />
  • - - layout.security_hotspots - + />
  • - - layout.measures - + />
  • - - code.page - + />
  • - - project_activity.page - + />
  • @@ -294,10 +401,10 @@ exports[`should work for a branch 2`] = `
  • - - issues.page - + />
  • - - layout.security_hotspots - + />
  • - - layout.measures - + />
  • - - code.page - + />
  • - - project_activity.page - + />
  • @@ -426,10 +523,10 @@ exports[`should work for all qualifiers 1`] = `
  • - - issues.page - + />
  • - - layout.security_hotspots - + />
  • - - layout.measures - + />
  • - - code.page - + />
  • - - project_activity.page - + />
  • @@ -650,10 +737,10 @@ exports[`should work for all qualifiers 2`] = `
  • - - issues.page - + />
  • - - layout.measures - + />
  • - - view_projects.page - + />
  • - - project_activity.page - + />
  • @@ -776,10 +855,10 @@ exports[`should work for all qualifiers 3`] = `
  • - - issues.page - + />
  • - - layout.measures - + />
  • - - view_projects.page - + />
  • - - project_activity.page - + />
  • @@ -872,10 +943,10 @@ exports[`should work for all qualifiers 4`] = `
  • - - issues.page - + />
  • - - layout.security_hotspots - + />
  • - - layout.measures - + />
  • - - view_projects.page - + />
  • - - project_activity.page - + />
  • @@ -1029,10 +1090,10 @@ exports[`should work for pull requests 1`] = `
  • - - issues.page - + />
  • - - layout.security_hotspots - + />
  • - - layout.measures - + />
  • - - code.page - + />
  • @@ -1130,10 +1183,10 @@ exports[`should work for pull requests 2`] = `
  • - - issues.page - + />
  • - - layout.security_hotspots - + />
  • - - layout.measures - + />
  • - - code.page - + />
  • diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 54e43cc7378..1a059e1664a 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -483,6 +483,7 @@ layout.settings.VW=Portfolio Settings layout.settings.SVW=Portfolio Settings layout.security_reports=Security Reports layout.sonar.slogan=Continuous Code Quality +layout.must_be_configured=This will be available once your project is configured and analyzed. sidebar.projects=Projects sidebar.project_settings=Configuration -- 2.39.5