import { fireEvent, screen, waitFor, within } from '@testing-library/react'; | import { fireEvent, screen, waitFor, within } from '@testing-library/react'; | ||||
import userEvent from '@testing-library/user-event'; | import userEvent from '@testing-library/user-event'; | ||||
import CodingRulesMock from '../../../api/mocks/CodingRulesMock'; | import CodingRulesMock from '../../../api/mocks/CodingRulesMock'; | ||||
import { mockLoggedInUser } from '../../../helpers/testMocks'; | |||||
import { mockCurrentUser, mockLoggedInUser } from '../../../helpers/testMocks'; | |||||
import { renderAppRoutes } from '../../../helpers/testReactTestingUtils'; | import { renderAppRoutes } from '../../../helpers/testReactTestingUtils'; | ||||
import { CurrentUser } from '../../../types/users'; | import { CurrentUser } from '../../../types/users'; | ||||
import routes from '../routes'; | import routes from '../routes'; | ||||
it('should show notification for rule advanced section and remove it after user visits', async () => { | it('should show notification for rule advanced section and remove it after user visits', async () => { | ||||
const user = userEvent.setup(); | const user = userEvent.setup(); | ||||
renderCodingRulesApp(undefined, 'coding_rules?open=rule8'); | |||||
renderCodingRulesApp(mockLoggedInUser(), 'coding_rules?open=rule8'); | |||||
await screen.findByRole('heading', { | await screen.findByRole('heading', { | ||||
level: 3, | level: 3, | ||||
name: 'Awesome Python rule with education principles' | name: 'Awesome Python rule with education principles' | ||||
it('should show notification for rule advanced section and removes it when user scroll to the principles', async () => { | it('should show notification for rule advanced section and removes it when user scroll to the principles', async () => { | ||||
const user = userEvent.setup(); | const user = userEvent.setup(); | ||||
renderCodingRulesApp(undefined, 'coding_rules?open=rule8'); | |||||
renderCodingRulesApp(mockLoggedInUser(), 'coding_rules?open=rule8'); | |||||
await screen.findByRole('heading', { | await screen.findByRole('heading', { | ||||
level: 3, | level: 3, | ||||
name: 'Awesome Python rule with education principles' | name: 'Awesome Python rule with education principles' | ||||
name: 'coding_rules.more_info.scroll_message' | name: 'coding_rules.more_info.scroll_message' | ||||
}) | }) | ||||
).toBeInTheDocument(); | ).toBeInTheDocument(); | ||||
fireEvent.scroll(screen.getByText('coding_rules.more_info.education_principles.title')); | fireEvent.scroll(screen.getByText('coding_rules.more_info.education_principles.title')); | ||||
// navigate away and come back | // navigate away and come back | ||||
await user.click( | await user.click( | ||||
screen.getByRole('button', { | screen.getByRole('button', { | ||||
expect(screen.queryByText('coding_rules.more_info.notification_message')).not.toBeInTheDocument(); | 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) { | function renderCodingRulesApp(currentUser?: CurrentUser, navigateTo?: string) { | ||||
renderAppRoutes('coding_rules', routes, { | renderAppRoutes('coding_rules', routes, { | ||||
navigateTo, | navigateTo, |
* along with this program; if not, write to the Free Software Foundation, | * along with this program; if not, write to the Free Software Foundation, | ||||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | ||||
*/ | */ | ||||
import { groupBy } from 'lodash'; | |||||
import * as React from 'react'; | 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 { sanitizeString } from '../../../helpers/sanitize'; | ||||
import { RuleDetails } from '../../../types/types'; | import { RuleDetails } from '../../../types/types'; | ||||
import { RuleDescriptionSections } from '../rule'; | import { RuleDescriptionSections } from '../rule'; | ||||
interface Props { | |||||
export interface RuleTabViewerProps { | |||||
ruleDetails: RuleDetails; | 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} /> | |||||
</> | |||||
); | |||||
} | } |
import React from 'react'; | import React from 'react'; | ||||
import IssuesServiceMock from '../../../api/mocks/IssuesServiceMock'; | import IssuesServiceMock from '../../../api/mocks/IssuesServiceMock'; | ||||
import { renderOwaspTop102021Category } from '../../../helpers/security-standard'; | import { renderOwaspTop102021Category } from '../../../helpers/security-standard'; | ||||
import { mockCurrentUser } from '../../../helpers/testMocks'; | |||||
import { renderApp, renderAppRoutes } from '../../../helpers/testReactTestingUtils'; | import { renderApp, renderAppRoutes } from '../../../helpers/testReactTestingUtils'; | ||||
import { IssueType } from '../../../types/issues'; | import { IssueType } from '../../../types/issues'; | ||||
import { CurrentUser } from '../../../types/users'; | |||||
import IssuesApp from '../components/IssuesApp'; | import IssuesApp from '../components/IssuesApp'; | ||||
import { projectIssuesRoutes } from '../routes'; | import { projectIssuesRoutes } from '../routes'; | ||||
it('should show education principles', async () => { | it('should show education principles', async () => { | ||||
const user = userEvent.setup(); | const user = userEvent.setup(); | ||||
renderProjectIssuesApp('project/issues?issues=issue2&open=issue2&id=myproject'); | 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(); | expect(screen.getByRole('heading', { name: 'Defense-In-Depth', level: 3 })).toBeInTheDocument(); | ||||
}); | }); | ||||
it('should open issue and navigate', async () => { | it('should open issue and navigate', async () => { | ||||
const user = userEvent.setup(); | const user = userEvent.setup(); | ||||
renderIssueApp(); | |||||
renderIssueApp(mockCurrentUser()); | |||||
// Select an issue with an advanced rule | // Select an issue with an advanced rule | ||||
expect(await screen.findByRole('region', { name: 'Fix that' })).toBeInTheDocument(); | expect(await screen.findByRole('region', { name: 'Fix that' })).toBeInTheDocument(); | ||||
expect(screen.getByRole('link', { name: 'advancedRuleId' })).toBeInTheDocument(); | expect(screen.getByRole('link', { name: 'advancedRuleId' })).toBeInTheDocument(); | ||||
// Select the "why is this an issue" tab and check its content | // 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(); | expect(screen.getByRole('heading', { name: 'Because' })).toBeInTheDocument(); | ||||
// Select the "how to fix it" tab | // 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? | // Is the context selector present with the expected values and default selection? | ||||
expect(screen.getByRole('button', { name: 'Context 2' })).toBeInTheDocument(); | expect(screen.getByRole('button', { name: 'Context 2' })).toBeInTheDocument(); | ||||
expect(screen.getByText('coding_rules.context.others.description.second')).toBeInTheDocument(); | expect(screen.getByText('coding_rules.context.others.description.second')).toBeInTheDocument(); | ||||
// Select the main info tab and check its content | // 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(); | expect(screen.getByRole('heading', { name: 'Link' })).toBeInTheDocument(); | ||||
// check for extended description | // check for extended description | ||||
expect(screen.getByRole('link', { name: 'simpleRuleId' })).toBeInTheDocument(); | expect(screen.getByRole('link', { name: 'simpleRuleId' })).toBeInTheDocument(); | ||||
// Select the "why is this an issue tab" and check its content | // 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(); | expect(screen.getByRole('heading', { name: 'Default' })).toBeInTheDocument(); | ||||
// Select the previous issue (with a simple rule) through keyboard shortcut | // Select the previous issue (with a simple rule) through keyboard shortcut | ||||
expect(screen.getByRole('heading', { level: 1, name: 'Issue on file' })).toBeInTheDocument(); | expect(screen.getByRole('heading', { level: 1, name: 'Issue on file' })).toBeInTheDocument(); | ||||
expect(screen.getByRole('link', { name: 'simpleRuleId' })).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('region', { name: 'Issue on file' })).toBeInTheDocument(); | ||||
expect( | expect( | ||||
screen.getByRole('row', { | screen.getByRole('row', { | ||||
}); | }); | ||||
}); | }); | ||||
function renderIssueApp() { | |||||
renderApp('project/issues', <IssuesApp />); | |||||
function renderIssueApp(currentUser?: CurrentUser) { | |||||
renderApp('project/issues', <IssuesApp />, { currentUser: mockCurrentUser(), ...currentUser }); | |||||
} | } | ||||
function renderProjectIssuesApp(navigateTo?: string) { | function renderProjectIssuesApp(navigateTo?: string) { |
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | ||||
*/ | */ | ||||
import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
import { groupBy } from 'lodash'; | |||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { Link } from 'react-router-dom'; | 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 { getRuleUrl } from '../../../helpers/urls'; | ||||
import { Component, Issue, RuleDetails } from '../../../types/types'; | import { Component, Issue, RuleDetails } from '../../../types/types'; | ||||
import { RuleDescriptionSections } from '../../coding-rules/rule'; | |||||
interface Props { | |||||
interface IssueViewerTabsProps { | |||||
component?: Component; | component?: Component; | ||||
issue: Issue; | issue: Issue; | ||||
codeTabContent: React.ReactNode; | codeTabContent: React.ReactNode; | ||||
ruleDetails: RuleDetails; | 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> | </div> | ||||
<TabViewer | |||||
ruleDetails={ruleDetails} | |||||
computeTabs={this.computeTabs} | |||||
codeTabContent={codeTabContent} | |||||
pageType="issues" | |||||
/> | |||||
</> | |||||
); | |||||
} | |||||
</div> | |||||
<TabViewer | |||||
ruleDetails={ruleDetails} | |||||
extendedDescription={ruleDetails.htmlNote} | |||||
ruleDescriptionContextKey={ruleDescriptionContextKey} | |||||
codeTabContent={codeTabContent} | |||||
pageType="issues" | |||||
/> | |||||
</> | |||||
); | |||||
} | } |
*/ | */ | ||||
import styled from '@emotion/styled'; | import styled from '@emotion/styled'; | ||||
import classNames from 'classnames'; | 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 * as React from 'react'; | ||||
import { Helmet } from 'react-helmet-async'; | import { Helmet } from 'react-helmet-async'; | ||||
import { FormattedMessage } from 'react-intl'; | import { FormattedMessage } from 'react-intl'; | ||||
import { SecurityStandard } from '../../../types/security'; | import { SecurityStandard } from '../../../types/security'; | ||||
import { Component, Dict, Issue, Paging, RawQuery, RuleDetails } from '../../../types/types'; | import { Component, Dict, Issue, Paging, RawQuery, RuleDetails } from '../../../types/types'; | ||||
import { CurrentUser, UserBase } from '../../../types/users'; | import { CurrentUser, UserBase } from '../../../types/users'; | ||||
import { RuleDescriptionSections } from '../../coding-rules/rule'; | |||||
import * as actions from '../actions'; | import * as actions from '../actions'; | ||||
import ConciseIssuesList from '../conciseIssuesList/ConciseIssuesList'; | import ConciseIssuesList from '../conciseIssuesList/ConciseIssuesList'; | ||||
import ConciseIssuesListHeader from '../conciseIssuesList/ConciseIssuesListHeader'; | import ConciseIssuesListHeader from '../conciseIssuesList/ConciseIssuesListHeader'; | ||||
} | } | ||||
this.setState({ loadingRule: true }); | this.setState({ loadingRule: true }); | ||||
const openRuleDetails = await getRuleDetails({ key: openIssue.rule }) | const openRuleDetails = await getRuleDetails({ key: openIssue.rule }) | ||||
.then(response => { | |||||
const ruleDetails = response.rule; | |||||
this.addExtendedDescription(ruleDetails); | |||||
return ruleDetails; | |||||
}) | |||||
.then(response => response.rule) | |||||
.catch(() => undefined); | .catch(() => undefined); | ||||
if (this.mounted) { | if (this.mounted) { | ||||
this.setState({ loadingRule: false, openRuleDetails }); | 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 = () => { | selectPreviousIssue = () => { | ||||
const { issues } = this.state; | const { issues } = this.state; | ||||
const selectedIndex = this.getSelectedIndex(); | const selectedIndex = this.getSelectedIndex(); |
import * as React from 'react'; | import * as React from 'react'; | ||||
import { searchIssues } from '../../../../api/issues'; | import { searchIssues } from '../../../../api/issues'; | ||||
import { getRuleDetails } from '../../../../api/rules'; | import { getRuleDetails } from '../../../../api/rules'; | ||||
import TabViewer from '../../../../components/rules/TabViewer'; | |||||
import handleRequiredAuthentication from '../../../../helpers/handleRequiredAuthentication'; | import handleRequiredAuthentication from '../../../../helpers/handleRequiredAuthentication'; | ||||
import { KeyboardKeys } from '../../../../helpers/keycodes'; | import { KeyboardKeys } from '../../../../helpers/keycodes'; | ||||
import { mockPullRequest } from '../../../../helpers/mocks/branch-like'; | import { mockPullRequest } from '../../../../helpers/mocks/branch-like'; | ||||
} from '../../actions'; | } from '../../actions'; | ||||
import BulkChangeModal from '../BulkChangeModal'; | import BulkChangeModal from '../BulkChangeModal'; | ||||
import { App } from '../IssuesApp'; | import { App } from '../IssuesApp'; | ||||
import IssuesSourceViewer from '../IssuesSourceViewer'; | |||||
import IssueViewerTabs from '../IssueTabViewer'; | |||||
jest.mock('../../../../helpers/pages', () => ({ | jest.mock('../../../../helpers/pages', () => ({ | ||||
addSideBarClass: jest.fn(), | addSideBarClass: jest.fn(), | ||||
expect(fetchFacet).lastCalledWith('owaspTop10'); | 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 () => { | it('should correctly bind key events for issue navigation', async () => { | ||||
const push = jest.fn(); | const push = jest.fn(); | ||||
const addEventListenerSpy = jest.spyOn(document, 'addEventListener'); | const addEventListenerSpy = jest.spyOn(document, 'addEventListener'); |
interface Props { | interface Props { | ||||
sections?: RuleDescriptionSection[]; | sections?: RuleDescriptionSection[]; | ||||
educationPrinciples?: string[]; | educationPrinciples?: string[]; | ||||
showNotification?: boolean; | |||||
displayEducationalPrinciplesNotification?: boolean; | |||||
educationPrinciplesRef?: React.RefObject<HTMLDivElement>; | educationPrinciplesRef?: React.RefObject<HTMLDivElement>; | ||||
} | } | ||||
}; | }; | ||||
render() { | render() { | ||||
const { showNotification, sections = [], educationPrinciples = [] } = this.props; | |||||
const { | |||||
displayEducationalPrinciplesNotification, | |||||
sections = [], | |||||
educationPrinciples = [], | |||||
educationPrinciplesRef | |||||
} = this.props; | |||||
return ( | return ( | ||||
<div className="big-padded rule-desc"> | <div className="big-padded rule-desc"> | ||||
{showNotification && ( | |||||
{displayEducationalPrinciplesNotification && ( | |||||
<Alert variant="info"> | <Alert variant="info"> | ||||
<p className="little-spacer-bottom little-spacer-top"> | <p className="little-spacer-bottom little-spacer-top"> | ||||
{translate('coding_rules.more_info.notification_message')} | {translate('coding_rules.more_info.notification_message')} | ||||
{educationPrinciples.length > 0 && ( | {educationPrinciples.length > 0 && ( | ||||
<> | <> | ||||
<h2 ref={this.props.educationPrinciplesRef}> | |||||
<h2 ref={educationPrinciplesRef}> | |||||
{translate('coding_rules.more_info.education_principles.title')} | {translate('coding_rules.more_info.education_principles.title')} | ||||
</h2> | </h2> | ||||
{educationPrinciples.map(key => { | {educationPrinciples.map(key => { |
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | ||||
*/ | */ | ||||
import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
import { debounce, Dictionary } from 'lodash'; | |||||
import { cloneDeep, debounce, groupBy } from 'lodash'; | |||||
import * as React from 'react'; | 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 { RuleDetails } from '../../types/types'; | ||||
import { CurrentUser, NoticeType } from '../../types/users'; | |||||
import { NoticeType } from '../../types/users'; | |||||
import BoxedTabs from '../controls/BoxedTabs'; | import BoxedTabs from '../controls/BoxedTabs'; | ||||
import MoreInfoRuleDescription from './MoreInfoRuleDescription'; | import MoreInfoRuleDescription from './MoreInfoRuleDescription'; | ||||
import RuleDescription from './RuleDescription'; | import RuleDescription from './RuleDescription'; | ||||
import './style.css'; | import './style.css'; | ||||
interface Props { | |||||
interface TabViewerProps extends CurrentUserContextInterface { | |||||
ruleDetails: RuleDetails; | ruleDetails: RuleDetails; | ||||
extendedDescription?: string; | |||||
ruleDescriptionContextKey?: string; | |||||
codeTabContent?: React.ReactNode; | codeTabContent?: React.ReactNode; | ||||
computeTabs: ( | |||||
showNotice: boolean, | |||||
educationPrinciplesRef: React.RefObject<HTMLDivElement> | |||||
) => Tab[]; | |||||
pageType?: string; | pageType?: string; | ||||
} | } | ||||
interface State { | interface State { | ||||
currentTab: Tab; | |||||
tabs: Tab[]; | tabs: Tab[]; | ||||
selectedTab?: Tab; | |||||
displayEducationalPrinciplesNotification?: boolean; | |||||
educationalPrinciplesNotificationHasBeenDismissed?: boolean; | |||||
} | } | ||||
export interface Tab { | export interface Tab { | ||||
const DEBOUNCE_FOR_SCROLL = 250; | 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>; | educationPrinciplesRef: React.RefObject<HTMLDivElement>; | ||||
constructor(props: Props) { | |||||
constructor(props: TabViewerProps) { | |||||
super(props); | super(props); | ||||
const tabs = this.getUpdatedTabs(false); | |||||
this.state = { | |||||
tabs, | |||||
currentTab: tabs[0] | |||||
}; | |||||
this.educationPrinciplesRef = React.createRef(); | 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() { | 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 ( | 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() { | 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) { | if (this.educationPrinciplesRef.current) { | ||||
const rect = this.educationPrinciplesRef.current.getBoundingClientRect(); | 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) | dismissNotice(NoticeType.EDUCATION_PRINCIPLES) | ||||
.then(() => { | .then(() => { | ||||
document.removeEventListener('scroll', this.checkIfConceptIsVisible, { capture: true }); | |||||
this.showNotification = false; | |||||
this.detachScrollEvent(); | |||||
this.setState({ educationalPrinciplesNotificationHasBeenDismissed: true }); | |||||
}) | }) | ||||
.catch(() => { | .catch(() => { | ||||
/* noop */ | /* noop */ | ||||
} | } | ||||
}; | }; | ||||
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) => { | handleSelectTabs = (currentTabKey: TabKeys) => { | ||||
this.setState(({ tabs }) => ({ | 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() { | render() { | ||||
const { tabs, currentTab } = this.state; | |||||
const { tabs, selectedTab } = this.state; | |||||
const { pageType } = this.props; | const { pageType } = this.props; | ||||
if (!tabs || tabs.length === 0 || !selectedTab) { | |||||
return null; | |||||
} | |||||
const tabContent = tabs.find(t => t.key === selectedTab.key)?.content; | |||||
return ( | return ( | ||||
<> | <> | ||||
<div | <div | ||||
<BoxedTabs | <BoxedTabs | ||||
className="big-spacer-top" | className="big-spacer-top" | ||||
onSelect={this.handleSelectTabs} | onSelect={this.handleSelectTabs} | ||||
selected={currentTab.key} | |||||
selected={selectedTab.key} | |||||
tabs={tabs} | tabs={tabs} | ||||
/> | /> | ||||
</div> | </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); |
issue.transition.resetastoreview=Reset as To Review | issue.transition.resetastoreview=Reset as To Review | ||||
issue.transition.resetastoreview.description=The Security Hotspot should be analyzed again | issue.transition.resetastoreview.description=The Security Hotspot should be analyzed again | ||||
issue.tabs.code=Where is the issue? | 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=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 | vulnerability.transition.resetastoreview.description=The vulnerability can't be fixed as is and needs more details. The security hotspot needs to be reviewed again | ||||
coding_rules.description_section.title.introduction=Introduction | 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=Why is this an issue? | ||||
coding_rules.description_section.title.root_cause.SECURITY_HOTSPOT=What is the risk? | 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.how_to_fix=How can I fix it? | ||||
coding_rules.description_section.title.more_info=More Info | coding_rules.description_section.title.more_info=More Info | ||||