]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-12718 Create the hotspot details section
authorPhilippe Perrin <philippe.perrin@sonarsource.com>
Tue, 10 Dec 2019 09:10:35 +0000 (10:10 +0100)
committerSonarTech <sonartech@sonarsource.com>
Mon, 13 Jan 2020 19:46:27 +0000 (20:46 +0100)
31 files changed:
server/sonar-web/src/main/js/api/security-hotspots.ts [new file with mode: 0644]
server/sonar-web/src/main/js/api/securityHotspots.ts [deleted file]
server/sonar-web/src/main/js/app/styles/init/misc.css
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__/SecurityHotspotsAppRenderer-test.tsx.snap
server/sonar-web/src/main/js/apps/securityHotspots/__tests__/utils-test.ts
server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotCategory.tsx
server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotList.tsx
server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotListItem.tsx
server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotViewer.tsx
server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotViewerRenderer.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotViewerTabs.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotCategory-test.tsx
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotList-test.tsx
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotListItem-test.tsx
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotViewer-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotViewerRenderer-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotViewerTabs-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotViewer-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotViewerRenderer-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotViewerTabs-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/securityHotspots/styles.css
server/sonar-web/src/main/js/apps/securityHotspots/utils.ts
server/sonar-web/src/main/js/helpers/mocks/security-hotspots.ts
server/sonar-web/src/main/js/types/security-hotspots.ts [new file with mode: 0644]
server/sonar-web/src/main/js/types/securityHotspots.ts [deleted file]
server/sonar-web/src/main/js/types/types.d.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

diff --git a/server/sonar-web/src/main/js/api/security-hotspots.ts b/server/sonar-web/src/main/js/api/security-hotspots.ts
new file mode 100644 (file)
index 0000000..d9ed71e
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * 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 { getJSON } from 'sonar-ui-common/helpers/request';
+import throwGlobalError from '../app/utils/throwGlobalError';
+import { DetailedHotspot, HotspotSearchResponse } from '../types/security-hotspots';
+
+export function getSecurityHotspots(data: {
+  projectKey: string;
+  p: number;
+  ps: number;
+}): Promise<HotspotSearchResponse> {
+  return getJSON('/api/hotspots/search', data).catch(throwGlobalError);
+}
+
+export function getSecurityHotspotDetails(securityHotspotKey: string): Promise<DetailedHotspot> {
+  return getJSON('/api/hotspots/show', { hotspot: securityHotspotKey }).catch(throwGlobalError);
+}
diff --git a/server/sonar-web/src/main/js/api/securityHotspots.ts b/server/sonar-web/src/main/js/api/securityHotspots.ts
deleted file mode 100644 (file)
index f6a0ab8..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * 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 { getJSON } from 'sonar-ui-common/helpers/request';
-import throwGlobalError from '../app/utils/throwGlobalError';
-import { HotspotSearchResponse } from '../types/securityHotspots';
-
-export function getSecurityHotspots(data: {
-  projectKey: string;
-  p: number;
-  ps: number;
-}): Promise<HotspotSearchResponse> {
-  return getJSON('/api/hotspots/search', data).catch(throwGlobalError);
-}
index 5f3b733f7d17cbd6e38033a54fedce05ce71af14..9c8de00be8e9c2207f943afba7c379feccd803af 100644 (file)
@@ -132,6 +132,10 @@ th.hide-overflow {
   margin-top: 4px !important;
 }
 
+.big-padded {
+  padding: calc(2 * var(--gridSize));
+}
+
 td.little-spacer-left {
   padding-left: 4px !important;
 }
index 9f889e176d7aecae285c332df5b69b1f2c59d150..f960ac80185ce383c81f38a2b582c650e0110f38 100644 (file)
  */
 import * as React from 'react';
 import { addNoFooterPageClass, removeNoFooterPageClass } from 'sonar-ui-common/helpers/pages';
-import { getSecurityHotspots } from '../../api/securityHotspots';
+import { getSecurityHotspots } from '../../api/security-hotspots';
 import { getStandards } from '../../helpers/security-standard';
 import { BranchLike } from '../../types/branch-like';
-import { RawHotspot } from '../../types/securityHotspots';
+import { RawHotspot } from '../../types/security-hotspots';
 import SecurityHotspotsAppRenderer from './SecurityHotspotsAppRenderer';
 import './styles.css';
 import { sortHotspots } from './utils';
@@ -37,7 +37,7 @@ interface Props {
 interface State {
   hotspots: RawHotspot[];
   loading: boolean;
-  securityCategories: T.Dict<{ title: string; description?: string }>;
+  securityCategories: T.StandardSecurityCategories;
   selectedHotspotKey: string | undefined;
 }
 
index 60dcbb1644b9d80ec4d412ad636ef36db8034228..078a1d5b51b1e4735c42524276579e862d880d04 100644 (file)
@@ -26,7 +26,7 @@ import { getBaseUrl } from 'sonar-ui-common/helpers/urls';
 import A11ySkipTarget from '../../app/components/a11y/A11ySkipTarget';
 import Suggestions from '../../app/components/embed-docs-modal/Suggestions';
 import ScreenPositionHelper from '../../components/common/ScreenPositionHelper';
-import { RawHotspot } from '../../types/securityHotspots';
+import { RawHotspot } from '../../types/security-hotspots';
 import FilterBar from './components/FilterBar';
 import HotspotList from './components/HotspotList';
 import HotspotViewer from './components/HotspotViewer';
@@ -37,7 +37,7 @@ export interface SecurityHotspotsAppRendererProps {
   loading: boolean;
   onHotspotClick: (key: string) => void;
   selectedHotspotKey?: string;
-  securityCategories: T.Dict<{ title: string; description?: string }>;
+  securityCategories: T.StandardSecurityCategories;
 }
 
 export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRendererProps) {
@@ -84,7 +84,12 @@ export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRe
                     />
                   </div>
                   <div className="main">
-                    <HotspotViewer />
+                    {selectedHotspotKey && (
+                      <HotspotViewer
+                        hotspotKey={selectedHotspotKey}
+                        securityCategories={securityCategories}
+                      />
+                    )}
                   </div>
                 </div>
               )}
index 3cec33f41d7f07e44f587a04342afbbaff3b2820..b813bc82417b5e67567fd84414f1fcbeba4bc898 100644 (file)
@@ -21,9 +21,9 @@ import { shallow } from 'enzyme';
 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/securityHotspots';
+import { getSecurityHotspots } from '../../../api/security-hotspots';
 import { mockMainBranch } from '../../../helpers/mocks/branch-like';
