]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-12717 add hotspot pagination
authorJeremy <jeremy.davis@sonarsource.com>
Mon, 6 Jan 2020 14:41:38 +0000 (15:41 +0100)
committerSonarTech <sonartech@sonarsource.com>
Mon, 13 Jan 2020 19:46:35 +0000 (20:46 +0100)
server/sonar-web/src/main/js/apps/securityHotspots/SecurityHotspotsApp.tsx
server/sonar-web/src/main/js/apps/securityHotspots/SecurityHotspotsAppRenderer.tsx
server/sonar-web/src/main/js/apps/securityHotspots/__tests__/SecurityHotspotsApp-test.tsx
server/sonar-web/src/main/js/apps/securityHotspots/__tests__/SecurityHotspotsAppRenderer-test.tsx
server/sonar-web/src/main/js/apps/securityHotspots/__tests__/__snapshots__/SecurityHotspotsApp-test.tsx.snap
server/sonar-web/src/main/js/apps/securityHotspots/__tests__/__snapshots__/SecurityHotspotsAppRenderer-test.tsx.snap
server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotList.tsx
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotList-test.tsx
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotList-test.tsx.snap

index 2178798ee2db024f8baee20e8639afec29b2c202..0060c223e4a2e7cdb9fb747869208ff06a63d760 100644 (file)
@@ -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<Props, State> {
 
     this.state = {
       loading: true,
+      loadingMore: false,
       hotspots: [],
+      hotspotsPageIndex: 1,
       securityCategories: {},
       selectedHotspotKey: undefined,
       filters: {
@@ -120,21 +124,20 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> {
 
   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<Props, State> {
       .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<Props, State> {
 
     return getSecurityHotspots({
       projectKey: component.key,
-      p: 1,
+      p: page,
       ps: PAGE_SIZE,
       status,
       resolution,
@@ -180,20 +183,18 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> {
   }
 
   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<Props, State> {
     });
   };
 
+  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<Props, State> {
         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}
index 7850fd95f56b3ebca6c30f62905ae7e08cd6b9eb..4e815b877019d5ed48b6fd9c496aef1d3e759f48 100644 (file)
@@ -38,10 +38,13 @@ export interface SecurityHotspotsAppRendererProps {
   branchLike?: BranchLike;
   filters: HotspotFilters;
   hotspots: RawHotspot[];
+  hotspotsTotal?: number;
   isStaticListOfHotspots: boolean;
   loading: boolean;
+  loadingMore: boolean;
   onChangeFilters: (filters: Partial<HotspotFilters>) => 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
                   <div className="sidebar">
                     <HotspotList
                       hotspots={hotspots}
+                      hotspotsTotal={hotspotsTotal}
                       isStaticListOfHotspots={isStaticListOfHotspots}
+                      loadingMore={loadingMore}
                       onHotspotClick={props.onHotspotClick}
+                      onLoadMore={props.onLoadMore}
                       securityCategories={securityCategories}
                       selectedHotspotKey={selectedHotspotKey}
                       statusFilter={filters.status}
index b51cdee698d22cbb7114f65993c7054c366ac0be..eed79943cbb1046a083390343e44bc33f8151c38 100644 (file)
@@ -47,12 +47,12 @@ jest.mock('sonar-ui-common/helpers/pages', () => ({
 }));
 
 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();
 
index d57ddc474f1b8366d9eaf38b71ba000f3e1717ef..4be898d2b22e59ab13d69bb873aa5f0f00c13798 100644 (file)
@@ -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<SecurityHotspotsAppRendererProps> = {}) {
   return shallow(
     <SecurityHotspotsAppRenderer
+      filters={{
+        assignedToMe: false,
+        newCode: false,
+        status: HotspotStatusFilter.TO_REVIEW
+      }}
       hotspots={[]}
       isStaticListOfHotspots={true}
       loading={false}
+      loadingMore={false}
       onChangeFilters={jest.fn()}
       onHotspotClick={jest.fn()}
+      onLoadMore={jest.fn()}
       onShowAllHotspots={jest.fn()}
       onUpdateHotspot={jest.fn()}
       securityCategories={{}}
-      filters={{
-        assignedToMe: false,
-        newCode: false,
-        status: HotspotStatusFilter.TO_REVIEW
-      }}
       {...props}
     />
   );
index b297c4c7e6d551512d581c7a8bf36e52acc04549..04a9a9b4fd58c8f18b079a232e2bc702d1fda721 100644 (file)
@@ -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 {}}
index fa151377e03f568d0cf6d48a1766765ca8b9d4e0..c9336052d04bd502877a542b8592783d4a5d9a5b 100644 (file)
@@ -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"
index 990cfebfa02c4c27dacd017d46bea8b9e632b759..d6b31fb2042c52621221b92aae98fa2d27ae0bb7 100644 (file)
@@ -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 (
-    <>
+    <div className="huge-spacer-bottom">
       <h1 className="hotspot-list-header bordered-bottom">
         <SecurityHotspotIcon className="spacer-right" />
         {translateWithParameters(
@@ -66,7 +72,7 @@ export default function HotspotList(props: HotspotListProps) {
           hotspots.length
         )}
       </h1>
-      <ul className="huge-spacer-bottom">
+      <ul className="big-spacer-bottom">
         {groupedHotspots.map(riskGroup => (
           <li className="big-spacer-bottom" key={riskGroup.risk}>
             <div className="hotspot-risk-header little-spacer-left">
@@ -90,6 +96,12 @@ export default function HotspotList(props: HotspotListProps) {
           </li>
         ))}
       </ul>
-    </>
+      <ListFooter
+        count={hotspots.length}
+        loadMore={!loadingMore ? props.onLoadMore : undefined}
+        loading={loadingMore}
+        total={hotspotsTotal}
+      />
+    </div>
   );
 }
index b1d597886207d3ac601efa30ee1b05a982e2aebb..caaa7bff5d7fcaf52067aa53465b5c44143aedbc 100644 (file)
@@ -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<HotspotListProps> = {}) {
@@ -59,7 +61,9 @@ function shallowRender(props: Partial<HotspotListProps> = {}) {
     <HotspotList
       hotspots={[]}
       isStaticListOfHotspots={false}
+      loadingMore={false}
       onHotspotClick={jest.fn()}
+      onLoadMore={jest.fn()}
       securityCategories={{}}
       selectedHotspotKey="h2"
       statusFilter={HotspotStatusFilter.TO_REVIEW}
index 4deb2cea0a17867122c359cdadaa5a8a62f895ef..872890c776479692a18d228dbe9e3ce063ac7963 100644 (file)
@@ -1,7 +1,9 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 exports[`should render correctly 1`] = `
-<Fragment>
+<div
+  className="huge-spacer-bottom"
+>
   <h1
     className="hotspot-list-header bordered-bottom"
   >
@@ -11,13 +13,42 @@ exports[`should render correctly 1`] = `
     hotspots.list_title.TO_REVIEW.0
   </h1>
   <ul
-    className="huge-spacer-bottom"
+    className="big-spacer-bottom"
   />
-</Fragment>
+  <ListFooter
+    count={0}
+    loadMore={[MockFunction]}
+    loading={false}
+  />
+</div>
+`;
+
+exports[`should render correctly 2`] = `
+<div
+  className="huge-spacer-bottom"
+>
+  <h1
+    className="hotspot-list-header bordered-bottom"
+  >
+    <SecurityHotspotIcon
+      className="spacer-right"
+    />
+    hotspots.list_title.TO_REVIEW.0
+  </h1>
+  <ul
+    className="big-spacer-bottom"
+  />
+  <ListFooter
+    count={0}
+    loading={true}
+  />
+</div>
 `;
 
 exports[`should render correctly when the list of hotspot is static 1`] = `
-<Fragment>
+<div
+  className="huge-spacer-bottom"
+>
   <h1
     className="hotspot-list-header bordered-bottom"
   >
@@ -27,13 +58,20 @@ exports[`should render correctly when the list of hotspot is static 1`] = `
     hotspots.list_title.0
   </h1>
   <ul
-    className="huge-spacer-bottom"
+    className="big-spacer-bottom"
   />
-</Fragment>
+  <ListFooter
+    count={0}
+    loadMore={[MockFunction]}
+    loading={false}
+  />
+</div>
 `;
 
-exports[`should render correctly with hotspots 1`] = `
-<Fragment>
+exports[`should render correctly with hotspots: no pagination 1`] = `
+<div
+  className="huge-spacer-bottom"
+>
   <h1
     className="hotspot-list-header bordered-bottom"
   >
@@ -43,7 +81,7 @@ exports[`should render correctly with hotspots 1`] = `
     hotspots.list_title.TO_REVIEW.5
   </h1>
   <ul
-    className="huge-spacer-bottom"
+    className="big-spacer-bottom"
   >
     <li
       className="big-spacer-bottom"
@@ -235,5 +273,224 @@ exports[`should render correctly with hotspots 1`] = `
       </ul>
     </li>
   </ul>
-</Fragment>
+  <ListFooter
+    count={5}
+    loadMore={[MockFunction]}
+    loading={false}
+  />
+</div>
+`;
+
+exports[`should render correctly with hotspots: pagination 1`] = `
+<div
+  className="huge-spacer-bottom"
+>
+  <h1
+    className="hotspot-list-header bordered-bottom"
+  >
+    <SecurityHotspotIcon
+      className="spacer-right"
+    />
+    hotspots.list_title.TO_REVIEW.5
+  </h1>
+  <ul
+    className="big-spacer-bottom"
+  >
+    <li
+      className="big-spacer-bottom"
+      key="HIGH"
+    >
+      <div
+        className="hotspot-risk-header little-spacer-left"
+      >
+        <span>
+          hotspots.risk_exposure
+        </span>
+        <div
+          className="hotspot-risk-badge spacer-left HIGH"
+        >
+          risk_exposure.HIGH
+        </div>
+      </div>
+      <ul>
+        <li
+          className="spacer-bottom"
+          key="cat1"
+        >
+          <HotspotCategory
+            category={
+              Object {
+                "key": "cat1",
+                "title": "cat1",
+              }
+            }
+            hotspots={
+              Array [
+                Object {
+                  "author": "Developer 1",
+                  "component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest",
+                  "creationDate": "2013-05-13T17:55:39+0200",
+                  "key": "h2",
+                  "line": 81,
+                  "message": "'3' is a magic number.",
+                  "project": "com.github.kevinsawicki:http-request",
+                  "resolution": undefined,
+                  "rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
+                  "securityCategory": "cat1",
+                  "status": "TO_REVIEW",
+                  "updateDate": "2013-05-13T17:55:39+0200",
+                  "vulnerabilityProbability": "HIGH",
+                },
+              ]
+            }
+            onHotspotClick={[MockFunction]}
+            selectedHotspotKey="h2"
+          />
+        </li>
+        <li
+          className="spacer-bottom"
+          key="cat2"
+        >
+          <HotspotCategory
+            category={
+              Object {
+                "key": "cat2",
+                "title": "cat2",
+              }
+            }
+            hotspots={
+              Array [
+                Object {
+                  "author": "Developer 1",
+                  "component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest",
+                  "creationDate": "2013-05-13T17:55:39+0200",
+                  "key": "h1",
+                  "line": 81,
+                  "message": "'3' is a magic number.",
+                  "project": "com.github.kevinsawicki:http-request",
+                  "resolution": undefined,
+                  "rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
+                  "securityCategory": "cat2",
+                  "status": "TO_REVIEW",
+                  "updateDate": "2013-05-13T17:55:39+0200",
+                  "vulnerabilityProbability": "HIGH",
+                },
+              ]
+            }
+            onHotspotClick={[MockFunction]}
+            selectedHotspotKey="h2"
+          />
+        </li>
+      </ul>
+    </li>
+    <li
+      className="big-spacer-bottom"
+      key="MEDIUM"
+    >
+      <div
+        className="hotspot-risk-header little-spacer-left"
+      >
+        <span>
+          hotspots.risk_exposure
+        </span>
+        <div
+          className="hotspot-risk-badge spacer-left MEDIUM"
+        >
+          risk_exposure.MEDIUM
+        </div>
+      </div>
+      <ul>
+        <li
+          className="spacer-bottom"
+          key="cat1"
+        >
+          <HotspotCategory
+            category={
+              Object {
+                "key": "cat1",
+                "title": "cat1",
+              }
+            }
+            hotspots={
+              Array [
+                Object {
+                  "author": "Developer 1",
+                  "component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest",
+                  "creationDate": "2013-05-13T17:55:39+0200",
+                  "key": "h3",
+                  "line": 81,
+                  "message": "'3' is a magic number.",
+                  "project": "com.github.kevinsawicki:http-request",
+                  "resolution": undefined,
+                  "rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
+                  "securityCategory": "cat1",
+                  "status": "TO_REVIEW",
+                  "updateDate": "2013-05-13T17:55:39+0200",
+                  "vulnerabilityProbability": "MEDIUM",
+                },
+                Object {
+                  "author": "Developer 1",
+                  "component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest",
+                  "creationDate": "2013-05-13T17:55:39+0200",
+                  "key": "h4",
+                  "line": 81,
+                  "message": "'3' is a magic number.",
+                  "project": "com.github.kevinsawicki:http-request",
+                  "resolution": undefined,
+                  "rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
+                  "securityCategory": "cat1",
+                  "status": "TO_REVIEW",
+                  "updateDate": "2013-05-13T17:55:39+0200",
+                  "vulnerabilityProbability": "MEDIUM",
+                },
+              ]
+            }
+            onHotspotClick={[MockFunction]}
+            selectedHotspotKey="h2"
+          />
+        </li>
+        <li
+          className="spacer-bottom"
+          key="cat2"
+        >
+          <HotspotCategory
+            category={
+              Object {
+                "key": "cat2",
+                "title": "cat2",
+              }
+            }
+            hotspots={
+              Array [
+                Object {
+                  "author": "Developer 1",
+                  "component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest",
+                  "creationDate": "2013-05-13T17:55:39+0200",
+                  "key": "h5",
+                  "line": 81,
+                  "message": "'3' is a magic number.",
+                  "project": "com.github.kevinsawicki:http-request",
+                  "resolution": undefined,
+                  "rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
+                  "securityCategory": "cat2",
+                  "status": "TO_REVIEW",
+                  "updateDate": "2013-05-13T17:55:39+0200",
+                  "vulnerabilityProbability": "MEDIUM",
+                },
+              ]
+            }
+            onHotspotClick={[MockFunction]}
+            selectedHotspotKey="h2"
+          />
+        </li>
+      </ul>
+    </li>
+  </ul>
+  <ListFooter
+    count={5}
+    loadMore={[MockFunction]}
+    loading={false}
+    total={7}
+  />
+</div>
 `;