Browse Source

SONAR-16599 Use withCurrentUserContext + refactoring

tags/9.6.0.59041
Philippe Perrin 1 year ago
parent
commit
015d68fb1c

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

@@ -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,

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

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

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

@@ -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) {

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

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

+ 2
- 30
server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx View File

@@ -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();

+ 0
- 21
server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesApp-test.tsx View File

@@ -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');

+ 9
- 4
server/sonar-web/src/main/js/components/rules/MoreInfoRuleDescription.tsx View File

@@ -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 => {

+ 204
- 131
server/sonar-web/src/main/js/components/rules/TabViewer.tsx View File

@@ -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);

+ 1
- 4
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -860,9 +860,6 @@ issue.transition.resolveasreviewed.description=There is no Vulnerability in the
issue.transition.resetastoreview=Reset as To Review
issue.transition.resetastoreview.description=The Security Hotspot should be analyzed again
issue.tabs.code=Where is the issue?
issue.tabs.why=Why is this an issue?
issue.tabs.how_to_fix=How can I fix it?
issue.tabs.more_info=More Info

vulnerability.transition.resetastoreview=Reset as To Review
vulnerability.transition.resetastoreview.description=The vulnerability can't be fixed as is and needs more details. The security hotspot needs to be reviewed again
@@ -1912,7 +1909,7 @@ coding_rules.external_rule.engine=Rule provided by an external rule engine: {0}
coding_rules.description_section.title.introduction=Introduction
coding_rules.description_section.title.root_cause=Why is this an issue?
coding_rules.description_section.title.root_cause.SECURITY_HOTSPOT=What is the risk?
coding_rules.description_section.title.assess_the_problem=Assess the risk?
coding_rules.description_section.title.assess_the_problem=Assess the risk
coding_rules.description_section.title.how_to_fix=How can I fix it?
coding_rules.description_section.title.more_info=More Info


Loading…
Cancel
Save