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