Browse Source

SONAR-8878 Add a sorting buttons on projects page facets (#1754)

tags/6.4-RC1
Grégoire Aubert 7 years ago
parent
commit
6ee67fb6f6

+ 22
- 1
server/sonar-web/src/main/js/apps/projects/filters/CoverageFilter.js View File

@@ -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 (
<span>
@@ -34,6 +43,17 @@ export default class CoverageFilter extends React.Component {
);
};

renderSort = () => {
return (
<SortingFilter
property={this.property}
query={this.props.query}
isFavorite={this.props.isFavorite}
organization={this.props.organization}
sortDesc="right"/>
);
}

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 (
<FilterContainer
property="coverage"
property={this.property}
getOptions={() => [1, 2, 3, 4, 5]}
renderName={() => 'Coverage'}
renderOption={this.renderOption}
renderSort={this.renderSort}
getFacetValueForOption={this.getFacetValueForOption}
query={this.props.query}
isFavorite={this.props.isFavorite}

+ 21
- 1
server/sonar-web/src/main/js/apps/projects/filters/DuplicationsFilter.js View File

@@ -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 (
<span>
@@ -34,6 +43,16 @@ export default class DuplicationsFilter extends React.Component {
);
};

renderSort = () => {
return (
<SortingFilter
property={this.property}
query={this.props.query}
isFavorite={this.props.isFavorite}
organization={this.props.organization}/>
);
}

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 (
<FilterContainer
property="duplications"
property={this.property}
getOptions={() => [1, 2, 3, 4, 5]}
renderName={() => 'Duplications'}
renderOption={this.renderOption}
renderSort={this.renderSort}
getFacetValueForOption={this.getFacetValueForOption}
query={this.props.query}
isFavorite={this.props.isFavorite}

+ 2
- 0
server/sonar-web/src/main/js/apps/projects/filters/Filter.js View File

@@ -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 (
<div className="search-navigator-facet-header projects-facet-header">
{this.props.renderName()}
{this.props.renderSort && this.props.renderSort()}
</div>
);
}

+ 20
- 0
server/sonar-web/src/main/js/apps/projects/filters/IssuesFilter.js View File

@@ -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 (
<span>
@@ -33,6 +42,16 @@ export default class IssuesFilter extends React.Component {
);
};

renderSort = () => {
return (
<SortingFilter
property={this.props.property}
query={this.props.query}
isFavorite={this.props.isFavorite}
organization={this.props.organization}/>
);
}

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}

+ 24
- 1
server/sonar-web/src/main/js/apps/projects/filters/SizeFilter.js View File

@@ -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 (
<span>
@@ -34,6 +44,18 @@ export default class SizeFilter extends React.Component {
);
};

renderSort = () => {
return (
<SortingFilter
property={this.property}
query={this.props.query}
isFavorite={this.props.isFavorite}
organization={this.props.organization}
leftText={translate('biggest')}
rightText={translate('smallest')}/>
);
}

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 (
<FilterContainer
property="size"
property={this.property}
getOptions={() => [1, 2, 3, 4, 5]}
renderName={() => 'Size'}
renderOption={this.renderOption}
renderSort={this.renderSort}
getFacetValueForOption={this.getFacetValueForOption}
query={this.props.query}
isFavorite={this.props.isFavorite}

+ 90
- 0
server/sonar-web/src/main/js/apps/projects/filters/SortingFilter.js View File

@@ -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 (
<div className="projects-facet-sort">
<span>{translate('projects.sort_list')}</span>
<div className="spacer-left button-group">
<Link
onClick={this.blurLink}
className={this.getLinkClass('left')}
to={this.getLinkPath('left')}>{leftText}</Link>
<Link
onClick={this.blurLink}
className={this.getLinkClass('right')}
to={this.getLinkPath('right')}>{rightText}</Link>
</div>
</div>
);
}
}

+ 3
- 17
server/sonar-web/src/main/js/apps/projects/store/actions.js View File

@@ -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));
};

+ 82
- 44
server/sonar-web/src/main/js/apps/projects/store/utils.js View File

@@ -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;
};

+ 18
- 0
server/sonar-web/src/main/js/apps/projects/styles.css View File

@@ -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;

+ 17
- 0
server/sonar-web/src/main/less/init/forms.less View File

@@ -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 {

+ 5
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -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


#------------------------------------------------------------------------------

Loading…
Cancel
Save