Browse Source

SONAR-16519 Add tab description for advance rule in rules page

tags/9.6.0.59041
Mathieu Suen 1 year ago
parent
commit
bb3a4ffe45

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

@@ -81,6 +81,7 @@ it('should show open rule with no description', async () => {
});

it('should show hotspot rule section', async () => {
const user = userEvent.setup();
renderCodingRulesApp(undefined, 'coding_rules?open=rule2');
expect(await screen.findByRole('heading', { level: 3, name: 'Hot hotspot' })).toBeInTheDocument();
expect(
@@ -89,25 +90,31 @@ it('should show hotspot rule section', async () => {
})
).toBeInTheDocument();
expect(
screen.getByRole('heading', {
screen.getByRole('button', {
name: 'coding_rules.description_section.title.root_cause.SECURITY_HOTSPOT'
})
).toBeInTheDocument();
expect(
screen.getByRole('heading', {
screen.getByRole('button', {
name: 'coding_rules.description_section.title.assess_the_problem'
})
).toBeInTheDocument();
expect(
screen.getByRole('heading', {
screen.getByRole('button', {
name: 'coding_rules.description_section.title.resources'
})
).toBeInTheDocument();
// Check that we render plain html
await user.click(
screen.getByRole('button', {
name: 'coding_rules.description_section.title.resources'
})
);
expect(screen.getByRole('link', { name: 'Awsome Reading' })).toBeInTheDocument();
});

it('should show rule advanced section', async () => {
const user = userEvent.setup();
renderCodingRulesApp(undefined, 'coding_rules?open=rule5');
expect(
await screen.findByRole('heading', { level: 3, name: 'Awsome Python rule' })
@@ -118,16 +125,21 @@ it('should show rule advanced section', async () => {
})
).toBeInTheDocument();
expect(
screen.getByRole('heading', {
screen.getByRole('button', {
name: 'coding_rules.description_section.title.how_to_fix'
})
).toBeInTheDocument();
expect(
screen.getByRole('heading', {
screen.getByRole('button', {
name: 'coding_rules.description_section.title.resources'
})
).toBeInTheDocument();
// Check that we render plain html
await user.click(
screen.getByRole('button', {
name: 'coding_rules.description_section.title.resources'
})
);
expect(screen.getByRole('link', { name: 'Awsome Reading' })).toBeInTheDocument();
});


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

@@ -17,29 +17,15 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { sortBy } from 'lodash';
import * as React from 'react';
import { updateRule } from '../../../api/rules';
import FormattingTips from '../../../components/common/FormattingTips';
import { Button, ResetButtonLink } from '../../../components/controls/buttons';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { sanitizeString } from '../../../helpers/sanitize';
import {
Dict,
RuleDescriptionSection,
RuleDescriptionSections,
RuleDetails
} from '../../../types/types';
import { RuleDescriptionSection, RuleDescriptionSections, RuleDetails } from '../../../types/types';
import RemoveExtendedDescriptionModal from './RemoveExtendedDescriptionModal';

const SECTION_ORDER: Dict<number> = {
[RuleDescriptionSections.DEFAULT]: 0,
[RuleDescriptionSections.INTRODUCTION]: 1,
[RuleDescriptionSections.ROOT_CAUSE]: 2,
[RuleDescriptionSections.ASSESS_THE_PROBLEM]: 3,
[RuleDescriptionSections.HOW_TO_FIX]: 4,
[RuleDescriptionSections.RESOURCES]: 5
};
import RuleTabViewer from './RuleTabViewer';

interface Props {
canWrite: boolean | undefined;
@@ -122,13 +108,6 @@ export default class RuleDetailsDescription extends React.PureComponent<Props, S
});
};

sortedDescriptionSections(ruleDetails: RuleDetails) {
return sortBy(
ruleDetails.descriptionSections?.filter(section => SECTION_ORDER[section.key] !== undefined),
s => SECTION_ORDER[s.key]
);
}

renderExtendedDescription = () => (
<div id="coding-rules-detail-description-extra">
{this.props.ruleDetails.htmlNote !== undefined && (
@@ -240,12 +219,34 @@ export default class RuleDetailsDescription extends React.PureComponent<Props, S
const { ruleDetails } = this.props;
const hasDescription = !ruleDetails.isExternal || ruleDetails.type !== 'UNKNOWN';

const hasDescriptionSection =
hasDescription &&
ruleDetails.descriptionSections &&
ruleDetails.descriptionSections.length > 0;

const defaultSection =
hasDescriptionSection &&
ruleDetails.descriptionSections?.length === 1 &&
ruleDetails.descriptionSections[0].key === RuleDescriptionSections.DEFAULT
? ruleDetails.descriptionSections[0]
: undefined;

return (
<div className="js-rule-description">
{hasDescription &&
ruleDetails.descriptionSections &&
ruleDetails.descriptionSections.length > 0 ? (
this.sortedDescriptionSections(ruleDetails).map(this.renderDescription)
{defaultSection && (
<>
<h2>{translate('coding_rules.description_section.title.root_cause')}</h2>
<section
className="coding-rules-detail-description markdown"
key={defaultSection.key}
/* eslint-disable-next-line react/no-danger */
dangerouslySetInnerHTML={{ __html: sanitizeString(defaultSection.content) }}
/>
</>
)}

{hasDescriptionSection && !defaultSection ? (
<RuleTabViewer ruleDetails={ruleDetails} />
) : (
<div className="coding-rules-detail-description rule-desc markdown">
{translateWithParameters('issue.external_issue_description', ruleDetails.name)}

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

@@ -0,0 +1,145 @@
/*
* 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 BoxedTabs from '../../../components/controls/BoxedTabs';
import { translate } from '../../../helpers/l10n';
import { sanitizeString } from '../../../helpers/sanitize';
import { RuleDescriptionSections, RuleDetails } from '../../../types/types';

interface Props {
ruleDetails: RuleDetails;
}

interface State {
currentTab: Tab;
tabs: Tab[];
}

interface Tab {
key: TabKeys;
label: React.ReactNode;
content: string;
}

enum TabKeys {
WhyIsThisAnIssue = 'why',
HowToFixIt = 'how_to_fix',
AssessTheIssue = 'assess_the_problem',
Resources = 'resources'
}

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

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

handleSelectTabs = (currentTabKey: TabKeys) => {
this.setState(({ tabs }) => ({
currentTab: tabs.find(tab => tab.key === currentTabKey) || tabs[0]
}));
};

computeState() {
const { ruleDetails } = this.props;

const tabs = [
{
key: TabKeys.WhyIsThisAnIssue,
label:
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
},
{
key: TabKeys.AssessTheIssue,
label: translate('coding_rules.description_section.title', TabKeys.AssessTheIssue),
content: ruleDetails.descriptionSections?.find(
section => section.key === RuleDescriptionSections.ASSESS_THE_PROBLEM
)?.content
},
{
key: TabKeys.HowToFixIt,
label: translate('coding_rules.description_section.title', TabKeys.HowToFixIt),
content: ruleDetails.descriptionSections?.find(
section => section.key === RuleDescriptionSections.HOW_TO_FIX
)?.content
},
{
key: TabKeys.Resources,
label: translate('coding_rules.description_section.title', TabKeys.Resources),
content: ruleDetails.descriptionSections?.find(
section => section.key === RuleDescriptionSections.RESOURCES
)?.content
}
].filter(tab => tab.content !== undefined) as Array<Tab>;

return {
currentTab: tabs[0],
tabs
};
}

render() {
const { ruleDetails } = this.props;
const { tabs, currentTab } = this.state;
const intro = ruleDetails.descriptionSections?.find(
section => section.key === RuleDescriptionSections.INTRODUCTION
)?.content;

return (
<>
{intro && (
<>
<h2>{translate('coding_rules.description_section.title.introduction')}</h2>
<div
className="big-padded rule-desc"
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: sanitizeString(intro) }}
/>
</>
)}
<BoxedTabs
className="bordered-bottom"
onSelect={this.handleSelectTabs}
selected={currentTab.key}
tabs={tabs}
/>

<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) }}
/>
</div>
</>
);
}
}

+ 4
- 4
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleDetails-test.tsx.snap View File

@@ -32,7 +32,7 @@ exports[`should render correctly: loaded 1`] = `
"descriptionSections": Array [
Object {
"content": "<b>Why</b> Because",
"key": "root_cause",
"key": "default",
},
],
"htmlDesc": "",
@@ -74,7 +74,7 @@ exports[`should render correctly: loaded 1`] = `
"descriptionSections": Array [
Object {
"content": "<b>Why</b> Because",
"key": "root_cause",
"key": "default",
},
],
"htmlDesc": "",
@@ -147,7 +147,7 @@ exports[`should render correctly: loaded 1`] = `
"descriptionSections": Array [
Object {
"content": "<b>Why</b> Because",
"key": "root_cause",
"key": "default",
},
],
"htmlDesc": "",
@@ -188,7 +188,7 @@ exports[`should render correctly: loaded 1`] = `
"descriptionSections": Array [
Object {
"content": "<b>Why</b> Because",
"key": "root_cause",
"key": "default",
},
],
"htmlDesc": "",

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

@@ -45,7 +45,7 @@ interface Tab {
isDefault: boolean;
}

export enum TabKeys {
enum TabKeys {
Code = 'code',
WhyIsThisAnIssue = 'why',
HowToFixIt = 'how',

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

@@ -570,7 +570,7 @@ export function mockRuleDetails(overrides: Partial<RuleDetails> = {}): RuleDetai
createdAt: '2014-12-16T17:26:54+0100',
descriptionSections: [
{
key: RuleDescriptionSections.ROOT_CAUSE,
key: RuleDescriptionSections.DEFAULT,
content: '<b>Why</b> Because'
}
],

Loading…
Cancel
Save