Browse Source

SONAR-15702 Disable menu if some project in application is inaccessible

tags/9.3.0.51899
Mathieu Suen 2 years ago
parent
commit
fabf4c68ba

+ 126
- 119
server/sonar-web/src/main/js/app/components/nav/component/Menu.tsx View File

@@ -18,6 +18,8 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import classNames from 'classnames';
import { LocationDescriptorObject } from 'history';
import { omit } from 'lodash';
import * as React from 'react';
import { Link, LinkProps } from 'react-router';
import Dropdown from '../../../../components/controls/Dropdown';
@@ -27,7 +29,7 @@ import BulletListIcon from '../../../../components/icons/BulletListIcon';
import DropdownIcon from '../../../../components/icons/DropdownIcon';
import NavBarTabs from '../../../../components/ui/NavBarTabs';
import { getBranchLikeQuery, isPullRequest } from '../../../../helpers/branch-like';
import { hasMessage, translate } from '../../../../helpers/l10n';
import { hasMessage, translate, translateWithParameters } from '../../../../helpers/l10n';
import { getPortfolioUrl, getProjectQueryUrl } from '../../../../helpers/urls';
import { BranchLike, BranchParameters } from '../../../../types/branch-like';
import { ComponentQualifier, isPortfolioLike } from '../../../../types/component';
@@ -62,22 +64,6 @@ interface Props {

type Query = BranchParameters & { id: string };

function MenuLink({
hasAnalysis,
label,
...props
}: LinkProps & { hasAnalysis: boolean; label: React.ReactNode }) {
return hasAnalysis ? (
<Link {...props}>{label}</Link>
) : (
<Tooltip overlay={translate('layout.must_be_configured')}>
<a aria-disabled="true" className="disabled-link">
{label}
</a>
</Tooltip>
);
}

export class Menu extends React.PureComponent<Props> {
hasAnalysis = () => {
const { branchLikes = [], component, isInProgress, isPending } = this.props;
@@ -102,6 +88,14 @@ export class Menu extends React.PureComponent<Props> {
return this.props.component.qualifier === ComponentQualifier.Application;
};

isAllChildProjectAccessible = () => {
return Boolean(this.props.component.canBrowseAllChildProjects);
};

isApplicationChildInaccessble = () => {
return this.isApplication() && !this.isAllChildProjectAccessible();
};

getConfiguration = () => {
return this.props.component.configuration || {};
};
@@ -110,103 +104,121 @@ export class Menu extends React.PureComponent<Props> {
return { id: this.props.component.key, ...getBranchLikeQuery(this.props.branchLike) };
};

renderDashboardLink = ({ id, ...branchLike }: Query, isPortfolio: boolean) => {
renderLinkWhenInaccessibleChild(label: React.ReactNode) {
return (
<li>
<Tooltip
overlay={translateWithParameters(
'layout.all_project_must_be_accessible',
translate('qualifier', this.props.component.qualifier)
)}>
<a aria-disabled="true" className="disabled-link">
{label}
</a>
</Tooltip>
</li>
);
}

renderMenuLink = ({
label,
to,
...props
}: Omit<LinkProps, 'to'> & {
label: React.ReactNode;
to: LocationDescriptorObject;
}) => {
const hasAnalysis = this.hasAnalysis();
const isApplicationChildInaccessble = this.isApplicationChildInaccessble();
const query = this.getQuery();
if (isApplicationChildInaccessble) {
return this.renderLinkWhenInaccessibleChild(label);
}
return (
<li>
{hasAnalysis ? (
<Link to={{ ...to, query: { ...query, ...to.query } }} {...omit(props, ['to'])}>
{label}
</Link>
) : (
<Tooltip overlay={translate('layout.must_be_configured')}>
<a aria-disabled="true" className="disabled-link">
{label}
</a>
</Tooltip>
)}
</li>
);
};

renderDashboardLink = () => {
const { id, ...branchLike } = this.getQuery();
const isApplicationChildInaccessble = this.isApplicationChildInaccessble();
if (isApplicationChildInaccessble) {
return this.renderLinkWhenInaccessibleChild(translate('overview.page'));
}
return (
<li>
<Link
activeClassName="active"
to={isPortfolio ? getPortfolioUrl(id) : getProjectQueryUrl(id, branchLike)}>
to={this.isPortfolio() ? getPortfolioUrl(id) : getProjectQueryUrl(id, branchLike)}>
{translate('overview.page')}
</Link>
</li>
);
};

renderCodeLink = (
hasAnalysis: boolean,
query: Query,
isApplication: boolean,
isPortfolio: boolean
) => {
renderCodeLink = () => {
const isPortfolio = this.isPortfolio();
const isApplication = this.isApplication();
const label =
isPortfolio || isApplication ? translate('view_projects.page') : translate('code.page');
if (this.isDeveloper()) {
return null;
}

return (
<li>
<MenuLink
activeClassName="active"
hasAnalysis={hasAnalysis}
label={
isPortfolio || isApplication ? translate('view_projects.page') : translate('code.page')
}
to={{ pathname: '/code', query }}
/>
</li>
);
return this.renderMenuLink({ label, to: { pathname: '/code' } });
};

renderActivityLink = (hasAnalysis: boolean, query: Query) => {
renderActivityLink = () => {
const { branchLike } = this.props;

if (isPullRequest(branchLike)) {
return null;
}

return (
<li>
<MenuLink
activeClassName="active"
hasAnalysis={hasAnalysis}
label={translate('project_activity.page')}
to={{ pathname: '/project/activity', query }}
/>
</li>
);
return this.renderMenuLink({
label: translate('project_activity.page'),
to: { pathname: '/project/activity' }
});
};

renderIssuesLink = (hasAnalysis: boolean, query: Query) => {
return (
<li>
<MenuLink
activeClassName="active"
hasAnalysis={hasAnalysis}
label={translate('issues.page')}
to={{ pathname: '/project/issues', query: { ...query, resolved: 'false' } }}
/>
</li>
);
renderIssuesLink = () => {
return this.renderMenuLink({
label: translate('issues.page'),
to: { pathname: '/project/issues', query: { resolved: 'false' } }
});
};

renderComponentMeasuresLink = (hasAnalysis: boolean, query: Query) => {
return (
<li>
<MenuLink
activeClassName="active"
hasAnalysis={hasAnalysis}
label={translate('layout.measures')}
to={{ pathname: '/component_measures', query }}
/>
</li>
);
renderComponentMeasuresLink = () => {
return this.renderMenuLink({
label: translate('layout.measures'),
to: { pathname: 'component_measures' }
});
};

renderSecurityHotspotsLink = (hasAnalysis: boolean, query: Query, isPortfolio: boolean) => {
renderSecurityHotspotsLink = () => {
const isPortfolio = this.isPortfolio();
return (
!isPortfolio && (
<li>
<MenuLink
activeClassName="active"
hasAnalysis={hasAnalysis}
label={translate('layout.security_hotspots')}
to={{ pathname: '/security_hotspots', query }}
/>
</li>
)
!isPortfolio &&
this.renderMenuLink({
label: translate('layout.security_hotspots'),
to: { pathname: '/security_hotspots' }
})
);
};

renderSecurityReports = (hasAnalysis: boolean, query: Query) => {
renderSecurityReports = () => {
const { branchLike, component } = this.props;
const { extensions = [] } = component;

@@ -222,28 +234,18 @@ export class Menu extends React.PureComponent<Props> {
return null;
}

return (
<li>
<MenuLink
activeClassName="active"
hasAnalysis={hasAnalysis}
label={translate('layout.security_reports')}
to={{
pathname: '/project/extension/securityreport/securityreport',
query
}}
/>
</li>
);
return this.renderMenuLink({
label: translate('layout.security_reports'),
to: { pathname: '/project/extension/securityreport/securityreport' }
});
};

renderAdministration = (
query: Query,
isProject: boolean,
isApplication: boolean,
isPortfolio: boolean
) => {
renderAdministration = () => {
const { branchLike, component } = this.props;
const isProject = this.isProject();
const isPortfolio = this.isPortfolio();
const isApplication = this.isApplication();
const query = this.getQuery();

if (!this.getConfiguration().showSettings || isPullRequest(branchLike)) {
return null;
@@ -305,11 +307,20 @@ export class Menu extends React.PureComponent<Props> {
];
};

renderProjectInformationButton = (isProject: boolean, isApplication: boolean) => {
renderProjectInformationButton = () => {
const isProject = this.isProject();
const isApplication = this.isApplication();
const label = translate(isProject ? 'project' : 'application', 'info.title');
const isApplicationChildInaccessble = this.isApplicationChildInaccessble();

if (isPullRequest(this.props.branchLike)) {
return null;
}

if (isApplicationChildInaccessble) {
return this.renderLinkWhenInaccessibleChild(label);
}

return (
(isProject || isApplication) && (
<li>
@@ -323,7 +334,7 @@ export class Menu extends React.PureComponent<Props> {
role="button"
tabIndex={0}>
<BulletListIcon className="little-spacer-right" />
{translate(isProject ? 'project' : 'application', 'info.title')}
{label}
</a>
</li>
)
@@ -550,7 +561,8 @@ export class Menu extends React.PureComponent<Props> {
.map(e => this.renderExtension(e, true, query));
};

renderExtensions = (query: Query) => {
renderExtensions = () => {
const query = this.getQuery();
const extensions = this.props.component.extensions || [];
const withoutSecurityExtension = extensions.filter(
extension => !extension.key.startsWith('securityreport/')
@@ -587,26 +599,21 @@ export class Menu extends React.PureComponent<Props> {
};

render() {
const isProject = this.isProject();
const isApplication = this.isApplication();
const isPortfolio = this.isPortfolio();
const hasAnalysis = this.hasAnalysis();
const query = this.getQuery();
return (
<div className="display-flex-center display-flex-space-between">
<NavBarTabs>
{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.renderDashboardLink()}
{this.renderIssuesLink()}
{this.renderSecurityHotspotsLink()}
{this.renderSecurityReports()}
{this.renderComponentMeasuresLink()}
{this.renderCodeLink()}
{this.renderActivityLink()}
{this.renderExtensions()}
</NavBarTabs>
<NavBarTabs>
{this.renderAdministration(query, isProject, isApplication, isPortfolio)}
{this.renderProjectInformationButton(isProject, isApplication)}
{this.renderAdministration()}
{this.renderProjectInformationButton()}
</NavBarTabs>
</div>
);

+ 19
- 1
server/sonar-web/src/main/js/app/components/nav/component/__tests__/Menu-test.tsx View File

@@ -130,7 +130,12 @@ it('should work for all qualifiers', () => {
expect.assertions(4);

function checkWithQualifier(qualifier: string) {
const component = { ...baseComponent, configuration: { showSettings: true }, qualifier };
const component = {
...baseComponent,
canBrowseAllChildProjects: true,
configuration: { showSettings: true },
qualifier
};
expect(shallowRender({ component })).toMatchSnapshot();
}
});
@@ -146,6 +151,19 @@ it('should disable links if no analysis has been done', () => {
).toMatchSnapshot();
});

it('should disable links if application has inaccessible projects', () => {
expect(
shallowRender({
component: {
...baseComponent,
qualifier: ComponentQualifier.Application,
canBrowseAllChildProjects: false,
configuration: { showSettings: true }
}
})
).toMatchSnapshot();
});

function shallowRender(props: Partial<Menu['props']>) {
return shallow<Menu>(
<Menu

+ 432
- 254
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/Menu-test.tsx.snap
File diff suppressed because it is too large
View File


+ 1
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -520,6 +520,7 @@ 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.
layout.all_project_must_be_accessible=You need access to all projects within this {0} to access it.

sidebar.projects=Projects
sidebar.project_settings=Configuration

Loading…
Cancel
Save