Browse Source

SONAR-20355 Enable Desactivate button for inherited rules if allowed by settings

Co-authored-by: Benjamin Raymond <31401273+7PH@users.noreply.github.com>
tags/10.3.0.82913
Wouter Admiraal 9 months ago
parent
commit
a64c351b61

+ 1
- 0
server/sonar-web/src/main/js/api/mocks/CodingRulesServiceMock.ts View File

} }
const responseRules = filteredRules.slice((currentP - 1) * currentPs, currentP * currentPs); const responseRules = filteredRules.slice((currentP - 1) * currentPs, currentP * currentPs);
return this.reply({ return this.reply({
actives: qprofile ? this.rulesActivations : undefined,
rules: responseRules, rules: responseRules,
facets: facetCounts, facets: facetCounts,
paging: mockPaging({ paging: mockPaging({

+ 4
- 0
server/sonar-web/src/main/js/api/mocks/SettingsServiceMock.ts View File

key: 'sonar.javascript.globals', key: 'sonar.javascript.globals',
values: ['angular', 'google', 'd3'], values: ['angular', 'google', 'd3'],
}, },
{
key: SettingsKey.QPAdminCanDisableInheritedRules,
value: 'true',
},
]; ];


#settingValues: SettingValue[] = cloneDeep(this.#defaultValues); #settingValues: SettingValue[] = cloneDeep(this.#defaultValues);

+ 1
- 1
server/sonar-web/src/main/js/api/mocks/data/qualityProfiles.ts View File

languageName: 'Java', languageName: 'Java',
actions: { edit: true }, actions: { edit: true },
}), }),
mockQualityProfile({ key: QP_2, name: 'QP Bar', language: 'js' }),
mockQualityProfile({ key: QP_2, name: 'QP Bar', language: 'py', languageName: 'Python' }),
mockQualityProfile({ key: QP_3, name: 'QP FooBar', language: 'java', languageName: 'Java' }), mockQualityProfile({ key: QP_3, name: 'QP FooBar', language: 'java', languageName: 'Java' }),
mockQualityProfile({ mockQualityProfile({
key: QP_4, key: QP_4,

+ 7
- 1
server/sonar-web/src/main/js/api/mocks/data/rules.ts View File

} from '../../../types/clean-code-taxonomy'; } from '../../../types/clean-code-taxonomy';
import { import {
ADVANCED_RULE, ADVANCED_RULE,
QP_1,
QP_2,
RULE_1, RULE_1,
RULE_10, RULE_10,
RULE_11, RULE_11,


export function mockRulesActivationsInQP() { export function mockRulesActivationsInQP() {
return { return {
[RULE_1]: [mockRuleActivation({ qProfile: 'p1' })],
[RULE_1]: [mockRuleActivation({ qProfile: QP_1 })],
[RULE_7]: [mockRuleActivation({ qProfile: QP_2 })],
[RULE_8]: [mockRuleActivation({ qProfile: QP_2 })],
[RULE_9]: [mockRuleActivation({ qProfile: QP_2, inherit: 'INHERITED' })],
[RULE_10]: [mockRuleActivation({ qProfile: QP_2, inherit: 'OVERRIDES' })],
}; };
} }

+ 51
- 27
server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts View File

import { act, fireEvent, screen } from '@testing-library/react'; import { act, fireEvent, screen } from '@testing-library/react';
import selectEvent from 'react-select-event'; import selectEvent from 'react-select-event';
import CodingRulesServiceMock, { RULE_TAGS_MOCK } from '../../../api/mocks/CodingRulesServiceMock'; import CodingRulesServiceMock, { RULE_TAGS_MOCK } from '../../../api/mocks/CodingRulesServiceMock';
import SettingsServiceMock from '../../../api/mocks/SettingsServiceMock';
import { QP_2 } from '../../../api/mocks/data/ids';
import { CLEAN_CODE_CATEGORIES, SOFTWARE_QUALITIES } from '../../../helpers/constants'; import { CLEAN_CODE_CATEGORIES, SOFTWARE_QUALITIES } from '../../../helpers/constants';
import { parseDate } from '../../../helpers/dates'; import { parseDate } from '../../../helpers/dates';
import { mockCurrentUser, mockLoggedInUser } from '../../../helpers/testMocks'; import { mockCurrentUser, mockLoggedInUser } from '../../../helpers/testMocks';
CleanCodeAttributeCategory, CleanCodeAttributeCategory,
SoftwareQuality, SoftwareQuality,
} from '../../../types/clean-code-taxonomy'; } from '../../../types/clean-code-taxonomy';
import { SettingsKey } from '../../../types/settings';
import { CurrentUser } from '../../../types/users'; import { CurrentUser } from '../../../types/users';
import routes from '../routes'; import routes from '../routes';
import { getPageObjects } from '../utils-tests'; import { getPageObjects } from '../utils-tests';


const handler: CodingRulesServiceMock = new CodingRulesServiceMock();
const rulesHandler = new CodingRulesServiceMock();
const settingsHandler = new SettingsServiceMock();


afterEach(() => handler.reset());
afterEach(() => {
rulesHandler.reset();
settingsHandler.reset();
});


describe('Rules app list', () => { describe('Rules app list', () => {
it('renders correctly', async () => { it('renders correctly', async () => {
await ui.appLoaded(); await ui.appLoaded();


// Renders list // Renders list
handler
rulesHandler
.allRulesName() .allRulesName()
.forEach((name) => expect(ui.ruleListItemLink(name).get()).toBeInTheDocument()); .forEach((name) => expect(ui.ruleListItemLink(name).get()).toBeInTheDocument());


describe('bulk change', () => { describe('bulk change', () => {
it('no quality profile for bulk change based on language search', async () => { it('no quality profile for bulk change based on language search', async () => {
const { ui, user } = getPageObjects(); const { ui, user } = getPageObjects();
handler.setIsAdmin();
rulesHandler.setIsAdmin();
renderCodingRulesApp(mockLoggedInUser()); renderCodingRulesApp(mockLoggedInUser());
await ui.appLoaded(); await ui.appLoaded();




it('should be able to bulk activate quality profile', async () => { it('should be able to bulk activate quality profile', async () => {
const { ui, user } = getPageObjects(); const { ui, user } = getPageObjects();
handler.setIsAdmin();
rulesHandler.setIsAdmin();
renderCodingRulesApp(mockLoggedInUser()); renderCodingRulesApp(mockLoggedInUser());
await ui.appLoaded(); await ui.appLoaded();


const [selectQPSuccess, selectQPWarning] = handler.allQualityProfile('java');
const [selectQPSuccess, selectQPWarning] = rulesHandler.allQualityProfile('java');


const rulesCount = handler.allRulesCount();
const rulesCount = rulesHandler.allRulesCount();


await ui.bulkActivate(rulesCount, selectQPSuccess); await ui.bulkActivate(rulesCount, selectQPSuccess);


await user.click(ui.bulkClose.get()); await user.click(ui.bulkClose.get());


// Try bulk change when quality profile has warnning. // Try bulk change when quality profile has warnning.
handler.activateWithWarning();
rulesHandler.activateWithWarning();


await ui.bulkActivate(rulesCount, selectQPWarning); await ui.bulkActivate(rulesCount, selectQPWarning);
expect( expect(


it('should be able to bulk deactivate quality profile', async () => { it('should be able to bulk deactivate quality profile', async () => {
const { ui } = getPageObjects(); const { ui } = getPageObjects();
handler.setIsAdmin();
rulesHandler.setIsAdmin();
renderCodingRulesApp(mockLoggedInUser()); renderCodingRulesApp(mockLoggedInUser());
await ui.appLoaded(); await ui.appLoaded();


const [selectQP] = handler.allQualityProfile('java');
const rulesCount = handler.allRulesCount();
const [selectQP] = rulesHandler.allQualityProfile('java');
const rulesCount = rulesHandler.allRulesCount();


await ui.bulkDeactivate(rulesCount, selectQP); await ui.bulkDeactivate(rulesCount, selectQP);




it('can activate/deactivate specific rule for quality profile', async () => { it('can activate/deactivate specific rule for quality profile', async () => {
const { ui, user } = getPageObjects(); const { ui, user } = getPageObjects();
rulesHandler.setIsAdmin();
renderCodingRulesApp(mockLoggedInUser()); renderCodingRulesApp(mockLoggedInUser());
await ui.appLoaded(); await ui.appLoaded();


await act(async () => { await act(async () => {
await user.click(ui.qpFacet.get()); await user.click(ui.qpFacet.get());
await user.click(ui.facetItem('QP Foo Java').get());
await user.click(ui.facetItem('QP Bar Python').get());
}); });


// Only one rule is activated in selected QP
expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(1);
// Only 4 rules are activated in selected QP
expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(4);


// Switch to inactive rules // Switch to inactive rules
await act(async () => { await act(async () => {
await user.click(ui.qpInactiveRadio.get(ui.facetItem('QP Foo Java').get()));
await user.click(ui.qpInactiveRadio.get(ui.facetItem('QP Bar Python').get()));
}); });
expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(1);
expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(2);
expect(ui.activateButton.getAll()).toHaveLength(2);


// Activate Rule for qp // Activate Rule for qp
await user.click(ui.activateButton.get());
await user.click(ui.activateButton.getAll()[0]);
await user.click(ui.activateButton.get(ui.activateQPDialog.get())); await user.click(ui.activateButton.get(ui.activateQPDialog.get()));
expect(ui.activateButton.query()).not.toBeInTheDocument();
expect(ui.deactivateButton.get()).toBeInTheDocument();
expect(ui.activateButton.getAll()).toHaveLength(1);
expect(ui.deactivateButton.getAll()).toHaveLength(1);


// Deactivate activated rule // Deactivate activated rule
await user.click(ui.deactivateButton.get()); await user.click(ui.deactivateButton.get());
await user.click(ui.yesButton.get()); await user.click(ui.yesButton.get());
expect(ui.deactivateButton.query()).not.toBeInTheDocument(); expect(ui.deactivateButton.query()).not.toBeInTheDocument();
expect(ui.activateButton.get()).toBeInTheDocument();
expect(ui.activateButton.getAll()).toHaveLength(2);
});

it('can not deactivate rules for quality profile if setting is false', async () => {
const { ui } = getPageObjects();
rulesHandler.setIsAdmin();
settingsHandler.set(SettingsKey.QPAdminCanDisableInheritedRules, 'false');
renderCodingRulesApp(
mockLoggedInUser(),
'coding_rules?activation=true&tags=cute&qprofile=' + QP_2,
);
await ui.appLoaded();

// Only rule 9 is shown (inherited, activated)
expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(1);
expect(ui.deactivateButton.get()).toBeDisabled();
}); });


it('navigates by keyboard', async () => { it('navigates by keyboard', async () => {


it('can activate/change/deactivate rule in quality profile', async () => { it('can activate/change/deactivate rule in quality profile', async () => {
const { ui, user } = getPageObjects(); const { ui, user } = getPageObjects();
handler.setIsAdmin();
rulesHandler.setIsAdmin();
renderCodingRulesApp(mockLoggedInUser(), 'coding_rules?open=rule1'); renderCodingRulesApp(mockLoggedInUser(), 'coding_rules?open=rule1');
await ui.appLoaded(); await ui.appLoaded();
expect(ui.qpLink('QP Foo').get()).toBeInTheDocument(); expect(ui.qpLink('QP Foo').get()).toBeInTheDocument();


it('can extend the rule description', async () => { it('can extend the rule description', async () => {
const { ui, user } = getPageObjects(); const { ui, user } = getPageObjects();
handler.setIsAdmin();
rulesHandler.setIsAdmin();
renderCodingRulesApp(undefined, 'coding_rules?open=rule5'); renderCodingRulesApp(undefined, 'coding_rules?open=rule5');
await ui.appLoaded(); await ui.appLoaded();
expect(ui.ruleTitle('Awsome Python rule').get()).toBeInTheDocument(); expect(ui.ruleTitle('Awsome Python rule').get()).toBeInTheDocument();


it('can set tags', async () => { it('can set tags', async () => {
const { ui, user } = getPageObjects(); const { ui, user } = getPageObjects();
handler.setIsAdmin();
rulesHandler.setIsAdmin();
renderCodingRulesApp(undefined, 'coding_rules?open=rule10'); renderCodingRulesApp(undefined, 'coding_rules?open=rule10');
await ui.appLoaded(); await ui.appLoaded();


describe('custom rule', () => { describe('custom rule', () => {
it('can create custom rule', async () => { it('can create custom rule', async () => {
const { ui, user } = getPageObjects(); const { ui, user } = getPageObjects();
handler.setIsAdmin();
rulesHandler.setIsAdmin();
renderCodingRulesApp(mockLoggedInUser()); renderCodingRulesApp(mockLoggedInUser());
await ui.appLoaded(); await ui.appLoaded();




it('can edit custom rule', async () => { it('can edit custom rule', async () => {
const { ui, user } = getPageObjects(); const { ui, user } = getPageObjects();
handler.setIsAdmin();
rulesHandler.setIsAdmin();
renderCodingRulesApp(mockLoggedInUser(), 'coding_rules?open=rule9'); renderCodingRulesApp(mockLoggedInUser(), 'coding_rules?open=rule9');
await ui.appLoaded(); await ui.appLoaded();




it('can delete custom rule', async () => { it('can delete custom rule', async () => {
const { ui, user } = getPageObjects(); const { ui, user } = getPageObjects();
handler.setIsAdmin();
rulesHandler.setIsAdmin();
renderCodingRulesApp(mockLoggedInUser(), 'coding_rules?open=rule9'); renderCodingRulesApp(mockLoggedInUser(), 'coding_rules?open=rule9');
await ui.appLoaded(); await ui.appLoaded();




it('can delete custom rule from template page', async () => { it('can delete custom rule from template page', async () => {
const { ui, user } = getPageObjects(); const { ui, user } = getPageObjects();
handler.setIsAdmin();
rulesHandler.setIsAdmin();
renderCodingRulesApp(mockLoggedInUser(), 'coding_rules?open=rule8'); renderCodingRulesApp(mockLoggedInUser(), 'coding_rules?open=rule8');
await ui.appLoaded(); await ui.appLoaded();



+ 18
- 11
server/sonar-web/src/main/js/apps/coding-rules/components/CodingRulesApp.tsx View File

import { Helmet } from 'react-helmet-async'; import { Helmet } from 'react-helmet-async';
import { Profile, searchQualityProfiles } from '../../../api/quality-profiles'; import { Profile, searchQualityProfiles } from '../../../api/quality-profiles';
import { getRulesApp, searchRules } from '../../../api/rules'; import { getRulesApp, searchRules } from '../../../api/rules';
import { getValue } from '../../../api/settings';
import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext'; import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext';
import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget'; import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget';
import FiltersHeader from '../../../components/common/FiltersHeader'; import FiltersHeader from '../../../components/common/FiltersHeader';
removeWhitePageClass, removeWhitePageClass,
} from '../../../helpers/pages'; } from '../../../helpers/pages';
import { SecurityStandard } from '../../../types/security'; import { SecurityStandard } from '../../../types/security';
import { SettingsKey } from '../../../types/settings';
import { Dict, Paging, RawQuery, Rule, RuleActivation } from '../../../types/types'; import { Dict, Paging, RawQuery, Rule, RuleActivation } from '../../../types/types';
import { CurrentUser, isLoggedIn } from '../../../types/users'; import { CurrentUser, isLoggedIn } from '../../../types/users';
import { import {
interface State { interface State {
actives?: Actives; actives?: Actives;
canWrite?: boolean; canWrite?: boolean;
canDeactivateInherited?: boolean;
facets?: Facets; facets?: Facets;
loading: boolean; loading: boolean;
openFacets: OpenFacets; openFacets: OpenFacets;


fetchInitialData = () => { fetchInitialData = () => {
this.setState({ loading: true }); this.setState({ loading: true });
Promise.all([getRulesApp(), searchQualityProfiles()]).then(
([{ canWrite, repositories }, { profiles }]) => {
this.setState({
canWrite,
referencedProfiles: keyBy(profiles, 'key'),
referencedRepositories: keyBy(repositories, 'key'),
});
this.fetchFirstRules();
},
this.stopLoading,
);

Promise.all([
getRulesApp(),
searchQualityProfiles(),
getValue({ key: SettingsKey.QPAdminCanDisableInheritedRules }),
]).then(([{ canWrite, repositories }, { profiles }, setting]) => {
this.setState({
canWrite,
canDeactivateInherited: setting?.value === 'true',
referencedProfiles: keyBy(profiles, 'key'),
referencedRepositories: keyBy(repositories, 'key'),
});
this.fetchFirstRules();
}, this.stopLoading);
}; };


makeFetchRequest = (query?: RawQuery) => makeFetchRequest = (query?: RawQuery) =>
<RuleListItem <RuleListItem
activation={this.getRuleActivation(rule.key)} activation={this.getRuleActivation(rule.key)}
isLoggedIn={isLoggedIn(this.props.currentUser)} isLoggedIn={isLoggedIn(this.props.currentUser)}
canDeactivateInherited={this.state.canDeactivateInherited}
key={rule.key} key={rule.key}
onActivate={this.handleRuleActivate} onActivate={this.handleRuleActivate}
onDeactivate={this.handleRuleDeactivate} onDeactivate={this.handleRuleDeactivate}

+ 7
- 3
server/sonar-web/src/main/js/apps/coding-rules/components/RuleListItem.tsx View File

interface Props { interface Props {
activation?: Activation; activation?: Activation;
isLoggedIn: boolean; isLoggedIn: boolean;
canDeactivateInherited?: boolean;
onActivate: (profile: string, rule: string, activation: Activation) => void; onActivate: (profile: string, rule: string, activation: Activation) => void;
onDeactivate: (profile: string, rule: string) => void; onDeactivate: (profile: string, rule: string) => void;
onOpen: (ruleKey: string) => void; onOpen: (ruleKey: string) => void;
}; };


renderActions = () => { renderActions = () => {
const { activation, isLoggedIn, rule, selectedProfile } = this.props;
const { activation, isLoggedIn, canDeactivateInherited, rule, selectedProfile } = this.props;


if (!selectedProfile || !isLoggedIn) { if (!selectedProfile || !isLoggedIn) {
return null; return null;
if (activation) { if (activation) {
return ( return (
<td className="coding-rule-table-meta-cell coding-rule-activation-actions"> <td className="coding-rule-table-meta-cell coding-rule-activation-actions">
{activation.inherit === 'NONE' ? (
{activation.inherit === 'NONE' || canDeactivateInherited ? (
<ConfirmButton <ConfirmButton
confirmButtonText={translate('yes')} confirmButtonText={translate('yes')}
modalBody={translate('coding_rules.deactivate.confirm')} modalBody={translate('coding_rules.deactivate.confirm')}
</ConfirmButton> </ConfirmButton>
) : ( ) : (
<Tooltip overlay={translate('coding_rules.can_not_deactivate')}> <Tooltip overlay={translate('coding_rules.can_not_deactivate')}>
<Button className="coding-rules-detail-quality-profile-deactivate button-red disabled">
<Button
className="coding-rules-detail-quality-profile-deactivate button-red"
disabled
>
{translate('coding_rules.deactivate')} {translate('coding_rules.deactivate')}
</Button> </Button>
</Tooltip> </Tooltip>

+ 1
- 0
server/sonar-web/src/main/js/types/settings.ts View File

PluginRiskConsent = 'sonar.plugins.risk.consent', PluginRiskConsent = 'sonar.plugins.risk.consent',
LicenceRemainingLocNotificationThreshold = 'sonar.license.notifications.remainingLocThreshold', LicenceRemainingLocNotificationThreshold = 'sonar.license.notifications.remainingLocThreshold',
TokenMaxAllowedLifetime = 'sonar.auth.token.max.allowed.lifetime', TokenMaxAllowedLifetime = 'sonar.auth.token.max.allowed.lifetime',
QPAdminCanDisableInheritedRules = 'sonar.allowQualityProfileAdminsDisableInheritedRules',
} }


export enum GlobalSettingKeys { export enum GlobalSettingKeys {

Loading…
Cancel
Save