component={component}
highlightedLine={finalLine}
onLoaded={this.scrollToLine}
+ showMeasures={true}
/>
);
}
onIssueUnselect?: () => void;
scroll?: (element: HTMLElement) => void;
selectedIssue?: string;
+ showMeasures?: boolean;
}
interface State {
{({ openComponent }) => (
<SourceViewerHeader
branchLike={this.props.branchLike}
+ issues={this.state.issues}
openComponent={openComponent}
+ showMeasures={this.props.showMeasures}
sourceViewerFile={component}
/>
)}
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';
interface Props {
branchLike: T.BranchLike | undefined;
+ issues?: T.Issue[];
openComponent: WorkspaceContextShape['openComponent'];
+ showMeasures?: boolean;
sourceViewerFile: T.SourceViewerFile;
}
};
render() {
+ const { issues, showMeasures } = this.props;
const {
key,
measures,
projectName,
q,
subProject,
- subProjectName,
- uuid
+ subProjectName
} = this.props.sourceViewerFile;
const isUnitTest = q === 'UTS';
const workspace = false;
// 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
</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>
<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>
);
}
--- /dev/null
+/*
+ * 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}
+ />
+ );
+}
--- /dev/null
+// 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>
+`;
}
.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 {
};
}
+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 {