From 01b4e8235eea4f1b4e78809a774279e71d7f0d5c Mon Sep 17 00:00:00 2001 From: =?utf8?q?Aubert=20Gr=C3=A9goire?= Date: Fri, 3 Mar 2017 16:15:22 +0100 Subject: [PATCH] SONAR-8875 Add language facet on projects page (#1737) --- .../apps/projects/components/PageSidebar.js | 5 ++ .../apps/projects/filters/CoverageFilter.js | 2 +- .../projects/filters/DuplicationsFilter.js | 2 +- .../main/js/apps/projects/filters/Filter.js | 49 ++++++++++++---- .../js/apps/projects/filters/IssuesFilter.js | 2 +- .../apps/projects/filters/LanguageFilter.js | 57 +++++++++++++++++++ .../projects/filters/LanguageFilterOption.js | 43 ++++++++++++++ .../projects/filters/QualityGateFilter.js | 2 +- .../js/apps/projects/filters/SizeFilter.js | 2 +- .../main/js/apps/projects/store/actions.js | 3 +- .../main/js/apps/projects/store/facetsDuck.js | 3 +- .../src/main/js/apps/projects/store/utils.js | 28 ++++++++- .../src/main/js/store/languages/reducer.js | 4 ++ .../src/main/js/store/rootReducer.js | 8 ++- .../less/components/search-navigator.less | 9 +++ 15 files changed, 198 insertions(+), 21 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/projects/filters/LanguageFilter.js create mode 100644 server/sonar-web/src/main/js/apps/projects/filters/LanguageFilterOption.js 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 a2c0ecd4a3e..9020ef0748c 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 @@ -26,6 +26,7 @@ import QualityGateFilter from '../filters/QualityGateFilter'; import ReliabilityFilter from '../filters/ReliabilityFilter'; import SecurityFilter from '../filters/SecurityFilter'; import MaintainabilityFilter from '../filters/MaintainabilityFilter'; +import LanguageFilter from '../filters/LanguageFilter'; import { translate } from '../../../helpers/l10n'; export default class PageSidebar extends React.Component { @@ -85,6 +86,10 @@ export default class PageSidebar extends React.Component { query={this.props.query} isFavorite={this.props.isFavorite} organization={this.props.organization}/> + ); } 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 aace2cebd68..ab7dd2fa336 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 @@ -43,7 +43,7 @@ export default class CoverageFilter extends React.Component { return ( [1, 2, 3, 4, 5]} renderName={() => 'Coverage'} renderOption={this.renderOption} getFacetValueForOption={this.getFacetValueForOption} 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 250e431c6b3..a7549c74589 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 @@ -43,7 +43,7 @@ export default class DuplicationsFilter extends React.Component { return ( [1, 2, 3, 4, 5]} renderName={() => 'Duplications'} renderOption={this.renderOption} getFacetValueForOption={this.getFacetValueForOption} 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 efb0f54ff7d..b10b4162d63 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 @@ -21,12 +21,13 @@ import React from 'react'; import classNames from 'classnames'; import { Link } from 'react-router'; import { formatMeasure } from '../../../helpers/measures'; +import { translate } from '../../../helpers/l10n'; export default class Filter extends React.Component { static propTypes = { value: React.PropTypes.any, property: React.PropTypes.string.isRequired, - options: React.PropTypes.array.isRequired, + getOptions: React.PropTypes.func.isRequired, maxFacetValue: React.PropTypes.number, optionClassName: React.PropTypes.string, @@ -44,6 +45,27 @@ export default class Filter extends React.Component { halfWidth: false }; + isSelected (option) { + const { value } = this.props; + return Array.isArray(value) ? value.includes(option) : option === value; + } + + getPath (option) { + const { property, value } = this.props; + let urlOption; + + if (Array.isArray(value)) { + if (this.isSelected(option)) { + urlOption = value.length > 1 ? value.filter(val => val !== option).join(',') : null; + } else { + urlOption = value.concat(option).join(','); + } + } else { + urlOption = this.isSelected(option) ? null : option; + } + return this.props.getFilterUrl({ [property]: urlOption }); + } + renderHeader () { return (
@@ -66,22 +88,20 @@ export default class Filter extends React.Component { } renderOption (option) { - const { property, value, facet, getFacetValueForOption } = this.props; + const { facet, getFacetValueForOption } = this.props; const className = classNames('facet', 'search-navigator-facet', 'projects-facet', { - 'active': option === value, + 'active': this.isSelected(option), 'search-navigator-facet-half': this.props.halfWidth }, this.props.optionClassName); - const path = option === value ? - this.props.getFilterUrl({ [property]: null }) : - this.props.getFilterUrl({ [property]: option }); + const path = this.getPath(option); const facetValue = (facet && getFacetValueForOption) ? getFacetValueForOption(facet, option) : null; return ( - {this.props.renderOption(option, option === value)} + {this.props.renderOption(option, this.isSelected(option))} {facetValue != null && ( @@ -94,11 +114,20 @@ export default class Filter extends React.Component { } renderOptions () { - return ( + const options = this.props.getOptions(this.props.facet); + if (options && options.length > 0) { + return (
- {this.props.options.map(option => this.renderOption(option))} + {options.map(option => this.renderOption(option))}
- ); + ); + } else { + return ( +
+ {translate('no_results')} +
+ ); + } } render () { 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 d7fc35e78e6..8d3bcc0c582 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 @@ -41,7 +41,7 @@ export default class IssuesFilter extends React.Component { return ( [1, 2, 3, 4, 5]} renderName={() => this.props.name} renderOption={this.renderOption} getFacetValueForOption={this.getFacetValueForOption} diff --git a/server/sonar-web/src/main/js/apps/projects/filters/LanguageFilter.js b/server/sonar-web/src/main/js/apps/projects/filters/LanguageFilter.js new file mode 100644 index 00000000000..4e3e10921f5 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/filters/LanguageFilter.js @@ -0,0 +1,57 @@ +/* + * 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 sortBy from 'lodash/sortBy'; +import FilterContainer from './FilterContainer'; +import LanguageFilterOption from './LanguageFilterOption'; + +export default class LanguageFilter extends React.Component { + static propTypes = { + query: React.PropTypes.object.isRequired, + isFavorite: React.PropTypes.bool, + organization: React.PropTypes.object + } + + renderOption = option => { + return ( + + ); + }; + + getSortedOptions (facet) { + return sortBy(Object.keys(facet), [option => -facet[option]]); + } + + getFacetValueForOption = (facet, option) => facet[option]; + + render () { + return ( + facet ? this.getSortedOptions(facet) : []} + renderName={() => 'Languages'} + renderOption={this.renderOption} + getFacetValueForOption={this.getFacetValueForOption} + query={this.props.query} + isFavorite={this.props.isFavorite} + organization={this.props.organization}/> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projects/filters/LanguageFilterOption.js b/server/sonar-web/src/main/js/apps/projects/filters/LanguageFilterOption.js new file mode 100644 index 00000000000..002a8f04ff0 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/filters/LanguageFilterOption.js @@ -0,0 +1,43 @@ +/* + * 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 { connect } from 'react-redux'; +import React from 'react'; +import { translate } from '../../../helpers/l10n'; +import { getLanguageByKey } from '../../../store/rootReducer'; + +class LanguageFilterOption extends React.Component { + static propTypes = { + languageKey: React.PropTypes.string.isRequired, + language: React.PropTypes.object + } + + render () { + const languageName = this.props.language ? this.props.language.name : this.props.languageKey; + return ( + {this.props.languageKey !== '' ? languageName : translate('unknown')} + ); + } +} + +const mapStateToProps = (state, ownProps) => ({ + language: getLanguageByKey(state, ownProps.languageKey) +}); + +export default connect(mapStateToProps)(LanguageFilterOption); 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 5e5b55a2f31..4adf70bb5e8 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 @@ -36,7 +36,7 @@ export default class QualityGateFilter extends React.Component { return ( ['OK', 'WARN', 'ERROR']} renderName={() => 'Quality Gate'} renderOption={this.renderOption} getFacetValueForOption={this.getFacetValueForOption} 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 8b49fc9d276..5633034b737 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 @@ -43,7 +43,7 @@ export default class SizeFilter extends React.Component { return ( [1, 2, 3, 4, 5]} renderName={() => 'Size'} renderOption={this.renderOption} getFacetValueForOption={this.getFacetValueForOption} 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 ccae02bfc5e..237df655e0f 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 @@ -53,7 +53,8 @@ const FACETS = [ 'coverage', 'duplicated_lines_density', 'ncloc', - 'alert_status' + 'alert_status', + 'language' ]; const onFail = dispatch => error => { diff --git a/server/sonar-web/src/main/js/apps/projects/store/facetsDuck.js b/server/sonar-web/src/main/js/apps/projects/store/facetsDuck.js index 03640a8b2f4..9523540b065 100644 --- a/server/sonar-web/src/main/js/apps/projects/store/facetsDuck.js +++ b/server/sonar-web/src/main/js/apps/projects/store/facetsDuck.js @@ -29,7 +29,8 @@ const CUMULATIVE_FACETS = [ 'maintainability', 'coverage', 'duplications', - 'size' + 'size', + 'language' ]; const REVERSED_FACETS = [ 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 58291976a09..c193d42d57f 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 @@ -32,6 +32,20 @@ const getAsLevel = value => { return null; }; +const getAsString = value => { + if (!value) { + return null; + } + return value; +}; + +const getAsArray = (values, elementGetter) => { + if (!values) { + return null; + } + return values.split(',').map(elementGetter); +}; + export const parseUrlQuery = urlQuery => ({ 'gate': getAsLevel(urlQuery['gate']), 'reliability': getAsNumericRating(urlQuery['reliability']), @@ -39,7 +53,8 @@ export const parseUrlQuery = urlQuery => ({ 'maintainability': getAsNumericRating(urlQuery['maintainability']), 'coverage': getAsNumericRating(urlQuery['coverage']), 'duplications': getAsNumericRating(urlQuery['duplications']), - 'size': getAsNumericRating(urlQuery['size']) + 'size': getAsNumericRating(urlQuery['size']), + 'language': getAsArray(urlQuery['language'], getAsString) }); const convertIssuesRating = (metric, rating) => { @@ -136,6 +151,14 @@ export const convertToFilter = (query, isFavorite) => { conditions.push(convertIssuesRating('sqale_rating', query['maintainability'])); } + if (query['language'] != null) { + if (!Array.isArray(query['language']) || query['language'].length < 2) { + conditions.push('language = ' + query['language']); + } else { + conditions.push(`language IN (${query['language'].join(', ')})`); + } + } + return conditions.join(' and '); }; @@ -147,7 +170,8 @@ export const mapMetricToProperty = metricKey => { 'coverage': 'coverage', 'duplicated_lines_density': 'duplications', 'ncloc': 'size', - 'alert_status': 'gate' + 'alert_status': 'gate', + 'language': 'language' }; return map[metricKey]; }; diff --git a/server/sonar-web/src/main/js/store/languages/reducer.js b/server/sonar-web/src/main/js/store/languages/reducer.js index 72483b92947..84aafb8632e 100644 --- a/server/sonar-web/src/main/js/store/languages/reducer.js +++ b/server/sonar-web/src/main/js/store/languages/reducer.js @@ -33,3 +33,7 @@ export default reducer; export const getLanguages = state => ( state ); + +export const getLanguageByKey = (state, key) => ( + state[key] +); diff --git a/server/sonar-web/src/main/js/store/rootReducer.js b/server/sonar-web/src/main/js/store/rootReducer.js index aee309845c2..4e6ce44a284 100644 --- a/server/sonar-web/src/main/js/store/rootReducer.js +++ b/server/sonar-web/src/main/js/store/rootReducer.js @@ -68,8 +68,12 @@ export const getGlobalMessages = state => ( fromGlobalMessages.getGlobalMessages(state.globalMessages) ); -export const getLanguages = (state, key) => ( - fromLanguages.getLanguages(state.languages, key) +export const getLanguages = state => ( + fromLanguages.getLanguages(state.languages) +); + +export const getLanguageByKey = (state, key) => ( + fromLanguages.getLanguageByKey(state.languages, key) ); export const getCurrentUser = state => ( diff --git a/server/sonar-web/src/main/less/components/search-navigator.less b/server/sonar-web/src/main/less/components/search-navigator.less index d283981ed1a..0a17c8ac4dc 100644 --- a/server/sonar-web/src/main/less/components/search-navigator.less +++ b/server/sonar-web/src/main/less/components/search-navigator.less @@ -84,6 +84,7 @@ background-color: transparent; .search-navigator-facet-list, + .search-navigator-facet-empty, .search-navigator-facet-container { display: none; } @@ -234,6 +235,14 @@ font-size: 0; } +.search-navigator-facet-empty { + margin: 0 0 0 0; + padding: 0 10px 10px; + color: @baseFontColor; + font-size: @smallFontSize; + white-space: nowrap; +} + .search-navigator-facet-list-align-right { .facet-name { -- 2.39.5