diff options
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/> + + .. + </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}/> + + <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 |