aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorPhilippe Perrin <philippe.perrin@sonarsource.com>2022-07-22 16:54:17 +0200
committersonartech <sonartech@sonarsource.com>2022-07-27 20:03:20 +0000
commit015d68fb1cdcb3c1e0fbce3cb725369a449c8003 (patch)
tree15df75f075912f788794cb59ffcdfb1c035e95ef /server
parent50925139d1437dd46cdbd906289674d3aa0c29a6 (diff)
downloadsonarqube-015d68fb1cdcb3c1e0fbce3cb725369a449c8003.tar.gz
sonarqube-015d68fb1cdcb3c1e0fbce3cb725369a449c8003.zip
SONAR-16599 Use withCurrentUserContext + refactoring
Diffstat (limited to 'server')
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts27
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/RuleTabViewer.tsx93
-rw-r--r--server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx48
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/IssueTabViewer.tsx121
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx32
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesApp-test.tsx21
-rw-r--r--server/sonar-web/src/main/js/components/rules/MoreInfoRuleDescription.tsx13
-rw-r--r--server/sonar-web/src/main/js/components/rules/TabViewer.tsx335
8 files changed, 326 insertions, 364 deletions
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts b/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts
index d723bab2c37..81ad6fa1d73 100644
--- a/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts
+++ b/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts
@@ -20,7 +20,7 @@
import { fireEvent, screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import CodingRulesMock from '../../../api/mocks/CodingRulesMock';
-import { mockLoggedInUser } from '../../../helpers/testMocks';
+import { mockCurrentUser, mockLoggedInUser } from '../../../helpers/testMocks';
import { renderAppRoutes } from '../../../helpers/testReactTestingUtils';
import { CurrentUser } from '../../../types/users';
import routes from '../routes';
@@ -378,7 +378,7 @@ it('should handle hash parameters', async () => {
it('should show notification for rule advanced section and remove it after user visits', async () => {
const user = userEvent.setup();
- renderCodingRulesApp(undefined, 'coding_rules?open=rule8');
+ renderCodingRulesApp(mockLoggedInUser(), 'coding_rules?open=rule8');
await screen.findByRole('heading', {
level: 3,
name: 'Awesome Python rule with education principles'
@@ -422,7 +422,8 @@ it('should show notification for rule advanced section and remove it after user
it('should show notification for rule advanced section and removes it when user scroll to the principles', async () => {
const user = userEvent.setup();
- renderCodingRulesApp(undefined, 'coding_rules?open=rule8');
+ renderCodingRulesApp(mockLoggedInUser(), 'coding_rules?open=rule8');
+
await screen.findByRole('heading', {
level: 3,
name: 'Awesome Python rule with education principles'
@@ -463,7 +464,9 @@ it('should show notification for rule advanced section and removes it when user
name: 'coding_rules.more_info.scroll_message'
})
).toBeInTheDocument();
+
fireEvent.scroll(screen.getByText('coding_rules.more_info.education_principles.title'));
+
// navigate away and come back
await user.click(
screen.getByRole('button', {
@@ -478,6 +481,24 @@ it('should show notification for rule advanced section and removes it when user
expect(screen.queryByText('coding_rules.more_info.notification_message')).not.toBeInTheDocument();
});
+it('should not show notification for anonymous users', async () => {
+ const user = userEvent.setup();
+ renderCodingRulesApp(mockCurrentUser(), 'coding_rules?open=rule8');
+
+ await user.click(
+ await screen.findByRole('button', {
+ name: 'coding_rules.description_section.title.more_info'
+ })
+ );
+
+ expect(screen.queryByText('coding_rules.more_info.notification_message')).not.toBeInTheDocument();
+ expect(
+ screen.queryByRole('button', {
+ name: 'coding_rules.more_info.scroll_message'
+ })
+ ).not.toBeInTheDocument();
+});
+
function renderCodingRulesApp(currentUser?: CurrentUser, navigateTo?: string) {
renderAppRoutes('coding_rules', routes, {
navigateTo,
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 6c39abe92f7..3a5c4b2d1b4 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
@@ -17,87 +17,32 @@
* 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 RuleDescription from '../../../components/rules/RuleDescription';
-import TabViewer, {
- getHowToFixTab,
- getMoreInfoTab,
- getWhyIsThisAnIssueTab,
- Tab,
- TabKeys
-} from '../../../components/rules/TabViewer';
-import { translate } from '../../../helpers/l10n';
+import TabViewer from '../../../components/rules/TabViewer';
import { sanitizeString } from '../../../helpers/sanitize';
import { RuleDetails } from '../../../types/types';
import { RuleDescriptionSections } from '../rule';
-interface Props {
+export interface RuleTabViewerProps {
ruleDetails: RuleDetails;
}
-export default class RuleTabViewer extends React.PureComponent<Props> {
- computeTabs = (showNotice: boolean, educationPrinciplesRef: React.RefObject<HTMLDivElement>) => {
- const { ruleDetails } = this.props;
- const descriptionSectionsByKey = groupBy(
- ruleDetails.descriptionSections,
- section => section.key
- );
- const hasEducationPrinciples =
- !!ruleDetails.educationPrinciples && ruleDetails.educationPrinciples.length > 0;
- const showNotification = showNotice && hasEducationPrinciples;
+export default function RuleTabViewer(props: RuleTabViewerProps) {
+ const { ruleDetails } = props;
+ const introduction = ruleDetails.descriptionSections?.find(
+ section => section.key === RuleDescriptionSections.INTRODUCTION
+ )?.content;
- const rootCauseTitle =
- ruleDetails.type === 'SECURITY_HOTSPOT'
- ? translate('coding_rules.description_section.title.root_cause.SECURITY_HOTSPOT')
- : translate('coding_rules.description_section.title.root_cause');
-
- return [
- getWhyIsThisAnIssueTab(
- descriptionSectionsByKey[RuleDescriptionSections.ROOT_CAUSE],
- descriptionSectionsByKey,
- rootCauseTitle
- ),
- {
- key: TabKeys.AssessTheIssue,
- label: translate('coding_rules.description_section.title', TabKeys.AssessTheIssue),
- content: descriptionSectionsByKey[RuleDescriptionSections.ASSESS_THE_PROBLEM] && (
- <RuleDescription
- className="big-padded"
- sections={descriptionSectionsByKey[RuleDescriptionSections.ASSESS_THE_PROBLEM]}
- />
- )
- },
- getHowToFixTab(
- descriptionSectionsByKey,
- translate('coding_rules.description_section.title', TabKeys.HowToFixIt)
- ),
- getMoreInfoTab(
- showNotification,
- descriptionSectionsByKey,
- educationPrinciplesRef,
- translate('coding_rules.description_section.title', TabKeys.MoreInfo),
- ruleDetails.educationPrinciples
- )
- ].filter(tab => tab.content) as Array<Tab>;
- };
-
- render() {
- const { ruleDetails } = this.props;
- const intro = ruleDetails.descriptionSections?.find(
- section => section.key === RuleDescriptionSections.INTRODUCTION
- )?.content;
- return (
- <>
- {intro && (
- <div
- className="rule-desc"
- // eslint-disable-next-line react/no-danger
- dangerouslySetInnerHTML={{ __html: sanitizeString(intro) }}
- />
- )}
- <TabViewer ruleDetails={this.props.ruleDetails} computeTabs={this.computeTabs} />
- </>
- );
- }
+ return (
+ <>
+ {introduction && (
+ <div
+ className="rule-desc"
+ // eslint-disable-next-line react/no-danger
+ dangerouslySetInnerHTML={{ __html: sanitizeString(introduction) }}
+ />
+ )}
+ <TabViewer ruleDetails={ruleDetails} />
+ </>
+ );
}
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 ddabf99750b..8df2ddb0af6 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
@@ -22,8 +22,10 @@ import userEvent from '@testing-library/user-event';
import React from 'react';
import IssuesServiceMock from '../../../api/mocks/IssuesServiceMock';
import { renderOwaspTop102021Category } from '../../../helpers/security-standard';
+import { mockCurrentUser } from '../../../helpers/testMocks';
import { renderApp, renderAppRoutes } from '../../../helpers/testReactTestingUtils';
import { IssueType } from '../../../types/issues';
+import { CurrentUser } from '../../../types/users';
import IssuesApp from '../components/IssuesApp';
import { projectIssuesRoutes } from '../routes';
@@ -42,14 +44,16 @@ beforeEach(() => {
it('should show education principles', async () => {
const user = userEvent.setup();
renderProjectIssuesApp('project/issues?issues=issue2&open=issue2&id=myproject');
- await user.click(await screen.findByRole('button', { name: `issue.tabs.more_info` }));
+ await user.click(
+ await screen.findByRole('button', { name: `coding_rules.description_section.title.more_info` })
+ );
expect(screen.getByRole('heading', { name: 'Defense-In-Depth', level: 3 })).toBeInTheDocument();
});
it('should open issue and navigate', async () => {
const user = userEvent.setup();
- renderIssueApp();
+ renderIssueApp(mockCurrentUser());
// Select an issue with an advanced rule
expect(await screen.findByRole('region', { name: 'Fix that' })).toBeInTheDocument();
@@ -60,13 +64,21 @@ it('should open issue and navigate', async () => {
expect(screen.getByRole('link', { name: 'advancedRuleId' })).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('button', { name: `coding_rules.description_section.title.root_cause` })
+ ).toBeInTheDocument();
+ await user.click(
+ screen.getByRole('button', { name: `coding_rules.description_section.title.root_cause` })
+ );
expect(screen.getByRole('heading', { name: 'Because' })).toBeInTheDocument();
// Select the "how to fix it" tab
- expect(screen.getByRole('button', { name: `issue.tabs.how_to_fix` })).toBeInTheDocument();
- await user.click(screen.getByRole('button', { name: `issue.tabs.how_to_fix` }));
+ 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` })
+ );
// Is the context selector present with the expected values and default selection?
expect(screen.getByRole('button', { name: 'Context 2' })).toBeInTheDocument();
@@ -88,8 +100,12 @@ it('should open issue and navigate', async () => {
expect(screen.getByText('coding_rules.context.others.description.second')).toBeInTheDocument();
// Select the main info 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('button', { name: `coding_rules.description_section.title.more_info` })
+ ).toBeInTheDocument();
+ await user.click(
+ screen.getByRole('button', { name: `coding_rules.description_section.title.more_info` })
+ );
expect(screen.getByRole('heading', { name: 'Link' })).toBeInTheDocument();
// check for extended description
@@ -104,8 +120,12 @@ it('should open issue and navigate', async () => {
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('button', { name: `coding_rules.description_section.title.root_cause` })
+ ).toBeInTheDocument();
+ await user.click(
+ screen.getByRole('button', { name: `coding_rules.description_section.title.root_cause` })
+ );
expect(screen.getByRole('heading', { name: 'Default' })).toBeInTheDocument();
// Select the previous issue (with a simple rule) through keyboard shortcut
@@ -115,9 +135,7 @@ it('should open issue and navigate', async () => {
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` }));
+ // The "Where is the issue" tab should be selected by default. Check its content
expect(screen.getByRole('region', { name: 'Issue on file' })).toBeInTheDocument();
expect(
screen.getByRole('row', {
@@ -171,8 +189,8 @@ describe('redirects', () => {
});
});
-function renderIssueApp() {
- renderApp('project/issues', <IssuesApp />);
+function renderIssueApp(currentUser?: CurrentUser) {
+ renderApp('project/issues', <IssuesApp />, { currentUser: mockCurrentUser(), ...currentUser });
}
function renderProjectIssuesApp(navigateTo?: string) {
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 578c7faec8c..01112b42f09 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
@@ -18,102 +18,51 @@
* 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 TabViewer, {
- getHowToFixTab,
- getMoreInfoTab,
- getWhyIsThisAnIssueTab,
- Tab,
- TabKeys
-} from '../../../components/rules/TabViewer';
-import { translate } from '../../../helpers/l10n';
+import TabViewer from '../../../components/rules/TabViewer';
import { getRuleUrl } from '../../../helpers/urls';
import { Component, Issue, RuleDetails } from '../../../types/types';
-import { RuleDescriptionSections } from '../../coding-rules/rule';
-interface Props {
+interface IssueViewerTabsProps {
component?: Component;
issue: Issue;
codeTabContent: React.ReactNode;
ruleDetails: RuleDetails;
}
-export default class IssueViewerTabs extends React.PureComponent<Props> {
- computeTabs = (showNotice: boolean, educationPrinciplesRef: React.RefObject<HTMLDivElement>) => {
- const {
- ruleDetails,
- codeTabContent,
- issue: { ruleDescriptionContextKey }
- } = this.props;
- const descriptionSectionsByKey = groupBy(
- ruleDetails.descriptionSections,
- section => section.key
- );
- const hasEducationPrinciples =
- !!ruleDetails.educationPrinciples && ruleDetails.educationPrinciples.length > 0;
- const showNotification = showNotice && hasEducationPrinciples;
-
- const rootCauseDescriptionSections =
- descriptionSectionsByKey[RuleDescriptionSections.DEFAULT] ||
- descriptionSectionsByKey[RuleDescriptionSections.ROOT_CAUSE];
-
- return [
- {
- key: TabKeys.Code,
- label: translate('issue.tabs', TabKeys.Code),
- content: <div className="padded">{codeTabContent}</div>
- },
- getWhyIsThisAnIssueTab(
- rootCauseDescriptionSections,
- descriptionSectionsByKey,
- translate('issue.tabs', TabKeys.WhyIsThisAnIssue),
- ruleDescriptionContextKey
- ),
- getHowToFixTab(
- descriptionSectionsByKey,
- translate('issue.tabs', TabKeys.HowToFixIt),
- ruleDescriptionContextKey
- ),
- getMoreInfoTab(
- showNotification,
- descriptionSectionsByKey,
- educationPrinciplesRef,
- translate('issue.tabs', TabKeys.MoreInfo),
- ruleDetails.educationPrinciples
- )
- ].filter(tab => tab.content) as Array<Tab>;
- };
-
- render() {
- const { ruleDetails, codeTabContent } = this.props;
- const {
- component,
- ruleDetails: { name, key },
- issue: { message }
- } = this.props;
- return (
- <>
- <div
- className={classNames('issue-header', {
- 'issue-project-level': component !== undefined
- })}>
- <h1 className="text-bold">{message}</h1>
- <div className="spacer-top big-spacer-bottom">
- <span className="note padded-right">{name}</span>
- <Link className="small" to={getRuleUrl(key)} target="_blank">
- {key}
- </Link>
- </div>
+export default function IssueViewerTabs(props: IssueViewerTabsProps) {
+ const {
+ ruleDetails,
+ codeTabContent,
+ issue: { ruleDescriptionContextKey }
+ } = props;
+ const {
+ component,
+ ruleDetails: { name, key },
+ issue: { message }
+ } = props;
+ return (
+ <>
+ <div
+ className={classNames('issue-header', {
+ 'issue-project-level': component !== undefined
+ })}>
+ <h1 className="text-bold">{message}</h1>
+ <div className="spacer-top big-spacer-bottom">
+ <span className="note padded-right">{name}</span>
+ <Link className="small" to={getRuleUrl(key)} target="_blank">
+ {key}
+ </Link>
</div>
- <TabViewer
- ruleDetails={ruleDetails}
- computeTabs={this.computeTabs}
- codeTabContent={codeTabContent}
- pageType="issues"
- />
- </>
- );
- }
+ </div>
+ <TabViewer
+ ruleDetails={ruleDetails}
+ extendedDescription={ruleDetails.htmlNote}
+ ruleDescriptionContextKey={ruleDescriptionContextKey}
+ codeTabContent={codeTabContent}
+ pageType="issues"
+ />
+ </>
+ );
}
diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx b/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx
index 49dc85b3a96..79f773c9221 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx
@@ -19,7 +19,7 @@
*/
import styled from '@emotion/styled';
import classNames from 'classnames';
-import { debounce, groupBy, keyBy, omit, without } from 'lodash';
+import { debounce, keyBy, omit, without } from 'lodash';
import * as React from 'react';
import { Helmet } from 'react-helmet-async';
import { FormattedMessage } from 'react-intl';
@@ -74,7 +74,6 @@ import {
import { SecurityStandard } from '../../../types/security';
import { Component, Dict, Issue, Paging, RawQuery, RuleDetails } from '../../../types/types';
import { CurrentUser, UserBase } from '../../../types/users';
-import { RuleDescriptionSections } from '../../coding-rules/rule';
import * as actions from '../actions';
import ConciseIssuesList from '../conciseIssuesList/ConciseIssuesList';
import ConciseIssuesListHeader from '../conciseIssuesList/ConciseIssuesListHeader';
@@ -355,40 +354,13 @@ export class App extends React.PureComponent<Props, State> {
}
this.setState({ loadingRule: true });
const openRuleDetails = await getRuleDetails({ key: openIssue.rule })
- .then(response => {
- const ruleDetails = response.rule;
- this.addExtendedDescription(ruleDetails);
- return ruleDetails;
- })
+ .then(response => response.rule)
.catch(() => undefined);
if (this.mounted) {
this.setState({ loadingRule: false, openRuleDetails });
}
}
- addExtendedDescription = (ruleDetails: RuleDetails) => {
- const descriptionSectionsByKey = groupBy(
- ruleDetails.descriptionSections,
- section => section.key
- );
-
- if (ruleDetails.htmlNote) {
- 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
- descriptionSectionsByKey[RuleDescriptionSections.RESOURCES][0].content +=
- '<br/>' + ruleDetails.htmlNote;
- } else {
- descriptionSectionsByKey[RuleDescriptionSections.RESOURCES] = [
- {
- key: RuleDescriptionSections.RESOURCES,
- content: ruleDetails.htmlNote
- }
- ];
- }
- }
- };
-
selectPreviousIssue = () => {
const { issues } = this.state;
const selectedIndex = this.getSelectedIndex();
diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesApp-test.tsx b/server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesApp-test.tsx
index 62a1983a216..77a3df3eb33 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesApp-test.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesApp-test.tsx
@@ -21,7 +21,6 @@ import { shallow } from 'enzyme';
import * as React from 'react';
import { searchIssues } from '../../../../api/issues';
import { getRuleDetails } from '../../../../api/rules';
-import TabViewer from '../../../../components/rules/TabViewer';
import handleRequiredAuthentication from '../../../../helpers/handleRequiredAuthentication';
import { KeyboardKeys } from '../../../../helpers/keycodes';
import { mockPullRequest } from '../../../../helpers/mocks/branch-like';
@@ -54,8 +53,6 @@ import {
} from '../../actions';
import BulkChangeModal from '../BulkChangeModal';
import { App } from '../IssuesApp';
-import IssuesSourceViewer from '../IssuesSourceViewer';
-import IssueViewerTabs from '../IssueTabViewer';
jest.mock('../../../../helpers/pages', () => ({
addSideBarClass: jest.fn(),
@@ -209,24 +206,6 @@ it('should open standard facets for vulnerabilities and hotspots', () => {
expect(fetchFacet).lastCalledWith('owaspTop10');
});
-it('should switch to source view if an issue is selected', async () => {
- const wrapper = shallowRender();
- await waitAndUpdate(wrapper);
- expect(wrapper.find(IssueViewerTabs).exists()).toBe(false);
-
- wrapper.setProps({ location: mockLocation({ query: { open: 'third' } }) });
- await waitAndUpdate(wrapper);
- expect(
- wrapper
- .find(IssueViewerTabs)
- .dive()
- .find(TabViewer)
- .dive()
- .find(IssuesSourceViewer)
- .exists()
- ).toBe(true);
-});
-
it('should correctly bind key events for issue navigation', async () => {
const push = jest.fn();
const addEventListenerSpy = jest.spyOn(document, 'addEventListener');
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 ef9cc3b4b3c..dc599849bb4 100644
--- a/server/sonar-web/src/main/js/components/rules/MoreInfoRuleDescription.tsx
+++ b/server/sonar-web/src/main/js/components/rules/MoreInfoRuleDescription.tsx
@@ -31,7 +31,7 @@ import './style.css';
interface Props {
sections?: RuleDescriptionSection[];
educationPrinciples?: string[];
- showNotification?: boolean;
+ displayEducationalPrinciplesNotification?: boolean;
educationPrinciplesRef?: React.RefObject<HTMLDivElement>;
}
@@ -48,10 +48,15 @@ export default class MoreInfoRuleDescription extends React.PureComponent<Props,
};
render() {
- const { showNotification, sections = [], educationPrinciples = [] } = this.props;
+ const {
+ displayEducationalPrinciplesNotification,
+ sections = [],
+ educationPrinciples = [],
+ educationPrinciplesRef
+ } = this.props;
return (
<div className="big-padded rule-desc">
- {showNotification && (
+ {displayEducationalPrinciplesNotification && (
<Alert variant="info">
<p className="little-spacer-bottom little-spacer-top">
{translate('coding_rules.more_info.notification_message')}
@@ -73,7 +78,7 @@ export default class MoreInfoRuleDescription extends React.PureComponent<Props,
{educationPrinciples.length > 0 && (
<>
- <h2 ref={this.props.educationPrinciplesRef}>
+ <h2 ref={educationPrinciplesRef}>
{translate('coding_rules.more_info.education_principles.title')}
</h2>
{educationPrinciples.map(key => {
diff --git a/server/sonar-web/src/main/js/components/rules/TabViewer.tsx b/server/sonar-web/src/main/js/components/rules/TabViewer.tsx
index df0c83d593c..22651ca8b75 100644
--- a/server/sonar-web/src/main/js/components/rules/TabViewer.tsx
+++ b/server/sonar-web/src/main/js/components/rules/TabViewer.tsx
@@ -18,30 +18,33 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import classNames from 'classnames';
-import { debounce, Dictionary } from 'lodash';
+import { cloneDeep, debounce, groupBy } from 'lodash';
import * as React from 'react';
-import { dismissNotice, getCurrentUser } from '../../api/users';
-import { RuleDescriptionSection, RuleDescriptionSections } from '../../apps/coding-rules/rule';
+import { dismissNotice } from '../../api/users';
+import { CurrentUserContextInterface } from '../../app/components/current-user/CurrentUserContext';
+import withCurrentUserContext from '../../app/components/current-user/withCurrentUserContext';
+import { RuleDescriptionSections } from '../../apps/coding-rules/rule';
+import { translate } from '../../helpers/l10n';
import { RuleDetails } from '../../types/types';
-import { CurrentUser, NoticeType } from '../../types/users';
+import { NoticeType } from '../../types/users';
import BoxedTabs from '../controls/BoxedTabs';
import MoreInfoRuleDescription from './MoreInfoRuleDescription';
import RuleDescription from './RuleDescription';
import './style.css';
-interface Props {
+interface TabViewerProps extends CurrentUserContextInterface {
ruleDetails: RuleDetails;
+ extendedDescription?: string;
+ ruleDescriptionContextKey?: string;
codeTabContent?: React.ReactNode;
- computeTabs: (
- showNotice: boolean,
- educationPrinciplesRef: React.RefObject<HTMLDivElement>
- ) => Tab[];
pageType?: string;
}
interface State {
- currentTab: Tab;
tabs: Tab[];
+ selectedTab?: Tab;
+ displayEducationalPrinciplesNotification?: boolean;
+ educationalPrinciplesNotificationHasBeenDismissed?: boolean;
}
export interface Tab {
@@ -60,62 +63,209 @@ export enum TabKeys {
const DEBOUNCE_FOR_SCROLL = 250;
-export default class TabViewer extends React.PureComponent<Props, State> {
- showNotification = false;
+export class TabViewer extends React.PureComponent<TabViewerProps, State> {
+ state: State = {
+ tabs: []
+ };
+
educationPrinciplesRef: React.RefObject<HTMLDivElement>;
- constructor(props: Props) {
+ constructor(props: TabViewerProps) {
super(props);
- const tabs = this.getUpdatedTabs(false);
- this.state = {
- tabs,
- currentTab: tabs[0]
- };
this.educationPrinciplesRef = React.createRef();
- this.checkIfConceptIsVisible = debounce(this.checkIfConceptIsVisible, DEBOUNCE_FOR_SCROLL);
- document.addEventListener('scroll', this.checkIfConceptIsVisible, { capture: true });
+ this.checkIfEducationPrinciplesAreVisible = debounce(
+ this.checkIfEducationPrinciplesAreVisible,
+ DEBOUNCE_FOR_SCROLL
+ );
}
componentDidMount() {
- this.getNotificationValue();
+ this.setState(prevState => this.computeState(prevState));
+ this.attachScrollEvent();
}
- componentDidUpdate(prevProps: Props, prevState: State) {
- const { currentTab } = this.state;
+ componentDidUpdate(prevProps: TabViewerProps, prevState: State) {
+ const { ruleDetails, codeTabContent, ruleDescriptionContextKey, currentUser } = this.props;
+ const { selectedTab } = this.state;
+
if (
- prevProps.ruleDetails !== this.props.ruleDetails ||
- prevProps.codeTabContent !== this.props.codeTabContent
+ prevProps.ruleDetails.key !== ruleDetails.key ||
+ prevProps.ruleDescriptionContextKey !== ruleDescriptionContextKey ||
+ prevProps.codeTabContent !== codeTabContent ||
+ prevProps.currentUser !== currentUser
) {
- const tabs = this.getUpdatedTabs(this.showNotification);
- this.getNotificationValue();
- this.setState({
- tabs,
- currentTab: tabs[0]
- });
+ this.setState(pState => this.computeState(pState, prevProps.ruleDetails !== ruleDetails));
}
- if (currentTab.key === TabKeys.MoreInfo) {
- this.checkIfConceptIsVisible();
+
+ if (selectedTab?.key === TabKeys.MoreInfo) {
+ this.checkIfEducationPrinciplesAreVisible();
}
- if (prevState.currentTab.key === TabKeys.MoreInfo && !this.showNotification) {
- const tabs = this.getUpdatedTabs(this.showNotification);
- this.setState({ tabs });
+ if (
+ prevState.selectedTab?.key === TabKeys.MoreInfo &&
+ prevState.displayEducationalPrinciplesNotification &&
+ prevState.educationalPrinciplesNotificationHasBeenDismissed
+ ) {
+ this.props.updateDismissedNotices(NoticeType.EDUCATION_PRINCIPLES, true);
}
}
componentWillUnmount() {
- document.removeEventListener('scroll', this.checkIfConceptIsVisible, { capture: true });
+ this.detachScrollEvent();
}
- checkIfConceptIsVisible = () => {
+ computeState = (prevState: State, resetSelectedTab: boolean = false) => {
+ const {
+ ruleDetails,
+ currentUser: { isLoggedIn, dismissedNotices }
+ } = this.props;
+
+ const displayEducationalPrinciplesNotification =
+ !!ruleDetails.educationPrinciples &&
+ ruleDetails.educationPrinciples.length > 0 &&
+ isLoggedIn &&
+ !dismissedNotices[NoticeType.EDUCATION_PRINCIPLES];
+ const tabs = this.computeTabs(displayEducationalPrinciplesNotification);
+
+ return {
+ tabs,
+ selectedTab: resetSelectedTab || !prevState.selectedTab ? tabs[0] : prevState.selectedTab,
+ displayEducationalPrinciplesNotification
+ };
+ };
+
+ computeTabs = (displayEducationalPrinciplesNotification: boolean) => {
+ const {
+ codeTabContent,
+ ruleDetails: { descriptionSections, educationPrinciples, type: ruleType },
+ ruleDescriptionContextKey,
+ extendedDescription
+ } = this.props;
+
+ // As we might tamper with the description later on, we clone to avoid any side effect
+ const descriptionSectionsByKey = cloneDeep(
+ groupBy(descriptionSections, section => section.key)
+ );
+
+ if (extendedDescription) {
+ if (descriptionSectionsByKey[RuleDescriptionSections.RESOURCES]?.length > 0) {
+ // We add the extended description (htmlNote) in the first context, in case there are contexts
+ // Extended description will get reworked in future
+ descriptionSectionsByKey[RuleDescriptionSections.RESOURCES][0].content +=
+ '<br/>' + extendedDescription;
+ } else {
+ descriptionSectionsByKey[RuleDescriptionSections.RESOURCES] = [
+ {
+ key: RuleDescriptionSections.RESOURCES,
+ content: extendedDescription
+ }
+ ];
+ }
+ }
+
+ const tabs: Tab[] = [
+ {
+ key: TabKeys.WhyIsThisAnIssue,
+ label:
+ ruleType === 'SECURITY_HOTSPOT'
+ ? translate('coding_rules.description_section.title.root_cause.SECURITY_HOTSPOT')
+ : translate('coding_rules.description_section.title.root_cause'),
+ content: (descriptionSectionsByKey[RuleDescriptionSections.DEFAULT] ||
+ descriptionSectionsByKey[RuleDescriptionSections.ROOT_CAUSE]) && (
+ <RuleDescription
+ className="big-padded"
+ sections={
+ descriptionSectionsByKey[RuleDescriptionSections.DEFAULT] ||
+ descriptionSectionsByKey[RuleDescriptionSections.ROOT_CAUSE]
+ }
+ isDefault={descriptionSectionsByKey[RuleDescriptionSections.DEFAULT] !== undefined}
+ defaultContextKey={ruleDescriptionContextKey}
+ />
+ )
+ },
+ {
+ key: TabKeys.AssessTheIssue,
+ label: translate('coding_rules.description_section.title', TabKeys.AssessTheIssue),
+ content: descriptionSectionsByKey[RuleDescriptionSections.ASSESS_THE_PROBLEM] && (
+ <RuleDescription
+ className="big-padded"
+ sections={descriptionSectionsByKey[RuleDescriptionSections.ASSESS_THE_PROBLEM]}
+ />
+ )
+ },
+ {
+ key: TabKeys.HowToFixIt,
+ label: translate('coding_rules.description_section.title', TabKeys.HowToFixIt),
+ content: descriptionSectionsByKey[RuleDescriptionSections.HOW_TO_FIX] && (
+ <RuleDescription
+ className="big-padded"
+ sections={descriptionSectionsByKey[RuleDescriptionSections.HOW_TO_FIX]}
+ defaultContextKey={ruleDescriptionContextKey}
+ />
+ )
+ },
+ {
+ key: TabKeys.MoreInfo,
+ label: (
+ <>
+ {translate('coding_rules.description_section.title', TabKeys.MoreInfo)}
+ {displayEducationalPrinciplesNotification && <div className="notice-dot" />}
+ </>
+ ),
+ content: ((educationPrinciples && educationPrinciples.length > 0) ||
+ descriptionSectionsByKey[RuleDescriptionSections.RESOURCES]) && (
+ <MoreInfoRuleDescription
+ educationPrinciples={educationPrinciples}
+ sections={descriptionSectionsByKey[RuleDescriptionSections.RESOURCES]}
+ displayEducationalPrinciplesNotification={displayEducationalPrinciplesNotification}
+ educationPrinciplesRef={this.educationPrinciplesRef}
+ />
+ )
+ }
+ ];
+
+ if (codeTabContent !== undefined) {
+ tabs.unshift({
+ key: TabKeys.Code,
+ label: translate('issue.tabs', TabKeys.Code),
+ content: <div className="padded">{codeTabContent}</div>
+ });
+ }
+
+ return tabs.filter(tab => tab.content);
+ };
+
+ attachScrollEvent = () => {
+ document.addEventListener('scroll', this.checkIfEducationPrinciplesAreVisible, {
+ capture: true
+ });
+ };
+
+ detachScrollEvent = () => {
+ document.removeEventListener('scroll', this.checkIfEducationPrinciplesAreVisible, {
+ capture: true
+ });
+ };
+
+ checkIfEducationPrinciplesAreVisible = () => {
+ const {
+ displayEducationalPrinciplesNotification,
+ educationalPrinciplesNotificationHasBeenDismissed
+ } = this.state;
+
if (this.educationPrinciplesRef.current) {
const rect = this.educationPrinciplesRef.current.getBoundingClientRect();
- const isView = rect.top <= (window.innerHeight || document.documentElement.clientHeight);
- if (isView && this.showNotification) {
+ const isVisible = rect.top <= (window.innerHeight || document.documentElement.clientHeight);
+
+ if (
+ isVisible &&
+ displayEducationalPrinciplesNotification &&
+ !educationalPrinciplesNotificationHasBeenDismissed
+ ) {
dismissNotice(NoticeType.EDUCATION_PRINCIPLES)
.then(() => {
- document.removeEventListener('scroll', this.checkIfConceptIsVisible, { capture: true });
- this.showNotification = false;
+ this.detachScrollEvent();
+ this.setState({ educationalPrinciplesNotificationHasBeenDismissed: true });
})
.catch(() => {
/* noop */
@@ -124,34 +274,22 @@ export default class TabViewer extends React.PureComponent<Props, State> {
}
};
- getNotificationValue() {
- getCurrentUser()
- .then((data: CurrentUser) => {
- const educationPrinciplesDismissed = data.dismissedNotices[NoticeType.EDUCATION_PRINCIPLES];
- if (educationPrinciplesDismissed !== undefined) {
- this.showNotification = !educationPrinciplesDismissed;
- const tabs = this.getUpdatedTabs(!educationPrinciplesDismissed);
- this.setState({ tabs });
- }
- })
- .catch(() => {
- /* noop */
- });
- }
-
handleSelectTabs = (currentTabKey: TabKeys) => {
this.setState(({ tabs }) => ({
- currentTab: tabs.find(tab => tab.key === currentTabKey) || tabs[0]
+ selectedTab: tabs.find(tab => tab.key === currentTabKey) || tabs[0]
}));
};
- getUpdatedTabs = (showNotification: boolean) => {
- return this.props.computeTabs(showNotification, this.educationPrinciplesRef);
- };
-
render() {
- const { tabs, currentTab } = this.state;
+ const { tabs, selectedTab } = this.state;
const { pageType } = this.props;
+
+ if (!tabs || tabs.length === 0 || !selectedTab) {
+ return null;
+ }
+
+ const tabContent = tabs.find(t => t.key === selectedTab.key)?.content;
+
return (
<>
<div
@@ -161,79 +299,14 @@ export default class TabViewer extends React.PureComponent<Props, State> {
<BoxedTabs
className="big-spacer-top"
onSelect={this.handleSelectTabs}
- selected={currentTab.key}
+ selected={selectedTab.key}
tabs={tabs}
/>
</div>
- <div className="bordered">{currentTab.content}</div>
+ <div className="bordered">{tabContent}</div>
</>
);
}
}
-export const getMoreInfoTab = (
- showNotification: boolean,
- descriptionSectionsByKey: Dictionary<RuleDescriptionSection[]>,
- educationPrinciplesRef: React.RefObject<HTMLDivElement>,
- title: string,
- educationPrinciples?: string[]
-) => {
- return {
- key: TabKeys.MoreInfo,
- label: showNotification ? (
- <div>
- {title}
- <div className="notice-dot" />
- </div>
- ) : (
- title
- ),
- content: ((educationPrinciples && educationPrinciples.length > 0) ||
- descriptionSectionsByKey[RuleDescriptionSections.RESOURCES]) && (
- <MoreInfoRuleDescription
- educationPrinciples={educationPrinciples}
- sections={descriptionSectionsByKey[RuleDescriptionSections.RESOURCES]}
- showNotification={showNotification}
- educationPrinciplesRef={educationPrinciplesRef}
- />
- )
- };
-};
-
-export const getHowToFixTab = (
- descriptionSectionsByKey: Dictionary<RuleDescriptionSection[]>,
- title: string,
- ruleDescriptionContextKey?: string
-) => {
- return {
- key: TabKeys.HowToFixIt,
- label: title,
- content: descriptionSectionsByKey[RuleDescriptionSections.HOW_TO_FIX] && (
- <RuleDescription
- className="big-padded"
- sections={descriptionSectionsByKey[RuleDescriptionSections.HOW_TO_FIX]}
- defaultContextKey={ruleDescriptionContextKey}
- />
- )
- };
-};
-
-export const getWhyIsThisAnIssueTab = (
- rootCauseDescriptionSections: RuleDescriptionSection[],
- descriptionSectionsByKey: Dictionary<RuleDescriptionSection[]>,
- title: string,
- ruleDescriptionContextKey?: string
-) => {
- return {
- key: TabKeys.WhyIsThisAnIssue,
- label: title,
- content: rootCauseDescriptionSections && (
- <RuleDescription
- className="big-padded"
- sections={rootCauseDescriptionSections}
- isDefault={descriptionSectionsByKey[RuleDescriptionSections.DEFAULT] !== undefined}
- defaultContextKey={ruleDescriptionContextKey}
- />
- )
- };
-};
+export default withCurrentUserContext(TabViewer);