@@ -79,7 +79,7 @@ export default function HotspotSnippetContainerRenderer( | |||
<p className="spacer-bottom">{translate('hotspots.no_associated_lines')}</p> | |||
)} | |||
<HotspotSnippetHeader hotspot={hotspot} component={component} branchLike={branchLike} /> | |||
<div className="bordered big-spacer-bottom"> | |||
<div className="bordered"> | |||
<DeferredSpinner className="big-spacer" loading={loading}> | |||
{sourceLines.length > 0 && ( | |||
<SnippetViewer |
@@ -71,13 +71,17 @@ export function HotspotViewerRenderer(props: HotspotViewerRendererProps) { | |||
{hotspot && ( | |||
<div className="big-padded hotspot-content"> | |||
<HotspotHeader hotspot={hotspot} onUpdateHotspot={props.onUpdateHotspot} /> | |||
<HotspotSnippetContainer | |||
branchLike={fillBranchLike(hotspot.project.branch, hotspot.project.pullRequest)} | |||
component={component} | |||
<HotspotViewerTabs | |||
codeTabContent={ | |||
<HotspotSnippetContainer | |||
branchLike={fillBranchLike(hotspot.project.branch, hotspot.project.pullRequest)} | |||
component={component} | |||
hotspot={hotspot} | |||
onCommentButtonClick={props.onShowCommentForm} | |||
/> | |||
} | |||
hotspot={hotspot} | |||
onCommentButtonClick={props.onShowCommentForm} | |||
/> | |||
<HotspotViewerTabs hotspot={hotspot} /> | |||
<HotspotReviewHistoryAndComments | |||
commentTextRef={commentTextRef} | |||
currentUser={currentUser} |
@@ -19,12 +19,12 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import BoxedTabs from '../../../components/controls/BoxedTabs'; | |||
import Tab from '../../../components/controls/Tabs'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { sanitizeString } from '../../../helpers/sanitize'; | |||
import { Hotspot } from '../../../types/security-hotspots'; | |||
interface Props { | |||
codeTabContent: React.ReactNode; | |||
hotspot: Hotspot; | |||
} | |||
@@ -40,6 +40,7 @@ interface Tab { | |||
} | |||
export enum TabKeys { | |||
Code = 'code', | |||
RiskDescription = 'risk', | |||
VulnerabilityDescription = 'vulnerability', | |||
FixRecommendation = 'fix' | |||
@@ -73,7 +74,8 @@ export default class HotspotViewerTabs extends React.PureComponent<Props, State> | |||
computeTabs() { | |||
const { hotspot } = this.props; | |||
return [ | |||
const descriptionTabs = [ | |||
{ | |||
key: TabKeys.RiskDescription, | |||
label: translate('hotspots.tabs.risk_description'), | |||
@@ -89,24 +91,35 @@ export default class HotspotViewerTabs extends React.PureComponent<Props, State> | |||
label: translate('hotspots.tabs.fix_recommendations'), | |||
content: hotspot.rule.fixRecommendations || '' | |||
} | |||
].filter(tab => Boolean(tab.content)); | |||
].filter(tab => tab.content.length > 0); | |||
return [ | |||
{ | |||
key: TabKeys.Code, | |||
label: translate('hotspots.tabs.code'), | |||
content: '' | |||
}, | |||
...descriptionTabs | |||
]; | |||
} | |||
render() { | |||
const { codeTabContent } = this.props; | |||
const { tabs, currentTab } = this.state; | |||
if (tabs.length === 0) { | |||
return null; | |||
} | |||
return ( | |||
<> | |||
<BoxedTabs onSelect={this.handleSelectTabs} selected={currentTab.key} tabs={tabs} /> | |||
<div className="bordered huge-spacer-bottom"> | |||
<div | |||
className="markdown big-padded" | |||
// eslint-disable-next-line react/no-danger | |||
dangerouslySetInnerHTML={{ __html: sanitizeString(currentTab.content) }} | |||
/> | |||
{currentTab.key === TabKeys.Code ? ( | |||
<div className="padded">{codeTabContent}</div> | |||
) : ( | |||
<div | |||
className="markdown big-padded" | |||
// eslint-disable-next-line react/no-danger | |||
dangerouslySetInnerHTML={{ __html: sanitizeString(currentTab.content) }} | |||
/> | |||
)} | |||
</div> | |||
</> | |||
); |
@@ -19,7 +19,7 @@ | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import BoxedTabs from '../../../../components/controls/BoxedTabs'; | |||
import BoxedTabs, { BoxedTabsProps } from '../../../../components/controls/BoxedTabs'; | |||
import { mockHotspot, mockHotspotRule } from '../../../../helpers/mocks/security-hotspots'; | |||
import { mockUser } from '../../../../helpers/testMocks'; | |||
import HotspotViewerTabs, { TabKeys } from '../HotspotViewerTabs'; | |||
@@ -28,7 +28,7 @@ it('should render correctly', () => { | |||
const wrapper = shallowRender(); | |||
expect(wrapper).toMatchSnapshot('risk'); | |||
const onSelect = wrapper.find(BoxedTabs).prop('onSelect') as (tab: TabKeys) => void; | |||
const onSelect: (tab: TabKeys) => void = wrapper.find(BoxedTabs).prop('onSelect'); | |||
onSelect(TabKeys.VulnerabilityDescription); | |||
expect(wrapper).toMatchSnapshot('vulnerability'); | |||
@@ -46,8 +46,10 @@ it('should render correctly', () => { | |||
vulnerabilityDescription: undefined | |||
}) | |||
}) | |||
}).type() | |||
).toBeNull(); | |||
}) | |||
.find<BoxedTabsProps<string>>(BoxedTabs) | |||
.props().tabs | |||
).toHaveLength(1); | |||
expect( | |||
shallowRender({ | |||
@@ -86,14 +88,20 @@ it('should filter empty tab', () => { | |||
it('should select first tab on hotspot update', () => { | |||
const wrapper = shallowRender(); | |||
const onSelect = wrapper.find(BoxedTabs).prop('onSelect') as (tab: TabKeys) => void; | |||
const onSelect: (tab: TabKeys) => void = wrapper.find(BoxedTabs).prop('onSelect'); | |||
onSelect(TabKeys.VulnerabilityDescription); | |||
expect(wrapper.state().currentTab.key).toBe(TabKeys.VulnerabilityDescription); | |||
wrapper.setProps({ hotspot: mockHotspot({ key: 'new_key' }) }); | |||
expect(wrapper.state().currentTab.key).toBe(TabKeys.RiskDescription); | |||
expect(wrapper.state().currentTab.key).toBe(TabKeys.Code); | |||
}); | |||
function shallowRender(props?: Partial<HotspotViewerTabs['props']>) { | |||
return shallow<HotspotViewerTabs>(<HotspotViewerTabs hotspot={mockHotspot()} {...props} />); | |||
return shallow<HotspotViewerTabs>( | |||
<HotspotViewerTabs | |||
codeTabContent={<div>CodeTabContent</div>} | |||
hotspot={mockHotspot()} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -126,7 +126,7 @@ exports[`should render correctly 1`] = ` | |||
} | |||
/> | |||
<div | |||
className="bordered big-spacer-bottom" | |||
className="bordered" | |||
> | |||
<DeferredSpinner | |||
className="big-spacer" | |||
@@ -257,7 +257,7 @@ exports[`should render correctly: with sourcelines 1`] = ` | |||
} | |||
/> | |||
<div | |||
className="bordered big-spacer-bottom" | |||
className="bordered" | |||
> | |||
<DeferredSpinner | |||
className="big-spacer" |
@@ -7,6 +7,11 @@ exports[`should render correctly: fix 1`] = ` | |||
selected="fix" | |||
tabs={ | |||
Array [ | |||
Object { | |||
"content": "", | |||
"key": "code", | |||
"label": "hotspots.tabs.code", | |||
}, | |||
Object { | |||
"content": "<p>This a <strong>strong</strong> message about risk !</p>", | |||
"key": "risk", | |||
@@ -44,9 +49,14 @@ exports[`should render correctly: risk 1`] = ` | |||
<Fragment> | |||
<BoxedTabs | |||
onSelect={[Function]} | |||
selected="risk" | |||
selected="code" | |||
tabs={ | |||
Array [ | |||
Object { | |||
"content": "", | |||
"key": "code", | |||
"label": "hotspots.tabs.code", | |||
}, | |||
Object { | |||
"content": "<p>This a <strong>strong</strong> message about risk !</p>", | |||
"key": "risk", | |||
@@ -69,13 +79,12 @@ exports[`should render correctly: risk 1`] = ` | |||
className="bordered huge-spacer-bottom" | |||
> | |||
<div | |||
className="markdown big-padded" | |||
dangerouslySetInnerHTML={ | |||
Object { | |||
"__html": "<p>This a <strong>strong</strong> message about risk !</p>", | |||
} | |||
} | |||
/> | |||
className="padded" | |||
> | |||
<div> | |||
CodeTabContent | |||
</div> | |||
</div> | |||
</div> | |||
</Fragment> | |||
`; | |||
@@ -87,6 +96,11 @@ exports[`should render correctly: vulnerability 1`] = ` | |||
selected="vulnerability" | |||
tabs={ | |||
Array [ | |||
Object { | |||
"content": "", | |||
"key": "code", | |||
"label": "hotspots.tabs.code", | |||
}, | |||
Object { | |||
"content": "<p>This a <strong>strong</strong> message about risk !</p>", | |||
"key": "risk", | |||
@@ -124,9 +138,14 @@ exports[`should render correctly: with comments or changelog element 1`] = ` | |||
<Fragment> | |||
<BoxedTabs | |||
onSelect={[Function]} | |||
selected="risk" | |||
selected="code" | |||
tabs={ | |||
Array [ | |||
Object { | |||
"content": "", | |||
"key": "code", | |||
"label": "hotspots.tabs.code", | |||
}, | |||
Object { | |||
"content": "<p>This a <strong>strong</strong> message about risk !</p>", | |||
"key": "risk", | |||
@@ -149,13 +168,12 @@ exports[`should render correctly: with comments or changelog element 1`] = ` | |||
className="bordered huge-spacer-bottom" | |||
> | |||
<div | |||
className="markdown big-padded" | |||
dangerouslySetInnerHTML={ | |||
Object { | |||
"__html": "<p>This a <strong>strong</strong> message about risk !</p>", | |||
} | |||
} | |||
/> | |||
className="padded" | |||
> | |||
<div> | |||
CodeTabContent | |||
</div> | |||
</div> | |||
</div> | |||
</Fragment> | |||
`; |
@@ -735,8 +735,9 @@ hotspots.list_title.FIXED={0} Security Hotspots reviewed as fixed | |||
hotspots.list_title.SAFE={0} Security Hotspots reviewed as safe | |||
hotspots.risk_exposure=Review priority | |||
hotspots.tabs.code=Where is the risk? | |||
hotspots.tabs.risk_description=What's the risk? | |||
hotspots.tabs.vulnerability_description=Are you at risk? | |||
hotspots.tabs.vulnerability_description=Assess the risk | |||
hotspots.tabs.fix_recommendations=How can you fix it? | |||
hotspots.review_history.created=created Security Hotspot | |||
hotspots.review_history.comment_added=added a comment |