@@ -52,6 +52,7 @@ export function getSecurityHotspots( | |||
ps: number; | |||
status?: HotspotStatus; | |||
resolution?: HotspotResolution; | |||
onlyMine?: boolean; | |||
} & BranchParameters | |||
): Promise<HotspotSearchResponse> { | |||
return getJSON('/api/hotspots/search', data).catch(throwGlobalError); |
@@ -20,13 +20,16 @@ | |||
import * as React from 'react'; | |||
import { addNoFooterPageClass, removeNoFooterPageClass } from 'sonar-ui-common/helpers/pages'; | |||
import { getSecurityHotspots } from '../../api/security-hotspots'; | |||
import { withCurrentUser } from '../../components/hoc/withCurrentUser'; | |||
import { getBranchLikeQuery } from '../../helpers/branch-like'; | |||
import { getStandards } from '../../helpers/security-standard'; | |||
import { isLoggedIn } from '../../helpers/users'; | |||
import { BranchLike } from '../../types/branch-like'; | |||
import { | |||
HotspotFilters, | |||
HotspotResolution, | |||
HotspotStatus, | |||
HotspotStatusFilters, | |||
HotspotStatusFilter, | |||
HotspotUpdate, | |||
RawHotspot | |||
} from '../../types/security-hotspots'; | |||
@@ -38,6 +41,7 @@ const PAGE_SIZE = 500; | |||
interface Props { | |||
branchLike?: BranchLike; | |||
currentUser: T.CurrentUser; | |||
component: T.Component; | |||
} | |||
@@ -46,18 +50,27 @@ interface State { | |||
loading: boolean; | |||
securityCategories: T.StandardSecurityCategories; | |||
selectedHotspotKey: string | undefined; | |||
statusFilter: HotspotStatusFilters; | |||
filters: HotspotFilters; | |||
} | |||
export default class SecurityHotspotsApp extends React.PureComponent<Props, State> { | |||
export class SecurityHotspotsApp extends React.PureComponent<Props, State> { | |||
mounted = false; | |||
state = { | |||
loading: true, | |||
hotspots: [], | |||
securityCategories: {}, | |||
selectedHotspotKey: undefined, | |||
statusFilter: HotspotStatusFilters.TO_REVIEW | |||
}; | |||
state: State; | |||
constructor(props: Props) { | |||
super(props); | |||
this.state = { | |||
loading: true, | |||
hotspots: [], | |||
securityCategories: {}, | |||
selectedHotspotKey: undefined, | |||
filters: { | |||
assignedToMe: isLoggedIn(this.props.currentUser) ? true : false, | |||
status: HotspotStatusFilter.TO_REVIEW | |||
} | |||
}; | |||
} | |||
componentDidMount() { | |||
this.mounted = true; | |||
@@ -103,15 +116,17 @@ export default class SecurityHotspotsApp extends React.PureComponent<Props, Stat | |||
fetchSecurityHotspots() { | |||
const { branchLike, component } = this.props; | |||
const { statusFilter } = this.state; | |||
const { filters } = this.state; | |||
const status = | |||
statusFilter === HotspotStatusFilters.TO_REVIEW | |||
filters.status === HotspotStatusFilter.TO_REVIEW | |||
? HotspotStatus.TO_REVIEW | |||
: HotspotStatus.REVIEWED; | |||
const resolution = | |||
statusFilter === HotspotStatusFilters.TO_REVIEW ? undefined : HotspotResolution[statusFilter]; | |||
filters.status === HotspotStatusFilter.TO_REVIEW | |||
? undefined | |||
: HotspotResolution[filters.status]; | |||
return getSecurityHotspots({ | |||
projectKey: component.key, | |||
@@ -119,6 +134,7 @@ export default class SecurityHotspotsApp extends React.PureComponent<Props, Stat | |||
ps: PAGE_SIZE, | |||
status, | |||
resolution, | |||
onlyMine: filters.assignedToMe, | |||
...getBranchLikeQuery(branchLike) | |||
}); | |||
} | |||
@@ -145,8 +161,11 @@ export default class SecurityHotspotsApp extends React.PureComponent<Props, Stat | |||
.catch(this.handleCallFailure); | |||
}; | |||
handleChangeStatusFilter = (statusFilter: HotspotStatusFilters) => { | |||
this.setState({ statusFilter }, this.reloadSecurityHotspotList); | |||
handleChangeFilters = (changes: Partial<HotspotFilters>) => { | |||
this.setState( | |||
({ filters }) => ({ filters: { ...filters, ...changes } }), | |||
this.reloadSecurityHotspotList | |||
); | |||
}; | |||
handleHotspotClick = (key: string) => this.setState({ selectedHotspotKey: key }); | |||
@@ -170,20 +189,22 @@ export default class SecurityHotspotsApp extends React.PureComponent<Props, Stat | |||
render() { | |||
const { branchLike } = this.props; | |||
const { hotspots, loading, securityCategories, selectedHotspotKey, statusFilter } = this.state; | |||
const { hotspots, loading, securityCategories, selectedHotspotKey, filters } = this.state; | |||
return ( | |||
<SecurityHotspotsAppRenderer | |||
branchLike={branchLike} | |||
filters={filters} | |||
hotspots={hotspots} | |||
loading={loading} | |||
onChangeStatusFilter={this.handleChangeStatusFilter} | |||
onChangeFilters={this.handleChangeFilters} | |||
onHotspotClick={this.handleHotspotClick} | |||
onUpdateHotspot={this.handleHotspotUpdate} | |||
securityCategories={securityCategories} | |||
selectedHotspotKey={selectedHotspotKey} | |||
statusFilter={statusFilter} | |||
/> | |||
); | |||
} | |||
} | |||
export default withCurrentUser(SecurityHotspotsApp); |
@@ -27,7 +27,7 @@ import A11ySkipTarget from '../../app/components/a11y/A11ySkipTarget'; | |||
import Suggestions from '../../app/components/embed-docs-modal/Suggestions'; | |||
import ScreenPositionHelper from '../../components/common/ScreenPositionHelper'; | |||
import { BranchLike } from '../../types/branch-like'; | |||
import { HotspotStatusFilters, HotspotUpdate, RawHotspot } from '../../types/security-hotspots'; | |||
import { HotspotFilters, HotspotUpdate, RawHotspot } from '../../types/security-hotspots'; | |||
import FilterBar from './components/FilterBar'; | |||
import HotspotList from './components/HotspotList'; | |||
import HotspotViewer from './components/HotspotViewer'; | |||
@@ -35,29 +35,22 @@ import './styles.css'; | |||
export interface SecurityHotspotsAppRendererProps { | |||
branchLike?: BranchLike; | |||
filters: HotspotFilters; | |||
hotspots: RawHotspot[]; | |||
loading: boolean; | |||
onChangeStatusFilter: (status: HotspotStatusFilters) => void; | |||
onChangeFilters: (filters: Partial<HotspotFilters>) => void; | |||
onHotspotClick: (key: string) => void; | |||
onUpdateHotspot: (hotspot: HotspotUpdate) => void; | |||
selectedHotspotKey?: string; | |||
securityCategories: T.StandardSecurityCategories; | |||
statusFilter: HotspotStatusFilters; | |||
} | |||
export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRendererProps) { | |||
const { | |||
branchLike, | |||
hotspots, | |||
loading, | |||
securityCategories, | |||
selectedHotspotKey, | |||
statusFilter | |||
} = props; | |||
const { branchLike, hotspots, loading, securityCategories, selectedHotspotKey, filters } = props; | |||
return ( | |||
<div id="security_hotspots"> | |||
<FilterBar onChangeStatus={props.onChangeStatusFilter} statusFilter={statusFilter} /> | |||
<FilterBar onChangeFilters={props.onChangeFilters} filters={filters} /> | |||
<ScreenPositionHelper> | |||
{({ top }) => ( | |||
<div className="wrapper" style={{ top }}> | |||
@@ -94,7 +87,7 @@ export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRe | |||
onHotspotClick={props.onHotspotClick} | |||
securityCategories={securityCategories} | |||
selectedHotspotKey={selectedHotspotKey} | |||
statusFilter={statusFilter} | |||
statusFilter={filters.status} | |||
/> | |||
</div> | |||
<div className="main"> |
@@ -25,13 +25,13 @@ import { getSecurityHotspots } from '../../../api/security-hotspots'; | |||
import { mockBranch } from '../../../helpers/mocks/branch-like'; | |||
import { mockRawHotspot } from '../../../helpers/mocks/security-hotspots'; | |||
import { getStandards } from '../../../helpers/security-standard'; | |||
import { mockComponent } from '../../../helpers/testMocks'; | |||
import { mockComponent, mockCurrentUser } from '../../../helpers/testMocks'; | |||
import { | |||
HotspotResolution, | |||
HotspotStatus, | |||
HotspotStatusFilters | |||
HotspotStatusFilter | |||
} from '../../../types/security-hotspots'; | |||
import SecurityHotspotsApp from '../SecurityHotspotsApp'; | |||
import { SecurityHotspotsApp } from '../SecurityHotspotsApp'; | |||
import SecurityHotspotsAppRenderer from '../SecurityHotspotsAppRenderer'; | |||
jest.mock('sonar-ui-common/helpers/pages', () => ({ | |||
@@ -125,7 +125,7 @@ it('should handle status filter change', async () => { | |||
await waitAndUpdate(wrapper); | |||
// Set filter to SAFE: | |||
wrapper.instance().handleChangeStatusFilter(HotspotStatusFilters.SAFE); | |||
wrapper.instance().handleChangeFilters({ status: HotspotStatusFilter.SAFE }); | |||
expect(getSecurityHotspots).toBeCalledWith( | |||
expect.objectContaining({ status: HotspotStatus.REVIEWED, resolution: HotspotResolution.SAFE }) | |||
@@ -136,7 +136,7 @@ it('should handle status filter change', async () => { | |||
expect(wrapper.state().hotspots[0]).toBe(hotspots2[0]); | |||
// Set filter to FIXED | |||
wrapper.instance().handleChangeStatusFilter(HotspotStatusFilters.FIXED); | |||
wrapper.instance().handleChangeFilters({ status: HotspotStatusFilter.FIXED }); | |||
expect(getSecurityHotspots).toBeCalledWith( | |||
expect.objectContaining({ status: HotspotStatus.REVIEWED, resolution: HotspotResolution.FIXED }) | |||
@@ -149,6 +149,11 @@ it('should handle status filter change', async () => { | |||
function shallowRender(props: Partial<SecurityHotspotsApp['props']> = {}) { | |||
return shallow<SecurityHotspotsApp>( | |||
<SecurityHotspotsApp branchLike={branch} component={mockComponent()} {...props} /> | |||
<SecurityHotspotsApp | |||
branchLike={branch} | |||
component={mockComponent()} | |||
currentUser={mockCurrentUser()} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -21,7 +21,7 @@ import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper'; | |||
import { mockRawHotspot } from '../../../helpers/mocks/security-hotspots'; | |||
import { HotspotStatusFilters } from '../../../types/security-hotspots'; | |||
import { HotspotStatusFilter } from '../../../types/security-hotspots'; | |||
import SecurityHotspotsAppRenderer, { | |||
SecurityHotspotsAppRendererProps | |||
} from '../SecurityHotspotsAppRenderer'; | |||
@@ -54,11 +54,11 @@ function shallowRender(props: Partial<SecurityHotspotsAppRendererProps> = {}) { | |||
<SecurityHotspotsAppRenderer | |||
hotspots={[]} | |||
loading={false} | |||
onChangeStatusFilter={jest.fn()} | |||
onChangeFilters={jest.fn()} | |||
onHotspotClick={jest.fn()} | |||
onUpdateHotspot={jest.fn()} | |||
securityCategories={{}} | |||
statusFilter={HotspotStatusFilters.TO_REVIEW} | |||
filters={{ assignedToMe: false, status: HotspotStatusFilter.TO_REVIEW }} | |||
{...props} | |||
/> | |||
); |
@@ -10,12 +10,17 @@ exports[`should render correctly 1`] = ` | |||
"name": "branch-6.7", | |||
} | |||
} | |||
filters={ | |||
Object { | |||
"assignedToMe": false, | |||
"status": "TO_REVIEW", | |||
} | |||
} | |||
hotspots={Array []} | |||
loading={true} | |||
onChangeStatusFilter={[Function]} | |||
onChangeFilters={[Function]} | |||
onHotspotClick={[Function]} | |||
onUpdateHotspot={[Function]} | |||
securityCategories={Object {}} | |||
statusFilter="TO_REVIEW" | |||
/> | |||
`; |
@@ -4,9 +4,14 @@ exports[`should render correctly 1`] = ` | |||
<div | |||
id="security_hotspots" | |||
> | |||
<FilterBar | |||
onChangeStatus={[MockFunction]} | |||
statusFilter="TO_REVIEW" | |||
<Connect(withCurrentUser(FilterBar)) | |||
filters={ | |||
Object { | |||
"assignedToMe": false, | |||
"status": "TO_REVIEW", | |||
} | |||
} | |||
onChangeFilters={[MockFunction]} | |||
/> | |||
<ScreenPositionHelper> | |||
<Component /> |
@@ -18,36 +18,66 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import RadioToggle from 'sonar-ui-common/components/controls/RadioToggle'; | |||
import Select from 'sonar-ui-common/components/controls/Select'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import { HotspotStatusFilters } from '../../../types/security-hotspots'; | |||
import { withCurrentUser } from '../../../components/hoc/withCurrentUser'; | |||
import { isLoggedIn } from '../../../helpers/users'; | |||
import { HotspotFilters, HotspotStatusFilter } from '../../../types/security-hotspots'; | |||
export interface FilterBarProps { | |||
onChangeStatus: (status: HotspotStatusFilters) => void; | |||
statusFilter: HotspotStatusFilters; | |||
currentUser: T.CurrentUser; | |||
filters: HotspotFilters; | |||
onChangeFilters: (filters: Partial<HotspotFilters>) => void; | |||
} | |||
const statusOptions: Array<{ label: string; value: string }> = [ | |||
{ label: translate('hotspot.filters.status.to_review'), value: HotspotStatusFilters.TO_REVIEW }, | |||
{ label: translate('hotspot.filters.status.fixed'), value: HotspotStatusFilters.FIXED }, | |||
{ label: translate('hotspot.filters.status.safe'), value: HotspotStatusFilters.SAFE } | |||
{ value: HotspotStatusFilter.TO_REVIEW, label: translate('hotspot.filters.status.to_review') }, | |||
{ value: HotspotStatusFilter.FIXED, label: translate('hotspot.filters.status.fixed') }, | |||
{ value: HotspotStatusFilter.SAFE, label: translate('hotspot.filters.status.safe') } | |||
]; | |||
export default function FilterBar(props: FilterBarProps) { | |||
const { statusFilter } = props; | |||
export enum AssigneeFilterOption { | |||
ALL = 'all', | |||
ME = 'me' | |||
} | |||
const assigneeFilterOptions = [ | |||
{ value: AssigneeFilterOption.ME, label: translate('hotspot.filters.assignee.assigned_to_me') }, | |||
{ value: AssigneeFilterOption.ALL, label: translate('hotspot.filters.assignee.all') } | |||
]; | |||
export function FilterBar(props: FilterBarProps) { | |||
const { currentUser, filters } = props; | |||
return ( | |||
<div className="filter-bar display-flex-center"> | |||
<h3 className="big-spacer-right">{translate('hotspot.filters.title')}</h3> | |||
<h3 className="huge-spacer-right">{translate('hotspot.filters.title')}</h3> | |||
{isLoggedIn(currentUser) && ( | |||
<RadioToggle | |||
className="huge-spacer-right" | |||
name="assignee-filter" | |||
onCheck={(value: AssigneeFilterOption) => | |||
props.onChangeFilters({ assignedToMe: value === AssigneeFilterOption.ME }) | |||
} | |||
options={assigneeFilterOptions} | |||
value={filters.assignedToMe ? AssigneeFilterOption.ME : AssigneeFilterOption.ALL} | |||
/> | |||
)} | |||
<span className="spacer-right">{translate('status')}</span> | |||
<Select | |||
className="input-medium big-spacer-right" | |||
clearable={false} | |||
onChange={(option: { value: HotspotStatusFilters }) => props.onChangeStatus(option.value)} | |||
onChange={(option: { value: HotspotStatusFilter }) => | |||
props.onChangeFilters({ status: option.value }) | |||
} | |||
options={statusOptions} | |||
searchable={false} | |||
value={statusFilter} | |||
value={filters.status} | |||
/> | |||
</div> | |||
); | |||
} | |||
export default withCurrentUser(FilterBar); |
@@ -23,7 +23,7 @@ import { | |||
HotspotResolution, | |||
HotspotSetStatusRequest, | |||
HotspotStatus, | |||
HotspotStatusOptions, | |||
HotspotStatusOption, | |||
HotspotUpdateFields | |||
} from '../../../types/security-hotspots'; | |||
import HotspotActionsFormRenderer from './HotspotActionsFormRenderer'; | |||
@@ -35,19 +35,19 @@ interface Props { | |||
interface State { | |||
comment: string; | |||
selectedOption: HotspotStatusOption; | |||
selectedUser?: T.UserActive; | |||
selectedOption: HotspotStatusOptions; | |||
submitting: boolean; | |||
} | |||
export default class HotspotActionsForm extends React.Component<Props, State> { | |||
state: State = { | |||
comment: '', | |||
selectedOption: HotspotStatusOptions.FIXED, | |||
selectedOption: HotspotStatusOption.FIXED, | |||
submitting: false | |||
}; | |||
handleSelectOption = (selectedOption: HotspotStatusOptions) => { | |||
handleSelectOption = (selectedOption: HotspotStatusOption) => { | |||
this.setState({ selectedOption }); | |||
}; | |||
@@ -66,24 +66,25 @@ export default class HotspotActionsForm extends React.Component<Props, State> { | |||
const { comment, selectedOption, selectedUser } = this.state; | |||
const status = | |||
selectedOption === HotspotStatusOptions.ADDITIONAL_REVIEW | |||
selectedOption === HotspotStatusOption.ADDITIONAL_REVIEW | |||
? HotspotStatus.TO_REVIEW | |||
: HotspotStatus.REVIEWED; | |||
const data: HotspotSetStatusRequest = { status }; | |||
// If reassigning, ignore comment for status update. It will be sent with the reassignment below | |||
if (comment && !(selectedOption === HotspotStatusOptions.ADDITIONAL_REVIEW && selectedUser)) { | |||
if (comment && !(selectedOption === HotspotStatusOption.ADDITIONAL_REVIEW && selectedUser)) { | |||
data.comment = comment; | |||
} | |||
if (selectedOption !== HotspotStatusOptions.ADDITIONAL_REVIEW) { | |||
if (selectedOption !== HotspotStatusOption.ADDITIONAL_REVIEW) { | |||
data.resolution = HotspotResolution[selectedOption]; | |||
} | |||
this.setState({ submitting: true }); | |||
return setSecurityHotspotStatus(hotspotKey, data) | |||
.then(() => { | |||
if (selectedOption === HotspotStatusOptions.ADDITIONAL_REVIEW && selectedUser) { | |||
if (selectedOption === HotspotStatusOption.ADDITIONAL_REVIEW && selectedUser) { | |||
return this.assignHotspot(selectedUser, comment); | |||
} | |||
return null; |
@@ -22,7 +22,7 @@ import { SubmitButton } from 'sonar-ui-common/components/controls/buttons'; | |||
import Radio from 'sonar-ui-common/components/controls/Radio'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import MarkdownTips from '../../../components/common/MarkdownTips'; | |||
import { HotspotStatusOptions } from '../../../types/security-hotspots'; | |||
import { HotspotStatusOption } from '../../../types/security-hotspots'; | |||
import HotspotAssigneeSelect from './HotspotAssigneeSelect'; | |||
export interface HotspotActionsFormRendererProps { | |||
@@ -30,9 +30,9 @@ export interface HotspotActionsFormRendererProps { | |||
hotspotKey: string; | |||
onAssign: (user: T.UserActive) => void; | |||
onChangeComment: (comment: string) => void; | |||
onSelectOption: (option: HotspotStatusOptions) => void; | |||
onSelectOption: (option: HotspotStatusOption) => void; | |||
onSubmit: (event: React.SyntheticEvent<HTMLFormElement>) => void; | |||
selectedOption: HotspotStatusOptions; | |||
selectedOption: HotspotStatusOption; | |||
selectedUser?: T.UserActive; | |||
submitting: boolean; | |||
} | |||
@@ -45,22 +45,22 @@ export default function HotspotActionsFormRenderer(props: HotspotActionsFormRend | |||
<h2>{translate('hotspots.form.title')}</h2> | |||
<div className="display-flex-column big-spacer-bottom"> | |||
{renderOption({ | |||
option: HotspotStatusOptions.FIXED, | |||
option: HotspotStatusOption.FIXED, | |||
selectedOption, | |||
onClick: props.onSelectOption | |||
})} | |||
{renderOption({ | |||
option: HotspotStatusOptions.SAFE, | |||
option: HotspotStatusOption.SAFE, | |||
selectedOption, | |||
onClick: props.onSelectOption | |||
})} | |||
{renderOption({ | |||
option: HotspotStatusOptions.ADDITIONAL_REVIEW, | |||
option: HotspotStatusOption.ADDITIONAL_REVIEW, | |||
selectedOption, | |||
onClick: props.onSelectOption | |||
})} | |||
</div> | |||
{selectedOption === HotspotStatusOptions.ADDITIONAL_REVIEW && ( | |||
{selectedOption === HotspotStatusOption.ADDITIONAL_REVIEW && ( | |||
<div className="form-field huge-spacer-left"> | |||
<label>{translate('hotspots.form.assign_to')}</label> | |||
<HotspotAssigneeSelect onSelect={props.onAssign} /> | |||
@@ -75,7 +75,7 @@ export default function HotspotActionsFormRenderer(props: HotspotActionsFormRend | |||
props.onChangeComment(event.currentTarget.value) | |||
} | |||
placeholder={ | |||
selectedOption === HotspotStatusOptions.SAFE | |||
selectedOption === HotspotStatusOption.SAFE | |||
? translate('hotspots.form.comment.placeholder') | |||
: '' | |||
} | |||
@@ -93,9 +93,9 @@ export default function HotspotActionsFormRenderer(props: HotspotActionsFormRend | |||
} | |||
function renderOption(params: { | |||
option: HotspotStatusOptions; | |||
onClick: (option: HotspotStatusOptions) => void; | |||
selectedOption: HotspotStatusOptions; | |||
option: HotspotStatusOption; | |||
onClick: (option: HotspotStatusOption) => void; | |||
selectedOption: HotspotStatusOption; | |||
}) { | |||
const { onClick, option, selectedOption } = params; | |||
return ( |
@@ -22,7 +22,7 @@ import { groupBy } from 'lodash'; | |||
import * as React from 'react'; | |||
import SecurityHotspotIcon from 'sonar-ui-common/components/icons/SecurityHotspotIcon'; | |||
import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n'; | |||
import { HotspotStatusFilters, RawHotspot, RiskExposure } from '../../../types/security-hotspots'; | |||
import { HotspotStatusFilter, RawHotspot, RiskExposure } from '../../../types/security-hotspots'; | |||
import { groupByCategory, RISK_EXPOSURE_LEVELS } from '../utils'; | |||
import HotspotCategory from './HotspotCategory'; | |||
import './HotspotList.css'; | |||
@@ -32,7 +32,7 @@ export interface HotspotListProps { | |||
onHotspotClick: (key: string) => void; | |||
securityCategories: T.StandardSecurityCategories; | |||
selectedHotspotKey: string | undefined; | |||
statusFilter: HotspotStatusFilters; | |||
statusFilter: HotspotStatusFilter; | |||
} | |||
export default function HotspotList(props: HotspotListProps) { |
@@ -19,32 +19,49 @@ | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import RadioToggle from 'sonar-ui-common/components/controls/RadioToggle'; | |||
import Select from 'sonar-ui-common/components/controls/Select'; | |||
import { HotspotStatusFilters } from '../../../../types/security-hotspots'; | |||
import FilterBar, { FilterBarProps } from '../FilterBar'; | |||
import { mockCurrentUser, mockLoggedInUser } from '../../../../helpers/testMocks'; | |||
import { HotspotStatusFilter } from '../../../../types/security-hotspots'; | |||
import { AssigneeFilterOption, FilterBar, FilterBarProps } from '../FilterBar'; | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
expect(shallowRender()).toMatchSnapshot('anonymous'); | |||
expect(shallowRender({ currentUser: mockLoggedInUser() })).toMatchSnapshot('logged-in'); | |||
}); | |||
it('should trigger onChange', () => { | |||
const onChangeStatus = jest.fn(); | |||
const wrapper = shallowRender({ onChangeStatus }); | |||
it('should trigger onChange for status', () => { | |||
const onChangeFilters = jest.fn(); | |||
const wrapper = shallowRender({ onChangeFilters }); | |||
const { onChange } = wrapper.find(Select).props(); | |||
if (!onChange) { | |||
return fail("Select's onChange should be defined"); | |||
} | |||
onChange({ value: HotspotStatusFilters.SAFE }); | |||
expect(onChangeStatus).toBeCalledWith(HotspotStatusFilters.SAFE); | |||
onChange({ value: HotspotStatusFilter.SAFE }); | |||
expect(onChangeFilters).toBeCalledWith({ status: HotspotStatusFilter.SAFE }); | |||
}); | |||
it('should trigger onChange for self-assigned toggle', () => { | |||
const onChangeFilters = jest.fn(); | |||
const wrapper = shallowRender({ currentUser: mockLoggedInUser(), onChangeFilters }); | |||
const { onCheck } = wrapper.find(RadioToggle).props(); | |||
if (!onCheck) { | |||
return fail("RadioToggle's onCheck should be defined"); | |||
} | |||
onCheck(AssigneeFilterOption.ALL); | |||
expect(onChangeFilters).toBeCalledWith({ assignedToMe: false }); | |||
}); | |||
function shallowRender(props: Partial<FilterBarProps> = {}) { | |||
return shallow( | |||
<FilterBar | |||
onChangeStatus={jest.fn()} | |||
statusFilter={HotspotStatusFilters.TO_REVIEW} | |||
currentUser={mockCurrentUser()} | |||
onChangeFilters={jest.fn()} | |||
filters={{ assignedToMe: false, status: HotspotStatusFilter.TO_REVIEW }} | |||
{...props} | |||
/> | |||
); |
@@ -25,7 +25,7 @@ import { mockLoggedInUser } from '../../../../helpers/testMocks'; | |||
import { | |||
HotspotResolution, | |||
HotspotStatus, | |||
HotspotStatusOptions | |||
HotspotStatusOption | |||
} from '../../../../types/security-hotspots'; | |||
import HotspotActionsForm from '../HotspotActionsForm'; | |||
@@ -40,9 +40,9 @@ it('should render correctly', () => { | |||
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); | |||
expect(wrapper.state().selectedOption).toBe(HotspotStatusOption.FIXED); | |||
wrapper.instance().handleSelectOption(HotspotStatusOption.SAFE); | |||
expect(wrapper.state().selectedOption).toBe(HotspotStatusOption.SAFE); | |||
}); | |||
it('should handle comment change', () => { | |||
@@ -54,7 +54,7 @@ it('should handle comment change', () => { | |||
it('should handle submit', async () => { | |||
const onSubmit = jest.fn(); | |||
const wrapper = shallowRender({ onSubmit }); | |||
wrapper.setState({ selectedOption: HotspotStatusOptions.ADDITIONAL_REVIEW }); | |||
wrapper.setState({ selectedOption: HotspotStatusOption.ADDITIONAL_REVIEW }); | |||
await waitAndUpdate(wrapper); | |||
const preventDefault = jest.fn(); | |||
@@ -69,7 +69,7 @@ it('should handle submit', async () => { | |||
expect(onSubmit).toBeCalled(); | |||
// SAFE | |||
wrapper.setState({ comment: 'commentsafe', selectedOption: HotspotStatusOptions.SAFE }); | |||
wrapper.setState({ comment: 'commentsafe', selectedOption: HotspotStatusOption.SAFE }); | |||
await waitAndUpdate(wrapper); | |||
await wrapper.instance().handleSubmit({ preventDefault } as any); | |||
expect(setSecurityHotspotStatus).toBeCalledWith('key', { | |||
@@ -79,7 +79,7 @@ it('should handle submit', async () => { | |||
}); | |||
// FIXED | |||
wrapper.setState({ comment: 'commentFixed', selectedOption: HotspotStatusOptions.FIXED }); | |||
wrapper.setState({ comment: 'commentFixed', selectedOption: HotspotStatusOption.FIXED }); | |||
await waitAndUpdate(wrapper); | |||
await wrapper.instance().handleSubmit({ preventDefault } as any); | |||
expect(setSecurityHotspotStatus).toBeCalledWith('key', { | |||
@@ -94,8 +94,9 @@ it('should handle assignment', async () => { | |||
const wrapper = shallowRender({ onSubmit }); | |||
wrapper.setState({ | |||
comment: 'assignment comment', | |||
selectedOption: HotspotStatusOptions.ADDITIONAL_REVIEW | |||
selectedOption: HotspotStatusOption.ADDITIONAL_REVIEW | |||
}); | |||
wrapper.instance().handleAssign(mockLoggedInUser({ login: 'userLogin' })); | |||
await waitAndUpdate(wrapper); | |||
@@ -20,7 +20,7 @@ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { mockLoggedInUser } from '../../../../helpers/testMocks'; | |||
import { HotspotStatusOptions } from '../../../../types/security-hotspots'; | |||
import { HotspotStatusOption } from '../../../../types/security-hotspots'; | |||
import HotspotActionsForm from '../HotspotActionsForm'; | |||
import HotspotActionsFormRenderer, { | |||
HotspotActionsFormRendererProps | |||
@@ -29,12 +29,12 @@ import HotspotActionsFormRenderer, { | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
expect(shallowRender({ submitting: true })).toMatchSnapshot('Submitting'); | |||
expect(shallowRender({ selectedOption: HotspotStatusOptions.SAFE })).toMatchSnapshot( | |||
expect(shallowRender({ selectedOption: HotspotStatusOption.SAFE })).toMatchSnapshot( | |||
'safe option selected' | |||
); | |||
expect( | |||
shallowRender({ | |||
selectedOption: HotspotStatusOptions.ADDITIONAL_REVIEW, | |||
selectedOption: HotspotStatusOption.ADDITIONAL_REVIEW, | |||
selectedUser: mockLoggedInUser() | |||
}) | |||
).toMatchSnapshot('user selected'); | |||
@@ -49,7 +49,7 @@ function shallowRender(props: Partial<HotspotActionsFormRendererProps> = {}) { | |||
onChangeComment={jest.fn()} | |||
onSelectOption={jest.fn()} | |||
onSubmit={jest.fn()} | |||
selectedOption={HotspotStatusOptions.FIXED} | |||
selectedOption={HotspotStatusOption.FIXED} | |||
submitting={false} | |||
{...props} | |||
/> |
@@ -20,7 +20,7 @@ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { mockRawHotspot } from '../../../../helpers/mocks/security-hotspots'; | |||
import { HotspotStatusFilters, RiskExposure } from '../../../../types/security-hotspots'; | |||
import { HotspotStatusFilter, RiskExposure } from '../../../../types/security-hotspots'; | |||
import HotspotList, { HotspotListProps } from '../HotspotList'; | |||
it('should render correctly', () => { | |||
@@ -57,7 +57,7 @@ function shallowRender(props: Partial<HotspotListProps> = {}) { | |||
onHotspotClick={jest.fn()} | |||
securityCategories={{}} | |||
selectedHotspotKey="h2" | |||
statusFilter={HotspotStatusFilters.TO_REVIEW} | |||
statusFilter={HotspotStatusFilter.TO_REVIEW} | |||
{...props} | |||
/> | |||
); |
@@ -1,11 +1,11 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly 1`] = ` | |||
exports[`should render correctly: anonymous 1`] = ` | |||
<div | |||
className="filter-bar display-flex-center" | |||
> | |||
<h3 | |||
className="big-spacer-right" | |||
className="huge-spacer-right" | |||
> | |||
hotspot.filters.title | |||
</h3> | |||
@@ -39,3 +39,62 @@ exports[`should render correctly 1`] = ` | |||
/> | |||
</div> | |||
`; | |||
exports[`should render correctly: logged-in 1`] = ` | |||
<div | |||
className="filter-bar display-flex-center" | |||
> | |||
<h3 | |||
className="huge-spacer-right" | |||
> | |||
hotspot.filters.title | |||
</h3> | |||
<RadioToggle | |||
className="huge-spacer-right" | |||
disabled={false} | |||
name="assignee-filter" | |||
onCheck={[Function]} | |||
options={ | |||
Array [ | |||
Object { | |||
"label": "hotspot.filters.assignee.assigned_to_me", | |||
"value": "me", | |||
}, | |||
Object { | |||
"label": "hotspot.filters.assignee.all", | |||
"value": "all", | |||
}, | |||
] | |||
} | |||
value="all" | |||
/> | |||
<span | |||
className="spacer-right" | |||
> | |||
status | |||
</span> | |||
<Select | |||
className="input-medium big-spacer-right" | |||
clearable={false} | |||
onChange={[Function]} | |||
options={ | |||
Array [ | |||
Object { | |||
"label": "hotspot.filters.status.to_review", | |||
"value": "TO_REVIEW", | |||
}, | |||
Object { | |||
"label": "hotspot.filters.status.fixed", | |||
"value": "FIXED", | |||
}, | |||
Object { | |||
"label": "hotspot.filters.status.safe", | |||
"value": "SAFE", | |||
}, | |||
] | |||
} | |||
searchable={false} | |||
value="TO_REVIEW" | |||
/> | |||
</div> | |||
`; |
@@ -33,18 +33,23 @@ export enum HotspotResolution { | |||
SAFE = 'SAFE' | |||
} | |||
export enum HotspotStatusFilters { | |||
export enum HotspotStatusFilter { | |||
FIXED = 'FIXED', | |||
SAFE = 'SAFE', | |||
TO_REVIEW = 'TO_REVIEW' | |||
} | |||
export enum HotspotStatusOptions { | |||
export enum HotspotStatusOption { | |||
FIXED = 'FIXED', | |||
SAFE = 'SAFE', | |||
ADDITIONAL_REVIEW = 'ADDITIONAL_REVIEW' | |||
} | |||
export interface HotspotFilters { | |||
assignedToMe: boolean; | |||
status: HotspotStatusFilter; | |||
} | |||
export interface RawHotspot { | |||
assignee?: string; | |||
author?: string; |
@@ -667,6 +667,8 @@ hotspot.status.FIXED=Fixed | |||
hotspot.status.SAFE=Safe | |||
hotspot.filters.title=Filters | |||
hotspot.filters.assignee.assigned_to_me=Assigned to me | |||
hotspot.filters.assignee.all=All | |||
hotspot.filters.status.to_review=To review | |||
hotspot.filters.status.fixed=Reviewed as fixed | |||
hotspot.filters.status.safe=Reviewed as safe |