diff options
11 files changed, 218 insertions, 13 deletions
diff --git a/server/sonar-web/src/main/js/apps/issues/components/App.js b/server/sonar-web/src/main/js/apps/issues/components/App.js index 60dd9d43f9f..48b6dbfcb0c 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/App.js +++ b/server/sonar-web/src/main/js/apps/issues/components/App.js @@ -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/OrganizationProjectsContainer.js b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationContainer.js index a08e864d1b9..a48cf7f5cd6 100644 --- a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationProjectsContainer.js +++ b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationContainer.js @@ -21,9 +21,8 @@ import React from 'react'; import { connect } from 'react-redux'; import { getCurrentUser, getOrganizationByKey } from '../../../store/rootReducer'; -import { updateOrganization } from '../actions'; -class OrganizationProjectsContainer extends React.PureComponent { +class OrganizationContainer extends React.PureComponent { render() { return React.cloneElement(this.props.children, { currentUser: this.props.currentUser, @@ -37,6 +36,4 @@ const mapStateToProps = (state, ownProps) => ({ currentUser: getCurrentUser(state) }); -const mapDispatchToProps = { updateOrganization }; - -export default connect(mapStateToProps, mapDispatchToProps)(OrganizationProjectsContainer); +export default connect(mapStateToProps)(OrganizationContainer); diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationPage.js b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationPage.js index d08cfabf5d4..9111c0c70e2 100644 --- a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationPage.js +++ b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationPage.js @@ -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/navigation/OrganizationNavigation.js b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.js index e872e6e2ac1..ab54c533014 100644 --- a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.js +++ b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.js @@ -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` || @@ -197,6 +199,19 @@ export default class OrganizationNavigation extends React.PureComponent { </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')} </Link> diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigation-test.js b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigation-test.js index 09254c5aaf3..f4a2b09a38b 100644 --- a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigation-test.js +++ b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigation-test.js @@ -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} /> diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigation-test.js.snap b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigation-test.js.snap index 05104c2e4d8..389fae8e92f 100644 --- a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigation-test.js.snap +++ b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigation-test.js.snap @@ -45,6 +45,23 @@ exports[`admin 1`] = ` activeClassName="active" onlyActiveOnIndex={false} style={Object {}} + to={ + Object { + "pathname": "/organizations/foo/issues", + "query": Object { + "resolved": "false", + }, + } + } + > + issues.page + </Link> + </li> + <li> + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} to="/organizations/foo/members" > organization.members.page @@ -198,6 +215,23 @@ exports[`regular user 1`] = ` activeClassName="active" onlyActiveOnIndex={false} style={Object {}} + to={ + Object { + "pathname": "/organizations/foo/issues", + "query": Object { + "resolved": "false", + }, + } + } + > + issues.page + </Link> + </li> + <li> + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} to="/organizations/foo/members" > organization.members.page @@ -272,6 +306,23 @@ exports[`undeletable org 1`] = ` activeClassName="active" onlyActiveOnIndex={false} style={Object {}} + to={ + Object { + "pathname": "/organizations/foo/issues", + "query": Object { + "resolved": "false", + }, + } + } + > + issues.page + </Link> + </li> + <li> + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} to="/organizations/foo/members" > organization.members.page diff --git a/server/sonar-web/src/main/js/apps/organizations/routes.js b/server/sonar-web/src/main/js/apps/organizations/routes.js index f5880608456..90c22b3270b 100644 --- a/server/sonar-web/src/main/js/apps/organizations/routes.js +++ b/server/sonar-web/src/main/js/apps/organizations/routes.js @@ -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: { @@ -62,6 +63,11 @@ const routes = [ ] }, { + path: 'issues', + component: OrganizationContainer, + childRoutes: issuesRoutes + }, + { path: 'members', component: OrganizationMembersContainer }, diff --git a/tests/src/test/java/org/sonarqube/pageobjects/Navigation.java b/tests/src/test/java/org/sonarqube/pageobjects/Navigation.java index 342d2635462..6bdf9be6156 100644 --- a/tests/src/test/java/org/sonarqube/pageobjects/Navigation.java +++ b/tests/src/test/java/org/sonarqube/pageobjects/Navigation.java @@ -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); } diff --git a/tests/src/test/java/org/sonarqube/pageobjects/issues/IssuesPage.java b/tests/src/test/java/org/sonarqube/pageobjects/issues/IssuesPage.java index d09c894f9d9..572b69b8454 100644 --- a/tests/src/test/java/org/sonarqube/pageobjects/issues/IssuesPage.java +++ b/tests/src/test/java/org/sonarqube/pageobjects/issues/IssuesPage.java @@ -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); diff --git a/tests/src/test/java/org/sonarqube/tests/Category6Suite.java b/tests/src/test/java/org/sonarqube/tests/Category6Suite.java index 40e1f20930f..69ea5870d25 100644 --- a/tests/src/test/java/org/sonarqube/tests/Category6Suite.java +++ b/tests/src/test/java/org/sonarqube/tests/Category6Suite.java @@ -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 index 00000000000..f6f5a1dcaeb --- /dev/null +++ b/tests/src/test/java/org/sonarqube/tests/issue/OrganizationIssuesPageTest.java @@ -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"); + } + +} |