]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-16365 Concatenate description section on rules page
authorMathieu Suen <mathieu.suen@sonarsource.com>
Thu, 5 May 2022 14:55:41 +0000 (16:55 +0200)
committersonartech <sonartech@sonarsource.com>
Tue, 10 May 2022 20:02:48 +0000 (20:02 +0000)
15 files changed:
server/sonar-web/src/main/js/api/mocks/CodingRulesMock.ts
server/sonar-web/src/main/js/api/rules.ts
server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts
server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsDescription.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/CustomRuleFormModal-test.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleDetails-test.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleDetailsDescription-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleDetailsMeta-test.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/CustomRuleFormModal-test.tsx.snap
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleDetails-test.tsx.snap
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleDetailsDescription-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleDetailsMeta-test.tsx.snap
server/sonar-web/src/main/js/helpers/testMocks.ts
server/sonar-web/src/main/js/types/types.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 7b3d6d2d07059b35611795a38b6b7275d21365f8..30934b14f667aba8ab016763781d786222b8544b 100644 (file)
  * 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 <a href="http://example.com">Awsome Reading</a>'
+          }
+        ]
+      }),
+      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<RuleDetails> => {
+    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));
index 0ab2ad21c44436cbc9ce090951b6163c8814beb6..583e684c20055399f576c54322a128b9fab2ae76 100644 (file)
@@ -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<GetRulesAppResponse> {
   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<RuleDetails> {
+export function updateRule(data: RulesUpdateRequest): Promise<RuleDetails> {
   return postJSON('/api/rules/update', data).then(r => r.rule, throwGlobalError);
 }
index 361460279061b459e7227224995813490f965fa1..8386c527c4592154cf626283fc436f9341fa204b 100644 (file)
@@ -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' }));
index aa234aa3726400c95bae41ff69f7559955ecac4a..10aee08b32f7869bda37f28f5e27bd9f3d054134 100644 (file)
  * 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<number> = {
+  [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<Props, S
     });
   };
 
-  renderDescription = () => (
+  sortedDescriptionSections(ruleDetails: RuleDetails) {
+    return sortBy(
+      ruleDetails.descriptionSections,
+      s => SECTION_ORDER[s.key] || Object.keys(SECTION_ORDER).length
+    );
+  }
+
+  renderExtendedDescription = () => (
     <div id="coding-rules-detail-description-extra">
       {this.props.ruleDetails.htmlNote !== undefined && (
         <div
@@ -185,18 +206,28 @@ export default class RuleDetailsDescription extends React.PureComponent<Props, S
     </div>
   );
 
+  renderDescription(section: RuleDescriptionSection) {
+    return (
+      <section
+        aria-label={translate('coding_rules.description_section.title', section.key)}
+        className="coding-rules-detail-description rule-desc markdown"
+        key={section.key}
+        /* eslint-disable-next-line react/no-danger */
+        dangerouslySetInnerHTML={{ __html: sanitizeString(section.content) }}
+      />
+    );
+  }
+
   render() {
     const { ruleDetails } = this.props;
     const hasDescription = !ruleDetails.isExternal || ruleDetails.type !== 'UNKNOWN';
 
     return (
       <div className="js-rule-description">
-        {hasDescription && ruleDetails.htmlDesc !== undefined ? (
-          <div
-            className="coding-rules-detail-description rule-desc markdown"
-            // eslint-disable-next-line react/no-danger
-            dangerouslySetInnerHTML={{ __html: sanitizeString(ruleDetails.htmlDesc) }}
-          />
+        {hasDescription &&
+        ruleDetails.descriptionSections &&
+        ruleDetails.descriptionSections.length > 0 ? (
+          this.sortedDescriptionSections(ruleDetails).map(this.renderDescription)
         ) : (
           <div className="coding-rules-detail-description rule-desc markdown">
             {translateWithParameters('issue.external_issue_description', ruleDetails.name)}
@@ -205,7 +236,7 @@ export default class RuleDetailsDescription extends React.PureComponent<Props, S
 
         {!ruleDetails.templateKey && (
           <div className="coding-rules-detail-description coding-rules-detail-description-extra">
-            {!this.state.descriptionForm && this.renderDescription()}
+            {!this.state.descriptionForm && this.renderExtendedDescription()}
             {this.state.descriptionForm && this.props.canWrite && this.renderForm()}
           </div>
         )}
index 01cb398d0f1016706a15d3403e4604feaacb03be..2a884db5be6196180443edfac99b801e0c5e6b46 100644 (file)
@@ -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<CustomRuleFormModal['props']> = {}) {
       onClose={jest.fn()}
       onDone={jest.fn()}
       templateRule={{
-        ...mockRule({
+        ...mockRuleDetails({
           params: [
             mockRuleDetailsParameter(),
             mockRuleDetailsParameter({ key: '2', type: 'TEXT', htmlDesc: undefined })
index 7417e640e770b07a1afcca4365d5cef39eea9fe1..2fe01a8770a3e570068d3e916988c5ee1ae5f9fc 100644 (file)
@@ -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 (file)
index d635c09..0000000
+++ /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: '<p>Html Description</p>',
-  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: '<p>Html Description</p>',
-  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(
-    <RuleDetailsDescription canWrite={false} onChange={jest.fn()} ruleDetails={RULE} {...props} />
-  );
-}
index 37cf2cc63f92b5bc184e36b8a1e8f021b87810c9..ef2493ea0a5cc38c2ef3de070d0abef5b1df5060 100644 (file)
@@ -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'],
index d3d7ca60de7611e5b053dcc4f91d43f72039b0c6..d2d08357848d502161628f34a9b493d7beb756cd 100644 (file)
@@ -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",
               }
             }
           />
index 5de95675322da663fe7050896b44027970a77f07..d00bb73d453d1997e4f5bf98e54afe2ab05a0d20 100644 (file)
@@ -29,6 +29,12 @@ exports[`should render correctly: loaded 1`] = `
           "defaultDebtRemFnType": "CONSTANT_ISSUE",
           "defaultRemFnBaseEffort": "5min",
           "defaultRemFnType": "CONSTANT_ISSUE",
+          "descriptionSections": Array [
+            Object {
+              "content": "<b>Why<b/> 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": "<b>Why<b/> 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": "<b>Why<b/> 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": "<b>Why<b/> 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 (file)
index 2e50c52..0000000
+++ /dev/null
@@ -1,64 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should display correctly 1`] = `
-<div
-  className="js-rule-description"
->
-  <div
-    className="coding-rules-detail-description rule-desc markdown"
-    dangerouslySetInnerHTML={
-      Object {
-        "__html": "<p>Html Description</p>",
-      }
-    }
-  />
-  <div
-    className="coding-rules-detail-description coding-rules-detail-description-extra"
-  >
-    <div
-      id="coding-rules-detail-description-extra"
-    />
-  </div>
-</div>
-`;
-
-exports[`should display correctly 2`] = `
-<div
-  className="js-rule-description"
->
-  <div
-    className="coding-rules-detail-description rule-desc markdown"
-  >
-    issue.external_issue_description.xoo:OneExternalIssuePerLine
-  </div>
-  <div
-    className="coding-rules-detail-description coding-rules-detail-description-extra"
-  >
-    <div
-      id="coding-rules-detail-description-extra"
-    />
-  </div>
-</div>
-`;
-
-exports[`should display correctly 3`] = `
-<div
-  className="js-rule-description"
->
-  <div
-    className="coding-rules-detail-description rule-desc markdown"
-    dangerouslySetInnerHTML={
-      Object {
-        "__html": "<p>Html Description</p>",
-      }
-    }
-  />
-  <div
-    className="coding-rules-detail-description coding-rules-detail-description-extra"
-  >
-    <div
-      id="coding-rules-detail-description-extra"
-    />
-  </div>
-</div>
-`;
index 9de6d69e248fea94f2d4e8fa74993828c83a4aa8..aec10d999a0bea99de5c5cfa7cfb2551ad31e443 100644 (file)
@@ -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",
index 437ed0de99e20160af46815b46ff37e952e85130..4cc1426456a0e2c1acf5b1d34e2ebfdce65df3dc 100644 (file)
@@ -47,6 +47,7 @@ import {
   ProfileInheritanceDetails,
   Rule,
   RuleActivation,
+  RuleDescriptionSections,
   RuleDetails,
   RuleParameter,
   SnippetsByComponent,
@@ -603,6 +604,12 @@ export function mockRuleDetails(overrides: Partial<RuleDetails> = {}): 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: '<b>Why<b/> Because'
+      }
+    ],
     htmlDesc: '',
     mdDesc: '',
     severity: 'MAJOR',
index a376854de4a1e536929dd7bbac4409ef208b1fcb..80a7c7eaa28e7c523e7c403f0694133efa2e1d8e 100644 (file)
@@ -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;
index 5754d4d99eb8d20895ebf42a4b8a9ebb266a18c7..aaf1bf7ec60fda4aac9679598daac35729e5ec3b 100644 (file)
@@ -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