aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--server/sonar-web/src/main/js/api/components.js6
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/app.js64
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/AllMeasuresList.js118
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/ComponentMeasuresApp.js73
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/ComponentsList.js69
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/ListIcon.js35
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/MeasureDetails.js94
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/MeasureDrilldown.js66
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/MeasurePlainList.js122
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/MeasureTree.js140
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/NoResults.js30
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/Spinner.js26
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/TreeIcon.js35
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/UpIcon.js36
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/styles.css159
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/utils.js37
-rw-r--r--server/sonar-web/src/main/js/helpers/measures.js8
-rw-r--r--server/sonar-web/src/main/js/main/nav/component/component-nav-menu.js6
-rw-r--r--server/sonar-web/src/main/webapp/WEB-INF/app/controllers/component_measures_controller.rb28
-rw-r--r--server/sonar-web/src/main/webapp/WEB-INF/app/views/component_measures/index.html.erb3
-rw-r--r--server/sonar-web/src/main/webapp/WEB-INF/config/routes.rb1
-rw-r--r--server/sonar-web/tests/helpers/measures-test.js2
-rw-r--r--server/sonar-web/webpack.config.js1
-rw-r--r--sonar-core/src/main/resources/org/sonar/l10n/core.properties12
24 files changed, 1164 insertions, 7 deletions
diff --git a/server/sonar-web/src/main/js/api/components.js b/server/sonar-web/src/main/js/api/components.js
index 6b61a89fe42..e567727e3d1 100644
--- a/server/sonar-web/src/main/js/api/components.js
+++ b/server/sonar-web/src/main/js/api/components.js
@@ -45,13 +45,13 @@ export function createProject (data) {
return postJSON(url, data);
}
-export function getChildren (componentKey, metrics = []) {
+export function getChildren (componentKey, metrics = [], additional = {}) {
const url = '/api/measures/component_tree';
- const data = {
+ const data = Object.assign({}, additional, {
baseComponentKey: componentKey,
metricKeys: metrics.join(','),
strategy: 'children'
- };
+ });
return getJSON(url, data).then(r => r.components);
}
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
new file mode 100644
index 00000000000..d805e9b42f5
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/app.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 React from 'react';
+import { render } from 'react-dom';
+import { Router, Route, IndexRoute, Redirect, IndexRedirect, useRouterHistory } from 'react-router';
+import { createHistory } from 'history';
+
+import ComponentMeasuresApp from './components/ComponentMeasuresApp';
+import AllMeasuresList from './components/AllMeasuresList';
+import MeasureDetails from './components/MeasureDetails';
+import MeasureTree from './components/MeasureTree';
+import MeasurePlainList from './components/MeasurePlainList';
+
+import './styles.css';
+
+window.sonarqube.appStarted.then(options => {
+ const el = document.querySelector(options.el);
+
+ const history = useRouterHistory(createHistory)({
+ basename: '/component_measures'
+ });
+
+ const Container = (props) => (
+ <ComponentMeasuresApp {...props} component={options.component}/>
+ );
+
+ const handleRouteUpdate = () => {
+ window.scrollTo(0, 0);
+ };
+
+ render((
+ <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={MeasureTree}/>
+ <Route path="list" component={MeasurePlainList}/>
+ </Route>
+ </Route>
+
+ <Redirect from="*" to="/"/>
+ </Router>
+ ), el);
+});
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/AllMeasuresList.js b/server/sonar-web/src/main/js/apps/component-measures/components/AllMeasuresList.js
new file mode 100644
index 00000000000..64ff6335ee8
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/AllMeasuresList.js
@@ -0,0 +1,118 @@
+/*
+ * 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 { getMeasures } from '../../../api/measures';
+import { formatMeasure } from '../../../helpers/measures';
+
+export default class ComponentMeasuresApp 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);
+
+ getMeasures(component.key, metricKeys).then(measures => {
+ if (this.mounted) {
+ const measuresWithMetrics = measures
+ .map(measure => {
+ const metric = metrics.find(metric => metric.key === measure.metric);
+ return { ...measure, metric };
+ })
+ .filter(measure => measure.value != null);
+
+ this.setState({
+ measures: measuresWithMetrics,
+ fetching: false
+ });
+ }
+ });
+ }
+
+ render () {
+ const { fetching, measures } = 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');
+
+ return (
+ <ul className="component-measures-domains">
+ {domains.map(domain => (
+ <li key={domain.name}>
+ <h3 className="component-measures-domain-name">{domain.name}</h3>
+
+ <table className="data zebra">
+ <tbody>
+ {domain.measures.map(measure => (
+ <tr key={measure.metric.key}>
+ <td>
+ {measure.metric.name}
+ </td>
+ <td className="thin nowrap text-right">
+ {measure.value != null && (
+ <div style={{ width: 80 }}>
+ <Link to={{ pathname: measure.metric.key, query: { id: component.key } }}>
+ {formatMeasure(measure.value, measure.metric.type)}
+ </Link>
+ </div>
+ )}
+ </td>
+ </tr>
+ ))}
+ </tbody>
+ </table>
+ </li>
+ ))}
+ </ul>
+ );
+ }
+}
+
+ComponentMeasuresApp.contextTypes = {
+ component: React.PropTypes.object
+};
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/ComponentMeasuresApp.js b/server/sonar-web/src/main/js/apps/component-measures/components/ComponentMeasuresApp.js
new file mode 100644
index 00000000000..5a12088b4c3
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/ComponentMeasuresApp.js
@@ -0,0 +1,73 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import React from 'react';
+
+import Spinner from './Spinner';
+import { getMetrics } from '../../../api/metrics';
+
+export default class ComponentMeasuresApp extends React.Component {
+ state = {
+ fetching: true,
+ metrics: []
+ };
+
+ getChildContext () {
+ return {
+ component: this.props.component
+ };
+ }
+
+ componentDidMount () {
+ this.mounted = true;
+ this.fetchMetrics();
+ }
+
+ componentWillUnmount () {
+ this.mounted = false;
+ }
+
+ fetchMetrics () {
+ getMetrics().then(metrics => {
+ if (this.mounted) {
+ this.setState({ metrics, fetching: false });
+ }
+ });
+ }
+
+ render () {
+ const { fetching, metrics } = this.state;
+
+ if (fetching) {
+ return <Spinner/>;
+ }
+
+ const child = React.cloneElement(this.props.children, { metrics });
+
+ return (
+ <div id="component-measures">
+ {child}
+ </div>
+ );
+ }
+}
+
+ComponentMeasuresApp.childContextTypes = {
+ component: React.PropTypes.object
+};
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/ComponentsList.js b/server/sonar-web/src/main/js/apps/component-measures/components/ComponentsList.js
new file mode 100644
index 00000000000..de7b13632f1
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/ComponentsList.js
@@ -0,0 +1,69 @@
+/*
+ * 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 UpIcon from './UpIcon';
+import QualifierIcon from '../../../components/shared/qualifier-icon';
+import { formatMeasure } from '../../../helpers/measures';
+
+export default function ComponentsList ({ components, selected, parent, metric, onClick }) {
+ const handleClick = (component, e) => {
+ e.preventDefault();
+ e.target.blur();
+ onClick(component);
+ };
+
+ return (
+ <ul>
+ {parent && (
+ <li key={parent.id} className="measure-details-components-parent">
+ <a href="#" onClick={handleClick.bind(this, parent)}>
+ <div className="measure-details-component-name">
+ <UpIcon/>
+ &nbsp;
+ ..
+ </div>
+ </a>
+ </li>
+ )}
+ {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)
+ )}
+ </div>
+
+ </a>
+ </li>
+ ))}
+ </ul>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/ListIcon.js b/server/sonar-web/src/main/js/apps/component-measures/components/ListIcon.js
new file mode 100644
index 00000000000..6725685c36b
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/ListIcon.js
@@ -0,0 +1,35 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import React from 'react';
+
+export default function ListIcon () {
+ /* eslint max-len: 0 */
+ return (
+ <svg className="measure-tab-icon"
+ viewBox="0 0 448 448"
+ fillRule="evenodd"
+ clipRule="evenodd"
+ strokeLinejoin="round"
+ strokeMiterlimit="1.414">
+ <path
+ d="M448 48c0-8.83-7.17-16-16-16H16C7.17 32 0 39.17 0 48v32c0 8.83 7.17 16 16 16h416c8.83 0 16-7.17 16-16V48zM448 144c0-8.83-7.17-16-16-16H16c-8.83 0-16 7.17-16 16v32c0 8.83 7.17 16 16 16h416c8.83 0 16-7.17 16-16v-32zM448 240c0-8.83-7.17-16-16-16H16c-8.83 0-16 7.17-16 16v32c0 8.83 7.17 16 16 16h416c8.83 0 16-7.17 16-16v-32zM448 336.03c0-8.83-7.17-16-16-16H16c-8.83 0-16 7.17-16 16v32c0 8.83 7.17 16 16 16h416c8.83 0 16-7.17 16-16v-32z"/>
+ </svg>
+ );
+}
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
new file mode 100644
index 00000000000..e5a3bb2d426
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDetails.js
@@ -0,0 +1,94 @@
+/*
+ * 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 MeasureDrilldown from './MeasureDrilldown';
+import { getMeasures } from '../../../api/measures';
+import { formatMeasure } from '../../../helpers/measures';
+
+export default class MeasureDetails extends React.Component {
+ state = {};
+
+ 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;
+
+ getMeasures(component.key, [metricKey]).then(measures => {
+ if (this.mounted && measures.length === 1) {
+ this.setState({ measure: measures[0] });
+ }
+ });
+ }
+
+ render () {
+ const { metricKey, tab } = this.props.params;
+ const { metrics, children } = this.props;
+ const { measure } = this.state;
+ const metric = metrics.find(metric => metric.key === metricKey);
+ const finalTab = tab || 'tree';
+
+ if (!measure) {
+ return <Spinner/>;
+ }
+
+ return (
+ <div className="measure-details">
+ <h2 className="measure-details-metric">
+ {metric.name}
+ </h2>
+
+ {measure && (
+ <div className="measure-details-value">
+ {measure.value != null && (
+ formatMeasure(measure.value, metric.type)
+ )}
+ </div>
+ )}
+
+ {measure && (
+ <MeasureDrilldown metric={metric} tab={finalTab}>
+ {children}
+ </MeasureDrilldown>
+ )}
+ </div>
+ );
+ }
+}
+
+MeasureDetails.contextTypes = {
+ component: React.PropTypes.object
+};
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDrilldown.js b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDrilldown.js
new file mode 100644
index 00000000000..7d1ebec1867
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDrilldown.js
@@ -0,0 +1,66 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import React from 'react';
+import { Link } from 'react-router';
+
+import ListIcon from './ListIcon';
+import TreeIcon from './TreeIcon';
+import { translate } from '../../../helpers/l10n';
+
+export default class MeasureDrilldown extends React.Component {
+ render () {
+ const { metric, children } = this.props;
+ const { component } = this.context;
+
+ const child = React.cloneElement(children, {
+ component,
+ metric
+ });
+
+ return (
+ <div className="measure-details-drilldown">
+ <ul className="measure-details-mode">
+ <li>
+ <Link
+ activeClassName="active"
+ to={{ pathname: `${metric.key}/tree`, query: { id: component.key } }}>
+ <TreeIcon/>
+ {translate('component_measures.tab.tree')}
+ </Link>
+ </li>
+ <li>
+ <Link
+ activeClassName="active"
+ to={{ pathname: `${metric.key}/list`, query: { id: component.key } }}>
+ <ListIcon/>
+ {translate('component_measures.tab.list')}
+ </Link>
+ </li>
+ </ul>
+
+ {child}
+ </div>
+ );
+ }
+}
+
+MeasureDrilldown.contextTypes = {
+ component: React.PropTypes.object
+};
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasurePlainList.js b/server/sonar-web/src/main/js/apps/component-measures/components/MeasurePlainList.js
new file mode 100644
index 00000000000..45ad7256a86
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasurePlainList.js
@@ -0,0 +1,122 @@
+/*
+ * 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 ComponentsList from './ComponentsList';
+import SourceViewer from '../../code/components/SourceViewer';
+import NoResults from './NoResults';
+import { getSingleMeasureValue } from '../utils';
+import { getFiles } from '../../../api/components';
+
+export default class MeasurePlainList extends React.Component {
+ state = {
+ components: [],
+ selected: null,
+ fetching: true
+ };
+
+ componentDidMount () {
+ this.mounted = true;
+ 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 } = this.props;
+ const asc = metric.direction === 1;
+
+ const options = {
+ s: 'metric,name',
+ metricSort: metric.key,
+ asc
+ };
+
+ this.setState({ fetching: true });
+
+ getFiles(baseComponent.key, [metric.key], options).then(children => {
+ if (this.mounted) {
+ const componentsWithMappedMeasure = children
+ .map(component => {
+ return {
+ ...component,
+ value: getSingleMeasureValue(component.measures)
+ };
+ })
+ .filter(component => component.value != null);
+
+ this.setState({
+ components: componentsWithMappedMeasure,
+ selected: null,
+ fetching: false
+ });
+ }
+ });
+ }
+
+ handleFileClick (selected) {
+ this.setState({ selected });
+ }
+
+ render () {
+ const { metric } = this.props;
+ const { components, selected, fetching } = this.state;
+
+ if (fetching) {
+ return <Spinner/>;
+ }
+
+ if (!components.length) {
+ return <NoResults/>;
+ }
+
+ return (
+ <div className="measure-details-plain-list">
+ <div className="measure-details-components">
+ <ComponentsList
+ components={components}
+ selected={selected}
+ metric={metric}
+ onClick={this.handleFileClick.bind(this)}/>
+ </div>
+
+ {selected && (
+ <div className="measure-details-viewer">
+ <SourceViewer component={selected}/>
+ </div>
+ )}
+ </div>
+ );
+ }
+}
+
+MeasurePlainList.contextTypes = {
+ component: React.PropTypes.object
+};
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureTree.js b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureTree.js
new file mode 100644
index 00000000000..8e4ceb16542
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureTree.js
@@ -0,0 +1,140 @@
+/*
+ * 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 ComponentsList from './ComponentsList';
+import NoResults from './NoResults';
+import SourceViewer from '../../code/components/SourceViewer';
+import { getSingleMeasureValue } from '../utils';
+import { getChildren } from '../../../api/components';
+
+export default class MeasureTree extends React.Component {
+ state = {
+ components: [],
+ breadcrumbs: [],
+ selected: null,
+ fetching: true
+ };
+
+ componentDidMount () {
+ this.mounted = true;
+ 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 } = this.props;
+ const asc = metric.direction === 1;
+
+ const options = {
+ s: 'metric,name',
+ metricSort: metric.key,
+ asc
+ };
+
+ this.setState({ fetching: true });
+
+ getChildren(baseComponent.key, [metric.key], options).then(children => {
+ if (this.mounted) {
+ const componentsWithMappedMeasure = children
+ .map(component => {
+ return {
+ ...component,
+ value: getSingleMeasureValue(component.measures)
+ };
+ })
+ .filter(component => component.value != null);
+
+ const indexInBreadcrumbs = this.state.breadcrumbs.findIndex(component => component === baseComponent);
+ const breadcrumbs = indexInBreadcrumbs !== -1 ?
+ this.state.breadcrumbs.slice(0, indexInBreadcrumbs + 1) :
+ [...this.state.breadcrumbs, baseComponent];
+
+ this.setState({
+ baseComponent,
+ breadcrumbs,
+ components: componentsWithMappedMeasure,
+ selected: null,
+ fetching: false
+ });
+ }
+ });
+ }
+
+ handleFileClick (component) {
+ if (component.qualifier === 'FIL' || component.qualifier === 'UTS') {
+ this.handleFileOpen(component);
+ } else {
+ this.fetchComponents(component);
+ }
+ }
+
+ handleFileOpen (selected) {
+ this.setState({ selected });
+ }
+
+ render () {
+ const { metric } = this.props;
+ const { components, selected, breadcrumbs, fetching } = this.state;
+ const parent = breadcrumbs.length > 1 ? breadcrumbs[breadcrumbs.length - 2] : null;
+
+ if (fetching) {
+ return <Spinner/>;
+ }
+
+ if (!components.length) {
+ return <NoResults/>;
+ }
+
+ return (
+ <div className="measure-details-tree">
+ <div className="measure-details-components">
+ <ComponentsList
+ components={components}
+ selected={selected}
+ parent={parent}
+ metric={metric}
+ onClick={this.handleFileClick.bind(this)}/>
+ </div>
+
+ {selected && (
+ <div className="measure-details-viewer">
+ <SourceViewer component={selected}/>
+ </div>
+ )}
+ </div>
+ );
+ }
+}
+
+MeasureTree.contextTypes = {
+ component: React.PropTypes.object
+};
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/NoResults.js b/server/sonar-web/src/main/js/apps/component-measures/components/NoResults.js
new file mode 100644
index 00000000000..db8afedd0be
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/NoResults.js
@@ -0,0 +1,30 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import React from 'react';
+
+import { translate } from '../../../helpers/l10n';
+
+export default function NoResults () {
+ return (
+ <div className="measures-details-components-empty note">
+ {translate('no_results')}
+ </div>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/Spinner.js b/server/sonar-web/src/main/js/apps/component-measures/components/Spinner.js
new file mode 100644
index 00000000000..610b42e559c
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/Spinner.js
@@ -0,0 +1,26 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import React from 'react';
+
+export default function Spinner () {
+ return (
+ <i className="spinner spinner-margin"/>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/TreeIcon.js b/server/sonar-web/src/main/js/apps/component-measures/components/TreeIcon.js
new file mode 100644
index 00000000000..cd8d28d0313
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/TreeIcon.js
@@ -0,0 +1,35 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import React from 'react';
+
+export default function TreeIcon () {
+ /* eslint max-len: 0 */
+ return (
+ <svg className="measure-tab-icon"
+ viewBox="0 0 448 448"
+ fillRule="evenodd"
+ clipRule="evenodd"
+ strokeLinejoin="round"
+ strokeMiterlimit="1.414">
+ <path
+ d="M448 48c0-8.83-7.17-16-16-16H16C7.17 32 0 39.17 0 48v32c0 8.83 7.17 16 16 16h416c8.83 0 16-7.17 16-16V48zM448 144c0-8.83-6.146-16-13.714-16H77.714C70.144 128 64 135.17 64 144v32c0 8.83 6.145 16 13.714 16h356.572c7.568 0 13.714-7.17 13.714-16v-32zM448 240c0-8.83-5.12-16-11.428-16H139.428C133.12 224 128 231.17 128 240v32c0 8.83 5.12 16 11.428 16h297.144c6.307 0 11.428-7.17 11.428-16v-32zM448 336.03c0-8.83-4.097-16-9.142-16H201.143c-5.046 0-9.143 7.17-9.143 16v32c0 8.83 4.097 16 9.143 16h237.715c5.045 0 9.142-7.17 9.142-16v-32z"/>
+ </svg>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/UpIcon.js b/server/sonar-web/src/main/js/apps/component-measures/components/UpIcon.js
new file mode 100644
index 00000000000..69c2dd828a1
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/UpIcon.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 React from 'react';
+
+export default function UpIcon () {
+ /* eslint max-len: 0 */
+ 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>
+ );
+}
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
new file mode 100644
index 00000000000..8be9ce18d5f
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/styles.css
@@ -0,0 +1,159 @@
+.component-measures-domains {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ width: 980px;
+ margin: 20px auto -20px;
+}
+
+.component-measures-domains > li {
+ width: 300px;
+ margin-right: 40px;
+ margin-bottom: 40px;
+}
+
+.component-measures-domains > li:nth-child(3n) {
+ margin-right: 0;
+}
+
+.component-measures-domain-name {
+ margin-bottom: 8px;
+}
+
+.measure-details {
+ margin-top: 10px;
+}
+
+.measure-details-metric,
+.measure-details-value {
+ margin-left: 20px;
+ margin-right: 20px;
+}
+
+.measure-details-metric {
+ margin-bottom: 10px;
+}
+
+.measure-details-value {
+ font-size: 24px;
+ font-weight: 300;
+}
+
+.measure-details-drilldown {
+ margin-top: 20px;
+}
+
+.measure-details-mode {
+ display: flex;
+ padding-left: 10px;
+ padding-right: 10px;
+ border-bottom: 1px solid #e6e6e6;
+}
+
+.measure-details-mode > li {
+ margin-bottom: -1px;
+}
+
+.measure-details-mode > li + li {
+ margin-left: 2px;
+}
+
+.measure-details-mode > li > a {
+ display: inline-block;
+ padding: 5px 10px;
+ border-bottom: 2px solid transparent;
+ color: #444;
+}
+
+.measure-details-mode > li > a:hover,
+.measure-details-mode > li > a.active {
+ border-bottom-color: #4b9fd5;
+}
+
+.measure-details-mode > li > a.active .measure-tab-icon path {
+ fill: #4b9fd5;
+}
+
+.measure-details-breadcrumbs {
+ margin-bottom: 10px;
+}
+
+.measure-details-components {
+ width: 300px;
+ padding: 10px;
+ box-sizing: border-box;
+}
+
+.measure-details-components-parent {
+ padding-bottom: 6px;
+}
+
+.measure-details-components > ul > li > a {
+ display: flex;
+ padding-top: 5px;
+ padding-bottom: 5px;
+ border: none;
+ color: #444;
+}
+
+.measure-details-components > ul > li > a:hover,
+.measure-details-components > ul > li > a.selected {
+ background-color: #cae3f2 !important;
+}
+
+.measure-details-component-name,
+.measure-details-component-value {
+ padding-right: 10px;
+ box-sizing: border-box;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.measure-details-component-name {
+ width: calc(100% - 60px);
+ padding-left: 10px;
+}
+
+.measure-details-component-value {
+ width: 60px;
+ text-align: right;
+}
+
+.measure-details-tree,
+.measure-details-plain-list {
+ display: flex;
+}
+
+.measure-details-viewer {
+ width: calc(100% - 300px);
+ margin-top: -1px;
+ margin-bottom: -1px;
+ box-sizing: border-box;
+}
+
+.measure-tab-icon {
+ display: inline-block;
+ width: 14px;
+ height: 14px;
+ margin-right: 6px;
+}
+
+.measure-tab-icon path {
+ fill: #aeaeae;
+ 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: 20px;
+}
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
new file mode 100644
index 00000000000..1f3da160aa7
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/utils.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.
+ */
+export function getLeakValue (measure) {
+ if (!measure) {
+ return null;
+ }
+
+ const period = measure.periods ?
+ measure.periods.find(period => period.index === 1) : null;
+
+ return period ? period.value : null;
+}
+
+export function getSingleMeasureValue (measures) {
+ if (!measures || !measures.length) {
+ return null;
+ }
+
+ return measures[0].value;
+}
diff --git a/server/sonar-web/src/main/js/helpers/measures.js b/server/sonar-web/src/main/js/helpers/measures.js
index 360380ec65f..d5cf7214289 100644
--- a/server/sonar-web/src/main/js/helpers/measures.js
+++ b/server/sonar-web/src/main/js/helpers/measures.js
@@ -290,15 +290,17 @@ function shortDurationFormatter (value) {
}
function durationVariationFormatter (value) {
- if (value === 0) {
- return '0';
+ /* eslint eqeqeq: 0 */
+ if (value == 0) {
+ return '+0';
}
const formatted = durationFormatter(value);
return formatted[0] !== '-' ? '+' + formatted : formatted;
}
function shortDurationVariationFormatter (value) {
- if (value === 0) {
+ /* eslint eqeqeq: 0 */
+ if (value == 0) {
return '+0';
}
const formatted = shortDurationFormatter(value);
diff --git a/server/sonar-web/src/main/js/main/nav/component/component-nav-menu.js b/server/sonar-web/src/main/js/main/nav/component/component-nav-menu.js
index b7bc2bae782..84c4f277689 100644
--- a/server/sonar-web/src/main/js/main/nav/component/component-nav-menu.js
+++ b/server/sonar-web/src/main/js/main/nav/component/component-nav-menu.js
@@ -181,6 +181,11 @@ export default React.createClass({
return this.renderLink(url, translate('issues.page'), '/component_issues');
},
+ renderComponentMeasuresLink() {
+ const url = `/component_measures/?id=${encodeURIComponent(this.props.component.key)}`;
+ return this.renderLink(url, translate('layout.measures'), '/component_measures');
+ },
+
renderAdministration() {
let shouldShowAdministration =
this.props.conf.showActionPlans ||
@@ -340,6 +345,7 @@ export default React.createClass({
{this.renderCodeLink()}
{this.renderProjectsLink()}
{this.renderComponentIssuesLink()}
+ {this.renderComponentMeasuresLink()}
{this.renderTools()}
{this.renderAdministration()}
</ul>
diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/component_measures_controller.rb b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/component_measures_controller.rb
new file mode 100644
index 00000000000..d84d7cfacc6
--- /dev/null
+++ b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/component_measures_controller.rb
@@ -0,0 +1,28 @@
+#
+# SonarQube, open source software quality management tool.
+# Copyright (C) 2008-2014 SonarSource
+# mailto:contact AT sonarsource DOT com
+#
+# SonarQube 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.
+#
+# SonarQube 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.
+#
+class ComponentMeasuresController < ApplicationController
+ before_filter :init_resource_for_user_role
+
+ SECTION=Navigation::SECTION_RESOURCE
+
+ def index
+ end
+
+end
diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/component_measures/index.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/component_measures/index.html.erb
new file mode 100644
index 00000000000..48f2b4c0e95
--- /dev/null
+++ b/server/sonar-web/src/main/webapp/WEB-INF/app/views/component_measures/index.html.erb
@@ -0,0 +1,3 @@
+<% content_for :extra_script do %>
+ <script src="/js/bundles/component-measures.js?v=<%= sonar_version -%>"></script>
+<% end %>
diff --git a/server/sonar-web/src/main/webapp/WEB-INF/config/routes.rb b/server/sonar-web/src/main/webapp/WEB-INF/config/routes.rb
index efa07fcac91..77d8bcd6e14 100644
--- a/server/sonar-web/src/main/webapp/WEB-INF/config/routes.rb
+++ b/server/sonar-web/src/main/webapp/WEB-INF/config/routes.rb
@@ -34,6 +34,7 @@ ActionController::Routing::Routes.draw do |map|
map.connect 'web_api/*other', :controller => 'web_api', :action => 'index'
map.connect 'quality_gates/*other', :controller => 'quality_gates', :action => 'index'
map.connect 'overview/*other', :controller => 'overview', :action => 'index'
+ map.connect 'component_measures/*other', :controller => 'component_measures', :action => 'index'
map.connect 'account/update_notifications', :controller => 'account', :action => 'update_notifications'
map.connect 'account/*other', :controller => 'account', :action => 'index'
diff --git a/server/sonar-web/tests/helpers/measures-test.js b/server/sonar-web/tests/helpers/measures-test.js
index 3e4569916d7..0b27578c852 100644
--- a/server/sonar-web/tests/helpers/measures-test.js
+++ b/server/sonar-web/tests/helpers/measures-test.js
@@ -189,7 +189,7 @@ describe('Measures', function () {
});
it('should format WORK_DUR', function () {
- expect(formatMeasureVariation(0, 'WORK_DUR')).to.equal('0');
+ expect(formatMeasureVariation(0, 'WORK_DUR')).to.equal('+0');
expect(formatMeasureVariation(5 * ONE_DAY, 'WORK_DUR')).to.equal('+5d');
expect(formatMeasureVariation(2 * ONE_HOUR, 'WORK_DUR')).to.equal('+2h');
expect(formatMeasureVariation(ONE_MINUTE, 'WORK_DUR')).to.equal('+1min');
diff --git a/server/sonar-web/webpack.config.js b/server/sonar-web/webpack.config.js
index feeba035aca..9c721550c2c 100644
--- a/server/sonar-web/webpack.config.js
+++ b/server/sonar-web/webpack.config.js
@@ -30,6 +30,7 @@ module.exports = {
'code': './src/main/js/apps/code/app.js',
'coding-rules': './src/main/js/apps/coding-rules/app.js',
'component-issues': './src/main/js/apps/component-issues/app.js',
+ 'component-measures': './src/main/js/apps/component-measures/app.js',
'custom-measures': './src/main/js/apps/custom-measures/app.js',
'dashboard': './src/main/js/apps/dashboard/app.js',
'drilldown': './src/main/js/apps/drilldown/app.js',
diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
index c8280f737a4..dcab3bf5134 100644
--- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties
+++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
@@ -3232,3 +3232,15 @@ api_documentation.internal_tooltip=Use at your own risk; internal services are s
#
#------------------------------------------------------------------------------
code.open_component_page=Open Component's Page
+
+
+#------------------------------------------------------------------------------
+#
+# COMPONENT MEASURES
+#
+#------------------------------------------------------------------------------
+component_measures.tab.tree=Tree
+component_measures.tab.list=List
+component_measures.tab.bubbles=Bubble Chart
+component_measures.tab.treemap=Treemap
+component_measures.tab.history=History