Browse Source

SONAR-16518 Show contexts in rules, issues and hotspot page

tags/9.6.0.59041
Revanshu Paliwal 1 year ago
parent
commit
bcbc1e6729
28 changed files with 536 additions and 397 deletions
  1. 33
    9
      server/sonar-web/src/main/js/api/mocks/CodingRulesMock.ts
  2. 22
    1
      server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts
  3. 33
    0
      server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts
  4. 2
    1
      server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsDescription.tsx
  5. 25
    21
      server/sonar-web/src/main/js/apps/coding-rules/components/RuleTabViewer.tsx
  6. 37
    0
      server/sonar-web/src/main/js/apps/coding-rules/rule.ts
  7. 8
    0
      server/sonar-web/src/main/js/apps/coding-rules/styles.css
  8. 14
    1
      server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx
  9. 60
    43
      server/sonar-web/src/main/js/apps/issues/components/IssueTabViewer.tsx
  10. 16
    20
      server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewer.tsx
  11. 5
    1
      server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerRenderer.tsx
  12. 31
    25
      server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerTabs.tsx
  13. 15
    6
      server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewer-test.tsx
  14. 30
    14
      server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewerTabs-test.tsx
  15. 0
    6
      server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotHeader-test.tsx.snap
  16. 0
    6
      server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotReviewHistoryAndComments-test.tsx.snap
  17. 0
    3
      server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotSnippetContainer-test.tsx.snap
  18. 0
    12
      server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotSnippetContainerRenderer-test.tsx.snap
  19. 2
    5
      server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewer-test.tsx.snap
  20. 0
    72
      server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewerRenderer-test.tsx.snap
  21. 78
    122
      server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewerTabs-test.tsx.snap
  22. 0
    9
      server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/Status-test.tsx.snap
  23. 121
    0
      server/sonar-web/src/main/js/components/rules/RuleContextDescription.tsx
  24. 0
    3
      server/sonar-web/src/main/js/helpers/mocks/security-hotspots.ts
  25. 1
    1
      server/sonar-web/src/main/js/helpers/testMocks.ts
  26. 0
    3
      server/sonar-web/src/main/js/types/security-hotspots.ts
  27. 1
    13
      server/sonar-web/src/main/js/types/types.ts
  28. 2
    0
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 33
- 9
server/sonar-web/src/main/js/api/mocks/CodingRulesMock.ts View File

@@ -18,17 +18,12 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { cloneDeep, countBy, pick, trim } from 'lodash';
import { RuleDescriptionSections } from '../../apps/coding-rules/rule';
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,
RuleDescriptionSections,
RuleDetails,
RulesUpdateRequest
} from '../../types/types';
import { Rule, RuleActivation, RuleDetails, RulesUpdateRequest } from '../../types/types';
import { getFacet } from '../issues';
import {
bulkActivateRules,
@@ -67,6 +62,8 @@ export default class CodingRulesMock {
mockQualityProfile({ key: 'p3', name: 'QP FooBar', language: 'java', languageName: 'Java' })
];

const resourceContent = 'Some link <a href="http://example.com">Awsome Reading</a>';

this.defaultRules = [
mockRuleDetails({
key: 'rule1',
@@ -86,7 +83,7 @@ export default class CodingRulesMock {
{ key: RuleDescriptionSections.ASSESS_THE_PROBLEM, content: 'Assess' },
{
key: RuleDescriptionSections.RESOURCES,
content: 'Some link <a href="http://example.com">Awsome Reading</a>'
content: resourceContent
}
],
langName: 'JavaScript'
@@ -110,7 +107,7 @@ export default class CodingRulesMock {
{ key: RuleDescriptionSections.HOW_TO_FIX, content: 'This how to fix' },
{
key: RuleDescriptionSections.RESOURCES,
content: 'Some link <a href="http://example.com">Awsome Reading</a>'
content: resourceContent
}
]
}),
@@ -122,6 +119,33 @@ export default class CodingRulesMock {
name: 'Bad Python rule',
isExternal: true,
descriptionSections: undefined
}),
mockRuleDetails({
key: 'rule7',
type: 'VULNERABILITY',
lang: 'py',
langName: 'Python',
name: 'Python rule with context',
descriptionSections: [
{
key: RuleDescriptionSections.INTRODUCTION,
content: 'Introduction to this rule with context'
},
{
key: RuleDescriptionSections.HOW_TO_FIX,
content: 'This how to fix for spring',
context: { displayName: 'Spring' }
},
{
key: RuleDescriptionSections.HOW_TO_FIX,
content: 'This how to fix for spring boot',
context: { displayName: 'Spring boot' }
},
{
key: RuleDescriptionSections.RESOURCES,
content: resourceContent
}
]
})
];


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

@@ -18,6 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { cloneDeep, keyBy, range, times } from 'lodash';
import { RuleDescriptionSections } from '../../apps/coding-rules/rule';
import {
mockSnippetsByComponent,
mockSourceLine,
@@ -32,7 +33,6 @@ import { Standards } from '../../types/security';
import {
Dict,
RuleActivation,
RuleDescriptionSections,
RuleDetails,
SnippetsByComponent,
SourceViewerFile
@@ -248,6 +248,27 @@ export default class IssuesServiceMock {
{ key: RuleDescriptionSections.INTRODUCTION, content: '<h1>Into</h1>' },
{ key: RuleDescriptionSections.ROOT_CAUSE, content: '<h1>Because</h1>' },
{ key: RuleDescriptionSections.HOW_TO_FIX, content: '<h1>Fix with</h1>' },
{
content: '<p> Context 1 content<p>',
key: RuleDescriptionSections.HOW_TO_FIX,
context: {
displayName: 'Spring'
}
},
{
content: '<p> Context 2 content<p>',
key: RuleDescriptionSections.HOW_TO_FIX,
context: {
displayName: 'Context 2'
}
},
{
content: '<p> Context 3 content<p>',
key: RuleDescriptionSections.HOW_TO_FIX,
context: {
displayName: 'Context 3'
}
},
{ key: RuleDescriptionSections.RESOURCES, content: '<h1>Link</h1>' }
]
})

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

@@ -134,6 +134,39 @@ it('should show rule advanced section', async () => {
expect(screen.getByRole('link', { name: 'Awsome Reading' })).toBeInTheDocument();
});

it('should show rule advanced section with context', async () => {
const user = userEvent.setup();
renderCodingRulesApp(undefined, 'coding_rules?open=rule7');
expect(
await screen.findByRole('heading', { level: 3, name: 'Python rule with context' })
).toBeInTheDocument();
expect(
screen.getByRole('button', {
name: 'coding_rules.description_section.title.how_to_fix'
})
).toBeInTheDocument();

await user.click(
screen.getByRole('button', {
name: 'coding_rules.description_section.title.how_to_fix'
})
);
expect(screen.getByRole('radio', { name: 'Spring' })).toBeInTheDocument();
expect(screen.getByRole('radio', { name: 'Spring boot' })).toBeInTheDocument();
expect(
screen.getByRole('radio', { name: 'coding_rules.description_context_other' })
).toBeInTheDocument();
expect(screen.getByText('This how to fix for spring')).toBeInTheDocument();

await user.click(screen.getByRole('radio', { name: 'Spring boot' }));
expect(screen.getByText('This how to fix for spring boot')).toBeInTheDocument();

await user.click(screen.getByRole('radio', { name: 'coding_rules.description_context_other' }));
expect(screen.getByText('coding_rules.context.others.title')).toBeInTheDocument();
expect(screen.getByText('coding_rules.context.others.description.first')).toBeInTheDocument();
expect(screen.getByText('coding_rules.context.others.description.second')).toBeInTheDocument();
});

