return getJSON('/api/hotspots/search', data).catch(throwGlobalError);
}
+export function getSecurityHotspotList(hotspotKeys: string[]): Promise<HotspotSearchResponse> {
+ return getJSON('/api/hotspots/search', { hotspots: hotspotKeys.join() }).catch(throwGlobalError);
+}
+
export function getSecurityHotspotDetails(securityHotspotKey: string): Promise<Hotspot> {
return getJSON('/api/hotspots/show', { hotspot: securityHotspotKey })
.then((response: Hotspot & { users: T.UserBase[] }) => {
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { Location } from 'history';
import * as React from 'react';
import { addNoFooterPageClass, removeNoFooterPageClass } from 'sonar-ui-common/helpers/pages';
-import { getSecurityHotspots } from '../../api/security-hotspots';
+import { getSecurityHotspotList, getSecurityHotspots } from '../../api/security-hotspots';
import { withCurrentUser } from '../../components/hoc/withCurrentUser';
+import { Router } from '../../components/hoc/withRouter';
import { getBranchLikeQuery } from '../../helpers/branch-like';
import { getStandards } from '../../helpers/security-standard';
import { isLoggedIn } from '../../helpers/users';
branchLike?: BranchLike;
currentUser: T.CurrentUser;
component: T.Component;
+ location: Location;
+ router: Router;
}
interface State {
+ hotspotKeys?: string[];
hotspots: RawHotspot[];
loading: boolean;
securityCategories: T.StandardSecurityCategories;
}
componentDidUpdate(previous: Props) {
- if (this.props.component.key !== previous.component.key) {
+ if (
+ this.props.component.key !== previous.component.key ||
+ this.props.location.query.hotspots !== previous.location.query.hotspots
+ ) {
this.fetchInitialData();
}
}
}
fetchSecurityHotspots() {
- const { branchLike, component } = this.props;
+ const { branchLike, component, location } = this.props;
const { filters } = this.state;
+ const hotspotKeys = location.query.hotspots
+ ? (location.query.hotspots as string).split(',')
+ : undefined;
+
+ this.setState({ hotspotKeys });
+
+ if (hotspotKeys && hotspotKeys.length > 0) {
+ return getSecurityHotspotList(hotspotKeys);
+ }
+
const status =
filters.status === HotspotStatusFilter.TO_REVIEW
? HotspotStatus.TO_REVIEW
});
};
+ handleShowAllHotspots = () => {
+ this.props.router.push({
+ ...this.props.location,
+ query: { ...this.props.location.query, hotspots: undefined }
+ });
+ };
+
render() {
const { branchLike } = this.props;
- const { hotspots, loading, securityCategories, selectedHotspotKey, filters } = this.state;
+ const {
+ hotspotKeys,
+ hotspots,
+ loading,
+ securityCategories,
+ selectedHotspotKey,
+ filters
+ } = this.state;
return (
<SecurityHotspotsAppRenderer
branchLike={branchLike}
filters={filters}
hotspots={hotspots}
+ isStaticListOfHotspots={Boolean(hotspotKeys && hotspotKeys.length > 0)}
loading={loading}
onChangeFilters={this.handleChangeFilters}
onHotspotClick={this.handleHotspotClick}
+ onShowAllHotspots={this.handleShowAllHotspots}
onUpdateHotspot={this.handleHotspotUpdate}
securityCategories={securityCategories}
selectedHotspotKey={selectedHotspotKey}
branchLike?: BranchLike;
filters: HotspotFilters;
hotspots: RawHotspot[];
+ isStaticListOfHotspots: boolean;
loading: boolean;
onChangeFilters: (filters: Partial<HotspotFilters>) => void;
onHotspotClick: (key: string) => void;
+ onShowAllHotspots: () => void;
onUpdateHotspot: (hotspot: HotspotUpdate) => void;
selectedHotspotKey?: string;
securityCategories: T.StandardSecurityCategories;
}
export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRendererProps) {
- const { branchLike, hotspots, loading, securityCategories, selectedHotspotKey, filters } = props;
+ const {
+ branchLike,
+ hotspots,
+ isStaticListOfHotspots,
+ loading,
+ securityCategories,
+ selectedHotspotKey,
+ filters
+ } = props;
return (
<div id="security_hotspots">
- <FilterBar onChangeFilters={props.onChangeFilters} filters={filters} />
+ <FilterBar
+ filters={filters}
+ isStaticListOfHotspots={isStaticListOfHotspots}
+ onChangeFilters={props.onChangeFilters}
+ onShowAllHotspots={props.onShowAllHotspots}
+ />
<ScreenPositionHelper>
{({ top }) => (
<div className="wrapper" style={{ top }}>
<div className="sidebar">
<HotspotList
hotspots={hotspots}
+ isStaticListOfHotspots={isStaticListOfHotspots}
onHotspotClick={props.onHotspotClick}
securityCategories={securityCategories}
selectedHotspotKey={selectedHotspotKey}
import * as React from 'react';
import { addNoFooterPageClass } from 'sonar-ui-common/helpers/pages';
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
-import { getSecurityHotspots } from '../../../api/security-hotspots';
+import { getSecurityHotspotList, 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, mockCurrentUser } from '../../../helpers/testMocks';
+import {
+ mockComponent,
+ mockCurrentUser,
+ mockLocation,
+ mockRouter
+} from '../../../helpers/testMocks';
import {
HotspotResolution,
HotspotStatus,
import { SecurityHotspotsApp } from '../SecurityHotspotsApp';
import SecurityHotspotsAppRenderer from '../SecurityHotspotsAppRenderer';
+beforeEach(() => jest.clearAllMocks());
+
jest.mock('sonar-ui-common/helpers/pages', () => ({
addNoFooterPageClass: jest.fn(),
removeNoFooterPageClass: jest.fn()
}));
jest.mock('../../../api/security-hotspots', () => ({
- getSecurityHotspots: jest.fn().mockResolvedValue({ hotspots: [], rules: [] })
+ getSecurityHotspots: jest.fn().mockResolvedValue({ hotspots: [], rules: [] }),
+ getSecurityHotspotList: jest.fn().mockResolvedValue({ hotspots: [], rules: [] })
}));
jest.mock('../../../helpers/security-standard', () => ({
expect(wrapper.state());
});
+it('should load data correctly when hotspot key list is forced', async () => {
+ const sonarsourceSecurity = { cat1: { title: 'cat 1' } };
+ (getStandards as jest.Mock).mockResolvedValue({ sonarsourceSecurity });
+
+ const hotspots = [
+ mockRawHotspot({ key: 'test1' }),
+ mockRawHotspot({ key: 'test2' }),
+ mockRawHotspot({ key: 'test3' })
+ ];
+ const hotspotKeys = hotspots.map(h => h.key);
+ (getSecurityHotspotList as jest.Mock).mockResolvedValueOnce({
+ hotspots
+ });
+
+ const location = mockLocation({ query: { hotspots: hotspotKeys.join() } });
+ const router = mockRouter();
+ const wrapper = shallowRender({
+ location,
+ router
+ });
+
+ await waitAndUpdate(wrapper);
+ expect(getSecurityHotspotList).toBeCalledWith(hotspotKeys);
+ expect(wrapper.state().hotspotKeys).toEqual(hotspotKeys);
+ expect(wrapper.find(SecurityHotspotsAppRenderer).props().isStaticListOfHotspots).toBeTruthy();
+
+ // Reset
+ (getSecurityHotspots as jest.Mock).mockClear();
+ (getSecurityHotspotList as jest.Mock).mockClear();
+ wrapper
+ .find(SecurityHotspotsAppRenderer)
+ .props()
+ .onShowAllHotspots();
+ expect(router.push).toHaveBeenCalledWith({
+ ...location,
+ query: { ...location.query, hotspots: undefined }
+ });
+
+ // Simulate a new location
+ wrapper.setProps({
+ location: { ...location, query: { ...location.query, hotspots: undefined } }
+ });
+ await waitAndUpdate(wrapper);
+ expect(wrapper.state().hotspotKeys).toBeUndefined();
+ expect(getSecurityHotspotList).not.toHaveBeenCalled();
+ expect(getSecurityHotspots).toHaveBeenCalled();
+});
+
it('should handle hotspot update', async () => {
const key = 'hotspotKey';
const hotspots = [mockRawHotspot(), mockRawHotspot({ key })];
branchLike={branch}
component={mockComponent()}
currentUser={mockCurrentUser()}
+ location={mockLocation()}
+ router={mockRouter()}
{...props}
/>
);
import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper';
import { mockRawHotspot } from '../../../helpers/mocks/security-hotspots';
import { HotspotStatusFilter } from '../../../types/security-hotspots';
+import FilterBar from '../components/FilterBar';
import SecurityHotspotsAppRenderer, {
SecurityHotspotsAppRendererProps
} from '../SecurityHotspotsAppRenderer';
).toMatchSnapshot();
});
+it('should properly propagate the "show all" call', () => {
+ const onShowAllHotspots = jest.fn();
+ const wrapper = shallowRender({ onShowAllHotspots });
+
+ wrapper
+ .find(FilterBar)
+ .props()
+ .onShowAllHotspots();
+
+ expect(onShowAllHotspots).toHaveBeenCalled();
+});
+
function shallowRender(props: Partial<SecurityHotspotsAppRendererProps> = {}) {
return shallow(
<SecurityHotspotsAppRenderer
hotspots={[]}
+ isStaticListOfHotspots={true}
loading={false}
onChangeFilters={jest.fn()}
onHotspotClick={jest.fn()}
+ onShowAllHotspots={jest.fn()}
onUpdateHotspot={jest.fn()}
securityCategories={{}}
filters={{ assignedToMe: false, status: HotspotStatusFilter.TO_REVIEW }}
}
}
hotspots={Array []}
+ isStaticListOfHotspots={false}
loading={true}
onChangeFilters={[Function]}
onHotspotClick={[Function]}
+ onShowAllHotspots={[Function]}
onUpdateHotspot={[Function]}
securityCategories={Object {}}
/>
"status": "TO_REVIEW",
}
}
+ isStaticListOfHotspots={true}
onChangeFilters={[MockFunction]}
+ onShowAllHotspots={[MockFunction]}
/>
<ScreenPositionHelper>
<Component />
},
]
}
+ isStaticListOfHotspots={true}
onHotspotClick={[MockFunction]}
securityCategories={Object {}}
statusFilter="TO_REVIEW"
},
]
}
+ isStaticListOfHotspots={true}
onHotspotClick={[MockFunction]}
securityCategories={Object {}}
selectedHotspotKey="h2"
export interface FilterBarProps {
currentUser: T.CurrentUser;
filters: HotspotFilters;
+ isStaticListOfHotspots: boolean;
onChangeFilters: (filters: Partial<HotspotFilters>) => void;
+ onShowAllHotspots: () => void;
}
const statusOptions: Array<{ label: string; value: string }> = [
];
export function FilterBar(props: FilterBarProps) {
- const { currentUser, filters } = props;
+ const { currentUser, filters, isStaticListOfHotspots } = props;
+
return (
<div className="filter-bar display-flex-center">
- <h3 className="huge-spacer-right">{translate('hotspot.filters.title')}</h3>
+ {isStaticListOfHotspots ? (
+ <a id="show_all_hotspot" onClick={() => props.onShowAllHotspots()} role="link" tabIndex={0}>
+ {translate('hotspot.filters.show_all')}
+ </a>
+ ) : (
+ <>
+ <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}
- />
- )}
+ {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: HotspotStatusFilter }) =>
- props.onChangeFilters({ status: option.value })
- }
- options={statusOptions}
- searchable={false}
- value={filters.status}
- />
+ <span className="spacer-right">{translate('status')}</span>
+ <Select
+ className="input-medium big-spacer-right"
+ clearable={false}
+ onChange={(option: { value: HotspotStatusFilter }) =>
+ props.onChangeFilters({ status: option.value })
+ }
+ options={statusOptions}
+ searchable={false}
+ value={filters.status}
+ />
+ </>
+ )}
</div>
);
}
export interface HotspotListProps {
hotspots: RawHotspot[];
+ isStaticListOfHotspots: boolean;
onHotspotClick: (key: string) => void;
securityCategories: T.StandardSecurityCategories;
selectedHotspotKey: string | undefined;
}
export default function HotspotList(props: HotspotListProps) {
- const { hotspots, securityCategories, selectedHotspotKey, statusFilter } = props;
+ const {
+ hotspots,
+ isStaticListOfHotspots,
+ securityCategories,
+ selectedHotspotKey,
+ statusFilter
+ } = props;
const groupedHotspots: Array<{
risk: RiskExposure;
<>
<h1 className="hotspot-list-header bordered-bottom">
<SecurityHotspotIcon className="spacer-right" />
- {translateWithParameters(`hotspots.list_title.${statusFilter}`, hotspots.length)}
+ {translateWithParameters(
+ isStaticListOfHotspots ? 'hotspots.list_title' : `hotspots.list_title.${statusFilter}`,
+ hotspots.length
+ )}
</h1>
<ul className="huge-spacer-bottom">
{groupedHotspots.map(riskGroup => (
expect(shallowRender({ currentUser: mockLoggedInUser() })).toMatchSnapshot('logged-in');
});
+it('should render correctly when the list of hotspot is static', () => {
+ const onShowAllHotspots = jest.fn();
+
+ const wrapper = shallowRender({
+ isStaticListOfHotspots: true,
+ onShowAllHotspots
+ });
+ expect(wrapper).toMatchSnapshot();
+
+ wrapper.find('a').simulate('click');
+ expect(onShowAllHotspots).toHaveBeenCalled();
+});
+
it('should trigger onChange for status', () => {
const onChangeFilters = jest.fn();
const wrapper = shallowRender({ onChangeFilters });
return shallow(
<FilterBar
currentUser={mockCurrentUser()}
+ isStaticListOfHotspots={false}
onChangeFilters={jest.fn()}
+ onShowAllHotspots={jest.fn()}
filters={{ assignedToMe: false, status: HotspotStatusFilter.TO_REVIEW }}
{...props}
/>
expect(shallowRender()).toMatchSnapshot();
});
+it('should render correctly when the list of hotspot is static', () => {
+ expect(shallowRender({ isStaticListOfHotspots: true })).toMatchSnapshot();
+});
+
it('should render correctly with hotspots', () => {
const hotspots = [
mockRawHotspot({ key: 'h1', securityCategory: 'cat2' }),
return shallow(
<HotspotList
hotspots={[]}
+ isStaticListOfHotspots={false}
onHotspotClick={jest.fn()}
securityCategories={{}}
selectedHotspotKey="h2"
// Jest Snapshot v1, https://goo.gl/fbAQLP
+exports[`should render correctly when the list of hotspot is static 1`] = `
+<div
+ className="filter-bar display-flex-center"
+>
+ <a
+ id="show_all_hotspot"
+ onClick={[Function]}
+ role="link"
+ tabIndex={0}
+ >
+ hotspot.filters.show_all
+ </a>
+</div>
+`;
+
exports[`should render correctly: anonymous 1`] = `
<div
className="filter-bar display-flex-center"
</Fragment>
`;
+exports[`should render correctly when the list of hotspot is static 1`] = `
+<Fragment>
+ <h1
+ className="hotspot-list-header bordered-bottom"
+ >
+ <SecurityHotspotIcon
+ className="spacer-right"
+ />
+ hotspots.list_title.0
+ </h1>
+ <ul
+ className="huge-spacer-bottom"
+ />
+</Fragment>
+`;
+
exports[`should render correctly with hotspots 1`] = `
<Fragment>
<h1
hotspots.no_hotspots.title=There are no Security Hotspots to review
hotspots.no_hotspots.description=Next time you analyse a piece of code that contains a potential security risk, it will show up here.
hotspots.learn_more=Learn more about Security Hotspots
+hotspots.list_title={0} Security Hotspots
hotspots.list_title.TO_REVIEW={0} Security Hotspots to review
hotspots.list_title.FIXED={0} Security Hotspots reviewed as fixed
hotspots.list_title.SAFE={0} Security Hotspots reviewed as safe
hotspot.filters.status.to_review=To review
hotspot.filters.status.fixed=Reviewed as fixed
hotspot.filters.status.safe=Reviewed as safe
+hotspot.filters.show_all=Show all hotspots
hotspots.review_hotspot=Review Hotspot
hotspots.form.title=Mark Security Hotspot as: