diff options
author | Philippe Perrin <philippe.perrin@sonarsource.com> | 2022-07-06 15:05:04 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2022-07-08 20:02:48 +0000 |
commit | 6bd83879b74dac63c4683e8b9f8703cbb3f7b617 (patch) | |
tree | 946eb21a4e93b1cfa6bacd0b7d7835282c0450f8 | |
parent | f2fecdae009dd0572fa13cd76ac2903b590265b0 (diff) | |
download | sonarqube-6bd83879b74dac63c4683e8b9f8703cbb3f7b617.tar.gz sonarqube-6bd83879b74dac63c4683e8b9f8703cbb3f7b617.zip |
SONAR-16614 Display the most relevant rule description context for an issue
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 |