From: Philippe Perrin Date: Mon, 23 Dec 2019 13:31:54 +0000 (+0100) Subject: SONAR-12797 Security Hotspots page allows to filter by hotspots keys param X-Git-Tag: 8.2.0.32929~167 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=2a80a0dc1032843fcd1403cf6488dcbb563fa180;p=sonarqube.git SONAR-12797 Security Hotspots page allows to filter by hotspots keys param --- diff --git a/server/sonar-web/src/main/js/api/security-hotspots.ts b/server/sonar-web/src/main/js/api/security-hotspots.ts index 78ec7567f62..ced115b7e49 100644 --- a/server/sonar-web/src/main/js/api/security-hotspots.ts +++ b/server/sonar-web/src/main/js/api/security-hotspots.ts @@ -58,6 +58,10 @@ export function getSecurityHotspots( return getJSON('/api/hotspots/search', data).catch(throwGlobalError); } +export function getSecurityHotspotList(hotspotKeys: string[]): Promise { + return getJSON('/api/hotspots/search', { hotspots: hotspotKeys.join() }).catch(throwGlobalError); +} + export function getSecurityHotspotDetails(securityHotspotKey: string): Promise { return getJSON('/api/hotspots/show', { hotspot: securityHotspotKey }) .then((response: Hotspot & { users: T.UserBase[] }) => { diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/SecurityHotspotsApp.tsx b/server/sonar-web/src/main/js/apps/securityHotspots/SecurityHotspotsApp.tsx index a1170faef13..4856ce425af 100644 --- a/server/sonar-web/src/main/js/apps/securityHotspots/SecurityHotspotsApp.tsx +++ b/server/sonar-web/src/main/js/apps/securityHotspots/SecurityHotspotsApp.tsx @@ -17,10 +17,12 @@ * 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'; @@ -43,9 +45,12 @@ interface Props { branchLike?: BranchLike; currentUser: T.CurrentUser; component: T.Component; + location: Location; + router: Router; } interface State { + hotspotKeys?: string[]; hotspots: RawHotspot[]; loading: boolean; securityCategories: T.StandardSecurityCategories; @@ -79,7 +84,10 @@ export class SecurityHotspotsApp extends React.PureComponent { } 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(); } } @@ -115,9 +123,19 @@ export class SecurityHotspotsApp extends React.PureComponent { } 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 @@ -187,18 +205,34 @@ export class SecurityHotspotsApp extends React.PureComponent { }); }; + 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 ( 0)} loading={loading} onChangeFilters={this.handleChangeFilters} onHotspotClick={this.handleHotspotClick} + onShowAllHotspots={this.handleShowAllHotspots} onUpdateHotspot={this.handleHotspotUpdate} securityCategories={securityCategories} selectedHotspotKey={selectedHotspotKey} diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/SecurityHotspotsAppRenderer.tsx b/server/sonar-web/src/main/js/apps/securityHotspots/SecurityHotspotsAppRenderer.tsx index 0ffe2fa8b5f..593e13b9291 100644 --- a/server/sonar-web/src/main/js/apps/securityHotspots/SecurityHotspotsAppRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/securityHotspots/SecurityHotspotsAppRenderer.tsx @@ -37,20 +37,35 @@ export interface SecurityHotspotsAppRendererProps { branchLike?: BranchLike; filters: HotspotFilters; hotspots: RawHotspot[]; + isStaticListOfHotspots: boolean; loading: boolean; onChangeFilters: (filters: Partial) => 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 (
- + {({ top }) => (
@@ -84,6 +99,7 @@ export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRe
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', () => ({ @@ -84,6 +92,54 @@ it('should load data correctly', async () => { 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 })]; @@ -153,6 +209,8 @@ function shallowRender(props: Partial = {}) { branchLike={branch} component={mockComponent()} currentUser={mockCurrentUser()} + location={mockLocation()} + router={mockRouter()} {...props} /> ); diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/__tests__/SecurityHotspotsAppRenderer-test.tsx b/server/sonar-web/src/main/js/apps/securityHotspots/__tests__/SecurityHotspotsAppRenderer-test.tsx index 9676eaa8715..a721ed06963 100644 --- a/server/sonar-web/src/main/js/apps/securityHotspots/__tests__/SecurityHotspotsAppRenderer-test.tsx +++ b/server/sonar-web/src/main/js/apps/securityHotspots/__tests__/SecurityHotspotsAppRenderer-test.tsx @@ -22,6 +22,7 @@ import * as React from 'react'; 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'; @@ -49,13 +50,27 @@ it('should render correctly with hotspots', () => { ).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 = {}) { return shallow( diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/__tests__/__snapshots__/SecurityHotspotsAppRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/securityHotspots/__tests__/__snapshots__/SecurityHotspotsAppRenderer-test.tsx.snap index 9994b676832..cb0a3655f9c 100644 --- a/server/sonar-web/src/main/js/apps/securityHotspots/__tests__/__snapshots__/SecurityHotspotsAppRenderer-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/securityHotspots/__tests__/__snapshots__/SecurityHotspotsAppRenderer-test.tsx.snap @@ -11,7 +11,9 @@ exports[`should render correctly 1`] = ` "status": "TO_REVIEW", } } + isStaticListOfHotspots={true} onChangeFilters={[MockFunction]} + onShowAllHotspots={[MockFunction]} /> @@ -150,6 +152,7 @@ exports[`should render correctly with hotspots 1`] = ` }, ] } + isStaticListOfHotspots={true} onHotspotClick={[MockFunction]} securityCategories={Object {}} statusFilter="TO_REVIEW" @@ -231,6 +234,7 @@ exports[`should render correctly with hotspots 2`] = ` }, ] } + isStaticListOfHotspots={true} onHotspotClick={[MockFunction]} securityCategories={Object {}} selectedHotspotKey="h2" diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/components/FilterBar.tsx b/server/sonar-web/src/main/js/apps/securityHotspots/components/FilterBar.tsx index 3d1050e40f8..4f42abac06f 100644 --- a/server/sonar-web/src/main/js/apps/securityHotspots/components/FilterBar.tsx +++ b/server/sonar-web/src/main/js/apps/securityHotspots/components/FilterBar.tsx @@ -28,7 +28,9 @@ import { HotspotFilters, HotspotStatusFilter } from '../../../types/security-hot export interface FilterBarProps { currentUser: T.CurrentUser; filters: HotspotFilters; + isStaticListOfHotspots: boolean; onChangeFilters: (filters: Partial) => void; + onShowAllHotspots: () => void; } const statusOptions: Array<{ label: string; value: string }> = [ @@ -48,34 +50,43 @@ const assigneeFilterOptions = [ ]; export function FilterBar(props: FilterBarProps) { - const { currentUser, filters } = props; + const { currentUser, filters, isStaticListOfHotspots } = props; + return (
-

{translate('hotspot.filters.title')}

+ {isStaticListOfHotspots ? ( + props.onShowAllHotspots()} role="link" tabIndex={0}> + {translate('hotspot.filters.show_all')} + + ) : ( + <> +

{translate('hotspot.filters.title')}

- {isLoggedIn(currentUser) && ( - - props.onChangeFilters({ assignedToMe: value === AssigneeFilterOption.ME }) - } - options={assigneeFilterOptions} - value={filters.assignedToMe ? AssigneeFilterOption.ME : AssigneeFilterOption.ALL} - /> - )} + {isLoggedIn(currentUser) && ( + + props.onChangeFilters({ assignedToMe: value === AssigneeFilterOption.ME }) + } + options={assigneeFilterOptions} + value={filters.assignedToMe ? AssigneeFilterOption.ME : AssigneeFilterOption.ALL} + /> + )} - {translate('status')} - + props.onChangeFilters({ status: option.value }) + } + options={statusOptions} + searchable={false} + value={filters.status} + /> + + )}
); } diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotList.tsx b/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotList.tsx index 7b706d4ea2e..990cfebfa02 100644 --- a/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotList.tsx +++ b/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotList.tsx @@ -29,6 +29,7 @@ import './HotspotList.css'; export interface HotspotListProps { hotspots: RawHotspot[]; + isStaticListOfHotspots: boolean; onHotspotClick: (key: string) => void; securityCategories: T.StandardSecurityCategories; selectedHotspotKey: string | undefined; @@ -36,7 +37,13 @@ export interface HotspotListProps { } export default function HotspotList(props: HotspotListProps) { - const { hotspots, securityCategories, selectedHotspotKey, statusFilter } = props; + const { + hotspots, + isStaticListOfHotspots, + securityCategories, + selectedHotspotKey, + statusFilter + } = props; const groupedHotspots: Array<{ risk: RiskExposure; @@ -54,7 +61,10 @@ export default function HotspotList(props: HotspotListProps) { <>

- {translateWithParameters(`hotspots.list_title.${statusFilter}`, hotspots.length)} + {translateWithParameters( + isStaticListOfHotspots ? 'hotspots.list_title' : `hotspots.list_title.${statusFilter}`, + hotspots.length + )}

    {groupedHotspots.map(riskGroup => ( diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/FilterBar-test.tsx b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/FilterBar-test.tsx index ec47ceec624..ff9536372b2 100644 --- a/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/FilterBar-test.tsx +++ b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/FilterBar-test.tsx @@ -30,6 +30,19 @@ it('should render correctly', () => { 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 }); @@ -60,7 +73,9 @@ function shallowRender(props: Partial = {}) { return shallow( diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotList-test.tsx b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotList-test.tsx index e01aae707bf..b1d59788620 100644 --- a/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotList-test.tsx +++ b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotList-test.tsx @@ -27,6 +27,10 @@ it('should render correctly', () => { 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' }), @@ -54,6 +58,7 @@ function shallowRender(props: Partial = {}) { return shallow( + + hotspot.filters.show_all + +
+`; + exports[`should render correctly: anonymous 1`] = `
`; +exports[`should render correctly when the list of hotspot is static 1`] = ` + +

+ + hotspots.list_title.0 +

+
    + +`; + exports[`should render correctly with hotspots 1`] = `