]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-9005 support rules for organizations
authorStas Vilchik <vilchiks@gmail.com>
Tue, 28 Mar 2017 14:42:37 +0000 (16:42 +0200)
committerStas Vilchik <stas-vilchik@users.noreply.github.com>
Mon, 3 Apr 2017 08:38:52 +0000 (10:38 +0200)
41 files changed:
server/sonar-web/src/main/js/apps/about/components/AboutApp.js
server/sonar-web/src/main/js/apps/about/components/AboutAppForSonarQubeDotCom.js
server/sonar-web/src/main/js/apps/about/components/AboutRulesForSonarQubeDotCom.js
server/sonar-web/src/main/js/apps/about/components/AboutStandards.js
server/sonar-web/src/main/js/apps/coding-rules/components/CodingRulesAppContainer.js
server/sonar-web/src/main/js/apps/coding-rules/init.js
server/sonar-web/src/main/js/apps/coding-rules/rule-details-view.js
server/sonar-web/src/main/js/apps/coding-rules/rule/custom-rule-view.js
server/sonar-web/src/main/js/apps/coding-rules/rule/custom-rules-view.js
server/sonar-web/src/main/js/apps/coding-rules/rule/rule-description-view.js
server/sonar-web/src/main/js/apps/coding-rules/rule/rule-issues-view.js
server/sonar-web/src/main/js/apps/coding-rules/rule/rule-meta-view.js
server/sonar-web/src/main/js/apps/coding-rules/rule/rule-parameters-view.js
server/sonar-web/src/main/js/apps/coding-rules/rule/rule-profile-view.js
server/sonar-web/src/main/js/apps/coding-rules/templates/rule/coding-rules-custom-rule.hbs
server/sonar-web/src/main/js/apps/coding-rules/templates/rule/coding-rules-custom-rules.hbs
server/sonar-web/src/main/js/apps/coding-rules/templates/rule/coding-rules-rule-description.hbs
server/sonar-web/src/main/js/apps/coding-rules/templates/rule/coding-rules-rule-issues.hbs
server/sonar-web/src/main/js/apps/coding-rules/templates/rule/coding-rules-rule-meta.hbs
server/sonar-web/src/main/js/apps/coding-rules/templates/rule/coding-rules-rule-profile.hbs
server/sonar-web/src/main/js/apps/organizations/components/OrganizationRules.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.js
server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigation-test.js.snap
server/sonar-web/src/main/js/apps/organizations/routes.js
server/sonar-web/src/main/js/apps/quality-profiles/changelog/Changelog.js
server/sonar-web/src/main/js/apps/quality-profiles/changelog/ChangelogContainer.js
server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonContainer.js
server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonResults.js
server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.js
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRules.js
server/sonar-web/src/main/js/apps/quality-profiles/home/Evolution.js
server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionDeprecated.js
server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionRules.js
server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListRow.js
server/sonar-web/src/main/js/components/issue/issue-view.js
server/sonar-web/src/main/js/components/workspace/main.js
server/sonar-web/src/main/js/components/workspace/templates/workspace-rule.hbs
server/sonar-web/src/main/js/components/workspace/views/rule-view.js
server/sonar-web/src/main/js/helpers/handlebars/profileUrl.js [deleted file]
server/sonar-web/src/main/js/helpers/handlebars/rulePermalink.js [deleted file]
server/sonar-web/src/main/js/helpers/urls.js

index a0f80af5dcaa8bbb3030c02745be86aff01907fc..d76e6e3b4ba41faaca5d8b853879b523d274d042 100644 (file)
@@ -33,7 +33,7 @@ import AboutStandards from './AboutStandards';
 import AboutScanners from './AboutScanners';
 import { searchProjects } from '../../../api/components';
 import { getFacet } from '../../../api/issues';
-import { getCurrentUser, getSettingValue } from '../../../store/rootReducer';
+import { getAppState, getCurrentUser, getSettingValue } from '../../../store/rootReducer';
 import { translate } from '../../../helpers/l10n';
 import { fetchAboutPageSettings } from '../actions';
 import AboutAppForSonarQubeDotCom from './AboutAppForSonarQubeDotCom';
