]> source.dussan.org Git - sonarqube.git/commitdiff
rewrite remaining popups in react (#3109)
authorStas Vilchik <stas.vilchik@sonarsource.com>
Fri, 2 Mar 2018 15:24:37 +0000 (16:24 +0100)
committerGitHub <noreply@github.com>
Fri, 2 Mar 2018 15:24:37 +0000 (16:24 +0100)
* extract baseFontFamily

* rewrite favorites store in ts

* add new types and change existing ones

* rewrite SourceViewer helpers in ts

* rewrite SourceViewer in ts and its popups in react

* drop popups

* fix iterating over nodelist

* fix quality flaws

101 files changed:
server/sonar-web/config/webpack.config.js
server/sonar-web/src/main/js/app/styles/init/type.css
server/sonar-web/src/main/js/app/theme.js
server/sonar-web/src/main/js/app/types.ts
server/sonar-web/src/main/js/apps/component/components/App.tsx
server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.js [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.js [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/CoveragePopup.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/DuplicationPopup.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/Line.js [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/components/Line.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.js [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.js [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.js [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplications.js [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplications.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicator.js [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicator.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesList.js [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesList.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/LineNumber.js [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/components/LineNumber.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/LineOptionsPopup.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.js [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/SCMPopup.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCode-test.js [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCode-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCoverage-test.js [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCoverage-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplicationBlock-test.js [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplicationBlock-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplications-test.js [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplications-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesIndicator-test.js [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesIndicator-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesList-test.js [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesList-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineNumber-test.js [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineNumber-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineOptionsPopup-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineSCM-test.js [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineSCM-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/SCMPopup-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.js.snap [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCoverage-test.js.snap [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCoverage-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplicationBlock-test.js.snap [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplicationBlock-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplications-test.js.snap [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplications-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesIndicator-test.js.snap [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesIndicator-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesList-test.js.snap [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesList-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineNumber-test.js.snap [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineNumber-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineOptionsPopup-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineSCM-test.js.snap [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineSCM-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/SCMPopup-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/highlight-test.js [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/highlight-test.ts [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/indexing-test.js [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/indexing-test.ts [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/helpers/getCoverageStatus.js [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/helpers/getCoverageStatus.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/helpers/highlight.js [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/helpers/highlight.ts [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/helpers/indexing.js [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/helpers/indexing.ts [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/helpers/issueLocations.js [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/helpers/issueLocations.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.js [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/popups/coverage-popup.js [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/popups/duplication-popup.js [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/popups/line-actions-popup.js [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/popups/scm-popup.js [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-coverage-popup.hbs [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-duplication-popup.hbs [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-line-options-popup.hbs [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-scm-popup.hbs [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/styles.css
server/sonar-web/src/main/js/components/SourceViewer/types.js [deleted file]
server/sonar-web/src/main/js/components/common/BubblePopupHelper.tsx
server/sonar-web/src/main/js/components/common/LocationIndex.css
server/sonar-web/src/main/js/components/common/LocationMessage.css
server/sonar-web/src/main/js/components/common/popup.js [deleted file]
server/sonar-web/src/main/js/components/issue/Issue.d.ts [new file with mode: 0644]
server/sonar-web/src/main/js/helpers/issues.ts
server/sonar-web/src/main/js/store/favorites/duck.js [deleted file]
server/sonar-web/src/main/js/store/favorites/duck.ts [new file with mode: 0644]

index f120681011fead8f404e8bcd7a71d5eecbaf6c98..3d1e7aba4e683eb20125a388a8bf1ffed0c5a391 100644 (file)
@@ -68,7 +68,7 @@ module.exports = ({ production = true, fast = false }) => ({
     app: [
       './src/main/js/app/utils/setPublicPath.js',
       './src/main/js/app/index.js',
-      './src/main/js/components/SourceViewer/SourceViewer.js'
+      './src/main/js/components/SourceViewer/SourceViewer'
     ]
   },
   output: {
index 80b61e7dbf32b2090ceae7ea49b3de8e569e5474..28ab828201fcd0055bd31d0d76c44d12bff1283a 100644 (file)
@@ -23,7 +23,7 @@ body {
 }
 
 body {
-  font-family: 'Helvetica Neue', 'Segoe UI', Helvetica, Arial, sans-serif;
+  font-family: var(--baseFontFamily);
   font-size: var(--baseFontSize);
   line-height: 1.23076923;
 }
index 04f518b8060b7caf35727d0b0183c2ade7092cb7..dd39fe0a445ea29a864a54d0d53ef8a8a6ed376b 100644 (file)
@@ -73,6 +73,7 @@ module.exports = {
   pagePadding: '20px',
 
   // different
+  baseFontFamily: "'Helvetica Neue', 'Segoe UI', Helvetica, Arial, sans-serif",
   defaultShadow: '0 6px 12px rgba(0, 0, 0, 0.175)',
 
   // z-index
index 252f6c135a2e6d04f35361d233f6a10014e584fb..92136630ff4f583c2a356eb287d8ca8115407fe2 100644 (file)
@@ -112,6 +112,25 @@ export interface CustomMeasure {
   updatedAt?: string;
 }
 
+export interface Duplication {
+  blocks: DuplicationBlock[];
+}
+
+export interface DuplicationBlock {
+  _ref: string;
+  from: number;
+  size: number;
+}
+
+export interface DuplicatedFile {
+  key: string;
+  name: string;
+  project: string;
+  projectName: string;
+  subProject?: string;
+  subProjectName?: string;
+}
+
 export interface Extension {
   key: string;
   name: string;
@@ -122,6 +141,11 @@ export interface FacetValue {
   val: string;
 }
 
+export interface FlowLocation {
+  msg: string;
+  textRange: TextRange;
+}
+
 export interface Group {
   default?: boolean;
   description?: string;
@@ -174,12 +198,72 @@ export function isSameHomePage(a: HomePage, b: HomePage) {
   );
 }
 
+export interface Issue {
+  actions?: string[];
+  assignee?: string;
+  assigneeActive?: string;
+  assigneeAvatar?: string;
+  assigneeLogin?: string;
+  assigneeName?: string;
+  author?: string;
+  comments?: IssueComment[];
+  component: string;
+  componentLongName: string;
+  componentQualifier: string;
+  componentUuid: string;
+  creationDate: string;
+  effort?: string;
+  key: string;
+  flows: FlowLocation[][];
+  line?: number;
+  message: string;
+  organization: string;
+  project: string;
+  projectName: string;
+  projectOrganization: string;
+  projectUuid: string;
+  resolution?: string;
+  rule: string;
+  ruleName: string;
+  secondaryLocations: FlowLocation[];
+  severity: string;
+  status: string;
+  subProject?: string;
+  subProjectName?: string;
+  subProjectUuid?: string;
+  tags?: string[];
+  textRange?: TextRange;
+  transitions?: string[];
+  type: string;
+}
+
+export interface IssueComment {
+  author?: string;
+  authorActive?: boolean;
+  authorAvatar?: string;
+  authorLogin?: string;
+  authorName?: string;
+  createdAt: string;
+  htmlText: string;
+  key: string;
+  markdown: string;
+  updatable: boolean;
+}
+
 export interface LightComponent {
   key: string;
   organization: string;
   qualifier: string;
 }
 
+export interface LinearIssueLocation {
+  from: number;
+  index?: number;
+  line: number;
+  startLine?: number;
+  to: number;
+}
+
 export interface LoggedInUser extends CurrentUser {
   avatar?: string;
   email?: string;
@@ -349,9 +433,24 @@ export interface ShortLivingBranch {
   type: BranchType.SHORT;
 }
 
+export interface SourceLine {
+  code?: string;
+  conditions?: number;
+  coverageStatus?: string;
+  coveredConditions?: number;
+  duplicated?: boolean;
+  line: number;
+  lineHits?: number;
+  scmAuthor?: string;
+  scmDate?: string;
+  scmRevision?: string;
+}
+
 export interface SourceViewerFile {
   canMarkAsFavorite?: boolean;
+  fav?: boolean;
   key: string;
+  leakPeriodDate?: string;
   measures: {
     coverage?: string;
     duplicationDensity?: string;
@@ -381,6 +480,13 @@ export interface TestCase {
   status: string;
 }
 
+export interface TextRange {
+  startLine: number;
+  startOffset: number;
+  endLine: number;
+  endOffset: number;
+}
+
 export interface User {
   active: boolean;
   avatar?: string;
index 06d009a07fa53ac952f85654b6e21f258a3c03b5..96ff380b9063a8060fec5aaba73bd1544aec8de0 100644 (file)
@@ -47,7 +47,7 @@ export default class App extends React.PureComponent<Props> {
   render() {
     const { branch, id, line } = this.props.location.query;
 
-    const finalLine = line != null ? Number(line) : null;
+    const finalLine = line ? Number(line) : undefined;
 
     return (
       <div className="page page-limited">
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.js b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.js
deleted file mode 100644 (file)
index 974eb8e..0000000
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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.
- */
-// @flow
-import { connect } from 'react-redux';
-import SourceViewerBase from './SourceViewerBase';
-import { receiveFavorites } from '../../store/favorites/duck';
-
-const mapStateToProps = null;
-
-const onReceiveComponent = (
-  component /*: {
-  key: string,
-  canMarkAsFavorite: boolean,
-  fav: boolean
-} */
-) => dispatch => {
-  if (component.canMarkAsFavorite) {
-    const favorites = [];
-    const notFavorites = [];
-    if (component.fav) {
-      favorites.push({ key: component.key });
-    } else {
-      notFavorites.push({ key: component.key });
-    }
-    dispatch(receiveFavorites(favorites, notFavorites));
-  }
-};
-
-const mapDispatchToProps = { onReceiveComponent };
-
-export default connect(mapStateToProps, mapDispatchToProps)(SourceViewerBase);
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.tsx b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.tsx
new file mode 100644 (file)
index 0000000..3b40ed8
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { Dispatch } from 'redux';
+import { connect } from 'react-redux';
+import SourceViewerBase from './SourceViewerBase';
+import { SourceViewerFile } from '../../app/types';
+import { receiveFavorites } from '../../store/favorites/duck';
+
+const mapStateToProps = null;
+
+interface DispatchProps {
+  onReceiveComponent: (component: SourceViewerFile) => void;
+}
+
+const onReceiveComponent = (component: SourceViewerFile) => (dispatch: Dispatch<any>) => {
+  if (component.canMarkAsFavorite) {
+    const favorites = [];
+    const notFavorites = [];
+    if (component.fav) {
+      favorites.push({ key: component.key });
+    } else {
+      notFavorites.push({ key: component.key });
+    }
+    dispatch(receiveFavorites(favorites, notFavorites));
+  }
+};
+
+const mapDispatchToProps: DispatchProps = { onReceiveComponent };
+
+export default connect(mapStateToProps, mapDispatchToProps)(SourceViewerBase);
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js
deleted file mode 100644 (file)
index 6f26745..0000000
+++ /dev/null
@@ -1,694 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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.
- */
-// @flow
-import React from 'react';
-import classNames from 'classnames';
-import { intersection, uniqBy } from 'lodash';
-import SourceViewerHeader from './SourceViewerHeader';
-import SourceViewerCode from './SourceViewerCode';
-import CoveragePopupView from './popups/coverage-popup';
-import DuplicationPopupView from './popups/duplication-popup';
-import LineActionsPopupView from './popups/line-actions-popup';
-import SCMPopupView from './popups/scm-popup';
-import loadIssues from './helpers/loadIssues';
-import getCoverageStatus from './helpers/getCoverageStatus';
-import {
-  issuesByLine,
-  locationsByLine,
-  duplicationsByLine,
-  symbolsByLine
-} from './helpers/indexing';
-/*:: import type { LinearIssueLocation } from './helpers/indexing'; */
-import {
-  getComponentForSourceViewer,
-  getComponentData,
-  getSources,
-  getDuplications,
-  getTests
-} from '../../api/components';
-import { parseDate } from '../../helpers/dates';
-import { translate } from '../../helpers/l10n';
-import { scrollToElement } from '../../helpers/scrolling';
-/*:: import type { SourceLine } from './types'; */
-/*:: import type { Issue, FlowLocation } from '../issue/types'; */
-import './styles.css';
-
-// TODO react-virtualized
-
-/*::
-type Props = {
-  aroundLine?: number,
-  branch?: string,
-  component: string,
-  displayAllIssues: boolean,
-  displayIssueLocationsCount?: boolean;
-  displayIssueLocationsLink?: boolean;
-  displayLocationMarkers?: boolean;
-  highlightedLine?: number,
-  highlightedLocations?: Array<FlowLocation>,
-  highlightedLocationMessage?: { index: number, text: string },
-  loadComponent: (component: string, branch?: string) => Promise<*>,
-  loadIssues: (component: string, from: number, to: number, branch?: string) => Promise<*>,
-  loadSources: (component: string, from: number, to: number, branch?: string) => Promise<*>,
-  onLoaded?: (component: Object, sources: Array<*>, issues: Array<*>) => void,
-  onLocationSelect?: number => void,
-  onIssueChange?: Issue => void,
-  onIssueSelect?: string => void,
-  onIssueUnselect?: () => void,
-  onReceiveComponent: ({ canMarkAsFavorite: boolean, fav: boolean, key: string }) => void,
-  scroll?: HTMLElement => void,
-  selectedIssue?: string
-};
-*/
-
-/*::
-type State = {
-  component?: Object,
-  displayDuplications: boolean,
-  duplications?: Array<{
-    blocks: Array<{
-      _ref: string,
-      from: number,
-      size: number
-    }>
-  }>,
-  duplicationsByLine: { [number]: Array<number> },
-  duplicatedFiles?: Array<{ key: string }>,
-  hasSourcesAfter: boolean,
-  highlightedLine: number | null,
-  highlightedSymbols: Array<string>,
-  issues?: Array<Issue>,
-  issuesByLine: { [number]: Array<Issue> },
-  issueLocationsByLine: { [number]: Array<LinearIssueLocation> },
-  loading: boolean,
-  loadingSourcesAfter: boolean,
-  loadingSourcesBefore: boolean,
-  notAccessible: boolean,
-  notExist: boolean,
-  openIssuesByLine: { [number]: boolean },
-  openPopup: ?{
-    issue: string,
-    name: string
-  },
-  selectedIssue?: string,
-  sources?: Array<SourceLine>,
-  sourceRemoved: boolean,
-  symbolsByLine: { [number]: Array<string> }
-};
-*/
-
-const LINES = 500;
-
-function loadComponent(key /*: string */, branch /*: string | void */) /*: Promise<*> */ {
-  return Promise.all([
-    getComponentForSourceViewer(key, branch),
-    getComponentData(key, branch)
-  ]).then(([component, data]) => ({
-    ...component,
-    leakPeriodDate: data.leakPeriodDate && parseDate(data.leakPeriodDate)
-  }));
-}
-
-function loadSources(
-  key /*: string */,
-  from /*: ?number */,
-  to /*: ?number */,
-  branch /*: string | void */
-) /*: Promise<Array<*>> */ {
-  return getSources(key, from, to, branch);
-}
-
-export default class SourceViewerBase extends React.PureComponent {
-  /*:: mounted: boolean; */
-  /*:: node: HTMLElement; */
-  /*:: props: Props; */
-  /*:: state: State; */
-
-  static defaultProps = {
-    displayAllIssues: false,
-    displayIssueLocationsCount: true,
-    displayIssueLocationsLink: true,
-    displayLocationMarkers: true,
-    loadComponent,
-    loadIssues,
-    loadSources
-  };
-
-  constructor(props /*: Props */) {
-    super(props);
-    this.state = {
-      displayDuplications: false,
-      duplicationsByLine: {},
-      hasSourcesAfter: false,
-      highlightedLine: props.highlightedLine || null,
-      highlightedSymbols: [],
-      issuesByLine: {},
-      issueLocationsByLine: {},
-      issueSecondaryLocationsByIssueByLine: {},
-      issueSecondaryLocationMessagesByIssueByLine: {},
-      loading: true,
-      loadingSourcesAfter: false,
-      loadingSourcesBefore: false,
-      notAccessible: false,
-      notExist: false,
-      openIssuesByLine: {},
-      openPopup: null,
-      selectedIssue: props.selectedIssue,
-      selectedIssueLocation: null,
-      sourceRemoved: false,
-      symbolsByLine: {}
-    };
-  }
-
-  componentDidMount() {
-    this.mounted = true;
-    this.fetchComponent();
-  }
-
-  componentWillReceiveProps(nextProps /*: Props */) {
-    if (nextProps.onIssueSelect != null && nextProps.selectedIssue !== this.props.selectedIssue) {
-      this.setState({ selectedIssue: nextProps.selectedIssue });
-    }
-  }
-
-  componentDidUpdate(prevProps /*: Props */) {
-    if (prevProps.component !== this.props.component || prevProps.branch !== this.props.branch) {
-      this.fetchComponent();
-    } else if (
-      this.props.aroundLine != null &&
-      prevProps.aroundLine !== this.props.aroundLine &&
-      this.isLineOutsideOfRange(this.props.aroundLine)
-    ) {
-      this.fetchSources();
-    } else {
-      const { selectedIssue } = this.props;
-      const { issues } = this.state;
-      if (
-        selectedIssue != null &&
-        issues != null &&
-        issues.find(issue => issue.key === selectedIssue) == null
-      ) {
-        this.reloadIssues();
-      }
-    }
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  scrollToLine(line /*: number */) {
-    const lineElement = this.node.querySelector(
-      `.source-line-code[data-line-number="${line}"] .source-line-issue-locations`
-    );
-    if (lineElement) {
-      scrollToElement(lineElement, { topOffset: 125, bottomOffset: 75 });
-    }
-  }
-
-  computeCoverageStatus(lines /*: Array<SourceLine> */) /*: Array<SourceLine> */ {
-    return lines.map(line => ({ ...line, coverageStatus: getCoverageStatus(line) }));
-  }
-
-  isLineOutsideOfRange(lineNumber /*: number */) {
-    const { sources } = this.state;
-    if (sources != null && sources.length > 0) {
-      const firstLine = sources[0];
-      const lastList = sources[sources.length - 1];
-      return lineNumber < firstLine.line || lineNumber > lastList.line;
-    } else {
-      return true;
-    }
-  }
-
-  fetchComponent() {
-    this.setState({ loading: true });
-    const loadIssues = (component, sources) => {
-      this.props.loadIssues(this.props.component, 1, LINES, this.props.branch).then(issues => {
-        if (this.mounted) {
-          const finalSources = sources.slice(0, LINES);
-          this.setState(
-            {
-              component,
-              issues,
-              issuesByLine: issuesByLine(issues),
-              issueLocationsByLine: locationsByLine(issues),
-              loading: false,
-              notAccessible: false,
-              notExist: false,
-              hasSourcesAfter: sources.length > LINES,
-              sources: this.computeCoverageStatus(finalSources),
-              sourceRemoved: false,
-              symbolsByLine: symbolsByLine(sources.slice(0, LINES))
-            },
-            () => {
-              if (this.props.onLoaded) {
-                this.props.onLoaded(component, finalSources, issues);
-              }
-            }
-          );
-        }
-      });
-    };
-
-    const onFailLoadComponent = ({ response }) => {
-      // TODO handle other statuses
-      if (this.mounted) {
-        if (response.status === 403) {
-          this.setState({ loading: false, notAccessible: true });
-        } else if (response.status === 404) {
-          this.setState({ loading: false, notExist: true });
-        }
-      }
-    };
-
-    const onFailLoadSources = (response, component) => {
-      // TODO handle other statuses
-      if (this.mounted) {
-        if (response.status === 403) {
-          this.setState({ component, loading: false, notAccessible: true });
-        } else if (response.status === 404) {
-          this.setState({ component, loading: false, sourceRemoved: true });
-        }
-      }
-    };
-
-    const onResolve = component => {
-      this.props.onReceiveComponent(component);
-      const sourcesRequest =
-        component.q === 'FIL' || component.q === 'UTS' ? this.loadSources() : Promise.resolve([]);
-      sourcesRequest.then(
-        sources => loadIssues(component, sources),
-        response => onFailLoadSources(response, component)
-      );
-    };
-
-    this.props
-      .loadComponent(this.props.component, this.props.branch)
-      .then(onResolve, onFailLoadComponent);
-  }
-
-  fetchSources() {
-    this.loadSources().then(sources => {
-      if (this.mounted) {
-        const finalSources = sources.slice(0, LINES);
-        this.setState(
-          {
-            sources: sources.slice(0, LINES),
-            hasSourcesAfter: sources.length > LINES
-          },
-          () => {
-            if (this.props.onLoaded) {
-              // $FlowFixMe
-              this.props.onLoaded(this.state.component, finalSources, this.state.issues);
-            }
-          }
-        );
-      }
-    });
-  }
-
-  reloadIssues() {
-    if (!this.state.sources) {
-      return;
-    }
-    const firstSourceLine = this.state.sources[0];
-    const lastSourceLine = this.state.sources[this.state.sources.length - 1];
-    this.props
-      .loadIssues(
-        this.props.component,
-        firstSourceLine && firstSourceLine.line,
-        lastSourceLine && lastSourceLine.line
-      )
-      .then(issues => {
-        if (this.mounted) {
-          this.setState({
-            issues,
-            issuesByLine: issuesByLine(issues),
-            issueLocationsByLine: locationsByLine(issues)
-          });
-        }
-      });
-  }
-
-  loadSources() {
-    return new Promise((resolve, reject) => {
-      const onFailLoadSources = ({ response }) => {
-        // TODO handle other statuses
-        if (this.mounted) {
-          if ([403, 404].includes(response.status)) {
-            reject(response);
-          } else {
-            resolve([]);
-          }
-        }
-      };
-
-      const from = this.props.aroundLine ? Math.max(1, this.props.aroundLine - LINES / 2 + 1) : 1;
-
-      let to = this.props.aroundLine ? this.props.aroundLine + LINES / 2 + 1 : LINES + 1;
-      // make sure we try to download `LINES` lines
-      if (from === 1 && to < LINES) {
-        to = LINES;
-      }
-      // request one additional line to define `hasSourcesAfter`
-      to++;
-
-      return this.props
-        .loadSources(this.props.component, from, to, this.props.branch)
-        .then(sources => resolve(sources), onFailLoadSources);
-    });
-  }
-
-  loadSourcesBefore = () => {
-    if (!this.state.sources) {
-      return;
-    }
-    const firstSourceLine = this.state.sources[0];
-    this.setState({ loadingSourcesBefore: true });
-    const from = Math.max(1, firstSourceLine.line - LINES);
-    this.props
-      .loadSources(this.props.component, from, firstSourceLine.line - 1, this.props.branch)
-      .then(sources => {
-        this.props.loadIssues(this.props.component, from, firstSourceLine.line - 1).then(issues => {
-          if (this.mounted) {
-            this.setState(prevState => {
-              const nextIssues = uniqBy([...issues, ...prevState.issues], issue => issue.key);
-              return {
-                issues: nextIssues,
-                issuesByLine: issuesByLine(nextIssues),
-                issueLocationsByLine: locationsByLine(nextIssues),
-                loadingSourcesBefore: false,
-                sources: [...this.computeCoverageStatus(sources), ...prevState.sources],
-                symbolsByLine: { ...prevState.symbolsByLine, ...symbolsByLine(sources) }
-              };
-            });
-          }
-        });
-      });
-  };
-
-  loadSourcesAfter = () => {
-    if (!this.state.sources) {
-      return;
-    }
-    const lastSourceLine = this.state.sources[this.state.sources.length - 1];
-    this.setState({ loadingSourcesAfter: true });
-    const fromLine = lastSourceLine.line + 1;
-    // request one additional line to define `hasSourcesAfter`
-    const toLine = lastSourceLine.line + LINES + 1;
-    this.props
-      .loadSources(this.props.component, fromLine, toLine, this.props.branch)
-      .then(sources => {
-        this.props.loadIssues(this.props.component, fromLine, toLine).then(issues => {
-          if (this.mounted) {
-            this.setState(prevState => {
-              const nextIssues = uniqBy([...prevState.issues, ...issues], issue => issue.key);
-              return {
-                issues: nextIssues,
-                issuesByLine: issuesByLine(nextIssues),
-                issueLocationsByLine: locationsByLine(nextIssues),
-                hasSourcesAfter: sources.length > LINES,
-                loadingSourcesAfter: false,
-                sources: [
-                  ...prevState.sources,
-                  ...this.computeCoverageStatus(sources.slice(0, LINES))
-                ],
-                symbolsByLine: {
-                  ...prevState.symbolsByLine,
-                  ...symbolsByLine(sources.slice(0, LINES))
-                }
-              };
-            });
-          }
-        });
-      });
-  };
-
-  loadDuplications = (line /*: SourceLine */) => {
-    getDuplications(this.props.component, this.props.branch).then(r => {
-      if (this.mounted) {
-        this.setState(
-          {
-            displayDuplications: true,
-            duplications: r.duplications,
-            duplicationsByLine: duplicationsByLine(r.duplications),
-            duplicatedFiles: r.files
-          },
-          () => {
-            // immediately show dropdown popup if there is only one duplicated block
-            if (r.duplications.length === 1) {
-              this.handleDuplicationClick(0, line.line);
-            }
-          }
-        );
-      }
-    });
-  };
-
-  handleCoverageClick = (line /*: SourceLine */, element /*: HTMLElement */) => {
-    getTests(this.props.component, line.line, this.props.branch).then(tests => {
-      const popup = new CoveragePopupView({
-        line,
-        tests,
-        triggerEl: element,
-        branch: this.props.branch
-      });
-      popup.render();
-    });
-  };
-
-  handleDuplicationClick = (index /*: number */, line /*: number */) => {
-    const duplication = this.state.duplications && this.state.duplications[index];
-    let blocks = (duplication && duplication.blocks) || [];
-    const inRemovedComponent = blocks.some(b => b._ref == null);
-    let foundOne = false;
-    blocks = blocks.filter(b => {
-      const outOfBounds = b.from > line || b.from + b.size < line;
-      const currentFile = b._ref === '1';
-      const shouldDisplayForCurrentFile = outOfBounds || foundOne;
-      const shouldDisplay = !currentFile || shouldDisplayForCurrentFile;
-      const isOk = b._ref != null && shouldDisplay;
-      if (b._ref === '1' && !outOfBounds) {
-        foundOne = true;
-      }
-      return isOk;
-    });
-
-    const element = this.node.querySelector(
-      `.source-line-duplications-extra[data-line-number="${line}"]`
-    );
-    if (element) {
-      const popup = new DuplicationPopupView({
-        blocks,
-        inRemovedComponent,
-        component: this.state.component,
-        files: this.state.duplicatedFiles,
-        triggerEl: element,
-        branch: this.props.branch
-      });
-      popup.render();
-    }
-  };
-
-  handlePopupToggle = (issue /*: string */, popupName /*: string */, open /*: ?boolean */) => {
-    this.setState((state /*: State */) => {
-      const samePopup =
-        state.openPopup && state.openPopup.name === popupName && state.openPopup.issue === issue;
-      if (open !== false && !samePopup) {
-        return { openPopup: { issue, name: popupName } };
-      } else if (open !== true && samePopup) {
-        return { openPopup: null };
-      }
-      return state;
-    });
-  };
-
-  displayLinePopup(line /*: number */, element /*: HTMLElement */) {
-    const popup = new LineActionsPopupView({
-      line,
-      triggerEl: element,
-      component: this.state.component,
-      branch: this.props.branch
-    });
-    popup.render();
-  }
-
-  handleLineClick = (line /*: SourceLine */, element /*: HTMLElement */) => {
-    this.setState(prevState => ({
-      highlightedLine: prevState.highlightedLine === line.line ? null : line
-    }));
-    this.displayLinePopup(line.line, element);
-  };
-
-  handleSymbolClick = (symbols /*: Array<string> */) => {
-    this.setState(state => {
-      const shouldDisable = intersection(state.highlightedSymbols, symbols).length > 0;
-      const highlightedSymbols = shouldDisable ? [] : symbols;
-      return { highlightedSymbols };
-    });
-  };
-
-  handleSCMClick = (line /*: SourceLine */, element /*: HTMLElement */) => {
-    const popup = new SCMPopupView({ triggerEl: element, line });
-    popup.render();
-  };
-
-  handleIssueSelect = (issue /*: string */) => {
-    if (this.props.onIssueSelect) {
-      this.props.onIssueSelect(issue);
-    } else {
-      this.setState({ selectedIssue: issue });
-    }
-  };
-
-  handleIssueUnselect = () => {
-    if (this.props.onIssueUnselect) {
-      this.props.onIssueUnselect();
-    } else {
-      this.setState({ selectedIssue: undefined });
-    }
-  };
-
-  handleOpenIssues = (line /*: SourceLine */) => {
-    this.setState(state => ({
-      openIssuesByLine: { ...state.openIssuesByLine, [line.line]: true }
-    }));
-  };
-
-  handleCloseIssues = (line /*: SourceLine */) => {
-    this.setState(state => ({
-      openIssuesByLine: { ...state.openIssuesByLine, [line.line]: false }
-    }));
-  };
-
-  handleIssueChange = (issue /*: Issue */) => {
-    this.setState(state => {
-      const issues = state.issues.map(
-        candidate => (candidate.key === issue.key ? issue : candidate)
-      );
-      return { issues, issuesByLine: issuesByLine(issues) };
-    });
-    if (this.props.onIssueChange) {
-      this.props.onIssueChange(issue);
-    }
-  };
-
-  handleFilterLine = (line /*: SourceLine */) => {
-    const { component } = this.state;
-    const leakPeriodDate = component && component.leakPeriodDate;
-    return leakPeriodDate
-      ? line.scmDate != null && parseDate(line.scmDate) > leakPeriodDate
-      : false;
-  };
-
-  renderCode(sources /*: Array<SourceLine> */) {
-    const hasSourcesBefore = sources.length > 0 && sources[0].line > 1;
-    return (
-      <SourceViewerCode
-        branch={this.props.branch}
-        displayAllIssues={this.props.displayAllIssues}
-        displayIssueLocationsCount={this.props.displayIssueLocationsCount}
-        displayIssueLocationsLink={this.props.displayIssueLocationsLink}
-        displayLocationMarkers={this.props.displayLocationMarkers}
-        duplications={this.state.duplications}
-        duplicationsByLine={this.state.duplicationsByLine}
-        duplicatedFiles={this.state.duplicatedFiles}
-        hasSourcesBefore={hasSourcesBefore}
-        hasSourcesAfter={this.state.hasSourcesAfter}
-        filterLine={this.handleFilterLine}
-        highlightedLine={this.state.highlightedLine}
-        highlightedLocations={this.props.highlightedLocations}
-        highlightedLocationMessage={this.props.highlightedLocationMessage}
-        highlightedSymbols={this.state.highlightedSymbols}
-        issues={this.state.issues}
-        issuesByLine={this.state.issuesByLine}
-        issueLocationsByLine={this.state.issueLocationsByLine}
-        loadDuplications={this.loadDuplications}
-        loadSourcesAfter={this.loadSourcesAfter}
-        loadSourcesBefore={this.loadSourcesBefore}
-        loadingSourcesAfter={this.state.loadingSourcesAfter}
-        loadingSourcesBefore={this.state.loadingSourcesBefore}
-        onCoverageClick={this.handleCoverageClick}
-        onDuplicationClick={this.handleDuplicationClick}
-        onIssueChange={this.handleIssueChange}
-        onIssueSelect={this.handleIssueSelect}
-        onIssueUnselect={this.handleIssueUnselect}
-        onIssuesOpen={this.handleOpenIssues}
-        onIssuesClose={this.handleCloseIssues}
-        onLineClick={this.handleLineClick}
-        onLocationSelect={this.props.onLocationSelect}
-        onPopupToggle={this.handlePopupToggle}
-        openPopup={this.state.openPopup}
-        onSCMClick={this.handleSCMClick}
-        onSymbolClick={this.handleSymbolClick}
-        openIssuesByLine={this.state.openIssuesByLine}
-        scroll={this.props.scroll}
-        selectedIssue={this.state.selectedIssue}
-        sources={sources}
-        symbolsByLine={this.state.symbolsByLine}
-      />
-    );
-  }
-
-  render() {
-    const { component, loading, sources, notAccessible, sourceRemoved } = this.state;
-
-    if (loading) {
-      return null;
-    }
-
-    if (this.state.notExist) {
-      return (
-        <div className="alert alert-warning spacer-top">
-          {translate('component_viewer.no_component')}
-        </div>
-      );
-    }
-
-    if (notAccessible) {
-      return (
-        <div className="alert alert-warning spacer-top">
-          {translate('code_viewer.no_source_code_displayed_due_to_security')}
-        </div>
-      );
-    }
-
-    if (component == null) {
-      return null;
-    }
-
-    const className = classNames('source-viewer', {
-      'source-duplications-expanded': this.state.displayDuplications
-    });
-
-    return (
-      <div className={className} ref={node => (this.node = node)}>
-        <SourceViewerHeader branch={this.props.branch} sourceViewerFile={this.state.component} />
-        {sourceRemoved && (
-          <div className="alert alert-warning spacer-top">
-            {translate('code_viewer.no_source_code_displayed_due_to_source_removed')}
-          </div>
-        )}
-        {!sourceRemoved && sources != null && this.renderCode(sources)}
-      </div>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.tsx b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.tsx
new file mode 100644 (file)
index 0000000..6e487e2
--- /dev/null
@@ -0,0 +1,739 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as classNames from 'classnames';
+import { intersection, uniqBy } from 'lodash';
+import SourceViewerHeader from './SourceViewerHeader';
+import SourceViewerCode from './SourceViewerCode';
+import DuplicationPopup from './components/DuplicationPopup';
+import defaultLoadIssues from './helpers/loadIssues';
+import getCoverageStatus from './helpers/getCoverageStatus';
+import {
+  duplicationsByLine,
+  issuesByLine,
+  locationsByLine,
+  symbolsByLine
+} from './helpers/indexing';
+import {
+  getComponentData,
+  getComponentForSourceViewer,
+  getDuplications,
+  getSources
+} from '../../api/components';
+import {
+  Duplication,
+  FlowLocation,
+  Issue,
+  LinearIssueLocation,
+  SourceLine,
+  SourceViewerFile,
+  DuplicatedFile
+} from '../../app/types';
+import { parseDate } from '../../helpers/dates';
+import { translate } from '../../helpers/l10n';
+import './styles.css';
+
+// TODO react-virtualized
+
+interface Props {
+  aroundLine?: number;
+  branch: string | undefined;
+  component: string;
+  displayAllIssues?: boolean;
+  displayIssueLocationsCount?: boolean;
+  displayIssueLocationsLink?: boolean;
+  displayLocationMarkers?: boolean;
+  highlightedLine?: number;
+  highlightedLocations?: FlowLocation[];
+  highlightedLocationMessage?: { index: number; text: string };
+  loadComponent?: (component: string, branch: string | undefined) => Promise<SourceViewerFile>;
+  loadIssues?: (
+    component: string,
+    from: number,
+    to: number,
+    branch: string | undefined
+  ) => Promise<Issue[]>;
+  loadSources?: (
+    component: string,
+    from: number,
+    to: number,
+    branch: string | undefined
+  ) => Promise<SourceLine[]>;
+  onLoaded?: (component: SourceViewerFile, sources: SourceLine[], issues: Issue[]) => void;
+  onLocationSelect?: (index: number) => void;
+  onIssueChange?: (issue: Issue) => void;
+  onIssueSelect?: (issueKey: string) => void;
+  onIssueUnselect?: () => void;
+  onReceiveComponent: (component: SourceViewerFile) => void;
+  scroll?: (element: HTMLElement) => void;
+  selectedIssue?: string;
+}
+
+interface State {
+  component?: SourceViewerFile;
+  displayDuplications: boolean;
+  duplications?: Duplication[];
+  duplicationsByLine: { [line: number]: number[] };
+  duplicatedFiles?: { [ref: string]: DuplicatedFile };
+  hasSourcesAfter: boolean;
+  highlightedLine?: number;
+  highlightedSymbols: string[];
+  issues?: Issue[];
+  issuesByLine: { [line: number]: Issue[] };
+  issueLocationsByLine: { [line: number]: LinearIssueLocation[] };
+  linePopup?: { index?: number; line: number; name: string };
+  loading: boolean;
+  loadingSourcesAfter: boolean;
+  loadingSourcesBefore: boolean;
+  notAccessible: boolean;
+  notExist: boolean;
+  openIssuesByLine: { [line: number]: boolean };
+  issuePopup?: { issue: string; name: string };
+  selectedIssue?: string;
+  sources?: SourceLine[];
+  sourceRemoved: boolean;
+  symbolsByLine: { [line: number]: string[] };
+}
+
+const LINES = 500;
+
+export default class SourceViewerBase extends React.PureComponent<Props, State> {
+  node?: HTMLElement | null;
+  mounted = false;
+
+  static defaultProps = {
+    displayAllIssues: false,
+    displayIssueLocationsCount: true,
+    displayIssueLocationsLink: true,
+    displayLocationMarkers: true
+  };
+
+  constructor(props: Props) {
+    super(props);
+    this.state = {
+      displayDuplications: false,
+      duplicationsByLine: {},
+      hasSourcesAfter: false,
+      highlightedLine: props.highlightedLine,
+      highlightedSymbols: [],
+      issuesByLine: {},
+      issueLocationsByLine: {},
+      loading: true,
+      loadingSourcesAfter: false,
+      loadingSourcesBefore: false,
+      notAccessible: false,
+      notExist: false,
+      openIssuesByLine: {},
+      selectedIssue: props.selectedIssue,
+      sourceRemoved: false,
+      symbolsByLine: {}
+    };
+  }
+
+  componentDidMount() {
+    this.mounted = true;
+    this.fetchComponent();
+  }
+
+  componentWillReceiveProps(nextProps: Props) {
+    if (
+      nextProps.onIssueSelect !== undefined &&
+      nextProps.selectedIssue !== this.props.selectedIssue
+    ) {
+      this.setState({ selectedIssue: nextProps.selectedIssue });
+    }
+  }
+
+  componentDidUpdate(prevProps: Props) {
+    if (prevProps.component !== this.props.component || prevProps.branch !== this.props.branch) {
+      this.fetchComponent();
+    } else if (
+      this.props.aroundLine !== undefined &&
+      prevProps.aroundLine !== this.props.aroundLine &&
+      this.isLineOutsideOfRange(this.props.aroundLine)
+    ) {
+      this.fetchSources();
+    } else {
+      const { selectedIssue } = this.props;
+      const { issues } = this.state;
+      if (
+        selectedIssue !== undefined &&
+        issues !== undefined &&
+        issues.find(issue => issue.key === selectedIssue) === undefined
+      ) {
+        this.reloadIssues();
+      }
+    }
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  // react typings do not take `defaultProps` into account,
+  // so use these getters to get type-safe methods
+
+  get safeLoadComponent() {
+    return this.props.loadComponent || defaultLoadComponent;
+  }
+
+  get safeLoadIssues() {
+    return this.props.loadIssues || defaultLoadIssues;
+  }
+
+  get safeLoadSources() {
+    return this.props.loadSources || defaultLoadSources;
+  }
+
+  computeCoverageStatus(lines: SourceLine[]) {
+    return lines.map(line => ({ ...line, coverageStatus: getCoverageStatus(line) }));
+  }
+
+  isLineOutsideOfRange(lineNumber: number) {
+    const { sources } = this.state;
+    if (sources && sources.length > 0) {
+      const firstLine = sources[0];
+      const lastList = sources[sources.length - 1];
+      return lineNumber < firstLine.line || lineNumber > lastList.line;
+    } else {
+      return true;
+    }
+  }
+
+  fetchComponent() {
+    this.setState({ loading: true });
+    const loadIssues = (component: SourceViewerFile, sources: SourceLine[]) => {
+      this.safeLoadIssues(this.props.component, 1, LINES, this.props.branch).then(
+        issues => {
+          if (this.mounted) {
+            const finalSources = sources.slice(0, LINES);
+            this.setState(
+              {
+                component,
+                issues,
+                issuesByLine: issuesByLine(issues),
+                issueLocationsByLine: locationsByLine(issues),
+                loading: false,
+                notAccessible: false,
+                notExist: false,
+                hasSourcesAfter: sources.length > LINES,
+                sources: this.computeCoverageStatus(finalSources),
+                sourceRemoved: false,
+                symbolsByLine: symbolsByLine(sources.slice(0, LINES))
+              },
+              () => {
+                if (this.props.onLoaded) {
+                  this.props.onLoaded(component, finalSources, issues);
+                }
+              }
+            );
+          }
+        },
+        () => {
+          // TODO
+        }
+      );
+    };
+
+    const onFailLoadComponent = ({ response }: { response: Response }) => {
+      // TODO handle other statuses
+      if (this.mounted) {
+        if (response.status === 403) {
+          this.setState({ loading: false, notAccessible: true });
+        } else if (response.status === 404) {
+          this.setState({ loading: false, notExist: true });
+        }
+      }
+    };
+
+    const onFailLoadSources = (response: Response, component: SourceViewerFile) => {
+      // TODO handle other statuses
+      if (this.mounted) {
+        if (response.status === 403) {
+          this.setState({ component, loading: false, notAccessible: true });
+        } else if (response.status === 404) {
+          this.setState({ component, loading: false, sourceRemoved: true });
+        }
+      }
+    };
+
+    const onResolve = (component: SourceViewerFile) => {
+      this.props.onReceiveComponent(component);
+      const sourcesRequest =
+        component.q === 'FIL' || component.q === 'UTS' ? this.loadSources() : Promise.resolve([]);
+      sourcesRequest.then(
+        sources => loadIssues(component, sources),
+        response => onFailLoadSources(response, component)
+      );
+    };
+
+    this.safeLoadComponent(this.props.component, this.props.branch).then(
+      onResolve,
+      onFailLoadComponent
+    );
+  }
+
+  fetchSources() {
+    this.loadSources().then(
+      sources => {
+        if (this.mounted) {
+          const finalSources = sources.slice(0, LINES);
+          this.setState(
+            {
+              sources: sources.slice(0, LINES),
+              hasSourcesAfter: sources.length > LINES
+            },
+            () => {
+              if (this.props.onLoaded && this.state.component && this.state.issues) {
+                this.props.onLoaded(this.state.component, finalSources, this.state.issues);
+              }
+            }
+          );
+        }
+      },
+      () => {
+        // TODO
+      }
+    );
+  }
+
+  reloadIssues() {
+    if (!this.state.sources) {
+      return;
+    }
+    const firstSourceLine = this.state.sources[0];
+    const lastSourceLine = this.state.sources[this.state.sources.length - 1];
+    this.safeLoadIssues(
+      this.props.component,
+      firstSourceLine && firstSourceLine.line,
+      lastSourceLine && lastSourceLine.line,
+      this.props.branch
+    ).then(
+      issues => {
+        if (this.mounted) {
+          this.setState({
+            issues,
+            issuesByLine: issuesByLine(issues),
+            issueLocationsByLine: locationsByLine(issues)
+          });
+        }
+      },
+      () => {
+        // TODO
+      }
+    );
+  }
+
+  loadSources = (): Promise<SourceLine[]> => {
+    return new Promise((resolve, reject) => {
+      const onFailLoadSources = ({ response }: { response: Response }) => {
+        // TODO handle other statuses
+        if (this.mounted) {
+          if ([403, 404].includes(response.status)) {
+            reject(response);
+          } else {
+            resolve([]);
+          }
+        }
+      };
+
+      const from = this.props.aroundLine ? Math.max(1, this.props.aroundLine - LINES / 2 + 1) : 1;
+
+      let to = this.props.aroundLine ? this.props.aroundLine + LINES / 2 + 1 : LINES + 1;
+      // make sure we try to download `LINES` lines
+      if (from === 1 && to < LINES) {
+        to = LINES;
+      }
+      // request one additional line to define `hasSourcesAfter`
+      to++;
+
+      return this.safeLoadSources(this.props.component, from, to, this.props.branch).then(
+        sources => resolve(sources),
+        onFailLoadSources
+      );
+    });
+  };
+
+  loadSourcesBefore = () => {
+    if (!this.state.sources) {
+      return;
+    }
+    const firstSourceLine = this.state.sources[0];
+    this.setState({ loadingSourcesBefore: true });
+    const from = Math.max(1, firstSourceLine.line - LINES);
+    this.safeLoadSources(
+      this.props.component,
+      from,
+      firstSourceLine.line - 1,
+      this.props.branch
+    ).then(
+      sources => {
+        this.safeLoadIssues(
+          this.props.component,
+          from,
+          firstSourceLine.line - 1,
+          this.props.branch
+        ).then(
+          issues => {
+            if (this.mounted) {
+              this.setState(prevState => {
+                const nextIssues = uniqBy(
+                  [...issues, ...(prevState.issues || [])],
+                  issue => issue.key
+                );
+                return {
+                  issues: nextIssues,
+                  issuesByLine: issuesByLine(nextIssues),
+                  issueLocationsByLine: locationsByLine(nextIssues),
+                  loadingSourcesBefore: false,
+                  sources: [...this.computeCoverageStatus(sources), ...(prevState.sources || [])],
+                  symbolsByLine: { ...prevState.symbolsByLine, ...symbolsByLine(sources) }
+                };
+              });
+            }
+          },
+          () => {
+            // TODO
+          }
+        );
+      },
+      () => {
+        // TODO
+      }
+    );
+  };
+
+  loadSourcesAfter = () => {
+    if (!this.state.sources) {
+      return;
+    }
+    const lastSourceLine = this.state.sources[this.state.sources.length - 1];
+    this.setState({ loadingSourcesAfter: true });
+    const fromLine = lastSourceLine.line + 1;
+    // request one additional line to define `hasSourcesAfter`
+    const toLine = lastSourceLine.line + LINES + 1;
+    this.safeLoadSources(this.props.component, fromLine, toLine, this.props.branch).then(
+      sources => {
+        this.safeLoadIssues(this.props.component, fromLine, toLine, this.props.branch).then(
+          issues => {
+            if (this.mounted) {
+              this.setState(prevState => {
+                const nextIssues = uniqBy(
+                  [...(prevState.issues || []), ...issues],
+                  issue => issue.key
+                );
+                return {
+                  issues: nextIssues,
+                  issuesByLine: issuesByLine(nextIssues),
+                  issueLocationsByLine: locationsByLine(nextIssues),
+                  hasSourcesAfter: sources.length > LINES,
+                  loadingSourcesAfter: false,
+                  sources: [
+                    ...(prevState.sources || []),
+                    ...this.computeCoverageStatus(sources.slice(0, LINES))
+                  ],
+                  symbolsByLine: {
+                    ...prevState.symbolsByLine,
+                    ...symbolsByLine(sources.slice(0, LINES))
+                  }
+                };
+              });
+            }
+          },
+          () => {
+            // TODO
+          }
+        );
+      },
+      () => {
+        // TODO
+      }
+    );
+  };
+
+  loadDuplications = (line: SourceLine) => {
+    getDuplications(this.props.component, this.props.branch).then(
+      r => {
+        if (this.mounted) {
+          this.setState(() => {
+            const changes: Partial<State> = {
+              displayDuplications: true,
+              duplications: r.duplications,
+              duplicationsByLine: duplicationsByLine(r.duplications),
+              duplicatedFiles: r.files
+            };
+            if (r.duplications.length === 1) {
+              changes.linePopup = { index: 0, line: line.line, name: 'duplications' };
+            }
+            return changes;
+          });
+        }
+      },
+      () => {
+        // TODO
+      }
+    );
+  };
+
+  handleLinePopupToggle = ({
+    index,
+    line,
+    name,
+    open
+  }: {
+    index?: number;
+    line: number;
+    name: string;
+    open?: boolean;
+  }) => {
+    this.setState((state: State) => {
+      const samePopup =
+        state.linePopup !== undefined &&
+        state.linePopup.name === name &&
+        state.linePopup.line === line &&
+        state.linePopup.index === index;
+      if (open !== false && !samePopup) {
+        return { linePopup: { index, line, name } };
+      } else if (open !== true && samePopup) {
+        return { linePopup: undefined };
+      }
+      return null;
+    });
+  };
+
+  closeLinePopup = () => {
+    this.setState({ linePopup: undefined });
+  };
+
+  handleIssuePopupToggle = (issue: string, popupName: string, open?: boolean) => {
+    this.setState((state: State) => {
+      const samePopup =
+        state.issuePopup && state.issuePopup.name === popupName && state.issuePopup.issue === issue;
+      if (open !== false && !samePopup) {
+        return { issuePopup: { issue, name: popupName } };
+      } else if (open !== true && samePopup) {
+        return { issuePopup: undefined };
+      }
+      return null;
+    });
+  };
+
+  handleSymbolClick = (symbols: string[]) => {
+    this.setState(state => {
+      const shouldDisable = intersection(state.highlightedSymbols, symbols).length > 0;
+      const highlightedSymbols = shouldDisable ? [] : symbols;
+      return { highlightedSymbols };
+    });
+  };
+
+  handleIssueSelect = (issue: string) => {
+    if (this.props.onIssueSelect) {
+      this.props.onIssueSelect(issue);
+    } else {
+      this.setState({ selectedIssue: issue });
+    }
+  };
+
+  handleIssueUnselect = () => {
+    if (this.props.onIssueUnselect) {
+      this.props.onIssueUnselect();
+    } else {
+      this.setState({ selectedIssue: undefined });
+    }
+  };
+
+  handleOpenIssues = (line: SourceLine) => {
+    this.setState(state => ({
+      openIssuesByLine: { ...state.openIssuesByLine, [line.line]: true }
+    }));
+  };
+
+  handleCloseIssues = (line: SourceLine) => {
+    this.setState(state => ({
+      openIssuesByLine: { ...state.openIssuesByLine, [line.line]: false }
+    }));
+  };
+
+  handleIssueChange = (issue: Issue) => {
+    this.setState(({ issues = [] }) => {
+      const newIssues = issues.map(candidate => (candidate.key === issue.key ? issue : candidate));
+      return { issues: newIssues, issuesByLine: issuesByLine(newIssues) };
+    });
+    if (this.props.onIssueChange) {
+      this.props.onIssueChange(issue);
+    }
+  };
+
+  handleFilterLine = (line: SourceLine) => {
+    const { component } = this.state;
+    const leakPeriodDate = component && component.leakPeriodDate;
+    return leakPeriodDate
+      ? line.scmDate !== undefined && parseDate(line.scmDate) > parseDate(leakPeriodDate)
+      : false;
+  };
+
+  renderDuplicationPopup = (index: number, line: number) => {
+    const { component, duplicatedFiles, duplications } = this.state;
+
+    if (!component || !duplicatedFiles) return <></>;
+
+    const duplication = duplications && duplications[index];
+    let blocks = (duplication && duplication.blocks) || [];
+    /* eslint-disable no-underscore-dangle */
+    const inRemovedComponent = blocks.some(b => b._ref === undefined);
+    let foundOne = false;
+    blocks = blocks.filter(b => {
+      const outOfBounds = b.from > line || b.from + b.size < line;
+      const currentFile = b._ref === '1';
+      const shouldDisplayForCurrentFile = outOfBounds || foundOne;
+      const shouldDisplay = !currentFile || shouldDisplayForCurrentFile;
+      const isOk = b._ref !== undefined && shouldDisplay;
+      if (b._ref === '1' && !outOfBounds) {
+        foundOne = true;
+      }
+      return isOk;
+    });
+    /* eslint-enable no-underscore-dangle */
+
+    return (
+      <DuplicationPopup
+        blocks={blocks}
+        branch={this.props.branch}
+        duplicatedFiles={duplicatedFiles}
+        inRemovedComponent={inRemovedComponent}
+        onClose={this.closeLinePopup}
+        sourceViewerFile={component}
+      />
+    );
+  };
+
+  renderCode(sources: SourceLine[]) {
+    const hasSourcesBefore = sources.length > 0 && sources[0].line > 1;
+    return (
+      <SourceViewerCode
+        branch={this.props.branch}
+        componentKey={this.props.component}
+        displayAllIssues={this.props.displayAllIssues}
+        displayIssueLocationsCount={this.props.displayIssueLocationsCount}
+        displayIssueLocationsLink={this.props.displayIssueLocationsLink}
+        displayLocationMarkers={this.props.displayLocationMarkers}
+        duplications={this.state.duplications}
+        duplicationsByLine={this.state.duplicationsByLine}
+        filterLine={this.handleFilterLine}
+        hasSourcesAfter={this.state.hasSourcesAfter}
+        hasSourcesBefore={hasSourcesBefore}
+        highlightedLine={this.state.highlightedLine}
+        highlightedLocationMessage={this.props.highlightedLocationMessage}
+        highlightedLocations={this.props.highlightedLocations}
+        highlightedSymbols={this.state.highlightedSymbols}
+        issueLocationsByLine={this.state.issueLocationsByLine}
+        issuePopup={this.state.issuePopup}
+        issues={this.state.issues}
+        issuesByLine={this.state.issuesByLine}
+        linePopup={this.state.linePopup}
+        loadDuplications={this.loadDuplications}
+        loadSourcesAfter={this.loadSourcesAfter}
+        loadSourcesBefore={this.loadSourcesBefore}
+        loadingSourcesAfter={this.state.loadingSourcesAfter}
+        loadingSourcesBefore={this.state.loadingSourcesBefore}
+        onIssueChange={this.handleIssueChange}
+        onIssuePopupToggle={this.handleIssuePopupToggle}
+        onIssueSelect={this.handleIssueSelect}
+        onIssueUnselect={this.handleIssueUnselect}
+        onIssuesClose={this.handleCloseIssues}
+        onIssuesOpen={this.handleOpenIssues}
+        onLinePopupToggle={this.handleLinePopupToggle}
+        onLocationSelect={this.props.onLocationSelect}
+        onSymbolClick={this.handleSymbolClick}
+        openIssuesByLine={this.state.openIssuesByLine}
+        renderDuplicationPopup={this.renderDuplicationPopup}
+        scroll={this.props.scroll}
+        selectedIssue={this.state.selectedIssue}
+        sources={sources}
+        symbolsByLine={this.state.symbolsByLine}
+      />
+    );
+  }
+
+  render() {
+    const { component, loading, sources, notAccessible, sourceRemoved } = this.state;
+
+    if (loading) {
+      return null;
+    }
+
+    if (this.state.notExist) {
+      return (
+        <div className="alert alert-warning spacer-top">
+          {translate('component_viewer.no_component')}
+        </div>
+      );
+    }
+
+    if (notAccessible) {
+      return (
+        <div className="alert alert-warning spacer-top">
+          {translate('code_viewer.no_source_code_displayed_due_to_security')}
+        </div>
+      );
+    }
+
+    if (!component) {
+      return null;
+    }
+
+    const className = classNames('source-viewer', {
+      'source-duplications-expanded': this.state.displayDuplications
+    });
+
+    return (
+      <div className={className} ref={node => (this.node = node)}>
+        {this.state.component && (
+          <SourceViewerHeader branch={this.props.branch} sourceViewerFile={this.state.component} />
+        )}
+        {sourceRemoved && (
+          <div className="alert alert-warning spacer-top">
+            {translate('code_viewer.no_source_code_displayed_due_to_source_removed')}
+          </div>
+        )}
+        {!sourceRemoved && sources !== undefined && this.renderCode(sources)}
+      </div>
+    );
+  }
+}
+
+function defaultLoadComponent(key: string, branch: string | undefined) {
+  return Promise.all([
+    getComponentForSourceViewer(key, branch),
+    getComponentData(key, branch)
+  ]).then(([component, data]) => ({
+    ...component,
+    leakPeriodDate: data.leakPeriodDate && parseDate(data.leakPeriodDate)
+  }));
+}
+
+function defaultLoadSources(
+  key: string,
+  from: number | undefined,
+  to: number | undefined,
+  branch: string | undefined
+) {
+  return getSources(key, from, to, branch);
+}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.js b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.js
deleted file mode 100644 (file)
index b3285d2..0000000
+++ /dev/null
@@ -1,256 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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.
- */
-// @flow
-import React from 'react';
-import { intersection } from 'lodash';
-import Line from './components/Line';
-import { getLinearLocations } from './helpers/issueLocations';
-import { translate } from '../../helpers/l10n';
-/*:: import type { Duplication, SourceLine } from './types'; */
-/*:: import type { Issue, FlowLocation } from '../issue/types'; */
-/*:: import type { LinearIssueLocation } from './helpers/indexing'; */
-
-const EMPTY_ARRAY = [];
-
-const ZERO_LINE = {
-  code: '',
-  duplicated: false,
-  line: 0
-};
-
-export default class SourceViewerCode extends React.PureComponent {
-  /*:: props: {|
-    branch?: string,
-    displayAllIssues: boolean,
-    displayIssueLocationsCount?: boolean;
-    displayIssueLocationsLink?: boolean;
-    displayLocationMarkers?: boolean;
-    duplications?: Array<Duplication>,
-    duplicationsByLine: { [number]: Array<number> },
-    duplicatedFiles?: Array<{ key: string }>,
-    filterLine?: SourceLine => boolean,
-    hasSourcesAfter: boolean,
-    hasSourcesBefore: boolean,
-    highlightedLine: number | null,
-    highlightedLocations?: Array<FlowLocation>,
-    highlightedLocationMessage?: { index: number, text: string },
-    highlightedSymbols: Array<string>,
-    issues: Array<Issue>,
-    issuesByLine: { [number]: Array<Issue> },
-    issueLocationsByLine: { [number]: Array<LinearIssueLocation> },
-    loadDuplications: SourceLine => void,
-    loadSourcesAfter: () => void,
-    loadSourcesBefore: () => void,
-    loadingSourcesAfter: boolean,
-    loadingSourcesBefore: boolean,
-    onCoverageClick: (SourceLine, HTMLElement) => void,
-    onDuplicationClick: (number, number) => void,
-    onIssueChange: Issue => void,
-    onIssueSelect: string => void,
-    onIssueUnselect: () => void,
-    onIssuesOpen: SourceLine => void,
-    onIssuesClose: SourceLine => void,
-    onLineClick: (SourceLine, HTMLElement) => void,
-    onLocationSelect?: number => void,
-    onSCMClick: (SourceLine, HTMLElement) => void,
-    onSymbolClick: (Array<string>) => void,
-    openIssuesByLine: { [number]: boolean },
-    onPopupToggle: (issue: string, popupName: string, open: ?boolean ) => void,
-    openPopup: ?{ issue: string, name: string},
-    scroll?: HTMLElement => void,
-    selectedIssue: string | null,
-    sources: Array<SourceLine>,
-    symbolsByLine: { [number]: Array<string> }
-  |};
-*/
-
-  getDuplicationsForLine(line /*: SourceLine */) {
-    return this.props.duplicationsByLine[line.line] || EMPTY_ARRAY;
-  }
-
-  getIssuesForLine(line /*: SourceLine */) /*: Array<Issue> */ {
-    return this.props.issuesByLine[line.line] || EMPTY_ARRAY;
-  }
-
-  getIssueLocationsForLine(line /*: SourceLine */) {
-    return this.props.issueLocationsByLine[line.line] || EMPTY_ARRAY;
-  }
-
-  getSecondaryIssueLocationsForLine(
-    line /*: SourceLine */
-  ) /*: Array<{ from: number, to: number, line: number, index: number, startLine: number }> */ {
-    const { highlightedLocations } = this.props;
-    if (!highlightedLocations) {
-      return EMPTY_ARRAY;
-    }
-    return highlightedLocations.reduce((locations, location, index) => {
-      const linearLocations = getLinearLocations(location.textRange)
-        .filter(l => l.line === line.line)
-        .map(l => ({ ...l, startLine: location.textRange.startLine, index }));
-      return [...locations, ...linearLocations];
-    }, []);
-  }
-
-  renderLine = (
-    line /*: SourceLine */,
-    index /*: number */,
-    displayCoverage /*: boolean */,
-    displayDuplications /*: boolean */,
-    displayIssues /*: boolean */
-  ) => {
-    const { filterLine, highlightedLocationMessage, selectedIssue, sources } = this.props;
-    const filtered = filterLine ? filterLine(line) : null;
-
-    const secondaryIssueLocations = this.getSecondaryIssueLocationsForLine(line);
-
-    const duplicationsCount = this.props.duplications ? this.props.duplications.length : 0;
-
-    const issuesForLine = this.getIssuesForLine(line);
-
-    // for the following properties pass null if the line for sure is not impacted
-    const symbolsForLine = this.props.symbolsByLine[line.line] || [];
-    const { highlightedSymbols } = this.props;
-    let optimizedHighlightedSymbols = intersection(symbolsForLine, highlightedSymbols);
-    if (!optimizedHighlightedSymbols.length) {
-      optimizedHighlightedSymbols = undefined;
-    }
-
-    const optimizedSelectedIssue =
-      selectedIssue != null && issuesForLine.find(issue => issue.key === selectedIssue)
-        ? selectedIssue
-        : null;
-
-    const optimizedSecondaryIssueLocations =
-      secondaryIssueLocations.length > 0 ? secondaryIssueLocations : EMPTY_ARRAY;
-
-    const optimizedLocationMessage =
-      highlightedLocationMessage != null &&
-      optimizedSecondaryIssueLocations.some(
-        location => location.index === highlightedLocationMessage.index
-      )
-        ? highlightedLocationMessage
-        : undefined;
-
-    return (
-      <Line
-        branch={this.props.branch}
-        displayAllIssues={this.props.displayAllIssues}
-        displayCoverage={displayCoverage}
-        displayDuplications={displayDuplications}
-        displayIssues={displayIssues}
-        displayIssueLocationsCount={this.props.displayIssueLocationsCount}
-        displayIssueLocationsLink={this.props.displayIssueLocationsLink}
-        displayLocationMarkers={this.props.displayLocationMarkers}
-        duplications={this.getDuplicationsForLine(line)}
-        duplicationsCount={duplicationsCount}
-        filtered={filtered}
-        highlighted={line.line === this.props.highlightedLine}
-        highlightedLocationMessage={optimizedLocationMessage}
-        highlightedSymbols={optimizedHighlightedSymbols}
-        issueLocations={this.getIssueLocationsForLine(line)}
-        issues={issuesForLine}
-        key={line.line}
-        last={index === this.props.sources.length - 1 && !this.props.hasSourcesAfter}
-        line={line}
-        loadDuplications={this.props.loadDuplications}
-        onClick={this.props.onLineClick}
-        onCoverageClick={this.props.onCoverageClick}
-        onDuplicationClick={this.props.onDuplicationClick}
-        onIssueChange={this.props.onIssueChange}
-        onIssueSelect={this.props.onIssueSelect}
-        onIssueUnselect={this.props.onIssueUnselect}
-        onIssuesOpen={this.props.onIssuesOpen}
-        onIssuesClose={this.props.onIssuesClose}
-        onLocationSelect={this.props.onLocationSelect}
-        onSCMClick={this.props.onSCMClick}
-        onSymbolClick={this.props.onSymbolClick}
-        openIssues={this.props.openIssuesByLine[line.line] || false}
-        onPopupToggle={this.props.onPopupToggle}
-        openPopup={this.props.openPopup}
-        previousLine={index > 0 ? sources[index - 1] : undefined}
-        scroll={this.props.scroll}
-        secondaryIssueLocations={optimizedSecondaryIssueLocations}
-        selectedIssue={optimizedSelectedIssue}
-      />
-    );
-  };
-
-  render() {
-    const { sources } = this.props;
-
-    const hasCoverage = sources.some(s => s.coverageStatus != null);
-    const hasDuplications = sources.some(s => s.duplicated);
-    const hasIssues = this.props.issues.length > 0;
-
-    const hasFileIssues = hasIssues && this.props.issues.some(issue => !issue.textRange);
-
-    return (
-      <div>
-        {this.props.hasSourcesBefore && (
-          <div className="source-viewer-more-code">
-            {this.props.loadingSourcesBefore ? (
-              <div className="js-component-viewer-loading-before">
-                <i className="spinner" />
-                <span className="note spacer-left">
-                  {translate('source_viewer.loading_more_code')}
-                </span>
-              </div>
-            ) : (
-              <button
-                className="js-component-viewer-source-before"
-                onClick={this.props.loadSourcesBefore}>
-                {translate('source_viewer.load_more_code')}
-              </button>
-            )}
-          </div>
-        )}
-
-        <table className="source-table">
-          <tbody>
-            {hasFileIssues &&
-              this.renderLine(ZERO_LINE, -1, hasCoverage, hasDuplications, hasIssues)}
-            {sources.map((line, index) =>
-              this.renderLine(line, index, hasCoverage, hasDuplications, hasIssues)
-            )}
-          </tbody>
-        </table>
-
-        {this.props.hasSourcesAfter && (
-          <div className="source-viewer-more-code">
-            {this.props.loadingSourcesAfter ? (
-              <div className="js-component-viewer-loading-after">
-                <i className="spinner" />
-                <span className="note spacer-left">
-                  {translate('source_viewer.loading_more_code')}
-                </span>
-              </div>
-            ) : (
-              <button
-                className="js-component-viewer-source-after"
-                onClick={this.props.loadSourcesAfter}>
-                {translate('source_viewer.load_more_code')}
-              </button>
-            )}
-          </div>
-        )}
-      </div>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.tsx b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.tsx
new file mode 100644 (file)
index 0000000..15db6dd
--- /dev/null
@@ -0,0 +1,265 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { intersection } from 'lodash';
+import Line from './components/Line';
+import { getLinearLocations } from './helpers/issueLocations';
+import { Duplication, FlowLocation, Issue, LinearIssueLocation, SourceLine } from '../../app/types';
+import { translate } from '../../helpers/l10n';
+import { Button } from '../ui/buttons';
+
+const EMPTY_ARRAY: any[] = [];
+
+const ZERO_LINE = {
+  code: '',
+  duplicated: false,
+  line: 0
+};
+
+interface Props {
+  branch: string | undefined;
+  componentKey: string;
+  displayAllIssues?: boolean;
+  displayIssueLocationsCount?: boolean;
+  displayIssueLocationsLink?: boolean;
+  displayLocationMarkers?: boolean;
+  duplications: Duplication[] | undefined;
+  duplicationsByLine: { [line: number]: number[] };
+  filterLine?: (line: SourceLine) => boolean;
+  hasSourcesAfter: boolean;
+  hasSourcesBefore: boolean;
+  highlightedLine: number | undefined;
+  highlightedLocationMessage: { index: number; text: string } | undefined;
+  highlightedLocations: FlowLocation[] | undefined;
+  highlightedSymbols: string[];
+  issueLocationsByLine: { [line: number]: LinearIssueLocation[] };
+  issuePopup: { issue: string; name: string } | undefined;
+  issues: Issue[] | undefined;
+  issuesByLine: { [line: number]: Issue[] };
+  linePopup: { index?: number; line: number; name: string } | undefined;
+  loadDuplications: (line: SourceLine) => void;
+  loadingSourcesAfter: boolean;
+  loadingSourcesBefore: boolean;
+  loadSourcesAfter: () => void;
+  loadSourcesBefore: () => void;
+  onIssueChange: (issue: Issue) => void;
+  onIssuePopupToggle: (issue: string, popupName: string, open?: boolean) => void;
+  onIssuesClose: (line: SourceLine) => void;
+  onIssueSelect: (issueKey: string) => void;
+  onIssuesOpen: (line: SourceLine) => void;
+  onIssueUnselect: () => void;
+  onLinePopupToggle: (x: { index?: number; line: number; name: string; open?: boolean }) => void;
+  onLocationSelect: ((index: number) => void) | undefined;
+  onSymbolClick: (symbols: string[]) => void;
+  openIssuesByLine: { [line: number]: boolean };
+  renderDuplicationPopup: (index: number, line: number) => JSX.Element;
+  scroll?: (element: HTMLElement) => void;
+  selectedIssue: string | undefined;
+  sources: SourceLine[];
+  symbolsByLine: { [line: number]: string[] };
+}
+
+export default class SourceViewerCode extends React.PureComponent<Props> {
+  getDuplicationsForLine = (line: SourceLine): number[] => {
+    return this.props.duplicationsByLine[line.line] || EMPTY_ARRAY;
+  };
+
+  getIssuesForLine = (line: SourceLine): Issue[] => {
+    return this.props.issuesByLine[line.line] || EMPTY_ARRAY;
+  };
+
+  getIssueLocationsForLine = (line: SourceLine): LinearIssueLocation[] => {
+    return this.props.issueLocationsByLine[line.line] || EMPTY_ARRAY;
+  };
+
+  getSecondaryIssueLocationsForLine = (line: SourceLine): LinearIssueLocation[] => {
+    const { highlightedLocations } = this.props;
+    if (!highlightedLocations) {
+      return EMPTY_ARRAY;
+    }
+    return highlightedLocations.reduce((locations, location, index) => {
+      const linearLocations: LinearIssueLocation[] = getLinearLocations(location.textRange)
+        .filter(l => l.line === line.line)
+        .map(l => ({ ...l, startLine: location.textRange.startLine, index }));
+      return [...locations, ...linearLocations];
+    }, []);
+  };
+
+  renderLine = ({
+    line,
+    index,
+    displayCoverage,
+    displayDuplications,
+    displayIssues
+  }: {
+    line: SourceLine;
+    index: number;
+    displayCoverage: boolean;
+    displayDuplications: boolean;
+    displayIssues: boolean;
+  }) => {
+    const { filterLine, highlightedLocationMessage, selectedIssue, sources } = this.props;
+    const filtered = filterLine && filterLine(line);
+
+    const secondaryIssueLocations = this.getSecondaryIssueLocationsForLine(line);
+
+    const duplicationsCount = this.props.duplications ? this.props.duplications.length : 0;
+
+    const issuesForLine = this.getIssuesForLine(line);
+
+    // for the following properties pass null if the line for sure is not impacted
+    const symbolsForLine = this.props.symbolsByLine[line.line] || [];
+    const { highlightedSymbols } = this.props;
+    let optimizedHighlightedSymbols: string[] | undefined = intersection(
+      symbolsForLine,
+      highlightedSymbols
+    );
+    if (!optimizedHighlightedSymbols.length) {
+      optimizedHighlightedSymbols = undefined;
+    }
+
+    const optimizedSelectedIssue =
+      selectedIssue !== undefined && issuesForLine.find(issue => issue.key === selectedIssue)
+        ? selectedIssue
+        : undefined;
+
+    const optimizedSecondaryIssueLocations =
+      secondaryIssueLocations.length > 0 ? secondaryIssueLocations : EMPTY_ARRAY;
+
+    const optimizedLocationMessage =
+      highlightedLocationMessage != null &&
+      optimizedSecondaryIssueLocations.some(
+        location => location.index === highlightedLocationMessage.index
+      )
+        ? highlightedLocationMessage
+        : undefined;
+
+    return (
+      <Line
+        branch={this.props.branch}
+        componentKey={this.props.componentKey}
+        displayAllIssues={this.props.displayAllIssues}
+        displayCoverage={displayCoverage}
+        displayDuplications={displayDuplications}
+        displayIssueLocationsCount={this.props.displayIssueLocationsCount}
+        displayIssueLocationsLink={this.props.displayIssueLocationsLink}
+        displayIssues={displayIssues}
+        displayLocationMarkers={this.props.displayLocationMarkers}
+        duplications={this.getDuplicationsForLine(line)}
+        duplicationsCount={duplicationsCount}
+        filtered={filtered}
+        highlighted={line.line === this.props.highlightedLine}
+        highlightedLocationMessage={optimizedLocationMessage}
+        highlightedSymbols={optimizedHighlightedSymbols}
+        issueLocations={this.getIssueLocationsForLine(line)}
+        issuePopup={this.props.issuePopup}
+        issues={issuesForLine}
+        key={line.line}
+        last={index === this.props.sources.length - 1 && !this.props.hasSourcesAfter}
+        line={line}
+        linePopup={this.props.linePopup}
+        loadDuplications={this.props.loadDuplications}
+        onIssueChange={this.props.onIssueChange}
+        onIssuePopupToggle={this.props.onIssuePopupToggle}
+        onIssueSelect={this.props.onIssueSelect}
+        onIssueUnselect={this.props.onIssueUnselect}
+        onIssuesClose={this.props.onIssuesClose}
+        onIssuesOpen={this.props.onIssuesOpen}
+        onLinePopupToggle={this.props.onLinePopupToggle}
+        onLocationSelect={this.props.onLocationSelect}
+        onSymbolClick={this.props.onSymbolClick}
+        openIssues={this.props.openIssuesByLine[line.line] || false}
+        previousLine={index > 0 ? sources[index - 1] : undefined}
+        renderDuplicationPopup={this.props.renderDuplicationPopup}
+        scroll={this.props.scroll}
+        secondaryIssueLocations={optimizedSecondaryIssueLocations}
+        selectedIssue={optimizedSelectedIssue}
+      />
+    );
+  };
+
+  render() {
+    const { issues = [], sources } = this.props;
+
+    const displayCoverage = sources.some(s => s.coverageStatus != null);
+    const displayDuplications = sources.some(s => !!s.duplicated);
+    const displayIssues = issues.length > 0;
+
+    const hasFileIssues = displayIssues && issues.some(issue => !issue.textRange);
+
+    return (
+      <div>
+        {this.props.hasSourcesBefore && (
+          <div className="source-viewer-more-code">
+            {this.props.loadingSourcesBefore ? (
+              <div className="js-component-viewer-loading-before">
+                <i className="spinner" />
+                <span className="note spacer-left">
+                  {translate('source_viewer.loading_more_code')}
+                </span>
+              </div>
+            ) : (
+              <Button
+                className="js-component-viewer-source-before"
+                onClick={this.props.loadSourcesBefore}>
+                {translate('source_viewer.load_more_code')}
+              </Button>
+            )}
+          </div>
+        )}
+
+        <table className="source-table">
+          <tbody>
+            {hasFileIssues &&
+              this.renderLine({
+                line: ZERO_LINE,
+                index: -1,
+                displayCoverage,
+                displayDuplications,
+                displayIssues
+              })}
+            {sources.map((line, index) =>
+              this.renderLine({ line, index, displayCoverage, displayDuplications, displayIssues })
+            )}
+          </tbody>
+        </table>
+
+        {this.props.hasSourcesAfter && (
+          <div className="source-viewer-more-code">
+            {this.props.loadingSourcesAfter ? (
+              <div className="js-component-viewer-loading-after">
+                <i className="spinner" />
+                <span className="note spacer-left">
+                  {translate('source_viewer.loading_more_code')}
+                </span>
+              </div>
+            ) : (
+              <Button
+                className="js-component-viewer-source-after"
+                onClick={this.props.loadSourcesAfter}>
+                {translate('source_viewer.load_more_code')}
+              </Button>
+            )}
+          </div>
+        )}
+      </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/CoveragePopup.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/CoveragePopup.tsx
new file mode 100644 (file)
index 0000000..2edff72
--- /dev/null
@@ -0,0 +1,153 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { groupBy } from 'lodash';
+import { getTests } from '../../../api/components';
+import { SourceLine, TestCase } from '../../../app/types';
+import BubblePopup from '../../common/BubblePopup';
+import TestStatusIcon from '../../shared/TestStatusIcon';
+import { translate } from '../../../helpers/l10n';
+import { collapsePath } from '../../../helpers/path';
+
+interface Props {
+  branch: string | undefined;
+  componentKey: string;
+  line: SourceLine;
+  onClose: () => void;
+  popupPosition?: any;
+}
+
+interface State {
+  loading: boolean;
+  testCases: TestCase[];
+}
+
+export default class CoveragePopup extends React.PureComponent<Props, State> {
+  mounted = false;
+  state: State = { loading: true, testCases: [] };
+
+  componentDidMount() {
+    this.mounted = true;
+    this.fetchTests();
+  }
+
+  componentDidUpdate(prevProps: Props) {
+    // TODO use branchLike
+    if (
+      prevProps.branch !== this.props.branch ||
+      prevProps.componentKey !== this.props.componentKey ||
+      prevProps.line.line !== this.props.line.line
+    ) {
+      this.fetchTests();
+    }
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  fetchTests = () => {
+    this.setState({ loading: true });
+    getTests(this.props.componentKey, this.props.line.line, this.props.branch).then(
+      testCases => {
+        if (this.mounted) {
+          this.setState({ loading: false, testCases });
+        }
+      },
+      () => {
+        if (this.mounted) {
+          this.setState({ loading: false });
+        }
+      }
+    );
+  };
+
+  handleTestClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
+    event.preventDefault();
+    event.currentTarget.blur();
+    const { key } = event.currentTarget.dataset;
+    const Workspace = require('../../workspace/main').default;
+    Workspace.openComponent({ key, branch: this.props.branch });
+    this.props.onClose();
+  };
+
+  render() {
+    const { line } = this.props;
+    const testCasesByFile = groupBy(this.state.testCases || [], 'fileKey');
+    const testFiles = Object.keys(testCasesByFile).map(fileKey => {
+      const testSet = testCasesByFile[fileKey];
+      const test = testSet[0];
+      return {
+        file: { key: test.fileKey, longName: test.fileName },
+        tests: testSet
+      };
+    });
+
+    return (
+      <BubblePopup customClass="source-viewer-bubble-popup" position={this.props.popupPosition}>
+        <div className="bubble-popup-title">
+          {translate('source_viewer.covered')}
+          {!!line.conditions && (
+            <div>
+              {'('}
+              {line.coveredConditions || '0'}
+              {' of '}
+              {line.conditions} {translate('source_viewer.conditions')}
+              {')'}
+            </div>
+          )}
+        </div>
+        {this.state.loading ? (
+          <i className="spinner" />
+        ) : (
+          <>
+            {testFiles.length === 0 &&
+              translate('source_viewer.tooltip.no_information_about_tests')}
+            {testFiles.map(testFile => (
+              <div className="bubble-popup-section" key={testFile.file.key}>
+                <a
+                  data-key={testFile.file.key}
+                  href="#"
+                  onClick={this.handleTestClick}
+                  title={testFile.file.longName}>
+                  <span>{collapsePath(testFile.file.longName)}</span>
+                </a>
+                <ul className="bubble-popup-list">
+                  {testFile.tests.map(testCase => (
+                    <li
+                      className="component-viewer-popup-test"
+                      key={testCase.id}
+                      title={testCase.name}>
+                      <TestStatusIcon className="spacer-right" status={testCase.status} />
+                      {testCase.name}
+                      {testCase.status !== 'SKIPPED' && (
+                        <span className="spacer-left note">{testCase.durationInMs}ms</span>
+                      )}
+                    </li>
+                  ))}
+                </ul>
+              </div>
+            ))}
+          </>
+        )}
+      </BubblePopup>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/DuplicationPopup.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/DuplicationPopup.tsx
new file mode 100644 (file)
index 0000000..9c58a3e
--- /dev/null
@@ -0,0 +1,158 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { Link } from 'react-router';
+import { groupBy, sortBy } from 'lodash';
+import { SourceViewerFile, DuplicationBlock, DuplicatedFile } from '../../../app/types';
+import BubblePopup from '../../common/BubblePopup';
+import QualifierIcon from '../../shared/QualifierIcon';
+import { translate } from '../../../helpers/l10n';
+import { collapsedDirFromPath, fileFromPath } from '../../../helpers/path';
+import { getProjectUrl } from '../../../helpers/urls';
+
+interface Props {
+  blocks: DuplicationBlock[];
+  // TODO use branchLike
+  branch: string | undefined;
+  duplicatedFiles?: { [ref: string]: DuplicatedFile };
+  inRemovedComponent: boolean;
+  onClose: () => void;
+  popupPosition?: any;
+  sourceViewerFile: SourceViewerFile;
+}
+
+export default class DuplicationPopup extends React.PureComponent<Props> {
+  isDifferentComponent = (
+    a: { project: string; subProject?: string },
+    b: { project: string; subProject?: string }
+  ) => {
+    return Boolean(a && b && (a.project !== b.project || a.subProject !== b.subProject));
+  };
+
+  handleFileClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
+    event.preventDefault();
+    event.currentTarget.blur();
+    const Workspace = require('../../workspace/main').default;
+    const { key, line } = event.currentTarget.dataset;
+    Workspace.openComponent({ key, line, branch: this.props.branch });
+    this.props.onClose();
+  };
+
+  render() {
+    const { duplicatedFiles = {}, sourceViewerFile } = this.props;
+
+    const groupedBlocks = groupBy(this.props.blocks, '_ref');
+    let duplications = Object.keys(groupedBlocks).map(fileRef => {
+      return {
+        blocks: groupedBlocks[fileRef],
+        file: duplicatedFiles[fileRef]
+      };
+    });
+
+    // first duplications in the same file
+    // then duplications in the same sub-project
+    // then duplications in the same project
+    // then duplications in other projects
+    duplications = sortBy(
+      duplications,
+      d => d.file.projectName !== sourceViewerFile.projectName,
+      d => d.file.subProjectName !== sourceViewerFile.subProjectName,
+      d => d.file.key !== sourceViewerFile.key
+    );
+
+    return (
+      <BubblePopup customClass="source-viewer-bubble-popup" position={this.props.popupPosition}>
+        <div className="bubble-popup-container">
+          {this.props.inRemovedComponent && (
+            <div className="alert alert-warning">
+              {translate('duplications.dups_found_on_deleted_resource')}
+            </div>
+          )}
+          {duplications.length > 0 && (
+            <>
+              <div className="bubble-popup-title">
+                {translate('component_viewer.transition.duplication')}
+              </div>
+              {duplications.map(duplication => (
+                <div className="bubble-popup-section" key={duplication.file.key}>
+                  <div className="component-name">
+                    {this.isDifferentComponent(duplication.file, this.props.sourceViewerFile) && (
+                      <>
+                        <div className="component-name-parent">
+                          <QualifierIcon className="little-spacer-right" qualifier="TRK" />
+                          <Link to={getProjectUrl(duplication.file.project)}>
+                            {duplication.file.projectName}
+                          </Link>
+                        </div>
+                        {duplication.file.subProject &&
+                          duplication.file.subProjectName && (
+                            <div className="component-name-parent">
+                              <QualifierIcon className="little-spacer-right" qualifier="BRC" />
+                              <Link to={getProjectUrl(duplication.file.subProject)}>
+                                {duplication.file.subProjectName}
+                              </Link>
+                            </div>
+                          )}
+                      </>
+                    )}
+
+                    {duplication.file.key !== this.props.sourceViewerFile.key && (
+                      <div className="component-name-path">
+                        <a
+                          className="link-action"
+                          data-key={duplication.file.key}
+                          href="#"
+                          onClick={this.handleFileClick}
+                          title={duplication.file.name}>
+                          <span>{collapsedDirFromPath(duplication.file.name)}</span>
+                          <span className="component-name-file">
+                            {fileFromPath(duplication.file.name)}
+                          </span>
+                        </a>
+                      </div>
+                    )}
+
+                    <div className="component-name-path">
+                      {'Lines: '}
+                      {duplication.blocks.map((block, index) => (
+                        <React.Fragment key={index}>
+                          <a
+                            data-key={duplication.file.key}
+                            data-line={block.from}
+                            href="#"
+                            onClick={this.handleFileClick}>
+                            {block.from}
+                            {' – '}
+                            {block.from + block.size - 1}
+                          </a>
+                          {index < duplication.blocks.length - 1 && ', '}
+                        </React.Fragment>
+                      ))}
+                    </div>
+                  </div>
+                </div>
+              ))}
+            </>
+          )}
+        </div>
+      </BubblePopup>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/Line.js b/server/sonar-web/src/main/js/components/SourceViewer/components/Line.js
deleted file mode 100644 (file)
index ff74df4..0000000
+++ /dev/null
@@ -1,172 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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.
- */
-// @flow
-import React from 'react';
-import classNames from 'classnames';
-import { times } from 'lodash';
-import LineNumber from './LineNumber';
-import LineSCM from './LineSCM';
-import LineCoverage from './LineCoverage';
-import LineDuplications from './LineDuplications';
-import LineDuplicationBlock from './LineDuplicationBlock';
-import LineIssuesIndicator from './LineIssuesIndicator';
-import LineCode from './LineCode';
-/*:: import type { SourceLine } from '../types'; */
-/*:: import type { LinearIssueLocation } from '../helpers/indexing'; */
-/*:: import type { Issue } from '../../issue/types'; */
-
-/*::
-type Props = {|
-  branch?: string,
-  displayAllIssues: boolean,
-  displayCoverage: boolean,
-  displayDuplications: boolean,
-  displayIssues: boolean,
-  displayIssueLocationsCount?: boolean;
-  displayIssueLocationsLink?: boolean;
-  displayLocationMarkers?: boolean;
-  duplications: Array<number>,
-  duplicationsCount: number,
-  filtered: boolean | null,
-  highlighted: boolean,
-  highlightedLocationMessage?: { index: number, text: string },
-  highlightedSymbols?: Array<string>,
-  issueLocations: Array<LinearIssueLocation>,
-  issues: Array<Issue>,
-  last: boolean,
-  line: SourceLine,
-  loadDuplications: SourceLine => void,
-  onClick: (SourceLine, HTMLElement) => void,
-  onCoverageClick: (SourceLine, HTMLElement) => void,
-  onDuplicationClick: (number, number) => void,
-  onIssueChange: Issue => void,
-  onIssueSelect: string => void,
-  onIssueUnselect: () => void,
-  onIssuesOpen: SourceLine => void,
-  onIssuesClose: SourceLine => void,
-  onLocationSelect?: number => void,
-  onSCMClick: (SourceLine, HTMLElement) => void,
-  onSymbolClick: (Array<string>) => void,
-  openIssues: boolean,
-  onPopupToggle: (issue: string, popupName: string, open: ?boolean ) => void,
-  openPopup: ?{ issue: string, name: string},
-  previousLine?: SourceLine,
-  scroll?: HTMLElement => void,
-  secondaryIssueLocations: Array<{
-    from: number,
-    to: number,
-    line: number,
-    index: number,
-    startLine: number
-  }>,
-  selectedIssue: string | null
-|};
-*/
-
-export default class Line extends React.PureComponent {
-  /*:: props: Props; */
-
-  handleIssuesIndicatorClick = () => {
-    if (this.props.openIssues) {
-      this.props.onIssuesClose(this.props.line);
-      this.props.onIssueUnselect();
-    } else {
-      this.props.onIssuesOpen(this.props.line);
-
-      const { issues } = this.props;
-      if (issues.length > 0) {
-        this.props.onIssueSelect(issues[0].key);
-      }
-    }
-  };
-
-  render() {
-    const { line, duplications, displayCoverage, duplicationsCount, filtered } = this.props;
-    const className = classNames('source-line', {
-      'source-line-highlighted': this.props.highlighted,
-      'source-line-filtered': filtered === true,
-      'source-line-filtered-dark':
-        displayCoverage &&
-        (line.coverageStatus === 'uncovered' || line.coverageStatus === 'partially-covered'),
-      'source-line-last': this.props.last
-    });
-
-    return (
-      <tr className={className} data-line-number={line.line}>
-        <LineNumber line={line} onClick={this.props.onClick} />
-
-        <LineSCM
-          line={line}
-          onClick={this.props.onSCMClick}
-          previousLine={this.props.previousLine}
-        />
-
-        {this.props.displayCoverage && (
-          <LineCoverage line={line} onClick={this.props.onCoverageClick} />
-        )}
-
-        {this.props.displayDuplications && (
-          <LineDuplications line={line} onClick={this.props.loadDuplications} />
-        )}
-
-        {times(duplicationsCount).map(index => (
-          <LineDuplicationBlock
-            duplicated={duplications.includes(index)}
-            index={index}
-            key={index}
-            line={this.props.line}
-            onClick={this.props.onDuplicationClick}
-          />
-        ))}
-
-        {this.props.displayIssues &&
-          !this.props.displayAllIssues && (
-            <LineIssuesIndicator
-              issues={this.props.issues}
-              line={line}
-              onClick={this.handleIssuesIndicatorClick}
-            />
-          )}
-
-        <LineCode
-          branch={this.props.branch}
-          displayIssueLocationsCount={this.props.displayIssueLocationsCount}
-          displayIssueLocationsLink={this.props.displayIssueLocationsLink}
-          displayLocationMarkers={this.props.displayLocationMarkers}
-          highlightedLocationMessage={this.props.highlightedLocationMessage}
-          highlightedSymbols={this.props.highlightedSymbols}
-          issues={this.props.issues}
-          issueLocations={this.props.issueLocations}
-          line={line}
-          onIssueChange={this.props.onIssueChange}
-          onIssueSelect={this.props.onIssueSelect}
-          onLocationSelect={this.props.onLocationSelect}
-          onSymbolClick={this.props.onSymbolClick}
-          onPopupToggle={this.props.onPopupToggle}
-          openPopup={this.props.openPopup}
-          scroll={this.props.scroll}
-          secondaryIssueLocations={this.props.secondaryIssueLocations}
-          selectedIssue={this.props.selectedIssue}
-          showIssues={this.props.openIssues || this.props.displayAllIssues}
-        />
-      </tr>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/Line.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/Line.tsx
new file mode 100644 (file)
index 0000000..a292555
--- /dev/null
@@ -0,0 +1,197 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as classNames from 'classnames';
+import { times } from 'lodash';
+import LineNumber from './LineNumber';
+import LineSCM from './LineSCM';
+import LineCoverage from './LineCoverage';
+import LineDuplications from './LineDuplications';
+import LineDuplicationBlock from './LineDuplicationBlock';
+import LineIssuesIndicator from './LineIssuesIndicator';
+import LineCode from './LineCode';
+import { Issue, LinearIssueLocation, SourceLine } from '../../../app/types';
+
+interface Props {
+  branch: string | undefined;
+  componentKey: string;
+  displayAllIssues?: boolean;
+  displayCoverage: boolean;
+  displayDuplications: boolean;
+  displayIssueLocationsCount?: boolean;
+  displayIssueLocationsLink?: boolean;
+  displayIssues: boolean;
+  displayLocationMarkers?: boolean;
+  duplications: number[];
+  duplicationsCount: number;
+  filtered: boolean | undefined;
+  highlighted: boolean;
+  highlightedLocationMessage: { index: number; text: string } | undefined;
+  highlightedSymbols: string[] | undefined;
+  issueLocations: LinearIssueLocation[];
+  issuePopup: { issue: string; name: string } | undefined;
+  issues: Issue[];
+  last: boolean;
+  line: SourceLine;
+  linePopup: { index?: number; line: number; name: string } | undefined;
+  loadDuplications: (line: SourceLine) => void;
+  onLinePopupToggle: (x: { index?: number; line: number; name: string; open?: boolean }) => void;
+  onIssueChange: (issue: Issue) => void;
+  onIssuePopupToggle: (issueKey: string, popupName: string, open?: boolean) => void;
+  onIssuesClose: (line: SourceLine) => void;
+  onIssueSelect: (issueKey: string) => void;
+  onIssuesOpen: (line: SourceLine) => void;
+  onIssueUnselect: () => void;
+  onLocationSelect: ((x: number) => void) | undefined;
+  onSymbolClick: (symbols: string[]) => void;
+  openIssues: boolean;
+  previousLine: SourceLine | undefined;
+  renderDuplicationPopup: (index: number, line: number) => JSX.Element;
+  scroll?: (element: HTMLElement) => void;
+  secondaryIssueLocations: Array<{
+    from: number;
+    to: number;
+    line: number;
+    index: number;
+    startLine: number;
+  }>;
+  selectedIssue: string | undefined;
+}
+
+export default class Line extends React.PureComponent<Props> {
+  isPopupOpen = (name: string, index?: number) => {
+    const { line, linePopup } = this.props;
+    return (
+      linePopup !== undefined &&
+      linePopup.index === index &&
+      linePopup.line === line.line &&
+      linePopup.name === name
+    );
+  };
+
+  handleIssuesIndicatorClick = () => {
+    if (this.props.openIssues) {
+      this.props.onIssuesClose(this.props.line);
+      this.props.onIssueUnselect();
+    } else {
+      this.props.onIssuesOpen(this.props.line);
+
+      const { issues } = this.props;
+      if (issues.length > 0) {
+        this.props.onIssueSelect(issues[0].key);
+      }
+    }
+  };
+
+  render() {
+    const {
+      displayCoverage,
+      duplications,
+      duplicationsCount,
+      filtered,
+      issuePopup,
+      line
+    } = this.props;
+    const className = classNames('source-line', {
+      'source-line-highlighted': this.props.highlighted,
+      'source-line-filtered': filtered === true,
+      'source-line-filtered-dark':
+        displayCoverage &&
+        (line.coverageStatus === 'uncovered' || line.coverageStatus === 'partially-covered'),
+      'source-line-last': this.props.last
+    });
+
+    return (
+      <tr className={className} data-line-number={line.line}>
+        <LineNumber
+          branch={this.props.branch}
+          componentKey={this.props.componentKey}
+          line={line}
+          onPopupToggle={this.props.onLinePopupToggle}
+          popupOpen={this.isPopupOpen('line-number')}
+        />
+
+        <LineSCM
+          line={line}
+          onPopupToggle={this.props.onLinePopupToggle}
+          popupOpen={this.isPopupOpen('scm')}
+          previousLine={this.props.previousLine}
+        />
+
+        {this.props.displayCoverage && (
+          <LineCoverage
+            branch={this.props.branch}
+            componentKey={this.props.componentKey}
+            line={line}
+            onPopupToggle={this.props.onLinePopupToggle}
+            popupOpen={this.isPopupOpen('coverage')}
+          />
+        )}
+
+        {this.props.displayDuplications && (
+          <LineDuplications line={line} onClick={this.props.loadDuplications} />
+        )}
+
+        {times(duplicationsCount).map(index => (
+          <LineDuplicationBlock
+            duplicated={duplications.includes(index)}
+            index={index}
+            key={index}
+            line={this.props.line}
+            onPopupToggle={this.props.onLinePopupToggle}
+            popupOpen={this.isPopupOpen('duplications', index)}
+            renderDuplicationPopup={this.props.renderDuplicationPopup}
+          />
+        ))}
+
+        {this.props.displayIssues &&
+          !this.props.displayAllIssues && (
+            <LineIssuesIndicator
+              issues={this.props.issues}
+              line={line}
+              onClick={this.handleIssuesIndicatorClick}
+            />
+          )}
+
+        <LineCode
+          branch={this.props.branch}
+          displayIssueLocationsCount={this.props.displayIssueLocationsCount}
+          displayIssueLocationsLink={this.props.displayIssueLocationsLink}
+          displayLocationMarkers={this.props.displayLocationMarkers}
+          highlightedLocationMessage={this.props.highlightedLocationMessage}
+          highlightedSymbols={this.props.highlightedSymbols}
+          issueLocations={this.props.issueLocations}
+          issuePopup={issuePopup}
+          issues={this.props.issues}
+          line={line}
+          onIssueChange={this.props.onIssueChange}
+          onIssuePopupToggle={this.props.onIssuePopupToggle}
+          onIssueSelect={this.props.onIssueSelect}
+          onLocationSelect={this.props.onLocationSelect}
+          onSymbolClick={this.props.onSymbolClick}
+          scroll={this.props.scroll}
+          secondaryIssueLocations={this.props.secondaryIssueLocations}
+          selectedIssue={this.props.selectedIssue}
+          showIssues={this.props.openIssues || this.props.displayAllIssues}
+        />
+      </tr>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.js b/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.js
deleted file mode 100644 (file)
index fc197bc..0000000
+++ /dev/null
@@ -1,250 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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.
- */
-// @flow
-import React from 'react';
-import classNames from 'classnames';
-import LineIssuesList from './LineIssuesList';
-import LocationIndex from '../../common/LocationIndex';
-import LocationMessage from '../../common/LocationMessage';
-import { splitByTokens, highlightSymbol, highlightIssueLocations } from '../helpers/highlight';
-/*:: import type { Tokens } from '../helpers/highlight'; */
-/*:: import type { SourceLine } from '../types'; */
-/*:: import type { LinearIssueLocation } from '../helpers/indexing'; */
-/*:: import type { Issue } from '../../issue/types'; */
-
-/*::
-type Props = {|
-  branch?: string,
-  displayIssueLocationsCount?: boolean,
-  displayIssueLocationsLink?: boolean,
-  displayLocationMarkers?: boolean,
-  highlightedLocationMessage?: { index: number, text: string },
-  highlightedSymbols?: Array<string>,
-  issues: Array<Issue>,
-  issueLocations: Array<LinearIssueLocation>,
-  line: SourceLine,
-  onIssueChange: Issue => void,
-  onIssueSelect: (issueKey: string) => void,
-  onLocationSelect?: number => void,
-  onSymbolClick: (Array<string>) => void,
-  onPopupToggle: (issue: string, popupName: string, open: ?boolean ) => void,
-  openPopup: ?{ issue: string, name: string},
-  scroll?: HTMLElement => void,
-  secondaryIssueLocations: Array<{
-    from: number,
-    to: number,
-    line: number,
-    index: number,
-    startLine: number
-  }>,
-  selectedIssue: string | null,
-  showIssues: boolean
-|};
-*/
-
-/*::
-type State = {
-  tokens: Tokens
-};
-*/
-
-export default class LineCode extends React.PureComponent {
-  /*:: activeMarkerNode: ?HTMLElement; */
-  /*:: codeNode: HTMLElement; */
-  /*:: props: Props; */
-  /*:: state: State; */
-  /*:: symbols: NodeList<HTMLElement>; */
-
-  constructor(props /*: Props */) {
-    super(props);
-    this.state = {
-      tokens: splitByTokens(props.line.code || '')
-    };
-  }
-
-  componentDidMount() {
-    this.attachEvents();
-    if (this.props.highlightedLocationMessage && this.activeMarkerNode && this.props.scroll) {
-      this.props.scroll(this.activeMarkerNode);
-    }
-  }
-
-  componentWillReceiveProps(nextProps /*: Props */) {
-    if (nextProps.line.code !== this.props.line.code) {
-      this.setState({
-        tokens: splitByTokens(nextProps.line.code || '')
-      });
-    }
-  }
-
-  componentWillUpdate() {
-    this.detachEvents();
-  }
-
-  componentDidUpdate(prevProps /*: Props */) {
-    this.attachEvents();
-    if (
-      this.props.highlightedLocationMessage &&
-      prevProps.highlightedLocationMessage !== this.props.highlightedLocationMessage &&
-      this.activeMarkerNode &&
-      this.props.scroll
-    ) {
-      this.props.scroll(this.activeMarkerNode);
-    }
-  }
-
-  componentWillUnmount() {
-    this.detachEvents();
-  }
-
-  attachEvents() {
-    if (this.codeNode) {
-      this.symbols = this.codeNode.querySelectorAll('.sym');
-      for (const symbol of this.symbols) {
-        symbol.addEventListener('click', this.handleSymbolClick);
-      }
-    }
-  }
-
-  detachEvents() {
-    if (this.symbols) {
-      for (const symbol of this.symbols) {
-        symbol.removeEventListener('click', this.handleSymbolClick);
-      }
-    }
-  }
-
-  handleSymbolClick = (e /*: Object */) => {
-    e.preventDefault();
-    const keys = e.currentTarget.className.match(/sym-\d+/g);
-    if (keys.length > 0) {
-      this.props.onSymbolClick(keys);
-    }
-  };
-
-  renderMarker(index /*: number */, message /*: ?string */, leading /*: boolean */ = false) {
-    const { onLocationSelect } = this.props;
-    const onClick = onLocationSelect ? () => onLocationSelect(index) : undefined;
-    const ref = message != null ? node => (this.activeMarkerNode = node) : undefined;
-    return (
-      <LocationIndex
-        key={`marker-${index}`}
-        leading={leading}
-        onClick={onClick}
-        selected={message != null}>
-        <span href="#" ref={ref}>
-          {index + 1}
-        </span>
-        {message != null && <LocationMessage selected={true}>{message}</LocationMessage>}
-      </LocationIndex>
-    );
-  }
-
-  render() {
-    const {
-      highlightedLocationMessage,
-      highlightedSymbols,
-      issues,
-      issueLocations,
-      line,
-      onIssueSelect,
-      secondaryIssueLocations,
-      selectedIssue,
-      showIssues
-    } = this.props;
-
-    let tokens = [...this.state.tokens];
-
-    if (highlightedSymbols) {
-      highlightedSymbols.forEach(symbol => {
-        tokens = highlightSymbol(tokens, symbol);
-      });
-    }
-
-    if (issueLocations.length > 0) {
-      tokens = highlightIssueLocations(tokens, issueLocations);
-    }
-
-    if (secondaryIssueLocations) {
-      tokens = highlightIssueLocations(tokens, secondaryIssueLocations, 'issue-location');
-
-      if (highlightedLocationMessage) {
-        const location = secondaryIssueLocations.find(
-          location => location.index === highlightedLocationMessage.index
-        );
-        if (location) {
-          tokens = highlightIssueLocations(tokens, [location], 'selected');
-        }
-      }
-    }
-
-    const className = classNames('source-line-code', 'code', {
-      'has-issues': issues.length > 0
-    });
-
-    const renderedTokens = [];
-
-    // track if the first marker is displayed before the source code
-    // set `false` for the first token in a row
-    let leadingMarker = false;
-
-    tokens.forEach((token, index) => {
-      if (this.props.displayLocationMarkers && token.markers.length > 0) {
-        token.markers.forEach(marker => {
-          const message =
-            highlightedLocationMessage != null && highlightedLocationMessage.index === marker
-              ? highlightedLocationMessage.text
-              : null;
-          renderedTokens.push(this.renderMarker(marker, message, leadingMarker));
-        });
-      }
-      renderedTokens.push(
-        <span className={token.className} key={index}>
-          {token.text}
-        </span>
-      );
-
-      // keep leadingMarker truthy if previous token has only whitespaces
-      leadingMarker = (index === 0 ? true : leadingMarker) && !token.text.trim().length;
-    });
-
-    return (
-      <td className={className} data-line-number={line.line}>
-        <div className="source-line-code-inner">
-          <pre ref={node => (this.codeNode = node)}>{renderedTokens}</pre>
-        </div>
-        {showIssues &&
-          issues.length > 0 && (
-            <LineIssuesList
-              branch={this.props.branch}
-              displayIssueLocationsCount={this.props.displayIssueLocationsCount}
-              displayIssueLocationsLink={this.props.displayIssueLocationsLink}
-              issues={issues}
-              onIssueChange={this.props.onIssueChange}
-              onIssueClick={onIssueSelect}
-              onPopupToggle={this.props.onPopupToggle}
-              openPopup={this.props.openPopup}
-              selectedIssue={selectedIssue}
-            />
-          )}
-      </td>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.tsx
new file mode 100644 (file)
index 0000000..c91f58c
--- /dev/null
@@ -0,0 +1,248 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as classNames from 'classnames';
+import LineIssuesList from './LineIssuesList';
+import { Issue, LinearIssueLocation, SourceLine } from '../../../app/types';
+import LocationIndex from '../../common/LocationIndex';
+import LocationMessage from '../../common/LocationMessage';
+import {
+  highlightIssueLocations,
+  highlightSymbol,
+  splitByTokens,
+  Token
+} from '../helpers/highlight';
+
+interface Props {
+  branch: string | undefined;
+  displayIssueLocationsCount?: boolean;
+  displayIssueLocationsLink?: boolean;
+  displayLocationMarkers?: boolean;
+  highlightedLocationMessage: { index: number; text: string } | undefined;
+  highlightedSymbols: string[] | undefined;
+  issueLocations: LinearIssueLocation[];
+  issuePopup: { issue: string; name: string } | undefined;
+  issues: Issue[];
+  line: SourceLine;
+  onIssueChange: (issue: Issue) => void;
+  onIssuePopupToggle: (issue: string, popupName: string, open?: boolean) => void;
+  onIssueSelect: (issueKey: string) => void;
+  onLocationSelect: ((index: number) => void) | undefined;
+  onSymbolClick: (symbols: Array<string>) => void;
+  scroll?: (element: HTMLElement) => void;
+  secondaryIssueLocations: Array<{
+    from: number;
+    to: number;
+    line: number;
+    index: number;
+    startLine: number;
+  }>;
+  selectedIssue: string | undefined;
+  showIssues?: boolean;
+}
+
+interface State {
+  tokens: Token[];
+}
+
+export default class LineCode extends React.PureComponent<Props, State> {
+  activeMarkerNode?: HTMLElement | null;
+  codeNode?: HTMLElement | null;
+  symbols?: NodeListOf<HTMLElement>;
+
+  constructor(props: Props) {
+    super(props);
+    this.state = {
+      tokens: splitByTokens(props.line.code || '')
+    };
+  }
+
+  componentDidMount() {
+    this.attachEvents();
+    if (this.props.highlightedLocationMessage && this.activeMarkerNode && this.props.scroll) {
+      this.props.scroll(this.activeMarkerNode);
+    }
+  }
+
+  componentWillReceiveProps(nextProps: Props) {
+    if (nextProps.line.code !== this.props.line.code) {
+      this.setState({
+        tokens: splitByTokens(nextProps.line.code || '')
+      });
+    }
+  }
+
+  componentWillUpdate() {
+    this.detachEvents();
+  }
+
+  componentDidUpdate(prevProps: Props) {
+    this.attachEvents();
+    if (
+      this.props.highlightedLocationMessage &&
+      prevProps.highlightedLocationMessage !== this.props.highlightedLocationMessage &&
+      this.activeMarkerNode &&
+      this.props.scroll
+    ) {
+      this.props.scroll(this.activeMarkerNode);
+    }
+  }
+
+  componentWillUnmount() {
+    this.detachEvents();
+  }
+
+  attachEvents() {
+    if (this.codeNode) {
+      this.symbols = this.codeNode.querySelectorAll('.sym');
+      if (this.symbols) {
+        for (let i = 0; i < this.symbols.length; i++) {
+          const symbol = this.symbols[i];
+          symbol.addEventListener('click', this.handleSymbolClick);
+        }
+      }
+    }
+  }
+
+  detachEvents() {
+    if (this.symbols) {
+      for (let i = 0; i < this.symbols.length; i++) {
+        const symbol = this.symbols[i];
+        symbol.addEventListener('click', this.handleSymbolClick);
+      }
+    }
+  }
+
+  handleSymbolClick = (event: MouseEvent) => {
+    event.preventDefault();
+    const keys = (event.currentTarget as HTMLElement).className.match(/sym-\d+/g);
+    if (keys && keys.length > 0) {
+      this.props.onSymbolClick(keys);
+    }
+  };
+
+  renderMarker(index: number, message: string | undefined, leading = false) {
+    const { onLocationSelect } = this.props;
+    const onClick = onLocationSelect ? () => onLocationSelect(index) : undefined;
+    const ref =
+      message != null ? (node: HTMLElement | null) => (this.activeMarkerNode = node) : undefined;
+    return (
+      <LocationIndex
+        key={`marker-${index}`}
+        leading={leading}
+        onClick={onClick}
+        selected={message != null}>
+        <span ref={ref}>{index + 1}</span>
+        {message != null && <LocationMessage selected={true}>{message}</LocationMessage>}
+      </LocationIndex>
+    );
+  }
+
+  render() {
+    const {
+      highlightedLocationMessage,
+      highlightedSymbols,
+      issues,
+      issueLocations,
+      line,
+      onIssueSelect,
+      secondaryIssueLocations,
+      selectedIssue,
+      showIssues
+    } = this.props;
+
+    let tokens = [...this.state.tokens];
+
+    if (highlightedSymbols) {
+      highlightedSymbols.forEach(symbol => {
+        tokens = highlightSymbol(tokens, symbol);
+      });
+    }
+
+    if (issueLocations.length > 0) {
+      tokens = highlightIssueLocations(tokens, issueLocations);
+    }
+
+    if (secondaryIssueLocations) {
+      tokens = highlightIssueLocations(tokens, secondaryIssueLocations, 'issue-location');
+
+      if (highlightedLocationMessage) {
+        const location = secondaryIssueLocations.find(
+          location => location.index === highlightedLocationMessage.index
+        );
+        if (location) {
+          tokens = highlightIssueLocations(tokens, [location], 'selected');
+        }
+      }
+    }
+
+    const className = classNames('source-line-code', 'code', {
+      'has-issues': issues.length > 0
+    });
+
+    const renderedTokens: React.ReactNode[] = [];
+
+    // track if the first marker is displayed before the source code
+    // set `false` for the first token in a row
+    let leadingMarker = false;
+
+    tokens.forEach((token, index) => {
+      if (this.props.displayLocationMarkers && token.markers.length > 0) {
+        token.markers.forEach(marker => {
+          const message =
+            highlightedLocationMessage != null && highlightedLocationMessage.index === marker
+              ? highlightedLocationMessage.text
+              : undefined;
+          renderedTokens.push(this.renderMarker(marker, message, leadingMarker));
+        });
+      }
+      renderedTokens.push(
+        <span className={token.className} key={index}>
+          {token.text}
+        </span>
+      );
+
+      // keep leadingMarker truthy if previous token has only whitespaces
+      leadingMarker = (index === 0 ? true : leadingMarker) && !token.text.trim().length;
+    });
+
+    return (
+      <td className={className} data-line-number={line.line}>
+        <div className="source-line-code-inner">
+          <pre ref={node => (this.codeNode = node)}>{renderedTokens}</pre>
+        </div>
+        {showIssues &&
+          issues.length > 0 && (
+            <LineIssuesList
+              branch={this.props.branch}
+              displayIssueLocationsCount={this.props.displayIssueLocationsCount}
+              displayIssueLocationsLink={this.props.displayIssueLocationsLink}
+              issuePopup={this.props.issuePopup}
+              issues={issues}
+              onIssueChange={this.props.onIssueChange}
+              onIssueClick={onIssueSelect}
+              onIssuePopupToggle={this.props.onIssuePopupToggle}
+              selectedIssue={selectedIssue}
+            />
+          )}
+      </td>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.js b/server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.js
deleted file mode 100644 (file)
index 30fbf0d..0000000
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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.
- */
-// @flow
-import React from 'react';
-import Tooltip from '../../controls/Tooltip';
-import { translate } from '../../../helpers/l10n';
-/*:: import type { SourceLine } from '../types'; */
-
-/*::
-type Props = {
-  line: SourceLine,
-  onClick: (SourceLine, HTMLElement) => void
-};
-*/
-
-export default class LineCoverage extends React.PureComponent {
-  /*:: props: Props; */
-
-  handleClick = (e /*: SyntheticInputEvent */) => {
-    e.preventDefault();
-    this.props.onClick(this.props.line, e.target);
-  };
-
-  render() {
-    const { line } = this.props;
-    const className =
-      'source-meta source-line-coverage' +
-      (line.coverageStatus != null ? ` source-line-${line.coverageStatus}` : '');
-    const hasPopup =
-      line.coverageStatus === 'covered' || line.coverageStatus === 'partially-covered';
-    const cell = (
-      <td
-        className={className}
-        data-line-number={line.line}
-        role={hasPopup ? 'button' : undefined}
-        tabIndex={hasPopup ? 0 : undefined}
-        onClick={hasPopup ? this.handleClick : undefined}>
-        <div className="source-line-bar" />
-      </td>
-    );
-
-    return line.coverageStatus != null ? (
-      <Tooltip overlay={translate('source_viewer.tooltip', line.coverageStatus)} placement="right">
-        {cell}
-      </Tooltip>
-    ) : (
-      cell
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.tsx
new file mode 100644 (file)
index 0000000..c6b9efb
--- /dev/null
@@ -0,0 +1,102 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 CoveragePopup from './CoveragePopup';
+import { SourceLine } from '../../../app/types';
+import Tooltip from '../../controls/Tooltip';
+import { translate } from '../../../helpers/l10n';
+import BubblePopupHelper from '../../common/BubblePopupHelper';
+
+interface Props {
+  branch: string | undefined;
+  componentKey: string;
+  line: SourceLine;
+  onPopupToggle: (x: { index?: number; line: number; name: string; open?: boolean }) => void;
+  popupOpen: boolean;
+}
+
+export default class LineCoverage extends React.PureComponent<Props> {
+  handleClick = (event: React.MouseEvent<HTMLElement>) => {
+    event.preventDefault();
+    event.stopPropagation();
+    event.currentTarget.blur();
+    this.props.onPopupToggle({ line: this.props.line.line, name: 'coverage' });
+  };
+
+  handleTogglePopup = (open: boolean) => {
+    this.props.onPopupToggle({ line: this.props.line.line, name: 'coverage', open });
+  };
+
+  closePopup = () => {
+    this.props.onPopupToggle({ line: this.props.line.line, name: 'coverage', open: false });
+  };
+
+  render() {
+    const { branch, componentKey, line, popupOpen } = this.props;
+
+    const className =
+      'source-meta source-line-coverage' +
+      (line.coverageStatus != null ? ` source-line-${line.coverageStatus}` : '');
+
+    const hasPopup =
+      line.coverageStatus === 'covered' || line.coverageStatus === 'partially-covered';
+
+    const cell = line.coverageStatus ? (
+      <Tooltip overlay={translate('source_viewer.tooltip', line.coverageStatus)} placement="right">
+        <div className="source-line-bar" />
+      </Tooltip>
+    ) : (
+      <div className="source-line-bar" />
+    );
+
+    if (hasPopup) {
+      return (
+        <td
+          className={className}
+          data-line-number={line.line}
+          onClick={this.handleClick}
+          // eslint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role
+          role="button"
+          tabIndex={0}>
+          {cell}
+          <BubblePopupHelper
+            isOpen={popupOpen}
+            popup={
+              <CoveragePopup
+                branch={branch}
+                componentKey={componentKey}
+                line={line}
+                onClose={this.closePopup}
+              />
+            }
+            position="bottomright"
+            togglePopup={this.handleTogglePopup}
+          />
+        </td>
+      );
+    }
+
+    return (
+      <td className={className} data-line-number={line.line}>
+        {cell}
+      </td>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.js b/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.js
deleted file mode 100644 (file)
index 51c83c9..0000000
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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.
- */
-// @flow
-import React from 'react';
-import classNames from 'classnames';
-import Tooltip from '../../controls/Tooltip';
-import { translate } from '../../../helpers/l10n';
-/*:: import type { SourceLine } from '../types'; */
-
-/*::
-type Props = {
-  duplicated: boolean,
-  index: number,
-  line: SourceLine,
-  onClick: (index: number, lineNumber: number) => void
-};
-*/
-
-export default class LineDuplicationBlock extends React.PureComponent {
-  /*:: props: Props; */
-
-  handleClick = (e /*: SyntheticInputEvent */) => {
-    e.preventDefault();
-    this.props.onClick(this.props.index, this.props.line.line);
-  };
-
-  render() {
-    const { duplicated, index, line } = this.props;
-    const className = classNames('source-meta', 'source-line-duplications-extra', {
-      'source-line-duplicated': duplicated
-    });
-
-    const cell = (
-      <td
-        key={index}
-        className={className}
-        data-line-number={line.line}
-        data-index={index}
-        role={duplicated ? 'button' : undefined}
-        tabIndex={duplicated ? '0' : undefined}
-        onClick={duplicated ? this.handleClick : undefined}>
-        <div className="source-line-bar" />
-      </td>
-    );
-
-    return duplicated ? (
-      <Tooltip overlay={translate('source_viewer.tooltip.duplicated_block')} placement="right">
-        {cell}
-      </Tooltip>
-    ) : (
-      cell
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.tsx
new file mode 100644 (file)
index 0000000..0121bfa
--- /dev/null
@@ -0,0 +1,90 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as classNames from 'classnames';
+import { SourceLine } from '../../../app/types';
+import Tooltip from '../../controls/Tooltip';
+import { translate } from '../../../helpers/l10n';
+import BubblePopupHelper from '../../common/BubblePopupHelper';
+
+interface Props {
+  duplicated: boolean;
+  index: number;
+  line: SourceLine;
+  onPopupToggle: (x: { index?: number; line: number; name: string; open?: boolean }) => void;
+  popupOpen: boolean;
+  renderDuplicationPopup: (index: number, line: number) => JSX.Element;
+}
+
+export default class LineDuplicationBlock extends React.PureComponent<Props> {
+  handleClick = (event: React.MouseEvent<HTMLElement>) => {
+    event.preventDefault();
+    event.stopPropagation();
+    event.currentTarget.blur();
+    this.props.onPopupToggle({
+      index: this.props.index,
+      line: this.props.line.line,
+      name: 'duplications'
+    });
+  };
+
+  handleTogglePopup = (open: boolean) => {
+    this.props.onPopupToggle({
+      index: this.props.index,
+      line: this.props.line.line,
+      name: 'duplications',
+      open
+    });
+  };
+
+  render() {
+    const { duplicated, index, line, popupOpen } = this.props;
+    const className = classNames('source-meta', 'source-line-duplications-extra', {
+      'source-line-duplicated': duplicated
+    });
+
+    const cell = <div className="source-line-bar" />;
+
+    return duplicated ? (
+      <td
+        className={className}
+        data-index={index}
+        data-line-number={line.line}
+        onClick={this.handleClick}
+        // eslint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role
+        role="button"
+        tabIndex={0}>
+        <Tooltip overlay={translate('source_viewer.tooltip.duplicated_block')} placement="right">
+          {cell}
+        </Tooltip>
+        <BubblePopupHelper
+          isOpen={popupOpen}
+          popup={this.props.renderDuplicationPopup(index, line.line)}
+          position="bottomright"
+          togglePopup={this.handleTogglePopup}
+        />
+      </td>
+    ) : (
+      <td className={className} data-index={index} data-line-number={line.line}>
+        {cell}
+      </td>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplications.js b/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplications.js
deleted file mode 100644 (file)
index 7e463ea..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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.
- */
-// @flow
-import React from 'react';
-import classNames from 'classnames';
-import Tooltip from '../../controls/Tooltip';
-import { translate } from '../../../helpers/l10n';
-/*:: import type { SourceLine } from '../types'; */
-
-/*::
-type Props = {
-  line: SourceLine,
-  onClick: SourceLine => void
-};
-*/
-
-export default class LineDuplications extends React.PureComponent {
-  /*:: props: Props; */
-
-  handleClick = (e /*: SyntheticInputEvent */) => {
-    e.preventDefault();
-    this.props.onClick(this.props.line);
-  };
-
-  render() {
-    const { line } = this.props;
-    const className = classNames('source-meta', 'source-line-duplications', {
-      'source-line-duplicated': line.duplicated
-    });
-
-    const cell = (
-      <td
-        className={className}
-        role={line.duplicated ? 'button' : undefined}
-        tabIndex={line.duplicated ? 0 : undefined}
-        onClick={line.duplicated ? this.handleClick : undefined}>
-        <div className="source-line-bar" />
-      </td>
-    );
-
-    return line.duplicated ? (
-      <Tooltip overlay={translate('source_viewer.tooltip.duplicated_line')} placement="right">
-        {cell}
-      </Tooltip>
-    ) : (
-      cell
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplications.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplications.tsx
new file mode 100644 (file)
index 0000000..c51c49d
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as classNames from 'classnames';
+import { SourceLine } from '../../../app/types';
+import Tooltip from '../../controls/Tooltip';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+  line: SourceLine;
+  onClick: (line: SourceLine) => void;
+}
+
+export default class LineDuplications extends React.PureComponent<Props> {
+  handleClick = (event: React.MouseEvent<HTMLElement>) => {
+    event.preventDefault();
+    this.props.onClick(this.props.line);
+  };
+
+  render() {
+    const { line } = this.props;
+    const className = classNames('source-meta', 'source-line-duplications', {
+      'source-line-duplicated': line.duplicated
+    });
+
+    const cell = (
+      <td
+        className={className}
+        onClick={line.duplicated ? this.handleClick : undefined}
+        role={line.duplicated ? 'button' : undefined}
+        tabIndex={line.duplicated ? 0 : undefined}>
+        <div className="source-line-bar" />
+      </td>
+    );
+
+    return line.duplicated ? (
+      <Tooltip overlay={translate('source_viewer.tooltip.duplicated_line')} placement="right">
+        {cell}
+      </Tooltip>
+    ) : (
+      cell
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicator.js b/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicator.js
deleted file mode 100644 (file)
index 2a7159b..0000000
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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.
- */
-// @flow
-import React from 'react';
-import classNames from 'classnames';
-import SeverityIcon from '../../shared/SeverityIcon';
-import { sortBySeverity } from '../../../helpers/issues';
-/*:: import type { SourceLine } from '../types'; */
-/*:: import type { Issue } from '../../issue/types'; */
-
-/*::
-type Props = {
-  issues: Array<Issue>,
-  line: SourceLine,
-  onClick: () => void
-};
-*/
-
-export default class LineIssuesIndicator extends React.PureComponent {
-  /*:: props: Props; */
-
-  handleClick = (e /*: SyntheticInputEvent */) => {
-    e.preventDefault();
-    this.props.onClick();
-  };
-
-  render() {
-    const { issues, line } = this.props;
-    const hasIssues = issues.length > 0;
-    const className = classNames('source-meta', 'source-line-issues', {
-      'source-line-with-issues': hasIssues
-    });
-    const mostImportantIssue = hasIssues ? sortBySeverity(issues)[0] : null;
-
-    return (
-      <td
-        className={className}
-        data-line-number={line.line}
-        role={hasIssues ? 'button' : undefined}
-        tabIndex={hasIssues ? '0' : undefined}
-        onClick={hasIssues ? this.handleClick : undefined}>
-        {mostImportantIssue != null && <SeverityIcon severity={mostImportantIssue.severity} />}
-        {issues.length > 1 && <span className="source-line-issues-counter">{issues.length}</span>}
-      </td>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicator.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicator.tsx
new file mode 100644 (file)
index 0000000..e5434c0
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as classNames from 'classnames';
+import SeverityIcon from '../../shared/SeverityIcon';
+import { sortBySeverity } from '../../../helpers/issues';
+import { Issue, SourceLine } from '../../../app/types';
+
+interface Props {
+  issues: Issue[];
+  line: SourceLine;
+  onClick: () => void;
+}
+
+export default class LineIssuesIndicator extends React.PureComponent<Props> {
+  handleClick = (event: React.MouseEvent<HTMLElement>) => {
+    event.preventDefault();
+    this.props.onClick();
+  };
+
+  render() {
+    const { issues, line } = this.props;
+    const hasIssues = issues.length > 0;
+    const className = classNames('source-meta', 'source-line-issues', {
+      'source-line-with-issues': hasIssues
+    });
+    const mostImportantIssue = hasIssues ? sortBySeverity(issues)[0] : null;
+
+    return (
+      <td
+        className={className}
+        data-line-number={line.line}
+        onClick={hasIssues ? this.handleClick : undefined}
+        role={hasIssues ? 'button' : undefined}
+        tabIndex={hasIssues ? 0 : undefined}>
+        {mostImportantIssue != null && <SeverityIcon severity={mostImportantIssue.severity} />}
+        {issues.length > 1 && <span className="source-line-issues-counter">{issues.length}</span>}
+      </td>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesList.js b/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesList.js
deleted file mode 100644 (file)
index 66ef758..0000000
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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.
- */
-// @flow
-import React from 'react';
-import Issue from '../../issue/Issue';
-/*:: import type { Issue as IssueType } from '../../issue/types'; */
-
-/*::
-type Props = {
-  branch?: string,
-  displayIssueLocationsCount?: boolean;
-  displayIssueLocationsLink?: boolean;
-  issues: Array<IssueType>,
-  onIssueChange: IssueType => void,
-  onIssueClick: (issueKey: string) => void,
-  onPopupToggle: (issue: string, popupName: string, open: ?boolean ) => void,
-  openPopup: ?{ issue: string, name: string},
-  selectedIssue: string | null
-};
-*/
-
-export default class LineIssuesList extends React.PureComponent {
-  /*:: props: Props; */
-
-  render() {
-    const { branch, issues, onIssueClick, openPopup, selectedIssue } = this.props;
-
-    return (
-      <div className="issue-list">
-        {issues.map(issue => (
-          <Issue
-            branch={branch}
-            displayLocationsCount={this.props.displayIssueLocationsCount}
-            displayLocationsLink={this.props.displayIssueLocationsLink}
-            issue={issue}
-            key={issue.key}
-            onChange={this.props.onIssueChange}
-            onClick={onIssueClick}
-            onPopupToggle={this.props.onPopupToggle}
-            openPopup={openPopup && openPopup.issue === issue.key ? openPopup.name : null}
-            selected={selectedIssue === issue.key}
-          />
-        ))}
-      </div>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesList.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesList.tsx
new file mode 100644 (file)
index 0000000..231cc63
--- /dev/null
@@ -0,0 +1,57 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { Issue as IssueType } from '../../../app/types';
+import Issue from '../../issue/Issue';
+
+interface Props {
+  branch: string | undefined;
+  displayIssueLocationsCount?: boolean;
+  displayIssueLocationsLink?: boolean;
+  issuePopup: { issue: string; name: string } | undefined;
+  issues: IssueType[];
+  onIssueChange: (issue: IssueType) => void;
+  onIssueClick: (issueKey: string) => void;
+  onIssuePopupToggle: (issue: string, popupName: string, open?: boolean) => void;
+  selectedIssue: string | undefined;
+}
+
+export default function LineIssuesList(props: Props) {
+  const { issuePopup } = props;
+
+  return (
+    <div className="issue-list">
+      {props.issues.map(issue => (
+        <Issue
+          branch={props.branch}
+          displayLocationsCount={props.displayIssueLocationsCount}
+          displayLocationsLink={props.displayIssueLocationsLink}
+          issue={issue}
+          key={issue.key}
+          onChange={props.onIssueChange}
+          onClick={props.onIssueClick}
+          onPopupToggle={props.onIssuePopupToggle}
+          openPopup={issuePopup && issuePopup.issue === issue.key ? issuePopup.name : undefined}
+          selected={props.selectedIssue === issue.key}
+        />
+      ))}
+    </div>
+  );
+}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineNumber.js b/server/sonar-web/src/main/js/components/SourceViewer/components/LineNumber.js
deleted file mode 100644 (file)
index 801830c..0000000
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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.
- */
-// @flow
-import React from 'react';
-/*:: import type { SourceLine } from '../types'; */
-
-/*::
-type Props = {
-  line: SourceLine,
-  onClick: (SourceLine, HTMLElement) => void
-};
-*/
-
-export default class LineNumber extends React.PureComponent {
-  /*:: props: Props; */
-
-  handleClick = (e /*: SyntheticInputEvent */) => {
-    e.preventDefault();
-    this.props.onClick(this.props.line, e.target);
-  };
-
-  render() {
-    const { line } = this.props.line;
-
-    return (
-      <td
-        className="source-meta source-line-number"
-        /* don't display 0 */
-        data-line-number={line ? line : undefined}
-        role={line ? 'button' : undefined}
-        tabIndex={line ? 0 : undefined}
-        onClick={line ? this.handleClick : undefined}
-      />
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineNumber.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineNumber.tsx
new file mode 100644 (file)
index 0000000..f6126f5
--- /dev/null
@@ -0,0 +1,69 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 LineOptionsPopup from './LineOptionsPopup';
+import { SourceLine } from '../../../app/types';
+import BubblePopupHelper from '../../common/BubblePopupHelper';
+
+interface Props {
+  // TODO use branchLike
+  branch: string | undefined;
+  componentKey: string;
+  line: SourceLine;
+  onPopupToggle: (x: { index?: number; line: number; name: string; open?: boolean }) => void;
+  popupOpen: boolean;
+}
+
+export default class LineNumber extends React.PureComponent<Props> {
+  handleClick = (event: React.MouseEvent<HTMLElement>) => {
+    event.preventDefault();
+    event.stopPropagation();
+    event.currentTarget.blur();
+    this.props.onPopupToggle({ line: this.props.line.line, name: 'line-number' });
+  };
+
+  handleTogglePopup = (open: boolean) => {
+    this.props.onPopupToggle({ line: this.props.line.line, name: 'line-number', open });
+  };
+
+  render() {
+    const { branch, componentKey, line, popupOpen } = this.props;
+    const { line: lineNumber } = line;
+    const hasLineNumber = !!lineNumber;
+    return hasLineNumber ? (
+      <td
+        className="source-meta source-line-number"
+        data-line-number={lineNumber}
+        onClick={this.handleClick}
+        // eslint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role
+        role="button"
+        tabIndex={0}>
+        <BubblePopupHelper
+          isOpen={popupOpen}
+          popup={<LineOptionsPopup branch={branch} componentKey={componentKey} line={line} />}
+          position="bottomright"
+          togglePopup={this.handleTogglePopup}
+        />
+      </td>
+    ) : (
+      <td className="source-meta source-line-number" />
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineOptionsPopup.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineOptionsPopup.tsx
new file mode 100644 (file)
index 0000000..db4634b
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { Link } from 'react-router';
+import { SourceLine } from '../../../app/types';
+import BubblePopup from '../../common/BubblePopup';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+  // TODO use branchLike
+  branch: string | undefined;
+  componentKey: string;
+  line: SourceLine;
+  popupPosition?: any;
+}
+
+export default function LineOptionsPopup({ branch, componentKey, line, popupPosition }: Props) {
+  const permalink = {
+    pathname: '/component',
+    query: { branch, id: componentKey, line: line.line }
+  };
+  return (
+    <BubblePopup customClass="source-viewer-bubble-popup" position={popupPosition}>
+      <div className="bubble-popup-section">
+        <Link className="js-get-permalink" to={permalink}>
+          {translate('component_viewer.get_permalink')}
+        </Link>
+      </div>
+    </BubblePopup>
+  );
+}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.js b/server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.js
deleted file mode 100644 (file)
index 274add2..0000000
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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.
- */
-// @flow
-import React from 'react';
-/*:: import type { SourceLine } from '../types'; */
-
-/*::
-type Props = {
-  line: SourceLine,
-  previousLine?: SourceLine,
-  onClick: (SourceLine, HTMLElement) => void
-};
-*/
-
-export default class LineSCM extends React.PureComponent {
-  /*:: props: Props; */
-
-  handleClick = (e /*: SyntheticInputEvent */) => {
-    e.preventDefault();
-    this.props.onClick(this.props.line, e.target);
-  };
-
-  isSCMChanged(s /*: SourceLine */, p /*: ?SourceLine */) {
-    let changed = true;
-    if (p != null && s.scmAuthor != null && p.scmAuthor != null) {
-      changed = s.scmAuthor !== p.scmAuthor || s.scmDate !== p.scmDate;
-    }
-    return changed;
-  }
-
-  render() {
-    const { line, previousLine } = this.props;
-    const clickable = !!line.line;
-    return (
-      <td
-        className="source-meta source-line-scm"
-        data-line-number={line.line}
-        role={clickable ? 'button' : undefined}
-        tabIndex={clickable ? 0 : undefined}
-        onClick={clickable ? this.handleClick : undefined}>
-        {this.isSCMChanged(line, previousLine) && (
-          <div className="source-line-scm-inner" data-author={line.scmAuthor} />
-        )}
-      </td>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.tsx
new file mode 100644 (file)
index 0000000..c1de61e
--- /dev/null
@@ -0,0 +1,80 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 SCMPopup from './SCMPopup';
+import { SourceLine } from '../../../app/types';
+import BubblePopupHelper from '../../common/BubblePopupHelper';
+
+interface Props {
+  line: SourceLine;
+  onPopupToggle: (x: { index?: number; line: number; name: string; open?: boolean }) => void;
+  popupOpen: boolean;
+  previousLine: SourceLine | undefined;
+}
+
+export default class LineSCM extends React.PureComponent<Props> {
+  handleClick = (event: React.MouseEvent<HTMLElement>) => {
+    event.preventDefault();
+    event.stopPropagation();
+    event.currentTarget.blur();
+    this.props.onPopupToggle({ line: this.props.line.line, name: 'scm' });
+  };
+
+  handleTogglePopup = (open: boolean) => {
+    this.props.onPopupToggle({ line: this.props.line.line, name: 'scm', open });
+  };
+
+  render() {
+    const { line, popupOpen, previousLine } = this.props;
+    const hasPopup = !!line.line;
+    const cell = isSCMChanged(line, previousLine) && (
+      <div className="source-line-scm-inner" data-author={line.scmAuthor} />
+    );
+    return hasPopup ? (
+      <td
+        className="source-meta source-line-scm"
+        data-line-number={line.line}
+        onClick={this.handleClick}
+        // eslint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role
+        role="button"
+        tabIndex={0}>
+        {cell}
+        <BubblePopupHelper
+          isOpen={popupOpen}
+          popup={<SCMPopup line={line} />}
+          position="bottomright"
+          togglePopup={this.handleTogglePopup}
+        />
+      </td>
+    ) : (
+      <td className="source-meta source-line-scm" data-line-number={line.line}>
+        {cell}
+      </td>
+    );
+  }
+}
+
+function isSCMChanged(s: SourceLine, p: SourceLine | undefined) {
+  let changed = true;
+  if (p != null && s.scmAuthor != null && p.scmAuthor != null) {
+    changed = s.scmAuthor !== p.scmAuthor || s.scmDate !== p.scmDate;
+  }
+  return changed;
+}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/SCMPopup.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/SCMPopup.tsx
new file mode 100644 (file)
index 0000000..1a94d48
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { SourceLine } from '../../../app/types';
+import BubblePopup from '../../common/BubblePopup';
+import DateFormatter from '../../intl/DateFormatter';
+
+interface Props {
+  line: SourceLine;
+  popupPosition?: any;
+}
+
+export default function SCMPopup({ line, popupPosition }: Props) {
+  return (
+    <BubblePopup customClass="source-viewer-bubble-popup" position={popupPosition}>
+      <div className="bubble-popup-section">{line.scmAuthor}</div>
+      {line.scmDate && (
+        <div className="bubble-popup-section">
+          <DateFormatter date={line.scmDate} />
+        </div>
+      )}
+      {line.scmRevision && <div className="bubble-popup-section">{line.scmRevision}</div>}
+    </BubblePopup>
+  );
+}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCode-test.js b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCode-test.js
deleted file mode 100644 (file)
index 3dc8487..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 React from 'react';
-import { shallow } from 'enzyme';
-import LineCode from '../LineCode';
-
-it('render code', () => {
-  const line = {
-    line: 3,
-    code: '<span class="k">class</span> <span class="sym sym-1">Foo</span> {'
-  };
-  const issueLocations = [{ from: 0, to: 5, line: 3 }];
-  const wrapper = shallow(
-    <LineCode
-      highlightedSymbols={['sym1']}
-      issues={[{ key: 'issue-1' }, { key: 'issue-2' }]}
-      issueLocations={issueLocations}
-      line={line}
-      onIssueSelect={jest.fn()}
-      onSelectLocation={jest.fn()}
-      onSymbolClick={jest.fn()}
-      onPopupToggle={jest.fn()}
-      openPopup={null}
-      selectedIssue="issue-1"
-      showIssues={true}
-    />
-  );
-  expect(wrapper).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCode-test.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCode-test.tsx
new file mode 100644 (file)
index 0000000..a9127ea
--- /dev/null
@@ -0,0 +1,73 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 LineCode from '../LineCode';
+import { Issue } from '../../../../app/types';
+
+const issueBase: Issue = {
+  component: '',
+  componentLongName: '',
+  componentQualifier: '',
+  componentUuid: '',
+  creationDate: '',
+  key: '',
+  flows: [],
+  message: '',
+  organization: '',
+  project: '',
+  projectName: '',
+  projectOrganization: '',
+  projectUuid: '',
+  rule: '',
+  ruleName: '',
+  secondaryLocations: [],
+  severity: '',
+  status: '',
+  type: ''
+};
+
+it('render code', () => {
+  const line = {
+    line: 3,
+    code: '<span class="k">class</span> <span class="sym sym-1">Foo</span> {'
+  };
+  const issueLocations = [{ from: 0, to: 5, line: 3 }];
+  const wrapper = shallow(
+    <LineCode
+      branch="feature"
+      highlightedLocationMessage={undefined}
+      highlightedSymbols={['sym1']}
+      issueLocations={issueLocations}
+      issuePopup={undefined}
+      issues={[{ ...issueBase, key: 'issue-1' }, { ...issueBase, key: 'issue-2' }]}
+      line={line}
+      onIssueChange={jest.fn()}
+      onIssuePopupToggle={jest.fn()}
+      onIssueSelect={jest.fn()}
+      onLocationSelect={jest.fn()}
+      onSymbolClick={jest.fn()}
+      secondaryIssueLocations={[]}
+      selectedIssue="issue-1"
+      showIssues={true}
+    />
+  );
+  expect(wrapper).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCoverage-test.js b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCoverage-test.js
deleted file mode 100644 (file)
index 35cac16..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 React from 'react';
-import { shallow } from 'enzyme';
-import { click } from '../../../../helpers/testUtils';
-import LineCoverage from '../LineCoverage';
-
-it('render covered line', () => {
-  const line = { line: 3, coverageStatus: 'covered' };
-  const onClick = jest.fn();
-  const wrapper = shallow(<LineCoverage line={line} onClick={onClick} />);
-  expect(wrapper).toMatchSnapshot();
-  click(wrapper.find('[tabIndex]'));
-  expect(onClick).toHaveBeenCalled();
-});
-
-it('render uncovered line', () => {
-  const line = { line: 3, coverageStatus: 'uncovered' };
-  const onClick = jest.fn();
-  const wrapper = shallow(<LineCoverage line={line} onClick={onClick} />);
-  expect(wrapper).toMatchSnapshot();
-});
-
-it('render line with unknown coverage', () => {
-  const line = { line: 3 };
-  const onClick = jest.fn();
-  const wrapper = shallow(<LineCoverage line={line} onClick={onClick} />);
-  expect(wrapper).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCoverage-test.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCoverage-test.tsx
new file mode 100644 (file)
index 0000000..a737504
--- /dev/null
@@ -0,0 +1,83 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 LineCoverage from '../LineCoverage';
+import { click } from '../../../../helpers/testUtils';
+import { SourceLine } from '../../../../app/types';
+
+it('render covered line', () => {
+  const line: SourceLine = { line: 3, coverageStatus: 'covered' };
+  const wrapper = shallow(
+    <LineCoverage
+      branch={undefined}
+      componentKey="foo"
+      line={line}
+      onPopupToggle={jest.fn()}
+      popupOpen={false}
+    />
+  );
+  expect(wrapper).toMatchSnapshot();
+  click(wrapper.find('[tabIndex]'));
+});
+
+it('render uncovered line', () => {
+  const line: SourceLine = { line: 3, coverageStatus: 'uncovered' };
+  const wrapper = shallow(
+    <LineCoverage
+      branch={undefined}
+      componentKey="foo"
+      line={line}
+      onPopupToggle={jest.fn()}
+      popupOpen={false}
+    />
+  );
+  expect(wrapper).toMatchSnapshot();
+});
+
+it('render line with unknown coverage', () => {
+  const line: SourceLine = { line: 3 };
+  const wrapper = shallow(
+    <LineCoverage
+      branch={undefined}
+      componentKey="foo"
+      line={line}
+      onPopupToggle={jest.fn()}
+      popupOpen={false}
+    />
+  );
+  expect(wrapper).toMatchSnapshot();
+});
+
+it('should open coverage popup', () => {
+  const line: SourceLine = { line: 3, coverageStatus: 'covered' };
+  const onPopupToggle = jest.fn();
+  const wrapper = shallow(
+    <LineCoverage
+      branch={undefined}
+      componentKey="foo"
+      line={line}
+      onPopupToggle={onPopupToggle}
+      popupOpen={false}
+    />
+  );
+  click(wrapper.find('[role="button"]'));
+  expect(onPopupToggle).toBeCalledWith({ line: 3, name: 'coverage' });
+});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplicationBlock-test.js b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplicationBlock-test.js
deleted file mode 100644 (file)
index cc2d147..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 React from 'react';
-import { shallow } from 'enzyme';
-import { click } from '../../../../helpers/testUtils';
-import LineDuplicationBlock from '../LineDuplicationBlock';
-
-it('render duplicated line', () => {
-  const line = { line: 3, duplicated: true };
-  const onClick = jest.fn();
-  const wrapper = shallow(
-    <LineDuplicationBlock index={1} duplicated={true} line={line} onClick={onClick} />
-  );
-  expect(wrapper).toMatchSnapshot();
-  click(wrapper.find('[tabIndex]'));
-  expect(onClick).toHaveBeenCalled();
-});
-
-it('render not duplicated line', () => {
-  const line = { line: 3, duplicated: false };
-  const onClick = jest.fn();
-  const wrapper = shallow(
-    <LineDuplicationBlock index={1} duplicated={false} line={line} onClick={onClick} />
-  );
-  expect(wrapper).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplicationBlock-test.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplicationBlock-test.tsx
new file mode 100644 (file)
index 0000000..0854c4c
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { click } from '../../../../helpers/testUtils';
+import LineDuplicationBlock from '../LineDuplicationBlock';
+
+it('render duplicated line', () => {
+  const line = { line: 3, duplicated: true };
+  const onPopupToggle = jest.fn();
+  const wrapper = shallow(
+    <LineDuplicationBlock
+      duplicated={true}
+      index={1}
+      line={line}
+      onPopupToggle={onPopupToggle}
+      popupOpen={false}
+      renderDuplicationPopup={jest.fn()}
+    />
+  );
+  expect(wrapper).toMatchSnapshot();
+  click(wrapper.find('[tabIndex]'));
+  expect(onPopupToggle).toHaveBeenCalled();
+});
+
+it('render not duplicated line', () => {
+  const line = { line: 3, duplicated: false };
+  const wrapper = shallow(
+    <LineDuplicationBlock
+      duplicated={false}
+      index={1}
+      line={line}
+      onPopupToggle={jest.fn()}
+      popupOpen={false}
+      renderDuplicationPopup={jest.fn()}
+    />
+  );
+  expect(wrapper).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplications-test.js b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplications-test.js
deleted file mode 100644 (file)
index aaa463e..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 React from 'react';
-import { shallow } from 'enzyme';
-import { click } from '../../../../helpers/testUtils';
-import LineDuplications from '../LineDuplications';
-
-it('render duplicated line', () => {
-  const line = { line: 3, duplicated: true };
-  const onClick = jest.fn();
-  const wrapper = shallow(<LineDuplications line={line} onClick={onClick} />);
-  expect(wrapper).toMatchSnapshot();
-  click(wrapper.find('[tabIndex]'));
-  expect(onClick).toHaveBeenCalled();
-});
-
-it('render not duplicated line', () => {
-  const line = { line: 3, duplicated: false };
-  const onClick = jest.fn();
-  const wrapper = shallow(<LineDuplications line={line} onClick={onClick} />);
-  expect(wrapper).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplications-test.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplications-test.tsx
new file mode 100644 (file)
index 0000000..b5fac8e
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { click } from '../../../../helpers/testUtils';
+import LineDuplications from '../LineDuplications';
+
+it('render duplicated line', () => {
+  const line = { line: 3, duplicated: true };
+  const onClick = jest.fn();
+  const wrapper = shallow(<LineDuplications line={line} onClick={onClick} />);
+  expect(wrapper).toMatchSnapshot();
+  click(wrapper.find('[tabIndex]'));
+  expect(onClick).toHaveBeenCalled();
+});
+
+it('render not duplicated line', () => {
+  const line = { line: 3, duplicated: false };
+  const onClick = jest.fn();
+  const wrapper = shallow(<LineDuplications line={line} onClick={onClick} />);
+  expect(wrapper).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesIndicator-test.js b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesIndicator-test.js
deleted file mode 100644 (file)
index 9f22629..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 React from 'react';
-import { shallow } from 'enzyme';
-import { click } from '../../../../helpers/testUtils';
-import LineIssuesIndicator from '../LineIssuesIndicator';
-
-it('render highest severity', () => {
-  const line = { line: 3 };
-  const issues = [{ severity: 'MINOR' }, { severity: 'CRITICAL' }];
-  const onClick = jest.fn();
-  const wrapper = shallow(<LineIssuesIndicator issues={issues} line={line} onClick={onClick} />);
-  expect(wrapper).toMatchSnapshot();
-
-  click(wrapper);
-  expect(onClick).toHaveBeenCalled();
-
-  const nextIssues = [{ severity: 'MINOR' }, { severity: 'INFO' }];
-  wrapper.setProps({ issues: nextIssues });
-  expect(wrapper).toMatchSnapshot();
-});
-
-it('no issues', () => {
-  const line = { line: 3 };
-  const issues = [];
-  const onClick = jest.fn();
-  const wrapper = shallow(<LineIssuesIndicator issues={issues} line={line} onClick={onClick} />);
-  expect(wrapper).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesIndicator-test.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesIndicator-test.tsx
new file mode 100644 (file)
index 0000000..b4fd9fc
--- /dev/null
@@ -0,0 +1,72 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { click } from '../../../../helpers/testUtils';
+import LineIssuesIndicator from '../LineIssuesIndicator';
+import { Issue } from '../../../../app/types';
+
+const issueBase: Issue = {
+  component: '',
+  componentLongName: '',
+  componentQualifier: '',
+  componentUuid: '',
+  creationDate: '',
+  key: '',
+  flows: [],
+  message: '',
+  organization: '',
+  project: '',
+  projectName: '',
+  projectOrganization: '',
+  projectUuid: '',
+  rule: '',
+  ruleName: '',
+  secondaryLocations: [],
+  severity: '',
+  status: '',
+  type: ''
+};
+
+it('render highest severity', () => {
+  const line = { line: 3 };
+  const issues = [
+    { ...issueBase, key: 'foo', severity: 'MINOR' },
+    { ...issueBase, key: 'bar', severity: 'CRITICAL' }
+  ];
+  const onClick = jest.fn();
+  const wrapper = shallow(<LineIssuesIndicator issues={issues} line={line} onClick={onClick} />);
+  expect(wrapper).toMatchSnapshot();
+
+  click(wrapper);
+  expect(onClick).toHaveBeenCalled();
+
+  const nextIssues = [{ severity: 'MINOR' }, { severity: 'INFO' }];
+  wrapper.setProps({ issues: nextIssues });
+  expect(wrapper).toMatchSnapshot();
+});
+
+it('no issues', () => {
+  const line = { line: 3 };
+  const issues: Issue[] = [];
+  const onClick = jest.fn();
+  const wrapper = shallow(<LineIssuesIndicator issues={issues} line={line} onClick={onClick} />);
+  expect(wrapper).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesList-test.js b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesList-test.js
deleted file mode 100644 (file)
index 354c3de..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 React from 'react';
-import { shallow } from 'enzyme';
-import LineIssuesList from '../LineIssuesList';
-
-it('render issues list', () => {
-  const line = { line: 3 };
-  const issues = [{ key: 'foo' }, { key: 'bar' }];
-  const onIssueClick = jest.fn();
-  const wrapper = shallow(
-    <LineIssuesList
-      issues={issues}
-      line={line}
-      onIssueClick={onIssueClick}
-      onPopupToggle={jest.fn()}
-      openPopup={null}
-      selectedIssue="foo"
-    />
-  );
-  expect(wrapper).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesList-test.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesList-test.tsx
new file mode 100644 (file)
index 0000000..8ab04fc
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 LineIssuesList from '../LineIssuesList';
+import { Issue } from '../../../../app/types';
+
+const issueBase: Issue = {
+  component: '',
+  componentLongName: '',
+  componentQualifier: '',
+  componentUuid: '',
+  creationDate: '',
+  key: '',
+  flows: [],
+  message: '',
+  organization: '',
+  project: '',
+  projectName: '',
+  projectOrganization: '',
+  projectUuid: '',
+  rule: '',
+  ruleName: '',
+  secondaryLocations: [],
+  severity: '',
+  status: '',
+  type: ''
+};
+
+it('render issues list', () => {
+  const issues: Issue[] = [{ ...issueBase, key: 'foo' }, { ...issueBase, key: 'bar' }];
+  const onIssueClick = jest.fn();
+  const wrapper = shallow(
+    <LineIssuesList
+      branch={undefined}
+      issuePopup={undefined}
+      issues={issues}
+      onIssueChange={jest.fn()}
+      onIssueClick={onIssueClick}
+      onIssuePopupToggle={jest.fn()}
+      selectedIssue="foo"
+    />
+  );
+  expect(wrapper).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineNumber-test.js b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineNumber-test.js
deleted file mode 100644 (file)
index 2afee7b..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 React from 'react';
-import { shallow } from 'enzyme';
-import { click } from '../../../../helpers/testUtils';
-import LineNumber from '../LineNumber';
-
-it('render line 3', () => {
-  const line = { line: 3 };
-  const onClick = jest.fn();
-  const wrapper = shallow(<LineNumber line={line} onClick={onClick} />);
-  expect(wrapper).toMatchSnapshot();
-  click(wrapper);
-  expect(onClick).toHaveBeenCalled();
-});
-
-it('render line 0', () => {
-  const line = { line: 0 };
-  const onClick = jest.fn();
-  const wrapper = shallow(<LineNumber line={line} onClick={onClick} />);
-  expect(wrapper).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineNumber-test.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineNumber-test.tsx
new file mode 100644 (file)
index 0000000..bf40ca9
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { click } from '../../../../helpers/testUtils';
+import LineNumber from '../LineNumber';
+
+it('render line 3', () => {
+  const line = { line: 3 };
+  const wrapper = shallow(
+    <LineNumber
+      branch={undefined}
+      componentKey="foo"
+      line={line}
+      onPopupToggle={jest.fn()}
+      popupOpen={false}
+    />
+  );
+  expect(wrapper).toMatchSnapshot();
+  click(wrapper);
+});
+
+it('render line 0', () => {
+  const line = { line: 0 };
+  const wrapper = shallow(
+    <LineNumber
+      branch={undefined}
+      componentKey="foo"
+      line={line}
+      onPopupToggle={jest.fn()}
+      popupOpen={false}
+    />
+  );
+  expect(wrapper).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineOptionsPopup-test.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineOptionsPopup-test.tsx
new file mode 100644 (file)
index 0000000..a5de2ec
--- /dev/null
@@ -0,0 +1,28 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 LineOptionsPopup from '../LineOptionsPopup';
+
+it('should render', () => {
+  const line = { line: 3 };
+  const wrapper = shallow(<LineOptionsPopup branch="feature" componentKey="foo" line={line} />);
+  expect(wrapper).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineSCM-test.js b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineSCM-test.js
deleted file mode 100644 (file)
index 99db276..0000000
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 React from 'react';
-import { shallow } from 'enzyme';
-import { click } from '../../../../helpers/testUtils';
-import LineSCM from '../LineSCM';
-
-it('render scm details', () => {
-  const line = { line: 3, scmAuthor: 'foo', scmDate: '2017-01-01' };
-  const previousLine = { line: 2, scmAuthor: 'bar', scmDate: '2017-01-02' };
-  const onClick = jest.fn();
-  const wrapper = shallow(<LineSCM line={line} onClick={onClick} previousLine={previousLine} />);
-  expect(wrapper).toMatchSnapshot();
-  click(wrapper);
-  expect(onClick).toHaveBeenCalled();
-});
-
-it('render scm details for the first line', () => {
-  const line = { line: 3, scmAuthor: 'foo', scmDate: '2017-01-01' };
-  const onClick = jest.fn();
-  const wrapper = shallow(<LineSCM line={line} onClick={onClick} />);
-  expect(wrapper).toMatchSnapshot();
-});
-
-it('does not render scm details', () => {
-  const line = { line: 3, scmAuthor: 'foo', scmDate: '2017-01-01' };
-  const previousLine = { line: 2, scmAuthor: 'foo', scmDate: '2017-01-01' };
-  const onClick = jest.fn();
-  const wrapper = shallow(<LineSCM line={line} onClick={onClick} previousLine={previousLine} />);
-  expect(wrapper).toMatchSnapshot();
-});
-
-it('does not allow to click', () => {
-  const line = { scmAuthor: 'foo', scmDate: '2017-01-01' };
-  const onClick = jest.fn();
-  const wrapper = shallow(<LineSCM line={line} onClick={onClick} />);
-  expect(wrapper).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineSCM-test.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineSCM-test.tsx
new file mode 100644 (file)
index 0000000..29e82eb
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 LineSCM from '../LineSCM';
+import { click } from '../../../../helpers/testUtils';
+
+it('render scm details', () => {
+  const line = { line: 3, scmAuthor: 'foo', scmDate: '2017-01-01' };
+  const previousLine = { line: 2, scmAuthor: 'bar', scmDate: '2017-01-02' };
+  const wrapper = shallow(
+    <LineSCM line={line} onPopupToggle={jest.fn()} popupOpen={false} previousLine={previousLine} />
+  );
+  expect(wrapper).toMatchSnapshot();
+});
+
+it('render scm details for the first line', () => {
+  const line = { line: 3, scmAuthor: 'foo', scmDate: '2017-01-01' };
+  const wrapper = shallow(
+    <LineSCM line={line} onPopupToggle={jest.fn()} popupOpen={false} previousLine={undefined} />
+  );
+  expect(wrapper).toMatchSnapshot();
+});
+
+it('does not render scm details', () => {
+  const line = { line: 3, scmAuthor: 'foo', scmDate: '2017-01-01' };
+  const previousLine = { line: 2, scmAuthor: 'foo', scmDate: '2017-01-01' };
+  const wrapper = shallow(
+    <LineSCM line={line} onPopupToggle={jest.fn()} popupOpen={false} previousLine={previousLine} />
+  );
+  expect(wrapper).toMatchSnapshot();
+});
+
+it('should open popup', () => {
+  const line = { line: 3, scmAuthor: 'foo', scmDate: '2017-01-01' };
+  const onPopupToggle = jest.fn();
+  const wrapper = shallow(
+    <LineSCM line={line} onPopupToggle={onPopupToggle} popupOpen={false} previousLine={undefined} />
+  );
+  click(wrapper.find('[role="button"]'));
+  expect(onPopupToggle).toBeCalledWith({ line: 3, name: 'scm' });
+});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/SCMPopup-test.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/SCMPopup-test.tsx
new file mode 100644 (file)
index 0000000..6a6fba2
--- /dev/null
@@ -0,0 +1,27 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 SCMPopup from '../SCMPopup';
+
+it('should render', () => {
+  const line = { line: 3, scmAuthor: 'foo', scmDate: '2017-01-01' };
+  expect(shallow(<SCMPopup line={line} />)).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.js.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.js.snap
deleted file mode 100644 (file)
index 1cb641e..0000000
+++ /dev/null
@@ -1,55 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`render code 1`] = `
-<td
-  className="source-line-code code has-issues"
-  data-line-number={3}
->
-  <div
-    className="source-line-code-inner"
-  >
-    <pre>
-      <span
-        className="k source-line-code-issue"
-        key="0"
-      >
-        class
-      </span>
-      <span
-        className=""
-        key="1"
-      >
-         
-      </span>
-      <span
-        className="sym sym-1"
-        key="2"
-      >
-        Foo
-      </span>
-      <span
-        className=""
-        key="3"
-      >
-         {
-      </span>
-    </pre>
-  </div>
-  <LineIssuesList
-    issues={
-      Array [
-        Object {
-          "key": "issue-1",
-        },
-        Object {
-          "key": "issue-2",
-        },
-      ]
-    }
-    onIssueClick={[MockFunction]}
-    onPopupToggle={[MockFunction]}
-    openPopup={null}
-    selectedIssue="issue-1"
-  />
-</td>
-`;
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.tsx.snap
new file mode 100644 (file)
index 0000000..f5e24cd
--- /dev/null
@@ -0,0 +1,92 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`render code 1`] = `
+<td
+  className="source-line-code code has-issues"
+  data-line-number={3}
+>
+  <div
+    className="source-line-code-inner"
+  >
+    <pre>
+      <span
+        className="k source-line-code-issue"
+        key="0"
+      >
+        class
+      </span>
+      <span
+        className=""
+        key="1"
+      >
+         
+      </span>
+      <span
+        className="sym sym-1"
+        key="2"
+      >
+        Foo
+      </span>
+      <span
+        className=""
+        key="3"
+      >
+         {
+      </span>
+    </pre>
+  </div>
+  <LineIssuesList
+    branch="feature"
+    issues={
+      Array [
+        Object {
+          "component": "",
+          "componentLongName": "",
+          "componentQualifier": "",
+          "componentUuid": "",
+          "creationDate": "",
+          "flows": Array [],
+          "key": "issue-1",
+          "message": "",
+          "organization": "",
+          "project": "",
+          "projectName": "",
+          "projectOrganization": "",
+          "projectUuid": "",
+          "rule": "",
+          "ruleName": "",
+          "secondaryLocations": Array [],
+          "severity": "",
+          "status": "",
+          "type": "",
+        },
+        Object {
+          "component": "",
+          "componentLongName": "",
+          "componentQualifier": "",
+          "componentUuid": "",
+          "creationDate": "",
+          "flows": Array [],
+          "key": "issue-2",
+          "message": "",
+          "organization": "",
+          "project": "",
+          "projectName": "",
+          "projectOrganization": "",
+          "projectUuid": "",
+          "rule": "",
+          "ruleName": "",
+          "secondaryLocations": Array [],
+          "severity": "",
+          "status": "",
+          "type": "",
+        },
+      ]
+    }
+    onIssueChange={[MockFunction]}
+    onIssueClick={[MockFunction]}
+    onIssuePopupToggle={[MockFunction]}
+    selectedIssue="issue-1"
+  />
+</td>
+`;
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCoverage-test.js.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCoverage-test.js.snap
deleted file mode 100644 (file)
index abef0b0..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`render covered line 1`] = `
-<Tooltip
-  overlay="source_viewer.tooltip.covered"
-  placement="right"
->
-  <td
-    className="source-meta source-line-coverage source-line-covered"
-    data-line-number={3}
-    onClick={[Function]}
-    role="button"
-    tabIndex={0}
-  >
-    <div
-      className="source-line-bar"
-    />
-  </td>
-</Tooltip>
-`;
-
-exports[`render line with unknown coverage 1`] = `
-<td
-  className="source-meta source-line-coverage"
-  data-line-number={3}
->
-  <div
-    className="source-line-bar"
-  />
-</td>
-`;
-
-exports[`render uncovered line 1`] = `
-<Tooltip
-  overlay="source_viewer.tooltip.uncovered"
-  placement="right"
->
-  <td
-    className="source-meta source-line-coverage source-line-uncovered"
-    data-line-number={3}
-  >
-    <div
-      className="source-line-bar"
-    />
-  </td>
-</Tooltip>
-`;
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCoverage-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCoverage-test.tsx.snap
new file mode 100644 (file)
index 0000000..664a60d
--- /dev/null
@@ -0,0 +1,65 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`render covered line 1`] = `
+<td
+  className="source-meta source-line-coverage source-line-covered"
+  data-line-number={3}
+  onClick={[Function]}
+  role="button"
+  tabIndex={0}
+>
+  <Tooltip
+    overlay="source_viewer.tooltip.covered"
+    placement="right"
+  >
+    <div
+      className="source-line-bar"
+    />
+  </Tooltip>
+  <BubblePopupHelper
+    isOpen={false}
+    popup={
+      <CoveragePopup
+        branch={undefined}
+        componentKey="foo"
+        line={
+          Object {
+            "coverageStatus": "covered",
+            "line": 3,
+          }
+        }
+        onClose={[Function]}
+      />
+    }
+    position="bottomright"
+    togglePopup={[Function]}
+  />
+</td>
+`;
+
+exports[`render line with unknown coverage 1`] = `
+<td
+  className="source-meta source-line-coverage"
+  data-line-number={3}
+>
+  <div
+    className="source-line-bar"
+  />
+</td>
+`;
+
+exports[`render uncovered line 1`] = `
+<td
+  className="source-meta source-line-coverage source-line-uncovered"
+  data-line-number={3}
+>
+  <Tooltip
+    overlay="source_viewer.tooltip.uncovered"
+    placement="right"
+  >
+    <div
+      className="source-line-bar"
+    />
+  </Tooltip>
+</td>
+`;
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplicationBlock-test.js.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplicationBlock-test.js.snap
deleted file mode 100644 (file)
index c8a4ee2..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`render duplicated line 1`] = `
-<Tooltip
-  overlay="source_viewer.tooltip.duplicated_block"
-  placement="right"
->
-  <td
-    className="source-meta source-line-duplications-extra source-line-duplicated"
-    data-index={1}
-    data-line-number={3}
-    key="1"
-    onClick={[Function]}
-    role="button"
-    tabIndex="0"
-  >
-    <div
-      className="source-line-bar"
-    />
-  </td>
-</Tooltip>
-`;
-
-exports[`render not duplicated line 1`] = `
-<td
-  className="source-meta source-line-duplications-extra"
-  data-index={1}
-  data-line-number={3}
-  key="1"
->
-  <div
-    className="source-line-bar"
-  />
-</td>
-`;
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplicationBlock-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplicationBlock-test.tsx.snap
new file mode 100644 (file)
index 0000000..9ea7771
--- /dev/null
@@ -0,0 +1,38 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`render duplicated line 1`] = `
+<td
+  className="source-meta source-line-duplications-extra source-line-duplicated"
+  data-index={1}
+  data-line-number={3}
+  onClick={[Function]}
+  role="button"
+  tabIndex={0}
+>
+  <Tooltip
+    overlay="source_viewer.tooltip.duplicated_block"
+    placement="right"
+  >
+    <div
+      className="source-line-bar"
+    />
+  </Tooltip>
+  <BubblePopupHelper
+    isOpen={false}
+    position="bottomright"
+    togglePopup={[Function]}
+  />
+</td>
+`;
+
+exports[`render not duplicated line 1`] = `
+<td
+  className="source-meta source-line-duplications-extra"
+  data-index={1}
+  data-line-number={3}
+>
+  <div
+    className="source-line-bar"
+  />
+</td>
+`;
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplications-test.js.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplications-test.js.snap
deleted file mode 100644 (file)
index d40e9e6..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`render duplicated line 1`] = `
-<Tooltip
-  overlay="source_viewer.tooltip.duplicated_line"
-  placement="right"
->
-  <td
-    className="source-meta source-line-duplications source-line-duplicated"
-    onClick={[Function]}
-    role="button"
-    tabIndex={0}
-  >
-    <div
-      className="source-line-bar"
-    />
-  </td>
-</Tooltip>
-`;
-
-exports[`render not duplicated line 1`] = `
-<td
-  className="source-meta source-line-duplications"
->
-  <div
-    className="source-line-bar"
-  />
-</td>
-`;
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplications-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplications-test.tsx.snap
new file mode 100644 (file)
index 0000000..d40e9e6
--- /dev/null
@@ -0,0 +1,29 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`render duplicated line 1`] = `
+<Tooltip
+  overlay="source_viewer.tooltip.duplicated_line"
+  placement="right"
+>
+  <td
+    className="source-meta source-line-duplications source-line-duplicated"
+    onClick={[Function]}
+    role="button"
+    tabIndex={0}
+  >
+    <div
+      className="source-line-bar"
+    />
+  </td>
+</Tooltip>
+`;
+
+exports[`render not duplicated line 1`] = `
+<td
+  className="source-meta source-line-duplications"
+>
+  <div
+    className="source-line-bar"
+  />
+</td>
+`;
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesIndicator-test.js.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesIndicator-test.js.snap
deleted file mode 100644 (file)
index 5636d7a..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`no issues 1`] = `
-<td
-  className="source-meta source-line-issues"
-  data-line-number={3}
-/>
-`;
-
-exports[`render highest severity 1`] = `
-<td
-  className="source-meta source-line-issues source-line-with-issues"
-  data-line-number={3}
-  onClick={[Function]}
-  role="button"
-  tabIndex="0"
->
-  <SeverityIcon
-    severity="CRITICAL"
-  />
-  <span
-    className="source-line-issues-counter"
-  >
-    2
-  </span>
-</td>
-`;
-
-exports[`render highest severity 2`] = `
-<td
-  className="source-meta source-line-issues source-line-with-issues"
-  data-line-number={3}
-  onClick={[Function]}
-  role="button"
-  tabIndex="0"
->
-  <SeverityIcon
-    severity="MINOR"
-  />
-  <span
-    className="source-line-issues-counter"
-  >
-    2
-  </span>
-</td>
-`;
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesIndicator-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesIndicator-test.tsx.snap
new file mode 100644 (file)
index 0000000..e941c78
--- /dev/null
@@ -0,0 +1,46 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`no issues 1`] = `
+<td
+  className="source-meta source-line-issues"
+  data-line-number={3}
+/>
+`;
+
+exports[`render highest severity 1`] = `
+<td
+  className="source-meta source-line-issues source-line-with-issues"
+  data-line-number={3}
+  onClick={[Function]}
+  role="button"
+  tabIndex={0}
+>
+  <SeverityIcon
+    severity="CRITICAL"
+  />
+  <span
+    className="source-line-issues-counter"
+  >
+    2
+  </span>
+</td>
+`;
+
+exports[`render highest severity 2`] = `
+<td
+  className="source-meta source-line-issues source-line-with-issues"
+  data-line-number={3}
+  onClick={[Function]}
+  role="button"
+  tabIndex={0}
+>
+  <SeverityIcon
+    severity="MINOR"
+  />
+  <span
+    className="source-line-issues-counter"
+  >
+    2
+  </span>
+</td>
+`;
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesList-test.js.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesList-test.js.snap
deleted file mode 100644 (file)
index 6612498..0000000
+++ /dev/null
@@ -1,36 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`render issues list 1`] = `
-<div
-  className="issue-list"
->
-  <Issue
-    displayLocationsCount={true}
-    displayLocationsLink={true}
-    issue={
-      Object {
-        "key": "foo",
-      }
-    }
-    key="foo"
-    onClick={[MockFunction]}
-    onPopupToggle={[MockFunction]}
-    openPopup={null}
-    selected={true}
-  />
-  <Issue
-    displayLocationsCount={true}
-    displayLocationsLink={true}
-    issue={
-      Object {
-        "key": "bar",
-      }
-    }
-    key="bar"
-    onClick={[MockFunction]}
-    onPopupToggle={[MockFunction]}
-    openPopup={null}
-    selected={false}
-  />
-</div>
-`;
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesList-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesList-test.tsx.snap
new file mode 100644 (file)
index 0000000..d19a4e9
--- /dev/null
@@ -0,0 +1,72 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`render issues list 1`] = `
+<div
+  className="issue-list"
+>
+  <Issue
+    displayLocationsCount={true}
+    displayLocationsLink={true}
+    issue={
+      Object {
+        "component": "",
+        "componentLongName": "",
+        "componentQualifier": "",
+        "componentUuid": "",
+        "creationDate": "",
+        "flows": Array [],
+        "key": "foo",
+        "message": "",
+        "organization": "",
+        "project": "",
+        "projectName": "",
+        "projectOrganization": "",
+        "projectUuid": "",
+        "rule": "",
+        "ruleName": "",
+        "secondaryLocations": Array [],
+        "severity": "",
+        "status": "",
+        "type": "",
+      }
+    }
+    key="foo"
+    onChange={[MockFunction]}
+    onClick={[MockFunction]}
+    onPopupToggle={[MockFunction]}
+    selected={true}
+  />
+  <Issue
+    displayLocationsCount={true}
+    displayLocationsLink={true}
+    issue={
+      Object {
+        "component": "",
+        "componentLongName": "",
+        "componentQualifier": "",
+        "componentUuid": "",
+        "creationDate": "",
+        "flows": Array [],
+        "key": "bar",
+        "message": "",
+        "organization": "",
+        "project": "",
+        "projectName": "",
+        "projectOrganization": "",
+        "projectUuid": "",
+        "rule": "",
+        "ruleName": "",
+        "secondaryLocations": Array [],
+        "severity": "",
+        "status": "",
+        "type": "",
+      }
+    }
+    key="bar"
+    onChange={[MockFunction]}
+    onClick={[MockFunction]}
+    onPopupToggle={[MockFunction]}
+    selected={false}
+  />
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineNumber-test.js.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineNumber-test.js.snap
deleted file mode 100644 (file)
index 86e1095..0000000
+++ /dev/null
@@ -1,17 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`render line 0 1`] = `
-<td
-  className="source-meta source-line-number"
-/>
-`;
-
-exports[`render line 3 1`] = `
-<td
-  className="source-meta source-line-number"
-  data-line-number={3}
-  onClick={[Function]}
-  role="button"
-  tabIndex={0}
-/>
-`;
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineNumber-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineNumber-test.tsx.snap
new file mode 100644 (file)
index 0000000..477d94d
--- /dev/null
@@ -0,0 +1,34 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`render line 0 1`] = `
+<td
+  className="source-meta source-line-number"
+/>
+`;
+
+exports[`render line 3 1`] = `
+<td
+  className="source-meta source-line-number"
+  data-line-number={3}
+  onClick={[Function]}
+  role="button"
+  tabIndex={0}
+>
+  <BubblePopupHelper
+    isOpen={false}
+    popup={
+      <LineOptionsPopup
+        branch={undefined}
+        componentKey="foo"
+        line={
+          Object {
+            "line": 3,
+          }
+        }
+      />
+    }
+    position="bottomright"
+    togglePopup={[Function]}
+  />
+</td>
+`;
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineOptionsPopup-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineOptionsPopup-test.tsx.snap
new file mode 100644 (file)
index 0000000..13d7b6e
--- /dev/null
@@ -0,0 +1,29 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render 1`] = `
+<BubblePopup
+  customClass="source-viewer-bubble-popup"
+>
+  <div
+    className="bubble-popup-section"
+  >
+    <Link
+      className="js-get-permalink"
+      onlyActiveOnIndex={false}
+      style={Object {}}
+      to={
+        Object {
+          "pathname": "/component",
+          "query": Object {
+            "branch": "feature",
+            "id": "foo",
+            "line": 3,
+          },
+        }
+      }
+    >
+      component_viewer.get_permalink
+    </Link>
+  </div>
+</BubblePopup>
+`;
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineSCM-test.js.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineSCM-test.js.snap
deleted file mode 100644 (file)
index ce8c036..0000000
+++ /dev/null
@@ -1,52 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`does not allow to click 1`] = `
-<td
-  className="source-meta source-line-scm"
->
-  <div
-    className="source-line-scm-inner"
-    data-author="foo"
-  />
-</td>
-`;
-
-exports[`does not render scm details 1`] = `
-<td
-  className="source-meta source-line-scm"
-  data-line-number={3}
-  onClick={[Function]}
-  role="button"
-  tabIndex={0}
-/>
-`;
-
-exports[`render scm details 1`] = `
-<td
-  className="source-meta source-line-scm"
-  data-line-number={3}
-  onClick={[Function]}
-  role="button"
-  tabIndex={0}
->
-  <div
-    className="source-line-scm-inner"
-    data-author="foo"
-  />
-</td>
-`;
-
-exports[`render scm details for the first line 1`] = `
-<td
-  className="source-meta source-line-scm"
-  data-line-number={3}
-  onClick={[Function]}
-  role="button"
-  tabIndex={0}
->
-  <div
-    className="source-line-scm-inner"
-    data-author="foo"
-  />
-</td>
-`;
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineSCM-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineSCM-test.tsx.snap
new file mode 100644 (file)
index 0000000..dcbaf7e
--- /dev/null
@@ -0,0 +1,90 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`does not render scm details 1`] = `
+<td
+  className="source-meta source-line-scm"
+  data-line-number={3}
+  onClick={[Function]}
+  role="button"
+  tabIndex={0}
+>
+  <BubblePopupHelper
+    isOpen={false}
+    popup={
+      <SCMPopup
+        line={
+          Object {
+            "line": 3,
+            "scmAuthor": "foo",
+            "scmDate": "2017-01-01",
+          }
+        }
+      />
+    }
+    position="bottomright"
+    togglePopup={[Function]}
+  />
+</td>
+`;
+
+exports[`render scm details 1`] = `
+<td
+  className="source-meta source-line-scm"
+  data-line-number={3}
+  onClick={[Function]}
+  role="button"
+  tabIndex={0}
+>
+  <div
+    className="source-line-scm-inner"
+    data-author="foo"
+  />
+  <BubblePopupHelper
+    isOpen={false}
+    popup={
+      <SCMPopup
+        line={
+          Object {
+            "line": 3,
+            "scmAuthor": "foo",
+            "scmDate": "2017-01-01",
+          }
+        }
+      />
+    }
+    position="bottomright"
+    togglePopup={[Function]}
+  />
+</td>
+`;
+
+exports[`render scm details for the first line 1`] = `
+<td
+  className="source-meta source-line-scm"
+  data-line-number={3}
+  onClick={[Function]}
+  role="button"
+  tabIndex={0}
+>
+  <div
+    className="source-line-scm-inner"
+    data-author="foo"
+  />
+  <BubblePopupHelper
+    isOpen={false}
+    popup={
+      <SCMPopup
+        line={
+          Object {
+            "line": 3,
+            "scmAuthor": "foo",
+            "scmDate": "2017-01-01",
+          }
+        }
+      />
+    }
+    position="bottomright"
+    togglePopup={[Function]}
+  />
+</td>
+`;
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/SCMPopup-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/SCMPopup-test.tsx.snap
new file mode 100644 (file)
index 0000000..d846069
--- /dev/null
@@ -0,0 +1,20 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render 1`] = `
+<BubblePopup
+  customClass="source-viewer-bubble-popup"
+>
+  <div
+    className="bubble-popup-section"
+  >
+    foo
+  </div>
+  <div
+    className="bubble-popup-section"
+  >
+    <DateFormatter
+      date="2017-01-01"
+    />
+  </div>
+</BubblePopup>
+`;
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/highlight-test.js b/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/highlight-test.js
deleted file mode 100644 (file)
index f252b17..0000000
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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.
- */
-// @flow
-import { highlightSymbol } from '../highlight';
-
-describe('highlightSymbol', () => {
-  it('should not highlight symbols with similar beginning', () => {
-    // test all positions of sym-X in the string: beginning, middle and ending
-    const tokens = [
-      { className: 'sym-18 b', markers: [], text: 'foo' },
-      { className: 'a sym-18', markers: [], text: 'foo' },
-      { className: 'a sym-18 b', markers: [], text: 'foo' },
-      { className: 'sym-1 d', markers: [], text: 'bar' },
-      { className: 'c sym-1', markers: [], text: 'bar' },
-      { className: 'c sym-1 d', markers: [], text: 'bar' }
-    ];
-    expect(highlightSymbol(tokens, 'sym-1')).toEqual([
-      { className: 'sym-18 b', markers: [], text: 'foo' },
-      { className: 'a sym-18', markers: [], text: 'foo' },
-      { className: 'a sym-18 b', markers: [], text: 'foo' },
-      { className: 'sym-1 d highlighted', markers: [], text: 'bar' },
-      { className: 'c sym-1 highlighted', markers: [], text: 'bar' },
-      { className: 'c sym-1 d highlighted', markers: [], text: 'bar' }
-    ]);
-  });
-
-  it('should highlight symbols marked twice', () => {
-    const tokens = [
-      { className: 'sym sym-1 sym sym-2', markers: [], text: 'foo' },
-      { className: 'sym sym-1', markers: [], text: 'bar' },
-      { className: 'sym sym-2', markers: [], text: 'qux' }
-    ];
-    expect(highlightSymbol(tokens, 'sym-1')).toEqual([
-      { className: 'sym sym-1 sym sym-2 highlighted', markers: [], text: 'foo' },
-      { className: 'sym sym-1 highlighted', markers: [], text: 'bar' },
-      { className: 'sym sym-2', markers: [], text: 'qux' }
-    ]);
-  });
-});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/highlight-test.ts b/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/highlight-test.ts
new file mode 100644 (file)
index 0000000..47900d5
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { highlightSymbol } from '../highlight';
+
+describe('highlightSymbol', () => {
+  it('should not highlight symbols with similar beginning', () => {
+    // test all positions of sym-X in the string: beginning, middle and ending
+    const tokens = [
+      { className: 'sym-18 b', markers: [], text: 'foo' },
+      { className: 'a sym-18', markers: [], text: 'foo' },
+      { className: 'a sym-18 b', markers: [], text: 'foo' },
+      { className: 'sym-1 d', markers: [], text: 'bar' },
+      { className: 'c sym-1', markers: [], text: 'bar' },
+      { className: 'c sym-1 d', markers: [], text: 'bar' }
+    ];
+    expect(highlightSymbol(tokens, 'sym-1')).toEqual([
+      { className: 'sym-18 b', markers: [], text: 'foo' },
+      { className: 'a sym-18', markers: [], text: 'foo' },
+      { className: 'a sym-18 b', markers: [], text: 'foo' },
+      { className: 'sym-1 d highlighted', markers: [], text: 'bar' },
+      { className: 'c sym-1 highlighted', markers: [], text: 'bar' },
+      { className: 'c sym-1 d highlighted', markers: [], text: 'bar' }
+    ]);
+  });
+
+  it('should highlight symbols marked twice', () => {
+    const tokens = [
+      { className: 'sym sym-1 sym sym-2', markers: [], text: 'foo' },
+      { className: 'sym sym-1', markers: [], text: 'bar' },
+      { className: 'sym sym-2', markers: [], text: 'qux' }
+    ];
+    expect(highlightSymbol(tokens, 'sym-1')).toEqual([
+      { className: 'sym sym-1 sym sym-2 highlighted', markers: [], text: 'foo' },
+      { className: 'sym sym-1 highlighted', markers: [], text: 'bar' },
+      { className: 'sym sym-2', markers: [], text: 'qux' }
+    ]);
+  });
+});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/indexing-test.js b/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/indexing-test.js
deleted file mode 100644 (file)
index 2e88dd6..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 { symbolsByLine } from '../indexing';
-
-describe('symbolsByLine', () => {
-  it('should highlight symbols marked twice', () => {
-    const lines = [
-      { line: 1, code: '<span class="sym-54 sym"><span class="sym-56 sym">foo</span></span>' },
-      { line: 2, code: '<span class="sym-56 sym">bar</span>' },
-      { line: 3, code: '<span class="k">qux</span>' }
-    ];
-    expect(symbolsByLine(lines)).toEqual({
-      1: ['sym-54', 'sym-56'],
-      2: ['sym-56'],
-      3: []
-    });
-  });
-});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/indexing-test.ts b/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/indexing-test.ts
new file mode 100644 (file)
index 0000000..2e88dd6
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { symbolsByLine } from '../indexing';
+
+describe('symbolsByLine', () => {
+  it('should highlight symbols marked twice', () => {
+    const lines = [
+      { line: 1, code: '<span class="sym-54 sym"><span class="sym-56 sym">foo</span></span>' },
+      { line: 2, code: '<span class="sym-56 sym">bar</span>' },
+      { line: 3, code: '<span class="k">qux</span>' }
+    ];
+    expect(symbolsByLine(lines)).toEqual({
+      1: ['sym-54', 'sym-56'],
+      2: ['sym-56'],
+      3: []
+    });
+  });
+});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/getCoverageStatus.js b/server/sonar-web/src/main/js/components/SourceViewer/helpers/getCoverageStatus.js
deleted file mode 100644 (file)
index 5a0ba74..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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.
- */
-// @flow
-/*:: import type { SourceLine } from '../types'; */
-
-export default function getCoverageStatus(s /*: SourceLine */) /*: string | null */ {
-  let status = null;
-  if (s.lineHits != null && s.lineHits > 0) {
-    status = 'partially-covered';
-  }
-  if (s.lineHits != null && s.lineHits > 0 && s.conditions === s.coveredConditions) {
-    status = 'covered';
-  }
-  if (s.lineHits === 0 || s.coveredConditions === 0) {
-    status = 'uncovered';
-  }
-  return status;
-}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/getCoverageStatus.tsx b/server/sonar-web/src/main/js/components/SourceViewer/helpers/getCoverageStatus.tsx
new file mode 100644 (file)
index 0000000..88fd50b
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { SourceLine } from '../../../app/types';
+
+export default function getCoverageStatus(s: SourceLine): string | undefined {
+  let status: string | undefined;
+  if (s.lineHits != null && s.lineHits > 0) {
+    status = 'partially-covered';
+  }
+  if (s.lineHits != null && s.lineHits > 0 && s.conditions === s.coveredConditions) {
+    status = 'covered';
+  }
+  if (s.lineHits === 0 || s.coveredConditions === 0) {
+    status = 'uncovered';
+  }
+  return status;
+}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/highlight.js b/server/sonar-web/src/main/js/components/SourceViewer/helpers/highlight.js
deleted file mode 100644 (file)
index afdd574..0000000
+++ /dev/null
@@ -1,143 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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.
- */
-// @flow
-import escapeHtml from 'escape-html';
-import { uniq } from 'lodash';
-
-/*::
-export type Token = { className: string, markers: Array<number>, text: string };
-*/
-/*::
-export type Tokens = Array<Token>; */
-
-const ISSUE_LOCATION_CLASS = 'source-line-code-issue';
-
-export function splitByTokens(code /*: string */, rootClassName /*: string */ = '') /*: Tokens */ {
-  const container = document.createElement('div');
-  let tokens = [];
-  container.innerHTML = code;
-  [].forEach.call(container.childNodes, node => {
-    if (node.nodeType === 1) {
-      // ELEMENT NODE
-      const fullClassName = rootClassName ? rootClassName + ' ' + node.className : node.className;
-      const innerTokens = splitByTokens(node.innerHTML, fullClassName);
-      tokens = tokens.concat(innerTokens);
-    }
-    if (node.nodeType === 3) {
-      // TEXT NODE
-      tokens.push({ className: rootClassName, markers: [], text: node.nodeValue });
-    }
-  });
-  return tokens;
-}
-
-export function highlightSymbol(tokens /*: Tokens */, symbol /*: string */) /*: Tokens */ {
-  const symbolRegExp = new RegExp(`\\b${symbol}\\b`);
-  return tokens.map(
-    token =>
-      symbolRegExp.test(token.className)
-        ? { ...token, className: `${token.className} highlighted` }
-        : token
-  );
-}
-
-/**
- * Intersect two ranges
- * @param s1 Start position of the first range
- * @param e1 End position of the first range
- * @param s2 Start position of the second range
- * @param e2 End position of the second range
- */
-function intersect(
-  s1 /*: number */,
-  e1 /*: number */,
-  s2 /*: number */,
-  e2 /*: number */
-) /*: { from: number, to: number } */ {
-  return { from: Math.max(s1, s2), to: Math.min(e1, e2) };
-}
-
-/**
- * Get the substring of a string
- * @param str A string
- * @param from "From" offset
- * @param to "To" offset
- * @param acc Global offset to eliminate
- */
-function part(
-  str /*: string */,
-  from /*: number */,
-  to /*: number */,
-  acc /*: number */
-) /*: string */ {
-  // we do not want negative number as the first argument of `substr`
-  return from >= acc ? str.substr(from - acc, to - from) : str.substr(0, to - from);
-}
-
-/**
- * Highlight issue locations in the list of tokens
- */
-export function highlightIssueLocations(
-  tokens /*: Tokens */,
-  issueLocations /*: Array<*> */,
-  rootClassName /*: string */ = ISSUE_LOCATION_CLASS
-) /*: Tokens */ {
-  issueLocations.forEach(location => {
-    const nextTokens = [];
-    let acc = 0;
-    let markerAdded = location.line !== location.startLine;
-    tokens.forEach(token => {
-      const x = intersect(acc, acc + token.text.length, location.from, location.to);
-      const p1 = part(token.text, acc, x.from, acc);
-      const p2 = part(token.text, x.from, x.to, acc);
-      const p3 = part(token.text, x.to, acc + token.text.length, acc);
-      if (p1.length) {
-        nextTokens.push({ ...token, text: p1 });
-      }
-      if (p2.length) {
-        const newClassName =
-          token.className.indexOf(rootClassName) === -1
-            ? `${token.className} ${rootClassName}`
-            : token.className;
-        nextTokens.push({
-          className: newClassName,
-          markers:
-            !markerAdded && location.index != null
-              ? uniq([...token.markers, location.index])
-              : token.markers,
-          text: p2
-        });
-        markerAdded = true;
-      }
-      if (p3.length) {
-        nextTokens.push({ ...token, text: p3 });
-      }
-      acc += token.text.length;
-    });
-    tokens = nextTokens.slice();
-  });
-  return tokens;
-}
-
-export function generateHTML(tokens /*: Tokens */) /*: string */ {
-  return tokens
-    .map(token => `<span class="${token.className}">${escapeHtml(token.text)}</span>`)
-    .join('');
-}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/highlight.ts b/server/sonar-web/src/main/js/components/SourceViewer/helpers/highlight.ts
new file mode 100644 (file)
index 0000000..2f105f7
--- /dev/null
@@ -0,0 +1,126 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { uniq } from 'lodash';
+import { LinearIssueLocation } from '../../../app/types';
+
+export interface Token {
+  className: string;
+  markers: number[];
+  text: string;
+}
+
+const ISSUE_LOCATION_CLASS = 'source-line-code-issue';
+
+export function splitByTokens(code: string, rootClassName = ''): Token[] {
+  const container = document.createElement('div');
+  let tokens: Token[] = [];
+  container.innerHTML = code;
+  [].forEach.call(container.childNodes, (node: Element) => {
+    if (node.nodeType === 1) {
+      // ELEMENT NODE
+      const fullClassName = rootClassName ? rootClassName + ' ' + node.className : node.className;
+      const innerTokens = splitByTokens(node.innerHTML, fullClassName);
+      tokens = tokens.concat(innerTokens);
+    }
+    if (node.nodeType === 3 && node.nodeValue) {
+      // TEXT NODE
+      tokens.push({ className: rootClassName, markers: [], text: node.nodeValue });
+    }
+  });
+  return tokens;
+}
+
+export function highlightSymbol(tokens: Token[], symbol: string): Token[] {
+  const symbolRegExp = new RegExp(`\\b${symbol}\\b`);
+  return tokens.map(
+    token =>
+      symbolRegExp.test(token.className)
+        ? { ...token, className: `${token.className} highlighted` }
+        : token
+  );
+}
+
+/**
+ * Intersect two ranges
+ * @param s1 Start position of the first range
+ * @param e1 End position of the first range
+ * @param s2 Start position of the second range
+ * @param e2 End position of the second range
+ */
+function intersect(s1: number, e1: number, s2: number, e2: number) {
+  return { from: Math.max(s1, s2), to: Math.min(e1, e2) };
+}
+
+/**
+ * Get the substring of a string
+ * @param str A string
+ * @param from "From" offset
+ * @param to "To" offset
+ * @param acc Global offset to eliminate
+ */
+function part(str: string, from: number, to: number, acc: number): string {
+  // we do not want negative number as the first argument of `substr`
+  return from >= acc ? str.substr(from - acc, to - from) : str.substr(0, to - from);
+}
+
+/**
+ * Highlight issue locations in the list of tokens
+ */
+export function highlightIssueLocations(
+  tokens: Token[],
+  issueLocations: LinearIssueLocation[],
+  rootClassName: string = ISSUE_LOCATION_CLASS
+): Token[] {
+  issueLocations.forEach(location => {
+    const nextTokens: Token[] = [];
+    let acc = 0;
+    let markerAdded = location.line !== location.startLine;
+    tokens.forEach(token => {
+      const x = intersect(acc, acc + token.text.length, location.from, location.to);
+      const p1 = part(token.text, acc, x.from, acc);
+      const p2 = part(token.text, x.from, x.to, acc);
+      const p3 = part(token.text, x.to, acc + token.text.length, acc);
+      if (p1.length) {
+        nextTokens.push({ ...token, text: p1 });
+      }
+      if (p2.length) {
+        const newClassName =
+          token.className.indexOf(rootClassName) === -1
+            ? `${token.className} ${rootClassName}`
+            : token.className;
+        nextTokens.push({
+          className: newClassName,
+          markers:
+            !markerAdded && location.index != null
+              ? uniq([...token.markers, location.index])
+              : token.markers,
+          text: p2
+        });
+        markerAdded = true;
+      }
+      if (p3.length) {
+        nextTokens.push({ ...token, text: p3 });
+      }
+      acc += token.text.length;
+    });
+    tokens = nextTokens.slice();
+  });
+  return tokens;
+}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/indexing.js b/server/sonar-web/src/main/js/components/SourceViewer/helpers/indexing.js
deleted file mode 100644 (file)
index c936426..0000000
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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.
- */
-// @flow
-import { flatten } from 'lodash';
-import { splitByTokens } from './highlight';
-import { getLinearLocations } from './issueLocations';
-/*:: import type { Issue } from '../../issue/types'; */
-/*:: import type { SourceLine } from '../types'; */
-
-/*::
-export type LinearIssueLocation = {
-  from: number,
-  line: number,
-  to: number,
-  index?: number
-};
-*/
-
-/*::
-export type IndexedIssueLocation = {
-  from: number,
-  line: number,
-  to: number
-};
-*/
-
-/*::
-export type IndexedIssueLocationMessage = {
-  flowIndex: number,
-  locationIndex: number,
-  msg?: string
-};
-*/
-
-export const issuesByLine = (issues /*: Array<Issue> */) => {
-  const index = {};
-  issues.forEach(issue => {
-    const line = issue.textRange ? issue.textRange.endLine : 0;
-    if (!(line in index)) {
-      index[line] = [];
-    }
-    index[line].push(issue);
-  });
-  return index;
-};
-
-export function locationsByLine(
-  issues /*: Array<Issue> */
-) /*: { [number]: Array<LinearIssueLocation> } */ {
-  const index = {};
-  issues.forEach(issue => {
-    getLinearLocations(issue.textRange).forEach(location => {
-      if (!(location.line in index)) {
-        index[location.line] = [];
-      }
-      index[location.line].push(location);
-    });
-  });
-  return index;
-}
-
-export const duplicationsByLine = (duplications /*: Array<*> | null */) => {
-  if (duplications == null) {
-    return {};
-  }
-
-  const duplicationsByLine = {};
-
-  duplications.forEach(({ blocks }, duplicationIndex) => {
-    blocks.forEach(block => {
-      if (block._ref === '1') {
-        for (let line = block.from; line < block.from + block.size; line++) {
-          if (!(line in duplicationsByLine)) {
-            duplicationsByLine[line] = [];
-          }
-          duplicationsByLine[line].push(duplicationIndex);
-        }
-      }
-    });
-  });
-
-  return duplicationsByLine;
-};
-
-export const symbolsByLine = (sources /*: Array<SourceLine> */) => {
-  const index = {};
-  sources.forEach(line => {
-    const tokens = splitByTokens(line.code);
-    const symbols = flatten(
-      tokens.map(token => {
-        const keys = token.className.match(/sym-\d+/g);
-        return keys != null ? keys : [];
-      })
-    );
-    index[line.line] = symbols.filter(key => key);
-  });
-  return index;
-};
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/indexing.ts b/server/sonar-web/src/main/js/components/SourceViewer/helpers/indexing.ts
new file mode 100644 (file)
index 0000000..bf103f0
--- /dev/null
@@ -0,0 +1,87 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { flatten } from 'lodash';
+import { splitByTokens } from './highlight';
+import { getLinearLocations } from './issueLocations';
+import { Duplication, Issue, LinearIssueLocation, SourceLine } from '../../../app/types';
+
+export function issuesByLine(issues: Issue[]) {
+  const index: { [line: number]: Issue[] } = {};
+  issues.forEach(issue => {
+    const line = issue.textRange ? issue.textRange.endLine : 0;
+    if (!(line in index)) {
+      index[line] = [];
+    }
+    index[line].push(issue);
+  });
+  return index;
+}
+
+export function locationsByLine(issues: Issue[]) {
+  const index: { [line: number]: LinearIssueLocation[] } = {};
+  issues.forEach(issue => {
+    getLinearLocations(issue.textRange).forEach(location => {
+      if (!(location.line in index)) {
+        index[location.line] = [];
+      }
+      index[location.line].push(location);
+    });
+  });
+  return index;
+}
+
+export function duplicationsByLine(duplications: Duplication[] | undefined) {
+  if (duplications == null) {
+    return {};
+  }
+
+  const duplicationsByLine: { [line: number]: number[] } = {};
+
+  duplications.forEach(({ blocks }, duplicationIndex) => {
+    blocks.forEach(block => {
+      // eslint-disable-next-line no-underscore-dangle
+      if (block._ref === '1') {
+        for (let line = block.from; line < block.from + block.size; line++) {
+          if (!(line in duplicationsByLine)) {
+            duplicationsByLine[line] = [];
+          }
+          duplicationsByLine[line].push(duplicationIndex);
+        }
+      }
+    });
+  });
+
+  return duplicationsByLine;
+}
+
+export function symbolsByLine(sources: SourceLine[]) {
+  const index: { [line: number]: string[] } = {};
+  sources.forEach(line => {
+    const tokens = splitByTokens(line.code || '');
+    const symbols = flatten(
+      tokens.map(token => {
+        const keys = token.className.match(/sym-\d+/g);
+        return keys != null ? keys : [];
+      })
+    );
+    index[line.line] = symbols.filter(key => key);
+  });
+  return index;
+}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/issueLocations.js b/server/sonar-web/src/main/js/components/SourceViewer/helpers/issueLocations.js
deleted file mode 100644 (file)
index 2e47fa6..0000000
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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.
- */
-// @flow
-/*:: import type { TextRange, Issue } from '../../issue/types'; */
-
-export function getLinearLocations(
-  textRange /*: ?TextRange */
-) /*: Array<{ line: number, from: number, to: number }> */ {
-  if (!textRange) {
-    return [];
-  }
-  const locations = [];
-
-  // go through all lines of the `textRange`
-  for (let line = textRange.startLine; line <= textRange.endLine; line++) {
-    // TODO fix 999999
-    const from = line === textRange.startLine ? textRange.startOffset : 0;
-    const to = line === textRange.endLine ? textRange.endOffset : 999999;
-    locations.push({ line, from, to });
-  }
-  return locations;
-}
-
-/*::
-type Location = {
-  msg: string,
-  flowIndex: number,
-  locationIndex: number,
-  textRange?: TextRange,
-  index?: number
-}
-*/
-
-export function getIssueLocations(issue /*: Issue */) /*: Array<Location> */ {
-  const allLocations = [];
-  issue.flows.forEach((locations, flowIndex) => {
-    if (locations) {
-      const locationsCount = locations.length;
-      locations.forEach((location, index) => {
-        const flowLocation = {
-          ...location,
-          flowIndex,
-          locationIndex: index,
-          // set index only for real flows, do not set for just secondary locations
-          index: locationsCount > 1 ? locationsCount - index : undefined
-        };
-        allLocations.push(flowLocation);
-      });
-    }
-  });
-  return allLocations;
-}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/issueLocations.tsx b/server/sonar-web/src/main/js/components/SourceViewer/helpers/issueLocations.tsx
new file mode 100644 (file)
index 0000000..5c0f7ee
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { LinearIssueLocation, TextRange } from '../../../app/types';
+
+export function getLinearLocations(textRange: TextRange | undefined): LinearIssueLocation[] {
+  if (!textRange) {
+    return [];
+  }
+  const locations = [];
+
+  // go through all lines of the `textRange`
+  for (let line = textRange.startLine; line <= textRange.endLine; line++) {
+    // TODO fix 999999
+    const from = line === textRange.startLine ? textRange.startOffset : 0;
+    const to = line === textRange.endLine ? textRange.endOffset : 999999;
+    locations.push({ line, from, to });
+  }
+  return locations;
+}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.js b/server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.js
deleted file mode 100644 (file)
index f1950d1..0000000
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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.
- */
-// @flow
-import { searchIssues } from '../../../api/issues';
-import { parseIssueFromResponse } from '../../../helpers/issues';
-
-/*::
-export type Query = { [string]: string | void };
-*/
-
-/*::
-export type Issues = Array<*>; */
-
-// maximum possible value
-const PAGE_SIZE = 500;
-
-function buildQuery(component /*: string */, branch /*: string | void */) /*: Query */ {
-  return {
-    additionalFields: '_all',
-    resolved: 'false',
-    componentKeys: component,
-    branch,
-    s: 'FILE_LINE'
-  };
-}
-
-export function loadPage(
-  query /*: Query */,
-  page /*: number */,
-  pageSize /*: number */ = PAGE_SIZE
-) /*: Promise<Issues> */ {
-  return searchIssues({
-    ...query,
-    p: page,
-    ps: pageSize
-  }).then(r =>
-    r.issues.map(issue => parseIssueFromResponse(issue, r.components, r.users, r.rules))
-  );
-}
-
-export function loadPageAndNext(
-  query /*: Query */,
-  toLine /*: number */,
-  page /*: number */,
-  pageSize /*: number */ = PAGE_SIZE
-) /*: Promise<Issues> */ {
-  return loadPage(query, page).then(issues => {
-    if (issues.length === 0) {
-      return [];
-    }
-
-    const lastIssue = issues[issues.length - 1];
-
-    if (
-      (lastIssue.textRange != null && lastIssue.textRange.endLine > toLine) ||
-      issues.length < pageSize
-    ) {
-      return issues;
-    }
-
-    return loadPageAndNext(query, toLine, page + 1, pageSize).then(nextIssues => {
-      return [...issues, ...nextIssues];
-    });
-  });
-}
-
-export default function loadIssues(
-  component /*: string */,
-  fromLine /*: number */,
-  toLine /*: number */,
-  branch /*: string | void */
-) /*: Promise<Issues> */ {
-  const query = buildQuery(component, branch);
-  return new Promise(resolve => {
-    loadPageAndNext(query, toLine, 1).then(issues => {
-      resolve(issues);
-    });
-  });
-}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.tsx b/server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.tsx
new file mode 100644 (file)
index 0000000..8354099
--- /dev/null
@@ -0,0 +1,86 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { searchIssues } from '../../../api/issues';
+import { Issue } from '../../../app/types';
+import { parseIssueFromResponse } from '../../../helpers/issues';
+import { RawQuery } from '../../../helpers/query';
+
+// maximum possible value
+const PAGE_SIZE = 500;
+
+function buildQuery(component: string, branch: string | undefined) {
+  return {
+    additionalFields: '_all',
+    resolved: 'false',
+    componentKeys: component,
+    branch,
+    s: 'FILE_LINE'
+  };
+}
+
+export function loadPage(query: RawQuery, page: number, pageSize = PAGE_SIZE): Promise<Issue[]> {
+  return searchIssues({
+    ...query,
+    p: page,
+    ps: pageSize
+  }).then(r =>
+    r.issues.map(issue => parseIssueFromResponse(issue, r.components, r.users, r.rules))
+  );
+}
+
+export function loadPageAndNext(
+  query: RawQuery,
+  toLine: number,
+  page: number,
+  pageSize = PAGE_SIZE
+): Promise<Issue[]> {
+  return loadPage(query, page).then(issues => {
+    if (issues.length === 0) {
+      return [];
+    }
+
+    const lastIssue = issues[issues.length - 1];
+
+    if (
+      (lastIssue.textRange != null && lastIssue.textRange.endLine > toLine) ||
+      issues.length < pageSize
+    ) {
+      return issues;
+    }
+
+    return loadPageAndNext(query, toLine, page + 1, pageSize).then(nextIssues => {
+      return [...issues, ...nextIssues];
+    });
+  });
+}
+
+export default function loadIssues(
+  component: string,
+  _fromLine: number,
+  toLine: number,
+  branch: string | undefined
+): Promise<Issue[]> {
+  const query = buildQuery(component, branch);
+  return new Promise(resolve => {
+    loadPageAndNext(query, toLine, 1).then(issues => {
+      resolve(issues);
+    });
+  });
+}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/popups/coverage-popup.js b/server/sonar-web/src/main/js/components/SourceViewer/popups/coverage-popup.js
deleted file mode 100644 (file)
index 694182c..0000000
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 $ from 'jquery';
-import { groupBy } from 'lodash';
-import Template from './templates/source-viewer-coverage-popup.hbs';
-import Popup from '../../common/popup';
-
-export default Popup.extend({
-  template: Template,
-
-  events: {
-    'click a[data-key]': 'goToFile'
-  },
-
-  onRender() {
-    Popup.prototype.onRender.apply(this, arguments);
-    this.$('.bubble-popup-container').isolatedScroll();
-  },
-
-  goToFile(e) {
-    e.stopPropagation();
-    const key = $(e.currentTarget).data('key');
-    const Workspace = require('../../workspace/main').default;
-    Workspace.openComponent({ key, branch: this.options.branch });
-  },
-
-  serializeData() {
-    const row = this.options.line || {};
-    const tests = groupBy(this.options.tests, 'fileKey');
-    const testFiles = Object.keys(tests).map(fileKey => {
-      const testSet = tests[fileKey];
-      const test = testSet[0];
-      return {
-        file: {
-          key: test.fileKey,
-          longName: test.fileName
-        },
-        tests: testSet
-      };
-    });
-    return { testFiles, row };
-  }
-});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/popups/duplication-popup.js b/server/sonar-web/src/main/js/components/SourceViewer/popups/duplication-popup.js
deleted file mode 100644 (file)
index f45a5dc..0000000
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 $ from 'jquery';
-import { groupBy, sortBy } from 'lodash';
-import Template from './templates/source-viewer-duplication-popup.hbs';
-import Popup from '../../common/popup';
-
-export default Popup.extend({
-  template: Template,
-
-  events: {
-    'click a[data-key]': 'goToFile'
-  },
-
-  goToFile(e) {
-    e.stopPropagation();
-    const key = $(e.currentTarget).data('key');
-    const line = $(e.currentTarget).data('line');
-    const Workspace = require('../../workspace/main').default;
-    Workspace.openComponent({ key, line, branch: this.options.branch });
-  },
-
-  serializeData() {
-    const that = this;
-    const groupedBlocks = groupBy(this.options.blocks, '_ref');
-    let duplications = Object.keys(groupedBlocks).map(fileRef => {
-      return {
-        blocks: groupedBlocks[fileRef],
-        file: this.options.files[fileRef]
-      };
-    });
-    duplications = sortBy(duplications, d => {
-      const a = d.file.projectName !== that.options.component.projectName;
-      const b = d.file.subProjectName !== that.options.component.subProjectName;
-      const c = d.file.key !== that.options.component.key;
-      return '' + a + b + c;
-    });
-    return {
-      duplications,
-      component: this.options.component,
-      inRemovedComponent: this.options.inRemovedComponent
-    };
-  }
-});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/popups/line-actions-popup.js b/server/sonar-web/src/main/js/components/SourceViewer/popups/line-actions-popup.js
deleted file mode 100644 (file)
index 33d44cf..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 Template from './templates/source-viewer-line-options-popup.hbs';
-import Popup from '../../common/popup';
-
-export default Popup.extend({
-  template: Template,
-
-  serializeData() {
-    const { component, line, branch } = this.options;
-    let permalink =
-      window.baseUrl + `/component?id=${encodeURIComponent(component.key)}&line=${line}`;
-    if (branch) {
-      permalink += `&branch=${encodeURIComponent(branch)}`;
-    }
-    return { permalink };
-  }
-});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/popups/scm-popup.js b/server/sonar-web/src/main/js/components/SourceViewer/popups/scm-popup.js
deleted file mode 100644 (file)
index 5478c6f..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 Template from './templates/source-viewer-scm-popup.hbs';
-import Popup from '../../common/popup';
-
-export default Popup.extend({
-  template: Template,
-
-  events: {
-    click: 'onClick'
-  },
-
-  onRender() {
-    Popup.prototype.onRender.apply(this, arguments);
-    this.$('.bubble-popup-container').isolatedScroll();
-  },
-
-  onClick(e) {
-    e.stopPropagation();
-  },
-
-  serializeData() {
-    return {
-      ...Popup.prototype.serializeData.apply(this, arguments),
-      line: this.options.line
-    };
-  }
-});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-coverage-popup.hbs b/server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-coverage-popup.hbs
deleted file mode 100644 (file)
index bad2779..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-<div class="bubble-popup-container">
-  <div class="bubble-popup-title">
-    {{t 'source_viewer.covered'}}
-    {{#if row.conditions}}
-      ({{default row.coveredConditions 0}} of {{row.conditions}} {{t 'source_viewer.conditions'}})
-    {{/if}}
-  </div>
-  {{#each testFiles}}
-    <div class="bubble-popup-section">
-      <a class="component-viewer-popup-test-file link-action" data-key="{{file.key}}" title="{{file.longName}}">
-        <span>{{collapsePath file.longName}}</span>
-      </a>
-      <ul class="bubble-popup-list">
-        {{#each tests}}
-          <li class="component-viewer-popup-test" title="{{name}}">
-            <i class="component-viewer-popup-test-status {{testStatusIconClass status}}"></i>
-            <span class="component-viewer-popup-test-name">
-              <a class="component-viewer-popup-test-file link-action" title="{{name}}"
-                data-key="{{../file.key}}" data-method="{{name}}">
-                {{name}}
-              </a>
-            </span>
-            <span class="component-viewer-popup-test-duration">{{durationInMs}}ms</span>
-          </li>
-        {{/each}}
-      </ul>
-    </div>
-  {{else}}
-    {{t 'source_viewer.tooltip.no_information_about_tests'}}
-  {{/each}}
-</div>
-
-<div class="bubble-popup-arrow"></div>
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-duplication-popup.hbs b/server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-duplication-popup.hbs
deleted file mode 100644 (file)
index ea8fc2b..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-<div class="bubble-popup-container">
-  {{#if inRemovedComponent}}
-    <div class="alert alert-warning spacer-bottom">{{t 'duplications.dups_found_on_deleted_resource'}}</div>
-  {{/if}}
-  {{#notEmpty duplications}}
-    <div class="bubble-popup-title">{{t 'component_viewer.transition.duplication'}}</div>
-    {{#each duplications}}
-      <div class="bubble-popup-section">
-        <div class="component-name">
-          {{#notEqComponents file ../component}}
-            <div class="component-name-parent">
-              <i class="icon-qualifier-trk"></i>&nbsp;<a href="{{dashboardUrl file.project}}">{{file.projectName}}</a>
-            </div>
-            {{#if file.subProjectName}}
-              <div class="component-name-parent">
-                <i class="icon-qualifier-trk"></i>&nbsp;<a
-                  href="{{dashboardUrl file.subProject}}">{{file.subProjectName}}</a>
-              </div>
-            {{/if}}
-          {{/notEqComponents}}
-
-          {{#notEq file.key ../component.key}}
-            <div class="component-name-path">
-              <a class="link-action" data-key="{{file.key}}" title="{{file.name}}">
-                <span>{{collapsedDirFromPath file.name}}</span><span
-                  class="component-name-file">{{fileFromPath file.name}}</span>
-              </a>
-            </div>
-          {{/notEq}}
-
-          <div class="component-name-path">
-            Lines:
-            {{#joinEach blocks ','}}
-              <a class="link-action" data-key="{{../file.key}}" data-line="{{this.from}}">
-                {{this.from}} – {{sum from size -1}}
-              </a>
-            {{/joinEach}}
-          </div>
-        </div>
-      </div>
-    {{/each}}
-  {{/notEmpty}}
-</div>
-
-<div class="bubble-popup-arrow"></div>
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-line-options-popup.hbs b/server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-line-options-popup.hbs
deleted file mode 100644 (file)
index cefd312..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-<div class="bubble-popup-container">
-  <div class="bubble-popup-section">
-    <a href={{permalink}} target="_blank" class="js-get-permalink link-action">{{t 'component_viewer.get_permalink'}}</a>
-  </div>
-</div>
-
-<div class="bubble-popup-arrow"></div>
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-scm-popup.hbs b/server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-scm-popup.hbs
deleted file mode 100644 (file)
index dd82aca..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-<div class="bubble-popup-container">
-  <div class="bubble-popup-section">
-    {{line.scmAuthor}}
-  </div>
-  <div class="bubble-popup-section">
-    {{dt line.scmDate}}
-  </div>
-  {{#if line.scmRevision}}
-    <div class="bubble-popup-section">
-      {{line.scmRevision}}
-    </div>
-  {{/if}}
-</div>
-
-<div class="bubble-popup-arrow"></div>
index 3bcdcdf4415227e481f4a7fa42c59620fbb2eb6e..73521086ad3e49efc9a0cc31029613cb8c8c81af 100644 (file)
@@ -94,7 +94,8 @@
 }
 
 .source-viewer pre,
-.source-meta {
+.source-line-number,
+.source-line-scm {
   line-height: 18px;
   font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;
   font-size: var(--smallFontSize);
 }
 
 .source-meta {
+  position: relative;
   vertical-align: top;
   width: 1px;
   background-clip: padding-box;
   border-top: 1px solid var(--barBorderColor);
 }
 
+.source-viewer-bubble-popup {
+  top: -16px;
+  left: 100%;
+  width: 480px;
+  font-family: var(--baseFontFamily);
+  font-size: var(--baseFontSize);
+  text-align: left;
+}
+
 .issue-location {
   display: inline-block;
   vertical-align: top;
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/types.js b/server/sonar-web/src/main/js/components/SourceViewer/types.js
deleted file mode 100644 (file)
index 63b638a..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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.
- */
-// @flow
-/*::
-export type SourceLine = {
-  code: string,
-  conditions?: number,
-  coverageStatus?: string | null,
-  coveredConditions?: number,
-  duplicated: boolean,
-  line: number,
-  lineHits?: number,
-  scmAuthor?: string,
-  scmDate?: string,
-  scmRevision?: string
-};
-*/
-
-/*::
-export type Duplication = {
-  blocks: Array<{
-    _ref: string,
-    from: number,
-    size: number
-  }>
-};
-*/
index 2cfb88cee4c36b902dcae26f62fcafcac7de6855..627dad9cde527f25df90c99c18f14528b4c8beaa 100644 (file)
@@ -25,7 +25,7 @@ interface Props {
   children?: React.ReactNode;
   isOpen: boolean;
   offset?: { vertical: number; horizontal: number };
-  popup: React.ReactElement<any>;
+  popup: JSX.Element;
   position: 'bottomleft' | 'bottomright';
   togglePopup: (show: boolean) => void;
 }
@@ -92,10 +92,10 @@ export default class BubblePopupHelper extends React.PureComponent<Props, State>
     return (
       <div
         className={classNames(this.props.className, 'bubble-popup-helper')}
-        ref={container => (this.container = container)}
         onClick={this.handleClick}
-        tabIndex={0}
-        role="tooltip">
+        ref={container => (this.container = container)}
+        role="tooltip"
+        tabIndex={0}>
         {this.props.children}
         {this.props.isOpen && (
           <div ref={popupContainer => (this.popupContainer = popupContainer)}>
index 21e7535b690028072fa6579ded2494e34a674541..97bdefa079ab65a939948684af074bbab4957306 100644 (file)
@@ -27,7 +27,7 @@
   border-radius: 2px;
   background-color: #d18582;
   color: #fff;
-  font-family: 'Helvetica Neue', 'Segoe UI', Helvetica, Arial, sans-serif;
+  font-family: var(--baseFontFamily);
   font-size: var(--smallFontSize);
   transition: background-color 0.3s ease;
 }
index 19a9af840715427e2308f65bef7d316ab9e1fac8..29fa3a17c3fac1545bb7e002b0bcafa9b31572d5 100644 (file)
@@ -25,7 +25,7 @@
   border-radius: 2px;
   background-color: #9e9e9e;
   color: #fff;
-  font-family: 'Helvetica Neue', 'Segoe UI', Helvetica, Arial, sans-serif;
+  font-family: var(--baseFontFamily);
   font-size: var(--smallFontSize);
   text-overflow: ellipsis;
   overflow: hidden;
diff --git a/server/sonar-web/src/main/js/components/common/popup.js b/server/sonar-web/src/main/js/components/common/popup.js
deleted file mode 100644 (file)
index b9a988b..0000000
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 $ from 'jquery';
-import Marionette from 'backbone.marionette';
-import key from 'keymaster';
-
-export default Marionette.ItemView.extend({
-  className: 'bubble-popup',
-
-  onRender() {
-    this.$el.detach().appendTo($('body'));
-    const triggerEl = $(this.options.triggerEl);
-    if (this.options.bottom) {
-      this.$el.addClass('bubble-popup-bottom');
-      this.$el.css({
-        top: triggerEl.offset().top + triggerEl.outerHeight(),
-        left: triggerEl.offset().left
-      });
-    } else if (this.options.bottomRight) {
-      this.$el.addClass('bubble-popup-bottom-right');
-      this.$el.css({
-        top: triggerEl.offset().top + triggerEl.outerHeight(),
-        right: $(window).width() - triggerEl.offset().left - triggerEl.outerWidth()
-      });
-    } else {
-      this.$el.css({
-        top: triggerEl.offset().top,
-        left: triggerEl.offset().left + triggerEl.outerWidth()
-      });
-    }
-    this.attachCloseEvents();
-  },
-
-  attachCloseEvents() {
-    const that = this;
-    const triggerEl = $(this.options.triggerEl);
-    key('escape', () => {
-      that.destroy();
-    });
-    $('body').on('click.bubble-popup', () => {
-      $('body').off('click.bubble-popup');
-      that.destroy();
-    });
-    triggerEl.on('click.bubble-popup', e => {
-      triggerEl.off('click.bubble-popup');
-      e.stopPropagation();
-      that.destroy();
-    });
-  },
-
-  onDestroy() {
-    $('body').off('click.bubble-popup');
-    const triggerEl = $(this.options.triggerEl);
-    triggerEl.off('click.bubble-popup');
-  }
-});
diff --git a/server/sonar-web/src/main/js/components/issue/Issue.d.ts b/server/sonar-web/src/main/js/components/issue/Issue.d.ts
new file mode 100644 (file)
index 0000000..5abf041
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { Issue as IssueType } from '../../app/types';
+
+interface IssueProps {
+  branch?: string;
+  checked?: boolean;
+  displayLocationsCount?: boolean;
+  displayLocationsLink?: boolean;
+  issue: IssueType;
+  onChange: (issue: IssueType) => void;
+  onCheck?: (issueKey: string) => void;
+  onClick: (issueKey: string) => void;
+  onFilter?: (property: string, issue: IssueType) => void;
+  onPopupToggle: (issue: string, popupName: string, open?: boolean) => void;
+  openPopup?: string;
+  selected: boolean;
+}
+
+export default class Issue extends React.PureComponent<IssueProps> {}
index e7910c837e3ed2445f741b0e835611b34cb69f92..a1ccedd665c0f34da76c8b18118af5c79fdc263d 100644 (file)
@@ -19,6 +19,7 @@
  */
 import { flatten, sortBy } from 'lodash';
 import { SEVERITIES } from './constants';
+import { Issue } from '../app/types';
 
 interface TextRange {
   startLine: number;
@@ -67,8 +68,6 @@ export interface RawIssue extends IssueBase {
   textRange?: TextRange;
 }
 
-interface Issue extends IssueBase {}
-
 export function sortBySeverity(issues: Issue[]): Issue[] {
   return sortBy(issues, issue => SEVERITIES.indexOf(issue.severity));
 }
@@ -173,5 +172,5 @@ export function parseIssueFromResponse(
     ...ensureTextRange(issue),
     secondaryLocations,
     flows
-  };
+  } as Issue;
 }
diff --git a/server/sonar-web/src/main/js/store/favorites/duck.js b/server/sonar-web/src/main/js/store/favorites/duck.js
deleted file mode 100644 (file)
index 3015e25..0000000
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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.
- */
-// @flow
-import { uniq, without } from 'lodash';
-
-/*::
-type Favorite = { key: string };
-*/
-
-/*::
-type ReceiveFavoritesAction = {
-  type: 'RECEIVE_FAVORITES',
-  favorites: Array<Favorite>,
-  notFavorites: Array<Favorite>
-};
-*/
-
-/*::
-type AddFavoriteAction = {
-  type: 'ADD_FAVORITE',
-  componentKey: string
-};
-*/
-
-/*::
-type RemoveFavoriteAction = {
-  type: 'REMOVE_FAVORITE',
-  componentKey: string
-};
-*/
-
-/*::
-type Action = ReceiveFavoritesAction | AddFavoriteAction | RemoveFavoriteAction;
-*/
-
-/*::
-type State = Array<string>;
-*/
-
-export const actions = {
-  RECEIVE_FAVORITES: 'RECEIVE_FAVORITES',
-  ADD_FAVORITE: 'ADD_FAVORITE',
-  REMOVE_FAVORITE: 'REMOVE_FAVORITE'
-};
-
-export function receiveFavorites(
-  favorites /*: Array<Favorite> */,
-  notFavorites /*: Array<Favorite> */ = []
-) /*: ReceiveFavoritesAction */ {
-  return {
-    type: actions.RECEIVE_FAVORITES,
-    favorites,
-    notFavorites
-  };
-}
-
-export function addFavorite(componentKey /*: string */) /*: AddFavoriteAction */ {
-  return {
-    type: actions.ADD_FAVORITE,
-    componentKey
-  };
-}
-
-export function removeFavorite(componentKey /*: string */) /*: RemoveFavoriteAction */ {
-  return {
-    type: actions.REMOVE_FAVORITE,
-    componentKey
-  };
-}
-
-export default function(state /*: State */ = [], action /*: Action */) /*: State */ {
-  if (action.type === actions.RECEIVE_FAVORITES) {
-    const toAdd = action.favorites.map(f => f.key);
-    const toRemove = action.notFavorites.map(f => f.key);
-    return without(uniq([...state, ...toAdd]), ...toRemove);
-  }
-
-  if (action.type === actions.ADD_FAVORITE) {
-    return uniq([...state, action.componentKey]);
-  }
-
-  if (action.type === actions.REMOVE_FAVORITE) {
-    return without(state, action.componentKey);
-  }
-
-  return state;
-}
-
-export function isFavorite(state /*: State */, componentKey /*: string */) {
-  return state.includes(componentKey);
-}
diff --git a/server/sonar-web/src/main/js/store/favorites/duck.ts b/server/sonar-web/src/main/js/store/favorites/duck.ts
new file mode 100644 (file)
index 0000000..710151e
--- /dev/null
@@ -0,0 +1,81 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { uniq, without } from 'lodash';
+
+interface Favorite {
+  key: string;
+}
+
+interface ReceiveFavoritesAction {
+  type: 'RECEIVE_FAVORITES';
+  favorites: Array<Favorite>;
+  notFavorites: Array<Favorite>;
+}
+
+interface AddFavoriteAction {
+  type: 'ADD_FAVORITE';
+  componentKey: string;
+}
+
+interface RemoveFavoriteAction {
+  type: 'REMOVE_FAVORITE';
+  componentKey: string;
+}
+
+type Action = ReceiveFavoritesAction | AddFavoriteAction | RemoveFavoriteAction;
+
+type State = string[];
+
+export function receiveFavorites(
+  favorites: Favorite[],
+  notFavorites: Favorite[] = []
+): ReceiveFavoritesAction {
+  return { type: 'RECEIVE_FAVORITES', favorites, notFavorites };
+}
+
+export function addFavorite(componentKey: string): AddFavoriteAction {
+  return { type: 'ADD_FAVORITE', componentKey };
+}
+
+export function removeFavorite(componentKey: string): RemoveFavoriteAction {
+  return { type: 'REMOVE_FAVORITE', componentKey };
+}
+
+export default function(state: State = [], action: Action): State {
+  if (action.type === 'RECEIVE_FAVORITES') {
+    const toAdd = action.favorites.map(f => f.key);
+    const toRemove = action.notFavorites.map(f => f.key);
+    return without(uniq([...state, ...toAdd]), ...toRemove);
+  }
+
+  if (action.type === 'ADD_FAVORITE') {
+    return uniq([...state, action.componentKey]);
+  }
+
+  if (action.type === 'REMOVE_FAVORITE') {
+    return without(state, action.componentKey);
+  }
+
+  return state;
+}
+
+export function isFavorite(state: State, componentKey: string) {
+  return state.includes(componentKey);
+}