summaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js
diff options
context:
space:
mode:
authorStas Vilchik <vilchiks@gmail.com>2016-03-21 18:28:05 +0100
committerStas Vilchik <vilchiks@gmail.com>2016-03-24 09:16:41 +0100
commitcc9c506efd50786649658baa0d243654c59c512e (patch)
treee36cc405f4e43fbb470312d911ddbf3eb1a85d6c /server/sonar-web/src/main/js
parent05f9e8ef050e34b9a53678f3cab0a0c4e2caca1e (diff)
downloadsonarqube-cc9c506efd50786649658baa0d243654c59c512e.tar.gz
sonarqube-cc9c506efd50786649658baa0d243654c59c512e.zip
SONAR-7402 improve list and tree views on measures page
Diffstat (limited to 'server/sonar-web/src/main/js')
-rw-r--r--server/sonar-web/src/main/js/api/components.js20
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/app.js55
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/app/App.js (renamed from server/sonar-web/src/main/js/apps/component-measures/components/ComponentMeasuresApp.js)49
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/app/AppContainer.js40
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/app/actions.js53
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/app/reducer.js34
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/AllMeasures.js110
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/MeasureDetails.js149
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/MeasureDetailsSeeAlso.js160
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/MeasureDrilldownComponents.js77
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/MeasureDrilldownList.js129
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/MeasureDrilldownTree.js139
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/config/bubbles.js (renamed from server/sonar-web/src/main/js/apps/component-measures/bubbles.js)0
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/details/MeasureDetails.js101
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/details/MeasureDetailsContainer.js44
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/details/MeasureDetailsHeader.js (renamed from server/sonar-web/src/main/js/apps/component-measures/components/MeasureDetailsHeader.js)30
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/details/actions.js77
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/details/bubbleChart/BubbleChart.js (renamed from server/sonar-web/src/main/js/apps/component-measures/components/MeasureBubbleChart.js)43
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/details/bubbleChart/MeasureBubbleChartContainer.js39
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/details/drilldown/Breadcrumb.js62
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/details/drilldown/Breadcrumbs.js37
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ComponentCell.js78
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ComponentsList.js (renamed from server/sonar-web/src/main/js/apps/component-measures/components/IconUp.js)36
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ComponentsListRow.js44
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/details/drilldown/EmptyComponentsList.js (renamed from server/sonar-web/src/main/js/apps/component-measures/components/MeasureDrilldownEmpty.js)10
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ListHeader.js68
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ListView.js130
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ListViewContainer.js54
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/details/drilldown/MeasureCell.js39
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/details/drilldown/MeasureDrilldown.js (renamed from server/sonar-web/src/main/js/apps/component-measures/components/MeasureDrilldown.js)65
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/details/drilldown/TreeView.js128
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/details/drilldown/TreeViewContainer.js65
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/details/history/MeasureHistory.js (renamed from server/sonar-web/src/main/js/apps/component-measures/components/MeasureHistory.js)21
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/details/history/MeasureHistoryContainer.js38
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/details/reducer.js42
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/details/treemap/MeasureTreemap.js (renamed from server/sonar-web/src/main/js/apps/component-measures/components/MeasureTreemap.js)39
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/details/treemap/MeasureTreemapContainer.js38
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/home/AllMeasures.js64
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/home/AllMeasuresContainer.js46
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/home/AllMeasuresDomain.js (renamed from server/sonar-web/src/main/js/apps/component-measures/components/AllMeasuresDomain.js)4
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/home/actions.js59
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/home/reducer.js34
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/store/configureStore.js41
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/store/listViewActions.js143
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/store/listViewReducer.js37
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/store/statusActions.js29
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/store/statusReducer.js36
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/store/treeViewActions.js244
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/store/treeViewReducer.js42
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/styles.css99
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/utils.js11
-rw-r--r--server/sonar-web/src/main/js/components/shared/list-footer.js1
-rw-r--r--server/sonar-web/src/main/js/components/source-viewer/main.js1
-rw-r--r--server/sonar-web/src/main/js/components/store/configureStore.js4
-rw-r--r--server/sonar-web/src/main/js/helpers/path.js13
-rwxr-xr-xserver/sonar-web/src/main/js/libs/third-party/jquery-ui.js2
56 files changed, 2203 insertions, 1050 deletions
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) => (
- <ComponentMeasuresApp {...props} component={options.component}/>
- );
+ const store = configureStore({
+ app: { component: options.component }
+ });
const handleRouteUpdate = () => {
window.scrollTo(0, 0);
};
render((
- <Router history={history} onUpdate={handleRouteUpdate}>
- <Redirect from="/index" to="/"/>
+ <Provider store={store}>
+ <Router history={history} onUpdate={handleRouteUpdate}>
+ <Redirect from="/index" to="/"/>
- <Route path="/" component={Container}>
- <IndexRoute component={AllMeasuresList}/>
- <Route path=":metricKey" component={MeasureDetails}>
- <IndexRedirect to="tree"/>
- <Route path="tree" component={MeasureDrilldownTree}/>
- <Route path="list" component={MeasureDrilldownList}/>
- <Route path="history" component={MeasureHistory} onEnter={checkHistoryExistence}/>
- <Route path="bubbles" component={MeasureBubbleChart} onEnter={checkBubbleChartExistence}/>
- <Route path="treemap" component={MeasureTreemap}/>
+ <Route path="/" component={AppContainer}>
+ <IndexRoute component={AllMeasuresContainer}/>
+ <Route path=":metricKey" component={MeasureDetailsContainer}>
+ <IndexRedirect to="list"/>
+ <Route path="list" component={ListViewContainer}/>
+ <Route path="tree" component={TreeViewContainer}/>
+ <Route path="history" component={MeasureHistoryContainer} onEnter={checkHistoryExistence}/>
+ <Route path="bubbles" component={MeasureBubbleChartContainer} onEnter={checkBubbleChartExistence}/>
+ <Route path="treemap" component={MeasureTreemapContainer}/>
+ </Route>
</Route>
- </Route>
- <Redirect from="*" to="/"/>
- </Router>
+ <Redirect from="*" to="/"/>
+ </Router>
+ </Provider>
), el);
});
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/ComponentMeasuresApp.js b/server/sonar-web/src/main/js/apps/component-measures/app/App.js
index c71d8c9fca7..fa23544cd91 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/components/ComponentMeasuresApp.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/app/App.js
@@ -19,57 +19,24 @@
*/
import React from 'react';
-import Spinner from './Spinner';
-import { getMetrics } from '../../../api/metrics';
-
-export default class ComponentMeasuresApp extends React.Component {
- state = {
- fetching: true,
- metrics: []
- };
-
- getChildContext () {
- return {
- component: this.props.component,
- metrics: this.state.metrics
- };
- }
+import Spinner from './../components/Spinner';
+export default class App extends React.Component {
componentDidMount () {
- this.mounted = true;
- this.fetchMetrics();
- }
-
- componentWillUnmount () {
- this.mounted = false;
- }
-
- fetchMetrics () {
- getMetrics().then(metrics => {
- if (this.mounted) {
- this.setState({ metrics, fetching: false });
- }
- });
+ this.props.fetchMetrics();
}
render () {
- const { fetching, metrics } = this.state;
+ const { metrics } = this.props;
- if (fetching) {
+ if (metrics == null) {
return <Spinner/>;
}
- const child = React.cloneElement(this.props.children, { metrics });
-
return (
- <div id="component-measures">
- {child}
- </div>
+ <main id="component-measures">
+ {this.props.children}
+ </main>
);
}
}
-
-ComponentMeasuresApp.childContextTypes = {
- component: React.PropTypes.object,
- metrics: React.PropTypes.array
-};
diff --git a/server/sonar-web/src/main/js/apps/component-measures/app/AppContainer.js b/server/sonar-web/src/main/js/apps/component-measures/app/AppContainer.js
new file mode 100644
index 00000000000..2bd53a0ab19
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/app/AppContainer.js
@@ -0,0 +1,40 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { connect } from 'react-redux';
+
+import App from './App';
+import { fetchMetrics } from './actions';
+
+const mapStateToProps = state => {
+ return {
+ metrics: state.app.metrics
+ };
+};
+
+const mapDispatchToProps = dispatch => {
+ return {
+ fetchMetrics: () => dispatch(fetchMetrics())
+ };
+};
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(App);
diff --git a/server/sonar-web/src/main/js/apps/component-measures/app/actions.js b/server/sonar-web/src/main/js/apps/component-measures/app/actions.js
new file mode 100644
index 00000000000..e38934a9bee
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/app/actions.js
@@ -0,0 +1,53 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { getMetrics } from '../../../api/metrics';
+
+/*
+ * Actions
+ */
+
+export const DISPLAY_HOME = 'app/DISPLAY_HOME';
+export const RECEIVE_METRICS = 'app/RECEIVE_METRICS';
+
+
+/*
+ * Action Creators
+ */
+
+export function displayHome () {
+ return { type: DISPLAY_HOME };
+}
+
+function receiveMetrics (metrics) {
+ return { type: RECEIVE_METRICS, metrics };
+}
+
+
+/*
+ * Workflow
+ */
+
+export function fetchMetrics () {
+ return dispatch => {
+ getMetrics().then(metrics => {
+ dispatch(receiveMetrics(metrics));
+ });
+ };
+}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/app/reducer.js b/server/sonar-web/src/main/js/apps/component-measures/app/reducer.js
new file mode 100644
index 00000000000..2484e2d0563
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/app/reducer.js
@@ -0,0 +1,34 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { RECEIVE_METRICS } from './actions';
+
+const initialState = {
+ metrics: undefined
+};
+
+export default function appReducer (state = initialState, action = {}) {
+ switch (action.type) {
+ case RECEIVE_METRICS:
+ return { ...state, metrics: action.metrics };
+ default:
+ return state;
+ }
+}
+
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/AllMeasures.js b/server/sonar-web/src/main/js/apps/component-measures/components/AllMeasures.js
deleted file mode 100644
index 69d1709ef86..00000000000
--- a/server/sonar-web/src/main/js/apps/component-measures/components/AllMeasures.js
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2016 SonarSource SA
- * mailto:contact AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import _ from 'underscore';
-import React from 'react';
-
-import Spinner from './Spinner';
-import AllMeasuresDomain from './AllMeasuresDomain';
-import { getLeakValue } from '../utils';
-import { getMeasuresAndMeta } from '../../../api/measures';
-import { getLeakPeriod, getLeakPeriodLabel } from '../../../helpers/periods';
-
-export default class AllMeasures extends React.Component {
- state = {
- fetching: true,
- measures: []
- };
-
- componentDidMount () {
- this.mounted = true;
- this.fetchMeasures();
- }
-
- componentWillUnmount () {
- this.mounted = false;
- }
-
- fetchMeasures () {
- const { component } = this.context;
- const { metrics } = this.props;
- const metricKeys = metrics
- .filter(metric => !metric.hidden)
- .filter(metric => metric.type !== 'DATA' && metric.type !== 'DISTRIB')
- .map(metric => metric.key);
-
- getMeasuresAndMeta(component.key, metricKeys, { additionalFields: 'periods' }).then(r => {
- if (this.mounted) {
- const leakPeriod = getLeakPeriod(r.periods);
- const measures = r.component.measures
- .map(measure => {
- const metric = metrics.find(metric => metric.key === measure.metric);
- const leak = getLeakValue(measure);
- return { ...measure, metric, leak };
- })
- .filter(measure => {
- const hasValue = measure.value != null;
- const hasLeakValue = !!leakPeriod && measure.leak != null;
- return hasValue || hasLeakValue;
- });
-
- this.setState({
- measures,
- periods: r.periods,
- fetching: false
- });
- }
- });
- }
-
- render () {
- const { fetching, measures, periods } = this.state;
-
- if (fetching) {
- return <Spinner/>;
- }
-
- 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 (
- <ul className="measures-domains">
- {domains.map((domain, index) => (
- <AllMeasuresDomain
- key={domain.name}
- domain={domain}
- component={component}
- displayLeakHeader={index === 0}
- leakPeriodLabel={leakPeriodLabel}/>
- ))}
- </ul>
- );
- }
-}
-
-AllMeasures.contextTypes = {
- component: React.PropTypes.object
-};
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDetails.js b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDetails.js
deleted file mode 100644
index e4e738870ed..00000000000
--- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDetails.js
+++ /dev/null
@@ -1,149 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2016 SonarSource SA
- * mailto:contact AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import React from 'react';
-
-import Spinner from './Spinner';
-import MeasureDetailsHeader from './MeasureDetailsHeader';
-import MeasureDrilldown from './MeasureDrilldown';
-
-import { enhanceWithLeak } from '../utils';
-import { getMeasuresAndMeta } from '../../../api/measures';
-import { getLeakPeriod, getPeriodDate, getPeriodLabel } from '../../../helpers/periods';
-
-export default class MeasureDetails extends React.Component {
- state = {
- metricSelectOpen: false
- };
-
- componentWillMount () {
- const { metrics } = this.props;
- const { metricKey } = this.props.params;
- const metric = metrics.find(metric => metric.key === metricKey);
-
- if (!metric) {
- const { router, component } = this.context;
-
- router.replace({
- pathname: '/',
- query: { id: component.key }
- });
- }
- }
-
- componentDidMount () {
- this.mounted = true;
- this.fetchMeasure();
- }
-
- componentDidUpdate (nextProps, nextState, nextContext) {
- if ((nextProps.params.metricKey !== this.props.params.metricKey) ||
- (nextContext.component !== this.context.component)) {
- this.fetchMeasure();
- }
- }
-
- componentWillUnmount () {
- this.mounted = false;
- }
-
- fetchMeasure () {
- const { metricKey } = this.props.params;
- const { component } = this.context;
- const metrics = [metricKey];
-
- if (metricKey === 'ncloc') {
- metrics.push('ncloc_language_distribution');
- }
-
- if (metricKey === 'function_complexity') {
- metrics.push('function_complexity_distribution');
- }
-
- if (metricKey === 'file_complexity') {
- metrics.push('file_complexity_distribution');
- }
-
- getMeasuresAndMeta(
- component.key,
- metrics,
- { additionalFields: 'periods' }
- ).then(r => {
- if (this.mounted) {
- const measures = enhanceWithLeak(r.component.measures);
- const measure = measures.find(measure => measure.metric === metricKey);
- const secondaryMeasure = measures.find(measure => measure.metric !== metricKey);
-
- this.setState({
- measure,
- secondaryMeasure,
- periods: r.periods,
- metricSelectOpen: false
- });
- }
- });
- }
-
- handleMetricClick() {
- this.setState({ metricSelectOpen: !this.state.metricSelectOpen });
- }
-
- render () {
- const { metrics } = this.props;
- const { metricKey } = this.props.params;
- const { measure, secondaryMeasure, periods, metricSelectOpen } = this.state;
- const metric = metrics.find(metric => metric.key === metricKey);
-
- if (!measure) {
- return <Spinner/>;
- }
-
- const { tab } = this.props.params;
- const leakPeriod = getLeakPeriod(periods);
- const leakPeriodLabel = getPeriodLabel(leakPeriod);
- const leakPeriodDate = getPeriodDate(leakPeriod);
-
- return (
- <div className="measure-details">
- <MeasureDetailsHeader
- measure={measure}
- metric={metric}
- secondaryMeasure={secondaryMeasure}
- leakPeriodLabel={leakPeriodLabel}
- metricSelectOpen={metricSelectOpen}
- onMetricClick={this.handleMetricClick.bind(this)}/>
-
- {measure && (
- <MeasureDrilldown
- metric={metric}
- tab={tab}
- leakPeriod={leakPeriod}
- leakPeriodDate={leakPeriodDate}>
- {this.props.children}
- </MeasureDrilldown>
- )}
- </div>
- );
- }
-}
-
-MeasureDetails.contextTypes = {
- component: React.PropTypes.object,
- router: React.PropTypes.object
-};
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDetailsSeeAlso.js b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDetailsSeeAlso.js
deleted file mode 100644
index 513473fc2f9..00000000000
--- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDetailsSeeAlso.js
+++ /dev/null
@@ -1,160 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2016 SonarSource SA
- * mailto:contact AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import _ from 'underscore';
-import React from 'react';
-import { Link } from 'react-router';
-
-import Spinner from './Spinner';
-import { getLeakValue, formatLeak } from '../utils';
-import { getMeasuresAndMeta } from '../../../api/measures';
-import { getLeakPeriod } from '../../../helpers/periods';
-import { formatMeasure } from '../../../helpers/measures';
-
-export default class MeasureDetailsSeeAlso extends React.Component {
- constructor (props) {
- super(props);
- this.state = {
- measures: [],
- fetching: true
- };
- this.handleKeyDown = this.handleKeyDown.bind(this);
- this.handleClick = this.handleClick.bind(this);
- }
-
- componentDidMount () {
- this.mounted = true;
- window.addEventListener('keydown', this.handleKeyDown);
- window.addEventListener('click', this.handleClick);
- this.fetchMeasure();
- }
-
- componentDidUpdate (nextProps, nextState, nextContext) {
- if ((nextProps.metric !== this.props.metric) ||
- (nextContext.component !== this.context.component)) {
- this.fetchMeasure();
- }
- }
-
- componentWillUnmount () {
- this.mounted = false;
- window.removeEventListener('keydown', this.handleKeyDown);
- window.removeEventListener('click', this.handleClick);
- }
-
- handleKeyDown (e) {
- // escape
- if (e.keyCode === 27) {
- this.props.onClose();
- }
- }
-
- handleClick () {
- this.props.onClose();
- }
-
- fetchMeasure () {
- const { metric } = this.props;
- const { component, metrics } = this.context;
- const sameDomainMetrics = metrics.filter(candidate => candidate.domain === metric.domain);
- const metricKeys = sameDomainMetrics
- .filter(m => !m.hidden)
- .filter(m => m.type !== 'DATA' && m.type !== 'DISTRIB')
- .filter(m => m.key !== metric.key)
- .map(m => m.key);
-
- getMeasuresAndMeta(component.key, metricKeys, { additionalFields: 'periods' }).then(r => {
- if (this.mounted) {
- const leakPeriod = getLeakPeriod(r.periods);
- const measures = r.component.measures
- .map(measure => {
- const metric = metrics.find(metric => metric.key === measure.metric);
- const leak = getLeakValue(measure);
- return { ...measure, metric, leak };
- })
- .filter(measure => {
- const hasValue = measure.value != null;
- const hasLeakValue = !!leakPeriod && measure.leak != null;
- return hasValue || hasLeakValue;
- });
- const sortedMeasures = _.sortBy(measures, measure => measure.metric.name);
-
- this.setState({
- measures: sortedMeasures,
- periods: r.periods,
- fetching: false
- });
- }
- });
- }
-
- render () {
- const { measures, fetching, periods } = this.state;
- const { component } = this.context;
-
- if (fetching) {
- return (
- <div className="measure-details-see-also">
- <Spinner/>
- </div>
- );
- }
-
- const leakPeriod = getLeakPeriod(periods);
- const hasLeak = !!leakPeriod;
-
- return (
- <div className="measure-details-see-also">
- <ul className="domain-measures">
- {measures.map(measure => (
- <li key={measure.metric.key}>
- <Link to={{ pathname: measure.metric.key, query: { id: component.key } }}>
- <div className="domain-measures-name">
- <span>{measure.metric.name}</span>
- </div>
- <div className="domain-measures-value">
- {measure.value != null && (
- <span>
- {formatMeasure(measure.value, measure.metric.type)}
- </span>
- )}
- </div>
- {hasLeak && (
- <div className="domain-measures-value domain-measures-leak">
- {measure.leak != null && (
- <span>
- {formatLeak(measure.leak, measure.metric)}
- </span>
- )}
- </div>
- )}
- </Link>
- </li>
- ))}
- </ul>
- </div>
- );
- }
-}
-
-MeasureDetailsSeeAlso.contextTypes = {
- component: React.PropTypes.object,
- metrics: React.PropTypes.array
-};
-
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDrilldownComponents.js b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDrilldownComponents.js
deleted file mode 100644
index a020e83f0db..00000000000
--- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDrilldownComponents.js
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2016 SonarSource SA
- * mailto:contact AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import React from 'react';
-
-import MeasureDrilldownEmpty from './MeasureDrilldownEmpty';
-import IconUp from './IconUp';
-import QualifierIcon from '../../../components/shared/qualifier-icon';
-import { formatMeasure } from '../../../helpers/measures';
-import { formatLeak } from '../utils';
-
-export default function MeasureDrilldownComponents ({ components, selected, parent, metric, onClick }) {
- const handleClick = (component, e) => {
- e.preventDefault();
- e.target.blur();
- onClick(component);
- };
-
- return (
- <ul className="measure-details-components">
- {parent && (
- <li key={parent.id} className="measure-details-components-parent">
- <a href="#" onClick={handleClick.bind(this, parent)}>
- <div className="measure-details-component-name">
- <IconUp/>&nbsp;..
- </div>
- </a>
- </li>
- )}
-
- {!components.length && <MeasureDrilldownEmpty/>}
-
- {components.map(component => (
- <li key={component.id}>
- <a
- className={component === selected ? 'selected' : undefined}
- href="#"
- onClick={handleClick.bind(this, component)}>
-
- <div className="measure-details-component-name">
- <QualifierIcon qualifier={component.qualifier}/>
- &nbsp;
- <span>{component.name}</span>
- </div>
-
- <div className="measure-details-component-value">
- {component.value != null ? (
- formatMeasure(component.value, metric.type)
- ) : (
- component.leak != null && (
- formatLeak(component.leak, metric)
- )
- )}
- </div>
-
- </a>
- </li>
- ))}
- </ul>
- );
-}
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 <Spinner/>;
- }
-
- const sourceViewerPeriod = metric.key.indexOf('new_') === 0 && !!leakPeriod ? leakPeriod : null;
-
- return (
- <div className="measure-details-plain-list">
- <MeasureDrilldownComponents
- components={components}
- selected={selected}
- metric={metric}
- onClick={this.handleFileClick.bind(this)}/>
-
- {selected && (
- <div className="measure-details-viewer">
- <SourceViewer component={selected} period={sourceViewerPeriod}/>
- </div>
- )}
- </div>
- );
- }
-}
-
-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 <Spinner/>;
- }
-
- const sourceViewerPeriod = metric.key.indexOf('new_') === 0 && !!leakPeriod ? leakPeriod : null;
-
- return (
- <div className="measure-details-tree">
- <MeasureDrilldownComponents
- components={components}
- selected={selected}
- parent={parent}
- metric={metric}
- onClick={this.handleFileClick.bind(this)}/>
-
- {selected && (
- <div className="measure-details-viewer">
- <SourceViewer component={selected} period={sourceViewerPeriod}/>
- </div>
- )}
- </div>
- );
- }
-}
-
-MeasureDrilldownTree.contextTypes = {
- component: React.PropTypes.object
-};
diff --git a/server/sonar-web/src/main/js/apps/component-measures/bubbles.js b/server/sonar-web/src/main/js/apps/component-measures/config/bubbles.js
index bfbaed418b7..bfbaed418b7 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/bubbles.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/config/bubbles.js
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 <Spinner/>;
+ }
+
+ const { tab } = this.props.params;
+ const leakPeriod = getLeakPeriod(periods);
+ const leakPeriodLabel = getPeriodLabel(leakPeriod);
+ const leakPeriodDate = getPeriodDate(leakPeriod);
+
+ return (
+ <section id="component-measures-details" className="page page-container page-limited">
+ <IndexLink
+ to={{ pathname: '/', query: { id: component.key } }}
+ id="component-measures-back-to-all-measures"
+ className="small text-muted">
+ {translate('component_measures.back_to_all_measures')}
+ </IndexLink>
+
+ <MeasureDetailsHeader
+ measure={measure}
+ metric={metric}
+ secondaryMeasure={secondaryMeasure}
+ leakPeriodLabel={leakPeriodLabel}/>
+
+ {measure && (
+ <MeasureDrilldown
+ component={component}
+ metric={metric}
+ tab={tab}
+ leakPeriod={leakPeriod}
+ leakPeriodDate={leakPeriodDate}>
+ {children}
+ </MeasureDrilldown>
+ )}
+ </section>
+ );
+ }
+}
+
+MeasureDetails.contextTypes = {
+ router: React.PropTypes.object
+};
diff --git a/server/sonar-web/src/main/js/apps/component-measures/details/MeasureDetailsContainer.js b/server/sonar-web/src/main/js/apps/component-measures/details/MeasureDetailsContainer.js
new file mode 100644
index 00000000000..80000e23ff3
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/details/MeasureDetailsContainer.js
@@ -0,0 +1,44 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { connect } from 'react-redux';
+
+import MeasureDetails from './MeasureDetails';
+import { fetchMeasure } from './actions';
+
+const mapStateToProps = state => {
+ return {
+ component: state.app.component,
+ metrics: state.app.metrics,
+ metric: state.details.metric,
+ measure: state.details.measure,
+ periods: state.details.periods
+ };
+};
+
+const mapDispatchToProps = dispatch => {
+ return {
+ fetchMeasure: metricKey => dispatch(fetchMeasure(metricKey))
+ };
+};
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(MeasureDetails);
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDetailsHeader.js b/server/sonar-web/src/main/js/apps/component-measures/details/MeasureDetailsHeader.js
index 59a9a3c9eee..13e3080a352 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDetailsHeader.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/details/MeasureDetailsHeader.js
@@ -19,24 +19,14 @@
*/
import React from 'react';
-import MeasureDetailsSeeAlso from './MeasureDetailsSeeAlso';
-import LanguageDistribution from './LanguageDistribution';
+import LanguageDistribution from './../components/LanguageDistribution';
import { ComplexityDistribution } from '../../overview/components/complexity-distribution';
import { formatLeak } from '../utils';
import { formatMeasure } from '../../../helpers/measures';
import { translateWithParameters } from '../../../helpers/l10n';
import { TooltipsContainer } from '../../../components/mixins/tooltips-mixin';
-export default function MeasureDetailsHeader (
- {
- measure,
- metric,
- secondaryMeasure,
- leakPeriodLabel,
- metricSelectOpen,
- onMetricClick
- }
-) {
+export default function MeasureDetailsHeader ({ measure, metric, secondaryMeasure, leakPeriodLabel }) {
const leakPeriodTooltip = translateWithParameters('overview.leak_period_x', leakPeriodLabel);
const displayLeak = measure.leak != null && metric.type !== 'RATING' && metric.type !== 'LEVEL';
@@ -44,17 +34,7 @@ export default function MeasureDetailsHeader (
return (
<header className="measure-details-header">
<h2 className="measure-details-metric">
- <a
- href="#"
- onClick={e => {
- e.stopPropagation();
- e.preventDefault();
- onMetricClick();
- }}>
- {metric.name}
- &nbsp;
- <i className="icon-dropdown"/>
- </a>
+ {metric.name}
</h2>
<TooltipsContainer options={{ html: false }}>
@@ -93,10 +73,6 @@ export default function MeasureDetailsHeader (
)}
</div>
</TooltipsContainer>
-
- {metricSelectOpen && (
- <MeasureDetailsSeeAlso metric={metric} onClose={onMetricClick}/>
- )}
</header>
);
}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/details/actions.js b/server/sonar-web/src/main/js/apps/component-measures/details/actions.js
new file mode 100644
index 00000000000..8992f530580
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/details/actions.js
@@ -0,0 +1,77 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { getMeasuresAndMeta } from '../../../api/measures';
+import { enhanceWithLeak } from '../utils';
+
+/*
+ * Actions
+ */
+
+export const REQUEST_MEASURE = 'details/REQUEST_MEASURE';
+export const RECEIVE_MEASURE = 'details/RECEIVE_MEASURE';
+
+
+/*
+ * Action Creators
+ */
+
+function requestMeasure (metric) {
+ return { type: REQUEST_MEASURE, metric };
+}
+
+function receiveMeasure (measure, secondaryMeasure, periods) {
+ return { type: RECEIVE_MEASURE, measure, secondaryMeasure, periods };
+}
+
+
+/*
+ * Workflow
+ */
+
+export function fetchMeasure (metricKey) {
+ return (dispatch, getState) => {
+ const { component, metrics } = getState().app;
+ const metricsToRequest = [metricKey];
+
+ if (metricKey === 'ncloc') {
+ metricsToRequest.push('ncloc_language_distribution');
+ }
+ if (metricKey === 'function_complexity') {
+ metricsToRequest.push('function_complexity_distribution');
+ }
+ if (metricKey === 'file_complexity') {
+ metricsToRequest.push('file_complexity_distribution');
+ }
+
+ const metric = metrics.find(m => m.key === metricKey);
+ dispatch(requestMeasure(metric));
+
+ getMeasuresAndMeta(
+ component.key,
+ metricsToRequest,
+ { additionalFields: 'periods' }
+ ).then(r => {
+ const measures = enhanceWithLeak(r.component.measures);
+ const measure = measures.find(m => m.metric === metricKey);
+ const secondaryMeasure = measures.find(m => m.metric !== metricKey);
+ dispatch(receiveMeasure(measure, secondaryMeasure, r.periods));
+ });
+ };
+}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureBubbleChart.js b/server/sonar-web/src/main/js/apps/component-measures/details/bubbleChart/BubbleChart.js
index 5654e20cd14..a43e93f53e7 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureBubbleChart.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/details/bubbleChart/BubbleChart.js
@@ -19,29 +19,28 @@
*/
import React from 'react';
-import Spinner from './Spinner';
-import { BubbleChart } from '../../../components/charts/bubble-chart';
-import bubbles from '../bubbles';
-import { getFiles } from '../../../api/components';
-import { formatMeasure } from '../../../helpers/measures';
-import Workspace from '../../../components/workspace/main';
-
-const HEIGHT = 360;
+import Spinner from './../../components/Spinner';
+import { BubbleChart as OriginalBubbleChart } from '../../../../components/charts/bubble-chart';
+import bubbles from '../../config/bubbles';
+import { getComponentLeaves } from '../../../../api/components';
+import { formatMeasure } from '../../../../helpers/measures';
+import Workspace from '../../../../components/workspace/main';
+
+const HEIGHT = 500;
const BUBBLES_LIMIT = 500;
function getMeasure (component, metric) {
return Number(component.measures[metric]) || 0;
}
-export default class MeasureBubbleChart extends React.Component {
+export default class BubbleChart extends React.Component {
state = {
fetching: true,
files: []
};
componentWillMount () {
- const { metric } = this.props;
- const { metrics } = this.context;
+ const { metric, metrics } = this.props;
const conf = bubbles[metric.key];
this.xMetric = metrics.find(m => m.key === conf.x);
@@ -54,9 +53,8 @@ export default class MeasureBubbleChart extends React.Component {
this.fetchFiles();
}
- componentDidUpdate (nextProps, nextState, nextContext) {
- if ((nextProps.metric !== this.props.metric) ||
- (nextContext.component !== this.context.component)) {
+ componentDidUpdate (nextProps) {
+ if (nextProps.metric !== this.props.metric) {
this.fetchFiles();
}
}
@@ -66,7 +64,7 @@ export default class MeasureBubbleChart extends React.Component {
}
fetchFiles () {
- const { component } = this.context;
+ const { component } = this.props;
const metrics = [this.xMetric.key, this.yMetric.key, this.sizeMetric.key];
const options = {
s: 'metric',
@@ -75,8 +73,8 @@ export default class MeasureBubbleChart extends React.Component {
ps: BUBBLES_LIMIT
};
- getFiles(component.key, metrics, options).then(r => {
- const files = r.map(file => {
+ getComponentLeaves(component.key, metrics, options).then(r => {
+ const files = r.components.map(file => {
const measures = {};
file.measures.forEach(measure => {
@@ -123,7 +121,7 @@ export default class MeasureBubbleChart extends React.Component {
const formatYTick = (tick) => formatMeasure(tick, this.yMetric.type);
return (
- <BubbleChart
+ <OriginalBubbleChart
items={items}
height={HEIGHT}
padding={[25, 60, 50, 60]}
@@ -148,7 +146,9 @@ export default class MeasureBubbleChart extends React.Component {
return (
<div className="measure-details-bubble-chart">
- {this.renderBubbleChart()}
+ <div>
+ {this.renderBubbleChart()}
+ </div>
<div className="measure-details-bubble-chart-axis x">{this.xMetric.name}</div>
<div className="measure-details-bubble-chart-axis y">{this.yMetric.name}</div>
@@ -157,8 +157,3 @@ export default class MeasureBubbleChart extends React.Component {
);
}
}
-
-MeasureBubbleChart.contextTypes = {
- component: React.PropTypes.object,
- metrics: React.PropTypes.array
-};
diff --git a/server/sonar-web/src/main/js/apps/component-measures/details/bubbleChart/MeasureBubbleChartContainer.js b/server/sonar-web/src/main/js/apps/component-measures/details/bubbleChart/MeasureBubbleChartContainer.js
new file mode 100644
index 00000000000..0dba5dcada8
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/details/bubbleChart/MeasureBubbleChartContainer.js
@@ -0,0 +1,39 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { connect } from 'react-redux';
+
+import MeasureBubbleChart from './BubbleChart';
+
+const mapStateToProps = state => {
+ return {
+ component: state.app.component,
+ metrics: state.app.metrics,
+ metric: state.details.metric
+ };
+};
+
+const mapDispatchToProps = () => {
+ return {};
+};
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(MeasureBubbleChart);
diff --git a/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/Breadcrumb.js b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/Breadcrumb.js
new file mode 100644
index 00000000000..15a44e30452
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/Breadcrumb.js
@@ -0,0 +1,62 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import React from 'react';
+import QualifierIcon from '../../../../components/shared/qualifier-icon';
+import { isDiffMetric, formatLeak } from '../../utils';
+import { formatMeasure } from '../../../../helpers/measures';
+
+const Breadcrumb = ({ component, metric, onBrowse }) => {
+ const handleClick = (e) => {
+ e.preventDefault();
+ e.target.blur();
+ onBrowse(component);
+ };
+
+ let inner;
+ if (onBrowse) {
+ inner = (
+ <a
+ id={'component-measures-breadcrumb-' + component.key}
+ href="#"
+ onClick={handleClick}>
+ {component.name}
+ </a>
+ );
+ } else {
+ inner = <span>{component.name}</span>;
+ }
+
+ const value = isDiffMetric(metric) ?
+ formatLeak(component.leak, metric) :
+ formatMeasure(component.value, metric.type);
+
+ return (
+ <span>
+ <QualifierIcon qualifier={component.qualifier}/>
+ &nbsp;
+ {inner}
+ {value != null && (
+ <span>{' (' + value + ')'}</span>
+ )}
+ </span>
+ );
+};
+
+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 }) => (
+ <ul className="component-measures-breadcrumbs">
+ {breadcrumbs.map((component, index) => (
+ <li key={component.key}>
+ <Breadcrumb
+ component={component}
+ metric={metric}
+ onBrowse={index + 1 < breadcrumbs.length ? onBrowse : null}/>
+ </li>
+ ))}
+ </ul>
+);
+
+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 = (
+ <span title={component.refKey || component.key}>
+ <QualifierIcon qualifier={component.qualifier}/>
+ &nbsp;
+ {head.length > 0 && (
+ <span className="note">{head}/</span>
+ )}
+ <span>{tail}</span>
+ </span>
+ );
+
+ return (
+ <td style={{ maxWidth: 0 }}>
+ <div style={{ maxWidth: '100%', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
+ {component.refId == null ? (
+ <a
+ id={'component-measures-component-link-' + component.key}
+ className={linkClassName}
+ href="#"
+ onClick={handleClick}>
+ {inner}
+ </a>
+ ) : (
+ <a
+ id={'component-measures-component-link-' + component.key}
+ className={linkClassName}
+ href={getComponentUrl(component.key)}>
+ <span className="big-spacer-right">
+ <i className="icon-detach"/>
+ </span>
+
+ {inner}
+ </a>
+ )}
+ </div>
+ </td>
+ );
+};
+
+export default ComponentCell;
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/IconUp.js b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ComponentsList.js
index 462d07bb17b..80793a9550f 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/components/IconUp.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ComponentsList.js
@@ -19,18 +19,28 @@
*/
import React from 'react';
-export default function IconUp () {
- /* eslint max-len: 0 */
+import ComponentsListRow from './ComponentsListRow';
+import EmptyComponentsList from './EmptyComponentsList';
+
+const ComponentsList = ({ components, selected, metric, onClick }) => {
+ if (!components.length) {
+ return <EmptyComponentsList/>;
+ }
+
return (
- <svg className="measure-details-components-up-icon"
- viewBox="0 0 256 448"
- fillRule="evenodd"
- clipRule="evenodd"
- strokeLinejoin="round"
- strokeMiterlimit="1.414">
- <path
- d="M1.75 150.75c3 6.167 7.833 9.25 14.5 9.25h48v216c0 2.333.75 4.25 2.25 5.75s3.417 2.25 5.75 2.25h176c3.5 0 5.917-1.5 7.25-4.5 1.333-3.333 1-6.25-1-8.75l-40-48c-1.5-1.833-3.583-2.75-6.25-2.75h-80V160h48c6.667 0 11.5-3.083 14.5-9.25 2.833-6.167 2.083-11.833-2.25-17l-80-96c-3-3.667-7.083-5.5-12.25-5.5S87 34.083 84 37.75l-80 96c-4.5 5.333-5.25 11-2.25 17z"
- fillRule="nonzero"/>
- </svg>
+ <table className="data zebra zebra-hover">
+ <tbody>
+ {components.map(component => (
+ <ComponentsListRow
+ key={component.id}
+ component={component}
+ isSelected={component === selected}
+ metric={metric}
+ onClick={onClick}/>
+ ))}
+ </tbody>
+ </table>
);
-}
+};
+
+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 (
+ <tr>
+ <ComponentCell
+ component={component}
+ isSelected={isSelected}
+ onClick={handleClick.bind(this, component)}/>
+
+ <MeasureCell
+ component={component}
+ metric={metric}/>
+ </tr>
+ );
+};
+
+export default ComponentsListRow;
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDrilldownEmpty.js b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/EmptyComponentsList.js
index ede861f8382..7b1a11d5791 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDrilldownEmpty.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/EmptyComponentsList.js
@@ -19,12 +19,14 @@
*/
import React from 'react';
-import { translate } from '../../../helpers/l10n';
+import { translate } from '../../../../helpers/l10n';
-export default function MeasureDrilldownEmpty () {
+const EmptyComponentsList = () => {
return (
- <div className="measures-details-components-empty note">
+ <div className="note">
{translate('no_results')}
</div>
);
-}
+};
+
+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 (
+ <header className="measure-details-viewer-header">
+ {breadcrumbs != null && breadcrumbs.length > 1 && (
+ <div className="measure-details-header-container">
+ <Breadcrumbs
+ breadcrumbs={breadcrumbs}
+ metric={metric}
+ onBrowse={onBrowse}/>
+ </div>
+ )}
+
+ {selectedIndex != null && selectedIndex !== -1 && (
+ <div className="pull-right">
+ <span className="note spacer-right">
+ {translateWithParameters('component_measures.x_of_y', selectedIndex + 1, componentsCount)}
+ </span>
+
+ <div className="button-group">
+ {hasPrevious && (
+ <button onClick={blur(onSelectPrevious)}>&lt;</button>
+ )}
+ {hasNext && (
+ <button onClick={blur(onSelectNext)}>&gt;</button>
+ )}
+ </div>
+ </div>
+ )}
+ </header>
+ );
+};
+
+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 (
+ <div ref="container" className="measure-details-plain-list">
+ <ListHeader
+ metric={metric}
+ breadcrumbs={breadcrumbs}
+ componentsCount={components.length}
+ selectedIndex={selectedIndex}
+ onSelectPrevious={onSelectPrevious}
+ onSelectNext={onSelectNext}
+ onBrowse={this.handleBreadcrumbClick.bind(this)}/>
+
+ {!selected && (
+ <div className={classNames({ 'new-loading': fetching })}>
+ <ComponentsList
+ components={components}
+ selected={selected}
+ metric={metric}
+ onClick={this.handleClick.bind(this)}/>
+ <ListFooter
+ count={components.length}
+ total={total}
+ loadMore={this.fetchMore.bind(this)}
+ ready={!fetching}/>
+ </div>
+ )}
+
+ {!!selected && (
+ <div className="measure-details-viewer">
+ <SourceViewer
+ component={selected}
+ period={sourceViewerPeriod}/>
+ </div>
+ )}
+ </div>
+ );
+ }
+}
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 (
+ <td className="thin nowrap text-right">
+ <span id={'component-measures-component-measure-' + component.key}>
+ {value != null ? value : '–'}
+ </span>
+ </td>
+ );
+};
+
+export default MeasureCell;
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDrilldown.js b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/MeasureDrilldown.js
index d0e78285813..3df7565a694 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDrilldown.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/MeasureDrilldown.js
@@ -20,43 +20,20 @@
import React from 'react';
import { Link } from 'react-router';
-import IconList from './IconList';
-import IconTree from './IconTree';
-import IconBubbles from './IconBubbles';
-import IconTreemap from './IconTreemap';
-import IconHistory from './IconHistory';
+import IconList from './../../components/IconList';
+import IconTree from './../../components/IconTree';
+import IconBubbles from './../../components/IconBubbles';
+import IconTreemap from './../../components/IconTreemap';
+import IconHistory from './../../components/IconHistory';
-import { hasHistory, hasBubbleChart, hasTreemap } from '../utils';
-import { translate } from '../../../helpers/l10n';
+import { hasHistory, hasBubbleChart, hasTreemap } from '../../utils';
+import { translate } from '../../../../helpers/l10n';
export default class MeasureDrilldown extends React.Component {
- state = {
- tree: {
- components: [],
- breadcrumbs: [],
- selected: null,
- fetching: true
- },
- list: {
- components: [],
- selected: null,
- fetching: true
- }
- };
-
render () {
- const { children, metric, ...other } = this.props;
- const { component } = this.context;
-
- const showListView = ['VW', 'SVW', 'DEV'].indexOf(component.qualifier) === -1;
+ const { children, component, metric, ...other } = this.props;
- const child = React.cloneElement(children, {
- component,
- metric,
- ...other,
- store: this.state,
- updateStore: this.setState.bind(this)
- });
+ const child = React.cloneElement(children, { ...other });
return (
<div className="measure-details-drilldown">
@@ -64,23 +41,21 @@ export default class MeasureDrilldown extends React.Component {
<li>
<Link
activeClassName="active"
+ to={{ pathname: `${metric.key}/list`, query: { id: component.key } }}>
+ <IconList/>
+ {translate('component_measures.tab.list')}
+ </Link>
+ </li>
+
+ <li>
+ <Link
+ activeClassName="active"
to={{ pathname: `${metric.key}/tree`, query: { id: component.key } }}>
<IconTree/>
{translate('component_measures.tab.tree')}
</Link>
</li>
- {showListView && (
- <li>
- <Link
- activeClassName="active"
- to={{ pathname: `${metric.key}/list`, query: { id: component.key } }}>
- <IconList/>
- {translate('component_measures.tab.list')}
- </Link>
- </li>
- )}
-
{hasBubbleChart(metric.key) && (
<li>
<Link
@@ -120,7 +95,3 @@ export default class MeasureDrilldown extends React.Component {
);
}
}
-
-MeasureDrilldown.contextTypes = {
- component: React.PropTypes.object
-};
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 (
+ <div ref="container" className="measure-details-plain-list">
+ <ListHeader
+ metric={metric}
+ breadcrumbs={breadcrumbs}
+ componentsCount={components.length}
+ selectedIndex={selectedIndex}
+ onSelectPrevious={onSelectPrevious}
+ onSelectNext={onSelectNext}
+ onBrowse={this.handleBreadcrumbClick.bind(this)}/>
+
+ {!selected && (
+ <div>
+ <ComponentsList
+ components={components}
+ selected={selected}
+ metric={metric}
+ onClick={this.handleClick.bind(this)}/>
+ <ListFooter
+ count={components.length}
+ total={total}
+ loadMore={onFetchMore}
+ ready={!fetching}/>
+ </div>
+ )}
+
+ {!!selected && (
+ <div className="measure-details-viewer">
+ <SourceViewer
+ component={selected}
+ period={sourceViewerPeriod}/>
+ </div>
+ )}
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/TreeViewContainer.js b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/TreeViewContainer.js
new file mode 100644
index 00000000000..f32bc7d222c
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/TreeViewContainer.js
@@ -0,0 +1,65 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { connect } from 'react-redux';
+import pick from '../../../../../../../node_modules/lodash/pick';
+
+import TreeView from './TreeView';
+import {
+ start,
+ fetchMore,
+ drilldown,
+ useBreadcrumbs,
+ selectComponent,
+ selectNext,
+ selectPrevious
+} from '../../store/treeViewActions';
+
+const mapStateToProps = state => {
+ const drilldown = pick(state.tree, [
+ 'components',
+ 'breadcrumbs',
+ 'selected',
+ 'total',
+ 'pageIndex'
+ ]);
+ return {
+ ...drilldown,
+ component: state.app.component,
+ metric: state.details.metric,
+ fetching: state.status.fetching
+ };
+};
+
+const mapDispatchToProps = dispatch => {
+ return {
+ onStart: (rootComponent, metric) => dispatch(start(rootComponent, metric)),
+ onFetchMore: () => dispatch(fetchMore()),
+ onDrilldown: component => dispatch(drilldown(component)),
+ onUseBreadcrumbs: component => dispatch(useBreadcrumbs(component)),
+ onSelect: component => dispatch(selectComponent(component)),
+ onSelectNext: component => dispatch(selectNext(component)),
+ onSelectPrevious: component => dispatch(selectPrevious(component))
+ };
+};
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(TreeView);
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHistory.js b/server/sonar-web/src/main/js/apps/component-measures/details/history/MeasureHistory.js
index a8b9acb7c44..4958e72d75f 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHistory.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/details/history/MeasureHistory.js
@@ -21,13 +21,13 @@ import _ from 'underscore';
import moment from 'moment';
import React from 'react';
-import Spinner from './Spinner';
-import Timeline from '../../../components/charts/Timeline';
-import { getTimeMachineData } from '../../../api/time-machine';
-import { getEvents } from '../../../api/events';
-import { formatMeasure, getShortType } from '../../../helpers/measures';
+import Spinner from './../../components/Spinner';
+import Timeline from '../../../../components/charts/Timeline';
+import { getTimeMachineData } from '../../../../api/time-machine';
+import { getEvents } from '../../../../api/events';
+import { formatMeasure, getShortType } from '../../../../helpers/measures';
-const HEIGHT = 360;
+const HEIGHT = 500;
function parseValue (value, type) {
return type === 'RATING' && typeof value === 'string' ? value.charCodeAt(0) - 'A'.charCodeAt(0) + 1 : value;
@@ -45,9 +45,8 @@ export default class MeasureHistory extends React.Component {
this.fetchHistory();
}
- componentDidUpdate (nextProps, nextState, nextContext) {
- if ((nextProps.metric !== this.props.metric) ||
- (nextContext.component !== this.context.component)) {
+ componentDidUpdate (nextProps) {
+ if (nextProps.metric !== this.props.metric) {
this.fetchHistory();
}
}
@@ -176,7 +175,3 @@ export default class MeasureHistory extends React.Component {
);
}
}
-
-MeasureHistory.contextTypes = {
- component: React.PropTypes.object
-};
diff --git a/server/sonar-web/src/main/js/apps/component-measures/details/history/MeasureHistoryContainer.js b/server/sonar-web/src/main/js/apps/component-measures/details/history/MeasureHistoryContainer.js
new file mode 100644
index 00000000000..bfe7decf1a3
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/details/history/MeasureHistoryContainer.js
@@ -0,0 +1,38 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { connect } from 'react-redux';
+
+import MeasureHistory from './MeasureHistory';
+
+const mapStateToProps = state => {
+ return {
+ component: state.app.component,
+ metric: state.details.metric
+ };
+};
+
+const mapDispatchToProps = () => {
+ return {};
+};
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(MeasureHistory);
diff --git a/server/sonar-web/src/main/js/apps/component-measures/details/reducer.js b/server/sonar-web/src/main/js/apps/component-measures/details/reducer.js
new file mode 100644
index 00000000000..146a8a3d6d6
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/details/reducer.js
@@ -0,0 +1,42 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { DISPLAY_HOME } from '../app/actions';
+import { REQUEST_MEASURE, RECEIVE_MEASURE } from './actions';
+
+const initialState = {
+ metric: undefined,
+ secondaryMeasure: undefined,
+ measure: undefined,
+ periods: undefined
+};
+
+export default function appReducer (state = initialState, action = {}) {
+ switch (action.type) {
+ case DISPLAY_HOME:
+ return initialState;
+ case REQUEST_MEASURE:
+ return { ...state, metric: action.metric };
+ case RECEIVE_MEASURE:
+ return { ...state, measure: action.measure, secondaryMeasure: action.secondaryMeasure, periods: action.periods };
+ default:
+ return state;
+ }
+}
+
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureTreemap.js b/server/sonar-web/src/main/js/apps/component-measures/details/treemap/MeasureTreemap.js
index 2364d1b75ce..92615167cc3 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureTreemap.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/details/treemap/MeasureTreemap.js
@@ -19,16 +19,16 @@
*/
import React from 'react';
-import Spinner from './Spinner';
-import { getLeakValue } from '../utils';
-import { Treemap } from '../../../components/charts/treemap';
-import { getChildren } from '../../../api/components';
-import { formatMeasure } from '../../../helpers/measures';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { getComponentUrl } from '../../../helpers/urls';
-import Workspace from '../../../components/workspace/main';
+import Spinner from './../../components/Spinner';
+import { getLeakValue } from '../../utils';
+import { Treemap } from '../../../../components/charts/treemap';
+import { getChildren } from '../../../../api/components';
+import { formatMeasure } from '../../../../helpers/measures';
+import { translate, translateWithParameters } from '../../../../helpers/l10n';
+import { getComponentUrl } from '../../../../helpers/urls';
+import Workspace from '../../../../components/workspace/main';
-const HEIGHT = 360;
+const HEIGHT = 500;
export default class MeasureTreemap extends React.Component {
state = {
@@ -38,16 +38,15 @@ export default class MeasureTreemap extends React.Component {
};
componentDidMount () {
- const { component } = this.context;
+ const { component } = this.props;
this.mounted = true;
this.fetchComponents(component.key);
}
- componentDidUpdate (nextProps, nextState, nextContext) {
- if ((nextProps.metric !== this.props.metric) ||
- (nextContext.component !== this.context.component)) {
- this.fetchComponents(nextContext.component.key);
+ componentDidUpdate (nextProps) {
+ if (nextProps.metric !== this.props.metric) {
+ this.fetchComponents(this.props.component.key);
}
}
@@ -158,7 +157,7 @@ export default class MeasureTreemap extends React.Component {
}
handleReset () {
- const { component } = this.context;
+ const { component } = this.props;
this.fetchComponents(component.key).then(() => {
this.setState({ breadcrumbs: [] });
});
@@ -185,14 +184,11 @@ export default class MeasureTreemap extends React.Component {
};
});
- // FIXME remove this magic number
- const height = HEIGHT - 35;
-
return (
<Treemap
items={items}
breadcrumbs={this.state.breadcrumbs}
- height={height}
+ height={HEIGHT}
canBeClicked={() => true}
onRectangleClick={this.handleRectangleClick.bind(this)}
onReset={this.handleReset.bind(this)}/>
@@ -228,8 +224,3 @@ export default class MeasureTreemap extends React.Component {
);
}
}
-
-MeasureTreemap.contextTypes = {
- component: React.PropTypes.object,
- metrics: React.PropTypes.array
-};
diff --git a/server/sonar-web/src/main/js/apps/component-measures/details/treemap/MeasureTreemapContainer.js b/server/sonar-web/src/main/js/apps/component-measures/details/treemap/MeasureTreemapContainer.js
new file mode 100644
index 00000000000..978a2a522e2
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/details/treemap/MeasureTreemapContainer.js
@@ -0,0 +1,38 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { connect } from 'react-redux';
+
+import MeasureTreemap from './MeasureTreemap';
+
+const mapStateToProps = state => {
+ return {
+ component: state.app.component,
+ metric: state.details.metric
+ };
+};
+
+const mapDispatchToProps = () => {
+ return {};
+};
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(MeasureTreemap);
diff --git a/server/sonar-web/src/main/js/apps/component-measures/home/AllMeasures.js b/server/sonar-web/src/main/js/apps/component-measures/home/AllMeasures.js
new file mode 100644
index 00000000000..c0bc2f6162c
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/home/AllMeasures.js
@@ -0,0 +1,64 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import _ from 'underscore';
+import React from 'react';
+
+import Spinner from './../components/Spinner';
+import AllMeasuresDomain from './AllMeasuresDomain';
+import { getLeakPeriodLabel } from '../../../helpers/periods';
+
+export default class AllMeasures extends React.Component {
+ componentDidMount () {
+ this.props.onDisplay();
+ this.props.fetchMeasures();
+ }
+
+ render () {
+ const { component, measures, periods, fetching } = this.props;
+
+ if (fetching) {
+ return <Spinner/>;
+ }
+
+ 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 (
+ <section id="component-measures-home" className="page page-container page-limited">
+ <ul className="measures-domains">
+ {domains.map((domain, index) => (
+ <AllMeasuresDomain
+ key={domain.name}
+ domain={domain}
+ component={component}
+ displayLeakHeader={index === 0}
+ leakPeriodLabel={leakPeriodLabel}/>
+ ))}
+ </ul>
+ </section>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/home/AllMeasuresContainer.js b/server/sonar-web/src/main/js/apps/component-measures/home/AllMeasuresContainer.js
new file mode 100644
index 00000000000..c12fac1ac13
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/home/AllMeasuresContainer.js
@@ -0,0 +1,46 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { connect } from 'react-redux';
+
+import AllMeasures from './AllMeasures';
+import { fetchMeasures } from './actions';
+import { displayHome } from '../app/actions';
+
+const mapStateToProps = state => {
+ return {
+ component: state.app.component,
+ metrics: state.app.metrics,
+ measures: state.home.measures,
+ periods: state.home.periods,
+ fetching: state.status.fetching
+ };
+};
+
+const mapDispatchToProps = dispatch => {
+ return {
+ onDisplay: () => dispatch(displayHome()),
+ fetchMeasures: () => dispatch(fetchMeasures())
+ };
+};
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(AllMeasures);
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/AllMeasuresDomain.js b/server/sonar-web/src/main/js/apps/component-measures/home/AllMeasuresDomain.js
index 6068cfd31b1..67fe53d31d7 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/components/AllMeasuresDomain.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/home/AllMeasuresDomain.js
@@ -17,8 +17,8 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import sortBy from 'lodash/sortBy';
-import partition from 'lodash/partition';
+import sortBy from '../../../../../../node_modules/lodash/sortBy';
+import partition from '../../../../../../node_modules/lodash/partition';
import React from 'react';
import { Link } from 'react-router';
diff --git a/server/sonar-web/src/main/js/apps/component-measures/home/actions.js b/server/sonar-web/src/main/js/apps/component-measures/home/actions.js
new file mode 100644
index 00000000000..542e2207cb4
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/home/actions.js
@@ -0,0 +1,59 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { startFetching, stopFetching } from '../store/statusActions';
+import { getMeasuresAndMeta } from '../../../api/measures';
+import { getLeakPeriod } from '../../../helpers/periods';
+import { getLeakValue } from '../utils';
+
+export const RECEIVE_MEASURES = 'home/RECEIVE_MEASURES';
+
+export function receiveMeasures (measures, periods) {
+ return { type: RECEIVE_MEASURES, measures, periods };
+}
+
+export function fetchMeasures () {
+ return (dispatch, getState) => {
+ dispatch(startFetching());
+
+ const { component, metrics } = getState().app;
+ const metricKeys = metrics
+ .filter(metric => !metric.hidden)
+ .filter(metric => metric.type !== 'DATA' && metric.type !== 'DISTRIB')
+ .map(metric => metric.key);
+
+ getMeasuresAndMeta(component.key, metricKeys, { additionalFields: 'periods' }).then(r => {
+ const leakPeriod = getLeakPeriod(r.periods);
+ const measures = r.component.measures
+ .map(measure => {
+ const metric = metrics.find(metric => metric.key === measure.metric);
+ const leak = getLeakValue(measure);
+ return { ...measure, metric, leak };
+ })
+ .filter(measure => {
+ const hasValue = measure.value != null;
+ const hasLeakValue = !!leakPeriod && measure.leak != null;
+ return hasValue || hasLeakValue;
+ });
+
+ dispatch(receiveMeasures(measures, r.periods));
+ dispatch(stopFetching());
+ });
+ };
+}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/home/reducer.js b/server/sonar-web/src/main/js/apps/component-measures/home/reducer.js
new file mode 100644
index 00000000000..e80c7b1129b
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/home/reducer.js
@@ -0,0 +1,34 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { RECEIVE_MEASURES } from './actions';
+
+const initialState = {
+ measures: undefined,
+ periods: undefined
+};
+
+export default function (state = initialState, action = {}) {
+ switch (action.type) {
+ case RECEIVE_MEASURES:
+ return { ...state, measures: action.measures, periods: action.periods };
+ default:
+ return state;
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/store/configureStore.js b/server/sonar-web/src/main/js/apps/component-measures/store/configureStore.js
new file mode 100644
index 00000000000..07d55655531
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/store/configureStore.js
@@ -0,0 +1,41 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { combineReducers } from 'redux';
+
+import appReducer from './../app/reducer';
+import statusReducer from './statusReducer';
+import homeReducer from '../home/reducer';
+import detailsReducer from '../details/reducer';
+import listViewReducer from './listViewReducer';
+import treeViewReducer from './treeViewReducer';
+import configureStore from '../../../components/store/configureStore';
+
+export default function customConfigureStore (initialState) {
+ const rootReducer = combineReducers({
+ app: appReducer,
+ home: homeReducer,
+ details: detailsReducer,
+ list: listViewReducer,
+ tree: treeViewReducer,
+ status: statusReducer
+ });
+
+ return configureStore(rootReducer, initialState);
+}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/store/listViewActions.js b/server/sonar-web/src/main/js/apps/component-measures/store/listViewActions.js
new file mode 100644
index 00000000000..6826e012786
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/store/listViewActions.js
@@ -0,0 +1,143 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { getComponentTree } from '../../../api/components';
+import { enhanceWithSingleMeasure } from '../utils';
+import { startFetching, stopFetching } from './statusActions';
+
+export const UPDATE_STORE = 'drilldown/list/UPDATE_STORE';
+
+function updateStore (state) {
+ return { type: UPDATE_STORE, state };
+}
+
+function makeRequest (baseComponent, metric, options) {
+ const asc = metric.direction === 1;
+ const ps = 100;
+ const finalOptions = { asc, ps };
+
+ if (metric.key.indexOf('new_') === 0) {
+ Object.assign(options, {
+ s: 'metricPeriod,name',
+ metricSort: metric.key,
+ metricPeriodSort: 1
+ });
+ } else {
+ Object.assign(options, {
+ s: 'metric,name',
+ metricSort: metric.key
+ });
+ }
+
+ Object.assign(finalOptions, options);
+ return getComponentTree('leaves', baseComponent.key, [metric.key], finalOptions);
+}
+
+function fetchLeaves (baseComponent, metric, pageIndex = 1) {
+ const options = { p: pageIndex };
+
+ return makeRequest(baseComponent, metric, options).then(r => {
+ const nextComponents = enhanceWithSingleMeasure(r.components);
+
+ return {
+ components: nextComponents,
+ pageIndex: r.paging.pageIndex,
+ total: r.paging.total
+ };
+ });
+}
+
+/**
+ * Fetch the first page of components for a given base component
+ * @param baseComponent
+ * @param metric
+ */
+export function fetchList (baseComponent, metric) {
+ return (dispatch, getState) => {
+ const { list } = getState();
+ if (list.baseComponent === baseComponent && list.metric === metric) {
+ return Promise.resolve();
+ }
+
+ dispatch(startFetching());
+ return fetchLeaves(baseComponent, metric).then(r => {
+ dispatch(updateStore({
+ ...r,
+ baseComponent,
+ metric
+ }));
+ dispatch(stopFetching());
+ });
+ };
+}
+
+/**
+ * Fetch next page of components
+ * @param baseComponent
+ * @param metric
+ */
+export function fetchMore (baseComponent, metric) {
+ return (dispatch, getState) => {
+ const { components, pageIndex } = getState().list;
+ dispatch(startFetching());
+ return fetchLeaves(baseComponent, metric, pageIndex + 1).then(r => {
+ const diff = { ...r, components: [...components, ...r.components] };
+ dispatch(updateStore(diff));
+ dispatch(stopFetching());
+ });
+ };
+}
+
+/**
+ * Select specified component from the list
+ * @param component A component to select
+ */
+export function selectComponent (component) {
+ return dispatch => {
+ dispatch(updateStore({ selected: component }));
+ };
+}
+
+/**
+ * Select next element from the list of components
+ */
+export function selectNext () {
+ return (dispatch, getState) => {
+ const { components, selected } = getState().list;
+ const selectedIndex = components.indexOf(selected);
+ if (selectedIndex < components.length - 1) {
+ const nextSelected = components[selectedIndex + 1];
+ dispatch(selectComponent(nextSelected));
+ }
+ };
+}
+
+/**
+ * Select previous element from the list of components
+ */
+export function selectPrevious () {
+ return (dispatch, getState) => {
+ const { components, selected } = getState().list;
+ const selectedIndex = components.indexOf(selected);
+ if (selectedIndex > 0) {
+ const nextSelected = components[selectedIndex - 1];
+ dispatch(selectComponent(nextSelected));
+ }
+ };
+}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/store/listViewReducer.js b/server/sonar-web/src/main/js/apps/component-measures/store/listViewReducer.js
new file mode 100644
index 00000000000..40105889660
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/store/listViewReducer.js
@@ -0,0 +1,37 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { DISPLAY_HOME } from './../app/actions';
+import { UPDATE_STORE } from './listViewActions';
+
+const initialState = {
+ components: [],
+ total: 0
+};
+
+export default function drilldownReducer (state = initialState, action = {}) {
+ switch (action.type) {
+ case DISPLAY_HOME:
+ return initialState;
+ case UPDATE_STORE:
+ return { ...state, ...action.state };
+ default:
+ return state;
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/store/statusActions.js b/server/sonar-web/src/main/js/apps/component-measures/store/statusActions.js
new file mode 100644
index 00000000000..5755ad5ff55
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/store/statusActions.js
@@ -0,0 +1,29 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+export const START_FETCHING = 'status/START_FETCHING';
+export const STOP_FETCHING = 'status/STOP_FETCHING';
+
+export function startFetching () {
+ return { type: START_FETCHING };
+}
+
+export function stopFetching () {
+ return { type: STOP_FETCHING };
+}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/store/statusReducer.js b/server/sonar-web/src/main/js/apps/component-measures/store/statusReducer.js
new file mode 100644
index 00000000000..09e27b232d8
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/store/statusReducer.js
@@ -0,0 +1,36 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { START_FETCHING, STOP_FETCHING } from './statusActions';
+
+const initialState = {
+ fetching: false
+};
+
+export default function drilldownReducer (state = initialState, action = {}) {
+ switch (action.type) {
+ case START_FETCHING:
+ return { ...state, fetching: true };
+ case STOP_FETCHING:
+ return { ...state, fetching: false };
+ default:
+ return state;
+ }
+}
+
diff --git a/server/sonar-web/src/main/js/apps/component-measures/store/treeViewActions.js b/server/sonar-web/src/main/js/apps/component-measures/store/treeViewActions.js
new file mode 100644
index 00000000000..a45cf2dfa5c
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/store/treeViewActions.js
@@ -0,0 +1,244 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import initial from 'lodash/initial';
+
+import { getComponentTree } from '../../../api/components';
+import { enhanceWithSingleMeasure } from '../utils';
+import { startFetching, stopFetching } from './statusActions';
+
+/*
+ * Actions
+ */
+
+export const UPDATE_STORE = 'drilldown/tree/UPDATE_STORE';
+export const INIT = 'drilldown/tree/INIT';
+
+
+/*
+ * Action Creators
+ */
+
+/**
+ * Internal
+ * Update store
+ * @param state
+ * @returns {{type: string, state: *}}
+ */
+function updateStore (state) {
+ return { type: UPDATE_STORE, state };
+}
+
+/**
+ * Init tree view drilldown for the given root component and given metric
+ * @param rootComponent
+ * @param metric
+ * @returns {{type: string, rootComponent: *, metric: *}}
+ */
+function init (rootComponent, metric) {
+ return { type: INIT, rootComponent, metric };
+}
+
+
+/*
+ * Workflow
+ */
+
+function makeRequest (baseComponent, metric, options) {
+ const asc = metric.direction === 1;
+ const ps = 100;
+ const finalOptions = { asc, ps };
+
+ if (metric.key.indexOf('new_') === 0) {
+ Object.assign(options, {
+ s: 'metricPeriod,name',
+ metricSort: metric.key,
+ metricPeriodSort: 1
+ });
+ } else {
+ Object.assign(options, {
+ s: 'metric,name',
+ metricSort: metric.key
+ });
+ }
+
+ Object.assign(finalOptions, options);
+ return getComponentTree('children', baseComponent.key, [metric.key], finalOptions);
+}
+
+function fetchComponents (baseComponent, metric, pageIndex = 1) {
+ const options = { p: pageIndex };
+
+ return makeRequest(baseComponent, metric, options).then(r => {
+ const nextComponents = enhanceWithSingleMeasure(r.components);
+
+ return {
+ baseComponent,
+ components: nextComponents,
+ pageIndex: r.paging.pageIndex,
+ total: r.paging.total
+ };
+ });
+}
+
+/**
+ * Fetch the first page of components for a given base component
+ * @param baseComponent
+ */
+function fetchList (baseComponent) {
+ return (dispatch, getState) => {
+ const { metric } = getState().tree;
+
+ dispatch(startFetching());
+ return fetchComponents(baseComponent, metric).then(r => {
+ dispatch(updateStore({
+ ...r,
+ baseComponent,
+ breadcrumbs: [baseComponent]
+ }));
+ dispatch(stopFetching());
+ });
+ };
+}
+
+/**
+ * Init tree view with root component and metric.
+ * Fetch the first page of components if needed.
+ * @param rootComponent
+ * @param metric
+ * @returns {function()}
+ */
+export function start (rootComponent, metric) {
+ return (dispatch, getState) => {
+ const { tree } = getState();
+ if (rootComponent === tree.rootComponent && metric === tree.metric) {
+ return Promise.resolve();
+ }
+
+ dispatch(init(rootComponent, metric));
+ dispatch(fetchList(rootComponent));
+ };
+}
+
+/**
+ * Fetch next page of components
+ */
+export function fetchMore () {
+ return (dispatch, getState) => {
+ const { metric, baseComponent, components, pageIndex } = getState().tree;
+ dispatch(startFetching());
+ return fetchComponents(baseComponent, metric, pageIndex + 1).then(r => {
+ dispatch(updateStore({
+ ...r,
+ components: [...components, ...r.components]
+ }));
+ dispatch(stopFetching());
+ });
+ };
+}
+
+/**
+ * Drilldown to the component
+ * @param component
+ */
+export function drilldown (component) {
+ return (dispatch, getState) => {
+ const { metric, breadcrumbs } = getState().tree;
+ dispatch(startFetching());
+ return fetchComponents(component, metric).then(r => {
+ dispatch(updateStore({
+ ...r,
+ breadcrumbs: [...breadcrumbs, component],
+ selected: undefined
+ }));
+ dispatch(stopFetching());
+ });
+ };
+}
+
+/**
+ * Go up using breadcrumbs
+ * @param component
+ */
+export function useBreadcrumbs (component) {
+ return (dispatch, getState) => {
+ const { metric, breadcrumbs } = getState().tree;
+ const index = breadcrumbs.indexOf(component);
+ dispatch(startFetching());
+ return fetchComponents(component, metric).then(r => {
+ dispatch(updateStore({
+ ...r,
+ breadcrumbs: breadcrumbs.slice(0, index + 1),
+ selected: undefined
+ }));
+ dispatch(stopFetching());
+ });
+ };
+}
+
+/**
+ * Select given component from the list
+ * @param component
+ */
+export function selectComponent (component) {
+ return (dispatch, getState) => {
+ const { breadcrumbs } = getState().tree;
+ const nextBreadcrumbs = [...breadcrumbs, component];
+ dispatch(updateStore({
+ selected: component,
+ breadcrumbs: nextBreadcrumbs
+ }));
+ };
+}
+
+/**
+ * Select next element from the list of components
+ */
+export function selectNext () {
+ return (dispatch, getState) => {
+ const { components, selected, breadcrumbs } = getState().tree;
+ const selectedIndex = components.indexOf(selected);
+ if (selectedIndex < components.length - 1) {
+ const nextSelected = components[selectedIndex + 1];
+ const nextBreadcrumbs = [...initial(breadcrumbs), nextSelected];
+ dispatch(updateStore({
+ selected: nextSelected,
+ breadcrumbs: nextBreadcrumbs
+ }));
+ }
+ };
+}
+
+/**
+ * Select previous element from the list of components
+ */
+export function selectPrevious () {
+ return (dispatch, getState) => {
+ const { components, selected, breadcrumbs } = getState().tree;
+ const selectedIndex = components.indexOf(selected);
+ if (selectedIndex > 0) {
+ const nextSelected = components[selectedIndex - 1];
+ const nextBreadcrumbs = [...initial(breadcrumbs), nextSelected];
+ dispatch(updateStore({
+ selected: nextSelected,
+ breadcrumbs: nextBreadcrumbs
+ }));
+ }
+ };
+}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/store/treeViewReducer.js b/server/sonar-web/src/main/js/apps/component-measures/store/treeViewReducer.js
new file mode 100644
index 00000000000..b647b538816
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/store/treeViewReducer.js
@@ -0,0 +1,42 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import pick from 'lodash/pick';
+
+import { DISPLAY_HOME } from './../app/actions';
+import { UPDATE_STORE, INIT } from './treeViewActions';
+
+const initialState = {
+ components: [],
+ breadcrumbs: [],
+ total: 0
+};
+
+export default function drilldownReducer (state = initialState, action = {}) {
+ switch (action.type) {
+ case DISPLAY_HOME:
+ return initialState;
+ case UPDATE_STORE:
+ return { ...state, ...action.state };
+ case INIT:
+ return { ...state, ...pick(action, ['rootComponent', 'metric']) };
+ default:
+ return state;
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/styles.css b/server/sonar-web/src/main/js/apps/component-measures/styles.css
index 68c85c91702..198f0aeea91 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/styles.css
+++ b/server/sonar-web/src/main/js/apps/component-measures/styles.css
@@ -1,8 +1,4 @@
.measures-domains {
- max-width: 980px;
- margin: 20px auto;
- column-count: 2;
- column-gap: 60px;
}
.measures-domains > li {
@@ -67,25 +63,12 @@
.measure-details-header {
position: relative;
-}
-
-.measure-details-see-also {
- position: absolute;
- z-index: 50;
- top: 29px;
- left: 10px;
- width: 400px;
- max-height: 450px;
- border: 1px solid #e6e6e6;
- background-color: #fff;
- box-shadow: 0 6px 12px rgba(0, 0, 0, .175);
- overflow: auto;
+ margin-top: 10px;
+ margin-bottom: 30px;
}
.measure-details-metric,
.measure-details-value {
- margin-left: 20px;
- margin-right: 20px;
}
.measure-details-metric {
@@ -138,8 +121,7 @@
.measure-details-drilldown-mode {
display: flex;
- padding-left: 10px;
- padding-right: 10px;
+ margin-bottom: 10px;
border-bottom: 1px solid #e6e6e6;
}
@@ -167,16 +149,15 @@
fill: #4b9fd5;
}
+.measure-details-plain-list {
+}
+
.measure-details-components {
width: 300px;
padding: 10px;
box-sizing: border-box;
}
-.measure-details-components-parent {
- padding-bottom: 6px;
-}
-
.measure-details-components > li > a {
display: flex;
padding-top: 5px;
@@ -209,16 +190,23 @@
text-align: right;
}
-.measure-details-tree,
-.measure-details-plain-list {
- display: flex;
+.measure-details-viewer {
+ min-height: 100vh;
}
-.measure-details-viewer {
- width: calc(100% - 300px);
- margin-top: -1px;
- margin-bottom: -1px;
- box-sizing: border-box;
+.measure-details-viewer-header {
+ margin-bottom: 10px;
+ line-height: 24px;
+}
+
+.measure-details-viewer-header .button-group {
+ vertical-align: top;
+}
+
+.measure-details-header-container {
+ display: inline-block;
+ line-height: 16px;
+ padding-right: 10px;
}
.measure-tab-icon {
@@ -233,28 +221,17 @@
transition: fill 0.3s ease;
}
-.measure-details-components-up-icon {
- display: inline-block;
- height: 14px;
- padding: 2px 4px 0;
-}
-
.measure-details-components-up-icon path {
fill: #aeaeae;
}
-.measures-details-components-empty {
- padding: 10px;
-}
-
.measure-details-history {
- padding: 10px;
+ padding: 10px 0;
}
.measure-details-bubble-chart {
position: relative;
- max-width: 960px;
- margin: 10px auto;
+ margin: 10px 0;
padding: 30px 0 30px 50px;
}
@@ -281,10 +258,36 @@
}
.measure-details-treemap {
- max-width: 960px;
- margin: 20px auto;
+ margin: 20px 0;
}
.measure-details-treemap-legend {
margin-bottom: 10px;
}
+
+
+.component-measures-breadcrumbs {
+ display: flex;
+ flex-wrap: wrap;
+}
+
+.component-measures-breadcrumbs > li {
+ padding: 5px 5px 3px;
+}
+
+.component-measures-breadcrumbs > li:first-child {
+ padding-left: 0;
+}
+
+.component-measures-breadcrumbs > li::after {
+ position: relative;
+ top: -1px;
+ padding-left: 10px;
+ color: #777;
+ font-size: 11px;
+ content: ">";
+}
+
+.component-measures-breadcrumbs > li:last-child::after {
+ display: none;
+}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/utils.js b/server/sonar-web/src/main/js/apps/component-measures/utils.js
index f20583bc2cb..5349f97864a 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/utils.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/utils.js
@@ -17,9 +17,13 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import bubbles from './bubbles';
+import bubbles from './config/bubbles';
import { formatMeasure, formatMeasureVariation } from '../../helpers/measures';
+export function isDiffMetric (metric) {
+ return metric.key.indexOf('new_') === 0;
+}
+
export function getLeakValue (measure) {
if (!measure) {
return null;
@@ -53,7 +57,7 @@ export function getSingleLeakValue (measures) {
}
export function formatLeak (value, metric) {
- if (metric.key.indexOf('new_') === 0) {
+ if (isDiffMetric(metric)) {
return formatMeasure(value, metric.type);
} else {
return formatMeasureVariation(value, metric.type);
@@ -80,8 +84,7 @@ export function enhanceWithSingleMeasure (components) {
value: getSingleMeasureValue(component.measures),
leak: getSingleLeakValue(component.measures)
};
- })
- .filter(component => component.value != null || component.leak != null);
+ });
}
export function hasHistory (metricKey) {
diff --git a/server/sonar-web/src/main/js/components/shared/list-footer.js b/server/sonar-web/src/main/js/components/shared/list-footer.js
index 5a501d7a8da..fd9392b05ab 100644
--- a/server/sonar-web/src/main/js/components/shared/list-footer.js
+++ b/server/sonar-web/src/main/js/components/shared/list-footer.js
@@ -41,6 +41,7 @@ export default React.createClass({
handleLoadMore(e) {
e.preventDefault();
+ e.target.blur();
if (this.canLoadMore()) {
this.props.loadMore();
}
diff --git a/server/sonar-web/src/main/js/components/source-viewer/main.js b/server/sonar-web/src/main/js/components/source-viewer/main.js
index c6cb2892ffd..58db3aa9ea9 100644
--- a/server/sonar-web/src/main/js/components/source-viewer/main.js
+++ b/server/sonar-web/src/main/js/components/source-viewer/main.js
@@ -108,6 +108,7 @@ export default Marionette.LayoutView.extend({
});
this.issueViews = [];
this.clearTooltips();
+ this.unbindScrollEvents();
},
clearTooltips () {
diff --git a/server/sonar-web/src/main/js/components/store/configureStore.js b/server/sonar-web/src/main/js/components/store/configureStore.js
index d7d69d3ecea..97397d836cd 100644
--- a/server/sonar-web/src/main/js/components/store/configureStore.js
+++ b/server/sonar-web/src/main/js/components/store/configureStore.js
@@ -35,6 +35,6 @@ const finalCreateStore = compose(
...composed
)(createStore);
-export default function configureStore (rootReducer) {
- return finalCreateStore(rootReducer);
+export default function configureStore (rootReducer, initialState) {
+ return finalCreateStore(rootReducer, initialState);
}
diff --git a/server/sonar-web/src/main/js/helpers/path.js b/server/sonar-web/src/main/js/helpers/path.js
index c0a16a4ea9e..a03c1421bf1 100644
--- a/server/sonar-web/src/main/js/helpers/path.js
+++ b/server/sonar-web/src/main/js/helpers/path.js
@@ -94,3 +94,16 @@ export function fileFromPath (path) {
return null;
}
}
+
+
+export function splitPath (path) {
+ if (typeof path === 'string') {
+ const tokens = path.split('/');
+ return {
+ head: _.initial(tokens).join('/'),
+ tail: _.last(tokens)
+ };
+ } else {
+ return null;
+ }
+}
diff --git a/server/sonar-web/src/main/js/libs/third-party/jquery-ui.js b/server/sonar-web/src/main/js/libs/third-party/jquery-ui.js
index 3062a520301..1645ce863df 100755
--- a/server/sonar-web/src/main/js/libs/third-party/jquery-ui.js
+++ b/server/sonar-web/src/main/js/libs/third-party/jquery-ui.js
@@ -493,7 +493,7 @@ $.widget.bridge = function( name, object ) {
args = slice.call( arguments, 1 ),
returnValue = this;
- // allow multiple hashes to be passed on init
+ // allow multiple hashes to be passed on start
options = !isMethodCall && args.length ?
$.widget.extend.apply( null, [ options ].concat(args) ) :
options;