From: Jeremy Date: Mon, 6 Jan 2020 14:41:38 +0000 (+0100) Subject: SONAR-12717 add hotspot pagination X-Git-Tag: 8.2.0.32929~151 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=f91c8fde272864fb938fd7c7c0a1fe3bd01cbb5c;p=sonarqube.git SONAR-12717 add hotspot pagination --- 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 2178798ee2d..0060c223e4a 100644 --- a/server/sonar-web/src/main/js/apps/securityHotspots/SecurityHotspotsApp.tsx +++ b/server/sonar-web/src/main/js/apps/securityHotspots/SecurityHotspotsApp.tsx @@ -37,7 +37,6 @@ import { } from '../../types/security-hotspots'; import SecurityHotspotsAppRenderer from './SecurityHotspotsAppRenderer'; import './styles.css'; -import { sortHotspots } from './utils'; const PAGE_SIZE = 500; @@ -52,7 +51,10 @@ interface Props { interface State { hotspotKeys?: string[]; hotspots: RawHotspot[]; + hotspotsPageIndex: number; + hotspotsTotal?: number; loading: boolean; + loadingMore: boolean; securityCategories: T.StandardSecurityCategories; selectedHotspotKey: string | undefined; filters: HotspotFilters; @@ -67,7 +69,9 @@ export class SecurityHotspotsApp extends React.PureComponent { this.state = { loading: true, + loadingMore: false, hotspots: [], + hotspotsPageIndex: 1, securityCategories: {}, selectedHotspotKey: undefined, filters: { @@ -120,21 +124,20 @@ export class SecurityHotspotsApp extends React.PureComponent { handleCallFailure = () => { if (this.mounted) { - this.setState({ loading: false }); + this.setState({ loading: false, loadingMore: false }); } }; fetchInitialData() { return Promise.all([getStandards(), this.fetchSecurityHotspots()]) - .then(([{ sonarsourceSecurity }, response]) => { + .then(([{ sonarsourceSecurity }, { hotspots, paging }]) => { if (!this.mounted) { return; } - const hotspots = sortHotspots(response.hotspots, sonarsourceSecurity); - this.setState({ hotspots, + hotspotsTotal: paging.total, loading: false, securityCategories: sonarsourceSecurity, selectedHotspotKey: hotspots.length > 0 ? hotspots[0].key : undefined @@ -143,7 +146,7 @@ export class SecurityHotspotsApp extends React.PureComponent { .catch(this.handleCallFailure); } - fetchSecurityHotspots() { + fetchSecurityHotspots(page = 1) { const { branchLike, component, location } = this.props; const { filters } = this.state; @@ -169,7 +172,7 @@ export class SecurityHotspotsApp extends React.PureComponent { return getSecurityHotspots({ projectKey: component.key, - p: 1, + p: page, ps: PAGE_SIZE, status, resolution, @@ -180,20 +183,18 @@ export class SecurityHotspotsApp extends React.PureComponent { } reloadSecurityHotspotList = () => { - const { securityCategories } = this.state; - this.setState({ loading: true }); return this.fetchSecurityHotspots() - .then(response => { + .then(({ hotspots, paging }) => { if (!this.mounted) { return; } - const hotspots = sortHotspots(response.hotspots, securityCategories); - this.setState({ hotspots, + hotspotsPageIndex: 1, + hotspotsTotal: paging.total, loading: false, selectedHotspotKey: hotspots.length > 0 ? hotspots[0].key : undefined }); @@ -234,12 +235,34 @@ export class SecurityHotspotsApp extends React.PureComponent { }); }; + handleLoadMore = () => { + const { hotspots, hotspotsPageIndex: hotspotPages } = this.state; + + this.setState({ loadingMore: true }); + + return this.fetchSecurityHotspots(hotspotPages + 1) + .then(({ hotspots: additionalHotspots }) => { + if (!this.mounted) { + return; + } + + this.setState({ + hotspots: [...hotspots, ...additionalHotspots], + hotspotsPageIndex: hotspotPages + 1, + loadingMore: false + }); + }) + .catch(this.handleCallFailure); + }; + render() { const { branchLike } = this.props; const { hotspotKeys, hotspots, + hotspotsTotal, loading, + loadingMore, securityCategories, selectedHotspotKey, filters @@ -250,10 +273,13 @@ export class SecurityHotspotsApp extends React.PureComponent { branchLike={branchLike} filters={filters} hotspots={hotspots} + hotspotsTotal={hotspotsTotal} isStaticListOfHotspots={Boolean(hotspotKeys && hotspotKeys.length > 0)} loading={loading} + loadingMore={loadingMore} onChangeFilters={this.handleChangeFilters} onHotspotClick={this.handleHotspotClick} + onLoadMore={this.handleLoadMore} onShowAllHotspots={this.handleShowAllHotspots} onUpdateHotspot={this.handleHotspotUpdate} securityCategories={securityCategories} 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 7850fd95f56..4e815b87701 100644 --- a/server/sonar-web/src/main/js/apps/securityHotspots/SecurityHotspotsAppRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/securityHotspots/SecurityHotspotsAppRenderer.tsx @@ -38,10 +38,13 @@ export interface SecurityHotspotsAppRendererProps { branchLike?: BranchLike; filters: HotspotFilters; hotspots: RawHotspot[]; + hotspotsTotal?: number; isStaticListOfHotspots: boolean; loading: boolean; + loadingMore: boolean; onChangeFilters: (filters: Partial) => void; onHotspotClick: (key: string) => void; + onLoadMore: () => void; onShowAllHotspots: () => void; onUpdateHotspot: (hotspot: HotspotUpdate) => void; selectedHotspotKey?: string; @@ -52,8 +55,10 @@ export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRe const { branchLike, hotspots, + hotspotsTotal, isStaticListOfHotspots, loading, + loadingMore, securityCategories, selectedHotspotKey, filters @@ -101,8 +106,11 @@ export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRe
({ })); jest.mock('../../../api/security-hotspots', () => ({ - getSecurityHotspots: jest.fn().mockResolvedValue({ hotspots: [], rules: [] }), + getSecurityHotspots: jest.fn().mockResolvedValue({ hotspots: [], paging: { total: 0 } }), getSecurityHotspotList: jest.fn().mockResolvedValue({ hotspots: [], rules: [] }) })); jest.mock('../../../helpers/security-standard', () => ({ - getStandards: jest.fn() + getStandards: jest.fn().mockResolvedValue({ sonarsourceSecurity: { cat1: { title: 'cat 1' } } }) })); const branch = mockBranch(); @@ -62,12 +62,12 @@ it('should render correctly', () => { }); it('should load data correctly', async () => { - const sonarsourceSecurity = { cat1: { title: 'cat 1' } }; - (getStandards as jest.Mock).mockResolvedValue({ sonarsourceSecurity }); - const hotspots = [mockRawHotspot()]; - (getSecurityHotspots as jest.Mock).mockResolvedValueOnce({ - hotspots + (getSecurityHotspots as jest.Mock).mockResolvedValue({ + hotspots, + paging: { + total: 1 + } }); const wrapper = shallowRender(); @@ -87,15 +87,14 @@ it('should load data correctly', async () => { expect(wrapper.state().loading).toBe(false); expect(wrapper.state().hotspots).toEqual(hotspots); expect(wrapper.state().selectedHotspotKey).toBe(hotspots[0].key); - expect(wrapper.state().securityCategories).toBe(sonarsourceSecurity); + expect(wrapper.state().securityCategories).toEqual({ + cat1: { title: 'cat 1' } + }); 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' }), @@ -149,11 +148,42 @@ it('should set "leakperiod" filter according to context (branchlike & location q ).toBe(true); }); +it('should handle loading more', async () => { + const hotspots = [mockRawHotspot({ key: '1' }), mockRawHotspot({ key: '2' })]; + const hotspots2 = [mockRawHotspot({ key: '3' }), mockRawHotspot({ key: '4' })]; + (getSecurityHotspots as jest.Mock) + .mockResolvedValueOnce({ + hotspots, + paging: { total: 5 } + }) + .mockResolvedValueOnce({ + hotspots: hotspots2, + paging: { total: 5 } + }); + + const wrapper = shallowRender(); + + await waitAndUpdate(wrapper); + + wrapper.instance().handleLoadMore(); + + expect(wrapper.state().loadingMore).toBe(true); + expect(getSecurityHotspots).toBeCalledTimes(2); + + await waitAndUpdate(wrapper); + + expect(wrapper.state().loadingMore).toBe(false); + expect(wrapper.state().hotspotsPageIndex).toBe(2); + expect(wrapper.state().hotspotsTotal).toBe(5); + expect(wrapper.state().hotspots).toHaveLength(4); +}); + it('should handle hotspot update', async () => { const key = 'hotspotKey'; const hotspots = [mockRawHotspot(), mockRawHotspot({ key })]; - (getSecurityHotspots as jest.Mock).mockResolvedValue({ - hotspots + (getSecurityHotspots as jest.Mock).mockResolvedValueOnce({ + hotspots, + paging: { total: 2 } }); const wrapper = shallowRender(); @@ -171,15 +201,24 @@ it('should handle hotspot update', async () => { status: HotspotStatus.REVIEWED, resolution: HotspotResolution.SAFE }); + + const previousState = wrapper.state(); + wrapper.instance().handleHotspotUpdate({ + key: 'unknown', + status: HotspotStatus.REVIEWED, + resolution: HotspotResolution.SAFE + }); + await waitAndUpdate(wrapper); + expect(wrapper.state()).toEqual(previousState); }); 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: [] }); + .mockResolvedValueOnce({ hotspots, paging: { total: 1 } }) + .mockResolvedValueOnce({ hotspots: hotspots2, paging: { total: 1 } }) + .mockResolvedValueOnce({ hotspots: [], paging: { total: 0 } }); const wrapper = shallowRender(); 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 d57ddc474f1..4be898d2b22 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 @@ -39,12 +39,12 @@ it('should render correctly', () => { it('should render correctly with hotspots', () => { const hotspots = [mockRawHotspot({ key: 'h1' }), mockRawHotspot({ key: 'h2' })]; expect( - shallowRender({ hotspots }) + shallowRender({ hotspots, hotspotsTotal: 2 }) .find(ScreenPositionHelper) .dive() ).toMatchSnapshot(); expect( - shallowRender({ hotspots, selectedHotspotKey: 'h2' }) + shallowRender({ hotspots, hotspotsTotal: 3, selectedHotspotKey: 'h2' }) .find(ScreenPositionHelper) .dive() ).toMatchSnapshot(); @@ -65,19 +65,21 @@ it('should properly propagate the "show all" call', () => { function shallowRender(props: Partial = {}) { return shallow( ); diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/__tests__/__snapshots__/SecurityHotspotsApp-test.tsx.snap b/server/sonar-web/src/main/js/apps/securityHotspots/__tests__/__snapshots__/SecurityHotspotsApp-test.tsx.snap index b297c4c7e6d..04a9a9b4fd5 100644 --- a/server/sonar-web/src/main/js/apps/securityHotspots/__tests__/__snapshots__/SecurityHotspotsApp-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/securityHotspots/__tests__/__snapshots__/SecurityHotspotsApp-test.tsx.snap @@ -20,8 +20,10 @@ exports[`should render correctly 1`] = ` hotspots={Array []} isStaticListOfHotspots={false} loading={true} + loadingMore={false} onChangeFilters={[Function]} onHotspotClick={[Function]} + onLoadMore={[Function]} onShowAllHotspots={[Function]} onUpdateHotspot={[Function]} securityCategories={Object {}} 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 fa151377e03..c9336052d04 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 @@ -154,8 +154,11 @@ exports[`should render correctly with hotspots 1`] = ` }, ] } + hotspotsTotal={2} isStaticListOfHotspots={true} + loadingMore={false} onHotspotClick={[MockFunction]} + onLoadMore={[MockFunction]} securityCategories={Object {}} statusFilter="TO_REVIEW" /> @@ -236,8 +239,11 @@ exports[`should render correctly with hotspots 2`] = ` }, ] } + hotspotsTotal={3} isStaticListOfHotspots={true} + loadingMore={false} onHotspotClick={[MockFunction]} + onLoadMore={[MockFunction]} securityCategories={Object {}} selectedHotspotKey="h2" statusFilter="TO_REVIEW" 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 990cfebfa02..d6b31fb2042 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 @@ -20,6 +20,7 @@ import * as classNames from 'classnames'; import { groupBy } from 'lodash'; import * as React from 'react'; +import ListFooter from 'sonar-ui-common/components/controls/ListFooter'; import SecurityHotspotIcon from 'sonar-ui-common/components/icons/SecurityHotspotIcon'; import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n'; import { HotspotStatusFilter, RawHotspot, RiskExposure } from '../../../types/security-hotspots'; @@ -29,8 +30,11 @@ import './HotspotList.css'; export interface HotspotListProps { hotspots: RawHotspot[]; + hotspotsTotal?: number; isStaticListOfHotspots: boolean; + loadingMore: boolean; onHotspotClick: (key: string) => void; + onLoadMore: () => void; securityCategories: T.StandardSecurityCategories; selectedHotspotKey: string | undefined; statusFilter: HotspotStatusFilter; @@ -39,7 +43,9 @@ export interface HotspotListProps { export default function HotspotList(props: HotspotListProps) { const { hotspots, + hotspotsTotal, isStaticListOfHotspots, + loadingMore, securityCategories, selectedHotspotKey, statusFilter @@ -58,7 +64,7 @@ export default function HotspotList(props: HotspotListProps) { }, [hotspots, securityCategories]); return ( - <> +

{translateWithParameters( @@ -66,7 +72,7 @@ export default function HotspotList(props: HotspotListProps) { hotspots.length )}

-
    +
      {groupedHotspots.map(riskGroup => (
    • @@ -90,6 +96,12 @@ export default function HotspotList(props: HotspotListProps) {
    • ))}
    - + +
); } 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 b1d59788620..caaa7bff5d7 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 @@ -25,6 +25,7 @@ import HotspotList, { HotspotListProps } from '../HotspotList'; it('should render correctly', () => { expect(shallowRender()).toMatchSnapshot(); + expect(shallowRender({ loadingMore: true })).toMatchSnapshot(); }); it('should render correctly when the list of hotspot is static', () => { @@ -51,7 +52,8 @@ it('should render correctly with hotspots', () => { vulnerabilityProbability: RiskExposure.MEDIUM }) ]; - expect(shallowRender({ hotspots })).toMatchSnapshot(); + expect(shallowRender({ hotspots })).toMatchSnapshot('no pagination'); + expect(shallowRender({ hotspots, hotspotsTotal: 7 })).toMatchSnapshot('pagination'); }); function shallowRender(props: Partial = {}) { @@ -59,7 +61,9 @@ function shallowRender(props: Partial = {}) { +

@@ -11,13 +13,42 @@ exports[`should render correctly 1`] = ` hotspots.list_title.TO_REVIEW.0

    - + +
+`; + +exports[`should render correctly 2`] = ` +
+

+ + hotspots.list_title.TO_REVIEW.0 +

+
    + +
`; exports[`should render correctly when the list of hotspot is static 1`] = ` - +

@@ -27,13 +58,20 @@ exports[`should render correctly when the list of hotspot is static 1`] = ` hotspots.list_title.0

    - + +
`; -exports[`should render correctly with hotspots 1`] = ` - +exports[`should render correctly with hotspots: no pagination 1`] = ` +

@@ -43,7 +81,7 @@ exports[`should render correctly with hotspots 1`] = ` hotspots.list_title.TO_REVIEW.5

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

+ + hotspots.list_title.TO_REVIEW.5 +

+
    +
  • +
    + + hotspots.risk_exposure + +
    + risk_exposure.HIGH +
    +
    +
      +
    • + +
    • +
    • + +
    • +
    +
  • +
  • +
    + + hotspots.risk_exposure + +
    + risk_exposure.MEDIUM +
    +
    +
      +
    • + +
    • +
    • + +
    • +
    +
  • +
+ +
`;