diff options
26 files changed, 298 insertions, 1433 deletions
diff --git a/server/sonar-web/src/main/js/api/mocks/QualityProfilesServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/QualityProfilesServiceMock.ts index e3f0f2bf79c..26d473f3f27 100644 --- a/server/sonar-web/src/main/js/api/mocks/QualityProfilesServiceMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/QualityProfilesServiceMock.ts @@ -19,11 +19,14 @@ */ import { cloneDeep } from 'lodash'; import { RequestData } from '../../helpers/request'; -import { mockQualityProfile } from '../../helpers/testMocks'; +import { mockCompareResult, mockQualityProfile, mockRuleDetails } from '../../helpers/testMocks'; import { SearchRulesResponse } from '../../types/coding-rules'; -import { Dict, Paging, ProfileInheritanceDetails } from '../../types/types'; +import { Dict, Paging, ProfileInheritanceDetails, RuleDetails } from '../../types/types'; import { + activateRule, changeProfileParent, + compareProfiles, + CompareResponse, copyProfile, createQualityProfile, getImporters, @@ -35,18 +38,20 @@ import { SearchQualityProfilesParameters, SearchQualityProfilesResponse, } from '../quality-profiles'; -import { searchRules } from '../rules'; +import { getRuleDetails, searchRules } from '../rules'; export default class QualityProfilesServiceMock { isAdmin = false; listQualityProfile: Profile[] = []; - languageMapping: Dict<Partial<Profile>> = { c: { language: 'c', languageName: 'C' }, }; + comparisonResult: CompareResponse = mockCompareResult(); + constructor() { this.resetQualityProfile(); + this.resetComparisonResult(); (searchQualityProfiles as jest.Mock).mockImplementation(this.handleSearchQualityProfiles); (createQualityProfile as jest.Mock).mockImplementation(this.handleCreateQualityProfile); @@ -56,6 +61,9 @@ export default class QualityProfilesServiceMock { (copyProfile as jest.Mock).mockImplementation(this.handleCopyProfile); (getImporters as jest.Mock).mockImplementation(this.handleGetImporters); (searchRules as jest.Mock).mockImplementation(this.handleSearchRules); + (compareProfiles as jest.Mock).mockImplementation(this.handleCompareQualityProfiles); + (activateRule as jest.Mock).mockImplementation(this.handleActivateRule); + (getRuleDetails as jest.Mock).mockImplementation(this.handleGetRuleDetails); } resetQualityProfile() { @@ -70,12 +78,27 @@ export default class QualityProfilesServiceMock { mockQualityProfile({ key: 'java-qp', language: 'java', + languageName: 'Java', name: 'java quality profile', activeDeprecatedRuleCount: 0, }), + mockQualityProfile({ + key: 'java-qp-1', + language: 'java', + languageName: 'Java', + name: 'java quality profile #2', + activeDeprecatedRuleCount: 1, + actions: { + edit: true, + }, + }), ]; } + resetComparisonResult() { + this.comparisonResult = mockCompareResult(); + } + handleGetImporters = () => { return this.reply([]); }; @@ -201,7 +224,7 @@ export default class QualityProfilesServiceMock { profiles = profiles.filter((p) => p.language === language); } if (this.isAdmin) { - profiles = profiles.map((p) => ({ ...p, actions: { copy: true } })); + profiles = profiles.map((p) => ({ ...p, actions: { ...p.actions, copy: true } })); } return this.reply({ @@ -210,6 +233,46 @@ export default class QualityProfilesServiceMock { }); }; + handleActivateRule = (data: { + key: string; + params?: Dict<string>; + reset?: boolean; + rule: string; + severity?: string; + }): Promise<undefined> => { + const profile = this.listQualityProfile.find((profile) => profile.key === data.key) as Profile; + const keyFilter = profile.name === this.comparisonResult.left.name ? 'inRight' : 'inLeft'; + + this.comparisonResult[keyFilter] = this.comparisonResult[keyFilter].filter( + ({ key }) => key !== data.rule + ); + + return this.reply(undefined); + }; + + handleCompareQualityProfiles = (leftKey: string, rightKey: string): Promise<CompareResponse> => { + const comparedProfiles = this.listQualityProfile.reduce((profiles, profile) => { + if (profile.key === leftKey || profile.key === rightKey) { + profiles.push(profile); + } + return profiles; + }, [] as Profile[]); + const [leftName, rightName] = comparedProfiles.map((profile) => profile.name); + + this.comparisonResult.left = { name: leftName }; + this.comparisonResult.right = { name: rightName }; + + return this.reply(this.comparisonResult); + }; + + handleGetRuleDetails = (params: { key: string }): Promise<{ rule: RuleDetails }> => { + return this.reply({ + rule: mockRuleDetails({ + key: params.key, + }), + }); + }; + setAdmin() { this.isAdmin = true; } @@ -217,6 +280,7 @@ export default class QualityProfilesServiceMock { reset() { this.isAdmin = false; this.resetQualityProfile(); + this.resetComparisonResult(); } reply<T>(response: T): Promise<T> { diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/__tests__/QualityProfilesApp-it.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/__tests__/QualityProfilesApp-it.tsx index 2e795272fda..f24cb1dd8ef 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/__tests__/QualityProfilesApp-it.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/__tests__/QualityProfilesApp-it.tsx @@ -39,10 +39,10 @@ const ui = { cQualityProfileName: 'c quality profile', newCQualityProfileName: 'New c quality profile', newCQualityProfileNameFromCreateButton: 'New c quality profile from create', - - actionOnCQualityProfile: byRole('button', { - name: 'quality_profiles.actions.c quality profile.C', - }), + profileActions: (name: string, language: string) => + byRole('button', { + name: `quality_profiles.actions.${name}.${language}`, + }), extendButton: byRole('button', { name: 'extend', }), @@ -50,6 +50,9 @@ const ui = { name: 'copy', }), createButton: byRole('button', { name: 'create' }), + compareButton: byRole('link', { name: 'compare' }), + compareDropdown: byRole('textbox', { name: 'quality_profiles.compare_with' }), + changelogLink: byRole('link', { name: 'changelog' }), popup: byRole('dialog'), copyRadio: byRole('radio', { name: 'quality_profiles.creation_from_copy quality_profiles.creation_from_copy_description_1 quality_profiles.creation_from_copy_description_2', @@ -57,6 +60,11 @@ const ui = { blankRadio: byRole('radio', { name: 'quality_profiles.creation_from_blank quality_profiles.creation_from_blank_description', }), + activeRuleButton: (profileName: string) => + byRole('button', { + name: `quality_profiles.comparison.activate_rule.${profileName}`, + }), + activateConfirmButton: byRole('button', { name: 'coding_rules.activate' }), namePropupInput: byRole('textbox', { name: 'quality_profiles.new_name field_required' }), filterByLang: byRole('textbox', { name: 'quality_profiles.filter_by:' }), listLinkCQualityProfile: byRole('link', { name: 'c quality profile' }), @@ -74,6 +82,12 @@ const ui = { name: 'quality_profiles.creation.choose_copy_quality_profile field_required', }), nameCreatePopupInput: byRole('textbox', { name: 'name field_required' }), + comparisonDiffTableHeading: (rulesQuantity: number, profileName: string) => + byRole('heading', { name: `quality_profiles.x_rules_only_in.${rulesQuantity} ${profileName}` }), + comparisonModifiedTableHeading: (rulesQuantity: number) => + byRole('heading', { + name: `quality_profiles.x_rules_have_different_configuration.${rulesQuantity}`, + }), }; it('should list Quality Profiles and filter by language', async () => { @@ -103,7 +117,7 @@ it('should be able to extend Quality Profile', async () => { serviceMock.setAdmin(); renderQualityProfiles(); - await user.click(await ui.actionOnCQualityProfile.find()); + await user.click(await ui.profileActions('c quality profile', 'C').find()); await user.click(ui.extendButton.get()); await user.clear(ui.namePropupInput.get()); @@ -129,7 +143,7 @@ it('should be able to copy Quality Profile', async () => { serviceMock.setAdmin(); renderQualityProfiles(); - await user.click(await ui.actionOnCQualityProfile.find()); + await user.click(await ui.profileActions('c quality profile', 'C').find()); await user.click(ui.copyButton.get()); await user.clear(ui.namePropupInput.get()); @@ -166,6 +180,38 @@ it('should be able to create blank Quality Profile', async () => { expect(await ui.listLinkNewCQualityProfile.find()).toBeInTheDocument(); }); +it('should be able to compare profiles', async () => { + // From the list page + const user = userEvent.setup(); + serviceMock.setAdmin(); + renderQualityProfiles(); + + // For language with 1 profle we should not see compare action + await user.click(await ui.profileActions('c quality profile', 'C').find()); + expect(ui.compareButton.query()).not.toBeInTheDocument(); + + await user.click(ui.profileActions('java quality profile', 'Java').get()); + expect(ui.compareButton.get()).toBeInTheDocument(); + await user.click(ui.compareButton.get()); + expect(ui.compareDropdown.get()).toBeInTheDocument(); + expect(ui.profileActions('java quality profile', 'Java').query()).not.toBeInTheDocument(); + expect(ui.changelogLink.query()).not.toBeInTheDocument(); + + await selectEvent.select(ui.compareDropdown.get(), 'java quality profile #2'); + expect(ui.comparisonDiffTableHeading(1, 'java quality profile').get()).toBeInTheDocument(); + expect(ui.comparisonDiffTableHeading(1, 'java quality profile #2').get()).toBeInTheDocument(); + expect(ui.comparisonModifiedTableHeading(1).query()).toBeInTheDocument(); + + // java quality profile is not editable + expect(ui.activeRuleButton('java quality profile').query()).not.toBeInTheDocument(); + + await user.click(ui.activeRuleButton('java quality profile #2').get()); + expect(ui.popup.get()).toBeInTheDocument(); + + await user.click(ui.activateConfirmButton.get()); + expect(ui.comparisonDiffTableHeading(1, 'java quality profile').query()).not.toBeInTheDocument(); +}); + function renderQualityProfiles() { renderAppRoutes('profiles', routes, { languages: { diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonResultActivation.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonResultActivation.tsx index 7b400b19a60..e82c0d872b6 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonResultActivation.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonResultActivation.tsx @@ -21,8 +21,9 @@ import * as React from 'react'; import { Profile } from '../../../api/quality-profiles'; import { getRuleDetails } from '../../../api/rules'; import { Button } from '../../../components/controls/buttons'; +import Tooltip from '../../../components/controls/Tooltip'; import DeferredSpinner from '../../../components/ui/DeferredSpinner'; -import { translate } from '../../../helpers/l10n'; +import { translate, translateWithParameters } from '../../../helpers/l10n'; import { RuleDetails } from '../../../types/types'; import ActivationFormModal from '../../coding-rules/components/ActivationFormModal'; @@ -83,9 +84,24 @@ export default class ComparisonResultActivation extends React.PureComponent<Prop return ( <DeferredSpinner loading={this.state.state === 'opening'}> - <Button disabled={this.state.state !== 'closed'} onClick={this.handleButtonClick}> - {this.props.children} - </Button> + <Tooltip + placement="bottom" + overlay={translateWithParameters( + 'quality_profiles.comparison.activate_rule', + profile.name + )} + > + <Button + disabled={this.state.state !== 'closed'} + aria-label={translateWithParameters( + 'quality_profiles.comparison.activate_rule', + profile.name + )} + onClick={this.handleButtonClick} + > + {this.props.children} + </Button> + </Tooltip> {this.isOpen(this.state) && ( <ActivationFormModal diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/compare/__tests__/ComparisonForm-test.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/compare/__tests__/ComparisonForm-test.tsx deleted file mode 100644 index 3694ed2ed33..00000000000 --- a/server/sonar-web/src/main/js/apps/quality-profiles/compare/__tests__/ComparisonForm-test.tsx +++ /dev/null @@ -1,86 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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 { shallow } from 'enzyme'; -import * as React from 'react'; -import { mockReactSelectOptionProps } from '../../../../helpers/mocks/react-select'; -import Select from '../../../../components/controls/Select'; -import { mockQualityProfile } from '../../../../helpers/testMocks'; -import ComparisonForm from '../ComparisonForm'; - -it('should render Select with right options', () => { - const output = shallowRender().find(Select); - - expect(output.length).toBe(1); - expect(output.prop('value')).toEqual([ - { isDefault: true, value: 'another', label: 'another name' }, - ]); - expect(output.prop('options')).toEqual([ - { isDefault: true, value: 'another', label: 'another name' }, - ]); -}); - -it('should render option correctly', () => { - const wrapper = shallowRender(); - const mockOptions = [ - { - value: 'val', - label: 'label', - isDefault: undefined, - }, - ]; - const OptionRenderer = wrapper.instance().optionRenderer.bind(null, mockOptions); - expect( - shallow(<OptionRenderer {...mockReactSelectOptionProps({ value: 'test' })} />) - ).toMatchSnapshot('option render'); -}); - -it('should render value correctly', () => { - const wrapper = shallowRender(); - const mockOptions = [ - { - value: 'val', - label: 'label', - isDefault: true, - }, - ]; - const ValueRenderer = wrapper.instance().singleValueRenderer.bind(null, mockOptions); - expect( - shallow(<ValueRenderer {...mockReactSelectOptionProps({ value: 'test' })} />) - ).toMatchSnapshot('value render'); -}); - -function shallowRender(overrides: Partial<ComparisonForm['props']> = {}) { - const profile = mockQualityProfile(); - const profiles = [ - profile, - mockQualityProfile({ key: 'another', name: 'another name', isDefault: true }), - mockQualityProfile({ key: 'java', name: 'java', language: 'java' }), - ]; - - return shallow<ComparisonForm>( - <ComparisonForm - onCompare={() => true} - profile={profile} - profiles={profiles} - withKey="another" - {...overrides} - /> - ); -} diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/compare/__tests__/ComparisonResultActivation-test.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/compare/__tests__/ComparisonResultActivation-test.tsx deleted file mode 100644 index faa6e4101e8..00000000000 --- a/server/sonar-web/src/main/js/apps/quality-profiles/compare/__tests__/ComparisonResultActivation-test.tsx +++ /dev/null @@ -1,45 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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 { shallow } from 'enzyme'; -import * as React from 'react'; -import { Profile } from '../../../../api/quality-profiles'; -import { click, waitAndUpdate } from '../../../../helpers/testUtils'; -import ComparisonResultActivation from '../ComparisonResultActivation'; - -jest.mock('../../../../api/rules', () => ({ - getRuleDetails: jest.fn().mockResolvedValue({ key: 'foo' }), -})); - -it('should activate', async () => { - const profile = { actions: { edit: true }, key: 'profile-key' } as Profile; - const wrapper = shallow( - <ComparisonResultActivation onDone={jest.fn()} profile={profile} ruleKey="foo" /> - ); - expect(wrapper).toMatchSnapshot(); - - click(wrapper.find('Button')); - expect(wrapper).toMatchSnapshot(); - - await waitAndUpdate(wrapper); - expect(wrapper).toMatchSnapshot(); - - wrapper.find('ActivationFormModal').prop<Function>('onClose')(); - expect(wrapper).toMatchSnapshot(); -}); diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/compare/__tests__/ComparisonResults-test.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/compare/__tests__/ComparisonResults-test.tsx deleted file mode 100644 index 3c4922760e1..00000000000 --- a/server/sonar-web/src/main/js/apps/quality-profiles/compare/__tests__/ComparisonResults-test.tsx +++ /dev/null @@ -1,104 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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 { shallow } from 'enzyme'; -import * as React from 'react'; -import { Profile } from '../../../../api/quality-profiles'; -import Link from '../../../../components/common/Link'; -import ComparisonEmpty from '../ComparisonEmpty'; -import ComparisonResults from '../ComparisonResults'; - -it('should render ComparisonEmpty', () => { - const output = shallow( - <ComparisonResults - inLeft={[]} - inRight={[]} - left={{ name: 'left' }} - leftProfile={{} as Profile} - modified={[]} - refresh={jest.fn()} - right={{ name: 'right' }} - /> - ); - expect(output.is(ComparisonEmpty)).toBe(true); -}); - -it('should compare', () => { - const inLeft = [{ key: 'rule1', name: 'rule1', severity: 'BLOCKER' }]; - const inRight = [ - { key: 'rule2', name: 'rule2', severity: 'CRITICAL' }, - { key: 'rule3', name: 'rule3', severity: 'MAJOR' }, - ]; - const modified = [ - { - key: 'rule4', - name: 'rule4', - left: { - severity: 'BLOCKER', - params: { foo: 'bar' }, - }, - right: { - severity: 'INFO', - params: { foo: 'qwe' }, - }, - }, - ]; - - const output = shallow( - <ComparisonResults - inLeft={inLeft} - inRight={inRight} - left={{ name: 'left' }} - leftProfile={{} as Profile} - modified={modified} - refresh={jest.fn()} - right={{ name: 'right' }} - /> - ); - - const leftDiffs = output.find('.js-comparison-in-left'); - expect(leftDiffs.length).toBe(1); - expect(leftDiffs.find(Link).length).toBe(1); - expect(leftDiffs.find(Link).prop('to')).toHaveProperty('search', '?rule_key=rule1&open=rule1'); - expect(leftDiffs.find(Link).prop('children')).toContain('rule1'); - expect(leftDiffs.find('SeverityIcon').length).toBe(1); - expect(leftDiffs.find('SeverityIcon').prop('severity')).toBe('BLOCKER'); - - const rightDiffs = output.find('.js-comparison-in-right'); - expect(rightDiffs.length).toBe(2); - expect(rightDiffs.at(0).find(Link).length).toBe(1); - expect(rightDiffs.at(0).find(Link).prop('to')).toHaveProperty( - 'search', - '?rule_key=rule2&open=rule2' - ); - expect(rightDiffs.at(0).find(Link).prop('children')).toContain('rule2'); - expect(rightDiffs.at(0).find('SeverityIcon').length).toBe(1); - expect(rightDiffs.at(0).find('SeverityIcon').prop('severity')).toBe('CRITICAL'); - - const modifiedDiffs = output.find('.js-comparison-modified'); - expect(modifiedDiffs.length).toBe(1); - expect(modifiedDiffs.find(Link).at(0).prop('to')).toHaveProperty( - 'search', - '?rule_key=rule4&open=rule4' - ); - expect(modifiedDiffs.find(Link).at(0).prop('children')).toContain('rule4'); - expect(modifiedDiffs.find('SeverityIcon').length).toBe(2); - expect(modifiedDiffs.text()).toContain('bar'); - expect(modifiedDiffs.text()).toContain('qwe'); -}); diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/compare/__tests__/__snapshots__/ComparisonForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/compare/__tests__/__snapshots__/ComparisonForm-test.tsx.snap deleted file mode 100644 index 70cd9a811d3..00000000000 --- a/server/sonar-web/src/main/js/apps/quality-profiles/compare/__tests__/__snapshots__/ComparisonForm-test.tsx.snap +++ /dev/null @@ -1,21 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render option correctly: option render 1`] = ` -<Option - data={ - Object { - "value": "test", - } - } -/> -`; - -exports[`should render value correctly: value render 1`] = ` -<SingleValue - data={ - Object { - "value": "test", - } - } -/> -`; diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/compare/__tests__/__snapshots__/ComparisonResultActivation-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/compare/__tests__/__snapshots__/ComparisonResultActivation-test.tsx.snap deleted file mode 100644 index ffb0851a94a..00000000000 --- a/server/sonar-web/src/main/js/apps/quality-profiles/compare/__tests__/__snapshots__/ComparisonResultActivation-test.tsx.snap +++ /dev/null @@ -1,60 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should activate 1`] = ` -<DeferredSpinner - loading={false} -> - <Button - disabled={false} - onClick={[Function]} - /> -</DeferredSpinner> -`; - -exports[`should activate 2`] = ` -<DeferredSpinner - loading={true} -> - <Button - disabled={true} - onClick={[Function]} - /> -</DeferredSpinner> -`; - -exports[`should activate 3`] = ` -<DeferredSpinner - loading={false} -> - <Button - disabled={true} - onClick={[Function]} - /> - <ActivationFormModal - modalHeader="coding_rules.activate_in_quality_profile" - onClose={[Function]} - onDone={[MockFunction]} - profiles={ - Array [ - Object { - "actions": Object { - "edit": true, - }, - "key": "profile-key", - }, - ] - } - /> -</DeferredSpinner> -`; - -exports[`should activate 4`] = ` -<DeferredSpinner - loading={false} -> - <Button - disabled={false} - onClick={[Function]} - /> -</DeferredSpinner> -`; diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx index a9418b29e80..3022d193a24 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx @@ -17,6 +17,8 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import classNames from 'classnames'; +import { some } from 'lodash'; import * as React from 'react'; import { changeProfileParent, @@ -36,8 +38,9 @@ import { Router, withRouter } from '../../../components/hoc/withRouter'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { getBaseUrl } from '../../../helpers/system'; import { getRulesUrl } from '../../../helpers/urls'; +import { PROFILE_PATH } from '../constants'; import { Profile, ProfileActionModals } from '../types'; -import { getProfileComparePath, getProfilePath, PROFILE_PATH } from '../utils'; +import { getProfileComparePath, getProfilePath } from '../utils'; import DeleteProfileForm from './DeleteProfileForm'; import ProfileModalForm from './ProfileModalForm'; @@ -45,6 +48,7 @@ interface Props { className?: string; profile: Profile; router: Router; + isComparable: boolean; updateProfiles: () => Promise<void>; } @@ -175,7 +179,7 @@ export class ProfileActions extends React.PureComponent<Props, State> { }; render() { - const { profile } = this.props; + const { profile, isComparable } = this.props; const { loading, openModal } = this.state; const { actions = {} } = profile; @@ -187,11 +191,12 @@ export class ProfileActions extends React.PureComponent<Props, State> { }); const hasNoActiveRules = profile.activeRuleCount === 0; + const hasAnyAction = some([...Object.values(actions), !profile.isBuiltIn, isComparable]); return ( <> <ActionsDropdown - className={this.props.className} + className={classNames(this.props.className, { invisible: !hasAnyAction })} label={translateWithParameters( 'quality_profiles.actions', profile.name, @@ -217,12 +222,14 @@ export class ProfileActions extends React.PureComponent<Props, State> { </ActionsDropdownItem> )} - <ActionsDropdownItem - className="it__quality-profiles__compare" - to={getProfileComparePath(profile.name, profile.language)} - > - {translate('compare')} - </ActionsDropdownItem> + {isComparable && ( + <ActionsDropdownItem + className="it__quality-profiles__compare" + to={getProfileComparePath(profile.name, profile.language)} + > + {translate('compare')} + </ActionsDropdownItem> + )} {actions.copy && ( <> diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileContainer.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileContainer.tsx index 5e18a430888..9f9f042ba00 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileContainer.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileContainer.tsx @@ -48,7 +48,8 @@ export function ProfileContainer(props: QualityProfilesContextProps) { return profileForKey ? null : <ProfileNotFound />; } - const profile = profiles.find((p) => p.language === language && p.name === name); + const filteredProfiles = profiles.filter((p) => p.language === language); + const profile = filteredProfiles.find((p) => p.name === name); if (!profile) { return <ProfileNotFound />; @@ -62,7 +63,11 @@ export function ProfileContainer(props: QualityProfilesContextProps) { return ( <div id="quality-profile"> <Helmet defer={false} title={profile.name} /> - <ProfileHeader profile={profile} updateProfiles={props.updateProfiles} /> + <ProfileHeader + profile={profile} + isComparable={filteredProfiles.length > 1} + updateProfiles={props.updateProfiles} + /> <Outlet context={context} /> </div> ); diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileNotFound.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileNotFound.tsx index 02665f130c1..28a49f85dd7 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileNotFound.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileNotFound.tsx @@ -20,7 +20,7 @@ import * as React from 'react'; import { NavLink } from 'react-router-dom'; import { translate } from '../../../helpers/l10n'; -import { PROFILE_PATH } from '../utils'; +import { PROFILE_PATH } from '../constants'; export default function ProfileNotFound() { return ( diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileActions-test.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileActions-test.tsx index 1b74b4165c0..b7aba04b96a 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileActions-test.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileActions-test.tsx @@ -30,8 +30,8 @@ import { import { mockQualityProfile, mockRouter } from '../../../../helpers/testMocks'; import { click, waitAndUpdate } from '../../../../helpers/testUtils'; import { queryToSearch } from '../../../../helpers/urls'; +import { PROFILE_PATH } from '../../constants'; import { ProfileActionModals } from '../../types'; -import { PROFILE_PATH } from '../../utils'; import DeleteProfileForm from '../DeleteProfileForm'; import { ProfileActions } from '../ProfileActions'; import ProfileModalForm from '../ProfileModalForm'; @@ -331,6 +331,12 @@ it('should not allow to set a profile as the default if the profile has no activ function shallowRender(props: Partial<ProfileActions['props']> = {}) { const router = mockRouter(); return shallow<ProfileActions>( - <ProfileActions profile={PROFILE} router={router} updateProfiles={jest.fn()} {...props} /> + <ProfileActions + isComparable={true} + profile={PROFILE} + router={router} + updateProfiles={jest.fn()} + {...props} + /> ); } diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileActions-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileActions-test.tsx.snap index 4b5c3a0a4ae..098839406ec 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileActions-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileActions-test.tsx.snap @@ -3,6 +3,7 @@ exports[`renders correctly: all permissions 1`] = ` <Fragment> <ActionsDropdown + className="" label="quality_profiles.actions.name.JavaScript" > <ActionsDropdownItem @@ -77,6 +78,7 @@ exports[`renders correctly: all permissions 1`] = ` exports[`renders correctly: copy modal 1`] = ` <Fragment> <ActionsDropdown + className="" label="quality_profiles.actions.name.JavaScript" > <ActionsDropdownItem @@ -127,6 +129,7 @@ exports[`renders correctly: copy modal 1`] = ` exports[`renders correctly: delete modal 1`] = ` <Fragment> <ActionsDropdown + className="" label="quality_profiles.actions.name.JavaScript" > <ActionsDropdownItem @@ -176,6 +179,7 @@ exports[`renders correctly: delete modal 1`] = ` exports[`renders correctly: edit only 1`] = ` <Fragment> <ActionsDropdown + className="" label="quality_profiles.actions.name.JavaScript" > <ActionsDropdownItem @@ -220,6 +224,7 @@ exports[`renders correctly: edit only 1`] = ` exports[`renders correctly: extend modal 1`] = ` <Fragment> <ActionsDropdown + className="" label="quality_profiles.actions.name.JavaScript" > <ActionsDropdownItem @@ -270,6 +275,7 @@ exports[`renders correctly: extend modal 1`] = ` exports[`renders correctly: no permissions 1`] = ` <Fragment> <ActionsDropdown + className="" label="quality_profiles.actions.name.JavaScript" > <ActionsDropdownItem @@ -297,6 +303,7 @@ exports[`renders correctly: no permissions 1`] = ` exports[`renders correctly: rename modal 1`] = ` <Fragment> <ActionsDropdown + className="" label="quality_profiles.actions.name.JavaScript" > <ActionsDropdownItem @@ -347,6 +354,7 @@ exports[`renders correctly: rename modal 1`] = ` exports[`should not allow to set a profile as the default if the profile has no active rules 1`] = ` <Fragment> <ActionsDropdown + className="" label="quality_profiles.actions.name.JavaScript" > <ActionsDropdownItem diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfileHeader-test.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/constants.ts index dd88c2f4502..ed4e1f71118 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfileHeader-test.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/constants.ts @@ -17,20 +17,5 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { shallow } from 'enzyme'; -import * as React from 'react'; -import { mockQualityProfile } from '../../../../helpers/testMocks'; -import ProfileHeader from '../ProfileHeader'; - -it('should render correctly', () => { - expect(shallowRender()).toMatchSnapshot(); - expect(shallowRender({ profile: mockQualityProfile({ isDefault: true }) })).toMatchSnapshot( - 'for default profile' - ); -}); - -function shallowRender(props: Partial<ProfileHeader['props']> = {}) { - return shallow( - <ProfileHeader profile={mockQualityProfile()} updateProfiles={jest.fn()} {...props} /> - ); -} +export const PROFILE_PATH = '/profiles'; +export const PROFILE_COMPARE_PATH = `${PROFILE_PATH}/compare`; diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileHeader.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileHeader.tsx index 972f92892c0..8fddc6e6e3e 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileHeader.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileHeader.tsx @@ -23,48 +23,55 @@ import { NavLink } from 'react-router-dom'; import Link from '../../../components/common/Link'; import HelpTooltip from '../../../components/controls/HelpTooltip'; import Tooltip from '../../../components/controls/Tooltip'; +import { useLocation } from '../../../components/hoc/withRouter'; import DateFromNow from '../../../components/intl/DateFromNow'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { getQualityProfileUrl } from '../../../helpers/urls'; import BuiltInQualityProfileBadge from '../components/BuiltInQualityProfileBadge'; import ProfileActions from '../components/ProfileActions'; import ProfileLink from '../components/ProfileLink'; +import { PROFILE_PATH } from '../constants'; import { Profile } from '../types'; -import { getProfileChangelogPath, getProfilesForLanguagePath, PROFILE_PATH } from '../utils'; +import { + getProfileChangelogPath, + getProfilesForLanguagePath, + isProfileComparePath, +} from '../utils'; interface Props { profile: Profile; + isComparable: boolean; updateProfiles: () => Promise<void>; } -export default class ProfileHeader extends React.PureComponent<Props> { - render() { - const { profile } = this.props; +export default function ProfileHeader(props: Props) { + const { profile, isComparable, updateProfiles } = props; + const location = useLocation(); - return ( - <div className="page-header quality-profile-header"> - <div className="note spacer-bottom"> - <NavLink end={true} to={PROFILE_PATH}> - {translate('quality_profiles.page')} - </NavLink> - {' / '} - <Link to={getProfilesForLanguagePath(profile.language)}>{profile.languageName}</Link> - </div> - - <h1 className="page-title"> - <ProfileLink language={profile.language} name={profile.name}> - <span>{profile.name}</span> - </ProfileLink> - {profile.isDefault && ( - <Tooltip overlay={translate('quality_profiles.list.default.help')}> - <span className=" spacer-left badge">{translate('default')}</span> - </Tooltip> - )} - {profile.isBuiltIn && ( - <BuiltInQualityProfileBadge className="spacer-left" tooltip={false} /> - )} - </h1> + return ( + <div className="page-header quality-profile-header"> + <div className="note spacer-bottom"> + <NavLink end={true} to={PROFILE_PATH}> + {translate('quality_profiles.page')} + </NavLink> + {' / '} + <Link to={getProfilesForLanguagePath(profile.language)}>{profile.languageName}</Link> + </div> + <h1 className="page-title"> + <ProfileLink language={profile.language} name={profile.name}> + <span>{profile.name}</span> + </ProfileLink> + {profile.isDefault && ( + <Tooltip overlay={translate('quality_profiles.list.default.help')}> + <span className=" spacer-left badge">{translate('default')}</span> + </Tooltip> + )} + {profile.isBuiltIn && ( + <BuiltInQualityProfileBadge className="spacer-left" tooltip={false} /> + )} + </h1> + {!isProfileComparePath(location.pathname) && ( <div className="pull-right"> <ul className="list-inline" style={{ lineHeight: '24px' }}> <li className="small spacer-right"> @@ -78,47 +85,47 @@ export default class ProfileHeader extends React.PureComponent<Props> { {translate('changelog')} </Link> </li> + <li> <ProfileActions className="pull-left" profile={profile} - updateProfiles={this.props.updateProfiles} + isComparable={isComparable} + updateProfiles={updateProfiles} /> </li> </ul> </div> + )} - {profile.isBuiltIn && ( - <div className="page-description"> - {translate('quality_profiles.built_in.description')} - </div> - )} + {profile.isBuiltIn && ( + <div className="page-description">{translate('quality_profiles.built_in.description')}</div> + )} - {profile.parentKey && profile.parentName && ( - <div className="page-description"> - <FormattedMessage - defaultMessage={translate('quality_profiles.extend_description')} - id="quality_profiles.extend_description" - values={{ - link: ( - <> - <Link to={getQualityProfileUrl(profile.parentName, profile.language)}> - {profile.parentName} - </Link> - <HelpTooltip - className="little-spacer-left" - overlay={translateWithParameters( - 'quality_profiles.extend_description_help', - profile.parentName - )} - /> - </> - ), - }} - /> - </div> - )} - </div> - ); - } + {profile.parentKey && profile.parentName && ( + <div className="page-description"> + <FormattedMessage + defaultMessage={translate('quality_profiles.extend_description')} + id="quality_profiles.extend_description" + values={{ + link: ( + <> + <Link to={getQualityProfileUrl(profile.parentName, profile.language)}> + {profile.parentName} + </Link> + <HelpTooltip + className="little-spacer-left" + overlay={translateWithParameters( + 'quality_profiles.extend_description_help', + profile.parentName + )} + /> + </> + ), + }} + /> + </div> + )} + </div> + ); } diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileHeader-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileHeader-test.tsx.snap deleted file mode 100644 index f181062d432..00000000000 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileHeader-test.tsx.snap +++ /dev/null @@ -1,214 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly 1`] = ` -<div - className="page-header quality-profile-header" -> - <div - className="note spacer-bottom" - > - <NavLink - end={true} - to="/profiles" - > - quality_profiles.page - </NavLink> - / - <ForwardRef(Link) - to={ - Object { - "pathname": "/profiles", - "search": "?language=js", - } - } - > - JavaScript - </ForwardRef(Link)> - </div> - <h1 - className="page-title" - > - <ProfileLink - language="js" - name="name" - > - <span> - name - </span> - </ProfileLink> - </h1> - <div - className="pull-right" - > - <ul - className="list-inline" - style={ - Object { - "lineHeight": "24px", - } - } - > - <li - className="small spacer-right" - > - quality_profiles.updated_ - - <DateFromNow /> - </li> - <li - className="small big-spacer-right" - > - quality_profiles.used_ - - <DateFromNow /> - </li> - <li> - <ForwardRef(Link) - className="button" - to={ - Object { - "pathname": "/profiles/changelog", - "search": "?language=js&name=name", - } - } - > - changelog - </ForwardRef(Link)> - </li> - <li> - <withRouter(ProfileActions) - className="pull-left" - profile={ - Object { - "activeDeprecatedRuleCount": 2, - "activeRuleCount": 10, - "childrenCount": 0, - "depth": 1, - "isBuiltIn": false, - "isDefault": false, - "isInherited": false, - "key": "key", - "language": "js", - "languageName": "JavaScript", - "name": "name", - "projectCount": 3, - } - } - updateProfiles={[MockFunction]} - /> - </li> - </ul> - </div> -</div> -`; - -exports[`should render correctly: for default profile 1`] = ` -<div - className="page-header quality-profile-header" -> - <div - className="note spacer-bottom" - > - <NavLink - end={true} - to="/profiles" - > - quality_profiles.page - </NavLink> - / - <ForwardRef(Link) - to={ - Object { - "pathname": "/profiles", - "search": "?language=js", - } - } - > - JavaScript - </ForwardRef(Link)> - </div> - <h1 - className="page-title" - > - <ProfileLink - language="js" - name="name" - > - <span> - name - </span> - </ProfileLink> - <Tooltip - overlay="quality_profiles.list.default.help" - > - <span - className=" spacer-left badge" - > - default - </span> - </Tooltip> - </h1> - <div - className="pull-right" - > - <ul - className="list-inline" - style={ - Object { - "lineHeight": "24px", - } - } - > - <li - className="small spacer-right" - > - quality_profiles.updated_ - - <DateFromNow /> - </li> - <li - className="small big-spacer-right" - > - quality_profiles.used_ - - <DateFromNow /> - </li> - <li> - <ForwardRef(Link) - className="button" - to={ - Object { - "pathname": "/profiles/changelog", - "search": "?language=js&name=name", - } - } - > - changelog - </ForwardRef(Link)> - </li> - <li> - <withRouter(ProfileActions) - className="pull-left" - profile={ - Object { - "activeDeprecatedRuleCount": 2, - "activeRuleCount": 10, - "childrenCount": 0, - "depth": 1, - "isBuiltIn": false, - "isDefault": true, - "isInherited": false, - "key": "key", - "language": "js", - "languageName": "JavaScript", - "name": "name", - "projectCount": 3, - } - } - updateProfiles={[MockFunction]} - /> - </li> - </ul> - </div> -</div> -`; diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesList.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesList.tsx index c54cc79976c..83cad70d3f0 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesList.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesList.tsx @@ -42,6 +42,7 @@ export default class ProfilesList extends React.PureComponent<Props> { key={profile.key} profile={profile} updateProfiles={this.props.updateProfiles} + isComparable={profiles.length > 1} /> )); } diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListHeader.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListHeader.tsx index ab015cac395..6d70b65f9d4 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListHeader.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListHeader.tsx @@ -21,7 +21,8 @@ import * as React from 'react'; import Select from '../../../components/controls/Select'; import { Router, withRouter } from '../../../components/hoc/withRouter'; import { translate } from '../../../helpers/l10n'; -import { getProfilesForLanguagePath, PROFILE_PATH } from '../utils'; +import { PROFILE_PATH } from '../constants'; +import { getProfilesForLanguagePath } from '../utils'; interface Props { currentFilter?: string; diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListRow.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListRow.tsx index 274e6d96ec1..2ef64f2f3a1 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListRow.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListRow.tsx @@ -31,10 +31,11 @@ import { Profile } from '../types'; export interface ProfilesListRowProps { profile: Profile; updateProfiles: () => Promise<void>; + isComparable: boolean; } export function ProfilesListRow(props: ProfilesListRowProps) { - const { profile } = props; + const { profile, isComparable } = props; const offset = 25 * (profile.depth - 1); const activeRulesUrl = getRulesUrl({ @@ -99,7 +100,11 @@ export function ProfilesListRow(props: ProfilesListRowProps) { </td> <td className="quality-profiles-table-actions thin nowrap text-middle text-right"> - <ProfileActions profile={profile} updateProfiles={props.updateProfiles} /> + <ProfileActions + isComparable={isComparable} + profile={profile} + updateProfiles={props.updateProfiles} + /> </td> </tr> ); diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/ProfilesList-test.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/ProfilesList-test.tsx deleted file mode 100644 index deda1b4c523..00000000000 --- a/server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/ProfilesList-test.tsx +++ /dev/null @@ -1,45 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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 { shallow } from 'enzyme'; -import * as React from 'react'; -import { mockLanguage, mockQualityProfile } from '../../../../helpers/testMocks'; -import ProfilesList from '../ProfilesList'; - -it('should render correctly', () => { - expect(shallowRender()).toMatchSnapshot(); - - expect(shallowRender({ language: 'css' })).toMatchSnapshot(); - - expect(shallowRender({ language: 'unknown' })).toMatchSnapshot(); -}); - -function shallowRender(props: Partial<ProfilesList['props']> = {}) { - return shallow( - <ProfilesList - languages={[mockLanguage(), mockLanguage({ key: 'js', name: 'JS' })]} - profiles={[ - mockQualityProfile(), - mockQualityProfile({ language: 'css', languageName: 'CSS' }), - ]} - updateProfiles={jest.fn()} - {...props} - /> - ); -} diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/ProfilesListRow-test.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/ProfilesListRow-test.tsx deleted file mode 100644 index b8415e754ec..00000000000 --- a/server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/ProfilesListRow-test.tsx +++ /dev/null @@ -1,46 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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 { shallow } from 'enzyme'; -import * as React from 'react'; -import { mockQualityProfile } from '../../../../helpers/testMocks'; -import { ProfilesListRow, ProfilesListRowProps } from '../ProfilesListRow'; - -it('should render correctly', () => { - expect(shallowRender()).toMatchSnapshot('default'); - expect(shallowRender({ profile: mockQualityProfile({ isBuiltIn: true }) })).toMatchSnapshot( - 'built-in profile' - ); - expect(shallowRender({ profile: mockQualityProfile({ isDefault: true }) })).toMatchSnapshot( - 'default profile' - ); - expect( - shallowRender({ profile: mockQualityProfile({ activeDeprecatedRuleCount: 10 }) }) - ).toMatchSnapshot('with deprecated rules'); -}); - -function shallowRender(props: Partial<ProfilesListRowProps> = {}) { - return shallow( - <ProfilesListRow - profile={mockQualityProfile({ activeDeprecatedRuleCount: 0 })} - updateProfiles={jest.fn()} - {...props} - /> - ); -} diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/__snapshots__/ProfilesList-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/__snapshots__/ProfilesList-test.tsx.snap deleted file mode 100644 index e9091091c0f..00000000000 --- a/server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/__snapshots__/ProfilesList-test.tsx.snap +++ /dev/null @@ -1,288 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly 1`] = ` -<div> - <withRouter(ProfilesListHeader) - languages={ - Array [ - Object { - "key": "css", - "name": "CSS", - }, - Object { - "key": "js", - "name": "JS", - }, - ] - } - /> - <div - className="boxed-group boxed-group-inner quality-profiles-table" - key="css" - > - <table - className="data zebra zebra-hover" - data-language="css" - > - <thead> - <tr> - <th> - CSS - , - quality_profiles.x_profiles.1 - </th> - <th - className="text-right nowrap" - > - quality_profiles.list.projects - <HelpTooltip - className="table-cell-doc" - overlay={ - <div - className="big-padded-top big-padded-bottom" - > - quality_profiles.list.projects.help - </div> - } - /> - </th> - <th - className="text-right nowrap" - > - quality_profiles.list.rules - </th> - <th - className="text-right nowrap" - > - quality_profiles.list.updated - </th> - <th - className="text-right nowrap" - > - quality_profiles.list.used - </th> - <th> - - </th> - </tr> - </thead> - <tbody> - <Memo(ProfilesListRow) - key="key" - profile={ - Object { - "activeDeprecatedRuleCount": 2, - "activeRuleCount": 10, - "childrenCount": 0, - "depth": 1, - "isBuiltIn": false, - "isDefault": false, - "isInherited": false, - "key": "key", - "language": "css", - "languageName": "CSS", - "name": "name", - "projectCount": 3, - } - } - updateProfiles={[MockFunction]} - /> - </tbody> - </table> - </div> - <div - className="boxed-group boxed-group-inner quality-profiles-table" - key="js" - > - <table - className="data zebra zebra-hover" - data-language="js" - > - <thead> - <tr> - <th> - JS - , - quality_profiles.x_profiles.1 - </th> - <th - className="text-right nowrap" - > - quality_profiles.list.projects - <HelpTooltip - className="table-cell-doc" - overlay={ - <div - className="big-padded-top big-padded-bottom" - > - quality_profiles.list.projects.help - </div> - } - /> - </th> - <th - className="text-right nowrap" - > - quality_profiles.list.rules - </th> - <th - className="text-right nowrap" - > - quality_profiles.list.updated - </th> - <th - className="text-right nowrap" - > - quality_profiles.list.used - </th> - <th> - - </th> - </tr> - </thead> - <tbody> - <Memo(ProfilesListRow) - key="key" - profile={ - Object { - "activeDeprecatedRuleCount": 2, - "activeRuleCount": 10, - "childrenCount": 0, - "depth": 1, - "isBuiltIn": false, - "isDefault": false, - "isInherited": false, - "key": "key", - "language": "js", - "languageName": "JavaScript", - "name": "name", - "projectCount": 3, - } - } - updateProfiles={[MockFunction]} - /> - </tbody> - </table> - </div> -</div> -`; - -exports[`should render correctly 2`] = ` -<div> - <withRouter(ProfilesListHeader) - currentFilter="css" - languages={ - Array [ - Object { - "key": "css", - "name": "CSS", - }, - Object { - "key": "js", - "name": "JS", - }, - ] - } - /> - <div - className="boxed-group boxed-group-inner quality-profiles-table" - key="css" - > - <table - className="data zebra zebra-hover" - data-language="css" - > - <thead> - <tr> - <th> - CSS - , - quality_profiles.x_profiles.1 - </th> - <th - className="text-right nowrap" - > - quality_profiles.list.projects - <HelpTooltip - className="table-cell-doc" - overlay={ - <div - className="big-padded-top big-padded-bottom" - > - quality_profiles.list.projects.help - </div> - } - /> - </th> - <th - className="text-right nowrap" - > - quality_profiles.list.rules - </th> - <th - className="text-right nowrap" - > - quality_profiles.list.updated - </th> - <th - className="text-right nowrap" - > - quality_profiles.list.used - </th> - <th> - - </th> - </tr> - </thead> - <tbody> - <Memo(ProfilesListRow) - key="key" - profile={ - Object { - "activeDeprecatedRuleCount": 2, - "activeRuleCount": 10, - "childrenCount": 0, - "depth": 1, - "isBuiltIn": false, - "isDefault": false, - "isInherited": false, - "key": "key", - "language": "css", - "languageName": "CSS", - "name": "name", - "projectCount": 3, - } - } - updateProfiles={[MockFunction]} - /> - </tbody> - </table> - </div> -</div> -`; - -exports[`should render correctly 3`] = ` -<div> - <withRouter(ProfilesListHeader) - currentFilter="unknown" - languages={ - Array [ - Object { - "key": "css", - "name": "CSS", - }, - Object { - "key": "js", - "name": "JS", - }, - ] - } - /> - <Alert - className="spacer-top" - variant="warning" - > - no_results - </Alert> -</div> -`; diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/__snapshots__/ProfilesListRow-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/__snapshots__/ProfilesListRow-test.tsx.snap deleted file mode 100644 index 90c5fc4af4e..00000000000 --- a/server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/__snapshots__/ProfilesListRow-test.tsx.snap +++ /dev/null @@ -1,411 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly: built-in profile 1`] = ` -<tr - className="quality-profiles-table-row text-middle" - data-key="key" - data-name="name" -> - <td - className="quality-profiles-table-name text-middle" - > - <div - className="display-flex-center" - style={ - Object { - "paddingLeft": 0, - } - } - > - <div> - <ProfileLink - language="js" - name="name" - > - name - </ProfileLink> - </div> - <BuiltInQualityProfileBadge - className="spacer-left" - /> - </div> - </td> - <td - className="quality-profiles-table-projects thin nowrap text-middle text-right" - > - <span> - 3 - </span> - </td> - <td - className="quality-profiles-table-rules thin nowrap text-middle text-right" - > - <div> - <span - className="spacer-right" - > - <Tooltip - overlay="quality_profiles.deprecated_rules" - > - <ForwardRef(Link) - className="badge badge-error" - to={ - Object { - "pathname": "/coding_rules", - "search": "?qprofile=key&activation=true&statuses=DEPRECATED", - } - } - > - 2 - </ForwardRef(Link)> - </Tooltip> - </span> - <ForwardRef(Link) - to={ - Object { - "pathname": "/coding_rules", - "search": "?qprofile=key&activation=true", - } - } - > - 10 - </ForwardRef(Link)> - </div> - </td> - <td - className="quality-profiles-table-date thin nowrap text-middle text-right" - > - <DateFromNow /> - </td> - <td - className="quality-profiles-table-date thin nowrap text-middle text-right" - > - <DateFromNow /> - </td> - <td - className="quality-profiles-table-actions thin nowrap text-middle text-right" - > - <withRouter(ProfileActions) - profile={ - Object { - "activeDeprecatedRuleCount": 2, - "activeRuleCount": 10, - "childrenCount": 0, - "depth": 1, - "isBuiltIn": true, - "isDefault": false, - "isInherited": false, - "key": "key", - "language": "js", - "languageName": "JavaScript", - "name": "name", - "projectCount": 3, - } - } - updateProfiles={[MockFunction]} - /> - </td> -</tr> -`; - -exports[`should render correctly: default 1`] = ` -<tr - className="quality-profiles-table-row text-middle" - data-key="key" - data-name="name" -> - <td - className="quality-profiles-table-name text-middle" - > - <div - className="display-flex-center" - style={ - Object { - "paddingLeft": 0, - } - } - > - <div> - <ProfileLink - language="js" - name="name" - > - name - </ProfileLink> - </div> - </div> - </td> - <td - className="quality-profiles-table-projects thin nowrap text-middle text-right" - > - <span> - 3 - </span> - </td> - <td - className="quality-profiles-table-rules thin nowrap text-middle text-right" - > - <div> - <ForwardRef(Link) - to={ - Object { - "pathname": "/coding_rules", - "search": "?qprofile=key&activation=true", - } - } - > - 10 - </ForwardRef(Link)> - </div> - </td> - <td - className="quality-profiles-table-date thin nowrap text-middle text-right" - > - <DateFromNow /> - </td> - <td - className="quality-profiles-table-date thin nowrap text-middle text-right" - > - <DateFromNow /> - </td> - <td - className="quality-profiles-table-actions thin nowrap text-middle text-right" - > - <withRouter(ProfileActions) - profile={ - Object { - "activeDeprecatedRuleCount": 0, - "activeRuleCount": 10, - "childrenCount": 0, - "depth": 1, - "isBuiltIn": false, - "isDefault": false, - "isInherited": false, - "key": "key", - "language": "js", - "languageName": "JavaScript", - "name": "name", - "projectCount": 3, - } - } - updateProfiles={[MockFunction]} - /> - </td> -</tr> -`; - -exports[`should render correctly: default profile 1`] = ` -<tr - className="quality-profiles-table-row text-middle" - data-key="key" - data-name="name" -> - <td - className="quality-profiles-table-name text-middle" - > - <div - className="display-flex-center" - style={ - Object { - "paddingLeft": 0, - } - } - > - <div> - <ProfileLink - language="js" - name="name" - > - name - </ProfileLink> - </div> - </div> - </td> - <td - className="quality-profiles-table-projects thin nowrap text-middle text-right" - > - <Tooltip - overlay="quality_profiles.list.default.help" - > - <span - className="badge" - > - default - </span> - </Tooltip> - </td> - <td - className="quality-profiles-table-rules thin nowrap text-middle text-right" - > - <div> - <span - className="spacer-right" - > - <Tooltip - overlay="quality_profiles.deprecated_rules" - > - <ForwardRef(Link) - className="badge badge-error" - to={ - Object { - "pathname": "/coding_rules", - "search": "?qprofile=key&activation=true&statuses=DEPRECATED", - } - } - > - 2 - </ForwardRef(Link)> - </Tooltip> - </span> - <ForwardRef(Link) - to={ - Object { - "pathname": "/coding_rules", - "search": "?qprofile=key&activation=true", - } - } - > - 10 - </ForwardRef(Link)> - </div> - </td> - <td - className="quality-profiles-table-date thin nowrap text-middle text-right" - > - <DateFromNow /> - </td> - <td - className="quality-profiles-table-date thin nowrap text-middle text-right" - > - <DateFromNow /> - </td> - <td - className="quality-profiles-table-actions thin nowrap text-middle text-right" - > - <withRouter(ProfileActions) - profile={ - Object { - "activeDeprecatedRuleCount": 2, - "activeRuleCount": 10, - "childrenCount": 0, - "depth": 1, - "isBuiltIn": false, - "isDefault": true, - "isInherited": false, - "key": "key", - "language": "js", - "languageName": "JavaScript", - "name": "name", - "projectCount": 3, - } - } - updateProfiles={[MockFunction]} - /> - </td> -</tr> -`; - -exports[`should render correctly: with deprecated rules 1`] = ` -<tr - className="quality-profiles-table-row text-middle" - data-key="key" - data-name="name" -> - <td - className="quality-profiles-table-name text-middle" - > - <div - className="display-flex-center" - style={ - Object { - "paddingLeft": 0, - } - } - > - <div> - <ProfileLink - language="js" - name="name" - > - name - </ProfileLink> - </div> - </div> - </td> - <td - className="quality-profiles-table-projects thin nowrap text-middle text-right" - > - <span> - 3 - </span> - </td> - <td - className="quality-profiles-table-rules thin nowrap text-middle text-right" - > - <div> - <span - className="spacer-right" - > - <Tooltip - overlay="quality_profiles.deprecated_rules" - > - <ForwardRef(Link) - className="badge badge-error" - to={ - Object { - "pathname": "/coding_rules", - "search": "?qprofile=key&activation=true&statuses=DEPRECATED", - } - } - > - 10 - </ForwardRef(Link)> - </Tooltip> - </span> - <ForwardRef(Link) - to={ - Object { - "pathname": "/coding_rules", - "search": "?qprofile=key&activation=true", - } - } - > - 10 - </ForwardRef(Link)> - </div> - </td> - <td - className="quality-profiles-table-date thin nowrap text-middle text-right" - > - <DateFromNow /> - </td> - <td - className="quality-profiles-table-date thin nowrap text-middle text-right" - > - <DateFromNow /> - </td> - <td - className="quality-profiles-table-actions thin nowrap text-middle text-right" - > - <withRouter(ProfileActions) - profile={ - Object { - "activeDeprecatedRuleCount": 10, - "activeRuleCount": 10, - "childrenCount": 0, - "depth": 1, - "isBuiltIn": false, - "isDefault": false, - "isInherited": false, - "key": "key", - "language": "js", - "languageName": "JavaScript", - "name": "name", - "projectCount": 3, - } - } - updateProfiles={[MockFunction]} - /> - </td> -</tr> -`; diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/utils.ts b/server/sonar-web/src/main/js/apps/quality-profiles/utils.ts index 326de09028f..2db3e7fcb78 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/utils.ts +++ b/server/sonar-web/src/main/js/apps/quality-profiles/utils.ts @@ -22,6 +22,7 @@ import { sortBy } from 'lodash'; import { Profile as BaseProfile } from '../../api/quality-profiles'; import { isValidDate, parseDate } from '../../helpers/dates'; import { queryToSearch } from '../../helpers/urls'; +import { PROFILE_COMPARE_PATH, PROFILE_PATH } from './constants'; import { Profile } from './types'; export function sortProfiles(profiles: BaseProfile[]): Profile[] { @@ -65,8 +66,6 @@ export function isStagnant(profile: Profile): boolean { return false; } -export const PROFILE_PATH = '/profiles'; - export const getProfilesForLanguagePath = (language: string) => ({ pathname: PROFILE_PATH, search: queryToSearch({ language }), @@ -83,7 +82,7 @@ export const getProfileComparePath = (name: string, language: string, withKey?: Object.assign(query, { withKey }); } return { - pathname: `${PROFILE_PATH}/compare`, + pathname: PROFILE_COMPARE_PATH, search: queryToSearch(query), }; }; @@ -107,3 +106,7 @@ export const getProfileChangelogPath = ( search: queryToSearch(query), }; }; + +export const isProfileComparePath = (pathname: string): boolean => { + return pathname === PROFILE_COMPARE_PATH; +}; diff --git a/server/sonar-web/src/main/js/helpers/testMocks.ts b/server/sonar-web/src/main/js/helpers/testMocks.ts index a38b03e8d5c..7e24416d250 100644 --- a/server/sonar-web/src/main/js/helpers/testMocks.ts +++ b/server/sonar-web/src/main/js/helpers/testMocks.ts @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import { To } from 'react-router-dom'; +import { CompareResponse } from '../api/quality-profiles'; import { RuleDescriptionSections } from '../apps/coding-rules/rule'; import { Exporter, Profile } from '../apps/quality-profiles/types'; import { Location, Router } from '../components/hoc/withRouter'; @@ -437,6 +438,36 @@ export function mockQualityProfile(overrides: Partial<Profile> = {}): Profile { }; } +export function mockCompareResult(overrides: Partial<CompareResponse> = {}): CompareResponse { + return { + left: { name: 'Profile A' }, + right: { name: 'Profile B' }, + inLeft: [ + { + key: 'java:S4604', + name: 'Rule in left', + severity: 'MINOR', + }, + ], + inRight: [ + { + key: 'java:S5128', + name: 'Rule in right', + severity: 'MAJOR', + }, + ], + modified: [ + { + key: 'java:S1698', + name: '== and != should not be used when equals is overridden', + left: { params: {}, severity: 'MINOR' }, + right: { params: {}, severity: 'CRITICAL' }, + }, + ], + ...overrides, + }; +} + export function mockQualityProfileInheritance( overrides: Partial<ProfileInheritanceDetails> = {} ): ProfileInheritanceDetails { 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 7704dea71d0..253cc61493c 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -1701,6 +1701,7 @@ quality_profile.empty_comparison=The quality profiles are equal. quality_profiles.activate_more=Activate More quality_profiles.activate_more.help.built_in=This quality profile is built in, and cannot be updated manually. If you want to activate more rules, create a new profile that inherits from this one and add rules there. quality_profiles.activate_more_rules=Activate More Rules +quality_profiles.comparison.activate_rule=Activate rule for profile "{0}" quality_profiles.intro1=Quality profiles are collections of rules to apply during an analysis. quality_profiles.intro2=For each language there is a default profile. All projects not explicitly assigned to some other profile will be analyzed with the default. Ideally, all projects will use the same profile for a language. quality_profiles.list.projects=Projects @@ -1758,7 +1759,6 @@ quality_profiles.actions=Open {0} {1} quality profile actions - #------------------------------------------------------------------------------ # # QUALITY GATES |