diff options
author | Stas Vilchik <vilchiks@gmail.com> | 2016-03-21 18:28:05 +0100 |
---|---|---|
committer | Stas Vilchik <vilchiks@gmail.com> | 2016-03-24 09:16:41 +0100 |
commit | cc9c506efd50786649658baa0d243654c59c512e (patch) | |
tree | e36cc405f4e43fbb470312d911ddbf3eb1a85d6c /server/sonar-web/src/main/js | |
parent | 05f9e8ef050e34b9a53678f3cab0a0c4e2caca1e (diff) | |
download | sonarqube-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')
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/> .. - </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}/> - - <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} - - <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}/> + + {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}/> + + {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)}><</button> + )} + {hasNext && ( + <button onClick={blur(onSelectNext)}>></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; |