@@ -77,7 +77,7 @@ a[class*=' icon-'] { | |||
transition: opacity 0.3s ease; | |||
} | |||
a:hover > .icon-radio { | |||
a:not(.disabled):hover > .icon-radio { | |||
border-color: var(--blue); | |||
} | |||
@@ -384,6 +384,11 @@ th.huge-spacer-right { | |||
justify-content: center; | |||
} | |||
.display-flex-justify-end { | |||
display: flex !important; | |||
justify-content: flex-end; | |||
} | |||
.display-flex-space-around { | |||
display: flex !important; | |||
justify-content: space-around; |
@@ -19,8 +19,21 @@ | |||
*/ | |||
import { mockHotspot, mockRawHotspot } from '../../../helpers/mocks/security-hotspots'; | |||
import { mockUser } from '../../../helpers/testMocks'; | |||
import { ReviewHistoryType, RiskExposure } from '../../../types/security-hotspots'; | |||
import { getHotspotReviewHistory, groupByCategory, mapRules, sortHotspots } from '../utils'; | |||
import { | |||
HotspotResolution, | |||
HotspotStatus, | |||
HotspotStatusOption, | |||
ReviewHistoryType, | |||
RiskExposure | |||
} from '../../../types/security-hotspots'; | |||
import { | |||
getHotspotReviewHistory, | |||
getStatusAndResolutionFromStatusOption, | |||
getStatusOptionFromStatusAndResolution, | |||
groupByCategory, | |||
mapRules, | |||
sortHotspots | |||
} from '../utils'; | |||
const hotspots = [ | |||
mockRawHotspot({ | |||
@@ -223,3 +236,37 @@ describe('getHotspotReviewHistory', () => { | |||
); | |||
}); | |||
}); | |||
describe('getStatusOptionFromStatusAndResolution', () => { | |||
it('should return the correct values', () => { | |||
expect( | |||
getStatusOptionFromStatusAndResolution(HotspotStatus.REVIEWED, HotspotResolution.FIXED) | |||
).toBe(HotspotStatusOption.FIXED); | |||
expect( | |||
getStatusOptionFromStatusAndResolution(HotspotStatus.REVIEWED, HotspotResolution.SAFE) | |||
).toBe(HotspotStatusOption.SAFE); | |||
expect(getStatusOptionFromStatusAndResolution(HotspotStatus.REVIEWED)).toBe( | |||
HotspotStatusOption.FIXED | |||
); | |||
expect(getStatusOptionFromStatusAndResolution(HotspotStatus.TO_REVIEW)).toBe( | |||
HotspotStatusOption.TO_REVIEW | |||
); | |||
}); | |||
}); | |||
describe('getStatusAndResolutionFromStatusOption', () => { | |||
it('should return the correct values', () => { | |||
expect(getStatusAndResolutionFromStatusOption(HotspotStatusOption.TO_REVIEW)).toEqual({ | |||
status: HotspotStatus.TO_REVIEW, | |||
resolution: undefined | |||
}); | |||
expect(getStatusAndResolutionFromStatusOption(HotspotStatusOption.FIXED)).toEqual({ | |||
status: HotspotStatus.REVIEWED, | |||
resolution: HotspotResolution.FIXED | |||
}); | |||
expect(getStatusAndResolutionFromStatusOption(HotspotStatusOption.SAFE)).toEqual({ | |||
status: HotspotStatus.REVIEWED, | |||
resolution: HotspotResolution.SAFE | |||
}); | |||
}); | |||
}); |
@@ -1,77 +0,0 @@ | |||
/* | |||
* 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 { Button } from 'sonar-ui-common/components/controls/buttons'; | |||
import { DropdownOverlay } from 'sonar-ui-common/components/controls/Dropdown'; | |||
import OutsideClickHandler from 'sonar-ui-common/components/controls/OutsideClickHandler'; | |||
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 { Hotspot } from '../../../types/security-hotspots'; | |||
import HotspotActionsForm from './HotspotActionsForm'; | |||
export interface HotspotActionsProps { | |||
hotspot: Hotspot; | |||
onSubmit: () => void; | |||
} | |||
const ESCAPE_KEY = 'Escape'; | |||
export default function HotspotActions(props: HotspotActionsProps) { | |||
const { hotspot } = props; | |||
const [open, setOpen] = React.useState(false); | |||
React.useEffect(() => { | |||
const handleKeyDown = (event: KeyboardEvent) => { | |||
if (event.key === ESCAPE_KEY) { | |||
setOpen(false); | |||
} | |||
}; | |||
document.addEventListener('keydown', handleKeyDown, false); | |||
return () => { | |||
document.removeEventListener('keydown', handleKeyDown, false); | |||
}; | |||
}); | |||
return ( | |||
<div className="dropdown big-spacer-left flex-0"> | |||
<Button onClick={() => setOpen(!open)}> | |||
{translate('hotspot.change_status', hotspot.status)} | |||
<DropdownIcon className="little-spacer-left" /> | |||
</Button> | |||
{open && ( | |||
<OutsideClickHandler onClickOutside={() => setOpen(false)}> | |||
<DropdownOverlay placement={PopupPlacement.BottomRight}> | |||
<HotspotActionsForm | |||
hotspot={hotspot} | |||
onSubmit={() => { | |||
setOpen(false); | |||
props.onSubmit(); | |||
}} | |||
/> | |||
</DropdownOverlay> | |||
</OutsideClickHandler> | |||
)} | |||
</div> | |||
); | |||
} |
@@ -1,163 +0,0 @@ | |||
/* | |||
* 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 { | |||
assignSecurityHotspot, | |||
commentSecurityHotspot, | |||
setSecurityHotspotStatus | |||
} from '../../../api/security-hotspots'; | |||
import { | |||
Hotspot, | |||
HotspotResolution, | |||
HotspotStatus, | |||
HotspotStatusOption | |||
} from '../../../types/security-hotspots'; | |||
import HotspotActionsFormRenderer from './HotspotActionsFormRenderer'; | |||
interface Props { | |||
hotspot: Hotspot; | |||
onSubmit: () => void; | |||
} | |||
interface State { | |||
comment: string; | |||
selectedOption: HotspotStatusOption; | |||
selectedUser?: T.UserActive; | |||
submitting: boolean; | |||
} | |||
export default class HotspotActionsForm extends React.Component<Props, State> { | |||
constructor(props: Props) { | |||
super(props); | |||
let selectedOption = HotspotStatusOption.FIXED; | |||
if (props.hotspot.status === HotspotStatus.TO_REVIEW) { | |||
selectedOption = HotspotStatusOption.ADDITIONAL_REVIEW; | |||
} else if (props.hotspot.resolution) { | |||
selectedOption = HotspotStatusOption[props.hotspot.resolution]; | |||
} | |||
this.state = { | |||
comment: '', | |||
selectedOption, | |||
submitting: false | |||
}; | |||
} | |||
handleSelectOption = (selectedOption: HotspotStatusOption) => { | |||
this.setState({ selectedOption }); | |||
}; | |||
handleAssign = (selectedUser: T.UserActive) => { | |||
this.setState({ selectedUser }); | |||
}; | |||
handleCommentChange = (comment: string) => { | |||
this.setState({ comment }); | |||
}; | |||
handleSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => { | |||
event.preventDefault(); | |||
const { hotspot } = this.props; | |||
const { comment, selectedOption, selectedUser } = this.state; | |||
const status = | |||
selectedOption === HotspotStatusOption.ADDITIONAL_REVIEW | |||
? HotspotStatus.TO_REVIEW | |||
: HotspotStatus.REVIEWED; | |||
const resolution = | |||
selectedOption !== HotspotStatusOption.ADDITIONAL_REVIEW | |||
? HotspotResolution[selectedOption] | |||
: undefined; | |||
this.setState({ submitting: true }); | |||
/* | |||
* updateAssignee depends on updateStatus, hence these are chained rather than | |||
* run in parallel. The comment should also appear last in the changelog. | |||
*/ | |||
return Promise.resolve() | |||
.then(() => this.updateStatus(hotspot, status, resolution)) | |||
.then(() => this.updateAssignee(hotspot, selectedOption, selectedUser)) | |||
.then(() => this.addComment(hotspot, comment)) | |||
.then(() => { | |||
this.props.onSubmit(); | |||
// No need to set "submitting", we are closing the window | |||
}) | |||
.catch(() => { | |||
this.setState({ submitting: false }); | |||
}); | |||
}; | |||
updateStatus = (hotspot: Hotspot, status: HotspotStatus, resolution?: HotspotResolution) => { | |||
if ( | |||
hotspot.canChangeStatus && | |||
(status !== hotspot.status || resolution !== hotspot.resolution) | |||
) { | |||
return setSecurityHotspotStatus(hotspot.key, { status, resolution }); | |||
} | |||
return Promise.resolve(); | |||
}; | |||
updateAssignee = ( | |||
hotspot: Hotspot, | |||
selectedOption: HotspotStatusOption, | |||
selectedUser?: T.UserActive | |||
) => { | |||
if ( | |||
selectedOption === HotspotStatusOption.ADDITIONAL_REVIEW && | |||
selectedUser && | |||
selectedUser.login !== hotspot.assignee | |||
) { | |||
return assignSecurityHotspot(hotspot.key, { | |||
assignee: selectedUser.login | |||
}); | |||
} | |||
return Promise.resolve(); | |||
}; | |||
addComment = (hotspot: Hotspot, comment: string) => { | |||
if (comment.length > 0) { | |||
return commentSecurityHotspot(hotspot.key, comment); | |||
} | |||
return Promise.resolve(); | |||
}; | |||
render() { | |||
const { hotspot } = this.props; | |||
const { comment, selectedOption, selectedUser, submitting } = this.state; | |||
return ( | |||
<HotspotActionsFormRenderer | |||
comment={comment} | |||
hotspot={hotspot} | |||
onAssign={this.handleAssign} | |||
onChangeComment={this.handleCommentChange} | |||
onSelectOption={this.handleSelectOption} | |||
onSubmit={this.handleSubmit} | |||
selectedOption={selectedOption} | |||
selectedUser={selectedUser} | |||
submitting={submitting} | |||
/> | |||
); | |||
} | |||
} |
@@ -1,166 +0,0 @@ | |||
/* | |||
* 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 classnames from 'classnames'; | |||
import * as React from 'react'; | |||
import { SubmitButton } from 'sonar-ui-common/components/controls/buttons'; | |||
import Radio from 'sonar-ui-common/components/controls/Radio'; | |||
import Tooltip from 'sonar-ui-common/components/controls/Tooltip'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import MarkdownTips from '../../../components/common/MarkdownTips'; | |||
import { | |||
Hotspot, | |||
HotspotResolution, | |||
HotspotStatus, | |||
HotspotStatusOption | |||
} from '../../../types/security-hotspots'; | |||
export interface HotspotActionsFormRendererProps { | |||
comment: string; | |||
hotspot: Hotspot; | |||
onAssign: (user: T.UserActive) => void; | |||
onChangeComment: (comment: string) => void; | |||
onSelectOption: (option: HotspotStatusOption) => void; | |||
onSubmit: (event: React.SyntheticEvent<HTMLFormElement>) => void; | |||
selectedOption: HotspotStatusOption; | |||
selectedUser?: T.UserActive; | |||
submitting: boolean; | |||
} | |||
export default function HotspotActionsFormRenderer(props: HotspotActionsFormRendererProps) { | |||
const { comment, hotspot, selectedOption, submitting } = props; | |||
const disableStatusChange = !hotspot.canChangeStatus; | |||
return ( | |||
<form className="abs-width-400 padded" onSubmit={props.onSubmit}> | |||
<h2> | |||
{disableStatusChange | |||
? translate('hotspots.form.title.disabled') | |||
: translate('hotspots.form.title')} | |||
</h2> | |||
<div className="display-flex-column big-spacer-bottom"> | |||
{renderOption({ | |||
disabled: disableStatusChange, | |||
option: HotspotStatusOption.FIXED, | |||
selectedOption, | |||
onClick: props.onSelectOption | |||
})} | |||
{renderOption({ | |||
disabled: disableStatusChange, | |||
option: HotspotStatusOption.SAFE, | |||
selectedOption, | |||
onClick: props.onSelectOption | |||
})} | |||
{renderOption({ | |||
disabled: disableStatusChange, | |||
option: HotspotStatusOption.ADDITIONAL_REVIEW, | |||
selectedOption, | |||
onClick: props.onSelectOption | |||
})} | |||
</div> | |||
<div className="display-flex-column big-spacer-bottom"> | |||
<label className="little-spacer-bottom">{translate('hotspots.form.comment')}</label> | |||
<textarea | |||
autoFocus={true} | |||
className="form-field fixed-width spacer-bottom" | |||
onChange={(event: React.ChangeEvent<HTMLTextAreaElement>) => | |||
props.onChangeComment(event.currentTarget.value) | |||
} | |||
placeholder={ | |||
selectedOption === HotspotStatusOption.SAFE | |||
? translate('hotspots.form.comment.placeholder') | |||
: '' | |||
} | |||
rows={6} | |||
value={comment} | |||
/> | |||
<MarkdownTips /> | |||
</div> | |||
<div className="text-right"> | |||
{submitting && <i className="spinner spacer-right" />} | |||
<SubmitButton disabled={submitting || !changes(props)}> | |||
{translate('hotspots.form.submit', hotspot.status)} | |||
</SubmitButton> | |||
</div> | |||
</form> | |||
); | |||
} | |||
const noop = () => {}; | |||
function changes(params: { | |||
comment: string; | |||
hotspot: Hotspot; | |||
selectedOption: HotspotStatusOption; | |||
selectedUser?: T.UserActive; | |||
}) { | |||
const { comment, hotspot, selectedOption, selectedUser } = params; | |||
const status = | |||
selectedOption === HotspotStatusOption.ADDITIONAL_REVIEW | |||
? HotspotStatus.TO_REVIEW | |||
: HotspotStatus.REVIEWED; | |||
const resolution = | |||
selectedOption !== HotspotStatusOption.ADDITIONAL_REVIEW | |||
? HotspotResolution[selectedOption] | |||
: undefined; | |||
return ( | |||
comment.length > 0 || | |||
selectedUser || | |||
status !== hotspot.status || | |||
resolution !== hotspot.resolution | |||
); | |||
} | |||
function renderOption(params: { | |||
disabled: boolean; | |||
option: HotspotStatusOption; | |||
onClick: (option: HotspotStatusOption) => void; | |||
selectedOption: HotspotStatusOption; | |||
}) { | |||
const { disabled, onClick, option, selectedOption } = params; | |||
const optionRender = ( | |||
<div className="big-spacer-top"> | |||
<Radio | |||
checked={selectedOption === option} | |||
className={classnames({ disabled })} | |||
onCheck={disabled ? noop : onClick} | |||
value={option}> | |||
<h3 className={classnames({ 'text-muted': disabled })}> | |||
{translate('hotspots.status_option', option)} | |||
</h3> | |||
</Radio> | |||
<div className={classnames('radio-button-description', { 'text-muted': disabled })}> | |||
{translate('hotspots.status_option', option, 'description')} | |||
</div> | |||
</div> | |||
); | |||
return disabled ? ( | |||
<Tooltip overlay={translate('hotspots.form.cannot_change_status')} placement="left"> | |||
{optionRender} | |||
</Tooltip> | |||
) : ( | |||
optionRender | |||
); | |||
} |
@@ -21,6 +21,7 @@ import * as classNames from 'classnames'; | |||
import * as React from 'react'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import { RawHotspot } from '../../../types/security-hotspots'; | |||
import { getStatusOptionFromStatusAndResolution } from '../utils'; | |||
export interface HotspotListItemProps { | |||
hotspot: RawHotspot; | |||
@@ -37,7 +38,10 @@ export default function HotspotListItem(props: HotspotListItemProps) { | |||
onClick={() => !selected && props.onClick(hotspot.key)}> | |||
<div className="little-spacer-left">{hotspot.message}</div> | |||
<div className="badge spacer-top"> | |||
{translate('hotspot.status', hotspot.resolution || hotspot.status)} | |||
{translate( | |||
'hotspots.status_option', | |||
getStatusOptionFromStatusAndResolution(hotspot.status, hotspot.resolution) | |||
)} | |||
</div> | |||
</a> | |||
); |
@@ -20,26 +20,23 @@ | |||
import * as React from 'react'; | |||
import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import { withCurrentUser } from '../../../components/hoc/withCurrentUser'; | |||
import { isLoggedIn } from '../../../helpers/users'; | |||
import { BranchLike } from '../../../types/branch-like'; | |||
import { Hotspot } from '../../../types/security-hotspots'; | |||
import Assignee from './assignee/Assignee'; | |||
import HotspotActions from './HotspotActions'; | |||
import HotspotSnippetContainer from './HotspotSnippetContainer'; | |||
import HotspotViewerTabs from './HotspotViewerTabs'; | |||
import Status from './status/Status'; | |||
export interface HotspotViewerRendererProps { | |||
branchLike?: BranchLike; | |||
currentUser: T.CurrentUser; | |||
hotspot?: Hotspot; | |||
loading: boolean; | |||
onUpdateHotspot: () => void; | |||
securityCategories: T.StandardSecurityCategories; | |||
} | |||
export function HotspotViewerRenderer(props: HotspotViewerRendererProps) { | |||
const { branchLike, currentUser, hotspot, loading, securityCategories } = props; | |||
export default function HotspotViewerRenderer(props: HotspotViewerRendererProps) { | |||
const { branchLike, hotspot, loading, securityCategories } = props; | |||
return ( | |||
<DeferredSpinner loading={loading}> | |||
@@ -48,9 +45,6 @@ export function HotspotViewerRenderer(props: HotspotViewerRendererProps) { | |||
<div className="big-spacer-bottom"> | |||
<div className="display-flex-space-between"> | |||
<h1>{hotspot.message}</h1> | |||
{isLoggedIn(currentUser) && ( | |||
<HotspotActions hotspot={hotspot} onSubmit={props.onUpdateHotspot} /> | |||
)} | |||
</div> | |||
<div className="text-muted"> | |||
<span>{translate('category')}:</span> | |||
@@ -59,12 +53,9 @@ export function HotspotViewerRenderer(props: HotspotViewerRendererProps) { | |||
</span> | |||
</div> | |||
</div> | |||
<div className="huge-spacer-bottom"> | |||
<span>{translate('status')}:</span> | |||
<span className="badge little-spacer-left"> | |||
{translate('hotspot.status', hotspot.resolution || hotspot.status)} | |||
</span> | |||
<div className="display-flex-row huge-spacer-bottom"> | |||
<Assignee hotspot={hotspot} onAssigneeChange={props.onUpdateHotspot} /> | |||
<Status hotspot={hotspot} onStatusChange={props.onUpdateHotspot} /> | |||
</div> | |||
<HotspotSnippetContainer branchLike={branchLike} hotspot={hotspot} /> | |||
<HotspotViewerTabs hotspot={hotspot} onUpdateHotspot={props.onUpdateHotspot} /> | |||
@@ -73,5 +64,3 @@ export function HotspotViewerRenderer(props: HotspotViewerRendererProps) { | |||
</DeferredSpinner> | |||
); | |||
} | |||
export default withCurrentUser(HotspotViewerRenderer); |
@@ -1,78 +0,0 @@ | |||
/* | |||
* 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 { Button } from 'sonar-ui-common/components/controls/buttons'; | |||
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; | |||
import { mockHotspot } from '../../../../helpers/mocks/security-hotspots'; | |||
import { HotspotStatus } from '../../../../types/security-hotspots'; | |||
import HotspotActions, { HotspotActionsProps } from '../HotspotActions'; | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
}); | |||
it('should open when clicked', async () => { | |||
const wrapper = shallowRender(); | |||
wrapper.find(Button).simulate('click'); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
it('should register an eventlistener', () => { | |||
let useEffectCleanup: void | (() => void | undefined) = () => | |||
fail('useEffect should clean after itself'); | |||
jest.spyOn(React, 'useEffect').mockImplementationOnce(f => { | |||
useEffectCleanup = f() || useEffectCleanup; | |||
}); | |||
let listenerCallback = (_event: { key: string }) => | |||
fail('Effect should have registered callback'); | |||
const addEventListener = jest.fn((_event, callback) => { | |||
listenerCallback = callback; | |||
}); | |||
jest.spyOn(document, 'addEventListener').mockImplementation(addEventListener); | |||
const removeEventListener = jest.spyOn(document, 'removeEventListener'); | |||
const wrapper = shallowRender(); | |||
wrapper.find(Button).simulate('click'); | |||
expect(wrapper).toMatchSnapshot('Dropdown open'); | |||
listenerCallback({ key: 'whatever' }); | |||
expect(wrapper).toMatchSnapshot('Dropdown still open'); | |||
listenerCallback({ key: 'Escape' }); | |||
expect(wrapper).toMatchSnapshot('Dropdown closed'); | |||
useEffectCleanup(); | |||
expect(removeEventListener).toBeCalledWith('keydown', listenerCallback, false); | |||
}); | |||
function shallowRender(props: Partial<HotspotActionsProps> = {}) { | |||
return shallow( | |||
<HotspotActions | |||
hotspot={mockHotspot({ key: 'key', status: HotspotStatus.TO_REVIEW })} | |||
onSubmit={jest.fn()} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -1,155 +0,0 @@ | |||
/* | |||
* 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 { mockEvent, waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; | |||
import { | |||
assignSecurityHotspot, | |||
commentSecurityHotspot, | |||
setSecurityHotspotStatus | |||
} from '../../../../api/security-hotspots'; | |||
import { mockHotspot } from '../../../../helpers/mocks/security-hotspots'; | |||
import { mockLoggedInUser } from '../../../../helpers/testMocks'; | |||
import { | |||
HotspotResolution, | |||
HotspotStatus, | |||
HotspotStatusOption | |||
} from '../../../../types/security-hotspots'; | |||
import HotspotActionsForm from '../HotspotActionsForm'; | |||
jest.mock('../../../../api/security-hotspots', () => ({ | |||
assignSecurityHotspot: jest.fn().mockResolvedValue(undefined), | |||
commentSecurityHotspot: jest.fn().mockResolvedValue(undefined), | |||
setSecurityHotspotStatus: jest.fn().mockResolvedValue(undefined) | |||
})); | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
}); | |||
it('should handle option selection', () => { | |||
const wrapper = shallowRender(); | |||
expect(wrapper.state().selectedOption).toBe(HotspotStatusOption.FIXED); | |||
wrapper.instance().handleSelectOption(HotspotStatusOption.SAFE); | |||
expect(wrapper.state().selectedOption).toBe(HotspotStatusOption.SAFE); | |||
}); | |||
it('should handle comment change', () => { | |||
const wrapper = shallowRender(); | |||
wrapper.instance().handleCommentChange('new comment'); | |||
expect(wrapper.state().comment).toBe('new comment'); | |||
}); | |||
describe('submit', () => { | |||
beforeEach(() => { | |||
jest.clearAllMocks(); | |||
}); | |||
it('should be handled for additional review', async () => { | |||
const onSubmit = jest.fn(); | |||
const wrapper = shallowRender({ onSubmit }); | |||
wrapper.setState({ selectedOption: HotspotStatusOption.ADDITIONAL_REVIEW }); | |||
const promise = wrapper.instance().handleSubmit(mockEvent()); | |||
expect(wrapper.state().submitting).toBe(true); | |||
await promise; | |||
expect(setSecurityHotspotStatus).toBeCalledWith('key', { | |||
status: HotspotStatus.TO_REVIEW | |||
}); | |||
expect(onSubmit).toBeCalled(); | |||
}); | |||
it('should be handled for SAFE', async () => { | |||
const wrapper = shallowRender(); | |||
wrapper.setState({ comment: 'commentsafe', selectedOption: HotspotStatusOption.SAFE }); | |||
await wrapper.instance().handleSubmit(mockEvent()); | |||
expect(setSecurityHotspotStatus).toBeCalledWith('key', { | |||
status: HotspotStatus.REVIEWED, | |||
resolution: HotspotResolution.SAFE | |||
}); | |||
expect(commentSecurityHotspot).toBeCalledWith('key', 'commentsafe'); | |||
}); | |||
it('should be handled for FIXED', async () => { | |||
const wrapper = shallowRender({ | |||
hotspot: mockHotspot({ key: 'key', status: HotspotStatus.TO_REVIEW }) | |||
}); | |||
wrapper.setState({ comment: 'commentfixed', selectedOption: HotspotStatusOption.FIXED }); | |||
await wrapper.instance().handleSubmit(mockEvent()); | |||
expect(setSecurityHotspotStatus).toBeCalledWith('key', { | |||
status: HotspotStatus.REVIEWED, | |||
resolution: HotspotResolution.FIXED | |||
}); | |||
expect(commentSecurityHotspot).toBeCalledWith('key', 'commentfixed'); | |||
}); | |||
it('should ignore no change', async () => { | |||
const wrapper = shallowRender(); | |||
wrapper.setState({ selectedOption: HotspotStatusOption.FIXED }); | |||
await wrapper.instance().handleSubmit(mockEvent()); | |||
expect(setSecurityHotspotStatus).not.toBeCalled(); | |||
}); | |||
}); | |||
it('should handle assignment', async () => { | |||
const onSubmit = jest.fn(); | |||
const wrapper = shallowRender({ onSubmit }); | |||
wrapper.setState({ | |||
comment: 'assignment comment', | |||
selectedOption: HotspotStatusOption.ADDITIONAL_REVIEW | |||
}); | |||
wrapper.instance().handleAssign(mockLoggedInUser({ login: 'userLogin' })); | |||
await waitAndUpdate(wrapper); | |||
const promise = wrapper.instance().handleSubmit({ preventDefault: jest.fn() } as any); | |||
expect(wrapper.state().submitting).toBe(true); | |||
await promise; | |||
expect(setSecurityHotspotStatus).toBeCalledWith('key', { | |||
status: HotspotStatus.TO_REVIEW | |||
}); | |||
expect(assignSecurityHotspot).toBeCalledWith('key', { | |||
assignee: 'userLogin' | |||
}); | |||
expect(commentSecurityHotspot).toBeCalledWith('key', 'assignment comment'); | |||
expect(onSubmit).toBeCalled(); | |||
}); | |||
it('should handle submit failure', async () => { | |||
const onSubmit = jest.fn(); | |||
(setSecurityHotspotStatus as jest.Mock).mockRejectedValueOnce('failure'); | |||
const wrapper = shallowRender({ onSubmit }); | |||
wrapper.setState({ selectedOption: HotspotStatusOption.ADDITIONAL_REVIEW }); | |||
const promise = wrapper.instance().handleSubmit({ preventDefault: jest.fn() } as any); | |||
expect(wrapper.state().submitting).toBe(true); | |||
await promise; | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper.state().submitting).toBe(false); | |||
expect(onSubmit).not.toBeCalled(); | |||
}); | |||
function shallowRender(props: Partial<HotspotActionsForm['props']> = {}) { | |||
return shallow<HotspotActionsForm>( | |||
<HotspotActionsForm hotspot={mockHotspot({ key: 'key' })} onSubmit={jest.fn()} {...props} /> | |||
); | |||
} |
@@ -1,109 +0,0 @@ | |||
/* | |||
* 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 { SubmitButton } from 'sonar-ui-common/components/controls/buttons'; | |||
import { mockHotspot } from '../../../../helpers/mocks/security-hotspots'; | |||
import { mockLoggedInUser } from '../../../../helpers/testMocks'; | |||
import { | |||
HotspotResolution, | |||
HotspotStatus, | |||
HotspotStatusOption | |||
} from '../../../../types/security-hotspots'; | |||
import HotspotActionsForm from '../HotspotActionsForm'; | |||
import HotspotActionsFormRenderer, { | |||
HotspotActionsFormRendererProps | |||
} from '../HotspotActionsFormRenderer'; | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
expect(shallowRender({ submitting: true })).toMatchSnapshot('Submitting'); | |||
expect(shallowRender({ selectedOption: HotspotStatusOption.SAFE })).toMatchSnapshot( | |||
'safe option selected' | |||
); | |||
expect( | |||
shallowRender({ | |||
selectedOption: HotspotStatusOption.ADDITIONAL_REVIEW, | |||
selectedUser: mockLoggedInUser() | |||
}) | |||
).toMatchSnapshot('user selected'); | |||
expect(shallowRender({ hotspot: mockHotspot({ canChangeStatus: false }) })).toMatchSnapshot( | |||
'restricted access' | |||
); | |||
}); | |||
it('should enable the submit button if anything has changed', () => { | |||
const hotspot = mockHotspot({ | |||
status: HotspotStatus.REVIEWED, | |||
resolution: HotspotResolution.SAFE | |||
}); | |||
const selectedOption = HotspotStatusOption.SAFE; | |||
expect( | |||
shallowRender({ comment: '', hotspot, selectedOption, selectedUser: undefined }) | |||
.find(SubmitButton) | |||
.props().disabled | |||
).toBe(true); | |||
expect( | |||
shallowRender({ comment: 'some comment', hotspot, selectedOption, selectedUser: undefined }) | |||
.find(SubmitButton) | |||
.props().disabled | |||
).toBe(false); | |||
expect( | |||
shallowRender({ comment: '', hotspot, selectedOption, selectedUser: mockLoggedInUser() }) | |||
.find(SubmitButton) | |||
.props().disabled | |||
).toBe(false); | |||
expect( | |||
shallowRender({ | |||
comment: '', | |||
hotspot, | |||
selectedOption: HotspotStatusOption.FIXED, | |||
selectedUser: undefined | |||
}) | |||
.find(SubmitButton) | |||
.props().disabled | |||
).toBe(false); | |||
expect( | |||
shallowRender({ | |||
comment: '', | |||
hotspot, | |||
selectedOption: HotspotStatusOption.ADDITIONAL_REVIEW, | |||
selectedUser: undefined | |||
}) | |||
.find(SubmitButton) | |||
.props().disabled | |||
).toBe(false); | |||
}); | |||
function shallowRender(props: Partial<HotspotActionsFormRendererProps> = {}) { | |||
return shallow<HotspotActionsForm>( | |||
<HotspotActionsFormRenderer | |||
comment="written comment" | |||
hotspot={mockHotspot({ key: 'key' })} | |||
onAssign={jest.fn()} | |||
onChangeComment={jest.fn()} | |||
onSelectOption={jest.fn()} | |||
onSubmit={jest.fn()} | |||
selectedOption={HotspotStatusOption.FIXED} | |||
submitting={false} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -20,8 +20,8 @@ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { mockHotspot } from '../../../../helpers/mocks/security-hotspots'; | |||
import { mockCurrentUser, mockLoggedInUser, mockUser } from '../../../../helpers/testMocks'; | |||
import { HotspotViewerRenderer, HotspotViewerRendererProps } from '../HotspotViewerRenderer'; | |||
import { mockUser } from '../../../../helpers/testMocks'; | |||
import HotspotViewerRenderer, { HotspotViewerRendererProps } from '../HotspotViewerRenderer'; | |||
it('should render correctly', () => { | |||
const wrapper = shallowRender(); | |||
@@ -41,13 +41,11 @@ it('should render correctly', () => { | |||
}) | |||
).toMatchSnapshot('assignee without name'); | |||
expect(shallowRender()).toMatchSnapshot('anonymous user'); | |||
expect(shallowRender({ currentUser: mockLoggedInUser() })).toMatchSnapshot('user logged in'); | |||
}); | |||
function shallowRender(props?: Partial<HotspotViewerRendererProps>) { | |||
return shallow( | |||
<HotspotViewerRenderer | |||
currentUser={mockCurrentUser()} | |||
hotspot={mockHotspot()} | |||
loading={false} | |||
onUpdateHotspot={jest.fn()} |
@@ -1,406 +0,0 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should open when clicked 1`] = ` | |||
<div | |||
className="dropdown big-spacer-left flex-0" | |||
> | |||
<Button | |||
onClick={[Function]} | |||
> | |||
hotspot.change_status.TO_REVIEW | |||
<DropdownIcon | |||
className="little-spacer-left" | |||
/> | |||
</Button> | |||
<OutsideClickHandler | |||
onClickOutside={[Function]} | |||
> | |||
<DropdownOverlay | |||
placement="bottom-right" | |||
> | |||
<HotspotActionsForm | |||
hotspot={ | |||
Object { | |||
"assignee": "assignee", | |||
"assigneeUser": Object { | |||
"active": true, | |||
"local": true, | |||
"login": "assignee", | |||
"name": "John Doe", | |||
}, | |||
"author": "author", | |||
"authorUser": Object { | |||
"active": true, | |||
"local": true, | |||
"login": "author", | |||
"name": "John Doe", | |||
}, | |||
"canChangeStatus": true, | |||
"changelog": Array [], | |||
"comment": 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": "key", | |||
"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": "TO_REVIEW", | |||
"textRange": Object { | |||
"endLine": 142, | |||
"endOffset": 83, | |||
"startLine": 142, | |||
"startOffset": 26, | |||
}, | |||
"updateDate": "2013-05-13T17:55:42+0200", | |||
"users": Array [ | |||
Object { | |||
"active": true, | |||
"local": true, | |||
"login": "assignee", | |||
"name": "John Doe", | |||
}, | |||
Object { | |||
"active": true, | |||
"local": true, | |||
"login": "author", | |||
"name": "John Doe", | |||
}, | |||
], | |||
} | |||
} | |||
onSubmit={[Function]} | |||
/> | |||
</DropdownOverlay> | |||
</OutsideClickHandler> | |||
</div> | |||
`; | |||
exports[`should register an eventlistener: Dropdown closed 1`] = ` | |||
<div | |||
className="dropdown big-spacer-left flex-0" | |||
> | |||
<Button | |||
onClick={[Function]} | |||
> | |||
hotspot.change_status.TO_REVIEW | |||
<DropdownIcon | |||
className="little-spacer-left" | |||
/> | |||
</Button> | |||
</div> | |||
`; | |||
exports[`should register an eventlistener: Dropdown open 1`] = ` | |||
<div | |||
className="dropdown big-spacer-left flex-0" | |||
> | |||
<Button | |||
onClick={[Function]} | |||
> | |||
hotspot.change_status.TO_REVIEW | |||
<DropdownIcon | |||
className="little-spacer-left" | |||
/> | |||
</Button> | |||
<OutsideClickHandler | |||
onClickOutside={[Function]} | |||
> | |||
<DropdownOverlay | |||
placement="bottom-right" | |||
> | |||
<HotspotActionsForm | |||
hotspot={ | |||
Object { | |||
"assignee": "assignee", | |||
"assigneeUser": Object { | |||
"active": true, | |||
"local": true, | |||
"login": "assignee", | |||
"name": "John Doe", | |||
}, | |||
"author": "author", | |||
"authorUser": Object { | |||
"active": true, | |||
"local": true, | |||
"login": "author", | |||
"name": "John Doe", | |||
}, | |||
"canChangeStatus": true, | |||
"changelog": Array [], | |||
"comment": 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": "key", | |||
"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": "TO_REVIEW", | |||
"textRange": Object { | |||
"endLine": 142, | |||
"endOffset": 83, | |||
"startLine": 142, | |||
"startOffset": 26, | |||
}, | |||
"updateDate": "2013-05-13T17:55:42+0200", | |||
"users": Array [ | |||
Object { | |||
"active": true, | |||
"local": true, | |||
"login": "assignee", | |||
"name": "John Doe", | |||
}, | |||
Object { | |||
"active": true, | |||
"local": true, | |||
"login": "author", | |||
"name": "John Doe", | |||
}, | |||
], | |||
} | |||
} | |||
onSubmit={[Function]} | |||
/> | |||
</DropdownOverlay> | |||
</OutsideClickHandler> | |||
</div> | |||
`; | |||
exports[`should register an eventlistener: Dropdown still open 1`] = ` | |||
<div | |||
className="dropdown big-spacer-left flex-0" | |||
> | |||
<Button | |||
onClick={[Function]} | |||
> | |||
hotspot.change_status.TO_REVIEW | |||
<DropdownIcon | |||
className="little-spacer-left" | |||
/> | |||
</Button> | |||
<OutsideClickHandler | |||
onClickOutside={[Function]} | |||
> | |||
<DropdownOverlay | |||
placement="bottom-right" | |||
> | |||
<HotspotActionsForm | |||
hotspot={ | |||
Object { | |||
"assignee": "assignee", | |||
"assigneeUser": Object { | |||
"active": true, | |||
"local": true, | |||
"login": "assignee", | |||
"name": "John Doe", | |||
}, | |||
"author": "author", | |||
"authorUser": Object { | |||
"active": true, | |||
"local": true, | |||
"login": "author", | |||
"name": "John Doe", | |||
}, | |||
"canChangeStatus": true, | |||
"changelog": Array [], | |||
"comment": 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": "key", | |||
"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": "TO_REVIEW", | |||
"textRange": Object { | |||
"endLine": 142, | |||
"endOffset": 83, | |||
"startLine": 142, | |||
"startOffset": 26, | |||
}, | |||
"updateDate": "2013-05-13T17:55:42+0200", | |||
"users": Array [ | |||
Object { | |||
"active": true, | |||
"local": true, | |||
"login": "assignee", | |||
"name": "John Doe", | |||
}, | |||
Object { | |||
"active": true, | |||
"local": true, | |||
"login": "author", | |||
"name": "John Doe", | |||
}, | |||
], | |||
} | |||
} | |||
onSubmit={[Function]} | |||
/> | |||
</DropdownOverlay> | |||
</OutsideClickHandler> | |||
</div> | |||
`; | |||
exports[`should render correctly 1`] = ` | |||
<div | |||
className="dropdown big-spacer-left flex-0" | |||
> | |||
<Button | |||
onClick={[Function]} | |||
> | |||
hotspot.change_status.TO_REVIEW | |||
<DropdownIcon | |||
className="little-spacer-left" | |||
/> | |||
</Button> | |||
</div> | |||
`; |
@@ -1,112 +0,0 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly 1`] = ` | |||
<HotspotActionsFormRenderer | |||
comment="" | |||
hotspot={ | |||
Object { | |||
"assignee": "assignee", | |||
"assigneeUser": Object { | |||
"active": true, | |||
"local": true, | |||
"login": "assignee", | |||
"name": "John Doe", | |||
}, | |||
"author": "author", | |||
"authorUser": Object { | |||
"active": true, | |||
"local": true, | |||
"login": "author", | |||
"name": "John Doe", | |||
}, | |||
"canChangeStatus": true, | |||
"changelog": Array [], | |||
"comment": 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": "key", | |||
"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", | |||
"users": Array [ | |||
Object { | |||
"active": true, | |||
"local": true, | |||
"login": "assignee", | |||
"name": "John Doe", | |||
}, | |||
Object { | |||
"active": true, | |||
"local": true, | |||
"login": "author", | |||
"name": "John Doe", | |||
}, | |||
], | |||
} | |||
} | |||
onAssign={[Function]} | |||
onChangeComment={[Function]} | |||
onSelectOption={[Function]} | |||
onSubmit={[Function]} | |||
selectedOption="FIXED" | |||
submitting={false} | |||
/> | |||
`; |
@@ -1,544 +0,0 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly 1`] = ` | |||
<form | |||
className="abs-width-400 padded" | |||
onSubmit={[MockFunction]} | |||
> | |||
<h2> | |||
hotspots.form.title | |||
</h2> | |||
<div | |||
className="display-flex-column big-spacer-bottom" | |||
> | |||
<div | |||
className="big-spacer-top" | |||
> | |||
<Radio | |||
checked={true} | |||
className="" | |||
onCheck={[MockFunction]} | |||
value="FIXED" | |||
> | |||
<h3 | |||
className="" | |||
> | |||
hotspots.status_option.FIXED | |||
</h3> | |||
</Radio> | |||
<div | |||
className="radio-button-description" | |||
> | |||
hotspots.status_option.FIXED.description | |||
</div> | |||
</div> | |||
<div | |||
className="big-spacer-top" | |||
> | |||
<Radio | |||
checked={false} | |||
className="" | |||
onCheck={[MockFunction]} | |||
value="SAFE" | |||
> | |||
<h3 | |||
className="" | |||
> | |||
hotspots.status_option.SAFE | |||
</h3> | |||
</Radio> | |||
<div | |||
className="radio-button-description" | |||
> | |||
hotspots.status_option.SAFE.description | |||
</div> | |||
</div> | |||
<div | |||
className="big-spacer-top" | |||
> | |||
<Radio | |||
checked={false} | |||
className="" | |||
onCheck={[MockFunction]} | |||
value="ADDITIONAL_REVIEW" | |||
> | |||
<h3 | |||
className="" | |||
> | |||
hotspots.status_option.ADDITIONAL_REVIEW | |||
</h3> | |||
</Radio> | |||
<div | |||
className="radio-button-description" | |||
> | |||
hotspots.status_option.ADDITIONAL_REVIEW.description | |||
</div> | |||
</div> | |||
</div> | |||
<div | |||
className="display-flex-column big-spacer-bottom" | |||
> | |||
<label | |||
className="little-spacer-bottom" | |||
> | |||
hotspots.form.comment | |||
</label> | |||
<textarea | |||
autoFocus={true} | |||
className="form-field fixed-width spacer-bottom" | |||
onChange={[Function]} | |||
placeholder="" | |||
rows={6} | |||
value="written comment" | |||
/> | |||
<MarkdownTips /> | |||
</div> | |||
<div | |||
className="text-right" | |||
> | |||
<SubmitButton | |||
disabled={false} | |||
> | |||
hotspots.form.submit.REVIEWED | |||
</SubmitButton> | |||
</div> | |||
</form> | |||
`; | |||
exports[`should render correctly: Submitting 1`] = ` | |||
<form | |||
className="abs-width-400 padded" | |||
onSubmit={[MockFunction]} | |||
> | |||
<h2> | |||
hotspots.form.title | |||
</h2> | |||
<div | |||
className="display-flex-column big-spacer-bottom" | |||
> | |||
<div | |||
className="big-spacer-top" | |||
> | |||
<Radio | |||
checked={true} | |||
className="" | |||
onCheck={[MockFunction]} | |||
value="FIXED" | |||
> | |||
<h3 | |||
className="" | |||
> | |||
hotspots.status_option.FIXED | |||
</h3> | |||
</Radio> | |||
<div | |||
className="radio-button-description" | |||
> | |||
hotspots.status_option.FIXED.description | |||
</div> | |||
</div> | |||
<div | |||
className="big-spacer-top" | |||
> | |||
<Radio | |||
checked={false} | |||
className="" | |||
onCheck={[MockFunction]} | |||
value="SAFE" | |||
> | |||
<h3 | |||
className="" | |||
> | |||
hotspots.status_option.SAFE | |||
</h3> | |||
</Radio> | |||
<div | |||
className="radio-button-description" | |||
> | |||
hotspots.status_option.SAFE.description | |||
</div> | |||
</div> | |||
<div | |||
className="big-spacer-top" | |||
> | |||
<Radio | |||
checked={false} | |||
className="" | |||
onCheck={[MockFunction]} | |||
value="ADDITIONAL_REVIEW" | |||
> | |||
<h3 | |||
className="" | |||
> | |||
hotspots.status_option.ADDITIONAL_REVIEW | |||
</h3> | |||
</Radio> | |||
<div | |||
className="radio-button-description" | |||
> | |||
hotspots.status_option.ADDITIONAL_REVIEW.description | |||
</div> | |||
</div> | |||
</div> | |||
<div | |||
className="display-flex-column big-spacer-bottom" | |||
> | |||
<label | |||
className="little-spacer-bottom" | |||
> | |||
hotspots.form.comment | |||
</label> | |||
<textarea | |||
autoFocus={true} | |||
className="form-field fixed-width spacer-bottom" | |||
onChange={[Function]} | |||
placeholder="" | |||
rows={6} | |||
value="written comment" | |||
/> | |||
<MarkdownTips /> | |||
</div> | |||
<div | |||
className="text-right" | |||
> | |||
<i | |||
className="spinner spacer-right" | |||
/> | |||
<SubmitButton | |||
disabled={true} | |||
> | |||
hotspots.form.submit.REVIEWED | |||
</SubmitButton> | |||
</div> | |||
</form> | |||
`; | |||
exports[`should render correctly: restricted access 1`] = ` | |||
<form | |||
className="abs-width-400 padded" | |||
onSubmit={[MockFunction]} | |||
> | |||
<h2> | |||
hotspots.form.title.disabled | |||
</h2> | |||
<div | |||
className="display-flex-column big-spacer-bottom" | |||
> | |||
<Tooltip | |||
overlay="hotspots.form.cannot_change_status" | |||
placement="left" | |||
> | |||
<div | |||
className="big-spacer-top" | |||
> | |||
<Radio | |||
checked={true} | |||
className="disabled" | |||
onCheck={[Function]} | |||
value="FIXED" | |||
> | |||
<h3 | |||
className="text-muted" | |||
> | |||
hotspots.status_option.FIXED | |||
</h3> | |||
</Radio> | |||
<div | |||
className="radio-button-description text-muted" | |||
> | |||
hotspots.status_option.FIXED.description | |||
</div> | |||
</div> | |||
</Tooltip> | |||
<Tooltip | |||
overlay="hotspots.form.cannot_change_status" | |||
placement="left" | |||
> | |||
<div | |||
className="big-spacer-top" | |||
> | |||
<Radio | |||
checked={false} | |||
className="disabled" | |||
onCheck={[Function]} | |||
value="SAFE" | |||
> | |||
<h3 | |||
className="text-muted" | |||
> | |||
hotspots.status_option.SAFE | |||
</h3> | |||
</Radio> | |||
<div | |||
className="radio-button-description text-muted" | |||
> | |||
hotspots.status_option.SAFE.description | |||
</div> | |||
</div> | |||
</Tooltip> | |||
<Tooltip | |||
overlay="hotspots.form.cannot_change_status" | |||
placement="left" | |||
> | |||
<div | |||
className="big-spacer-top" | |||
> | |||
<Radio | |||
checked={false} | |||
className="disabled" | |||
onCheck={[Function]} | |||
value="ADDITIONAL_REVIEW" | |||
> | |||
<h3 | |||
className="text-muted" | |||
> | |||
hotspots.status_option.ADDITIONAL_REVIEW | |||
</h3> | |||
</Radio> | |||
<div | |||
className="radio-button-description text-muted" | |||
> | |||
hotspots.status_option.ADDITIONAL_REVIEW.description | |||
</div> | |||
</div> | |||
</Tooltip> | |||
</div> | |||
<div | |||
className="display-flex-column big-spacer-bottom" | |||
> | |||
<label | |||
className="little-spacer-bottom" | |||
> | |||
hotspots.form.comment | |||
</label> | |||
<textarea | |||
autoFocus={true} | |||
className="form-field fixed-width spacer-bottom" | |||
onChange={[Function]} | |||
placeholder="" | |||
rows={6} | |||
value="written comment" | |||
/> | |||
<MarkdownTips /> | |||
</div> | |||
<div | |||
className="text-right" | |||
> | |||
<SubmitButton | |||
disabled={false} | |||
> | |||
hotspots.form.submit.REVIEWED | |||
</SubmitButton> | |||
</div> | |||
</form> | |||
`; | |||
exports[`should render correctly: safe option selected 1`] = ` | |||
<form | |||
className="abs-width-400 padded" | |||
onSubmit={[MockFunction]} | |||
> | |||
<h2> | |||
hotspots.form.title | |||
</h2> | |||
<div | |||
className="display-flex-column big-spacer-bottom" | |||
> | |||
<div | |||
className="big-spacer-top" | |||
> | |||
<Radio | |||
checked={false} | |||
className="" | |||
onCheck={[MockFunction]} | |||
value="FIXED" | |||
> | |||
<h3 | |||
className="" | |||
> | |||
hotspots.status_option.FIXED | |||
</h3> | |||
</Radio> | |||
<div | |||
className="radio-button-description" | |||
> | |||
hotspots.status_option.FIXED.description | |||
</div> | |||
</div> | |||
<div | |||
className="big-spacer-top" | |||
> | |||
<Radio | |||
checked={true} | |||
className="" | |||
onCheck={[MockFunction]} | |||
value="SAFE" | |||
> | |||
<h3 | |||
className="" | |||
> | |||
hotspots.status_option.SAFE | |||
</h3> | |||
</Radio> | |||
<div | |||
className="radio-button-description" | |||
> | |||
hotspots.status_option.SAFE.description | |||
</div> | |||
</div> | |||
<div | |||
className="big-spacer-top" | |||
> | |||
<Radio | |||
checked={false} | |||
className="" | |||
onCheck={[MockFunction]} | |||
value="ADDITIONAL_REVIEW" | |||
> | |||
<h3 | |||
className="" | |||
> | |||
hotspots.status_option.ADDITIONAL_REVIEW | |||
</h3> | |||
</Radio> | |||
<div | |||
className="radio-button-description" | |||
> | |||
hotspots.status_option.ADDITIONAL_REVIEW.description | |||
</div> | |||
</div> | |||
</div> | |||
<div | |||
className="display-flex-column big-spacer-bottom" | |||
> | |||
<label | |||
className="little-spacer-bottom" | |||
> | |||
hotspots.form.comment | |||
</label> | |||
<textarea | |||
autoFocus={true} | |||
className="form-field fixed-width spacer-bottom" | |||
onChange={[Function]} | |||
placeholder="hotspots.form.comment.placeholder" | |||
rows={6} | |||
value="written comment" | |||
/> | |||
<MarkdownTips /> | |||
</div> | |||
<div | |||
className="text-right" | |||
> | |||
<SubmitButton | |||
disabled={false} | |||
> | |||
hotspots.form.submit.REVIEWED | |||
</SubmitButton> | |||
</div> | |||
</form> | |||
`; | |||
exports[`should render correctly: user selected 1`] = ` | |||
<form | |||
className="abs-width-400 padded" | |||
onSubmit={[MockFunction]} | |||
> | |||
<h2> | |||
hotspots.form.title | |||
</h2> | |||
<div | |||
className="display-flex-column big-spacer-bottom" | |||
> | |||
<div | |||
className="big-spacer-top" | |||
> | |||
<Radio | |||
checked={false} | |||
className="" | |||
onCheck={[MockFunction]} | |||
value="FIXED" | |||
> | |||
<h3 | |||
className="" | |||
> | |||
hotspots.status_option.FIXED | |||
</h3> | |||
</Radio> | |||
<div | |||
className="radio-button-description" | |||
> | |||
hotspots.status_option.FIXED.description | |||
</div> | |||
</div> | |||
<div | |||
className="big-spacer-top" | |||
> | |||
<Radio | |||
checked={false} | |||
className="" | |||
onCheck={[MockFunction]} | |||
value="SAFE" | |||
> | |||
<h3 | |||
className="" | |||
> | |||
hotspots.status_option.SAFE | |||
</h3> | |||
</Radio> | |||
<div | |||
className="radio-button-description" | |||
> | |||
hotspots.status_option.SAFE.description | |||
</div> | |||
</div> | |||
<div | |||
className="big-spacer-top" | |||
> | |||
<Radio | |||
checked={true} | |||
className="" | |||
onCheck={[MockFunction]} | |||
value="ADDITIONAL_REVIEW" | |||
> | |||
<h3 | |||
className="" | |||
> | |||
hotspots.status_option.ADDITIONAL_REVIEW | |||
</h3> | |||
</Radio> | |||
<div | |||
className="radio-button-description" | |||
> | |||
hotspots.status_option.ADDITIONAL_REVIEW.description | |||
</div> | |||
</div> | |||
</div> | |||
<div | |||
className="display-flex-column big-spacer-bottom" | |||
> | |||
<label | |||
className="little-spacer-bottom" | |||
> | |||
hotspots.form.comment | |||
</label> | |||
<textarea | |||
autoFocus={true} | |||
className="form-field fixed-width spacer-bottom" | |||
onChange={[Function]} | |||
placeholder="" | |||
rows={6} | |||
value="written comment" | |||
/> | |||
<MarkdownTips /> | |||
</div> | |||
<div | |||
className="text-right" | |||
> | |||
<SubmitButton | |||
disabled={false} | |||
> | |||
hotspots.form.submit.REVIEWED | |||
</SubmitButton> | |||
</div> | |||
</form> | |||
`; |
@@ -14,7 +14,7 @@ exports[`should render correctly 1`] = ` | |||
<div | |||
className="badge spacer-top" | |||
> | |||
hotspot.status.TO_REVIEW | |||
hotspots.status_option.TO_REVIEW | |||
</div> | |||
</a> | |||
`; | |||
@@ -33,7 +33,7 @@ exports[`should render correctly 2`] = ` | |||
<div | |||
className="badge spacer-top" | |||
> | |||
hotspot.status.TO_REVIEW | |||
hotspots.status_option.TO_REVIEW | |||
</div> | |||
</a> | |||
`; |
@@ -1,7 +1,7 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly 1`] = ` | |||
<Connect(withCurrentUser(HotspotViewerRenderer)) | |||
<HotspotViewerRenderer | |||
loading={true} | |||
onUpdateHotspot={[Function]} | |||
securityCategories={ | |||
@@ -15,7 +15,7 @@ exports[`should render correctly 1`] = ` | |||
`; | |||
exports[`should render correctly 2`] = ` | |||
<Connect(withCurrentUser(HotspotViewerRenderer)) | |||
<HotspotViewerRenderer | |||
hotspot={ | |||
Object { | |||
"id": "I am a detailled hotspot", |
@@ -47,7 +47,7 @@ export default function AssigneeSelectionRenderer(props: HotspotAssigneeSelectRe | |||
autoFocus={true} | |||
onChange={props.onSearch} | |||
onKeyDown={props.onKeyDown} | |||
placeholder={translate('hotspots.form.select_user')} | |||
placeholder={translate('hotspots.assignee.select_user')} | |||
value={query} | |||
/> | |||
@@ -9,7 +9,7 @@ exports[`should render correctly 1`] = ` | |||
autoFocus={true} | |||
onChange={[MockFunction]} | |||
onKeyDown={[MockFunction]} | |||
placeholder="hotspots.form.select_user" | |||
placeholder="hotspots.assignee.select_user" | |||
/> | |||
</div> | |||
</Fragment> | |||
@@ -24,7 +24,7 @@ exports[`should render correctly: loading 1`] = ` | |||
autoFocus={true} | |||
onChange={[MockFunction]} | |||
onKeyDown={[MockFunction]} | |||
placeholder="hotspots.form.select_user" | |||
placeholder="hotspots.assignee.select_user" | |||
/> | |||
<DeferredSpinner | |||
className="spacer-left" | |||
@@ -43,7 +43,7 @@ exports[`should render correctly: open 1`] = ` | |||
autoFocus={true} | |||
onChange={[MockFunction]} | |||
onKeyDown={[MockFunction]} | |||
placeholder="hotspots.form.select_user" | |||
placeholder="hotspots.assignee.select_user" | |||
/> | |||
</div> | |||
<div | |||
@@ -73,7 +73,7 @@ exports[`should render correctly: open with results 1`] = ` | |||
autoFocus={true} | |||
onChange={[MockFunction]} | |||
onKeyDown={[MockFunction]} | |||
placeholder="hotspots.form.select_user" | |||
placeholder="hotspots.assignee.select_user" | |||
/> | |||
</div> | |||
<div |
@@ -0,0 +1,44 @@ | |||
/* | |||
* 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. | |||
*/ | |||
#status-trigger, | |||
.popup { | |||
width: 400px; | |||
box-sizing: border-box; | |||
} | |||
#status-trigger { | |||
height: 80px; | |||
border-radius: 4px; | |||
outline: none; | |||
} | |||
#status-trigger.readonly { | |||
cursor: not-allowed; | |||
} | |||
#status-trigger:not(.readonly) { | |||
cursor: pointer; | |||
background-color: var(--darkBlue); | |||
} | |||
#status-trigger:not(.readonly) * { | |||
color: white; | |||
} |
@@ -0,0 +1,107 @@ | |||
/* | |||
* 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 classNames from 'classnames'; | |||
import * as React from 'react'; | |||
import { DropdownOverlay } from 'sonar-ui-common/components/controls/Dropdown'; | |||
import Toggler from 'sonar-ui-common/components/controls/Toggler'; | |||
import Tooltip from 'sonar-ui-common/components/controls/Tooltip'; | |||
import ChevronDownIcon from 'sonar-ui-common/components/icons/ChevronDownIcon'; | |||
import { PopupPlacement } from 'sonar-ui-common/components/ui/popups'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import { withCurrentUser } from '../../../../components/hoc/withCurrentUser'; | |||
import { isLoggedIn } from '../../../../helpers/users'; | |||
import { Hotspot } from '../../../../types/security-hotspots'; | |||
import { getStatusOptionFromStatusAndResolution } from '../../utils'; | |||
import './Status.css'; | |||
import StatusDescription from './StatusDescription'; | |||
import StatusSelection from './StatusSelection'; | |||
export interface StatusProps { | |||
currentUser: T.CurrentUser; | |||
hotspot: Hotspot; | |||
onStatusChange: () => void; | |||
} | |||
export function Status(props: StatusProps) { | |||
const { currentUser, hotspot } = props; | |||
const [isOpen, setIsOpen] = React.useState(false); | |||
const statusOption = getStatusOptionFromStatusAndResolution(hotspot.status, hotspot.resolution); | |||
const readonly = !hotspot.canChangeStatus || !isLoggedIn(currentUser); | |||
const trigger = ( | |||
<div | |||
aria-expanded={isOpen} | |||
aria-haspopup={true} | |||
className={classNames('padded bordered display-flex-column display-flex-justify-center', { | |||
readonly | |||
})} | |||
id="status-trigger" | |||
onClick={() => !readonly && setIsOpen(true)} | |||
role="button" | |||
tabIndex={0}> | |||
<div className="display-flex-center display-flex-space-between"> | |||
{isOpen ? ( | |||
<span className="h3">{translate('hotspots.status.select_status')}</span> | |||
) : ( | |||
<StatusDescription showTitle={true} statusOption={statusOption} /> | |||
)} | |||
{!readonly && <ChevronDownIcon className="big-spacer-left" />} | |||
</div> | |||
</div> | |||
); | |||
const actionableTrigger = ( | |||
<Toggler | |||
closeOnClickOutside={true} | |||
closeOnEscape={true} | |||
onRequestClose={() => setIsOpen(false)} | |||
open={isOpen} | |||
overlay={ | |||
<DropdownOverlay noPadding={true} placement={PopupPlacement.Bottom}> | |||
<StatusSelection | |||
hotspot={hotspot} | |||
onStatusOptionChange={() => { | |||
setIsOpen(false); | |||
props.onStatusChange(); | |||
}} | |||
/> | |||
</DropdownOverlay> | |||
}> | |||
{trigger} | |||
</Toggler> | |||
); | |||
return ( | |||
<div className="dropdown huge-spacer-left"> | |||
{readonly ? ( | |||
<Tooltip overlay={translate('hotspots.status.cannot_change_status')} placement="bottom"> | |||
{actionableTrigger} | |||
</Tooltip> | |||
) : ( | |||
actionableTrigger | |||
)} | |||
</div> | |||
); | |||
} | |||
export default withCurrentUser(Status); |
@@ -0,0 +1,41 @@ | |||
/* | |||
* 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 } from 'sonar-ui-common/helpers/l10n'; | |||
import { HotspotStatusOption } from '../../../../types/security-hotspots'; | |||
export interface StatusDescriptionProps { | |||
statusOption: HotspotStatusOption; | |||
showTitle?: boolean; | |||
} | |||
export default function StatusDescription(props: StatusDescriptionProps) { | |||
const { statusOption, showTitle } = props; | |||
return ( | |||
<div> | |||
<h3> | |||
{showTitle && `${translate('status')}: `} | |||
{translate('hotspots.status_option', statusOption)} | |||
</h3> | |||
<span>{translate('hotspots.status_option', statusOption, 'description')}</span> | |||
</div> | |||
); | |||
} |
@@ -0,0 +1,109 @@ | |||
/* | |||
* 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 { setSecurityHotspotStatus } from '../../../../api/security-hotspots'; | |||
import { Hotspot, HotspotStatusOption } from '../../../../types/security-hotspots'; | |||
import { | |||
getStatusAndResolutionFromStatusOption, | |||
getStatusOptionFromStatusAndResolution | |||
} from '../../utils'; | |||
import StatusSelectionRenderer from './StatusSelectionRenderer'; | |||
interface Props { | |||
hotspot: Hotspot; | |||
onStatusOptionChange: (statusOption: HotspotStatusOption) => void; | |||
} | |||
interface State { | |||
comment?: string; | |||
loading: boolean; | |||
initialStatus: HotspotStatusOption; | |||
selectedStatus: HotspotStatusOption; | |||
} | |||
export default class StatusSelection extends React.PureComponent<Props, State> { | |||
mounted = false; | |||
constructor(props: Props) { | |||
super(props); | |||
const initialStatus = getStatusOptionFromStatusAndResolution( | |||
props.hotspot.status, | |||
props.hotspot.resolution | |||
); | |||
this.state = { | |||
loading: false, | |||
initialStatus, | |||
selectedStatus: initialStatus | |||
}; | |||
} | |||
componentDidMount() { | |||
this.mounted = true; | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
} | |||
handleStatusChange = (selectedStatus: HotspotStatusOption) => { | |||
this.setState({ selectedStatus }); | |||
}; | |||
handleCommentChange = (comment: string) => { | |||
this.setState({ comment }); | |||
}; | |||
handleSubmit = () => { | |||
const { hotspot } = this.props; | |||
const { comment, initialStatus, selectedStatus } = this.state; | |||
if (selectedStatus && selectedStatus !== initialStatus) { | |||
this.setState({ loading: true }); | |||
setSecurityHotspotStatus(hotspot.key, { | |||
...getStatusAndResolutionFromStatusOption(selectedStatus), | |||
comment: comment || undefined | |||
}) | |||
.then(() => { | |||
this.setState({ loading: false }); | |||
this.props.onStatusOptionChange(selectedStatus); | |||
}) | |||
.catch(() => this.setState({ loading: false })); | |||
} | |||
}; | |||
render() { | |||
const { comment, initialStatus, loading, selectedStatus } = this.state; | |||
const submitDisabled = selectedStatus === initialStatus; | |||
return ( | |||
<StatusSelectionRenderer | |||
comment={comment} | |||
loading={loading} | |||
onCommentChange={this.handleCommentChange} | |||
onStatusChange={this.handleStatusChange} | |||
onSubmit={this.handleSubmit} | |||
selectedStatus={selectedStatus} | |||
submitDisabled={submitDisabled} | |||
/> | |||
); | |||
} | |||
} |
@@ -0,0 +1,91 @@ | |||
/* | |||
* 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 { SubmitButton } from 'sonar-ui-common/components/controls/buttons'; | |||
import Radio from 'sonar-ui-common/components/controls/Radio'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import MarkdownTips from '../../../../components/common/MarkdownTips'; | |||
import { HotspotStatusOption } from '../../../../types/security-hotspots'; | |||
import StatusDescription from './StatusDescription'; | |||
export interface StatusSelectionRendererProps { | |||
selectedStatus: HotspotStatusOption; | |||
onStatusChange: (statusOption: HotspotStatusOption) => void; | |||
comment?: string; | |||
onCommentChange: (comment: string) => void; | |||
onSubmit: () => void; | |||
loading: boolean; | |||
submitDisabled: boolean; | |||
} | |||
export default function StatusSelectionRenderer(props: StatusSelectionRendererProps) { | |||
const { comment, loading, selectedStatus, submitDisabled } = props; | |||
const renderOption = (status: HotspotStatusOption) => { | |||
return ( | |||
<Radio | |||
checked={selectedStatus === status} | |||
className="big-spacer-bottom" | |||
onCheck={props.onStatusChange} | |||
value={status}> | |||
<StatusDescription statusOption={status} /> | |||
</Radio> | |||
); | |||
}; | |||
return ( | |||
<> | |||
<div className="big-padded"> | |||
{renderOption(HotspotStatusOption.TO_REVIEW)} | |||
{renderOption(HotspotStatusOption.FIXED)} | |||
{renderOption(HotspotStatusOption.SAFE)} | |||
</div> | |||
<hr /> | |||
<div className="big-padded display-flex-column"> | |||
<label className="text-bold" htmlFor="comment-textarea"> | |||
{translate('hotspots.status.add_comment')} | |||
</label> | |||
<textarea | |||
className="spacer-top form-field fixed-width spacer-bottom" | |||
id="comment-textarea" | |||
onChange={(event: React.ChangeEvent<HTMLTextAreaElement>) => | |||
props.onCommentChange(event.currentTarget.value) | |||
} | |||
rows={4} | |||
value={comment} | |||
/> | |||
<MarkdownTips /> | |||
<div className="big-spacer-top display-flex-justify-end display-flex-center"> | |||
<SubmitButton disabled={submitDisabled || loading} onClick={props.onSubmit}> | |||
{translate('hotspots.status.change_status')} | |||
</SubmitButton> | |||
{loading && <i className="spacer-left spinner" />} | |||
</div> | |||
</div> | |||
</> | |||
); | |||
} |
@@ -0,0 +1,74 @@ | |||
/* | |||
* 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 { DropdownOverlay } from 'sonar-ui-common/components/controls/Dropdown'; | |||
import Toggler from 'sonar-ui-common/components/controls/Toggler'; | |||
import { click } from 'sonar-ui-common/helpers/testUtils'; | |||
import { mockHotspot } from '../../../../../helpers/mocks/security-hotspots'; | |||
import { mockCurrentUser } from '../../../../../helpers/testMocks'; | |||
import { HotspotStatusOption } from '../../../../../types/security-hotspots'; | |||
import { Status, StatusProps } from '../Status'; | |||
import StatusSelection from '../StatusSelection'; | |||
it('should render correctly', () => { | |||
const wrapper = shallowRender(); | |||
expect(wrapper).toMatchSnapshot('closed'); | |||
click(wrapper.find('#status-trigger')); | |||
expect(wrapper).toMatchSnapshot('open'); | |||
wrapper | |||
.find(Toggler) | |||
.props() | |||
.onRequestClose(); | |||
expect(wrapper.find(DropdownOverlay).length).toBe(0); | |||
expect(shallowRender({ hotspot: mockHotspot({ canChangeStatus: false }) })).toMatchSnapshot( | |||
'readonly' | |||
); | |||
}); | |||
it('should properly deal with status changes', () => { | |||
const onStatusChange = jest.fn(); | |||
const wrapper = shallowRender({ onStatusChange }); | |||
click(wrapper.find('#status-trigger')); | |||
wrapper | |||
.find(Toggler) | |||
.dive() | |||
.find(StatusSelection) | |||
.props() | |||
.onStatusOptionChange(HotspotStatusOption.SAFE); | |||
expect(onStatusChange).toHaveBeenCalled(); | |||
expect(wrapper.find(DropdownOverlay).length).toBe(0); | |||
}); | |||
function shallowRender(props?: Partial<StatusProps>) { | |||
return shallow<StatusProps>( | |||
<Status | |||
currentUser={mockCurrentUser({ isLoggedIn: true })} | |||
hotspot={mockHotspot()} | |||
onStatusChange={jest.fn()} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -0,0 +1,35 @@ | |||
/* | |||
* 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 { HotspotStatusOption } from '../../../../../types/security-hotspots'; | |||
import StatusDescription, { StatusDescriptionProps } from '../StatusDescription'; | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
expect(shallowRender({ showTitle: true })).toMatchSnapshot('with title'); | |||
}); | |||
function shallowRender(props?: Partial<StatusDescriptionProps>) { | |||
return shallow<StatusDescriptionProps>( | |||
<StatusDescription statusOption={HotspotStatusOption.TO_REVIEW} {...props} /> | |||
); | |||
} |
@@ -0,0 +1,78 @@ | |||
/* | |||
* 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 { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; | |||
import { setSecurityHotspotStatus } from '../../../../../api/security-hotspots'; | |||
import { mockHotspot } from '../../../../../helpers/mocks/security-hotspots'; | |||
import { HotspotStatus, HotspotStatusOption } from '../../../../../types/security-hotspots'; | |||
import StatusSelection from '../StatusSelection'; | |||
import StatusSelectionRenderer from '../StatusSelectionRenderer'; | |||
jest.mock('../../../../../api/security-hotspots', () => ({ | |||
setSecurityHotspotStatus: jest.fn() | |||
})); | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
}); | |||
it('should properly deal with comment/status/submit events', async () => { | |||
const hotspot = mockHotspot(); | |||
const onStatusOptionChange = jest.fn(); | |||
const wrapper = shallowRender({ hotspot, onStatusOptionChange }); | |||
const newStatusOption = HotspotStatusOption.SAFE; | |||
wrapper | |||
.find(StatusSelectionRenderer) | |||
.props() | |||
.onStatusChange(newStatusOption); | |||
expect(wrapper.state().selectedStatus).toBe(newStatusOption); | |||
expect(wrapper.find(StatusSelectionRenderer).props().submitDisabled).toBe(false); | |||
const newComment = 'TEST-COMMENT'; | |||
wrapper | |||
.find(StatusSelectionRenderer) | |||
.props() | |||
.onCommentChange(newComment); | |||
expect(wrapper.state().comment).toBe(newComment); | |||
(setSecurityHotspotStatus as jest.Mock).mockResolvedValueOnce({}); | |||
wrapper | |||
.find(StatusSelectionRenderer) | |||
.props() | |||
.onSubmit(); | |||
expect(setSecurityHotspotStatus).toHaveBeenCalledWith(hotspot.key, { | |||
status: HotspotStatus.REVIEWED, | |||
resolution: HotspotStatusOption.SAFE, | |||
comment: newComment | |||
}); | |||
await waitAndUpdate(wrapper); | |||
expect(onStatusOptionChange).toHaveBeenCalledWith(newStatusOption); | |||
}); | |||
function shallowRender(props?: Partial<StatusSelection['props']>) { | |||
return shallow<StatusSelection>( | |||
<StatusSelection hotspot={mockHotspot()} onStatusOptionChange={jest.fn()} {...props} /> | |||
); | |||
} |
@@ -0,0 +1,72 @@ | |||
/* | |||
* 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 { SubmitButton } from 'sonar-ui-common/components/controls/buttons'; | |||
import Radio from 'sonar-ui-common/components/controls/Radio'; | |||
import { change, click } from 'sonar-ui-common/helpers/testUtils'; | |||
import { HotspotStatusOption } from '../../../../../types/security-hotspots'; | |||
import StatusSelectionRenderer, { StatusSelectionRendererProps } from '../StatusSelectionRenderer'; | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
expect(shallowRender({ loading: true })).toMatchSnapshot('loading'); | |||
expect( | |||
shallowRender({ submitDisabled: true }) | |||
.find(SubmitButton) | |||
.props().disabled | |||
).toBe(true); | |||
}); | |||
it('should call proper callbacks on actions', () => { | |||
const onCommentChange = jest.fn(); | |||
const onStatusChange = jest.fn(); | |||
const onSubmit = jest.fn(); | |||
const wrapper = shallowRender({ onCommentChange, onStatusChange, onSubmit }); | |||
change(wrapper.find('textarea'), 'TATA'); | |||
expect(onCommentChange).toHaveBeenCalledWith('TATA'); | |||
wrapper | |||
.find(Radio) | |||
.first() | |||
.props() | |||
.onCheck(HotspotStatusOption.SAFE); | |||
expect(onStatusChange).toHaveBeenCalledWith(HotspotStatusOption.SAFE); | |||
click(wrapper.find(SubmitButton)); | |||
expect(onSubmit).toHaveBeenCalled(); | |||
}); | |||
function shallowRender(props?: Partial<StatusSelectionRendererProps>) { | |||
return shallow<StatusSelectionRendererProps>( | |||
<StatusSelectionRenderer | |||
comment="TEST-COMMENT" | |||
loading={false} | |||
onCommentChange={jest.fn()} | |||
onStatusChange={jest.fn()} | |||
onSubmit={jest.fn()} | |||
selectedStatus={HotspotStatusOption.TO_REVIEW} | |||
submitDisabled={false} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -0,0 +1,436 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly: closed 1`] = ` | |||
<div | |||
className="dropdown huge-spacer-left" | |||
> | |||
<Toggler | |||
closeOnClickOutside={true} | |||
closeOnEscape={true} | |||
onRequestClose={[Function]} | |||
open={false} | |||
overlay={ | |||
<DropdownOverlay | |||
noPadding={true} | |||
placement="bottom" | |||
> | |||
<StatusSelection | |||
hotspot={ | |||
Object { | |||
"assignee": "assignee", | |||
"assigneeUser": Object { | |||
"active": true, | |||
"local": true, | |||
"login": "assignee", | |||
"name": "John Doe", | |||
}, | |||
"author": "author", | |||
"authorUser": Object { | |||
"active": true, | |||
"local": true, | |||
"login": "author", | |||
"name": "John Doe", | |||
}, | |||
"canChangeStatus": true, | |||
"changelog": Array [], | |||
"comment": 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", | |||
"users": Array [ | |||
Object { | |||
"active": true, | |||
"local": true, | |||
"login": "assignee", | |||
"name": "John Doe", | |||
}, | |||
Object { | |||
"active": true, | |||
"local": true, | |||
"login": "author", | |||
"name": "John Doe", | |||
}, | |||
], | |||
} | |||
} | |||
onStatusOptionChange={[Function]} | |||
/> | |||
</DropdownOverlay> | |||
} | |||
> | |||
<div | |||
aria-expanded={false} | |||
aria-haspopup={true} | |||
className="padded bordered display-flex-column display-flex-justify-center" | |||
id="status-trigger" | |||
onClick={[Function]} | |||
role="button" | |||
tabIndex={0} | |||
> | |||
<div | |||
className="display-flex-center display-flex-space-between" | |||
> | |||
<StatusDescription | |||
showTitle={true} | |||
statusOption="FIXED" | |||
/> | |||
<ChevronDownIcon | |||
className="big-spacer-left" | |||
/> | |||
</div> | |||
</div> | |||
</Toggler> | |||
</div> | |||
`; | |||
exports[`should render correctly: open 1`] = ` | |||
<div | |||
className="dropdown huge-spacer-left" | |||
> | |||
<Toggler | |||
closeOnClickOutside={true} | |||
closeOnEscape={true} | |||
onRequestClose={[Function]} | |||
open={true} | |||
overlay={ | |||
<DropdownOverlay | |||
noPadding={true} | |||
placement="bottom" | |||
> | |||
<StatusSelection | |||
hotspot={ | |||
Object { | |||
"assignee": "assignee", | |||
"assigneeUser": Object { | |||
"active": true, | |||
"local": true, | |||
"login": "assignee", | |||
"name": "John Doe", | |||
}, | |||
"author": "author", | |||
"authorUser": Object { | |||
"active": true, | |||
"local": true, | |||
"login": "author", | |||
"name": "John Doe", | |||
}, | |||
"canChangeStatus": true, | |||
"changelog": Array [], | |||
"comment": 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", | |||
"users": Array [ | |||
Object { | |||
"active": true, | |||
"local": true, | |||
"login": "assignee", | |||
"name": "John Doe", | |||
}, | |||
Object { | |||
"active": true, | |||
"local": true, | |||
"login": "author", | |||
"name": "John Doe", | |||
}, | |||
], | |||
} | |||
} | |||
onStatusOptionChange={[Function]} | |||
/> | |||
</DropdownOverlay> | |||
} | |||
> | |||
<div | |||
aria-expanded={true} | |||
aria-haspopup={true} | |||
className="padded bordered display-flex-column display-flex-justify-center" | |||
id="status-trigger" | |||
onClick={[Function]} | |||
role="button" | |||
tabIndex={0} | |||
> | |||
<div | |||
className="display-flex-center display-flex-space-between" | |||
> | |||
<span | |||
className="h3" | |||
> | |||
hotspots.status.select_status | |||
</span> | |||
<ChevronDownIcon | |||
className="big-spacer-left" | |||
/> | |||
</div> | |||
</div> | |||
</Toggler> | |||
</div> | |||
`; | |||
exports[`should render correctly: readonly 1`] = ` | |||
<div | |||
className="dropdown huge-spacer-left" | |||
> | |||
<Tooltip | |||
overlay="hotspots.status.cannot_change_status" | |||
placement="bottom" | |||
> | |||
<Toggler | |||
closeOnClickOutside={true} | |||
closeOnEscape={true} | |||
onRequestClose={[Function]} | |||
open={false} | |||
overlay={ | |||
<DropdownOverlay | |||
noPadding={true} | |||
placement="bottom" | |||
> | |||
<StatusSelection | |||
hotspot={ | |||
Object { | |||
"assignee": "assignee", | |||
"assigneeUser": Object { | |||
"active": true, | |||
"local": true, | |||
"login": "assignee", | |||
"name": "John Doe", | |||
}, | |||
"author": "author", | |||
"authorUser": Object { | |||
"active": true, | |||
"local": true, | |||
"login": "author", | |||
"name": "John Doe", | |||
}, | |||
"canChangeStatus": false, | |||
"changelog": Array [], | |||
"comment": 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", | |||
"users": Array [ | |||
Object { | |||
"active": true, | |||
"local": true, | |||
"login": "assignee", | |||
"name": "John Doe", | |||
}, | |||
Object { | |||
"active": true, | |||
"local": true, | |||
"login": "author", | |||
"name": "John Doe", | |||
}, | |||
], | |||
} | |||
} | |||
onStatusOptionChange={[Function]} | |||
/> | |||
</DropdownOverlay> | |||
} | |||
> | |||
<div | |||
aria-expanded={false} | |||
aria-haspopup={true} | |||
className="padded bordered display-flex-column display-flex-justify-center readonly" | |||
id="status-trigger" | |||
onClick={[Function]} | |||
role="button" | |||
tabIndex={0} | |||
> | |||
<div | |||
className="display-flex-center display-flex-space-between" | |||
> | |||
<StatusDescription | |||
showTitle={true} | |||
statusOption="FIXED" | |||
/> | |||
</div> | |||
</div> | |||
</Toggler> | |||
</Tooltip> | |||
</div> | |||
`; |
@@ -0,0 +1,24 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly 1`] = ` | |||
<div> | |||
<h3> | |||
hotspots.status_option.TO_REVIEW | |||
</h3> | |||
<span> | |||
hotspots.status_option.TO_REVIEW.description | |||
</span> | |||
</div> | |||
`; | |||
exports[`should render correctly: with title 1`] = ` | |||
<div> | |||
<h3> | |||
status: | |||
hotspots.status_option.TO_REVIEW | |||
</h3> | |||
<span> | |||
hotspots.status_option.TO_REVIEW.description | |||
</span> | |||
</div> | |||
`; |
@@ -0,0 +1,12 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly 1`] = ` | |||
<StatusSelectionRenderer | |||
loading={false} | |||
onCommentChange={[Function]} | |||
onStatusChange={[Function]} | |||
onSubmit={[Function]} | |||
selectedStatus="FIXED" | |||
submitDisabled={true} | |||
/> | |||
`; |
@@ -0,0 +1,140 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly 1`] = ` | |||
<Fragment> | |||
<div | |||
className="big-padded" | |||
> | |||
<Radio | |||
checked={true} | |||
className="big-spacer-bottom" | |||
onCheck={[MockFunction]} | |||
value="TO_REVIEW" | |||
> | |||
<StatusDescription | |||
statusOption="TO_REVIEW" | |||
/> | |||
</Radio> | |||
<Radio | |||
checked={false} | |||
className="big-spacer-bottom" | |||
onCheck={[MockFunction]} | |||
value="FIXED" | |||
> | |||
<StatusDescription | |||
statusOption="FIXED" | |||
/> | |||
</Radio> | |||
<Radio | |||
checked={false} | |||
className="big-spacer-bottom" | |||
onCheck={[MockFunction]} | |||
value="SAFE" | |||
> | |||
<StatusDescription | |||
statusOption="SAFE" | |||
/> | |||
</Radio> | |||
</div> | |||
<hr /> | |||
<div | |||
className="big-padded display-flex-column" | |||
> | |||
<label | |||
className="text-bold" | |||
htmlFor="comment-textarea" | |||
> | |||
hotspots.status.add_comment | |||
</label> | |||
<textarea | |||
className="spacer-top form-field fixed-width spacer-bottom" | |||
id="comment-textarea" | |||
onChange={[Function]} | |||
rows={4} | |||
value="TEST-COMMENT" | |||
/> | |||
<MarkdownTips /> | |||
<div | |||
className="big-spacer-top display-flex-justify-end display-flex-center" | |||
> | |||
<SubmitButton | |||
disabled={false} | |||
onClick={[MockFunction]} | |||
> | |||
hotspots.status.change_status | |||
</SubmitButton> | |||
</div> | |||
</div> | |||
</Fragment> | |||
`; | |||
exports[`should render correctly: loading 1`] = ` | |||
<Fragment> | |||
<div | |||
className="big-padded" | |||
> | |||
<Radio | |||
checked={true} | |||
className="big-spacer-bottom" | |||
onCheck={[MockFunction]} | |||
value="TO_REVIEW" | |||
> | |||
<StatusDescription | |||
statusOption="TO_REVIEW" | |||
/> | |||
</Radio> | |||
<Radio | |||
checked={false} | |||
className="big-spacer-bottom" | |||
onCheck={[MockFunction]} | |||
value="FIXED" | |||
> | |||
<StatusDescription | |||
statusOption="FIXED" | |||
/> | |||
</Radio> | |||
<Radio | |||
checked={false} | |||
className="big-spacer-bottom" | |||
onCheck={[MockFunction]} | |||
value="SAFE" | |||
> | |||
<StatusDescription | |||
statusOption="SAFE" | |||
/> | |||
</Radio> | |||
</div> | |||
<hr /> | |||
<div | |||
className="big-padded display-flex-column" | |||
> | |||
<label | |||
className="text-bold" | |||
htmlFor="comment-textarea" | |||
> | |||
hotspots.status.add_comment | |||
</label> | |||
<textarea | |||
className="spacer-top form-field fixed-width spacer-bottom" | |||
id="comment-textarea" | |||
onChange={[Function]} | |||
rows={4} | |||
value="TEST-COMMENT" | |||
/> | |||
<MarkdownTips /> | |||
<div | |||
className="big-spacer-top display-flex-justify-end display-flex-center" | |||
> | |||
<SubmitButton | |||
disabled={true} | |||
onClick={[MockFunction]} | |||
> | |||
hotspots.status.change_status | |||
</SubmitButton> | |||
<i | |||
className="spacer-left spinner" | |||
/> | |||
</div> | |||
</div> | |||
</Fragment> | |||
`; |
@@ -51,10 +51,3 @@ | |||
overflow-y: auto; | |||
background-color: white; | |||
} | |||
/* | |||
* Align description with label by offsetting by width of radio + margin | |||
*/ | |||
#security_hotspots .radio-button-description { | |||
margin-left: 23px; | |||
} |
@@ -20,6 +20,9 @@ | |||
import { groupBy, sortBy } from 'lodash'; | |||
import { | |||
Hotspot, | |||
HotspotResolution, | |||
HotspotStatus, | |||
HotspotStatusOption, | |||
RawHotspot, | |||
ReviewHistoryElement, | |||
ReviewHistoryType, | |||
@@ -137,3 +140,35 @@ export function getHotspotReviewHistory( | |||
functionalCount | |||
}; | |||
} | |||
const STATUS_AND_RESOLUTION_TO_STATUS_OPTION = { | |||
[HotspotStatus.TO_REVIEW]: HotspotStatusOption.TO_REVIEW, | |||
[HotspotStatus.REVIEWED]: HotspotStatusOption.FIXED, | |||
[HotspotResolution.FIXED]: HotspotStatusOption.FIXED, | |||
[HotspotResolution.SAFE]: HotspotStatusOption.SAFE | |||
}; | |||
export function getStatusOptionFromStatusAndResolution( | |||
status: HotspotStatus, | |||
resolution?: HotspotResolution | |||
) { | |||
// Resolution is the most determinist info here, so we use it first to get the matching status option | |||
// If not provided, we use the status (which will be TO_REVIEW) | |||
return STATUS_AND_RESOLUTION_TO_STATUS_OPTION[resolution ?? status]; | |||
} | |||
const STATUS_OPTION_TO_STATUS_AND_RESOLUTION_MAP = { | |||
[HotspotStatusOption.TO_REVIEW]: { status: HotspotStatus.TO_REVIEW, resolution: undefined }, | |||
[HotspotStatusOption.FIXED]: { | |||
status: HotspotStatus.REVIEWED, | |||
resolution: HotspotResolution.FIXED | |||
}, | |||
[HotspotStatusOption.SAFE]: { | |||
status: HotspotStatus.REVIEWED, | |||
resolution: HotspotResolution.SAFE | |||
} | |||
}; | |||
export function getStatusAndResolutionFromStatusOption(statusOption: HotspotStatusOption) { | |||
return STATUS_OPTION_TO_STATUS_AND_RESOLUTION_MAP[statusOption]; | |||
} |
@@ -42,7 +42,7 @@ export enum HotspotStatusFilter { | |||
export enum HotspotStatusOption { | |||
FIXED = 'FIXED', | |||
SAFE = 'SAFE', | |||
ADDITIONAL_REVIEW = 'ADDITIONAL_REVIEW' | |||
TO_REVIEW = 'TO_REVIEW' | |||
} | |||
export interface HotspotFilters { | |||
@@ -60,10 +60,10 @@ export interface RawHotspot { | |||
line?: number; | |||
message: string; | |||
project: string; | |||
resolution?: string; | |||
resolution?: HotspotResolution; | |||
rule: string; | |||
securityCategory: string; | |||
status: string; | |||
status: HotspotStatus; | |||
subProject?: string; | |||
updateDate: string; | |||
vulnerabilityProbability: RiskExposure; |
@@ -664,9 +664,6 @@ 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: | |||
hotspot.category=Category: | |||
hotspot.status=Status: | |||
hotspot.assigned_to=Assigned to: | |||
hotspots.tabs.risk_description=What's the risk? | |||
hotspots.tabs.vulnerability_description=Are you at risk? | |||
hotspots.tabs.fix_recommendations=How can you fix it? | |||
@@ -677,12 +674,17 @@ hotspots.tabs.review_history.comment.add=Add a comment | |||
hotspots.tabs.review_history.comment.field=Comment: | |||
hotspots.tabs.review_history.comment.submit=Comment | |||
hotspot.change_status.REVIEWED=Change status | |||
hotspot.change_status.TO_REVIEW=Review Hotspot | |||
hotspot.status.TO_REVIEW=To review | |||
hotspot.status.FIXED=Fixed | |||
hotspot.status.SAFE=Safe | |||
hotspots.assignee.select_user=Select a user... | |||
hotspots.status.cannot_change_status=Changing a hotspot's status requires permission. | |||
hotspots.status.select_status=Select a status... | |||
hotspots.status.add_comment=Add a comment (Optional) | |||
hotspots.status.change_status=Change status | |||
hotspots.status_option.TO_REVIEW=To review | |||
hotspots.status_option.TO_REVIEW.description=This Security Hotspot needs to be reviewed to assess whether the code poses a risk. | |||
hotspots.status_option.FIXED=Fixed | |||
hotspots.status_option.FIXED.description=The code has been modified to follow recommended secure coding practices. | |||
hotspots.status_option.SAFE=Safe | |||
hotspots.status_option.SAFE.description=The code is not at risk and doesn't need to be modified. | |||
hotspot.filters.title=Filters | |||
hotspot.filters.assignee.assigned_to_me=Assigned to me | |||
@@ -697,24 +699,6 @@ hotspot.filters.show_all=Show all hotspots | |||
hotspots.reviewed.tooltip=Percentage of Security Hotspots reviewed (fixed or safe) among all non-closed Security Hotspots. | |||
hotspots.review_hotspot=Review Hotspot | |||
hotspots.form.title=Mark Security Hotspot as: | |||
hotspots.form.title.disabled=Security Hotspot is marked as: | |||
hotspots.form.cannot_change_status=Changing a hotspot's status requires permission. | |||
hotspots.form.assign_to=Assign to: | |||
hotspots.form.select_user=Select a user... | |||
hotspots.form.comment=Comment: | |||
hotspots.form.comment.placeholder=For tracking purposes, we highly recommend explaining why the code is safe. | |||
hotspots.form.submit.TO_REVIEW=Submit Review | |||
hotspots.form.submit.REVIEWED=Apply changes | |||
hotspots.status_option.FIXED=Fixed | |||
hotspots.status_option.FIXED.description=The code has been modified to follow recommended secure coding practices. | |||
hotspots.status_option.SAFE=Safe | |||
hotspots.status_option.SAFE.description=The code is not at risk and doesn't need to be modified. | |||
hotspots.status_option.ADDITIONAL_REVIEW=Needs additional review | |||
hotspots.status_option.ADDITIONAL_REVIEW.description=Someone else needs to review this Security Hotspot. | |||
#------------------------------------------------------------------------------ | |||
# | |||
# ISSUES |