From 451eec53f11c3f64787b488d06a415b5139055c7 Mon Sep 17 00:00:00 2001 From: Stas Vilchik Date: Mon, 30 May 2016 16:49:22 +0200 Subject: [PATCH] refactor code page (#912) --- .../java/it/sourceCode/ProjectCodeTest.java | 4 +- .../code_page_should_expand_root_dir.html | 2 +- .../sourceCode/ProjectCodeTest/permalink.html | 35 ++ .../sourceCode/ProjectCodeTest/search.html | 60 +++ .../src/main/js/apps/code/actions/index.js | 271 ------------ server/sonar-web/src/main/js/apps/code/app.js | 38 +- .../{store/configureStore.js => bucket.js} | 41 +- .../main/js/apps/code/{styles => }/code.css | 18 +- .../src/main/js/apps/code/components/App.js | 212 ++++++++++ .../js/apps/code/components/Breadcrumb.js | 5 +- .../js/apps/code/components/Breadcrumbs.js | 5 +- .../src/main/js/apps/code/components/Code.js | 150 ------- .../main/js/apps/code/components/Component.js | 25 +- .../js/apps/code/components/ComponentName.js | 20 +- .../js/apps/code/components/Components.js | 7 +- .../apps/code/components/ComponentsEmpty.js | 2 +- .../main/js/apps/code/components/Search.js | 212 ++++++++-- .../src/main/js/apps/code/reducers/index.js | 206 ---------- .../sonar-web/src/main/js/apps/code/utils.js | 175 ++++++++ .../details/drilldown/ListView.js | 2 +- .../details/drilldown/TreeView.js | 2 +- .../source-viewer}/SourceViewer.js | 17 +- .../sonar-web/src/main/js/helpers/measures.js | 19 + .../main/nav/component/component-nav-menu.js | 18 +- .../tests/apps/code/components-test.js | 204 --------- .../sonar-web/tests/apps/code/store-test.js | 387 ------------------ 26 files changed, 782 insertions(+), 1355 deletions(-) create mode 100644 it/it-tests/src/test/resources/sourceCode/ProjectCodeTest/permalink.html create mode 100644 it/it-tests/src/test/resources/sourceCode/ProjectCodeTest/search.html delete mode 100644 server/sonar-web/src/main/js/apps/code/actions/index.js rename server/sonar-web/src/main/js/apps/code/{store/configureStore.js => bucket.js} (55%) rename server/sonar-web/src/main/js/apps/code/{styles => }/code.css (77%) create mode 100644 server/sonar-web/src/main/js/apps/code/components/App.js delete mode 100644 server/sonar-web/src/main/js/apps/code/components/Code.js delete mode 100644 server/sonar-web/src/main/js/apps/code/reducers/index.js create mode 100644 server/sonar-web/src/main/js/apps/code/utils.js rename server/sonar-web/src/main/js/{apps/code/components => components/source-viewer}/SourceViewer.js (81%) delete mode 100644 server/sonar-web/tests/apps/code/components-test.js delete mode 100644 server/sonar-web/tests/apps/code/store-test.js diff --git a/it/it-tests/src/test/java/it/sourceCode/ProjectCodeTest.java b/it/it-tests/src/test/java/it/sourceCode/ProjectCodeTest.java index bd794bf6682..c48b11d6380 100644 --- a/it/it-tests/src/test/java/it/sourceCode/ProjectCodeTest.java +++ b/it/it-tests/src/test/java/it/sourceCode/ProjectCodeTest.java @@ -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); } diff --git a/it/it-tests/src/test/resources/sourceCode/ProjectCodeTest/code_page_should_expand_root_dir.html b/it/it-tests/src/test/resources/sourceCode/ProjectCodeTest/code_page_should_expand_root_dir.html index 4cc72851fb5..c9737d5fe25 100644 --- a/it/it-tests/src/test/resources/sourceCode/ProjectCodeTest/code_page_should_expand_root_dir.html +++ b/it/it-tests/src/test/resources/sourceCode/ProjectCodeTest/code_page_should_expand_root_dir.html @@ -22,7 +22,7 @@ waitForText css=#content - *Hello.xoo* + *Hello.xoo*src/main/xoo/sample* 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 index 00000000000..69364a69079 --- /dev/null +++ b/it/it-tests/src/test/resources/sourceCode/ProjectCodeTest/permalink.html @@ -0,0 +1,35 @@ + + + + + + + test_project_code_page + + + + + + + + + + + + + + + + + + + + + + + + + +
test_project_code_page
open/code?id=project-for-code&selected=project-for-code%3Asrc%2Fmain%2Fxoo%2Fsample%2FSample.xoo
waitForTextcss=#content*public class Sample*
waitForTextcss=.code-breadcrumbs*Project For Code*src/main/xoo/sample*Sample.xoo*
+ + 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 index 00000000000..1594ee28cd5 --- /dev/null +++ b/it/it-tests/src/test/resources/sourceCode/ProjectCodeTest/search.html @@ -0,0 +1,60 @@ + + + + + + + test_project_code_page + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
test_project_code_page
open/code?id=project-for-code
waitForTextcss=#content*Project For Code*13*0*0*0.0%*
typecss=.search-box-inputxoo
clickcss=.search-box-submit
waitForTextcss=#content*Sample.xoo*
clickcss=.code-name-cell a
waitForTextcss=#content*public class Sample*
waitForTextcss=.code-breadcrumbs*Project For Code*src/main/xoo/sample*Sample.xoo*
+ + 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 index 2422d993362..00000000000 --- a/server/sonar-web/src/main/js/apps/code/actions/index.js +++ /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)); - } - }; -} diff --git a/server/sonar-web/src/main/js/apps/code/app.js b/server/sonar-web/src/main/js/apps/code/app.js index c907e417174..fc13bb30a31 100644 --- a/server/sonar-web/src/main/js/apps/code/app.js +++ b/server/sonar-web/src/main/js/apps/code/app.js @@ -19,32 +19,26 @@ */ 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 ; + const AppWithComponent = (props) => { + return ; }; - render( - - - - - - , - document.querySelector(el)); + render(( + + + + + ), el); }); diff --git a/server/sonar-web/src/main/js/apps/code/store/configureStore.js b/server/sonar-web/src/main/js/apps/code/bucket.js similarity index 55% rename from server/sonar-web/src/main/js/apps/code/store/configureStore.js rename to server/sonar-web/src/main/js/apps/code/bucket.js index 92ce2ac3998..ced8587df24 100644 --- a/server/sonar-web/src/main/js/apps/code/store/configureStore.js +++ b/server/sonar-web/src/main/js/apps/code/bucket.js @@ -17,27 +17,30 @@ * 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 bucket = {}; +const childrenBucket = {}; +const breadcrumbsBucket = {}; -const logger = createLogger({ - predicate: () => process.env.NODE_ENV !== 'production' -}); +export function addComponent (component) { + bucket[component.key] = component; +} + +export function getComponent (componentKey) { + return bucket[componentKey]; +} -const createStoreWithMiddleware = applyMiddleware( - thunk, - logger -)(createStore); +export function addComponentChildren (componentKey, children, total) { + childrenBucket[componentKey] = { children, total }; +} -const reducer = combineReducers({ - routing: routeReducer, - current, - bucket -}); +export function getComponentChildren (componentKey) { + return childrenBucket[componentKey]; +} + +export function addComponentBreadcrumbs (componentKey, breadcrumbs) { + breadcrumbsBucket[componentKey] = breadcrumbs; +} -export default function configureStore () { - return createStoreWithMiddleware(reducer); +export function getComponentBreadcrumbs (componentKey) { + return breadcrumbsBucket[componentKey]; } diff --git a/server/sonar-web/src/main/js/apps/code/styles/code.css b/server/sonar-web/src/main/js/apps/code/code.css similarity index 77% rename from server/sonar-web/src/main/js/apps/code/styles/code.css rename to server/sonar-web/src/main/js/apps/code/code.css index 90fe862a144..297babde59d 100644 --- a/server/sonar-web/src/main/js/apps/code/styles/code.css +++ b/server/sonar-web/src/main/js/apps/code/code.css @@ -47,18 +47,24 @@ max-width: 100%; } -.code-search-box { - float: left; +.code-search { + margin-bottom: 10px; +} + +.code-search-with-results + .code-components { + display: none; +} + +.code-search .search-box { padding-right: 10px; } -.code-search-box .note { - margin-top: 4px; - margin-left: 25px; +.code-search .search-box .note { + vertical-align: middle; opacity: 0; transition: opacity 0.3s ease; } -.code-search-box input.touched ~ .note { +.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 index 00000000000..185f988c548 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/code/components/App.js @@ -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 ( +
+ {error && ( +
+ {error} +
+ )} + + + + +
+ {shouldShowBreadcrumbs && ( + + )} + + {shouldShowComponents && ( +
+ +
+ )} + + {shouldShowComponents && ( + + )} + + {shouldShowSourceViewer && ( +
+ +
+ )} +
+
+ ); + } +} diff --git a/server/sonar-web/src/main/js/apps/code/components/Breadcrumb.js b/server/sonar-web/src/main/js/apps/code/components/Breadcrumb.js index ad6f2786c46..1280e2289b4 100644 --- a/server/sonar-web/src/main/js/apps/code/components/Breadcrumb.js +++ b/server/sonar-web/src/main/js/apps/code/components/Breadcrumb.js @@ -21,10 +21,11 @@ import React from 'react'; import ComponentName from './ComponentName'; -const Breadcrumb = ({ component, onBrowse }) => ( +const Breadcrumb = ({ rootComponent, component, canBrowse }) => ( + canBrowse={canBrowse}/> ); export default Breadcrumb; diff --git a/server/sonar-web/src/main/js/apps/code/components/Breadcrumbs.js b/server/sonar-web/src/main/js/apps/code/components/Breadcrumbs.js index a724db37337..611b98b1b5d 100644 --- a/server/sonar-web/src/main/js/apps/code/components/Breadcrumbs.js +++ b/server/sonar-web/src/main/js/apps/code/components/Breadcrumbs.js @@ -21,13 +21,14 @@ import React from 'react'; import Breadcrumb from './Breadcrumb'; -const Breadcrumbs = ({ breadcrumbs, onBrowse }) => ( +const Breadcrumbs = ({ rootComponent, breadcrumbs }) => (
    {breadcrumbs.map((component, index) => (
  • + canBrowse={index < breadcrumbs.length - 1}/>
  • ))}
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 index 16ea3bd1767..00000000000 --- a/server/sonar-web/src/main/js/apps/code/components/Code.js +++ /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 ( -
-
- - -
- -
-
- - {errorMessage && ( -
- {errorMessage} -
- )} - - {shouldShowBreadcrumbs && ( - - )} - - {shouldShowSearchResults && ( -
- -
- )} - - {shouldShowComponents && ( -
- -
- )} - - {shouldShowComponents && ( - - )} - - {shouldShowSourceViewer && ( -
- -
- )} -
- ); - } -} - -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); diff --git a/server/sonar-web/src/main/js/apps/code/components/Component.js b/server/sonar-web/src/main/js/apps/code/components/Component.js index 47efb73c58c..5134427d03d 100644 --- a/server/sonar-web/src/main/js/apps/code/components/Component.js +++ b/server/sonar-web/src/main/js/apps/code/components/Component.js @@ -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 { )} + canBrowse={canBrowse}/>
@@ -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); diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentName.js b/server/sonar-web/src/main/js/apps/code/components/ComponentName.js index 2b14457fb25..c33815f511d 100644 --- a/server/sonar-web/src/main/js/apps/code/components/ComponentName.js +++ b/server/sonar-web/src/main/js/apps/code/components/ComponentName.js @@ -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 = {name}; } else { - if (onBrowse) { - inner = {name}; + if (canBrowse) { + const query = { id: rootComponent.key }; + if (component.key !== rootComponent.key) { + Object.assign(query, { selected: component.key }); + } + inner = ( + + {name} + + ); } else { inner = {name}; } diff --git a/server/sonar-web/src/main/js/apps/code/components/Components.js b/server/sonar-web/src/main/js/apps/code/components/Components.js index 91300d647ed..249e3e540e6 100644 --- a/server/sonar-web/src/main/js/apps/code/components/Components.js +++ b/server/sonar-web/src/main/js/apps/code/components/Components.js @@ -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 }) => ( {baseComponent && ( @@ -42,10 +43,12 @@ const Components = ({ baseComponent, components, coverageMetric, onBrowse }) => components.map((component, index, list) => ( 0 ? list[index - 1] : null} coverageMetric={coverageMetric} - onBrowse={onBrowse}/> + canBrowse={true}/> )) ) : ( diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentsEmpty.js b/server/sonar-web/src/main/js/apps/code/components/ComponentsEmpty.js index 753b2de8b13..c2afced5689 100644 --- a/server/sonar-web/src/main/js/apps/code/components/ComponentsEmpty.js +++ b/server/sonar-web/src/main/js/apps/code/components/ComponentsEmpty.js @@ -25,7 +25,7 @@ const ComponentsEmpty = () => ( - diff --git a/server/sonar-web/src/main/js/apps/code/components/Search.js b/server/sonar-web/src/main/js/apps/code/components/Search.js index fb373856470..d54f727e5f3 100644 --- a/server/sonar-web/src/main/js/apps/code/components/Search.js +++ b/server/sonar-web/src/main/js/apps/code/components/Search.js @@ -17,78 +17,212 @@ * 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 ( - - - -
- {translateWithParameters('select2.tooShort', 3)} -
- + ); } } - -export default connect(state => { - return { query: state.current.searchQuery }; -})(Search); 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 index a554d94d26d..00000000000 --- a/server/sonar-web/src/main/js/apps/code/reducers/index.js +++ /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/utils.js b/server/sonar-web/src/main/js/apps/code/utils.js new file mode 100644 index 00000000000..b236ee82d92 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/code/utils.js @@ -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')); + } +} diff --git a/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ListView.js b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ListView.js index 3fd24c2c265..0f4ec641155 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ListView.js +++ b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ListView.js @@ -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 { diff --git a/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/TreeView.js b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/TreeView.js index d42bbf7167b..04875916ef2 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/TreeView.js +++ b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/TreeView.js @@ -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/apps/code/components/SourceViewer.js b/server/sonar-web/src/main/js/components/source-viewer/SourceViewer.js similarity index 81% rename from server/sonar-web/src/main/js/apps/code/components/SourceViewer.js rename to server/sonar-web/src/main/js/components/source-viewer/SourceViewer.js index d79983625c6..024d7501b5b 100644 --- a/server/sonar-web/src/main/js/apps/code/components/SourceViewer.js +++ b/server/sonar-web/src/main/js/components/source-viewer/SourceViewer.js @@ -17,12 +17,19 @@ * 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 React from 'react'; -import BaseSourceViewer from '../../../components/source-viewer/main'; -import { getPeriodDate, getPeriodLabel } from '../../../helpers/periods'; +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 + }; -export default class SourceViewer extends Component { componentDidMount () { this.renderSourceViewer(); } @@ -65,6 +72,6 @@ export default class SourceViewer extends Component { } render () { - return
; + return
; } } diff --git a/server/sonar-web/src/main/js/helpers/measures.js b/server/sonar-web/src/main/js/helpers/measures.js index bf6df21284a..c965c1577a6 100644 --- a/server/sonar-web/src/main/js/helpers/measures.js +++ b/server/sonar-web/src/main/js/helpers/measures.js @@ -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 */ diff --git a/server/sonar-web/src/main/js/main/nav/component/component-nav-menu.js b/server/sonar-web/src/main/js/main/nav/component/component-nav-menu.js index 885318c9030..6ce4822c186 100644 --- a/server/sonar-web/src/main/js/main/nav/component/component-nav-menu.js +++ b/server/sonar-web/src/main/js/main/nav/component/component-nav-menu.js @@ -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()} @@ -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 index 9613032c75f..00000000000 --- a/server/sonar-web/tests/apps/code/components-test.js +++ /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('', () => { - it('should render ', () => { - const output = shallow( - - ); - - expect(output.type()) - .to.equal(ComponentName); - expect(output.props()) - .to.deep.equal({ component: exampleComponent, onBrowse: exampleOnBrowse }) - }); - }); - - describe('', () => { - let output; - let list; - - before(() => { - output = shallow( - ); - list = output.find(Breadcrumb); - }); - - it('should render list of 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('', () => { - it('should render link', () => { - const output = shallow( - ); - const expectedUrl = getComponentUrl(exampleComponent.key); - - expect(output.type()) - .to.equal('a'); - expect(output.prop('href')) - .to.equal(expectedUrl); - }); - }); - - describe('', () => { - it('should render formatted measure', () => { - const output = shallow( - ); - - expect(output.text()) - .to.equal('9.8k'); - }); - - it('should not render measure', () => { - const output = shallow( - ); - - expect(output.text()) - .to.equal(''); - }); - }); - - describe('', () => { - it('should render ', () => { - const output = shallow( - ); - 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( - ); - 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( - ); - 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( - ); - 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('', () => { - it('should render', () => { - const output = shallow(); - - expect(output.text()) - .to.include('no_results'); - }); - }); - - describe('', () => { - it('should render and set title', () => { - const output = shallow(123); - - 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 index 155c84a8720..00000000000 --- a/server/sonar-web/tests/apps/code/store-test.js +++ /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); - }); - }); - }); -}); -- 2.39.5
{translate('no_results')} +