From cc9c506efd50786649658baa0d243654c59c512e Mon Sep 17 00:00:00 2001 From: Stas Vilchik Date: Mon, 21 Mar 2016 18:28:05 +0100 Subject: [PATCH] SONAR-7402 improve list and tree views on measures page --- .../it/measure/ProjectMeasuresPageTest.java | 32 ++- .../should_display_measures_page.html | 5 + .../should_drilldown_on_list_view.html | 69 +++++ .../should_drilldown_on_tree_view.html | 84 ++++++ server/sonar-web/package.json | 4 +- .../sonar-web/src/main/js/api/components.js | 20 +- .../main/js/apps/component-measures/app.js | 55 ++-- .../ComponentMeasuresApp.js => app/App.js} | 49 +--- .../component-measures/app/AppContainer.js | 40 +++ .../js/apps/component-measures/app/actions.js | 53 ++++ .../js/apps/component-measures/app/reducer.js | 34 +++ .../components/AllMeasures.js | 110 -------- .../components/MeasureDetails.js | 149 ----------- .../components/MeasureDetailsSeeAlso.js | 160 ------------ .../components/MeasureDrilldownComponents.js | 77 ------ .../components/MeasureDrilldownList.js | 129 --------- .../components/MeasureDrilldownTree.js | 139 ---------- .../{ => config}/bubbles.js | 0 .../details/MeasureDetails.js | 101 ++++++++ .../details/MeasureDetailsContainer.js | 44 ++++ .../MeasureDetailsHeader.js | 30 +-- .../component-measures/details/actions.js | 77 ++++++ .../bubbleChart/BubbleChart.js} | 43 ++- .../MeasureBubbleChartContainer.js | 39 +++ .../details/drilldown/Breadcrumb.js | 62 +++++ .../details/drilldown/Breadcrumbs.js | 37 +++ .../details/drilldown/ComponentCell.js | 78 ++++++ .../drilldown/ComponentsList.js} | 36 ++- .../details/drilldown/ComponentsListRow.js | 44 ++++ .../drilldown/EmptyComponentsList.js} | 10 +- .../details/drilldown/ListHeader.js | 68 +++++ .../details/drilldown/ListView.js | 130 ++++++++++ .../details/drilldown/ListViewContainer.js | 54 ++++ .../details/drilldown/MeasureCell.js | 39 +++ .../drilldown}/MeasureDrilldown.js | 65 ++--- .../details/drilldown/TreeView.js | 128 +++++++++ .../details/drilldown/TreeViewContainer.js | 65 +++++ .../history}/MeasureHistory.js | 21 +- .../history/MeasureHistoryContainer.js | 38 +++ .../component-measures/details/reducer.js | 42 +++ .../treemap}/MeasureTreemap.js | 39 ++- .../treemap/MeasureTreemapContainer.js | 38 +++ .../component-measures/home/AllMeasures.js | 64 +++++ .../home/AllMeasuresContainer.js | 46 ++++ .../{components => home}/AllMeasuresDomain.js | 4 +- .../apps/component-measures/home/actions.js | 59 +++++ .../apps/component-measures/home/reducer.js | 34 +++ .../store/configureStore.js | 41 +++ .../store/listViewActions.js | 143 ++++++++++ .../store/listViewReducer.js | 37 +++ .../component-measures/store/statusActions.js | 29 +++ .../component-measures/store/statusReducer.js | 36 +++ .../store/treeViewActions.js | 244 ++++++++++++++++++ .../store/treeViewReducer.js | 42 +++ .../js/apps/component-measures/styles.css | 99 +++---- .../main/js/apps/component-measures/utils.js | 11 +- .../main/js/components/shared/list-footer.js | 1 + .../main/js/components/source-viewer/main.js | 1 + .../js/components/store/configureStore.js | 4 +- server/sonar-web/src/main/js/helpers/path.js | 13 + .../src/main/js/libs/third-party/jquery-ui.js | 2 +- .../resources/org/sonar/l10n/core.properties | 3 + 62 files changed, 2395 insertions(+), 1055 deletions(-) create mode 100644 it/it-tests/src/test/resources/measure/ProjectMeasuresPageTest/should_drilldown_on_list_view.html create mode 100644 it/it-tests/src/test/resources/measure/ProjectMeasuresPageTest/should_drilldown_on_tree_view.html rename server/sonar-web/src/main/js/apps/component-measures/{components/ComponentMeasuresApp.js => app/App.js} (51%) create mode 100644 server/sonar-web/src/main/js/apps/component-measures/app/AppContainer.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/app/actions.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/app/reducer.js delete mode 100644 server/sonar-web/src/main/js/apps/component-measures/components/AllMeasures.js delete mode 100644 server/sonar-web/src/main/js/apps/component-measures/components/MeasureDetails.js delete mode 100644 server/sonar-web/src/main/js/apps/component-measures/components/MeasureDetailsSeeAlso.js delete mode 100644 server/sonar-web/src/main/js/apps/component-measures/components/MeasureDrilldownComponents.js delete mode 100644 server/sonar-web/src/main/js/apps/component-measures/components/MeasureDrilldownList.js delete mode 100644 server/sonar-web/src/main/js/apps/component-measures/components/MeasureDrilldownTree.js rename server/sonar-web/src/main/js/apps/component-measures/{ => config}/bubbles.js (100%) create mode 100644 server/sonar-web/src/main/js/apps/component-measures/details/MeasureDetails.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/details/MeasureDetailsContainer.js rename server/sonar-web/src/main/js/apps/component-measures/{components => details}/MeasureDetailsHeader.js (80%) create mode 100644 server/sonar-web/src/main/js/apps/component-measures/details/actions.js rename server/sonar-web/src/main/js/apps/component-measures/{components/MeasureBubbleChart.js => details/bubbleChart/BubbleChart.js} (80%) create mode 100644 server/sonar-web/src/main/js/apps/component-measures/details/bubbleChart/MeasureBubbleChartContainer.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/details/drilldown/Breadcrumb.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/details/drilldown/Breadcrumbs.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ComponentCell.js rename server/sonar-web/src/main/js/apps/component-measures/{components/IconUp.js => details/drilldown/ComponentsList.js} (55%) create mode 100644 server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ComponentsListRow.js rename server/sonar-web/src/main/js/apps/component-measures/{components/MeasureDrilldownEmpty.js => details/drilldown/EmptyComponentsList.js} (84%) create mode 100644 server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ListHeader.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ListView.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ListViewContainer.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/details/drilldown/MeasureCell.js rename server/sonar-web/src/main/js/apps/component-measures/{components => details/drilldown}/MeasureDrilldown.js (66%) create mode 100644 server/sonar-web/src/main/js/apps/component-measures/details/drilldown/TreeView.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/details/drilldown/TreeViewContainer.js rename server/sonar-web/src/main/js/apps/component-measures/{components => details/history}/MeasureHistory.js (89%) create mode 100644 server/sonar-web/src/main/js/apps/component-measures/details/history/MeasureHistoryContainer.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/details/reducer.js rename server/sonar-web/src/main/js/apps/component-measures/{components => details/treemap}/MeasureTreemap.js (85%) create mode 100644 server/sonar-web/src/main/js/apps/component-measures/details/treemap/MeasureTreemapContainer.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/home/AllMeasures.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/home/AllMeasuresContainer.js rename server/sonar-web/src/main/js/apps/component-measures/{components => home}/AllMeasuresDomain.js (95%) create mode 100644 server/sonar-web/src/main/js/apps/component-measures/home/actions.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/home/reducer.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/store/configureStore.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/store/listViewActions.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/store/listViewReducer.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/store/statusActions.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/store/statusReducer.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/store/treeViewActions.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/store/treeViewReducer.js diff --git a/it/it-tests/src/test/java/it/measure/ProjectMeasuresPageTest.java b/it/it-tests/src/test/java/it/measure/ProjectMeasuresPageTest.java index 10738476d27..a84550085ec 100644 --- a/it/it-tests/src/test/java/it/measure/ProjectMeasuresPageTest.java +++ b/it/it-tests/src/test/java/it/measure/ProjectMeasuresPageTest.java @@ -21,6 +21,7 @@ package it.measure; import com.sonar.orchestrator.Orchestrator; import com.sonar.orchestrator.build.SonarRunner; +import com.sonar.orchestrator.build.SonarScanner; import com.sonar.orchestrator.selenium.Selenese; import it.Category1Suite; import org.junit.BeforeClass; @@ -38,9 +39,18 @@ public class ProjectMeasuresPageTest { @BeforeClass public static void inspectProject() { orchestrator.executeBuild( - SonarRunner.create(projectDir("shared/xoo-sample")) - .setProjectKey("project-measures-page-test-project") - .setProjectName("ProjectMeasuresPageTest Project") + SonarScanner + .create(projectDir("shared/xoo-sample")) + .setProperty("sonar.projectKey", "project-measures-page-test-project") + .setProperty("sonar.projectName", "ProjectMeasuresPageTest Project") + ); + + // one more time + orchestrator.executeBuild( + SonarScanner + .create(projectDir("shared/xoo-sample")) + .setProperty("sonar.projectKey", "project-measures-page-test-project") + .setProperty("sonar.projectName", "ProjectMeasuresPageTest Project") ); } @@ -52,4 +62,20 @@ public class ProjectMeasuresPageTest { new SeleneseTest(selenese).runOn(orchestrator); } + @Test + public void should_drilldown_on_list_view() { + Selenese selenese = Selenese.builder().setHtmlTestsInClasspath("should_drilldown_on_list_view", + "/measure/ProjectMeasuresPageTest/should_drilldown_on_list_view.html" + ).build(); + new SeleneseTest(selenese).runOn(orchestrator); + } + + @Test + public void should_drilldown_on_tree_view() { + Selenese selenese = Selenese.builder().setHtmlTestsInClasspath("should_drilldown_on_tree_view", + "/measure/ProjectMeasuresPageTest/should_drilldown_on_tree_view.html" + ).build(); + new SeleneseTest(selenese).runOn(orchestrator); + } + } diff --git a/it/it-tests/src/test/resources/measure/ProjectMeasuresPageTest/should_display_measures_page.html b/it/it-tests/src/test/resources/measure/ProjectMeasuresPageTest/should_display_measures_page.html index 67fdb23d451..40992b13252 100644 --- a/it/it-tests/src/test/resources/measure/ProjectMeasuresPageTest/should_display_measures_page.html +++ b/it/it-tests/src/test/resources/measure/ProjectMeasuresPageTest/should_display_measures_page.html @@ -23,6 +23,11 @@ id=measure-ncloc *13* + + assertText + id=measure-ncloc-leak + *+0* + click css=#measure-ncloc > a diff --git a/it/it-tests/src/test/resources/measure/ProjectMeasuresPageTest/should_drilldown_on_list_view.html b/it/it-tests/src/test/resources/measure/ProjectMeasuresPageTest/should_drilldown_on_list_view.html new file mode 100644 index 00000000000..ee439b1286c --- /dev/null +++ b/it/it-tests/src/test/resources/measure/ProjectMeasuresPageTest/should_drilldown_on_list_view.html @@ -0,0 +1,69 @@ + + + + + + should_drilldown_on_list_view + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
open/component_measures/ncloc/list?id=project-measures-page-test-project
waitForElementPresentid=component-measures-component-link-project-measures-page-test-project:src/main/xoo/sample/Sample.xoo
assertTextid=component-measures-component-link-project-measures-page-test-project:src/main/xoo/sample/Sample.xoo*src/main/xoo/sample/Sample.xoo*
assertTextid=component-measures-component-measure-project-measures-page-test-project:src/main/xoo/sample/Sample.xoo*13*
clickid=component-measures-component-link-project-measures-page-test-project:src/main/xoo/sample/Sample.xoo
waitForElementPresentcss=.source-line-code
waitForElementPresentcss=.component-measures-breadcrumbs
waitForElementPresentid=component-measures-breadcrumb-project-measures-page-test-project
assertTextcss=.component-measures-breadcrumbs*Sample.xoo*13*
clickid=component-measures-breadcrumb-project-measures-page-test-project
waitForElementPresentid=component-measures-component-link-project-measures-page-test-project:src/main/xoo/sample/Sample.xoo
+ + diff --git a/it/it-tests/src/test/resources/measure/ProjectMeasuresPageTest/should_drilldown_on_tree_view.html b/it/it-tests/src/test/resources/measure/ProjectMeasuresPageTest/should_drilldown_on_tree_view.html new file mode 100644 index 00000000000..399b22cf4d3 --- /dev/null +++ b/it/it-tests/src/test/resources/measure/ProjectMeasuresPageTest/should_drilldown_on_tree_view.html @@ -0,0 +1,84 @@ + + + + + + should_drilldown_on_tree_view + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
open/component_measures/ncloc/tree?id=project-measures-page-test-project
waitForElementPresentid=component-measures-component-link-project-measures-page-test-project:src/main/xoo/sample
assertTextid=component-measures-component-link-project-measures-page-test-project:src/main/xoo/sample*src/main/xoo/sample*
assertTextid=component-measures-component-measure-project-measures-page-test-project:src/main/xoo/sample*13*
clickid=component-measures-component-link-project-measures-page-test-project:src/main/xoo/sample
waitForElementPresentid=component-measures-component-link-project-measures-page-test-project:src/main/xoo/sample/Sample.xoo
assertTextid=component-measures-component-link-project-measures-page-test-project:src/main/xoo/sample/Sample.xoo*src/main/xoo/sample/Sample.xoo*
assertTextid=component-measures-component-measure-project-measures-page-test-project:src/main/xoo/sample/Sample.xoo*13*
assertTextcss=.component-measures-breadcrumbs*ProjectMeasuresPageTest Project*src/main/xoo/sample*13*
clickid=component-measures-component-link-project-measures-page-test-project:src/main/xoo/sample/Sample.xoo
waitForElementPresentcss=.source-line-code
assertTextcss=.component-measures-breadcrumbs*ProjectMeasuresPageTest Project*src/main/xoo/sample*13*Sample.xoo*13*
clickid=component-measures-breadcrumb-project-measures-page-test-project
waitForElementPresentid=component-measures-component-link-project-measures-page-test-project:src/main/xoo/sample
+ + diff --git a/server/sonar-web/package.json b/server/sonar-web/package.json index 43a3f6e3f3d..2a2b3149855 100644 --- a/server/sonar-web/package.json +++ b/server/sonar-web/package.json @@ -58,9 +58,9 @@ "react-dom": "0.14.2", "react-redux": "4.0.1", "react-router": "2.0.0", - "react-router-redux": "^4.0.0", + "react-router-redux": "4.0.0", "react-select": "1.0.0-beta6", - "redux": "3.0.5", + "redux": "3.3.1", "redux-logger": "2.2.1", "redux-simple-router": "1.0.1", "redux-thunk": "1.0.2", diff --git a/server/sonar-web/src/main/js/api/components.js b/server/sonar-web/src/main/js/api/components.js index e567727e3d1..b77f6a684ce 100644 --- a/server/sonar-web/src/main/js/api/components.js +++ b/server/sonar-web/src/main/js/api/components.js @@ -45,24 +45,22 @@ export function createProject (data) { return postJSON(url, data); } -export function getChildren (componentKey, metrics = [], additional = {}) { +export function getComponentTree (strategy, componentKey, metrics = [], additional = {}) { const url = '/api/measures/component_tree'; const data = Object.assign({}, additional, { baseComponentKey: componentKey, metricKeys: metrics.join(','), - strategy: 'children' + strategy }); - return getJSON(url, data).then(r => r.components); + return getJSON(url, data); } -export function getFiles (componentKey, metrics = [], additional = {}) { - const url = '/api/measures/component_tree'; - const data = Object.assign({}, additional, { - baseComponentKey: componentKey, - metricKeys: metrics.join(','), - strategy: 'leaves' - }); - return getJSON(url, data).then(r => r.components); +export function getChildren (componentKey, metrics, additional) { + return getComponentTree('children', componentKey, metrics, additional).then(r => r.components); +} + +export function getComponentLeaves (componentKey, metrics, additional) { + return getComponentTree('leaves', componentKey, metrics, additional); } export function getComponent (componentKey, metrics = []) { diff --git a/server/sonar-web/src/main/js/apps/component-measures/app.js b/server/sonar-web/src/main/js/apps/component-measures/app.js index a2679d42d6e..f3340803c41 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/app.js +++ b/server/sonar-web/src/main/js/apps/component-measures/app.js @@ -21,15 +21,18 @@ import React from 'react'; import { render } from 'react-dom'; import { Router, Route, IndexRoute, Redirect, IndexRedirect, useRouterHistory } from 'react-router'; import { createHistory } from 'history'; +import { Provider } from 'react-redux'; -import ComponentMeasuresApp from './components/ComponentMeasuresApp'; -import AllMeasuresList from './components/AllMeasures'; -import MeasureDetails from './components/MeasureDetails'; -import MeasureDrilldownTree from './components/MeasureDrilldownTree'; -import MeasureDrilldownList from './components/MeasureDrilldownList'; -import MeasureHistory from './components/MeasureHistory'; -import MeasureBubbleChart from './components/MeasureBubbleChart'; -import MeasureTreemap from './components/MeasureTreemap'; +import AppContainer from './app/AppContainer'; +import AllMeasuresContainer from './home/AllMeasuresContainer'; +import MeasureDetailsContainer from './details/MeasureDetailsContainer'; +import ListViewContainer from './details/drilldown/ListViewContainer'; +import TreeViewContainer from './details/drilldown/TreeViewContainer'; +import MeasureHistoryContainer from './details/history/MeasureHistoryContainer'; +import MeasureBubbleChartContainer from './details/bubbleChart/MeasureBubbleChartContainer'; +import MeasureTreemapContainer from './details/treemap/MeasureTreemapContainer'; + +import configureStore from './store/configureStore'; import { checkHistoryExistence, checkBubbleChartExistence } from './hooks'; @@ -42,31 +45,33 @@ window.sonarqube.appStarted.then(options => { basename: '/component_measures' }); - const Container = (props) => ( - - ); + const store = configureStore({ + app: { component: options.component } + }); const handleRouteUpdate = () => { window.scrollTo(0, 0); }; render(( - - + + + - - - - - - - - - + + + + + + + + + + - - - + + + ), el); }); diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/ComponentMeasuresApp.js b/server/sonar-web/src/main/js/apps/component-measures/app/App.js similarity index 51% rename from server/sonar-web/src/main/js/apps/component-measures/components/ComponentMeasuresApp.js rename to server/sonar-web/src/main/js/apps/component-measures/app/App.js index c71d8c9fca7..fa23544cd91 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/ComponentMeasuresApp.js +++ b/server/sonar-web/src/main/js/apps/component-measures/app/App.js @@ -19,57 +19,24 @@ */ import React from 'react'; -import Spinner from './Spinner'; -import { getMetrics } from '../../../api/metrics'; - -export default class ComponentMeasuresApp extends React.Component { - state = { - fetching: true, - metrics: [] - }; - - getChildContext () { - return { - component: this.props.component, - metrics: this.state.metrics - }; - } +import Spinner from './../components/Spinner'; +export default class App extends React.Component { componentDidMount () { - this.mounted = true; - this.fetchMetrics(); - } - - componentWillUnmount () { - this.mounted = false; - } - - fetchMetrics () { - getMetrics().then(metrics => { - if (this.mounted) { - this.setState({ metrics, fetching: false }); - } - }); + this.props.fetchMetrics(); } render () { - const { fetching, metrics } = this.state; + const { metrics } = this.props; - if (fetching) { + if (metrics == null) { return ; } - const child = React.cloneElement(this.props.children, { metrics }); - return ( -
- {child} -
+
+ {this.props.children} +
); } } - -ComponentMeasuresApp.childContextTypes = { - component: React.PropTypes.object, - metrics: React.PropTypes.array -}; diff --git a/server/sonar-web/src/main/js/apps/component-measures/app/AppContainer.js b/server/sonar-web/src/main/js/apps/component-measures/app/AppContainer.js new file mode 100644 index 00000000000..2bd53a0ab19 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/app/AppContainer.js @@ -0,0 +1,40 @@ +/* + * 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 { connect } from 'react-redux'; + +import App from './App'; +import { fetchMetrics } from './actions'; + +const mapStateToProps = state => { + return { + metrics: state.app.metrics + }; +}; + +const mapDispatchToProps = dispatch => { + return { + fetchMetrics: () => dispatch(fetchMetrics()) + }; +}; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(App); diff --git a/server/sonar-web/src/main/js/apps/component-measures/app/actions.js b/server/sonar-web/src/main/js/apps/component-measures/app/actions.js new file mode 100644 index 00000000000..e38934a9bee --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/app/actions.js @@ -0,0 +1,53 @@ +/* + * 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 { getMetrics } from '../../../api/metrics'; + +/* + * Actions + */ + +export const DISPLAY_HOME = 'app/DISPLAY_HOME'; +export const RECEIVE_METRICS = 'app/RECEIVE_METRICS'; + + +/* + * Action Creators + */ + +export function displayHome () { + return { type: DISPLAY_HOME }; +} + +function receiveMetrics (metrics) { + return { type: RECEIVE_METRICS, metrics }; +} + + +/* + * Workflow + */ + +export function fetchMetrics () { + return dispatch => { + getMetrics().then(metrics => { + dispatch(receiveMetrics(metrics)); + }); + }; +} diff --git a/server/sonar-web/src/main/js/apps/component-measures/app/reducer.js b/server/sonar-web/src/main/js/apps/component-measures/app/reducer.js new file mode 100644 index 00000000000..2484e2d0563 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/app/reducer.js @@ -0,0 +1,34 @@ +/* + * 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 { RECEIVE_METRICS } from './actions'; + +const initialState = { + metrics: undefined +}; + +export default function appReducer (state = initialState, action = {}) { + switch (action.type) { + case RECEIVE_METRICS: + return { ...state, metrics: action.metrics }; + default: + return state; + } +} + diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/AllMeasures.js b/server/sonar-web/src/main/js/apps/component-measures/components/AllMeasures.js deleted file mode 100644 index 69d1709ef86..00000000000 --- a/server/sonar-web/src/main/js/apps/component-measures/components/AllMeasures.js +++ /dev/null @@ -1,110 +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 React from 'react'; - -import Spinner from './Spinner'; -import AllMeasuresDomain from './AllMeasuresDomain'; -import { getLeakValue } from '../utils'; -import { getMeasuresAndMeta } from '../../../api/measures'; -import { getLeakPeriod, getLeakPeriodLabel } from '../../../helpers/periods'; - -export default class AllMeasures extends React.Component { - state = { - fetching: true, - measures: [] - }; - - componentDidMount () { - this.mounted = true; - this.fetchMeasures(); - } - - componentWillUnmount () { - this.mounted = false; - } - - fetchMeasures () { - const { component } = this.context; - const { metrics } = this.props; - const metricKeys = metrics - .filter(metric => !metric.hidden) - .filter(metric => metric.type !== 'DATA' && metric.type !== 'DISTRIB') - .map(metric => metric.key); - - getMeasuresAndMeta(component.key, metricKeys, { additionalFields: 'periods' }).then(r => { - if (this.mounted) { - const leakPeriod = getLeakPeriod(r.periods); - const measures = r.component.measures - .map(measure => { - const metric = metrics.find(metric => metric.key === measure.metric); - const leak = getLeakValue(measure); - return { ...measure, metric, leak }; - }) - .filter(measure => { - const hasValue = measure.value != null; - const hasLeakValue = !!leakPeriod && measure.leak != null; - return hasValue || hasLeakValue; - }); - - this.setState({ - measures, - periods: r.periods, - fetching: false - }); - } - }); - } - - render () { - const { fetching, measures, periods } = this.state; - - if (fetching) { - return ; - } - - const { component } = this.context; - const domains = _.sortBy(_.pairs(_.groupBy(measures, measure => measure.metric.domain)).map(r => { - const [name, measures] = r; - const sortedMeasures = _.sortBy(measures, measure => measure.metric.name); - - return { name, measures: sortedMeasures }; - }), 'name'); - - const leakPeriodLabel = getLeakPeriodLabel(periods); - - return ( -
    - {domains.map((domain, index) => ( - - ))} -
- ); - } -} - -AllMeasures.contextTypes = { - component: React.PropTypes.object -}; diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDetails.js b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDetails.js deleted file mode 100644 index e4e738870ed..00000000000 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDetails.js +++ /dev/null @@ -1,149 +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 from 'react'; - -import Spinner from './Spinner'; -import MeasureDetailsHeader from './MeasureDetailsHeader'; -import MeasureDrilldown from './MeasureDrilldown'; - -import { enhanceWithLeak } from '../utils'; -import { getMeasuresAndMeta } from '../../../api/measures'; -import { getLeakPeriod, getPeriodDate, getPeriodLabel } from '../../../helpers/periods'; - -export default class MeasureDetails extends React.Component { - state = { - metricSelectOpen: false - }; - - componentWillMount () { - const { metrics } = this.props; - const { metricKey } = this.props.params; - const metric = metrics.find(metric => metric.key === metricKey); - - if (!metric) { - const { router, component } = this.context; - - router.replace({ - pathname: '/', - query: { id: component.key } - }); - } - } - - componentDidMount () { - this.mounted = true; - this.fetchMeasure(); - } - - componentDidUpdate (nextProps, nextState, nextContext) { - if ((nextProps.params.metricKey !== this.props.params.metricKey) || - (nextContext.component !== this.context.component)) { - this.fetchMeasure(); - } - } - - componentWillUnmount () { - this.mounted = false; - } - - fetchMeasure () { - const { metricKey } = this.props.params; - const { component } = this.context; - const metrics = [metricKey]; - - if (metricKey === 'ncloc') { - metrics.push('ncloc_language_distribution'); - } - - if (metricKey === 'function_complexity') { - metrics.push('function_complexity_distribution'); - } - - if (metricKey === 'file_complexity') { - metrics.push('file_complexity_distribution'); - } - - getMeasuresAndMeta( - component.key, - metrics, - { additionalFields: 'periods' } - ).then(r => { - if (this.mounted) { - const measures = enhanceWithLeak(r.component.measures); - const measure = measures.find(measure => measure.metric === metricKey); - const secondaryMeasure = measures.find(measure => measure.metric !== metricKey); - - this.setState({ - measure, - secondaryMeasure, - periods: r.periods, - metricSelectOpen: false - }); - } - }); - } - - handleMetricClick() { - this.setState({ metricSelectOpen: !this.state.metricSelectOpen }); - } - - render () { - const { metrics } = this.props; - const { metricKey } = this.props.params; - const { measure, secondaryMeasure, periods, metricSelectOpen } = this.state; - const metric = metrics.find(metric => metric.key === metricKey); - - if (!measure) { - return ; - } - - const { tab } = this.props.params; - const leakPeriod = getLeakPeriod(periods); - const leakPeriodLabel = getPeriodLabel(leakPeriod); - const leakPeriodDate = getPeriodDate(leakPeriod); - - return ( -
- - - {measure && ( - - {this.props.children} - - )} -
- ); - } -} - -MeasureDetails.contextTypes = { - component: React.PropTypes.object, - router: React.PropTypes.object -}; diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDetailsSeeAlso.js b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDetailsSeeAlso.js deleted file mode 100644 index 513473fc2f9..00000000000 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDetailsSeeAlso.js +++ /dev/null @@ -1,160 +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 React from 'react'; -import { Link } from 'react-router'; - -import Spinner from './Spinner'; -import { getLeakValue, formatLeak } from '../utils'; -import { getMeasuresAndMeta } from '../../../api/measures'; -import { getLeakPeriod } from '../../../helpers/periods'; -import { formatMeasure } from '../../../helpers/measures'; - -export default class MeasureDetailsSeeAlso extends React.Component { - constructor (props) { - super(props); - this.state = { - measures: [], - fetching: true - }; - this.handleKeyDown = this.handleKeyDown.bind(this); - this.handleClick = this.handleClick.bind(this); - } - - componentDidMount () { - this.mounted = true; - window.addEventListener('keydown', this.handleKeyDown); - window.addEventListener('click', this.handleClick); - this.fetchMeasure(); - } - - componentDidUpdate (nextProps, nextState, nextContext) { - if ((nextProps.metric !== this.props.metric) || - (nextContext.component !== this.context.component)) { - this.fetchMeasure(); - } - } - - componentWillUnmount () { - this.mounted = false; - window.removeEventListener('keydown', this.handleKeyDown); - window.removeEventListener('click', this.handleClick); - } - - handleKeyDown (e) { - // escape - if (e.keyCode === 27) { - this.props.onClose(); - } - } - - handleClick () { - this.props.onClose(); - } - - fetchMeasure () { - const { metric } = this.props; - const { component, metrics } = this.context; - const sameDomainMetrics = metrics.filter(candidate => candidate.domain === metric.domain); - const metricKeys = sameDomainMetrics - .filter(m => !m.hidden) - .filter(m => m.type !== 'DATA' && m.type !== 'DISTRIB') - .filter(m => m.key !== metric.key) - .map(m => m.key); - - getMeasuresAndMeta(component.key, metricKeys, { additionalFields: 'periods' }).then(r => { - if (this.mounted) { - const leakPeriod = getLeakPeriod(r.periods); - const measures = r.component.measures - .map(measure => { - const metric = metrics.find(metric => metric.key === measure.metric); - const leak = getLeakValue(measure); - return { ...measure, metric, leak }; - }) - .filter(measure => { - const hasValue = measure.value != null; - const hasLeakValue = !!leakPeriod && measure.leak != null; - return hasValue || hasLeakValue; - }); - const sortedMeasures = _.sortBy(measures, measure => measure.metric.name); - - this.setState({ - measures: sortedMeasures, - periods: r.periods, - fetching: false - }); - } - }); - } - - render () { - const { measures, fetching, periods } = this.state; - const { component } = this.context; - - if (fetching) { - return ( -
- -
- ); - } - - const leakPeriod = getLeakPeriod(periods); - const hasLeak = !!leakPeriod; - - return ( -
-
    - {measures.map(measure => ( -
  • - -
    - {measure.metric.name} -
    -
    - {measure.value != null && ( - - {formatMeasure(measure.value, measure.metric.type)} - - )} -
    - {hasLeak && ( -
    - {measure.leak != null && ( - - {formatLeak(measure.leak, measure.metric)} - - )} -
    - )} - -
  • - ))} -
-
- ); - } -} - -MeasureDetailsSeeAlso.contextTypes = { - component: React.PropTypes.object, - metrics: React.PropTypes.array -}; - diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDrilldownComponents.js b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDrilldownComponents.js deleted file mode 100644 index a020e83f0db..00000000000 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDrilldownComponents.js +++ /dev/null @@ -1,77 +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 from 'react'; - -import MeasureDrilldownEmpty from './MeasureDrilldownEmpty'; -import IconUp from './IconUp'; -import QualifierIcon from '../../../components/shared/qualifier-icon'; -import { formatMeasure } from '../../../helpers/measures'; -import { formatLeak } from '../utils'; - -export default function MeasureDrilldownComponents ({ components, selected, parent, metric, onClick }) { - const handleClick = (component, e) => { - e.preventDefault(); - e.target.blur(); - onClick(component); - }; - - return ( - - ); -} diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDrilldownList.js b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDrilldownList.js deleted file mode 100644 index 3610b3d0bb4..00000000000 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDrilldownList.js +++ /dev/null @@ -1,129 +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 from 'react'; - -import Spinner from './Spinner'; -import MeasureDrilldownComponents from './MeasureDrilldownComponents'; -import SourceViewer from '../../code/components/SourceViewer'; - -import { enhanceWithSingleMeasure } from '../utils'; -import { getFiles } from '../../../api/components'; - -export default class MeasureDrilldownList extends React.Component { - componentDidMount () { - this.mounted = true; - if (this.props.store.list.fetching) { - this.fetchComponents(this.context.component); - } - } - - componentDidUpdate (nextProps, nextState, nextContext) { - if ((nextProps.metric !== this.props.metric) || - (nextContext.component !== this.context.component)) { - this.fetchComponents(nextContext.component); - } - } - - componentWillUnmount () { - this.mounted = false; - } - - fetchComponents (baseComponent) { - const { metric, store, updateStore } = this.props; - const asc = metric.direction === 1; - - const options = { asc }; - if (metric.key.indexOf('new_') === 0) { - Object.assign(options, { - s: 'metricPeriod,name', - metricSort: metric.key, - metricPeriodSort: 1 - }); - } else { - Object.assign(options, { - s: 'metric,name', - metricSort: metric.key - }); - } - - updateStore({ - list: { - ...store.list, - fetching: true - } - }); - - getFiles(baseComponent.key, [metric.key], options).then(files => { - if (this.mounted) { - const components = enhanceWithSingleMeasure(files); - - updateStore({ - list: { - ...store.list, - components, - selected: null, - fetching: false - } - }); - } - }); - } - - handleFileClick (selected) { - const { store, updateStore } = this.props; - updateStore({ - list: { - ...store.list, - selected - } - }); - } - - render () { - const { metric, store, leakPeriod } = this.props; - const { components, selected, fetching } = store.list; - - if (fetching) { - return ; - } - - const sourceViewerPeriod = metric.key.indexOf('new_') === 0 && !!leakPeriod ? leakPeriod : null; - - return ( -
- - - {selected && ( -
- -
- )} -
- ); - } -} - -MeasureDrilldownList.contextTypes = { - component: React.PropTypes.object -}; diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDrilldownTree.js b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDrilldownTree.js deleted file mode 100644 index 11501e990c7..00000000000 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDrilldownTree.js +++ /dev/null @@ -1,139 +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 from 'react'; - -import Spinner from './Spinner'; -import MeasureDrilldownComponents from './MeasureDrilldownComponents'; -import SourceViewer from '../../code/components/SourceViewer'; - -import { enhanceWithSingleMeasure } from '../utils'; -import { getChildren } from '../../../api/components'; - -export default class MeasureDrilldownTree extends React.Component { - componentDidMount () { - this.mounted = true; - if (this.props.store.tree.fetching) { - this.fetchComponents(this.context.component); - } - } - - componentDidUpdate (nextProps, nextState, nextContext) { - if ((nextProps.metric !== this.props.metric) || - (nextContext.component !== this.context.component)) { - this.fetchComponents(nextContext.component); - } - } - - componentWillUnmount () { - this.mounted = false; - } - - fetchComponents (baseComponent) { - const { metric, store, updateStore } = this.props; - const asc = metric.direction === 1; - - const options = { asc }; - if (metric.key.indexOf('new_') === 0) { - Object.assign(options, { - s: 'metricPeriod,name', - metricSort: metric.key, - metricPeriodSort: 1 - }); - } else { - Object.assign(options, { - s: 'metric,name', - metricSort: metric.key - }); - } - - const componentKey = baseComponent.refKey || baseComponent.key; - - updateStore({ tree: { ...store.tree, fetching: true } }); - - getChildren(componentKey, [metric.key], options).then(children => { - if (this.mounted) { - const components = enhanceWithSingleMeasure(children); - - const indexInBreadcrumbs = store.tree.breadcrumbs - .findIndex(component => component === baseComponent); - - const breadcrumbs = indexInBreadcrumbs !== -1 ? - store.tree.breadcrumbs.slice(0, indexInBreadcrumbs + 1) : - [...store.tree.breadcrumbs, baseComponent]; - - const tree = { - ...store.tree, - baseComponent, - breadcrumbs, - components, - selected: null, - fetching: false - }; - - updateStore({ tree }); - } - }); - } - - handleFileClick (component) { - if (component.qualifier === 'FIL' || component.qualifier === 'UTS') { - this.handleFileOpen(component); - } else { - this.fetchComponents(component); - } - } - - handleFileOpen (selected) { - this.props.updateStore({ tree: { ...this.props.store.tree, selected } }); - } - - render () { - const { metric, store, leakPeriod } = this.props; - const { components, selected, breadcrumbs, fetching } = store.tree; - const parent = breadcrumbs.length > 1 ? breadcrumbs[breadcrumbs.length - 2] : null; - - if (fetching) { - return ; - } - - const sourceViewerPeriod = metric.key.indexOf('new_') === 0 && !!leakPeriod ? leakPeriod : null; - - return ( -
- - - {selected && ( -
- -
- )} -
- ); - } -} - -MeasureDrilldownTree.contextTypes = { - component: React.PropTypes.object -}; diff --git a/server/sonar-web/src/main/js/apps/component-measures/bubbles.js b/server/sonar-web/src/main/js/apps/component-measures/config/bubbles.js similarity index 100% rename from server/sonar-web/src/main/js/apps/component-measures/bubbles.js rename to server/sonar-web/src/main/js/apps/component-measures/config/bubbles.js diff --git a/server/sonar-web/src/main/js/apps/component-measures/details/MeasureDetails.js b/server/sonar-web/src/main/js/apps/component-measures/details/MeasureDetails.js new file mode 100644 index 00000000000..8cd30894cdd --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/details/MeasureDetails.js @@ -0,0 +1,101 @@ +/* + * 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 { IndexLink } from 'react-router'; + +import Spinner from './../components/Spinner'; +import MeasureDetailsHeader from './MeasureDetailsHeader'; +import MeasureDrilldown from './drilldown/MeasureDrilldown'; + +import { getLeakPeriod, getPeriodDate, getPeriodLabel } from '../../../helpers/periods'; +import { translate } from '../../../helpers/l10n'; + +export default class MeasureDetails extends React.Component { + componentWillMount () { + const { metrics } = this.props; + const { metricKey } = this.props.params; + const metric = metrics.find(metric => metric.key === metricKey); + + if (!metric) { + const { component } = this.props; + const { router } = this.context; + + router.replace({ + pathname: '/', + query: { id: component.key } + }); + } + } + + componentDidMount () { + this.props.fetchMeasure(this.props.params.metricKey); + } + + componentDidUpdate (nextProps) { + if (nextProps.params.metricKey !== this.props.params.metricKey) { + this.props.fetchMeasure(nextProps.params.metricKey); + } + } + + render () { + const { component, metric, secondaryMeasure, measure, periods, children } = this.props; + + if (measure == null) { + return ; + } + + const { tab } = this.props.params; + const leakPeriod = getLeakPeriod(periods); + const leakPeriodLabel = getPeriodLabel(leakPeriod); + const leakPeriodDate = getPeriodDate(leakPeriod); + + return ( +
+ + {translate('component_measures.back_to_all_measures')} + + + + + {measure && ( + + {children} + + )} +
+ ); + } +} + +MeasureDetails.contextTypes = { + router: React.PropTypes.object +}; diff --git a/server/sonar-web/src/main/js/apps/component-measures/details/MeasureDetailsContainer.js b/server/sonar-web/src/main/js/apps/component-measures/details/MeasureDetailsContainer.js new file mode 100644 index 00000000000..80000e23ff3 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/details/MeasureDetailsContainer.js @@ -0,0 +1,44 @@ +/* + * 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 { connect } from 'react-redux'; + +import MeasureDetails from './MeasureDetails'; +import { fetchMeasure } from './actions'; + +const mapStateToProps = state => { + return { + component: state.app.component, + metrics: state.app.metrics, + metric: state.details.metric, + measure: state.details.measure, + periods: state.details.periods + }; +}; + +const mapDispatchToProps = dispatch => { + return { + fetchMeasure: metricKey => dispatch(fetchMeasure(metricKey)) + }; +}; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(MeasureDetails); diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDetailsHeader.js b/server/sonar-web/src/main/js/apps/component-measures/details/MeasureDetailsHeader.js similarity index 80% rename from server/sonar-web/src/main/js/apps/component-measures/components/MeasureDetailsHeader.js rename to server/sonar-web/src/main/js/apps/component-measures/details/MeasureDetailsHeader.js index 59a9a3c9eee..13e3080a352 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDetailsHeader.js +++ b/server/sonar-web/src/main/js/apps/component-measures/details/MeasureDetailsHeader.js @@ -19,24 +19,14 @@ */ import React from 'react'; -import MeasureDetailsSeeAlso from './MeasureDetailsSeeAlso'; -import LanguageDistribution from './LanguageDistribution'; +import LanguageDistribution from './../components/LanguageDistribution'; import { ComplexityDistribution } from '../../overview/components/complexity-distribution'; import { formatLeak } from '../utils'; import { formatMeasure } from '../../../helpers/measures'; import { translateWithParameters } from '../../../helpers/l10n'; import { TooltipsContainer } from '../../../components/mixins/tooltips-mixin'; -export default function MeasureDetailsHeader ( - { - measure, - metric, - secondaryMeasure, - leakPeriodLabel, - metricSelectOpen, - onMetricClick - } -) { +export default function MeasureDetailsHeader ({ measure, metric, secondaryMeasure, leakPeriodLabel }) { const leakPeriodTooltip = translateWithParameters('overview.leak_period_x', leakPeriodLabel); const displayLeak = measure.leak != null && metric.type !== 'RATING' && metric.type !== 'LEVEL'; @@ -44,17 +34,7 @@ export default function MeasureDetailsHeader ( return (

- { - e.stopPropagation(); - e.preventDefault(); - onMetricClick(); - }}> - {metric.name} -   - - + {metric.name}

@@ -93,10 +73,6 @@ export default function MeasureDetailsHeader ( )} - - {metricSelectOpen && ( - - )}
); } diff --git a/server/sonar-web/src/main/js/apps/component-measures/details/actions.js b/server/sonar-web/src/main/js/apps/component-measures/details/actions.js new file mode 100644 index 00000000000..8992f530580 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/details/actions.js @@ -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 { getMeasuresAndMeta } from '../../../api/measures'; +import { enhanceWithLeak } from '../utils'; + +/* + * Actions + */ + +export const REQUEST_MEASURE = 'details/REQUEST_MEASURE'; +export const RECEIVE_MEASURE = 'details/RECEIVE_MEASURE'; + + +/* + * Action Creators + */ + +function requestMeasure (metric) { + return { type: REQUEST_MEASURE, metric }; +} + +function receiveMeasure (measure, secondaryMeasure, periods) { + return { type: RECEIVE_MEASURE, measure, secondaryMeasure, periods }; +} + + +/* + * Workflow + */ + +export function fetchMeasure (metricKey) { + return (dispatch, getState) => { + const { component, metrics } = getState().app; + const metricsToRequest = [metricKey]; + + if (metricKey === 'ncloc') { + metricsToRequest.push('ncloc_language_distribution'); + } + if (metricKey === 'function_complexity') { + metricsToRequest.push('function_complexity_distribution'); + } + if (metricKey === 'file_complexity') { + metricsToRequest.push('file_complexity_distribution'); + } + + const metric = metrics.find(m => m.key === metricKey); + dispatch(requestMeasure(metric)); + + getMeasuresAndMeta( + component.key, + metricsToRequest, + { additionalFields: 'periods' } + ).then(r => { + const measures = enhanceWithLeak(r.component.measures); + const measure = measures.find(m => m.metric === metricKey); + const secondaryMeasure = measures.find(m => m.metric !== metricKey); + dispatch(receiveMeasure(measure, secondaryMeasure, r.periods)); + }); + }; +} diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureBubbleChart.js b/server/sonar-web/src/main/js/apps/component-measures/details/bubbleChart/BubbleChart.js similarity index 80% rename from server/sonar-web/src/main/js/apps/component-measures/components/MeasureBubbleChart.js rename to server/sonar-web/src/main/js/apps/component-measures/details/bubbleChart/BubbleChart.js index 5654e20cd14..a43e93f53e7 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureBubbleChart.js +++ b/server/sonar-web/src/main/js/apps/component-measures/details/bubbleChart/BubbleChart.js @@ -19,29 +19,28 @@ */ import React from 'react'; -import Spinner from './Spinner'; -import { BubbleChart } from '../../../components/charts/bubble-chart'; -import bubbles from '../bubbles'; -import { getFiles } from '../../../api/components'; -import { formatMeasure } from '../../../helpers/measures'; -import Workspace from '../../../components/workspace/main'; - -const HEIGHT = 360; +import Spinner from './../../components/Spinner'; +import { BubbleChart as OriginalBubbleChart } from '../../../../components/charts/bubble-chart'; +import bubbles from '../../config/bubbles'; +import { getComponentLeaves } from '../../../../api/components'; +import { formatMeasure } from '../../../../helpers/measures'; +import Workspace from '../../../../components/workspace/main'; + +const HEIGHT = 500; const BUBBLES_LIMIT = 500; function getMeasure (component, metric) { return Number(component.measures[metric]) || 0; } -export default class MeasureBubbleChart extends React.Component { +export default class BubbleChart extends React.Component { state = { fetching: true, files: [] }; componentWillMount () { - const { metric } = this.props; - const { metrics } = this.context; + const { metric, metrics } = this.props; const conf = bubbles[metric.key]; this.xMetric = metrics.find(m => m.key === conf.x); @@ -54,9 +53,8 @@ export default class MeasureBubbleChart extends React.Component { this.fetchFiles(); } - componentDidUpdate (nextProps, nextState, nextContext) { - if ((nextProps.metric !== this.props.metric) || - (nextContext.component !== this.context.component)) { + componentDidUpdate (nextProps) { + if (nextProps.metric !== this.props.metric) { this.fetchFiles(); } } @@ -66,7 +64,7 @@ export default class MeasureBubbleChart extends React.Component { } fetchFiles () { - const { component } = this.context; + const { component } = this.props; const metrics = [this.xMetric.key, this.yMetric.key, this.sizeMetric.key]; const options = { s: 'metric', @@ -75,8 +73,8 @@ export default class MeasureBubbleChart extends React.Component { ps: BUBBLES_LIMIT }; - getFiles(component.key, metrics, options).then(r => { - const files = r.map(file => { + getComponentLeaves(component.key, metrics, options).then(r => { + const files = r.components.map(file => { const measures = {}; file.measures.forEach(measure => { @@ -123,7 +121,7 @@ export default class MeasureBubbleChart extends React.Component { const formatYTick = (tick) => formatMeasure(tick, this.yMetric.type); return ( - - {this.renderBubbleChart()} +
+ {this.renderBubbleChart()} +
{this.xMetric.name}
{this.yMetric.name}
@@ -157,8 +157,3 @@ export default class MeasureBubbleChart extends React.Component { ); } } - -MeasureBubbleChart.contextTypes = { - component: React.PropTypes.object, - metrics: React.PropTypes.array -}; diff --git a/server/sonar-web/src/main/js/apps/component-measures/details/bubbleChart/MeasureBubbleChartContainer.js b/server/sonar-web/src/main/js/apps/component-measures/details/bubbleChart/MeasureBubbleChartContainer.js new file mode 100644 index 00000000000..0dba5dcada8 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/details/bubbleChart/MeasureBubbleChartContainer.js @@ -0,0 +1,39 @@ +/* + * 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 { connect } from 'react-redux'; + +import MeasureBubbleChart from './BubbleChart'; + +const mapStateToProps = state => { + return { + component: state.app.component, + metrics: state.app.metrics, + metric: state.details.metric + }; +}; + +const mapDispatchToProps = () => { + return {}; +}; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(MeasureBubbleChart); diff --git a/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/Breadcrumb.js b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/Breadcrumb.js new file mode 100644 index 00000000000..15a44e30452 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/Breadcrumb.js @@ -0,0 +1,62 @@ +/* + * 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 QualifierIcon from '../../../../components/shared/qualifier-icon'; +import { isDiffMetric, formatLeak } from '../../utils'; +import { formatMeasure } from '../../../../helpers/measures'; + +const Breadcrumb = ({ component, metric, onBrowse }) => { + const handleClick = (e) => { + e.preventDefault(); + e.target.blur(); + onBrowse(component); + }; + + let inner; + if (onBrowse) { + inner = ( + + {component.name} + + ); + } else { + inner = {component.name}; + } + + const value = isDiffMetric(metric) ? + formatLeak(component.leak, metric) : + formatMeasure(component.value, metric.type); + + return ( + + +   + {inner} + {value != null && ( + {' (' + value + ')'} + )} + + ); +}; + +export default Breadcrumb; diff --git a/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/Breadcrumbs.js b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/Breadcrumbs.js new file mode 100644 index 00000000000..dfcf88d9bef --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/Breadcrumbs.js @@ -0,0 +1,37 @@ +/* + * 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 Breadcrumb from './Breadcrumb'; + +const Breadcrumbs = ({ breadcrumbs, metric, onBrowse }) => ( +
    + {breadcrumbs.map((component, index) => ( +
  • + +
  • + ))} +
+); + +export default Breadcrumbs; diff --git a/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ComponentCell.js b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ComponentCell.js new file mode 100644 index 00000000000..558c80219d4 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ComponentCell.js @@ -0,0 +1,78 @@ +/* + * 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 classNames from 'classnames'; + +import QualifierIcon from '../../../../components/shared/qualifier-icon'; +import { splitPath } from '../../../../helpers/path'; +import { getComponentUrl } from '../../../../helpers/urls'; + +const ComponentCell = ({ component, isSelected, onClick }) => { + const linkClassName = classNames('link-no-underline', { + 'selected': isSelected + }); + + const handleClick = (e) => { + e.preventDefault(); + onClick(); + }; + + const { head, tail } = splitPath(component.path || component.name); + + const inner = ( + + +   + {head.length > 0 && ( + {head}/ + )} + {tail} + + ); + + return ( + +
+ {component.refId == null ? ( + + {inner} + + ) : ( + + + + + + {inner} + + )} +
+ + ); +}; + +export default ComponentCell; diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/IconUp.js b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ComponentsList.js similarity index 55% rename from server/sonar-web/src/main/js/apps/component-measures/components/IconUp.js rename to server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ComponentsList.js index 462d07bb17b..80793a9550f 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/IconUp.js +++ b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ComponentsList.js @@ -19,18 +19,28 @@ */ import React from 'react'; -export default function IconUp () { - /* eslint max-len: 0 */ +import ComponentsListRow from './ComponentsListRow'; +import EmptyComponentsList from './EmptyComponentsList'; + +const ComponentsList = ({ components, selected, metric, onClick }) => { + if (!components.length) { + return ; + } + return ( - - - + + + {components.map(component => ( + + ))} + +
); -} +}; + +export default ComponentsList; diff --git a/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ComponentsListRow.js b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ComponentsListRow.js new file mode 100644 index 00000000000..48c871cfac1 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ComponentsListRow.js @@ -0,0 +1,44 @@ +/* + * 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 ComponentCell from './ComponentCell'; +import MeasureCell from './MeasureCell'; + +const ComponentsListRow = ({ component, isSelected, metric, onClick }) => { + const handleClick = () => { + onClick(component); + }; + + return ( + + + + + + ); +}; + +export default ComponentsListRow; diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDrilldownEmpty.js b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/EmptyComponentsList.js similarity index 84% rename from server/sonar-web/src/main/js/apps/component-measures/components/MeasureDrilldownEmpty.js rename to server/sonar-web/src/main/js/apps/component-measures/details/drilldown/EmptyComponentsList.js index ede861f8382..7b1a11d5791 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDrilldownEmpty.js +++ b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/EmptyComponentsList.js @@ -19,12 +19,14 @@ */ import React from 'react'; -import { translate } from '../../../helpers/l10n'; +import { translate } from '../../../../helpers/l10n'; -export default function MeasureDrilldownEmpty () { +const EmptyComponentsList = () => { return ( -
+
{translate('no_results')}
); -} +}; + +export default EmptyComponentsList; diff --git a/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ListHeader.js b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ListHeader.js new file mode 100644 index 00000000000..a12be76c7e4 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ListHeader.js @@ -0,0 +1,68 @@ +/* + * 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 Breadcrumbs from './Breadcrumbs'; +import { translateWithParameters } from '../../../../helpers/l10n'; + +const ListHeader = (props) => { + const { metric, breadcrumbs, onBrowse } = props; + const { selectedIndex, componentsCount, onSelectPrevious, onSelectNext } = props; + const hasPrevious = selectedIndex > 0; + const hasNext = selectedIndex < componentsCount - 1; + const blur = fn => { + return e => { + e.target.blur(); + fn(); + }; + }; + + return ( +
+ {breadcrumbs != null && breadcrumbs.length > 1 && ( +
+ +
+ )} + + {selectedIndex != null && selectedIndex !== -1 && ( +
+ + {translateWithParameters('component_measures.x_of_y', selectedIndex + 1, componentsCount)} + + +
+ {hasPrevious && ( + + )} + {hasNext && ( + + )} +
+
+ )} +
+ ); +}; + +export default ListHeader; 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 new file mode 100644 index 00000000000..97c25a1b723 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ListView.js @@ -0,0 +1,130 @@ +/* + * 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 classNames from 'classnames'; + +import ComponentsList from './ComponentsList'; +import ListHeader from './ListHeader'; +import SourceViewer from '../../../code/components/SourceViewer'; +import ListFooter from '../../../../components/shared/list-footer'; + +export default class ListView extends React.Component { + componentDidMount () { + this.handleChangeBaseComponent(this.props.component); + } + + componentDidUpdate (nextProps) { + if (nextProps.metric !== this.props.metric) { + this.handleChangeBaseComponent(this.props.component); + } + + if (this.props.selected) { + this.scrollToViewer(); + } else if (this.scrollTop) { + this.scrollToStoredPosition(); + } + } + + fetchMore () { + const { metric, component, onFetchMore } = this.props; + onFetchMore(component, metric); + } + + scrollToViewer () { + const { container } = this.refs; + const top = container.getBoundingClientRect().top + window.scrollY - 95 - 10; + window.scrollTo(0, top); + } + + scrollToStoredPosition () { + window.scrollTo(0, this.scrollTop); + this.scrollTop = null; + } + + storeScrollPosition () { + this.scrollTop = window.scrollY; + } + + handleChangeBaseComponent (baseComponent) { + const { metric, onFetchList } = this.props; + onFetchList(baseComponent, metric); + } + + changeSelected (selected) { + this.props.onSelect(selected); + } + + handleClick (selected) { + this.storeScrollPosition(); + this.props.onSelect(selected); + } + + handleBreadcrumbClick () { + this.props.onSelect(undefined); + } + + render () { + const { component, components, metric, leakPeriod, selected, fetching, total } = this.props; + const { onSelectNext, onSelectPrevious } = this.props; + + const breadcrumbs = [component]; + if (selected) { + breadcrumbs.push(selected); + } + const selectedIndex = components.indexOf(selected); + const sourceViewerPeriod = metric.key.indexOf('new_') === 0 && !!leakPeriod ? leakPeriod : null; + + return ( +
+ + + {!selected && ( +
+ + +
+ )} + + {!!selected && ( +
+ +
+ )} +
+ ); + } +} diff --git a/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ListViewContainer.js b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ListViewContainer.js new file mode 100644 index 00000000000..74e1faba30e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ListViewContainer.js @@ -0,0 +1,54 @@ +/* + * 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 { connect } from 'react-redux'; +import pick from '../../../../../../../node_modules/lodash/pick'; + +import ListView from './ListView'; +import { fetchList, fetchMore, selectComponent, selectNext, selectPrevious } from '../../store/listViewActions'; + +const mapStateToProps = state => { + const drilldown = pick(state.list, [ + 'components', + 'selected', + 'total', + 'pageIndex' + ]); + return { + ...drilldown, + component: state.app.component, + metric: state.details.metric, + fetching: state.status.fetching + }; +}; + +const mapDispatchToProps = dispatch => { + return { + onFetchList: (baseComponent, metric) => dispatch(fetchList(baseComponent, metric)), + onFetchMore: (baseComponent, metric) => dispatch(fetchMore(baseComponent, metric)), + onSelect: component => dispatch(selectComponent(component)), + onSelectNext: component => dispatch(selectNext(component)), + onSelectPrevious: component => dispatch(selectPrevious(component)) + }; +}; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(ListView); diff --git a/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/MeasureCell.js b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/MeasureCell.js new file mode 100644 index 00000000000..8d3678ea57e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/MeasureCell.js @@ -0,0 +1,39 @@ +/* + * 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 { formatMeasure } from '../../../../helpers/measures'; +import { isDiffMetric, formatLeak } from '../../utils'; + +const MeasureCell = ({ component, metric }) => { + const value = isDiffMetric(metric) ? + formatLeak(component.leak, metric) : + formatMeasure(component.value, metric.type); + + return ( + + + {value != null ? value : '–'} + + + ); +}; + +export default MeasureCell; diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDrilldown.js b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/MeasureDrilldown.js similarity index 66% rename from server/sonar-web/src/main/js/apps/component-measures/components/MeasureDrilldown.js rename to server/sonar-web/src/main/js/apps/component-measures/details/drilldown/MeasureDrilldown.js index d0e78285813..3df7565a694 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDrilldown.js +++ b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/MeasureDrilldown.js @@ -20,47 +20,33 @@ import React from 'react'; import { Link } from 'react-router'; -import IconList from './IconList'; -import IconTree from './IconTree'; -import IconBubbles from './IconBubbles'; -import IconTreemap from './IconTreemap'; -import IconHistory from './IconHistory'; +import IconList from './../../components/IconList'; +import IconTree from './../../components/IconTree'; +import IconBubbles from './../../components/IconBubbles'; +import IconTreemap from './../../components/IconTreemap'; +import IconHistory from './../../components/IconHistory'; -import { hasHistory, hasBubbleChart, hasTreemap } from '../utils'; -import { translate } from '../../../helpers/l10n'; +import { hasHistory, hasBubbleChart, hasTreemap } from '../../utils'; +import { translate } from '../../../../helpers/l10n'; export default class MeasureDrilldown extends React.Component { - state = { - tree: { - components: [], - breadcrumbs: [], - selected: null, - fetching: true - }, - list: { - components: [], - selected: null, - fetching: true - } - }; - render () { - const { children, metric, ...other } = this.props; - const { component } = this.context; - - const showListView = ['VW', 'SVW', 'DEV'].indexOf(component.qualifier) === -1; + const { children, component, metric, ...other } = this.props; - const child = React.cloneElement(children, { - component, - metric, - ...other, - store: this.state, - updateStore: this.setState.bind(this) - }); + const child = React.cloneElement(children, { ...other }); return (
    +
  • + + + {translate('component_measures.tab.list')} + +
  • +
  • - {showListView && ( -
  • - - - {translate('component_measures.tab.list')} - -
  • - )} - {hasBubbleChart(metric.key) && (
  • + + + {!selected && ( +
    + + +
    + )} + + {!!selected && ( +
    + +
    + )} +
+ ); + } +} diff --git a/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/TreeViewContainer.js b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/TreeViewContainer.js new file mode 100644 index 00000000000..f32bc7d222c --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/TreeViewContainer.js @@ -0,0 +1,65 @@ +/* + * 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 { connect } from 'react-redux'; +import pick from '../../../../../../../node_modules/lodash/pick'; + +import TreeView from './TreeView'; +import { + start, + fetchMore, + drilldown, + useBreadcrumbs, + selectComponent, + selectNext, + selectPrevious +} from '../../store/treeViewActions'; + +const mapStateToProps = state => { + const drilldown = pick(state.tree, [ + 'components', + 'breadcrumbs', + 'selected', + 'total', + 'pageIndex' + ]); + return { + ...drilldown, + component: state.app.component, + metric: state.details.metric, + fetching: state.status.fetching + }; +}; + +const mapDispatchToProps = dispatch => { + return { + onStart: (rootComponent, metric) => dispatch(start(rootComponent, metric)), + onFetchMore: () => dispatch(fetchMore()), + onDrilldown: component => dispatch(drilldown(component)), + onUseBreadcrumbs: component => dispatch(useBreadcrumbs(component)), + onSelect: component => dispatch(selectComponent(component)), + onSelectNext: component => dispatch(selectNext(component)), + onSelectPrevious: component => dispatch(selectPrevious(component)) + }; +}; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(TreeView); diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHistory.js b/server/sonar-web/src/main/js/apps/component-measures/details/history/MeasureHistory.js similarity index 89% rename from server/sonar-web/src/main/js/apps/component-measures/components/MeasureHistory.js rename to server/sonar-web/src/main/js/apps/component-measures/details/history/MeasureHistory.js index a8b9acb7c44..4958e72d75f 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHistory.js +++ b/server/sonar-web/src/main/js/apps/component-measures/details/history/MeasureHistory.js @@ -21,13 +21,13 @@ import _ from 'underscore'; import moment from 'moment'; import React from 'react'; -import Spinner from './Spinner'; -import Timeline from '../../../components/charts/Timeline'; -import { getTimeMachineData } from '../../../api/time-machine'; -import { getEvents } from '../../../api/events'; -import { formatMeasure, getShortType } from '../../../helpers/measures'; +import Spinner from './../../components/Spinner'; +import Timeline from '../../../../components/charts/Timeline'; +import { getTimeMachineData } from '../../../../api/time-machine'; +import { getEvents } from '../../../../api/events'; +import { formatMeasure, getShortType } from '../../../../helpers/measures'; -const HEIGHT = 360; +const HEIGHT = 500; function parseValue (value, type) { return type === 'RATING' && typeof value === 'string' ? value.charCodeAt(0) - 'A'.charCodeAt(0) + 1 : value; @@ -45,9 +45,8 @@ export default class MeasureHistory extends React.Component { this.fetchHistory(); } - componentDidUpdate (nextProps, nextState, nextContext) { - if ((nextProps.metric !== this.props.metric) || - (nextContext.component !== this.context.component)) { + componentDidUpdate (nextProps) { + if (nextProps.metric !== this.props.metric) { this.fetchHistory(); } } @@ -176,7 +175,3 @@ export default class MeasureHistory extends React.Component { ); } } - -MeasureHistory.contextTypes = { - component: React.PropTypes.object -}; diff --git a/server/sonar-web/src/main/js/apps/component-measures/details/history/MeasureHistoryContainer.js b/server/sonar-web/src/main/js/apps/component-measures/details/history/MeasureHistoryContainer.js new file mode 100644 index 00000000000..bfe7decf1a3 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/details/history/MeasureHistoryContainer.js @@ -0,0 +1,38 @@ +/* + * 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 { connect } from 'react-redux'; + +import MeasureHistory from './MeasureHistory'; + +const mapStateToProps = state => { + return { + component: state.app.component, + metric: state.details.metric + }; +}; + +const mapDispatchToProps = () => { + return {}; +}; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(MeasureHistory); diff --git a/server/sonar-web/src/main/js/apps/component-measures/details/reducer.js b/server/sonar-web/src/main/js/apps/component-measures/details/reducer.js new file mode 100644 index 00000000000..146a8a3d6d6 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/details/reducer.js @@ -0,0 +1,42 @@ +/* + * 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 { DISPLAY_HOME } from '../app/actions'; +import { REQUEST_MEASURE, RECEIVE_MEASURE } from './actions'; + +const initialState = { + metric: undefined, + secondaryMeasure: undefined, + measure: undefined, + periods: undefined +}; + +export default function appReducer (state = initialState, action = {}) { + switch (action.type) { + case DISPLAY_HOME: + return initialState; + case REQUEST_MEASURE: + return { ...state, metric: action.metric }; + case RECEIVE_MEASURE: + return { ...state, measure: action.measure, secondaryMeasure: action.secondaryMeasure, periods: action.periods }; + default: + return state; + } +} + diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureTreemap.js b/server/sonar-web/src/main/js/apps/component-measures/details/treemap/MeasureTreemap.js similarity index 85% rename from server/sonar-web/src/main/js/apps/component-measures/components/MeasureTreemap.js rename to server/sonar-web/src/main/js/apps/component-measures/details/treemap/MeasureTreemap.js index 2364d1b75ce..92615167cc3 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureTreemap.js +++ b/server/sonar-web/src/main/js/apps/component-measures/details/treemap/MeasureTreemap.js @@ -19,16 +19,16 @@ */ import React from 'react'; -import Spinner from './Spinner'; -import { getLeakValue } from '../utils'; -import { Treemap } from '../../../components/charts/treemap'; -import { getChildren } from '../../../api/components'; -import { formatMeasure } from '../../../helpers/measures'; -import { translate, translateWithParameters } from '../../../helpers/l10n'; -import { getComponentUrl } from '../../../helpers/urls'; -import Workspace from '../../../components/workspace/main'; +import Spinner from './../../components/Spinner'; +import { getLeakValue } from '../../utils'; +import { Treemap } from '../../../../components/charts/treemap'; +import { getChildren } from '../../../../api/components'; +import { formatMeasure } from '../../../../helpers/measures'; +import { translate, translateWithParameters } from '../../../../helpers/l10n'; +import { getComponentUrl } from '../../../../helpers/urls'; +import Workspace from '../../../../components/workspace/main'; -const HEIGHT = 360; +const HEIGHT = 500; export default class MeasureTreemap extends React.Component { state = { @@ -38,16 +38,15 @@ export default class MeasureTreemap extends React.Component { }; componentDidMount () { - const { component } = this.context; + const { component } = this.props; this.mounted = true; this.fetchComponents(component.key); } - componentDidUpdate (nextProps, nextState, nextContext) { - if ((nextProps.metric !== this.props.metric) || - (nextContext.component !== this.context.component)) { - this.fetchComponents(nextContext.component.key); + componentDidUpdate (nextProps) { + if (nextProps.metric !== this.props.metric) { + this.fetchComponents(this.props.component.key); } } @@ -158,7 +157,7 @@ export default class MeasureTreemap extends React.Component { } handleReset () { - const { component } = this.context; + const { component } = this.props; this.fetchComponents(component.key).then(() => { this.setState({ breadcrumbs: [] }); }); @@ -185,14 +184,11 @@ export default class MeasureTreemap extends React.Component { }; }); - // FIXME remove this magic number - const height = HEIGHT - 35; - return ( true} onRectangleClick={this.handleRectangleClick.bind(this)} onReset={this.handleReset.bind(this)}/> @@ -228,8 +224,3 @@ export default class MeasureTreemap extends React.Component { ); } } - -MeasureTreemap.contextTypes = { - component: React.PropTypes.object, - metrics: React.PropTypes.array -}; diff --git a/server/sonar-web/src/main/js/apps/component-measures/details/treemap/MeasureTreemapContainer.js b/server/sonar-web/src/main/js/apps/component-measures/details/treemap/MeasureTreemapContainer.js new file mode 100644 index 00000000000..978a2a522e2 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/details/treemap/MeasureTreemapContainer.js @@ -0,0 +1,38 @@ +/* + * 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 { connect } from 'react-redux'; + +import MeasureTreemap from './MeasureTreemap'; + +const mapStateToProps = state => { + return { + component: state.app.component, + metric: state.details.metric + }; +}; + +const mapDispatchToProps = () => { + return {}; +}; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(MeasureTreemap); diff --git a/server/sonar-web/src/main/js/apps/component-measures/home/AllMeasures.js b/server/sonar-web/src/main/js/apps/component-measures/home/AllMeasures.js new file mode 100644 index 00000000000..c0bc2f6162c --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/home/AllMeasures.js @@ -0,0 +1,64 @@ +/* + * 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 React from 'react'; + +import Spinner from './../components/Spinner'; +import AllMeasuresDomain from './AllMeasuresDomain'; +import { getLeakPeriodLabel } from '../../../helpers/periods'; + +export default class AllMeasures extends React.Component { + componentDidMount () { + this.props.onDisplay(); + this.props.fetchMeasures(); + } + + render () { + const { component, measures, periods, fetching } = this.props; + + if (fetching) { + return ; + } + + const domains = _.sortBy(_.pairs(_.groupBy(measures, measure => measure.metric.domain)).map(r => { + const [name, measures] = r; + const sortedMeasures = _.sortBy(measures, measure => measure.metric.name); + + return { name, measures: sortedMeasures }; + }), 'name'); + + const leakPeriodLabel = getLeakPeriodLabel(periods); + + return ( +
+
    + {domains.map((domain, index) => ( + + ))} +
+
+ ); + } +} diff --git a/server/sonar-web/src/main/js/apps/component-measures/home/AllMeasuresContainer.js b/server/sonar-web/src/main/js/apps/component-measures/home/AllMeasuresContainer.js new file mode 100644 index 00000000000..c12fac1ac13 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/home/AllMeasuresContainer.js @@ -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. + */ +import { connect } from 'react-redux'; + +import AllMeasures from './AllMeasures'; +import { fetchMeasures } from './actions'; +import { displayHome } from '../app/actions'; + +const mapStateToProps = state => { + return { + component: state.app.component, + metrics: state.app.metrics, + measures: state.home.measures, + periods: state.home.periods, + fetching: state.status.fetching + }; +}; + +const mapDispatchToProps = dispatch => { + return { + onDisplay: () => dispatch(displayHome()), + fetchMeasures: () => dispatch(fetchMeasures()) + }; +}; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(AllMeasures); diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/AllMeasuresDomain.js b/server/sonar-web/src/main/js/apps/component-measures/home/AllMeasuresDomain.js similarity index 95% rename from server/sonar-web/src/main/js/apps/component-measures/components/AllMeasuresDomain.js rename to server/sonar-web/src/main/js/apps/component-measures/home/AllMeasuresDomain.js index 6068cfd31b1..67fe53d31d7 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/AllMeasuresDomain.js +++ b/server/sonar-web/src/main/js/apps/component-measures/home/AllMeasuresDomain.js @@ -17,8 +17,8 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import sortBy from 'lodash/sortBy'; -import partition from 'lodash/partition'; +import sortBy from '../../../../../../node_modules/lodash/sortBy'; +import partition from '../../../../../../node_modules/lodash/partition'; import React from 'react'; import { Link } from 'react-router'; diff --git a/server/sonar-web/src/main/js/apps/component-measures/home/actions.js b/server/sonar-web/src/main/js/apps/component-measures/home/actions.js new file mode 100644 index 00000000000..542e2207cb4 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/home/actions.js @@ -0,0 +1,59 @@ +/* + * 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 { startFetching, stopFetching } from '../store/statusActions'; +import { getMeasuresAndMeta } from '../../../api/measures'; +import { getLeakPeriod } from '../../../helpers/periods'; +import { getLeakValue } from '../utils'; + +export const RECEIVE_MEASURES = 'home/RECEIVE_MEASURES'; + +export function receiveMeasures (measures, periods) { + return { type: RECEIVE_MEASURES, measures, periods }; +} + +export function fetchMeasures () { + return (dispatch, getState) => { + dispatch(startFetching()); + + const { component, metrics } = getState().app; + const metricKeys = metrics + .filter(metric => !metric.hidden) + .filter(metric => metric.type !== 'DATA' && metric.type !== 'DISTRIB') + .map(metric => metric.key); + + getMeasuresAndMeta(component.key, metricKeys, { additionalFields: 'periods' }).then(r => { + const leakPeriod = getLeakPeriod(r.periods); + const measures = r.component.measures + .map(measure => { + const metric = metrics.find(metric => metric.key === measure.metric); + const leak = getLeakValue(measure); + return { ...measure, metric, leak }; + }) + .filter(measure => { + const hasValue = measure.value != null; + const hasLeakValue = !!leakPeriod && measure.leak != null; + return hasValue || hasLeakValue; + }); + + dispatch(receiveMeasures(measures, r.periods)); + dispatch(stopFetching()); + }); + }; +} diff --git a/server/sonar-web/src/main/js/apps/component-measures/home/reducer.js b/server/sonar-web/src/main/js/apps/component-measures/home/reducer.js new file mode 100644 index 00000000000..e80c7b1129b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/home/reducer.js @@ -0,0 +1,34 @@ +/* + * 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 { RECEIVE_MEASURES } from './actions'; + +const initialState = { + measures: undefined, + periods: undefined +}; + +export default function (state = initialState, action = {}) { + switch (action.type) { + case RECEIVE_MEASURES: + return { ...state, measures: action.measures, periods: action.periods }; + default: + return state; + } +} diff --git a/server/sonar-web/src/main/js/apps/component-measures/store/configureStore.js b/server/sonar-web/src/main/js/apps/component-measures/store/configureStore.js new file mode 100644 index 00000000000..07d55655531 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/store/configureStore.js @@ -0,0 +1,41 @@ +/* + * 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 { combineReducers } from 'redux'; + +import appReducer from './../app/reducer'; +import statusReducer from './statusReducer'; +import homeReducer from '../home/reducer'; +import detailsReducer from '../details/reducer'; +import listViewReducer from './listViewReducer'; +import treeViewReducer from './treeViewReducer'; +import configureStore from '../../../components/store/configureStore'; + +export default function customConfigureStore (initialState) { + const rootReducer = combineReducers({ + app: appReducer, + home: homeReducer, + details: detailsReducer, + list: listViewReducer, + tree: treeViewReducer, + status: statusReducer + }); + + return configureStore(rootReducer, initialState); +} diff --git a/server/sonar-web/src/main/js/apps/component-measures/store/listViewActions.js b/server/sonar-web/src/main/js/apps/component-measures/store/listViewActions.js new file mode 100644 index 00000000000..6826e012786 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/store/listViewActions.js @@ -0,0 +1,143 @@ +/* + * 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 { getComponentTree } from '../../../api/components'; +import { enhanceWithSingleMeasure } from '../utils'; +import { startFetching, stopFetching } from './statusActions'; + +export const UPDATE_STORE = 'drilldown/list/UPDATE_STORE'; + +function updateStore (state) { + return { type: UPDATE_STORE, state }; +} + +function makeRequest (baseComponent, metric, options) { + const asc = metric.direction === 1; + const ps = 100; + const finalOptions = { asc, ps }; + + if (metric.key.indexOf('new_') === 0) { + Object.assign(options, { + s: 'metricPeriod,name', + metricSort: metric.key, + metricPeriodSort: 1 + }); + } else { + Object.assign(options, { + s: 'metric,name', + metricSort: metric.key + }); + } + + Object.assign(finalOptions, options); + return getComponentTree('leaves', baseComponent.key, [metric.key], finalOptions); +} + +function fetchLeaves (baseComponent, metric, pageIndex = 1) { + const options = { p: pageIndex }; + + return makeRequest(baseComponent, metric, options).then(r => { + const nextComponents = enhanceWithSingleMeasure(r.components); + + return { + components: nextComponents, + pageIndex: r.paging.pageIndex, + total: r.paging.total + }; + }); +} + +/** + * Fetch the first page of components for a given base component + * @param baseComponent + * @param metric + */ +export function fetchList (baseComponent, metric) { + return (dispatch, getState) => { + const { list } = getState(); + if (list.baseComponent === baseComponent && list.metric === metric) { + return Promise.resolve(); + } + + dispatch(startFetching()); + return fetchLeaves(baseComponent, metric).then(r => { + dispatch(updateStore({ + ...r, + baseComponent, + metric + })); + dispatch(stopFetching()); + }); + }; +} + +/** + * Fetch next page of components + * @param baseComponent + * @param metric + */ +export function fetchMore (baseComponent, metric) { + return (dispatch, getState) => { + const { components, pageIndex } = getState().list; + dispatch(startFetching()); + return fetchLeaves(baseComponent, metric, pageIndex + 1).then(r => { + const diff = { ...r, components: [...components, ...r.components] }; + dispatch(updateStore(diff)); + dispatch(stopFetching()); + }); + }; +} + +/** + * Select specified component from the list + * @param component A component to select + */ +export function selectComponent (component) { + return dispatch => { + dispatch(updateStore({ selected: component })); + }; +} + +/** + * Select next element from the list of components + */ +export function selectNext () { + return (dispatch, getState) => { + const { components, selected } = getState().list; + const selectedIndex = components.indexOf(selected); + if (selectedIndex < components.length - 1) { + const nextSelected = components[selectedIndex + 1]; + dispatch(selectComponent(nextSelected)); + } + }; +} + +/** + * Select previous element from the list of components + */ +export function selectPrevious () { + return (dispatch, getState) => { + const { components, selected } = getState().list; + const selectedIndex = components.indexOf(selected); + if (selectedIndex > 0) { + const nextSelected = components[selectedIndex - 1]; + dispatch(selectComponent(nextSelected)); + } + }; +} diff --git a/server/sonar-web/src/main/js/apps/component-measures/store/listViewReducer.js b/server/sonar-web/src/main/js/apps/component-measures/store/listViewReducer.js new file mode 100644 index 00000000000..40105889660 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/store/listViewReducer.js @@ -0,0 +1,37 @@ +/* + * 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 { DISPLAY_HOME } from './../app/actions'; +import { UPDATE_STORE } from './listViewActions'; + +const initialState = { + components: [], + total: 0 +}; + +export default function drilldownReducer (state = initialState, action = {}) { + switch (action.type) { + case DISPLAY_HOME: + return initialState; + case UPDATE_STORE: + return { ...state, ...action.state }; + default: + return state; + } +} diff --git a/server/sonar-web/src/main/js/apps/component-measures/store/statusActions.js b/server/sonar-web/src/main/js/apps/component-measures/store/statusActions.js new file mode 100644 index 00000000000..5755ad5ff55 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/store/statusActions.js @@ -0,0 +1,29 @@ +/* + * 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. + */ +export const START_FETCHING = 'status/START_FETCHING'; +export const STOP_FETCHING = 'status/STOP_FETCHING'; + +export function startFetching () { + return { type: START_FETCHING }; +} + +export function stopFetching () { + return { type: STOP_FETCHING }; +} diff --git a/server/sonar-web/src/main/js/apps/component-measures/store/statusReducer.js b/server/sonar-web/src/main/js/apps/component-measures/store/statusReducer.js new file mode 100644 index 00000000000..09e27b232d8 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/store/statusReducer.js @@ -0,0 +1,36 @@ +/* + * 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 { START_FETCHING, STOP_FETCHING } from './statusActions'; + +const initialState = { + fetching: false +}; + +export default function drilldownReducer (state = initialState, action = {}) { + switch (action.type) { + case START_FETCHING: + return { ...state, fetching: true }; + case STOP_FETCHING: + return { ...state, fetching: false }; + default: + return state; + } +} + diff --git a/server/sonar-web/src/main/js/apps/component-measures/store/treeViewActions.js b/server/sonar-web/src/main/js/apps/component-measures/store/treeViewActions.js new file mode 100644 index 00000000000..a45cf2dfa5c --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/store/treeViewActions.js @@ -0,0 +1,244 @@ +/* + * 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 initial from 'lodash/initial'; + +import { getComponentTree } from '../../../api/components'; +import { enhanceWithSingleMeasure } from '../utils'; +import { startFetching, stopFetching } from './statusActions'; + +/* + * Actions + */ + +export const UPDATE_STORE = 'drilldown/tree/UPDATE_STORE'; +export const INIT = 'drilldown/tree/INIT'; + + +/* + * Action Creators + */ + +/** + * Internal + * Update store + * @param state + * @returns {{type: string, state: *}} + */ +function updateStore (state) { + return { type: UPDATE_STORE, state }; +} + +/** + * Init tree view drilldown for the given root component and given metric + * @param rootComponent + * @param metric + * @returns {{type: string, rootComponent: *, metric: *}} + */ +function init (rootComponent, metric) { + return { type: INIT, rootComponent, metric }; +} + + +/* + * Workflow + */ + +function makeRequest (baseComponent, metric, options) { + const asc = metric.direction === 1; + const ps = 100; + const finalOptions = { asc, ps }; + + if (metric.key.indexOf('new_') === 0) { + Object.assign(options, { + s: 'metricPeriod,name', + metricSort: metric.key, + metricPeriodSort: 1 + }); + } else { + Object.assign(options, { + s: 'metric,name', + metricSort: metric.key + }); + } + + Object.assign(finalOptions, options); + return getComponentTree('children', baseComponent.key, [metric.key], finalOptions); +} + +function fetchComponents (baseComponent, metric, pageIndex = 1) { + const options = { p: pageIndex }; + + return makeRequest(baseComponent, metric, options).then(r => { + const nextComponents = enhanceWithSingleMeasure(r.components); + + return { + baseComponent, + components: nextComponents, + pageIndex: r.paging.pageIndex, + total: r.paging.total + }; + }); +} + +/** + * Fetch the first page of components for a given base component + * @param baseComponent + */ +function fetchList (baseComponent) { + return (dispatch, getState) => { + const { metric } = getState().tree; + + dispatch(startFetching()); + return fetchComponents(baseComponent, metric).then(r => { + dispatch(updateStore({ + ...r, + baseComponent, + breadcrumbs: [baseComponent] + })); + dispatch(stopFetching()); + }); + }; +} + +/** + * Init tree view with root component and metric. + * Fetch the first page of components if needed. + * @param rootComponent + * @param metric + * @returns {function()} + */ +export function start (rootComponent, metric) { + return (dispatch, getState) => { + const { tree } = getState(); + if (rootComponent === tree.rootComponent && metric === tree.metric) { + return Promise.resolve(); + } + + dispatch(init(rootComponent, metric)); + dispatch(fetchList(rootComponent)); + }; +} + +/** + * Fetch next page of components + */ +export function fetchMore () { + return (dispatch, getState) => { + const { metric, baseComponent, components, pageIndex } = getState().tree; + dispatch(startFetching()); + return fetchComponents(baseComponent, metric, pageIndex + 1).then(r => { + dispatch(updateStore({ + ...r, + components: [...components, ...r.components] + })); + dispatch(stopFetching()); + }); + }; +} + +/** + * Drilldown to the component + * @param component + */ +export function drilldown (component) { + return (dispatch, getState) => { + const { metric, breadcrumbs } = getState().tree; + dispatch(startFetching()); + return fetchComponents(component, metric).then(r => { + dispatch(updateStore({ + ...r, + breadcrumbs: [...breadcrumbs, component], + selected: undefined + })); + dispatch(stopFetching()); + }); + }; +} + +/** + * Go up using breadcrumbs + * @param component + */ +export function useBreadcrumbs (component) { + return (dispatch, getState) => { + const { metric, breadcrumbs } = getState().tree; + const index = breadcrumbs.indexOf(component); + dispatch(startFetching()); + return fetchComponents(component, metric).then(r => { + dispatch(updateStore({ + ...r, + breadcrumbs: breadcrumbs.slice(0, index + 1), + selected: undefined + })); + dispatch(stopFetching()); + }); + }; +} + +/** + * Select given component from the list + * @param component + */ +export function selectComponent (component) { + return (dispatch, getState) => { + const { breadcrumbs } = getState().tree; + const nextBreadcrumbs = [...breadcrumbs, component]; + dispatch(updateStore({ + selected: component, + breadcrumbs: nextBreadcrumbs + })); + }; +} + +/** + * Select next element from the list of components + */ +export function selectNext () { + return (dispatch, getState) => { + const { components, selected, breadcrumbs } = getState().tree; + const selectedIndex = components.indexOf(selected); + if (selectedIndex < components.length - 1) { + const nextSelected = components[selectedIndex + 1]; + const nextBreadcrumbs = [...initial(breadcrumbs), nextSelected]; + dispatch(updateStore({ + selected: nextSelected, + breadcrumbs: nextBreadcrumbs + })); + } + }; +} + +/** + * Select previous element from the list of components + */ +export function selectPrevious () { + return (dispatch, getState) => { + const { components, selected, breadcrumbs } = getState().tree; + const selectedIndex = components.indexOf(selected); + if (selectedIndex > 0) { + const nextSelected = components[selectedIndex - 1]; + const nextBreadcrumbs = [...initial(breadcrumbs), nextSelected]; + dispatch(updateStore({ + selected: nextSelected, + breadcrumbs: nextBreadcrumbs + })); + } + }; +} diff --git a/server/sonar-web/src/main/js/apps/component-measures/store/treeViewReducer.js b/server/sonar-web/src/main/js/apps/component-measures/store/treeViewReducer.js new file mode 100644 index 00000000000..b647b538816 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/store/treeViewReducer.js @@ -0,0 +1,42 @@ +/* + * 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 pick from 'lodash/pick'; + +import { DISPLAY_HOME } from './../app/actions'; +import { UPDATE_STORE, INIT } from './treeViewActions'; + +const initialState = { + components: [], + breadcrumbs: [], + total: 0 +}; + +export default function drilldownReducer (state = initialState, action = {}) { + switch (action.type) { + case DISPLAY_HOME: + return initialState; + case UPDATE_STORE: + return { ...state, ...action.state }; + case INIT: + return { ...state, ...pick(action, ['rootComponent', 'metric']) }; + default: + return state; + } +} diff --git a/server/sonar-web/src/main/js/apps/component-measures/styles.css b/server/sonar-web/src/main/js/apps/component-measures/styles.css index 68c85c91702..198f0aeea91 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/styles.css +++ b/server/sonar-web/src/main/js/apps/component-measures/styles.css @@ -1,8 +1,4 @@ .measures-domains { - max-width: 980px; - margin: 20px auto; - column-count: 2; - column-gap: 60px; } .measures-domains > li { @@ -67,25 +63,12 @@ .measure-details-header { position: relative; -} - -.measure-details-see-also { - position: absolute; - z-index: 50; - top: 29px; - left: 10px; - width: 400px; - max-height: 450px; - border: 1px solid #e6e6e6; - background-color: #fff; - box-shadow: 0 6px 12px rgba(0, 0, 0, .175); - overflow: auto; + margin-top: 10px; + margin-bottom: 30px; } .measure-details-metric, .measure-details-value { - margin-left: 20px; - margin-right: 20px; } .measure-details-metric { @@ -138,8 +121,7 @@ .measure-details-drilldown-mode { display: flex; - padding-left: 10px; - padding-right: 10px; + margin-bottom: 10px; border-bottom: 1px solid #e6e6e6; } @@ -167,16 +149,15 @@ fill: #4b9fd5; } +.measure-details-plain-list { +} + .measure-details-components { width: 300px; padding: 10px; box-sizing: border-box; } -.measure-details-components-parent { - padding-bottom: 6px; -} - .measure-details-components > li > a { display: flex; padding-top: 5px; @@ -209,16 +190,23 @@ text-align: right; } -.measure-details-tree, -.measure-details-plain-list { - display: flex; +.measure-details-viewer { + min-height: 100vh; } -.measure-details-viewer { - width: calc(100% - 300px); - margin-top: -1px; - margin-bottom: -1px; - box-sizing: border-box; +.measure-details-viewer-header { + margin-bottom: 10px; + line-height: 24px; +} + +.measure-details-viewer-header .button-group { + vertical-align: top; +} + +.measure-details-header-container { + display: inline-block; + line-height: 16px; + padding-right: 10px; } .measure-tab-icon { @@ -233,28 +221,17 @@ transition: fill 0.3s ease; } -.measure-details-components-up-icon { - display: inline-block; - height: 14px; - padding: 2px 4px 0; -} - .measure-details-components-up-icon path { fill: #aeaeae; } -.measures-details-components-empty { - padding: 10px; -} - .measure-details-history { - padding: 10px; + padding: 10px 0; } .measure-details-bubble-chart { position: relative; - max-width: 960px; - margin: 10px auto; + margin: 10px 0; padding: 30px 0 30px 50px; } @@ -281,10 +258,36 @@ } .measure-details-treemap { - max-width: 960px; - margin: 20px auto; + margin: 20px 0; } .measure-details-treemap-legend { margin-bottom: 10px; } + + +.component-measures-breadcrumbs { + display: flex; + flex-wrap: wrap; +} + +.component-measures-breadcrumbs > li { + padding: 5px 5px 3px; +} + +.component-measures-breadcrumbs > li:first-child { + padding-left: 0; +} + +.component-measures-breadcrumbs > li::after { + position: relative; + top: -1px; + padding-left: 10px; + color: #777; + font-size: 11px; + content: ">"; +} + +.component-measures-breadcrumbs > li:last-child::after { + display: none; +} diff --git a/server/sonar-web/src/main/js/apps/component-measures/utils.js b/server/sonar-web/src/main/js/apps/component-measures/utils.js index f20583bc2cb..5349f97864a 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/utils.js +++ b/server/sonar-web/src/main/js/apps/component-measures/utils.js @@ -17,9 +17,13 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import bubbles from './bubbles'; +import bubbles from './config/bubbles'; import { formatMeasure, formatMeasureVariation } from '../../helpers/measures'; +export function isDiffMetric (metric) { + return metric.key.indexOf('new_') === 0; +} + export function getLeakValue (measure) { if (!measure) { return null; @@ -53,7 +57,7 @@ export function getSingleLeakValue (measures) { } export function formatLeak (value, metric) { - if (metric.key.indexOf('new_') === 0) { + if (isDiffMetric(metric)) { return formatMeasure(value, metric.type); } else { return formatMeasureVariation(value, metric.type); @@ -80,8 +84,7 @@ export function enhanceWithSingleMeasure (components) { value: getSingleMeasureValue(component.measures), leak: getSingleLeakValue(component.measures) }; - }) - .filter(component => component.value != null || component.leak != null); + }); } export function hasHistory (metricKey) { diff --git a/server/sonar-web/src/main/js/components/shared/list-footer.js b/server/sonar-web/src/main/js/components/shared/list-footer.js index 5a501d7a8da..fd9392b05ab 100644 --- a/server/sonar-web/src/main/js/components/shared/list-footer.js +++ b/server/sonar-web/src/main/js/components/shared/list-footer.js @@ -41,6 +41,7 @@ export default React.createClass({ handleLoadMore(e) { e.preventDefault(); + e.target.blur(); if (this.canLoadMore()) { this.props.loadMore(); } diff --git a/server/sonar-web/src/main/js/components/source-viewer/main.js b/server/sonar-web/src/main/js/components/source-viewer/main.js index c6cb2892ffd..58db3aa9ea9 100644 --- a/server/sonar-web/src/main/js/components/source-viewer/main.js +++ b/server/sonar-web/src/main/js/components/source-viewer/main.js @@ -108,6 +108,7 @@ export default Marionette.LayoutView.extend({ }); this.issueViews = []; this.clearTooltips(); + this.unbindScrollEvents(); }, clearTooltips () { diff --git a/server/sonar-web/src/main/js/components/store/configureStore.js b/server/sonar-web/src/main/js/components/store/configureStore.js index d7d69d3ecea..97397d836cd 100644 --- a/server/sonar-web/src/main/js/components/store/configureStore.js +++ b/server/sonar-web/src/main/js/components/store/configureStore.js @@ -35,6 +35,6 @@ const finalCreateStore = compose( ...composed )(createStore); -export default function configureStore (rootReducer) { - return finalCreateStore(rootReducer); +export default function configureStore (rootReducer, initialState) { + return finalCreateStore(rootReducer, initialState); } diff --git a/server/sonar-web/src/main/js/helpers/path.js b/server/sonar-web/src/main/js/helpers/path.js index c0a16a4ea9e..a03c1421bf1 100644 --- a/server/sonar-web/src/main/js/helpers/path.js +++ b/server/sonar-web/src/main/js/helpers/path.js @@ -94,3 +94,16 @@ export function fileFromPath (path) { return null; } } + + +export function splitPath (path) { + if (typeof path === 'string') { + const tokens = path.split('/'); + return { + head: _.initial(tokens).join('/'), + tail: _.last(tokens) + }; + } else { + return null; + } +} diff --git a/server/sonar-web/src/main/js/libs/third-party/jquery-ui.js b/server/sonar-web/src/main/js/libs/third-party/jquery-ui.js index 3062a520301..1645ce863df 100755 --- a/server/sonar-web/src/main/js/libs/third-party/jquery-ui.js +++ b/server/sonar-web/src/main/js/libs/third-party/jquery-ui.js @@ -493,7 +493,7 @@ $.widget.bridge = function( name, object ) { args = slice.call( arguments, 1 ), returnValue = this; - // allow multiple hashes to be passed on init + // allow multiple hashes to be passed on start options = !isMethodCall && args.length ? $.widget.extend.apply( null, [ options ].concat(args) ) : options; diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 7306c76a101..931389b118e 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -3232,6 +3232,8 @@ code.open_component_page=Open Component's Page # COMPONENT MEASURES # #------------------------------------------------------------------------------ +component_measures.back_to_all_measures=Back to All Measures +component_measures.back_to_list=Back to List component_measures.tab.tree=Tree component_measures.tab.list=List component_measures.tab.bubbles=Bubble Chart @@ -3239,3 +3241,4 @@ component_measures.tab.treemap=Treemap component_measures.tab.history=History component_measures.legend.color_x=Color: {0} component_measures.legend.size_x=Color: {0} +component_measures.x_of_y={0} of {1} -- 2.39.5