aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPhilippe Perrin <philippe.perrin@sonarsource.com>2022-07-06 15:05:04 +0200
committersonartech <sonartech@sonarsource.com>2022-07-08 20:02:48 +0000
commit6bd83879b74dac63c4683e8b9f8703cbb3f7b617 (patch)
tree946eb21a4e93b1cfa6bacd0b7d7835282c0450f8
parentf2fecdae009dd0572fa13cd76ac2903b590265b0 (diff)
downloadsonarqube-6bd83879b74dac63c4683e8b9f8703cbb3f7b617.tar.gz
sonarqube-6bd83879b74dac63c4683e8b9f8703cbb3f7b617.zip
SONAR-16614 Display the most relevant rule description context for an issue
-rw-r--r--server/sonar-web/src/main/js/api/mocks/CodingRulesMock.ts4
-rw-r--r--server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts6
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/RuleTabViewer.tsx25
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/rule.ts1
-rw-r--r--server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx31
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/IssueTabViewer.tsx41
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerTabs.tsx20
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewerTabs-test.tsx.snap56
-rw-r--r--server/sonar-web/src/main/js/components/rules/MoreInfoRuleDescription.tsx8
-rw-r--r--server/sonar-web/src/main/js/components/rules/RuleDescription.tsx122
-rw-r--r--server/sonar-web/src/main/js/types/issues.ts1
-rw-r--r--server/sonar-web/src/main/js/types/types.ts1
-rw-r--r--sonar-core/src/main/resources/org/sonar/l10n/core.properties1
13 files changed, 195 insertions, 122 deletions
diff --git a/server/sonar-web/src/main/js/api/mocks/CodingRulesMock.ts b/server/sonar-web/src/main/js/api/mocks/CodingRulesMock.ts
index 1c64cb8aca2..f6e2563257a 100644
--- a/server/sonar-web/src/main/js/api/mocks/CodingRulesMock.ts
+++ b/server/sonar-web/src/main/js/api/mocks/CodingRulesMock.ts
@@ -134,12 +134,12 @@ export default class CodingRulesMock {
{
key: RuleDescriptionSections.HOW_TO_FIX,
content: 'This how to fix for spring',
- context: { displayName: 'Spring' }
+ context: { key: 'spring', displayName: 'Spring' }
},
{
key: RuleDescriptionSections.HOW_TO_FIX,
content: 'This how to fix for spring boot',
- context: { displayName: 'Spring boot' }
+ context: { key: 'spring_boot', displayName: 'Spring boot' }
},
{
key: RuleDescriptionSections.RESOURCES,
diff --git a/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts
index 0957a3b5f80..054ca7cde59 100644
--- a/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts
+++ b/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts
@@ -146,7 +146,8 @@ export default class IssuesServiceMock {
endLine: 25,
startOffset: 0,
endOffset: 1
- }
+ },
+ ruleDescriptionContextKey: 'spring'
}),
snippets: keyBy(
[
@@ -253,6 +254,7 @@ export default class IssuesServiceMock {
content: '<p> Context 1 content<p>',
key: RuleDescriptionSections.HOW_TO_FIX,
context: {
+ key: 'spring',
displayName: 'Spring'
}
},
@@ -260,6 +262,7 @@ export default class IssuesServiceMock {
content: '<p> Context 2 content<p>',
key: RuleDescriptionSections.HOW_TO_FIX,
context: {
+ key: 'context_2',
displayName: 'Context 2'
}
},
@@ -267,6 +270,7 @@ export default class IssuesServiceMock {
content: '<p> Context 3 content<p>',
key: RuleDescriptionSections.HOW_TO_FIX,
context: {
+ key: 'context_3',
displayName: 'Context 3'
}
},
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleTabViewer.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleTabViewer.tsx
index 3c3e647ef73..3422bce50d5 100644
--- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleTabViewer.tsx
+++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleTabViewer.tsx
@@ -69,7 +69,10 @@ export default class RuleViewerTabs extends React.PureComponent<Props, State> {
computeState() {
const { ruleDetails } = this.props;
- const groupedDescriptions = groupBy(ruleDetails.descriptionSections, 'key');
+ const descriptionSectionsByKey = groupBy(
+ ruleDetails.descriptionSections,
+ section => section.key
+ );
const tabs = [
{
@@ -78,34 +81,38 @@ 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: groupedDescriptions[RuleDescriptionSections.ROOT_CAUSE] && (
- <RuleDescription description={groupedDescriptions[RuleDescriptionSections.ROOT_CAUSE]} />
+ content: descriptionSectionsByKey[RuleDescriptionSections.ROOT_CAUSE] && (
+ <RuleDescription
+ sections={descriptionSectionsByKey[RuleDescriptionSections.ROOT_CAUSE]}
+ />
)
},
{
key: RuleTabKeys.AssessTheIssue,
label: translate('coding_rules.description_section.title', RuleTabKeys.AssessTheIssue),
- content: groupedDescriptions[RuleDescriptionSections.ASSESS_THE_PROBLEM] && (
+ content: descriptionSectionsByKey[RuleDescriptionSections.ASSESS_THE_PROBLEM] && (
<RuleDescription
- description={groupedDescriptions[RuleDescriptionSections.ASSESS_THE_PROBLEM]}
+ sections={descriptionSectionsByKey[RuleDescriptionSections.ASSESS_THE_PROBLEM]}
/>
)
},
{
key: RuleTabKeys.HowToFixIt,
label: translate('coding_rules.description_section.title', RuleTabKeys.HowToFixIt),
- content: groupedDescriptions[RuleDescriptionSections.HOW_TO_FIX] && (
- <RuleDescription description={groupedDescriptions[RuleDescriptionSections.HOW_TO_FIX]} />
+ content: descriptionSectionsByKey[RuleDescriptionSections.HOW_TO_FIX] && (
+ <RuleDescription
+ sections={descriptionSectionsByKey[RuleDescriptionSections.HOW_TO_FIX]}
+ />
)
},
{
key: RuleTabKeys.MoreInfo,
label: translate('coding_rules.description_section.title', RuleTabKeys.MoreInfo),
content: (ruleDetails.genericConcepts ||
- groupedDescriptions[RuleDescriptionSections.RESOURCES]) && (
+ descriptionSectionsByKey[RuleDescriptionSections.RESOURCES]) && (
<MoreInfoRuleDescription
genericConcepts={ruleDetails.genericConcepts}
- description={groupedDescriptions[RuleDescriptionSections.RESOURCES]}
+ sections={descriptionSectionsByKey[RuleDescriptionSections.RESOURCES]}
/>
)
}
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/rule.ts b/server/sonar-web/src/main/js/apps/coding-rules/rule.ts
index a6d2310a637..c0482e6d22e 100644
--- a/server/sonar-web/src/main/js/apps/coding-rules/rule.ts
+++ b/server/sonar-web/src/main/js/apps/coding-rules/rule.ts
@@ -27,6 +27,7 @@ export enum RuleDescriptionSections {
}
export interface RuleDescriptionContext {
+ key: string;
displayName: string;
}
diff --git a/server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx b/server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx
index 36f41a9600f..40e6a7092fb 100644
--- a/server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx
@@ -47,51 +47,70 @@ it('should show generic concpet', async () => {
it('should open issue and navigate', async () => {
const user = userEvent.setup();
+
renderIssueApp();
+
+ // Select an issue with an advanced rule
expect(await screen.findByRole('region', { name: 'Fix that' })).toBeInTheDocument();
await user.click(screen.getByRole('region', { name: 'Fix that' }));
+
+ // Are rule headers present?
expect(screen.getByRole('heading', { level: 1, name: 'Fix that' })).toBeInTheDocument();
expect(screen.getByRole('link', { name: 'advancedRuleId' })).toBeInTheDocument();
- expect(screen.getByRole('button', { name: `issue.tabs.more_info` })).toBeInTheDocument();
- await user.click(screen.getByRole('button', { name: `issue.tabs.more_info` }));
- expect(screen.getByRole('heading', { name: 'Link' })).toBeInTheDocument();
+ // Select the "why is this an issue" tab and check its content
+ expect(screen.getByRole('button', { name: `issue.tabs.why` })).toBeInTheDocument();
+ await user.click(screen.getByRole('button', { name: `issue.tabs.why` }));
+ expect(screen.getByRole('heading', { name: 'Because' })).toBeInTheDocument();
+ // Select the "how to fix it" tab
expect(screen.getByRole('button', { name: `issue.tabs.how` })).toBeInTheDocument();
await user.click(screen.getByRole('button', { name: `issue.tabs.how` }));
+
+ // Is the context selector present with the expected values and default selection?
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();
+ expect(screen.getByRole('radio', { name: 'Spring' })).toBeChecked();
+ // Select context 2 and check tab content
await user.click(screen.getByRole('radio', { name: 'Context 2' }));
expect(screen.getByText('Context 2 content')).toBeInTheDocument();
+ // Select the "other" context and check tab content
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` }));
- expect(screen.getByRole('heading', { name: 'Because' })).toBeInTheDocument();
+ // Select the resources tab and check its content
+ expect(screen.getByRole('button', { name: `issue.tabs.more_info` })).toBeInTheDocument();
+ await user.click(screen.getByRole('button', { name: `issue.tabs.more_info` }));
+ expect(screen.getByRole('heading', { name: 'Link' })).toBeInTheDocument();
+ // Select the previous issue (with a simple rule) through keyboard shortcut
await user.keyboard('{ArrowUp}');
+ // Are rule headers present?
expect(screen.getByRole('heading', { level: 1, name: 'Fix this' })).toBeInTheDocument();
expect(screen.getByRole('link', { name: 'simpleRuleId' })).toBeInTheDocument();
+ // Select the "why is this an issue tab" and check its content
expect(screen.getByRole('button', { name: `issue.tabs.why` })).toBeInTheDocument();
await user.click(screen.getByRole('button', { name: `issue.tabs.why` }));
expect(screen.getByRole('heading', { name: 'Default' })).toBeInTheDocument();
+ // Select the previous issue (with a simple rule) through keyboard shortcut
await user.keyboard('{ArrowUp}');
+ // Are rule headers present?
expect(screen.getByRole('heading', { level: 1, name: 'Issue on file' })).toBeInTheDocument();
expect(screen.getByRole('link', { name: 'simpleRuleId' })).toBeInTheDocument();
+ // Select the "Where is the issue" tab and check its content
expect(screen.getByRole('button', { name: `issue.tabs.code` })).toBeInTheDocument();
await user.click(screen.getByRole('button', { name: `issue.tabs.code` }));
expect(screen.getByRole('region', { name: 'Issue on file' })).toBeInTheDocument();
diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssueTabViewer.tsx b/server/sonar-web/src/main/js/apps/issues/components/IssueTabViewer.tsx
index 4821f37896c..cfd70022e55 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/IssueTabViewer.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/components/IssueTabViewer.tsx
@@ -82,17 +82,24 @@ export default class IssueViewerTabs extends React.PureComponent<Props, State> {
};
computeTabs() {
- const { ruleDetails, codeTabContent } = this.props;
- const groupedDescriptions = groupBy(ruleDetails.descriptionSections, 'key');
+ const {
+ ruleDetails,
+ codeTabContent,
+ issue: { ruleDescriptionContextKey }
+ } = this.props;
+ const descriptionSectionsByKey = groupBy(
+ ruleDetails.descriptionSections,
+ section => section.key
+ );
if (ruleDetails.htmlNote) {
- if (groupedDescriptions[RuleDescriptionSections.RESOURCES] !== undefined) {
+ if (descriptionSectionsByKey[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 +=
+ descriptionSectionsByKey[RuleDescriptionSections.RESOURCES][0].content +=
'<br/>' + ruleDetails.htmlNote;
} else {
- groupedDescriptions[RuleDescriptionSections.RESOURCES] = [
+ descriptionSectionsByKey[RuleDescriptionSections.RESOURCES] = [
{
key: RuleDescriptionSections.RESOURCES,
content: ruleDetails.htmlNote
@@ -101,9 +108,9 @@ export default class IssueViewerTabs extends React.PureComponent<Props, State> {
}
}
- const rootCause =
- groupedDescriptions[RuleDescriptionSections.DEFAULT] ||
- groupedDescriptions[RuleDescriptionSections.ROOT_CAUSE];
+ const rootCauseDescriptionSections =
+ descriptionSectionsByKey[RuleDescriptionSections.DEFAULT] ||
+ descriptionSectionsByKey[RuleDescriptionSections.ROOT_CAUSE];
return [
{
@@ -114,28 +121,32 @@ export default class IssueViewerTabs extends React.PureComponent<Props, State> {
{
key: IssueTabKeys.WhyIsThisAnIssue,
label: translate('issue.tabs', IssueTabKeys.WhyIsThisAnIssue),
- content: rootCause && (
+ content: rootCauseDescriptionSections && (
<RuleDescription
- description={rootCause}
- isDefault={groupedDescriptions[RuleDescriptionSections.DEFAULT] !== undefined}
+ sections={rootCauseDescriptionSections}
+ isDefault={descriptionSectionsByKey[RuleDescriptionSections.DEFAULT] !== undefined}
+ defaultContextKey={ruleDescriptionContextKey}
/>
)
},
{
key: IssueTabKeys.HowToFixIt,
label: translate('issue.tabs', IssueTabKeys.HowToFixIt),
- content: groupedDescriptions[RuleDescriptionSections.HOW_TO_FIX] && (
- <RuleDescription description={groupedDescriptions[RuleDescriptionSections.HOW_TO_FIX]} />
+ content: descriptionSectionsByKey[RuleDescriptionSections.HOW_TO_FIX] && (
+ <RuleDescription
+ sections={descriptionSectionsByKey[RuleDescriptionSections.HOW_TO_FIX]}
+ defaultContextKey={ruleDescriptionContextKey}
+ />
)
},
{
key: IssueTabKeys.MoreInfo,
label: translate('issue.tabs', IssueTabKeys.MoreInfo),
content: (ruleDetails.genericConcepts ||
- groupedDescriptions[RuleDescriptionSections.RESOURCES]) && (
+ descriptionSectionsByKey[RuleDescriptionSections.RESOURCES]) && (
<MoreInfoRuleDescription
genericConcepts={ruleDetails.genericConcepts}
- description={groupedDescriptions[RuleDescriptionSections.RESOURCES]}
+ sections={descriptionSectionsByKey[RuleDescriptionSections.RESOURCES]}
/>
)
}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerTabs.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerTabs.tsx
index 7e53f144e69..e57ac147357 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerTabs.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerTabs.tsx
@@ -120,10 +120,10 @@ export default class HotspotViewerTabs extends React.PureComponent<Props, State>
computeTabs() {
const { ruleDescriptionSections, codeTabContent } = this.props;
- const groupedDescriptions = groupBy(ruleDescriptionSections, description => description.key);
- const rootCause =
- groupedDescriptions[RuleDescriptionSections.DEFAULT] ||
- groupedDescriptions[RuleDescriptionSections.ROOT_CAUSE];
+ const descriptionSectionsByKey = groupBy(ruleDescriptionSections, section => section.key);
+ const rootCauseDescriptionSections =
+ descriptionSectionsByKey[RuleDescriptionSections.DEFAULT] ||
+ descriptionSectionsByKey[RuleDescriptionSections.ROOT_CAUSE];
return [
{
@@ -134,14 +134,16 @@ export default class HotspotViewerTabs extends React.PureComponent<Props, State>
{
key: TabKeys.RiskDescription,
label: translate('hotspots.tabs.risk_description'),
- content: rootCause && <RuleDescription description={rootCause} isDefault={true} />
+ content: rootCauseDescriptionSections && (
+ <RuleDescription sections={rootCauseDescriptionSections} isDefault={true} />
+ )
},
{
key: TabKeys.VulnerabilityDescription,
label: translate('hotspots.tabs.vulnerability_description'),
- content: groupedDescriptions[RuleDescriptionSections.ASSESS_THE_PROBLEM] && (
+ content: descriptionSectionsByKey[RuleDescriptionSections.ASSESS_THE_PROBLEM] && (
<RuleDescription
- description={groupedDescriptions[RuleDescriptionSections.ASSESS_THE_PROBLEM]}
+ sections={descriptionSectionsByKey[RuleDescriptionSections.ASSESS_THE_PROBLEM]}
isDefault={true}
/>
)
@@ -149,9 +151,9 @@ export default class HotspotViewerTabs extends React.PureComponent<Props, State>
{
key: TabKeys.FixRecommendation,
label: translate('hotspots.tabs.fix_recommendations'),
- content: groupedDescriptions[RuleDescriptionSections.HOW_TO_FIX] && (
+ content: descriptionSectionsByKey[RuleDescriptionSections.HOW_TO_FIX] && (
<RuleDescription
- description={groupedDescriptions[RuleDescriptionSections.HOW_TO_FIX]}
+ sections={descriptionSectionsByKey[RuleDescriptionSections.HOW_TO_FIX]}
isDefault={true}
/>
)
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewerTabs-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewerTabs-test.tsx.snap
index ab90bf8c2ba..844b9f05c07 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewerTabs-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewerTabs-test.tsx.snap
@@ -20,7 +20,8 @@ exports[`should render correctly: fix 1`] = `
},
Object {
"content": <RuleDescription
- description={
+ isDefault={true}
+ sections={
Array [
Object {
"content": "cause",
@@ -28,14 +29,14 @@ exports[`should render correctly: fix 1`] = `
},
]
}
- isDefault={true}
/>,
"key": "risk",
"label": "hotspots.tabs.risk_description",
},
Object {
"content": <RuleDescription
- description={
+ isDefault={true}
+ sections={
Array [
Object {
"content": "assess",
@@ -43,14 +44,14 @@ exports[`should render correctly: fix 1`] = `
},
]
}
- isDefault={true}
/>,
"key": "vulnerability",
"label": "hotspots.tabs.vulnerability_description",
},
Object {
"content": <RuleDescription
- description={
+ isDefault={true}
+ sections={
Array [
Object {
"content": "how",
@@ -58,7 +59,6 @@ exports[`should render correctly: fix 1`] = `
},
]
}
- isDefault={true}
/>,
"key": "fix",
"label": "hotspots.tabs.fix_recommendations",
@@ -70,7 +70,8 @@ exports[`should render correctly: fix 1`] = `
className="bordered huge-spacer-bottom"
>
<RuleDescription
- description={
+ isDefault={true}
+ sections={
Array [
Object {
"content": "how",
@@ -78,7 +79,6 @@ exports[`should render correctly: fix 1`] = `
},
]
}
- isDefault={true}
/>
</div>
</Fragment>
@@ -104,7 +104,8 @@ exports[`should render correctly: risk 1`] = `
},
Object {
"content": <RuleDescription
- description={
+ isDefault={true}
+ sections={
Array [
Object {
"content": "cause",
@@ -112,14 +113,14 @@ exports[`should render correctly: risk 1`] = `
},
]
}
- isDefault={true}
/>,
"key": "risk",
"label": "hotspots.tabs.risk_description",
},
Object {
"content": <RuleDescription
- description={
+ isDefault={true}
+ sections={
Array [
Object {
"content": "assess",
@@ -127,14 +128,14 @@ exports[`should render correctly: risk 1`] = `
},
]
}
- isDefault={true}
/>,
"key": "vulnerability",
"label": "hotspots.tabs.vulnerability_description",
},
Object {
"content": <RuleDescription
- description={
+ isDefault={true}
+ sections={
Array [
Object {
"content": "how",
@@ -142,7 +143,6 @@ exports[`should render correctly: risk 1`] = `
},
]
}
- isDefault={true}
/>,
"key": "fix",
"label": "hotspots.tabs.fix_recommendations",
@@ -184,7 +184,8 @@ exports[`should render correctly: vulnerability 1`] = `
},
Object {
"content": <RuleDescription
- description={
+ isDefault={true}
+ sections={
Array [
Object {
"content": "cause",
@@ -192,14 +193,14 @@ exports[`should render correctly: vulnerability 1`] = `
},
]
}
- isDefault={true}
/>,
"key": "risk",
"label": "hotspots.tabs.risk_description",
},
Object {
"content": <RuleDescription
- description={
+ isDefault={true}
+ sections={
Array [
Object {
"content": "assess",
@@ -207,14 +208,14 @@ exports[`should render correctly: vulnerability 1`] = `
},
]
}
- isDefault={true}
/>,
"key": "vulnerability",
"label": "hotspots.tabs.vulnerability_description",
},
Object {
"content": <RuleDescription
- description={
+ isDefault={true}
+ sections={
Array [
Object {
"content": "how",
@@ -222,7 +223,6 @@ exports[`should render correctly: vulnerability 1`] = `
},
]
}
- isDefault={true}
/>,
"key": "fix",
"label": "hotspots.tabs.fix_recommendations",
@@ -234,7 +234,8 @@ exports[`should render correctly: vulnerability 1`] = `
className="bordered huge-spacer-bottom"
>
<RuleDescription
- description={
+ isDefault={true}
+ sections={
Array [
Object {
"content": "assess",
@@ -242,7 +243,6 @@ exports[`should render correctly: vulnerability 1`] = `
},
]
}
- isDefault={true}
/>
</div>
</Fragment>
@@ -268,7 +268,8 @@ exports[`should render correctly: with comments or changelog element 1`] = `
},
Object {
"content": <RuleDescription
- description={
+ isDefault={true}
+ sections={
Array [
Object {
"content": "cause",
@@ -276,14 +277,14 @@ exports[`should render correctly: with comments or changelog element 1`] = `
},
]
}
- isDefault={true}
/>,
"key": "risk",
"label": "hotspots.tabs.risk_description",
},
Object {
"content": <RuleDescription
- description={
+ isDefault={true}
+ sections={
Array [
Object {
"content": "assess",
@@ -291,14 +292,14 @@ exports[`should render correctly: with comments or changelog element 1`] = `
},
]
}
- isDefault={true}
/>,
"key": "vulnerability",
"label": "hotspots.tabs.vulnerability_description",
},
Object {
"content": <RuleDescription
- description={
+ isDefault={true}
+ sections={
Array [
Object {
"content": "how",
@@ -306,7 +307,6 @@ exports[`should render correctly: with comments or changelog element 1`] = `
},
]
}
- isDefault={true}
/>,
"key": "fix",
"label": "hotspots.tabs.fix_recommendations",
diff --git a/server/sonar-web/src/main/js/components/rules/MoreInfoRuleDescription.tsx b/server/sonar-web/src/main/js/components/rules/MoreInfoRuleDescription.tsx
index bafca86796f..7e51817a4f4 100644
--- a/server/sonar-web/src/main/js/components/rules/MoreInfoRuleDescription.tsx
+++ b/server/sonar-web/src/main/js/components/rules/MoreInfoRuleDescription.tsx
@@ -27,7 +27,7 @@ import RuleDescription from './RuleDescription';
import './style.css';
interface Props {
- description?: RuleDescriptionSection[];
+ sections?: RuleDescriptionSection[];
genericConcepts?: string[];
}
@@ -36,17 +36,17 @@ const GENERIC_CONCPET_MAP: Dict<React.ComponentType> = {
least_trust_principle: LeastTrustPrinciple
};
-export default function MoreInfoRuleDescription({ description = [], genericConcepts = [] }: Props) {
+export default function MoreInfoRuleDescription({ sections = [], genericConcepts = [] }: Props) {
return (
<>
- {description.length > 0 && (
+ {sections.length > 0 && (
<>
<div className="big-padded-left big-padded-right big-padded-top rule-desc">
<h2 className="null-spacer-bottom">
{translate('coding_rules.more_info.resources.title')}
</h2>
</div>
- <RuleDescription key="more-info" description={description} />
+ <RuleDescription key="more-info" sections={sections} />
</>
)}
diff --git a/server/sonar-web/src/main/js/components/rules/RuleDescription.tsx b/server/sonar-web/src/main/js/components/rules/RuleDescription.tsx
index 8935766ded8..fcf0b3784c0 100644
--- a/server/sonar-web/src/main/js/components/rules/RuleDescription.tsx
+++ b/server/sonar-web/src/main/js/components/rules/RuleDescription.tsx
@@ -20,20 +20,23 @@
import classNames from 'classnames';
import * as React from 'react';
import { RuleDescriptionSection } from '../../apps/coding-rules/rule';
-import { translate } from '../../helpers/l10n';
+import { translate, translateWithParameters } from '../../helpers/l10n';
import { sanitizeString } from '../../helpers/sanitize';
import RadioToggle from '../controls/RadioToggle';
+import { Alert } from '../ui/Alert';
import OtherContextOption from './OtherContextOption';
const OTHERS_KEY = 'others';
interface Props {
isDefault?: boolean;
- description: RuleDescriptionSection[];
+ sections: RuleDescriptionSection[];
+ defaultContextKey?: string;
}
interface State {
contexts: RuleDescriptionContextDisplay[];
+ defaultContext?: RuleDescriptionContextDisplay;
selectedContext?: RuleDescriptionContextDisplay;
}
@@ -46,23 +49,32 @@ interface RuleDescriptionContextDisplay {
export default class RuleDescription extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props);
- this.state = this.computeState(props.description);
+ this.state = this.computeState();
}
componentDidUpdate(prevProps: Props) {
- if (prevProps.description !== this.props.description) {
- this.setState(this.computeState(this.props.description));
+ const { sections, defaultContextKey } = this.props;
+
+ if (prevProps.sections !== sections || prevProps.defaultContextKey !== defaultContextKey) {
+ this.setState(this.computeState());
}
}
- computeState = (descriptions: RuleDescriptionSection[]) => {
- const contexts = descriptions
- .map(sec => ({
- displayName: sec.context?.displayName || '',
- content: sec.content,
- key: sec.key.toString()
+ computeState = () => {
+ const { sections, defaultContextKey } = this.props;
+
+ const contexts = sections
+ .filter(
+ (
+ section
+ ): section is RuleDescriptionSection & Required<Pick<RuleDescriptionSection, 'context'>> =>
+ section.context != null
+ )
+ .map(section => ({
+ displayName: section.context.displayName || section.context.key,
+ content: section.content,
+ key: section.context.key
}))
- .filter(sec => sec.displayName !== '')
.sort((a, b) => a.displayName.localeCompare(b.displayName));
if (contexts.length > 0) {
@@ -73,9 +85,16 @@ export default class RuleDescription extends React.PureComponent<Props, State> {
});
}
+ let defaultContext: RuleDescriptionContextDisplay | undefined;
+
+ if (defaultContextKey) {
+ defaultContext = contexts.find(context => context.key === defaultContextKey);
+ }
+
return {
contexts,
- selectedContext: contexts[0]
+ defaultContext,
+ selectedContext: defaultContext ?? contexts[0]
};
};
@@ -89,59 +108,66 @@ export default class RuleDescription extends React.PureComponent<Props, State> {
};
render() {
- const { description, isDefault } = this.props;
- const { contexts } = this.state;
- const { selectedContext } = this.state;
+ const { sections, isDefault } = this.props;
+ const { contexts, defaultContext, selectedContext } = this.state;
const options = contexts.map(ctxt => ({
label: ctxt.displayName,
value: ctxt.displayName
}));
- if (!description[0].context && description.length === 1) {
+ if (contexts.length > 0 && selectedContext) {
return (
<div
className={classNames('big-padded', {
markdown: isDefault,
'rule-desc': !isDefault
- })}
- // eslint-disable-next-line react/no-danger
- dangerouslySetInnerHTML={{
- __html: sanitizeString(description[0].content)
- }}
- />
+ })}>
+ <div className="rules-context-description">
+ <h2 className="rule-contexts-title">
+ {translate('coding_rules.description_context_title')}
+ </h2>
+ {defaultContext && (
+ <Alert variant="info" display="inline" className="big-spacer-bottom">
+ {translateWithParameters(
+ 'coding_rules.description_context_default_information',
+ defaultContext.displayName
+ )}
+ </Alert>
+ )}
+ <div>
+ <RadioToggle
+ className="big-spacer-bottom"
+ name="filter"
+ onCheck={this.handleToggleContext}
+ options={options}
+ value={selectedContext.displayName}
+ />
+ </div>
+ {selectedContext.key === OTHERS_KEY ? (
+ <OtherContextOption />
+ ) : (
+ <div
+ /* eslint-disable-next-line react/no-danger */
+ dangerouslySetInnerHTML={{ __html: sanitizeString(selectedContext.content) }}
+ />
+ )}
+ </div>
+ </div>
);
}
- if (!selectedContext) {
- return null;
- }
+
return (
<div
className={classNames('big-padded', {
markdown: isDefault,
'rule-desc': !isDefault
- })}>
- <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>
- </div>
+ })}
+ // eslint-disable-next-line react/no-danger
+ dangerouslySetInnerHTML={{
+ __html: sanitizeString(sections[0].content)
+ }}
+ />
);
}
}
diff --git a/server/sonar-web/src/main/js/types/issues.ts b/server/sonar-web/src/main/js/types/issues.ts
index ea82a595d9f..eca49843e04 100644
--- a/server/sonar-web/src/main/js/types/issues.ts
+++ b/server/sonar-web/src/main/js/types/issues.ts
@@ -60,6 +60,7 @@ export interface RawIssue {
status: string;
textRange?: TextRange;
type: IssueType;
+ ruleDescriptionContextKey?: string;
}
export interface IssueResponse {
diff --git a/server/sonar-web/src/main/js/types/types.ts b/server/sonar-web/src/main/js/types/types.ts
index 64d9b22c788..ba2717c72de 100644
--- a/server/sonar-web/src/main/js/types/types.ts
+++ b/server/sonar-web/src/main/js/types/types.ts
@@ -285,6 +285,7 @@ export interface Issue {
pullRequest?: string;
resolution?: string;
rule: string;
+ ruleDescriptionContextKey?: string;
ruleName: string;
ruleStatus?: string;
secondaryLocations: FlowLocation[];
diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
index 2815bb88e1c..add27b64310 100644
--- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties
+++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
@@ -1913,6 +1913,7 @@ coding_rules.description_section.title.how_to_fix=How to fix it?
coding_rules.description_section.title.more_info=More Info
coding_rules.description_context_title=Which component or framework contains the issue?
+coding_rules.description_context_default_information={0} was detected as the most relevant component or framework for this issue.
coding_rules.description_context_other=Other
coding_rules.more_info.generic_concept.title=Security principles