-import { mockHotspot } from '../../../helpers/mocks/security-hotspots';
+import { mockRawHotspot } from '../../../helpers/mocks/security-hotspots';
 import { getStandards } from '../../../helpers/security-standard';
 import { mockComponent } from '../../../helpers/testMocks';
 import SecurityHotspotsApp from '../SecurityHotspotsApp';
@@ -33,7 +33,7 @@ jest.mock('sonar-ui-common/helpers/pages', () => ({
   removeNoFooterPageClass: jest.fn()
 }));
 
-jest.mock('../../../api/securityHotspots', () => ({
+jest.mock('../../../api/security-hotspots', () => ({
   getSecurityHotspots: jest.fn().mockResolvedValue({ hotspots: [], rules: [] })
 }));
 
@@ -49,7 +49,7 @@ it('should load data correctly', async () => {
   const sonarsourceSecurity = { cat1: { title: 'cat 1' } };
   (getStandards as jest.Mock).mockResolvedValue({ sonarsourceSecurity });
 
-  const hotspots = [mockHotspot()];
+  const hotspots = [mockRawHotspot()];
   (getSecurityHotspots as jest.Mock).mockResolvedValue({
     hotspots
   });
index f3a3f832a204d7f57c59941f5c9caa48d6f1f9c6..8d7db84876b5a21009c27458e447923365ad6fe6 100644 (file)
  */
 import { shallow } from 'enzyme';
 import * as React from 'react';
-import { mockHotspot } from '../../../helpers/mocks/security-hotspots';
+import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper';
+import { mockRawHotspot } from '../../../helpers/mocks/security-hotspots';
 import SecurityHotspotsAppRenderer, {
   SecurityHotspotsAppRendererProps
 } from '../SecurityHotspotsAppRenderer';
 
 it('should render correctly', () => {
   expect(shallowRender()).toMatchSnapshot();
+  expect(
+    shallowRender()
+      .find(ScreenPositionHelper)
+      .dive()
+  ).toMatchSnapshot();
 });
 
 it('should render correctly with hotspots', () => {
-  const hotspots = [mockHotspot({ key: 'h1' }), mockHotspot({ key: 'h2' })];
-  expect(shallowRender({ hotspots })).toMatchSnapshot();
-  expect(shallowRender({ hotspots, selectedHotspotKey: 'h2' })).toMatchSnapshot();
+  const hotspots = [mockRawHotspot({ key: 'h1' }), mockRawHotspot({ key: 'h2' })];
+  expect(
+    shallowRender({ hotspots })
+      .find(ScreenPositionHelper)
+      .dive()
+  ).toMatchSnapshot();
+  expect(
+    shallowRender({ hotspots, selectedHotspotKey: 'h2' })
+      .find(ScreenPositionHelper)
+      .dive()
+  ).toMatchSnapshot();
 });
 
 function shallowRender(props: Partial<SecurityHotspotsAppRendererProps> = {}) {
index 67d90c3488460c82ef27d1ae3bd15dba459f2b22..f73832adf0837fc2cc61c011967c2a4bfc2e5616 100644 (file)
@@ -11,24 +11,232 @@ exports[`should render correctly 1`] = `
 </div>
 `;
 
+exports[`should render correctly 2`] = `
+<div>
+  <div
+    className="wrapper"
+    style={
+      Object {
+        "top": 0,
+      }
+    }
+  >
+    <Suggestions
+      suggestions="security_hotspots"
+    />
+    <HelmetWrapper
+      defer={true}
+      encodeSpecialCharacters={true}
+      title="hotspots.page"
+    />
+    <A11ySkipTarget
+      anchor="security_hotspots_main"
+    />
+    <DeferredSpinner
+      className="huge-spacer-left big-spacer-top"
+      loading={false}
+      timeout={100}
+    >
+      <div
+        className="display-flex-column display-flex-center"
+      >
+        <img
+          alt="hotspots.page"
+          className="huge-spacer-top"
+          height={166}
+          src="/images/hotspot-large.svg"
+        />
+        <h1
+          className="huge-spacer-top"
+        >
+          hotspots.no_hotspots.title
+        </h1>
+        <div
+          className="abs-width-400 text-center big-spacer-top"
+        >
+          hotspots.no_hotspots.description
+        </div>
+        <Link
+          className="big-spacer-top"
+          onlyActiveOnIndex={false}
+          style={Object {}}
+          target="_blank"
+          to={
+            Object {
+              "pathname": "/documentation/user-guide/security-hotspots/",
+            }
+          }
+        >
+          hotspots.learn_more
+        </Link>
+      </div>
+    </DeferredSpinner>
+  </div>
+</div>
+`;
+
 exports[`should render correctly with hotspots 1`] = `
-<div
-  id="security_hotspots"
->
-  <FilterBar />
-  <ScreenPositionHelper>
-    <Component />
-  </ScreenPositionHelper>
+<div>
+  <div
+    className="wrapper"
+    style={
+      Object {
+        "top": 0,
+      }
+    }
+  >
+    <Suggestions
+      suggestions="security_hotspots"
+    />
+    <HelmetWrapper
+      defer={true}
+      encodeSpecialCharacters={true}
+      title="hotspots.page"
+    />
+    <A11ySkipTarget
+      anchor="security_hotspots_main"
+    />
+    <DeferredSpinner
+      className="huge-spacer-left big-spacer-top"
+      loading={false}
+      timeout={100}
+    >
+      <div
+        className="layout-page"
+      >
+        <div
+          className="sidebar"
+        >
+          <HotspotList
+            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": "FALSE-POSITIVE",
+                  "rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
+                  "securityCategory": "command-injection",
+                  "status": "RESOLVED",
+                  "updateDate": "2013-05-13T17:55:39+0200",
+                  "vulnerabilityProbability": "HIGH",
+                },
+                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": "FALSE-POSITIVE",
+                  "rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
+                  "securityCategory": "command-injection",
+                  "status": "RESOLVED",
+                  "updateDate": "2013-05-13T17:55:39+0200",
+                  "vulnerabilityProbability": "HIGH",
+                },
+              ]
+            }
+            onHotspotClick={[MockFunction]}
+            securityCategories={Object {}}
+          />
+        </div>
+        <div
+          className="main"
+        />
+      </div>
+    </DeferredSpinner>
+  </div>
 </div>
 `;
 
 exports[`should render correctly with hotspots 2`] = `
-<div
-  id="security_hotspots"
->
-  <FilterBar />
-  <ScreenPositionHelper>
-    <Component />
-  </ScreenPositionHelper>
+<div>
+  <div
+    className="wrapper"
+    style={
+      Object {
+        "top": 0,
+      }
+    }
+  >
+    <Suggestions
+      suggestions="security_hotspots"
+    />
+    <HelmetWrapper
+      defer={true}
+      encodeSpecialCharacters={true}
+      title="hotspots.page"
+    />
+    <A11ySkipTarget
+      anchor="security_hotspots_main"
+    />
+    <DeferredSpinner
+      className="huge-spacer-left big-spacer-top"
+      loading={false}
+      timeout={100}
+    >
+      <div
+        className="layout-page"
+      >
+        <div
+          className="sidebar"
+        >
+          <HotspotList
+            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": "FALSE-POSITIVE",
+                  "rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
+                  "securityCategory": "command-injection",
+                  "status": "RESOLVED",
+                  "updateDate": "2013-05-13T17:55:39+0200",
+                  "vulnerabilityProbability": "HIGH",
+                },
+                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": "FALSE-POSITIVE",
+                  "rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
+                  "securityCategory": "command-injection",
+                  "status": "RESOLVED",
+                  "updateDate": "2013-05-13T17:55:39+0200",
+                  "vulnerabilityProbability": "HIGH",
+                },
+              ]
+            }
+            onHotspotClick={[MockFunction]}
+            securityCategories={Object {}}
+            selectedHotspotKey="h2"
+          />
+        </div>
+        <div
+          className="main"
+        >
+          <HotspotViewer
+            hotspotKey="h2"
+            securityCategories={Object {}}
+          />
+        </div>
+      </div>
+    </DeferredSpinner>
+  </div>
 </div>
 `;
index 1221e827d3f196ba020299458815174d9f8acc97..3d4fa35952482b8df3f951830b6a5eaacae7502d 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { mockHotspot } from '../../../helpers/mocks/security-hotspots';
-import { RiskExposure } from '../../../types/securityHotspots';
+import { mockRawHotspot } from '../../../helpers/mocks/security-hotspots';
+import { RiskExposure } from '../../../types/security-hotspots';
 import { groupByCategory, mapRules, sortHotspots } from '../utils';
 
 const hotspots = [
-  mockHotspot({
+  mockRawHotspot({
     key: '3',
     vulnerabilityProbability: RiskExposure.HIGH,
     securityCategory: 'object-injection',
     message: 'tfdh'
   }),
-  mockHotspot({
+  mockRawHotspot({
     key: '5',
     vulnerabilityProbability: RiskExposure.MEDIUM,
     securityCategory: 'xpath-injection',
     message: 'asdf'
   }),
-  mockHotspot({
+  mockRawHotspot({
     key: '1',
     vulnerabilityProbability: RiskExposure.HIGH,
     securityCategory: 'dos',
     message: 'a'
   }),
-  mockHotspot({
+  mockRawHotspot({
     key: '7',
     vulnerabilityProbability: RiskExposure.LOW,
     securityCategory: 'ssrf',
     message: 'rrrr'
   }),
-  mockHotspot({
+  mockRawHotspot({
     key: '2',
     vulnerabilityProbability: RiskExposure.HIGH,
     securityCategory: 'dos',
     message: 'b'
   }),
-  mockHotspot({
+  mockRawHotspot({
     key: '8',
     vulnerabilityProbability: RiskExposure.LOW,
     securityCategory: 'ssrf',
     message: 'sssss'
   }),
-  mockHotspot({
+  mockRawHotspot({
     key: '4',
     vulnerabilityProbability: RiskExposure.MEDIUM,
     securityCategory: 'log-injection',
     message: 'asdf'
   }),
-  mockHotspot({
+  mockRawHotspot({
     key: '9',
     vulnerabilityProbability: RiskExposure.LOW,
     securityCategory: 'xxe',
     message: 'aaa'
   }),
-  mockHotspot({
+  mockRawHotspot({
     key: '6',
     vulnerabilityProbability: RiskExposure.LOW,
     securityCategory: 'xss',
index ee9109cee463487422d76015bb22cbc9ce0c020f..e4c32574ac23efa2e553b44bc138452dde7d0e4e 100644 (file)
@@ -21,7 +21,7 @@ import * as classNames from 'classnames';
 import * as React from 'react';
 import ChevronDownIcon from 'sonar-ui-common/components/icons/ChevronDownIcon';
 import ChevronUpIcon from 'sonar-ui-common/components/icons/ChevronUpIcon';
-import { RawHotspot } from '../../../types/securityHotspots';
+import { RawHotspot } from '../../../types/security-hotspots';
 import HotspotListItem from './HotspotListItem';
 
 export interface HotspotCategoryProps {
index a33c3b84c3543f93d9ab9fe1fa9f7cc716e97606..d9a054f379e6ed22eaea9fbe3cd61f257844e37f 100644 (file)
@@ -22,7 +22,7 @@ import { groupBy } from 'lodash';
 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/securityHotspots';
+import { RawHotspot, RiskExposure } from '../../../types/security-hotspots';
 import { groupByCategory, RISK_EXPOSURE_LEVELS } from '../utils';
 import HotspotCategory from './HotspotCategory';
 import './HotspotList.css';
@@ -30,7 +30,7 @@ import './HotspotList.css';
 export interface HotspotListProps {
   hotspots: RawHotspot[];
   onHotspotClick: (key: string) => void;
-  securityCategories: T.Dict<{ title: string; description?: string }>;
+  securityCategories: T.StandardSecurityCategories;
   selectedHotspotKey: string | undefined;
 }
 
index 549c8e35bbaebb0ca50a907f9d593f26e0e5f6d7..1bdfa7830a3fbe63e51fb9029bcf5175fdbbcb5f 100644 (file)
@@ -20,7 +20,7 @@
 import * as classNames from 'classnames';
 import * as React from 'react';
 import { translate } from 'sonar-ui-common/helpers/l10n';
-import { RawHotspot } from '../../../types/securityHotspots';
+import { RawHotspot } from '../../../types/security-hotspots';
 
 export interface HotspotListItemProps {
   hotspot: RawHotspot;
index c4e8c911f98281fa0237356ea363fe36c54497b7..35192c96b62860a263d9aad979026dd19318abf4 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+
 import * as React from 'react';
+import { getSecurityHotspotDetails } from '../../../api/security-hotspots';
+import { DetailedHotspot } from '../../../types/security-hotspots';
+import HotspotViewerRenderer from './HotspotViewerRenderer';
+
+interface Props {
+  hotspotKey: string;
+  securityCategories: T.StandardSecurityCategories;
+}
+
+interface State {
+  hotspot?: DetailedHotspot;
+  loading: boolean;
+}
+
+export default class HotspotViewer extends React.PureComponent<Props, State> {
+  mounted = false;
+
+  componentWillMount() {
+    this.mounted = true;
+    this.fetchHotspot();
+  }
+
+  componentDidUpdate(prevProps: Props) {
+    if (prevProps.hotspotKey !== this.props.hotspotKey) {
+      this.fetchHotspot();
+    }
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  fetchHotspot() {
+    this.setState({ loading: true });
+    return getSecurityHotspotDetails(this.props.hotspotKey)
+      .then(hotspot => this.mounted && this.setState({ hotspot }))
+      .finally(() => this.mounted && this.setState({ loading: false }));
+  }
 
-export interface Props {}
+  render() {
+    const { securityCategories } = this.props;
+    const { hotspot, loading } = this.state;
 
-export default function HotspotViewer(props: Props) {
-  return (
-    <div {...props} className="hotspot-viewer">
-      Show hotspot details
-    </div>
-  );
+    return (
+      <HotspotViewerRenderer
+        hotspot={hotspot}
+        loading={loading}
+        securityCategories={securityCategories}
+      />
+    );
+  }
 }
diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotViewerRenderer.tsx b/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotViewerRenderer.tsx
new file mode 100644 (file)
index 0000000..9764ff4
--- /dev/null
@@ -0,0 +1,69 @@
+/*
+ * 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 * as React from 'react';
+import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
+import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
+import { DetailedHotspot } from '../../../types/security-hotspots';
+import HotspotViewerTabs from './HotspotViewerTabs';
+
+export interface HotspotViewerRendererProps {
+  hotspot?: DetailedHotspot;
+  loading: boolean;
+  securityCategories: T.StandardSecurityCategories;
+}
+
+export default function HotspotViewerRenderer(props: HotspotViewerRendererProps) {
+  const { hotspot, loading, securityCategories } = props;
+
+  return (
+    <DeferredSpinner loading={loading}>
+      {hotspot && (
+        <div className="big-padded">
+          <div className="big-spacer-bottom">
+            <h1>{hotspot.message}</h1>
+            <div className="text-muted">
+              <span>{translate('hotspot.category')}</span>
+              <span className="little-spacer-left">
+                {securityCategories[hotspot.rule.securityCategory].title}
+              </span>
+            </div>
+          </div>
+          <div className="huge-spacer-bottom">
+            <span>{translate('hotspot.status')}</span>
+            <span className="badge little-spacer-left">
+              {translate('issue.status', hotspot.status)}
+            </span>
+            {hotspot.assignee && hotspot.assignee.name && (
+              <>
+                <span className="huge-spacer-left">{translate('hotspot.assigned_to')}</span>
+                <strong className="little-spacer-left">
+                  {hotspot.assignee.active
+                    ? hotspot.assignee.name
+                    : translateWithParameters('user.x_deleted', hotspot.assignee.name)}
+                </strong>
+              </>
+            )}
+          </div>
+          <HotspotViewerTabs hotspot={hotspot} />
+        </div>
+      )}
+    </DeferredSpinner>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotViewerTabs.tsx b/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotViewerTabs.tsx
new file mode 100644 (file)
index 0000000..e02c11b
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ * 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 { sanitize } from 'dompurify';
+import * as React from 'react';
+import BoxedTabs from 'sonar-ui-common/components/controls/BoxedTabs';
+import { translate } from 'sonar-ui-common/helpers/l10n';
+import { DetailedHotspot } from '../../../types/security-hotspots';
+
+export interface HotspotViewerTabsProps {
+  hotspot: DetailedHotspot;
+}
+
+export enum Tabs {
+  RiskDescription = 'risk',
+  VulnerabilityDescription = 'vulnerability',
+  FixRecommendation = 'fix'
+}
+
+export default function HotspotViewerTabs(props: HotspotViewerTabsProps) {
+  const { hotspot } = props;
+  const [currentTab, setCurrentTab] = React.useState(Tabs.RiskDescription);
+
+  const tabs = {
+    [Tabs.RiskDescription]: {
+      title: translate('hotspot.tabs.risk_description'),
+      content: hotspot.rule.riskDescription || ''
+    },
+    [Tabs.VulnerabilityDescription]: {
+      title: translate('hotspot.tabs.vulnerability_description'),
+      content: hotspot.rule.vulnerabilityDescription || ''
+    },
+    [Tabs.FixRecommendation]: {
+      title: translate('hotspot.tabs.fix_recommendations'),
+      content: hotspot.rule.fixRecommendations || ''
+    }
+  };
+
+  const tabsToDisplay = Object.values(Tabs)
+    .filter(tab => Boolean(tabs[tab].content))
+    .map(tab => ({ key: tab, label: tabs[tab].title }));
+
+  if (tabsToDisplay.length === 0) {
+    return null;
+  }
+
+  if (!tabsToDisplay.find(tab => tab.key === currentTab)) {
+    setCurrentTab(tabsToDisplay[0].key);
+  }
+
+  return (
+    <>
+      <BoxedTabs onSelect={tab => setCurrentTab(tab)} selected={currentTab} tabs={tabsToDisplay} />
+      <div
+        className="boxed-group markdown big-padded"
+        dangerouslySetInnerHTML={{ __html: sanitize(tabs[currentTab].content) }}
+      />
+    </>
+  );
+}
index f1d3312579c4afb4703771587d8bf9cc380174d2..757850291aa7d617f5ce4110528e58a7e8a7a264 100644 (file)
@@ -19,7 +19,7 @@
  */
 import { shallow } from 'enzyme';
 import * as React from 'react';
-import { mockHotspot } from '../../../../helpers/mocks/security-hotspots';
+import { mockRawHotspot } from '../../../../helpers/mocks/security-hotspots';
 import HotspotCategory, { HotspotCategoryProps } from '../HotspotCategory';
 
 it('should render correctly', () => {
@@ -27,12 +27,12 @@ it('should render correctly', () => {
 });
 
 it('should render correctly with hotspots', () => {
-  const hotspots = [mockHotspot({ key: 'h1' }), mockHotspot({ key: 'h2' })];
+  const hotspots = [mockRawHotspot({ key: 'h1' }), mockRawHotspot({ key: 'h2' })];
   expect(shallowRender({ hotspots })).toMatchSnapshot();
 });
 
 it('should handle collapse and expand', () => {
-  const wrapper = shallowRender({ hotspots: [mockHotspot()] });
+  const wrapper = shallowRender({ hotspots: [mockRawHotspot()] });
 
   wrapper.find('.hotspot-category-header').simulate('click');
 
index b48e336cd1961559b6d9e8423608cc1681f359ec..217a182cccda7b90aac1e0749f964af02ea9991e 100644 (file)
@@ -19,8 +19,8 @@
  */
 import { shallow } from 'enzyme';
 import * as React from 'react';
-import { mockHotspot } from '../../../../helpers/mocks/security-hotspots';
-import { RiskExposure } from '../../../../types/securityHotspots';
+import { mockRawHotspot } from '../../../../helpers/mocks/security-hotspots';
+import { RiskExposure } from '../../../../types/security-hotspots';
 import HotspotList, { HotspotListProps } from '../HotspotList';
 
 it('should render correctly', () => {
@@ -29,19 +29,19 @@ it('should render correctly', () => {
 
 it('should render correctly with hotspots', () => {
   const hotspots = [
-    mockHotspot({ key: 'h1', securityCategory: 'cat2' }),
-    mockHotspot({ key: 'h2', securityCategory: 'cat1' }),
-    mockHotspot({
+    mockRawHotspot({ key: 'h1', securityCategory: 'cat2' }),
+    mockRawHotspot({ key: 'h2', securityCategory: 'cat1' }),
+    mockRawHotspot({
       key: 'h3',
       securityCategory: 'cat1',
       vulnerabilityProbability: RiskExposure.MEDIUM
     }),
-    mockHotspot({
+    mockRawHotspot({
       key: 'h4',
       securityCategory: 'cat1',
       vulnerabilityProbability: RiskExposure.MEDIUM
     }),
-    mockHotspot({
+    mockRawHotspot({
       key: 'h5',
       securityCategory: 'cat2',
       vulnerabilityProbability: RiskExposure.MEDIUM
index e9a7b7d0082f70065a2b5b5487946a8ed137b6d9..d69518bed0f9b4dbe8bd70463532183177ae84ad 100644 (file)
@@ -19,7 +19,7 @@
  */
 import { shallow } from 'enzyme';
 import * as React from 'react';
-import { mockHotspot } from '../../../../helpers/mocks/security-hotspots';
+import { mockRawHotspot } from '../../../../helpers/mocks/security-hotspots';
 import { HotspotListItem, HotspotListItemProps } from '../HotspotListItem';
 
 it('should render correctly', () => {
@@ -28,7 +28,7 @@ it('should render correctly', () => {
 });
 
 it('should handle click', () => {
-  const hotspot = mockHotspot({ key: 'hotspotKey' });
+  const hotspot = mockRawHotspot({ key: 'hotspotKey' });
   const onClick = jest.fn();
   const wrapper = shallowRender({ hotspot, onClick });
 
@@ -39,6 +39,6 @@ it('should handle click', () => {
 
 function shallowRender(props: Partial<HotspotListItemProps> = {}) {
   return shallow(
-    <HotspotListItem hotspot={mockHotspot()} onClick={jest.fn()} selected={false} {...props} />
+    <HotspotListItem hotspot={mockRawHotspot()} onClick={jest.fn()} selected={false} {...props} />
   );
 }
diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotViewer-test.tsx b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotViewer-test.tsx
new file mode 100644 (file)
index 0000000..380d91b
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+ * 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 { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
+import { getSecurityHotspotDetails } from '../../../../api/security-hotspots';
+import HotspotViewer from '../HotspotViewer';
+
+const hotspotKey = 'hotspot-key';
+
+jest.mock('../../../../api/security-hotspots', () => ({
+  getSecurityHotspotDetails: jest.fn().mockResolvedValue({ id: `I am a detailled hotspot` })
+}));
+
+it('should render correctly', async () => {
+  const wrapper = shallowRender();
+  expect(wrapper).toMatchSnapshot();
+
+  await waitAndUpdate(wrapper);
+
+  expect(wrapper).toMatchSnapshot();
+  expect(getSecurityHotspotDetails).toHaveBeenCalledWith(hotspotKey);
+
+  const newHotspotKey = `new-${hotspotKey}`;
+  wrapper.setProps({ hotspotKey: newHotspotKey });
+
+  await waitAndUpdate(wrapper);
+  expect(getSecurityHotspotDetails).toHaveBeenCalledWith(newHotspotKey);
+});
+
+function shallowRender(props?: Partial<HotspotViewer['props']>) {
+  return shallow<HotspotViewer>(
+    <HotspotViewer
+      hotspotKey={hotspotKey}
+      securityCategories={{ cat1: { title: 'cat1' } }}
+      {...props}
+    />
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotViewerRenderer-test.tsx b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotViewerRenderer-test.tsx
new file mode 100644 (file)
index 0000000..700d0cf
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * 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 { mockDetailledHotspot } from '../../../../helpers/mocks/security-hotspots';
+import { mockUser } from '../../../../helpers/testMocks';
+import HotspotViewerRenderer, { HotspotViewerRendererProps } from '../HotspotViewerRenderer';
+
+it('should render correctly', () => {
+  const wrapper = shallowRender();
+  expect(wrapper).toMatchSnapshot();
+  expect(shallowRender({ hotspot: undefined })).toMatchSnapshot('no hotspot');
+  expect(
+    shallowRender({ hotspot: mockDetailledHotspot({ assignee: mockUser({ active: false }) }) })
+  ).toMatchSnapshot('deleted assignee');
+});
+
+function shallowRender(props?: Partial<HotspotViewerRendererProps>) {
+  return shallow(
+    <HotspotViewerRenderer
+      hotspot={mockDetailledHotspot()}
+      loading={false}
+      securityCategories={{ 'sql-injection': { title: 'SQL injection' } }}
+      {...props}
+    />
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotViewerTabs-test.tsx b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotViewerTabs-test.tsx
new file mode 100644 (file)
index 0000000..12fa762
--- /dev/null
@@ -0,0 +1,68 @@
+/*
+ * 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 BoxedTabs from 'sonar-ui-common/components/controls/BoxedTabs';
+import {
+  mockDetailledHotspot,
+  mockDetailledHotspotRule
+} from '../../../../helpers/mocks/security-hotspots';
+import HotspotViewerTabs, { HotspotViewerTabsProps, Tabs } from '../HotspotViewerTabs';
+
+it('should render correctly', () => {
+  const wrapper = shallowRender();
+  expect(wrapper).toMatchSnapshot('risk');
+
+  const onSelect = wrapper.find(BoxedTabs).prop('onSelect') as (tab: Tabs) => void;
+
+  if (!onSelect) {
+    fail('onSelect should be defined');
+  } else {
+    onSelect(Tabs.VulnerabilityDescription);
+    expect(wrapper).toMatchSnapshot('vulnerability');
+
+    onSelect(Tabs.FixRecommendation);
+    expect(wrapper).toMatchSnapshot('fix');
+  }
+
+  expect(
+    shallowRender({
+      hotspot: mockDetailledHotspot({
+        rule: mockDetailledHotspotRule({ riskDescription: undefined })
+      })
+    })
+  ).toMatchSnapshot('empty tab');
+
+  expect(
+    shallowRender({
+      hotspot: mockDetailledHotspot({
+        rule: mockDetailledHotspotRule({
+          riskDescription: undefined,
+          fixRecommendations: undefined,
+          vulnerabilityDescription: undefined
+        })
+      })
+    })
+  ).toMatchSnapshot('no tabs');
+});
+
+function shallowRender(props?: Partial<HotspotViewerTabsProps>) {
+  return shallow(<HotspotViewerTabs hotspot={mockDetailledHotspot()} {...props} />);
+}
diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotViewer-test.tsx.snap b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotViewer-test.tsx.snap
new file mode 100644 (file)
index 0000000..edce37a
--- /dev/null
@@ -0,0 +1,32 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<HotspotViewerRenderer
+  loading={true}
+  securityCategories={
+    Object {
+      "cat1": Object {
+        "title": "cat1",
+      },
+    }
+  }
+/>
+`;
+
+exports[`should render correctly 2`] = `
+<HotspotViewerRenderer
+  hotspot={
+    Object {
+      "id": "I am a detailled hotspot",
+    }
+  }
+  loading={false}
+  securityCategories={
+    Object {
+      "cat1": Object {
+        "title": "cat1",
+      },
+    }
+  }
+/>
+`;
diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotViewerRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotViewerRenderer-test.tsx.snap
new file mode 100644 (file)
index 0000000..dc2f581
--- /dev/null
@@ -0,0 +1,278 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<DeferredSpinner
+  loading={false}
+  timeout={100}
+>
+  <div
+    className="big-padded"
+  >
+    <div
+      className="big-spacer-bottom"
+    >
+      <h1>
+        '3' is a magic number.
+      </h1>
+      <div
+        className="text-muted"
+      >
+        <span>
+          hotspot.category
+        </span>
+        <span
+          className="little-spacer-left"
+        >
+          SQL injection
+        </span>
+      </div>
+    </div>
+    <div
+      className="huge-spacer-bottom"
+    >
+      <span>
+        hotspot.status
+      </span>
+      <span
+        className="badge little-spacer-left"
+      >
+        issue.status.RESOLVED
+      </span>
+      <span
+        className="huge-spacer-left"
+      >
+        hotspot.assigned_to
+      </span>
+      <strong
+        className="little-spacer-left"
+      >
+        John Doe
+      </strong>
+    </div>
+    <HotspotViewerTabs
+      hotspot={
+        Object {
+          "assignee": Object {
+            "active": true,
+            "local": true,
+            "login": "john.doe",
+            "name": "John Doe",
+          },
+          "author": Object {
+            "active": true,
+            "local": true,
+            "login": "john.doe",
+            "name": "John Doe",
+          },
+          "component": Object {
+            "breadcrumbs": Array [],
+            "key": "my-project",
+            "name": "MyProject",
+            "organization": "foo",
+            "qualifier": "FIL",
+            "qualityGate": Object {
+              "isDefault": true,
+              "key": "30",
+              "name": "Sonar way",
+            },
+            "qualityProfiles": Array [
+              Object {
+                "deleted": false,
+                "key": "my-qp",
+                "language": "ts",
+                "name": "Sonar way",
+              },
+            ],
+            "tags": Array [],
+          },
+          "creationDate": "2013-05-13T17:55:41+0200",
+          "key": "01fc972e-2a3c-433e-bcae-0bd7f88f5123",
+          "line": 142,
+          "message": "'3' is a magic number.",
+          "project": Object {
+            "breadcrumbs": Array [],
+            "key": "my-project",
+            "name": "MyProject",
+            "organization": "foo",
+            "qualifier": "TRK",
+            "qualityGate": Object {
+              "isDefault": true,
+              "key": "30",
+              "name": "Sonar way",
+            },
+            "qualityProfiles": Array [
+              Object {
+                "deleted": false,
+                "key": "my-qp",
+                "language": "ts",
+                "name": "Sonar way",
+              },
+            ],
+            "tags": Array [],
+          },
+          "resolution": "FALSE-POSITIVE",
+          "rule": Object {
+            "fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>",
+            "key": "squid:S2077",
+            "name": "That rule",
+            "riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>",
+            "securityCategory": "sql-injection",
+            "vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
+            "vulnerabilityProbability": "HIGH",
+          },
+          "status": "RESOLVED",
+          "textRange": Object {
+            "endLine": 142,
+            "endOffset": 83,
+            "startLine": 142,
+            "startOffset": 26,
+          },
+          "updateDate": "2013-05-13T17:55:42+0200",
+        }
+      }
+    />
+  </div>
+</DeferredSpinner>
+`;
+
+exports[`should render correctly: deleted assignee 1`] = `
+<DeferredSpinner
+  loading={false}
+  timeout={100}
+>
+  <div
+    className="big-padded"
+  >
+    <div
+      className="big-spacer-bottom"
+    >
+      <h1>
+        '3' is a magic number.
+      </h1>
+      <div
+        className="text-muted"
+      >
+        <span>
+          hotspot.category
+        </span>
+        <span
+          className="little-spacer-left"
+        >
+          SQL injection
+        </span>
+      </div>
+    </div>
+    <div
+      className="huge-spacer-bottom"
+    >
+      <span>
+        hotspot.status
+      </span>
+      <span
+        className="badge little-spacer-left"
+      >
+        issue.status.RESOLVED
+      </span>
+      <span
+        className="huge-spacer-left"
+      >
+        hotspot.assigned_to
+      </span>
+      <strong
+        className="little-spacer-left"
+      >
+        user.x_deleted.John Doe
+      </strong>
+    </div>
+    <HotspotViewerTabs
+      hotspot={
+        Object {
+          "assignee": Object {
+            "active": false,
+            "local": true,
+            "login": "john.doe",
+            "name": "John Doe",
+          },
+          "author": Object {
+            "active": true,
+            "local": true,
+            "login": "john.doe",
+            "name": "John Doe",
+          },
+          "component": Object {
+            "breadcrumbs": Array [],
+            "key": "my-project",
+            "name": "MyProject",
+            "organization": "foo",
+            "qualifier": "FIL",
+            "qualityGate": Object {
+              "isDefault": true,
+              "key": "30",
+              "name": "Sonar way",
+            },
+            "qualityProfiles": Array [
+              Object {
+                "deleted": false,
+                "key": "my-qp",
+                "language": "ts",
+                "name": "Sonar way",
+              },
+            ],
+            "tags": Array [],
+          },
+          "creationDate": "2013-05-13T17:55:41+0200",
+          "key": "01fc972e-2a3c-433e-bcae-0bd7f88f5123",
+          "line": 142,
+          "message": "'3' is a magic number.",
+          "project": Object {
+            "breadcrumbs": Array [],
+            "key": "my-project",
+            "name": "MyProject",
+            "organization": "foo",
+            "qualifier": "TRK",
+            "qualityGate": Object {
+              "isDefault": true,
+              "key": "30",
+              "name": "Sonar way",
+            },
+            "qualityProfiles": Array [
+              Object {
+                "deleted": false,
+                "key": "my-qp",
+                "language": "ts",
+                "name": "Sonar way",
+              },
+            ],
+            "tags": Array [],
+          },
+          "resolution": "FALSE-POSITIVE",
+          "rule": Object {
+            "fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>",
+            "key": "squid:S2077",
+            "name": "That rule",
+            "riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>",
+            "securityCategory": "sql-injection",
+            "vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
+            "vulnerabilityProbability": "HIGH",
+          },
+          "status": "RESOLVED",
+          "textRange": Object {
+            "endLine": 142,
+            "endOffset": 83,
+            "startLine": 142,
+            "startOffset": 26,
+          },
+          "updateDate": "2013-05-13T17:55:42+0200",
+        }
+      }
+    />
+  </div>
+</DeferredSpinner>
+`;
+
+exports[`should render correctly: no hotspot 1`] = `
+<DeferredSpinner
+  loading={false}
+  timeout={100}
+/>
+`;
diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotViewerTabs-test.tsx.snap b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotViewerTabs-test.tsx.snap
new file mode 100644 (file)
index 0000000..5e0f68e
--- /dev/null
@@ -0,0 +1,131 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly: empty tab 1`] = `
+<Fragment>
+  <BoxedTabs
+    onSelect={[Function]}
+    selected="vulnerability"
+    tabs={
+      Array [
+        Object {
+          "key": "vulnerability",
+          "label": "hotspot.tabs.vulnerability_description",
+        },
+        Object {
+          "key": "fix",
+          "label": "hotspot.tabs.fix_recommendations",
+        },
+      ]
+    }
+  />
+  <div
+    className="boxed-group markdown big-padded"
+    dangerouslySetInnerHTML={
+      Object {
+        "__html": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
+      }
+    }
+  />
+</Fragment>
+`;
+
+exports[`should render correctly: fix 1`] = `
+<Fragment>
+  <BoxedTabs
+    onSelect={[Function]}
+    selected="fix"
+    tabs={
+      Array [
+        Object {
+          "key": "risk",
+          "label": "hotspot.tabs.risk_description",
+        },
+        Object {
+          "key": "vulnerability",
+          "label": "hotspot.tabs.vulnerability_description",
+        },
+        Object {
+          "key": "fix",
+          "label": "hotspot.tabs.fix_recommendations",
+        },
+      ]
+    }
+  />
+  <div
+    className="boxed-group markdown big-padded"
+    dangerouslySetInnerHTML={
+      Object {
+        "__html": "<p>This a <strong>strong</strong> message about fixing !</p>",
+      }
+    }
+  />
+</Fragment>
+`;
+
+exports[`should render correctly: no tabs 1`] = `""`;
+
+exports[`should render correctly: risk 1`] = `
+<Fragment>
+  <BoxedTabs
+    onSelect={[Function]}
+    selected="risk"
+    tabs={
+      Array [
+        Object {
+          "key": "risk",
+          "label": "hotspot.tabs.risk_description",
+        },
+        Object {
+          "key": "vulnerability",
+          "label": "hotspot.tabs.vulnerability_description",
+        },
+        Object {
+          "key": "fix",
+          "label": "hotspot.tabs.fix_recommendations",
+        },
+      ]
+    }
+  />
+  <div
+    className="boxed-group markdown big-padded"
+    dangerouslySetInnerHTML={
+      Object {
+        "__html": "<p>This a <strong>strong</strong> message about risk !</p>",
+      }
+    }
+  />
+</Fragment>
+`;
+
+exports[`should render correctly: vulnerability 1`] = `
+<Fragment>
+  <BoxedTabs
+    onSelect={[Function]}
+    selected="vulnerability"
+    tabs={
+      Array [
+        Object {
+          "key": "risk",
+          "label": "hotspot.tabs.risk_description",
+        },
+        Object {
+          "key": "vulnerability",
+          "label": "hotspot.tabs.vulnerability_description",
+        },
+        Object {
+          "key": "fix",
+          "label": "hotspot.tabs.fix_recommendations",
+        },
+      ]
+    }
+  />
+  <div
+    className="boxed-group markdown big-padded"
+    dangerouslySetInnerHTML={
+      Object {
+        "__html": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
+      }
+    }
+  />
+</Fragment>
+`;
index b76b508053d8e354911f18fac6b80cd3b5a4a867..187f9e9a3402be27b103af8b78e71ba0220a4253 100644 (file)
@@ -48,4 +48,5 @@
 #security_hotspots .main {
   flex: 1 0 70%;
   overflow-y: auto;
+  background-color: white;
 }
index 147588deaa07561375d5b224b62abc73574b88cd..c02298354b54185942c60d6b724312e18f8eccc5 100644 (file)
@@ -18,7 +18,7 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import { groupBy, sortBy } from 'lodash';
-import { RawHotspot, RiskExposure } from '../../types/securityHotspots';
+import { RawHotspot, RiskExposure } from '../../types/security-hotspots';
 
 export const RISK_EXPOSURE_LEVELS = [RiskExposure.HIGH, RiskExposure.MEDIUM, RiskExposure.LOW];
 
@@ -31,7 +31,7 @@ export function mapRules(rules: Array<{ key: string; name: string }>): T.Dict<st
 
 export function groupByCategory(
   hotspots: RawHotspot[] = [],
-  securityCategories: T.Dict<{ title: string; description?: string }>
+  securityCategories: T.StandardSecurityCategories
 ) {
   const groups = groupBy(hotspots, h => h.securityCategory);
 
@@ -56,9 +56,6 @@ export function sortHotspots(
   ]);
 }
 
-function getCategoryTitle(
-  key: string,
-  securityCategories: T.Dict<{ title: string; description?: string }>
-) {
+function getCategoryTitle(key: string, securityCategories: T.StandardSecurityCategories) {
   return securityCategories[key] ? securityCategories[key].title : key;
 }
index f1c4be442c9e3633860cac7aa236afc9310a56cc..007a7c29df272bae8b6902eaa74a9718d5b15e38 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { RawHotspot, RiskExposure } from '../../types/securityHotspots';
+import { ComponentQualifier } from '../../types/component';
+import {
+  DetailedHotspot,
+  DetailedHotspotRule,
+  RawHotspot,
+  RiskExposure
+} from '../../types/security-hotspots';
+import { mockComponent, mockUser } from '../testMocks';
 
-export function mockHotspot(overrides: Partial<RawHotspot> = {}): RawHotspot {
+export function mockRawHotspot(overrides: Partial<RawHotspot> = {}): RawHotspot {
   return {
     key: '01fc972e-2a3c-433e-bcae-0bd7f88f5123',
     component: 'com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest',
@@ -37,3 +44,42 @@ export function mockHotspot(overrides: Partial<RawHotspot> = {}): RawHotspot {
     ...overrides
   };
 }
+
+export function mockDetailledHotspot(overrides?: Partial<DetailedHotspot>): DetailedHotspot {
+  return {
+    assignee: mockUser(),
+    author: mockUser(),
+    component: mockComponent({ qualifier: ComponentQualifier.File }),
+    creationDate: '2013-05-13T17:55:41+0200',
+    key: '01fc972e-2a3c-433e-bcae-0bd7f88f5123',
+    line: 142,
+    message: "'3' is a magic number.",
+    project: mockComponent({ qualifier: ComponentQualifier.Project }),
+    resolution: 'FALSE-POSITIVE',
+    rule: mockDetailledHotspotRule(),
+    status: 'RESOLVED',
+    textRange: {
+      startLine: 142,
+      endLine: 142,
+      startOffset: 26,
+      endOffset: 83
+    },
+    updateDate: '2013-05-13T17:55:42+0200',
+    ...overrides
+  };
+}
+
+export function mockDetailledHotspotRule(
+  overrides?: Partial<DetailedHotspotRule>
+): DetailedHotspotRule {
+  return {
+    key: 'squid:S2077',
+    name: 'That rule',
+    fixRecommendations: '<p>This a <strong>strong</strong> message about fixing !</p>',
+    riskDescription: '<p>This a <strong>strong</strong> message about risk !</p>',
+    vulnerabilityDescription: '<p>This a <strong>strong</strong> message about vulnerability !</p>',
+    vulnerabilityProbability: RiskExposure.HIGH,
+    securityCategory: 'sql-injection',
+    ...overrides
+  };
+}
diff --git a/server/sonar-web/src/main/js/types/security-hotspots.ts b/server/sonar-web/src/main/js/types/security-hotspots.ts
new file mode 100644 (file)
index 0000000..76f816e
--- /dev/null
@@ -0,0 +1,74 @@
+/*
+ * 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.
+ */
+export enum RiskExposure {
+  LOW = 'LOW',
+  MEDIUM = 'MEDIUM',
+  HIGH = 'HIGH'
+}
+
+export interface RawHotspot {
+  assignee?: string;
+  author?: string;
+  component: string;
+  creationDate: string;
+  key: string;
+  line?: number;
+  message: string;
+  project: string;
+  resolution: string;
+  rule: string;
+  securityCategory: string;
+  status: string;
+  subProject?: string;
+  updateDate: string;
+  vulnerabilityProbability: RiskExposure;
+}
+
+export interface DetailedHotspot {
+  assignee?: Pick<T.UserBase, 'active' | 'login' | 'name'>;
+  author?: Pick<T.UserBase, 'login'>;
+  component: T.Component;
+  creationDate: string;
+  key: string;
+  line?: number;
+  message: string;
+  project: T.Component;
+  resolution: string;
+  rule: DetailedHotspotRule;
+  status: string;
+  textRange: T.TextRange;
+  updateDate: string;
+}
+
+export interface DetailedHotspotRule {
+  fixRecommendations?: string;
+  key: string;
+  name: string;
+  riskDescription?: string;
+  securityCategory: string;
+  vulnerabilityDescription?: string;
+  vulnerabilityProbability: RiskExposure;
+}
+
+export interface HotspotSearchResponse {
+  components?: { key: string; qualifier: string; name: string }[];
+  hotspots: RawHotspot[];
+  paging: T.Paging;
+}
diff --git a/server/sonar-web/src/main/js/types/securityHotspots.ts b/server/sonar-web/src/main/js/types/securityHotspots.ts
deleted file mode 100644 (file)
index dc95ad9..0000000
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * 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.
- */
-export enum RiskExposure {
-  LOW = 'LOW',
-  MEDIUM = 'MEDIUM',
-  HIGH = 'HIGH'
-}
-
-export interface RawHotspot {
-  assignee?: string;
-  author?: string;
-  component: string;
-  creationDate: string;
-  key: string;
-  line?: number;
-  message: string;
-  project: string;
-  resolution: string;
-  rule: string;
-  securityCategory: string;
-  updateDate: string;
-  vulnerabilityProbability: RiskExposure;
-  status: string;
-  subProject?: string;
-}
-
-export interface HotspotSearchResponse {
-  components?: { key: string; qualifier: string; name: string }[];
-  hotspots: RawHotspot[];
-  paging: T.Paging;
-}
index d18eeabd44733ab0e60d558e2a9a178cec22ddf9..0dc02ac6b69c5e9ef9adf6bbeed490712e7ea956 100644 (file)
@@ -849,6 +849,8 @@ declare namespace T {
     uuid: string;
   }
 
+  export type StandardSecurityCategories = T.Dict<{ title: string; description?: string }>;
+
   export type Standards = {
     [key in StandardType]: T.Dict<{ title: string; description?: string }>;
   };
index 34eacaf3ec6d065c051f52a278bba77de1bd1576..c565392d3ab971eefb8604d7d931fd4e2dd9721a 100644 (file)
@@ -652,6 +652,13 @@ hotspots.list_title.TO_REVIEW={0} Security Hotspots to review
 hotspots.list_title.REVIEWED={0} reviewed Security Hotspots 
 hotspots.risk_exposure=Review priority:
 
+hotspot.category=Category:
+hotspot.status=Status:
+hotspot.assigned_to=Assigned to:
+hotspot.tabs.risk_description=What's the risk?
+hotspot.tabs.vulnerability_description=Are you vulnerable?
+hotspot.tabs.fix_recommendations=How can you fix it?
+
 #------------------------------------------------------------------------------
 #
 # ISSUES