]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-8875 Add language facet on projects page (#1737)
authorAubert Grégoire <gregaubert@users.noreply.github.com>
Fri, 3 Mar 2017 15:15:22 +0000 (16:15 +0100)
committerGitHub <noreply@github.com>
Fri, 3 Mar 2017 15:15:22 +0000 (16:15 +0100)
15 files changed:
server/sonar-web/src/main/js/apps/projects/components/PageSidebar.js
server/sonar-web/src/main/js/apps/projects/filters/CoverageFilter.js
server/sonar-web/src/main/js/apps/projects/filters/DuplicationsFilter.js
server/sonar-web/src/main/js/apps/projects/filters/Filter.js
server/sonar-web/src/main/js/apps/projects/filters/IssuesFilter.js
server/sonar-web/src/main/js/apps/projects/filters/LanguageFilter.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projects/filters/LanguageFilterOption.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projects/filters/QualityGateFilter.js
server/sonar-web/src/main/js/apps/projects/filters/SizeFilter.js
server/sonar-web/src/main/js/apps/projects/store/actions.js
server/sonar-web/src/main/js/apps/projects/store/facetsDuck.js
server/sonar-web/src/main/js/apps/projects/store/utils.js
server/sonar-web/src/main/js/store/languages/reducer.js
server/sonar-web/src/main/js/store/rootReducer.js
server/sonar-web/src/main/less/components/search-navigator.less

index a2c0ecd4a3e1d39868912f507d03ed5edd15b837..9020ef0748cca1ebdf6590cf7124deea76832cc6 100644 (file)
@@ -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>
     );
   }
index aace2cebd68f61c9e29f482305b466f50a33e11c..ab7dd2fa33603671e92bf7603e43eb00919ff1f3 100644 (file)
@@ -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}
index 250e431c6b3fbb2298d50816db76bbea79aa9446..a7549c745895f374069cf18c55f8828ef0fb350c 100644 (file)
@@ -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}
index efb0f54ff7d3f204f7064cd5dbdb92e39043f56b..b10b4162d63c24dfb813d785006ded11aa6ec797 100644 (file)
@@ -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 () {
index d7fc35e78e62a2ff317fba6f2b3f991593aed347..8d3bcc0c582b8424c4c6575e9719c5b87d2025ae 100644 (file)
@@ -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 (file)
index 0000000..4e3e109
--- /dev/null
@@ -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 (file)
index 0000000..002a8f0
--- /dev/null
@@ -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);
index 5e5b55a2f316642c16b96ac3b47e178b3681cf7d..4adf70bb5e8dc940624d401414bfdefb8448b473 100644 (file)
@@ -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}
index 8b49fc9d2763ec8aa03d06662a4eff54438db9a2..5633034b737baf6814d2a51c9f2a709b1a2b653c 100644 (file)
@@ -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}
index ccae02bfc5e9a7fc68368839768183393cfa8d14..237df655e0f875a083201a26f518d52ca61fd961 100644 (file)
@@ -53,7 +53,8 @@ const FACETS = [
   'coverage',
   'duplicated_lines_density',
   'ncloc',
-  'alert_status'
+  'alert_status',
+  'language'
 ];
 
 const onFail = dispatch => error => {
index 03640a8b2f47ef8c93d25a40229dd28d5eddce3b..9523540b065004f915d0437b2ae846126f9d12aa 100644 (file)
@@ -29,7 +29,8 @@ const CUMULATIVE_FACETS = [
   'maintainability',
   'coverage',
   'duplications',
-  'size'
+  'size',
+  'language'
 ];
 
 const REVERSED_FACETS = [
index 58291976a09714be15b10476ed92c5bbda303519..c193d42d57f2ba777a9a6ff9d60842632d76eca8 100644 (file)
@@ -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];
 };
index 72483b929472d2487a16b5a1f32c7e21ef1b2672..84aafb8632e79dc5c66342619443310568412d15 100644 (file)
@@ -33,3 +33,7 @@ export default reducer;
 export const getLanguages = state => (
     state
 );
+
+export const getLanguageByKey = (state, key) => (
+    state[key]
+);
index aee309845c2e5ead300d74a8abc911efa6b2f4d3..4e6ce44a284e2e31b0a9a91a81beecf925bafe3f 100644 (file)
@@ -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 => (
index d283981ed1a6bdf3a062286c7ab283f2596ecc28..0a17c8ac4dcc6ed587c81cd0177ba966f7cb49c9 100644 (file)
@@ -84,6 +84,7 @@
   background-color: transparent;
 
   .search-navigator-facet-list,
+  .search-navigator-facet-empty,
   .search-navigator-facet-container {
     display: none;
   }
   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 {