ps: number; | ps: number; | ||||
status?: HotspotStatus; | status?: HotspotStatus; | ||||
resolution?: HotspotResolution; | resolution?: HotspotResolution; | ||||
onlyMine?: boolean; | |||||
} & BranchParameters | } & BranchParameters | ||||
): Promise<HotspotSearchResponse> { | ): Promise<HotspotSearchResponse> { | ||||
return getJSON('/api/hotspots/search', data).catch(throwGlobalError); | return getJSON('/api/hotspots/search', data).catch(throwGlobalError); |
import * as React from 'react'; | import * as React from 'react'; | ||||
import { addNoFooterPageClass, removeNoFooterPageClass } from 'sonar-ui-common/helpers/pages'; | import { addNoFooterPageClass, removeNoFooterPageClass } from 'sonar-ui-common/helpers/pages'; | ||||
import { getSecurityHotspots } from '../../api/security-hotspots'; | import { getSecurityHotspots } from '../../api/security-hotspots'; | ||||
import { withCurrentUser } from '../../components/hoc/withCurrentUser'; | |||||
import { getBranchLikeQuery } from '../../helpers/branch-like'; | import { getBranchLikeQuery } from '../../helpers/branch-like'; | ||||
import { getStandards } from '../../helpers/security-standard'; | import { getStandards } from '../../helpers/security-standard'; | ||||
import { isLoggedIn } from '../../helpers/users'; | |||||
import { BranchLike } from '../../types/branch-like'; | import { BranchLike } from '../../types/branch-like'; | ||||
import { | import { | ||||
HotspotFilters, | |||||
HotspotResolution, | HotspotResolution, | ||||
HotspotStatus, | HotspotStatus, | ||||
HotspotStatusFilters, | |||||
HotspotStatusFilter, | |||||
HotspotUpdate, | HotspotUpdate, | ||||
RawHotspot | RawHotspot | ||||
} from '../../types/security-hotspots'; | } from '../../types/security-hotspots'; | ||||
interface Props { | interface Props { | ||||
branchLike?: BranchLike; | branchLike?: BranchLike; | ||||
currentUser: T.CurrentUser; | |||||
component: T.Component; | component: T.Component; | ||||
} | } | ||||
loading: boolean; | loading: boolean; | ||||
securityCategories: T.StandardSecurityCategories; | securityCategories: T.StandardSecurityCategories; | ||||
selectedHotspotKey: string | undefined; | 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; | 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() { | componentDidMount() { | ||||
this.mounted = true; | this.mounted = true; | ||||
fetchSecurityHotspots() { | fetchSecurityHotspots() { | ||||
const { branchLike, component } = this.props; | const { branchLike, component } = this.props; | ||||
const { statusFilter } = this.state; | |||||
const { filters } = this.state; | |||||
const status = | const status = | ||||
statusFilter === HotspotStatusFilters.TO_REVIEW | |||||
filters.status === HotspotStatusFilter.TO_REVIEW | |||||
? HotspotStatus.TO_REVIEW | ? HotspotStatus.TO_REVIEW | ||||
: HotspotStatus.REVIEWED; | : HotspotStatus.REVIEWED; | ||||
const resolution = | const resolution = | ||||
statusFilter === HotspotStatusFilters.TO_REVIEW ? undefined : HotspotResolution[statusFilter]; | |||||
filters.status === HotspotStatusFilter.TO_REVIEW | |||||
? undefined | |||||
: HotspotResolution[filters.status]; | |||||
return getSecurityHotspots({ | return getSecurityHotspots({ | ||||
projectKey: component.key, | projectKey: component.key, | ||||
ps: PAGE_SIZE, | ps: PAGE_SIZE, | ||||
status, | status, | ||||
resolution, | resolution, | ||||
onlyMine: filters.assignedToMe, | |||||
...getBranchLikeQuery(branchLike) | ...getBranchLikeQuery(branchLike) | ||||
}); | }); | ||||
} | } | ||||
.catch(this.handleCallFailure); | .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 }); | handleHotspotClick = (key: string) => this.setState({ selectedHotspotKey: key }); | ||||
render() { | render() { | ||||
const { branchLike } = this.props; | const { branchLike } = this.props; | ||||
const { hotspots, loading, securityCategories, selectedHotspotKey, statusFilter } = this.state; | |||||
const { hotspots, loading, securityCategories, selectedHotspotKey, filters } = this.state; | |||||
return ( | return ( | ||||
<SecurityHotspotsAppRenderer | <SecurityHotspotsAppRenderer | ||||
branchLike={branchLike} | branchLike={branchLike} | ||||
filters={filters} | |||||
hotspots={hotspots} | hotspots={hotspots} | ||||
loading={loading} | loading={loading} | ||||
onChangeStatusFilter={this.handleChangeStatusFilter} | |||||
onChangeFilters={this.handleChangeFilters} | |||||
onHotspotClick={this.handleHotspotClick} | onHotspotClick={this.handleHotspotClick} | ||||
onUpdateHotspot={this.handleHotspotUpdate} | onUpdateHotspot={this.handleHotspotUpdate} | ||||
securityCategories={securityCategories} | securityCategories={securityCategories} | ||||
selectedHotspotKey={selectedHotspotKey} | selectedHotspotKey={selectedHotspotKey} | ||||
statusFilter={statusFilter} | |||||
/> | /> | ||||
); | ); | ||||
} | } | ||||
} | } | ||||
export default withCurrentUser(SecurityHotspotsApp); |
import Suggestions from '../../app/components/embed-docs-modal/Suggestions'; | import Suggestions from '../../app/components/embed-docs-modal/Suggestions'; | ||||
import ScreenPositionHelper from '../../components/common/ScreenPositionHelper'; | import ScreenPositionHelper from '../../components/common/ScreenPositionHelper'; | ||||
import { BranchLike } from '../../types/branch-like'; | 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 FilterBar from './components/FilterBar'; | ||||
import HotspotList from './components/HotspotList'; | import HotspotList from './components/HotspotList'; | ||||
import HotspotViewer from './components/HotspotViewer'; | import HotspotViewer from './components/HotspotViewer'; | ||||
export interface SecurityHotspotsAppRendererProps { | export interface SecurityHotspotsAppRendererProps { | ||||
branchLike?: BranchLike; | branchLike?: BranchLike; | ||||
filters: HotspotFilters; | |||||
hotspots: RawHotspot[]; | hotspots: RawHotspot[]; | ||||
loading: boolean; | loading: boolean; | ||||
onChangeStatusFilter: (status: HotspotStatusFilters) => void; | |||||
onChangeFilters: (filters: Partial<HotspotFilters>) => void; | |||||
onHotspotClick: (key: string) => void; | onHotspotClick: (key: string) => void; | ||||
onUpdateHotspot: (hotspot: HotspotUpdate) => void; | onUpdateHotspot: (hotspot: HotspotUpdate) => void; | ||||
selectedHotspotKey?: string; | selectedHotspotKey?: string; | ||||
securityCategories: T.StandardSecurityCategories; | securityCategories: T.StandardSecurityCategories; | ||||
statusFilter: HotspotStatusFilters; | |||||
} | } | ||||
export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRendererProps) { | export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRendererProps) { | ||||
const { | |||||
branchLike, | |||||
hotspots, | |||||
loading, | |||||
securityCategories, | |||||
selectedHotspotKey, | |||||
statusFilter | |||||
} = props; | |||||
const { branchLike, hotspots, loading, securityCategories, selectedHotspotKey, filters } = props; | |||||
return ( | return ( | ||||
<div id="security_hotspots"> | <div id="security_hotspots"> | ||||
<FilterBar onChangeStatus={props.onChangeStatusFilter} statusFilter={statusFilter} /> | |||||
<FilterBar onChangeFilters={props.onChangeFilters} filters={filters} /> | |||||
<ScreenPositionHelper> | <ScreenPositionHelper> | ||||
{({ top }) => ( | {({ top }) => ( | ||||
<div className="wrapper" style={{ top }}> | <div className="wrapper" style={{ top }}> | ||||
onHotspotClick={props.onHotspotClick} | onHotspotClick={props.onHotspotClick} | ||||
securityCategories={securityCategories} | securityCategories={securityCategories} | ||||
selectedHotspotKey={selectedHotspotKey} | selectedHotspotKey={selectedHotspotKey} | ||||
statusFilter={statusFilter} | |||||
statusFilter={filters.status} | |||||
/> | /> | ||||
</div> | </div> | ||||
<div className="main"> | <div className="main"> |
import { mockBranch } from '../../../helpers/mocks/branch-like'; | import { mockBranch } from '../../../helpers/mocks/branch-like'; | ||||
import { mockRawHotspot } from '../../../helpers/mocks/security-hotspots'; | import { mockRawHotspot } from '../../../helpers/mocks/security-hotspots'; | ||||
import { getStandards } from '../../../helpers/security-standard'; | import { getStandards } from '../../../helpers/security-standard'; | ||||
import { mockComponent } from '../../../helpers/testMocks'; | |||||
import { mockComponent, mockCurrentUser } from '../../../helpers/testMocks'; | |||||
import { | import { | ||||
HotspotResolution, | HotspotResolution, | ||||
HotspotStatus, | HotspotStatus, | ||||
HotspotStatusFilters | |||||
HotspotStatusFilter | |||||
} from '../../../types/security-hotspots'; | } from '../../../types/security-hotspots'; | ||||
import SecurityHotspotsApp from '../SecurityHotspotsApp'; | |||||
import { SecurityHotspotsApp } from '../SecurityHotspotsApp'; | |||||
import SecurityHotspotsAppRenderer from '../SecurityHotspotsAppRenderer'; | import SecurityHotspotsAppRenderer from '../SecurityHotspotsAppRenderer'; | ||||
jest.mock('sonar-ui-common/helpers/pages', () => ({ | jest.mock('sonar-ui-common/helpers/pages', () => ({ | ||||
await waitAndUpdate(wrapper); | await waitAndUpdate(wrapper); | ||||
// Set filter to SAFE: | // Set filter to SAFE: | ||||
wrapper.instance().handleChangeStatusFilter(HotspotStatusFilters.SAFE); | |||||
wrapper.instance().handleChangeFilters({ status: HotspotStatusFilter.SAFE }); | |||||
expect(getSecurityHotspots).toBeCalledWith( | expect(getSecurityHotspots).toBeCalledWith( | ||||
expect.objectContaining({ status: HotspotStatus.REVIEWED, resolution: HotspotResolution.SAFE }) | expect.objectContaining({ status: HotspotStatus.REVIEWED, resolution: HotspotResolution.SAFE }) | ||||
expect(wrapper.state().hotspots[0]).toBe(hotspots2[0]); | expect(wrapper.state().hotspots[0]).toBe(hotspots2[0]); | ||||
// Set filter to FIXED | // Set filter to FIXED | ||||
wrapper.instance().handleChangeStatusFilter(HotspotStatusFilters.FIXED); | |||||
wrapper.instance().handleChangeFilters({ status: HotspotStatusFilter.FIXED }); | |||||
expect(getSecurityHotspots).toBeCalledWith( | expect(getSecurityHotspots).toBeCalledWith( | ||||
expect.objectContaining({ status: HotspotStatus.REVIEWED, resolution: HotspotResolution.FIXED }) | expect.objectContaining({ status: HotspotStatus.REVIEWED, resolution: HotspotResolution.FIXED }) | ||||
function shallowRender(props: Partial<SecurityHotspotsApp['props']> = {}) { | function shallowRender(props: Partial<SecurityHotspotsApp['props']> = {}) { | ||||
return shallow<SecurityHotspotsApp>( | return shallow<SecurityHotspotsApp>( | ||||
<SecurityHotspotsApp branchLike={branch} component={mockComponent()} {...props} /> | |||||
<SecurityHotspotsApp | |||||
branchLike={branch} | |||||
component={mockComponent()} | |||||
currentUser={mockCurrentUser()} | |||||
{...props} | |||||
/> | |||||
); | ); | ||||
} | } |
import * as React from 'react'; | import * as React from 'react'; | ||||
import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper'; | import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper'; | ||||
import { mockRawHotspot } from '../../../helpers/mocks/security-hotspots'; | import { mockRawHotspot } from '../../../helpers/mocks/security-hotspots'; | ||||
import { HotspotStatusFilters } from '../../../types/security-hotspots'; | |||||
import { HotspotStatusFilter } from '../../../types/security-hotspots'; | |||||
import SecurityHotspotsAppRenderer, { | import SecurityHotspotsAppRenderer, { | ||||
SecurityHotspotsAppRendererProps | SecurityHotspotsAppRendererProps | ||||
} from '../SecurityHotspotsAppRenderer'; | } from '../SecurityHotspotsAppRenderer'; | ||||
<SecurityHotspotsAppRenderer | <SecurityHotspotsAppRenderer | ||||
hotspots={[]} | hotspots={[]} | ||||
loading={false} | loading={false} | ||||
onChangeStatusFilter={jest.fn()} | |||||
onChangeFilters={jest.fn()} | |||||
onHotspotClick={jest.fn()} | onHotspotClick={jest.fn()} | ||||
onUpdateHotspot={jest.fn()} | onUpdateHotspot={jest.fn()} | ||||
securityCategories={{}} | securityCategories={{}} | ||||
statusFilter={HotspotStatusFilters.TO_REVIEW} | |||||
filters={{ assignedToMe: false, status: HotspotStatusFilter.TO_REVIEW }} | |||||
{...props} | {...props} | ||||
/> | /> | ||||
); | ); |
"name": "branch-6.7", | "name": "branch-6.7", | ||||
} | } | ||||
} | } | ||||
filters={ | |||||
Object { | |||||
"assignedToMe": false, | |||||
"status": "TO_REVIEW", | |||||
} | |||||
} | |||||
hotspots={Array []} | hotspots={Array []} | ||||
loading={true} | loading={true} | ||||
onChangeStatusFilter={[Function]} | |||||
onChangeFilters={[Function]} | |||||
onHotspotClick={[Function]} | onHotspotClick={[Function]} | ||||
onUpdateHotspot={[Function]} | onUpdateHotspot={[Function]} | ||||
securityCategories={Object {}} | securityCategories={Object {}} | ||||
statusFilter="TO_REVIEW" | |||||
/> | /> | ||||
`; | `; |
<div | <div | ||||
id="security_hotspots" | id="security_hotspots" | ||||
> | > | ||||
<FilterBar | |||||
onChangeStatus={[MockFunction]} | |||||
statusFilter="TO_REVIEW" | |||||
<Connect(withCurrentUser(FilterBar)) | |||||
filters={ | |||||
Object { | |||||
"assignedToMe": false, | |||||
"status": "TO_REVIEW", | |||||
} | |||||
} | |||||
onChangeFilters={[MockFunction]} | |||||
/> | /> | ||||
<ScreenPositionHelper> | <ScreenPositionHelper> | ||||
<Component /> | <Component /> |
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | ||||
*/ | */ | ||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import RadioToggle from 'sonar-ui-common/components/controls/RadioToggle'; | |||||
import Select from 'sonar-ui-common/components/controls/Select'; | import Select from 'sonar-ui-common/components/controls/Select'; | ||||
import { translate } from 'sonar-ui-common/helpers/l10n'; | 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 { | 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 }> = [ | 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 ( | return ( | ||||
<div className="filter-bar display-flex-center"> | <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> | <span className="spacer-right">{translate('status')}</span> | ||||
<Select | <Select | ||||
className="input-medium big-spacer-right" | className="input-medium big-spacer-right" | ||||
clearable={false} | clearable={false} | ||||
onChange={(option: { value: HotspotStatusFilters }) => props.onChangeStatus(option.value)} | |||||
onChange={(option: { value: HotspotStatusFilter }) => | |||||
props.onChangeFilters({ status: option.value }) | |||||
} | |||||
options={statusOptions} | options={statusOptions} | ||||
searchable={false} | searchable={false} | ||||
value={statusFilter} | |||||
value={filters.status} | |||||
/> | /> | ||||
</div> | </div> | ||||
); | ); | ||||
} | } | ||||
export default withCurrentUser(FilterBar); |
HotspotResolution, | HotspotResolution, | ||||
HotspotSetStatusRequest, | HotspotSetStatusRequest, | ||||
HotspotStatus, | HotspotStatus, | ||||
HotspotStatusOptions, | |||||
HotspotStatusOption, | |||||
HotspotUpdateFields | HotspotUpdateFields | ||||
} from '../../../types/security-hotspots'; | } from '../../../types/security-hotspots'; | ||||
import HotspotActionsFormRenderer from './HotspotActionsFormRenderer'; | import HotspotActionsFormRenderer from './HotspotActionsFormRenderer'; | ||||
interface State { | interface State { | ||||
comment: string; | comment: string; | ||||
selectedOption: HotspotStatusOption; | |||||
selectedUser?: T.UserActive; | selectedUser?: T.UserActive; | ||||
selectedOption: HotspotStatusOptions; | |||||
submitting: boolean; | submitting: boolean; | ||||
} | } | ||||
export default class HotspotActionsForm extends React.Component<Props, State> { | export default class HotspotActionsForm extends React.Component<Props, State> { | ||||
state: State = { | state: State = { | ||||
comment: '', | comment: '', | ||||
selectedOption: HotspotStatusOptions.FIXED, | |||||
selectedOption: HotspotStatusOption.FIXED, | |||||
submitting: false | submitting: false | ||||
}; | }; | ||||
handleSelectOption = (selectedOption: HotspotStatusOptions) => { | |||||
handleSelectOption = (selectedOption: HotspotStatusOption) => { | |||||
this.setState({ selectedOption }); | this.setState({ selectedOption }); | ||||
}; | }; | ||||
const { comment, selectedOption, selectedUser } = this.state; | const { comment, selectedOption, selectedUser } = this.state; | ||||
const status = | const status = | ||||
selectedOption === HotspotStatusOptions.ADDITIONAL_REVIEW | |||||
selectedOption === HotspotStatusOption.ADDITIONAL_REVIEW | |||||
? HotspotStatus.TO_REVIEW | ? HotspotStatus.TO_REVIEW | ||||
: HotspotStatus.REVIEWED; | : HotspotStatus.REVIEWED; | ||||
const data: HotspotSetStatusRequest = { status }; | const data: HotspotSetStatusRequest = { status }; | ||||
// If reassigning, ignore comment for status update. It will be sent with the reassignment below | // 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; | data.comment = comment; | ||||
} | } | ||||
if (selectedOption !== HotspotStatusOptions.ADDITIONAL_REVIEW) { | |||||
if (selectedOption !== HotspotStatusOption.ADDITIONAL_REVIEW) { | |||||
data.resolution = HotspotResolution[selectedOption]; | data.resolution = HotspotResolution[selectedOption]; | ||||
} | } | ||||
this.setState({ submitting: true }); | this.setState({ submitting: true }); | ||||
return setSecurityHotspotStatus(hotspotKey, data) | return setSecurityHotspotStatus(hotspotKey, data) | ||||
.then(() => { | .then(() => { | ||||
if (selectedOption === HotspotStatusOptions.ADDITIONAL_REVIEW && selectedUser) { | |||||
if (selectedOption === HotspotStatusOption.ADDITIONAL_REVIEW && selectedUser) { | |||||
return this.assignHotspot(selectedUser, comment); | return this.assignHotspot(selectedUser, comment); | ||||
} | } | ||||
return null; | return null; |
import Radio from 'sonar-ui-common/components/controls/Radio'; | import Radio from 'sonar-ui-common/components/controls/Radio'; | ||||
import { translate } from 'sonar-ui-common/helpers/l10n'; | import { translate } from 'sonar-ui-common/helpers/l10n'; | ||||
import MarkdownTips from '../../../components/common/MarkdownTips'; | import MarkdownTips from '../../../components/common/MarkdownTips'; | ||||
import { HotspotStatusOptions } from '../../../types/security-hotspots'; | |||||
import { HotspotStatusOption } from '../../../types/security-hotspots'; | |||||
import HotspotAssigneeSelect from './HotspotAssigneeSelect'; | import HotspotAssigneeSelect from './HotspotAssigneeSelect'; | ||||
export interface HotspotActionsFormRendererProps { | export interface HotspotActionsFormRendererProps { | ||||
hotspotKey: string; | hotspotKey: string; | ||||
onAssign: (user: T.UserActive) => void; | onAssign: (user: T.UserActive) => void; | ||||
onChangeComment: (comment: string) => void; | onChangeComment: (comment: string) => void; | ||||
onSelectOption: (option: HotspotStatusOptions) => void; | |||||
onSelectOption: (option: HotspotStatusOption) => void; | |||||
onSubmit: (event: React.SyntheticEvent<HTMLFormElement>) => void; | onSubmit: (event: React.SyntheticEvent<HTMLFormElement>) => void; | ||||
selectedOption: HotspotStatusOptions; | |||||
selectedOption: HotspotStatusOption; | |||||
selectedUser?: T.UserActive; | selectedUser?: T.UserActive; | ||||
submitting: boolean; | submitting: boolean; | ||||
} | } | ||||
<h2>{translate('hotspots.form.title')}</h2> | <h2>{translate('hotspots.form.title')}</h2> | ||||
<div className="display-flex-column big-spacer-bottom"> | <div className="display-flex-column big-spacer-bottom"> | ||||
{renderOption({ | {renderOption({ | ||||
option: HotspotStatusOptions.FIXED, | |||||
option: HotspotStatusOption.FIXED, | |||||
selectedOption, | selectedOption, | ||||
onClick: props.onSelectOption | onClick: props.onSelectOption | ||||
})} | })} | ||||
{renderOption({ | {renderOption({ | ||||
option: HotspotStatusOptions.SAFE, | |||||
option: HotspotStatusOption.SAFE, | |||||
selectedOption, | selectedOption, | ||||
onClick: props.onSelectOption | onClick: props.onSelectOption | ||||
})} | })} | ||||
{renderOption({ | {renderOption({ | ||||
option: HotspotStatusOptions.ADDITIONAL_REVIEW, | |||||
option: HotspotStatusOption.ADDITIONAL_REVIEW, | |||||
selectedOption, | selectedOption, | ||||
onClick: props.onSelectOption | onClick: props.onSelectOption | ||||
})} | })} | ||||
</div> | </div> | ||||
{selectedOption === HotspotStatusOptions.ADDITIONAL_REVIEW && ( | |||||
{selectedOption === HotspotStatusOption.ADDITIONAL_REVIEW && ( | |||||
<div className="form-field huge-spacer-left"> | <div className="form-field huge-spacer-left"> | ||||
<label>{translate('hotspots.form.assign_to')}</label> | <label>{translate('hotspots.form.assign_to')}</label> | ||||
<HotspotAssigneeSelect onSelect={props.onAssign} /> | <HotspotAssigneeSelect onSelect={props.onAssign} /> | ||||
props.onChangeComment(event.currentTarget.value) | props.onChangeComment(event.currentTarget.value) | ||||
} | } | ||||
placeholder={ | placeholder={ | ||||
selectedOption === HotspotStatusOptions.SAFE | |||||
selectedOption === HotspotStatusOption.SAFE | |||||
? translate('hotspots.form.comment.placeholder') | ? translate('hotspots.form.comment.placeholder') | ||||
: '' | : '' | ||||
} | } | ||||
} | } | ||||
function renderOption(params: { | function renderOption(params: { | ||||
option: HotspotStatusOptions; | |||||
onClick: (option: HotspotStatusOptions) => void; | |||||
selectedOption: HotspotStatusOptions; | |||||
option: HotspotStatusOption; | |||||
onClick: (option: HotspotStatusOption) => void; | |||||
selectedOption: HotspotStatusOption; | |||||
}) { | }) { | ||||
const { onClick, option, selectedOption } = params; | const { onClick, option, selectedOption } = params; | ||||
return ( | return ( |
import * as React from 'react'; | import * as React from 'react'; | ||||
import SecurityHotspotIcon from 'sonar-ui-common/components/icons/SecurityHotspotIcon'; | import SecurityHotspotIcon from 'sonar-ui-common/components/icons/SecurityHotspotIcon'; | ||||
import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n'; | 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 { groupByCategory, RISK_EXPOSURE_LEVELS } from '../utils'; | ||||
import HotspotCategory from './HotspotCategory'; | import HotspotCategory from './HotspotCategory'; | ||||
import './HotspotList.css'; | import './HotspotList.css'; | ||||
onHotspotClick: (key: string) => void; | onHotspotClick: (key: string) => void; | ||||
securityCategories: T.StandardSecurityCategories; | securityCategories: T.StandardSecurityCategories; | ||||
selectedHotspotKey: string | undefined; | selectedHotspotKey: string | undefined; | ||||
statusFilter: HotspotStatusFilters; | |||||
statusFilter: HotspotStatusFilter; | |||||
} | } | ||||
export default function HotspotList(props: HotspotListProps) { | export default function HotspotList(props: HotspotListProps) { |
*/ | */ | ||||
import { shallow } from 'enzyme'; | import { shallow } from 'enzyme'; | ||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import RadioToggle from 'sonar-ui-common/components/controls/RadioToggle'; | |||||
import Select from 'sonar-ui-common/components/controls/Select'; | 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', () => { | 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(); | const { onChange } = wrapper.find(Select).props(); | ||||
if (!onChange) { | if (!onChange) { | ||||
return fail("Select's onChange should be defined"); | 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> = {}) { | function shallowRender(props: Partial<FilterBarProps> = {}) { | ||||
return shallow( | return shallow( | ||||
<FilterBar | <FilterBar | ||||
onChangeStatus={jest.fn()} | |||||
statusFilter={HotspotStatusFilters.TO_REVIEW} | |||||
currentUser={mockCurrentUser()} | |||||
onChangeFilters={jest.fn()} | |||||
filters={{ assignedToMe: false, status: HotspotStatusFilter.TO_REVIEW }} | |||||
{...props} | {...props} | ||||
/> | /> | ||||
); | ); |
import { | import { | ||||
HotspotResolution, | HotspotResolution, | ||||
HotspotStatus, | HotspotStatus, | ||||
HotspotStatusOptions | |||||
HotspotStatusOption | |||||
} from '../../../../types/security-hotspots'; | } from '../../../../types/security-hotspots'; | ||||
import HotspotActionsForm from '../HotspotActionsForm'; | import HotspotActionsForm from '../HotspotActionsForm'; | ||||
it('should handle option selection', () => { | it('should handle option selection', () => { | ||||
const wrapper = shallowRender(); | 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', () => { | it('should handle comment change', () => { | ||||
it('should handle submit', async () => { | it('should handle submit', async () => { | ||||
const onSubmit = jest.fn(); | const onSubmit = jest.fn(); | ||||
const wrapper = shallowRender({ onSubmit }); | const wrapper = shallowRender({ onSubmit }); | ||||
wrapper.setState({ selectedOption: HotspotStatusOptions.ADDITIONAL_REVIEW }); | |||||
wrapper.setState({ selectedOption: HotspotStatusOption.ADDITIONAL_REVIEW }); | |||||
await waitAndUpdate(wrapper); | await waitAndUpdate(wrapper); | ||||
const preventDefault = jest.fn(); | const preventDefault = jest.fn(); | ||||
expect(onSubmit).toBeCalled(); | expect(onSubmit).toBeCalled(); | ||||
// SAFE | // SAFE | ||||
wrapper.setState({ comment: 'commentsafe', selectedOption: HotspotStatusOptions.SAFE }); | |||||
wrapper.setState({ comment: 'commentsafe', selectedOption: HotspotStatusOption.SAFE }); | |||||
await waitAndUpdate(wrapper); | await waitAndUpdate(wrapper); | ||||
await wrapper.instance().handleSubmit({ preventDefault } as any); | await wrapper.instance().handleSubmit({ preventDefault } as any); | ||||
expect(setSecurityHotspotStatus).toBeCalledWith('key', { | expect(setSecurityHotspotStatus).toBeCalledWith('key', { | ||||
}); | }); | ||||
// FIXED | // FIXED | ||||
wrapper.setState({ comment: 'commentFixed', selectedOption: HotspotStatusOptions.FIXED }); | |||||
wrapper.setState({ comment: 'commentFixed', selectedOption: HotspotStatusOption.FIXED }); | |||||
await waitAndUpdate(wrapper); | await waitAndUpdate(wrapper); | ||||
await wrapper.instance().handleSubmit({ preventDefault } as any); | await wrapper.instance().handleSubmit({ preventDefault } as any); | ||||
expect(setSecurityHotspotStatus).toBeCalledWith('key', { | expect(setSecurityHotspotStatus).toBeCalledWith('key', { | ||||
const wrapper = shallowRender({ onSubmit }); | const wrapper = shallowRender({ onSubmit }); | ||||
wrapper.setState({ | wrapper.setState({ | ||||
comment: 'assignment comment', | comment: 'assignment comment', | ||||
selectedOption: HotspotStatusOptions.ADDITIONAL_REVIEW | |||||
selectedOption: HotspotStatusOption.ADDITIONAL_REVIEW | |||||
}); | }); | ||||
wrapper.instance().handleAssign(mockLoggedInUser({ login: 'userLogin' })); | wrapper.instance().handleAssign(mockLoggedInUser({ login: 'userLogin' })); | ||||
await waitAndUpdate(wrapper); | await waitAndUpdate(wrapper); | ||||
import { shallow } from 'enzyme'; | import { shallow } from 'enzyme'; | ||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { mockLoggedInUser } from '../../../../helpers/testMocks'; | import { mockLoggedInUser } from '../../../../helpers/testMocks'; | ||||
import { HotspotStatusOptions } from '../../../../types/security-hotspots'; | |||||
import { HotspotStatusOption } from '../../../../types/security-hotspots'; | |||||
import HotspotActionsForm from '../HotspotActionsForm'; | import HotspotActionsForm from '../HotspotActionsForm'; | ||||
import HotspotActionsFormRenderer, { | import HotspotActionsFormRenderer, { | ||||
HotspotActionsFormRendererProps | HotspotActionsFormRendererProps | ||||
it('should render correctly', () => { | it('should render correctly', () => { | ||||
expect(shallowRender()).toMatchSnapshot(); | expect(shallowRender()).toMatchSnapshot(); | ||||
expect(shallowRender({ submitting: true })).toMatchSnapshot('Submitting'); | expect(shallowRender({ submitting: true })).toMatchSnapshot('Submitting'); | ||||
expect(shallowRender({ selectedOption: HotspotStatusOptions.SAFE })).toMatchSnapshot( | |||||
expect(shallowRender({ selectedOption: HotspotStatusOption.SAFE })).toMatchSnapshot( | |||||
'safe option selected' | 'safe option selected' | ||||
); | ); | ||||
expect( | expect( | ||||
shallowRender({ | shallowRender({ | ||||
selectedOption: HotspotStatusOptions.ADDITIONAL_REVIEW, | |||||
selectedOption: HotspotStatusOption.ADDITIONAL_REVIEW, | |||||
selectedUser: mockLoggedInUser() | selectedUser: mockLoggedInUser() | ||||
}) | }) | ||||
).toMatchSnapshot('user selected'); | ).toMatchSnapshot('user selected'); | ||||
onChangeComment={jest.fn()} | onChangeComment={jest.fn()} | ||||
onSelectOption={jest.fn()} | onSelectOption={jest.fn()} | ||||
onSubmit={jest.fn()} | onSubmit={jest.fn()} | ||||
selectedOption={HotspotStatusOptions.FIXED} | |||||
selectedOption={HotspotStatusOption.FIXED} | |||||
submitting={false} | submitting={false} | ||||
{...props} | {...props} | ||||
/> | /> |
import { shallow } from 'enzyme'; | import { shallow } from 'enzyme'; | ||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { mockRawHotspot } from '../../../../helpers/mocks/security-hotspots'; | 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'; | import HotspotList, { HotspotListProps } from '../HotspotList'; | ||||
it('should render correctly', () => { | it('should render correctly', () => { | ||||
onHotspotClick={jest.fn()} | onHotspotClick={jest.fn()} | ||||
securityCategories={{}} | securityCategories={{}} | ||||
selectedHotspotKey="h2" | selectedHotspotKey="h2" | ||||
statusFilter={HotspotStatusFilters.TO_REVIEW} | |||||
statusFilter={HotspotStatusFilter.TO_REVIEW} | |||||
{...props} | {...props} | ||||
/> | /> | ||||
); | ); |
// Jest Snapshot v1, https://goo.gl/fbAQLP | // Jest Snapshot v1, https://goo.gl/fbAQLP | ||||
exports[`should render correctly 1`] = ` | |||||
exports[`should render correctly: anonymous 1`] = ` | |||||
<div | <div | ||||
className="filter-bar display-flex-center" | className="filter-bar display-flex-center" | ||||
> | > | ||||
<h3 | <h3 | ||||
className="big-spacer-right" | |||||
className="huge-spacer-right" | |||||
> | > | ||||
hotspot.filters.title | hotspot.filters.title | ||||
</h3> | </h3> | ||||
/> | /> | ||||
</div> | </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> | |||||
`; |
SAFE = 'SAFE' | SAFE = 'SAFE' | ||||
} | } | ||||
export enum HotspotStatusFilters { | |||||
export enum HotspotStatusFilter { | |||||
FIXED = 'FIXED', | FIXED = 'FIXED', | ||||
SAFE = 'SAFE', | SAFE = 'SAFE', | ||||
TO_REVIEW = 'TO_REVIEW' | TO_REVIEW = 'TO_REVIEW' | ||||
} | } | ||||
export enum HotspotStatusOptions { | |||||
export enum HotspotStatusOption { | |||||
FIXED = 'FIXED', | FIXED = 'FIXED', | ||||
SAFE = 'SAFE', | SAFE = 'SAFE', | ||||
ADDITIONAL_REVIEW = 'ADDITIONAL_REVIEW' | ADDITIONAL_REVIEW = 'ADDITIONAL_REVIEW' | ||||
} | } | ||||
export interface HotspotFilters { | |||||
assignedToMe: boolean; | |||||
status: HotspotStatusFilter; | |||||
} | |||||
export interface RawHotspot { | export interface RawHotspot { | ||||
assignee?: string; | assignee?: string; | ||||
author?: string; | author?: string; |
hotspot.status.SAFE=Safe | hotspot.status.SAFE=Safe | ||||
hotspot.filters.title=Filters | 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.to_review=To review | ||||
hotspot.filters.status.fixed=Reviewed as fixed | hotspot.filters.status.fixed=Reviewed as fixed | ||||
hotspot.filters.status.safe=Reviewed as safe | hotspot.filters.status.safe=Reviewed as safe |