aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main
diff options
context:
space:
mode:
authorAubert Grégoire <gregaubert@users.noreply.github.com>2017-03-03 16:15:22 +0100
committerGitHub <noreply@github.com>2017-03-03 16:15:22 +0100
commit01b4e8235eea4f1b4e78809a774279e71d7f0d5c (patch)
tree80d2a8b1fc03b4559a82e0519828c9445d96a901 /server/sonar-web/src/main
parentb6baff8775a45fd057ed9f15ba9ac29633261fea (diff)
downloadsonarqube-01b4e8235eea4f1b4e78809a774279e71d7f0d5c.tar.gz
sonarqube-01b4e8235eea4f1b4e78809a774279e71d7f0d5c.zip
SONAR-8875 Add language facet on projects page (#1737)
Diffstat (limited to 'server/sonar-web/src/main')
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/PageSidebar.js5
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/CoverageFilter.js2
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/DuplicationsFilter.js2
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/Filter.js49
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/IssuesFilter.js2
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/LanguageFilter.js57
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/LanguageFilterOption.js43
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/QualityGateFilter.js2
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/SizeFilter.js2
-rw-r--r--server/sonar-web/src/main/js/apps/projects/store/actions.js3
-rw-r--r--server/sonar-web/src/main/js/apps/projects/store/facetsDuck.js3
-rw-r--r--server/sonar-web/src/main/js/apps/projects/store/utils.js28
-rw-r--r--server/sonar-web/src/main/js/store/languages/reducer.js4
-rw-r--r--server/sonar-web/src/main/js/store/rootReducer.js8
-rw-r--r--server/sonar-web/src/main/less/components/search-navigator.less9
15 files changed, 198 insertions, 21 deletions
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}/>
+ <LanguageFilter
+ query={this.props.query}
+ isFavorite={this.props.isFavorite}
+ organization={this.props.organization}/>
</div>
);
}
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 (
<FilterContainer
property="coverage"
- options={[1, 2, 3, 4, 5]}
+ getOptions={() => [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 (
<FilterContainer
property="duplications"
- options={[1, 2, 3, 4, 5]}
+ getOptions={() => [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 (
<div className="search-navigator-facet-header projects-facet-header">
@@ -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 (
<Link key={option} className={className} to={path} data-key={option}>
<span className="facet-name">
- {this.props.renderOption(option, option === value)}
+ {this.props.renderOption(option, this.isSelected(option))}
</span>
{facetValue != null && (
<span className="facet-stat">
@@ -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 (
<div className="search-navigator-facet-list">
- {this.props.options.map(option => this.renderOption(option))}
+ {options.map(option => this.renderOption(option))}
</div>
- );
+ );
+ } else {
+ return (
+ <div className="search-navigator-facet-empty">
+ {translate('no_results')}
+ </div>
+ );
+ }
}
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 (
<FilterContainer
property={this.props.property}
- options={[1, 2, 3, 4, 5]}
+ getOptions={() => [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 (
+ <LanguageFilterOption languageKey={option}/>
+ );
+ };
+
+ getSortedOptions (facet) {
+ return sortBy(Object.keys(facet), [option => -facet[option]]);
+ }
+
+ getFacetValueForOption = (facet, option) => facet[option];
+
+ render () {
+ return (
+ <FilterContainer
+ property="language"
+ getOptions={facet => 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 (
+ <span>{this.props.languageKey !== '<null>' ? languageName : translate('unknown')}</span>
+ );
+ }
+}
+
+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 (
<FilterContainer
property="gate"
- options={['OK', 'WARN', 'ERROR']}
+ getOptions={() => ['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 (
<FilterContainer
property="size"
- options={[1, 2, 3, 4, 5]}
+ getOptions={() => [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 {