]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-9566 Add Issues link at organization level navbar
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>
Wed, 19 Jul 2017 13:10:29 +0000 (15:10 +0200)
committerGrégoire Aubert <gregoire.aubert@sonarsource.com>
Tue, 25 Jul 2017 07:20:30 +0000 (09:20 +0200)
12 files changed:
server/sonar-web/src/main/js/apps/issues/components/App.js
server/sonar-web/src/main/js/apps/organizations/components/OrganizationContainer.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/organizations/components/OrganizationPage.js
server/sonar-web/src/main/js/apps/organizations/components/OrganizationProjectsContainer.js [deleted file]
server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.js
server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigation-test.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
tests/src/test/java/org/sonarqube/pageobjects/Navigation.java
tests/src/test/java/org/sonarqube/pageobjects/issues/IssuesPage.java
tests/src/test/java/org/sonarqube/tests/Category6Suite.java
tests/src/test/java/org/sonarqube/tests/issue/OrganizationIssuesPageTest.java [new file with mode: 0644]

index 60dd9d43f9fe3a98fea614ea3e4a6768a35d49a3..48b6dbfcb0c58e477194485c4cb783ce39b5b4b7 100644 (file)
@@ -67,6 +67,7 @@ export type Props = {
   fetchIssues: (query: RawQuery) => Promise<*>,
   location: { pathname: string, query: RawQuery },
   onRequestFail: Error => void,
+  organization?: { key: string },
   router: {
     push: ({ pathname: string, query?: RawQuery }) => void,
     replace: ({ pathname: string, query?: RawQuery }) => void
@@ -342,7 +343,7 @@ export default class App extends React.PureComponent {
   };
 
   fetchIssues = (additional?: {}, requestFacets?: boolean = false): Promise<*> => {
-    const { component } = this.props;
+    const { component, organization } = this.props;
     const { myIssues, openFacets, query } = this.state;
 
     const facets = requestFacets
@@ -358,6 +359,10 @@ export default class App extends React.PureComponent {
       ...additional
     };
 
+    if (organization) {
+      parameters.organization = organization.key;
+    }
+
     // only sorting by CREATION_DATE is allowed, so let's sort DESC
     if (query.sort) {
       Object.assign(parameters, { asc: 'false' });
@@ -730,7 +735,7 @@ export default class App extends React.PureComponent {
   }
 
   renderSide(openIssue: ?Issue) {
-    const top = this.props.component ? 95 : 30;
+    const top = this.props.component || this.props.organization ? 95 : 30;
 
     return (
       <div className="layout-page-side-outer">
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationContainer.js b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationContainer.js
new file mode 100644 (file)
index 0000000..a48cf7f
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * 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 { connect } from 'react-redux';
+import { getCurrentUser, getOrganizationByKey } from '../../../store/rootReducer';
+
+class OrganizationContainer extends React.PureComponent {
+  render() {
+    return React.cloneElement(this.props.children, {
+      currentUser: this.props.currentUser,
+      organization: this.props.organization
+    });
+  }
+}
+
+const mapStateToProps = (state, ownProps) => ({
+  organization: getOrganizationByKey(state, ownProps.params.organizationKey),
+  currentUser: getCurrentUser(state)
+});
+
+export default connect(mapStateToProps)(OrganizationContainer);
index d08cfabf5d4b7308c78b31535b6f456914c28672..9111c0c70e2b4442931b6005270b90ddda419168 100644 (file)
@@ -22,10 +22,10 @@ import React from 'react';
 import Helmet from 'react-helmet';
 import { connect } from 'react-redux';
 import OrganizationNavigation from '../navigation/OrganizationNavigation';
+import NotFound from '../../../app/components/NotFound';
 import { fetchOrganization } from '../actions';
-import { getOrganizationByKey } from '../../../store/rootReducer';
+import { getCurrentUser, getOrganizationByKey } from '../../../store/rootReducer';
 import type { Organization } from '../../../store/organizations/duck';
-import NotFound from '../../../app/components/NotFound';
 
 type OwnProps = {
   params: { organizationKey: string }
@@ -33,6 +33,7 @@ type OwnProps = {
 
 type Props = {
   children?: React.Element<*>,
+  currentUser: { isLoggedIn: boolean, showOnboardingTutorial: true },
   location: Object,
   organization: null | Organization,
   params: { organizationKey: string },
@@ -88,7 +89,11 @@ class OrganizationPage extends React.PureComponent {
     return (
       <div>
         <Helmet defaultTitle={organization.name} titleTemplate={'%s - ' + organization.name} />
-        <OrganizationNavigation organization={organization} location={this.props.location} />
+        <OrganizationNavigation
+          currentUser={this.props.currentUser}
+          organization={organization}
+          location={this.props.location}
+        />
         {this.props.children}
       </div>
     );
@@ -96,6 +101,7 @@ class OrganizationPage extends React.PureComponent {
 }
 
 const mapStateToProps = (state, ownProps: OwnProps) => ({
+  currentUser: getCurrentUser(state),
   organization: getOrganizationByKey(state, ownProps.params.organizationKey)
 });
 
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationProjectsContainer.js b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationProjectsContainer.js
deleted file mode 100644 (file)
index a08e864..0000000
+++ /dev/null
@@ -1,42 +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.
- */
-//@flow
-import React from 'react';
-import { connect } from 'react-redux';
-import { getCurrentUser, getOrganizationByKey } from '../../../store/rootReducer';
-import { updateOrganization } from '../actions';
-
-class OrganizationProjectsContainer extends React.PureComponent {
-  render() {
-    return React.cloneElement(this.props.children, {
-      currentUser: this.props.currentUser,
-      organization: this.props.organization
-    });
-  }
-}
-
-const mapStateToProps = (state, ownProps) => ({
-  organization: getOrganizationByKey(state, ownProps.params.organizationKey),
-  currentUser: getCurrentUser(state)
-});
-
-const mapDispatchToProps = { updateOrganization };
-
-export default connect(mapStateToProps, mapDispatchToProps)(OrganizationProjectsContainer);
index e872e6e2ac1ce46d84fe025dac24b4232ac5bf07..ab54c5330143ceeab59f5225f7274b630673b165 100644 (file)
@@ -25,6 +25,7 @@ import { translate } from '../../../helpers/l10n';
 import ContextNavBar from '../../../components/nav/ContextNavBar';
 import NavBarTabs from '../../../components/nav/NavBarTabs';
 import OrganizationIcon from '../../../components/icons-components/OrganizationIcon';
+import { isMySet } from '../../issues/utils';
 import type { Organization } from '../../../store/organizations/duck';
 
 const ADMIN_PATHS = [
@@ -38,6 +39,7 @@ const ADMIN_PATHS = [
 
 export default class OrganizationNavigation extends React.PureComponent {
   props: {
+    currentUser: { isLoggedIn: boolean, showOnboardingTutorial: true },
     location: { pathname: string },
     organization: Organization
   };
@@ -135,7 +137,7 @@ export default class OrganizationNavigation extends React.PureComponent {
   }
 
   render() {
-    const { organization, location } = this.props;
+    const { currentUser, organization, location } = this.props;
 
     const isHomeActive =
       location.pathname === `organizations/${organization.key}/projects` ||
@@ -196,6 +198,19 @@ export default class OrganizationNavigation extends React.PureComponent {
               {translate('projects.page')}
             </Link>
           </li>
+          <li>
+            <Link
+              to={{
+                pathname: `/organizations/${organization.key}/issues`,
+                query:
+                  currentUser.isLoggedIn && isMySet()
+                    ? { resolved: 'false', myIssues: 'true' }
+                    : { resolved: 'false' }
+              }}
+              activeClassName="active">
+              {translate('issues.page')}
+            </Link>
+          </li>
           <li>
             <Link to={`/organizations/${organization.key}/members`} activeClassName="active">
               {translate('organization.members.page')}
index 09254c5aaf399d7aaade83674d1f8f1d5499e5c1..f4a2b09a38b2a0875bed87240cb180684d33a841 100644 (file)
@@ -21,11 +21,16 @@ import React from 'react';
 import { shallow } from 'enzyme';
 import OrganizationNavigation from '../OrganizationNavigation';
 
+jest.mock('../../../issues/utils', () => ({
+  isMySet: () => false
+}));
+
 it('regular user', () => {
   const organization = { key: 'foo', name: 'Foo', canAdmin: false, canDelete: false };
   expect(
     shallow(
       <OrganizationNavigation
+        currentUser={{ isLoggedIn: true }}
         location={{ pathname: '/organizations/foo' }}
         organization={organization}
       />
@@ -38,6 +43,7 @@ it('admin', () => {
   expect(
     shallow(
       <OrganizationNavigation
+        currentUser={{ isLoggedIn: true }}
         location={{ pathname: '/organizations/foo' }}
         organization={organization}
       />
@@ -50,6 +56,7 @@ it('undeletable org', () => {
   expect(
     shallow(
       <OrganizationNavigation
+        currentUser={{ isLoggedIn: true }}
         location={{ pathname: '/organizations/foo' }}
         organization={organization}
       />
index 05104c2e4d81446e1ecc4de6bed0b1552df7cfbb..389fae8e92fabc8916c3c9271c16e543f97f5a82 100644 (file)
@@ -40,6 +40,23 @@ exports[`admin 1`] = `
         projects.page
       </Link>
     </li>
+    <li>
+      <Link
+        activeClassName="active"
+        onlyActiveOnIndex={false}
+        style={Object {}}
+        to={
+          Object {
+            "pathname": "/organizations/foo/issues",
+            "query": Object {
+              "resolved": "false",
+            },
+          }
+        }
+      >
+        issues.page
+      </Link>
+    </li>
     <li>
       <Link
         activeClassName="active"
@@ -193,6 +210,23 @@ exports[`regular user 1`] = `
         projects.page
       </Link>
     </li>
+    <li>
+      <Link
+        activeClassName="active"
+        onlyActiveOnIndex={false}
+        style={Object {}}
+        to={
+          Object {
+            "pathname": "/organizations/foo/issues",
+            "query": Object {
+              "resolved": "false",
+            },
+          }
+        }
+      >
+        issues.page
+      </Link>
+    </li>
     <li>
       <Link
         activeClassName="active"
@@ -267,6 +301,23 @@ exports[`undeletable org 1`] = `
         projects.page
       </Link>
     </li>
+    <li>
+      <Link
+        activeClassName="active"
+        onlyActiveOnIndex={false}
+        style={Object {}}
+        to={
+          Object {
+            "pathname": "/organizations/foo/issues",
+            "query": Object {
+              "resolved": "false",
+            },
+          }
+        }
+      >
+        issues.page
+      </Link>
+    </li>
     <li>
       <Link
         activeClassName="active"
index f58806084563f36dd4afb84e8f0fc355ad86af9f..90c22b3270b2bc25aaa64ffaa4345239c5dbac0d 100644 (file)
@@ -19,8 +19,8 @@
  */
 import OrganizationPage from './components/OrganizationPage';
 import OrganizationPageExtension from '../../app/components/extensions/OrganizationPageExtension';
+import OrganizationContainer from './components/OrganizationContainer';
 import OrganizationProjects from './components/OrganizationProjects';
-import OrganizationProjectsContainer from './components/OrganizationProjectsContainer';
 import OrganizationFavoriteProjects from './components/OrganizationFavoriteProjects';
 import OrganizationRules from './components/OrganizationRules';
 import OrganizationAdmin from './components/OrganizationAdmin';
@@ -32,6 +32,7 @@ import OrganizationPermissionTemplates from './components/OrganizationPermission
 import OrganizationProjectsManagement from './components/OrganizationProjectsManagement';
 import OrganizationDelete from './components/OrganizationDelete';
 import qualityProfilesRoutes from '../quality-profiles/routes';
+import issuesRoutes from '../issues/routes';
 
 const routes = [
   {
@@ -48,7 +49,7 @@ const routes = [
       },
       {
         path: 'projects',
-        component: OrganizationProjectsContainer,
+        component: OrganizationContainer,
         childRoutes: [
           {
             indexRoute: {
@@ -61,6 +62,11 @@ const routes = [
           }
         ]
       },
+      {
+        path: 'issues',
+        component: OrganizationContainer,
+        childRoutes: issuesRoutes
+      },
       {
         path: 'members',
         component: OrganizationMembersContainer
index 342d263546258d478f19168c84531c0e449e72c7..6bdf9be6156a39341578c9dc0cc60ba98a8efb0d 100644 (file)
@@ -31,6 +31,7 @@ import javax.annotation.Nullable;
 import org.openqa.selenium.By;
 import org.openqa.selenium.WebDriver;
 import org.openqa.selenium.html5.WebStorage;
+import org.sonarqube.pageobjects.issues.Issue;
 import org.sonarqube.tests.Tester;
 import org.sonarqube.pageobjects.issues.IssuesPage;
 import org.sonarqube.pageobjects.licenses.LicensesPage;
@@ -88,6 +89,10 @@ public class Navigation {
     return open("/issues", IssuesPage.class);
   }
 
+  public IssuesPage openIssues(String organization) {
+    return open("/organizations/" + organization + "/issues", IssuesPage.class);
+  }
+
   public IssuesPage openComponentIssues(String component) {
     return open("/component_issues?id=" + component, IssuesPage.class);
   }
index d09c894f9d947eaad2478c27872b7079cb79e956..572b69b84549c96c9dc6f96076ce8c769a45b498 100644 (file)
@@ -25,6 +25,7 @@ import java.util.stream.Collectors;
 
 import static com.codeborne.selenide.CollectionCondition.sizeGreaterThan;
 import static com.codeborne.selenide.Condition.exist;
+import static com.codeborne.selenide.Condition.text;
 import static com.codeborne.selenide.Condition.visible;
 import static com.codeborne.selenide.Selenide.$;
 import static com.codeborne.selenide.Selenide.$$;
@@ -39,6 +40,10 @@ public class IssuesPage {
     return $$(".issues .issue");
   }
 
+  private ElementsCollection getIssuesPathComponents() {
+    return $$(".issues-workspace-list-component");
+  }
+
   public List<Issue> getIssues() {
     return getIssuesElements()
       .stream()
@@ -46,11 +51,21 @@ public class IssuesPage {
       .collect(Collectors.toList());
   }
 
+  public IssuesPage issuesCount(Integer count) {
+    this.getIssuesElements().shouldHaveSize(count);
+    return this;
+  }
+
   public Issue getFirstIssue() {
     getIssuesElements().shouldHave(sizeGreaterThan(0));
     return new Issue(getIssuesElements().first());
   }
 
+  public IssuesPage componentsShouldContain(String path) {
+    this.getIssuesPathComponents().forEach(element -> element.shouldHave(text(path)));
+    return this;
+  }
+
   public IssuesPage bulkChangeOpen() {
     $("#issues-bulk-change").shouldBe(visible).click();
     $("#bulk-change-form").shouldBe(visible);
index 40e1f20930f2b9a936f6ad934a643eb9440bf3ff..69ea5870d2557153a669149c9be72bfdcd76adf6 100644 (file)
@@ -28,6 +28,7 @@ import org.junit.runners.Suite;
 import org.sonarqube.tests.authorisation.PermissionTemplateTest;
 import org.sonarqube.tests.issue.IssueTagsTest;
 import org.sonarqube.tests.issue.OrganizationIssueAssignTest;
+import org.sonarqube.tests.issue.OrganizationIssuesPageTest;
 import org.sonarqube.tests.organization.BillingTest;
 import org.sonarqube.tests.organization.OrganizationMembershipTest;
 import org.sonarqube.tests.organization.OrganizationMembershipUiTest;
@@ -57,6 +58,7 @@ import static util.ItUtils.xooPlugin;
 @Suite.SuiteClasses({
   OrganizationIdentityProviderTest.class,
   OrganizationIssueAssignTest.class,
+  OrganizationIssuesPageTest.class,
   OrganizationMembershipTest.class,
   OrganizationMembershipUiTest.class,
   OrganizationQualityProfilesUiTest.class,
diff --git a/tests/src/test/java/org/sonarqube/tests/issue/OrganizationIssuesPageTest.java b/tests/src/test/java/org/sonarqube/tests/issue/OrganizationIssuesPageTest.java
new file mode 100644 (file)
index 0000000..f6f5a1d
--- /dev/null
@@ -0,0 +1,96 @@
+/*
+ * 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.
+ */
+
+package org.sonarqube.tests.issue;
+
+import com.sonar.orchestrator.Orchestrator;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonarqube.pageobjects.Navigation;
+import org.sonarqube.tests.Category6Suite;
+import org.sonarqube.tests.Tester;
+import org.sonarqube.ws.Organizations;
+import org.sonarqube.ws.WsUsers;
+import util.issue.IssueRule;
+
+import static util.ItUtils.restoreProfile;
+import static util.ItUtils.runProjectAnalysis;
+
+public class OrganizationIssuesPageTest {
+
+  @ClassRule
+  public static Orchestrator orchestrator = Category6Suite.ORCHESTRATOR;
+
+  @Rule
+  public Tester tester = new Tester(orchestrator);
+
+  @Rule
+  public IssueRule issueRule = IssueRule.from(orchestrator);
+
+  private Organizations.Organization org1;
+  private Organizations.Organization org2;
+  private WsUsers.CreateWsResponse.User user1;
+  private WsUsers.CreateWsResponse.User user2;
+
+  @Before
+  public void setUp() throws Exception {
+    org1 = tester.organizations().generate();
+    org2 = tester.organizations().generate();
+    user1 = tester.users().generate();
+    user2 = tester.users().generate();
+    tester.organizations().addMember(org1, user1);
+    tester.organizations().addMember(org2, user2);
+    restoreProfile(orchestrator, getClass().getResource("/issue/with-many-rules.xml"), org1.getKey());
+    restoreProfile(orchestrator, getClass().getResource("/issue/with-many-rules.xml"), org2.getKey());
+  }
+
+  @Test
+  public void display_organization_rules_only() {
+    String project1 = provisionProject(org1);
+    analyseProject(project1, org1.getKey());
+    String project2 = provisionProject(org2);
+    analyseProject(project2, org2.getKey());
+    Navigation nav = tester.openBrowser().logIn().submitCredentials(user1.getLogin());
+
+    nav.openIssues(org1.getKey())
+      .issuesCount(2)
+      .componentsShouldContain(org1.getName());
+
+    nav.openIssues()
+      .issuesCount(4);
+  }
+
+  private String provisionProject(Organizations.Organization organization) {
+    return tester.projects().generate(organization).getKey();
+  }
+
+  private void analyseProject(String projectKey, String organization) {
+    runProjectAnalysis(orchestrator, "shared/xoo-multi-modules-sample",
+      "sonar.projectKey", projectKey,
+      "sonar.organization", organization,
+      "sonar.login", "admin",
+      "sonar.password", "admin",
+      "sonar.scm.disabled", "false",
+      "sonar.scm.provider", "xoo");
+  }
+
+}