Browse Source

SONAR-12727 Add Radio to show 'only my' hotspots

tags/8.2.0.32929
Jeremy 4 years ago
parent
commit
8ef630c419
18 changed files with 250 additions and 105 deletions
  1. 1
    0
      server/sonar-web/src/main/js/api/security-hotspots.ts
  2. 39
    18
      server/sonar-web/src/main/js/apps/securityHotspots/SecurityHotspotsApp.tsx
  3. 6
    13
      server/sonar-web/src/main/js/apps/securityHotspots/SecurityHotspotsAppRenderer.tsx
  4. 11
    6
      server/sonar-web/src/main/js/apps/securityHotspots/__tests__/SecurityHotspotsApp-test.tsx
  5. 3
    3
      server/sonar-web/src/main/js/apps/securityHotspots/__tests__/SecurityHotspotsAppRenderer-test.tsx
  6. 7
    2
      server/sonar-web/src/main/js/apps/securityHotspots/__tests__/__snapshots__/SecurityHotspotsApp-test.tsx.snap
  7. 8
    3
      server/sonar-web/src/main/js/apps/securityHotspots/__tests__/__snapshots__/SecurityHotspotsAppRenderer-test.tsx.snap
  8. 41
    11
      server/sonar-web/src/main/js/apps/securityHotspots/components/FilterBar.tsx
  9. 9
    8
      server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotActionsForm.tsx
  10. 11
    11
      server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotActionsFormRenderer.tsx
  11. 2
    2
      server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotList.tsx
  12. 27
    10
      server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/FilterBar-test.tsx
  13. 9
    8
      server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotActionsForm-test.tsx
  14. 4
    4
      server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotActionsFormRenderer-test.tsx
  15. 2
    2
      server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotList-test.tsx
  16. 61
    2
      server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/FilterBar-test.tsx.snap
  17. 7
    2
      server/sonar-web/src/main/js/types/security-hotspots.ts
  18. 2
    0
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 1
- 0
server/sonar-web/src/main/js/api/security-hotspots.ts View File

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);

+ 39
- 18
server/sonar-web/src/main/js/apps/securityHotspots/SecurityHotspotsApp.tsx View File

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);

+ 6
- 13
server/sonar-web/src/main/js/apps/securityHotspots/SecurityHotspotsAppRenderer.tsx View File

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">

+ 11
- 6
server/sonar-web/src/main/js/apps/securityHotspots/__tests__/SecurityHotspotsApp-test.tsx View File

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}
/>
); );
} }

+ 3
- 3
server/sonar-web/src/main/js/apps/securityHotspots/__tests__/SecurityHotspotsAppRenderer-test.tsx View File

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}
/> />
); );

+ 7
- 2
server/sonar-web/src/main/js/apps/securityHotspots/__tests__/__snapshots__/SecurityHotspotsApp-test.tsx.snap View File

"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"
/> />
`; `;

+ 8
- 3
server/sonar-web/src/main/js/apps/securityHotspots/__tests__/__snapshots__/SecurityHotspotsAppRenderer-test.tsx.snap View File

<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 />

+ 41
- 11
server/sonar-web/src/main/js/apps/securityHotspots/components/FilterBar.tsx View File

* 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);

+ 9
- 8
server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotActionsForm.tsx View File

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;

+ 11
- 11
server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotActionsFormRenderer.tsx View File

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 (

+ 2
- 2
server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotList.tsx View File

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) {

+ 27
- 10
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/FilterBar-test.tsx View File

*/ */
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}
/> />
); );

+ 9
- 8
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotActionsForm-test.tsx View File

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);



+ 4
- 4
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotActionsFormRenderer-test.tsx View File

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}
/> />

+ 2
- 2
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotList-test.tsx View File

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}
/> />
); );

+ 61
- 2
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/FilterBar-test.tsx.snap View File

// 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>
`;

+ 7
- 2
server/sonar-web/src/main/js/types/security-hotspots.ts View File

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;

+ 2
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

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

Loading…
Cancel
Save