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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -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 (

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

Loading…
Cancel
Save