]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-8990 Add the list of members of an organization
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>
Fri, 24 Mar 2017 10:26:27 +0000 (11:26 +0100)
committerGrégoire Aubert <gregaubert@users.noreply.github.com>
Fri, 31 Mar 2017 08:29:27 +0000 (10:29 +0200)
25 files changed:
server/sonar-web/src/main/js/apps/organizations/components/MembersList.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/organizations/components/MembersListItem.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/organizations/components/OrganizationMembers.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/organizations/components/OrganizationMembersContainer.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/organizations/components/PageHeader.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/organizations/components/__tests__/MembersList-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/organizations/components/__tests__/MembersListItem-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationMembers-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/organizations/components/__tests__/PageHeader-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersList-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersListItem-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationMembers-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/PageHeader-test.js.snap [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/users/components/UsersSearch.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/users/components/__tests__/UserSearch-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserSearch-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/components/controls/__tests__/ListFooter-test.js
server/sonar-web/src/main/js/components/ui/Avatar.js
server/sonar-web/src/main/js/components/ui/__tests__/Avatar-test.js
server/sonar-web/src/main/less/components/page.less
server/sonar-web/src/main/less/components/search.less
sonar-core/src/main/resources/org/sonar/l10n/core.properties

diff --git a/server/sonar-web/src/main/js/apps/organizations/components/MembersList.js b/server/sonar-web/src/main/js/apps/organizations/components/MembersList.js
new file mode 100644 (file)
index 0000000..156f826
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * 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 MembersListItem from './MembersListItem';
+import type { Member } from '../../../store/organizationsMembers/actions';
+import type { Organization } from '../../../store/organizations/duck';
+
+type Props = {
+  members: Array<Member>,
+  organization?: Organization
+};
+
+export default class MembersList extends React.PureComponent {
+  props: Props;
+
+  render() {
+    return (
+      <table className="data zebra">
+        <tbody>
+          {this.props.members.map(member => (
+            <MembersListItem key={member.login} member={member} organization={this.props.organization} />
+          ))}
+        </tbody>
+      </table>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/MembersListItem.js b/server/sonar-web/src/main/js/apps/organizations/components/MembersListItem.js
new file mode 100644 (file)
index 0000000..496d950
--- /dev/null
@@ -0,0 +1,54 @@
+/*
+ * 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 Avatar from '../../../components/ui/Avatar';
+import { translate } from '../../../helpers/l10n';
+import { formatMeasure } from '../../../helpers/measures';
+import type { Member } from '../../../store/organizationsMembers/actions';
+import type { Organization } from '../../../store/organizations/duck';
+
+type Props = {
+  member: Member,
+  organization: Organization
+};
+
+const AVATAR_SIZE: number = 36;
+
+export default class MembersListItem extends React.PureComponent {
+  props: Props;
+
+  render() {
+    const { member, organization } = this.props;
+    return (
+      <tr>
+        <td className="thin nowrap">
+          <Avatar hash={member.avatar} size={AVATAR_SIZE} />
+        </td>
+        <td className="nowrap text-middle"><strong>{member.name}</strong></td>
+        {organization.canAdmin &&
+          <td className="text-right text-middle">
+            {translate('organization.members.x_group(s)', formatMeasure(member.groupCount, 'INT'))}
+          </td>
+        }
+      </tr>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationMembers.js b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationMembers.js
new file mode 100644 (file)
index 0000000..b3e252c
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ * 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 PageHeader from './PageHeader';
+import MembersList from './MembersList';
+import UsersSearch from '../../users/components/UsersSearch';
+import ListFooter from '../../../components/controls/ListFooter';
+import type { Organization } from '../../../store/organizations/duck';
+import type { Member } from '../../../store/organizationsMembers/actions';
+
+type Props = {
+  members: Array<Member>,
+  status: { loading?: boolean, total?: number, pageIndex?: number, query?: string },
+  organization: Organization,
+  fetchOrganizationMembers: (organizationKey: string, query?: string) => void,
+  fetchMoreOrganizationMembers: (organizationKey: string, query?: string) => void,
+};
+
+export default class OrganizationMembers extends React.PureComponent {
+  props: Props;
+
+  componentDidMount() {
+    const notLoadedYet = this.props.members.length < 1 || this.props.status.query != null;
+    if (!this.props.loading && notLoadedYet) {
+      this.handleSearchMembers();
+    }
+  }
+
+  handleSearchMembers = (query?: string) => {
+    this.props.fetchOrganizationMembers(this.props.organization.key, query);
+  };
+
+  handleLoadMoreMembers = () => {
+    this.props.fetchMoreOrganizationMembers(this.props.organization.key, this.props.status.query);
+  };
+
+  addMember() {
+    // TODO Not yet implemented
+  }
+
+  render() {
+    const { organization, status, members } = this.props;
+    return (
+      <div className="page page-limited">
+        <PageHeader loading={status.loading} total={status.total} />
+        <UsersSearch onSearch={this.handleSearchMembers} />
+        <MembersList members={members} organization={organization} />
+        {status.total != null &&
+          <ListFooter
+            count={members.length}
+            total={status.total}
+            ready={!status.loading}
+            loadMore={this.handleLoadMoreMembers}
+          />}
+      </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationMembersContainer.js b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationMembersContainer.js
new file mode 100644 (file)
index 0000000..27dd084
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+ * 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 OrganizationMembers from './OrganizationMembers';
+import {
+  getOrganizationByKey,
+  getOrganizationMembersLogins,
+  getUsersByLogins,
+  getOrganizationMembersState
+} from '../../../store/rootReducer';
+import { fetchOrganizationMembers, fetchMoreOrganizationMembers } from '../actions';
+
+const mapStateToProps = (state, ownProps) => {
+  const { organizationKey } = ownProps.params;
+  const memberLogins = getOrganizationMembersLogins(state, organizationKey);
+  return {
+    members: getUsersByLogins(state, memberLogins),
+    organization: getOrganizationByKey(state, organizationKey),
+    status: getOrganizationMembersState(state, organizationKey)
+  };
+};
+
+export default connect(mapStateToProps, { fetchOrganizationMembers, fetchMoreOrganizationMembers })(
+  OrganizationMembers
+);
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/PageHeader.js b/server/sonar-web/src/main/js/apps/organizations/components/PageHeader.js
new file mode 100644 (file)
index 0000000..7ea0ae7
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * 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 { formatMeasure } from '../../../helpers/measures';
+import { translate } from '../../../helpers/l10n';
+
+type Props = {
+  loading: boolean,
+  total?: number,
+  children?: {}
+};
+
+export default class PageHeader extends React.PureComponent {
+  props: Props;
+
+  render() {
+    return (
+      <header className="page-header">
+        {this.props.loading && <i className="spinner" />}
+        {this.props.children}
+        {this.props.total != null &&
+          <span className="page-totalcount">
+            <strong>{formatMeasure(this.props.total, 'INT')}</strong>
+            {' '}
+            {translate('organization.members.member(s)')}
+          </span>}
+      </header>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/MembersList-test.js b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/MembersList-test.js
new file mode 100644 (file)
index 0000000..d96f628
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ * 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 { shallow } from 'enzyme';
+import MembersList from '../MembersList';
+
+const organization = { key: 'foo', name: 'Foo' };
+const members = [
+  { login: 'admin', name: 'Admin Istrator', avatar: '', groupCount: 3 },
+  { login: 'john', name: 'John Doe', avatar: '7daf6c79d4802916d83f6266e24850af', groupCount: 1 }
+];
+
+it('should render a list of members of an organization', () => {
+  const wrapper = shallow(
+    <MembersList
+      organization={organization}
+      members={members}
+    />
+  );
+  expect(wrapper).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/MembersListItem-test.js b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/MembersListItem-test.js
new file mode 100644 (file)
index 0000000..d0cb52e
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * 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 { shallow } from 'enzyme';
+import MembersListItem from '../MembersListItem';
+
+const organization = { key: 'foo', name: 'Foo' };
+const admin = { login: 'admin', name: 'Admin Istrator', avatar: '', groupCount: 3 };
+const john = { login: 'john', name: 'John Doe', avatar: '7daf6c79d4802916d83f6266e24850af', groupCount: 1 };
+
+it('should not render actions and groups for non admin', () => {
+  const wrapper = shallow(
+    <MembersListItem
+      organization={organization}
+      member={john}
+    />
+  );
+  expect(wrapper).toMatchSnapshot();
+});
+
+it('should render actions and groups for admin', () => {
+  const wrapper = shallow(
+    <MembersListItem
+      organization={{ ...organization, canAdmin: true }}
+      member={admin}
+    />
+  );
+  expect(wrapper).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationMembers-test.js b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationMembers-test.js
new file mode 100644 (file)
index 0000000..461722d
--- /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 { shallow } from 'enzyme';
+import OrganizationMembers from '../OrganizationMembers';
+
+const organization = { key: 'foo', name: 'Foo' };
+const members = [
+  { login: 'admin', name: 'Admin Istrator', avatar: '', groupCount: 3 },
+  { login: 'john', name: 'John Doe', avatar: '7daf6c79d4802916d83f6266e24850af', groupCount: 1 }
+];
+const status = { total: members.length };
+const fetchOrganizationMembers = jest.fn();
+const fetchMoreOrganizationMembers = jest.fn();
+
+it('should not render actions for non admin', () => {
+  const wrapper = shallow(
+    <OrganizationMembers
+      organization={organization}
+      members={members}
+      status={status}
+      fetchOrganizationMembers={fetchOrganizationMembers}
+      fetchMoreOrganizationMembers={fetchMoreOrganizationMembers}
+    />
+  );
+  expect(wrapper).toMatchSnapshot();
+});
+
+it('should render actions for admin', () => {
+  const wrapper = shallow(
+    <OrganizationMembers
+      organization={{ ...organization, canAdmin: true }}
+      members={members}
+      status={{ ...status, loading: true }}
+      fetchOrganizationMembers={fetchOrganizationMembers}
+      fetchMoreOrganizationMembers={fetchMoreOrganizationMembers}
+    />
+  );
+  expect(wrapper).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/PageHeader-test.js b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/PageHeader-test.js
new file mode 100644 (file)
index 0000000..06f9ef0
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * 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 { shallow } from 'enzyme';
+import PageHeader from '../PageHeader';
+
+it('should render the members page header', () => {
+  const wrapper = shallow(
+    <PageHeader />
+  );
+  expect(wrapper).toMatchSnapshot();
+  wrapper.setProps({ loading: true });
+  expect(wrapper.find('.spinner')).toMatchSnapshot();
+});
+
+it('should render the members page header with the total', () => {
+  const wrapper = shallow(<PageHeader total="5" />);
+  expect(wrapper).toMatchSnapshot();
+});
+
+it('should render its children', () => {
+  const wrapper = shallow(
+    <PageHeader loading={true} total="5">
+      <span>children test</span>
+    </PageHeader>
+  );
+  expect(wrapper).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersList-test.js.snap b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersList-test.js.snap
new file mode 100644 (file)
index 0000000..11a42a6
--- /dev/null
@@ -0,0 +1,37 @@
+exports[`test should render a list of members of an organization 1`] = `
+<table
+  className="data zebra">
+  <tbody>
+    <MembersListItem
+      member={
+        Object {
+          "avatar": "",
+          "groupCount": 3,
+          "login": "admin",
+          "name": "Admin Istrator",
+        }
+      }
+      organization={
+        Object {
+          "key": "foo",
+          "name": "Foo",
+        }
+      } />
+    <MembersListItem
+      member={
+        Object {
+          "avatar": "7daf6c79d4802916d83f6266e24850af",
+          "groupCount": 1,
+          "login": "john",
+          "name": "John Doe",
+        }
+      }
+      organization={
+        Object {
+          "key": "foo",
+          "name": "Foo",
+        }
+      } />
+  </tbody>
+</table>
+`;
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersListItem-test.js.snap b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersListItem-test.js.snap
new file mode 100644 (file)
index 0000000..debea9c
--- /dev/null
@@ -0,0 +1,37 @@
+exports[`test should not render actions and groups for non admin 1`] = `
+<tr>
+  <td
+    className="thin nowrap">
+    <Connect(Avatar)
+      hash="7daf6c79d4802916d83f6266e24850af"
+      size={36} />
+  </td>
+  <td
+    className="nowrap text-middle">
+    <strong>
+      John Doe
+    </strong>
+  </td>
+</tr>
+`;
+
+exports[`test should render actions and groups for admin 1`] = `
+<tr>
+  <td
+    className="thin nowrap">
+    <Connect(Avatar)
+      hash=""
+      size={36} />
+  </td>
+  <td
+    className="nowrap text-middle">
+    <strong>
+      Admin Istrator
+    </strong>
+  </td>
+  <td
+    className="text-right text-middle">
+    organization.members.x_group(s).3
+  </td>
+</tr>
+`;
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationMembers-test.js.snap b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationMembers-test.js.snap
new file mode 100644 (file)
index 0000000..3af82c1
--- /dev/null
@@ -0,0 +1,77 @@
+exports[`test should not render actions for non admin 1`] = `
+<div
+  className="page page-limited">
+  <PageHeader
+    total={2} />
+  <UsersSearch
+    onSearch={[Function]} />
+  <MembersList
+    members={
+      Array [
+        Object {
+          "avatar": "",
+          "groupCount": 3,
+          "login": "admin",
+          "name": "Admin Istrator",
+        },
+        Object {
+          "avatar": "7daf6c79d4802916d83f6266e24850af",
+          "groupCount": 1,
+          "login": "john",
+          "name": "John Doe",
+        },
+      ]
+    }
+    organization={
+      Object {
+        "key": "foo",
+        "name": "Foo",
+      }
+    } />
+  <ListFooter
+    count={2}
+    loadMore={[Function]}
+    ready={true}
+    total={2} />
+</div>
+`;
+
+exports[`test should render actions for admin 1`] = `
+<div
+  className="page page-limited">
+  <PageHeader
+    loading={true}
+    total={2} />
+  <UsersSearch
+    onSearch={[Function]} />
+  <MembersList
+    members={
+      Array [
+        Object {
+          "avatar": "",
+          "groupCount": 3,
+          "login": "admin",
+          "name": "Admin Istrator",
+        },
+        Object {
+          "avatar": "7daf6c79d4802916d83f6266e24850af",
+          "groupCount": 1,
+          "login": "john",
+          "name": "John Doe",
+        },
+      ]
+    }
+    organization={
+      Object {
+        "canAdmin": true,
+        "key": "foo",
+        "name": "Foo",
+      }
+    } />
+  <ListFooter
+    count={2}
+    loadMore={[Function]}
+    ready={false}
+    total={2} />
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/PageHeader-test.js.snap b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/PageHeader-test.js.snap
new file mode 100644 (file)
index 0000000..9e31b22
--- /dev/null
@@ -0,0 +1,42 @@
+exports[`test should render its children 1`] = `
+<header
+  className="page-header">
+  <i
+    className="spinner" />
+  <span>
+    children test
+  </span>
+  <span
+    className="page-totalcount">
+    <strong>
+      5
+    </strong>
+     
+    organization.members.member(s)
+  </span>
+</header>
+`;
+
+exports[`test should render the members page header 1`] = `
+<header
+  className="page-header" />
+`;
+
+exports[`test should render the members page header 2`] = `
+<i
+  className="spinner" />
+`;
+
+exports[`test should render the members page header with the total 1`] = `
+<header
+  className="page-header">
+  <span
+    className="page-totalcount">
+    <strong>
+      5
+    </strong>
+     
+    organization.members.member(s)
+  </span>
+</header>
+`;
index c1ffa729acf018fdbeb6a59e5f1e72889c940a8d..4e221313939f7cc5b67eb12790c62ead1d562ba4 100644 (file)
@@ -153,6 +153,11 @@ export default class OrganizationNavigation extends React.Component {
                   {translate('projects.page')}
                 </Link>
               </li>
+              <li>
+                <Link to={`/organizations/${organization.key}/members`} activeClassName="active">
+                  {translate('organization.members.page')}
+                </Link>
+              </li>
               {organization.canAdmin && this.renderAdministration()}
             </ul>
           </div>
index 02421f1020dc3ee540004251145a7c654406782d..8fbbcd9b6a30bc93ff2255c56c7dd53ed8dcf6fe 100644 (file)
@@ -35,6 +35,15 @@ exports[`test admin 1`] = `
             projects.page
           </Link>
         </li>
+        <li>
+          <Link
+            activeClassName="active"
+            onlyActiveOnIndex={false}
+            style={Object {}}
+            to="/organizations/foo/members">
+            organization.members.page
+          </Link>
+        </li>
         <li
           className="">
           <a
@@ -147,6 +156,15 @@ exports[`test regular user 1`] = `
             projects.page
           </Link>
         </li>
+        <li>
+          <Link
+            activeClassName="active"
+            onlyActiveOnIndex={false}
+            style={Object {}}
+            to="/organizations/foo/members">
+            organization.members.page
+          </Link>
+        </li>
       </ul>
     </div>
   </div>
@@ -190,6 +208,15 @@ exports[`test undeletable org 1`] = `
             projects.page
           </Link>
         </li>
+        <li>
+          <Link
+            activeClassName="active"
+            onlyActiveOnIndex={false}
+            style={Object {}}
+            to="/organizations/foo/members">
+            organization.members.page
+          </Link>
+        </li>
         <li
           className="">
           <a
index 0fdd10615268faa67a9aa9fc4981ca0979154dd4..eae57052d3ea28bb65ed611cd0dacb01e8d39266 100644 (file)
@@ -23,6 +23,7 @@ import OrganizationFavoriteProjects from './components/OrganizationFavoriteProje
 import OrganizationAdmin from './components/OrganizationAdmin';
 import OrganizationEdit from './components/OrganizationEdit';
 import OrganizationGroups from './components/OrganizationGroups';
+import OrganizationMembersContainer from './components/OrganizationMembersContainer';
 import OrganizationPermissions from './components/OrganizationPermissions';
 import OrganizationPermissionTemplates from './components/OrganizationPermissionTemplates';
 import OrganizationProjectsManagement from './components/OrganizationProjectsManagement';
@@ -49,6 +50,10 @@ const routes = [
         path: 'projects/favorite',
         component: OrganizationFavoriteProjects
       },
+      {
+        path: 'members',
+        component: OrganizationMembersContainer
+      },
       {
         component: OrganizationAdmin,
         childRoutes: [
diff --git a/server/sonar-web/src/main/js/apps/users/components/UsersSearch.js b/server/sonar-web/src/main/js/apps/users/components/UsersSearch.js
new file mode 100644 (file)
index 0000000..5c17f90
--- /dev/null
@@ -0,0 +1,82 @@
+/*
+ * 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 { debounce } from 'lodash';
+import classNames from 'classnames';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+
+type Props = {
+  onSearch: (query?: string) => void
+};
+
+type State = {
+  query?: string
+};
+
+export default class UsersSearch extends React.PureComponent {
+  props: Props;
+  state: State = {
+    query: ''
+  }
+
+  constructor(props: Props) {
+    super(props);
+    this.handleSearch = debounce(this.handleSearch, 250);
+  }
+
+  handleSearch = (query: string) => {
+    this.props.onSearch(query);
+  }
+
+  handleInputChange = ({ target }: { target: HTMLInputElement }) => {
+    this.setState({ query: target.value });
+    if (!target.value || target.value.length >= 2) {
+      this.handleSearch(target.value);
+    }
+  };
+
+  render() {
+    const { query } = this.state;
+    const inputClassName = classNames('search-box-input', {
+      touched: query != null && query !== '' && query.length < 2
+    });
+    return (
+      <div className="panel panel-vertical bordered-bottom spacer-bottom">
+        <div className="search-box">
+          <button className="search-box-submit button-clean">
+            <i className="icon-search" />
+          </button>
+          <input
+            type="search"
+            value={query}
+            className={inputClassName}
+            placeholder={translate('search_verb')}
+            onChange={this.handleInputChange}
+            autoComplete="off"
+          />
+          <span className="note spacer-left text-middle">
+            {translateWithParameters('select2.tooShort', 2)}
+          </span>
+        </div>
+      </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/UserSearch-test.js b/server/sonar-web/src/main/js/apps/users/components/__tests__/UserSearch-test.js
new file mode 100644 (file)
index 0000000..33a4c3c
--- /dev/null
@@ -0,0 +1,41 @@
+/*
+ * 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 { shallow } from 'enzyme';
+import UsersSearch from '../UsersSearch';
+
+const onSearch = jest.fn();
+
+it('should render correctly without any search query', () => {
+  const wrapper = shallow(<UsersSearch onSearch={onSearch} query={null} />);
+  expect(wrapper).toMatchSnapshot();
+});
+
+it('should render with a search query', () => {
+  const wrapper = shallow(<UsersSearch onSearch={onSearch} query={'foo'} />);
+  expect(wrapper).toMatchSnapshot();
+});
+
+it('should display a help message when there is less than 2 characters', () => {
+  const wrapper = shallow(<UsersSearch onSearch={onSearch} query={'a'} />);
+  expect(wrapper).toMatchSnapshot();
+  wrapper.setState({ query: 'foo' });
+  expect(wrapper).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserSearch-test.js.snap b/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserSearch-test.js.snap
new file mode 100644 (file)
index 0000000..edc0fee
--- /dev/null
@@ -0,0 +1,99 @@
+exports[`test should display a help message when there is less than 2 characters 1`] = `
+<div
+  className="panel panel-vertical bordered-bottom spacer-bottom">
+  <div
+    className="search-box">
+    <button
+      className="search-box-submit button-clean">
+      <i
+        className="icon-search" />
+    </button>
+    <input
+      autoComplete="off"
+      className="search-box-input"
+      onChange={[Function]}
+      placeholder="search_verb"
+      type="search"
+      value="" />
+    <span
+      className="note spacer-left text-middle">
+      select2.tooShort.2
+    </span>
+  </div>
+</div>
+`;
+
+exports[`test should display a help message when there is less than 2 characters 2`] = `
+<div
+  className="panel panel-vertical bordered-bottom spacer-bottom">
+  <div
+    className="search-box">
+    <button
+      className="search-box-submit button-clean">
+      <i
+        className="icon-search" />
+    </button>
+    <input
+      autoComplete="off"
+      className="search-box-input"
+      onChange={[Function]}
+      placeholder="search_verb"
+      type="search"
+      value="foo" />
+    <span
+      className="note spacer-left text-middle">
+      select2.tooShort.2
+    </span>
+  </div>
+</div>
+`;
+
+exports[`test should render correctly without any search query 1`] = `
+<div
+  className="panel panel-vertical bordered-bottom spacer-bottom">
+  <div
+    className="search-box">
+    <button
+      className="search-box-submit button-clean">
+      <i
+        className="icon-search" />
+    </button>
+    <input
+      autoComplete="off"
+      className="search-box-input"
+      onChange={[Function]}
+      placeholder="search_verb"
+      type="search"
+      value="" />
+    <span
+      className="note spacer-left text-middle">
+      select2.tooShort.2
+    </span>
+  </div>
+</div>
+`;
+
+exports[`test should render with a search query 1`] = `
+<div
+  className="panel panel-vertical bordered-bottom spacer-bottom">
+  <div
+    className="search-box">
+    <button
+      className="search-box-submit button-clean">
+      <i
+        className="icon-search" />
+    </button>
+    <input
+      autoComplete="off"
+      className="search-box-input"
+      onChange={[Function]}
+      placeholder="search_verb"
+      type="search"
+      value="" />
+    <span
+      className="note spacer-left text-middle">
+      select2.tooShort.2
+    </span>
+  </div>
+</div>
+`;
index c8adb510bd563a851039feda5cdca1213cf05252..8d5f42cb719e4f6f69d5d744fc9a3b0eac9462d3 100644 (file)
@@ -22,6 +22,8 @@ import React from 'react';
 import ListFooter from '../ListFooter';
 import { click } from '../../../helpers/testUtils';
 
+const loadMore = jest.fn();
+
 it('should render "3 of 5 shown"', () => {
   const listFooter = shallow(<ListFooter count={3} total={5} />);
   expect(listFooter.text()).toContain('x_of_y_shown.3.5');
@@ -32,8 +34,12 @@ it('should not render "show more"', () => {
   expect(listFooter.find('a').length).toBe(0);
 });
 
+it('should not render "show more"', () => {
+  const listFooter = shallow(<ListFooter count={5} total={5} loadMore={loadMore} />);
+  expect(listFooter.find('a').length).toBe(0);
+});
+
 it('should "show more"', () => {
-  const loadMore = jest.fn();
   const listFooter = shallow(<ListFooter count={3} total={5} loadMore={loadMore} />);
   const link = listFooter.find('a');
   expect(link.length).toBe(1);
index 12e6e5500a9e247cbffba396a205febb2b354752..dea1f233288d38426a4b073a2932cc859e1079bc 100644 (file)
@@ -28,6 +28,7 @@ class Avatar extends React.Component {
     enableGravatar: React.PropTypes.bool.isRequired,
     gravatarServerUrl: React.PropTypes.string.isRequired,
     email: React.PropTypes.string,
+    hash: React.PropTypes.string,
     size: React.PropTypes.number.isRequired,
     className: React.PropTypes.string
   };
@@ -37,7 +38,7 @@ class Avatar extends React.Component {
       return null;
     }
 
-    const emailHash = md5.md5((this.props.email || '').toLowerCase()).trim();
+    const emailHash = this.props.hash || md5.md5((this.props.email || '').toLowerCase()).trim();
     const url = this.props.gravatarServerUrl
       .replace('{EMAIL_MD5}', emailHash)
       .replace('{SIZE}', this.props.size * 2);
index a83df0fa337f91d9b570b8d63c0e35c8649c1fe9..d52a47ad4ac8919efb1c68d191ad5607a0046921 100644 (file)
@@ -50,3 +50,19 @@ it('should not render', () => {
   );
   expect(avatar.is('img')).toBe(false);
 });
+
+it('should be able to render with hash only', () => {
+  const avatar = shallow(
+    <Avatar
+      enableGravatar={true}
+      gravatarServerUrl={gravatarServerUrl}
+      hash="7daf6c79d4802916d83f6266e24850af"
+      size={30}
+    />
+  );
+  expect(avatar.is('img')).toBe(true);
+  expect(avatar.prop('width')).toBe(30);
+  expect(avatar.prop('height')).toBe(30);
+  expect(avatar.prop('alt')).toBeUndefined();
+  expect(avatar.prop('src')).toBe('http://example.com/7daf6c79d4802916d83f6266e24850af.jpg?s=60');
+});
index fc40dab1f57a14074009e3efe0ded52b11d5a7f5..ec4df7160779654311a0edcd5d6ed2b1b207eea8 100644 (file)
@@ -74,6 +74,8 @@
 }
 
 .page-header {
+  position: relative;
+
   .clearfix;
   margin-bottom: 20px;
 
     top: 3px;
     margin-left: 8px;
   }
+
+  .page-totalcount {
+    position: absolute;
+    bottom: -50px;
+    right: 0;
+  }
 }
 
 .page-title {
       width: 260px;
     }
   }
-}
\ No newline at end of file
+}
index f76c61c340b8c6ffb239afe0ce671f38d8eff736..c7da998c101edea72243f017dc639a2f06567911 100644 (file)
   width: 250px;
   border: none !important;
   font-size: @baseFontSize;
+
+  & ~ .note {
+    opacity: 0;
+    transition: opacity 0.3s ease;
+  }
+
+  &.touched ~ .note {
+    opacity: 1;
+  }
 }
 
 .search-box-submit {
index eaeb0a821f5e0e5b2a9cc32ae7acf23eb4d5b12b..0d4555c94bad0ac080531e7d7445398c5a8f7e94 100644 (file)
@@ -1765,6 +1765,12 @@ user.scm_account_already_used=The scm account '{0}' is already used by user(s) :
 user.login_or_email_used_as_scm_account=Login and email are automatically considered as SCM accounts
 user.password_cant_be_changed_on_external_auth=Password cannot be changed when external authentication is used
 
+#------------------------------------------------------------------------------
+#
+# USERS PAGE
+#
+#------------------------------------------------------------------------------
+users.create=Create User
 
 #------------------------------------------------------------------------------
 #
@@ -2810,3 +2816,7 @@ organization.name.description=Name of the organization (2 to 64 characters).
 organization.updated=Organization details have been updated.
 organization.url=Url
 organization.url.description=Url of the homepage of the organization.
+organization.members.page=Members
+organization.members.add=Add member
+organization.members.x_group(s)={0} group(s)
+organization.members.member(s)=member(s)