aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--server/sonar-web/src/main/js/app/store/rootReducer.js4
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/App.js4
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/PageSidebar.js41
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/PageSidebarContainer.js (renamed from server/sonar-web/src/main/js/apps/projects/filters/QualityGateFilterContainer.js)12
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/CoverageFilter.js113
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/DuplicationsFilter.js113
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/Filter.js122
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/FilterContainer.js (renamed from server/sonar-web/src/main/js/apps/projects/filters/connectFilter.js)36
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/IssuesFilter.js47
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/MaintainabilityFilter.js (renamed from server/sonar-web/src/main/js/apps/projects/filters/CoverageFilterContainer.js)21
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/QualityGateFilter.js56
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/ReliabilityFilter.js (renamed from server/sonar-web/src/main/js/apps/projects/filters/SizeFilterContainer.js)21
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/SecurityFilter.js (renamed from server/sonar-web/src/main/js/apps/projects/filters/DuplicationsFilterContainer.js)21
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/SizeFilter.js114
-rw-r--r--server/sonar-web/src/main/js/apps/projects/store/actions.js14
-rw-r--r--server/sonar-web/src/main/js/apps/projects/store/facets/reducer.js52
-rw-r--r--server/sonar-web/src/main/js/apps/projects/store/filters/statuses/actions.js19
-rw-r--r--server/sonar-web/src/main/js/apps/projects/store/filters/statuses/reducer.js26
-rw-r--r--server/sonar-web/src/main/js/apps/projects/store/projects/actions.js5
-rw-r--r--server/sonar-web/src/main/js/apps/projects/store/reducer.js7
-rw-r--r--server/sonar-web/src/main/js/apps/projects/store/utils.js106
-rw-r--r--server/sonar-web/src/main/js/apps/projects/styles.css81
-rw-r--r--server/sonar-web/src/main/js/components/store/generalReducers.js95
-rw-r--r--server/sonar-web/src/main/js/helpers/ratings.js63
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];
+};