diff options
24 files changed, 671 insertions, 522 deletions
diff --git a/server/sonar-web/src/main/js/app/store/rootReducer.js b/server/sonar-web/src/main/js/app/store/rootReducer.js index 102ae24d908..bac98bab0e9 100644 --- a/server/sonar-web/src/main/js/app/store/rootReducer.js +++ b/server/sonar-web/src/main/js/app/store/rootReducer.js @@ -84,3 +84,7 @@ export const getProjectsAppState = state => ( export const getProjectsAppFilterStatus = (state, key) => ( fromProjectsApp.getFilterStatus(state.projectsApp, key) ); + +export const getProjectsAppFacetByProperty = (state, property) => ( + fromProjectsApp.getFacetByProperty(state.projectsApp, property) +); diff --git a/server/sonar-web/src/main/js/apps/projects/components/App.js b/server/sonar-web/src/main/js/apps/projects/components/App.js index b6abec6f45f..c86cd6935df 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/App.js +++ b/server/sonar-web/src/main/js/apps/projects/components/App.js @@ -22,7 +22,7 @@ import Helmet from 'react-helmet'; import PageHeaderContainer from './PageHeaderContainer'; import ProjectsListContainer from './ProjectsListContainer'; import ProjectsListFooterContainer from './ProjectsListFooterContainer'; -import PageSidebar from './PageSidebar'; +import PageSidebarContainer from './PageSidebarContainer'; import GlobalMessagesContainer from '../../../app/components/GlobalMessagesContainer'; import { parseUrlQuery } from '../store/utils'; import '../styles.css'; @@ -73,7 +73,7 @@ export default class App extends React.Component { <ProjectsListFooterContainer query={this.state.query}/> </div> <aside className="page-sidebar-fixed"> - <PageSidebar query={this.state.query}/> + <PageSidebarContainer query={this.state.query}/> </aside> </div> </div> diff --git a/server/sonar-web/src/main/js/apps/projects/components/PageSidebar.js b/server/sonar-web/src/main/js/apps/projects/components/PageSidebar.js index c250d97c141..705ade84aea 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/PageSidebar.js +++ b/server/sonar-web/src/main/js/apps/projects/components/PageSidebar.js @@ -18,19 +18,42 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import React from 'react'; -import CoverageFilterContainer from '../filters/CoverageFilterContainer'; -import DuplicationsFilterContainer from '../filters/DuplicationsFilterContainer'; -import SizeFilterContainer from '../filters/SizeFilterContainer'; -import QualityGateFilterContainer from '../filters/QualityGateFilterContainer'; +import { Link } from 'react-router'; +import CoverageFilter from '../filters/CoverageFilter'; +import DuplicationsFilter from '../filters/DuplicationsFilter'; +import SizeFilter from '../filters/SizeFilter'; +import QualityGateFilter from '../filters/QualityGateFilter'; +import ReliabilityFilter from '../filters/ReliabilityFilter'; +import SecurityFilter from '../filters/SecurityFilter'; +import MaintainabilityFilter from '../filters/MaintainabilityFilter'; +import { translate } from '../../../helpers/l10n'; export default class PageSidebar extends React.Component { + static propTypes = { + query: React.PropTypes.object.isRequired, + closeAllFilters: React.PropTypes.func.isRequired + }; + render () { + const isFiltered = Object.keys(this.props.query).some(key => this.props.query[key] != null); + return ( - <div> - <CoverageFilterContainer query={this.props.query}/> - <DuplicationsFilterContainer query={this.props.query}/> - <SizeFilterContainer query={this.props.query}/> - <QualityGateFilterContainer query={this.props.query}/> + <div className="search-navigator-facets-list"> + <ReliabilityFilter query={this.props.query}/> + <SecurityFilter query={this.props.query}/> + <MaintainabilityFilter query={this.props.query}/> + <CoverageFilter query={this.props.query}/> + <DuplicationsFilter query={this.props.query}/> + <SizeFilter query={this.props.query}/> + <QualityGateFilter query={this.props.query}/> + + {isFiltered && ( + <div className="projects-facets-reset"> + <Link to="/projects" className="button button-red" onClick={this.props.closeAllFilters}> + {translate('reset_verb')} + </Link> + </div> + )} </div> ); } diff --git a/server/sonar-web/src/main/js/apps/projects/filters/QualityGateFilterContainer.js b/server/sonar-web/src/main/js/apps/projects/components/PageSidebarContainer.js index 13bc5b67b01..c61e554c94b 100644 --- a/server/sonar-web/src/main/js/apps/projects/filters/QualityGateFilterContainer.js +++ b/server/sonar-web/src/main/js/apps/projects/components/PageSidebarContainer.js @@ -17,9 +17,11 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import QualityGateFilter from './QualityGateFilter'; -import connectFilter from './connectFilter'; +import { connect } from 'react-redux'; +import PageSidebar from './PageSidebar'; +import { closeAllFilters } from '../store/filters/statuses/actions'; -const getValue = query => query.gate; - -export default connectFilter('gate', getValue)(QualityGateFilter); +export default connect( + () => ({}), + { closeAllFilters } +)(PageSidebar); diff --git a/server/sonar-web/src/main/js/apps/projects/filters/CoverageFilter.js b/server/sonar-web/src/main/js/apps/projects/filters/CoverageFilter.js index b6bab069112..4ff3f821130 100644 --- a/server/sonar-web/src/main/js/apps/projects/filters/CoverageFilter.js +++ b/server/sonar-web/src/main/js/apps/projects/filters/CoverageFilter.js @@ -18,109 +18,36 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import React from 'react'; -import { Link } from 'react-router'; -import Filter from './Filter'; +import FilterContainer from './FilterContainer'; import CoverageRating from '../../../components/ui/CoverageRating'; -import { translate } from '../../../helpers/l10n'; +import { getCoverageRatingLabel, getCoverageRatingAverageValue } from '../../../helpers/ratings'; export default class CoverageFilter extends React.Component { - static propTypes = { - value: React.PropTypes.shape({ - from: React.PropTypes.number, - to: React.PropTypes.number - }), - getFilterUrl: React.PropTypes.func.isRequired, - toggleFilter: React.PropTypes.func.isRequired - }; - - isOptionAction (from, to) { - const { value } = this.props; - - if (value == null) { - return false; - } - - return value.from === from && value.to === to; - } - - renderLabel (value) { - let label; - if (value.to == null) { - label = '>' + value.from; - } else if (value.from == null) { - label = '<' + value.to; - } else { - label = value.from + '–' + value.to; - } - return label + '%'; - } - - renderValue () { - const { value } = this.props; - - let average; - if (value.to == null) { - average = value.from; - } else if (value.from == null) { - average = value.to / 2; - } else { - average = (value.from + value.to) / 2; - } - - const label = this.renderLabel(value); - + renderOption = option => { return ( - <div className="projects-filter-value"> - <CoverageRating value={average}/> - - <div className="projects-filter-hint note"> - {label} - </div> - </div> + <span> + <CoverageRating value={getCoverageRatingAverageValue(option)}/> + <span className="spacer-left"> + {getCoverageRatingLabel(option)} + </span> + </span> ); - } - - renderOptions () { - const options = [ - [null, 30, 15], - [30, 50, 40], - [50, 70, 60], - [70, 80, 75], - [80, null, 90], - ]; + }; - return ( - <div> - {options.map(option => ( - <Link key={option[2]} - className={this.isOptionAction(option[0], option[1]) ? 'active' : ''} - to={this.props.getFilterUrl({ 'coverage__gte': option[0], 'coverage__lt': option[1] })} - onClick={this.props.toggleFilter}> - <CoverageRating value={option[2]}/> - <span className="spacer-left">{this.renderLabel({ from: option[0], to: option[1] })}</span> - </Link> - ))} - {this.props.value != null && ( - <div> - <hr/> - <Link className="text-center" - to={this.props.getFilterUrl({ 'coverage__gte': null, 'coverage__lt': null })} - onClick={this.props.toggleFilter}> - <span className="text-danger">{translate('reset_verb')}</span> - </Link> - </div> - )} - </div> - ); - } + getFacetValueForOption = (facet, option) => { + const map = ['*-30.0', '30.0-50.0', '50.0-70.0', '70.0-80.0', '80.0-*']; + return facet[map[option - 1]]; + }; render () { return ( - <Filter + <FilterContainer + property="coverage" + options={[1, 2, 3, 4, 5]} renderName={() => 'Coverage'} - renderOptions={() => this.renderOptions()} - renderValue={() => this.renderValue()} - {...this.props}/> + renderOption={this.renderOption} + getFacetValueForOption={this.getFacetValueForOption} + query={this.props.query}/> ); } } diff --git a/server/sonar-web/src/main/js/apps/projects/filters/DuplicationsFilter.js b/server/sonar-web/src/main/js/apps/projects/filters/DuplicationsFilter.js index b44b437133b..c74cd364838 100644 --- a/server/sonar-web/src/main/js/apps/projects/filters/DuplicationsFilter.js +++ b/server/sonar-web/src/main/js/apps/projects/filters/DuplicationsFilter.js @@ -18,109 +18,36 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import React from 'react'; -import { Link } from 'react-router'; -import Filter from './Filter'; +import FilterContainer from './FilterContainer'; import DuplicationsRating from '../../../components/ui/DuplicationsRating'; -import { translate } from '../../../helpers/l10n'; +import { getDuplicationsRatingLabel, getDuplicationsRatingAverageValue } from '../../../helpers/ratings'; export default class DuplicationsFilter extends React.Component { - static propTypes = { - value: React.PropTypes.shape({ - from: React.PropTypes.number, - to: React.PropTypes.number - }), - getFilterUrl: React.PropTypes.func.isRequired, - toggleFilter: React.PropTypes.func.isRequired - }; - - isOptionAction (from, to) { - const { value } = this.props; - - if (value == null) { - return false; - } - - return value.from === from && value.to === to; - } - - renderLabel (value) { - let label; - if (value.to == null) { - label = '>' + value.from; - } else if (value.from == null) { - label = '<' + value.to; - } else { - label = value.from + '–' + value.to; - } - return label + '%'; - } - - renderValue () { - const { value } = this.props; - - let average; - if (value.to == null) { - average = value.from; - } else if (value.from == null) { - average = value.to / 2; - } else { - average = (value.from + value.to) / 2; - } - - const label = this.renderLabel(value); - + renderOption = option => { return ( - <div className="projects-filter-value"> - <DuplicationsRating value={average}/> - - <div className="projects-filter-hint note"> - {label} - </div> - </div> + <span> + <DuplicationsRating value={getDuplicationsRatingAverageValue(option)}/> + <span className="spacer-left"> + {getDuplicationsRatingLabel(option)} + </span> + </span> ); - } - - renderOptions () { - const options = [ - [null, 3, 1.5], - [3, 5, 4], - [5, 10, 7.5], - [10, 20, 15], - [20, null, 30], - ]; + }; - return ( - <div> - {options.map(option => ( - <Link key={option[2]} - className={this.isOptionAction(option[0], option[1]) ? 'active' : ''} - to={this.props.getFilterUrl({ 'duplications__gte': option[0], 'duplications__lt': option[1] })} - onClick={this.props.toggleFilter}> - <DuplicationsRating value={option[2]}/> - <span className="spacer-left">{this.renderLabel({ from: option[0], to: option[1] })}</span> - </Link> - ))} - {this.props.value != null && ( - <div> - <hr/> - <Link className="text-center" - to={this.props.getFilterUrl({ 'duplications__gte': null, 'duplications__lt': null })} - onClick={this.props.toggleFilter}> - <span className="text-danger">{translate('reset_verb')}</span> - </Link> - </div> - )} - </div> - ); - } + getFacetValueForOption = (facet, option) => { + const map = ['*-3.0', '3.0-5.0', '5.0-10.0', '10.0-20.0', '20.0-*']; + return facet[map[option - 1]]; + }; render () { return ( - <Filter + <FilterContainer + property="duplications" + options={[1, 2, 3, 4, 5]} renderName={() => 'Duplications'} - renderOptions={() => this.renderOptions()} - renderValue={() => this.renderValue()} - {...this.props}/> + renderOption={this.renderOption} + getFacetValueForOption={this.getFacetValueForOption} + query={this.props.query}/> ); } } diff --git a/server/sonar-web/src/main/js/apps/projects/filters/Filter.js b/server/sonar-web/src/main/js/apps/projects/filters/Filter.js index 5295b5628c8..72880998bfa 100644 --- a/server/sonar-web/src/main/js/apps/projects/filters/Filter.js +++ b/server/sonar-web/src/main/js/apps/projects/filters/Filter.js @@ -19,51 +19,117 @@ */ import React from 'react'; import classNames from 'classnames'; +import { Link } from 'react-router'; +import { formatMeasure } from '../../../helpers/measures'; export default class Filter extends React.Component { static propTypes = { - getFilterUrl: React.PropTypes.func.isRequired, isOpen: React.PropTypes.bool.isRequired, + value: React.PropTypes.any, + property: React.PropTypes.string.isRequired, + options: React.PropTypes.array.isRequired, + renderName: React.PropTypes.func.isRequired, - renderOptions: React.PropTypes.func.isRequired, - renderValue: React.PropTypes.func.isRequired, - toggleFilter: React.PropTypes.func.isRequired, - value: React.PropTypes.any + renderOption: React.PropTypes.func.isRequired, + + getFacetValueForOption: React.PropTypes.func, + + halfWidth: React.PropTypes.bool, + + getFilterUrl: React.PropTypes.func.isRequired, + openFilter: React.PropTypes.func.isRequired, + closeFilter: React.PropTypes.func.isRequired, + + router: React.PropTypes.object + }; + + static defaultProps = { + halfWidth: false }; - handleClick (e) { + handleHeaderClick = e => { e.preventDefault(); e.target.blur(); - this.props.toggleFilter(); + + const { value, isOpen, property } = this.props; + const hasValue = value != null; + const isDisplayedOpen = isOpen || hasValue; + + if (isDisplayedOpen) { + this.props.closeFilter(); + } else { + this.props.openFilter(); + } + + if (hasValue) { + this.props.router.push(this.props.getFilterUrl({ [property]: null })); + } + }; + + renderHeader () { + const { value, isOpen, renderName } = this.props; + const hasValue = value != null; + const checkboxClassName = classNames('icon-checkbox', { + 'icon-checkbox-checked': hasValue || isOpen + }); + + return ( + <a className="search-navigator-facet-header projects-facet-header" href="#" onClick={this.handleHeaderClick}> + <i className={checkboxClassName}/> {renderName()} + </a> + ); + } + + renderOption (option) { + const { property, value, facet, getFacetValueForOption } = this.props; + const className = classNames('facet', 'search-navigator-facet', 'projects-facet', { + active: option === value, + 'search-navigator-facet-half': this.props.halfWidth + }); + const path = this.props.getFilterUrl({ [property]: option }); + + const facetValue = (facet && getFacetValueForOption) ? getFacetValueForOption(facet, option) : null; + + return ( + <Link key={option} className={className} to={path}> + <span className="facet-name"> + {this.props.renderOption(option)} + </span> + {facetValue != null && ( + <span className="facet-stat"> + {formatMeasure(facetValue, 'SHORT_INT')} + </span> + )} + </Link> + ); + } + + renderOptions () { + const { value, isOpen, options } = this.props; + const hasValue = value != null; + + if (!hasValue && !isOpen) { + return null; + } + + return ( + <div className="search-navigator-facet-list"> + {options.map(option => this.renderOption(option))} + </div> + ); } render () { const { value, isOpen } = this.props; - const { renderName, renderOptions, renderValue } = this.props; - const className = classNames('projects-filter', { - 'projects-filter-active': value != null, - 'projects-filter-open': isOpen + const hasValue = value != null; + const className = classNames('search-navigator-facet-box', { + 'search-navigator-facet-box-collapsed': !hasValue && !isOpen }); return ( <div className={className}> - <a className="projects-filter-header clearfix" href="#" onClick={e => this.handleClick(e)}> - <div className="projects-filter-name"> - {renderName()} - {' '} - {!isOpen && ( - <i className="icon-dropdown"/> - )} - </div> - - {value != null && renderValue()} - </a> - - {isOpen && ( - <div className="projects-filter-options"> - {renderOptions()} - </div> - )} + {this.renderHeader()} + {this.renderOptions()} </div> ); } diff --git a/server/sonar-web/src/main/js/apps/projects/filters/connectFilter.js b/server/sonar-web/src/main/js/apps/projects/filters/FilterContainer.js index fed9b5a7980..70e2576e01a 100644 --- a/server/sonar-web/src/main/js/apps/projects/filters/connectFilter.js +++ b/server/sonar-web/src/main/js/apps/projects/filters/FilterContainer.js @@ -17,28 +17,28 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import Filter from './Filter'; import { connect } from 'react-redux'; +import { withRouter } from 'react-router'; import omitBy from 'lodash/omitBy'; import isNil from 'lodash/isNil'; -import { getProjectsAppFilterStatus } from '../../../app/store/rootReducer'; -import { toggleFilter } from '../store/filters/statuses/actions'; +import { getProjectsAppFilterStatus, getProjectsAppFacetByProperty } from '../../../app/store/rootReducer'; +import { openFilter, closeFilter } from '../store/filters/statuses/actions'; import { OPEN } from '../store/filters/statuses/reducer'; -const connectFilter = (key, getValue) => Component => { - const mapStateToProps = (state, ownProps) => ({ - isOpen: getProjectsAppFilterStatus(state, key) === OPEN, - value: getValue(ownProps.query), - getFilterUrl: part => { - const query = omitBy({ ...ownProps.query, ...part }, isNil); - return { pathname: '/projects', query }; - } - }); +const mapStateToProps = (state, ownProps) => ({ + isOpen: getProjectsAppFilterStatus(state, ownProps.property) === OPEN, + value: ownProps.query[ownProps.property], + facet: getProjectsAppFacetByProperty(state, ownProps.property), + getFilterUrl: part => { + const query = omitBy({ ...ownProps.query, ...part }, isNil); + return { pathname: '/projects', query }; + } +}); - const mapDispatchToProps = dispatch => ({ - toggleFilter: () => dispatch(toggleFilter(key)) - }); +const mapDispatchToProps = (dispatch, ownProps) => ({ + openFilter: () => dispatch(openFilter(ownProps.property)), + closeFilter: () => dispatch(closeFilter(ownProps.property)) +}); - return connect(mapStateToProps, mapDispatchToProps)(Component); -}; - -export default connectFilter; +export default connect(mapStateToProps, mapDispatchToProps)(withRouter(Filter)); diff --git a/server/sonar-web/src/main/js/apps/projects/filters/IssuesFilter.js b/server/sonar-web/src/main/js/apps/projects/filters/IssuesFilter.js new file mode 100644 index 00000000000..42199463b17 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/filters/IssuesFilter.js @@ -0,0 +1,47 @@ +/* + * 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 FilterContainer from './FilterContainer'; +import Rating from '../../../components/ui/Rating'; + +export default class IssuesFilter extends React.Component { + renderOption = option => { + return ( + <Rating value={option}/> + ); + }; + + getFacetValueForOption = (facet, option) => { + return facet[option]; + }; + + render () { + return ( + <FilterContainer + property={this.props.property} + options={[1, 4, 2, 5, 3]} + renderName={() => this.props.name} + renderOption={this.renderOption} + getFacetValueForOption={this.getFacetValueForOption} + halfWidth={true} + query={this.props.query}/> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projects/filters/CoverageFilterContainer.js b/server/sonar-web/src/main/js/apps/projects/filters/MaintainabilityFilter.js index ed2a55e8b04..fe96c1c3b4a 100644 --- a/server/sonar-web/src/main/js/apps/projects/filters/CoverageFilterContainer.js +++ b/server/sonar-web/src/main/js/apps/projects/filters/MaintainabilityFilter.js @@ -17,13 +17,16 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import CoverageFilter from './CoverageFilter'; -import connectFilter from './connectFilter'; +import React from 'react'; +import IssuesFilter from './IssuesFilter'; -const getValue = query => { - const from = query['coverage__gte']; - const to = query['coverage__lt']; - return from == null && to == null ? null : { from, to }; -}; - -export default connectFilter('coverage', getValue)(CoverageFilter); +export default class MaintainabilityFilter extends React.Component { + render () { + return ( + <IssuesFilter + {...this.props} + name="Maintainability" + property="maintainability"/> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projects/filters/QualityGateFilter.js b/server/sonar-web/src/main/js/apps/projects/filters/QualityGateFilter.js index 91b58140155..e236a62e22e 100644 --- a/server/sonar-web/src/main/js/apps/projects/filters/QualityGateFilter.js +++ b/server/sonar-web/src/main/js/apps/projects/filters/QualityGateFilter.js @@ -18,59 +18,29 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import React from 'react'; -import { Link } from 'react-router'; -import Filter from './Filter'; +import FilterContainer from './FilterContainer'; import Level from '../../../components/ui/Level'; -import { translate } from '../../../helpers/l10n'; export default class QualityGateFilter extends React.Component { - static propTypes = { - value: React.PropTypes.any, - getFilterUrl: React.PropTypes.func.isRequired, - toggleFilter: React.PropTypes.func.isRequired - }; - - renderValue () { + renderOption = option => { return ( - <div className="projects-filter-value"> - <Level level={this.props.value}/> - </div> + <Level level={option}/> ); - } - - renderOptions () { - const options = ['ERROR', 'WARN', 'OK']; + }; - return ( - <div> - {options.map(option => ( - <Link key={option} - className={option === this.props.value ? 'active' : ''} - to={this.props.getFilterUrl({ gate: option })} - onClick={this.props.toggleFilter}> - <Level level={option}/> - </Link> - ))} - {this.props.value != null && ( - <div> - <hr/> - <Link className="text-center" to={this.props.getFilterUrl({ gate: null })} - onClick={this.props.toggleFilter}> - <span className="text-danger">{translate('reset_verb')}</span> - </Link> - </div> - )} - </div> - ); - } + getFacetValueForOption = (facet, option) => { + return facet[option]; + }; render () { return ( - <Filter + <FilterContainer + property="gate" + options={['ERROR', 'WARN', 'OK']} renderName={() => 'Quality Gate'} - renderOptions={() => this.renderOptions()} - renderValue={() => this.renderValue()} - {...this.props}/> + renderOption={this.renderOption} + getFacetValueForOption={this.getFacetValueForOption} + query={this.props.query}/> ); } } diff --git a/server/sonar-web/src/main/js/apps/projects/filters/SizeFilterContainer.js b/server/sonar-web/src/main/js/apps/projects/filters/ReliabilityFilter.js index 3cf6fd50e44..ff1d2ed9ff7 100644 --- a/server/sonar-web/src/main/js/apps/projects/filters/SizeFilterContainer.js +++ b/server/sonar-web/src/main/js/apps/projects/filters/ReliabilityFilter.js @@ -17,13 +17,16 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import SizeFilter from './SizeFilter'; -import connectFilter from './connectFilter'; +import React from 'react'; +import IssuesFilter from './IssuesFilter'; -const getValue = query => { - const from = query['size__gte']; - const to = query['size__lt']; - return from == null && to == null ? null : { from, to }; -}; - -export default connectFilter('size', getValue)(SizeFilter); +export default class ReliabilityFilter extends React.Component { + render () { + return ( + <IssuesFilter + {...this.props} + name="Reliability" + property="reliability"/> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projects/filters/DuplicationsFilterContainer.js b/server/sonar-web/src/main/js/apps/projects/filters/SecurityFilter.js index eae923e3766..5d712edb88e 100644 --- a/server/sonar-web/src/main/js/apps/projects/filters/DuplicationsFilterContainer.js +++ b/server/sonar-web/src/main/js/apps/projects/filters/SecurityFilter.js @@ -17,13 +17,16 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import DuplicationsFilter from './DuplicationsFilter'; -import connectFilter from './connectFilter'; +import React from 'react'; +import IssuesFilter from './IssuesFilter'; -const getValue = query => { - const from = query['duplications__gte']; - const to = query['duplications__lt']; - return from == null && to == null ? null : { from, to }; -}; - -export default connectFilter('duplications', getValue)(DuplicationsFilter); +export default class SecurityFilter extends React.Component { + render () { + return ( + <IssuesFilter + {...this.props} + name="Security" + property="security"/> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projects/filters/SizeFilter.js b/server/sonar-web/src/main/js/apps/projects/filters/SizeFilter.js index 3bd702dd3e0..ed331913ac8 100644 --- a/server/sonar-web/src/main/js/apps/projects/filters/SizeFilter.js +++ b/server/sonar-web/src/main/js/apps/projects/filters/SizeFilter.js @@ -18,110 +18,36 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import React from 'react'; -import { Link } from 'react-router'; -import Filter from './Filter'; +import FilterContainer from './FilterContainer'; import SizeRating from '../../../components/ui/SizeRating'; -import { formatMeasure } from '../../../helpers/measures'; -import { translate } from '../../../helpers/l10n'; +import { getSizeRatingLabel, getSizeRatingAverageValue } from '../../../helpers/ratings'; export default class SizeFilter extends React.Component { - static propTypes = { - value: React.PropTypes.shape({ - from: React.PropTypes.number, - to: React.PropTypes.number - }), - getFilterUrl: React.PropTypes.func.isRequired, - toggleFilter: React.PropTypes.func.isRequired - }; - - isOptionAction (from, to) { - const { value } = this.props; - - if (value == null) { - return false; - } - - return value.from === from && value.to === to; - } - - renderLabel (value) { - let label; - if (value.to == null) { - label = '>' + formatMeasure(value.from, 'SHORT_INT'); - } else if (value.from == null) { - label = '<' + formatMeasure(value.to, 'SHORT_INT'); - } else { - label = formatMeasure(value.from, 'SHORT_INT') + '–' + formatMeasure(value.to, 'SHORT_INT'); - } - return label; - } - - renderValue () { - const { value } = this.props; - - let average; - if (value.to == null) { - average = value.from; - } else if (value.from == null) { - average = value.to / 2; - } else { - average = (value.from + value.to) / 2; - } - - const label = this.renderLabel(value); - + renderOption = option => { return ( - <div className="projects-filter-value"> - <SizeRating value={average}/> - - <div className="projects-filter-hint note"> - {label} - </div> - </div> + <span> + <SizeRating value={getSizeRatingAverageValue(option)}/> + <span className="spacer-left"> + {getSizeRatingLabel(option)} + </span> + </span> ); - } - - renderOptions () { - const options = [ - [null, 1000, 0], - [1000, 10000, 1000], - [10000, 100000, 10000], - [100000, 500000, 100000], - [500000, null, 500000], - ]; + }; - return ( - <div> - {options.map(option => ( - <Link key={option[2]} - className={this.isOptionAction(option[0], option[1]) ? 'active' : ''} - to={this.props.getFilterUrl({ 'size__gte': option[0], 'size__lt': option[1] })} - onClick={this.props.toggleFilter}> - <SizeRating value={option[2]}/> - <span className="spacer-left">{this.renderLabel({ from: option[0], to: option[1] })}</span> - </Link> - ))} - {this.props.value != null && ( - <div> - <hr/> - <Link className="text-center" - to={this.props.getFilterUrl({ 'size__gte': null, 'size__lt': null })} - onClick={this.props.toggleFilter}> - <span className="text-danger">{translate('reset_verb')}</span> - </Link> - </div> - )} - </div> - ); - } + getFacetValueForOption = (facet, option) => { + const map = ['*-1000.0', '1000.0-10000.0', '10000.0-100000.0', '100000.0-500000.0', '500000.0-*']; + return facet[map[option - 1]]; + }; render () { return ( - <Filter + <FilterContainer + property="size" + options={[1, 2, 3, 4, 5]} renderName={() => 'Size'} - renderOptions={() => this.renderOptions()} - renderValue={() => this.renderValue()} - {...this.props}/> + renderOption={this.renderOption} + getFacetValueForOption={this.getFacetValueForOption} + query={this.props.query}/> ); } } diff --git a/server/sonar-web/src/main/js/apps/projects/store/actions.js b/server/sonar-web/src/main/js/apps/projects/store/actions.js index 5c0f3f8b2e6..93dbc409a4d 100644 --- a/server/sonar-web/src/main/js/apps/projects/store/actions.js +++ b/server/sonar-web/src/main/js/apps/projects/store/actions.js @@ -42,6 +42,16 @@ const METRICS = [ 'ncloc_language_distribution' ]; +const FACETS = [ + 'reliability_rating', + 'security_rating', + 'sqale_rating', + 'coverage', + 'duplicated_lines_density', + 'ncloc', + 'alert_status' +]; + const onFail = dispatch => error => { parseError(error).then(message => dispatch(addGlobalErrorMessage(message))); dispatch(updateState({ loading: false })); @@ -74,7 +84,7 @@ const fetchProjectMeasures = projects => dispatch => { const onReceiveProjects = dispatch => response => { dispatch(receiveComponents(response.components)); - dispatch(receiveProjects(response.components)); + dispatch(receiveProjects(response.components, response.facets)); dispatch(fetchProjectMeasures(response.components)).then(() => { dispatch(updateState({ loading: false })); }); @@ -95,7 +105,7 @@ const onReceiveMoreProjects = dispatch => response => { export const fetchProjects = query => dispatch => { dispatch(updateState({ loading: true })); - const data = { ps: PAGE_SIZE }; + const data = { ps: PAGE_SIZE, facets: FACETS.join() }; const filter = convertToFilter(query); if (filter) { data.filter = filter; diff --git a/server/sonar-web/src/main/js/apps/projects/store/facets/reducer.js b/server/sonar-web/src/main/js/apps/projects/store/facets/reducer.js new file mode 100644 index 00000000000..8207424a6ab --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/store/facets/reducer.js @@ -0,0 +1,52 @@ +/* + * 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 { createMap } from '../../../../components/store/generalReducers'; +import { RECEIVE_PROJECTS } from '../projects/actions'; +import { mapMetricToProperty } from '../utils'; + +const mapFacetValues = values => { + const map = {}; + values.forEach(value => { + map[value.val] = value.count; + }); + return map; +}; + +const getFacetsMap = facets => { + const map = {}; + facets.forEach(facet => { + const property = mapMetricToProperty(facet.property); + map[property] = mapFacetValues(facet.values); + }); + return map; +}; + +const reducer = createMap( + (state, action) => action.type === RECEIVE_PROJECTS, + () => false, + (state, action) => getFacetsMap(action.facets) +); + +export default reducer; + +export const getFacetByProperty = (state, property) => ( + state[property] +); + diff --git a/server/sonar-web/src/main/js/apps/projects/store/filters/statuses/actions.js b/server/sonar-web/src/main/js/apps/projects/store/filters/statuses/actions.js index d21b9bd4505..d67bbc2bf57 100644 --- a/server/sonar-web/src/main/js/apps/projects/store/filters/statuses/actions.js +++ b/server/sonar-web/src/main/js/apps/projects/store/filters/statuses/actions.js @@ -17,9 +17,22 @@ * 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 TOGGLE_FILTER = 'projects/TOGGLE_FILTER'; +export const OPEN_FILTER = 'projects/OPEN_FILTER'; -export const toggleFilter = key => ({ - type: TOGGLE_FILTER, +export const openFilter = key => ({ + type: OPEN_FILTER, key }); + +export const CLOSE_FILTER = 'projects/CLOSE_FILTER'; + +export const closeFilter = key => ({ + type: CLOSE_FILTER, + key +}); + +export const CLOSE_ALL_FILTERS = 'projects/CLOSE_ALL_FILTERS'; + +export const closeAllFilters = () => ({ + type: CLOSE_ALL_FILTERS +}); diff --git a/server/sonar-web/src/main/js/apps/projects/store/filters/statuses/reducer.js b/server/sonar-web/src/main/js/apps/projects/store/filters/statuses/reducer.js index baf76f152a1..6606f476a7b 100644 --- a/server/sonar-web/src/main/js/apps/projects/store/filters/statuses/reducer.js +++ b/server/sonar-web/src/main/js/apps/projects/store/filters/statuses/reducer.js @@ -17,24 +17,28 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { TOGGLE_FILTER } from './actions'; +import { OPEN_FILTER, CLOSE_FILTER, CLOSE_ALL_FILTERS } from './actions'; export const OPEN = 'OPEN'; export const CLOSED = 'CLOSED'; -const allClosedState = { - coverage: CLOSED, - duplications: CLOSED, - size: CLOSED +const closeAll = state => { + const newState = { ...state }; + Object.keys(newState).forEach(key => newState[key] = CLOSED); + return newState; }; -const reducer = (state = allClosedState, action = {}) => { - if (action.type === TOGGLE_FILTER) { - const newStatus = state[action.key] === OPEN ? CLOSED : OPEN; - return { ...allClosedState, [action.key]: newStatus }; +const reducer = (state = {}, action = {}) => { + switch (action.type) { + case OPEN_FILTER: + return { ...state, [action.key]: OPEN }; + case CLOSE_FILTER: + return { ...state, [action.key]: CLOSED }; + case CLOSE_ALL_FILTERS: + return closeAll(state); + default: + return state; } - - return state; }; export default reducer; diff --git a/server/sonar-web/src/main/js/apps/projects/store/projects/actions.js b/server/sonar-web/src/main/js/apps/projects/store/projects/actions.js index 0fe328cd60e..ed411c05a38 100644 --- a/server/sonar-web/src/main/js/apps/projects/store/projects/actions.js +++ b/server/sonar-web/src/main/js/apps/projects/store/projects/actions.js @@ -19,9 +19,10 @@ */ export const RECEIVE_PROJECTS = 'projects/RECEIVE_PROJECTS'; -export const receiveProjects = projects => ({ +export const receiveProjects = (projects, facets) => ({ type: RECEIVE_PROJECTS, - projects + projects, + facets }); export const RECEIVE_MORE_PROJECTS = 'projects/RECEIVE_MORE_PROJECTS'; diff --git a/server/sonar-web/src/main/js/apps/projects/store/reducer.js b/server/sonar-web/src/main/js/apps/projects/store/reducer.js index 4837b84ee04..9735bfca1aa 100644 --- a/server/sonar-web/src/main/js/apps/projects/store/reducer.js +++ b/server/sonar-web/src/main/js/apps/projects/store/reducer.js @@ -21,8 +21,9 @@ import { combineReducers } from 'redux'; import projects, * as fromProjects from './projects/reducer'; import state from './state/reducer'; import filters, * as fromFilters from './filters/reducer'; +import facets, * as fromFacets from './facets/reducer'; -export default combineReducers({ projects, state, filters }); +export default combineReducers({ projects, state, filters, facets }); export const getProjects = state => ( fromProjects.getProjects(state.projects) @@ -35,3 +36,7 @@ export const getState = state => ( export const getFilterStatus = (state, key) => ( fromFilters.getFilterStatus(state.filters, key) ); + +export const getFacetByProperty = (state, property) => ( + fromFacets.getFacetByProperty(state.facets, property) +); diff --git a/server/sonar-web/src/main/js/apps/projects/store/utils.js b/server/sonar-web/src/main/js/apps/projects/store/utils.js index 110dc3fa5d6..ac937893b18 100644 --- a/server/sonar-web/src/main/js/apps/projects/store/utils.js +++ b/server/sonar-web/src/main/js/apps/projects/store/utils.js @@ -17,11 +17,12 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -const getAsNumber = value => { - if (value === '' || value == null) { +const getAsNumericRating = value => { + if (value === '' || value == null || isNaN(value)) { return null; } - return isNaN(value) ? null : Number(value); + const num = Number(value); + return (num > 0 && num < 6) ? num : null; }; const getAsLevel = value => { @@ -33,16 +34,64 @@ const getAsLevel = value => { export const parseUrlQuery = urlQuery => ({ 'gate': getAsLevel(urlQuery['gate']), + 'reliability': getAsNumericRating(urlQuery['reliability']), + 'security': getAsNumericRating(urlQuery['security']), + 'maintainability': getAsNumericRating(urlQuery['maintainability']), + 'coverage': getAsNumericRating(urlQuery['coverage']), + 'duplications': getAsNumericRating(urlQuery['duplications']), + 'size': getAsNumericRating(urlQuery['size']), +}); - 'coverage__gte': getAsNumber(urlQuery['coverage__gte']), - 'coverage__lt': getAsNumber(urlQuery['coverage__lt']), +const convertCoverage = coverage => { + switch (coverage) { + case 1: + return 'coverage < 30'; + case 2: + return 'coverage >= 30 and coverage < 50'; + case 3: + return 'coverage >= 50 and coverage < 70'; + case 4: + return 'coverage >= 70 and coverage < 80'; + case 5: + return 'coverage >= 80'; + default: + return ''; + } +}; - 'duplications__gte': getAsNumber(urlQuery['duplications__gte']), - 'duplications__lt': getAsNumber(urlQuery['duplications__lt']), +const convertDuplications = duplications => { + switch (duplications) { + case 1: + return 'duplicated_lines_density < 3'; + case 2: + return 'duplicated_lines_density >= 3 and duplicated_lines_density < 5'; + case 3: + return 'duplicated_lines_density >= 5 and duplicated_lines_density < 10'; + case 4: + return 'duplicated_lines_density >= 10 duplicated_lines_density < 20'; + case 5: + return 'duplicated_lines_density >= 20'; + default: + return ''; + } +}; - 'size__gte': getAsNumber(urlQuery['size__gte']), - 'size__lt': getAsNumber(urlQuery['size__lt']) -}); +const convertSize = size => { + switch (size) { + case 1: + return 'ncloc < 1000'; + case 2: + return 'ncloc >= 1000 and ncloc < 10000'; + case 3: + return 'ncloc >= 10000 and ncloc < 100000'; + case 4: + return 'ncloc >= 100000 ncloc < 500000'; + case 5: + return 'ncloc >= 500000'; + default: + return ''; + } +}; export const convertToFilter = query => { const conditions = []; @@ -51,29 +100,42 @@ export const convertToFilter = query => { conditions.push('alert_status = ' + query['gate']); } - if (query['coverage__gte'] != null) { - conditions.push('coverage >= ' + query['coverage__gte']); + if (query['coverage'] != null) { + conditions.push(convertCoverage(query['coverage'])); } - if (query['coverage__lt'] != null) { - conditions.push('coverage < ' + query['coverage__lt']); + if (query['duplications'] != null) { + conditions.push(convertDuplications(query['duplications'])); } - if (query['duplications__gte'] != null) { - conditions.push('duplicated_lines_density >= ' + query['duplications__gte']); + if (query['size'] != null) { + conditions.push(convertSize(query['size'])); } - if (query['duplications__lt'] != null) { - conditions.push('duplicated_lines_density < ' + query['duplications__lt']); + if (query['reliability'] != null) { + conditions.push('reliability_rating = ' + query['reliability']); } - if (query['size__gte'] != null) { - conditions.push('ncloc >= ' + query['size__gte']); + if (query['security'] != null) { + conditions.push('security_rating = ' + query['security']); } - if (query['size__lt'] != null) { - conditions.push('ncloc < ' + query['size__lt']); + if (query['maintainability'] != null) { + conditions.push('sqale_rating = ' + query['maintainability']); } return conditions.join(' and '); }; + +export const mapMetricToProperty = metricKey => { + const map = { + 'reliability_rating': 'reliability', + 'security_rating': 'security', + 'sqale_rating': 'maintainability', + 'coverage': 'coverage', + 'duplicated_lines_density': 'duplications', + 'ncloc': 'size', + 'alert_status': 'gate' + }; + return map[metricKey]; +}; diff --git a/server/sonar-web/src/main/js/apps/projects/styles.css b/server/sonar-web/src/main/js/apps/projects/styles.css index 8c5913e0d4d..e1a4cc1245b 100644 --- a/server/sonar-web/src/main/js/apps/projects/styles.css +++ b/server/sonar-web/src/main/js/apps/projects/styles.css @@ -67,81 +67,24 @@ font-size: 12px; } -.projects-filter { - border: 1px solid transparent; - border-top-color: #e6e6e6; -} - -.projects-filter:first-child { - border-top-color: transparent; -} - -.projects-filter-active .projects-filter-name { - font-weight: 600; -} - -.projects-filter-open { - border-color: #e6e6e6 !important; - background-color: #fff; -} - -.projects-filter-open + .projects-filter { - border-top-color: transparent; -} - -.projects-filter-open .projects-filter-header { - border-bottom: 1px solid #e6e6e6; +.projects-facet-header { + padding-top: 10px; + padding-bottom: 10px; transition: none; } -.projects-filter-header { - display: block; - padding: 15px; - color: #444; - border: none; -} - -.projects-filter-checkbox { - float: left; - padding: 4px 4px 4px 0; -} - -.projects-filter-name { - float: left; - padding: 4px; - line-height: 16px; -} - -.projects-filter-hint { - float: left; - margin-right: 6px; - padding: 4px; - line-height: 16px; -} - -.projects-filter-value { - float: right; -} - -.projects-filter-options { - padding-top: 5px; - padding-bottom: 5px; +.projects-facet .facet-name, +.projects-facet .facet-stat { + line-height: 24px !important; } -.projects-filter-options a { - display: block; - padding: 5px 15px; - line-height: 24px; - border: none; - color: #444; +.projects-facets-reset { + margin-top: 20px; + padding: 10px; + border-top: 1px solid #e6e6e6; + text-align: center; } -.projects-filter-options a:hover, -.projects-filter-options a:active, -.projects-filter-options a:focus { - background-color: #f3f3f3; -} +.projects-facets-reset .button { -.projects-filter-options a.active { - background-color: #cae3f2; } diff --git a/server/sonar-web/src/main/js/components/store/generalReducers.js b/server/sonar-web/src/main/js/components/store/generalReducers.js new file mode 100644 index 00000000000..8f88ff41b0b --- /dev/null +++ b/server/sonar-web/src/main/js/components/store/generalReducers.js @@ -0,0 +1,95 @@ +/* + * 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. + */ +// Author: Christoffer Niska <christofferniska@gmail.com> +// https://gist.github.com/crisu83/42ecffccad9d04c74605fbc75c9dc9d1 +import uniq from 'lodash/uniq'; + +/** + * Creates a reducer that manages a single value. + * + * @param {function(state, action)} shouldUpdate + * @param {function(state, action)} shouldReset + * @param {function(state, action)} getValue + * @param {*} defaultValue + * @returns {function(state, action)} + */ +export const createValue = ( + shouldUpdate = () => true, + shouldReset = () => false, + getValue = (state, action) => action.payload, + defaultValue = null +) => (state = defaultValue, action = {}) => { + if (shouldReset(state, action)) { + return defaultValue; + } + if (shouldUpdate(state, action)) { + return getValue(state, action); + } + return state; +}; + +/** + * Creates a reducer that manages a map. + * + * @param {function(state, action)} shouldUpdate + * @param {function(state, action)} shouldReset + * @param {function(state, action)} getValues + * @returns {function(state, action)} + */ +export const createMap = ( + shouldUpdate = () => true, + shouldReset = () => false, + getValues = (state, action) => action.payload +) => createValue(shouldUpdate, shouldReset, (state, action) => + ({ ...state, ...getValues(state, action) }), {}); + +/** + * Creates a reducer that manages a set. + * + * @param {function(state, action)} shouldUpdate + * @param {function(state, action)} shouldReset + * @param {function(state, action)} getValues + * @returns {function(state, action)} + */ +export const createSet = ( + shouldUpdate = () => true, + shouldReset = () => false, + getValues = (state, action) => action.payload +) => createValue(shouldUpdate, shouldReset, (state, action) => + uniq([...state, ...getValues(state, action)]), []); + +/** + * Creates a reducer that manages a flag. + * + * @param {function(state, action)} shouldTurnOn + * @param {function(state, action)} shouldTurnOff + * @param {bool} defaultValue + * @returns {function(state, action)} + */ +export const createFlag = (shouldTurnOn, shouldTurnOff, defaultValue = false) => + (state = defaultValue, action = {}) => { + if (shouldTurnOn(state, action)) { + return true; + } + if (shouldTurnOff(state, action)) { + return false; + } + return state; + }; diff --git a/server/sonar-web/src/main/js/helpers/ratings.js b/server/sonar-web/src/main/js/helpers/ratings.js new file mode 100644 index 00000000000..1d8347316d8 --- /dev/null +++ b/server/sonar-web/src/main/js/helpers/ratings.js @@ -0,0 +1,63 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +const checkNumberRating = coverageRating => { + if (!(typeof coverageRating === 'number' && coverageRating > 0 && coverageRating < 6)) { + throw new Error(`Unknown number rating: "${coverageRating}"`); + } +}; + +export const getCoverageRatingLabel = rating => { + checkNumberRating(rating); + + const mapping = ['< 30%', '30–50%', '50–70%', '70–80%', '> 80%']; + return mapping[rating - 1]; +}; + +export const getCoverageRatingAverageValue = rating => { + checkNumberRating(rating); + const mapping = [15, 40, 60, 75, 90]; + return mapping[rating - 1]; +}; + +export const getDuplicationsRatingLabel = rating => { + checkNumberRating(rating); + + const mapping = ['< 3%', '3–5%', '5–10%', '10–20%', '> 20%']; + return mapping[rating - 1]; +}; + +export const getDuplicationsRatingAverageValue = rating => { + checkNumberRating(rating); + const mapping = [1.5, 4, 7.5, 15, 30]; + return mapping[rating - 1]; +}; + +export const getSizeRatingLabel = rating => { + checkNumberRating(rating); + + const mapping = ['< 1k', '1k–10k', '10k–100k', '100k–500k', '> 500k']; + return mapping[rating - 1]; +}; + +export const getSizeRatingAverageValue = rating => { + checkNumberRating(rating); + const mapping = [500, 5000, 50000, 250000, 750000]; + return mapping[rating - 1]; +}; |