} | } | ||||
.padded { | .padded { | ||||
padding: var(--gridSize); | |||||
padding: var(--gridSize) !important; | |||||
} | } | ||||
.big-padded { | .big-padded { |
small, | small, | ||||
.small { | .small { | ||||
font-size: var(--smallFontSize); | |||||
font-size: var(--smallFontSize) !important; | |||||
} | } | ||||
.medium { | .medium { | ||||
font-size: var(--mediumFontSize); | |||||
font-size: var(--mediumFontSize) !important; | |||||
} | } | ||||
.big { | .big { | ||||
font-size: var(--bigFontSize); | |||||
font-size: var(--bigFontSize) !important; | |||||
} | } | ||||
.huge { | .huge { | ||||
font-size: var(--hugeFontSize); | |||||
font-size: var(--hugeFontSize) !important; | |||||
} | } | ||||
.gigantic { | .gigantic { | ||||
font-size: var(--giganticFontSize); | |||||
font-size: var(--giganticFontSize) !important; | |||||
} | } | ||||
.zero-font-size { | .zero-font-size { |
); | ); | ||||
}; | }; | ||||
handleChangeStatusFilter = (status: HotspotStatusFilter) => { | |||||
this.handleChangeFilters({ status }); | |||||
}; | |||||
handleHotspotClick = (selectedHotspot: RawHotspot) => this.setState({ selectedHotspot }); | handleHotspotClick = (selectedHotspot: RawHotspot) => this.setState({ selectedHotspot }); | ||||
handleHotspotUpdate = (hotspotKey: string) => { | handleHotspotUpdate = (hotspotKey: string) => { | ||||
onHotspotClick={this.handleHotspotClick} | onHotspotClick={this.handleHotspotClick} | ||||
onLoadMore={this.handleLoadMore} | onLoadMore={this.handleLoadMore} | ||||
onShowAllHotspots={this.handleShowAllHotspots} | onShowAllHotspots={this.handleShowAllHotspots} | ||||
onSwitchStatusFilter={this.handleChangeStatusFilter} | |||||
onUpdateHotspot={this.handleHotspotUpdate} | onUpdateHotspot={this.handleHotspotUpdate} | ||||
securityCategories={standards[SecurityStandard.SONARSOURCE]} | securityCategories={standards[SecurityStandard.SONARSOURCE]} | ||||
selectedHotspot={selectedHotspot} | selectedHotspot={selectedHotspot} |
onHotspotClick: (hotspot: RawHotspot) => void; | onHotspotClick: (hotspot: RawHotspot) => void; | ||||
onLoadMore: () => void; | onLoadMore: () => void; | ||||
onShowAllHotspots: () => void; | onShowAllHotspots: () => void; | ||||
onSwitchStatusFilter: (option: HotspotStatusFilter) => void; | |||||
onUpdateHotspot: (hotspotKey: string) => Promise<void>; | onUpdateHotspot: (hotspotKey: string) => Promise<void>; | ||||
selectedHotspot: RawHotspot | undefined; | selectedHotspot: RawHotspot | undefined; | ||||
securityCategories: T.StandardSecurityCategories; | securityCategories: T.StandardSecurityCategories; | ||||
branchLike={branchLike} | branchLike={branchLike} | ||||
component={component} | component={component} | ||||
hotspotKey={selectedHotspot.key} | hotspotKey={selectedHotspot.key} | ||||
hotspotsReviewedMeasure={hotspotsReviewedMeasure} | |||||
onSwitchStatusFilter={props.onSwitchStatusFilter} | |||||
onUpdateHotspot={props.onUpdateHotspot} | onUpdateHotspot={props.onUpdateHotspot} | ||||
securityCategories={securityCategories} | securityCategories={securityCategories} | ||||
/> | /> |
expect(wrapper.state().hotspots[0]).toBe(hotspots2[0]); | expect(wrapper.state().hotspots[0]).toBe(hotspots2[0]); | ||||
// Set filter to FIXED | |||||
wrapper.instance().handleChangeFilters({ status: HotspotStatusFilter.FIXED }); | |||||
// Set filter to FIXED (use the other method to check this one): | |||||
wrapper.instance().handleChangeStatusFilter(HotspotStatusFilter.FIXED); | |||||
expect(getSecurityHotspots).toBeCalledWith( | expect(getSecurityHotspots).toBeCalledWith( | ||||
expect.objectContaining({ status: HotspotStatus.REVIEWED, resolution: HotspotResolution.FIXED }) | expect.objectContaining({ status: HotspotStatus.REVIEWED, resolution: HotspotResolution.FIXED }) |
onHotspotClick={jest.fn()} | onHotspotClick={jest.fn()} | ||||
onLoadMore={jest.fn()} | onLoadMore={jest.fn()} | ||||
onShowAllHotspots={jest.fn()} | onShowAllHotspots={jest.fn()} | ||||
onSwitchStatusFilter={jest.fn()} | |||||
onUpdateHotspot={jest.fn()} | onUpdateHotspot={jest.fn()} | ||||
securityCategories={{}} | securityCategories={{}} | ||||
selectedHotspot={undefined} | selectedHotspot={undefined} |
onHotspotClick={[Function]} | onHotspotClick={[Function]} | ||||
onLoadMore={[Function]} | onLoadMore={[Function]} | ||||
onShowAllHotspots={[Function]} | onShowAllHotspots={[Function]} | ||||
onSwitchStatusFilter={[Function]} | |||||
onUpdateHotspot={[Function]} | onUpdateHotspot={[Function]} | ||||
securityCategories={Object {}} | securityCategories={Object {}} | ||||
standards={ | standards={ |
import { | import { | ||||
HotspotResolution, | HotspotResolution, | ||||
HotspotStatus, | HotspotStatus, | ||||
HotspotStatusFilter, | |||||
HotspotStatusOption, | HotspotStatusOption, | ||||
ReviewHistoryType, | ReviewHistoryType, | ||||
RiskExposure | RiskExposure | ||||
import { | import { | ||||
getHotspotReviewHistory, | getHotspotReviewHistory, | ||||
getStatusAndResolutionFromStatusOption, | getStatusAndResolutionFromStatusOption, | ||||
getStatusFilterFromStatusOption, | |||||
getStatusOptionFromStatusAndResolution, | getStatusOptionFromStatusAndResolution, | ||||
groupByCategory, | groupByCategory, | ||||
mapRules, | mapRules, | ||||
}); | }); | ||||
}); | }); | ||||
}); | }); | ||||
describe('getStatusFilterFromStatusOption', () => { | |||||
it('should return the correct values', () => { | |||||
expect(getStatusFilterFromStatusOption(HotspotStatusOption.TO_REVIEW)).toEqual( | |||||
HotspotStatusFilter.TO_REVIEW | |||||
); | |||||
expect(getStatusFilterFromStatusOption(HotspotStatusOption.SAFE)).toEqual( | |||||
HotspotStatusFilter.SAFE | |||||
); | |||||
expect(getStatusFilterFromStatusOption(HotspotStatusOption.FIXED)).toEqual( | |||||
HotspotStatusFilter.FIXED | |||||
); | |||||
}); | |||||
}); |
import { getSecurityHotspotDetails } from '../../../api/security-hotspots'; | import { getSecurityHotspotDetails } from '../../../api/security-hotspots'; | ||||
import { scrollToElement } from '../../../helpers/scrolling'; | import { scrollToElement } from '../../../helpers/scrolling'; | ||||
import { BranchLike } from '../../../types/branch-like'; | import { BranchLike } from '../../../types/branch-like'; | ||||
import { Hotspot } from '../../../types/security-hotspots'; | |||||
import { | |||||
Hotspot, | |||||
HotspotStatusFilter, | |||||
HotspotStatusOption | |||||
} from '../../../types/security-hotspots'; | |||||
import { getStatusFilterFromStatusOption } from '../utils'; | |||||
import HotspotViewerRenderer from './HotspotViewerRenderer'; | import HotspotViewerRenderer from './HotspotViewerRenderer'; | ||||
interface Props { | interface Props { | ||||
branchLike?: BranchLike; | branchLike?: BranchLike; | ||||
component: T.Component; | component: T.Component; | ||||
hotspotKey: string; | hotspotKey: string; | ||||
hotspotsReviewedMeasure?: string; | |||||
onSwitchStatusFilter: (option: HotspotStatusFilter) => void; | |||||
onUpdateHotspot: (hotspotKey: string) => Promise<void>; | onUpdateHotspot: (hotspotKey: string) => Promise<void>; | ||||
securityCategories: T.StandardSecurityCategories; | securityCategories: T.StandardSecurityCategories; | ||||
} | } | ||||
interface State { | interface State { | ||||
hotspot?: Hotspot; | hotspot?: Hotspot; | ||||
lastStatusChangedTo?: HotspotStatusOption; | |||||
loading: boolean; | loading: boolean; | ||||
commentVisible: boolean; | commentVisible: boolean; | ||||
showStatusUpdateSuccessModal: boolean; | |||||
} | } | ||||
export default class HotspotViewer extends React.PureComponent<Props, State> { | export default class HotspotViewer extends React.PureComponent<Props, State> { | ||||
constructor(props: Props) { | constructor(props: Props) { | ||||
super(props); | super(props); | ||||
this.commentTextRef = React.createRef<HTMLTextAreaElement>(); | this.commentTextRef = React.createRef<HTMLTextAreaElement>(); | ||||
this.state = { loading: false, commentVisible: false }; | |||||
this.state = { loading: false, commentVisible: false, showStatusUpdateSuccessModal: false }; | |||||
} | } | ||||
componentDidMount() { | componentDidMount() { | ||||
.catch(() => this.mounted && this.setState({ loading: false })); | .catch(() => this.mounted && this.setState({ loading: false })); | ||||
}; | }; | ||||
handleHotspotUpdate = async (statusUpdate = false) => { | |||||
handleHotspotUpdate = async (statusUpdate = false, statusOption?: HotspotStatusOption) => { | |||||
const { hotspotKey } = this.props; | const { hotspotKey } = this.props; | ||||
if (statusUpdate) { | if (statusUpdate) { | ||||
this.setState({ lastStatusChangedTo: statusOption, showStatusUpdateSuccessModal: true }); | |||||
await this.props.onUpdateHotspot(hotspotKey); | await this.props.onUpdateHotspot(hotspotKey); | ||||
} else { | } else { | ||||
await this.fetchHotspot(); | await this.fetchHotspot(); | ||||
this.setState({ commentVisible: false }); | this.setState({ commentVisible: false }); | ||||
}; | }; | ||||
handleSwitchFilterToStatusOfUpdatedHotspot = () => { | |||||
const { lastStatusChangedTo } = this.state; | |||||
if (lastStatusChangedTo) { | |||||
this.props.onSwitchStatusFilter(getStatusFilterFromStatusOption(lastStatusChangedTo)); | |||||
} | |||||
}; | |||||
handleCloseStatusUpdateSuccessModal = () => { | |||||
this.setState({ showStatusUpdateSuccessModal: false }); | |||||
}; | |||||
render() { | render() { | ||||
const { branchLike, component, securityCategories } = this.props; | |||||
const { hotspot, loading, commentVisible } = this.state; | |||||
const { branchLike, component, hotspotsReviewedMeasure, securityCategories } = this.props; | |||||
const { | |||||
hotspot, | |||||
lastStatusChangedTo, | |||||
loading, | |||||
commentVisible, | |||||
showStatusUpdateSuccessModal | |||||
} = this.state; | |||||
return ( | return ( | ||||
<HotspotViewerRenderer | <HotspotViewerRenderer | ||||
commentTextRef={this.commentTextRef} | commentTextRef={this.commentTextRef} | ||||
commentVisible={commentVisible} | commentVisible={commentVisible} | ||||
hotspot={hotspot} | hotspot={hotspot} | ||||
hotspotsReviewedMeasure={hotspotsReviewedMeasure} | |||||
lastStatusChangedTo={lastStatusChangedTo} | |||||
loading={loading} | loading={loading} | ||||
onCloseComment={this.handleCloseComment} | onCloseComment={this.handleCloseComment} | ||||
onCloseStatusUpdateSuccessModal={this.handleCloseStatusUpdateSuccessModal} | |||||
onOpenComment={this.handleOpenComment} | onOpenComment={this.handleOpenComment} | ||||
onSwitchFilterToStatusOfUpdatedHotspot={this.handleSwitchFilterToStatusOfUpdatedHotspot} | |||||
onUpdateHotspot={this.handleHotspotUpdate} | onUpdateHotspot={this.handleHotspotUpdate} | ||||
showStatusUpdateSuccessModal={showStatusUpdateSuccessModal} | |||||
securityCategories={securityCategories} | securityCategories={securityCategories} | ||||
/> | /> | ||||
); | ); |
} from '../../../helpers/urls'; | } from '../../../helpers/urls'; | ||||
import { isLoggedIn } from '../../../helpers/users'; | import { isLoggedIn } from '../../../helpers/users'; | ||||
import { BranchLike } from '../../../types/branch-like'; | import { BranchLike } from '../../../types/branch-like'; | ||||
import { Hotspot } from '../../../types/security-hotspots'; | |||||
import { Hotspot, HotspotStatusOption } from '../../../types/security-hotspots'; | |||||
import Assignee from './assignee/Assignee'; | import Assignee from './assignee/Assignee'; | ||||
import HotspotOpenInIdeButton from './HotspotOpenInIdeButton'; | import HotspotOpenInIdeButton from './HotspotOpenInIdeButton'; | ||||
import HotspotReviewHistoryAndComments from './HotspotReviewHistoryAndComments'; | import HotspotReviewHistoryAndComments from './HotspotReviewHistoryAndComments'; | ||||
import './HotspotViewer.css'; | import './HotspotViewer.css'; | ||||
import HotspotViewerTabs from './HotspotViewerTabs'; | import HotspotViewerTabs from './HotspotViewerTabs'; | ||||
import Status from './status/Status'; | import Status from './status/Status'; | ||||
import StatusUpdateSuccessModal from './StatusUpdateSuccessModal'; | |||||
export interface HotspotViewerRendererProps { | export interface HotspotViewerRendererProps { | ||||
branchLike?: BranchLike; | branchLike?: BranchLike; | ||||
component: T.Component; | component: T.Component; | ||||
currentUser: T.CurrentUser; | currentUser: T.CurrentUser; | ||||
hotspot?: Hotspot; | hotspot?: Hotspot; | ||||
hotspotsReviewedMeasure?: string; | |||||
lastStatusChangedTo?: HotspotStatusOption; | |||||
loading: boolean; | loading: boolean; | ||||
commentVisible: boolean; | commentVisible: boolean; | ||||
commentTextRef: React.RefObject<HTMLTextAreaElement>; | commentTextRef: React.RefObject<HTMLTextAreaElement>; | ||||
onOpenComment: () => void; | onOpenComment: () => void; | ||||
onCloseComment: () => void; | onCloseComment: () => void; | ||||
onUpdateHotspot: (statusUpdate?: boolean) => Promise<void>; | |||||
onCloseStatusUpdateSuccessModal: () => void; | |||||
onUpdateHotspot: (statusUpdate?: boolean, statusOption?: HotspotStatusOption) => Promise<void>; | |||||
onSwitchFilterToStatusOfUpdatedHotspot: () => void; | |||||
showStatusUpdateSuccessModal: boolean; | |||||
securityCategories: T.StandardSecurityCategories; | securityCategories: T.StandardSecurityCategories; | ||||
} | } | ||||
component, | component, | ||||
currentUser, | currentUser, | ||||
hotspot, | hotspot, | ||||
hotspotsReviewedMeasure, | |||||
loading, | loading, | ||||
lastStatusChangedTo, | |||||
showStatusUpdateSuccessModal, | |||||
securityCategories, | securityCategories, | ||||
commentTextRef, | commentTextRef, | ||||
commentVisible | commentVisible | ||||
return ( | return ( | ||||
<DeferredSpinner className="big-spacer-left big-spacer-top" loading={loading}> | <DeferredSpinner className="big-spacer-left big-spacer-top" loading={loading}> | ||||
{showStatusUpdateSuccessModal && ( | |||||
<StatusUpdateSuccessModal | |||||
hotspotsReviewedMeasure={hotspotsReviewedMeasure} | |||||
lastStatusChangedTo={lastStatusChangedTo} | |||||
onClose={props.onCloseStatusUpdateSuccessModal} | |||||
onSwitchFilterToStatusOfUpdatedHotspot={props.onSwitchFilterToStatusOfUpdatedHotspot} | |||||
/> | |||||
)} | |||||
{hotspot && ( | {hotspot && ( | ||||
<div className="big-padded hotspot-content"> | <div className="big-padded hotspot-content"> | ||||
<div className="huge-spacer-bottom display-flex-space-between"> | <div className="huge-spacer-bottom display-flex-space-between"> | ||||
</div> | </div> | ||||
</div> | </div> | ||||
<div className="huge-spacer-left abs-width-400"> | <div className="huge-spacer-left abs-width-400"> | ||||
<Status hotspot={hotspot} onStatusChange={() => props.onUpdateHotspot(true)} /> | |||||
<Status | |||||
hotspot={hotspot} | |||||
onStatusChange={statusOption => props.onUpdateHotspot(true, statusOption)} | |||||
/> | |||||
</div> | </div> | ||||
</div> | </div> | ||||
/* | |||||
* SonarQube | |||||
* Copyright (C) 2009-2021 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 { FormattedMessage } from 'react-intl'; | |||||
import { Button, ButtonLink } from '../../../components/controls/buttons'; | |||||
import Modal from '../../../components/controls/Modal'; | |||||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||||
import { formatMeasure } from '../../../helpers/measures'; | |||||
import { HotspotStatusOption } from '../../../types/security-hotspots'; | |||||
export interface StatusUpdateSuccessModalProps { | |||||
hotspotsReviewedMeasure?: string; | |||||
lastStatusChangedTo?: HotspotStatusOption; | |||||
onClose: () => void; | |||||
onSwitchFilterToStatusOfUpdatedHotspot: () => void; | |||||
} | |||||
export default function StatusUpdateSuccessModal(props: StatusUpdateSuccessModalProps) { | |||||
const { hotspotsReviewedMeasure, lastStatusChangedTo } = props; | |||||
if (!lastStatusChangedTo) { | |||||
return null; | |||||
} | |||||
const closingHotspots = lastStatusChangedTo !== HotspotStatusOption.TO_REVIEW; | |||||
const statusLabel = translate('hotspots.status_option', lastStatusChangedTo); | |||||
const modalTitle = closingHotspots | |||||
? translate('hotspots.congratulations') | |||||
: translate('hotspots.update.success'); | |||||
return ( | |||||
<Modal contentLabel={modalTitle}> | |||||
<div className="modal-head"> | |||||
<h2 | |||||
className={classNames('huge text-normal', { | |||||
'text-success': closingHotspots | |||||
})}> | |||||
{modalTitle} | |||||
</h2> | |||||
</div> | |||||
<div className="modal-body"> | |||||
<FormattedMessage | |||||
id="hotspots.successfully_changed_to_x" | |||||
defaultMessage={translate('hotspots.successfully_changed_to_x')} | |||||
values={{ | |||||
status_label: statusLabel, | |||||
status_change: ( | |||||
<strong> | |||||
{translateWithParameters('hotspots.successful_status_change_to_x', statusLabel)} | |||||
</strong> | |||||
) | |||||
}} | |||||
/> | |||||
{closingHotspots && ( | |||||
<p className="spacer-top"> | |||||
<FormattedMessage | |||||
id="hotspots.x_done_keep_going" | |||||
defaultMessage={translate('hotspots.x_done_keep_going')} | |||||
values={{ | |||||
percentage: ( | |||||
<strong> | |||||
{formatMeasure(hotspotsReviewedMeasure, 'PERCENT', { | |||||
omitExtraDecimalZeros: true | |||||
})} | |||||
</strong> | |||||
) | |||||
}} | |||||
/> | |||||
</p> | |||||
)} | |||||
</div> | |||||
<div className="modal-foot modal-foot-clear display-flex-center display-flex-space-between"> | |||||
<ButtonLink onClick={props.onSwitchFilterToStatusOfUpdatedHotspot}> | |||||
{translateWithParameters('hotspots.see_x_hotspots', statusLabel)} | |||||
</ButtonLink> | |||||
<Button className="button-primary padded" onClick={props.onClose}> | |||||
{translate('hotspots.continue_to_next_hotspot')} | |||||
</Button> | |||||
</div> | |||||
</Modal> | |||||
); | |||||
} |
import { mockComponent } from '../../../../helpers/mocks/component'; | import { mockComponent } from '../../../../helpers/mocks/component'; | ||||
import { scrollToElement } from '../../../../helpers/scrolling'; | import { scrollToElement } from '../../../../helpers/scrolling'; | ||||
import { waitAndUpdate } from '../../../../helpers/testUtils'; | import { waitAndUpdate } from '../../../../helpers/testUtils'; | ||||
import { HotspotStatusOption } from '../../../../types/security-hotspots'; | |||||
import HotspotViewer from '../HotspotViewer'; | import HotspotViewer from '../HotspotViewer'; | ||||
import HotspotViewerRenderer from '../HotspotViewerRenderer'; | import HotspotViewerRenderer from '../HotspotViewerRenderer'; | ||||
expect(onUpdateHotspot).toHaveBeenCalled(); | expect(onUpdateHotspot).toHaveBeenCalled(); | ||||
}); | }); | ||||
it('should store last status selected when updating a hotspot status', () => { | |||||
const wrapper = shallowRender(); | |||||
expect(wrapper.state().lastStatusChangedTo).toBeUndefined(); | |||||
wrapper | |||||
.find(HotspotViewerRenderer) | |||||
.props() | |||||
.onUpdateHotspot(true, HotspotStatusOption.FIXED); | |||||
expect(wrapper.state().lastStatusChangedTo).toBe(HotspotStatusOption.FIXED); | |||||
}); | |||||
it('should correctly propagate a request to switch the status filter', () => { | |||||
const onSwitchStatusFilter = jest.fn(); | |||||
const wrapper = shallowRender({ onSwitchStatusFilter }); | |||||
wrapper.instance().handleSwitchFilterToStatusOfUpdatedHotspot(); | |||||
expect(onSwitchStatusFilter).not.toBeCalled(); | |||||
wrapper.setState({ lastStatusChangedTo: HotspotStatusOption.FIXED }); | |||||
wrapper.instance().handleSwitchFilterToStatusOfUpdatedHotspot(); | |||||
expect(onSwitchStatusFilter).toBeCalledWith(HotspotStatusOption.FIXED); | |||||
}); | |||||
it('should correctly close the success modal', () => { | |||||
const wrapper = shallowRender(); | |||||
wrapper.setState({ showStatusUpdateSuccessModal: true }); | |||||
wrapper.instance().handleCloseStatusUpdateSuccessModal(); | |||||
expect(wrapper.state().showStatusUpdateSuccessModal).toBe(false); | |||||
}); | |||||
it('should NOT refresh hotspot list on assignee/comment updates', () => { | it('should NOT refresh hotspot list on assignee/comment updates', () => { | ||||
const onUpdateHotspot = jest.fn(); | const onUpdateHotspot = jest.fn(); | ||||
const wrapper = shallowRender({ onUpdateHotspot }); | const wrapper = shallowRender({ onUpdateHotspot }); | ||||
<HotspotViewer | <HotspotViewer | ||||
component={mockComponent()} | component={mockComponent()} | ||||
hotspotKey={hotspotKey} | hotspotKey={hotspotKey} | ||||
onSwitchStatusFilter={jest.fn()} | |||||
onUpdateHotspot={jest.fn()} | onUpdateHotspot={jest.fn()} | ||||
securityCategories={{ cat1: { title: 'cat1' } }} | securityCategories={{ cat1: { title: 'cat1' } }} | ||||
{...props} | {...props} |
import { mockComponent } from '../../../../helpers/mocks/component'; | import { mockComponent } from '../../../../helpers/mocks/component'; | ||||
import { mockHotspot } from '../../../../helpers/mocks/security-hotspots'; | import { mockHotspot } from '../../../../helpers/mocks/security-hotspots'; | ||||
import { mockCurrentUser, mockUser } from '../../../../helpers/testMocks'; | import { mockCurrentUser, mockUser } from '../../../../helpers/testMocks'; | ||||
import { HotspotStatusOption } from '../../../../types/security-hotspots'; | |||||
import { HotspotViewerRenderer, HotspotViewerRendererProps } from '../HotspotViewerRenderer'; | import { HotspotViewerRenderer, HotspotViewerRendererProps } from '../HotspotViewerRenderer'; | ||||
import Status from '../status/Status'; | |||||
jest.mock('../../../../helpers/users', () => ({ isLoggedIn: jest.fn(() => true) })); | jest.mock('../../../../helpers/users', () => ({ isLoggedIn: jest.fn(() => true) })); | ||||
it('should render correctly', () => { | it('should render correctly', () => { | ||||
const wrapper = shallowRender(); | |||||
expect(wrapper).toMatchSnapshot(); | |||||
expect(shallowRender()).toMatchSnapshot('default'); | |||||
expect(shallowRender({ showStatusUpdateSuccessModal: true })).toMatchSnapshot( | |||||
'show success modal' | |||||
); | |||||
expect(shallowRender({ hotspot: undefined })).toMatchSnapshot('no hotspot'); | expect(shallowRender({ hotspot: undefined })).toMatchSnapshot('no hotspot'); | ||||
expect(shallowRender({ hotspot: mockHotspot({ assignee: undefined }) })).toMatchSnapshot( | expect(shallowRender({ hotspot: mockHotspot({ assignee: undefined }) })).toMatchSnapshot( | ||||
'unassigned' | 'unassigned' | ||||
expect(shallowRender()).toMatchSnapshot('anonymous user'); | expect(shallowRender()).toMatchSnapshot('anonymous user'); | ||||
}); | }); | ||||
it('correctly propagates the status change', () => { | |||||
const onUpdateHotspot = jest.fn(); | |||||
const wrapper = shallowRender({ onUpdateHotspot }); | |||||
wrapper | |||||
.find(Status) | |||||
.props() | |||||
.onStatusChange(HotspotStatusOption.FIXED); | |||||
expect(onUpdateHotspot).toHaveBeenCalledWith(true, HotspotStatusOption.FIXED); | |||||
}); | |||||
function shallowRender(props?: Partial<HotspotViewerRendererProps>) { | function shallowRender(props?: Partial<HotspotViewerRendererProps>) { | ||||
return shallow( | return shallow( | ||||
<HotspotViewerRenderer | <HotspotViewerRenderer | ||||
commentVisible={false} | commentVisible={false} | ||||
currentUser={mockCurrentUser()} | currentUser={mockCurrentUser()} | ||||
hotspot={mockHotspot()} | hotspot={mockHotspot()} | ||||
hotspotsReviewedMeasure="75" | |||||
lastStatusChangedTo={HotspotStatusOption.FIXED} | |||||
loading={false} | loading={false} | ||||
onCloseComment={jest.fn()} | onCloseComment={jest.fn()} | ||||
onCloseStatusUpdateSuccessModal={jest.fn()} | |||||
onOpenComment={jest.fn()} | onOpenComment={jest.fn()} | ||||
onSwitchFilterToStatusOfUpdatedHotspot={jest.fn()} | |||||
onUpdateHotspot={jest.fn()} | onUpdateHotspot={jest.fn()} | ||||
securityCategories={{ 'sql-injection': { title: 'SQL injection' } }} | securityCategories={{ 'sql-injection': { title: 'SQL injection' } }} | ||||
showStatusUpdateSuccessModal={false} | |||||
{...props} | {...props} | ||||
/> | /> | ||||
); | ); |
/* | |||||
* SonarQube | |||||
* Copyright (C) 2009-2021 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 StatusUpdateSuccessModal, { | |||||
StatusUpdateSuccessModalProps | |||||
} from '../StatusUpdateSuccessModal'; | |||||
it('should render correctly', () => { | |||||
expect(shallowRender()).toMatchSnapshot('default'); | |||||
expect(shallowRender({ lastStatusChangedTo: HotspotStatusOption.TO_REVIEW })).toMatchSnapshot( | |||||
'opening hotspots again' | |||||
); | |||||
expect(shallowRender({ lastStatusChangedTo: undefined }).type()).toBeNull(); | |||||
}); | |||||
function shallowRender(props: Partial<StatusUpdateSuccessModalProps> = {}) { | |||||
return shallow<StatusUpdateSuccessModalProps>( | |||||
<StatusUpdateSuccessModal | |||||
onClose={jest.fn()} | |||||
lastStatusChangedTo={HotspotStatusOption.FIXED} | |||||
onSwitchFilterToStatusOfUpdatedHotspot={jest.fn()} | |||||
{...props} | |||||
/> | |||||
); | |||||
} |
} | } | ||||
loading={true} | loading={true} | ||||
onCloseComment={[Function]} | onCloseComment={[Function]} | ||||
onCloseStatusUpdateSuccessModal={[Function]} | |||||
onOpenComment={[Function]} | onOpenComment={[Function]} | ||||
onSwitchFilterToStatusOfUpdatedHotspot={[Function]} | |||||
onUpdateHotspot={[Function]} | onUpdateHotspot={[Function]} | ||||
securityCategories={ | securityCategories={ | ||||
Object { | Object { | ||||
}, | }, | ||||
} | } | ||||
} | } | ||||
showStatusUpdateSuccessModal={false} | |||||
/> | /> | ||||
`; | `; | ||||
} | } | ||||
loading={false} | loading={false} | ||||
onCloseComment={[Function]} | onCloseComment={[Function]} | ||||
onCloseStatusUpdateSuccessModal={[Function]} | |||||
onOpenComment={[Function]} | onOpenComment={[Function]} | ||||
onSwitchFilterToStatusOfUpdatedHotspot={[Function]} | |||||
onUpdateHotspot={[Function]} | onUpdateHotspot={[Function]} | ||||
securityCategories={ | securityCategories={ | ||||
Object { | Object { | ||||
}, | }, | ||||
} | } | ||||
} | } | ||||
showStatusUpdateSuccessModal={false} | |||||
/> | /> | ||||
`; | `; |
// Jest Snapshot v1, https://goo.gl/fbAQLP | // Jest Snapshot v1, https://goo.gl/fbAQLP | ||||
exports[`should render correctly 1`] = ` | |||||
exports[`should render correctly: anonymous user 1`] = ` | |||||
<DeferredSpinner | <DeferredSpinner | ||||
className="big-spacer-left big-spacer-top" | className="big-spacer-left big-spacer-top" | ||||
loading={false} | loading={false} | ||||
</DeferredSpinner> | </DeferredSpinner> | ||||
`; | `; | ||||
exports[`should render correctly: anonymous user 1`] = ` | |||||
exports[`should render correctly: assignee without name 1`] = ` | |||||
<DeferredSpinner | <DeferredSpinner | ||||
className="big-spacer-left big-spacer-top" | className="big-spacer-left big-spacer-top" | ||||
loading={false} | loading={false} | ||||
"assigneeUser": Object { | "assigneeUser": Object { | ||||
"active": true, | "active": true, | ||||
"local": true, | "local": true, | ||||
"login": "assignee", | |||||
"name": "John Doe", | |||||
"login": "assignee_login", | |||||
"name": undefined, | |||||
}, | }, | ||||
"author": "author", | "author": "author", | ||||
"authorUser": Object { | "authorUser": Object { | ||||
"assigneeUser": Object { | "assigneeUser": Object { | ||||
"active": true, | "active": true, | ||||
"local": true, | "local": true, | ||||
"login": "assignee", | |||||
"name": "John Doe", | |||||
"login": "assignee_login", | |||||
"name": undefined, | |||||
}, | }, | ||||
"author": "author", | "author": "author", | ||||
"authorUser": Object { | "authorUser": Object { | ||||
"assigneeUser": Object { | "assigneeUser": Object { | ||||
"active": true, | "active": true, | ||||
"local": true, | "local": true, | ||||
"login": "assignee", | |||||
"name": "John Doe", | |||||
"login": "assignee_login", | |||||
"name": undefined, | |||||
}, | }, | ||||
"author": "author", | "author": "author", | ||||
"authorUser": Object { | "authorUser": Object { | ||||
"assigneeUser": Object { | "assigneeUser": Object { | ||||
"active": true, | "active": true, | ||||
"local": true, | "local": true, | ||||
"login": "assignee", | |||||
"name": "John Doe", | |||||
"login": "assignee_login", | |||||
"name": undefined, | |||||
}, | }, | ||||
"author": "author", | "author": "author", | ||||
"authorUser": Object { | "authorUser": Object { | ||||
"assigneeUser": Object { | "assigneeUser": Object { | ||||
"active": true, | "active": true, | ||||
"local": true, | "local": true, | ||||
"login": "assignee", | |||||
"name": "John Doe", | |||||
"login": "assignee_login", | |||||
"name": undefined, | |||||
}, | }, | ||||
"author": "author", | "author": "author", | ||||
"authorUser": Object { | "authorUser": Object { | ||||
</DeferredSpinner> | </DeferredSpinner> | ||||
`; | `; | ||||
exports[`should render correctly: assignee without name 1`] = ` | |||||
exports[`should render correctly: default 1`] = ` | |||||
<DeferredSpinner | <DeferredSpinner | ||||
className="big-spacer-left big-spacer-top" | className="big-spacer-left big-spacer-top" | ||||
loading={false} | loading={false} | ||||
"assigneeUser": Object { | "assigneeUser": Object { | ||||
"active": true, | "active": true, | ||||
"local": true, | "local": true, | ||||
"login": "assignee_login", | |||||
"name": undefined, | |||||
"login": "assignee", | |||||
"name": "John Doe", | |||||
}, | }, | ||||
"author": "author", | "author": "author", | ||||
"authorUser": Object { | "authorUser": Object { | ||||
"assigneeUser": Object { | "assigneeUser": Object { | ||||
"active": true, | "active": true, | ||||
"local": true, | "local": true, | ||||
"login": "assignee_login", | |||||
"name": undefined, | |||||
"login": "assignee", | |||||
"name": "John Doe", | |||||
}, | }, | ||||
"author": "author", | "author": "author", | ||||
"authorUser": Object { | "authorUser": Object { | ||||
"assigneeUser": Object { | "assigneeUser": Object { | ||||
"active": true, | "active": true, | ||||
"local": true, | "local": true, | ||||
"login": "assignee_login", | |||||
"name": undefined, | |||||
"login": "assignee", | |||||
"name": "John Doe", | |||||
}, | }, | ||||
"author": "author", | "author": "author", | ||||
"authorUser": Object { | "authorUser": Object { | ||||
"assigneeUser": Object { | "assigneeUser": Object { | ||||
"active": true, | "active": true, | ||||
"local": true, | "local": true, | ||||
"login": "assignee_login", | |||||
"name": undefined, | |||||
"login": "assignee", | |||||
"name": "John Doe", | |||||
}, | }, | ||||
"author": "author", | "author": "author", | ||||
"authorUser": Object { | "authorUser": Object { | ||||
"assigneeUser": Object { | "assigneeUser": Object { | ||||
"active": true, | "active": true, | ||||
"local": true, | "local": true, | ||||
"login": "assignee_login", | |||||
"name": undefined, | |||||
"login": "assignee", | |||||
"name": "John Doe", | |||||
}, | }, | ||||
"author": "author", | "author": "author", | ||||
"authorUser": Object { | "authorUser": Object { | ||||
/> | /> | ||||
`; | `; | ||||
exports[`should render correctly: show success modal 1`] = ` | |||||
<DeferredSpinner | |||||
className="big-spacer-left big-spacer-top" | |||||
loading={false} | |||||
> | |||||
<StatusUpdateSuccessModal | |||||
hotspotsReviewedMeasure="75" | |||||
lastStatusChangedTo="FIXED" | |||||
onClose={[MockFunction]} | |||||
onSwitchFilterToStatusOfUpdatedHotspot={[MockFunction]} | |||||
/> | |||||
<div | |||||
className="big-padded hotspot-content" | |||||
> | |||||
<div | |||||
className="huge-spacer-bottom display-flex-space-between" | |||||
> | |||||
<div | |||||
className="display-flex-column" | |||||
> | |||||
<strong | |||||
className="big big-spacer-right little-spacer-bottom" | |||||
> | |||||
'3' is a magic number. | |||||
</strong> | |||||
<div> | |||||
<span | |||||
className="note padded-right" | |||||
> | |||||
That rule | |||||
</span> | |||||
<Link | |||||
className="small" | |||||
onlyActiveOnIndex={false} | |||||
style={Object {}} | |||||
target="_blank" | |||||
to={ | |||||
Object { | |||||
"pathname": "/coding_rules", | |||||
"query": Object { | |||||
"open": "squid:S2077", | |||||
"rule_key": "squid:S2077", | |||||
}, | |||||
} | |||||
} | |||||
> | |||||
squid:S2077 | |||||
</Link> | |||||
</div> | |||||
</div> | |||||
<div | |||||
className="display-flex-row flex-0" | |||||
> | |||||
<div | |||||
className="dropdown spacer-right flex-1-0-auto" | |||||
> | |||||
<Button | |||||
className="it__hs-add-comment" | |||||
onClick={[MockFunction]} | |||||
> | |||||
hotspots.comment.open | |||||
</Button> | |||||
</div> | |||||
<div | |||||
className="dropdown spacer-right flex-1-0-auto" | |||||
> | |||||
<HotspotOpenInIdeButton | |||||
hotspotKey="01fc972e-2a3c-433e-bcae-0bd7f88f5123" | |||||
projectKey="hotspot-component" | |||||
/> | |||||
</div> | |||||
<ClipboardButton | |||||
className="flex-1-0-auto" | |||||
copyValue="http://localhost/security_hotspots?id=my-project&branch=branch-6.7&hotspots=01fc972e-2a3c-433e-bcae-0bd7f88f5123" | |||||
> | |||||
<LinkIcon | |||||
className="spacer-right" | |||||
/> | |||||
<span> | |||||
hotspots.get_permalink | |||||
</span> | |||||
</ClipboardButton> | |||||
</div> | |||||
</div> | |||||
<div | |||||
className="huge-spacer-bottom display-flex-row display-flex-space-between" | |||||
> | |||||
<div | |||||
className="hotspot-information display-flex-column display-flex-space-between" | |||||
> | |||||
<div | |||||
className="display-flex-center" | |||||
> | |||||
<span | |||||
className="big-spacer-right" | |||||
> | |||||
category | |||||
</span> | |||||
<strong | |||||
className="nowrap" | |||||
> | |||||
SQL injection | |||||
</strong> | |||||
</div> | |||||
<div | |||||
className="display-flex-center" | |||||
> | |||||
<span | |||||
className="big-spacer-right" | |||||
> | |||||
hotspots.risk_exposure | |||||
</span> | |||||
<div | |||||
className="hotspot-risk-badge HIGH" | |||||
> | |||||
risk_exposure.HIGH | |||||
</div> | |||||
</div> | |||||
<div | |||||
className="display-flex-center it__hs-assignee" | |||||
> | |||||
<span | |||||
className="big-spacer-right" | |||||
> | |||||
assignee | |||||
</span> | |||||
<div> | |||||
<Connect(withCurrentUser(Assignee)) | |||||
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 { | |||||
"key": "hotspot-component", | |||||
"longName": "Hotspot component long name", | |||||
"name": "Hotspot Component", | |||||
"path": "path/to/component", | |||||
"qualifier": "FIL", | |||||
}, | |||||
"creationDate": "2013-05-13T17:55:41+0200", | |||||
"key": "01fc972e-2a3c-433e-bcae-0bd7f88f5123", | |||||
"line": 142, | |||||
"message": "'3' is a magic number.", | |||||
"project": Object { | |||||
"key": "hotspot-component", | |||||
"longName": "Hotspot component long name", | |||||
"name": "Hotspot Component", | |||||
"path": "path/to/component", | |||||
"qualifier": "TRK", | |||||
}, | |||||
"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", | |||||
}, | |||||
], | |||||
} | |||||
} | |||||
onAssigneeChange={[MockFunction]} | |||||
/> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<div | |||||
className="huge-spacer-left abs-width-400" | |||||
> | |||||
<Connect(withCurrentUser(Status)) | |||||
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 { | |||||
"key": "hotspot-component", | |||||
"longName": "Hotspot component long name", | |||||
"name": "Hotspot Component", | |||||
"path": "path/to/component", | |||||
"qualifier": "FIL", | |||||
}, | |||||
"creationDate": "2013-05-13T17:55:41+0200", | |||||
"key": "01fc972e-2a3c-433e-bcae-0bd7f88f5123", | |||||
"line": 142, | |||||
"message": "'3' is a magic number.", | |||||
"project": Object { | |||||
"key": "hotspot-component", | |||||
"longName": "Hotspot component long name", | |||||
"name": "Hotspot Component", | |||||
"path": "path/to/component", | |||||
"qualifier": "TRK", | |||||
}, | |||||
"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", | |||||
}, | |||||
], | |||||
} | |||||
} | |||||
onStatusChange={[Function]} | |||||
/> | |||||
</div> | |||||
</div> | |||||
<HotspotSnippetContainer | |||||
component={ | |||||
Object { | |||||
"breadcrumbs": Array [], | |||||
"key": "my-project", | |||||
"name": "MyProject", | |||||
"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 [], | |||||
} | |||||
} | |||||
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 { | |||||
"key": "hotspot-component", | |||||
"longName": "Hotspot component long name", | |||||
"name": "Hotspot Component", | |||||
"path": "path/to/component", | |||||
"qualifier": "FIL", | |||||
}, | |||||
"creationDate": "2013-05-13T17:55:41+0200", | |||||
"key": "01fc972e-2a3c-433e-bcae-0bd7f88f5123", | |||||
"line": 142, | |||||
"message": "'3' is a magic number.", | |||||
"project": Object { | |||||
"key": "hotspot-component", | |||||
"longName": "Hotspot component long name", | |||||
"name": "Hotspot Component", | |||||
"path": "path/to/component", | |||||
"qualifier": "TRK", | |||||
}, | |||||
"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", | |||||
}, | |||||
], | |||||
} | |||||
} | |||||
/> | |||||
<HotspotViewerTabs | |||||
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 { | |||||
"key": "hotspot-component", | |||||
"longName": "Hotspot component long name", | |||||
"name": "Hotspot Component", | |||||
"path": "path/to/component", | |||||
"qualifier": "FIL", | |||||
}, | |||||
"creationDate": "2013-05-13T17:55:41+0200", | |||||
"key": "01fc972e-2a3c-433e-bcae-0bd7f88f5123", | |||||
"line": 142, | |||||
"message": "'3' is a magic number.", | |||||
"project": Object { | |||||
"key": "hotspot-component", | |||||
"longName": "Hotspot component long name", | |||||
"name": "Hotspot Component", | |||||
"path": "path/to/component", | |||||
"qualifier": "TRK", | |||||
}, | |||||
"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", | |||||
}, | |||||
], | |||||
} | |||||
} | |||||
/> | |||||
<HotspotReviewHistoryAndComments | |||||
commentTextRef={ | |||||
Object { | |||||
"current": null, | |||||
} | |||||
} | |||||
commentVisible={false} | |||||
currentUser={ | |||||
Object { | |||||
"isLoggedIn": false, | |||||
} | |||||
} | |||||
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 { | |||||
"key": "hotspot-component", | |||||
"longName": "Hotspot component long name", | |||||
"name": "Hotspot Component", | |||||
"path": "path/to/component", | |||||
"qualifier": "FIL", | |||||
}, | |||||
"creationDate": "2013-05-13T17:55:41+0200", | |||||
"key": "01fc972e-2a3c-433e-bcae-0bd7f88f5123", | |||||
"line": 142, | |||||
"message": "'3' is a magic number.", | |||||
"project": Object { | |||||
"key": "hotspot-component", | |||||
"longName": "Hotspot component long name", | |||||
"name": "Hotspot Component", | |||||
"path": "path/to/component", | |||||
"qualifier": "TRK", | |||||
}, | |||||
"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", | |||||
}, | |||||
], | |||||
} | |||||
} | |||||
onCloseComment={[MockFunction]} | |||||
onCommentUpdate={[MockFunction]} | |||||
onOpenComment={[MockFunction]} | |||||
/> | |||||
</div> | |||||
</DeferredSpinner> | |||||
`; | |||||
exports[`should render correctly: unassigned 1`] = ` | exports[`should render correctly: unassigned 1`] = ` | ||||
<DeferredSpinner | <DeferredSpinner | ||||
className="big-spacer-left big-spacer-top" | className="big-spacer-left big-spacer-top" |
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||||
exports[`should render correctly: default 1`] = ` | |||||
<Modal | |||||
contentLabel="hotspots.congratulations" | |||||
> | |||||
<div | |||||
className="modal-head" | |||||
> | |||||
<h2 | |||||
className="huge text-normal text-success" | |||||
> | |||||
hotspots.congratulations | |||||
</h2> | |||||
</div> | |||||
<div | |||||
className="modal-body" | |||||
> | |||||
<FormattedMessage | |||||
defaultMessage="hotspots.successfully_changed_to_x" | |||||
id="hotspots.successfully_changed_to_x" | |||||
values={ | |||||
Object { | |||||
"status_change": <strong> | |||||
hotspots.successful_status_change_to_x.hotspots.status_option.FIXED | |||||
</strong>, | |||||
"status_label": "hotspots.status_option.FIXED", | |||||
} | |||||
} | |||||
/> | |||||
<p | |||||
className="spacer-top" | |||||
> | |||||
<FormattedMessage | |||||
defaultMessage="hotspots.x_done_keep_going" | |||||
id="hotspots.x_done_keep_going" | |||||
values={ | |||||
Object { | |||||
"percentage": <strong> | |||||
</strong>, | |||||
} | |||||
} | |||||
/> | |||||
</p> | |||||
</div> | |||||
<div | |||||
className="modal-foot modal-foot-clear display-flex-center display-flex-space-between" | |||||
> | |||||
<ButtonLink | |||||
onClick={[MockFunction]} | |||||
> | |||||
hotspots.see_x_hotspots.hotspots.status_option.FIXED | |||||
</ButtonLink> | |||||
<Button | |||||
className="button-primary padded" | |||||
onClick={[MockFunction]} | |||||
> | |||||
hotspots.continue_to_next_hotspot | |||||
</Button> | |||||
</div> | |||||
</Modal> | |||||
`; | |||||
exports[`should render correctly: opening hotspots again 1`] = ` | |||||
<Modal | |||||
contentLabel="hotspots.update.success" | |||||
> | |||||
<div | |||||
className="modal-head" | |||||
> | |||||
<h2 | |||||
className="huge text-normal" | |||||
> | |||||
hotspots.update.success | |||||
</h2> | |||||
</div> | |||||
<div | |||||
className="modal-body" | |||||
> | |||||
<FormattedMessage | |||||
defaultMessage="hotspots.successfully_changed_to_x" | |||||
id="hotspots.successfully_changed_to_x" | |||||
values={ | |||||
Object { | |||||
"status_change": <strong> | |||||
hotspots.successful_status_change_to_x.hotspots.status_option.TO_REVIEW | |||||
</strong>, | |||||
"status_label": "hotspots.status_option.TO_REVIEW", | |||||
} | |||||
} | |||||
/> | |||||
</div> | |||||
<div | |||||
className="modal-foot modal-foot-clear display-flex-center display-flex-space-between" | |||||
> | |||||
<ButtonLink | |||||
onClick={[MockFunction]} | |||||
> | |||||
hotspots.see_x_hotspots.hotspots.status_option.TO_REVIEW | |||||
</ButtonLink> | |||||
<Button | |||||
className="button-primary padded" | |||||
onClick={[MockFunction]} | |||||
> | |||||
hotspots.continue_to_next_hotspot | |||||
</Button> | |||||
</div> | |||||
</Modal> | |||||
`; |
import { PopupPlacement } from '../../../../components/ui/popups'; | import { PopupPlacement } from '../../../../components/ui/popups'; | ||||
import { translate } from '../../../../helpers/l10n'; | import { translate } from '../../../../helpers/l10n'; | ||||
import { isLoggedIn } from '../../../../helpers/users'; | import { isLoggedIn } from '../../../../helpers/users'; | ||||
import { Hotspot } from '../../../../types/security-hotspots'; | |||||
import { Hotspot, HotspotStatusOption } from '../../../../types/security-hotspots'; | |||||
import { getStatusOptionFromStatusAndResolution } from '../../utils'; | import { getStatusOptionFromStatusAndResolution } from '../../utils'; | ||||
import StatusDescription from './StatusDescription'; | import StatusDescription from './StatusDescription'; | ||||
import StatusSelection from './StatusSelection'; | import StatusSelection from './StatusSelection'; | ||||
currentUser: T.CurrentUser; | currentUser: T.CurrentUser; | ||||
hotspot: Hotspot; | hotspot: Hotspot; | ||||
onStatusChange: () => Promise<void>; | |||||
onStatusChange: (statusOption: HotspotStatusOption) => Promise<void>; | |||||
} | } | ||||
export function Status(props: StatusProps) { | export function Status(props: StatusProps) { | ||||
<DropdownOverlay noPadding={true} placement={PopupPlacement.Bottom}> | <DropdownOverlay noPadding={true} placement={PopupPlacement.Bottom}> | ||||
<StatusSelection | <StatusSelection | ||||
hotspot={hotspot} | hotspot={hotspot} | ||||
onStatusOptionChange={async () => { | |||||
await props.onStatusChange(); | |||||
onStatusOptionChange={async status => { | |||||
await props.onStatusChange(status); | |||||
setIsOpen(false); | setIsOpen(false); | ||||
}} | }} | ||||
/> | /> |
*/ | */ | ||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { setSecurityHotspotStatus } from '../../../../api/security-hotspots'; | import { setSecurityHotspotStatus } from '../../../../api/security-hotspots'; | ||||
import addGlobalSuccessMessage from '../../../../app/utils/addGlobalSuccessMessage'; | |||||
import { translate, translateWithParameters } from '../../../../helpers/l10n'; | |||||
import { Hotspot, HotspotStatusOption } from '../../../../types/security-hotspots'; | import { Hotspot, HotspotStatusOption } from '../../../../types/security-hotspots'; | ||||
import { | import { | ||||
getStatusAndResolutionFromStatusOption, | getStatusAndResolutionFromStatusOption, | ||||
await this.props.onStatusOptionChange(selectedStatus); | await this.props.onStatusOptionChange(selectedStatus); | ||||
this.setState({ loading: false }); | this.setState({ loading: false }); | ||||
}) | }) | ||||
.then(() => | |||||
addGlobalSuccessMessage( | |||||
translateWithParameters( | |||||
'hotspots.update.success', | |||||
translate('hotspots.status_option', selectedStatus) | |||||
) | |||||
) | |||||
) | |||||
.catch(() => this.setState({ loading: false })); | .catch(() => this.setState({ loading: false })); | ||||
} | } | ||||
}; | }; |
Hotspot, | Hotspot, | ||||
HotspotResolution, | HotspotResolution, | ||||
HotspotStatus, | HotspotStatus, | ||||
HotspotStatusFilter, | |||||
HotspotStatusOption, | HotspotStatusOption, | ||||
RawHotspot, | RawHotspot, | ||||
ReviewHistoryElement, | ReviewHistoryElement, | ||||
export function getStatusAndResolutionFromStatusOption(statusOption: HotspotStatusOption) { | export function getStatusAndResolutionFromStatusOption(statusOption: HotspotStatusOption) { | ||||
return STATUS_OPTION_TO_STATUS_AND_RESOLUTION_MAP[statusOption]; | return STATUS_OPTION_TO_STATUS_AND_RESOLUTION_MAP[statusOption]; | ||||
} | } | ||||
const STATUS_OPTION_TO_STATUS_FILTER = { | |||||
[HotspotStatusOption.TO_REVIEW]: HotspotStatusFilter.TO_REVIEW, | |||||
[HotspotStatusOption.FIXED]: HotspotStatusFilter.FIXED, | |||||
[HotspotStatusOption.SAFE]: HotspotStatusFilter.SAFE | |||||
}; | |||||
export function getStatusFilterFromStatusOption(statusOption: HotspotStatusOption) { | |||||
return STATUS_OPTION_TO_STATUS_FILTER[statusOption]; | |||||
} |
.modal-foot input[type='button'] { | .modal-foot input[type='button'] { | ||||
margin-left: var(--gridSize); | margin-left: var(--gridSize); | ||||
} | } | ||||
.modal-foot button:first-of-type, | |||||
.modal-foot .button:first-of-type, | |||||
.modal-foot input[type='submit']:first-of-type, | |||||
.modal-foot input[type='button']:first-of-type { | |||||
margin-left: 0; | |||||
} | |||||
.modal-foot-clear { | |||||
border-top: 0; | |||||
background-color: transparent; | |||||
} |
hotspots.status_option.SAFE.description=The code is not at risk and doesn't need to be modified. | hotspots.status_option.SAFE.description=The code is not at risk and doesn't need to be modified. | ||||
hotspots.get_permalink=Get Permalink | hotspots.get_permalink=Get Permalink | ||||
hotspots.no_associated_lines=Security Hotspot raised on the following file: | hotspots.no_associated_lines=Security Hotspot raised on the following file: | ||||
hotspots.congratulations=Congratulations! | |||||
hotspots.successfully_changed_to_x=The Security Hotspot was {status_change}. You can find it by changing the top filter to display "{status_label}" Security Hotspots. | |||||
hotspots.successful_status_change_to_x=successfully changed to "{0}" | |||||
hotspots.x_done_keep_going={percentage} of the Security Hotspots have been reviewed, keep going! | |||||
hotspots.see_x_hotspots=See "{0}" Security Hotspots | |||||
hotspots.continue_to_next_hotspot=Continue reviewing next Security Hotspot | |||||
hotspot.filters.title=Filters | hotspot.filters.title=Filters | ||||
hotspot.filters.assignee.assigned_to_me=Assigned to me | hotspot.filters.assignee.assigned_to_me=Assigned to me | ||||
hotspots.assign.success=Security Hotspot was successfully assigned to {0} | hotspots.assign.success=Security Hotspot was successfully assigned to {0} | ||||
hotspots.assign.unassign.success=Security Hotspot was successfully unassigned | hotspots.assign.unassign.success=Security Hotspot was successfully unassigned | ||||
hotspots.update.success=Security Hotspot status was successfully changed to {0} | |||||
hotspots.update.success=Update successful | |||||
#------------------------------------------------------------------------------ | #------------------------------------------------------------------------------ | ||||
# | # |