From cc9c506efd50786649658baa0d243654c59c512e Mon Sep 17 00:00:00 2001 From: Stas Vilchik Date: Mon, 21 Mar 2016 18:28:05 +0100 Subject: SONAR-7402 improve list and tree views on measures page --- server/sonar-web/src/main/js/api/components.js | 20 +- .../src/main/js/apps/component-measures/app.js | 55 ++--- .../src/main/js/apps/component-measures/app/App.js | 42 ++++ .../js/apps/component-measures/app/AppContainer.js | 40 ++++ .../main/js/apps/component-measures/app/actions.js | 53 +++++ .../main/js/apps/component-measures/app/reducer.js | 34 +++ .../src/main/js/apps/component-measures/bubbles.js | 40 ---- .../component-measures/components/AllMeasures.js | 110 ---------- .../components/AllMeasuresDomain.js | 83 ------- .../components/ComponentMeasuresApp.js | 75 ------- .../apps/component-measures/components/IconUp.js | 36 --- .../components/MeasureBubbleChart.js | 164 -------------- .../components/MeasureDetails.js | 149 ------------- .../components/MeasureDetailsHeader.js | 102 --------- .../components/MeasureDetailsSeeAlso.js | 160 -------------- .../components/MeasureDrilldown.js | 126 ----------- .../components/MeasureDrilldownComponents.js | 77 ------- .../components/MeasureDrilldownEmpty.js | 30 --- .../components/MeasureDrilldownList.js | 129 ----------- .../components/MeasureDrilldownTree.js | 139 ------------ .../components/MeasureHistory.js | 182 --------------- .../components/MeasureTreemap.js | 235 -------------------- .../js/apps/component-measures/config/bubbles.js | 40 ++++ .../component-measures/details/MeasureDetails.js | 101 +++++++++ .../details/MeasureDetailsContainer.js | 44 ++++ .../details/MeasureDetailsHeader.js | 78 +++++++ .../js/apps/component-measures/details/actions.js | 77 +++++++ .../details/bubbleChart/BubbleChart.js | 159 ++++++++++++++ .../bubbleChart/MeasureBubbleChartContainer.js | 39 ++++ .../details/drilldown/Breadcrumb.js | 62 ++++++ .../details/drilldown/Breadcrumbs.js | 37 ++++ .../details/drilldown/ComponentCell.js | 78 +++++++ .../details/drilldown/ComponentsList.js | 46 ++++ .../details/drilldown/ComponentsListRow.js | 44 ++++ .../details/drilldown/EmptyComponentsList.js | 32 +++ .../details/drilldown/ListHeader.js | 68 ++++++ .../details/drilldown/ListView.js | 130 +++++++++++ .../details/drilldown/ListViewContainer.js | 54 +++++ .../details/drilldown/MeasureCell.js | 39 ++++ .../details/drilldown/MeasureDrilldown.js | 97 ++++++++ .../details/drilldown/TreeView.js | 128 +++++++++++ .../details/drilldown/TreeViewContainer.js | 65 ++++++ .../details/history/MeasureHistory.js | 177 +++++++++++++++ .../details/history/MeasureHistoryContainer.js | 38 ++++ .../js/apps/component-measures/details/reducer.js | 42 ++++ .../details/treemap/MeasureTreemap.js | 226 +++++++++++++++++++ .../details/treemap/MeasureTreemapContainer.js | 38 ++++ .../js/apps/component-measures/home/AllMeasures.js | 64 ++++++ .../home/AllMeasuresContainer.js | 46 ++++ .../component-measures/home/AllMeasuresDomain.js | 83 +++++++ .../js/apps/component-measures/home/actions.js | 59 +++++ .../js/apps/component-measures/home/reducer.js | 34 +++ .../component-measures/store/configureStore.js | 41 ++++ .../component-measures/store/listViewActions.js | 143 ++++++++++++ .../component-measures/store/listViewReducer.js | 37 ++++ .../apps/component-measures/store/statusActions.js | 29 +++ .../apps/component-measures/store/statusReducer.js | 36 +++ .../component-measures/store/treeViewActions.js | 244 +++++++++++++++++++++ .../component-measures/store/treeViewReducer.js | 42 ++++ .../src/main/js/apps/component-measures/styles.css | 99 +++++---- .../src/main/js/apps/component-measures/utils.js | 11 +- .../src/main/js/components/shared/list-footer.js | 1 + .../src/main/js/components/source-viewer/main.js | 1 + .../src/main/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 +- 66 files changed, 3081 insertions(+), 1928 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/component-measures/app/App.js 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/bubbles.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/AllMeasuresDomain.js delete mode 100644 server/sonar-web/src/main/js/apps/component-measures/components/ComponentMeasuresApp.js delete mode 100644 server/sonar-web/src/main/js/apps/component-measures/components/IconUp.js delete mode 100644 server/sonar-web/src/main/js/apps/component-measures/components/MeasureBubbleChart.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/MeasureDetailsHeader.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/MeasureDrilldown.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/MeasureDrilldownEmpty.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 delete mode 100644 server/sonar-web/src/main/js/apps/component-measures/components/MeasureHistory.js delete mode 100644 server/sonar-web/src/main/js/apps/component-measures/components/MeasureTreemap.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/config/bubbles.js 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 create mode 100644 server/sonar-web/src/main/js/apps/component-measures/details/MeasureDetailsHeader.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/details/actions.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/details/bubbleChart/BubbleChart.js 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 create mode 100644 server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ComponentsList.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ComponentsListRow.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/details/drilldown/EmptyComponentsList.js 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 create mode 100644 server/sonar-web/src/main/js/apps/component-measures/details/drilldown/MeasureDrilldown.js 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 create mode 100644 server/sonar-web/src/main/js/apps/component-measures/details/history/MeasureHistory.js 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 create mode 100644 server/sonar-web/src/main/js/apps/component-measures/details/treemap/MeasureTreemap.js 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 create mode 100644 server/sonar-web/src/main/js/apps/component-measures/home/AllMeasuresDomain.js 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 (limited to 'server/sonar-web/src/main/js') 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/app/App.js b/server/sonar-web/src/main/js/apps/component-measures/app/App.js new file mode 100644 index 00000000000..fa23544cd91 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/app/App.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 React from 'react'; + +import Spinner from './../components/Spinner'; + +export default class App extends React.Component { + componentDidMount () { + this.props.fetchMetrics(); + } + + render () { + const { metrics } = this.props; + + if (metrics == null) { + return ; + } + + return ( +
+ {this.props.children} +
+ ); + } +} 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/bubbles.js b/server/sonar-web/src/main/js/apps/component-measures/bubbles.js deleted file mode 100644 index bfbaed418b7..00000000000 --- a/server/sonar-web/src/main/js/apps/component-measures/bubbles.js +++ /dev/null @@ -1,40 +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. - */ -const bubblesConfig = { - 'code_smells': { x: 'ncloc', y: 'sqale_index', size: 'code_smells' }, - 'sqale_index': { x: 'ncloc', y: 'code_smells', size: 'sqale_index' }, - - 'coverage': { x: 'complexity', y: 'coverage', size: 'uncovered_lines' }, - 'it_coverage': { x: 'complexity', y: 'it_coverage', size: 'it_uncovered_lines' }, - 'overall_coverage': { x: 'complexity', y: 'overall_coverage', size: 'overall_uncovered_lines' }, - - 'uncovered_lines': { x: 'complexity', y: 'coverage', size: 'uncovered_lines' }, - 'it_uncovered_lines': { x: 'complexity', y: 'it_coverage', size: 'it_uncovered_lines' }, - 'overall_uncovered_lines': { x: 'complexity', y: 'overall_coverage', size: 'overall_uncovered_lines' }, - - 'uncovered_conditions': { x: 'complexity', y: 'coverage', size: 'uncovered_conditions' }, - 'it_uncovered_conditions': { x: 'complexity', y: 'it_coverage', size: 'it_uncovered_conditions' }, - 'overall_uncovered_conditions': { x: 'complexity', y: 'overall_coverage', size: 'overall_uncovered_conditions' }, - - 'duplicated_lines': { x: 'ncloc', y: 'duplicated_lines', size: 'duplicated_blocks' }, - 'duplicated_blocks': { x: 'ncloc', y: 'duplicated_lines', size: 'duplicated_blocks' } -}; - -export default bubblesConfig; 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/AllMeasuresDomain.js b/server/sonar-web/src/main/js/apps/component-measures/components/AllMeasuresDomain.js deleted file mode 100644 index 6068cfd31b1..00000000000 --- a/server/sonar-web/src/main/js/apps/component-measures/components/AllMeasuresDomain.js +++ /dev/null @@ -1,83 +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 sortBy from 'lodash/sortBy'; -import partition from 'lodash/partition'; -import React from 'react'; -import { Link } from 'react-router'; - -import domains from '../config/domains'; -import { formatLeak } from '../utils'; -import { formatMeasure } from '../../../helpers/measures'; -import { translateWithParameters } from '../../../helpers/l10n'; - -export default function AllMeasuresDomain ({ domain, component, displayLeakHeader, leakPeriodLabel }) { - const hasLeak = !!leakPeriodLabel; - const { measures } = domain; - const knownMetrics = domains[domain.name] || []; - - const [knownMeasures, otherMeasures] = - partition(measures, measure => knownMetrics.indexOf(measure.metric.key) !== -1); - - const finalMeasures = [ - ...sortBy(knownMeasures, measure => knownMetrics.indexOf(measure.metric.key)), - ...sortBy(otherMeasures, measure => measure.metric.name) - ]; - - return ( -
  • -
    -

    {domain.name}

    - {displayLeakHeader && hasLeak && ( -
    - {translateWithParameters('overview.leak_period_x', leakPeriodLabel)} -
    - )} -
    - -
      - {finalMeasures.map(measure => ( -
    • - -
      - - {measure.metric.name} - -
      -
      - {measure.value != null && ( - - {formatMeasure(measure.value, measure.metric.type)} - - )} -
      - {hasLeak && measure.leak != null && ( -
      - - {formatLeak(measure.leak, measure.metric)} - -
      - )} - -
    • - ))} -
    -
  • - ); -} 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/components/ComponentMeasuresApp.js deleted file mode 100644 index c71d8c9fca7..00000000000 --- a/server/sonar-web/src/main/js/apps/component-measures/components/ComponentMeasuresApp.js +++ /dev/null @@ -1,75 +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 { 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 - }; - } - - componentDidMount () { - this.mounted = true; - this.fetchMetrics(); - } - - componentWillUnmount () { - this.mounted = false; - } - - fetchMetrics () { - getMetrics().then(metrics => { - if (this.mounted) { - this.setState({ metrics, fetching: false }); - } - }); - } - - render () { - const { fetching, metrics } = this.state; - - if (fetching) { - return ; - } - - const child = React.cloneElement(this.props.children, { metrics }); - - return ( -
    - {child} -
    - ); - } -} - -ComponentMeasuresApp.childContextTypes = { - component: React.PropTypes.object, - metrics: React.PropTypes.array -}; 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/components/IconUp.js deleted file mode 100644 index 462d07bb17b..00000000000 --- a/server/sonar-web/src/main/js/apps/component-measures/components/IconUp.js +++ /dev/null @@ -1,36 +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'; - -export default function IconUp () { - /* eslint max-len: 0 */ - return ( - - - - ); -} 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/components/MeasureBubbleChart.js deleted file mode 100644 index 5654e20cd14..00000000000 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureBubbleChart.js +++ /dev/null @@ -1,164 +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 { 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; -const BUBBLES_LIMIT = 500; - -function getMeasure (component, metric) { - return Number(component.measures[metric]) || 0; -} - -export default class MeasureBubbleChart extends React.Component { - state = { - fetching: true, - files: [] - }; - - componentWillMount () { - const { metric } = this.props; - const { metrics } = this.context; - const conf = bubbles[metric.key]; - - this.xMetric = metrics.find(m => m.key === conf.x); - this.yMetric = metrics.find(m => m.key === conf.y); - this.sizeMetric = metrics.find(m => m.key === conf.size); - } - - componentDidMount () { - this.mounted = true; - this.fetchFiles(); - } - - componentDidUpdate (nextProps, nextState, nextContext) { - if ((nextProps.metric !== this.props.metric) || - (nextContext.component !== this.context.component)) { - this.fetchFiles(); - } - } - - componentWillUnmount () { - this.mounted = false; - } - - fetchFiles () { - const { component } = this.context; - const metrics = [this.xMetric.key, this.yMetric.key, this.sizeMetric.key]; - const options = { - s: 'metric', - metricSort: this.sizeMetric.key, - asc: false, - ps: BUBBLES_LIMIT - }; - - getFiles(component.key, metrics, options).then(r => { - const files = r.map(file => { - const measures = {}; - - file.measures.forEach(measure => { - measures[measure.metric] = measure.value; - }); - return { ...file, measures }; - }); - - this.setState({ - files, - fetching: false, - total: files.length - }); - }); - } - - getTooltip (component) { - const inner = [ - component.name, - `${this.xMetric.name}: ${formatMeasure(getMeasure(component, this.xMetric.key), this.xMetric.type)}`, - `${this.yMetric.name}: ${formatMeasure(getMeasure(component, this.yMetric.key), this.yMetric.type)}`, - `${this.sizeMetric.name}: ${formatMeasure(getMeasure(component, this.sizeMetric.key), this.sizeMetric.type)}` - ].join('
    '); - - return `
    ${inner}
    `; - } - - handleBubbleClick (id) { - Workspace.openComponent({ uuid: id }); - } - - renderBubbleChart () { - const items = this.state.files.map(file => { - return { - x: getMeasure(file, this.xMetric.key), - y: getMeasure(file, this.yMetric.key), - size: getMeasure(file, this.sizeMetric.key), - link: file.id, - tooltip: this.getTooltip(file) - }; - }); - - const formatXTick = (tick) => formatMeasure(tick, this.xMetric.type); - const formatYTick = (tick) => formatMeasure(tick, this.yMetric.type); - - return ( - - ); - } - - render () { - const { fetching } = this.state; - - if (fetching) { - return ( -
    -
    - -
    -
    - ); - } - - return ( -
    - {this.renderBubbleChart()} - -
    {this.xMetric.name}
    -
    {this.yMetric.name}
    -
    Size: {this.sizeMetric.name}
    -
    - ); - } -} - -MeasureBubbleChart.contextTypes = { - component: React.PropTypes.object, - metrics: React.PropTypes.array -}; 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/MeasureDetailsHeader.js b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDetailsHeader.js deleted file mode 100644 index 59a9a3c9eee..00000000000 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDetailsHeader.js +++ /dev/null @@ -1,102 +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 MeasureDetailsSeeAlso from './MeasureDetailsSeeAlso'; -import LanguageDistribution from './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 - } -) { - const leakPeriodTooltip = translateWithParameters('overview.leak_period_x', leakPeriodLabel); - - const displayLeak = measure.leak != null && metric.type !== 'RATING' && metric.type !== 'LEVEL'; - - return ( -
    -

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

    - - -
    - {measure.value != null && ( -
    - {formatMeasure(measure.value, metric.type)} -
    - )} - - {displayLeak && ( -
    - {formatLeak(measure.leak, metric)} -
    - )} - - {secondaryMeasure && secondaryMeasure.metric === 'ncloc_language_distribution' && ( -
    - -
    - )} - - {secondaryMeasure && secondaryMeasure.metric === 'function_complexity_distribution' && ( -
    - -
    - )} - - {secondaryMeasure && secondaryMeasure.metric === 'file_complexity_distribution' && ( -
    - -
    - )} -
    -
    - - {metricSelectOpen && ( - - )} -
    - ); -} 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/MeasureDrilldown.js b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDrilldown.js deleted file mode 100644 index d0e78285813..00000000000 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDrilldown.js +++ /dev/null @@ -1,126 +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 { 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 { 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 child = React.cloneElement(children, { - component, - metric, - ...other, - store: this.state, - updateStore: this.setState.bind(this) - }); - - return ( -
    -
      -
    • - - - {translate('component_measures.tab.tree')} - -
    • - - {showListView && ( -
    • - - - {translate('component_measures.tab.list')} - -
    • - )} - - {hasBubbleChart(metric.key) && ( -
    • - - - {translate('component_measures.tab.bubbles')} - -
    • - )} - - {hasTreemap(metric) && ( -
    • - - - {translate('component_measures.tab.treemap')} - -
    • - )} - - {hasHistory(metric.key) && ( -
    • - - - {translate('component_measures.tab.history')} - -
    • - )} -
    - - {child} -
    - ); - } -} - -MeasureDrilldown.contextTypes = { - component: React.PropTypes.object -}; 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/MeasureDrilldownEmpty.js b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDrilldownEmpty.js deleted file mode 100644 index ede861f8382..00000000000 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDrilldownEmpty.js +++ /dev/null @@ -1,30 +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 { translate } from '../../../helpers/l10n'; - -export default function MeasureDrilldownEmpty () { - return ( -
    - {translate('no_results')} -
    - ); -} 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/components/MeasureHistory.js b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHistory.js deleted file mode 100644 index a8b9acb7c44..00000000000 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHistory.js +++ /dev/null @@ -1,182 +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 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'; - -const HEIGHT = 360; - -function parseValue (value, type) { - return type === 'RATING' && typeof value === 'string' ? value.charCodeAt(0) - 'A'.charCodeAt(0) + 1 : value; -} - -export default class MeasureHistory extends React.Component { - state = { - components: [], - selected: null, - fetching: true - }; - - componentDidMount () { - this.mounted = true; - this.fetchHistory(); - } - - componentDidUpdate (nextProps, nextState, nextContext) { - if ((nextProps.metric !== this.props.metric) || - (nextContext.component !== this.context.component)) { - this.fetchHistory(); - } - } - - componentWillUnmount () { - this.mounted = false; - } - - fetchHistory () { - const { metric } = this.props; - - Promise.all([ - this.fetchTimeMachineData(metric.key), - this.fetchEvents() - ]).then(responses => { - if (this.mounted) { - this.setState({ - snapshots: responses[0], - events: responses[1], - fetching: false - }); - } - }); - } - - fetchTimeMachineData (currentMetric, comparisonMetric) { - const metricsToRequest = [currentMetric]; - - if (comparisonMetric) { - metricsToRequest.push(comparisonMetric); - } - - return getTimeMachineData(this.props.component.key, metricsToRequest.join()).then(r => { - return r[0].cells.map(cell => { - return { - date: moment(cell.d).toDate(), - values: cell.v - }; - }); - }); - } - - fetchEvents () { - return getEvents(this.props.component.key, 'Version').then(r => { - const events = r.map(event => { - return { version: event.n, date: moment(event.dt).toDate() }; - }); - - return _.sortBy(events, 'date'); - }); - } - - renderLineChart (snapshots, metric, index) { - if (!metric) { - return null; - } - - if (snapshots.length < 2) { - return this.renderWhenNoHistoricalData(); - } - - const data = snapshots.map(snapshot => { - return { - x: snapshot.date, - y: parseValue(snapshot.values[index], metric.type) - }; - }); - - const formatValue = (value) => formatMeasure(value, metric.type); - const formatYTick = (tick) => formatMeasure(tick, getShortType(metric.type)); - - return ( -
    - -
    - ); - } - - renderLineCharts () { - const { metric } = this.props; - - return ( -
    - {this.renderLineChart(this.state.snapshots, metric, 0)} - {this.renderLineChart(this.state.snapshots, this.state.comparisonMetric, 1)} -
    - ); - } - - render () { - const { fetching, snapshots } = this.state; - - if (fetching) { - return ( -
    -
    - -
    -
    - ); - } - - if (!snapshots || snapshots.length < 2) { - return ( -
    -
    - There is no historical data. -
    -
    - ); - } - - return ( -
    - {this.renderLineCharts()} -
    - ); - } -} - -MeasureHistory.contextTypes = { - component: React.PropTypes.object -}; 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/components/MeasureTreemap.js deleted file mode 100644 index 2364d1b75ce..00000000000 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureTreemap.js +++ /dev/null @@ -1,235 +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 { 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; - -export default class MeasureTreemap extends React.Component { - state = { - fetching: true, - components: [], - breadcrumbs: [] - }; - - componentDidMount () { - const { component } = this.context; - - 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); - } - } - - componentWillUnmount () { - this.mounted = false; - } - - fetchComponents (componentKey) { - const { metric } = this.props; - const metrics = ['ncloc', metric.key]; - const options = { - s: 'metric', - metricSort: 'ncloc', - asc: false - }; - - return getChildren(componentKey, metrics, options).then(r => { - const components = r.map(component => { - const measures = {}; - const key = component.refKey || component.key; - - component.measures.forEach(measure => { - const shouldUseLeak = measure.metric.indexOf('new_') === 0; - measures[measure.metric] = shouldUseLeak ? getLeakValue(measure) : measure.value; - }); - return { ...component, measures, key }; - }); - - this.setState({ - components, - fetching: false - }); - }); - } - - getTooltip (component) { - const { metric } = this.props; - - let inner = [ - component.name, - `${translate('metric.ncloc.name')}: ${formatMeasure(component.measures['ncloc'], 'INT')}` - ]; - - const colorMeasure = component.measures[metric.key]; - const formatted = colorMeasure != null ? formatMeasure(colorMeasure, metric.type) : '—'; - - inner.push(`${metric.name}: ${formatted}`); - inner = inner.join('
    '); - - return `
    ${inner}
    `; - } - - getPercentColorScale (metric) { - const color = d3.scale.linear().domain([0, 25, 50, 75, 100]); - color.range(metric.direction === 1 ? - ['#ee0000', '#f77700', '#ffee00', '#80cc00', '#00aa00'] : - ['#00aa00', '#80cc00', '#ffee00', '#f77700', '#ee0000']); - return color; - } - - getRatingColorScale () { - return d3.scale.linear() - .domain([1, 2, 3, 4, 5]) - .range(['#00aa00', '#80cc00', '#ffee00', '#f77700', '#ee0000']); - } - - getLevelColorScale () { - return d3.scale.ordinal() - .domain(['ERROR', 'WARN', 'OK', 'NONE']) - .range(['#d4333f', '#ff9900', '#85bb43', '##b4b4b4']); - } - - getScale () { - const { metric } = this.props; - - if (metric.type === 'LEVEL') { - return this.getLevelColorScale(); - } - if (metric.type === 'RATING') { - return this.getRatingColorScale(); - } - return this.getPercentColorScale(metric); - } - - handleRectangleClick (node) { - const isFile = node.qualifier === 'FIL' || node.qualifier === 'UTS'; - - if (isFile) { - return Workspace.openComponent({ uuid: node.id }); - } - - this.fetchComponents(node.key).then(() => { - let nextBreadcrumbs = [...this.state.breadcrumbs]; - const index = this.state.breadcrumbs.findIndex(b => b.key === node.key); - - if (index !== -1) { - nextBreadcrumbs = nextBreadcrumbs.slice(0, index); - } - - nextBreadcrumbs = [...nextBreadcrumbs, { - key: node.key, - name: node.name, - qualifier: node.qualifier - }]; - - this.setState({ breadcrumbs: nextBreadcrumbs }); - }); - } - - handleReset () { - const { component } = this.context; - this.fetchComponents(component.key).then(() => { - this.setState({ breadcrumbs: [] }); - }); - } - - renderTreemap () { - const { metric } = this.props; - const colorScale = this.getScale(); - const items = this.state.components - .filter(component => component.measures['ncloc']) - .map(component => { - const colorMeasure = component.measures[metric.key]; - - return { - id: component.id, - key: component.key, - name: component.name, - qualifier: component.qualifier, - size: component.measures['ncloc'], - color: colorMeasure != null ? colorScale(colorMeasure) : '#777', - tooltip: this.getTooltip(component), - label: component.name, - link: getComponentUrl(component.key) - }; - }); - - // FIXME remove this magic number - const height = HEIGHT - 35; - - return ( - true} - onRectangleClick={this.handleRectangleClick.bind(this)} - onReset={this.handleReset.bind(this)}/> - ); - } - - render () { - const { metric } = this.props; - const { fetching } = this.state; - - if (fetching) { - return ( -
    -
    - -
    -
    - ); - } - - return ( -
    -
      -
    • - {translateWithParameters('component_measures.legend.color_x', metric.name)} -
    • -
    • - {translateWithParameters('component_measures.legend.size_x', translate('metric.ncloc.name'))} -
    • -
    - {this.renderTreemap()} -
    - ); - } -} - -MeasureTreemap.contextTypes = { - component: React.PropTypes.object, - metrics: React.PropTypes.array -}; diff --git a/server/sonar-web/src/main/js/apps/component-measures/config/bubbles.js b/server/sonar-web/src/main/js/apps/component-measures/config/bubbles.js new file mode 100644 index 00000000000..bfbaed418b7 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/config/bubbles.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. + */ +const bubblesConfig = { + 'code_smells': { x: 'ncloc', y: 'sqale_index', size: 'code_smells' }, + 'sqale_index': { x: 'ncloc', y: 'code_smells', size: 'sqale_index' }, + + 'coverage': { x: 'complexity', y: 'coverage', size: 'uncovered_lines' }, + 'it_coverage': { x: 'complexity', y: 'it_coverage', size: 'it_uncovered_lines' }, + 'overall_coverage': { x: 'complexity', y: 'overall_coverage', size: 'overall_uncovered_lines' }, + + 'uncovered_lines': { x: 'complexity', y: 'coverage', size: 'uncovered_lines' }, + 'it_uncovered_lines': { x: 'complexity', y: 'it_coverage', size: 'it_uncovered_lines' }, + 'overall_uncovered_lines': { x: 'complexity', y: 'overall_coverage', size: 'overall_uncovered_lines' }, + + 'uncovered_conditions': { x: 'complexity', y: 'coverage', size: 'uncovered_conditions' }, + 'it_uncovered_conditions': { x: 'complexity', y: 'it_coverage', size: 'it_uncovered_conditions' }, + 'overall_uncovered_conditions': { x: 'complexity', y: 'overall_coverage', size: 'overall_uncovered_conditions' }, + + 'duplicated_lines': { x: 'ncloc', y: 'duplicated_lines', size: 'duplicated_blocks' }, + 'duplicated_blocks': { x: 'ncloc', y: 'duplicated_lines', size: 'duplicated_blocks' } +}; + +export default bubblesConfig; 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/details/MeasureDetailsHeader.js b/server/sonar-web/src/main/js/apps/component-measures/details/MeasureDetailsHeader.js new file mode 100644 index 00000000000..13e3080a352 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/details/MeasureDetailsHeader.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 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 }) { + const leakPeriodTooltip = translateWithParameters('overview.leak_period_x', leakPeriodLabel); + + const displayLeak = measure.leak != null && metric.type !== 'RATING' && metric.type !== 'LEVEL'; + + return ( +
    +

    + {metric.name} +

    + + +
    + {measure.value != null && ( +
    + {formatMeasure(measure.value, metric.type)} +
    + )} + + {displayLeak && ( +
    + {formatLeak(measure.leak, metric)} +
    + )} + + {secondaryMeasure && secondaryMeasure.metric === 'ncloc_language_distribution' && ( +
    + +
    + )} + + {secondaryMeasure && secondaryMeasure.metric === 'function_complexity_distribution' && ( +
    + +
    + )} + + {secondaryMeasure && secondaryMeasure.metric === 'file_complexity_distribution' && ( +
    + +
    + )} +
    +
    +
    + ); +} 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/details/bubbleChart/BubbleChart.js b/server/sonar-web/src/main/js/apps/component-measures/details/bubbleChart/BubbleChart.js new file mode 100644 index 00000000000..a43e93f53e7 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/details/bubbleChart/BubbleChart.js @@ -0,0 +1,159 @@ +/* + * 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 './../../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 BubbleChart extends React.Component { + state = { + fetching: true, + files: [] + }; + + componentWillMount () { + const { metric, metrics } = this.props; + const conf = bubbles[metric.key]; + + this.xMetric = metrics.find(m => m.key === conf.x); + this.yMetric = metrics.find(m => m.key === conf.y); + this.sizeMetric = metrics.find(m => m.key === conf.size); + } + + componentDidMount () { + this.mounted = true; + this.fetchFiles(); + } + + componentDidUpdate (nextProps) { + if (nextProps.metric !== this.props.metric) { + this.fetchFiles(); + } + } + + componentWillUnmount () { + this.mounted = false; + } + + fetchFiles () { + const { component } = this.props; + const metrics = [this.xMetric.key, this.yMetric.key, this.sizeMetric.key]; + const options = { + s: 'metric', + metricSort: this.sizeMetric.key, + asc: false, + ps: BUBBLES_LIMIT + }; + + getComponentLeaves(component.key, metrics, options).then(r => { + const files = r.components.map(file => { + const measures = {}; + + file.measures.forEach(measure => { + measures[measure.metric] = measure.value; + }); + return { ...file, measures }; + }); + + this.setState({ + files, + fetching: false, + total: files.length + }); + }); + } + + getTooltip (component) { + const inner = [ + component.name, + `${this.xMetric.name}: ${formatMeasure(getMeasure(component, this.xMetric.key), this.xMetric.type)}`, + `${this.yMetric.name}: ${formatMeasure(getMeasure(component, this.yMetric.key), this.yMetric.type)}`, + `${this.sizeMetric.name}: ${formatMeasure(getMeasure(component, this.sizeMetric.key), this.sizeMetric.type)}` + ].join('
    '); + + return `
    ${inner}
    `; + } + + handleBubbleClick (id) { + Workspace.openComponent({ uuid: id }); + } + + renderBubbleChart () { + const items = this.state.files.map(file => { + return { + x: getMeasure(file, this.xMetric.key), + y: getMeasure(file, this.yMetric.key), + size: getMeasure(file, this.sizeMetric.key), + link: file.id, + tooltip: this.getTooltip(file) + }; + }); + + const formatXTick = (tick) => formatMeasure(tick, this.xMetric.type); + const formatYTick = (tick) => formatMeasure(tick, this.yMetric.type); + + return ( + + ); + } + + render () { + const { fetching } = this.state; + + if (fetching) { + return ( +
    +
    + +
    +
    + ); + } + + return ( +
    +
    + {this.renderBubbleChart()} +
    + +
    {this.xMetric.name}
    +
    {this.yMetric.name}
    +
    Size: {this.sizeMetric.name}
    +
    + ); + } +} 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/details/drilldown/ComponentsList.js b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ComponentsList.js new file mode 100644 index 00000000000..80793a9550f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ComponentsList.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 React from 'react'; + +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/details/drilldown/EmptyComponentsList.js b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/EmptyComponentsList.js new file mode 100644 index 00000000000..7b1a11d5791 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/EmptyComponentsList.js @@ -0,0 +1,32 @@ +/* + * 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 { translate } from '../../../../helpers/l10n'; + +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/details/drilldown/MeasureDrilldown.js b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/MeasureDrilldown.js new file mode 100644 index 00000000000..3df7565a694 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/MeasureDrilldown.js @@ -0,0 +1,97 @@ +/* + * 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 { Link } from 'react-router'; + +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'; + +export default class MeasureDrilldown extends React.Component { + render () { + const { children, component, metric, ...other } = this.props; + + const child = React.cloneElement(children, { ...other }); + + return ( +
    +
      +
    • + + + {translate('component_measures.tab.list')} + +
    • + +
    • + + + {translate('component_measures.tab.tree')} + +
    • + + {hasBubbleChart(metric.key) && ( +
    • + + + {translate('component_measures.tab.bubbles')} + +
    • + )} + + {hasTreemap(metric) && ( +
    • + + + {translate('component_measures.tab.treemap')} + +
    • + )} + + {hasHistory(metric.key) && ( +
    • + + + {translate('component_measures.tab.history')} + +
    • + )} +
    + + {child} +
    + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/TreeView.js b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/TreeView.js new file mode 100644 index 00000000000..1d10919bd2f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/TreeView.js @@ -0,0 +1,128 @@ +/* + * 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 ComponentsList from './ComponentsList'; +import ListHeader from './ListHeader'; +import SourceViewer from '../../../code/components/SourceViewer'; +import ListFooter from '../../../../components/shared/list-footer'; + +export default class TreeView 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(); + } + } + + 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, onStart } = this.props; + onStart(baseComponent, metric); + } + + changeSelected (selected) { + this.props.onSelect(selected); + } + + canDrilldown (component) { + return !['FIL', 'UTS'].includes(component.qualifier); + } + + handleClick (selected) { + if (this.canDrilldown(selected)) { + this.props.onDrilldown(selected); + } else { + this.storeScrollPosition(); + this.props.onSelect(selected); + } + } + + handleBreadcrumbClick (component) { + this.props.onUseBreadcrumbs(component, this.props.metric); + } + + render () { + const { components, breadcrumbs, metric, leakPeriod, selected, fetching, total } = this.props; + const { onSelectNext, onSelectPrevious, onFetchMore } = this.props; + + 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/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/details/history/MeasureHistory.js b/server/sonar-web/src/main/js/apps/component-measures/details/history/MeasureHistory.js new file mode 100644 index 00000000000..4958e72d75f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/details/history/MeasureHistory.js @@ -0,0 +1,177 @@ +/* + * 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 moment from 'moment'; +import React from 'react'; + +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 = 500; + +function parseValue (value, type) { + return type === 'RATING' && typeof value === 'string' ? value.charCodeAt(0) - 'A'.charCodeAt(0) + 1 : value; +} + +export default class MeasureHistory extends React.Component { + state = { + components: [], + selected: null, + fetching: true + }; + + componentDidMount () { + this.mounted = true; + this.fetchHistory(); + } + + componentDidUpdate (nextProps) { + if (nextProps.metric !== this.props.metric) { + this.fetchHistory(); + } + } + + componentWillUnmount () { + this.mounted = false; + } + + fetchHistory () { + const { metric } = this.props; + + Promise.all([ + this.fetchTimeMachineData(metric.key), + this.fetchEvents() + ]).then(responses => { + if (this.mounted) { + this.setState({ + snapshots: responses[0], + events: responses[1], + fetching: false + }); + } + }); + } + + fetchTimeMachineData (currentMetric, comparisonMetric) { + const metricsToRequest = [currentMetric]; + + if (comparisonMetric) { + metricsToRequest.push(comparisonMetric); + } + + return getTimeMachineData(this.props.component.key, metricsToRequest.join()).then(r => { + return r[0].cells.map(cell => { + return { + date: moment(cell.d).toDate(), + values: cell.v + }; + }); + }); + } + + fetchEvents () { + return getEvents(this.props.component.key, 'Version').then(r => { + const events = r.map(event => { + return { version: event.n, date: moment(event.dt).toDate() }; + }); + + return _.sortBy(events, 'date'); + }); + } + + renderLineChart (snapshots, metric, index) { + if (!metric) { + return null; + } + + if (snapshots.length < 2) { + return this.renderWhenNoHistoricalData(); + } + + const data = snapshots.map(snapshot => { + return { + x: snapshot.date, + y: parseValue(snapshot.values[index], metric.type) + }; + }); + + const formatValue = (value) => formatMeasure(value, metric.type); + const formatYTick = (tick) => formatMeasure(tick, getShortType(metric.type)); + + return ( +
    + +
    + ); + } + + renderLineCharts () { + const { metric } = this.props; + + return ( +
    + {this.renderLineChart(this.state.snapshots, metric, 0)} + {this.renderLineChart(this.state.snapshots, this.state.comparisonMetric, 1)} +
    + ); + } + + render () { + const { fetching, snapshots } = this.state; + + if (fetching) { + return ( +
    +
    + +
    +
    + ); + } + + if (!snapshots || snapshots.length < 2) { + return ( +
    +
    + There is no historical data. +
    +
    + ); + } + + return ( +
    + {this.renderLineCharts()} +
    + ); + } +} 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/details/treemap/MeasureTreemap.js b/server/sonar-web/src/main/js/apps/component-measures/details/treemap/MeasureTreemap.js new file mode 100644 index 00000000000..92615167cc3 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/details/treemap/MeasureTreemap.js @@ -0,0 +1,226 @@ +/* + * 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 './../../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 = 500; + +export default class MeasureTreemap extends React.Component { + state = { + fetching: true, + components: [], + breadcrumbs: [] + }; + + componentDidMount () { + const { component } = this.props; + + this.mounted = true; + this.fetchComponents(component.key); + } + + componentDidUpdate (nextProps) { + if (nextProps.metric !== this.props.metric) { + this.fetchComponents(this.props.component.key); + } + } + + componentWillUnmount () { + this.mounted = false; + } + + fetchComponents (componentKey) { + const { metric } = this.props; + const metrics = ['ncloc', metric.key]; + const options = { + s: 'metric', + metricSort: 'ncloc', + asc: false + }; + + return getChildren(componentKey, metrics, options).then(r => { + const components = r.map(component => { + const measures = {}; + const key = component.refKey || component.key; + + component.measures.forEach(measure => { + const shouldUseLeak = measure.metric.indexOf('new_') === 0; + measures[measure.metric] = shouldUseLeak ? getLeakValue(measure) : measure.value; + }); + return { ...component, measures, key }; + }); + + this.setState({ + components, + fetching: false + }); + }); + } + + getTooltip (component) { + const { metric } = this.props; + + let inner = [ + component.name, + `${translate('metric.ncloc.name')}: ${formatMeasure(component.measures['ncloc'], 'INT')}` + ]; + + const colorMeasure = component.measures[metric.key]; + const formatted = colorMeasure != null ? formatMeasure(colorMeasure, metric.type) : '—'; + + inner.push(`${metric.name}: ${formatted}`); + inner = inner.join('
    '); + + return `
    ${inner}
    `; + } + + getPercentColorScale (metric) { + const color = d3.scale.linear().domain([0, 25, 50, 75, 100]); + color.range(metric.direction === 1 ? + ['#ee0000', '#f77700', '#ffee00', '#80cc00', '#00aa00'] : + ['#00aa00', '#80cc00', '#ffee00', '#f77700', '#ee0000']); + return color; + } + + getRatingColorScale () { + return d3.scale.linear() + .domain([1, 2, 3, 4, 5]) + .range(['#00aa00', '#80cc00', '#ffee00', '#f77700', '#ee0000']); + } + + getLevelColorScale () { + return d3.scale.ordinal() + .domain(['ERROR', 'WARN', 'OK', 'NONE']) + .range(['#d4333f', '#ff9900', '#85bb43', '##b4b4b4']); + } + + getScale () { + const { metric } = this.props; + + if (metric.type === 'LEVEL') { + return this.getLevelColorScale(); + } + if (metric.type === 'RATING') { + return this.getRatingColorScale(); + } + return this.getPercentColorScale(metric); + } + + handleRectangleClick (node) { + const isFile = node.qualifier === 'FIL' || node.qualifier === 'UTS'; + + if (isFile) { + return Workspace.openComponent({ uuid: node.id }); + } + + this.fetchComponents(node.key).then(() => { + let nextBreadcrumbs = [...this.state.breadcrumbs]; + const index = this.state.breadcrumbs.findIndex(b => b.key === node.key); + + if (index !== -1) { + nextBreadcrumbs = nextBreadcrumbs.slice(0, index); + } + + nextBreadcrumbs = [...nextBreadcrumbs, { + key: node.key, + name: node.name, + qualifier: node.qualifier + }]; + + this.setState({ breadcrumbs: nextBreadcrumbs }); + }); + } + + handleReset () { + const { component } = this.props; + this.fetchComponents(component.key).then(() => { + this.setState({ breadcrumbs: [] }); + }); + } + + renderTreemap () { + const { metric } = this.props; + const colorScale = this.getScale(); + const items = this.state.components + .filter(component => component.measures['ncloc']) + .map(component => { + const colorMeasure = component.measures[metric.key]; + + return { + id: component.id, + key: component.key, + name: component.name, + qualifier: component.qualifier, + size: component.measures['ncloc'], + color: colorMeasure != null ? colorScale(colorMeasure) : '#777', + tooltip: this.getTooltip(component), + label: component.name, + link: getComponentUrl(component.key) + }; + }); + + return ( + true} + onRectangleClick={this.handleRectangleClick.bind(this)} + onReset={this.handleReset.bind(this)}/> + ); + } + + render () { + const { metric } = this.props; + const { fetching } = this.state; + + if (fetching) { + return ( +
    +
    + +
    +
    + ); + } + + return ( +
    +
      +
    • + {translateWithParameters('component_measures.legend.color_x', metric.name)} +
    • +
    • + {translateWithParameters('component_measures.legend.size_x', translate('metric.ncloc.name'))} +
    • +
    + {this.renderTreemap()} +
    + ); + } +} 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/home/AllMeasuresDomain.js b/server/sonar-web/src/main/js/apps/component-measures/home/AllMeasuresDomain.js new file mode 100644 index 00000000000..67fe53d31d7 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/home/AllMeasuresDomain.js @@ -0,0 +1,83 @@ +/* + * 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 sortBy from '../../../../../../node_modules/lodash/sortBy'; +import partition from '../../../../../../node_modules/lodash/partition'; +import React from 'react'; +import { Link } from 'react-router'; + +import domains from '../config/domains'; +import { formatLeak } from '../utils'; +import { formatMeasure } from '../../../helpers/measures'; +import { translateWithParameters } from '../../../helpers/l10n'; + +export default function AllMeasuresDomain ({ domain, component, displayLeakHeader, leakPeriodLabel }) { + const hasLeak = !!leakPeriodLabel; + const { measures } = domain; + const knownMetrics = domains[domain.name] || []; + + const [knownMeasures, otherMeasures] = + partition(measures, measure => knownMetrics.indexOf(measure.metric.key) !== -1); + + const finalMeasures = [ + ...sortBy(knownMeasures, measure => knownMetrics.indexOf(measure.metric.key)), + ...sortBy(otherMeasures, measure => measure.metric.name) + ]; + + return ( +
  • +
    +

    {domain.name}

    + {displayLeakHeader && hasLeak && ( +
    + {translateWithParameters('overview.leak_period_x', leakPeriodLabel)} +
    + )} +
    + +
      + {finalMeasures.map(measure => ( +
    • + +
      + + {measure.metric.name} + +
      +
      + {measure.value != null && ( + + {formatMeasure(measure.value, measure.metric.type)} + + )} +
      + {hasLeak && measure.leak != null && ( +
      + + {formatLeak(measure.leak, measure.metric)} + +
      + )} + +
    • + ))} +
    +
  • + ); +} 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; -- cgit v1.2.3