@@ -21,7 +21,7 @@ import { getJSON, post } from 'sonar-ui-common/helpers/request'; | |||
import throwGlobalError from '../app/utils/throwGlobalError'; | |||
import { BranchParameters } from '../types/branch-like'; | |||
import { | |||
DetailedHotspot, | |||
Hotspot, | |||
HotspotAssignRequest, | |||
HotspotResolution, | |||
HotspotSearchResponse, | |||
@@ -58,6 +58,6 @@ export function getSecurityHotspots( | |||
return getJSON('/api/hotspots/search', data).catch(throwGlobalError); | |||
} | |||
export function getSecurityHotspotDetails(securityHotspotKey: string): Promise<DetailedHotspot> { | |||
export function getSecurityHotspotDetails(securityHotspotKey: string): Promise<Hotspot> { | |||
return getJSON('/api/hotspots/show', { hotspot: securityHotspotKey }).catch(throwGlobalError); | |||
} |
@@ -69,3 +69,14 @@ a.badge { | |||
background-color: var(--alertBackgroundError); | |||
color: var(--alertTextError); | |||
} | |||
.counter-badge { | |||
color: var(--badgeBlueColor); | |||
background-color: var(--badgeBlueBackground); | |||
padding: calc(var(--gridSize) / 2) var(--gridSize); | |||
border-radius: 1em; | |||
} | |||
.counter-badge:empty { | |||
display: none; | |||
} |
@@ -103,6 +103,10 @@ module.exports = { | |||
alertTextInfo: '#0e516f', | |||
alertIconInfo: '#0271b9', | |||
// badge | |||
badgeBlueBackground: '#2E7CB5', | |||
badgeBlueColor: '#FFFFFF', | |||
// alm | |||
azure: '#0078d7', | |||
bitbucket: '#0052CC', |
@@ -17,9 +17,9 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { mockRawHotspot } from '../../../helpers/mocks/security-hotspots'; | |||
import { RiskExposure } from '../../../types/security-hotspots'; | |||
import { groupByCategory, mapRules, sortHotspots } from '../utils'; | |||
import { mockHotspot, mockRawHotspot } from '../../../helpers/mocks/security-hotspots'; | |||
import { ReviewHistoryType, RiskExposure } from '../../../types/security-hotspots'; | |||
import { getHotspotReviewHistory, groupByCategory, mapRules, sortHotspots } from '../utils'; | |||
const hotspots = [ | |||
mockRawHotspot({ | |||
@@ -142,3 +142,51 @@ describe('mapRules', () => { | |||
}); | |||
}); | |||
}); | |||
describe('getHotspotReviewHistory', () => { | |||
it('should properly create the review history', () => { | |||
const changelogElement: T.IssueChangelog = { | |||
creationDate: '2018-10-01', | |||
isUserActive: true, | |||
user: 'me', | |||
userName: 'me-name', | |||
diffs: [ | |||
{ | |||
key: 'assign', | |||
newValue: 'me', | |||
oldValue: 'him' | |||
} | |||
] | |||
}; | |||
const hotspot = mockHotspot({ | |||
creationDate: '2018-09-01', | |||
changelog: [changelogElement] | |||
}); | |||
const history = getHotspotReviewHistory(hotspot); | |||
expect(history.length).toBe(2); | |||
expect(history[0]).toEqual( | |||
expect.objectContaining({ | |||
type: ReviewHistoryType.Creation, | |||
date: hotspot.creationDate, | |||
user: { | |||
avatar: hotspot.author.avatar, | |||
name: hotspot.author.name, | |||
active: hotspot.author.active | |||
} | |||
}) | |||
); | |||
expect(history[1]).toEqual( | |||
expect.objectContaining({ | |||
type: ReviewHistoryType.Diff, | |||
date: changelogElement.creationDate, | |||
user: { | |||
avatar: changelogElement.avatar, | |||
name: changelogElement.userName, | |||
active: changelogElement.isUserActive | |||
}, | |||
diffs: changelogElement.diffs | |||
}) | |||
); | |||
}); | |||
}); |
@@ -24,11 +24,11 @@ import OutsideClickHandler from 'sonar-ui-common/components/controls/OutsideClic | |||
import DropdownIcon from 'sonar-ui-common/components/icons/DropdownIcon'; | |||
import { PopupPlacement } from 'sonar-ui-common/components/ui/popups'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import { DetailedHotspot, HotspotUpdateFields } from '../../../types/security-hotspots'; | |||
import { Hotspot, HotspotUpdateFields } from '../../../types/security-hotspots'; | |||
import HotspotActionsForm from './HotspotActionsForm'; | |||
export interface HotspotActionsProps { | |||
hotspot: DetailedHotspot; | |||
hotspot: Hotspot; | |||
onSubmit: (hotspot: HotspotUpdateFields) => void; | |||
} | |||
@@ -53,7 +53,7 @@ export default function HotspotCategory(props: HotspotCategoryProps) { | |||
onClick={() => setExpanded(!expanded)}> | |||
<strong className="flex-1">{category.title}</strong> | |||
<span> | |||
<span className="hotspot-counter">{hotspots.length}</span> | |||
<span className="counter-badge">{hotspots.length}</span> | |||
{expanded ? ( | |||
<ChevronUpIcon className="big-spacer-left" /> | |||
) : ( |
@@ -75,13 +75,6 @@ | |||
cursor: unset; | |||
} | |||
.hotspot-counter { | |||
color: var(--baseFontColor); | |||
background-color: var(--gray94); | |||
border-radius: 50%; | |||
padding: calc(var(--gridSize) / 2) var(--gridSize); | |||
} | |||
.hotspot-risk-badge { | |||
color: white; | |||
text-transform: uppercase; |
@@ -22,13 +22,13 @@ import { getSources } from '../../../api/components'; | |||
import { locationsByLine } from '../../../components/SourceViewer/helpers/indexing'; | |||
import { getBranchLikeQuery } from '../../../helpers/branch-like'; | |||
import { BranchLike } from '../../../types/branch-like'; | |||
import { DetailedHotspot } from '../../../types/security-hotspots'; | |||
import { Hotspot } from '../../../types/security-hotspots'; | |||
import { constructSourceViewerFile } from '../utils'; | |||
import HotspotSnippetContainerRenderer from './HotspotSnippetContainerRenderer'; | |||
interface Props { | |||
branchLike?: BranchLike; | |||
hotspot: DetailedHotspot; | |||
hotspot: Hotspot; | |||
} | |||
interface State { |
@@ -22,13 +22,13 @@ import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner'; | |||
import { SourceViewerContext } from '../../../components/SourceViewer/SourceViewerContext'; | |||
import SourceViewerHeaderSlim from '../../../components/SourceViewer/SourceViewerHeaderSlim'; | |||
import { BranchLike } from '../../../types/branch-like'; | |||
import { DetailedHotspot } from '../../../types/security-hotspots'; | |||
import { Hotspot } from '../../../types/security-hotspots'; | |||
import SnippetViewer from '../../issues/crossComponentSourceViewer/SnippetViewer'; | |||
export interface HotspotSnippetContainerRendererProps { | |||
branchLike?: BranchLike; | |||
highlightedSymbols: string[]; | |||
hotspot: DetailedHotspot; | |||
hotspot: Hotspot; | |||
lastLine?: number; | |||
loading: boolean; | |||
locations: { [line: number]: T.LinearIssueLocation[] }; |
@@ -21,11 +21,7 @@ | |||
import * as React from 'react'; | |||
import { getSecurityHotspotDetails } from '../../../api/security-hotspots'; | |||
import { BranchLike } from '../../../types/branch-like'; | |||
import { | |||
DetailedHotspot, | |||
HotspotUpdate, | |||
HotspotUpdateFields | |||
} from '../../../types/security-hotspots'; | |||
import { Hotspot, HotspotUpdate, HotspotUpdateFields } from '../../../types/security-hotspots'; | |||
import HotspotViewerRenderer from './HotspotViewerRenderer'; | |||
interface Props { | |||
@@ -36,14 +32,15 @@ interface Props { | |||
} | |||
interface State { | |||
hotspot?: DetailedHotspot; | |||
hotspot?: Hotspot; | |||
loading: boolean; | |||
} | |||
export default class HotspotViewer extends React.PureComponent<Props, State> { | |||
mounted = false; | |||
state: State = { loading: false }; | |||
componentWillMount() { | |||
componentDidMount() { | |||
this.mounted = true; | |||
this.fetchHotspot(); | |||
} | |||
@@ -61,8 +58,8 @@ export default class HotspotViewer extends React.PureComponent<Props, State> { | |||
fetchHotspot() { | |||
this.setState({ loading: true }); | |||
return getSecurityHotspotDetails(this.props.hotspotKey) | |||
.then(hotspot => this.mounted && this.setState({ hotspot })) | |||
.finally(() => this.mounted && this.setState({ loading: false })); | |||
.then(hotspot => this.mounted && this.setState({ hotspot, loading: false })) | |||
.catch(() => this.mounted && this.setState({ loading: false })); | |||
} | |||
handleHotspotUpdate = (data: HotspotUpdateFields) => { |
@@ -23,7 +23,7 @@ import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n | |||
import { withCurrentUser } from '../../../components/hoc/withCurrentUser'; | |||
import { isLoggedIn } from '../../../helpers/users'; | |||
import { BranchLike } from '../../../types/branch-like'; | |||
import { DetailedHotspot, HotspotUpdateFields } from '../../../types/security-hotspots'; | |||
import { Hotspot, HotspotUpdateFields } from '../../../types/security-hotspots'; | |||
import HotspotActions from './HotspotActions'; | |||
import HotspotSnippetContainer from './HotspotSnippetContainer'; | |||
import HotspotViewerTabs from './HotspotViewerTabs'; | |||
@@ -31,7 +31,7 @@ import HotspotViewerTabs from './HotspotViewerTabs'; | |||
export interface HotspotViewerRendererProps { | |||
branchLike?: BranchLike; | |||
currentUser: T.CurrentUser; | |||
hotspot?: DetailedHotspot; | |||
hotspot?: Hotspot; | |||
loading: boolean; | |||
onUpdateHotspot: (hotspot: HotspotUpdateFields) => void; | |||
securityCategories: T.StandardSecurityCategories; | |||
@@ -52,20 +52,20 @@ export function HotspotViewerRenderer(props: HotspotViewerRendererProps) { | |||
)} | |||
</div> | |||
<div className="text-muted"> | |||
<span>{translate('hotspot.category')}</span> | |||
<span>{translate('category')}:</span> | |||
<span className="little-spacer-left"> | |||
{securityCategories[hotspot.rule.securityCategory].title} | |||
</span> | |||
</div> | |||
</div> | |||
<div className="huge-spacer-bottom"> | |||
<span>{translate('hotspot.status')}</span> | |||
<span>{translate('status')}:</span> | |||
<span className="badge little-spacer-left"> | |||
{translate('hotspot.status', hotspot.resolution || hotspot.status)} | |||
</span> | |||
{hotspot.assignee && hotspot.assignee.name && ( | |||
<> | |||
<span className="huge-spacer-left">{translate('hotspot.assigned_to')}</span> | |||
<span className="huge-spacer-left">{translate('assigned_to')}:</span> | |||
<strong className="little-spacer-left"> | |||
{hotspot.assignee.active | |||
? hotspot.assignee.name |
@@ -0,0 +1,80 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n'; | |||
import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; | |||
import IssueChangelogDiff from '../../../components/issue/components/IssueChangelogDiff'; | |||
import Avatar from '../../../components/ui/Avatar'; | |||
import { ReviewHistoryElement, ReviewHistoryType } from '../../../types/security-hotspots'; | |||
export interface HotspotViewerReviewHistoryTabProps { | |||
history: ReviewHistoryElement[]; | |||
} | |||
export default function HotspotViewerReviewHistoryTab(props: HotspotViewerReviewHistoryTabProps) { | |||
const { history } = props; | |||
return ( | |||
<> | |||
{history.map((elt, i) => ( | |||
<React.Fragment key={`${elt.user.name}-${elt.date}`}> | |||
{i > 0 && <hr />} | |||
<div> | |||
<div className="display-flex-center"> | |||
{elt.user.name && ( | |||
<> | |||
<Avatar | |||
className="little-spacer-right" | |||
hash={elt.user.avatar} | |||
name={elt.user.name} | |||
size={20} | |||
/> | |||
<strong> | |||
{elt.user.active | |||
? elt.user.name | |||
: translateWithParameters('user.x_deleted', elt.user.name)} | |||
</strong> | |||
{elt.type === ReviewHistoryType.Creation && ( | |||
<span className="little-spacer-left"> | |||
{translate('hotspots.tabs.review_history.created')} | |||
</span> | |||
)} | |||
<span className="little-spacer-left little-spacer-right">-</span> | |||
</> | |||
)} | |||
<DateTimeFormatter date={elt.date} /> | |||
</div> | |||
{elt.type === ReviewHistoryType.Diff && elt.diffs && ( | |||
<div className="spacer-top"> | |||
{elt.diffs.map(diff => ( | |||
<IssueChangelogDiff | |||
diff={diff} | |||
key={`${diff.key}-${diff.oldValue}-${diff.newValue}`} | |||
/> | |||
))} | |||
</div> | |||
)} | |||
</div> | |||
</React.Fragment> | |||
))} | |||
</> | |||
); | |||
} |
@@ -21,56 +21,79 @@ import { sanitize } from 'dompurify'; | |||
import * as React from 'react'; | |||
import BoxedTabs from 'sonar-ui-common/components/controls/BoxedTabs'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import { DetailedHotspot } from '../../../types/security-hotspots'; | |||
import { Hotspot } from '../../../types/security-hotspots'; | |||
import { getHotspotReviewHistory } from '../utils'; | |||
import HotspotViewerReviewHistoryTab from './HotspotViewerReviewHistoryTab'; | |||
export interface HotspotViewerTabsProps { | |||
hotspot: DetailedHotspot; | |||
hotspot: Hotspot; | |||
} | |||
export enum Tabs { | |||
RiskDescription = 'risk', | |||
VulnerabilityDescription = 'vulnerability', | |||
FixRecommendation = 'fix' | |||
FixRecommendation = 'fix', | |||
ReviewHistory = 'review' | |||
} | |||
export default function HotspotViewerTabs(props: HotspotViewerTabsProps) { | |||
const { hotspot } = props; | |||
const [currentTab, setCurrentTab] = React.useState(Tabs.RiskDescription); | |||
const [currentTabKey, setCurrentTabKey] = React.useState(Tabs.RiskDescription); | |||
const hotspotReviewHistory = React.useMemo(() => getHotspotReviewHistory(hotspot), [hotspot]); | |||
const tabs = { | |||
[Tabs.RiskDescription]: { | |||
title: translate('hotspot.tabs.risk_description'), | |||
const tabs = [ | |||
{ | |||
key: Tabs.RiskDescription, | |||
label: translate('hotspots.tabs.risk_description'), | |||
content: hotspot.rule.riskDescription || '' | |||
}, | |||
[Tabs.VulnerabilityDescription]: { | |||
title: translate('hotspot.tabs.vulnerability_description'), | |||
{ | |||
key: Tabs.VulnerabilityDescription, | |||
label: translate('hotspots.tabs.vulnerability_description'), | |||
content: hotspot.rule.vulnerabilityDescription || '' | |||
}, | |||
[Tabs.FixRecommendation]: { | |||
title: translate('hotspot.tabs.fix_recommendations'), | |||
{ | |||
key: Tabs.FixRecommendation, | |||
label: translate('hotspots.tabs.fix_recommendations'), | |||
content: hotspot.rule.fixRecommendations || '' | |||
}, | |||
{ | |||
key: Tabs.ReviewHistory, | |||
label: ( | |||
<> | |||
<span>{translate('hotspots.tabs.review_history')}</span> | |||
<span className="counter-badge spacer-left">{hotspotReviewHistory.length}</span> | |||
</> | |||
), | |||
content: hotspotReviewHistory.length > 0 && ( | |||
<HotspotViewerReviewHistoryTab history={hotspotReviewHistory} /> | |||
) | |||
} | |||
}; | |||
const tabsToDisplay = Object.values(Tabs) | |||
.filter(tab => Boolean(tabs[tab].content)) | |||
.map(tab => ({ key: tab, label: tabs[tab].title })); | |||
].filter(tab => Boolean(tab.content)); | |||
if (tabsToDisplay.length === 0) { | |||
if (tabs.length === 0) { | |||
return null; | |||
} | |||
if (!tabsToDisplay.find(tab => tab.key === currentTab)) { | |||
setCurrentTab(tabsToDisplay[0].key); | |||
} | |||
const currentTab = tabs.find(tab => tab.key === currentTabKey) || tabs[0]; | |||
return ( | |||
<> | |||
<BoxedTabs onSelect={tab => setCurrentTab(tab)} selected={currentTab} tabs={tabsToDisplay} /> | |||
<div | |||
className="boxed-group markdown big-padded" | |||
dangerouslySetInnerHTML={{ __html: sanitize(tabs[currentTab].content) }} | |||
<BoxedTabs | |||
onSelect={tabKey => setCurrentTabKey(tabKey)} | |||
selected={currentTabKey} | |||
tabs={tabs} | |||
/> | |||
<div className="boxed-group big-padded"> | |||
{typeof currentTab.content === 'string' ? ( | |||
<div | |||
className="markdown" | |||
dangerouslySetInnerHTML={{ __html: sanitize(currentTab.content) }} | |||
/> | |||
) : ( | |||
<>{currentTab.content}</> | |||
)} | |||
</div> | |||
</> | |||
); | |||
} |
@@ -21,7 +21,7 @@ import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { Button } from 'sonar-ui-common/components/controls/buttons'; | |||
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; | |||
import { mockDetailledHotspot } from '../../../../helpers/mocks/security-hotspots'; | |||
import { mockHotspot } from '../../../../helpers/mocks/security-hotspots'; | |||
import { HotspotStatus } from '../../../../types/security-hotspots'; | |||
import HotspotActions, { HotspotActionsProps } from '../HotspotActions'; | |||
@@ -70,7 +70,7 @@ it('should register an eventlistener', () => { | |||
function shallowRender(props: Partial<HotspotActionsProps> = {}) { | |||
return shallow( | |||
<HotspotActions | |||
hotspot={mockDetailledHotspot({ key: 'key', status: HotspotStatus.TO_REVIEW })} | |||
hotspot={mockHotspot({ key: 'key', status: HotspotStatus.TO_REVIEW })} | |||
onSubmit={jest.fn()} | |||
{...props} | |||
/> |
@@ -23,7 +23,7 @@ import * as React from 'react'; | |||
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; | |||
import { getSources } from '../../../../api/components'; | |||
import { mockBranch } from '../../../../helpers/mocks/branch-like'; | |||
import { mockDetailledHotspot } from '../../../../helpers/mocks/security-hotspots'; | |||
import { mockHotspot } from '../../../../helpers/mocks/security-hotspots'; | |||
import { mockSourceLine } from '../../../../helpers/testMocks'; | |||
import HotspotSnippetContainer from '../HotspotSnippetContainer'; | |||
import HotspotSnippetContainerRenderer from '../HotspotSnippetContainerRenderer'; | |||
@@ -43,7 +43,7 @@ it('should load sources on mount', async () => { | |||
range(5, 18).map(line => mockSourceLine({ line })) | |||
); | |||
const hotspot = mockDetailledHotspot({ | |||
const hotspot = mockHotspot({ | |||
textRange: { startLine: 10, endLine: 11, startOffset: 0, endOffset: 12 } | |||
}); | |||
@@ -65,7 +65,7 @@ it('should handle end-of-file on mount', async () => { | |||
range(5, 15).map(line => mockSourceLine({ line })) | |||
); | |||
const hotspot = mockDetailledHotspot({ | |||
const hotspot = mockHotspot({ | |||
textRange: { startLine: 10, endLine: 11, startOffset: 0, endOffset: 12 } | |||
}); | |||
@@ -85,7 +85,7 @@ describe('Expansion', () => { | |||
); | |||
}); | |||
const hotspot = mockDetailledHotspot({ | |||
const hotspot = mockHotspot({ | |||
textRange: { startLine: 10, endLine: 11, startOffset: 0, endOffset: 12 } | |||
}); | |||
@@ -193,6 +193,6 @@ it('should handle symbol click', () => { | |||
function shallowRender(props?: Partial<HotspotSnippetContainer['props']>) { | |||
return shallow<HotspotSnippetContainer>( | |||
<HotspotSnippetContainer branchLike={branch} hotspot={mockDetailledHotspot()} {...props} /> | |||
<HotspotSnippetContainer branchLike={branch} hotspot={mockHotspot()} {...props} /> | |||
); | |||
} |
@@ -20,7 +20,7 @@ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { mockMainBranch } from '../../../../helpers/mocks/branch-like'; | |||
import { mockDetailledHotspot } from '../../../../helpers/mocks/security-hotspots'; | |||
import { mockHotspot } from '../../../../helpers/mocks/security-hotspots'; | |||
import { mockSourceLine, mockSourceViewerFile } from '../../../../helpers/testMocks'; | |||
import HotspotSnippetContainerRenderer, { | |||
HotspotSnippetContainerRendererProps | |||
@@ -36,7 +36,7 @@ function shallowRender(props?: Partial<HotspotSnippetContainerRendererProps>) { | |||
<HotspotSnippetContainerRenderer | |||
branchLike={mockMainBranch()} | |||
highlightedSymbols={[]} | |||
hotspot={mockDetailledHotspot()} | |||
hotspot={mockHotspot()} | |||
lastLine={undefined} | |||
linePopup={undefined} | |||
loading={false} |
@@ -19,7 +19,7 @@ | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { mockDetailledHotspot } from '../../../../helpers/mocks/security-hotspots'; | |||
import { mockHotspot } from '../../../../helpers/mocks/security-hotspots'; | |||
import { mockCurrentUser, mockLoggedInUser, mockUser } from '../../../../helpers/testMocks'; | |||
import { HotspotViewerRenderer, HotspotViewerRendererProps } from '../HotspotViewerRenderer'; | |||
@@ -27,9 +27,19 @@ it('should render correctly', () => { | |||
const wrapper = shallowRender(); | |||
expect(wrapper).toMatchSnapshot(); | |||
expect(shallowRender({ hotspot: undefined })).toMatchSnapshot('no hotspot'); | |||
expect(shallowRender({ hotspot: mockHotspot({ assignee: undefined }) })).toMatchSnapshot( | |||
'unassigned' | |||
); | |||
expect( | |||
shallowRender({ hotspot: mockDetailledHotspot({ assignee: mockUser({ active: false }) }) }) | |||
shallowRender({ hotspot: mockHotspot({ assignee: mockUser({ active: false }) }) }) | |||
).toMatchSnapshot('deleted assignee'); | |||
expect( | |||
shallowRender({ | |||
hotspot: mockHotspot({ | |||
assignee: mockUser({ name: undefined, login: 'assignee_login' }) | |||
}) | |||
}) | |||
).toMatchSnapshot('assignee without name'); | |||
expect(shallowRender()).toMatchSnapshot('anonymous user'); | |||
expect(shallowRender({ currentUser: mockLoggedInUser() })).toMatchSnapshot('user logged in'); | |||
}); | |||
@@ -38,7 +48,7 @@ function shallowRender(props?: Partial<HotspotViewerRendererProps>) { | |||
return shallow( | |||
<HotspotViewerRenderer | |||
currentUser={mockCurrentUser()} | |||
hotspot={mockDetailledHotspot()} | |||
hotspot={mockHotspot()} | |||
loading={false} | |||
onUpdateHotspot={jest.fn()} | |||
securityCategories={{ 'sql-injection': { title: 'SQL injection' } }} |
@@ -0,0 +1,52 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { mockHotspotReviewHistoryElement } from '../../../../helpers/mocks/security-hotspots'; | |||
import { mockUser } from '../../../../helpers/testMocks'; | |||
import { ReviewHistoryType } from '../../../../types/security-hotspots'; | |||
import HotspotViewerReviewHistoryTab, { | |||
HotspotViewerReviewHistoryTabProps | |||
} from '../HotspotViewerReviewHistoryTab'; | |||
it('should render correctly', () => { | |||
const wrapper = shallowRender(); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
function shallowRender(props?: Partial<HotspotViewerReviewHistoryTabProps>) { | |||
return shallow( | |||
<HotspotViewerReviewHistoryTab | |||
history={[ | |||
mockHotspotReviewHistoryElement({ user: mockUser({ avatar: 'with-avatar' }) }), | |||
mockHotspotReviewHistoryElement({ user: mockUser({ active: false }) }), | |||
mockHotspotReviewHistoryElement({ user: mockUser({ login: undefined, name: undefined }) }), | |||
mockHotspotReviewHistoryElement({ | |||
type: ReviewHistoryType.Diff, | |||
diffs: [ | |||
{ key: 'test', oldValue: 'old', newValue: 'new' }, | |||
{ key: 'test-1', oldValue: 'old-1', newValue: 'new-1' } | |||
] | |||
}) | |||
]} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -20,10 +20,7 @@ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import BoxedTabs from 'sonar-ui-common/components/controls/BoxedTabs'; | |||
import { | |||
mockDetailledHotspot, | |||
mockDetailledHotspotRule | |||
} from '../../../../helpers/mocks/security-hotspots'; | |||
import { mockHotspot, mockHotspotRule } from '../../../../helpers/mocks/security-hotspots'; | |||
import HotspotViewerTabs, { HotspotViewerTabsProps, Tabs } from '../HotspotViewerTabs'; | |||
it('should render correctly', () => { | |||
@@ -40,20 +37,24 @@ it('should render correctly', () => { | |||
onSelect(Tabs.FixRecommendation); | |||
expect(wrapper).toMatchSnapshot('fix'); | |||
onSelect(Tabs.ReviewHistory); | |||
expect(wrapper).toMatchSnapshot('review'); | |||
} | |||
expect( | |||
shallowRender({ | |||
hotspot: mockDetailledHotspot({ | |||
rule: mockDetailledHotspotRule({ riskDescription: undefined }) | |||
hotspot: mockHotspot({ | |||
rule: mockHotspotRule({ riskDescription: undefined }) | |||
}) | |||
}) | |||
).toMatchSnapshot('empty tab'); | |||
expect( | |||
shallowRender({ | |||
hotspot: mockDetailledHotspot({ | |||
rule: mockDetailledHotspotRule({ | |||
hotspot: mockHotspot({ | |||
creationDate: undefined, | |||
rule: mockHotspotRule({ | |||
riskDescription: undefined, | |||
fixRecommendations: undefined, | |||
vulnerabilityDescription: undefined | |||
@@ -64,5 +65,5 @@ it('should render correctly', () => { | |||
}); | |||
function shallowRender(props?: Partial<HotspotViewerTabsProps>) { | |||
return shallow(<HotspotViewerTabs hotspot={mockDetailledHotspot()} {...props} />); | |||
return shallow(<HotspotViewerTabs hotspot={mockHotspot()} {...props} />); | |||
} |
@@ -16,7 +16,7 @@ exports[`should handle collapse and expand 1`] = ` | |||
</strong> | |||
<span> | |||
<span | |||
className="hotspot-counter" | |||
className="counter-badge" | |||
> | |||
1 | |||
</span> | |||
@@ -44,7 +44,7 @@ exports[`should handle collapse and expand 2`] = ` | |||
</strong> | |||
<span> | |||
<span | |||
className="hotspot-counter" | |||
className="counter-badge" | |||
> | |||
1 | |||
</span> | |||
@@ -101,7 +101,7 @@ exports[`should render correctly with hotspots 1`] = ` | |||
</strong> | |||
<span> | |||
<span | |||
className="hotspot-counter" | |||
className="counter-badge" | |||
> | |||
2 | |||
</span> |
@@ -25,6 +25,7 @@ exports[`should render correctly 1`] = ` | |||
"login": "john.doe", | |||
"name": "John Doe", | |||
}, | |||
"changelog": Array [], | |||
"component": Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", |
@@ -149,6 +149,7 @@ exports[`should render correctly: with sourcelines 1`] = ` | |||
"login": "john.doe", | |||
"name": "John Doe", | |||
}, | |||
"changelog": Array [], | |||
"component": Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", |
@@ -22,7 +22,8 @@ exports[`should render correctly 1`] = ` | |||
className="text-muted" | |||
> | |||
<span> | |||
hotspot.category | |||
category | |||
: | |||
</span> | |||
<span | |||
className="little-spacer-left" | |||
@@ -35,7 +36,8 @@ exports[`should render correctly 1`] = ` | |||
className="huge-spacer-bottom" | |||
> | |||
<span> | |||
hotspot.status | |||
status | |||
: | |||
</span> | |||
<span | |||
className="badge little-spacer-left" | |||
@@ -45,7 +47,8 @@ exports[`should render correctly 1`] = ` | |||
<span | |||
className="huge-spacer-left" | |||
> | |||
hotspot.assigned_to | |||
assigned_to | |||
: | |||
</span> | |||
<strong | |||
className="little-spacer-left" | |||
@@ -68,6 +71,7 @@ exports[`should render correctly 1`] = ` | |||
"login": "john.doe", | |||
"name": "John Doe", | |||
}, | |||
"changelog": Array [], | |||
"component": Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", | |||
@@ -150,6 +154,7 @@ exports[`should render correctly 1`] = ` | |||
"login": "john.doe", | |||
"name": "John Doe", | |||
}, | |||
"changelog": Array [], | |||
"component": Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", | |||
@@ -243,7 +248,8 @@ exports[`should render correctly: anonymous user 1`] = ` | |||
className="text-muted" | |||
> | |||
<span> | |||
hotspot.category | |||
category | |||
: | |||
</span> | |||
<span | |||
className="little-spacer-left" | |||
@@ -256,7 +262,8 @@ exports[`should render correctly: anonymous user 1`] = ` | |||
className="huge-spacer-bottom" | |||
> | |||
<span> | |||
hotspot.status | |||
status | |||
: | |||
</span> | |||
<span | |||
className="badge little-spacer-left" | |||
@@ -266,7 +273,8 @@ exports[`should render correctly: anonymous user 1`] = ` | |||
<span | |||
className="huge-spacer-left" | |||
> | |||
hotspot.assigned_to | |||
assigned_to | |||
: | |||
</span> | |||
<strong | |||
className="little-spacer-left" | |||
@@ -289,6 +297,7 @@ exports[`should render correctly: anonymous user 1`] = ` | |||
"login": "john.doe", | |||
"name": "John Doe", | |||
}, | |||
"changelog": Array [], | |||
"component": Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", | |||
@@ -371,6 +380,222 @@ exports[`should render correctly: anonymous user 1`] = ` | |||
"login": "john.doe", | |||
"name": "John Doe", | |||
}, | |||
"changelog": Array [], | |||
"component": Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", | |||
"name": "MyProject", | |||
"organization": "foo", | |||
"qualifier": "FIL", | |||
"qualityGate": Object { | |||
"isDefault": true, | |||
"key": "30", | |||
"name": "Sonar way", | |||
}, | |||
"qualityProfiles": Array [ | |||
Object { | |||
"deleted": false, | |||
"key": "my-qp", | |||
"language": "ts", | |||
"name": "Sonar way", | |||
}, | |||
], | |||
"tags": Array [], | |||
}, | |||
"creationDate": "2013-05-13T17:55:41+0200", | |||
"key": "01fc972e-2a3c-433e-bcae-0bd7f88f5123", | |||
"line": 142, | |||
"message": "'3' is a magic number.", | |||
"project": Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", | |||
"name": "MyProject", | |||
"organization": "foo", | |||
"qualifier": "TRK", | |||
"qualityGate": Object { | |||
"isDefault": true, | |||
"key": "30", | |||
"name": "Sonar way", | |||
}, | |||
"qualityProfiles": Array [ | |||
Object { | |||
"deleted": false, | |||
"key": "my-qp", | |||
"language": "ts", | |||
"name": "Sonar way", | |||
}, | |||
], | |||
"tags": Array [], | |||
}, | |||
"resolution": "FIXED", | |||
"rule": Object { | |||
"fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>", | |||
"key": "squid:S2077", | |||
"name": "That rule", | |||
"riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>", | |||
"securityCategory": "sql-injection", | |||
"vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>", | |||
"vulnerabilityProbability": "HIGH", | |||
}, | |||
"status": "REVIEWED", | |||
"textRange": Object { | |||
"endLine": 142, | |||
"endOffset": 83, | |||
"startLine": 142, | |||
"startOffset": 26, | |||
}, | |||
"updateDate": "2013-05-13T17:55:42+0200", | |||
} | |||
} | |||
/> | |||
</div> | |||
</DeferredSpinner> | |||
`; | |||
exports[`should render correctly: assignee without name 1`] = ` | |||
<DeferredSpinner | |||
loading={false} | |||
timeout={100} | |||
> | |||
<div | |||
className="big-padded" | |||
> | |||
<div | |||
className="big-spacer-bottom" | |||
> | |||
<div | |||
className="display-flex-space-between" | |||
> | |||
<h1> | |||
'3' is a magic number. | |||
</h1> | |||
</div> | |||
<div | |||
className="text-muted" | |||
> | |||
<span> | |||
category | |||
: | |||
</span> | |||
<span | |||
className="little-spacer-left" | |||
> | |||
SQL injection | |||
</span> | |||
</div> | |||
</div> | |||
<div | |||
className="huge-spacer-bottom" | |||
> | |||
<span> | |||
status | |||
: | |||
</span> | |||
<span | |||
className="badge little-spacer-left" | |||
> | |||
hotspot.status.FIXED | |||
</span> | |||
</div> | |||
<HotspotSnippetContainer | |||
hotspot={ | |||
Object { | |||
"assignee": Object { | |||
"active": true, | |||
"local": true, | |||
"login": "assignee_login", | |||
"name": undefined, | |||
}, | |||
"author": Object { | |||
"active": true, | |||
"local": true, | |||
"login": "john.doe", | |||
"name": "John Doe", | |||
}, | |||
"changelog": Array [], | |||
"component": Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", | |||
"name": "MyProject", | |||
"organization": "foo", | |||
"qualifier": "FIL", | |||
"qualityGate": Object { | |||
"isDefault": true, | |||
"key": "30", | |||
"name": "Sonar way", | |||
}, | |||
"qualityProfiles": Array [ | |||
Object { | |||
"deleted": false, | |||
"key": "my-qp", | |||
"language": "ts", | |||
"name": "Sonar way", | |||
}, | |||
], | |||
"tags": Array [], | |||
}, | |||
"creationDate": "2013-05-13T17:55:41+0200", | |||
"key": "01fc972e-2a3c-433e-bcae-0bd7f88f5123", | |||
"line": 142, | |||
"message": "'3' is a magic number.", | |||
"project": Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", | |||
"name": "MyProject", | |||
"organization": "foo", | |||
"qualifier": "TRK", | |||
"qualityGate": Object { | |||
"isDefault": true, | |||
"key": "30", | |||
"name": "Sonar way", | |||
}, | |||
"qualityProfiles": Array [ | |||
Object { | |||
"deleted": false, | |||
"key": "my-qp", | |||
"language": "ts", | |||
"name": "Sonar way", | |||
}, | |||
], | |||
"tags": Array [], | |||
}, | |||
"resolution": "FIXED", | |||
"rule": Object { | |||
"fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>", | |||
"key": "squid:S2077", | |||
"name": "That rule", | |||
"riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>", | |||
"securityCategory": "sql-injection", | |||
"vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>", | |||
"vulnerabilityProbability": "HIGH", | |||
}, | |||
"status": "REVIEWED", | |||
"textRange": Object { | |||
"endLine": 142, | |||
"endOffset": 83, | |||
"startLine": 142, | |||
"startOffset": 26, | |||
}, | |||
"updateDate": "2013-05-13T17:55:42+0200", | |||
} | |||
} | |||
/> | |||
<HotspotViewerTabs | |||
hotspot={ | |||
Object { | |||
"assignee": Object { | |||
"active": true, | |||
"local": true, | |||
"login": "assignee_login", | |||
"name": undefined, | |||
}, | |||
"author": Object { | |||
"active": true, | |||
"local": true, | |||
"login": "john.doe", | |||
"name": "John Doe", | |||
}, | |||
"changelog": Array [], | |||
"component": Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", | |||
@@ -464,7 +689,8 @@ exports[`should render correctly: deleted assignee 1`] = ` | |||
className="text-muted" | |||
> | |||
<span> | |||
hotspot.category | |||
category | |||
: | |||
</span> | |||
<span | |||
className="little-spacer-left" | |||
@@ -477,7 +703,8 @@ exports[`should render correctly: deleted assignee 1`] = ` | |||
className="huge-spacer-bottom" | |||
> | |||
<span> | |||
hotspot.status | |||
status | |||
: | |||
</span> | |||
<span | |||
className="badge little-spacer-left" | |||
@@ -487,7 +714,8 @@ exports[`should render correctly: deleted assignee 1`] = ` | |||
<span | |||
className="huge-spacer-left" | |||
> | |||
hotspot.assigned_to | |||
assigned_to | |||
: | |||
</span> | |||
<strong | |||
className="little-spacer-left" | |||
@@ -510,6 +738,7 @@ exports[`should render correctly: deleted assignee 1`] = ` | |||
"login": "john.doe", | |||
"name": "John Doe", | |||
}, | |||
"changelog": Array [], | |||
"component": Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", | |||
@@ -592,6 +821,7 @@ exports[`should render correctly: deleted assignee 1`] = ` | |||
"login": "john.doe", | |||
"name": "John Doe", | |||
}, | |||
"changelog": Array [], | |||
"component": Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", | |||
@@ -670,6 +900,211 @@ exports[`should render correctly: no hotspot 1`] = ` | |||
/> | |||
`; | |||
exports[`should render correctly: unassigned 1`] = ` | |||
<DeferredSpinner | |||
loading={false} | |||
timeout={100} | |||
> | |||
<div | |||
className="big-padded" | |||
> | |||
<div | |||
className="big-spacer-bottom" | |||
> | |||
<div | |||
className="display-flex-space-between" | |||
> | |||
<h1> | |||
'3' is a magic number. | |||
</h1> | |||
</div> | |||
<div | |||
className="text-muted" | |||
> | |||
<span> | |||
category | |||
: | |||
</span> | |||
<span | |||
className="little-spacer-left" | |||
> | |||
SQL injection | |||
</span> | |||
</div> | |||
</div> | |||
<div | |||
className="huge-spacer-bottom" | |||
> | |||
<span> | |||
status | |||
: | |||
</span> | |||
<span | |||
className="badge little-spacer-left" | |||
> | |||
hotspot.status.FIXED | |||
</span> | |||
</div> | |||
<HotspotSnippetContainer | |||
hotspot={ | |||
Object { | |||
"assignee": undefined, | |||
"author": Object { | |||
"active": true, | |||
"local": true, | |||
"login": "john.doe", | |||
"name": "John Doe", | |||
}, | |||
"changelog": Array [], | |||
"component": Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", | |||
"name": "MyProject", | |||
"organization": "foo", | |||
"qualifier": "FIL", | |||
"qualityGate": Object { | |||
"isDefault": true, | |||
"key": "30", | |||
"name": "Sonar way", | |||
}, | |||
"qualityProfiles": Array [ | |||
Object { | |||
"deleted": false, | |||
"key": "my-qp", | |||
"language": "ts", | |||
"name": "Sonar way", | |||
}, | |||
], | |||
"tags": Array [], | |||
}, | |||
"creationDate": "2013-05-13T17:55:41+0200", | |||
"key": "01fc972e-2a3c-433e-bcae-0bd7f88f5123", | |||
"line": 142, | |||
"message": "'3' is a magic number.", | |||
"project": Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", | |||
"name": "MyProject", | |||
"organization": "foo", | |||
"qualifier": "TRK", | |||
"qualityGate": Object { | |||
"isDefault": true, | |||
"key": "30", | |||
"name": "Sonar way", | |||
}, | |||
"qualityProfiles": Array [ | |||
Object { | |||
"deleted": false, | |||
"key": "my-qp", | |||
"language": "ts", | |||
"name": "Sonar way", | |||
}, | |||
], | |||
"tags": Array [], | |||
}, | |||
"resolution": "FIXED", | |||
"rule": Object { | |||
"fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>", | |||
"key": "squid:S2077", | |||
"name": "That rule", | |||
"riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>", | |||
"securityCategory": "sql-injection", | |||
"vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>", | |||
"vulnerabilityProbability": "HIGH", | |||
}, | |||
"status": "REVIEWED", | |||
"textRange": Object { | |||
"endLine": 142, | |||
"endOffset": 83, | |||
"startLine": 142, | |||
"startOffset": 26, | |||
}, | |||
"updateDate": "2013-05-13T17:55:42+0200", | |||
} | |||
} | |||
/> | |||
<HotspotViewerTabs | |||
hotspot={ | |||
Object { | |||
"assignee": undefined, | |||
"author": Object { | |||
"active": true, | |||
"local": true, | |||
"login": "john.doe", | |||
"name": "John Doe", | |||
}, | |||
"changelog": Array [], | |||
"component": Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", | |||
"name": "MyProject", | |||
"organization": "foo", | |||
"qualifier": "FIL", | |||
"qualityGate": Object { | |||
"isDefault": true, | |||
"key": "30", | |||
"name": "Sonar way", | |||
}, | |||
"qualityProfiles": Array [ | |||
Object { | |||
"deleted": false, | |||
"key": "my-qp", | |||
"language": "ts", | |||
"name": "Sonar way", | |||
}, | |||
], | |||
"tags": Array [], | |||
}, | |||
"creationDate": "2013-05-13T17:55:41+0200", | |||
"key": "01fc972e-2a3c-433e-bcae-0bd7f88f5123", | |||
"line": 142, | |||
"message": "'3' is a magic number.", | |||
"project": Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", | |||
"name": "MyProject", | |||
"organization": "foo", | |||
"qualifier": "TRK", | |||
"qualityGate": Object { | |||
"isDefault": true, | |||
"key": "30", | |||
"name": "Sonar way", | |||
}, | |||
"qualityProfiles": Array [ | |||
Object { | |||
"deleted": false, | |||
"key": "my-qp", | |||
"language": "ts", | |||
"name": "Sonar way", | |||
}, | |||
], | |||
"tags": Array [], | |||
}, | |||
"resolution": "FIXED", | |||
"rule": Object { | |||
"fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>", | |||
"key": "squid:S2077", | |||
"name": "That rule", | |||
"riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>", | |||
"securityCategory": "sql-injection", | |||
"vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>", | |||
"vulnerabilityProbability": "HIGH", | |||
}, | |||
"status": "REVIEWED", | |||
"textRange": Object { | |||
"endLine": 142, | |||
"endOffset": 83, | |||
"startLine": 142, | |||
"startOffset": 26, | |||
}, | |||
"updateDate": "2013-05-13T17:55:42+0200", | |||
} | |||
} | |||
/> | |||
</div> | |||
</DeferredSpinner> | |||
`; | |||
exports[`should render correctly: user logged in 1`] = ` | |||
<DeferredSpinner | |||
loading={false} | |||
@@ -702,6 +1137,7 @@ exports[`should render correctly: user logged in 1`] = ` | |||
"login": "john.doe", | |||
"name": "John Doe", | |||
}, | |||
"changelog": Array [], | |||
"component": Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", | |||
@@ -775,7 +1211,8 @@ exports[`should render correctly: user logged in 1`] = ` | |||
className="text-muted" | |||
> | |||
<span> | |||
hotspot.category | |||
category | |||
: | |||
</span> | |||
<span | |||
className="little-spacer-left" | |||
@@ -788,7 +1225,8 @@ exports[`should render correctly: user logged in 1`] = ` | |||
className="huge-spacer-bottom" | |||
> | |||
<span> | |||
hotspot.status | |||
status | |||
: | |||
</span> | |||
<span | |||
className="badge little-spacer-left" | |||
@@ -798,7 +1236,8 @@ exports[`should render correctly: user logged in 1`] = ` | |||
<span | |||
className="huge-spacer-left" | |||
> | |||
hotspot.assigned_to | |||
assigned_to | |||
: | |||
</span> | |||
<strong | |||
className="little-spacer-left" | |||
@@ -821,6 +1260,7 @@ exports[`should render correctly: user logged in 1`] = ` | |||
"login": "john.doe", | |||
"name": "John Doe", | |||
}, | |||
"changelog": Array [], | |||
"component": Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", | |||
@@ -903,6 +1343,7 @@ exports[`should render correctly: user logged in 1`] = ` | |||
"login": "john.doe", | |||
"name": "John Doe", | |||
}, | |||
"changelog": Array [], | |||
"component": Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", |
@@ -0,0 +1,119 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly 1`] = ` | |||
<Fragment> | |||
<div> | |||
<div | |||
className="display-flex-center" | |||
> | |||
<Connect(Avatar) | |||
className="little-spacer-right" | |||
hash="with-avatar" | |||
name="John Doe" | |||
size={20} | |||
/> | |||
<strong> | |||
John Doe | |||
</strong> | |||
<span | |||
className="little-spacer-left" | |||
> | |||
hotspots.tabs.review_history.created | |||
</span> | |||
<span | |||
className="little-spacer-left little-spacer-right" | |||
> | |||
- | |||
</span> | |||
<DateTimeFormatter | |||
date="2019-09-13T17:55:42+0200" | |||
/> | |||
</div> | |||
</div> | |||
<hr /> | |||
<div> | |||
<div | |||
className="display-flex-center" | |||
> | |||
<Connect(Avatar) | |||
className="little-spacer-right" | |||
name="John Doe" | |||
size={20} | |||
/> | |||
<strong> | |||
user.x_deleted.John Doe | |||
</strong> | |||
<span | |||
className="little-spacer-left" | |||
> | |||
hotspots.tabs.review_history.created | |||
</span> | |||
<span | |||
className="little-spacer-left little-spacer-right" | |||
> | |||
- | |||
</span> | |||
<DateTimeFormatter | |||
date="2019-09-13T17:55:42+0200" | |||
/> | |||
</div> | |||
</div> | |||
<hr /> | |||
<div> | |||
<div | |||
className="display-flex-center" | |||
> | |||
<DateTimeFormatter | |||
date="2019-09-13T17:55:42+0200" | |||
/> | |||
</div> | |||
</div> | |||
<hr /> | |||
<div> | |||
<div | |||
className="display-flex-center" | |||
> | |||
<Connect(Avatar) | |||
className="little-spacer-right" | |||
name="John Doe" | |||
size={20} | |||
/> | |||
<strong> | |||
John Doe | |||
</strong> | |||
<span | |||
className="little-spacer-left little-spacer-right" | |||
> | |||
- | |||
</span> | |||
<DateTimeFormatter | |||
date="2019-09-13T17:55:42+0200" | |||
/> | |||
</div> | |||
<div | |||
className="spacer-top" | |||
> | |||
<IssueChangelogDiff | |||
diff={ | |||
Object { | |||
"key": "test", | |||
"newValue": "new", | |||
"oldValue": "old", | |||
} | |||
} | |||
key="test-old-new" | |||
/> | |||
<IssueChangelogDiff | |||
diff={ | |||
Object { | |||
"key": "test-1", | |||
"newValue": "new-1", | |||
"oldValue": "old-1", | |||
} | |||
} | |||
key="test-1-old-1-new-1" | |||
/> | |||
</div> | |||
</div> | |||
</Fragment> | |||
`; |
@@ -4,28 +4,62 @@ exports[`should render correctly: empty tab 1`] = ` | |||
<Fragment> | |||
<BoxedTabs | |||
onSelect={[Function]} | |||
selected="vulnerability" | |||
selected="risk" | |||
tabs={ | |||
Array [ | |||
Object { | |||
"content": "<p>This a <strong>strong</strong> message about vulnerability !</p>", | |||
"key": "vulnerability", | |||
"label": "hotspot.tabs.vulnerability_description", | |||
"label": "hotspots.tabs.vulnerability_description", | |||
}, | |||
Object { | |||
"content": "<p>This a <strong>strong</strong> message about fixing !</p>", | |||
"key": "fix", | |||
"label": "hotspot.tabs.fix_recommendations", | |||
"label": "hotspots.tabs.fix_recommendations", | |||
}, | |||
Object { | |||
"content": <HotspotViewerReviewHistoryTab | |||
history={ | |||
Array [ | |||
Object { | |||
"date": "2013-05-13T17:55:41+0200", | |||
"type": 0, | |||
"user": Object { | |||
"active": true, | |||
"avatar": undefined, | |||
"name": "John Doe", | |||
}, | |||
}, | |||
] | |||
} | |||
/>, | |||
"key": "review", | |||
"label": <React.Fragment> | |||
<span> | |||
hotspots.tabs.review_history | |||
</span> | |||
<span | |||
className="counter-badge spacer-left" | |||
> | |||
1 | |||
</span> | |||
</React.Fragment>, | |||
}, | |||
] | |||
} | |||
/> | |||
<div | |||
className="boxed-group markdown big-padded" | |||
dangerouslySetInnerHTML={ | |||
Object { | |||
"__html": "<p>This a <strong>strong</strong> message about vulnerability !</p>", | |||
className="boxed-group big-padded" | |||
> | |||
<div | |||
className="markdown" | |||
dangerouslySetInnerHTML={ | |||
Object { | |||
"__html": "<p>This a <strong>strong</strong> message about vulnerability !</p>", | |||
} | |||
} | |||
} | |||
/> | |||
/> | |||
</div> | |||
</Fragment> | |||
`; | |||
@@ -37,63 +71,208 @@ exports[`should render correctly: fix 1`] = ` | |||
tabs={ | |||
Array [ | |||
Object { | |||
"content": "<p>This a <strong>strong</strong> message about risk !</p>", | |||
"key": "risk", | |||
"label": "hotspot.tabs.risk_description", | |||
"label": "hotspots.tabs.risk_description", | |||
}, | |||
Object { | |||
"content": "<p>This a <strong>strong</strong> message about vulnerability !</p>", | |||
"key": "vulnerability", | |||
"label": "hotspot.tabs.vulnerability_description", | |||
"label": "hotspots.tabs.vulnerability_description", | |||
}, | |||
Object { | |||
"content": "<p>This a <strong>strong</strong> message about fixing !</p>", | |||
"key": "fix", | |||
"label": "hotspot.tabs.fix_recommendations", | |||
"label": "hotspots.tabs.fix_recommendations", | |||
}, | |||
Object { | |||
"content": <HotspotViewerReviewHistoryTab | |||
history={ | |||
Array [ | |||
Object { | |||
"date": "2013-05-13T17:55:41+0200", | |||
"type": 0, | |||
"user": Object { | |||
"active": true, | |||
"avatar": undefined, | |||
"name": "John Doe", | |||
}, | |||
}, | |||
] | |||
} | |||
/>, | |||
"key": "review", | |||
"label": <React.Fragment> | |||
<span> | |||
hotspots.tabs.review_history | |||
</span> | |||
<span | |||
className="counter-badge spacer-left" | |||
> | |||
1 | |||
</span> | |||
</React.Fragment>, | |||
}, | |||
] | |||
} | |||
/> | |||
<div | |||
className="boxed-group markdown big-padded" | |||
dangerouslySetInnerHTML={ | |||
Object { | |||
"__html": "<p>This a <strong>strong</strong> message about fixing !</p>", | |||
className="boxed-group big-padded" | |||
> | |||
<div | |||
className="markdown" | |||
dangerouslySetInnerHTML={ | |||
Object { | |||
"__html": "<p>This a <strong>strong</strong> message about fixing !</p>", | |||
} | |||
} | |||
} | |||
/> | |||
/> | |||
</div> | |||
</Fragment> | |||
`; | |||
exports[`should render correctly: no tabs 1`] = `""`; | |||
exports[`should render correctly: risk 1`] = ` | |||
exports[`should render correctly: review 1`] = ` | |||
<Fragment> | |||
<BoxedTabs | |||
onSelect={[Function]} | |||
selected="risk" | |||
selected="review" | |||
tabs={ | |||
Array [ | |||
Object { | |||
"content": "<p>This a <strong>strong</strong> message about risk !</p>", | |||
"key": "risk", | |||
"label": "hotspot.tabs.risk_description", | |||
"label": "hotspots.tabs.risk_description", | |||
}, | |||
Object { | |||
"content": "<p>This a <strong>strong</strong> message about vulnerability !</p>", | |||
"key": "vulnerability", | |||
"label": "hotspot.tabs.vulnerability_description", | |||
"label": "hotspots.tabs.vulnerability_description", | |||
}, | |||
Object { | |||
"content": "<p>This a <strong>strong</strong> message about fixing !</p>", | |||
"key": "fix", | |||
"label": "hotspot.tabs.fix_recommendations", | |||
"label": "hotspots.tabs.fix_recommendations", | |||
}, | |||
Object { | |||
"content": <HotspotViewerReviewHistoryTab | |||
history={ | |||
Array [ | |||
Object { | |||
"date": "2013-05-13T17:55:41+0200", | |||
"type": 0, | |||
"user": Object { | |||
"active": true, | |||
"avatar": undefined, | |||
"name": "John Doe", | |||
}, | |||
}, | |||
] | |||
} | |||
/>, | |||
"key": "review", | |||
"label": <React.Fragment> | |||
<span> | |||
hotspots.tabs.review_history | |||
</span> | |||
<span | |||
className="counter-badge spacer-left" | |||
> | |||
1 | |||
</span> | |||
</React.Fragment>, | |||
}, | |||
] | |||
} | |||
/> | |||
<div | |||
className="boxed-group markdown big-padded" | |||
dangerouslySetInnerHTML={ | |||
Object { | |||
"__html": "<p>This a <strong>strong</strong> message about risk !</p>", | |||
className="boxed-group big-padded" | |||
> | |||
<HotspotViewerReviewHistoryTab | |||
history={ | |||
Array [ | |||
Object { | |||
"date": "2013-05-13T17:55:41+0200", | |||
"type": 0, | |||
"user": Object { | |||
"active": true, | |||
"avatar": undefined, | |||
"name": "John Doe", | |||
}, | |||
}, | |||
] | |||
} | |||
/> | |||
</div> | |||
</Fragment> | |||
`; | |||
exports[`should render correctly: risk 1`] = ` | |||
<Fragment> | |||
<BoxedTabs | |||
onSelect={[Function]} | |||
selected="risk" | |||
tabs={ | |||
Array [ | |||
Object { | |||
"content": "<p>This a <strong>strong</strong> message about risk !</p>", | |||
"key": "risk", | |||
"label": "hotspots.tabs.risk_description", | |||
}, | |||
Object { | |||
"content": "<p>This a <strong>strong</strong> message about vulnerability !</p>", | |||
"key": "vulnerability", | |||
"label": "hotspots.tabs.vulnerability_description", | |||
}, | |||
Object { | |||
"content": "<p>This a <strong>strong</strong> message about fixing !</p>", | |||
"key": "fix", | |||
"label": "hotspots.tabs.fix_recommendations", | |||
}, | |||
Object { | |||
"content": <HotspotViewerReviewHistoryTab | |||
history={ | |||
Array [ | |||
Object { | |||
"date": "2013-05-13T17:55:41+0200", | |||
"type": 0, | |||
"user": Object { | |||
"active": true, | |||
"avatar": undefined, | |||
"name": "John Doe", | |||
}, | |||
}, | |||
] | |||
} | |||
/>, | |||
"key": "review", | |||
"label": <React.Fragment> | |||
<span> | |||
hotspots.tabs.review_history | |||
</span> | |||
<span | |||
className="counter-badge spacer-left" | |||
> | |||
1 | |||
</span> | |||
</React.Fragment>, | |||
}, | |||
] | |||
} | |||
/> | |||
<div | |||
className="boxed-group big-padded" | |||
> | |||
<div | |||
className="markdown" | |||
dangerouslySetInnerHTML={ | |||
Object { | |||
"__html": "<p>This a <strong>strong</strong> message about risk !</p>", | |||
} | |||
} | |||
/> | |||
</div> | |||
</Fragment> | |||
`; | |||
@@ -105,27 +284,62 @@ exports[`should render correctly: vulnerability 1`] = ` | |||
tabs={ | |||
Array [ | |||
Object { | |||
"content": "<p>This a <strong>strong</strong> message about risk !</p>", | |||
"key": "risk", | |||
"label": "hotspot.tabs.risk_description", | |||
"label": "hotspots.tabs.risk_description", | |||
}, | |||
Object { | |||
"content": "<p>This a <strong>strong</strong> message about vulnerability !</p>", | |||
"key": "vulnerability", | |||
"label": "hotspot.tabs.vulnerability_description", | |||
"label": "hotspots.tabs.vulnerability_description", | |||
}, | |||
Object { | |||
"content": "<p>This a <strong>strong</strong> message about fixing !</p>", | |||
"key": "fix", | |||
"label": "hotspot.tabs.fix_recommendations", | |||
"label": "hotspots.tabs.fix_recommendations", | |||
}, | |||
Object { | |||
"content": <HotspotViewerReviewHistoryTab | |||
history={ | |||
Array [ | |||
Object { | |||
"date": "2013-05-13T17:55:41+0200", | |||
"type": 0, | |||
"user": Object { | |||
"active": true, | |||
"avatar": undefined, | |||
"name": "John Doe", | |||
}, | |||
}, | |||
] | |||
} | |||
/>, | |||
"key": "review", | |||
"label": <React.Fragment> | |||
<span> | |||
hotspots.tabs.review_history | |||
</span> | |||
<span | |||
className="counter-badge spacer-left" | |||
> | |||
1 | |||
</span> | |||
</React.Fragment>, | |||
}, | |||
] | |||
} | |||
/> | |||
<div | |||
className="boxed-group markdown big-padded" | |||
dangerouslySetInnerHTML={ | |||
Object { | |||
"__html": "<p>This a <strong>strong</strong> message about vulnerability !</p>", | |||
className="boxed-group big-padded" | |||
> | |||
<div | |||
className="markdown" | |||
dangerouslySetInnerHTML={ | |||
Object { | |||
"__html": "<p>This a <strong>strong</strong> message about vulnerability !</p>", | |||
} | |||
} | |||
} | |||
/> | |||
/> | |||
</div> | |||
</Fragment> | |||
`; |
@@ -18,7 +18,13 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { groupBy, sortBy } from 'lodash'; | |||
import { DetailedHotspot, RawHotspot, RiskExposure } from '../../types/security-hotspots'; | |||
import { | |||
Hotspot, | |||
RawHotspot, | |||
ReviewHistoryElement, | |||
ReviewHistoryType, | |||
RiskExposure | |||
} from '../../types/security-hotspots'; | |||
export const RISK_EXPOSURE_LEVELS = [RiskExposure.HIGH, RiskExposure.MEDIUM, RiskExposure.LOW]; | |||
@@ -61,7 +67,7 @@ function getCategoryTitle(key: string, securityCategories: T.StandardSecurityCat | |||
} | |||
export function constructSourceViewerFile( | |||
{ component, project }: DetailedHotspot, | |||
{ component, project }: Hotspot, | |||
lines?: number | |||
): T.SourceViewerFile { | |||
return { | |||
@@ -74,3 +80,36 @@ export function constructSourceViewerFile( | |||
uuid: '' | |||
}; | |||
} | |||
export function getHotspotReviewHistory(hotspot: Hotspot): ReviewHistoryElement[] { | |||
const history: ReviewHistoryElement[] = []; | |||
if (hotspot.creationDate) { | |||
history.push({ | |||
type: ReviewHistoryType.Creation, | |||
date: hotspot.creationDate, | |||
user: { | |||
avatar: hotspot.author.avatar, | |||
name: hotspot.author.name || hotspot.author.login, | |||
active: hotspot.author.active | |||
} | |||
}); | |||
} | |||
if (hotspot.changelog) { | |||
history.push( | |||
...hotspot.changelog.map(log => ({ | |||
type: ReviewHistoryType.Diff, | |||
date: log.creationDate, | |||
user: { | |||
avatar: log.avatar, | |||
name: log.userName || log.user, | |||
active: log.isUserActive | |||
}, | |||
diffs: log.diffs | |||
})) | |||
); | |||
} | |||
return sortBy(history, elt => elt.date); | |||
} |
@@ -19,11 +19,13 @@ | |||
*/ | |||
import { ComponentQualifier } from '../../types/component'; | |||
import { | |||
DetailedHotspot, | |||
DetailedHotspotRule, | |||
Hotspot, | |||
HotspotResolution, | |||
HotspotRule, | |||
HotspotStatus, | |||
RawHotspot, | |||
ReviewHistoryElement, | |||
ReviewHistoryType, | |||
RiskExposure | |||
} from '../../types/security-hotspots'; | |||
import { mockComponent, mockUser } from '../testMocks'; | |||
@@ -47,10 +49,11 @@ export function mockRawHotspot(overrides: Partial<RawHotspot> = {}): RawHotspot | |||
}; | |||
} | |||
export function mockDetailledHotspot(overrides?: Partial<DetailedHotspot>): DetailedHotspot { | |||
export function mockHotspot(overrides?: Partial<Hotspot>): Hotspot { | |||
return { | |||
assignee: mockUser(), | |||
author: mockUser(), | |||
changelog: [], | |||
component: mockComponent({ qualifier: ComponentQualifier.File }), | |||
creationDate: '2013-05-13T17:55:41+0200', | |||
key: '01fc972e-2a3c-433e-bcae-0bd7f88f5123', | |||
@@ -58,7 +61,7 @@ export function mockDetailledHotspot(overrides?: Partial<DetailedHotspot>): Deta | |||
message: "'3' is a magic number.", | |||
project: mockComponent({ qualifier: ComponentQualifier.Project }), | |||
resolution: HotspotResolution.FIXED, | |||
rule: mockDetailledHotspotRule(), | |||
rule: mockHotspotRule(), | |||
status: HotspotStatus.REVIEWED, | |||
textRange: { | |||
startLine: 142, | |||
@@ -71,9 +74,7 @@ export function mockDetailledHotspot(overrides?: Partial<DetailedHotspot>): Deta | |||
}; | |||
} | |||
export function mockDetailledHotspotRule( | |||
overrides?: Partial<DetailedHotspotRule> | |||
): DetailedHotspotRule { | |||
export function mockHotspotRule(overrides?: Partial<HotspotRule>): HotspotRule { | |||
return { | |||
key: 'squid:S2077', | |||
name: 'That rule', | |||
@@ -85,3 +86,14 @@ export function mockDetailledHotspotRule( | |||
...overrides | |||
}; | |||
} | |||
export function mockHotspotReviewHistoryElement( | |||
overrides?: Partial<ReviewHistoryElement> | |||
): ReviewHistoryElement { | |||
return { | |||
date: '2019-09-13T17:55:42+0200', | |||
type: ReviewHistoryType.Creation, | |||
user: mockUser(), | |||
...overrides | |||
}; | |||
} |
@@ -68,9 +68,10 @@ export interface RawHotspot { | |||
vulnerabilityProbability: RiskExposure; | |||
} | |||
export interface DetailedHotspot { | |||
export interface Hotspot { | |||
assignee?: Pick<T.UserBase, 'active' | 'login' | 'name'>; | |||
author?: Pick<T.UserBase, 'login'>; | |||
author: Pick<T.UserBase, 'active' | 'avatar' | 'login' | 'name'>; | |||
changelog?: T.IssueChangelog[]; | |||
component: T.Component; | |||
creationDate: string; | |||
key: string; | |||
@@ -78,7 +79,7 @@ export interface DetailedHotspot { | |||
message: string; | |||
project: T.Component; | |||
resolution?: string; | |||
rule: DetailedHotspotRule; | |||
rule: HotspotRule; | |||
status: string; | |||
textRange: T.TextRange; | |||
updateDate: string; | |||
@@ -93,7 +94,7 @@ export interface HotspotUpdate extends HotspotUpdateFields { | |||
key: string; | |||
} | |||
export interface DetailedHotspotRule { | |||
export interface HotspotRule { | |||
fixRecommendations?: string; | |||
key: string; | |||
name: string; | |||
@@ -103,6 +104,18 @@ export interface DetailedHotspotRule { | |||
vulnerabilityProbability: RiskExposure; | |||
} | |||
export interface ReviewHistoryElement { | |||
type: ReviewHistoryType; | |||
date: string; | |||
user: Pick<T.UserBase, 'active' | 'avatar' | 'name'>; | |||
diffs?: T.IssueChangelogDiff[]; | |||
} | |||
export enum ReviewHistoryType { | |||
Creation, | |||
Diff | |||
} | |||
export interface HotspotSearchResponse { | |||
components?: { key: string; qualifier: string; name: string }[]; | |||
hotspots: RawHotspot[]; |
@@ -656,9 +656,12 @@ hotspots.risk_exposure=Review priority: | |||
hotspot.category=Category: | |||
hotspot.status=Status: | |||
hotspot.assigned_to=Assigned to: | |||
hotspot.tabs.risk_description=What's the risk? | |||
hotspot.tabs.vulnerability_description=Are you vulnerable? | |||
hotspot.tabs.fix_recommendations=How can you fix it? | |||
hotspots.tabs.risk_description=What's the risk? | |||
hotspots.tabs.vulnerability_description=Are you vulnerable? | |||
hotspots.tabs.fix_recommendations=How can you fix it? | |||
hotspots.tabs.review_history=Review history | |||
hotspots.tabs.review_history.created=created Security Hotspot | |||
hotspot.change_status.REVIEWED=Change status | |||
hotspot.change_status.TO_REVIEW=Review Hotspot | |||
@@ -672,6 +675,7 @@ hotspot.filters.assignee.all=All | |||
hotspot.filters.status.to_review=To review | |||
hotspot.filters.status.fixed=Reviewed as fixed | |||
hotspot.filters.status.safe=Reviewed as safe | |||
hotspots.review_hotspot=Review Hotspot | |||
hotspots.form.title=Mark Security Hotspot as: | |||