diff options
author | Mathieu Suen <mathieu.suen@sonarsource.com> | 2022-03-23 18:32:30 +0100 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2022-03-28 20:02:52 +0000 |
commit | 63ad64c7c2b365736eb4c9c91a68d9580c549d69 (patch) | |
tree | def5b30f8cd510966f5a0663f1e6210c7463ef05 /server/sonar-web/src | |
parent | a39698d0cd444d4e9c56fafb8da4171a3e3454bf (diff) | |
download | sonarqube-63ad64c7c2b365736eb4c9c91a68d9580c549d69.tar.gz sonarqube-63ad64c7c2b365736eb4c9c91a68d9580c549d69.zip |
SONAR-16085 Adding IT and change select from activation modal
Diffstat (limited to 'server/sonar-web/src')
15 files changed, 683 insertions, 581 deletions
diff --git a/server/sonar-web/src/main/js/api/mocks/CodingRulesMock.ts b/server/sonar-web/src/main/js/api/mocks/CodingRulesMock.ts new file mode 100644 index 00000000000..198e6cf585c --- /dev/null +++ b/server/sonar-web/src/main/js/api/mocks/CodingRulesMock.ts @@ -0,0 +1,176 @@ +/* + * 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 { cloneDeep, countBy } from 'lodash'; +import { mockQualityProfile, mockRule, mockRuleRepository } from '../../helpers/testMocks'; +import { RuleRepository } from '../../types/coding-rules'; +import { SearchRulesQuery } from '../../types/rules'; +import { Rule } from '../../types/types'; +import { + bulkActivateRules, + bulkDeactivateRules, + Profile, + searchQualityProfiles, + SearchQualityProfilesParameters, + SearchQualityProfilesResponse +} from '../quality-profiles'; +import { getRulesApp, searchRules } from '../rules'; + +interface FacetFilter { + languages?: string; +} + +const FACET_RULE_MAP: { [key: string]: keyof Rule } = { + languages: 'lang', + types: 'type' +}; +export default class CodingRulesMock { + defaultRules: Rule[] = []; + rules: Rule[] = []; + qualityProfile: Profile[] = []; + repositories: RuleRepository[] = []; + isAdmin = false; + applyWithWarning = false; + + constructor() { + this.repositories = [ + mockRuleRepository({ key: 'repo1' }), + mockRuleRepository({ key: 'repo2' }) + ]; + this.qualityProfile = [ + mockQualityProfile({ key: 'p1', name: 'QP Foo', language: 'java', languageName: 'Java' }), + mockQualityProfile({ key: 'p2', name: 'QP Bar', language: 'js' }), + mockQualityProfile({ key: 'p3', name: 'QP FooBar', language: 'java', languageName: 'Java' }) + ]; + + this.defaultRules = [ + mockRule({ + key: 'rule1', + type: 'BUG', + lang: 'java', + langName: 'Java', + name: 'Awsome java rule' + }), + mockRule({ key: 'rule2', name: 'Hot hotspot', type: 'SECURITY_HOTSPOT' }), + mockRule({ key: 'rule3', name: 'Unknown rule' }), + mockRule({ key: 'rule4', type: 'BUG', lang: 'c', langName: 'C', name: 'Awsome C rule' }) + ]; + + (searchRules as jest.Mock).mockImplementation(this.handleSearchRules); + (searchQualityProfiles as jest.Mock).mockImplementation(this.handleSearchQualityProfiles); + (getRulesApp as jest.Mock).mockImplementation(this.handleGetRulesApp); + (bulkActivateRules as jest.Mock).mockImplementation(this.handleBulkActivateRules); + (bulkDeactivateRules as jest.Mock).mockImplementation(this.handleBulkDeactivateRules); + + this.rules = cloneDeep(this.defaultRules); + } + + filterFacet({ languages }: FacetFilter) { + let filteredRules = this.rules; + if (languages) { + filteredRules = filteredRules.filter(r => r.lang && languages.includes(r.lang)); + } + return filteredRules; + } + + setIsAdmin() { + this.isAdmin = true; + } + + activateWithWarning() { + this.applyWithWarning = true; + } + + reset() { + this.isAdmin = false; + this.applyWithWarning = false; + this.rules = cloneDeep(this.defaultRules); + } + + allRulesName() { + return this.rules.map(r => r.name); + } + + allQualityProfile(language: string) { + return this.qualityProfile.filter(qp => qp.language === language); + } + + handleSearchRules = ({ facets, languages, p, ps }: SearchRulesQuery) => { + const countFacet = (facets || '').split(',').map((facet: keyof Rule) => { + const facetCount = countBy(this.rules.map(r => r[FACET_RULE_MAP[facet] || facet] as string)); + return { + property: facet, + values: Object.keys(facetCount).map(val => ({ val, count: facetCount[val] })) + }; + }); + const currentPs = ps || 10; + const currentP = p || 1; + const filteredRules = this.filterFacet({ languages }); + const responseRules = filteredRules.slice((currentP - 1) * currentPs, currentP * currentPs); + return this.reply({ + total: filteredRules.length, + p: currentP, + ps: currentPs, + rules: responseRules, + facets: countFacet + }); + }; + + handleBulkActivateRules = () => { + if (this.applyWithWarning) { + return this.reply({ + succeeded: this.rules.length - 1, + failed: 1, + errors: [{ msg: 'c rule c:S6069 cannot be activated on cpp profile SonarSource' }] + }); + } + return this.reply({ + succeeded: this.rules.length, + failed: 0, + errors: [] + }); + }; + + handleBulkDeactivateRules = () => { + return this.reply({ + succeeded: this.rules.length, + failed: 0 + }); + }; + + handleSearchQualityProfiles = ({ language }: SearchQualityProfilesParameters = {}): Promise< + SearchQualityProfilesResponse + > => { + let profiles: Profile[] = this.isAdmin + ? this.qualityProfile.map(p => ({ ...p, actions: { edit: true } })) + : this.qualityProfile; + if (language) { + profiles = profiles.filter(p => p.language === language); + } + return this.reply({ profiles }); + }; + + handleGetRulesApp = () => { + return this.reply({ canWrite: this.isAdmin, repositories: this.repositories }); + }; + + reply<T>(response: T): Promise<T> { + return Promise.resolve(cloneDeep(response)); + } +} diff --git a/server/sonar-web/src/main/js/api/rules.ts b/server/sonar-web/src/main/js/api/rules.ts index a2ada44f0a8..419e7ed7eef 100644 --- a/server/sonar-web/src/main/js/api/rules.ts +++ b/server/sonar-web/src/main/js/api/rules.ts @@ -20,40 +20,14 @@ import throwGlobalError from '../app/utils/throwGlobalError'; import { getJSON, post, postJSON } from '../helpers/request'; import { GetRulesAppResponse, SearchRulesResponse } from '../types/coding-rules'; +import { SearchRulesQuery } from '../types/rules'; import { RuleActivation, RuleDetails } from '../types/types'; export function getRulesApp(): Promise<GetRulesAppResponse> { return getJSON('/api/rules/app').catch(throwGlobalError); } -export function searchRules(data: { - activation?: boolean | string; - active_severities?: string; - asc?: boolean | string; - available_since?: string; - cwe?: string; - f?: string; - facets?: string; - include_external?: boolean | string; - inheritance?: string; - is_template?: boolean | string; - languages?: string; - owaspTop10?: string; - p?: number; - ps?: number; - q?: string; - qprofile?: string; - repositories?: string; - rule_key?: string; - s?: string; - sansTop25?: string; - severities?: string; - sonarsourceSecurity?: string; - statuses?: string; - tags?: string; - template_key?: string; - types?: string; -}): Promise<SearchRulesResponse> { +export function searchRules(data: SearchRulesQuery): Promise<SearchRulesResponse> { return getJSON('/api/rules/search', data).catch(throwGlobalError); } diff --git a/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts b/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts new file mode 100644 index 00000000000..a8de3c4f436 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts @@ -0,0 +1,197 @@ +/* + * 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 { screen, waitFor, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import CodingRulesMock from '../../../api/mocks/CodingRulesMock'; +import { mockLoggedInUser } from '../../../helpers/testMocks'; +import { renderApp } from '../../../helpers/testReactTestingUtils'; +import { CurrentUser } from '../../../types/users'; +import routes from '../routes'; + +jest.mock('../../../api/rules'); +jest.mock('../../../api/quality-profiles'); + +let handler: CodingRulesMock; + +beforeAll(() => { + window.scrollTo = jest.fn(); + handler = new CodingRulesMock(); +}); + +afterEach(() => handler.reset()); + +jest.setTimeout(10_000); + +it('should list all rules', async () => { + renderCodingRulesApp(); + + await waitFor(() => { + handler + .allRulesName() + .forEach(name => expect(screen.getByRole('link', { name })).toBeInTheDocument()); + }); +}); + +it('should have all type facet', async () => { + renderCodingRulesApp(); + + await waitFor(() => { + [ + 'issue.type.BUG', + 'issue.type.VULNERABILITY', + 'issue.type.CODE_SMELL', + 'issue.type.SECURITY_HOTSPOT' + ].forEach(name => expect(screen.getByRole('link', { name })).toBeInTheDocument()); + }); +}); + +it('select the correct quality profile for bulk change base on language search', async () => { + const user = userEvent.setup(); + handler.setIsAdmin(); + renderCodingRulesApp(mockLoggedInUser()); + const selectQP = handler.allQualityProfile('js')[0]; + + await user.click(await screen.findByRole('link', { name: 'JavaScript' })); + await user.click(await screen.findByRole('button', { name: 'bulk_change' })); + await user.click(await screen.findByRole('link', { name: 'coding_rules.activate_in…' })); + const dialog = screen.getByRole('dialog', { + name: 'coding_rules.activate_in_quality_profile (2 coding_rules._rules)' + }); + + expect(dialog).toBeInTheDocument(); + const dialogScreen = within(dialog); + expect(dialogScreen.getByText(`${selectQP.name} - ${selectQP.languageName}`)).toBeInTheDocument(); +}); + +it('no quality profile for bulk cahnge base on language search', async () => { + const user = userEvent.setup(); + handler.setIsAdmin(); + renderCodingRulesApp(mockLoggedInUser()); + + await user.click(await screen.findByRole('link', { name: 'C' })); + await user.click(await screen.findByRole('button', { name: 'bulk_change' })); + await user.click(await screen.findByRole('link', { name: 'coding_rules.activate_in…' })); + const dialog = screen.getByRole('dialog', { + name: 'coding_rules.activate_in_quality_profile (1 coding_rules._rules)' + }); + + expect(dialog).toBeInTheDocument(); + const dialogScreen = within(dialog); + await user.click(dialogScreen.getByRole('textbox', { name: 'coding_rules.activate_in' })); + expect(dialogScreen.getByText('coding_rules.bulk_change.no_quality_profile')).toBeInTheDocument(); +}); + +it('should be able to bulk activate quality profile', async () => { + const user = userEvent.setup(); + handler.setIsAdmin(); + renderCodingRulesApp(mockLoggedInUser()); + + const selectQPSuccess = handler.allQualityProfile('java')[0]; + const selectQPWarning = handler.allQualityProfile('java')[1]; + + await user.click(await screen.findByRole('button', { name: 'bulk_change' })); + await user.click(await screen.findByRole('link', { name: 'coding_rules.activate_in…' })); + + const dialog = screen.getByRole('dialog', { + name: 'coding_rules.activate_in_quality_profile (4 coding_rules._rules)' + }); + expect(dialog).toBeInTheDocument(); + + let dialogScreen = within(dialog); + await user.click(dialogScreen.getByRole('textbox', { name: 'coding_rules.activate_in' })); + await user.click( + dialogScreen.getByText(`${selectQPSuccess.name} - ${selectQPSuccess.languageName}`) + ); + expect( + dialogScreen.getByText(`${selectQPSuccess.name} - ${selectQPSuccess.languageName}`) + ).toBeInTheDocument(); + + await user.click(dialogScreen.getByRole('button', { name: 'apply' })); + expect( + dialogScreen.getByText( + `coding_rules.bulk_change.success.${selectQPSuccess.name}.${selectQPSuccess.languageName}.${ + handler.allRulesName().length + }` + ) + ).toBeInTheDocument(); + + await user.click(dialogScreen.getByRole('button', { name: 'close' })); + + // Try bulk change when quality profile has warnning. + handler.activateWithWarning(); + + await user.click(await screen.findByRole('button', { name: 'bulk_change' })); + await user.click(await screen.findByRole('link', { name: 'coding_rules.activate_in…' })); + dialogScreen = within( + screen.getByRole('dialog', { + name: 'coding_rules.activate_in_quality_profile (4 coding_rules._rules)' + }) + ); + await user.click(dialogScreen.getByRole('textbox', { name: 'coding_rules.activate_in' })); + await user.click( + dialogScreen.getByText(`${selectQPWarning.name} - ${selectQPWarning.languageName}`) + ); + await user.click(dialogScreen.getByRole('button', { name: 'apply' })); + expect( + dialogScreen.getByText( + `coding_rules.bulk_change.warning.${selectQPWarning.name}.${ + selectQPWarning.languageName + }.${handler.allRulesName().length - 1}.1` + ) + ).toBeInTheDocument(); +}); + +it('should be able to bulk deactivate quality profile', async () => { + const user = userEvent.setup(); + handler.setIsAdmin(); + renderCodingRulesApp(mockLoggedInUser()); + + const selectQP = handler.allQualityProfile('java')[0]; + + await user.click(await screen.findByRole('button', { name: 'bulk_change' })); + await user.click(await screen.findByRole('link', { name: 'coding_rules.deactivate_in…' })); + const dialogScreen = within( + screen.getByRole('dialog', { + name: 'coding_rules.deactivate_in_quality_profile (4 coding_rules._rules)' + }) + ); + await user.click(dialogScreen.getByRole('textbox', { name: 'coding_rules.deactivate_in' })); + + await user.click(dialogScreen.getByText(`${selectQP.name} - ${selectQP.languageName}`)); + await user.click(dialogScreen.getByRole('button', { name: 'apply' })); + expect( + dialogScreen.getByText( + `coding_rules.bulk_change.success.${selectQP.name}.${selectQP.languageName}.${ + handler.allRulesName().length + }` + ) + ).toBeInTheDocument(); +}); + +function renderCodingRulesApp(currentUser?: CurrentUser) { + renderApp('coding_rules', routes, { + currentUser, + languages: { + js: { key: 'js', name: 'JavaScript' }, + java: { key: 'java', name: 'Java' }, + c: { key: 'c', name: 'C' } + } + }); +} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/ActivationFormModal.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/ActivationFormModal.tsx index 76f8c2f22e9..97a0c93be8e 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/ActivationFormModal.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/ActivationFormModal.tsx @@ -19,10 +19,11 @@ */ import classNames from 'classnames'; import * as React from 'react'; +import { components, OptionProps, OptionTypeBase, SingleValueProps } from 'react-select'; import { activateRule, Profile } from '../../../api/quality-profiles'; import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons'; import Modal from '../../../components/controls/Modal'; -import SelectLegacy from '../../../components/controls/SelectLegacy'; +import Select from '../../../components/controls/Select'; import SeverityHelper from '../../../components/shared/SeverityHelper'; import { Alert } from '../../../components/ui/Alert'; import { SEVERITIES } from '../../../helpers/constants'; @@ -40,9 +41,13 @@ interface Props { rule: Rule | RuleDetails; } +interface ProfileWithDeph extends Profile { + depth: number; +} + interface State { params: Dict<string>; - profile: string; + profile?: ProfileWithDeph; severity: string; submitting: boolean; } @@ -55,7 +60,7 @@ export default class ActivationFormModal extends React.PureComponent<Props, Stat const profilesWithDepth = this.getQualityProfilesWithDepth(props); this.state = { params: this.getParams(props), - profile: profilesWithDepth.length > 0 ? profilesWithDepth[0].key : '', + profile: profilesWithDepth.length > 0 ? profilesWithDepth[0] : undefined, severity: props.activation ? props.activation.severity : props.rule.severity, submitting: false }; @@ -105,7 +110,7 @@ export default class ActivationFormModal extends React.PureComponent<Props, Stat event.preventDefault(); this.setState({ submitting: true }); const data = { - key: this.state.profile, + key: this.state.profile?.key || '', params: this.state.params, rule: this.props.rule.key, severity: this.state.severity @@ -132,18 +137,14 @@ export default class ActivationFormModal extends React.PureComponent<Props, Stat this.setState((state: State) => ({ params: { ...state.params, [name]: value } })); }; - handleProfileChange = ({ value }: { value: string }) => { - this.setState({ profile: value }); + handleProfileChange = (profile: ProfileWithDeph) => { + this.setState({ profile }); }; - handleSeverityChange = ({ value }: { value: string }) => { + handleSeverityChange = ({ value }: OptionTypeBase) => { this.setState({ severity: value }); }; - renderSeverityOption = ({ value }: { value: string }) => { - return <SeverityHelper severity={value} />; - }; - render() { const { activation, rule } = this.props; const { profile, severity, submitting } = this.state; @@ -152,6 +153,26 @@ export default class ActivationFormModal extends React.PureComponent<Props, Stat const isCustomRule = !!(rule as RuleDetails).templateKey; const activeInAllProfiles = profilesWithDepth.length <= 0; const isUpdateMode = !!activation; + const serverityOption = SEVERITIES.map(severity => ({ + label: translate('severity', severity), + value: severity + })); + + function Option(props: OptionProps<OptionTypeBase, false>) { + return ( + <components.Option {...props}> + <SeverityHelper severity={props.data.value} /> + </components.Option> + ); + } + + function SingleValue(props: SingleValueProps<OptionTypeBase>) { + return ( + <components.SingleValue {...props}> + <SeverityHelper className="coding-rules-severity-value" severity={props.data.value} /> + </components.SingleValue> + ); + } return ( <Modal contentLabel={this.props.modalHeader} onRequestClose={this.props.onClose} size="small"> @@ -166,34 +187,32 @@ export default class ActivationFormModal extends React.PureComponent<Props, Stat )} <div className="modal-field"> - <label>{translate('coding_rules.quality_profile')}</label> - <SelectLegacy + <label id="coding-rules-quality-profile-select"> + {translate('coding_rules.quality_profile')} + </label> + <Select className="js-profile" - clearable={false} - disabled={submitting || profilesWithDepth.length === 1} + aria-labelledby="coding-rules-quality-profile-select" + isClearable={false} + isDisabled={submitting || profilesWithDepth.length === 1} onChange={this.handleProfileChange} - options={profilesWithDepth.map(profile => ({ - label: ' '.repeat(profile.depth) + profile.name, - value: profile.key - }))} + getOptionLabel={p => ' '.repeat(p.depth) + p.name} + options={profilesWithDepth} value={profile} /> </div> <div className="modal-field"> - <label>{translate('severity')}</label> - <SelectLegacy + <label id="coding-rules-severity-select">{translate('severity')}</label> + <Select className="js-severity" - clearable={false} - disabled={submitting} + isClearable={false} + isDisabled={submitting} + aria-labelledby="coding-rules-severity-select" onChange={this.handleSeverityChange} - optionRenderer={this.renderSeverityOption} - options={SEVERITIES.map(severity => ({ - label: translate('severity', severity), - value: severity - }))} - searchable={false} - value={severity} - valueRenderer={this.renderSeverityOption} + components={{ Option, SingleValue }} + options={serverityOption} + isSearchable={false} + value={serverityOption.find(s => s.value === severity)} /> </div> {isCustomRule ? ( diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/BulkChangeModal.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/BulkChangeModal.tsx index 32c3b9b691d..9221083055e 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/BulkChangeModal.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/BulkChangeModal.tsx @@ -150,9 +150,7 @@ export class BulkChangeModal extends React.PureComponent<Props, State> { renderResult = (result: ActivationResult) => { const { profile: profileKey } = result; const profile = this.props.referencedProfiles[profileKey]; - if (!profile) { - return null; - } + const { languages } = this.props; const language = languages[profile.language] ? languages[profile.language].name diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetails.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetails.tsx index 64987cf37ce..96f59bce97d 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetails.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetails.tsx @@ -38,7 +38,6 @@ import RuleDetailsProfiles from './RuleDetailsProfiles'; interface Props { allowCustomRules?: boolean; canWrite?: boolean; - hideQualityProfiles?: boolean; onActivate: (profile: string, rule: string, activation: Activation) => void; onDeactivate: (profile: string, rule: string) => void; onDelete: (rule: string) => void; @@ -156,7 +155,7 @@ export default class RuleDetails extends React.PureComponent<Props, State> { return <div className="coding-rule-details" />; } - const { allowCustomRules, canWrite, hideQualityProfiles, referencedProfiles } = this.props; + const { allowCustomRules, canWrite, referencedProfiles } = this.props; const { params = [] } = ruleDetails; const isCustom = !!ruleDetails.templateKey; @@ -236,7 +235,7 @@ export default class RuleDetails extends React.PureComponent<Props, State> { /> )} - {!ruleDetails.isTemplate && !hideQualityProfiles && ( + {!ruleDetails.isTemplate && ( <RuleDetailsProfiles activations={this.state.actives} canWrite={canWrite} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/ActivationFormModal-test.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/ActivationFormModal-test.tsx index f19aaa1e16c..b4138caae36 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/ActivationFormModal-test.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/ActivationFormModal-test.tsx @@ -19,6 +19,7 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; +import { activateRule } from '../../../../api/quality-profiles'; import { mockQualityProfile, mockRule, @@ -28,6 +29,10 @@ import { } from '../../../../helpers/testMocks'; import ActivationFormModal from '../ActivationFormModal'; +jest.mock('../../../../api/quality-profiles', () => ({ + activateRule: jest.fn().mockResolvedValueOnce({}) +})); + it('should render correctly', () => { expect(shallowRender()).toMatchSnapshot('default'); expect( @@ -47,6 +52,29 @@ it('should render correctly', () => { expect(wrapper).toMatchSnapshot('submitting'); }); +it('should activate rule on quality profile when submit', () => { + const wrapper = shallowRender(); + wrapper.instance().handleFormSubmit(({ + preventDefault: jest.fn() + } as any) as React.SyntheticEvent<HTMLFormElement>); + expect(activateRule).toHaveBeenCalledWith({ + key: '', + params: { + '1': '1', + '2': '1' + }, + rule: 'javascript:S1067', + severity: 'MAJOR' + }); +}); + +it('should handle profile change correctly', () => { + const wrapper = shallowRender(); + const qualityProfile = mockQualityProfile(); + wrapper.instance().handleProfileChange(qualityProfile); + expect(wrapper.state().profile).toBe(qualityProfile); +}); + function shallowRender(props: Partial<ActivationFormModal['props']> = {}) { return shallow<ActivationFormModal>( <ActivationFormModal diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/BulkChangeModal-test.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/BulkChangeModal-test.tsx deleted file mode 100644 index 893553a0d52..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/BulkChangeModal-test.tsx +++ /dev/null @@ -1,123 +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 { bulkActivateRules, bulkDeactivateRules } from '../../../../api/quality-profiles'; -import { mockLanguage, mockQualityProfile } from '../../../../helpers/testMocks'; -import { submit, waitAndUpdate } from '../../../../helpers/testUtils'; -import { Query } from '../../query'; -import { BulkChangeModal } from '../BulkChangeModal'; - -jest.mock('../../../../api/quality-profiles', () => ({ - bulkActivateRules: jest.fn().mockResolvedValue({ failed: 0, succeeded: 2 }), - bulkDeactivateRules: jest.fn().mockResolvedValue({ failed: 2, succeeded: 0 }) -})); - -beforeEach(jest.clearAllMocks); - -it('should render correctly', () => { - expect(shallowRender()).toMatchSnapshot('default'); - expect(shallowRender({ profile: undefined })).toMatchSnapshot('no profile pre-selected'); - expect(shallowRender({ action: 'deactivate' })).toMatchSnapshot('deactivate action'); - expect( - shallowRender().setState({ - results: [ - { failed: 2, profile: 'foo', succeeded: 0 }, - { failed: 0, profile: 'bar', succeeded: 2 } - ] - }) - ).toMatchSnapshot('results'); - expect(shallowRender().setState({ submitting: true })).toMatchSnapshot('submitting'); - expect(shallowRender().setState({ finished: true })).toMatchSnapshot('finished'); -}); - -it('should pre-select a profile if only 1 is available', () => { - const profile = mockQualityProfile({ - actions: { edit: true }, - isBuiltIn: false, - key: 'foo', - language: 'js' - }); - const wrapper = shallowRender({ profile: undefined, referencedProfiles: { foo: profile } }); - expect(wrapper.state().selectedProfiles).toEqual([profile]); -}); - -it('should handle profile selection', () => { - const wrapper = shallowRender(); - const profiles = [mockQualityProfile({ name: 'foo' }), mockQualityProfile({ name: 'bar' })]; - wrapper.instance().handleProfileSelect(profiles); - expect(wrapper.state().selectedProfiles).toEqual(profiles); -}); - -it('should handle form submission', async () => { - const wrapper = shallowRender({ profile: undefined }); - const profiles = [ - mockQualityProfile({ name: 'foo', key: 'foo' }), - mockQualityProfile({ name: 'bar', key: 'bar' }) - ]; - wrapper.setState({ selectedProfiles: profiles }); - - // Activate. - submit(wrapper.find('form')); - await waitAndUpdate(wrapper); - expect(bulkActivateRules).toBeCalledWith(expect.objectContaining({ targetKey: 'foo' })); - - await waitAndUpdate(wrapper); - expect(bulkActivateRules).toBeCalledWith(expect.objectContaining({ targetKey: 'bar' })); - - await waitAndUpdate(wrapper); - expect(wrapper.state().results).toEqual([ - { failed: 0, profile: 'foo', succeeded: 2 }, - { failed: 0, profile: 'bar', succeeded: 2 } - ]); - - // Deactivate. - wrapper.setProps({ action: 'deactivate' }).setState({ results: [] }); - submit(wrapper.find('form')); - await waitAndUpdate(wrapper); - expect(bulkDeactivateRules).toBeCalledWith(expect.objectContaining({ targetKey: 'foo' })); - - await waitAndUpdate(wrapper); - expect(bulkDeactivateRules).toBeCalledWith(expect.objectContaining({ targetKey: 'bar' })); - - await waitAndUpdate(wrapper); - expect(wrapper.state().results).toEqual([ - { failed: 2, profile: 'foo', succeeded: 0 }, - { failed: 2, profile: 'bar', succeeded: 0 } - ]); -}); - -function shallowRender(props: Partial<BulkChangeModal['props']> = {}) { - return shallow<BulkChangeModal>( - <BulkChangeModal - action="activate" - languages={{ js: mockLanguage() }} - onClose={jest.fn()} - profile={mockQualityProfile()} - query={{ languages: ['js'] } as Query} - referencedProfiles={{ - foo: mockQualityProfile({ key: 'foo' }), - bar: mockQualityProfile({ key: 'bar' }) - }} - total={42} - {...props} - /> - ); -} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/ActivationFormModal-test.tsx.snap b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/ActivationFormModal-test.tsx.snap index 772f62dee6b..ccfb4ebf108 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/ActivationFormModal-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/ActivationFormModal-test.tsx.snap @@ -27,30 +27,42 @@ exports[`should render correctly: custom rule 1`] = ` <div className="modal-field" > - <label> + <label + id="coding-rules-quality-profile-select" + > coding_rules.quality_profile </label> - <SelectLegacy + <Select + aria-labelledby="coding-rules-quality-profile-select" className="js-profile" - clearable={false} - disabled={false} + getOptionLabel={[Function]} + isClearable={false} + isDisabled={false} onChange={[Function]} options={Array []} - value="" /> </div> <div className="modal-field" > - <label> + <label + id="coding-rules-severity-select" + > severity </label> - <SelectLegacy + <Select + aria-labelledby="coding-rules-severity-select" className="js-severity" - clearable={false} - disabled={false} + components={ + Object { + "Option": [Function], + "SingleValue": [Function], + } + } + isClearable={false} + isDisabled={false} + isSearchable={false} onChange={[Function]} - optionRenderer={[Function]} options={ Array [ Object { @@ -75,9 +87,12 @@ exports[`should render correctly: custom rule 1`] = ` }, ] } - searchable={false} - value="MAJOR" - valueRenderer={[Function]} + value={ + Object { + "label": "severity.MAJOR", + "value": "MAJOR", + } + } /> </div> <div @@ -136,30 +151,42 @@ exports[`should render correctly: default 1`] = ` <div className="modal-field" > - <label> + <label + id="coding-rules-quality-profile-select" + > coding_rules.quality_profile </label> - <SelectLegacy + <Select + aria-labelledby="coding-rules-quality-profile-select" className="js-profile" - clearable={false} - disabled={false} + getOptionLabel={[Function]} + isClearable={false} + isDisabled={false} onChange={[Function]} options={Array []} - value="" /> </div> <div className="modal-field" > - <label> + <label + id="coding-rules-severity-select" + > severity </label> - <SelectLegacy + <Select + aria-labelledby="coding-rules-severity-select" className="js-severity" - clearable={false} - disabled={false} + components={ + Object { + "Option": [Function], + "SingleValue": [Function], + } + } + isClearable={false} + isDisabled={false} + isSearchable={false} onChange={[Function]} - optionRenderer={[Function]} options={ Array [ Object { @@ -184,9 +211,12 @@ exports[`should render correctly: default 1`] = ` }, ] } - searchable={false} - value="MAJOR" - valueRenderer={[Function]} + value={ + Object { + "label": "severity.MAJOR", + "value": "MAJOR", + } + } /> </div> <div @@ -280,30 +310,42 @@ exports[`should render correctly: submitting 1`] = ` <div className="modal-field" > - <label> + <label + id="coding-rules-quality-profile-select" + > coding_rules.quality_profile </label> - <SelectLegacy + <Select + aria-labelledby="coding-rules-quality-profile-select" className="js-profile" - clearable={false} - disabled={true} + getOptionLabel={[Function]} + isClearable={false} + isDisabled={true} onChange={[Function]} options={Array []} - value="" /> </div> <div className="modal-field" > - <label> + <label + id="coding-rules-severity-select" + > severity </label> - <SelectLegacy + <Select + aria-labelledby="coding-rules-severity-select" className="js-severity" - clearable={false} - disabled={true} + components={ + Object { + "Option": [Function], + "SingleValue": [Function], + } + } + isClearable={false} + isDisabled={true} + isSearchable={false} onChange={[Function]} - optionRenderer={[Function]} options={ Array [ Object { @@ -328,9 +370,12 @@ exports[`should render correctly: submitting 1`] = ` }, ] } - searchable={false} - value="MAJOR" - valueRenderer={[Function]} + value={ + Object { + "label": "severity.MAJOR", + "value": "MAJOR", + } + } /> </div> <div @@ -422,30 +467,42 @@ exports[`should render correctly: update mode 1`] = ` <div className="modal-field" > - <label> + <label + id="coding-rules-quality-profile-select" + > coding_rules.quality_profile </label> - <SelectLegacy + <Select + aria-labelledby="coding-rules-quality-profile-select" className="js-profile" - clearable={false} - disabled={false} + getOptionLabel={[Function]} + isClearable={false} + isDisabled={false} onChange={[Function]} options={Array []} - value="" /> </div> <div className="modal-field" > - <label> + <label + id="coding-rules-severity-select" + > severity </label> - <SelectLegacy + <Select + aria-labelledby="coding-rules-severity-select" className="js-severity" - clearable={false} - disabled={false} + components={ + Object { + "Option": [Function], + "SingleValue": [Function], + } + } + isClearable={false} + isDisabled={false} + isSearchable={false} onChange={[Function]} - optionRenderer={[Function]} options={ Array [ Object { @@ -470,9 +527,12 @@ exports[`should render correctly: update mode 1`] = ` }, ] } - searchable={false} - value="MAJOR" - valueRenderer={[Function]} + value={ + Object { + "label": "severity.MAJOR", + "value": "MAJOR", + } + } /> </div> <div @@ -561,37 +621,81 @@ exports[`should render correctly: with deep profiles 1`] = ` <div className="modal-field" > - <label> + <label + id="coding-rules-quality-profile-select" + > coding_rules.quality_profile </label> - <SelectLegacy + <Select + aria-labelledby="coding-rules-quality-profile-select" className="js-profile" - clearable={false} - disabled={true} + getOptionLabel={[Function]} + isClearable={false} + isDisabled={true} onChange={[Function]} options={ Array [ Object { - "label": "name", - "value": "key", + "actions": Object { + "edit": true, + }, + "activeDeprecatedRuleCount": 2, + "activeRuleCount": 10, + "childrenCount": 0, + "depth": 0, + "isBuiltIn": false, + "isDefault": false, + "isInherited": false, + "key": "key", + "language": "js", + "languageName": "JavaScript", + "name": "name", + "projectCount": 3, }, ] } - value="key" + value={ + Object { + "actions": Object { + "edit": true, + }, + "activeDeprecatedRuleCount": 2, + "activeRuleCount": 10, + "childrenCount": 0, + "depth": 0, + "isBuiltIn": false, + "isDefault": false, + "isInherited": false, + "key": "key", + "language": "js", + "languageName": "JavaScript", + "name": "name", + "projectCount": 3, + } + } /> </div> <div className="modal-field" > - <label> + <label + id="coding-rules-severity-select" + > severity </label> - <SelectLegacy + <Select + aria-labelledby="coding-rules-severity-select" className="js-severity" - clearable={false} - disabled={false} + components={ + Object { + "Option": [Function], + "SingleValue": [Function], + } + } + isClearable={false} + isDisabled={false} + isSearchable={false} onChange={[Function]} - optionRenderer={[Function]} options={ Array [ Object { @@ -616,9 +720,12 @@ exports[`should render correctly: with deep profiles 1`] = ` }, ] } - searchable={false} - value="MAJOR" - valueRenderer={[Function]} + value={ + Object { + "label": "severity.MAJOR", + "value": "MAJOR", + } + } /> </div> <div diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/BulkChangeModal-test.tsx.snap b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/BulkChangeModal-test.tsx.snap deleted file mode 100644 index de47bfb5ebe..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/BulkChangeModal-test.tsx.snap +++ /dev/null @@ -1,313 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly: deactivate action 1`] = ` -<Modal - contentLabel="coding_rules.deactivate_in_quality_profile (42 coding_rules._rules)" - onRequestClose={[MockFunction]} - size="small" -> - <form - onSubmit={[Function]} - > - <header - className="modal-head" - > - <h2> - coding_rules.deactivate_in_quality_profile (42 coding_rules._rules) - </h2> - </header> - <div - className="modal-body" - > - <div - className="modal-field" - > - <h3> - <label - id="coding-rules-bulk-change-profile" - > - coding_rules.deactivate_in - </label> - </h3> - <span> - name - — - are_you_sure - </span> - </div> - </div> - <footer - className="modal-foot" - > - <SubmitButton - disabled={false} - id="coding-rules-submit-bulk-change" - > - apply - </SubmitButton> - <ResetButtonLink - onClick={[MockFunction]} - > - cancel - </ResetButtonLink> - </footer> - </form> -</Modal> -`; - -exports[`should render correctly: default 1`] = ` -<Modal - contentLabel="coding_rules.activate_in_quality_profile (42 coding_rules._rules)" - onRequestClose={[MockFunction]} - size="small" -> - <form - onSubmit={[Function]} - > - <header - className="modal-head" - > - <h2> - coding_rules.activate_in_quality_profile (42 coding_rules._rules) - </h2> - </header> - <div - className="modal-body" - > - <div - className="modal-field" - > - <h3> - <label - id="coding-rules-bulk-change-profile" - > - coding_rules.activate_in - </label> - </h3> - <span> - name - — - are_you_sure - </span> - </div> - </div> - <footer - className="modal-foot" - > - <SubmitButton - disabled={false} - id="coding-rules-submit-bulk-change" - > - apply - </SubmitButton> - <ResetButtonLink - onClick={[MockFunction]} - > - cancel - </ResetButtonLink> - </footer> - </form> -</Modal> -`; - -exports[`should render correctly: finished 1`] = ` -<Modal - contentLabel="coding_rules.activate_in_quality_profile (42 coding_rules._rules)" - onRequestClose={[MockFunction]} - size="small" -> - <form - onSubmit={[Function]} - > - <header - className="modal-head" - > - <h2> - coding_rules.activate_in_quality_profile (42 coding_rules._rules) - </h2> - </header> - <div - className="modal-body" - /> - <footer - className="modal-foot" - > - <ResetButtonLink - onClick={[MockFunction]} - > - close - </ResetButtonLink> - </footer> - </form> -</Modal> -`; - -exports[`should render correctly: no profile pre-selected 1`] = ` -<Modal - contentLabel="coding_rules.activate_in_quality_profile (42 coding_rules._rules)" - onRequestClose={[MockFunction]} - size="small" -> - <form - onSubmit={[Function]} - > - <header - className="modal-head" - > - <h2> - coding_rules.activate_in_quality_profile (42 coding_rules._rules) - </h2> - </header> - <div - className="modal-body" - > - <div - className="modal-field" - > - <h3> - <label - id="coding-rules-bulk-change-profile" - > - coding_rules.activate_in - </label> - </h3> - <Select - aria-labelledby="coding-rules-bulk-change-profile" - getOptionLabel={[Function]} - getOptionValue={[Function]} - isClearable={false} - isMulti={true} - isSearchable={true} - noOptionsMessage={[Function]} - onChange={[Function]} - options={Array []} - value={Array []} - /> - </div> - </div> - <footer - className="modal-foot" - > - <SubmitButton - disabled={false} - id="coding-rules-submit-bulk-change" - > - apply - </SubmitButton> - <ResetButtonLink - onClick={[MockFunction]} - > - cancel - </ResetButtonLink> - </footer> - </form> -</Modal> -`; - -exports[`should render correctly: results 1`] = ` -<Modal - contentLabel="coding_rules.activate_in_quality_profile (42 coding_rules._rules)" - onRequestClose={[MockFunction]} - size="small" -> - <form - onSubmit={[Function]} - > - <header - className="modal-head" - > - <h2> - coding_rules.activate_in_quality_profile (42 coding_rules._rules) - </h2> - </header> - <div - className="modal-body" - > - <Alert - key="foo" - variant="warning" - > - coding_rules.bulk_change.warning.name.CSS.0.2 - </Alert> - <Alert - key="bar" - variant="success" - > - coding_rules.bulk_change.success.name.CSS.2 - </Alert> - <div - className="modal-field" - > - <h3> - <label - id="coding-rules-bulk-change-profile" - > - coding_rules.activate_in - </label> - </h3> - <span> - name - — - are_you_sure - </span> - </div> - </div> - <footer - className="modal-foot" - > - <SubmitButton - disabled={false} - id="coding-rules-submit-bulk-change" - > - apply - </SubmitButton> - <ResetButtonLink - onClick={[MockFunction]} - > - cancel - </ResetButtonLink> - </footer> - </form> -</Modal> -`; - -exports[`should render correctly: submitting 1`] = ` -<Modal - contentLabel="coding_rules.activate_in_quality_profile (42 coding_rules._rules)" - onRequestClose={[MockFunction]} - size="small" -> - <form - onSubmit={[Function]} - > - <header - className="modal-head" - > - <h2> - coding_rules.activate_in_quality_profile (42 coding_rules._rules) - </h2> - </header> - <div - className="modal-body" - /> - <footer - className="modal-foot" - > - <i - className="spinner spacer-right" - /> - <SubmitButton - disabled={true} - id="coding-rules-submit-bulk-change" - > - apply - </SubmitButton> - <ResetButtonLink - onClick={[MockFunction]} - > - cancel - </ResetButtonLink> - </footer> - </form> -</Modal> -`; diff --git a/server/sonar-web/src/main/js/apps/coding-rules/styles.css b/server/sonar-web/src/main/js/apps/coding-rules/styles.css index f3d5897218d..d8f26b52468 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/styles.css +++ b/server/sonar-web/src/main/js/apps/coding-rules/styles.css @@ -101,7 +101,8 @@ line-height: 1; } -.coding-rules-details-tag-edit-cancel { +.coding-rules-details-tag-edit-cancel, +.coding-rules-severity-value svg { vertical-align: middle; } diff --git a/server/sonar-web/src/main/js/helpers/testMocks.ts b/server/sonar-web/src/main/js/helpers/testMocks.ts index 7269073b760..ff43df271a4 100644 --- a/server/sonar-web/src/main/js/helpers/testMocks.ts +++ b/server/sonar-web/src/main/js/helpers/testMocks.ts @@ -23,6 +23,7 @@ import { createStore, Store } from 'redux'; import { DocumentationEntry } from '../apps/documentation/utils'; import { Exporter, Profile } from '../apps/quality-profiles/types'; import { AppState } from '../types/appstate'; +import { RuleRepository } from '../types/coding-rules'; import { EditionKey } from '../types/editions'; import { RawIssue } from '../types/issues'; import { Language } from '../types/languages'; @@ -832,8 +833,6 @@ export function mockDumpStatus(props: Partial<DumpStatus> = {}): DumpStatus { }; } -export function mockRuleRepository( - override: Partial<{ key: string; language: string; name: string }> = {} -) { +export function mockRuleRepository(override: Partial<RuleRepository> = {}) { return { key: 'css', language: 'css', name: 'SonarQube', ...override }; } diff --git a/server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx b/server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx index 92025a107c2..68c07d46158 100644 --- a/server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx +++ b/server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx @@ -27,12 +27,13 @@ import { createMemoryHistory, Route, RouteComponent, RouteConfig, Router } from import { Store } from 'redux'; import AppStateContextProvider from '../app/components/app-state/AppStateContextProvider'; import CurrentUserContextProvider from '../app/components/current-user/CurrentUserContextProvider'; +import { LanguagesContext } from '../app/components/languages/LanguagesContext'; import { MetricsContext } from '../app/components/metrics/MetricsContext'; import getStore from '../app/utils/getStore'; import { RouteWithChildRoutes } from '../app/utils/startReactApp'; import { Store as State } from '../store/rootReducer'; import { AppState } from '../types/appstate'; -import { Dict, Metric } from '../types/types'; +import { Dict, Languages, Metric } from '../types/types'; import { CurrentUser } from '../types/users'; import { DEFAULT_METRICS } from './mocks/metrics'; import { mockAppState, mockCurrentUser } from './testMocks'; @@ -42,6 +43,7 @@ interface RenderContext { store?: Store<State, any>; history?: History; appState?: AppState; + languages?: Languages; currentUser?: CurrentUser; } @@ -56,7 +58,7 @@ export function renderComponentApp( export function renderApp( indexPath: string, routes: RouteConfig, - context: RenderContext + context?: RenderContext ): RenderResult { return renderRoutedApp( <RouteWithChildRoutes path={indexPath} childRoutes={routes} />, @@ -73,7 +75,8 @@ function renderRoutedApp( metrics = DEFAULT_METRICS, store = getStore(), appState = mockAppState(), - history = createMemoryHistory() + history = createMemoryHistory(), + languages = {} }: RenderContext = {} ): RenderResult { history.push(`/${indexPath}`); @@ -82,11 +85,13 @@ function renderRoutedApp( <IntlProvider defaultLocale="en" locale="en"> <MetricsContext.Provider value={metrics}> <Provider store={store}> - <CurrentUserContextProvider currentUser={currentUser}> - <AppStateContextProvider appState={appState}> - <Router history={history}>{children}</Router> - </AppStateContextProvider> - </CurrentUserContextProvider> + <LanguagesContext.Provider value={languages}> + <CurrentUserContextProvider currentUser={currentUser}> + <AppStateContextProvider appState={appState}> + <Router history={history}>{children}</Router> + </AppStateContextProvider> + </CurrentUserContextProvider> + </LanguagesContext.Provider> </Provider> </MetricsContext.Provider> </IntlProvider> diff --git a/server/sonar-web/src/main/js/types/coding-rules.ts b/server/sonar-web/src/main/js/types/coding-rules.ts index b902375855b..0c2a761ec1b 100644 --- a/server/sonar-web/src/main/js/types/coding-rules.ts +++ b/server/sonar-web/src/main/js/types/coding-rules.ts @@ -19,9 +19,15 @@ */ import { Dict, Rule, RuleActivation } from './types'; +export interface RuleRepository { + key: string; + language: string; + name: string; +} + export interface GetRulesAppResponse { canWrite?: boolean; - repositories: { key: string; language: string; name: string }[]; + repositories: RuleRepository[]; } export interface SearchRulesResponse { diff --git a/server/sonar-web/src/main/js/types/rules.ts b/server/sonar-web/src/main/js/types/rules.ts index 1c9d9810ee5..7d8a9a6f4f3 100644 --- a/server/sonar-web/src/main/js/types/rules.ts +++ b/server/sonar-web/src/main/js/types/rules.ts @@ -23,3 +23,32 @@ export enum RuleStatus { Deprecated = 'DEPRECATED', Removed = 'REMOVED' } + +export interface SearchRulesQuery { + activation?: boolean | string; + active_severities?: string; + asc?: boolean | string; + available_since?: string; + cwe?: string; + f?: string; + facets?: string; + include_external?: boolean | string; + inheritance?: string; + is_template?: boolean | string; + languages?: string; + owaspTop10?: string; + p?: number; + ps?: number; + q?: string; + qprofile?: string; + repositories?: string; + rule_key?: string; + s?: string; + sansTop25?: string; + severities?: string; + sonarsourceSecurity?: string; + statuses?: string; + tags?: string; + template_key?: string; + types?: string; +} |