From 6ee67fb6f66b53ef3f4791fc937a10fecb6b0f60 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Gr=C3=A9goire=20Aubert?= Date: Wed, 8 Mar 2017 10:54:50 +0100 Subject: [PATCH] SONAR-8878 Add a sorting buttons on projects page facets (#1754) --- .../apps/projects/filters/CoverageFilter.js | 23 +++- .../projects/filters/DuplicationsFilter.js | 22 ++- .../main/js/apps/projects/filters/Filter.js | 2 + .../js/apps/projects/filters/IssuesFilter.js | 20 +++ .../js/apps/projects/filters/SizeFilter.js | 25 +++- .../js/apps/projects/filters/SortingFilter.js | 90 +++++++++++++ .../main/js/apps/projects/store/actions.js | 20 +-- .../src/main/js/apps/projects/store/utils.js | 126 ++++++++++++------ .../src/main/js/apps/projects/styles.css | 18 +++ .../sonar-web/src/main/less/init/forms.less | 17 +++ .../resources/org/sonar/l10n/core.properties | 5 + 11 files changed, 304 insertions(+), 64 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/projects/filters/SortingFilter.js 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 ab7dd2fa336..f13699a0807 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 @@ -19,10 +19,19 @@ */ import React from 'react'; import FilterContainer from './FilterContainer'; +import SortingFilter from './SortingFilter'; import CoverageRating from '../../../components/ui/CoverageRating'; import { getCoverageRatingLabel, getCoverageRatingAverageValue } from '../../../helpers/ratings'; export default class CoverageFilter extends React.Component { + static propTypes = { + query: React.PropTypes.object.isRequired, + isFavorite: React.PropTypes.bool, + organization: React.PropTypes.object + } + + property = 'coverage'; + renderOption = (option, selected) => { return ( @@ -34,6 +43,17 @@ export default class CoverageFilter extends React.Component { ); }; + renderSort = () => { + return ( + + ); + } + getFacetValueForOption = (facet, option) => { const map = ['80.0-*', '70.0-80.0', '50.0-70.0', '30.0-50.0', '*-30.0']; return facet[map[option - 1]]; @@ -42,10 +62,11 @@ export default class CoverageFilter extends React.Component { render () { return ( [1, 2, 3, 4, 5]} renderName={() => 'Coverage'} renderOption={this.renderOption} + renderSort={this.renderSort} getFacetValueForOption={this.getFacetValueForOption} query={this.props.query} isFavorite={this.props.isFavorite} 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 a7549c74589..818e80e6eca 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 @@ -19,10 +19,19 @@ */ import React from 'react'; import FilterContainer from './FilterContainer'; +import SortingFilter from './SortingFilter'; import DuplicationsRating from '../../../components/ui/DuplicationsRating'; import { getDuplicationsRatingLabel, getDuplicationsRatingAverageValue } from '../../../helpers/ratings'; export default class DuplicationsFilter extends React.Component { + static propTypes = { + query: React.PropTypes.object.isRequired, + isFavorite: React.PropTypes.bool, + organization: React.PropTypes.object + } + + property = 'duplications'; + renderOption = (option, selected) => { return ( @@ -34,6 +43,16 @@ export default class DuplicationsFilter extends React.Component { ); }; + renderSort = () => { + return ( + + ); + } + 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]]; @@ -42,10 +61,11 @@ export default class DuplicationsFilter extends React.Component { render () { return ( [1, 2, 3, 4, 5]} renderName={() => 'Duplications'} renderOption={this.renderOption} + renderSort={this.renderSort} getFacetValueForOption={this.getFacetValueForOption} query={this.props.query} isFavorite={this.props.isFavorite} 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 d8d6c4562f0..9f2bda8460c 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 @@ -35,6 +35,7 @@ export default class Filter extends React.Component { renderName: React.PropTypes.func.isRequired, renderOption: React.PropTypes.func.isRequired, renderFooter: React.PropTypes.func, + renderSort: React.PropTypes.func, getFacetValueForOption: React.PropTypes.func, @@ -70,6 +71,7 @@ export default class Filter extends React.Component { return (
{this.props.renderName()} + {this.props.renderSort && this.props.renderSort()}
); } 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 index 8d3bcc0c582..0250eafae0f 100644 --- a/server/sonar-web/src/main/js/apps/projects/filters/IssuesFilter.js +++ b/server/sonar-web/src/main/js/apps/projects/filters/IssuesFilter.js @@ -19,9 +19,18 @@ */ import React from 'react'; import FilterContainer from './FilterContainer'; +import SortingFilter from './SortingFilter'; import Rating from '../../../components/ui/Rating'; export default class IssuesFilter extends React.Component { + static propTypes = { + property: React.PropTypes.string.isRequired, + name: React.PropTypes.string.isRequired, + query: React.PropTypes.object.isRequired, + isFavorite: React.PropTypes.bool, + organization: React.PropTypes.object + } + renderOption = (option, selected) => { return ( @@ -33,6 +42,16 @@ export default class IssuesFilter extends React.Component { ); }; + renderSort = () => { + return ( + + ); + } + getFacetValueForOption = (facet, option) => { return facet[option]; }; @@ -44,6 +63,7 @@ export default class IssuesFilter extends React.Component { getOptions={() => [1, 2, 3, 4, 5]} renderName={() => this.props.name} renderOption={this.renderOption} + renderSort={this.renderSort} getFacetValueForOption={this.getFacetValueForOption} query={this.props.query} isFavorite={this.props.isFavorite} 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 5633034b737..0bc240dd12d 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 @@ -19,10 +19,20 @@ */ import React from 'react'; import FilterContainer from './FilterContainer'; +import SortingFilter from './SortingFilter'; import SizeRating from '../../../components/ui/SizeRating'; +import { translate } from '../../../helpers/l10n'; import { getSizeRatingLabel, getSizeRatingAverageValue } from '../../../helpers/ratings'; export default class SizeFilter extends React.Component { + static propTypes = { + query: React.PropTypes.object.isRequired, + isFavorite: React.PropTypes.bool, + organization: React.PropTypes.object + } + + property = 'size'; + renderOption = (option, selected) => { return ( @@ -34,6 +44,18 @@ export default class SizeFilter extends React.Component { ); }; + renderSort = () => { + return ( + + ); + } + 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]]; @@ -42,10 +64,11 @@ export default class SizeFilter extends React.Component { render () { return ( [1, 2, 3, 4, 5]} renderName={() => 'Size'} renderOption={this.renderOption} + renderSort={this.renderSort} getFacetValueForOption={this.getFacetValueForOption} query={this.props.query} isFavorite={this.props.isFavorite} diff --git a/server/sonar-web/src/main/js/apps/projects/filters/SortingFilter.js b/server/sonar-web/src/main/js/apps/projects/filters/SortingFilter.js new file mode 100644 index 00000000000..842a3596f4f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/filters/SortingFilter.js @@ -0,0 +1,90 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import classNames from 'classnames'; +import { Link } from 'react-router'; +import { getFilterUrl } from './utils'; +import { translate } from '../../../helpers/l10n'; + +export default class SortingFilter extends React.Component { + static propTypes = { + property: React.PropTypes.string.isRequired, + query: React.PropTypes.object.isRequired, + isFavorite: React.PropTypes.bool, + organization: React.PropTypes.object, + sortDesc: React.PropTypes.oneOf(['left', 'right']), + leftText: React.PropTypes.string, + rightText: React.PropTypes.string + } + + static defaultProps = { + sortDesc: 'left', + leftText: translate('worst'), + rightText: translate('best') + }; + + isSortActive (side) { + const { sort } = this.props.query; + if (sort && sort[0] === '-') { + return sort.substr(1) === this.props.property && side === this.props.sortDesc; + } else { + return sort === this.props.property && side !== this.props.sortDesc; + } + } + + getLinkClass (side) { + return classNames('button button-small button-grey', { + 'button-active': this.isSortActive(side) + }); + } + + getLinkPath (side) { + if (this.isSortActive(side)) { + return getFilterUrl(this.props, { sort: null }); + } + return getFilterUrl(this.props, { + sort: (this.props.sortDesc === side ? '-' : '') + this.props.property + }); + } + + blurLink (event) { + event.target.blur(); + } + + render () { + const { leftText, rightText } = this.props; + + return ( +
+ {translate('projects.sort_list')} +
+ {leftText} + {rightText} +
+
+ ); + } +} 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 b609227c95f..7cca5a9ec47 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 @@ -28,7 +28,7 @@ import { updateState } from './stateDuck'; import { getProjectsAppState } from '../../../store/rootReducer'; import { getMeasuresForProjects } from '../../../api/measures'; import { receiveComponentsMeasures } from '../../../store/measures/actions'; -import { convertToFilter } from './utils'; +import { convertToQueryData } from './utils'; import { receiveFavorites } from '../../../store/favorites/duck'; import { getOrganizations } from '../../../api/organizations'; import { receiveOrganizations } from '../../../store/organizations/duck'; @@ -143,14 +143,7 @@ const onReceiveMoreProjects = dispatch => response => { export const fetchProjects = (query, isFavorite, organization) => dispatch => { dispatch(updateState({ loading: true })); - const data = { ps: PAGE_SIZE, facets: FACETS.join() }; - const filter = convertToFilter(query, isFavorite); - if (filter) { - data.filter = filter; - } - if (organization) { - data.organization = organization.key; - } + const data = convertToQueryData(query, isFavorite, organization, { ps: PAGE_SIZE, facets: FACETS.join() }); return searchProjects(data).then(onReceiveProjects(dispatch), onFail(dispatch)); }; @@ -158,13 +151,6 @@ export const fetchMoreProjects = (query, isFavorite, organization) => (dispatch, dispatch(updateState({ loading: true })); const state = getState(); const { pageIndex } = getProjectsAppState(state); - const data = { ps: PAGE_SIZE, p: pageIndex + 1 }; - const filter = convertToFilter(query, isFavorite); - if (filter) { - data.filter = filter; - } - if (organization) { - data.organization = organization.key; - } + const data = convertToQueryData(query, isFavorite, organization, { ps: PAGE_SIZE, p: pageIndex + 1 }); return searchProjects(data).then(onReceiveMoreProjects(dispatch), onFail(dispatch)); }; 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 e2cd838941b..b9d15c5140c 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 @@ -55,9 +55,40 @@ export const parseUrlQuery = urlQuery => ({ 'duplications': getAsNumericRating(urlQuery['duplications']), 'size': getAsNumericRating(urlQuery['size']), 'languages': getAsArray(urlQuery['languages'], getAsString), - 'search': getAsString(urlQuery['search']) + 'search': getAsString(urlQuery['search']), + 'sort': getAsString(urlQuery['sort']) }); +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', + 'languages': 'languages', + 'query': 'search' + }; + return map[metricKey]; +}; + +export const mapPropertyToMetric = property => { + const map = { + 'reliability': 'reliability_rating', + 'security': 'security_rating', + 'maintainability': 'sqale_rating', + 'coverage': 'coverage', + 'duplications': 'duplicated_lines_density', + 'size': 'ncloc', + 'gate': 'alert_status', + 'languages': 'languages', + 'search': 'query' + }; + return map[property]; +}; + const convertIssuesRating = (metric, rating) => { if (rating > 1 && rating < 5) { return `${metric} >= ${rating}`; @@ -69,15 +100,15 @@ const convertIssuesRating = (metric, rating) => { const convertCoverage = coverage => { switch (coverage) { case 1: - return 'coverage >= 80'; + return mapPropertyToMetric('coverage') + ' >= 80'; case 2: - return 'coverage < 80'; + return mapPropertyToMetric('coverage') + ' < 80'; case 3: - return 'coverage < 70'; + return mapPropertyToMetric('coverage') + ' < 70'; case 4: - return 'coverage < 50'; + return mapPropertyToMetric('coverage') + ' < 50'; case 5: - return 'coverage < 30'; + return mapPropertyToMetric('coverage') + ' < 30'; default: return ''; } @@ -86,15 +117,15 @@ const convertCoverage = coverage => { const convertDuplications = duplications => { switch (duplications) { case 1: - return 'duplicated_lines_density < 3'; + return mapPropertyToMetric('duplications') + ' < 3'; case 2: - return 'duplicated_lines_density >= 3'; + return mapPropertyToMetric('duplications') + ' >= 3'; case 3: - return 'duplicated_lines_density >= 5'; + return mapPropertyToMetric('duplications') + ' >= 5'; case 4: - return 'duplicated_lines_density >= 10'; + return mapPropertyToMetric('duplications') + ' >= 10'; case 5: - return 'duplicated_lines_density >= 20'; + return mapPropertyToMetric('duplications') + ' >= 20'; default: return ''; } @@ -103,21 +134,21 @@ const convertDuplications = duplications => { const convertSize = size => { switch (size) { case 1: - return 'ncloc < 1000'; + return mapPropertyToMetric('size') + ' < 1000'; case 2: - return 'ncloc >= 1000'; + return mapPropertyToMetric('size') + ' >= 1000'; case 3: - return 'ncloc >= 10000'; + return mapPropertyToMetric('size') + ' >= 10000'; case 4: - return 'ncloc >= 100000'; + return mapPropertyToMetric('size') + ' >= 100000'; case 5: - return 'ncloc >= 500000'; + return mapPropertyToMetric('size') + ' >= 500000'; default: return ''; } }; -export const convertToFilter = (query, isFavorite) => { +const convertToFilter = (query, isFavorite) => { const conditions = []; if (isFavorite) { @@ -125,7 +156,7 @@ export const convertToFilter = (query, isFavorite) => { } if (query['gate'] != null) { - conditions.push('alert_status = ' + query['gate']); + conditions.push(mapPropertyToMetric('gate') + ' = ' + query['gate']); } if (query['coverage'] != null) { @@ -140,44 +171,51 @@ export const convertToFilter = (query, isFavorite) => { conditions.push(convertSize(query['size'])); } - if (query['reliability'] != null) { - conditions.push(convertIssuesRating('reliability_rating', query['reliability'])); - } - - if (query['security'] != null) { - conditions.push(convertIssuesRating('security_rating', query['security'])); - } - - if (query['maintainability'] != null) { - conditions.push(convertIssuesRating('sqale_rating', query['maintainability'])); - } + ['reliability', 'security', 'maintainability'].forEach(property => { + if (query[property] != null) { + conditions.push(convertIssuesRating(mapPropertyToMetric(property), query[property])); + } + }); const { languages } = query; if (languages != null) { if (!Array.isArray(languages) || languages.length < 2) { - conditions.push('languages = ' + languages); + conditions.push(mapPropertyToMetric('languages') + ' = ' + languages); } else { - conditions.push(`languages IN (${languages.join(', ')})`); + conditions.push(`${mapPropertyToMetric('languages')} IN (${languages.join(', ')})`); } } if (query['search'] != null) { - conditions.push(`query = "${query['search']}"`); + conditions.push(`${mapPropertyToMetric('search')} = "${query['search']}"`); } 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', - 'languages': 'languages' - }; - return map[metricKey]; +export const convertToSorting = ({ sort }) => { + if (sort && sort[0] === '-') { + return { s: mapPropertyToMetric(sort.substr(1)), asc: false }; + } + return { s: mapPropertyToMetric(sort) }; +}; + +export const convertToQueryData = (query, isFavorite, organization, defaultData = {}) => { + const data = { ...defaultData }; + const filter = convertToFilter(query, isFavorite); + const sort = convertToSorting(query); + + if (filter) { + data.filter = filter; + } + if (sort.s) { + data.s = sort.s; + } + if (sort.hasOwnProperty('asc')) { + data.asc = sort.asc; + } + if (organization) { + data.organization = organization.key; + } + return data; }; 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 0729984e8c7..176c2e5713b 100644 --- a/server/sonar-web/src/main/js/apps/projects/styles.css +++ b/server/sonar-web/src/main/js/apps/projects/styles.css @@ -106,6 +106,24 @@ transition: none; } +.projects-facet-sort { + float: right; + font-weight: normal; + font-size: 11px; + color: #777; + text-transform: lowercase; +} + +.projects-facet-sort .button-group { + margin-top: -3px; +} + +.projects-facet-sort .button-small { + padding: 0 6px; + font-size: 11px; + font-weight: normal; +} + .projects-facets-header { margin-bottom: 10px; padding: 10px 0; diff --git a/server/sonar-web/src/main/less/init/forms.less b/server/sonar-web/src/main/less/init/forms.less index 79bbe7cb4cb..bca41f0afcf 100644 --- a/server/sonar-web/src/main/less/init/forms.less +++ b/server/sonar-web/src/main/less/init/forms.less @@ -149,6 +149,23 @@ input[type="submit"].button-success { } } +.button-grey, +input[type="submit"].button-grey { + border-color: @middleGrey; + color: @secondFontColor; + + &:hover, &:focus, &.active { + background: @middleGrey; + color: @white; + } + + &.button-active { + background: @secondFontColor; + border-color: @secondFontColor; + color: @white; + } +} + .button-clean, .button-clean:hover, .button-clean:focus { diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 25332bd9dd5..5e5ca82eddd 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -24,6 +24,8 @@ author=Author back=Back backup=Backup backup_verb=Back up +best=Best +biggest=Biggest blocker=Blocker bold=Bold branch=Branch @@ -158,6 +160,7 @@ shared=Shared show_verb=Show x_of_y_shown={0} of {1} shown size=Size +smallest=Smallest status=Status status_abbreviated=St. sub_project=Sub-project @@ -194,6 +197,7 @@ view=View views=Views violations=Violations with=With +worst=Worst @@ -824,6 +828,7 @@ projects.no_favorite_projects.engagement=Discover and mark as favorites projects projects.explore_projects=Explore Projects projects.not_analyzed=Project is not analyzed yet. projects.search=Search by project name or key +projects.sort_list=Sort list by #------------------------------------------------------------------------------ -- 2.39.5