- Make Bulk Change available only to user who can use it.
- Make «Deactivate» button available only to user who can use it.
import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper';
import '../../../components/search-navigator.css';
import { hasPrivateAccess } from '../../../helpers/organizations';
+import { isSonarCloud } from '../../../helpers/system';
+import { isLoggedIn } from '../../../helpers/users';
import {
getAppState,
getCurrentUser,
isFiltered = () => Object.keys(serializeQuery(this.state.query)).length > 0;
+ renderBulkButton = () => {
+ const { currentUser, languages } = this.props;
+ const { canWrite, paging, query, referencedProfiles } = this.state;
+ const organization = this.props.organization && this.props.organization.key;
+
+ if (!isLoggedIn(currentUser) || (isSonarCloud() && !organization) || !canWrite) {
+ return null;
+ }
+
+ return (
+ paging && (
+ <BulkChange
+ languages={languages}
+ organization={organization}
+ query={query}
+ referencedProfiles={referencedProfiles}
+ total={paging.total}
+ />
+ )
+ );
+ };
+
render() {
const { paging, rules } = this.state;
const selectedIndex = this.getSelectedIndex();
this.props.organization,
this.props.userOrganizations
);
+
return (
<>
<Suggestions suggestions="coding_rules" />
{translate('coding_rules.return_to_list')}
</a>
) : (
- this.state.paging && (
- <BulkChange
- languages={this.props.languages}
- organization={organization}
- query={this.state.query}
- referencedProfiles={this.state.referencedProfiles}
- total={this.state.paging.total}
- />
- )
+ this.renderBulkButton()
)}
<PageActions
loading={this.state.loading}
{rules.map(rule => (
<RuleListItem
activation={this.getRuleActivation(rule.key)}
+ canWrite={this.state.canWrite}
+ isLoggedIn={isLoggedIn(this.props.currentUser)}
key={rule.key}
onActivate={this.handleRuleActivate}
onDeactivate={this.handleRuleDeactivate}
import * as React from 'react';
import { Button } from 'sonar-ui-common/components/controls/buttons';
import Dropdown from 'sonar-ui-common/components/controls/Dropdown';
+import Tooltip from 'sonar-ui-common/components/controls/Tooltip';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { Profile } from '../../../api/quality-profiles';
import { Query } from '../query';
Boolean(profile.actions && profile.actions.edit)
);
if (!canBulkChange) {
- return null;
+ return (
+ <Tooltip overlay={translate('coding_rules.can_not_bulk_change')}>
+ <Button className="js-bulk-change" disabled={true}>
+ {translate('bulk_change')}
+ </Button>
+ </Tooltip>
+ );
}
const { activation } = this.props.query;
interface Props {
activation?: Activation;
+ canWrite?: boolean;
+ isLoggedIn: boolean;
onActivate: (profile: string, rule: string, activation: Activation) => void;
onDeactivate: (profile: string, rule: string) => void;
onFilterChange: (changes: Partial<Query>) => void;
};
renderActions = () => {
- const { activation, rule, selectedProfile } = this.props;
- if (!selectedProfile) {
+ const { activation, isLoggedIn, rule, selectedProfile } = this.props;
+ if (!selectedProfile || !isLoggedIn) {
return null;
}
- const canEdit = selectedProfile.actions && selectedProfile.actions.edit;
- if (!canEdit || selectedProfile.isBuiltIn) {
+ const canCopy = selectedProfile.actions && selectedProfile.actions.copy;
+ if (selectedProfile.isBuiltIn && canCopy) {
return (
<td className="coding-rule-table-meta-cell coding-rule-activation-actions">
{this.renderDeactivateButton('', 'coding_rules.need_extend_or_copy')}
);
}
+ const canEdit = selectedProfile.actions && selectedProfile.actions.edit;
+ if (!canEdit) {
+ return null;
+ }
+
return (
<td className="coding-rule-table-meta-cell coding-rule-activation-actions">
{activation
</ConfirmButton>
) : (
<Tooltip overlay={translate(overlayTranslationKey)}>
- <Button className="coding-rules-detail-quality-profile-deactivate button-red disabled">
+ <Button
+ className="coding-rules-detail-quality-profile-deactivate button-red"
+ disabled={true}>
{translate('coding_rules.deactivate')}
</Button>
</Tooltip>
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
+import { App } from '../App';
+import {
+ mockAppState,
+ mockCurrentUser,
+ mockLocation,
+ mockOrganization,
+ mockRouter
+} from '../../../../helpers/testMocks';
+import { getRulesApp } from '../../../../api/rules';
+import { isSonarCloud } from '../../../../helpers/system';
+
+jest.mock('../../../../api/rules', () => ({
+ getRulesApp: jest.fn().mockResolvedValue({ canWrite: true, repositories: [] }),
+ searchRules: jest.fn().mockResolvedValue({
+ actives: [],
+ rawActives: [],
+ facets: [],
+ rawFacets: [],
+ p: 0,
+ ps: 100,
+ rules: [],
+ total: 0
+ })
+}));
+
+jest.mock('../../../../api/quality-profiles', () => ({
+ searchQualityProfiles: jest.fn().mockResolvedValue({ profiles: [] })
+}));
+
+jest.mock('../../../../helpers/system', () => ({
+ isSonarCloud: jest.fn().mockResolvedValue(false)
+}));
+
+it('should render correctly', async () => {
+ const wrapper = shallowRender();
+ expect(wrapper).toMatchSnapshot();
+
+ await waitAndUpdate(wrapper);
+ expect(wrapper).toMatchSnapshot();
+});
+
+describe('renderBulkButton', () => {
+ it('should be null when the user is not logged in', () => {
+ const wrapper = shallowRender({
+ currentUser: mockCurrentUser()
+ });
+ expect(wrapper.instance().renderBulkButton()).toBeNull();
+ });
+
+ it('should be null when on SonarCloud and no organization is given', () => {
+ (isSonarCloud as jest.Mock).mockReturnValue(true);
+
+ const wrapper = shallowRender({
+ organization: undefined
+ });
+ expect(wrapper.instance().renderBulkButton()).toBeNull();
+ });
+
+ it('should be null when the user does not have the sufficient permission', () => {
+ (getRulesApp as jest.Mock).mockReturnValue({ canWrite: false, repositories: [] });
+
+ const wrapper = shallowRender();
+ expect(wrapper.instance().renderBulkButton()).toBeNull();
+ });
+
+ it('should show bulk change button when everything is fine', async () => {
+ (getRulesApp as jest.Mock).mockReturnValue({ canWrite: true, repositories: [] });
+ const wrapper = shallowRender();
+ await waitAndUpdate(wrapper);
+
+ expect(wrapper.instance().renderBulkButton()).toMatchSnapshot();
+ });
+});
+
+function shallowRender(props: Partial<App['props']> = {}) {
+ const organization = mockOrganization();
+ return shallow<App>(
+ <App
+ appState={mockAppState()}
+ currentUser={mockCurrentUser({
+ isLoggedIn: true
+ })}
+ languages={{ js: { key: 'js', name: 'JavaScript' } }}
+ location={mockLocation()}
+ organization={organization}
+ params={{}}
+ router={mockRouter()}
+ routes={[]}
+ userOrganizations={[organization]}
+ {...props}
+ />
+ );
+}
expect(wrapper).toMatchSnapshot();
});
-it('should not render anything', () => {
+it('should not a disabled button when edition is not possible', () => {
const wrapper = shallowRender({
referencedProfiles: { key: { ...profile, actions: { ...profile.actions, edit: false } } }
});
- expect(wrapper.type()).toBeNull();
+ expect(wrapper).toMatchSnapshot();
});
it('should display BulkChangeModal', () => {
*/
import { shallow } from 'enzyme';
import * as React from 'react';
-import { mockEvent, mockRule } from '../../../../helpers/testMocks';
+import { mockEvent, mockQualityProfile, mockRule } from '../../../../helpers/testMocks';
import RuleListItem from '../RuleListItem';
it('should render', () => {
expect(instance.renderDeactivateButton('', 'coding_rules.need_extend_or_copy')).toMatchSnapshot();
});
+describe('renderActions', () => {
+ it('should be null when there is no selected profile', () => {
+ const wrapper = shallowRender({
+ isLoggedIn: true
+ });
+
+ expect(wrapper.instance().renderActions()).toBeNull();
+ });
+
+ it('should be null when I am not logged in', () => {
+ const wrapper = shallowRender({
+ isLoggedIn: false,
+ selectedProfile: mockQualityProfile()
+ });
+
+ expect(wrapper.instance().renderActions()).toBeNull();
+ });
+
+ it('should be null when the user does not have the sufficient permissions', () => {
+ const wrapper = shallowRender({
+ isLoggedIn: true,
+ selectedProfile: mockQualityProfile()
+ });
+
+ expect(wrapper.instance().renderActions()).toBeNull();
+ });
+
+ it('should disable the button when I am on a built-in profile', () => {
+ const wrapper = shallowRender({
+ selectedProfile: mockQualityProfile({
+ actions: {
+ copy: true
+ },
+ isBuiltIn: true
+ })
+ });
+
+ expect(wrapper.instance().renderActions()).toMatchSnapshot();
+ });
+
+ it('should render the deactivate button', () => {
+ const wrapper = shallowRender({
+ activation: {
+ inherit: 'NONE',
+ severity: 'warning'
+ },
+ selectedProfile: mockQualityProfile({
+ actions: {
+ edit: true
+ },
+ isBuiltIn: false
+ })
+ });
+
+ expect(wrapper.instance().renderActions()).toMatchSnapshot();
+ });
+
+ it('should render the activate button', () => {
+ const wrapper = shallowRender({
+ rule: mockRule({
+ isTemplate: false
+ }),
+ selectedProfile: mockQualityProfile({
+ actions: {
+ edit: true
+ },
+ isBuiltIn: false
+ })
+ });
+
+ expect(wrapper.instance().renderActions()).toMatchSnapshot();
+ });
+});
+
function shallowRender(props?: Partial<RuleListItem['props']>) {
return shallow<RuleListItem>(
<RuleListItem
+ isLoggedIn={true}
onActivate={jest.fn()}
onDeactivate={jest.fn()}
onFilterChange={jest.fn()}
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renderBulkButton should show bulk change button when everything is fine 1`] = `
+<BulkChange
+ languages={
+ Object {
+ "js": Object {
+ "key": "js",
+ "name": "JavaScript",
+ },
+ }
+ }
+ organization="foo"
+ query={
+ Object {
+ "activation": undefined,
+ "activationSeverities": Array [],
+ "availableSince": undefined,
+ "compareToProfile": undefined,
+ "cwe": Array [],
+ "inheritance": undefined,
+ "languages": Array [],
+ "owaspTop10": Array [],
+ "profile": undefined,
+ "repositories": Array [],
+ "ruleKey": undefined,
+ "sansTop25": Array [],
+ "searchQuery": undefined,
+ "severities": Array [],
+ "sonarsourceSecurity": Array [],
+ "statuses": Array [],
+ "tags": Array [],
+ "template": undefined,
+ "types": Array [],
+ }
+ }
+ referencedProfiles={Object {}}
+ total={0}
+/>
+`;
+
+exports[`should render correctly 1`] = `
+<Fragment>
+ <Suggestions
+ suggestions="coding_rules"
+ />
+ <HelmetWrapper
+ defer={true}
+ encodeSpecialCharacters={true}
+ title="coding_rules.page"
+ >
+ <meta
+ content="noindex"
+ name="robots"
+ />
+ </HelmetWrapper>
+ <div
+ className="layout-page"
+ id="coding-rules-page"
+ >
+ <ScreenPositionHelper
+ className="layout-page-side-outer"
+ >
+ <Component />
+ </ScreenPositionHelper>
+ <div
+ className="layout-page-main"
+ >
+ <div
+ className="layout-page-header-panel layout-page-main-header"
+ >
+ <div
+ className="layout-page-header-panel-inner layout-page-main-header-inner"
+ >
+ <div
+ className="layout-page-main-inner"
+ >
+ <A11ySkipTarget
+ anchor="rules_main"
+ />
+ <PageActions
+ loading={true}
+ onReload={[Function]}
+ />
+ </div>
+ </div>
+ </div>
+ <div
+ className="layout-page-main-inner"
+ />
+ </div>
+ </div>
+</Fragment>
+`;
+
+exports[`should render correctly 2`] = `
+<Fragment>
+ <Suggestions
+ suggestions="coding_rules"
+ />
+ <HelmetWrapper
+ defer={true}
+ encodeSpecialCharacters={true}
+ title="coding_rules.page"
+ >
+ <meta
+ content="noindex"
+ name="robots"
+ />
+ </HelmetWrapper>
+ <div
+ className="layout-page"
+ id="coding-rules-page"
+ >
+ <ScreenPositionHelper
+ className="layout-page-side-outer"
+ >
+ <Component />
+ </ScreenPositionHelper>
+ <div
+ className="layout-page-main"
+ >
+ <div
+ className="layout-page-header-panel layout-page-main-header"
+ >
+ <div
+ className="layout-page-header-panel-inner layout-page-main-header-inner"
+ >
+ <div
+ className="layout-page-main-inner"
+ >
+ <A11ySkipTarget
+ anchor="rules_main"
+ />
+ <BulkChange
+ languages={
+ Object {
+ "js": Object {
+ "key": "js",
+ "name": "JavaScript",
+ },
+ }
+ }
+ organization="foo"
+ query={
+ Object {
+ "activation": undefined,
+ "activationSeverities": Array [],
+ "availableSince": undefined,
+ "compareToProfile": undefined,
+ "cwe": Array [],
+ "inheritance": undefined,
+ "languages": Array [],
+ "owaspTop10": Array [],
+ "profile": undefined,
+ "repositories": Array [],
+ "ruleKey": undefined,
+ "sansTop25": Array [],
+ "searchQuery": undefined,
+ "severities": Array [],
+ "sonarsourceSecurity": Array [],
+ "statuses": Array [],
+ "tags": Array [],
+ "template": undefined,
+ "types": Array [],
+ }
+ }
+ referencedProfiles={Object {}}
+ total={0}
+ />
+ <PageActions
+ loading={false}
+ onReload={[Function]}
+ paging={
+ Object {
+ "pageIndex": 0,
+ "pageSize": 100,
+ "total": 0,
+ }
+ }
+ />
+ </div>
+ </div>
+ </div>
+ <div
+ className="layout-page-main-inner"
+ >
+ <ListFooter
+ count={0}
+ loadMore={[Function]}
+ ready={true}
+ total={0}
+ />
+ </div>
+ </div>
+ </div>
+</Fragment>
+`;
/>
`;
+exports[`should not a disabled button when edition is not possible 1`] = `
+<Tooltip
+ overlay="coding_rules.can_not_bulk_change"
+>
+ <Button
+ className="js-bulk-change"
+ disabled={true}
+ >
+ bulk_change
+ </Button>
+</Tooltip>
+`;
+
exports[`should render correctly 1`] = `
<Fragment>
<Dropdown
// Jest Snapshot v1, https://goo.gl/fbAQLP
+exports[`renderActions should disable the button when I am on a built-in profile 1`] = `
+<td
+ className="coding-rule-table-meta-cell coding-rule-activation-actions"
+>
+ <Tooltip
+ overlay="coding_rules.need_extend_or_copy"
+ >
+ <Button
+ className="coding-rules-detail-quality-profile-deactivate button-red"
+ disabled={true}
+ >
+ coding_rules.deactivate
+ </Button>
+ </Tooltip>
+</td>
+`;
+
+exports[`renderActions should render the activate button 1`] = `
+<td
+ className="coding-rule-table-meta-cell coding-rule-activation-actions"
+>
+ <ActivationButton
+ buttonText="coding_rules.activate"
+ className="coding-rules-detail-quality-profile-activate"
+ modalHeader="coding_rules.activate_in_quality_profile"
+ onDone={[Function]}
+ organization="org"
+ profiles={
+ Array [
+ Object {
+ "actions": Object {
+ "edit": true,
+ },
+ "activeDeprecatedRuleCount": 2,
+ "activeRuleCount": 10,
+ "childrenCount": 0,
+ "depth": 1,
+ "isBuiltIn": false,
+ "isDefault": false,
+ "isInherited": false,
+ "key": "key",
+ "language": "js",
+ "languageName": "JavaScript",
+ "name": "name",
+ "organization": "foo",
+ "projectCount": 3,
+ },
+ ]
+ }
+ rule={
+ Object {
+ "isTemplate": false,
+ "key": "javascript:S1067",
+ "lang": "js",
+ "langName": "JavaScript",
+ "name": "Use foo",
+ "severity": "MAJOR",
+ "status": "READY",
+ "sysTags": Array [
+ "a",
+ "b",
+ ],
+ "tags": Array [
+ "x",
+ ],
+ "type": "CODE_SMELL",
+ }
+ }
+ />
+</td>
+`;
+
+exports[`renderActions should render the deactivate button 1`] = `
+<td
+ className="coding-rule-table-meta-cell coding-rule-activation-actions"
+>
+ <ConfirmButton
+ confirmButtonText="yes"
+ modalBody="coding_rules.deactivate.confirm"
+ modalHeader="coding_rules.deactivate"
+ onConfirm={[Function]}
+ >
+ [Function]
+ </ConfirmButton>
+</td>
+`;
+
exports[`should render 1`] = `
<div
className="coding-rule"
overlay="coding_rules.need_extend_or_copy"
>
<Button
- className="coding-rules-detail-quality-profile-deactivate button-red disabled"
+ className="coding-rules-detail-quality-profile-deactivate button-red"
+ disabled={true}
>
coding_rules.deactivate
</Button>
coding_rules.bulk_change=Bulk Change
coding_rules.bulk_change.success={2} rule(s) changed in profile {0} - {1}
coding_rules.bulk_change.warning={2} rule(s) changed, {3} rule(s) ignored in profile {0} - {1}
+coding_rules.can_not_bulk_change=Bulk change is only available when you have a custom Quality Profile to target. You can create a customizable Quality Profile based on a built-in one by Copying or Extending it in the Quality Profiles list.
coding_rules.can_not_deactivate=This rule is inherited and can not be deactivated.
coding_rules.change_details=Change Details of Quality Profile
coding_rules.create=Create