@@ -53,6 +53,10 @@ class AboutApp extends React.Component {
   mounted: boolean;
 
   props: {
+    appState: {
+      defaultOrganization: string,
+      organizationsEnabled: boolean
+    },
     currentUser: { isLoggedIn: boolean },
     customText?: string,
     fetchAboutPageSettings: () => Promise<*>,
@@ -115,6 +119,7 @@ class AboutApp extends React.Component {
     if (sonarqubeDotCom && sonarqubeDotCom.value === 'true') {
       return (
         <AboutAppForSonarQubeDotCom
+          appState={this.props.appState}
           bugs={bugs}
           codeSmells={codeSmells}
           currentUser={this.props.currentUser}
@@ -180,7 +185,7 @@ class AboutApp extends React.Component {
               <AboutQualityGates />
             </div>
             <div className="flex-column flex-column-half about-page-group-boxes">
-              <AboutStandards />
+              <AboutStandards appState={this.props.appState} />
             </div>
           </div>
 
@@ -192,6 +197,7 @@ class AboutApp extends React.Component {
 }
 
 const mapStateToProps = state => ({
+  appState: getAppState(state),
   currentUser: getCurrentUser(state),
   customText: getSettingValue(state, 'sonar.lf.aboutText'),
   sonarqubeDotCom: getSettingValue(state, 'sonar.lf.sonarqube.com.enabled')
index 6d43c38a8f7707518207862dde369b96aba90fa3..addce0a24965b8fc910d95f829776c6f96021f5e 100644 (file)
@@ -34,6 +34,10 @@ import '../sonarqube-dot-com-styles.css';
 
 export default class AboutAppForSonarQubeDotCom extends React.Component {
   props: {
+    appState: {
+      defaultOrganization: string,
+      organizationsEnabled: boolean
+    },
     bugs: number,
     codeSmells: number,
     currentUser: { isLoggedIn: boolean },
@@ -76,7 +80,7 @@ export default class AboutAppForSonarQubeDotCom extends React.Component {
           </div>
         </div>
 
-        <AboutRulesForSonarQubeDotCom />
+        <AboutRulesForSonarQubeDotCom appState={this.props.appState} />
 
         <div className="about-page-container">
           {customText != null &&
@@ -102,7 +106,7 @@ export default class AboutAppForSonarQubeDotCom extends React.Component {
               <AboutQualityGates />
             </div>
             <div className="flex-column flex-column-half about-page-group-boxes">
-              <AboutStandards />
+              <AboutStandards appState={this.props.appState} />
             </div>
           </div>
 
index 140ab0444d06f94e1e65b8d6caaa5d5bcf0ad45b..59fef3421c96a6b7a337402c94818faf3c70805b 100644 (file)
@@ -23,11 +23,20 @@ import { Link } from 'react-router';
 import { getRulesUrl } from '../../../helpers/urls';
 
 export default class AboutRulesForSonarQubeDotCom extends React.Component {
+  props: {
+    appState: {
+      defaultOrganization: string,
+      organizationsEnabled: boolean
+    }
+  };
+
   render() {
+    const organization = this.props.appState.defaultOrganization;
+
     return (
       <div className="sqcom-about-rules">
         <div className="about-page-container">
-          <Link to={getRulesUrl()} className="sqcom-about-rules-link">
+          <Link to={getRulesUrl(null, organization)} className="sqcom-about-rules-link">
             +3,000 rules
             <span className="spacer-left">
               <svg width="15" height="36" viewBox="0 0 15 36">
@@ -40,17 +49,27 @@ export default class AboutRulesForSonarQubeDotCom extends React.Component {
               </svg>
             </span>
           </Link>
-          <Link to={getRulesUrl({ languages: 'js' })} className="sqcom-about-rules-link">
+          <Link
+            to={getRulesUrl({ languages: 'js' }, organization)}
+            className="sqcom-about-rules-link">
             JavaScript
           </Link>
-          <Link to={getRulesUrl({ languages: 'java' })} className="sqcom-about-rules-link">
+          <Link
+            to={getRulesUrl({ languages: 'java' }, organization)}
+            className="sqcom-about-rules-link">
             Java
           </Link>
-          <Link to={getRulesUrl({ languages: 'c,cpp' })} className="sqcom-about-rules-link">
+          <Link
+            to={getRulesUrl({ languages: 'c,cpp' }, organization)}
+            className="sqcom-about-rules-link">
             C/C++
           </Link>
-          <Link to={getRulesUrl({ languages: 'cs' })} className="sqcom-about-rules-link">C#</Link>
-          <Link to={getRulesUrl()} className="button">And More</Link>
+          <Link
+            to={getRulesUrl({ languages: 'cs' }, organization)}
+            className="sqcom-about-rules-link">
+            C#
+          </Link>
+          <Link to={getRulesUrl(null, organization)} className="button">And More</Link>
         </div>
       </div>
     );
index 295fabe38df9681dc446bf38863baabde5c18e68..a2991500f3162ec8fde7c83b2faf012aef29989c 100644 (file)
@@ -27,9 +27,21 @@ const link = 'https://redirect.sonarsource.com/doc/rules.html';
 
 const owaspTags = 'owasp-a1,owasp-a2,owasp-a3,owasp-a4,owasp-a5,' +
   'owasp-a6,owasp-a7,owasp-a8,owasp-a9,owasp-a10';
+const sans25Tags = 'sans-top25-porous,sans-top25-risky,sans-top25-insecure';
 
 export default class AboutStandards extends React.Component {
+  props: {
+    appState: {
+      defaultOrganization: string,
+      organizationsEnabled: boolean
+    }
+  };
+
   render() {
+    const organization = this.props.appState.organizationsEnabled
+      ? this.props.appState.defaultOrganization
+      : null;
+
     return (
       <div className="boxed-group">
         <h2>{translate('about_page.standards')}</h2>
@@ -39,34 +51,34 @@ export default class AboutStandards extends React.Component {
           <div className="spacer-top">
             <ul className="list-inline">
               <li>
-                <Link to={getRulesUrl({ tags: 'misra' })} className="link-with-icon">
+                <Link to={getRulesUrl({ tags: 'misra' }, organization)} className="link-with-icon">
                   <i className="icon-tags" />
                   <span className="little-spacer-left">MISRA</span>
                 </Link>
               </li>
               <li>
-                <Link to={getRulesUrl({ tags: 'cert' })} className="link-with-icon">
+                <Link to={getRulesUrl({ tags: 'cert' }, organization)} className="link-with-icon">
                   <i className="icon-tags" />
                   <span className="little-spacer-left">CERT</span>
                 </Link>
               </li>
               <li>
-                <Link to={getRulesUrl({ tags: 'cwe' })} className="link-with-icon">
+                <Link to={getRulesUrl({ tags: 'cwe' }, organization)} className="link-with-icon">
                   <i className="icon-tags" />
                   <span className="little-spacer-left">CWE</span>
                 </Link>
               </li>
               <li>
-                <Link to={getRulesUrl({ tags: owaspTags })} className="link-with-icon">
+                <Link
+                  to={getRulesUrl({ tags: owaspTags }, organization)}
+                  className="link-with-icon">
                   <i className="icon-tags" />
                   <span className="little-spacer-left">OWASP Top 10</span>
                 </Link>
               </li>
               <li>
                 <Link
-                  to={getRulesUrl({
-                    tags: 'sans-top25-porous,sans-top25-risky,sans-top25-insecure'
-                  })}
+                  to={getRulesUrl({ tags: sans25Tags }, organization)}
                   className="link-with-icon">
                   <i className="icon-tags" />
                   <span className="little-spacer-left">SANS Top 25</span>
index 91b561597b92933f66b6c67eafc3d715bedf1319..c7bb6c70ed3529c9c21e7500578f38f52e7538c6 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+// @flow
 import React from 'react';
 import init from '../init';
 
-export default class CodingRulesAppContainer extends React.Component {
+export default class CodingRulesAppContainer extends React.PureComponent {
+  stop: ?() => void;
+  props: {
+    params: {
+      organizationKey?: string
+    }
+  };
+
   componentDidMount() {
-    this.stop = init(this.refs.container);
+    this.stop = init(this.refs.container, this.props.params.organizationKey);
   }
 
   componentWillUnmount() {
-    this.stop();
+    if (this.stop) {
+      this.stop();
+    }
   }
 
   render() {
index f25d2ad0c10be092d1c3ae8fd0ba5a8fb5dfc2fa..1a92814b70c7e17423cfebbacac201af338fb6ee 100644 (file)
@@ -17,6 +17,7 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+// @flow
 import $ from 'jquery';
 import { sortBy } from 'lodash';
 import Backbone from 'backbone';
@@ -34,10 +35,14 @@ import FiltersView from './filters-view';
 
 const App = new Marionette.Application();
 
-App.on('start', function(el) {
-  $.get(window.baseUrl + '/api/rules/app')
+App.on('start', function(options: { el: HTMLElement, organization: ?string }) {
+  const data = options.organization ? { organization: options.organization } : {};
+  $.get(window.baseUrl + '/api/rules/app', data)
     .done(r => {
+      App.canCreateCustomRule = r.canCreateCustomRule;
+      App.canCustomizeRule = r.canCustomizeRule;
       App.canWrite = r.canWrite;
+      App.organization = options.organization;
       App.qualityProfiles = sortBy(r.qualityprofiles, ['name', 'lang']);
       App.languages = { ...r.languages, none: 'None' };
       App.qualityProfiles.forEach(profile => {
@@ -47,7 +52,7 @@ App.on('start', function(el) {
       App.statuses = r.statuses;
     })
     .done(() => {
-      this.layout = new Layout({ el });
+      this.layout = new Layout({ el: options.el });
       this.layout.render();
       $('#footer').addClass('search-navigator-footer');
 
@@ -81,7 +86,7 @@ App.on('start', function(el) {
       });
       this.layout.filtersRegion.show(this.filtersView);
 
-      key.setScope('list');
+      window.key.setScope('list');
       this.router = new Router({
         app: this
       });
@@ -89,10 +94,11 @@ App.on('start', function(el) {
     });
 });
 
-export default function(el) {
-  App.start(el);
+export default function(el: HTMLElement, organization: ?string) {
+  App.start({ el, organization });
 
   return () => {
+    // $FlowFixMe
     Backbone.history.stop();
     App.layout.destroy();
     $('#footer').removeClass('search-navigator-footer');
index 2fa5eb3f2f4039e5ad1b84125460ceb9983f294a..11ed2ef6b74b52ba4308911526d054926bd71903 100644 (file)
@@ -171,7 +171,7 @@ export default Marionette.LayoutView.extend({
 
   serializeData() {
     const isCustom = this.model.has('templateKey');
-    const isEditable = this.options.app.canWrite && isCustom;
+    const isEditable = this.options.app.canCustomizeRule && isCustom;
     let qualityProfilesVisible = true;
 
     if (this.model.get('isTemplate')) {
@@ -182,7 +182,6 @@ export default Marionette.LayoutView.extend({
       ...Marionette.ItemView.prototype.serializeData.apply(this, arguments),
       isEditable,
       qualityProfilesVisible,
-      canWrite: this.options.app.canWrite,
       allTags: union(this.model.get('sysTags'), this.model.get('tags'))
     };
   }
index 1a5ff0ccdfe69231c3dfde04f2733958a0a2ca9e..295590a3bcbacf7b475ae3ed9e05e31e6c15b30a 100644 (file)
@@ -47,7 +47,7 @@ export default Marionette.ItemView.extend({
   serializeData() {
     return {
       ...Marionette.ItemView.prototype.serializeData.apply(this, arguments),
-      canWrite: this.options.app.canWrite,
+      canDeleteCustomRule: this.options.app.canCreateCustomRule,
       templateRule: this.options.templateRule,
       permalink: window.baseUrl + '/coding_rules/#rule_key=' + encodeURIComponent(this.model.id)
     };
index e8f9c0752c11af806a9ea66db26fdec8582531cc..d6ec8a461cdd895e7b1f057f28efb804939af05a 100644 (file)
@@ -56,7 +56,7 @@ export default Marionette.CompositeView.extend({
   serializeData() {
     return {
       ...Marionette.ItemView.prototype.serializeData.apply(this, arguments),
-      canWrite: this.options.app.canWrite
+      canCreateCustomRule: this.options.app.canCreateCustomRule
     };
   }
 });
index 62d516c543d6f269c3fdf1c6718c9185aa7fc55a..0e942ef4bfb017fa688399fdcb8dbf62f9e2b353 100644 (file)
@@ -94,12 +94,10 @@ export default Marionette.ItemView.extend({
   },
 
   serializeData() {
-    const isEditable = this.options.app.canWrite && this.model.get('isCustom');
-
     return {
       ...Marionette.ItemView.prototype.serializeData.apply(this, arguments),
-      isEditable,
-      canWrite: this.options.app.canWrite
+      isCustom: this.model.get('isCustom'),
+      canCustomizeRule: this.options.app.canCustomizeRule
     };
   }
 });
index d13deae57e0d912c1a46157ebedb22a27a4d502d..384270c29193f59036a6f2ad4906baf5c230ceff 100644 (file)
@@ -20,6 +20,7 @@
 import $ from 'jquery';
 import Marionette from 'backbone.marionette';
 import Template from '../templates/rule/coding-rules-rule-issues.hbs';
+import { getComponentIssuesUrl } from '../../../helpers/urls';
 
 export default Marionette.ItemView.extend({
   template: Template,
@@ -34,7 +35,6 @@ export default Marionette.ItemView.extend({
   },
 
   requestIssues() {
-    const that = this;
     const url = window.baseUrl + '/api/issues/search';
     const options = {
       rules: this.model.id,
@@ -42,6 +42,10 @@ export default Marionette.ItemView.extend({
       ps: 1,
       facets: 'projectUuids'
     };
+    const { organization } = this.options.app;
+    if (organization) {
+      Object.assign(options, { organization });
+    }
     return $.get(url, options).done(r => {
       const projectsFacet = r.facets.find(facet => facet.property === 'projectUuids');
       let projects = projectsFacet != null ? projectsFacet.values : [];
@@ -49,11 +53,16 @@ export default Marionette.ItemView.extend({
         const projectBase = r.components.find(component => component.uuid === project.val);
         return {
           ...project,
-          name: projectBase != null ? projectBase.longName : ''
+          name: projectBase != null ? projectBase.longName : '',
+          issuesUrl: projectBase != null &&
+            getComponentIssuesUrl(projectBase.key, {
+              resolved: 'false',
+              rules: this.model.id
+            })
         };
       });
-      that.projects = projects;
-      that.total = r.total;
+      this.projects = projects;
+      this.total = r.total;
     });
   },
 
@@ -61,10 +70,7 @@ export default Marionette.ItemView.extend({
     return {
       ...Marionette.ItemView.prototype.serializeData.apply(this, arguments),
       total: this.total,
-      projects: this.projects,
-      baseSearchUrl: window.baseUrl +
-        '/issues/search#resolved=false|rules=' +
-        encodeURIComponent(this.model.id)
+      projects: this.projects
     };
   }
 });
index ba8ed5311ba60903efd31e5c73e206f76c8c5df8..0a0b22ade850d4e488b01b76603e46cd6d9a4bf3 100644 (file)
@@ -106,11 +106,15 @@ export default Marionette.ItemView.extend(RuleFilterMixin).extend({
   },
 
   serializeData() {
+    const permalinkPath = this.options.app.organization
+      ? `/organizations/${this.options.app.organization}/rules`
+      : '/coding_rules';
+
     return {
       ...Marionette.ItemView.prototype.serializeData.apply(this, arguments),
-      canWrite: this.options.app.canWrite,
+      canCustomizeRule: this.options.app.canCustomizeRule,
       allTags: union(this.model.get('sysTags'), this.model.get('tags')),
-      permalink: window.baseUrl + '/coding_rules#rule_key=' + encodeURIComponent(this.model.id)
+      permalink: window.baseUrl + permalinkPath + '#rule_key=' + encodeURIComponent(this.model.id)
     };
   }
 });
index 8923b74d374c8adcde25c443cba9671fbbfe12a7..209860ab4cda63d6e36c4f53cc29d88f963e6eeb 100644 (file)
@@ -30,15 +30,5 @@ export default Marionette.ItemView.extend({
   onRender() {
     const params = this.model.get('params');
     this.$el.toggleClass('hidden', params == null || params.length === 0);
-  },
-
-  serializeData() {
-    const isEditable = this.options.app.canWrite && this.model.get('isCustom');
-
-    return {
-      ...Marionette.ItemView.prototype.serializeData.apply(this, arguments),
-      isEditable,
-      canWrite: this.options.app.canWrite
-    };
   }
 });
index eab8d1be804e34f7894a55b0e9ec28f916900e6f..8120484ae41d7f51d817f3891d2a5dd07ba1473f 100644 (file)
@@ -123,6 +123,9 @@ export default Marionette.ItemView.extend({
     const myProfile = this.options.app.qualityProfiles.find(
       p => p.key === this.model.get('qProfile')
     );
+    if (!myProfile) {
+      return null;
+    }
     const parentKey = myProfile.parentKey;
     const parent = { ...this.options.app.qualityProfiles.find(p => p.key === parentKey) };
     const parentActiveInfo = this.model.collection.findWhere({ qProfile: parentKey }) ||
@@ -147,14 +150,26 @@ export default Marionette.ItemView.extend({
     });
   },
 
+  getProfilePath(profileKey) {
+    const { organization } = this.options.app;
+    const encodedKey = encodeURIComponent(profileKey);
+    return organization
+      ? `${window.baseUrl}/organizations/${organization}/quality_profiles/show?key=${encodedKey}`
+      : `${window.baseUrl}/profiles/show?key=${encodedKey}`;
+  },
+
   serializeData() {
+    const parent = this.getParent();
+
     return {
       ...Marionette.ItemView.prototype.serializeData.apply(this, arguments),
+      parent,
       canWrite: this.options.app.canWrite,
-      parent: this.getParent(),
       parameters: this.enhanceParameters(),
       templateKey: this.options.rule.get('templateKey'),
-      isTemplate: this.options.rule.get('isTemplate')
+      isTemplate: this.options.rule.get('isTemplate'),
+      profilePath: this.getProfilePath(this.model.get('key')),
+      parentProfilePath: parent && this.getProfilePath(parent.key)
     };
   }
 });
index b5371ee37f3f81b36f528ec7f2bf5d198d1242a6..eaff503fc170ab58b21916d7a7f6bc3614f9ec1d 100644 (file)
@@ -17,7 +17,7 @@
   &nbsp;
 </td>
 
-{{#if canWrite}}
+{{#if canDeleteCustomRule}}
 <td class="coding-rules-detail-list-actions">
   <div class="button-group">
     <button class="js-delete-custom-rule button-red">
index 36a79d1951ab2426d70028ccccd32eec0e267fd9..bd3899109cfe8c3e78e7698d6c2e7298abbcf001 100644 (file)
@@ -3,7 +3,7 @@
 
   <h3 class="coding-rules-detail-title">{{t 'coding_rules.custom_rules'}}</h3>
 
-  {{#if canWrite}}
+  {{#if canCreateCustomRule}}
     <div class="button-group coding-rules-detail-quality-profiles-activation">
       <button class="js-create-custom-rule">{{t 'coding_rules.create'}}</button>
     </div>
index 3b86bc6d54e8737df309bcb8a15a27112a23841b..d0ab5457eeb2e976b9d902f276ea2ec1410ad328 100644 (file)
@@ -1,19 +1,19 @@
 <div class="coding-rules-detail-description rule-desc markdown">{{{htmlDesc}}}</div>
 
-{{#unless isEditable}}
+{{#unless isCustom}}
   <div class="coding-rules-detail-description coding-rules-detail-description-extra">
     <div id="coding-rules-detail-description-extra">
       {{#if htmlNote}}
         <div class="rule-desc spacer-bottom markdown">{{{htmlNote}}}</div>
       {{/if}}
-      {{#if canWrite}}
+      {{#if canCustomizeRule}}
         <div class="button-group">
           <button id="coding-rules-detail-extend-description">{{t 'coding_rules.extend_description'}}</button>
         </div>
       {{/if}}
     </div>
 
-    {{#if canWrite}}
+    {{#if canCustomizeRule}}
       <div class="coding-rules-detail-extend-description-form hidden">
         <table class="width100">
           <tbody>
index da9a5336d36eecd3ab63f6b62e0697632ab9edeb..f6dc9fe76194af145ef00788611e5a5234a5bfb0 100644 (file)
@@ -1,7 +1,7 @@
 <div class="coding-rule-section-separator"></div>
 
 <h3 class="coding-rules-detail-title">
-  {{t 'coding_rules.issues'}} (<a class="js-rule-issues" href="{{baseSearchUrl}}">{{total}}</a>)
+  {{t 'coding_rules.issues'}} ({{total}})
 </h3>
 
 {{#notEmpty projects}}
@@ -13,7 +13,7 @@
       <tr>
         <td class="coding-rules-detail-list-name">{{name}}</td>
         <td class="coding-rules-detail-list-parameters">
-          <a href="{{../baseSearchUrl}}|projectUuids={{val}}" target="_blank">{{count}}</a>
+          <a href="{{issuesUrl}}" target="_blank">{{count}}</a>
         </td>
       </tr>
     {{/each}}
index fc38adfff0ccfdb7f2d70614ad6c7ed0ea4c81fe..255f97fea0887e59a735e81ea8bfa8c832fd31aa 100644 (file)
     </li>
   {{/notEq}}
 
-  <li class="coding-rules-detail-property coding-rules-detail-tag-list {{#if canWrite}}coding-rules-detail-tags-change{{/if}}"
+  <li class="coding-rules-detail-property coding-rules-detail-tag-list {{#if canCustomizeRule}}coding-rules-detail-tags-change{{/if}}"
       data-toggle="tooltip" data-placement="bottom" title="Rule tags">
     <i class="icon-tags"></i>
     <span>{{#if allTags}}{{join allTags ', '}}{{else}}{{t 'coding_rules.no_tags'}}{{/if}}</span>
     {{#if canWrite}}<i class="icon-dropdown"></i>{{/if}}
   </li>
 
-  {{#if canWrite}}
+  {{#if canCustomizeRule}}
     <li class="coding-rules-detail-property coding-rules-detail-tag-edit hidden">
       {{#if sysTags}}<i class="icon-tags"></i>
         <span>{{join sysTags ', '}}</span>{{/if}}
index 929a1e6d4540972c0ed554b09fc316cd906de009..12d1fe950d5794325fb3f8bfb1e50c965602aa78 100644 (file)
@@ -1,5 +1,5 @@
 <td class="coding-rules-detail-quality-profile-name">
-  <a href="{{profileUrl key}}">
+  <a href="{{profilePath}}">
     {{name}}
   </a>
   {{#if parent}}
@@ -10,7 +10,7 @@
       {{#eq inherit 'INHERITED'}}
         <i class="icon-inheritance" title="{{tp 'coding_rules.inherits' name parent.name}}"></i>
       {{/eq}}
-      <a class="link-base-color" href="{{profileUrl parent.key}}">
+      <a class="link-base-color" href="{{parentProfilePath}}">
         {{parent.name}}
       </a>
     </div>
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationRules.js b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationRules.js
new file mode 100644 (file)
index 0000000..c705464
--- /dev/null
@@ -0,0 +1,28 @@
+/*
+ * 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.
+ */
+// @flow
+import React from 'react';
+import CodingRulesAppContainer from '../../coding-rules/components/CodingRulesAppContainer';
+
+export default class OrganizationRules extends React.PureComponent {
+  render() {
+    return <CodingRulesAppContainer {...this.props} />;
+  }
+}
index fdf72a5009e1464ba36c6c201818af77d09365fa..0675c1872d8354cb5098cf1d92581faf44316e21 100644 (file)
@@ -161,6 +161,11 @@ export default class OrganizationNavigation extends React.Component {
                   {translate('quality_profiles.page')}
                 </Link>
               </li>
+              <li>
+                <Link to={`/organizations/${organization.key}/rules`} activeClassName="active">
+                  {translate('coding_rules.page')}
+                </Link>
+              </li>
               {organization.canAdmin && this.renderAdministration()}
             </ul>
           </div>
index 46358abc847f055a64e3c925c3d219916f34e94d..0d9b7f5706c46114567446f38e101447f82338f4 100644 (file)
@@ -53,6 +53,15 @@ exports[`test admin 1`] = `
             quality_profiles.page
           </Link>
         </li>
+        <li>
+          <Link
+            activeClassName="active"
+            onlyActiveOnIndex={false}
+            style={Object {}}
+            to="/organizations/foo/rules">
+            coding_rules.page
+          </Link>
+        </li>
         <li
           className="">
           <a
@@ -183,6 +192,15 @@ exports[`test regular user 1`] = `
             quality_profiles.page
           </Link>
         </li>
+        <li>
+          <Link
+            activeClassName="active"
+            onlyActiveOnIndex={false}
+            style={Object {}}
+            to="/organizations/foo/rules">
+            coding_rules.page
+          </Link>
+        </li>
       </ul>
     </div>
   </div>
@@ -244,6 +262,15 @@ exports[`test undeletable org 1`] = `
             quality_profiles.page
           </Link>
         </li>
+        <li>
+          <Link
+            activeClassName="active"
+            onlyActiveOnIndex={false}
+            style={Object {}}
+            to="/organizations/foo/rules">
+            coding_rules.page
+          </Link>
+        </li>
         <li
           className="">
           <a
index 1b7f81eda3daa2e6bc32eeab1aee3d9a83e4966e..7f91a2f344d71451f0c2ff349e965826ed392ed9 100644 (file)
@@ -20,6 +20,7 @@
 import OrganizationPage from './components/OrganizationPage';
 import OrganizationProjects from './components/OrganizationProjects';
 import OrganizationFavoriteProjects from './components/OrganizationFavoriteProjects';
+import OrganizationRules from './components/OrganizationRules';
 import OrganizationAdmin from './components/OrganizationAdmin';
 import OrganizationEdit from './components/OrganizationEdit';
 import OrganizationGroups from './components/OrganizationGroups';
@@ -55,6 +56,10 @@ const routes = [
         path: 'members',
         component: OrganizationMembersContainer
       },
+      {
+        path: 'rules',
+        component: OrganizationRules
+      },
       {
         path: 'quality_profiles',
         childRoutes: qualityProfilesRoutes
index 2e84e64d2a30d9c96447c75cf9b2efefee6fa4dc..0996a1d7ce402a5fa72c138f0b02c60caf966d5e 100644 (file)
@@ -33,7 +33,8 @@ type Props = {
     params?: {},
     ruleKey: string,
     ruleName: string
-  }>
+  }>,
+  organization: ?string
 };
 
 export default class Changelog extends React.PureComponent {
@@ -74,7 +75,7 @@ export default class Changelog extends React.PureComponent {
           </td>
 
           <td style={{ lineHeight: '1.5' }}>
-            <Link to={getRulesUrl({ rule_key: event.ruleKey })}>
+            <Link to={getRulesUrl({ rule_key: event.ruleKey }, this.props.organization)}>
               {event.ruleName}
             </Link>
           </td>
index c7ea2d89572620897918e8db9072402e95e42d2f..a1fe68f50afcfdb33bfc90a1c07691c80d8b75cf 100644 (file)
@@ -170,7 +170,7 @@ export default class ChangelogContainer extends React.PureComponent {
 
         {this.state.events != null &&
           this.state.events.length > 0 &&
-          <Changelog events={this.state.events} />}
+          <Changelog events={this.state.events} organization={this.props.organization} />}
 
         {shouldDisplayFooter &&
           <footer className="text-center spacer-top small">
index cdc70a050ed34434ee171d4cad199483d2a25c4c..6428c0606841ded047486b37f2c43f36d056bd50 100644 (file)
@@ -122,6 +122,7 @@ export default class ComparisonContainer extends React.PureComponent {
             inLeft={inLeft}
             inRight={inRight}
             modified={modified}
+            organization={this.props.organization}
           />}
       </div>
     );
index c6acf7385469e287a96789c59afdc55508565adc..ad79758344ab8ef0f20490cd5909b2547812f32b 100644 (file)
@@ -32,7 +32,8 @@ type Props = {
   right: { name: string },
   inLeft: Array<*>,
   inRight: Array<*>,
-  modified: Array<*>
+  modified: Array<*>,
+  organization: ?string
 };
 
 export default class ComparisonResults extends React.PureComponent {
@@ -43,7 +44,7 @@ export default class ComparisonResults extends React.PureComponent {
       <div>
         <SeverityIcon severity={severity} />
         {' '}
-        <Link to={getRulesUrl({ rule_key: rule.key })}>
+        <Link to={getRulesUrl({ rule_key: rule.key }, this.props.organization)}>
           {rule.name}
         </Link>
       </div>
index 1101c1560ce4c62a579f79803e35b8bc9b356a47..d1398b7f90ece1aa333fc655ffe8db8c0636b2c9 100644 (file)
@@ -84,11 +84,13 @@ export default class ProfileActions extends React.PureComponent {
       '/api/qualityprofiles/backup?profileKey=' +
       encodeURIComponent(profile.key);
 
-    // FIXME getRulesUrl
-    const activateMoreUrl = getRulesUrl({
-      qprofile: profile.key,
-      activation: 'false'
-    });
+    const activateMoreUrl = getRulesUrl(
+      {
+        qprofile: profile.key,
+        activation: 'false'
+      },
+      this.props.organization
+    );
 
     return (
       <ul className="dropdown-menu dropdown-menu-right">
index 7a68617ab25cc767b5fbb8f1f86772cf682819c8..6b726e51e96a86bb511977fbf3fd1f1a7fcfa64e 100644 (file)
@@ -33,6 +33,7 @@ const TYPES = ['BUG', 'VULNERABILITY', 'CODE_SMELL'];
 
 type Props = {
   canAdmin: boolean,
+  organization: ?string,
   profile: Profile
 };
 
@@ -133,10 +134,13 @@ export default class ProfileRules extends React.PureComponent {
   }
 
   renderActiveCount() {
-    const rulesUrl = getRulesUrl({
-      qprofile: this.props.profile.key,
-      activation: 'true'
-    });
+    const rulesUrl = getRulesUrl(
+      {
+        qprofile: this.props.profile.key,
+        activation: 'true'
+      },
+      this.props.organization
+    );
 
     if (this.state.activatedTotal == null) {
       return null;
@@ -152,10 +156,13 @@ export default class ProfileRules extends React.PureComponent {
   }
 
   renderActiveTotal() {
-    const rulesUrl = getRulesUrl({
-      qprofile: this.props.profile.key,
-      activation: 'false'
-    });
+    const rulesUrl = getRulesUrl(
+      {
+        qprofile: this.props.profile.key,
+        activation: 'false'
+      },
+      this.props.organization
+    );
 
     if (this.state.total == null || this.state.activatedTotal == null) {
       return null;
@@ -184,11 +191,14 @@ export default class ProfileRules extends React.PureComponent {
   }
 
   renderCountForType(type: string) {
-    const rulesUrl = getRulesUrl({
-      qprofile: this.props.profile.key,
-      activation: 'true',
-      types: type
-    });
+    const rulesUrl = getRulesUrl(
+      {
+        qprofile: this.props.profile.key,
+        activation: 'true',
+        types: type
+      },
+      this.props.organization
+    );
 
     const count = this.state.activatedByType && this.state.activatedByType[type]
       ? this.state.activatedByType[type].count
@@ -206,11 +216,14 @@ export default class ProfileRules extends React.PureComponent {
   }
 
   renderTotalForType(type: string) {
-    const rulesUrl = getRulesUrl({
-      qprofile: this.props.profile.key,
-      activation: 'false',
-      types: type
-    });
+    const rulesUrl = getRulesUrl(
+      {
+        qprofile: this.props.profile.key,
+        activation: 'false',
+        types: type
+      },
+      this.props.organization
+    );
 
     const count = this.state.activatedByType && this.state.activatedByType[type]
       ? this.state.activatedByType[type].count
@@ -242,7 +255,7 @@ export default class ProfileRules extends React.PureComponent {
       return null;
     }
 
-    const url = getDeprecatedActiveRulesUrl({ qprofile: profile.key });
+    const url = getDeprecatedActiveRulesUrl({ qprofile: profile.key }, this.props.organization);
 
     return (
       <div className="quality-profile-rules-deprecated clearfix">
@@ -259,10 +272,13 @@ export default class ProfileRules extends React.PureComponent {
   }
 
   render() {
-    const activateMoreUrl = getRulesUrl({
-      qprofile: this.props.profile.key,
-      activation: 'false'
-    });
+    const activateMoreUrl = getRulesUrl(
+      {
+        qprofile: this.props.profile.key,
+        activation: 'false'
+      },
+      this.props.organization
+    );
 
     return (
       <div className="quality-profile-rules">
index 23995d62e195d55f15a5ace981fac8536e58c1c5..eca194a8209553f47948ac4a1510fe43d34b6153 100644 (file)
@@ -39,7 +39,7 @@ export default class Evolution extends React.PureComponent {
       <div className="quality-profiles-evolution">
         <EvolutionDeprecated organization={organization} profiles={profiles} />
         <EvolutionStagnant organization={organization} profiles={profiles} />
-        <EvolutionRules />
+        <EvolutionRules organization={organization} />
       </div>
     );
   }
index ce8f4ddafbc846ceccc936bf462e24e5590099a4..89d9d87beabd64f3ae21e2e8bcbb1309002aa09b 100644 (file)
@@ -35,8 +35,6 @@ export default class EvolutionDeprecated extends React.PureComponent {
   props: Props;
 
   render() {
-    // FIXME getDeprecatedActiveRulesUrl
-
     const profilesWithDeprecations = this.props.profiles.filter(
       profile => profile.activeDeprecatedRuleCount > 0
     );
@@ -73,7 +71,10 @@ export default class EvolutionDeprecated extends React.PureComponent {
                 {profile.languageName}
                 {', '}
                 <Link
-                  to={getDeprecatedActiveRulesUrl({ qprofile: profile.key })}
+                  to={getDeprecatedActiveRulesUrl(
+                    { qprofile: profile.key },
+                    this.props.organization
+                  )}
                   className="text-muted">
                   {translateWithParameters(
                     'quality_profile.x_rules',
index 553e6bdf62c9d50de0393c0f5d51525ec8555103..123b9394b9130a3b0507a04cc6b870b760b7bd4a 100644 (file)
@@ -39,7 +39,9 @@ function parseRules(r) {
   });
 }
 
-type Props = {};
+type Props = {
+  organization: ?string
+};
 
 export default class EvolutionRules extends React.PureComponent {
   mounted: boolean;
@@ -75,15 +77,16 @@ export default class EvolutionRules extends React.PureComponent {
   }
 
   render() {
-    // FIXME getRulesUrl
-
     if (!this.state.latestRulesTotal) {
       return null;
     }
 
-    const newRulesUrl = getRulesUrl({
-      available_since: PERIOD_START_MOMENT.format('YYYY-MM-DD')
-    });
+    const newRulesUrl = getRulesUrl(
+      {
+        available_since: PERIOD_START_MOMENT.format('YYYY-MM-DD')
+      },
+      this.props.organization
+    );
 
     return (
       <div className="quality-profile-box quality-profiles-evolution-rules">
@@ -96,7 +99,9 @@ export default class EvolutionRules extends React.PureComponent {
           {this.state.latestRules.map(rule => (
             <li key={rule.key} className="spacer-top">
               <div className="text-ellipsis">
-                <Link to={getRulesUrl({ rule_key: rule.key })} className="link-no-underline">
+                <Link
+                  to={getRulesUrl({ rule_key: rule.key }, this.props.organization)}
+                  className="link-no-underline">
                   {' '}
                   {rule.name}
                 </Link>
index 3a53ee66e19f818c2d2d60f778c64e6e66e7a08a..a38a5e9954f3abfe39919938d9221dfd425473d2 100644 (file)
@@ -74,20 +74,24 @@ export default class ProfilesListRow extends React.PureComponent {
   }
 
   renderRules() {
-    // FIXME getRulesUrl
-
     const { profile } = this.props;
 
-    const activeRulesUrl = getRulesUrl({
-      qprofile: profile.key,
-      activation: 'true'
-    });
+    const activeRulesUrl = getRulesUrl(
+      {
+        qprofile: profile.key,
+        activation: 'true'
+      },
+      this.props.organization
+    );
 
-    const deprecatedRulesUrl = getRulesUrl({
-      qprofile: profile.key,
-      activation: 'true',
-      statuses: 'DEPRECATED'
-    });
+    const deprecatedRulesUrl = getRulesUrl(
+      {
+        qprofile: profile.key,
+        activation: 'true',
+        statuses: 'DEPRECATED'
+      },
+      this.props.organization
+    );
 
     return (
       <div>
index eca902adbe9d681187845d88bbb8eed0043eea61..e9b4c47cfcd81598146886496e35cc161dc40060 100644 (file)
@@ -245,7 +245,7 @@ export default Marionette.ItemView.extend({
     const ruleKey = this.model.get('rule');
     // lazy load Workspace
     const Workspace = require('../workspace/main').default;
-    Workspace.openRule({ key: ruleKey });
+    Workspace.openRule({ key: ruleKey, organization: this.model.get('projectOrganization') });
   },
 
   action(action) {
index 8c8cfd05f4406a23d3f95ebddf718ddb7ea03bf1..bb834be5a3ad462a10b1169d4924c0cc21a00ca3 100644 (file)
@@ -17,6 +17,7 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+// @flow
 import $ from 'jquery';
 import Item from './models/item';
 import Items from './models/items';
@@ -77,7 +78,7 @@ Workspace.prototype = {
     return this.open({ ...options, __type__: 'component' });
   },
 
-  openRule(options) {
+  openRule(options: { key: string, organization: string }) {
     return this.open({ ...options, __type__: 'rule' });
   },
 
index 9c43694986bd19bc28c195490844ed9898df8d78..377035a4d2f4af7e51f9e4c0f43ba4d3de4be75a 100644 (file)
@@ -42,7 +42,7 @@
       {{/if}}
 
       <li class="pull-right spacer-left">
-        <a class="icon-link" target="_blank" href="{{rulePermalink key}}"></a>
+        <a class="icon-link" target="_blank" href="{{permalink}}"></a>
       </li>
 
       <li class="pull-right">
index 1fe6440d64ce4d70adbf9493f1f331e89e4d795e..be86ab3e38d8c922ec743a893ec8de1fb7f9d4f1 100644 (file)
@@ -21,6 +21,8 @@ import { union } from 'lodash';
 import Marionette from 'backbone.marionette';
 import BaseView from './base-viewer-view';
 import Template from '../templates/workspace-rule.hbs';
+import { getRulesUrl } from '../../../helpers/urls';
+import { areThereCustomOrganizations } from '../../../store/organizations/utils';
 
 export default BaseView.extend({
   template: Template,
@@ -35,9 +37,15 @@ export default BaseView.extend({
   },
 
   serializeData() {
+    const query = { rule_key: this.model.get('key') };
+    const permalink = areThereCustomOrganizations()
+      ? getRulesUrl(query, this.model.get('organization'))
+      : getRulesUrl(query);
+
     return {
       ...Marionette.LayoutView.prototype.serializeData.apply(this, arguments),
-      allTags: union(this.model.get('sysTags'), this.model.get('tags'))
+      allTags: union(this.model.get('sysTags'), this.model.get('tags')),
+      permalink
     };
   }
 });
diff --git a/server/sonar-web/src/main/js/helpers/handlebars/profileUrl.js b/server/sonar-web/src/main/js/helpers/handlebars/profileUrl.js
deleted file mode 100644 (file)
index dab5f85..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
- * 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.
- */
-module.exports = function(key) {
-  return window.baseUrl + '/profiles/show?key=' + encodeURIComponent(key);
-};
diff --git a/server/sonar-web/src/main/js/helpers/handlebars/rulePermalink.js b/server/sonar-web/src/main/js/helpers/handlebars/rulePermalink.js
deleted file mode 100644 (file)
index e338339..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
- * 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.
- */
-module.exports = function(ruleKey) {
-  return window.baseUrl + '/coding_rules#rule_key=' + encodeURIComponent(ruleKey);
-};
index de25aa553ee134e91cdbe8a2eb57f020c0c0954c..1464e336fb73cc688c815cccaee605373896ac28 100644 (file)
@@ -114,17 +114,19 @@ export function getQualityGateUrl(key) {
  * @param {object} query
  * @returns {string}
  */
-export function getRulesUrl(query) {
+export function getRulesUrl(query, organization?: string) {
+  const path = organization ? `/organizations/${organization}/rules` : '/coding_rules';
+
   if (query) {
     const serializedQuery = Object.keys(query)
       .map(criterion => `${encodeURIComponent(criterion)}=${encodeURIComponent(query[criterion])}`)
       .join('|');
 
     // return a string (not { pathname }) to help react-router's Link handle this properly
-    return '/coding_rules#' + serializedQuery;
+    return path + '#' + serializedQuery;
   }
 
-  return '/coding_rules';
+  return path;
 }
 
 /**
@@ -132,9 +134,9 @@ export function getRulesUrl(query) {
  * @param {object} query
  * @returns {string}
  */
-export function getDeprecatedActiveRulesUrl(query = {}) {
+export function getDeprecatedActiveRulesUrl(query = {}, organization?: string) {
   const baseQuery = { activation: 'true', statuses: 'DEPRECATED' };
-  return getRulesUrl({ ...query, ...baseQuery });
+  return getRulesUrl({ ...query, ...baseQuery }, organization);
 }
 
 export const getProjectsUrl = () => {