]> source.dussan.org Git - sonarqube.git/commitdiff
refactor code page (#912)
authorStas Vilchik <vilchiks@gmail.com>
Mon, 30 May 2016 14:49:22 +0000 (16:49 +0200)
committerStas Vilchik <vilchiks@gmail.com>
Mon, 30 May 2016 14:49:22 +0000 (16:49 +0200)
29 files changed:
it/it-tests/src/test/java/it/sourceCode/ProjectCodeTest.java
it/it-tests/src/test/resources/sourceCode/ProjectCodeTest/code_page_should_expand_root_dir.html
it/it-tests/src/test/resources/sourceCode/ProjectCodeTest/permalink.html [new file with mode: 0644]
it/it-tests/src/test/resources/sourceCode/ProjectCodeTest/search.html [new file with mode: 0644]
server/sonar-web/src/main/js/apps/code/actions/index.js [deleted file]
server/sonar-web/src/main/js/apps/code/app.js
server/sonar-web/src/main/js/apps/code/bucket.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/code/code.css [new file with mode: 0644]
server/sonar-web/src/main/js/apps/code/components/App.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/code/components/Breadcrumb.js
server/sonar-web/src/main/js/apps/code/components/Breadcrumbs.js
server/sonar-web/src/main/js/apps/code/components/Code.js [deleted file]
server/sonar-web/src/main/js/apps/code/components/Component.js
server/sonar-web/src/main/js/apps/code/components/ComponentName.js
server/sonar-web/src/main/js/apps/code/components/Components.js
server/sonar-web/src/main/js/apps/code/components/ComponentsEmpty.js
server/sonar-web/src/main/js/apps/code/components/Search.js
server/sonar-web/src/main/js/apps/code/components/SourceViewer.js [deleted file]
server/sonar-web/src/main/js/apps/code/reducers/index.js [deleted file]
server/sonar-web/src/main/js/apps/code/store/configureStore.js [deleted file]
server/sonar-web/src/main/js/apps/code/styles/code.css [deleted file]
server/sonar-web/src/main/js/apps/code/utils.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ListView.js
server/sonar-web/src/main/js/apps/component-measures/details/drilldown/TreeView.js
server/sonar-web/src/main/js/components/source-viewer/SourceViewer.js [new file with mode: 0644]
server/sonar-web/src/main/js/helpers/measures.js
server/sonar-web/src/main/js/main/nav/component/component-nav-menu.js
server/sonar-web/tests/apps/code/components-test.js [deleted file]
server/sonar-web/tests/apps/code/store-test.js [deleted file]

index bd794bf66820cfe49c42a013e1ad27a52f94c7e4..c48b11d63804449f0633e746c053cf90dbed6a10 100644 (file)
@@ -39,7 +39,9 @@ public class ProjectCodeTest {
     executeBuild("shared/xoo-sample", "project-for-code", "Project For Code");
 
     Selenese selenese = Selenese.builder().setHtmlTestsInClasspath("test_project_code_page",
-      "/sourceCode/ProjectCodeTest/test_project_code_page.html"
+      "/sourceCode/ProjectCodeTest/test_project_code_page.html",
+      "/sourceCode/ProjectCodeTest/search.html",
+      "/sourceCode/ProjectCodeTest/permalink.html"
     ).build();
     new SeleneseTest(selenese).runOn(orchestrator);
   }
index 4cc72851fb53ddc9bdef02060bc6786fe6c00dcb..c9737d5fe25b5585fdb8af76424b7b93fdde9b9e 100644 (file)
@@ -22,7 +22,7 @@
 <tr>
        <td>waitForText</td>
        <td>css=#content</td>
-       <td>*Hello.xoo*</td>
+       <td>*Hello.xoo*src/main/xoo/sample*</td>
 </tr>
 </tbody>
 </table>
diff --git a/it/it-tests/src/test/resources/sourceCode/ProjectCodeTest/permalink.html b/it/it-tests/src/test/resources/sourceCode/ProjectCodeTest/permalink.html
new file mode 100644 (file)
index 0000000..69364a6
--- /dev/null
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+  <link rel="selenium.base" href="http://localhost:49506"/>
+  <title>test_project_code_page</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+  <thead>
+  <tr>
+    <td rowspan="1" colspan="3">test_project_code_page</td>
+  </tr>
+  </thead>
+  <tbody>
+  <tr>
+       <td>open</td>
+       <td>/code?id=project-for-code&amp;selected=project-for-code%3Asrc%2Fmain%2Fxoo%2Fsample%2FSample.xoo</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForText</td>
+       <td>css=#content</td>
+       <td>*public class Sample*</td>
+</tr>
+<tr>
+       <td>waitForText</td>
+       <td>css=.code-breadcrumbs</td>
+       <td>*Project For Code*src/main/xoo/sample*Sample.xoo*</td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/it/it-tests/src/test/resources/sourceCode/ProjectCodeTest/search.html b/it/it-tests/src/test/resources/sourceCode/ProjectCodeTest/search.html
new file mode 100644 (file)
index 0000000..1594ee2
--- /dev/null
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+  <link rel="selenium.base" href="http://localhost:49506"/>
+  <title>test_project_code_page</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+  <thead>
+  <tr>
+    <td rowspan="1" colspan="3">test_project_code_page</td>
+  </tr>
+  </thead>
+  <tbody>
+  <tr>
+       <td>open</td>
+       <td>/code?id=project-for-code</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForText</td>
+       <td>css=#content</td>
+       <td>*Project For Code*13*0*0*0.0%*</td>
+</tr>
+<tr>
+       <td>type</td>
+       <td>css=.search-box-input</td>
+       <td>xoo</td>
+</tr>
+<tr>
+       <td>click</td>
+       <td>css=.search-box-submit</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForText</td>
+       <td>css=#content</td>
+       <td>*Sample.xoo*</td>
+</tr>
+<tr>
+       <td>click</td>
+       <td>css=.code-name-cell a</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForText</td>
+       <td>css=#content</td>
+       <td>*public class Sample*</td>
+</tr>
+<tr>
+       <td>waitForText</td>
+       <td>css=.code-breadcrumbs</td>
+       <td>*Project For Code*src/main/xoo/sample*Sample.xoo*</td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/server/sonar-web/src/main/js/apps/code/actions/index.js b/server/sonar-web/src/main/js/apps/code/actions/index.js
deleted file mode 100644 (file)
index 2422d99..0000000
+++ /dev/null
@@ -1,271 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2016 SonarSource SA
- * mailto:contact 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 'underscore';
-import { pushPath, replacePath } from 'redux-simple-router';
-
-import { getChildren, getComponent, getTree, getBreadcrumbs } from '../../../api/components';
-import { translate } from '../../../helpers/l10n';
-import { getComponentUrl } from '../../../helpers/urls';
-
-const METRICS = [
-  'ncloc',
-  'code_smells',
-  'bugs',
-  'vulnerabilities',
-  'duplicated_lines_density',
-  'alert_status'
-];
-
-const METRICS_WITH_COVERAGE = [
-  ...METRICS,
-  'coverage',
-  'it_coverage',
-  'overall_coverage'
-];
-
-const PAGE_SIZE = 100;
-
-export const INIT = 'INIT';
-export const BROWSE = 'BROWSE';
-export const LOAD_MORE = 'LOAD_MORE';
-export const SEARCH = 'SEARCH';
-export const SELECT_NEXT = 'SELECT_NEXT';
-export const SELECT_PREV = 'SELECT_PREV';
-export const UPDATE_QUERY = 'UPDATE_QUERY';
-export const START_FETCHING = 'START_FETCHING';
-export const STOP_FETCHING = 'STOP_FETCHING';
-export const RAISE_ERROR = 'RAISE_ERROR';
-
-export function initComponentAction (component, breadcrumbs = []) {
-  return {
-    type: INIT,
-    component,
-    breadcrumbs
-  };
-}
-
-export function browseAction (component, children = [], breadcrumbs = [], total = 0) {
-  return {
-    type: BROWSE,
-    component,
-    children,
-    breadcrumbs,
-    total
-  };
-}
-
-export function loadMoreAction (children, page) {
-  return {
-    type: LOAD_MORE,
-    children,
-    page
-  };
-}
-
-export function searchAction (components) {
-  return {
-    type: SEARCH,
-    components
-  };
-}
-
-export function updateQueryAction (query) {
-  return {
-    type: UPDATE_QUERY,
-    query
-  };
-}
-
-export function selectNext () {
-  return { type: SELECT_NEXT };
-}
-
-export function selectPrev () {
-  return { type: SELECT_PREV };
-}
-
-export function startFetching () {
-  return { type: START_FETCHING };
-}
-
-export function stopFetching () {
-  return { type: STOP_FETCHING };
-}
-
-export function raiseError (message) {
-  return {
-    type: RAISE_ERROR,
-    message
-  };
-}
-
-function getPath (componentKey) {
-  return '/' + encodeURIComponent(componentKey);
-}
-
-function expandRootDir ({ children, total, ...other }) {
-  const rootDir = children.find(component => component.qualifier === 'DIR' && component.name === '/');
-  if (rootDir) {
-    return getChildren(rootDir.key, METRICS_WITH_COVERAGE).then(r => {
-      const nextChildren = _.without([...children, ...r.components], rootDir);
-      const nextTotal = total + r.components.length - /* root dir */ 1;
-      return { children: nextChildren, total: nextTotal, ...other };
-    });
-  } else {
-    return { children, total, ...other };
-  }
-}
-
-function prepareChildren (r) {
-  return { children: r.components, total: r.paging.total, page: r.paging.pageIndex };
-}
-
-function skipRootDir (breadcrumbs) {
-  return breadcrumbs.filter(component => {
-    return !(component.qualifier === 'DIR' && component.name === '/');
-  });
-}
-
-function retrieveComponentBase (componentKey, candidate) {
-  return candidate ?
-      Promise.resolve(candidate) :
-      getComponent(componentKey, METRICS_WITH_COVERAGE);
-}
-
-function retrieveComponentChildren (componentKey, candidate) {
-  return candidate && candidate.children ?
-      Promise.resolve({ children: candidate.children, total: candidate.total }) :
-      getChildren(componentKey, METRICS_WITH_COVERAGE, { ps: PAGE_SIZE }).then(prepareChildren).then(expandRootDir);
-}
-
-function retrieveComponentBreadcrumbs (componentKey, candidate) {
-  return candidate && candidate.breadcrumbs ?
-      Promise.resolve(candidate.breadcrumbs) :
-      getBreadcrumbs({ key: componentKey }).then(skipRootDir);
-}
-
-function retrieveComponent (componentKey, bucket) {
-  const candidate = _.findWhere(bucket, { key: componentKey });
-  return Promise.all([
-    retrieveComponentBase(componentKey, candidate),
-    retrieveComponentChildren(componentKey, candidate),
-    retrieveComponentBreadcrumbs(componentKey, candidate)
-  ]);
-}
-
-function requestTree (query, baseComponent, dispatch) {
-  dispatch(startFetching());
-  return getTree(baseComponent.key, { q: query, s: 'qualifier,name' })
-      .then(r => dispatch(searchAction(r.components)))
-      .then(() => dispatch(stopFetching()));
-}
-
-async function getErrorMessage (response) {
-  switch (response.status) {
-    case 401:
-      return translate('not_authorized');
-    default:
-      try {
-        const json = await response.json();
-        return json['err_msg'] ||
-            (json.errors && _.pluck(json.errors, 'msg').join('. ')) ||
-            translate('default_error_message');
-      } catch (e) {
-        return translate('default_error_message');
-      }
-  }
-}
-
-export function initComponent (componentKey, breadcrumbs) {
-  return dispatch => {
-    dispatch(startFetching());
-    return getComponent(componentKey, METRICS_WITH_COVERAGE)
-        .then(component => dispatch(initComponentAction(component, breadcrumbs)))
-        .then(() => dispatch(replacePath(getPath(componentKey))))
-        .then(() => dispatch(stopFetching()));
-  };
-}
-
-export function browse (componentKey) {
-  return (dispatch, getState) => {
-    const { bucket } = getState();
-    dispatch(startFetching());
-    return retrieveComponent(componentKey, bucket)
-        .then(([component, children, breadcrumbs]) => {
-          if (component.refKey) {
-            window.location = getComponentUrl(component.refKey);
-            return new Promise();
-          } else {
-            dispatch(browseAction(component, children.children, breadcrumbs, children.total));
-          }
-        })
-        .then(() => dispatch(pushPath(getPath(componentKey))))
-        .then(() => dispatch(stopFetching()))
-        .catch(e => {
-          getErrorMessage(e.response)
-              .then(message => dispatch(raiseError(message)));
-        });
-  };
-}
-
-export function loadMore () {
-  return (dispatch, getState) => {
-    const { baseComponent, page } = getState().current;
-    return getChildren(baseComponent.key, METRICS_WITH_COVERAGE, { p: page + 1, ps: PAGE_SIZE })
-        .then(prepareChildren)
-        .then(({ children }) => {
-          dispatch(loadMoreAction(children, page + 1));
-          dispatch(stopFetching());
-        })
-        .catch(e => {
-          getErrorMessage(e.response)
-              .then(message => dispatch(raiseError(message)));
-        });
-  };
-}
-
-let debouncedSearch = function (query, baseComponent, dispatch) {
-  if (query) {
-    requestTree(query, baseComponent, dispatch);
-  } else {
-    dispatch(searchAction(null));
-  }
-};
-debouncedSearch = _.debounce(debouncedSearch, 250);
-
-export function search (query, baseComponent) {
-  return dispatch => {
-    dispatch(updateQueryAction(query));
-
-    if (query.length > 2 || !query.length) {
-      debouncedSearch(query, baseComponent, dispatch);
-    }
-  };
-}
-
-export function selectCurrent () {
-  return (dispatch, getState) => {
-    const { searchResults } = getState().current;
-    if (searchResults) {
-      const componentKey = getState().current.searchSelectedItem.key;
-      dispatch(browse(componentKey));
-    }
-  };
-}
index c907e417174318cd72d6dd162dc3b4fc884c5aca..fc13bb30a31a97a3512f373e5c5622ffb76457bf 100644 (file)
  */
 import React from 'react';
 import { render } from 'react-dom';
-import { Provider } from 'react-redux';
-import { Router, Route, useRouterHistory } from 'react-router';
-import { createHashHistory } from 'history';
-import { syncReduxAndRouter } from 'redux-simple-router';
+import { Router, Route, Redirect, useRouterHistory } from 'react-router';
+import { createHistory } from 'history';
 
-import Code from './components/Code';
-import configureStore from './store/configureStore';
+import App from './components/App';
 
-import './styles/code.css';
+window.sonarqube.appStarted.then(options => {
+  const el = document.querySelector(options.el);
 
-const store = configureStore();
-const history = useRouterHistory(createHashHistory)({ queryKey: false });
+  const history = useRouterHistory(createHistory)({
+    basename: window.baseUrl + '/code'
+  });
 
-syncReduxAndRouter(history, store);
-
-window.sonarqube.appStarted.then(({ el, component }) => {
-  const CodeWithComponent = () => {
-    return <Code component={component}/>;
+  const AppWithComponent = (props) => {
+    return <App {...props} component={options.component}/>;
   };
 
-  render(
-      <Provider store={store}>
-        <Router history={history}>
-          <Route path="/" component={CodeWithComponent}/>
-          <Route path="/:path" component={CodeWithComponent}/>
-        </Router>
-      </Provider>,
-      document.querySelector(el));
+  render((
+      <Router history={history}>
+        <Redirect from="/index" to="/"/>
+        <Route path="/" component={AppWithComponent}/>
+      </Router>
+  ), el);
 });
diff --git a/server/sonar-web/src/main/js/apps/code/bucket.js b/server/sonar-web/src/main/js/apps/code/bucket.js
new file mode 100644 (file)
index 0000000..ced8587
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+const bucket = {};
+const childrenBucket = {};
+const breadcrumbsBucket = {};
+
+export function addComponent (component) {
+  bucket[component.key] = component;
+}
+
+export function getComponent (componentKey) {
+  return bucket[componentKey];
+}
+
+export function addComponentChildren (componentKey, children, total) {
+  childrenBucket[componentKey] = { children, total };
+}
+
+export function getComponentChildren (componentKey) {
+  return childrenBucket[componentKey];
+}
+
+export function addComponentBreadcrumbs (componentKey, breadcrumbs) {
+  breadcrumbsBucket[componentKey] = breadcrumbs;
+}
+
+export function getComponentBreadcrumbs (componentKey) {
+  return breadcrumbsBucket[componentKey];
+}
diff --git a/server/sonar-web/src/main/js/apps/code/code.css b/server/sonar-web/src/main/js/apps/code/code.css
new file mode 100644 (file)
index 0000000..297babd
--- /dev/null
@@ -0,0 +1,70 @@
+.code-breadcrumbs {
+  display: flex;
+  flex-wrap: wrap;
+}
+
+.code-breadcrumbs > li {
+  padding: 5px 5px 3px;
+}
+
+.code-breadcrumbs > li:first-child {
+  padding-left: 0;
+}
+
+.code-breadcrumbs > li::after {
+  position: relative;
+  top: -1px;
+  padding-left: 10px;
+  color: #777;
+  font-size: 11px;
+  content: ">";
+}
+
+.code-breadcrumbs > li:last-child::after {
+  display: none;
+}
+
+.code-components-cell {
+  min-width: 80px;
+  padding-left: 30px !important;
+  box-sizing: border-box;
+}
+
+.code-truncated {
+  display: inline-block;
+  vertical-align: text-top;
+  max-width: 50vw;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.code-name-cell {
+  max-width: 0;
+}
+
+.code-name-cell .code-truncated {
+  max-width: 100%;
+}
+
+.code-search {
+  margin-bottom: 10px;
+}
+
+.code-search-with-results + .code-components {
+  display: none;
+}
+
+.code-search .search-box {
+  padding-right: 10px;
+}
+
+.code-search .search-box .note {
+  vertical-align: middle;
+  opacity: 0;
+  transition: opacity 0.3s ease;
+}
+
+.code-search .search-box input.touched ~ .note {
+  opacity: 1;
+}
diff --git a/server/sonar-web/src/main/js/apps/code/components/App.js b/server/sonar-web/src/main/js/apps/code/components/App.js
new file mode 100644 (file)
index 0000000..185f988
--- /dev/null
@@ -0,0 +1,212 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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 classNames from 'classnames';
+import React from 'react';
+
+import Components from './Components';
+import Breadcrumbs from './Breadcrumbs';
+import SourceViewer from './../../../components/source-viewer/SourceViewer';
+import Search from './Search';
+import ListFooter from '../../../components/shared/list-footer';
+import { retrieveComponentBase, retrieveComponent, loadMoreChildren, parseError } from '../utils';
+import { addComponentBreadcrumbs } from '../bucket';
+import { selectCoverageMetric } from '../../../helpers/measures';
+
+import '../code.css';
+
+export default class App extends React.Component {
+  state = {
+    loading: true,
+    baseComponent: null,
+    components: null,
+    breadcrumbs: [],
+    total: 0,
+    page: 0,
+    sourceViewer: null,
+    error: null
+  };
+
+  componentDidMount () {
+    this.mounted = true;
+    this.handleComponentChange();
+  }
+
+  componentDidUpdate (prevProps) {
+    if (prevProps.component !== this.props.component) {
+      this.handleComponentChange();
+    } else if (prevProps.location !== this.props.location) {
+      this.handleUpdate();
+    }
+  }
+
+  componentWillUnmount () {
+    this.mounted = false;
+  }
+
+  handleComponentChange () {
+    const { component } = this.props;
+
+    // we already know component's breadcrumbs,
+    addComponentBreadcrumbs(component.key, component.breadcrumbs);
+
+    this.setState({ loading: true });
+    retrieveComponentBase(component.key).then(component => {
+      const prefix = selectCoverageMetric(component.measures);
+      this.coverageMetric = `${prefix}coverage`;
+      this.handleUpdate();
+    }).catch(e => {
+      if (this.mounted) {
+        this.setState({ loading: false });
+        parseError(e).then(this.handleError.bind(this));
+      }
+    });
+  }
+
+  loadComponent (componentKey) {
+    this.setState({ loading: true });
+
+    retrieveComponent(componentKey).then(r => {
+      if (this.mounted) {
+        if (['FIL', 'UTS'].includes(r.component.qualifier)) {
+          this.setState({
+            loading: false,
+            sourceViewer: r.component,
+            breadcrumbs: r.breadcrumbs,
+            searchResults: null
+          });
+        } else {
+          this.setState({
+            loading: false,
+            baseComponent: r.component,
+            components: r.components,
+            breadcrumbs: r.breadcrumbs,
+            total: r.total,
+            page: r.page,
+            sourceViewer: null,
+            searchResults: null
+          });
+        }
+      }
+    }).catch(e => {
+      if (this.mounted) {
+        this.setState({ loading: false });
+        parseError(e).then(this.handleError.bind(this));
+      }
+    });
+  }
+
+  handleUpdate () {
+    const { component, location } = this.props;
+    const { selected } = location.query;
+    const finalKey = selected || component.key;
+
+    this.loadComponent(finalKey);
+  }
+
+  handleLoadMore () {
+    const { baseComponent, page } = this.state;
+    loadMoreChildren(baseComponent.key, page + 1).then(r => {
+      if (this.mounted) {
+        this.setState({
+          components: [...this.state.components, ...r.components],
+          page: r.page,
+          total: r.total
+        });
+      }
+    }).catch(e => {
+      if (this.mounted) {
+        this.setState({ loading: false });
+        parseError(e).then(this.handleError.bind(this));
+      }
+    });
+  }
+
+  handleError (error) {
+    if (this.mounted) {
+      this.setState({ error });
+    }
+  }
+
+  render () {
+    const { component, location } = this.props;
+    const {
+        loading,
+        error,
+        baseComponent,
+        components,
+        breadcrumbs,
+        total,
+        sourceViewer
+    } = this.state;
+
+    const shouldShowSourceViewer = !!sourceViewer;
+    const shouldShowComponents = !shouldShowSourceViewer && components;
+    const shouldShowBreadcrumbs = Array.isArray(breadcrumbs) && breadcrumbs.length > 1;
+
+    const componentsClassName = classNames('spacer-top', { 'new-loading': loading });
+
+    return (
+        <div className="page page-limited">
+          {error && (
+              <div className="alert alert-danger">
+                {error}
+              </div>
+          )}
+
+          <Search
+              location={location}
+              component={component}
+              onError={this.handleError.bind(this)}/>
+
+
+          <div className="code-components">
+            {shouldShowBreadcrumbs && (
+                <Breadcrumbs
+                    rootComponent={component}
+                    breadcrumbs={breadcrumbs}/>
+            )}
+
+            {shouldShowComponents && (
+                <div className={componentsClassName}>
+                  <Components
+                      rootComponent={component}
+                      baseComponent={baseComponent}
+                      components={components}
+                      coverageMetric={this.coverageMetric}/>
+                </div>
+            )}
+
+            {shouldShowComponents && (
+                <ListFooter
+                    count={components.length}
+                    total={total}
+                    loadMore={this.handleLoadMore.bind(this)}/>
+            )}
+
+            {shouldShowSourceViewer && (
+                <div className="spacer-top">
+                  <SourceViewer component={sourceViewer}/>
+                </div>
+            )}
+          </div>
+        </div>
+    );
+  }
+}
index ad6f2786c468aa6d785bb206dbcccd55aff3e837..1280e2289b49cad7b7fa4835814dcb93abb2f182 100644 (file)
@@ -21,10 +21,11 @@ import React from 'react';
 
 import ComponentName from './ComponentName';
 
-const Breadcrumb = ({ component, onBrowse }) => (
+const Breadcrumb = ({ rootComponent, component, canBrowse }) => (
     <ComponentName
+        rootComponent={rootComponent}
         component={component}
-        onBrowse={onBrowse}/>
+        canBrowse={canBrowse}/>
 );
 
 export default Breadcrumb;
index a724db37337ea2ca30d8feca6bb6e22031f73bdf..611b98b1b5d50dc94e0092d68cd5737cd7cbe73f 100644 (file)
@@ -21,13 +21,14 @@ import React from 'react';
 
 import Breadcrumb from './Breadcrumb';
 
-const Breadcrumbs = ({ breadcrumbs, onBrowse }) => (
+const Breadcrumbs = ({ rootComponent, breadcrumbs }) => (
     <ul className="code-breadcrumbs">
       {breadcrumbs.map((component, index) => (
           <li key={component.key}>
             <Breadcrumb
+                rootComponent={rootComponent}
                 component={component}
-                onBrowse={index + 1 < breadcrumbs.length ? onBrowse : null}/>
+                canBrowse={index < breadcrumbs.length - 1}/>
           </li>
       ))}
     </ul>
diff --git a/server/sonar-web/src/main/js/apps/code/components/Code.js b/server/sonar-web/src/main/js/apps/code/components/Code.js
deleted file mode 100644 (file)
index 16ea3bd..0000000
+++ /dev/null
@@ -1,150 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2016 SonarSource SA
- * mailto:contact 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 classNames from 'classnames';
-import React, { Component } from 'react';
-import { connect } from 'react-redux';
-
-import Components from './Components';
-import Breadcrumbs from './Breadcrumbs';
-import SourceViewer from './SourceViewer';
-import Search from './Search';
-import ListFooter from '../../../components/shared/list-footer';
-import { initComponent, browse, loadMore } from '../actions';
-
-class Code extends Component {
-  componentDidMount () {
-    const { dispatch, component, routing } = this.props;
-    const selectedKey = (routing.path && decodeURIComponent(routing.path.substr(1))) || component.key;
-    dispatch(initComponent(component.key, component.breadcrumbs))
-        .then(() => dispatch(browse(selectedKey)));
-  }
-
-  componentWillReceiveProps (nextProps) {
-    if (nextProps.routing !== this.props.routing) {
-      const { dispatch, routing, component, fetching } = nextProps;
-      if (!fetching) {
-        const selectedKey = (routing.path && decodeURIComponent(routing.path.substr(1))) || component.key;
-        dispatch(browse(selectedKey));
-      }
-    }
-  }
-
-  handleBrowse (component) {
-    const { dispatch } = this.props;
-    dispatch(browse(component.key));
-  }
-
-  handleLoadMore () {
-    const { dispatch } = this.props;
-    dispatch(loadMore());
-  }
-
-  render () {
-    const {
-        fetching,
-        baseComponent,
-        components,
-        breadcrumbs,
-        sourceViewer,
-        coverageMetric,
-        searchResults,
-        errorMessage,
-        total
-    } = this.props;
-    const shouldShowSearchResults = !!searchResults;
-    const shouldShowSourceViewer = !!sourceViewer;
-    const shouldShowComponents = !shouldShowSearchResults && !shouldShowSourceViewer && components;
-    const shouldShowBreadcrumbs = !shouldShowSearchResults && Array.isArray(breadcrumbs) && breadcrumbs.length > 1;
-
-    const componentsClassName = classNames('spacer-top', { 'new-loading': fetching });
-
-    return (
-        <div className="page page-limited">
-          <header className="page-header">
-            <Search component={this.props.component}/>
-
-            <div
-                className="pull-left"
-                style={{ visibility: fetching ? 'visible' : 'hidden' }}>
-              <i className="spinner"/>
-            </div>
-          </header>
-
-          {errorMessage && (
-              <div className="alert alert-danger">
-                {errorMessage}
-              </div>
-          )}
-
-          {shouldShowBreadcrumbs && (
-              <Breadcrumbs
-                  breadcrumbs={breadcrumbs}
-                  onBrowse={this.handleBrowse.bind(this)}/>
-          )}
-
-          {shouldShowSearchResults && (
-              <div className={componentsClassName}>
-                <Components
-                    components={searchResults}
-                    onBrowse={this.handleBrowse.bind(this)}/>
-              </div>
-          )}
-
-          {shouldShowComponents && (
-              <div className={componentsClassName}>
-                <Components
-                    baseComponent={baseComponent}
-                    components={components}
-                    coverageMetric={coverageMetric}
-                    onBrowse={this.handleBrowse.bind(this)}/>
-              </div>
-          )}
-
-          {shouldShowComponents && (
-              <ListFooter
-                  count={components.length}
-                  total={total}
-                  loadMore={this.handleLoadMore.bind(this)}/>
-          )}
-
-          {shouldShowSourceViewer && (
-              <div className="spacer-top">
-                <SourceViewer component={sourceViewer}/>
-              </div>
-          )}
-        </div>
-    );
-  }
-}
-
-export default connect(state => {
-  return {
-    routing: state.routing,
-    fetching: state.current.fetching,
-    baseComponent: state.current.baseComponent,
-    components: state.current.components,
-    breadcrumbs: state.current.breadcrumbs,
-    sourceViewer: state.current.sourceViewer,
-    coverageMetric: state.current.coverageMetric,
-    searchResults: state.current.searchResults,
-    errorMessage: state.current.errorMessage,
-    total: state.current.total
-  };
-})(Code);
index 47efb73c58cc1fe97d9b1ea8dd506c36bc3d608c..5134427d03da6d0fffa1481f440131b6f8fa035e 100644 (file)
@@ -20,7 +20,7 @@
 import classNames from 'classnames';
 import React from 'react';
 import ReactDOM from 'react-dom';
-import { connect } from 'react-redux';
+import shallowCompare from 'react-addons-shallow-compare';
 
 import ComponentName from './ComponentName';
 import ComponentMeasure from './ComponentMeasure';
@@ -31,17 +31,23 @@ import ComponentPin from './ComponentPin';
 const TOP_OFFSET = 200;
 const BOTTOM_OFFSET = 10;
 
-class Component extends React.Component {
+export default class Component extends React.Component {
   componentDidMount () {
     this.handleUpdate();
   }
 
+  shouldComponentUpdate (nextProps, nextState) {
+    return shallowCompare(this, nextProps, nextState);
+  }
+
   componentDidUpdate () {
     this.handleUpdate();
   }
 
   handleUpdate () {
     const { selected } = this.props;
+
+    // scroll viewport so the current selected component is visible
     if (selected) {
       setTimeout(() => {
         this.handleScroll();
@@ -61,7 +67,8 @@ class Component extends React.Component {
   }
 
   render () {
-    const { component, selected, previous, coverageMetric, onBrowse, isView } = this.props;
+    const { component, rootComponent, selected, previous, coverageMetric, canBrowse } = this.props;
+    const isView = ['VW', 'SVW'].includes(rootComponent.qualifier);
 
     let componentAction = null;
 
@@ -91,8 +98,9 @@ class Component extends React.Component {
             )}
             <ComponentName
                 component={component}
+                rootComponent={rootComponent}
                 previous={previous}
-                onBrowse={onBrowse}/>
+                canBrowse={canBrowse}/>
           </td>
           <td className="thin nowrap text-right">
             <div className="code-components-cell">
@@ -146,12 +154,3 @@ class Component extends React.Component {
     );
   }
 }
-
-function mapStateToProps (state, ownProps) {
-  return {
-    selected: state.current.searchSelectedItem === ownProps.component,
-    isView: state.current.isView
-  };
-}
-
-export default connect(mapStateToProps)(Component);
index 2b14457fb25be52d43528b3fffffc869969c171b..c33815f511d4a79f12caba5492ecb4b1959ef945 100644 (file)
@@ -19,6 +19,7 @@
  */
 import _ from 'underscore';
 import React from 'react';
+import { Link } from 'react-router';
 
 import Truncated from './Truncated';
 import QualifierIcon from '../../../components/shared/qualifier-icon';
@@ -47,12 +48,7 @@ function mostCommitPrefix (strings) {
   return prefix.substr(0, prefix.length - lastPrefixPart.length);
 }
 
-const Component = ({ component, previous, onBrowse }) => {
-  const handleClick = (e) => {
-    e.preventDefault();
-    onBrowse(component);
-  };
-
+const Component = ({ component, rootComponent, previous, canBrowse }) => {
   const areBothDirs = component.qualifier === 'DIR' && previous && previous.qualifier === 'DIR';
   const prefix = areBothDirs ? mostCommitPrefix([component.name + '/', previous.name + '/']) : '';
   const name = prefix ? (
@@ -67,8 +63,16 @@ const Component = ({ component, previous, onBrowse }) => {
   if (component.refKey) {
     inner = <a href={getComponentUrl(component.refKey)}>{name}</a>;
   } else {
-    if (onBrowse) {
-      inner = <a onClick={handleClick} href="#">{name}</a>;
+    if (canBrowse) {
+      const query = { id: rootComponent.key };
+      if (component.key !== rootComponent.key) {
+        Object.assign(query, { selected: component.key });
+      }
+      inner = (
+          <Link to={{ pathname: '/', query }}>
+            {name}
+          </Link>
+      );
     } else {
       inner = <span>{name}</span>;
     }
index 91300d647ed261dc07a3c5d75480628d685b974c..249e3e540e63f6f3a737113956e0f5c6b4f1c1da 100644 (file)
@@ -23,13 +23,14 @@ import Component from './Component';
 import ComponentsEmpty from './ComponentsEmpty';
 import ComponentsHeader from './ComponentsHeader';
 
-const Components = ({ baseComponent, components, coverageMetric, onBrowse }) => (
+const Components = ({ rootComponent, baseComponent, components, selected, coverageMetric }) => (
     <table className="data zebra">
       <ComponentsHeader baseComponent={baseComponent}/>
       {baseComponent && (
           <tbody>
             <Component
                 key={baseComponent.key}
+                rootComponent={rootComponent}
                 component={baseComponent}
                 coverageMetric={coverageMetric}/>
             <tr className="blank">
@@ -42,10 +43,12 @@ const Components = ({ baseComponent, components, coverageMetric, onBrowse }) =>
             components.map((component, index, list) => (
                 <Component
                     key={component.key}
+                    rootComponent={rootComponent}
                     component={component}
+                    selected={component === selected}
                     previous={index > 0 ? list[index - 1] : null}
                     coverageMetric={coverageMetric}
-                    onBrowse={onBrowse}/>
+                    canBrowse={true}/>
             ))
         ) : (
             <ComponentsEmpty/>
index 753b2de8b133e558d211ed7cfc20038073be93e6..c2afced5689f503929c3edfacc470dd05a9b6c89 100644 (file)
@@ -25,7 +25,7 @@ const ComponentsEmpty = () => (
       <td colSpan="2">
         {translate('no_results')}
       </td>
-      <td colSpan="5">
+      <td colSpan="6">
         &nbsp;
       </td>
     </tr>
index fb373856470c14d16c008432415c28ea61b6bdf8..d54f727e5f3edfed65e43e10915e3a279f347801 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import React, { Component } from 'react';
-import { connect } from 'react-redux';
+import React from 'react';
+import shallowCompare from 'react-addons-shallow-compare';
 import classNames from 'classnames';
+import debounce from 'lodash/debounce';
 
-import { search, selectCurrent, selectNext, selectPrev } from '../actions';
+import Components from './Components';
+import { getTree } from '../../../api/components';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { parseError } from '../utils';
+import { getComponentUrl } from '../../../helpers/urls';
+
+export default class Search extends React.Component {
+  static contextTypes = {
+    router: React.PropTypes.object.isRequired
+  };
+
+  static propTypes = {
+    component: React.PropTypes.object.isRequired,
+    location: React.PropTypes.object.isRequired,
+    onError: React.PropTypes.func.isRequired
+  };
+
+  state = {
+    query: '',
+    loading: false,
+    results: null,
+    selectedIndex: null
+  };
+
+  componentWillMount () {
+    this.handleSearch = debounce(this.handleSearch.bind(this), 250);
+  }
 
-class Search extends Component {
   componentDidMount () {
+    this.mounted = true;
     this.refs.input.focus();
   }
 
+  componentWillReceiveProps (nextProps) {
+    // if the url has change, reset the current state
+    if (nextProps.location !== this.props.location) {
+      this.setState({
+        query: '',
+        loading: false,
+        results: null,
+        selectedIndex: null
+      });
+    }
+  }
+
+  shouldComponentUpdate (nextProps, nextState) {
+    return shallowCompare(this, nextProps, nextState);
+  }
+
+  componentWillUnmount () {
+    this.mounted = false;
+  }
+
+  checkInputValue (query) {
+    return this.refs.input.value === query;
+  }
+
+  handleSelectNext () {
+    const { selectedIndex, results } = this.state;
+    if (results != null && selectedIndex != null && selectedIndex < results.length - 1) {
+      this.setState({ selectedIndex: selectedIndex + 1 });
+    }
+  }
+
+  handleSelectPrevious () {
+    const { selectedIndex, results } = this.state;
+    if (results != null && selectedIndex != null && selectedIndex > 0) {
+      this.setState({ selectedIndex: selectedIndex - 1 });
+    }
+  }
+
+  handleSelectCurrent () {
+    const { component } = this.props;
+    const { results, selectedIndex } = this.state;
+    if (results != null && selectedIndex != null) {
+      const selected = results[selectedIndex];
+
+      if (selected.refKey) {
+        window.location = getComponentUrl(selected.refKey);
+      } else {
+        this.context.router.push({
+          pathname: '/',
+          query: {
+            id: component.key,
+            selected: selected.key
+          }
+        });
+      }
+    }
+  }
+
   handleKeyDown (e) {
-    const { dispatch } = this.props;
     switch (e.keyCode) {
       case 13:
         e.preventDefault();
-        dispatch(selectCurrent());
+        this.handleSelectCurrent();
         break;
       case 38:
         e.preventDefault();
-        dispatch(selectPrev());
+        this.handleSelectPrevious();
         break;
       case 40:
         e.preventDefault();
-        dispatch(selectNext());
+        this.handleSelectNext();
         break;
-      default:
+      default: // do nothing
+    }
+  }
+
+  handleSearch (query) {
+    // first time check if value has changed due to debounce
+    if (this.mounted && this.checkInputValue(query)) {
+      const { component, onError } = this.props;
+      this.setState({ loading: true });
+      getTree(component.key, { q: query, s: 'qualifier,name' })
+          .then(r => {
+            // second time check if value has change due to api request
+            if (this.mounted && this.checkInputValue(query)) {
+              this.setState({
+                results: r.components,
+                selectedIndex: r.components.length > 0 ? 0 : null,
+                loading: false
+              });
+            }
+          })
+          .catch(e => {
+            // second time check if value has change due to api request
+            if (this.mounted && this.checkInputValue(query)) {
+              this.setState({ loading: false });
+              parseError(e).then(onError);
+            }
+          });
+    }
+  }
 
-      // do nothing
+  handleQueryChange (query) {
+    this.setState({ query });
+    if (query.length < 3) {
+      this.setState({ results: null });
+    } else {
+      this.handleSearch(query);
     }
   }
 
-  handleSearch (e) {
+  handleInputChange (e) {
+    const query = e.target.value;
+    this.handleQueryChange(query);
+  }
+
+  handleSubmit (e) {
     e.preventDefault();
-    const { dispatch, component } = this.props;
     const query = this.refs.input.value;
-    dispatch(search(query, component));
+    this.handleQueryChange(query);
   }
 
   render () {
-    const { query } = this.props;
+    const { component } = this.props;
+    const { query, loading, selectedIndex, results } = this.state;
+    const selected = selectedIndex != null && results != null ? results[selectedIndex] : null;
+    const containerClassName = classNames('code-search', {
+      'code-search-with-results': results != null
+    });
     const inputClassName = classNames('search-box-input', {
       'touched': query.length > 0 && query.length < 3
     });
 
     return (
-        <form
-            onSubmit={this.handleSearch.bind(this)}
-            className="search-box code-search-box">
-          <button className="search-box-submit button-clean">
-            <i className="icon-search"></i>
-          </button>
-          <input
-              ref="input"
-              onKeyDown={this.handleKeyDown.bind(this)}
-              onChange={this.handleSearch.bind(this)}
-              value={query}
-              className={inputClassName}
-              type="search"
-              name="q"
-              placeholder={translate('search_verb')}
-              maxLength="100"
-              autoComplete="off"/>
-          <div className="note">
-            {translateWithParameters('select2.tooShort', 3)}
-          </div>
-        </form>
+        <div id="code-search" className={containerClassName}>
+          <form className="search-box" onSubmit={this.handleSubmit.bind(this)}>
+            <button className="search-box-submit button-clean">
+              <i className="icon-search"></i>
+            </button>
+
+            <input
+                ref="input"
+                onKeyDown={this.handleKeyDown.bind(this)}
+                onChange={this.handleInputChange.bind(this)}
+                value={query}
+                className={inputClassName}
+                type="search"
+                name="q"
+                placeholder={translate('search_verb')}
+                maxLength="100"
+                autoComplete="off"/>
+
+            {loading && (
+                <i className="spinner spacer-left"/>
+            )}
+
+            <span className="note spacer-left">
+              {translateWithParameters('select2.tooShort', 3)}
+            </span>
+          </form>
+
+          {results != null && (
+              <Components
+                  rootComponent={component}
+                  components={results}
+                  selected={selected}/>
+          )}
+        </div>
     );
   }
 }
-
-export default connect(state => {
-  return { query: state.current.searchQuery };
-})(Search);
diff --git a/server/sonar-web/src/main/js/apps/code/components/SourceViewer.js b/server/sonar-web/src/main/js/apps/code/components/SourceViewer.js
deleted file mode 100644 (file)
index d799836..0000000
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2016 SonarSource SA
- * mailto:contact 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, { Component } from 'react';
-
-import BaseSourceViewer from '../../../components/source-viewer/main';
-import { getPeriodDate, getPeriodLabel } from '../../../helpers/periods';
-
-export default class SourceViewer extends Component {
-  componentDidMount () {
-    this.renderSourceViewer();
-  }
-
-  shouldComponentUpdate (nextProps) {
-    return nextProps.component.id !== this.props.component.id;
-  }
-
-  componentWillUpdate () {
-    this.destroySourceViewer();
-  }
-
-  componentDidUpdate () {
-    this.renderSourceViewer();
-  }
-
-  componentWillUnmount () {
-    this.destroySourceViewer();
-  }
-
-  renderSourceViewer () {
-    this.sourceViewer = new BaseSourceViewer();
-    this.sourceViewer.render().$el.appendTo(this.refs.container);
-    this.sourceViewer.open(this.props.component.id);
-    this.sourceViewer.on('loaded', this.handleLoad.bind(this));
-  }
-
-  destroySourceViewer () {
-    this.sourceViewer.destroy();
-  }
-
-  handleLoad () {
-    const { period } = this.props;
-
-    if (period) {
-      const periodDate = getPeriodDate(period);
-      const periodLabel = getPeriodLabel(period);
-      this.sourceViewer.filterLinesByDate(periodDate, periodLabel);
-    }
-  }
-
-  render () {
-    return <div ref="container" className="code-source-viewer"></div>;
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/code/reducers/index.js b/server/sonar-web/src/main/js/apps/code/reducers/index.js
deleted file mode 100644 (file)
index a554d94..0000000
+++ /dev/null
@@ -1,206 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2016 SonarSource SA
- * mailto:contact 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 'underscore';
-
-import {
-    INIT,
-    BROWSE,
-    LOAD_MORE,
-    SEARCH,
-    UPDATE_QUERY,
-    SELECT_NEXT,
-    SELECT_PREV,
-    START_FETCHING,
-    STOP_FETCHING,
-    RAISE_ERROR
-} from '../actions';
-
-function hasSourceCode (component) {
-  return component.qualifier === 'FIL' || component.qualifier === 'UTS';
-}
-
-function selectCoverageMetric (component) {
-  const coverage = _.findWhere(component.measures, { metric: 'coverage' });
-  const itCoverage = _.findWhere(component.measures, { metric: 'it_coverage' });
-  const overallCoverage = _.findWhere(component.measures, { metric: 'overall_coverage' });
-
-  if (coverage != null && itCoverage != null && overallCoverage != null) {
-    return 'overall_coverage';
-  } else if (coverage != null) {
-    return 'coverage';
-  } else {
-    return 'it_coverage';
-  }
-}
-
-function merge (components, candidate) {
-  const found = _.findWhere(components, { key: candidate.key });
-  const newEntry = Object.assign({}, found, candidate);
-  return [...(_.without(components, found)), newEntry];
-}
-
-function compare (a, b) {
-  if (a === b) {
-    return 0;
-  }
-  return a > b ? 1 : -1;
-}
-
-function sortChildren (children) {
-  const QUALIFIERS_ORDER = ['FIL', 'UTS', 'DIR'];
-  const temp = [...children];
-  temp.sort((a, b) => {
-    const qualifierA = QUALIFIERS_ORDER.indexOf(a.qualifier);
-    const qualifierB = QUALIFIERS_ORDER.indexOf(b.qualifier);
-    if (qualifierA !== qualifierB) {
-      return compare(qualifierA, qualifierB);
-    } else {
-      return compare(a.name, b.name);
-    }
-  });
-  return temp;
-}
-
-function getNext (element, list) {
-  if (list) {
-    const length = list.length;
-    const index = list.indexOf(element);
-    return index < length - 1 ? list[index + 1] : element;
-  } else {
-    return element;
-  }
-}
-
-function getPrev (element, list) {
-  if (list) {
-    const index = list.indexOf(element);
-    return index > 0 ? list[index - 1] : element;
-  } else {
-    return element;
-  }
-}
-
-export const initialState = {
-  fetching: false,
-  baseComponent: null,
-  components: null,
-  breadcrumbs: null,
-  sourceViewer: null,
-  searchResults: null,
-  searchQuery: '',
-  searchSelectedItem: null,
-  coverageMetric: null,
-  isView: false,
-  baseBreadcrumbs: [],
-  errorMessage: null
-};
-
-export function current (state = initialState, action = {}) {
-  /* eslint no-case-declarations: 0 */
-  /* FIXME fix it ^^^ */
-  switch (action.type) {
-    case INIT:
-      const coverageMetric = selectCoverageMetric(action.component);
-      const baseBreadcrumbs = action.breadcrumbs.length > 1 ? _.initial(action.breadcrumbs) : [];
-      const isView = action.component.qualifier === 'VW' || action.component.qualifier === 'SVW';
-
-      return { ...state, coverageMetric, baseBreadcrumbs, isView };
-    case BROWSE:
-      const baseComponent = hasSourceCode(action.component) ? null : action.component;
-      const components = hasSourceCode(action.component) ? null : sortChildren(action.children);
-      const baseBreadcrumbsLength = state.baseBreadcrumbs.length;
-      const breadcrumbs = action.breadcrumbs.slice(baseBreadcrumbsLength);
-      const sourceViewer = hasSourceCode(action.component) ? action.component : null;
-
-      return {
-        ...state,
-        baseComponent,
-        components,
-        breadcrumbs,
-        sourceViewer,
-        total: action.total,
-        page: 1,
-        searchResults: null,
-        searchQuery: '',
-        searchSelectedItem: null,
-        errorMessage: null
-      };
-    case LOAD_MORE:
-      return {
-        ...state,
-        components: sortChildren([...state.components, ...action.children]),
-        page: action.page
-      };
-    case SEARCH:
-      return {
-        ...state,
-        searchResults: action.components,
-        searchSelectedItem: _.first(action.components),
-        sourceViewer: null,
-        errorMessage: null
-      };
-    case UPDATE_QUERY:
-      return { ...state, searchQuery: action.query };
-    case SELECT_NEXT:
-      return {
-        ...state,
-        searchSelectedItem: getNext(state.searchSelectedItem, state.searchResults)
-      };
-    case SELECT_PREV:
-      return {
-        ...state,
-        searchSelectedItem: getPrev(state.searchSelectedItem, state.searchResults)
-      };
-    case START_FETCHING:
-      return { ...state, fetching: true };
-    case STOP_FETCHING:
-      return { ...state, fetching: false };
-    case RAISE_ERROR:
-      return {
-        ...state,
-        errorMessage: action.message,
-        fetching: false
-      };
-    default:
-      return state;
-  }
-}
-
-export function bucket (state = [], action = {}) {
-  switch (action.type) {
-    case INIT:
-      return merge(state, action.component);
-    case BROWSE:
-      const candidate = Object.assign({}, action.component, {
-        children: action.children,
-        total: action.total,
-        breadcrumbs: action.breadcrumbs
-      });
-      const nextState = merge(state, candidate);
-      return action.children.reduce((currentState, nextComponent) => {
-        const nextComponentWidthBreadcrumbs = Object.assign({}, nextComponent, {
-          breadcrumbs: [...action.breadcrumbs, nextComponent]
-        });
-        return merge(currentState, nextComponentWidthBreadcrumbs);
-      }, nextState);
-    default:
-      return state;
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/code/store/configureStore.js b/server/sonar-web/src/main/js/apps/code/store/configureStore.js
deleted file mode 100644 (file)
index 92ce2ac..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2016 SonarSource SA
- * mailto:contact 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 { createStore, applyMiddleware, combineReducers } from 'redux';
-import thunk from 'redux-thunk';
-import createLogger from 'redux-logger';
-import { routeReducer } from 'redux-simple-router';
-import { current, bucket } from '../reducers';
-
-const logger = createLogger({
-  predicate: () => process.env.NODE_ENV !== 'production'
-});
-
-const createStoreWithMiddleware = applyMiddleware(
-    thunk,
-    logger
-)(createStore);
-
-const reducer = combineReducers({
-  routing: routeReducer,
-  current,
-  bucket
-});
-
-export default function configureStore () {
-  return createStoreWithMiddleware(reducer);
-}
diff --git a/server/sonar-web/src/main/js/apps/code/styles/code.css b/server/sonar-web/src/main/js/apps/code/styles/code.css
deleted file mode 100644 (file)
index 90fe862..0000000
+++ /dev/null
@@ -1,64 +0,0 @@
-.code-breadcrumbs {
-  display: flex;
-  flex-wrap: wrap;
-}
-
-.code-breadcrumbs > li {
-  padding: 5px 5px 3px;
-}
-
-.code-breadcrumbs > li:first-child {
-  padding-left: 0;
-}
-
-.code-breadcrumbs > li::after {
-  position: relative;
-  top: -1px;
-  padding-left: 10px;
-  color: #777;
-  font-size: 11px;
-  content: ">";
-}
-
-.code-breadcrumbs > li:last-child::after {
-  display: none;
-}
-
-.code-components-cell {
-  min-width: 80px;
-  padding-left: 30px !important;
-  box-sizing: border-box;
-}
-
-.code-truncated {
-  display: inline-block;
-  vertical-align: text-top;
-  max-width: 50vw;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: nowrap;
-}
-
-.code-name-cell {
-  max-width: 0;
-}
-
-.code-name-cell .code-truncated {
-  max-width: 100%;
-}
-
-.code-search-box {
-  float: left;
-  padding-right: 10px;
-}
-
-.code-search-box .note {
-  margin-top: 4px;
-  margin-left: 25px;
-  opacity: 0;
-  transition: opacity 0.3s ease;
-}
-
-.code-search-box input.touched ~ .note {
-  opacity: 1;
-}
diff --git a/server/sonar-web/src/main/js/apps/code/utils.js b/server/sonar-web/src/main/js/apps/code/utils.js
new file mode 100644 (file)
index 0000000..b236ee8
--- /dev/null
@@ -0,0 +1,175 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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 without from 'lodash/without';
+import sortBy from 'lodash/sortBy';
+
+import {
+    addComponent,
+    getComponent as getComponentFromBucket,
+    addComponentChildren,
+    getComponentChildren,
+    addComponentBreadcrumbs,
+    getComponentBreadcrumbs
+} from './bucket';
+import { getChildren, getComponent, getBreadcrumbs } from '../../api/components';
+import { translate } from '../../helpers/l10n';
+
+const METRICS = [
+  'ncloc',
+  'code_smells',
+  'bugs',
+  'vulnerabilities',
+  'duplicated_lines_density',
+  'alert_status'
+];
+
+const METRICS_WITH_COVERAGE = [
+  ...METRICS,
+  'coverage',
+  'it_coverage',
+  'overall_coverage'
+];
+
+const PAGE_SIZE = 100;
+
+function expandRootDir ({ components, total, ...other }) {
+  const rootDir = components.find(component => component.qualifier === 'DIR' && component.name === '/');
+  if (rootDir) {
+    return getChildren(rootDir.key, METRICS_WITH_COVERAGE).then(r => {
+      const nextComponents = without([...r.components, ...components], rootDir);
+      const nextTotal = total + r.components.length - /* root dir */ 1;
+      return { components: nextComponents, total: nextTotal, ...other };
+    });
+  } else {
+    return { components, total, ...other };
+  }
+}
+
+function prepareChildren (r) {
+  return {
+    components: r.components,
+    total: r.paging.total,
+    page: r.paging.pageIndex
+  };
+}
+
+function skipRootDir (breadcrumbs) {
+  return breadcrumbs.filter(component => {
+    return !(component.qualifier === 'DIR' && component.name === '/');
+  });
+}
+
+function storeChildrenBase (children) {
+  children.forEach(addComponent);
+}
+
+function storeChildrenBreadcrumbs (parentComponentKey, children) {
+  const parentBreadcrumbs = getComponentBreadcrumbs(parentComponentKey);
+  if (parentBreadcrumbs) {
+    children.forEach(child => {
+      const breadcrumbs = [...parentBreadcrumbs, child];
+      addComponentBreadcrumbs(child.key, breadcrumbs);
+    });
+  }
+}
+
+export function retrieveComponentBase (componentKey) {
+  const existing = getComponentFromBucket(componentKey);
+  if (existing) {
+    return Promise.resolve(existing);
+  }
+
+  return getComponent(componentKey, METRICS_WITH_COVERAGE).then(component => {
+    addComponent(component);
+    return component;
+  });
+}
+
+function retrieveComponentChildren (componentKey) {
+  const existing = getComponentChildren(componentKey);
+  if (existing) {
+    return Promise.resolve({
+      components: existing.children,
+      total: existing.total
+    });
+  }
+
+  return getChildren(componentKey, METRICS_WITH_COVERAGE, { ps: PAGE_SIZE, s: 'name' })
+      .then(prepareChildren)
+      .then(expandRootDir)
+      .then(r => {
+        addComponentChildren(componentKey, r.components, r.total);
+        storeChildrenBase(r.components);
+        storeChildrenBreadcrumbs(componentKey, r.components);
+        return r;
+      });
+}
+
+function retrieveComponentBreadcrumbs (componentKey) {
+  const existing = getComponentBreadcrumbs(componentKey);
+  if (existing) {
+    return Promise.resolve(existing);
+  }
+
+  return getBreadcrumbs({ key: componentKey })
+      .then(skipRootDir)
+      .then(breadcrumbs => {
+        addComponentBreadcrumbs(componentKey, breadcrumbs);
+        return breadcrumbs;
+      });
+}
+
+export function retrieveComponent (componentKey) {
+  return Promise.all([
+    retrieveComponentBase(componentKey),
+    retrieveComponentChildren(componentKey),
+    retrieveComponentBreadcrumbs(componentKey)
+  ]).then(r => {
+    return {
+      component: r[0],
+      components: r[1].components,
+      total: r[1].total,
+      page: r[1].page,
+      breadcrumbs: r[2]
+    };
+  });
+}
+
+export function loadMoreChildren (componentKey, page) {
+  return getChildren(componentKey, METRICS_WITH_COVERAGE, { ps: PAGE_SIZE, p: page })
+      .then(prepareChildren)
+      .then(expandRootDir)
+      .then(r => {
+        addComponentChildren(componentKey, r.components, r.total);
+        storeChildrenBase(r.components);
+        storeChildrenBreadcrumbs(componentKey, r.components);
+        return r;
+      });
+}
+
+export function parseError (error) {
+  try {
+    return error.response.json().then(r => {
+      return r.errors.map(error => error.msg).join('. ');
+    });
+  } catch (ex) {
+    return Promise.resolve(translate('default_error_message'));
+  }
+}
index 3fd24c2c26547d562c0500b52106b3e11b52fd1a..0f4ec6411554cfed673e9462cd6ee6cda4e141df 100644 (file)
@@ -23,7 +23,7 @@ import classNames from 'classnames';
 import ComponentsList from './ComponentsList';
 import ListHeader from './ListHeader';
 import Spinner from '../../components/Spinner';
-import SourceViewer from '../../../code/components/SourceViewer';
+import SourceViewer from '../../../../components/source-viewer/SourceViewer';
 import ListFooter from '../../../../components/shared/list-footer';
 
 export default class ListView extends React.Component {
index d42bbf7167b5d7f81540414b0187d8a3e5f4c93e..04875916ef2cc2bceccfa1da42c4a154c8cda640 100644 (file)
@@ -22,7 +22,7 @@ import React from 'react';
 import ComponentsList from './ComponentsList';
 import ListHeader from './ListHeader';
 import Spinner from '../../components/Spinner';
-import SourceViewer from '../../../code/components/SourceViewer';
+import SourceViewer from '../../../../components/source-viewer/SourceViewer';
 import ListFooter from '../../../../components/shared/list-footer';
 
 export default class TreeView extends React.Component {
diff --git a/server/sonar-web/src/main/js/components/source-viewer/SourceViewer.js b/server/sonar-web/src/main/js/components/source-viewer/SourceViewer.js
new file mode 100644 (file)
index 0000000..024d750
--- /dev/null
@@ -0,0 +1,77 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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 BaseSourceViewer from './main';
+import { getPeriodDate, getPeriodLabel } from '../../helpers/periods';
+
+export default class SourceViewer extends React.Component {
+  static propTypes = {
+    component: React.PropTypes.shape({
+      id: React.PropTypes.string.isRequired
+    }).isRequired,
+    period: React.PropTypes.object
+  };
+
+  componentDidMount () {
+    this.renderSourceViewer();
+  }
+
+  shouldComponentUpdate (nextProps) {
+    return nextProps.component.id !== this.props.component.id;
+  }
+
+  componentWillUpdate () {
+    this.destroySourceViewer();
+  }
+
+  componentDidUpdate () {
+    this.renderSourceViewer();
+  }
+
+  componentWillUnmount () {
+    this.destroySourceViewer();
+  }
+
+  renderSourceViewer () {
+    this.sourceViewer = new BaseSourceViewer();
+    this.sourceViewer.render().$el.appendTo(this.refs.container);
+    this.sourceViewer.open(this.props.component.id);
+    this.sourceViewer.on('loaded', this.handleLoad.bind(this));
+  }
+
+  destroySourceViewer () {
+    this.sourceViewer.destroy();
+  }
+
+  handleLoad () {
+    const { period } = this.props;
+
+    if (period) {
+      const periodDate = getPeriodDate(period);
+      const periodLabel = getPeriodLabel(period);
+      this.sourceViewer.filterLinesByDate(periodDate, periodLabel);
+    }
+  }
+
+  render () {
+    return <div ref="container"/>;
+  }
+}
index bf6df21284a7d902c3eab09095763ff43677a595..c965c1577a63a505687fea9da746b4a509068450 100644 (file)
@@ -113,6 +113,25 @@ export function isDiffMetric (metricKey) {
   return metricKey.indexOf('new_') === 0;
 }
 
+/**
+ * Check all types of coverage and return most suitable one
+ * @param {Array} measures
+ * @returns {string}
+ */
+export function selectCoverageMetric (measures) {
+  const hasOverallCoverage = !!measures.find(measure => measure.metric === 'overall_coverage');
+  const hasUTCoverage = !!measures.find(measure => measure.metric === 'coverage');
+  const hasITCoverage = !!measures.find(measure => measure.metric === 'it_coverage');
+
+  if (hasOverallCoverage && hasUTCoverage && hasITCoverage) {
+    return 'overall_';
+  } else if (hasITCoverage) {
+    return 'it_';
+  } else {
+    return '';
+  }
+}
+
 /*
  * Helpers
  */
index 885318c90305e13cd0e4cdbf1bdc150cb6eb53f7..6ce4822c1867767a5a179e5855f94effacb20b22 100644 (file)
@@ -146,21 +146,13 @@ export default React.createClass({
   },
 
   renderCodeLink() {
-    if (this.isView() || this.isDeveloper()) {
-      return null;
-    }
-
-    const url = `/code/index?id=${encodeURIComponent(this.props.component.key)}`;
-    return this.renderLink(url, translate('code.page'), '/code');
-  },
-
-  renderProjectsLink() {
-    if (!this.isView()) {
+    if (this.isDeveloper()) {
       return null;
     }
 
-    const url = `/view_projects/index?id=${encodeURIComponent(this.props.component.key)}`;
-    return this.renderLink(url, translate('view_projects.page'), '/view_projects');
+    const url = `/code/?id=${encodeURIComponent(this.props.component.key)}`;
+    const header = this.isView() ? translate('view_projects.page') : translate('code.page');
+    return this.renderLink(url, header, '/code');
   },
 
   renderComponentIssuesLink() {
@@ -334,7 +326,6 @@ export default React.createClass({
             {this.renderComponentIssuesLink()}
             {this.renderComponentMeasuresLink()}
             {this.renderCodeLink()}
-            {this.renderProjectsLink()}
             {this.renderTools()}
             {this.renderAdministration()}
           </ul>
@@ -346,7 +337,6 @@ export default React.createClass({
             {this.renderComponentIssuesLink()}
             {this.renderComponentMeasuresLink()}
             {this.renderCodeLink()}
-            {this.renderProjectsLink()}
             {this.renderCustomDashboards()}
             {this.renderTools()}
             {this.renderAdministration()}
diff --git a/server/sonar-web/tests/apps/code/components-test.js b/server/sonar-web/tests/apps/code/components-test.js
deleted file mode 100644 (file)
index 9613032..0000000
+++ /dev/null
@@ -1,204 +0,0 @@
-import chai, { expect } from 'chai';
-import sinon from 'sinon';
-import sinonChai from 'sinon-chai';
-import { shallow } from 'enzyme';
-import React from 'react';
-import TestUtils from 'react-addons-test-utils';
-
-import Breadcrumb from '../../../src/main/js/apps/code/components/Breadcrumb';
-import Breadcrumbs from '../../../src/main/js/apps/code/components/Breadcrumbs';
-import ComponentDetach from '../../../src/main/js/apps/code/components/ComponentDetach';
-import ComponentMeasure from '../../../src/main/js/apps/code/components/ComponentMeasure';
-import ComponentName from '../../../src/main/js/apps/code/components/ComponentName';
-import ComponentsEmpty from '../../../src/main/js/apps/code/components/ComponentsEmpty';
-import Truncated from '../../../src/main/js/apps/code/components/Truncated';
-
-import { getComponentUrl } from '../../../src/main/js/helpers/urls';
-import QualifierIcon from '../../../src/main/js/components/shared/qualifier-icon';
-
-
-chai.use(sinonChai);
-
-
-const measures = [
-  { metric: 'ncloc', value: 9757 }
-];
-const exampleComponent = {
-  uuid: 'A1',
-  key: 'A',
-  name: 'AA',
-  qualifier: 'TRK',
-  measures: measures
-};
-const exampleComponent2 = { uuid: 'B2', key: 'B' };
-const exampleComponent3 = { uuid: 'C3', key: 'C' };
-const exampleOnBrowse = sinon.spy();
-
-
-describe('Code :: Components', () => {
-
-  describe('<Breadcrumb/>', () => {
-    it('should render <ComponentName/>', () => {
-      const output = shallow(
-          <Breadcrumb
-              component={exampleComponent}
-              onBrowse={exampleOnBrowse}/>
-      );
-
-      expect(output.type())
-          .to.equal(ComponentName);
-      expect(output.props())
-          .to.deep.equal({ component: exampleComponent, onBrowse: exampleOnBrowse })
-    });
-  });
-
-  describe('<Breadcrumbs/>', () => {
-    let output;
-    let list;
-
-    before(() => {
-      output = shallow(
-          <Breadcrumbs
-              breadcrumbs={[exampleComponent, exampleComponent2, exampleComponent3]}
-              onBrowse={exampleOnBrowse}/>);
-      list = output.find(Breadcrumb);
-    });
-
-    it('should render list of <Breadcrumb/>s', () => {
-      expect(list)
-          .to.have.length(3);
-      expect(list.at(0).prop('component'))
-          .to.equal(exampleComponent);
-      expect(list.at(1).prop('component'))
-          .to.equal(exampleComponent2);
-      expect(list.at(2).prop('component'))
-          .to.equal(exampleComponent3);
-    });
-
-    it('should pass onBrowse to all components except the last one', () => {
-      expect(list.at(0).prop('onBrowse'))
-          .to.equal(exampleOnBrowse);
-      expect(list.at(1).prop('onBrowse'))
-          .to.equal(exampleOnBrowse);
-      expect(list.at(2).prop('onBrowse'))
-          .to.equal(null);
-    });
-  });
-
-  describe('<ComponentDetach/>', () => {
-    it('should render link', () => {
-      const output = shallow(
-          <ComponentDetach component={exampleComponent}/>);
-      const expectedUrl = getComponentUrl(exampleComponent.key);
-
-      expect(output.type())
-          .to.equal('a');
-      expect(output.prop('href'))
-          .to.equal(expectedUrl);
-    });
-  });
-
-  describe('<ComponentMeasure/>', () => {
-    it('should render formatted measure', () => {
-      const output = shallow(
-          <ComponentMeasure
-              component={exampleComponent}
-              metricKey="ncloc"
-              metricType="SHORT_INT"/>);
-
-      expect(output.text())
-          .to.equal('9.8k');
-    });
-
-    it('should not render measure', () => {
-      const output = shallow(
-          <ComponentMeasure
-              component={exampleComponent}
-              metricKey="random"
-              metricType="SHORT_INT"/>);
-
-      expect(output.text())
-          .to.equal('');
-    });
-  });
-
-  describe('<ComponentName/>', () => {
-    it('should render <QualifierIcon/>', () => {
-      const output = shallow(
-          <ComponentName
-              component={exampleComponent}
-              onBrowse={exampleOnBrowse}/>);
-      const findings = output.find(QualifierIcon);
-
-      expect(findings)
-          .to.have.length(1);
-      expect(findings.first().prop('qualifier'))
-          .to.equal('TRK');
-    });
-
-    it('should render link to component', () => {
-      const output = shallow(
-          <ComponentName
-              component={exampleComponent}
-              onBrowse={exampleOnBrowse}/>);
-      const findings = output.find('a');
-
-      expect(findings)
-          .to.have.length(1);
-      expect(findings.first().text())
-          .to.equal('AA');
-    });
-
-    it('should not render link to component', () => {
-      const output = shallow(
-          <ComponentName
-              component={exampleComponent}
-              onBrowse={null}/>);
-      const findings = output.find('span');
-
-      expect(output.find('a'))
-          .to.have.length(0);
-      expect(findings)
-          .to.have.length(1);
-      expect(findings.first().text())
-          .to.equal('AA');
-    });
-
-    it('should browse on click', () => {
-      const spy = sinon.spy();
-      const preventDefaultSpy = sinon.spy();
-      const output = shallow(
-          <ComponentName
-              component={exampleComponent}
-              onBrowse={spy}/>);
-      const findings = output.find('a');
-
-      findings.first().simulate('click', { preventDefault: preventDefaultSpy });
-
-      expect(preventDefaultSpy).to.have.been.called;
-      expect(spy).to.have.been.calledWith(exampleComponent);
-    });
-  });
-
-  describe('<ComponentsEmpty/>', () => {
-    it('should render', () => {
-      const output = shallow(<ComponentsEmpty/>);
-
-      expect(output.text())
-          .to.include('no_results');
-    });
-  });
-
-  describe('<Truncated/>', () => {
-    it('should render and set title', () => {
-      const output = shallow(<Truncated title="ABC">123</Truncated>);
-
-      expect(output.type())
-          .to.equal('span');
-      expect(output.text())
-          .to.equal('123');
-      expect(output.prop('title'))
-          .to.equal('ABC');
-    });
-  });
-});
diff --git a/server/sonar-web/tests/apps/code/store-test.js b/server/sonar-web/tests/apps/code/store-test.js
deleted file mode 100644 (file)
index 155c84a..0000000
+++ /dev/null
@@ -1,387 +0,0 @@
-import { expect } from 'chai';
-
-import { current, bucket, initialState } from '../../../src/main/js/apps/code/reducers';
-import {
-    initComponentAction,
-    browseAction,
-    searchAction,
-    updateQueryAction,
-    selectNext,
-    selectPrev,
-    startFetching,
-    stopFetching,
-    raiseError
-} from '../../../src/main/js/apps/code/actions';
-
-
-const exampleComponent = { key: 'A' };
-const exampleComponent2 = { key: 'B' };
-const exampleComponents = [
-  { key: 'B' },
-  { key: 'C' }
-];
-
-
-describe('Code :: Store', () => {
-  //describe('action creators');
-
-  describe('reducers', () => {
-    describe('current', () => {
-      describe('fetching', () => {
-        it('should be set to true', () => {
-          expect(current({ ...initialState, fetching: false }, startFetching()).fetching)
-              .to.equal(true);
-        });
-
-        it('should be false', () => {
-          expect(current({ ...initialState, fetching: true }, stopFetching()).fetching)
-              .to.equal(false);
-        });
-      });
-      describe('baseComponent', () => {
-        it('should be set', () => {
-          const component = {};
-          expect(current(initialState, browseAction(component)).baseComponent)
-              .to.equal(component);
-        });
-
-        it('should not be set for components with source code', () => {
-          const file = { qualifier: 'FIL' };
-          expect(current(initialState, browseAction(file, exampleComponents)).baseComponent)
-              .to.be.null;
-          const test = { qualifier: 'UTS' };
-          expect(current(initialState, browseAction(test, exampleComponents)).baseComponent)
-              .to.be.null;
-        });
-      });
-      describe('components', () => {
-        it('should be set', () => {
-          const component = {};
-          expect(current(initialState, browseAction(component, exampleComponents)).components)
-              .to.deep.equal(exampleComponents);
-        });
-
-        it('should sort components by name', () => {
-          const component = {};
-          const componentsBefore = [
-            { key: 'A', name: 'B' },
-            { key: 'B', name: 'A' }
-          ];
-          const componentsAfter = [
-            { key: 'B', name: 'A' },
-            { key: 'A', name: 'B' }
-          ];
-          expect(current(initialState, browseAction(component, componentsBefore)).components)
-              .to.deep.equal(componentsAfter);
-        });
-
-        it('should sort components by qualifier and then by name', () => {
-          const component = {};
-          const componentsBefore = [
-            { key: 'A', name: 'A', qualifier: 'DIR' },
-            { key: 'B', name: 'B', qualifier: 'FIL' }
-          ];
-          const componentsAfter = [
-            { key: 'B', name: 'B', qualifier: 'FIL' },
-            { key: 'A', name: 'A', qualifier: 'DIR' }
-          ];
-          expect(current(initialState, browseAction(component, componentsBefore)).components)
-              .to.deep.equal(componentsAfter);
-        });
-
-        it('should not be set for components with source code', () => {
-          const file = { qualifier: 'FIL' };
-          expect(current(initialState, browseAction(file, exampleComponents)).components)
-              .to.be.null;
-          const test = { qualifier: 'UTS' };
-          expect(current(initialState, browseAction(test, exampleComponents)).components)
-              .to.be.null;
-        });
-      });
-      describe('breadcrumbs', () => {
-        it('should be set', () => {
-          expect(current(initialState, browseAction(exampleComponent, [], exampleComponents)).breadcrumbs)
-              .to.deep.equal(exampleComponents);
-        });
-
-        it('should respect baseBreadcrumbs', () => {
-          const baseBreadcrumbs = [{ key: 'BASE1' }];
-          const breadcrumbsBefore = [{ key: 'BASE1' }, { key: 'BASE2' }, { key: 'C' }];
-          const breadcrumbsAfter = [{ key: 'BASE2' }, { key: 'C' }];
-          expect(current(
-              { ...initialState, baseBreadcrumbs },
-              browseAction(exampleComponent, [], breadcrumbsBefore)).breadcrumbs
-          ).to.deep.equal(breadcrumbsAfter);
-        });
-      });
-      describe('sourceViewer', () => {
-        it('should be set for components with source code', () => {
-          const file = { qualifier: 'FIL' };
-          expect(current(initialState, browseAction(file, exampleComponents)).sourceViewer)
-              .to.equal(file);
-          const test = { qualifier: 'UTS' };
-          expect(current(initialState, browseAction(test, exampleComponents)).sourceViewer)
-              .to.equal(test);
-        });
-
-        it('should not be set for components without source code', () => {
-          const project = { qualifier: 'TRK' };
-          expect(current(initialState, browseAction(project, exampleComponents)).sourceViewer)
-              .to.be.null;
-          const unknown = {};
-          expect(current(initialState, browseAction(unknown, exampleComponents)).sourceViewer)
-              .to.be.null;
-        });
-
-        it('should be reset', () => {
-          const stateBefore = Object.assign({}, initialState, { sourceViewer: exampleComponent });
-          expect(current(stateBefore, searchAction(exampleComponents)).sourceViewer)
-              .to.be.null;
-        });
-      });
-      describe('coverageMetric', () => {
-        it('should be set to "coverage"', () => {
-          const componentWithCoverage = {
-            ...exampleComponent,
-            measures: [
-              { metric: 'coverage', value: 13 }
-            ]
-          };
-
-          expect(current(initialState, initComponentAction(componentWithCoverage)).coverageMetric)
-              .to.equal('coverage');
-        });
-
-        it('should be set to "it_coverage"', () => {
-          const componentWithCoverage = {
-            ...exampleComponent,
-            measures: [
-              { metric: 'it_coverage', value: 13 }
-            ]
-          };
-
-          expect(current(initialState, initComponentAction(componentWithCoverage)).coverageMetric)
-              .to.equal('it_coverage');
-        });
-
-        it('should be set to "overall_coverage"', () => {
-          const componentWithCoverage = {
-            ...exampleComponent,
-            measures: [
-              { metric: 'coverage', value: 11 },
-              { metric: 'it_coverage', value: 12 },
-              { metric: 'overall_coverage', value: 13 }
-            ]
-          };
-
-          expect(current(initialState, initComponentAction(componentWithCoverage)).coverageMetric)
-              .to.equal('overall_coverage');
-        });
-
-        it('should fallback to "it_coverage"', () => {
-          const componentWithCoverage = {
-            ...exampleComponent,
-            measures: []
-          };
-
-          expect(current(initialState, initComponentAction(componentWithCoverage)).coverageMetric)
-              .to.equal('it_coverage');
-        });
-      });
-      describe('baseBreadcrumbs', () => {
-        it('should be empty', () => {
-          const component = { key: 'A' };
-          const breadcrumbs = [{ key: 'A' }];
-
-          expect(current(initialState, initComponentAction(component, breadcrumbs)).baseBreadcrumbs)
-              .to.have.length(0);
-        });
-
-        it('should set baseBreadcrumbs', () => {
-          const component = { key: 'A' };
-          const breadcrumbs = [{ key: 'BASE' }, { key: 'A' }];
-
-          expect(current(initialState, initComponentAction(component, breadcrumbs)).baseBreadcrumbs)
-              .to.have.length(1);
-        });
-      });
-      describe('searchResults', () => {
-        it('should be set', () => {
-          const results = [{ key: 'A' }, { key: 'B' }];
-          expect(current(initialState, searchAction(results)).searchResults)
-              .to.deep.equal(results)
-        });
-
-        it('should be reset', () => {
-          const results = [{ key: 'A' }, { key: 'B' }];
-          const stateBefore = Object.assign({}, initialState, { searchResults: results });
-          expect(current(stateBefore, browseAction(exampleComponent)).searchResults)
-              .to.be.null;
-        });
-      });
-      describe('searchQuery', () => {
-        it('should be set', () => {
-          expect(current(initialState, updateQueryAction('query')).searchQuery)
-              .to.equal('query');
-        });
-
-        it('should be reset', () => {
-          const stateBefore = Object.assign({}, initialState, { searchQuery: 'query' });
-          expect(current(stateBefore, browseAction(exampleComponent)).searchQuery)
-              .to.equal('');
-        });
-      });
-      describe('searchSelectedItem', () => {
-        it('should be set to the first result', () => {
-          const results = [exampleComponent, exampleComponent2];
-          expect(current(initialState, searchAction(results)).searchSelectedItem)
-              .to.equal(exampleComponent);
-        });
-
-        it('should select next', () => {
-          const results = [exampleComponent, exampleComponent2];
-          const stateBefore = current(initialState, searchAction(results));
-          const stateAfter = current(stateBefore, selectNext());
-          expect(stateAfter.searchSelectedItem)
-              .to.equal(exampleComponent2);
-        });
-
-        it('should not select next', () => {
-          const results = [exampleComponent, exampleComponent2];
-          const stateBefore = Object.assign({}, current(initialState, searchAction(results)), {
-            searchSelectedItem: exampleComponent2
-          });
-          expect(current(stateBefore, selectNext()).searchSelectedItem)
-              .to.equal(exampleComponent2);
-        });
-
-        it('should select prev', () => {
-          const results = [exampleComponent, exampleComponent2];
-          const stateBefore = Object.assign({}, current(initialState, searchAction(results)), {
-            searchSelectedItem: exampleComponent2
-          });
-          expect(current(stateBefore, selectPrev()).searchSelectedItem)
-              .to.equal(exampleComponent);
-        });
-
-        it('should not select prev', () => {
-          const results = [exampleComponent, exampleComponent2];
-          const stateBefore = current(initialState, searchAction(results));
-          expect(current(stateBefore, selectPrev()).searchSelectedItem)
-              .to.equal(exampleComponent);
-        });
-
-        it('should ignore if no results', () => {
-          expect(current(initialState, selectNext()).searchSelectedItem)
-              .to.be.null;
-          expect(current(initialState, selectPrev()).searchSelectedItem)
-              .to.be.null;
-        });
-
-        it('should be reset on browse', () => {
-          const results = [exampleComponent, exampleComponent2];
-          const stateBefore = current(initialState, searchAction(results));
-          const stateAfter = current(stateBefore, browseAction(exampleComponent));
-          expect(stateAfter.searchSelectedItem)
-              .to.be.null;
-        });
-      });
-      describe('errorMessage', () => {
-        it('should be set', () => {
-          expect(current(initialState, raiseError('error!')).errorMessage)
-              .to.equal('error!');
-        });
-
-        it('should be reset', () => {
-          const stateBefore = Object.assign({}, initialState, { errorMessage: 'error!' });
-          expect(current(stateBefore, browseAction(exampleComponent)).errorMessage)
-              .to.be.null;
-          expect(current(stateBefore, searchAction(exampleComponents)).errorMessage)
-              .to.be.null;
-        });
-      });
-    });
-    describe('bucket', () => {
-      it('should add initial component', () => {
-        expect(bucket([], initComponentAction(exampleComponent)))
-            .to.deep.equal([exampleComponent]);
-      });
-
-      it('should add browsed component', () => {
-        const componentBefore = { key: 'A' };
-        const childrenBefore = [{ key: 'B' }];
-        const breadcrumbsBefore = [{ key: 'A' }];
-
-        const bucketAfter = [
-          { key: 'A', breadcrumbs: [{ key: 'A' }], children: [{ key: 'B' }], total: 0 },
-          { key: 'B', breadcrumbs: [{ key: 'A' }, { key: 'B' }] }
-        ];
-
-        expect(bucket([], browseAction(componentBefore, childrenBefore, breadcrumbsBefore)))
-            .to.deep.equal(bucketAfter);
-      });
-
-      it('should merge new components', () => {
-        const componentBefore = { key: 'A' };
-        const childrenBefore = [{ key: 'B' }];
-        const breadcrumbsBefore = [{ key: 'A' }];
-
-        const bucketBefore = [
-          { key: 'A' },
-          { key: 'B' }
-        ];
-
-        const bucketAfter = [
-          {
-            key: 'A',
-            breadcrumbs: [{ key: 'A' }],
-            children: [{ key: 'B' }],
-            total: 0
-          },
-          {
-            key: 'B',
-            breadcrumbs: [{ key: 'A' }, { key: 'B' }]
-          }
-        ];
-
-        expect(bucket(bucketBefore, browseAction(componentBefore, childrenBefore, breadcrumbsBefore)))
-            .to.deep.equal(bucketAfter);
-      });
-
-      it('should work twice in a row', () => {
-        const componentA = { key: 'A' };
-        const childrenA = [{ key: 'B' }];
-        const breadcrumbsA = [{ key: 'A' }];
-
-        const componentB = { key: 'B' };
-        const childrenB = [{ key: 'C' }];
-        const breadcrumbsB = [{ key: 'A' }, { key: 'B' }];
-
-        const bucketAfter = [
-          {
-            key: 'A',
-            breadcrumbs: [{ key: 'A' }],
-            children: [{ key: 'B' }],
-            total: 0
-          },
-          {
-            key: 'B',
-            breadcrumbs: [{ key: 'A' }, { key: 'B' }],
-            children: [{ key: 'C' }],
-            total: 0
-          },
-          {
-            key: 'C',
-            breadcrumbs: [{ key: 'A' }, { key: 'B' }, { key: 'C' }]
-          }
-        ];
-
-        const afterFirstPass = bucket([], browseAction(componentA, childrenA, breadcrumbsA));
-        const afterSecondPass = bucket(afterFirstPass, browseAction(componentB, childrenB, breadcrumbsB));
-
-        expect(afterSecondPass)
-            .to.deep.equal(bucketAfter);
-      });
-    });
-  });
-});