]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-8741 Display breakdown by issue types in SourceViewerHeader
authorWouter Admiraal <wouter.admiraal@sonarsource.com>
Mon, 8 Apr 2019 07:58:28 +0000 (09:58 +0200)
committerSonarTech <sonartech@sonarsource.com>
Tue, 23 Apr 2019 18:21:10 +0000 (20:21 +0200)
* Only display the measure breakdown on Code

server/sonar-web/src/main/js/apps/code/components/SourceViewerWrapper.tsx
server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.tsx
server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx
server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewerHeader-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/__tests__/__snapshots__/SourceViewerHeader-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/styles.css
server/sonar-web/src/main/js/helpers/testMocks.ts

index fd7bfe9a91db8a32db59765a2f8d60b275306f30..76d669bd7573ed65d59ca3a750c4f04404cf310c 100644 (file)
@@ -54,6 +54,7 @@ export class SourceViewerWrapper extends React.PureComponent<Props> {
         component={component}
         highlightedLine={finalLine}
         onLoaded={this.scrollToLine}
+        showMeasures={true}
       />
     );
   }
index 071cdbf65e5b29dc1629c83af30fc86f7b5d5e3d..3628a9efefd068b702f267d81954487f2f23fec7 100644 (file)
@@ -82,6 +82,7 @@ export interface Props {
   onIssueUnselect?: () => void;
   scroll?: (element: HTMLElement) => void;
   selectedIssue?: string;
+  showMeasures?: boolean;
 }
 
 interface State {
@@ -710,7 +711,9 @@ export default class SourceViewerBase extends React.PureComponent<Props, State>
             {({ openComponent }) => (
               <SourceViewerHeader
                 branchLike={this.props.branchLike}
+                issues={this.state.issues}
                 openComponent={openComponent}
+                showMeasures={this.props.showMeasures}
                 sourceViewerFile={component}
               />
             )}
index 385312ac53d0554d08ca08f6886ed9e15c78dec9..4f367fdf5acee7c3e962899690db2aa1b8d839cf 100644 (file)
@@ -28,13 +28,7 @@ import ListIcon from '../icons-components/ListIcon';
 import { ButtonIcon } from '../ui/buttons';
 import { PopupPlacement } from '../ui/popups';
 import { WorkspaceContextShape } from '../workspace/context';
-import {
-  getPathUrlAsString,
-  getBranchLikeUrl,
-  getComponentIssuesUrl,
-  getBaseUrl,
-  getCodeUrl
-} from '../../helpers/urls';
+import { getPathUrlAsString, getBranchLikeUrl, getBaseUrl, getCodeUrl } from '../../helpers/urls';
 import { collapsedDirFromPath, fileFromPath } from '../../helpers/path';
 import { translate } from '../../helpers/l10n';
 import { getBranchLikeQuery, isMainBranch } from '../../helpers/branches';
@@ -43,7 +37,9 @@ import { omitNil } from '../../helpers/request';
 
 interface Props {
   branchLike: T.BranchLike | undefined;
+  issues?: T.Issue[];
   openComponent: WorkspaceContextShape['openComponent'];
+  showMeasures?: boolean;
   sourceViewerFile: T.SourceViewerFile;
 }
 
@@ -70,6 +66,7 @@ export default class SourceViewerHeader extends React.PureComponent<Props, State
   };
 
   render() {
+    const { issues, showMeasures } = this.props;
     const {
       key,
       measures,
@@ -78,8 +75,7 @@ export default class SourceViewerHeader extends React.PureComponent<Props, State
       projectName,
       q,
       subProject,
-      subProjectName,
-      uuid
+      subProjectName
     } = this.props.sourceViewerFile;
     const isUnitTest = q === 'UTS';
     const workspace = false;
@@ -90,8 +86,8 @@ export default class SourceViewerHeader extends React.PureComponent<Props, State
 
     // TODO favorite
     return (
-      <div className="source-viewer-header">
-        <div className="source-viewer-header-component">
+      <div className="source-viewer-header display-flex-center">
+        <div className="source-viewer-header-component flex-1">
           <div className="component-name">
             <div className="component-name-parent">
               <a
@@ -123,8 +119,86 @@ export default class SourceViewerHeader extends React.PureComponent<Props, State
           </div>
         </div>
 
+        {this.state.measuresOverlay && (
+          <MeasuresOverlay
+            branchLike={this.props.branchLike}
+            onClose={this.handleMeasuresOverlayClose}
+            sourceViewerFile={this.props.sourceViewerFile}
+          />
+        )}
+
+        {showMeasures && (
+          <div className="display-flex-center">
+            {isUnitTest && (
+              <div className="source-viewer-header-measure">
+                <span className="source-viewer-header-measure-label">
+                  {translate('metric.tests.name')}
+                </span>
+                <span className="source-viewer-header-measure-value">
+                  {formatMeasure(measures.tests, 'SHORT_INT')}
+                </span>
+              </div>
+            )}
+
+            {!isUnitTest && (
+              <div className="source-viewer-header-measure">
+                <span className="source-viewer-header-measure-label">
+                  {translate('metric.lines.name')}
+                </span>
+                <span className="source-viewer-header-measure-value">
+                  {formatMeasure(measures.lines, 'SHORT_INT')}
+                </span>
+              </div>
+            )}
+
+            {measures.coverage !== undefined && (
+              <div className="source-viewer-header-measure">
+                <span className="source-viewer-header-measure-label">
+                  {translate('metric.coverage.name')}
+                </span>
+                <span className="source-viewer-header-measure-value">
+                  {formatMeasure(measures.coverage, 'PERCENT')}
+                </span>
+              </div>
+            )}
+
+            {measures.duplicationDensity !== undefined && (
+              <div className="source-viewer-header-measure">
+                <span className="source-viewer-header-measure-label">
+                  {translate('duplications')}
+                </span>
+                <span className="source-viewer-header-measure-value">
+                  {formatMeasure(measures.duplicationDensity, 'PERCENT')}
+                </span>
+              </div>
+            )}
+
+            {issues && issues.length > 0 && (
+              <>
+                <div className="source-viewer-header-measure-separator" />
+
+                {['BUG', 'VULNERABILITY', 'CODE_SMELL', 'SECURITY_HOTSPOT'].map(
+                  (type: T.IssueType) => {
+                    const total = issues.filter(issue => issue.type === type).length;
+                    return (
+                      <div className="source-viewer-header-measure" key={type}>
+                        <span className="source-viewer-header-measure-label">
+                          {translate('issue.type', type)}
+                        </span>
+                        <span className="source-viewer-header-measure-value">
+                          {formatMeasure(total, 'INT')}
+                        </span>
+                      </div>
+                    );
+                  }
+                )}
+              </>
+            )}
+          </div>
+        )}
+
         <Dropdown
-          className="source-viewer-header-actions"
+          className="source-viewer-header-actions flex-0"
           overlay={
             <ul className="menu">
               <li>
@@ -164,76 +238,6 @@ export default class SourceViewerHeader extends React.PureComponent<Props, State
             <ListIcon />
           </ButtonIcon>
         </Dropdown>
-
-        {this.state.measuresOverlay && (
-          <MeasuresOverlay
-            branchLike={this.props.branchLike}
-            onClose={this.handleMeasuresOverlayClose}
-            sourceViewerFile={this.props.sourceViewerFile}
-          />
-        )}
-
-        <div className="source-viewer-header-measures">
-          {isUnitTest && (
-            <div className="source-viewer-header-measure">
-              <span className="source-viewer-header-measure-value">
-                {formatMeasure(measures.tests, 'SHORT_INT')}
-              </span>
-              <span className="source-viewer-header-measure-label">
-                {translate('metric.tests.name')}
-              </span>
-            </div>
-          )}
-
-          {!isUnitTest && (
-            <div className="source-viewer-header-measure">
-              <span className="source-viewer-header-measure-value">
-                {formatMeasure(measures.lines, 'SHORT_INT')}
-              </span>
-              <span className="source-viewer-header-measure-label">
-                {translate('metric.lines.name')}
-              </span>
-            </div>
-          )}
-
-          <div className="source-viewer-header-measure">
-            <span className="source-viewer-header-measure-value">
-              <Link
-                to={getComponentIssuesUrl(project, {
-                  resolved: 'false',
-                  fileUuids: uuid,
-                  ...getBranchLikeQuery(this.props.branchLike)
-                })}>
-                {measures.issues != null ? formatMeasure(measures.issues, 'SHORT_INT') : 0}
-              </Link>
-            </span>
-            <span className="source-viewer-header-measure-label">
-              {translate('metric.violations.name')}
-            </span>
-          </div>
-
-          {measures.coverage != null && (
-            <div className="source-viewer-header-measure">
-              <span className="source-viewer-header-measure-value">
-                {formatMeasure(measures.coverage, 'PERCENT')}
-              </span>
-              <span className="source-viewer-header-measure-label">
-                {translate('metric.coverage.name')}
-              </span>
-            </div>
-          )}
-
-          {measures.duplicationDensity != null && (
-            <div className="source-viewer-header-measure">
-              <span className="source-viewer-header-measure-value">
-                {formatMeasure(measures.duplicationDensity, 'PERCENT')}
-              </span>
-              <span className="source-viewer-header-measure-label">
-                {translate('duplications')}
-              </span>
-            </div>
-          )}
-        </div>
       </div>
     );
   }
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewerHeader-test.tsx b/server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewerHeader-test.tsx
new file mode 100644 (file)
index 0000000..0d494a2
--- /dev/null
@@ -0,0 +1,73 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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 { shallow } from 'enzyme';
+import SourceViewerHeader from '../SourceViewerHeader';
+import { mockMainBranch, mockSourceViewerFile, mockIssue } from '../../../helpers/testMocks';
+
+it('should render correctly for a regular file', () => {
+  expect(shallowRender()).toMatchSnapshot();
+});
+
+it('should render correctly for a unit test', () => {
+  expect(
+    shallowRender({
+      showMeasures: true,
+      sourceViewerFile: mockSourceViewerFile({ q: 'UTS', measures: { tests: '12' } })
+    })
+  ).toMatchSnapshot();
+});
+
+it('should render correctly if issue details are passed', () => {
+  const issues = [
+    mockIssue(false, { type: 'VULNERABILITY' }),
+    mockIssue(false, { type: 'VULNERABILITY' }),
+    mockIssue(false, { type: 'CODE_SMELL' }),
+    mockIssue(false, { type: 'SECURITY_HOTSPOT' }),
+    mockIssue(false, { type: 'SECURITY_HOTSPOT' })
+  ];
+
+  expect(
+    shallowRender({
+      issues,
+      showMeasures: true
+    })
+  ).toMatchSnapshot();
+
+  expect(
+    shallowRender({
+      issues,
+      showMeasures: false
+    })
+      .find('.source-viewer-header-measure')
+      .exists()
+  ).toBe(false);
+});
+
+function shallowRender(props: Partial<SourceViewerHeader['props']> = {}) {
+  return shallow(
+    <SourceViewerHeader
+      branchLike={mockMainBranch()}
+      openComponent={jest.fn()}
+      sourceViewerFile={mockSourceViewerFile()}
+      {...props}
+    />
+  );
+}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/__tests__/__snapshots__/SourceViewerHeader-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/__tests__/__snapshots__/SourceViewerHeader-test.tsx.snap
new file mode 100644 (file)
index 0000000..f5b7d58
--- /dev/null
@@ -0,0 +1,464 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly for a regular file 1`] = `
+<div
+  className="source-viewer-header display-flex-center"
+>
+  <div
+    className="source-viewer-header-component flex-1"
+  >
+    <div
+      className="component-name"
+    >
+      <div
+        className="component-name-parent"
+      >
+        <a
+          className="link-with-icon"
+          href="/dashboard?id=my-project"
+        >
+          <QualifierIcon
+            qualifier="TRK"
+          />
+           
+          <span>
+            MyProject
+          </span>
+        </a>
+      </div>
+      <div
+        className="component-name-path"
+      >
+        <QualifierIcon
+          qualifier="FIL"
+        />
+         
+        <span>
+          foo/
+        </span>
+        <span
+          className="component-name-file"
+        >
+          bar.ts
+        </span>
+      </div>
+    </div>
+  </div>
+  <Dropdown
+    className="source-viewer-header-actions flex-0"
+    overlay={
+      <ul
+        className="menu"
+      >
+        <li>
+          <a
+            className="js-measures"
+            href="#"
+            onClick={[Function]}
+          >
+            component_viewer.show_details
+          </a>
+        </li>
+        <li>
+          <Link
+            className="js-new-window"
+            onlyActiveOnIndex={false}
+            rel="noopener noreferrer"
+            style={Object {}}
+            target="_blank"
+            to={
+              Object {
+                "pathname": "/code",
+                "query": Object {
+                  "id": "my-project",
+                  "line": undefined,
+                  "selected": "foo",
+                },
+              }
+            }
+          >
+            component_viewer.new_window
+          </Link>
+        </li>
+        <li>
+          <a
+            className="js-workspace"
+            href="#"
+            onClick={[Function]}
+          >
+            component_viewer.open_in_workspace
+          </a>
+        </li>
+        <li>
+          <a
+            className="js-raw-source"
+            href="/api/sources/raw?key=foo"
+            rel="noopener noreferrer"
+            target="_blank"
+          >
+            component_viewer.show_raw_source
+          </a>
+        </li>
+      </ul>
+    }
+    overlayPlacement="bottom-right"
+  >
+    <ButtonIcon
+      className="js-actions"
+    >
+      <ListIcon />
+    </ButtonIcon>
+  </Dropdown>
+</div>
+`;
+
+exports[`should render correctly for a unit test 1`] = `
+<div
+  className="source-viewer-header display-flex-center"
+>
+  <div
+    className="source-viewer-header-component flex-1"
+  >
+    <div
+      className="component-name"
+    >
+      <div
+        className="component-name-parent"
+      >
+        <a
+          className="link-with-icon"
+          href="/dashboard?id=my-project"
+        >
+          <QualifierIcon
+            qualifier="TRK"
+          />
+           
+          <span>
+            MyProject
+          </span>
+        </a>
+      </div>
+      <div
+        className="component-name-path"
+      >
+        <QualifierIcon
+          qualifier="UTS"
+        />
+         
+        <span>
+          foo/
+        </span>
+        <span
+          className="component-name-file"
+        >
+          bar.ts
+        </span>
+      </div>
+    </div>
+  </div>
+  <div
+    className="display-flex-center"
+  >
+    <div
+      className="source-viewer-header-measure"
+    >
+      <span
+        className="source-viewer-header-measure-label"
+      >
+        metric.tests.name
+      </span>
+      <span
+        className="source-viewer-header-measure-value"
+      >
+        12
+      </span>
+    </div>
+  </div>
+  <Dropdown
+    className="source-viewer-header-actions flex-0"
+    overlay={
+      <ul
+        className="menu"
+      >
+        <li>
+          <a
+            className="js-measures"
+            href="#"
+            onClick={[Function]}
+          >
+            component_viewer.show_details
+          </a>
+        </li>
+        <li>
+          <Link
+            className="js-new-window"
+            onlyActiveOnIndex={false}
+            rel="noopener noreferrer"
+            style={Object {}}
+            target="_blank"
+            to={
+              Object {
+                "pathname": "/code",
+                "query": Object {
+                  "id": "my-project",
+                  "line": undefined,
+                  "selected": "foo",
+                },
+              }
+            }
+          >
+            component_viewer.new_window
+          </Link>
+        </li>
+        <li>
+          <a
+            className="js-workspace"
+            href="#"
+            onClick={[Function]}
+          >
+            component_viewer.open_in_workspace
+          </a>
+        </li>
+        <li>
+          <a
+            className="js-raw-source"
+            href="/api/sources/raw?key=foo"
+            rel="noopener noreferrer"
+            target="_blank"
+          >
+            component_viewer.show_raw_source
+          </a>
+        </li>
+      </ul>
+    }
+    overlayPlacement="bottom-right"
+  >
+    <ButtonIcon
+      className="js-actions"
+    >
+      <ListIcon />
+    </ButtonIcon>
+  </Dropdown>
+</div>
+`;
+
+exports[`should render correctly if issue details are passed 1`] = `
+<div
+  className="source-viewer-header display-flex-center"
+>
+  <div
+    className="source-viewer-header-component flex-1"
+  >
+    <div
+      className="component-name"
+    >
+      <div
+        className="component-name-parent"
+      >
+        <a
+          className="link-with-icon"
+          href="/dashboard?id=my-project"
+        >
+          <QualifierIcon
+            qualifier="TRK"
+          />
+           
+          <span>
+            MyProject
+          </span>
+        </a>
+      </div>
+      <div
+        className="component-name-path"
+      >
+        <QualifierIcon
+          qualifier="FIL"
+        />
+         
+        <span>
+          foo/
+        </span>
+        <span
+          className="component-name-file"
+        >
+          bar.ts
+        </span>
+      </div>
+    </div>
+  </div>
+  <div
+    className="display-flex-center"
+  >
+    <div
+      className="source-viewer-header-measure"
+    >
+      <span
+        className="source-viewer-header-measure-label"
+      >
+        metric.lines.name
+      </span>
+      <span
+        className="source-viewer-header-measure-value"
+      >
+        56
+      </span>
+    </div>
+    <div
+      className="source-viewer-header-measure"
+    >
+      <span
+        className="source-viewer-header-measure-label"
+      >
+        metric.coverage.name
+      </span>
+      <span
+        className="source-viewer-header-measure-value"
+      >
+        85.2%
+      </span>
+    </div>
+    <div
+      className="source-viewer-header-measure"
+    >
+      <span
+        className="source-viewer-header-measure-label"
+      >
+        duplications
+      </span>
+      <span
+        className="source-viewer-header-measure-value"
+      >
+        1.0%
+      </span>
+    </div>
+    <div
+      className="source-viewer-header-measure-separator"
+    />
+    <div
+      className="source-viewer-header-measure"
+      key="BUG"
+    >
+      <span
+        className="source-viewer-header-measure-label"
+      >
+        issue.type.BUG
+      </span>
+      <span
+        className="source-viewer-header-measure-value"
+      >
+        0
+      </span>
+    </div>
+    <div
+      className="source-viewer-header-measure"
+      key="VULNERABILITY"
+    >
+      <span
+        className="source-viewer-header-measure-label"
+      >
+        issue.type.VULNERABILITY
+      </span>
+      <span
+        className="source-viewer-header-measure-value"
+      >
+        2
+      </span>
+    </div>
+    <div
+      className="source-viewer-header-measure"
+      key="CODE_SMELL"
+    >
+      <span
+        className="source-viewer-header-measure-label"
+      >
+        issue.type.CODE_SMELL
+      </span>
+      <span
+        className="source-viewer-header-measure-value"
+      >
+        1
+      </span>
+    </div>
+    <div
+      className="source-viewer-header-measure"
+      key="SECURITY_HOTSPOT"
+    >
+      <span
+        className="source-viewer-header-measure-label"
+      >
+        issue.type.SECURITY_HOTSPOT
+      </span>
+      <span
+        className="source-viewer-header-measure-value"
+      >
+        2
+      </span>
+    </div>
+  </div>
+  <Dropdown
+    className="source-viewer-header-actions flex-0"
+    overlay={
+      <ul
+        className="menu"
+      >
+        <li>
+          <a
+            className="js-measures"
+            href="#"
+            onClick={[Function]}
+          >
+            component_viewer.show_details
+          </a>
+        </li>
+        <li>
+          <Link
+            className="js-new-window"
+            onlyActiveOnIndex={false}
+            rel="noopener noreferrer"
+            style={Object {}}
+            target="_blank"
+            to={
+              Object {
+                "pathname": "/code",
+                "query": Object {
+                  "id": "my-project",
+                  "line": undefined,
+                  "selected": "foo",
+                },
+              }
+            }
+          >
+            component_viewer.new_window
+          </Link>
+        </li>
+        <li>
+          <a
+            className="js-workspace"
+            href="#"
+            onClick={[Function]}
+          >
+            component_viewer.open_in_workspace
+          </a>
+        </li>
+        <li>
+          <a
+            className="js-raw-source"
+            href="/api/sources/raw?key=foo"
+            rel="noopener noreferrer"
+            target="_blank"
+          >
+            component_viewer.show_raw_source
+          </a>
+        </li>
+      </ul>
+    }
+    overlayPlacement="bottom-right"
+  >
+    <ButtonIcon
+      className="js-actions"
+    >
+      <ListIcon />
+    </ButtonIcon>
+  </Dropdown>
+</div>
+`;
index 5b3843f3438f25397a64db216c4dd2ff5b221258..beb73998c2ebd62bed2ad82d93006969132f14c8 100644 (file)
 }
 
 .source-viewer-header-component {
-  float: left;
   padding-top: 4px;
 }
 
   border-bottom: none;
 }
 
-.source-viewer-header-measures {
-  float: right;
-}
-
 .source-viewer-header-measures-scope {
   position: relative;
   float: left;
 }
 
 .source-viewer-header-measure {
-  display: inline-block;
   vertical-align: middle;
-  padding: 3px 0;
   font-size: var(--baseFontSize);
 }
 
   font-size: 18px;
 }
 
+.source-viewer-header-measure-separator {
+  margin: 0 calc(3 * var(--gridSize));
+  height: 30px;
+  border-right: 1px solid var(--gray80);
+}
+
 .source-viewer-header-measure + .source-viewer-header-measure {
-  margin-left: 25px;
+  margin-left: calc(3 * var(--gridSize));
 }
 
 .source-viewer-header-measure-label {
   display: block;
-  margin-top: 4px;
   line-height: var(--smallFontSize);
   color: var(--secondFontColor);
   font-size: var(--smallFontSize);
 
 .source-viewer-header-measure-value {
   display: block;
+  margin-top: 2px;
   line-height: 18px;
   color: var(--baseFontColor);
   font-size: var(--bigFontSize);
 }
 
 .source-viewer-header-actions {
-  float: right;
   display: block;
-  margin-left: 25px;
-  padding: 8px 5px;
+  margin-left: calc(3 * var(--gridSize));
+  padding: var(--gridSize) calc(var(--gridSize) / 2);
 }
 
 .source-viewer-header-actions svg {
index dd3b736a8456ff9fc843b0c1454da3ff485f0591..1cc767c2b09ed00efcd6bed02e858009d8796b0e 100644 (file)
@@ -441,6 +441,26 @@ export function mockShortLivingBranch(
   };
 }
 
+export function mockSourceViewerFile(
+  overrides: Partial<T.SourceViewerFile> = {}
+): T.SourceViewerFile {
+  return {
+    key: 'foo',
+    measures: {
+      coverage: '85.2',
+      duplicationDensity: '1.0',
+      issues: '12',
+      lines: '56'
+    },
+    path: 'foo/bar.ts',
+    project: 'my-project',
+    projectName: 'MyProject',
+    q: 'FIL',
+    uuid: 'foo-bar',
+    ...overrides
+  };
+}
+
 export function mockLongLivingBranch(
   overrides: Partial<T.LongLivingBranch> = {}
 ): T.LongLivingBranch {