@@ -17,9 +17,17 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { getJSON } from 'sonar-ui-common/helpers/request'; | |||
import { getJSON, post } from 'sonar-ui-common/helpers/request'; | |||
import throwGlobalError from '../app/utils/throwGlobalError'; | |||
import { DetailedHotspot, HotspotSearchResponse } from '../types/security-hotspots'; | |||
import { | |||
DetailedHotspot, | |||
HotspotSearchResponse, | |||
HotspotSetStatusRequest | |||
} from '../types/security-hotspots'; | |||
export function setSecurityHotspotStatus(data: HotspotSetStatusRequest): Promise<void> { | |||
return post('/api/hotspots/change_status', data).catch(throwGlobalError); | |||
} | |||
export function getSecurityHotspots(data: { | |||
projectKey: string; |
@@ -42,6 +42,7 @@ export interface SecurityHotspotsAppRendererProps { | |||
export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRendererProps) { | |||
const { hotspots, loading, securityCategories, selectedHotspotKey } = props; | |||
return ( | |||
<div id="security_hotspots"> | |||
<FilterBar /> |
@@ -0,0 +1,68 @@ | |||
/* | |||
* 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 HotspotActionsForm from './HotspotActionsForm'; | |||
export interface HotspotActionsProps { | |||
hotspotKey: string; | |||
} | |||
const ESCAPE_KEY = 'Escape'; | |||
export default function HotspotActions(props: HotspotActionsProps) { | |||
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"> | |||
<Button onClick={() => setOpen(!open)}> | |||
{translate('hotspots.review_hotspot')} | |||
<DropdownIcon className="little-spacer-left" /> | |||
</Button> | |||
{open && ( | |||
<OutsideClickHandler onClickOutside={() => setOpen(false)}> | |||
<DropdownOverlay placement={PopupPlacement.BottomRight}> | |||
<HotspotActionsForm hotspotKey={props.hotspotKey} onSubmit={() => setOpen(false)} /> | |||
</DropdownOverlay> | |||
</OutsideClickHandler> | |||
)} | |||
</div> | |||
); | |||
} |
@@ -0,0 +1,89 @@ | |||
/* | |||
* 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 { | |||
HotspotResolution, | |||
HotspotSetStatusRequest, | |||
HotspotStatus, | |||
HotspotStatusOptions | |||
} from '../../../types/security-hotspots'; | |||
import HotspotActionsFormRenderer from './HotspotActionsFormRenderer'; | |||
interface Props { | |||
hotspotKey: string; | |||
onSubmit: () => void; | |||
} | |||
interface State { | |||
selectedOption: HotspotStatusOptions; | |||
submitting: boolean; | |||
} | |||
export default class HotspotActionsForm extends React.Component<Props, State> { | |||
state: State = { | |||
selectedOption: HotspotStatusOptions.FIXED, | |||
submitting: false | |||
}; | |||
handleSelectOption = (selectedOption: HotspotStatusOptions) => { | |||
this.setState({ selectedOption }); | |||
}; | |||
handleSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => { | |||
event.preventDefault(); | |||
const { hotspotKey } = this.props; | |||
const { selectedOption } = this.state; | |||
const status = | |||
selectedOption === HotspotStatusOptions.ADDITIONAL_REVIEW | |||
? HotspotStatus.TO_REVIEW | |||
: HotspotStatus.REVIEWED; | |||
const data: HotspotSetStatusRequest = { hotspot: hotspotKey, status }; | |||
if (selectedOption !== HotspotStatusOptions.ADDITIONAL_REVIEW) { | |||
data.resolution = HotspotResolution[selectedOption]; | |||
} | |||
this.setState({ submitting: true }); | |||
return setSecurityHotspotStatus(data) | |||
.then(() => { | |||
this.props.onSubmit(); | |||
}) | |||
.finally(() => { | |||
this.setState({ submitting: false }); | |||
}); | |||
}; | |||
render() { | |||
const { hotspotKey } = this.props; | |||
const { selectedOption, submitting } = this.state; | |||
return ( | |||
<HotspotActionsFormRenderer | |||
hotspotKey={hotspotKey} | |||
onSelectOption={this.handleSelectOption} | |||
onSubmit={this.handleSubmit} | |||
selectedOption={selectedOption} | |||
submitting={submitting} | |||
/> | |||
); | |||
} | |||
} |
@@ -0,0 +1,81 @@ | |||
/* | |||
* 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 { HotspotStatusOptions } from '../../../types/security-hotspots'; | |||
export interface HotspotActionsFormRendererProps { | |||
hotspotKey: string; | |||
onSelectOption: (option: HotspotStatusOptions) => void; | |||
onSubmit: (event: React.SyntheticEvent<HTMLFormElement>) => void; | |||
selectedOption: HotspotStatusOptions; | |||
submitting: boolean; | |||
} | |||
export default function HotspotActionsFormRenderer(props: HotspotActionsFormRendererProps) { | |||
const { selectedOption, submitting } = props; | |||
return ( | |||
<form className="abs-width-400" onSubmit={props.onSubmit}> | |||
<h2>{translate('hotspots.form.title')}</h2> | |||
<div className="display-flex-column big-spacer-bottom"> | |||
{renderOption({ | |||
option: HotspotStatusOptions.FIXED, | |||
selectedOption, | |||
onClick: props.onSelectOption | |||
})} | |||
{renderOption({ | |||
option: HotspotStatusOptions.SAFE, | |||
selectedOption, | |||
onClick: props.onSelectOption | |||
})} | |||
{renderOption({ | |||
option: HotspotStatusOptions.ADDITIONAL_REVIEW, | |||
selectedOption, | |||
onClick: props.onSelectOption | |||
})} | |||
</div> | |||
<div className="text-right"> | |||
{submitting && <i className="spinner spacer-right" />} | |||
<SubmitButton disabled={submitting}>{translate('hotspots.form.submit')}</SubmitButton> | |||
</div> | |||
</form> | |||
); | |||
} | |||
function renderOption(params: { | |||
option: HotspotStatusOptions; | |||
onClick: (option: HotspotStatusOptions) => void; | |||
selectedOption: HotspotStatusOptions; | |||
}) { | |||
const { onClick, option, selectedOption } = params; | |||
return ( | |||
<div className="big-spacer-top"> | |||
<Radio checked={selectedOption === option} onCheck={onClick} value={option}> | |||
<h3>{translate('hotspots.status_option', option)}</h3> | |||
</Radio> | |||
<div className="radio-button-description"> | |||
{translate('hotspots.status_option', option, 'description')} | |||
</div> | |||
</div> | |||
); | |||
} |
@@ -20,24 +20,31 @@ | |||
import * as React from 'react'; | |||
import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner'; | |||
import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n'; | |||
import { withCurrentUser } from '../../../components/hoc/withCurrentUser'; | |||
import { isLoggedIn } from '../../../helpers/users'; | |||
import { DetailedHotspot } from '../../../types/security-hotspots'; | |||
import HotspotActions from './HotspotActions'; | |||
import HotspotViewerTabs from './HotspotViewerTabs'; | |||
export interface HotspotViewerRendererProps { | |||
currentUser: T.CurrentUser; | |||
hotspot?: DetailedHotspot; | |||
loading: boolean; | |||
securityCategories: T.StandardSecurityCategories; | |||
} | |||
export default function HotspotViewerRenderer(props: HotspotViewerRendererProps) { | |||
const { hotspot, loading, securityCategories } = props; | |||
export function HotspotViewerRenderer(props: HotspotViewerRendererProps) { | |||
const { currentUser, hotspot, loading, securityCategories } = props; | |||
return ( | |||
<DeferredSpinner loading={loading}> | |||
{hotspot && ( | |||
<div className="big-padded"> | |||
<div className="big-spacer-bottom"> | |||
<h1>{hotspot.message}</h1> | |||
<div className="display-flex-space-between"> | |||
<h1>{hotspot.message}</h1> | |||
{isLoggedIn(currentUser) && <HotspotActions hotspotKey={hotspot.key} />} | |||
</div> | |||
<div className="text-muted"> | |||
<span>{translate('hotspot.category')}</span> | |||
<span className="little-spacer-left"> | |||
@@ -67,3 +74,5 @@ export default function HotspotViewerRenderer(props: HotspotViewerRendererProps) | |||
</DeferredSpinner> | |||
); | |||
} | |||
export default withCurrentUser(HotspotViewerRenderer); |
@@ -0,0 +1,70 @@ | |||
/* | |||
* 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 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 hotspotKey="key" {...props} />); | |||
} |
@@ -0,0 +1,101 @@ | |||
/* | |||
* 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 { | |||
HotspotResolution, | |||
HotspotStatus, | |||
HotspotStatusOptions | |||
} from '../../../../types/security-hotspots'; | |||
import HotspotActionsForm from '../HotspotActionsForm'; | |||
jest.mock('../../../../api/security-hotspots', () => ({ | |||
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(HotspotStatusOptions.FIXED); | |||
wrapper.instance().handleSelectOption(HotspotStatusOptions.SAFE); | |||
expect(wrapper.state().selectedOption).toBe(HotspotStatusOptions.SAFE); | |||
}); | |||
it('should handle submit', async () => { | |||
const onSubmit = jest.fn(); | |||
const wrapper = shallowRender({ onSubmit }); | |||
wrapper.setState({ selectedOption: HotspotStatusOptions.ADDITIONAL_REVIEW }); | |||
await waitAndUpdate(wrapper); | |||
const preventDefault = jest.fn(); | |||
const promise = wrapper.instance().handleSubmit({ preventDefault } as any); | |||
expect(preventDefault).toBeCalled(); | |||
expect(wrapper.state().submitting).toBe(true); | |||
await promise; | |||
expect(wrapper.state().submitting).toBe(false); | |||
expect(setSecurityHotspotStatus).toBeCalledWith({ | |||
hotspot: 'key', | |||
status: HotspotStatus.TO_REVIEW | |||
}); | |||
expect(onSubmit).toBeCalled(); | |||
// SAFE | |||
wrapper.setState({ selectedOption: HotspotStatusOptions.SAFE }); | |||
await waitAndUpdate(wrapper); | |||
await wrapper.instance().handleSubmit({ preventDefault } as any); | |||
expect(setSecurityHotspotStatus).toBeCalledWith({ | |||
hotspot: 'key', | |||
status: HotspotStatus.REVIEWED, | |||
resolution: HotspotResolution.SAFE | |||
}); | |||
// FIXED | |||
wrapper.setState({ selectedOption: HotspotStatusOptions.FIXED }); | |||
await waitAndUpdate(wrapper); | |||
await wrapper.instance().handleSubmit({ preventDefault } as any); | |||
expect(setSecurityHotspotStatus).toBeCalledWith({ | |||
hotspot: 'key', | |||
status: HotspotStatus.REVIEWED, | |||
resolution: HotspotResolution.FIXED | |||
}); | |||
}); | |||
it('should handle submit failure', async () => { | |||
const onSubmit = jest.fn(); | |||
(setSecurityHotspotStatus as jest.Mock).mockRejectedValueOnce('failure'); | |||
const wrapper = shallowRender({ onSubmit }); | |||
const promise = wrapper.instance().handleSubmit({ preventDefault: jest.fn() } as any); | |||
expect(wrapper.state().submitting).toBe(true); | |||
await promise.catch(() => {}); | |||
expect(wrapper.state().submitting).toBe(false); | |||
expect(onSubmit).not.toBeCalled(); | |||
}); | |||
function shallowRender(props: Partial<HotspotActionsForm['props']> = {}) { | |||
return shallow<HotspotActionsForm>( | |||
<HotspotActionsForm hotspotKey="key" onSubmit={jest.fn()} {...props} /> | |||
); | |||
} |
@@ -0,0 +1,47 @@ | |||
/* | |||
* 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 { HotspotStatusOptions } 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: HotspotStatusOptions.SAFE })).toMatchSnapshot( | |||
'safe option selected' | |||
); | |||
}); | |||
function shallowRender(props: Partial<HotspotActionsFormRendererProps> = {}) { | |||
return shallow<HotspotActionsForm>( | |||
<HotspotActionsFormRenderer | |||
hotspotKey="key" | |||
onSelectOption={jest.fn()} | |||
onSubmit={jest.fn()} | |||
selectedOption={HotspotStatusOptions.FIXED} | |||
submitting={false} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -20,8 +20,8 @@ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { mockDetailledHotspot } from '../../../../helpers/mocks/security-hotspots'; | |||
import { mockUser } from '../../../../helpers/testMocks'; | |||
import HotspotViewerRenderer, { HotspotViewerRendererProps } from '../HotspotViewerRenderer'; | |||
import { mockCurrentUser, mockLoggedInUser, mockUser } from '../../../../helpers/testMocks'; | |||
import { HotspotViewerRenderer, HotspotViewerRendererProps } from '../HotspotViewerRenderer'; | |||
it('should render correctly', () => { | |||
const wrapper = shallowRender(); | |||
@@ -30,11 +30,14 @@ it('should render correctly', () => { | |||
expect( | |||
shallowRender({ hotspot: mockDetailledHotspot({ assignee: mockUser({ active: false }) }) }) | |||
).toMatchSnapshot('deleted assignee'); | |||
expect(shallowRender()).toMatchSnapshot('anonymous user'); | |||
expect(shallowRender({ currentUser: mockLoggedInUser() })).toMatchSnapshot('user logged in'); | |||
}); | |||
function shallowRender(props?: Partial<HotspotViewerRendererProps>) { | |||
return shallow( | |||
<HotspotViewerRenderer | |||
currentUser={mockCurrentUser()} | |||
hotspot={mockDetailledHotspot()} | |||
loading={false} | |||
securityCategories={{ 'sql-injection': { title: 'SQL injection' } }} |
@@ -0,0 +1,112 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should open when clicked 1`] = ` | |||
<div | |||
className="dropdown" | |||
> | |||
<Button | |||
onClick={[Function]} | |||
> | |||
hotspots.review_hotspot | |||
<DropdownIcon | |||
className="little-spacer-left" | |||
/> | |||
</Button> | |||
<OutsideClickHandler | |||
onClickOutside={[Function]} | |||
> | |||
<DropdownOverlay | |||
placement="bottom-right" | |||
> | |||
<HotspotActionsForm | |||
hotspotKey="key" | |||
onSubmit={[Function]} | |||
/> | |||
</DropdownOverlay> | |||
</OutsideClickHandler> | |||
</div> | |||
`; | |||
exports[`should register an eventlistener: Dropdown closed 1`] = ` | |||
<div | |||
className="dropdown" | |||
> | |||
<Button | |||
onClick={[Function]} | |||
> | |||
hotspots.review_hotspot | |||
<DropdownIcon | |||
className="little-spacer-left" | |||
/> | |||
</Button> | |||
</div> | |||
`; | |||
exports[`should register an eventlistener: Dropdown open 1`] = ` | |||
<div | |||
className="dropdown" | |||
> | |||
<Button | |||
onClick={[Function]} | |||
> | |||
hotspots.review_hotspot | |||
<DropdownIcon | |||
className="little-spacer-left" | |||
/> | |||
</Button> | |||
<OutsideClickHandler | |||
onClickOutside={[Function]} | |||
> | |||
<DropdownOverlay | |||
placement="bottom-right" | |||
> | |||
<HotspotActionsForm | |||
hotspotKey="key" | |||
onSubmit={[Function]} | |||
/> | |||
</DropdownOverlay> | |||
</OutsideClickHandler> | |||
</div> | |||
`; | |||
exports[`should register an eventlistener: Dropdown still open 1`] = ` | |||
<div | |||
className="dropdown" | |||
> | |||
<Button | |||
onClick={[Function]} | |||
> | |||
hotspots.review_hotspot | |||
<DropdownIcon | |||
className="little-spacer-left" | |||
/> | |||
</Button> | |||
<OutsideClickHandler | |||
onClickOutside={[Function]} | |||
> | |||
<DropdownOverlay | |||
placement="bottom-right" | |||
> | |||
<HotspotActionsForm | |||
hotspotKey="key" | |||
onSubmit={[Function]} | |||
/> | |||
</DropdownOverlay> | |||
</OutsideClickHandler> | |||
</div> | |||
`; | |||
exports[`should render correctly 1`] = ` | |||
<div | |||
className="dropdown" | |||
> | |||
<Button | |||
onClick={[Function]} | |||
> | |||
hotspots.review_hotspot | |||
<DropdownIcon | |||
className="little-spacer-left" | |||
/> | |||
</Button> | |||
</div> | |||
`; |
@@ -0,0 +1,11 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly 1`] = ` | |||
<HotspotActionsFormRenderer | |||
hotspotKey="key" | |||
onSelectOption={[Function]} | |||
onSubmit={[Function]} | |||
selectedOption="FIXED" | |||
submitting={false} | |||
/> | |||
`; |
@@ -0,0 +1,238 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly 1`] = ` | |||
<form | |||
className="abs-width-400" | |||
onSubmit={[MockFunction]} | |||
> | |||
<h2> | |||
hotspots.form.title | |||
</h2> | |||
<div | |||
className="display-flex-column big-spacer-bottom" | |||
> | |||
<div | |||
className="big-spacer-top" | |||
> | |||
<Radio | |||
checked={true} | |||
onCheck={[MockFunction]} | |||
value="FIXED" | |||
> | |||
<h3> | |||
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} | |||
onCheck={[MockFunction]} | |||
value="SAFE" | |||
> | |||
<h3> | |||
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} | |||
onCheck={[MockFunction]} | |||
value="ADDITIONAL_REVIEW" | |||
> | |||
<h3> | |||
hotspots.status_option.ADDITIONAL_REVIEW | |||
</h3> | |||
</Radio> | |||
<div | |||
className="radio-button-description" | |||
> | |||
hotspots.status_option.ADDITIONAL_REVIEW.description | |||
</div> | |||
</div> | |||
</div> | |||
<div | |||
className="text-right" | |||
> | |||
<SubmitButton | |||
disabled={false} | |||
> | |||
hotspots.form.submit | |||
</SubmitButton> | |||
</div> | |||
</form> | |||
`; | |||
exports[`should render correctly: Submitting 1`] = ` | |||
<form | |||
className="abs-width-400" | |||
onSubmit={[MockFunction]} | |||
> | |||
<h2> | |||
hotspots.form.title | |||
</h2> | |||
<div | |||
className="display-flex-column big-spacer-bottom" | |||
> | |||
<div | |||
className="big-spacer-top" | |||
> | |||
<Radio | |||
checked={true} | |||
onCheck={[MockFunction]} | |||
value="FIXED" | |||
> | |||
<h3> | |||
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} | |||
onCheck={[MockFunction]} | |||
value="SAFE" | |||
> | |||
<h3> | |||
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} | |||
onCheck={[MockFunction]} | |||
value="ADDITIONAL_REVIEW" | |||
> | |||
<h3> | |||
hotspots.status_option.ADDITIONAL_REVIEW | |||
</h3> | |||
</Radio> | |||
<div | |||
className="radio-button-description" | |||
> | |||
hotspots.status_option.ADDITIONAL_REVIEW.description | |||
</div> | |||
</div> | |||
</div> | |||
<div | |||
className="text-right" | |||
> | |||
<i | |||
className="spinner spacer-right" | |||
/> | |||
<SubmitButton | |||
disabled={true} | |||
> | |||
hotspots.form.submit | |||
</SubmitButton> | |||
</div> | |||
</form> | |||
`; | |||
exports[`should render correctly: safe option selected 1`] = ` | |||
<form | |||
className="abs-width-400" | |||
onSubmit={[MockFunction]} | |||
> | |||
<h2> | |||
hotspots.form.title | |||
</h2> | |||
<div | |||
className="display-flex-column big-spacer-bottom" | |||
> | |||
<div | |||
className="big-spacer-top" | |||
> | |||
<Radio | |||
checked={false} | |||
onCheck={[MockFunction]} | |||
value="FIXED" | |||
> | |||
<h3> | |||
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} | |||
onCheck={[MockFunction]} | |||
value="SAFE" | |||
> | |||
<h3> | |||
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} | |||
onCheck={[MockFunction]} | |||
value="ADDITIONAL_REVIEW" | |||
> | |||
<h3> | |||
hotspots.status_option.ADDITIONAL_REVIEW | |||
</h3> | |||
</Radio> | |||
<div | |||
className="radio-button-description" | |||
> | |||
hotspots.status_option.ADDITIONAL_REVIEW.description | |||
</div> | |||
</div> | |||
</div> | |||
<div | |||
className="text-right" | |||
> | |||
<SubmitButton | |||
disabled={false} | |||
> | |||
hotspots.form.submit | |||
</SubmitButton> | |||
</div> | |||
</form> | |||
`; |
@@ -1,7 +1,7 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly 1`] = ` | |||
<HotspotViewerRenderer | |||
<Connect(withCurrentUser(HotspotViewerRenderer)) | |||
loading={true} | |||
securityCategories={ | |||
Object { | |||
@@ -14,7 +14,7 @@ exports[`should render correctly 1`] = ` | |||
`; | |||
exports[`should render correctly 2`] = ` | |||
<HotspotViewerRenderer | |||
<Connect(withCurrentUser(HotspotViewerRenderer)) | |||
hotspot={ | |||
Object { | |||
"id": "I am a detailled hotspot", |
@@ -11,9 +11,152 @@ exports[`should render correctly 1`] = ` | |||
<div | |||
className="big-spacer-bottom" | |||
> | |||
<h1> | |||
'3' is a magic number. | |||
</h1> | |||
<div | |||
className="display-flex-space-between" | |||
> | |||
<h1> | |||
'3' is a magic number. | |||
</h1> | |||
</div> | |||
<div | |||
className="text-muted" | |||
> | |||
<span> | |||
hotspot.category | |||
</span> | |||
<span | |||
className="little-spacer-left" | |||
> | |||
SQL injection | |||
</span> | |||
</div> | |||
</div> | |||
<div | |||
className="huge-spacer-bottom" | |||
> | |||
<span> | |||
hotspot.status | |||
</span> | |||
<span | |||
className="badge little-spacer-left" | |||
> | |||
issue.status.RESOLVED | |||
</span> | |||
<span | |||
className="huge-spacer-left" | |||
> | |||
hotspot.assigned_to | |||
</span> | |||
<strong | |||
className="little-spacer-left" | |||
> | |||
John Doe | |||
</strong> | |||
</div> | |||
<HotspotViewerTabs | |||
hotspot={ | |||
Object { | |||
"assignee": Object { | |||
"active": true, | |||
"local": true, | |||
"login": "john.doe", | |||
"name": "John Doe", | |||
}, | |||
"author": Object { | |||
"active": true, | |||
"local": true, | |||
"login": "john.doe", | |||
"name": "John Doe", | |||
}, | |||
"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": "FALSE-POSITIVE", | |||
"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": "RESOLVED", | |||
"textRange": Object { | |||
"endLine": 142, | |||
"endOffset": 83, | |||
"startLine": 142, | |||
"startOffset": 26, | |||
}, | |||
"updateDate": "2013-05-13T17:55:42+0200", | |||
} | |||
} | |||
/> | |||
</div> | |||
</DeferredSpinner> | |||
`; | |||
exports[`should render correctly: anonymous user 1`] = ` | |||
<DeferredSpinner | |||
loading={false} | |||
timeout={100} | |||
> | |||
<div | |||
className="big-padded" | |||
> | |||
<div | |||
className="big-spacer-bottom" | |||
> | |||
<div | |||
className="display-flex-space-between" | |||
> | |||
<h1> | |||
'3' is a magic number. | |||
</h1> | |||
</div> | |||
<div | |||
className="text-muted" | |||
> | |||
@@ -146,9 +289,13 @@ exports[`should render correctly: deleted assignee 1`] = ` | |||
<div | |||
className="big-spacer-bottom" | |||
> | |||
<h1> | |||
'3' is a magic number. | |||
</h1> | |||
<div | |||
className="display-flex-space-between" | |||
> | |||
<h1> | |||
'3' is a magic number. | |||
</h1> | |||
</div> | |||
<div | |||
className="text-muted" | |||
> | |||
@@ -276,3 +423,145 @@ exports[`should render correctly: no hotspot 1`] = ` | |||
timeout={100} | |||
/> | |||
`; | |||
exports[`should render correctly: user logged in 1`] = ` | |||
<DeferredSpinner | |||
loading={false} | |||
timeout={100} | |||
> | |||
<div | |||
className="big-padded" | |||
> | |||
<div | |||
className="big-spacer-bottom" | |||
> | |||
<div | |||
className="display-flex-space-between" | |||
> | |||
<h1> | |||
'3' is a magic number. | |||
</h1> | |||
<HotspotActions | |||
hotspotKey="01fc972e-2a3c-433e-bcae-0bd7f88f5123" | |||
/> | |||
</div> | |||
<div | |||
className="text-muted" | |||
> | |||
<span> | |||
hotspot.category | |||
</span> | |||
<span | |||
className="little-spacer-left" | |||
> | |||
SQL injection | |||
</span> | |||
</div> | |||
</div> | |||
<div | |||
className="huge-spacer-bottom" | |||
> | |||
<span> | |||
hotspot.status | |||
</span> | |||
<span | |||
className="badge little-spacer-left" | |||
> | |||
issue.status.RESOLVED | |||
</span> | |||
<span | |||
className="huge-spacer-left" | |||
> | |||
hotspot.assigned_to | |||
</span> | |||
<strong | |||
className="little-spacer-left" | |||
> | |||
John Doe | |||
</strong> | |||
</div> | |||
<HotspotViewerTabs | |||
hotspot={ | |||
Object { | |||
"assignee": Object { | |||
"active": true, | |||
"local": true, | |||
"login": "john.doe", | |||
"name": "John Doe", | |||
}, | |||
"author": Object { | |||
"active": true, | |||
"local": true, | |||
"login": "john.doe", | |||
"name": "John Doe", | |||
}, | |||
"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": "FALSE-POSITIVE", | |||
"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": "RESOLVED", | |||
"textRange": Object { | |||
"endLine": 142, | |||
"endOffset": 83, | |||
"startLine": 142, | |||
"startOffset": 26, | |||
}, | |||
"updateDate": "2013-05-13T17:55:42+0200", | |||
} | |||
} | |||
/> | |||
</div> | |||
</DeferredSpinner> | |||
`; |
@@ -50,3 +50,10 @@ | |||
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; | |||
} |
@@ -23,6 +23,22 @@ export enum RiskExposure { | |||
HIGH = 'HIGH' | |||
} | |||
export enum HotspotStatus { | |||
TO_REVIEW = 'TO_REVIEW', | |||
REVIEWED = 'REVIEWED' | |||
} | |||
export enum HotspotResolution { | |||
FIXED = 'FIXED', | |||
SAFE = 'SAFE' | |||
} | |||
export enum HotspotStatusOptions { | |||
FIXED = 'FIXED', | |||
SAFE = 'SAFE', | |||
ADDITIONAL_REVIEW = 'ADDITIONAL_REVIEW' | |||
} | |||
export interface RawHotspot { | |||
assignee?: string; | |||
author?: string; | |||
@@ -72,3 +88,9 @@ export interface HotspotSearchResponse { | |||
hotspots: RawHotspot[]; | |||
paging: T.Paging; | |||
} | |||
export interface HotspotSetStatusRequest { | |||
hotspot: string; | |||
status: HotspotStatus; | |||
resolution?: HotspotResolution; | |||
} |
@@ -658,6 +658,21 @@ hotspot.assigned_to=Assigned to: | |||
hotspot.tabs.risk_description=What's the risk? | |||
hotspot.tabs.vulnerability_description=Are you vulnerable? | |||
hotspot.tabs.fix_recommendations=How can you fix it? | |||
hotspots.review_hotspot=Review Hotspot | |||
hotspots.form.title=Mark Security Hotspot as: | |||
hotspots.form.assign_to=Assign to: | |||
hotspots.form.select_user=Select a user... | |||
hotspots.form.comment=Comment | |||
hotspots.form.submit=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. | |||
#------------------------------------------------------------------------------ | |||
# |