import {
DetailedHotspot,
HotspotAssignRequest,
+ HotspotResolution,
HotspotSearchResponse,
- HotspotSetStatusRequest
+ HotspotSetStatusRequest,
+ HotspotStatus
} from '../types/security-hotspots';
export function assignSecurityHotspot(
projectKey: string;
p: number;
ps: number;
+ status?: HotspotStatus;
+ resolution?: HotspotResolution;
} & BranchParameters
): Promise<HotspotSearchResponse> {
return getJSON('/api/hotspots/search', data).catch(throwGlobalError);
import { getBranchLikeQuery } from '../../helpers/branch-like';
import { getStandards } from '../../helpers/security-standard';
import { BranchLike } from '../../types/branch-like';
-import { HotspotUpdate, RawHotspot } from '../../types/security-hotspots';
+import {
+ HotspotResolution,
+ HotspotStatus,
+ HotspotStatusFilters,
+ HotspotUpdate,
+ RawHotspot
+} from '../../types/security-hotspots';
import SecurityHotspotsAppRenderer from './SecurityHotspotsAppRenderer';
import './styles.css';
import { sortHotspots } from './utils';
loading: boolean;
securityCategories: T.StandardSecurityCategories;
selectedHotspotKey: string | undefined;
+ statusFilter: HotspotStatusFilters;
}
export default class SecurityHotspotsApp extends React.PureComponent<Props, State> {
loading: true,
hotspots: [],
securityCategories: {},
- selectedHotspotKey: undefined
+ selectedHotspotKey: undefined,
+ statusFilter: HotspotStatusFilters.TO_REVIEW
};
componentDidMount() {
this.mounted = false;
}
- fetchInitialData() {
- const { branchLike, component } = this.props;
+ handleCallFailure = () => {
+ if (this.mounted) {
+ this.setState({ loading: false });
+ }
+ };
- return Promise.all([
- getStandards(),
- getSecurityHotspots({
- projectKey: component.key,
- p: 1,
- ps: PAGE_SIZE,
- ...getBranchLikeQuery(branchLike)
- })
- ])
+ fetchInitialData() {
+ return Promise.all([getStandards(), this.fetchSecurityHotspots()])
.then(([{ sonarsourceSecurity }, response]) => {
if (!this.mounted) {
return;
selectedHotspotKey: hotspots.length > 0 ? hotspots[0].key : undefined
});
})
- .catch(() => {
- if (this.mounted) {
- this.setState({ loading: false });
- }
- });
+ .catch(this.handleCallFailure);
}
+ fetchSecurityHotspots() {
+ const { branchLike, component } = this.props;
+ const { statusFilter } = this.state;
+
+ const status =
+ statusFilter === HotspotStatusFilters.TO_REVIEW
+ ? HotspotStatus.TO_REVIEW
+ : HotspotStatus.REVIEWED;
+
+ const resolution =
+ statusFilter === HotspotStatusFilters.TO_REVIEW ? undefined : HotspotResolution[statusFilter];
+
+ return getSecurityHotspots({
+ projectKey: component.key,
+ p: 1,
+ ps: PAGE_SIZE,
+ status,
+ resolution,
+ ...getBranchLikeQuery(branchLike)
+ });
+ }
+
+ reloadSecurityHotspotList = () => {
+ const { securityCategories } = this.state;
+
+ this.setState({ loading: true });
+
+ return this.fetchSecurityHotspots()
+ .then(response => {
+ if (!this.mounted) {
+ return;
+ }
+
+ const hotspots = sortHotspots(response.hotspots, securityCategories);
+
+ this.setState({
+ hotspots,
+ loading: false,
+ selectedHotspotKey: hotspots.length > 0 ? hotspots[0].key : undefined
+ });
+ })
+ .catch(this.handleCallFailure);
+ };
+
+ handleChangeStatusFilter = (statusFilter: HotspotStatusFilters) => {
+ this.setState({ statusFilter }, this.reloadSecurityHotspotList);
+ };
+
handleHotspotClick = (key: string) => this.setState({ selectedHotspotKey: key });
handleHotspotUpdate = ({ key, status, resolution }: HotspotUpdate) => {
render() {
const { branchLike } = this.props;
- const { hotspots, loading, securityCategories, selectedHotspotKey } = this.state;
+ const { hotspots, loading, securityCategories, selectedHotspotKey, statusFilter } = this.state;
return (
<SecurityHotspotsAppRenderer
branchLike={branchLike}
hotspots={hotspots}
loading={loading}
+ onChangeStatusFilter={this.handleChangeStatusFilter}
onHotspotClick={this.handleHotspotClick}
onUpdateHotspot={this.handleHotspotUpdate}
securityCategories={securityCategories}
selectedHotspotKey={selectedHotspotKey}
+ statusFilter={statusFilter}
/>
);
}
import Suggestions from '../../app/components/embed-docs-modal/Suggestions';
import ScreenPositionHelper from '../../components/common/ScreenPositionHelper';
import { BranchLike } from '../../types/branch-like';
-import { HotspotUpdate, RawHotspot } from '../../types/security-hotspots';
+import { HotspotStatusFilters, HotspotUpdate, RawHotspot } from '../../types/security-hotspots';
import FilterBar from './components/FilterBar';
import HotspotList from './components/HotspotList';
import HotspotViewer from './components/HotspotViewer';
branchLike?: BranchLike;
hotspots: RawHotspot[];
loading: boolean;
+ onChangeStatusFilter: (status: HotspotStatusFilters) => 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 } = props;
+ const {
+ branchLike,
+ hotspots,
+ loading,
+ securityCategories,
+ selectedHotspotKey,
+ statusFilter
+ } = props;
return (
<div id="security_hotspots">
- <FilterBar />
+ <FilterBar onChangeStatus={props.onChangeStatusFilter} statusFilter={statusFilter} />
<ScreenPositionHelper>
{({ top }) => (
<div className="wrapper" style={{ top }}>
onHotspotClick={props.onHotspotClick}
securityCategories={securityCategories}
selectedHotspotKey={selectedHotspotKey}
+ statusFilter={statusFilter}
/>
</div>
<div className="main">
import { mockRawHotspot } from '../../../helpers/mocks/security-hotspots';
import { getStandards } from '../../../helpers/security-standard';
import { mockComponent } from '../../../helpers/testMocks';
-import { HotspotResolution, HotspotStatus } from '../../../types/security-hotspots';
+import {
+ HotspotResolution,
+ HotspotStatus,
+ HotspotStatusFilters
+} from '../../../types/security-hotspots';
import SecurityHotspotsApp from '../SecurityHotspotsApp';
import SecurityHotspotsAppRenderer from '../SecurityHotspotsAppRenderer';
(getStandards as jest.Mock).mockResolvedValue({ sonarsourceSecurity });
const hotspots = [mockRawHotspot()];
- (getSecurityHotspots as jest.Mock).mockResolvedValue({
+ (getSecurityHotspots as jest.Mock).mockResolvedValueOnce({
hotspots
});
});
});
+it('should handle status filter change', async () => {
+ const hotspots = [mockRawHotspot({ key: 'key1' })];
+ const hotspots2 = [mockRawHotspot({ key: 'key2' })];
+ (getSecurityHotspots as jest.Mock)
+ .mockResolvedValueOnce({ hotspots })
+ .mockResolvedValueOnce({ hotspots: hotspots2 })
+ .mockResolvedValueOnce({ hotspots: [] });
+
+ const wrapper = shallowRender();
+
+ expect(getSecurityHotspots).toBeCalledWith(
+ expect.objectContaining({ status: HotspotStatus.TO_REVIEW, resolution: undefined })
+ );
+
+ await waitAndUpdate(wrapper);
+
+ // Set filter to SAFE:
+ wrapper.instance().handleChangeStatusFilter(HotspotStatusFilters.SAFE);
+
+ expect(getSecurityHotspots).toBeCalledWith(
+ expect.objectContaining({ status: HotspotStatus.REVIEWED, resolution: HotspotResolution.SAFE })
+ );
+
+ await waitAndUpdate(wrapper);
+
+ expect(wrapper.state().hotspots[0]).toBe(hotspots2[0]);
+
+ // Set filter to FIXED
+ wrapper.instance().handleChangeStatusFilter(HotspotStatusFilters.FIXED);
+
+ expect(getSecurityHotspots).toBeCalledWith(
+ expect.objectContaining({ status: HotspotStatus.REVIEWED, resolution: HotspotResolution.FIXED })
+ );
+
+ await waitAndUpdate(wrapper);
+
+ expect(wrapper.state().hotspots).toHaveLength(0);
+});
+
function shallowRender(props: Partial<SecurityHotspotsApp['props']> = {}) {
return shallow<SecurityHotspotsApp>(
<SecurityHotspotsApp branchLike={branch} component={mockComponent()} {...props} />
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 SecurityHotspotsAppRenderer, {
SecurityHotspotsAppRendererProps
} from '../SecurityHotspotsAppRenderer';
<SecurityHotspotsAppRenderer
hotspots={[]}
loading={false}
+ onChangeStatusFilter={jest.fn()}
onHotspotClick={jest.fn()}
onUpdateHotspot={jest.fn()}
securityCategories={{}}
+ statusFilter={HotspotStatusFilters.TO_REVIEW}
{...props}
/>
);
}
hotspots={Array []}
loading={true}
+ onChangeStatusFilter={[Function]}
onHotspotClick={[Function]}
onUpdateHotspot={[Function]}
securityCategories={Object {}}
+ statusFilter="TO_REVIEW"
/>
`;
<div
id="security_hotspots"
>
- <FilterBar />
+ <FilterBar
+ onChangeStatus={[MockFunction]}
+ statusFilter="TO_REVIEW"
+ />
<ScreenPositionHelper>
<Component />
</ScreenPositionHelper>
}
onHotspotClick={[MockFunction]}
securityCategories={Object {}}
+ statusFilter="TO_REVIEW"
/>
</div>
<div
onHotspotClick={[MockFunction]}
securityCategories={Object {}}
selectedHotspotKey="h2"
+ statusFilter="TO_REVIEW"
/>
</div>
<div
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
+import Select from 'sonar-ui-common/components/controls/Select';
+import { translate } from 'sonar-ui-common/helpers/l10n';
+import { HotspotStatusFilters } from '../../../types/security-hotspots';
-export interface FilterBarProps {}
+export interface FilterBarProps {
+ onChangeStatus: (status: HotspotStatusFilters) => void;
+ statusFilter: HotspotStatusFilters;
+}
+
+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 }
+];
export default function FilterBar(props: FilterBarProps) {
+ const { statusFilter } = props;
return (
<div className="filter-bar display-flex-center">
- <h3 {...props}>Filter</h3>
+ <h3 className="big-spacer-right">{translate('hotspot.filters.title')}</h3>
+
+ <span className="spacer-right">{translate('status')}</span>
+ <Select
+ className="input-medium big-spacer-right"
+ clearable={false}
+ onChange={(option: { value: HotspotStatusFilters }) => props.onChangeStatus(option.value)}
+ options={statusOptions}
+ searchable={false}
+ value={statusFilter}
+ />
</div>
);
}
import * as React from 'react';
import SecurityHotspotIcon from 'sonar-ui-common/components/icons/SecurityHotspotIcon';
import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
-import { RawHotspot, RiskExposure } from '../../../types/security-hotspots';
+import { HotspotStatusFilters, RawHotspot, RiskExposure } from '../../../types/security-hotspots';
import { groupByCategory, RISK_EXPOSURE_LEVELS } from '../utils';
import HotspotCategory from './HotspotCategory';
import './HotspotList.css';
onHotspotClick: (key: string) => void;
securityCategories: T.StandardSecurityCategories;
selectedHotspotKey: string | undefined;
+ statusFilter: HotspotStatusFilters;
}
export default function HotspotList(props: HotspotListProps) {
- const { hotspots, securityCategories, selectedHotspotKey } = props;
+ const { hotspots, securityCategories, selectedHotspotKey, statusFilter } = props;
const groupedHotspots: Array<{
risk: RiskExposure;
<>
<h1 className="hotspot-list-header bordered-bottom">
<SecurityHotspotIcon className="spacer-right" />
- {translateWithParameters(`hotspots.list_title.TO_REVIEW`, hotspots.length)}
+ {translateWithParameters(`hotspots.list_title.${statusFilter}`, hotspots.length)}
</h1>
<ul className="huge-spacer-bottom">
{groupedHotspots.map(riskGroup => (
selected: boolean;
}
-export function HotspotListItem(props: HotspotListItemProps) {
+export default function HotspotListItem(props: HotspotListItemProps) {
const { hotspot, selected } = props;
return (
<a
href="#"
onClick={() => !selected && props.onClick(hotspot.key)}>
<div className="little-spacer-left">{hotspot.message}</div>
- <div className="badge spacer-top">{translate('issue.status', hotspot.status)}</div>
+ <div className="badge spacer-top">
+ {translate('hotspot.status', hotspot.resolution || hotspot.status)}
+ </div>
</a>
);
}
-
-export default React.memo(HotspotListItem);
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import Select from 'sonar-ui-common/components/controls/Select';
+import { HotspotStatusFilters } from '../../../../types/security-hotspots';
+import FilterBar, { FilterBarProps } from '../FilterBar';
+
+it('should render correctly', () => {
+ expect(shallowRender()).toMatchSnapshot();
+});
+
+it('should trigger onChange', () => {
+ const onChangeStatus = jest.fn();
+ const wrapper = shallowRender({ onChangeStatus });
+
+ 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);
+});
+
+function shallowRender(props: Partial<FilterBarProps> = {}) {
+ return shallow(
+ <FilterBar
+ onChangeStatus={jest.fn()}
+ statusFilter={HotspotStatusFilters.TO_REVIEW}
+ {...props}
+ />
+ );
+}
import { shallow } from 'enzyme';
import * as React from 'react';
import { mockRawHotspot } from '../../../../helpers/mocks/security-hotspots';
-import { RiskExposure } from '../../../../types/security-hotspots';
+import { HotspotStatusFilters, RiskExposure } from '../../../../types/security-hotspots';
import HotspotList, { HotspotListProps } from '../HotspotList';
it('should render correctly', () => {
onHotspotClick={jest.fn()}
securityCategories={{}}
selectedHotspotKey="h2"
+ statusFilter={HotspotStatusFilters.TO_REVIEW}
{...props}
/>
);
import { shallow } from 'enzyme';
import * as React from 'react';
import { mockRawHotspot } from '../../../../helpers/mocks/security-hotspots';
-import { HotspotListItem, HotspotListItemProps } from '../HotspotListItem';
+import HotspotListItem, { HotspotListItemProps } from '../HotspotListItem';
it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot();
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<div
+ className="filter-bar display-flex-center"
+>
+ <h3
+ className="big-spacer-right"
+ >
+ hotspot.filters.title
+ </h3>
+ <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>
+`;
<div
className="badge spacer-top"
>
- issue.status.TO_REVIEW
+ hotspot.status.TO_REVIEW
</div>
</a>
`;
<div
className="badge spacer-top"
>
- issue.status.TO_REVIEW
+ hotspot.status.TO_REVIEW
</div>
</a>
`;
#security_hotspots .filter-bar {
max-width: 1280px;
margin: 0 auto;
- padding: var(--gridSize) 20px;
+ padding: calc(2 * var(--gridSize)) 20px;
border-bottom: 1px solid var(--barBorderColor);
}
SAFE = 'SAFE'
}
+export enum HotspotStatusFilters {
+ FIXED = 'FIXED',
+ SAFE = 'SAFE',
+ TO_REVIEW = 'TO_REVIEW'
+}
+
export enum HotspotStatusOptions {
FIXED = 'FIXED',
SAFE = 'SAFE',
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.TO_REVIEW={0} Security Hotspots to review
-hotspots.list_title.REVIEWED={0} reviewed Security Hotspots
+hotspots.list_title.FIXED={0} Security Hotspots reviewed as fixed
+hotspots.list_title.SAFE={0} Security Hotspots reviewed as safe
hotspots.risk_exposure=Review priority:
hotspot.category=Category:
hotspot.tabs.fix_recommendations=How can you fix it?
hotspots.review_hotspot=Review Hotspot
+hotspot.status.TO_REVIEW=To review
+hotspot.status.FIXED=Fixed
+hotspot.status.SAFE=Safe
+
+hotspot.filters.title=Filters
+hotspot.filters.status.to_review=To review
+hotspot.filters.status.fixed=Reviewed as fixed
+hotspot.filters.status.safe=Reviewed as safe
+
hotspots.form.title=Mark Security Hotspot as:
hotspots.form.assign_to=Assign to: