From: Mathieu Suen Date: Thu, 5 May 2022 14:55:41 +0000 (+0200) Subject: SONAR-16365 Concatenate description section on rules page X-Git-Tag: 9.5.0.56709~155 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=b48c885961c8b9be1f341913f513236b95759ff5;p=sonarqube.git SONAR-16365 Concatenate description section on rules page --- diff --git a/server/sonar-web/src/main/js/api/mocks/CodingRulesMock.ts b/server/sonar-web/src/main/js/api/mocks/CodingRulesMock.ts index 7b3d6d2d070..30934b14f66 100644 --- a/server/sonar-web/src/main/js/api/mocks/CodingRulesMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/CodingRulesMock.ts @@ -17,12 +17,18 @@ * 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, pick } from 'lodash'; +import { cloneDeep, countBy, pick, trim } from 'lodash'; import { mockQualityProfile, mockRuleDetails, mockRuleRepository } from '../../helpers/testMocks'; import { RuleRepository } from '../../types/coding-rules'; import { RawIssuesResponse } from '../../types/issues'; import { SearchRulesQuery } from '../../types/rules'; -import { Rule, RuleActivation, RuleDetails } from '../../types/types'; +import { + Rule, + RuleActivation, + RuleDescriptionSections, + RuleDetails, + RulesUpdateRequest +} from '../../types/types'; import { getFacet } from '../issues'; import { bulkActivateRules, @@ -32,7 +38,7 @@ import { SearchQualityProfilesParameters, SearchQualityProfilesResponse } from '../quality-profiles'; -import { getRuleDetails, getRulesApp, searchRules } from '../rules'; +import { getRuleDetails, getRulesApp, searchRules, updateRule } from '../rules'; interface FacetFilter { languages?: string; @@ -83,9 +89,33 @@ export default class CodingRulesMock { lang: 'c', langName: 'C', name: 'Awsome C rule' + }), + mockRuleDetails({ + key: 'rule5', + type: 'VULNERABILITY', + lang: 'py', + langName: 'Python', + name: 'Awsome Python rule', + descriptionSections: [ + { key: RuleDescriptionSections.INTRODUCTION, content: 'Introduction to this rule' }, + { key: RuleDescriptionSections.HOW_TO_FIX, content: 'This how to fix' }, + { + key: RuleDescriptionSections.RESOURCES, + content: 'Some link Awsome Reading' + } + ] + }), + mockRuleDetails({ + key: 'rule6', + type: 'VULNERABILITY', + lang: 'py', + langName: 'Python', + name: 'Bad Python rule', + descriptionSections: undefined }) ]; + (updateRule as jest.Mock).mockImplementation(this.handleUpdateRule); (searchRules as jest.Mock).mockImplementation(this.handleSearchRules); (getRuleDetails as jest.Mock).mockImplementation(this.handleGetRuleDetails); (searchQualityProfiles as jest.Mock).mockImplementation(this.handleSearchQualityProfiles); @@ -137,6 +167,10 @@ export default class CodingRulesMock { this.rules = cloneDeep(this.defaultRules); } + allRulesCount() { + return this.rules.length; + } + allRulesName() { return this.rules.map(r => r.name); } @@ -175,6 +209,49 @@ export default class CodingRulesMock { return this.reply({ actives: parameters.actives ? [] : undefined, rule }); }; + handleUpdateRule = (data: RulesUpdateRequest): Promise => { + const rule = this.rules.find(r => r.key === data.key); + if (rule === undefined) { + return Promise.reject({ + errors: [{ msg: `No rule has been found for id ${data.key}` }] + }); + } + const template = this.rules.find(r => r.key === rule.templateKey); + + // Lets not convert the md to html in test. + rule.mdDesc = data.markdown_description !== undefined ? data.markdown_description : rule.mdDesc; + rule.htmlDesc = + data.markdown_description !== undefined ? data.markdown_description : rule.htmlDesc; + rule.mdNote = data.markdown_note !== undefined ? data.markdown_note : rule.mdNote; + rule.htmlNote = data.markdown_note !== undefined ? data.markdown_note : rule.htmlNote; + rule.name = data.name !== undefined ? data.name : rule.name; + if (template && data.params) { + rule.params = []; + data.params.split(';').forEach(param => { + const parts = param.split('='); + const paramsDef = template.params?.find(p => p.key === parts[0]); + rule.params?.push({ + key: parts[0], + type: paramsDef?.type || 'STRING', + defaultValue: trim(parts[1], '" '), + htmlDesc: paramsDef?.htmlDesc + }); + }); + } + + rule.remFnBaseEffort = + data.remediation_fn_base_effort !== undefined + ? data.remediation_fn_base_effort + : rule.remFnBaseEffort; + rule.remFnType = + data.remediation_fn_type !== undefined ? data.remediation_fn_type : rule.remFnType; + rule.severity = data.severity !== undefined ? data.severity : rule.severity; + rule.status = data.status !== undefined ? data.status : rule.status; + rule.tags = data.tags !== undefined ? data.tags.split(';') : rule.tags; + + return this.reply(rule); + }; + handleSearchRules = ({ facets, languages, p, ps, rule_key }: SearchRulesQuery) => { const countFacet = (facets || '').split(',').map((facet: keyof Rule) => { const facetCount = countBy(this.rules.map(r => r[FACET_RULE_MAP[facet] || facet] as string)); diff --git a/server/sonar-web/src/main/js/api/rules.ts b/server/sonar-web/src/main/js/api/rules.ts index 0ab2ad21c44..583e684c200 100644 --- a/server/sonar-web/src/main/js/api/rules.ts +++ b/server/sonar-web/src/main/js/api/rules.ts @@ -21,7 +21,7 @@ import { throwGlobalError } from '../helpers/error'; 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'; +import { RuleActivation, RuleDetails, RulesUpdateRequest } from '../types/types'; export function getRulesApp(): Promise { return getJSON('/api/rules/app').catch(throwGlobalError); @@ -85,18 +85,6 @@ export function deleteRule(parameters: { key: string }) { return post('/api/rules/delete', parameters).catch(throwGlobalError); } -export function updateRule(data: { - key: string; - markdown_description?: string; - markdown_note?: string; - name?: string; - params?: string; - remediation_fn_base_effort?: string; - remediation_fn_type?: string; - remediation_fy_gap_multiplier?: string; - severity?: string; - status?: string; - tags?: string; -}): Promise { +export function updateRule(data: RulesUpdateRequest): Promise { return postJSON('/api/rules/update', data).then(r => r.rule, 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 index 36146027906..8386c527c45 100644 --- 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 @@ -62,11 +62,77 @@ it('should open with permalink', async () => { expect(screen.queryByRole('link', { name: 'Hot hotspot' })).not.toBeInTheDocument(); }); -it('should show open rule', async () => { +it('should show open rule with default description section', async () => { renderCodingRulesApp(undefined, 'coding_rules?open=rule1'); expect( await screen.findByRole('heading', { level: 3, name: 'Awsome java rule' }) ).toBeInTheDocument(); + expect( + screen.getByRole('region', { name: 'coding_rules.description_section.title.root_cause' }) + ).toBeInTheDocument(); +}); + +it('should show open rule with no description', async () => { + renderCodingRulesApp(undefined, 'coding_rules?open=rule6'); + expect( + await screen.findByRole('heading', { level: 3, name: 'Bad Python rule' }) + ).toBeInTheDocument(); + expect(screen.getByText('issue.external_issue_description.Bad Python rule')).toBeInTheDocument(); +}); + +it('should show open rule advance section', async () => { + renderCodingRulesApp(undefined, 'coding_rules?open=rule5'); + expect( + await screen.findByRole('heading', { level: 3, name: 'Awsome Python rule' }) + ).toBeInTheDocument(); + expect( + screen.getByRole('region', { name: 'coding_rules.description_section.title.introduction' }) + ).toBeInTheDocument(); + expect( + screen.getByRole('region', { name: 'coding_rules.description_section.title.how_to_fix' }) + ).toBeInTheDocument(); + expect( + screen.getByRole('region', { name: 'coding_rules.description_section.title.resources' }) + ).toBeInTheDocument(); + // Check that we render plain html + expect(screen.getByRole('link', { name: 'Awsome Reading' })).toBeInTheDocument(); +}); + +it('should be able to extend the rule description', async () => { + const user = userEvent.setup(); + handler.setIsAdmin(); + renderCodingRulesApp(undefined, 'coding_rules?open=rule5'); + expect( + await screen.findByRole('heading', { level: 3, name: 'Awsome Python rule' }) + ).toBeInTheDocument(); + + // Add + await user.click(screen.getByRole('button', { name: 'coding_rules.extend_description' })); + expect(screen.getByRole('textbox')).toBeInTheDocument(); + await user.click(screen.getByRole('textbox')); + await user.keyboard('TEST DESC'); + await user.click(screen.getByRole('button', { name: 'save' })); + expect(await screen.findByText('TEST DESC')).toBeInTheDocument(); + + // Edit + await user.click(screen.getByRole('button', { name: 'coding_rules.extend_description' })); + await user.click(screen.getByRole('textbox')); + await user.keyboard('{Control>}A{/Control}NEW DESC'); + await user.click(screen.getByRole('button', { name: 'save' })); + expect(await screen.findByText('NEW DESC')).toBeInTheDocument(); + + //Cancel + await user.click(screen.getByRole('button', { name: 'coding_rules.extend_description' })); + await user.dblClick(screen.getByRole('textbox')); + await user.keyboard('DIFFERENCE'); + await user.click(screen.getByRole('button', { name: 'cancel' })); + expect(await screen.findByText('NEW DESC')).toBeInTheDocument(); + + //Remove + await user.click(screen.getByRole('button', { name: 'coding_rules.extend_description' })); + await user.click(screen.getByRole('button', { name: 'remove' })); + await user.click(within(screen.getByRole('dialog')).getByRole('button', { name: 'remove' })); + await waitFor(() => expect(screen.queryByText('NEW DESC')).not.toBeInTheDocument()); }); it('should list all rules', async () => { @@ -140,7 +206,7 @@ it('should be able to bulk activate quality profile', async () => { 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)' + name: `coding_rules.activate_in_quality_profile (${handler.allRulesCount()} coding_rules._rules)` }); expect(dialog).toBeInTheDocument(); @@ -171,7 +237,7 @@ it('should be able to bulk activate quality profile', async () => { 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)' + name: `coding_rules.activate_in_quality_profile (${handler.allRulesCount()} coding_rules._rules)` }) ); await user.click(dialogScreen.getByRole('textbox', { name: 'coding_rules.activate_in' })); @@ -199,7 +265,7 @@ it('should be able to bulk deactivate quality profile', async () => { 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)' + name: `coding_rules.deactivate_in_quality_profile (${handler.allRulesCount()} coding_rules._rules)` }) ); await user.click(dialogScreen.getByRole('textbox', { name: 'coding_rules.deactivate_in' })); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsDescription.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsDescription.tsx index aa234aa3726..10aee08b32f 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsDescription.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsDescription.tsx @@ -17,15 +17,29 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { sortBy } from 'lodash'; import * as React from 'react'; import { updateRule } from '../../../api/rules'; import FormattingTips from '../../../components/common/FormattingTips'; import { Button, ResetButtonLink } from '../../../components/controls/buttons'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { sanitizeString } from '../../../helpers/sanitize'; -import { RuleDetails } from '../../../types/types'; +import { + Dict, + RuleDescriptionSection, + RuleDescriptionSections, + RuleDetails +} from '../../../types/types'; import RemoveExtendedDescriptionModal from './RemoveExtendedDescriptionModal'; +const SECTION_ORDER: Dict = { + [RuleDescriptionSections.INTRODUCTION]: 0, + [RuleDescriptionSections.ROOT_CAUSE]: 1, + [RuleDescriptionSections.ASSESS_THE_PROBLEM]: 2, + [RuleDescriptionSections.HOW_TO_FIX]: 3, + [RuleDescriptionSections.RESOURCES]: 4 +}; + interface Props { canWrite: boolean | undefined; onChange: (newRuleDetails: RuleDetails) => void; @@ -107,7 +121,14 @@ export default class RuleDetailsDescription extends React.PureComponent ( + sortedDescriptionSections(ruleDetails: RuleDetails) { + return sortBy( + ruleDetails.descriptionSections, + s => SECTION_ORDER[s.key] || Object.keys(SECTION_ORDER).length + ); + } + + renderExtendedDescription = () => (
{this.props.ruleDetails.htmlNote !== undefined && (
); + renderDescription(section: RuleDescriptionSection) { + return ( +
+ ); + } + render() { const { ruleDetails } = this.props; const hasDescription = !ruleDetails.isExternal || ruleDetails.type !== 'UNKNOWN'; return (
- {hasDescription && ruleDetails.htmlDesc !== undefined ? ( -
+ {hasDescription && + ruleDetails.descriptionSections && + ruleDetails.descriptionSections.length > 0 ? ( + this.sortedDescriptionSections(ruleDetails).map(this.renderDescription) ) : (
{translateWithParameters('issue.external_issue_description', ruleDetails.name)} @@ -205,7 +236,7 @@ export default class RuleDetailsDescription extends React.PureComponent - {!this.state.descriptionForm && this.renderDescription()} + {!this.state.descriptionForm && this.renderExtendedDescription()} {this.state.descriptionForm && this.props.canWrite && this.renderForm()}
)} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/CustomRuleFormModal-test.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/CustomRuleFormModal-test.tsx index 01cb398d0f1..2a884db5be6 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/CustomRuleFormModal-test.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/CustomRuleFormModal-test.tsx @@ -20,7 +20,7 @@ import { shallow } from 'enzyme'; import * as React from 'react'; import { createRule } from '../../../../api/rules'; -import { mockRule, mockRuleDetailsParameter } from '../../../../helpers/testMocks'; +import { mockRuleDetails, mockRuleDetailsParameter } from '../../../../helpers/testMocks'; import { submit, waitAndUpdate } from '../../../../helpers/testUtils'; import CustomRuleFormModal from '../CustomRuleFormModal'; @@ -44,7 +44,7 @@ function shallowRender(props: Partial = {}) { onClose={jest.fn()} onDone={jest.fn()} templateRule={{ - ...mockRule({ + ...mockRuleDetails({ params: [ mockRuleDetailsParameter(), mockRuleDetailsParameter({ key: '2', type: 'TEXT', htmlDesc: undefined }) diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleDetails-test.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleDetails-test.tsx index 7417e640e77..2fe01a8770a 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleDetails-test.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleDetails-test.tsx @@ -99,6 +99,7 @@ it('should correctly handle rule changes', () => { const wrapper = shallowRender(); const ruleChange = { createdAt: '2019-02-01', + descriptionSections: [], key: 'foo', name: 'Foo', repo: 'bar', diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleDetailsDescription-test.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleDetailsDescription-test.tsx deleted file mode 100644 index d635c09ad43..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleDetailsDescription-test.tsx +++ /dev/null @@ -1,88 +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 { change, click, waitAndUpdate } from '../../../../helpers/testUtils'; -import { RuleDetails } from '../../../../types/types'; -import RuleDetailsDescription from '../RuleDetailsDescription'; - -jest.mock('../../../../api/rules', () => ({ - updateRule: jest.fn().mockResolvedValue('updatedrule') -})); - -const RULE: RuleDetails = { - key: 'squid:S1133', - repo: 'squid', - name: 'Deprecated code should be removed', - createdAt: '2013-07-26T09:40:51+0200', - htmlDesc: '

Html Description

', - mdNote: 'Md Note', - severity: 'INFO', - status: 'READY', - lang: 'java', - langName: 'Java', - type: 'CODE_SMELL' -}; - -const EXTERNAL_RULE: RuleDetails = { - createdAt: '2013-07-26T09:40:51+0200', - key: 'external_xoo:OneExternalIssuePerLine', - repo: 'external_xoo', - name: 'xoo:OneExternalIssuePerLine', - severity: 'MAJOR', - status: 'READY', - isExternal: true, - type: 'UNKNOWN' -}; - -const EXTERNAL_RULE_WITH_DATA: RuleDetails = { - key: 'external_xoo:OneExternalIssueWithDetailsPerLine', - repo: 'external_xoo', - name: 'One external issue per line', - createdAt: '2018-05-31T11:19:51+0200', - htmlDesc: '

Html Description

', - severity: 'MAJOR', - status: 'READY', - isExternal: true, - type: 'BUG' -}; - -it('should display correctly', () => { - expect(getWrapper()).toMatchSnapshot(); - expect(getWrapper({ ruleDetails: EXTERNAL_RULE })).toMatchSnapshot(); - expect(getWrapper({ ruleDetails: EXTERNAL_RULE_WITH_DATA })).toMatchSnapshot(); -}); - -it('should add extra description', async () => { - const onChange = jest.fn(); - const wrapper = getWrapper({ canWrite: true, onChange }); - click(wrapper.find('#coding-rules-detail-extend-description')); - expect(wrapper.find('textarea').exists()).toBe(true); - change(wrapper.find('textarea'), 'new description'); - click(wrapper.find('#coding-rules-detail-extend-description-submit')); - await waitAndUpdate(wrapper); - expect(onChange).toBeCalledWith('updatedrule'); -}); - -function getWrapper(props = {}) { - return shallow( - - ); -} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleDetailsMeta-test.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleDetailsMeta-test.tsx index 37cf2cc63f9..ef2493ea0a5 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleDetailsMeta-test.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleDetailsMeta-test.tsx @@ -28,6 +28,7 @@ const RULE: RuleDetails = { repo: 'squid', name: 'Deprecated code should be removed', createdAt: '2013-07-26T09:40:51+0200', + descriptionSections: [], severity: 'INFO', status: 'READY', lang: 'java', @@ -41,6 +42,7 @@ const EXTERNAL_RULE: RuleDetails = { repo: 'external_xoo', name: 'xoo:OneExternalIssuePerLine', createdAt: '2018-05-31T11:22:13+0200', + descriptionSections: [], severity: 'MAJOR', status: 'READY', scope: 'ALL', @@ -53,6 +55,7 @@ const EXTERNAL_RULE_WITH_DATA: RuleDetails = { repo: 'external_xoo', name: 'One external issue per line', createdAt: '2018-05-31T11:19:51+0200', + descriptionSections: [], severity: 'MAJOR', status: 'READY', tags: ['tag'], diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/CustomRuleFormModal-test.tsx.snap b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/CustomRuleFormModal-test.tsx.snap index d3d7ca60de7..d2d08357848 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/CustomRuleFormModal-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/CustomRuleFormModal-test.tsx.snap @@ -110,8 +110,8 @@ exports[`should handle re-activation 1`] = ` } value={ Object { - "label": "issue.type.CODE_SMELL", - "value": "CODE_SMELL", + "label": "issue.type.BUG", + "value": "BUG", } } /> @@ -365,8 +365,8 @@ exports[`should render correctly: default 1`] = ` } value={ Object { - "label": "issue.type.CODE_SMELL", - "value": "CODE_SMELL", + "label": "issue.type.BUG", + "value": "BUG", } } /> diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleDetails-test.tsx.snap b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleDetails-test.tsx.snap index 5de95675322..d00bb73d453 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleDetails-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleDetails-test.tsx.snap @@ -29,6 +29,12 @@ exports[`should render correctly: loaded 1`] = ` "defaultDebtRemFnType": "CONSTANT_ISSUE", "defaultRemFnBaseEffort": "5min", "defaultRemFnType": "CONSTANT_ISSUE", + "descriptionSections": Array [ + Object { + "content": "Why Because", + "key": "root_cause", + }, + ], "htmlDesc": "", "isExternal": false, "isTemplate": false, @@ -65,6 +71,12 @@ exports[`should render correctly: loaded 1`] = ` "defaultDebtRemFnType": "CONSTANT_ISSUE", "defaultRemFnBaseEffort": "5min", "defaultRemFnType": "CONSTANT_ISSUE", + "descriptionSections": Array [ + Object { + "content": "Why Because", + "key": "root_cause", + }, + ], "htmlDesc": "", "isExternal": false, "isTemplate": false, @@ -132,6 +144,12 @@ exports[`should render correctly: loaded 1`] = ` "defaultDebtRemFnType": "CONSTANT_ISSUE", "defaultRemFnBaseEffort": "5min", "defaultRemFnType": "CONSTANT_ISSUE", + "descriptionSections": Array [ + Object { + "content": "Why Because", + "key": "root_cause", + }, + ], "htmlDesc": "", "isExternal": false, "isTemplate": false, @@ -167,6 +185,12 @@ exports[`should render correctly: loaded 1`] = ` "defaultDebtRemFnType": "CONSTANT_ISSUE", "defaultRemFnBaseEffort": "5min", "defaultRemFnType": "CONSTANT_ISSUE", + "descriptionSections": Array [ + Object { + "content": "Why Because", + "key": "root_cause", + }, + ], "htmlDesc": "", "isExternal": false, "isTemplate": false, diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleDetailsDescription-test.tsx.snap b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleDetailsDescription-test.tsx.snap deleted file mode 100644 index 2e50c52359b..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleDetailsDescription-test.tsx.snap +++ /dev/null @@ -1,64 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should display correctly 1`] = ` -
-
Html Description

", - } - } - /> -
-
-
-
-`; - -exports[`should display correctly 2`] = ` -
-
- issue.external_issue_description.xoo:OneExternalIssuePerLine -
-
-
-
-
-`; - -exports[`should display correctly 3`] = ` -
-
Html Description

", - } - } - /> -
-
-
-
-`; diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleDetailsMeta-test.tsx.snap b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleDetailsMeta-test.tsx.snap index 9de6d69e248..aec10d999a0 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleDetailsMeta-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleDetailsMeta-test.tsx.snap @@ -37,6 +37,7 @@ exports[`should display right meta info 1`] = ` rule={ Object { "createdAt": "2013-07-26T09:40:51+0200", + "descriptionSections": Array [], "key": "squid:S1133", "lang": "java", "langName": "Java", diff --git a/server/sonar-web/src/main/js/helpers/testMocks.ts b/server/sonar-web/src/main/js/helpers/testMocks.ts index 437ed0de99e..4cc1426456a 100644 --- a/server/sonar-web/src/main/js/helpers/testMocks.ts +++ b/server/sonar-web/src/main/js/helpers/testMocks.ts @@ -47,6 +47,7 @@ import { ProfileInheritanceDetails, Rule, RuleActivation, + RuleDescriptionSections, RuleDetails, RuleParameter, SnippetsByComponent, @@ -603,6 +604,12 @@ export function mockRuleDetails(overrides: Partial = {}): RuleDetai repo: 'squid', name: '".equals()" should not be used to test the values of "Atomic" classes', createdAt: '2014-12-16T17:26:54+0100', + descriptionSections: [ + { + key: RuleDescriptionSections.ROOT_CAUSE, + content: 'Why Because' + } + ], htmlDesc: '', mdDesc: '', severity: 'MAJOR', diff --git a/server/sonar-web/src/main/js/types/types.ts b/server/sonar-web/src/main/js/types/types.ts index a376854de4a..80a7c7eaa28 100644 --- a/server/sonar-web/src/main/js/types/types.ts +++ b/server/sonar-web/src/main/js/types/types.ts @@ -565,6 +565,34 @@ export interface RuleActivation { severity: string; } +export enum RuleDescriptionSections { + DEFAULT = 'default', + INTRODUCTION = 'introduction', + ROOT_CAUSE = 'root_cause', + ASSESS_THE_PROBLEM = 'assess_the_problem', + HOW_TO_FIX = 'how_to_fix', + RESOURCES = 'resources' +} + +export interface RuleDescriptionSection { + key: RuleDescriptionSections; + content: string; +} + +export interface RulesUpdateRequest { + key: string; + markdown_description?: string; + markdown_note?: string; + name?: string; + params?: string; + remediation_fn_base_effort?: string; + remediation_fn_type?: string; + remediation_fy_gap_multiplier?: string; + severity?: string; + status?: string; + tags?: string; +} + export interface RuleDetails extends Rule { createdAt: string; debtOverloaded?: boolean; @@ -576,6 +604,7 @@ export interface RuleDetails extends Rule { defaultRemFnBaseEffort?: string; defaultRemFnType?: string; effortToFixDescription?: string; + descriptionSections?: RuleDescriptionSection[]; htmlDesc?: string; htmlNote?: string; internalKey?: string; 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 5754d4d99eb..aaf1bf7ec60 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -1884,6 +1884,14 @@ coding_rules.remediation_function.constant=Constant coding_rules.external_rule.engine=Rule provided by an external rule engine: {0} +coding_rules.description_section.title.default= +coding_rules.description_section.title.introduction=Introduction +coding_rules.description_section.title.root_cause=Why is this an issue? +coding_rules.description_section.title.assess_the_problem=Assess the risk? +coding_rules.description_section.title.how_to_fix=How to fix it? +coding_rules.description_section.title.resources=Resources + + #------------------------------------------------------------------------------ # # EMAIL CONFIGURATION