it('should be able to extend the rule description', async () => {
const user = userEvent.setup();
handler.setIsAdmin();

+ 2
- 1
server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsDescription.tsx View File

@@ -23,7 +23,8 @@ 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 { RuleDescriptionSections, RuleDetails } from '../../../types/types';
import { RuleDetails } from '../../../types/types';
import { RuleDescriptionSections } from '../rule';
import RemoveExtendedDescriptionModal from './RemoveExtendedDescriptionModal';
import RuleTabViewer from './RuleTabViewer';


+ 25
- 21
server/sonar-web/src/main/js/apps/coding-rules/components/RuleTabViewer.tsx View File

@@ -17,11 +17,14 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { groupBy } from 'lodash';
import * as React from 'react';
import BoxedTabs from '../../../components/controls/BoxedTabs';
import { translate } from '../../../helpers/l10n';
import { sanitizeString } from '../../../helpers/sanitize';
import { RuleDescriptionSections, RuleDetails } from '../../../types/types';
import { RuleDetails } from '../../../types/types';
import { RuleDescriptionSection, RuleDescriptionSections } from '../rule';
import RuleContextDescription from '../../../components/rules/RuleContextDescription';

interface Props {
ruleDetails: RuleDetails;
@@ -35,7 +38,7 @@ interface State {
interface Tab {
key: TabKeys;
label: React.ReactNode;
content: string;
descriptionSections: RuleDescriptionSection[];
}

enum TabKeys {
@@ -65,6 +68,7 @@ export default class RuleViewerTabs extends React.PureComponent<Props, State> {

computeState() {
const { ruleDetails } = this.props;
const groupedDescriptions = groupBy(ruleDetails.descriptionSections, 'key');

const tabs = [
{
@@ -73,32 +77,24 @@ export default class RuleViewerTabs extends React.PureComponent<Props, State> {
ruleDetails.type === 'SECURITY_HOTSPOT'
? translate('coding_rules.description_section.title.root_cause.SECURITY_HOTSPOT')
: translate('coding_rules.description_section.title.root_cause'),
content: ruleDetails.descriptionSections?.find(
section => section.key === RuleDescriptionSections.ROOT_CAUSE
)?.content
descriptionSections: groupedDescriptions[RuleDescriptionSections.ROOT_CAUSE]
},
{
key: TabKeys.AssessTheIssue,
label: translate('coding_rules.description_section.title', TabKeys.AssessTheIssue),
content: ruleDetails.descriptionSections?.find(
section => section.key === RuleDescriptionSections.ASSESS_THE_PROBLEM
)?.content
descriptionSections: groupedDescriptions[RuleDescriptionSections.ASSESS_THE_PROBLEM]
},
{
key: TabKeys.HowToFixIt,
label: translate('coding_rules.description_section.title', TabKeys.HowToFixIt),
content: ruleDetails.descriptionSections?.find(
section => section.key === RuleDescriptionSections.HOW_TO_FIX
)?.content
descriptionSections: groupedDescriptions[RuleDescriptionSections.HOW_TO_FIX]
},
{
key: TabKeys.Resources,
label: translate('coding_rules.description_section.title', TabKeys.Resources),
content: ruleDetails.descriptionSections?.find(
section => section.key === RuleDescriptionSections.RESOURCES
)?.content
descriptionSections: groupedDescriptions[RuleDescriptionSections.RESOURCES]
}
].filter(tab => tab.content !== undefined) as Array<Tab>;
].filter(tab => tab.descriptionSections) as Array<Tab>;

return {
currentTab: tabs[0],
@@ -112,7 +108,6 @@ export default class RuleViewerTabs extends React.PureComponent<Props, State> {
const intro = ruleDetails.descriptionSections?.find(
section => section.key === RuleDescriptionSections.INTRODUCTION
)?.content;

return (
<>
{intro && (
@@ -130,11 +125,20 @@ export default class RuleViewerTabs extends React.PureComponent<Props, State> {
/>

<div className="bordered-right bordered-left bordered-bottom huge-spacer-bottom">
<div
className="big-padded rule-desc"
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: sanitizeString(currentTab.content) }}
/>
{currentTab.descriptionSections.length === 1 &&
!currentTab.descriptionSections[0].context ? (
<div
className="big-padded rule-desc"
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{
__html: sanitizeString(currentTab.descriptionSections[0].content)
}}
/>
) : (
<div className="big-padded rule-desc">
<RuleContextDescription description={currentTab.descriptionSections} />
</div>
)}
</div>
</>
);

+ 37
- 0
server/sonar-web/src/main/js/apps/coding-rules/rule.ts View File

@@ -0,0 +1,37 @@
/*
* 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.
*/
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 RuleDescriptionContext {
displayName: string;
}

export interface RuleDescriptionSection {
key: RuleDescriptionSections;
content: string;
context?: RuleDescriptionContext;
}

+ 8
- 0
server/sonar-web/src/main/js/apps/coding-rules/styles.css View File

@@ -274,3 +274,11 @@
.coding-rule-activation-actions {
padding-left: 20px;
}

.rules-context-description ul {
padding: 0px;
}

.rules-context-description h2.rule-contexts-title {
border: 0px;
}

+ 14
- 1
server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx View File

@@ -52,7 +52,20 @@ it('should open issue and navigate', async () => {

expect(screen.getByRole('button', { name: `issue.tabs.how` })).toBeInTheDocument();
await user.click(screen.getByRole('button', { name: `issue.tabs.how` }));
expect(screen.getByRole('heading', { name: 'Fix with' })).toBeInTheDocument();
expect(screen.getByRole('radio', { name: 'Context 2' })).toBeInTheDocument();
expect(screen.getByRole('radio', { name: 'Context 3' })).toBeInTheDocument();
expect(screen.getByRole('radio', { name: 'Spring' })).toBeInTheDocument();
expect(
screen.getByRole('radio', { name: 'coding_rules.description_context_other' })
).toBeInTheDocument();

await user.click(screen.getByRole('radio', { name: 'Context 2' }));
expect(screen.getByText('Context 2 content')).toBeInTheDocument();

await user.click(screen.getByRole('radio', { name: 'coding_rules.description_context_other' }));
expect(screen.getByText('coding_rules.context.others.title')).toBeInTheDocument();
expect(screen.getByText('coding_rules.context.others.description.first')).toBeInTheDocument();
expect(screen.getByText('coding_rules.context.others.description.second')).toBeInTheDocument();

expect(screen.getByRole('button', { name: `issue.tabs.why` })).toBeInTheDocument();
await user.click(screen.getByRole('button', { name: `issue.tabs.why` }));

+ 60
- 43
server/sonar-web/src/main/js/apps/issues/components/IssueTabViewer.tsx View File

@@ -18,13 +18,16 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import classNames from 'classnames';
import { groupBy } from 'lodash';
import * as React from 'react';
import { Link } from 'react-router-dom';
import BoxedTabs from '../../../components/controls/BoxedTabs';
import { translate } from '../../../helpers/l10n';
import { sanitizeString } from '../../../helpers/sanitize';
import { getRuleUrl } from '../../../helpers/urls';
import { Component, Issue, RuleDescriptionSections, RuleDetails } from '../../../types/types';
import { Component, Issue, RuleDetails } from '../../../types/types';
import RuleContextDescription from '../../../components/rules/RuleContextDescription';
import { RuleDescriptionSection, RuleDescriptionSections } from '../../coding-rules/rule';

interface Props {
component?: Component;
@@ -41,7 +44,7 @@ interface State {
interface Tab {
key: TabKeys;
label: React.ReactNode;
content: string;
descriptionSections: RuleDescriptionSection[];
isDefault: boolean;
}

@@ -78,49 +81,54 @@ export default class IssueViewerTabs extends React.PureComponent<Props, State> {

computeTabs() {
const { ruleDetails } = this.props;
const groupedDescriptions = groupBy(ruleDetails.descriptionSections, 'key');

const tabs = [
if (ruleDetails.htmlNote) {
if (groupedDescriptions[RuleDescriptionSections.RESOURCES] !== undefined) {
// We add the extended description (htmlNote) in the first context, in case there are contexts
// Extended description will get reworked in future
groupedDescriptions[RuleDescriptionSections.RESOURCES][0].content +=
'<br/>' + ruleDetails.htmlNote;
} else {
groupedDescriptions[RuleDescriptionSections.RESOURCES] = [
{
key: RuleDescriptionSections.RESOURCES,
content: ruleDetails.htmlNote
}
];
}
}

return [
{
key: TabKeys.Code,
label: translate('issue.tabs', TabKeys.Code),
content: ''
descriptionSections: []
},
{
key: TabKeys.WhyIsThisAnIssue,
label: translate('issue.tabs', TabKeys.WhyIsThisAnIssue),
content: ruleDetails.descriptionSections?.find(section =>
[RuleDescriptionSections.DEFAULT, RuleDescriptionSections.ROOT_CAUSE].includes(
section.key
)
)?.content,
descriptionSections:
groupedDescriptions[RuleDescriptionSections.DEFAULT] ||
groupedDescriptions[RuleDescriptionSections.ROOT_CAUSE],
isDefault:
ruleDetails.descriptionSections?.find(
ruleDetails.descriptionSections?.filter(
section => section.key === RuleDescriptionSections.DEFAULT
) !== undefined
},
{
key: TabKeys.HowToFixIt,
label: translate('issue.tabs', TabKeys.HowToFixIt),
content: ruleDetails.descriptionSections?.find(
section => section.key === RuleDescriptionSections.HOW_TO_FIX
)?.content,
descriptionSections: groupedDescriptions[RuleDescriptionSections.HOW_TO_FIX],
isDefault: false
},
{
key: TabKeys.Resources,
label: translate('issue.tabs', TabKeys.Resources),
content: ruleDetails.descriptionSections?.find(
section => section.key === RuleDescriptionSections.RESOURCES
)?.content,
descriptionSections: groupedDescriptions[RuleDescriptionSections.RESOURCES],
isDefault: false
}
].filter(tab => tab.content !== undefined) as Array<Tab>;

if (ruleDetails.htmlNote) {
tabs[tabs.length - 1].content += '<br/>' + ruleDetails.htmlNote;
}

return tabs;
].filter(tab => tab.descriptionSections) as Array<Tab>;
}

render() {
@@ -131,7 +139,7 @@ export default class IssueViewerTabs extends React.PureComponent<Props, State> {
issue: { message }
} = this.props;
const { tabs, currentTabKey } = this.state;
const selectedTab = tabs.find(tab => tab.key === currentTabKey);
return (
<>
<div
@@ -152,26 +160,35 @@ export default class IssueViewerTabs extends React.PureComponent<Props, State> {
tabs={tabs}
/>
</div>
<div className="bordered-right bordered-left bordered-bottom huge-spacer-bottom">
<div
className={classNames('padded', {
hidden: currentTabKey !== TabKeys.Code
})}>
{codeTabContent}
{selectedTab && (
<div className="bordered-right bordered-left bordered-bottom huge-spacer-bottom">
{selectedTab.key === TabKeys.Code && <div className="padded">{codeTabContent}</div>}
{selectedTab.key !== TabKeys.Code &&
(selectedTab.descriptionSections.length === 1 &&
!selectedTab.descriptionSections[0].context ? (
<div
key={selectedTab.key}
className={classNames('big-padded', {
markdown: selectedTab.isDefault,
'rule-desc': !selectedTab.isDefault
})}
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{
__html: sanitizeString(selectedTab.descriptionSections[0].content)
}}
/>
) : (
<div
key={selectedTab.key}
className={classNames('big-padded', {
markdown: selectedTab.isDefault,
'rule-desc': !selectedTab.isDefault
})}>
<RuleContextDescription description={selectedTab.descriptionSections} />
</div>
))}
</div>
{tabs.slice(1).map(tab => (
<div
key={tab.key}
className={classNames('big-padded', {
hidden: currentTabKey !== tab.key,
markdown: tab.isDefault,
'rule-desc': !tab.isDefault
})}
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: sanitizeString(tab.content) }}
/>
))}
</div>
)}
</>
);
}

+ 16
- 20
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewer.tsx View File

@@ -26,7 +26,8 @@ import {
HotspotStatusFilter,
HotspotStatusOption
} from '../../../types/security-hotspots';
import { Component, RuleDescriptionSections } from '../../../types/types';
import { Component } from '../../../types/types';
import { RuleDescriptionSection } from '../../coding-rules/rule';
import { getStatusFilterFromStatusOption } from '../utils';
import HotspotViewerRenderer from './HotspotViewerRenderer';

@@ -42,6 +43,7 @@ interface Props {

interface State {
hotspot?: Hotspot;
ruleDescriptionSections?: RuleDescriptionSection[];
lastStatusChangedTo?: HotspotStatusOption;
loading: boolean;
showStatusUpdateSuccessModal: boolean;
@@ -82,24 +84,11 @@ export default class HotspotViewer extends React.PureComponent<Props, State> {
const ruleDetails = await getRuleDetails({ key: hotspot.rule.key }).then(r => r.rule);

if (this.mounted) {
hotspot.rule.riskDescription =
ruleDetails.descriptionSections?.find(section =>
[RuleDescriptionSections.DEFAULT, RuleDescriptionSections.ROOT_CAUSE].includes(
section.key
)
)?.content || hotspot.rule.riskDescription;

hotspot.rule.fixRecommendations =
ruleDetails.descriptionSections?.find(
section => RuleDescriptionSections.HOW_TO_FIX === section.key
)?.content || hotspot.rule.fixRecommendations;

hotspot.rule.vulnerabilityDescription =
ruleDetails.descriptionSections?.find(
section => RuleDescriptionSections.ASSESS_THE_PROBLEM === section.key
)?.content || hotspot.rule.vulnerabilityDescription;

this.setState({ hotspot, loading: false });
this.setState({
hotspot,
loading: false,
ruleDescriptionSections: ruleDetails.descriptionSections
});
}
} catch (error) {
if (this.mounted) {
@@ -141,13 +130,20 @@ export default class HotspotViewer extends React.PureComponent<Props, State> {

render() {
const { component, hotspotsReviewedMeasure, selectedHotspotLocation } = this.props;
const { hotspot, lastStatusChangedTo, loading, showStatusUpdateSuccessModal } = this.state;
const {
hotspot,
ruleDescriptionSections,
lastStatusChangedTo,
loading,
showStatusUpdateSuccessModal
} = this.state;

return (
<HotspotViewerRenderer
component={component}
commentTextRef={this.commentTextRef}
hotspot={hotspot}
ruleDescriptionSections={ruleDescriptionSections}
hotspotsReviewedMeasure={hotspotsReviewedMeasure}
lastStatusChangedTo={lastStatusChangedTo}
loading={loading}

+ 5
- 1
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerRenderer.tsx View File

@@ -24,6 +24,7 @@ import { fillBranchLike } from '../../../helpers/branch-like';
import { Hotspot, HotspotStatusOption } from '../../../types/security-hotspots';
import { Component } from '../../../types/types';
import { CurrentUser } from '../../../types/users';
import { RuleDescriptionSection } from '../../coding-rules/rule';
import { HotspotHeader } from './HotspotHeader';
import HotspotReviewHistoryAndComments from './HotspotReviewHistoryAndComments';
import HotspotSnippetContainer from './HotspotSnippetContainer';
@@ -35,6 +36,7 @@ export interface HotspotViewerRendererProps {
component: Component;
currentUser: CurrentUser;
hotspot?: Hotspot;
ruleDescriptionSections?: RuleDescriptionSection[];
hotspotsReviewedMeasure?: string;
lastStatusChangedTo?: HotspotStatusOption;
loading: boolean;
@@ -58,7 +60,8 @@ export function HotspotViewerRenderer(props: HotspotViewerRendererProps) {
lastStatusChangedTo,
showStatusUpdateSuccessModal,
commentTextRef,
selectedHotspotLocation
selectedHotspotLocation,
ruleDescriptionSections
} = props;

return (
@@ -87,6 +90,7 @@ export function HotspotViewerRenderer(props: HotspotViewerRendererProps) {
/>
}
hotspot={hotspot}
ruleDescriptionSections={ruleDescriptionSections}
selectedHotspotLocation={selectedHotspotLocation}
/>
<HotspotReviewHistoryAndComments

+ 31
- 25
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerTabs.tsx View File

@@ -17,7 +17,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import classNames from 'classnames';
import { groupBy } from 'lodash';
import * as React from 'react';
import BoxedTabs from '../../../components/controls/BoxedTabs';
import { isInput, isShortcut } from '../../../helpers/keyboardEventHelpers';
@@ -25,10 +25,13 @@ import { KeyboardKeys } from '../../../helpers/keycodes';
import { translate } from '../../../helpers/l10n';
import { sanitizeString } from '../../../helpers/sanitize';
import { Hotspot } from '../../../types/security-hotspots';
import RuleContextDescription from '../../../components/rules/RuleContextDescription';
import { RuleDescriptionSection, RuleDescriptionSections } from '../../coding-rules/rule';

interface Props {
codeTabContent: React.ReactNode;
hotspot: Hotspot;
ruleDescriptionSections?: RuleDescriptionSection[];
selectedHotspotLocation?: number;
}

@@ -40,7 +43,7 @@ interface State {
interface Tab {
key: TabKeys;
label: React.ReactNode;
content: string;
descriptionSections: RuleDescriptionSection[];
}

export enum TabKeys {
@@ -114,31 +117,34 @@ export default class HotspotViewerTabs extends React.PureComponent<Props, State>
};

computeTabs() {
const { hotspot } = this.props;
const { ruleDescriptionSections } = this.props;
const groupedDescriptions = groupBy(ruleDescriptionSections, description => description.key);

const descriptionTabs = [
{
key: TabKeys.RiskDescription,
label: translate('hotspots.tabs.risk_description'),
content: hotspot.rule.riskDescription || ''
descriptionSections:
groupedDescriptions[RuleDescriptionSections.DEFAULT] ||
groupedDescriptions[RuleDescriptionSections.ROOT_CAUSE]
},
{
key: TabKeys.VulnerabilityDescription,
label: translate('hotspots.tabs.vulnerability_description'),
content: hotspot.rule.vulnerabilityDescription || ''
descriptionSections: groupedDescriptions[RuleDescriptionSections.ASSESS_THE_PROBLEM]
},
{
key: TabKeys.FixRecommendation,
label: translate('hotspots.tabs.fix_recommendations'),
content: hotspot.rule.fixRecommendations || ''
descriptionSections: groupedDescriptions[RuleDescriptionSections.HOW_TO_FIX]
}
].filter(tab => tab.content.length > 0);
].filter(tab => tab.descriptionSections);

return [
{
key: TabKeys.Code,
label: translate('hotspots.tabs.code'),
content: ''
descriptionSections: []
},
...descriptionTabs
];
@@ -162,27 +168,27 @@ export default class HotspotViewerTabs extends React.PureComponent<Props, State>
render() {
const { codeTabContent } = this.props;
const { tabs, currentTab } = this.state;

return (
<>
<BoxedTabs onSelect={this.handleSelectTabs} selected={currentTab.key} tabs={tabs} />
<div className="bordered huge-spacer-bottom">
<div
className={classNames('padded', {
hidden: currentTab.key !== TabKeys.Code
})}>
{codeTabContent}
</div>
{tabs.slice(1).map(tab => (
<div
key={tab.key}
className={classNames('markdown big-padded', {
hidden: currentTab.key !== tab.key
})}
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: sanitizeString(tab.content) }}
/>
))}
{currentTab.key === TabKeys.Code && <div className="padded">{codeTabContent}</div>}
{currentTab.key !== TabKeys.Code &&
(currentTab.descriptionSections.length === 1 &&
!currentTab.descriptionSections[0].context ? (
<div
key={currentTab.key}
className="markdown big-padded"
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{
__html: sanitizeString(currentTab.descriptionSections[0].content)
}}
/>
) : (
<div className="markdown big-padded">
<RuleContextDescription description={currentTab.descriptionSections} />
</div>
))}
</div>
</>
);

+ 15
- 6
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewer-test.tsx View File

@@ -27,7 +27,7 @@ import { scrollToElement } from '../../../../helpers/scrolling';
import { mockRuleDetails } from '../../../../helpers/testMocks';
import { waitAndUpdate } from '../../../../helpers/testUtils';
import { HotspotStatusOption } from '../../../../types/security-hotspots';
import { RuleDescriptionSections } from '../../../../types/types';
import { RuleDescriptionSections } from '../../../coding-rules/rule';
import HotspotViewer from '../HotspotViewer';
import HotspotViewerRenderer from '../HotspotViewerRenderer';

@@ -85,11 +85,20 @@ it('should render fetch rule details', async () => {
const wrapper = shallowRender();
await waitAndUpdate(wrapper);

expect(wrapper.state().hotspot?.rule).toStrictEqual({
fixRecommendations: 'how',
riskDescription: 'cause',
vulnerabilityDescription: 'assess'
});
expect(wrapper.state().ruleDescriptionSections).toStrictEqual([
{
key: RuleDescriptionSections.ASSESS_THE_PROBLEM,
content: 'assess'
},
{
key: RuleDescriptionSections.ROOT_CAUSE,
content: 'cause'
},
{
key: RuleDescriptionSections.HOW_TO_FIX,
content: 'how'
}
]);
});

it('should refresh hotspot list on status update', () => {

+ 30
- 14
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewerTabs-test.tsx View File

@@ -21,9 +21,10 @@ import { mount, shallow } from 'enzyme';
import * as React from 'react';
import BoxedTabs, { BoxedTabsProps } from '../../../../components/controls/BoxedTabs';
import { KeyboardKeys } from '../../../../helpers/keycodes';
import { mockHotspot, mockHotspotRule } from '../../../../helpers/mocks/security-hotspots';
import { mockHotspot } from '../../../../helpers/mocks/security-hotspots';
import { mockUser } from '../../../../helpers/testMocks';
import { mockEvent } from '../../../../helpers/testUtils';
import { RuleDescriptionSections } from '../../../coding-rules/rule';
import HotspotViewerTabs, { TabKeys } from '../HotspotViewerTabs';

const originalAddEventListener = window.addEventListener;
@@ -62,13 +63,9 @@ it('should render correctly', () => {
expect(
shallowRender({
hotspot: mockHotspot({
creationDate: undefined,
rule: mockHotspotRule({
riskDescription: undefined,
fixRecommendations: undefined,
vulnerabilityDescription: undefined
})
})
creationDate: undefined
}),
ruleDescriptionSections: undefined
})
.find<BoxedTabsProps<string>>(BoxedTabs)
.props().tabs
@@ -95,16 +92,21 @@ it('should render correctly', () => {

it('should filter empty tab', () => {
const count = shallowRender({
hotspot: mockHotspot({
rule: mockHotspotRule()
})
hotspot: mockHotspot()
}).state().tabs.length;

expect(
shallowRender({
hotspot: mockHotspot({
rule: mockHotspotRule({ riskDescription: undefined })
})
ruleDescriptionSections: [
{
key: RuleDescriptionSections.ROOT_CAUSE,
content: 'cause'
},
{
key: RuleDescriptionSections.HOW_TO_FIX,
content: 'how'
}
]
}).state().tabs.length
).toBe(count - 1);
});
@@ -188,6 +190,20 @@ function shallowRender(props?: Partial<HotspotViewerTabs['props']>) {
<HotspotViewerTabs
codeTabContent={<div>CodeTabContent</div>}
hotspot={mockHotspot()}
ruleDescriptionSections={[
{
key: RuleDescriptionSections.ASSESS_THE_PROBLEM,
content: 'assess'
},
{
key: RuleDescriptionSections.ROOT_CAUSE,
content: 'cause'
},
{
key: RuleDescriptionSections.HOW_TO_FIX,
content: 'how'
}
]}
{...props}
/>
);

+ 0
- 6
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotHeader-test.tsx.snap View File

@@ -92,12 +92,9 @@ exports[`should render correctly 1`] = `
},
"resolution": "FIXED",
"rule": Object {
"fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>",
"key": "squid:S2077",
"name": "That rule",
"riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>",
"securityCategory": "sql-injection",
"vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"vulnerabilityProbability": "HIGH",
},
"status": "REVIEWED",
@@ -192,12 +189,9 @@ exports[`should render correctly 1`] = `
},
"resolution": "FIXED",
"rule": Object {
"fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>",
"key": "squid:S2077",
"name": "That rule",
"riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>",
"securityCategory": "sql-injection",
"vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"vulnerabilityProbability": "HIGH",
},
"status": "REVIEWED",

+ 0
- 6
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotReviewHistoryAndComments-test.tsx.snap View File

@@ -93,12 +93,9 @@ exports[`should render correctly 1`] = `
},
"resolution": "FIXED",
"rule": Object {
"fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>",
"key": "squid:S2077",
"name": "That rule",
"riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>",
"securityCategory": "sql-injection",
"vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"vulnerabilityProbability": "HIGH",
},
"status": "REVIEWED",
@@ -197,12 +194,9 @@ exports[`should render correctly without user 1`] = `
},
"resolution": "FIXED",
"rule": Object {
"fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>",
"key": "squid:S2077",
"name": "That rule",
"riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>",
"securityCategory": "sql-injection",
"vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"vulnerabilityProbability": "HIGH",
},
"status": "REVIEWED",

+ 0
- 3
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotSnippetContainer-test.tsx.snap View File

@@ -87,12 +87,9 @@ exports[`should render correctly 1`] = `
},
"resolution": "FIXED",
"rule": Object {
"fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>",
"key": "squid:S2077",
"name": "That rule",
"riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>",
"securityCategory": "sql-injection",
"vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"vulnerabilityProbability": "HIGH",
},
"status": "REVIEWED",

+ 0
- 12
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotSnippetContainerRenderer-test.tsx.snap View File

@@ -92,12 +92,9 @@ exports[`should render correctly 1`] = `
},
"resolution": "FIXED",
"rule": Object {
"fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>",
"key": "squid:S2077",
"name": "That rule",
"riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>",
"securityCategory": "sql-injection",
"vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"vulnerabilityProbability": "HIGH",
},
"status": "REVIEWED",
@@ -228,12 +225,9 @@ exports[`should render correctly when secondary location is selected: with selec
},
"resolution": "FIXED",
"rule": Object {
"fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>",
"key": "squid:S2077",
"name": "That rule",
"riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>",
"securityCategory": "sql-injection",
"vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"vulnerabilityProbability": "HIGH",
},
"status": "REVIEWED",
@@ -359,12 +353,9 @@ exports[`should render correctly: with sourcelines 1`] = `
},
"resolution": "FIXED",
"rule": Object {
"fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>",
"key": "squid:S2077",
"name": "That rule",
"riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>",
"securityCategory": "sql-injection",
"vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"vulnerabilityProbability": "HIGH",
},
"status": "REVIEWED",
@@ -480,12 +471,9 @@ exports[`should render correctly: with sourcelines 1`] = `
},
"resolution": "FIXED",
"rule": Object {
"fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>",
"key": "squid:S2077",
"name": "That rule",
"riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>",
"securityCategory": "sql-injection",
"vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"vulnerabilityProbability": "HIGH",
},
"status": "REVIEWED",

+ 2
- 5
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewer-test.tsx.snap View File

@@ -71,11 +71,7 @@ exports[`should render correctly 2`] = `
hotspot={
Object {
"id": "I am a detailled hotspot",
"rule": Object {
"fixRecommendations": undefined,
"riskDescription": undefined,
"vulnerabilityDescription": undefined,
},
"rule": Object {},
}
}
loading={false}
@@ -84,6 +80,7 @@ exports[`should render correctly 2`] = `
onShowCommentForm={[Function]}
onSwitchFilterToStatusOfUpdatedHotspot={[Function]}
onUpdateHotspot={[Function]}
ruleDescriptionSections={Array []}
showStatusUpdateSuccessModal={false}
/>
`;

+ 0
- 72
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewerRenderer-test.tsx.snap View File

@@ -63,12 +63,9 @@ exports[`should render correctly: anonymous user 1`] = `
},
"resolution": "FIXED",
"rule": Object {
"fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>",
"key": "squid:S2077",
"name": "That rule",
"riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>",
"securityCategory": "sql-injection",
"vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"vulnerabilityProbability": "HIGH",
},
"status": "REVIEWED",
@@ -176,12 +173,9 @@ exports[`should render correctly: anonymous user 1`] = `
},
"resolution": "FIXED",
"rule": Object {
"fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>",
"key": "squid:S2077",
"name": "That rule",
"riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>",
"securityCategory": "sql-injection",
"vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"vulnerabilityProbability": "HIGH",
},
"status": "REVIEWED",
@@ -266,12 +260,9 @@ exports[`should render correctly: anonymous user 1`] = `
},
"resolution": "FIXED",
"rule": Object {
"fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>",
"key": "squid:S2077",
"name": "That rule",
"riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>",
"securityCategory": "sql-injection",
"vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"vulnerabilityProbability": "HIGH",
},
"status": "REVIEWED",
@@ -364,12 +355,9 @@ exports[`should render correctly: anonymous user 1`] = `
},
"resolution": "FIXED",
"rule": Object {
"fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>",
"key": "squid:S2077",
"name": "That rule",
"riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>",
"securityCategory": "sql-injection",
"vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"vulnerabilityProbability": "HIGH",
},
"status": "REVIEWED",
@@ -465,12 +453,9 @@ exports[`should render correctly: assignee without name 1`] = `
},
"resolution": "FIXED",
"rule": Object {
"fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>",
"key": "squid:S2077",
"name": "That rule",
"riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>",
"securityCategory": "sql-injection",
"vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"vulnerabilityProbability": "HIGH",
},
"status": "REVIEWED",
@@ -578,12 +563,9 @@ exports[`should render correctly: assignee without name 1`] = `
},
"resolution": "FIXED",
"rule": Object {
"fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>",
"key": "squid:S2077",
"name": "That rule",
"riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>",
"securityCategory": "sql-injection",
"vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"vulnerabilityProbability": "HIGH",
},
"status": "REVIEWED",
@@ -668,12 +650,9 @@ exports[`should render correctly: assignee without name 1`] = `
},
"resolution": "FIXED",
"rule": Object {
"fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>",
"key": "squid:S2077",
"name": "That rule",
"riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>",
"securityCategory": "sql-injection",
"vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"vulnerabilityProbability": "HIGH",
},
"status": "REVIEWED",
@@ -766,12 +745,9 @@ exports[`should render correctly: assignee without name 1`] = `
},
"resolution": "FIXED",
"rule": Object {
"fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>",
"key": "squid:S2077",
"name": "That rule",
"riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>",
"securityCategory": "sql-injection",
"vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"vulnerabilityProbability": "HIGH",
},
"status": "REVIEWED",
@@ -867,12 +843,9 @@ exports[`should render correctly: default 1`] = `
},
"resolution": "FIXED",
"rule": Object {
"fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>",
"key": "squid:S2077",
"name": "That rule",
"riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>",
"securityCategory": "sql-injection",
"vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"vulnerabilityProbability": "HIGH",
},
"status": "REVIEWED",
@@ -980,12 +953,9 @@ exports[`should render correctly: default 1`] = `
},
"resolution": "FIXED",
"rule": Object {
"fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>",
"key": "squid:S2077",
"name": "That rule",
"riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>",
"securityCategory": "sql-injection",
"vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"vulnerabilityProbability": "HIGH",
},
"status": "REVIEWED",
@@ -1070,12 +1040,9 @@ exports[`should render correctly: default 1`] = `
},
"resolution": "FIXED",
"rule": Object {
"fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>",
"key": "squid:S2077",
"name": "That rule",
"riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>",
"securityCategory": "sql-injection",
"vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"vulnerabilityProbability": "HIGH",
},
"status": "REVIEWED",
@@ -1168,12 +1135,9 @@ exports[`should render correctly: default 1`] = `
},
"resolution": "FIXED",
"rule": Object {
"fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>",
"key": "squid:S2077",
"name": "That rule",
"riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>",
"securityCategory": "sql-injection",
"vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"vulnerabilityProbability": "HIGH",
},
"status": "REVIEWED",
@@ -1269,12 +1233,9 @@ exports[`should render correctly: deleted assignee 1`] = `
},
"resolution": "FIXED",
"rule": Object {
"fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>",
"key": "squid:S2077",
"name": "That rule",
"riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>",
"securityCategory": "sql-injection",
"vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"vulnerabilityProbability": "HIGH",
},
"status": "REVIEWED",
@@ -1382,12 +1343,9 @@ exports[`should render correctly: deleted assignee 1`] = `
},
"resolution": "FIXED",
"rule": Object {
"fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>",
"key": "squid:S2077",
"name": "That rule",
"riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>",
"securityCategory": "sql-injection",
"vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"vulnerabilityProbability": "HIGH",
},
"status": "REVIEWED",
@@ -1472,12 +1430,9 @@ exports[`should render correctly: deleted assignee 1`] = `
},
"resolution": "FIXED",
"rule": Object {
"fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>",
"key": "squid:S2077",
"name": "That rule",
"riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>",
"securityCategory": "sql-injection",
"vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"vulnerabilityProbability": "HIGH",
},
"status": "REVIEWED",
@@ -1570,12 +1525,9 @@ exports[`should render correctly: deleted assignee 1`] = `
},
"resolution": "FIXED",
"rule": Object {
"fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>",
"key": "squid:S2077",
"name": "That rule",
"riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>",
"securityCategory": "sql-injection",
"vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"vulnerabilityProbability": "HIGH",
},
"status": "REVIEWED",
@@ -1684,12 +1636,9 @@ exports[`should render correctly: show success modal 1`] = `
},
"resolution": "FIXED",
"rule": Object {
"fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>",
"key": "squid:S2077",
"name": "That rule",
"riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>",
"securityCategory": "sql-injection",
"vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"vulnerabilityProbability": "HIGH",
},
"status": "REVIEWED",
@@ -1797,12 +1746,9 @@ exports[`should render correctly: show success modal 1`] = `
},
"resolution": "FIXED",
"rule": Object {
"fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>",
"key": "squid:S2077",
"name": "That rule",
"riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>",
"securityCategory": "sql-injection",
"vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"vulnerabilityProbability": "HIGH",
},
"status": "REVIEWED",
@@ -1887,12 +1833,9 @@ exports[`should render correctly: show success modal 1`] = `
},
"resolution": "FIXED",
"rule": Object {
"fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>",
"key": "squid:S2077",
"name": "That rule",
"riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>",
"securityCategory": "sql-injection",
"vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"vulnerabilityProbability": "HIGH",
},
"status": "REVIEWED",
@@ -1985,12 +1928,9 @@ exports[`should render correctly: show success modal 1`] = `
},
"resolution": "FIXED",
"rule": Object {
"fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>",
"key": "squid:S2077",
"name": "That rule",
"riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>",
"securityCategory": "sql-injection",
"vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"vulnerabilityProbability": "HIGH",
},
"status": "REVIEWED",
@@ -2086,12 +2026,9 @@ exports[`should render correctly: unassigned 1`] = `
},
"resolution": "FIXED",
"rule": Object {
"fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>",
"key": "squid:S2077",
"name": "That rule",
"riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>",
"securityCategory": "sql-injection",
"vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"vulnerabilityProbability": "HIGH",
},
"status": "REVIEWED",
@@ -2199,12 +2136,9 @@ exports[`should render correctly: unassigned 1`] = `
},
"resolution": "FIXED",
"rule": Object {
"fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>",
"key": "squid:S2077",
"name": "That rule",
"riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>",
"securityCategory": "sql-injection",
"vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"vulnerabilityProbability": "HIGH",
},
"status": "REVIEWED",
@@ -2289,12 +2223,9 @@ exports[`should render correctly: unassigned 1`] = `
},
"resolution": "FIXED",
"rule": Object {
"fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>",
"key": "squid:S2077",
"name": "That rule",
"riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>",
"securityCategory": "sql-injection",
"vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"vulnerabilityProbability": "HIGH",
},
"status": "REVIEWED",
@@ -2387,12 +2318,9 @@ exports[`should render correctly: unassigned 1`] = `
},
"resolution": "FIXED",
"rule": Object {
"fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>",
"key": "squid:S2077",
"name": "That rule",
"riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>",
"securityCategory": "sql-injection",
"vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"vulnerabilityProbability": "HIGH",
},
"status": "REVIEWED",

+ 78
- 122
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewerTabs-test.tsx.snap View File

@@ -8,22 +8,37 @@ exports[`should render correctly: fix 1`] = `
tabs={
Array [
Object {
"content": "",
"descriptionSections": Array [],
"key": "code",
"label": "hotspots.tabs.code",
},
Object {
"content": "<p>This a <strong>strong</strong> message about risk !</p>",
"descriptionSections": Array [
Object {
"content": "cause",
"key": "root_cause",
},
],
"key": "risk",
"label": "hotspots.tabs.risk_description",
},
Object {
"content": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"descriptionSections": Array [
Object {
"content": "assess",
"key": "assess_the_problem",
},
],
"key": "vulnerability",
"label": "hotspots.tabs.vulnerability_description",
},
Object {
"content": "<p>This a <strong>strong</strong> message about fixing !</p>",
"descriptionSections": Array [
Object {
"content": "how",
"key": "how_to_fix",
},
],
"key": "fix",
"label": "hotspots.tabs.fix_recommendations",
},
@@ -33,36 +48,11 @@ exports[`should render correctly: fix 1`] = `
<div
className="bordered huge-spacer-bottom"
>
<div
className="padded hidden"
>
<div>
CodeTabContent
</div>
</div>
<div
className="markdown big-padded hidden"
dangerouslySetInnerHTML={
Object {
"__html": "<p>This a <strong>strong</strong> message about risk !</p>",
}
}
key="risk"
/>
<div
className="markdown big-padded hidden"
dangerouslySetInnerHTML={
Object {
"__html": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
}
}
key="vulnerability"
/>
<div
className="markdown big-padded"
dangerouslySetInnerHTML={
Object {
"__html": "<p>This a <strong>strong</strong> message about fixing !</p>",
"__html": "how",
}
}
key="fix"
@@ -79,22 +69,37 @@ exports[`should render correctly: risk 1`] = `
tabs={
Array [
Object {
"content": "",
"descriptionSections": Array [],
"key": "code",
"label": "hotspots.tabs.code",
},
Object {
"content": "<p>This a <strong>strong</strong> message about risk !</p>",
"descriptionSections": Array [
Object {
"content": "cause",
"key": "root_cause",
},
],
"key": "risk",
"label": "hotspots.tabs.risk_description",
},
Object {
"content": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"descriptionSections": Array [
Object {
"content": "assess",
"key": "assess_the_problem",
},
],
"key": "vulnerability",
"label": "hotspots.tabs.vulnerability_description",
},
Object {
"content": "<p>This a <strong>strong</strong> message about fixing !</p>",
"descriptionSections": Array [
Object {
"content": "how",
"key": "how_to_fix",
},
],
"key": "fix",
"label": "hotspots.tabs.fix_recommendations",
},
@@ -111,33 +116,6 @@ exports[`should render correctly: risk 1`] = `
CodeTabContent
</div>
</div>
<div
className="markdown big-padded hidden"
dangerouslySetInnerHTML={
Object {
"__html": "<p>This a <strong>strong</strong> message about risk !</p>",
}
}
key="risk"
/>
<div
className="markdown big-padded hidden"
dangerouslySetInnerHTML={
Object {
"__html": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
}
}
key="vulnerability"
/>
<div
className="markdown big-padded hidden"
dangerouslySetInnerHTML={
Object {
"__html": "<p>This a <strong>strong</strong> message about fixing !</p>",
}
}
key="fix"
/>
</div>
</Fragment>
`;
@@ -150,22 +128,37 @@ exports[`should render correctly: vulnerability 1`] = `
tabs={
Array [
Object {
"content": "",
"descriptionSections": Array [],
"key": "code",
"label": "hotspots.tabs.code",
},
Object {
"content": "<p>This a <strong>strong</strong> message about risk !</p>",
"descriptionSections": Array [
Object {
"content": "cause",
"key": "root_cause",
},
],
"key": "risk",
"label": "hotspots.tabs.risk_description",
},
Object {
"content": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"descriptionSections": Array [
Object {
"content": "assess",
"key": "assess_the_problem",
},
],
"key": "vulnerability",
"label": "hotspots.tabs.vulnerability_description",
},
Object {
"content": "<p>This a <strong>strong</strong> message about fixing !</p>",
"descriptionSections": Array [
Object {
"content": "how",
"key": "how_to_fix",
},
],
"key": "fix",
"label": "hotspots.tabs.fix_recommendations",
},
@@ -175,40 +168,15 @@ exports[`should render correctly: vulnerability 1`] = `
<div
className="bordered huge-spacer-bottom"
>
<div
className="padded hidden"
>
<div>
CodeTabContent
</div>
</div>
<div
className="markdown big-padded hidden"
dangerouslySetInnerHTML={
Object {
"__html": "<p>This a <strong>strong</strong> message about risk !</p>",
}
}
key="risk"
/>
<div
className="markdown big-padded"
dangerouslySetInnerHTML={
Object {
"__html": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"__html": "assess",
}
}
key="vulnerability"
/>
<div
className="markdown big-padded hidden"
dangerouslySetInnerHTML={
Object {
"__html": "<p>This a <strong>strong</strong> message about fixing !</p>",
}
}
key="fix"
/>
</div>
</Fragment>
`;
@@ -221,22 +189,37 @@ exports[`should render correctly: with comments or changelog element 1`] = `
tabs={
Array [
Object {
"content": "",
"descriptionSections": Array [],
"key": "code",
"label": "hotspots.tabs.code",
},
Object {
"content": "<p>This a <strong>strong</strong> message about risk !</p>",
"descriptionSections": Array [
Object {
"content": "cause",
"key": "root_cause",
},
],
"key": "risk",
"label": "hotspots.tabs.risk_description",
},
Object {
"content": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"descriptionSections": Array [
Object {
"content": "assess",
"key": "assess_the_problem",
},
],
"key": "vulnerability",
"label": "hotspots.tabs.vulnerability_description",
},
Object {
"content": "<p>This a <strong>strong</strong> message about fixing !</p>",
"descriptionSections": Array [
Object {
"content": "how",
"key": "how_to_fix",
},
],
"key": "fix",
"label": "hotspots.tabs.fix_recommendations",
},
@@ -253,33 +236,6 @@ exports[`should render correctly: with comments or changelog element 1`] = `
CodeTabContent
</div>
</div>
<div
className="markdown big-padded hidden"
dangerouslySetInnerHTML={
Object {
"__html": "<p>This a <strong>strong</strong> message about risk !</p>",
}
}
key="risk"
/>
<div
className="markdown big-padded hidden"
dangerouslySetInnerHTML={
Object {
"__html": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
}
}
key="vulnerability"
/>
<div
className="markdown big-padded hidden"
dangerouslySetInnerHTML={
Object {
"__html": "<p>This a <strong>strong</strong> message about fixing !</p>",
}
}
key="fix"
/>
</div>
</Fragment>
`;

+ 0
- 9
server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/Status-test.tsx.snap View File

@@ -83,12 +83,9 @@ exports[`should render correctly: closed 1`] = `
},
"resolution": "FIXED",
"rule": Object {
"fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>",
"key": "squid:S2077",
"name": "That rule",
"riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>",
"securityCategory": "sql-injection",
"vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"vulnerabilityProbability": "HIGH",
},
"status": "REVIEWED",
@@ -223,12 +220,9 @@ exports[`should render correctly: open 1`] = `
},
"resolution": "FIXED",
"rule": Object {
"fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>",
"key": "squid:S2077",
"name": "That rule",
"riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>",
"securityCategory": "sql-injection",
"vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"vulnerabilityProbability": "HIGH",
},
"status": "REVIEWED",
@@ -363,12 +357,9 @@ exports[`should render correctly: readonly 1`] = `
},
"resolution": "FIXED",
"rule": Object {
"fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>",
"key": "squid:S2077",
"name": "That rule",
"riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>",
"securityCategory": "sql-injection",
"vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"vulnerabilityProbability": "HIGH",
},
"status": "REVIEWED",

+ 121
- 0
server/sonar-web/src/main/js/components/rules/RuleContextDescription.tsx View File

@@ -0,0 +1,121 @@
/*
* 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 * as React from 'react';
import { translate } from '../../helpers/l10n';
import RadioToggle from '../controls/RadioToggle';
import { sanitizeString } from '../../helpers/sanitize';
import { RuleDescriptionSection } from '../../apps/coding-rules/rule';
import OtherContextOption from './OtherContextOption';

const OTHERS_KEY = 'others';

interface Props {
description: RuleDescriptionSection[];
}

interface State {
contexts: RuleDescriptionContextDisplay[];
selectedContext: RuleDescriptionContextDisplay;
}

interface RuleDescriptionContextDisplay {
displayName: string;
content: string;
key: string;
}

export default class RuleContextDescription extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props);
this.state = this.computeState(props.description);
}

componentDidUpdate(prevProps: Props) {
if (prevProps.description !== this.props.description) {
this.setState(this.computeState(this.props.description));
}
}

computeState = (descriptions: RuleDescriptionSection[]) => {
const contexts = descriptions
.map(sec => ({
displayName: sec.context?.displayName || '',
content: sec.content,
key: sec.key.toString()
}))
.filter(sec => sec.displayName !== '')
.sort((a, b) => a.displayName.localeCompare(b.displayName));

if (contexts.length > 0) {
contexts.push({
displayName: translate('coding_rules.description_context_other'),
content: '',
key: OTHERS_KEY
});
}

return {
contexts,
selectedContext: contexts[0]
};
};

handleToggleContext = (value: string) => {
const { contexts } = this.state;

const selected = contexts.find(ctxt => ctxt.displayName === value);
if (selected) {
this.setState({ selectedContext: selected });
}
};

render() {
const { contexts } = this.state;
const { selectedContext } = this.state;

const options = contexts.map(ctxt => ({
label: ctxt.displayName,
value: ctxt.displayName
}));

return (
<div className="rules-context-description">
<h2 className="rule-contexts-title">
{translate('coding_rules.description_context_title')}
</h2>
<RadioToggle
className="big-spacer-bottom"
name="filter"
onCheck={this.handleToggleContext}
options={options}
value={selectedContext.displayName}
/>
{selectedContext.key === OTHERS_KEY ? (
<OtherContextOption />
) : (
<div
/* eslint-disable-next-line react/no-danger */
dangerouslySetInnerHTML={{ __html: sanitizeString(selectedContext.content) }}
/>
)}
</div>
);
}
}

+ 0
- 3
server/sonar-web/src/main/js/helpers/mocks/security-hotspots.ts View File

@@ -113,9 +113,6 @@ export function mockHotspotRule(overrides?: Partial<HotspotRule>): HotspotRule {
return {
key: 'squid:S2077',
name: 'That rule',
fixRecommendations: '<p>This a <strong>strong</strong> message about fixing !</p>',
riskDescription: '<p>This a <strong>strong</strong> message about risk !</p>',
vulnerabilityDescription: '<p>This a <strong>strong</strong> message about vulnerability !</p>',
vulnerabilityProbability: RiskExposure.HIGH,
securityCategory: 'sql-injection',
...overrides

+ 1
- 1
server/sonar-web/src/main/js/helpers/testMocks.ts View File

@@ -18,6 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { To } from 'react-router-dom';
import { RuleDescriptionSections } from '../apps/coding-rules/rule';
import { DocumentationEntry } from '../apps/documentation/utils';
import { Exporter, Profile } from '../apps/quality-profiles/types';
import { Location, Router } from '../components/hoc/withRouter';
@@ -47,7 +48,6 @@ import {
ProfileInheritanceDetails,
Rule,
RuleActivation,
RuleDescriptionSections,
RuleDetails,
RuleParameter,
SysInfoBase,

+ 0
- 3
server/sonar-web/src/main/js/types/security-hotspots.ts View File

@@ -121,12 +121,9 @@ export interface HotspotUpdate extends HotspotUpdateFields {
}

export interface HotspotRule {
fixRecommendations?: string;
key: string;
name: string;
riskDescription?: string;
securityCategory: string;
vulnerabilityDescription?: string;
vulnerabilityProbability: RiskExposure;
}


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

@@ -18,6 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

import { RuleDescriptionSection } from '../apps/coding-rules/rule';
import { ComponentQualifier } from './component';
import { UserActive, UserBase } from './users';

@@ -562,19 +563,6 @@ 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;

+ 2
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -1912,6 +1912,8 @@ 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

coding_rules.description_context_title=Which component or framework contains the issue?
coding_rules.description_context_other=Other

#------------------------------------------------------------------------------
#

Loading…
Cancel
Save