]> source.dussan.org Git - sonarqube.git/commitdiff
Rewrite part of the permissions app to Typescript
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>
Wed, 29 Nov 2017 13:27:13 +0000 (14:27 +0100)
committerJulien Lancelot <julien.lancelot@sonarsource.com>
Thu, 14 Dec 2017 16:03:35 +0000 (17:03 +0100)
31 files changed:
server/sonar-web/src/main/js/api/permissions.ts
server/sonar-web/src/main/js/app/types.ts
server/sonar-web/src/main/js/apps/organizations/components/OrganizationPermissions.js [deleted file]
server/sonar-web/src/main/js/apps/organizations/components/OrganizationPermissions.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.js [deleted file]
server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigation-test.js [deleted file]
server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigation-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigation-test.js.snap [deleted file]
server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigation-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/permissions/global/components/AllHoldersList.js [deleted file]
server/sonar-web/src/main/js/apps/permissions/global/components/AllHoldersList.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/permissions/global/components/AllHoldersListContainer.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/permissions/global/components/App.js [deleted file]
server/sonar-web/src/main/js/apps/permissions/global/components/App.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/permissions/shared/components/GroupHolder.js [deleted file]
server/sonar-web/src/main/js/apps/permissions/shared/components/GroupHolder.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/permissions/shared/components/HoldersList.js [deleted file]
server/sonar-web/src/main/js/apps/permissions/shared/components/HoldersList.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/permissions/shared/components/PageError.js [deleted file]
server/sonar-web/src/main/js/apps/permissions/shared/components/PageError.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/permissions/shared/components/PermissionHeader.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/permissions/shared/components/SearchForm.js [deleted file]
server/sonar-web/src/main/js/apps/permissions/shared/components/SearchForm.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/permissions/shared/components/UserHolder.js [deleted file]
server/sonar-web/src/main/js/apps/permissions/shared/components/UserHolder.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/App-test.tsx
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ChangeVisibilityForm-test.tsx
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/CreateProjectForm-test.tsx
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Header-test.tsx
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Search-test.tsx

index 8d2cf3e55968bd539caca24443649b56193c25db..39d77276f02ea039dcd99fc9436d59c805e9d14d 100644 (file)
@@ -178,12 +178,20 @@ export function removeProjectCreatorFromTemplate(
   return post('/api/permissions/remove_project_creator_from_template', { templateId, permission });
 }
 
+export interface PermissionUser {
+  login: string;
+  name: string;
+  email?: string;
+  permissions: string[];
+  avatar?: string;
+}
+
 export function getPermissionsUsersForComponent(
   projectKey: string,
   query?: string,
   permission?: string,
   organization?: string
-): Promise<any> {
+): Promise<PermissionUser[]> {
   const data: RequestData = { projectKey, ps: PAGE_SIZE };
   if (query) {
     data.q = query;
@@ -197,12 +205,19 @@ export function getPermissionsUsersForComponent(
   return getJSON('/api/permissions/users', data).then(r => r.users);
 }
 
+export interface PermissionGroup {
+  id: string;
+  name: string;
+  description?: string;
+  permissions: string[];
+}
+
 export function getPermissionsGroupsForComponent(
   projectKey: string,
   query: string = '',
   permission?: string,
   organization?: string
-): Promise<any> {
+): Promise<PermissionGroup[]> {
   const data: RequestData = { projectKey, ps: PAGE_SIZE };
   if (query) {
     data.q = query;
@@ -220,7 +235,7 @@ export function getGlobalPermissionsUsers(
   query?: string,
   permission?: string,
   organization?: string
-): Promise<any> {
+): Promise<PermissionUser[]> {
   const data: RequestData = { ps: PAGE_SIZE };
   if (query) {
     data.q = query;
@@ -238,7 +253,7 @@ export function getGlobalPermissionsGroups(
   query?: string,
   permission?: string,
   organization?: string
-): Promise<any> {
+): Promise<PermissionGroup[]> {
   const data: RequestData = { ps: PAGE_SIZE };
   if (query) {
     data.q = query;
index f18764601e4e38b877c7a236f1933866e468d7cb..a180856c47ad00315c1fa5e6b6634b3b3e31d379 100644 (file)
@@ -108,7 +108,7 @@ export interface Metric {
 }
 
 export interface Organization {
-  adminPages?: Array<{ key: string; name: string }>;
+  adminPages?: { key: string; name: string }[];
   avatar?: string;
   canAdmin?: boolean;
   canDelete?: boolean;
@@ -118,8 +118,8 @@ export interface Organization {
   isDefault?: boolean;
   key: string;
   name: string;
-  pages?: Array<{ key: string; name: string }>;
-  projectVisibility: string;
+  pages?: { key: string; name: string }[];
+  projectVisibility: Visibility;
   url?: string;
 }
 
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationPermissions.js b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationPermissions.js
deleted file mode 100644 (file)
index cab087b..0000000
+++ /dev/null
@@ -1,41 +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 GlobalPermissionsApp from '../../permissions/global/components/App';
-import { getOrganizationByKey } from '../../../store/rootReducer';
-/*:: import type { Organization } from '../../../store/organizations/duck'; */
-
-/*::
-type Props = {
-  organization: Organization
-};
-*/
-
-function OrganizationPermissions(props /*: Props */) {
-  return <GlobalPermissionsApp organization={props.organization} />;
-}
-
-const mapStateToProps = (state, ownProps) => ({
-  organization: getOrganizationByKey(state, ownProps.params.organizationKey)
-});
-
-export default connect(mapStateToProps)(OrganizationPermissions);
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationPermissions.tsx b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationPermissions.tsx
new file mode 100644 (file)
index 0000000..b2136ea
--- /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 * as React from 'react';
+import { connect } from 'react-redux';
+import GlobalPermissionsApp from '../../permissions/global/components/App';
+import { getOrganizationByKey } from '../../../store/rootReducer';
+import { Organization } from '../../../app/types';
+
+interface Props {
+  organization: Organization;
+}
+
+function OrganizationPermissions({ organization }: Props) {
+  return <GlobalPermissionsApp organization={organization} />;
+}
+
+const mapStateToProps = (state: any, ownProps: any) => ({
+  organization: getOrganizationByKey(state, ownProps.params.organizationKey)
+});
+
+export default connect(mapStateToProps)(OrganizationPermissions);
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
deleted file mode 100644 (file)
index 507d43b..0000000
+++ /dev/null
@@ -1,241 +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 { Link } from 'react-router';
-import classNames from 'classnames';
-import * as theme from '../../../app/theme';
-import { translate } from '../../../helpers/l10n';
-import ContextNavBar from '../../../components/nav/ContextNavBar';
-import NavBarTabs from '../../../components/nav/NavBarTabs';
-import OrganizationAvatar from '../../../components/common/OrganizationAvatar';
-import { getQualityGatesUrl } from '../../../helpers/urls';
-/*:: import type { Organization } from '../../../store/organizations/duck'; */
-
-const ADMIN_PATHS = [
-  'edit',
-  'groups',
-  'delete',
-  'permissions',
-  'permission_templates',
-  'projects_management'
-];
-
-export default class OrganizationNavigation extends React.PureComponent {
-  /*:: props: {
-    location: { pathname: string },
-    organization: Organization
-  };
-*/
-
-  renderAdministration(adminActive /*: boolean */) {
-    const { organization } = this.props;
-
-    return (
-      <li className="dropdown">
-        <a
-          className={classNames('dropdown-toggle', { active: adminActive })}
-          data-toggle="dropdown"
-          id="organization-navigation-admin"
-          href="#">
-          {translate('layout.settings')}&nbsp;<i className="icon-dropdown" />
-        </a>
-        <ul className="dropdown-menu">
-          {this.renderAdminExtensions()}
-          <li>
-            <Link to={`/organizations/${organization.key}/groups`} activeClassName="active">
-              {translate('user_groups.page')}
-            </Link>
-          </li>
-          <li>
-            <Link to={`/organizations/${organization.key}/permissions`} activeClassName="active">
-              {translate('permissions.page')}
-            </Link>
-          </li>
-          <li>
-            <Link
-              to={`/organizations/${organization.key}/permission_templates`}
-              activeClassName="active">
-              {translate('permission_templates')}
-            </Link>
-          </li>
-          <li>
-            <Link
-              to={`/organizations/${organization.key}/projects_management`}
-              activeClassName="active">
-              {translate('projects_management')}
-            </Link>
-          </li>
-          <li>
-            <Link to={`/organizations/${organization.key}/edit`} activeClassName="active">
-              {translate('edit')}
-            </Link>
-          </li>
-          {organization.canDelete && (
-            <li>
-              <Link to={`/organizations/${organization.key}/delete`} activeClassName="active">
-                {translate('delete')}
-              </Link>
-            </li>
-          )}
-        </ul>
-      </li>
-    );
-  }
-
-  renderAdminExtensions() {
-    const extensions = this.props.organization.adminPages || [];
-    return extensions.map(this.renderExtension);
-  }
-
-  renderExtension = (extension /*: { key: string, name: string } */) => {
-    const { organization } = this.props;
-    const pathname = `/organizations/${organization.key}/extension/${extension.key}`;
-    return (
-      <li key={extension.key}>
-        <Link to={pathname} activeClassName="active">
-          {extension.name}
-        </Link>
-      </li>
-    );
-  };
-
-  renderExtensions(moreActive /*: boolean */) {
-    const extensions = this.props.organization.pages || [];
-    if (extensions.length > 0) {
-      return (
-        <li className={moreActive ? 'active' : ''}>
-          <a
-            className="dropdown-toggle"
-            id="organization-navigation-more"
-            data-toggle="dropdown"
-            href="#">
-            {translate('more')}&nbsp;<i className="icon-dropdown" />
-          </a>
-          <ul className="dropdown-menu">{extensions.map(this.renderExtension)}</ul>
-        </li>
-      );
-    } else {
-      return null;
-    }
-  }
-
-  render() {
-    const { organization, location } = this.props;
-
-    const isHomeActive =
-      location.pathname === `organizations/${organization.key}/projects` ||
-      location.pathname === `organizations/${organization.key}/projects/favorite`;
-
-    const adminPathsWithExtensions = (organization.adminPages || [])
-      .map(e => `extension/${e.key}`)
-      .concat(ADMIN_PATHS);
-
-    const adminActive = adminPathsWithExtensions.some(path =>
-      location.pathname.endsWith(`organizations/${organization.key}/${path}`)
-    );
-    const moreActive = !adminActive && location.pathname.includes('/extension/');
-
-    return (
-      <ContextNavBar id="context-navigation" height={theme.contextNavHeightRaw}>
-        <div className="navbar-context-header">
-          <h1 className="display-inline-block">
-            <OrganizationAvatar organization={organization} />
-            <Link
-              to={`/organizations/${organization.key}`}
-              className="link-base-color link-no-underline spacer-left">
-              {organization.name}
-            </Link>
-          </h1>
-          {organization.description != null && (
-            <div className="navbar-context-description">
-              <p className="text-limited text-top" title={organization.description}>
-                {organization.description}
-              </p>
-            </div>
-          )}
-        </div>
-
-        <div className="navbar-context-meta">
-          <div className="text-muted">
-            <strong>{translate('organization.key')}:</strong> {organization.key}
-          </div>
-          {organization.url != null && (
-            <div>
-              <p className="text-limited text-top">
-                <a
-                  className="link-underline"
-                  href={organization.url}
-                  title={organization.url}
-                  rel="nofollow">
-                  {organization.url}
-                </a>
-              </p>
-            </div>
-          )}
-        </div>
-
-        <NavBarTabs className="navbar-context-tabs">
-          <li>
-            <Link
-              to={`/organizations/${organization.key}/projects`}
-              className={isHomeActive ? 'active' : ''}>
-              {translate('projects.page')}
-            </Link>
-          </li>
-          <li>
-            <Link
-              to={{
-                pathname: `/organizations/${organization.key}/issues`,
-                query: { resolved: 'false' }
-              }}
-              activeClassName="active">
-              {translate('issues.page')}
-            </Link>
-          </li>
-          <li>
-            <Link
-              to={`/organizations/${organization.key}/quality_profiles`}
-              activeClassName="active">
-              {translate('quality_profiles.page')}
-            </Link>
-          </li>
-          <li>
-            <Link to={`/organizations/${organization.key}/rules`} activeClassName="active">
-              {translate('coding_rules.page')}
-            </Link>
-          </li>
-          <li>
-            <Link to={getQualityGatesUrl(organization.key)} activeClassName="active">
-              {translate('quality_gates.page')}
-            </Link>
-          </li>
-          <li>
-            <Link to={`/organizations/${organization.key}/members`} activeClassName="active">
-              {translate('organization.members.page')}
-            </Link>
-          </li>
-          {this.renderExtensions(moreActive)}
-          {organization.canAdmin && this.renderAdministration(adminActive)}
-        </NavBarTabs>
-      </ContextNavBar>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.tsx b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.tsx
new file mode 100644 (file)
index 0000000..4cffcc4
--- /dev/null
@@ -0,0 +1,239 @@
+/*
+ * 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 * as React from 'react';
+import * as classNames from 'classnames';
+import { Link } from 'react-router';
+import * as theme from '../../../app/theme';
+import { translate } from '../../../helpers/l10n';
+import ContextNavBar from '../../../components/nav/ContextNavBar';
+import NavBarTabs from '../../../components/nav/NavBarTabs';
+import OrganizationAvatar from '../../../components/common/OrganizationAvatar';
+import { getQualityGatesUrl } from '../../../helpers/urls';
+import { Extension, Organization } from '../../../app/types';
+
+const ADMIN_PATHS = [
+  'edit',
+  'groups',
+  'delete',
+  'permissions',
+  'permission_templates',
+  'projects_management'
+];
+
+interface Props {
+  location: { pathname: string };
+  organization: Organization;
+}
+
+export default class OrganizationNavigation extends React.PureComponent<Props> {
+  renderAdministration(adminActive: boolean) {
+    const { organization } = this.props;
+
+    return (
+      <li className="dropdown">
+        <a
+          className={classNames('dropdown-toggle', { active: adminActive })}
+          data-toggle="dropdown"
+          id="organization-navigation-admin"
+          href="#">
+          {translate('layout.settings')}&nbsp;<i className="icon-dropdown" />
+        </a>
+        <ul className="dropdown-menu">
+          {this.renderAdminExtensions()}
+          <li>
+            <Link to={`/organizations/${organization.key}/groups`} activeClassName="active">
+              {translate('user_groups.page')}
+            </Link>
+          </li>
+          <li>
+            <Link to={`/organizations/${organization.key}/permissions`} activeClassName="active">
+              {translate('permissions.page')}
+            </Link>
+          </li>
+          <li>
+            <Link
+              to={`/organizations/${organization.key}/permission_templates`}
+              activeClassName="active">
+              {translate('permission_templates')}
+            </Link>
+          </li>
+          <li>
+            <Link
+              to={`/organizations/${organization.key}/projects_management`}
+              activeClassName="active">
+              {translate('projects_management')}
+            </Link>
+          </li>
+          <li>
+            <Link to={`/organizations/${organization.key}/edit`} activeClassName="active">
+              {translate('edit')}
+            </Link>
+          </li>
+          {organization.canDelete && (
+            <li>
+              <Link to={`/organizations/${organization.key}/delete`} activeClassName="active">
+                {translate('delete')}
+              </Link>
+            </li>
+          )}
+        </ul>
+      </li>
+    );
+  }
+
+  renderAdminExtensions() {
+    const extensions = this.props.organization.adminPages || [];
+    return extensions.map(this.renderExtension);
+  }
+
+  renderExtension = (extension: Extension) => {
+    const { organization } = this.props;
+    const pathname = `/organizations/${organization.key}/extension/${extension.key}`;
+    return (
+      <li key={extension.key}>
+        <Link to={pathname} activeClassName="active">
+          {extension.name}
+        </Link>
+      </li>
+    );
+  };
+
+  renderExtensions(moreActive: boolean) {
+    const extensions = this.props.organization.pages || [];
+    if (extensions.length > 0) {
+      return (
+        <li className={moreActive ? 'active' : ''}>
+          <a
+            className="dropdown-toggle"
+            id="organization-navigation-more"
+            data-toggle="dropdown"
+            href="#">
+            {translate('more')}&nbsp;<i className="icon-dropdown" />
+          </a>
+          <ul className="dropdown-menu">{extensions.map(this.renderExtension)}</ul>
+        </li>
+      );
+    } else {
+      return null;
+    }
+  }
+
+  render() {
+    const { organization, location } = this.props;
+
+    const isHomeActive =
+      location.pathname === `organizations/${organization.key}/projects` ||
+      location.pathname === `organizations/${organization.key}/projects/favorite`;
+
+    const adminPathsWithExtensions = (organization.adminPages || [])
+      .map(e => `extension/${e.key}`)
+      .concat(ADMIN_PATHS);
+
+    const adminActive = adminPathsWithExtensions.some(path =>
+      location.pathname.endsWith(`organizations/${organization.key}/${path}`)
+    );
+    const moreActive = !adminActive && location.pathname.includes('/extension/');
+
+    return (
+      <ContextNavBar id="context-navigation" height={theme.contextNavHeightRaw}>
+        <div className="navbar-context-header">
+          <h1 className="display-inline-block">
+            <OrganizationAvatar organization={organization} />
+            <Link
+              to={`/organizations/${organization.key}`}
+              className="link-base-color link-no-underline spacer-left">
+              {organization.name}
+            </Link>
+          </h1>
+          {organization.description != null && (
+            <div className="navbar-context-description">
+              <p className="text-limited text-top" title={organization.description}>
+                {organization.description}
+              </p>
+            </div>
+          )}
+        </div>
+
+        <div className="navbar-context-meta">
+          <div className="text-muted">
+            <strong>{translate('organization.key')}:</strong> {organization.key}
+          </div>
+          {organization.url != null && (
+            <div>
+              <p className="text-limited text-top">
+                <a
+                  className="link-underline"
+                  href={organization.url}
+                  title={organization.url}
+                  rel="nofollow">
+                  {organization.url}
+                </a>
+              </p>
+            </div>
+          )}
+        </div>
+
+        <NavBarTabs className="navbar-context-tabs">
+          <li>
+            <Link
+              to={`/organizations/${organization.key}/projects`}
+              className={isHomeActive ? 'active' : ''}>
+              {translate('projects.page')}
+            </Link>
+          </li>
+          <li>
+            <Link
+              to={{
+                pathname: `/organizations/${organization.key}/issues`,
+                query: { resolved: 'false' }
+              }}
+              activeClassName="active">
+              {translate('issues.page')}
+            </Link>
+          </li>
+          <li>
+            <Link
+              to={`/organizations/${organization.key}/quality_profiles`}
+              activeClassName="active">
+              {translate('quality_profiles.page')}
+            </Link>
+          </li>
+          <li>
+            <Link to={`/organizations/${organization.key}/rules`} activeClassName="active">
+              {translate('coding_rules.page')}
+            </Link>
+          </li>
+          <li>
+            <Link to={getQualityGatesUrl(organization.key)} activeClassName="active">
+              {translate('quality_gates.page')}
+            </Link>
+          </li>
+          <li>
+            <Link to={`/organizations/${organization.key}/members`} activeClassName="active">
+              {translate('organization.members.page')}
+            </Link>
+          </li>
+          {this.renderExtensions(moreActive)}
+          {organization.canAdmin && this.renderAdministration(adminActive)}
+        </NavBarTabs>
+      </ContextNavBar>
+    );
+  }
+}
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
deleted file mode 100644 (file)
index 3f337c0..0000000
+++ /dev/null
@@ -1,62 +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.
- */
-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
-        location={{ pathname: '/organizations/foo' }}
-        organization={organization}
-      />
-    )
-  ).toMatchSnapshot();
-});
-
-it('admin', () => {
-  const organization = { key: 'foo', name: 'Foo', canAdmin: true, canDelete: true };
-  expect(
-    shallow(
-      <OrganizationNavigation
-        location={{ pathname: '/organizations/foo' }}
-        organization={organization}
-      />
-    )
-  ).toMatchSnapshot();
-});
-
-it('undeletable org', () => {
-  const organization = { key: 'foo', name: 'Foo', canAdmin: true, canDelete: false };
-  expect(
-    shallow(
-      <OrganizationNavigation
-        location={{ pathname: '/organizations/foo' }}
-        organization={organization}
-      />
-    )
-  ).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigation-test.tsx b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigation-test.tsx
new file mode 100644 (file)
index 0000000..e4884f8
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+ * 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 * as React from 'react';
+import { shallow } from 'enzyme';
+import OrganizationNavigation from '../OrganizationNavigation';
+import { Visibility } from '../../../../app/types';
+
+jest.mock('../../../issues/utils', () => ({
+  isMySet: () => false
+}));
+
+const organization = {
+  key: 'foo',
+  name: 'Foo',
+  canAdmin: false,
+  canDelete: false,
+  projectVisibility: Visibility.Public
+};
+
+it('regular user', () => {
+  expect(getWrapper()).toMatchSnapshot();
+});
+
+it('admin', () => {
+  expect(
+    getWrapper({ organization: { ...organization, canAdmin: true, canDelete: true } })
+  ).toMatchSnapshot();
+});
+
+it('undeletable org', () => {
+  expect(
+    getWrapper({ organization: { ...organization, canAdmin: true, canDelete: false } })
+  ).toMatchSnapshot();
+});
+
+function getWrapper(props = {}) {
+  return shallow(
+    <OrganizationNavigation
+      location={{ pathname: '/organizations/foo' }}
+      organization={organization}
+      {...props}
+    />
+  );
+}
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
deleted file mode 100644 (file)
index 66184bb..0000000
+++ /dev/null
@@ -1,520 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`admin 1`] = `
-<ContextNavBar
-  height={72}
-  id="context-navigation"
->
-  <div
-    className="navbar-context-header"
-  >
-    <h1
-      className="display-inline-block"
-    >
-      <OrganizationAvatar
-        organization={
-          Object {
-            "canAdmin": true,
-            "canDelete": true,
-            "key": "foo",
-            "name": "Foo",
-          }
-        }
-      />
-      <Link
-        className="link-base-color link-no-underline spacer-left"
-        onlyActiveOnIndex={false}
-        style={Object {}}
-        to="/organizations/foo"
-      >
-        Foo
-      </Link>
-    </h1>
-  </div>
-  <div
-    className="navbar-context-meta"
-  >
-    <div
-      className="text-muted"
-    >
-      <strong>
-        organization.key
-        :
-      </strong>
-       
-      foo
-    </div>
-  </div>
-  <NavBarTabs
-    className="navbar-context-tabs"
-  >
-    <li>
-      <Link
-        className=""
-        onlyActiveOnIndex={false}
-        style={Object {}}
-        to="/organizations/foo/projects"
-      >
-        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"
-        onlyActiveOnIndex={false}
-        style={Object {}}
-        to="/organizations/foo/quality_profiles"
-      >
-        quality_profiles.page
-      </Link>
-    </li>
-    <li>
-      <Link
-        activeClassName="active"
-        onlyActiveOnIndex={false}
-        style={Object {}}
-        to="/organizations/foo/rules"
-      >
-        coding_rules.page
-      </Link>
-    </li>
-    <li>
-      <Link
-        activeClassName="active"
-        onlyActiveOnIndex={false}
-        style={Object {}}
-        to={
-          Object {
-            "pathname": "/organizations/foo/quality_gates",
-          }
-        }
-      >
-        quality_gates.page
-      </Link>
-    </li>
-    <li>
-      <Link
-        activeClassName="active"
-        onlyActiveOnIndex={false}
-        style={Object {}}
-        to="/organizations/foo/members"
-      >
-        organization.members.page
-      </Link>
-    </li>
-    <li
-      className="dropdown"
-    >
-      <a
-        className="dropdown-toggle"
-        data-toggle="dropdown"
-        href="#"
-        id="organization-navigation-admin"
-      >
-        layout.settings
-        Â 
-        <i
-          className="icon-dropdown"
-        />
-      </a>
-      <ul
-        className="dropdown-menu"
-      >
-        <li>
-          <Link
-            activeClassName="active"
-            onlyActiveOnIndex={false}
-            style={Object {}}
-            to="/organizations/foo/groups"
-          >
-            user_groups.page
-          </Link>
-        </li>
-        <li>
-          <Link
-            activeClassName="active"
-            onlyActiveOnIndex={false}
-            style={Object {}}
-            to="/organizations/foo/permissions"
-          >
-            permissions.page
-          </Link>
-        </li>
-        <li>
-          <Link
-            activeClassName="active"
-            onlyActiveOnIndex={false}
-            style={Object {}}
-            to="/organizations/foo/permission_templates"
-          >
-            permission_templates
-          </Link>
-        </li>
-        <li>
-          <Link
-            activeClassName="active"
-            onlyActiveOnIndex={false}
-            style={Object {}}
-            to="/organizations/foo/projects_management"
-          >
-            projects_management
-          </Link>
-        </li>
-        <li>
-          <Link
-            activeClassName="active"
-            onlyActiveOnIndex={false}
-            style={Object {}}
-            to="/organizations/foo/edit"
-          >
-            edit
-          </Link>
-        </li>
-        <li>
-          <Link
-            activeClassName="active"
-            onlyActiveOnIndex={false}
-            style={Object {}}
-            to="/organizations/foo/delete"
-          >
-            delete
-          </Link>
-        </li>
-      </ul>
-    </li>
-  </NavBarTabs>
-</ContextNavBar>
-`;
-
-exports[`regular user 1`] = `
-<ContextNavBar
-  height={72}
-  id="context-navigation"
->
-  <div
-    className="navbar-context-header"
-  >
-    <h1
-      className="display-inline-block"
-    >
-      <OrganizationAvatar
-        organization={
-          Object {
-            "canAdmin": false,
-            "canDelete": false,
-            "key": "foo",
-            "name": "Foo",
-          }
-        }
-      />
-      <Link
-        className="link-base-color link-no-underline spacer-left"
-        onlyActiveOnIndex={false}
-        style={Object {}}
-        to="/organizations/foo"
-      >
-        Foo
-      </Link>
-    </h1>
-  </div>
-  <div
-    className="navbar-context-meta"
-  >
-    <div
-      className="text-muted"
-    >
-      <strong>
-        organization.key
-        :
-      </strong>
-       
-      foo
-    </div>
-  </div>
-  <NavBarTabs
-    className="navbar-context-tabs"
-  >
-    <li>
-      <Link
-        className=""
-        onlyActiveOnIndex={false}
-        style={Object {}}
-        to="/organizations/foo/projects"
-      >
-        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"
-        onlyActiveOnIndex={false}
-        style={Object {}}
-        to="/organizations/foo/quality_profiles"
-      >
-        quality_profiles.page
-      </Link>
-    </li>
-    <li>
-      <Link
-        activeClassName="active"
-        onlyActiveOnIndex={false}
-        style={Object {}}
-        to="/organizations/foo/rules"
-      >
-        coding_rules.page
-      </Link>
-    </li>
-    <li>
-      <Link
-        activeClassName="active"
-        onlyActiveOnIndex={false}
-        style={Object {}}
-        to={
-          Object {
-            "pathname": "/organizations/foo/quality_gates",
-          }
-        }
-      >
-        quality_gates.page
-      </Link>
-    </li>
-    <li>
-      <Link
-        activeClassName="active"
-        onlyActiveOnIndex={false}
-        style={Object {}}
-        to="/organizations/foo/members"
-      >
-        organization.members.page
-      </Link>
-    </li>
-  </NavBarTabs>
-</ContextNavBar>
-`;
-
-exports[`undeletable org 1`] = `
-<ContextNavBar
-  height={72}
-  id="context-navigation"
->
-  <div
-    className="navbar-context-header"
-  >
-    <h1
-      className="display-inline-block"
-    >
-      <OrganizationAvatar
-        organization={
-          Object {
-            "canAdmin": true,
-            "canDelete": false,
-            "key": "foo",
-            "name": "Foo",
-          }
-        }
-      />
-      <Link
-        className="link-base-color link-no-underline spacer-left"
-        onlyActiveOnIndex={false}
-        style={Object {}}
-        to="/organizations/foo"
-      >
-        Foo
-      </Link>
-    </h1>
-  </div>
-  <div
-    className="navbar-context-meta"
-  >
-    <div
-      className="text-muted"
-    >
-      <strong>
-        organization.key
-        :
-      </strong>
-       
-      foo
-    </div>
-  </div>
-  <NavBarTabs
-    className="navbar-context-tabs"
-  >
-    <li>
-      <Link
-        className=""
-        onlyActiveOnIndex={false}
-        style={Object {}}
-        to="/organizations/foo/projects"
-      >
-        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"
-        onlyActiveOnIndex={false}
-        style={Object {}}
-        to="/organizations/foo/quality_profiles"
-      >
-        quality_profiles.page
-      </Link>
-    </li>
-    <li>
-      <Link
-        activeClassName="active"
-        onlyActiveOnIndex={false}
-        style={Object {}}
-        to="/organizations/foo/rules"
-      >
-        coding_rules.page
-      </Link>
-    </li>
-    <li>
-      <Link
-        activeClassName="active"
-        onlyActiveOnIndex={false}
-        style={Object {}}
-        to={
-          Object {
-            "pathname": "/organizations/foo/quality_gates",
-          }
-        }
-      >
-        quality_gates.page
-      </Link>
-    </li>
-    <li>
-      <Link
-        activeClassName="active"
-        onlyActiveOnIndex={false}
-        style={Object {}}
-        to="/organizations/foo/members"
-      >
-        organization.members.page
-      </Link>
-    </li>
-    <li
-      className="dropdown"
-    >
-      <a
-        className="dropdown-toggle"
-        data-toggle="dropdown"
-        href="#"
-        id="organization-navigation-admin"
-      >
-        layout.settings
-        Â 
-        <i
-          className="icon-dropdown"
-        />
-      </a>
-      <ul
-        className="dropdown-menu"
-      >
-        <li>
-          <Link
-            activeClassName="active"
-            onlyActiveOnIndex={false}
-            style={Object {}}
-            to="/organizations/foo/groups"
-          >
-            user_groups.page
-          </Link>
-        </li>
-        <li>
-          <Link
-            activeClassName="active"
-            onlyActiveOnIndex={false}
-            style={Object {}}
-            to="/organizations/foo/permissions"
-          >
-            permissions.page
-          </Link>
-        </li>
-        <li>
-          <Link
-            activeClassName="active"
-            onlyActiveOnIndex={false}
-            style={Object {}}
-            to="/organizations/foo/permission_templates"
-          >
-            permission_templates
-          </Link>
-        </li>
-        <li>
-          <Link
-            activeClassName="active"
-            onlyActiveOnIndex={false}
-            style={Object {}}
-            to="/organizations/foo/projects_management"
-          >
-            projects_management
-          </Link>
-        </li>
-        <li>
-          <Link
-            activeClassName="active"
-            onlyActiveOnIndex={false}
-            style={Object {}}
-            to="/organizations/foo/edit"
-          >
-            edit
-          </Link>
-        </li>
-      </ul>
-    </li>
-  </NavBarTabs>
-</ContextNavBar>
-`;
diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigation-test.tsx.snap b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigation-test.tsx.snap
new file mode 100644 (file)
index 0000000..66184bb
--- /dev/null
@@ -0,0 +1,520 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`admin 1`] = `
+<ContextNavBar
+  height={72}
+  id="context-navigation"
+>
+  <div
+    className="navbar-context-header"
+  >
+    <h1
+      className="display-inline-block"
+    >
+      <OrganizationAvatar
+        organization={
+          Object {
+            "canAdmin": true,
+            "canDelete": true,
+            "key": "foo",
+            "name": "Foo",
+          }
+        }
+      />
+      <Link
+        className="link-base-color link-no-underline spacer-left"
+        onlyActiveOnIndex={false}
+        style={Object {}}
+        to="/organizations/foo"
+      >
+        Foo
+      </Link>
+    </h1>
+  </div>
+  <div
+    className="navbar-context-meta"
+  >
+    <div
+      className="text-muted"
+    >
+      <strong>
+        organization.key
+        :
+      </strong>
+       
+      foo
+    </div>
+  </div>
+  <NavBarTabs
+    className="navbar-context-tabs"
+  >
+    <li>
+      <Link
+        className=""
+        onlyActiveOnIndex={false}
+        style={Object {}}
+        to="/organizations/foo/projects"
+      >
+        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"
+        onlyActiveOnIndex={false}
+        style={Object {}}
+        to="/organizations/foo/quality_profiles"
+      >
+        quality_profiles.page
+      </Link>
+    </li>
+    <li>
+      <Link
+        activeClassName="active"
+        onlyActiveOnIndex={false}
+        style={Object {}}
+        to="/organizations/foo/rules"
+      >
+        coding_rules.page
+      </Link>
+    </li>
+    <li>
+      <Link
+        activeClassName="active"
+        onlyActiveOnIndex={false}
+        style={Object {}}
+        to={
+          Object {
+            "pathname": "/organizations/foo/quality_gates",
+          }
+        }
+      >
+        quality_gates.page
+      </Link>
+    </li>
+    <li>
+      <Link
+        activeClassName="active"
+        onlyActiveOnIndex={false}
+        style={Object {}}
+        to="/organizations/foo/members"
+      >
+        organization.members.page
+      </Link>
+    </li>
+    <li
+      className="dropdown"
+    >
+      <a
+        className="dropdown-toggle"
+        data-toggle="dropdown"
+        href="#"
+        id="organization-navigation-admin"
+      >
+        layout.settings
+        Â 
+        <i
+          className="icon-dropdown"
+        />
+      </a>
+      <ul
+        className="dropdown-menu"
+      >
+        <li>
+          <Link
+            activeClassName="active"
+            onlyActiveOnIndex={false}
+            style={Object {}}
+            to="/organizations/foo/groups"
+          >
+            user_groups.page
+          </Link>
+        </li>
+        <li>
+          <Link
+            activeClassName="active"
+            onlyActiveOnIndex={false}
+            style={Object {}}
+            to="/organizations/foo/permissions"
+          >
+            permissions.page
+          </Link>
+        </li>
+        <li>
+          <Link
+            activeClassName="active"
+            onlyActiveOnIndex={false}
+            style={Object {}}
+            to="/organizations/foo/permission_templates"
+          >
+            permission_templates
+          </Link>
+        </li>
+        <li>
+          <Link
+            activeClassName="active"
+            onlyActiveOnIndex={false}
+            style={Object {}}
+            to="/organizations/foo/projects_management"
+          >
+            projects_management
+          </Link>
+        </li>
+        <li>
+          <Link
+            activeClassName="active"
+            onlyActiveOnIndex={false}
+            style={Object {}}
+            to="/organizations/foo/edit"
+          >
+            edit
+          </Link>
+        </li>
+        <li>
+          <Link
+            activeClassName="active"
+            onlyActiveOnIndex={false}
+            style={Object {}}
+            to="/organizations/foo/delete"
+          >
+            delete
+          </Link>
+        </li>
+      </ul>
+    </li>
+  </NavBarTabs>
+</ContextNavBar>
+`;
+
+exports[`regular user 1`] = `
+<ContextNavBar
+  height={72}
+  id="context-navigation"
+>
+  <div
+    className="navbar-context-header"
+  >
+    <h1
+      className="display-inline-block"
+    >
+      <OrganizationAvatar
+        organization={
+          Object {
+            "canAdmin": false,
+            "canDelete": false,
+            "key": "foo",
+            "name": "Foo",
+          }
+        }
+      />
+      <Link
+        className="link-base-color link-no-underline spacer-left"
+        onlyActiveOnIndex={false}
+        style={Object {}}
+        to="/organizations/foo"
+      >
+        Foo
+      </Link>
+    </h1>
+  </div>
+  <div
+    className="navbar-context-meta"
+  >
+    <div
+      className="text-muted"
+    >
+      <strong>
+        organization.key
+        :
+      </strong>
+       
+      foo
+    </div>
+  </div>
+  <NavBarTabs
+    className="navbar-context-tabs"
+  >
+    <li>
+      <Link
+        className=""
+        onlyActiveOnIndex={false}
+        style={Object {}}
+        to="/organizations/foo/projects"
+      >
+        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"
+        onlyActiveOnIndex={false}
+        style={Object {}}
+        to="/organizations/foo/quality_profiles"
+      >
+        quality_profiles.page
+      </Link>
+    </li>
+    <li>
+      <Link
+        activeClassName="active"
+        onlyActiveOnIndex={false}
+        style={Object {}}
+        to="/organizations/foo/rules"
+      >
+        coding_rules.page
+      </Link>
+    </li>
+    <li>
+      <Link
+        activeClassName="active"
+        onlyActiveOnIndex={false}
+        style={Object {}}
+        to={
+          Object {
+            "pathname": "/organizations/foo/quality_gates",
+          }
+        }
+      >
+        quality_gates.page
+      </Link>
+    </li>
+    <li>
+      <Link
+        activeClassName="active"
+        onlyActiveOnIndex={false}
+        style={Object {}}
+        to="/organizations/foo/members"
+      >
+        organization.members.page
+      </Link>
+    </li>
+  </NavBarTabs>
+</ContextNavBar>
+`;
+
+exports[`undeletable org 1`] = `
+<ContextNavBar
+  height={72}
+  id="context-navigation"
+>
+  <div
+    className="navbar-context-header"
+  >
+    <h1
+      className="display-inline-block"
+    >
+      <OrganizationAvatar
+        organization={
+          Object {
+            "canAdmin": true,
+            "canDelete": false,
+            "key": "foo",
+            "name": "Foo",
+          }
+        }
+      />
+      <Link
+        className="link-base-color link-no-underline spacer-left"
+        onlyActiveOnIndex={false}
+        style={Object {}}
+        to="/organizations/foo"
+      >
+        Foo
+      </Link>
+    </h1>
+  </div>
+  <div
+    className="navbar-context-meta"
+  >
+    <div
+      className="text-muted"
+    >
+      <strong>
+        organization.key
+        :
+      </strong>
+       
+      foo
+    </div>
+  </div>
+  <NavBarTabs
+    className="navbar-context-tabs"
+  >
+    <li>
+      <Link
+        className=""
+        onlyActiveOnIndex={false}
+        style={Object {}}
+        to="/organizations/foo/projects"
+      >
+        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"
+        onlyActiveOnIndex={false}
+        style={Object {}}
+        to="/organizations/foo/quality_profiles"
+      >
+        quality_profiles.page
+      </Link>
+    </li>
+    <li>
+      <Link
+        activeClassName="active"
+        onlyActiveOnIndex={false}
+        style={Object {}}
+        to="/organizations/foo/rules"
+      >
+        coding_rules.page
+      </Link>
+    </li>
+    <li>
+      <Link
+        activeClassName="active"
+        onlyActiveOnIndex={false}
+        style={Object {}}
+        to={
+          Object {
+            "pathname": "/organizations/foo/quality_gates",
+          }
+        }
+      >
+        quality_gates.page
+      </Link>
+    </li>
+    <li>
+      <Link
+        activeClassName="active"
+        onlyActiveOnIndex={false}
+        style={Object {}}
+        to="/organizations/foo/members"
+      >
+        organization.members.page
+      </Link>
+    </li>
+    <li
+      className="dropdown"
+    >
+      <a
+        className="dropdown-toggle"
+        data-toggle="dropdown"
+        href="#"
+        id="organization-navigation-admin"
+      >
+        layout.settings
+        Â 
+        <i
+          className="icon-dropdown"
+        />
+      </a>
+      <ul
+        className="dropdown-menu"
+      >
+        <li>
+          <Link
+            activeClassName="active"
+            onlyActiveOnIndex={false}
+            style={Object {}}
+            to="/organizations/foo/groups"
+          >
+            user_groups.page
+          </Link>
+        </li>
+        <li>
+          <Link
+            activeClassName="active"
+            onlyActiveOnIndex={false}
+            style={Object {}}
+            to="/organizations/foo/permissions"
+          >
+            permissions.page
+          </Link>
+        </li>
+        <li>
+          <Link
+            activeClassName="active"
+            onlyActiveOnIndex={false}
+            style={Object {}}
+            to="/organizations/foo/permission_templates"
+          >
+            permission_templates
+          </Link>
+        </li>
+        <li>
+          <Link
+            activeClassName="active"
+            onlyActiveOnIndex={false}
+            style={Object {}}
+            to="/organizations/foo/projects_management"
+          >
+            projects_management
+          </Link>
+        </li>
+        <li>
+          <Link
+            activeClassName="active"
+            onlyActiveOnIndex={false}
+            style={Object {}}
+            to="/organizations/foo/edit"
+          >
+            edit
+          </Link>
+        </li>
+      </ul>
+    </li>
+  </NavBarTabs>
+</ContextNavBar>
+`;
diff --git a/server/sonar-web/src/main/js/apps/permissions/global/components/AllHoldersList.js b/server/sonar-web/src/main/js/apps/permissions/global/components/AllHoldersList.js
deleted file mode 100644 (file)
index f340d7e..0000000
+++ /dev/null
@@ -1,139 +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 SearchForm from '../../shared/components/SearchForm';
-import HoldersList from '../../shared/components/HoldersList';
-import {
-  loadHolders,
-  grantToUser,
-  revokeFromUser,
-  grantToGroup,
-  revokeFromGroup,
-  updateFilter,
-  updateQuery,
-  selectPermission
-} from '../store/actions';
-import { translate } from '../../../../helpers/l10n';
-import {
-  getPermissionsAppUsers,
-  getPermissionsAppGroups,
-  getPermissionsAppQuery,
-  getPermissionsAppFilter,
-  getPermissionsAppSelectedPermission
-} from '../../../../store/rootReducer';
-
-const PERMISSIONS_ORDER = ['admin', 'profileadmin', 'gateadmin', 'scan', 'provisioning'];
-
-const PERMISSIONS_FOR_CUSTOM_ORG = ['admin', 'profileadmin', 'scan', 'provisioning'];
-
-class AllHoldersList extends React.PureComponent {
-  componentDidMount() {
-    this.props.loadHolders();
-  }
-
-  handleToggleUser(user, permission) {
-    const hasPermission = user.permissions.includes(permission);
-
-    if (hasPermission) {
-      this.props.revokePermissionFromUser(user.login, permission);
-    } else {
-      this.props.grantPermissionToUser(user.login, permission);
-    }
-  }
-
-  handleToggleGroup(group, permission) {
-    const hasPermission = group.permissions.includes(permission);
-
-    if (hasPermission) {
-      this.props.revokePermissionFromGroup(group.name, permission);
-    } else {
-      this.props.grantPermissionToGroup(group.name, permission);
-    }
-  }
-
-  render() {
-    const order =
-      this.props.organization && !this.props.organization.isDefault
-        ? PERMISSIONS_FOR_CUSTOM_ORG
-        : PERMISSIONS_ORDER;
-
-    const l10nPrefix = this.props.organization ? 'organizations_permissions' : 'global_permissions';
-
-    const permissions = order.map(p => ({
-      key: p,
-      name: translate(l10nPrefix, p),
-      description: translate(l10nPrefix, p, 'desc')
-    }));
-
-    return (
-      <HoldersList
-        permissions={permissions}
-        selectedPermission={this.props.selectedPermission}
-        users={this.props.users}
-        groups={this.props.groups}
-        onSelectPermission={this.props.onSelectPermission}
-        onToggleUser={this.handleToggleUser.bind(this)}
-        onToggleGroup={this.handleToggleGroup.bind(this)}>
-        <SearchForm
-          query={this.props.query}
-          filter={this.props.filter}
-          onSearch={this.props.onSearch}
-          onFilter={this.props.onFilter}
-        />
-      </HoldersList>
-    );
-  }
-}
-
-const mapStateToProps = state => ({
-  users: getPermissionsAppUsers(state),
-  groups: getPermissionsAppGroups(state),
-  query: getPermissionsAppQuery(state),
-  filter: getPermissionsAppFilter(state),
-  selectedPermission: getPermissionsAppSelectedPermission(state)
-});
-
-/*::
-type OwnProps = {
-  organization?: { key: string }
-};
-*/
-
-const mapDispatchToProps = (dispatch /*: Function */, ownProps /*: OwnProps */) => {
-  const organizationKey = ownProps.organization ? ownProps.organization.key : undefined;
-  return {
-    loadHolders: () => dispatch(loadHolders(organizationKey)),
-    onSearch: query => dispatch(updateQuery(query, organizationKey)),
-    onFilter: filter => dispatch(updateFilter(filter, organizationKey)),
-    onSelectPermission: permission => dispatch(selectPermission(permission, organizationKey)),
-    grantPermissionToUser: (login, permission) =>
-      dispatch(grantToUser(login, permission, organizationKey)),
-    revokePermissionFromUser: (login, permission) =>
-      dispatch(revokeFromUser(login, permission, organizationKey)),
-    grantPermissionToGroup: (groupName, permission) =>
-      dispatch(grantToGroup(groupName, permission, organizationKey)),
-    revokePermissionFromGroup: (groupName, permission) =>
-      dispatch(revokeFromGroup(groupName, permission, organizationKey))
-  };
-};
-
-export default connect(mapStateToProps, mapDispatchToProps)(AllHoldersList);
diff --git a/server/sonar-web/src/main/js/apps/permissions/global/components/AllHoldersList.tsx b/server/sonar-web/src/main/js/apps/permissions/global/components/AllHoldersList.tsx
new file mode 100644 (file)
index 0000000..b9ee749
--- /dev/null
@@ -0,0 +1,104 @@
+/*
+ * 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 * as React from 'react';
+import SearchForm from '../../shared/components/SearchForm';
+import HoldersList from '../../shared/components/HoldersList';
+import { translate } from '../../../../helpers/l10n';
+import { Organization } from '../../../../app/types';
+import { PermissionUser, PermissionGroup } from '../../../../api/permissions';
+
+const PERMISSIONS_ORDER = ['admin', 'profileadmin', 'gateadmin', 'scan', 'provisioning'];
+const PERMISSIONS_FOR_CUSTOM_ORG = ['admin', 'profileadmin', 'scan', 'provisioning'];
+
+interface Props {
+  filter: string;
+  grantPermissionToGroup: (groupName: string, permission: string) => void;
+  grantPermissionToUser: (login: string, permission: string) => void;
+  groups: PermissionGroup[];
+  loadHolders: () => void;
+  onFilter: (filter: string) => void;
+  onSearch: (query: string) => void;
+  onSelectPermission: (permission: string) => void;
+  organization?: Organization;
+  query: string;
+  revokePermissionFromGroup: (groupName: string, permission: string) => void;
+  revokePermissionFromUser: (login: string, permission: string) => void;
+  selectedPermission?: string;
+  users: PermissionUser[];
+}
+
+export default class AllHoldersList extends React.PureComponent<Props> {
+  componentDidMount() {
+    this.props.loadHolders();
+  }
+
+  handleToggleUser = (user: PermissionUser, permission: string) => {
+    const hasPermission = user.permissions.includes(permission);
+
+    if (hasPermission) {
+      this.props.revokePermissionFromUser(user.login, permission);
+    } else {
+      this.props.grantPermissionToUser(user.login, permission);
+    }
+  };
+
+  handleToggleGroup = (group: PermissionGroup, permission: string) => {
+    const hasPermission = group.permissions.includes(permission);
+
+    if (hasPermission) {
+      this.props.revokePermissionFromGroup(group.name, permission);
+    } else {
+      this.props.grantPermissionToGroup(group.name, permission);
+    }
+  };
+
+  render() {
+    const order =
+      this.props.organization && !this.props.organization.isDefault
+        ? PERMISSIONS_FOR_CUSTOM_ORG
+        : PERMISSIONS_ORDER;
+
+    const l10nPrefix = this.props.organization ? 'organizations_permissions' : 'global_permissions';
+
+    const permissions = order.map(p => ({
+      key: p,
+      name: translate(l10nPrefix, p),
+      description: translate(l10nPrefix, p, 'desc')
+    }));
+
+    return (
+      <HoldersList
+        permissions={permissions}
+        selectedPermission={this.props.selectedPermission}
+        users={this.props.users}
+        groups={this.props.groups}
+        onSelectPermission={this.props.onSelectPermission}
+        onToggleUser={this.handleToggleUser}
+        onToggleGroup={this.handleToggleGroup}>
+        <SearchForm
+          query={this.props.query}
+          filter={this.props.filter}
+          onSearch={this.props.onSearch}
+          onFilter={this.props.onFilter}
+        />
+      </HoldersList>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/permissions/global/components/AllHoldersListContainer.tsx b/server/sonar-web/src/main/js/apps/permissions/global/components/AllHoldersListContainer.tsx
new file mode 100644 (file)
index 0000000..bb06475
--- /dev/null
@@ -0,0 +1,95 @@
+/*
+ * 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 {
+  loadHolders,
+  grantToUser,
+  revokeFromUser,
+  grantToGroup,
+  revokeFromGroup,
+  updateFilter,
+  updateQuery,
+  selectPermission
+} from '../store/actions';
+import {
+  getPermissionsAppUsers,
+  getPermissionsAppGroups,
+  getPermissionsAppQuery,
+  getPermissionsAppFilter,
+  getPermissionsAppSelectedPermission
+} from '../../../../store/rootReducer';
+import AllHoldersList from './AllHoldersList';
+import { Organization } from '../../../../app/types';
+import { PermissionUser, PermissionGroup } from '../../../../api/permissions';
+
+interface OwnProps {
+  organization?: Organization;
+}
+
+interface StateToProps {
+  filter: string;
+  groups: PermissionGroup[];
+  query: string;
+  selectedPermission?: string;
+  users: PermissionUser[];
+}
+
+interface DispatchToProps {
+  grantPermissionToGroup: (groupName: string, permission: string) => void;
+  grantPermissionToUser: (login: string, permission: string) => void;
+  loadHolders: () => void;
+  onFilter: (filter: string) => void;
+  onSearch: (query: string) => void;
+  onSelectPermission: (permission: string) => void;
+  revokePermissionFromGroup: (groupName: string, permission: string) => void;
+  revokePermissionFromUser: (login: string, permission: string) => void;
+}
+
+const mapStateToProps = (state: any) => ({
+  filter: getPermissionsAppFilter(state),
+  groups: getPermissionsAppGroups(state),
+  query: getPermissionsAppQuery(state),
+  selectedPermission: getPermissionsAppSelectedPermission(state),
+  users: getPermissionsAppUsers(state)
+});
+
+const mapDispatchToProps = (dispatch: Function, ownProps: OwnProps) => {
+  const organizationKey = ownProps.organization ? ownProps.organization.key : undefined;
+  return {
+    grantPermissionToGroup: (groupName: string, permission: string) =>
+      dispatch(grantToGroup(groupName, permission, organizationKey)),
+    grantPermissionToUser: (login: string, permission: string) =>
+      dispatch(grantToUser(login, permission, organizationKey)),
+    loadHolders: () => dispatch(loadHolders(organizationKey)),
+    onFilter: (filter: string) => dispatch(updateFilter(filter, organizationKey)),
+    onSearch: (query: string) => dispatch(updateQuery(query, organizationKey)),
+    onSelectPermission: (permission: string) =>
+      dispatch(selectPermission(permission, organizationKey)),
+    revokePermissionFromGroup: (groupName: string, permission: string) =>
+      dispatch(revokeFromGroup(groupName, permission, organizationKey)),
+    revokePermissionFromUser: (login: string, permission: string) =>
+      dispatch(revokeFromUser(login, permission, organizationKey))
+  };
+};
+
+export default connect<StateToProps, DispatchToProps, OwnProps>(
+  mapStateToProps,
+  mapDispatchToProps
+)(AllHoldersList);
diff --git a/server/sonar-web/src/main/js/apps/permissions/global/components/App.js b/server/sonar-web/src/main/js/apps/permissions/global/components/App.js
deleted file mode 100644 (file)
index 4b7ea9f..0000000
+++ /dev/null
@@ -1,38 +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 Helmet from 'react-helmet';
-import PageHeader from './PageHeader';
-import AllHoldersList from './AllHoldersList';
-import PageError from '../../shared/components/PageError';
-import { translate } from '../../../../helpers/l10n';
-import '../../styles.css';
-
-export default function App(props /*: { organization?: {} } */) {
-  return (
-    <div className="page page-limited">
-      <Helmet title={translate('global_permissions.permission')} />
-      <PageHeader organization={props.organization} />
-      <PageError />
-      <AllHoldersList organization={props.organization} />
-    </div>
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/permissions/global/components/App.tsx b/server/sonar-web/src/main/js/apps/permissions/global/components/App.tsx
new file mode 100644 (file)
index 0000000..fdd4aec
--- /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 * as React from 'react';
+import Helmet from 'react-helmet';
+import PageHeader from './PageHeader';
+import AllHoldersListContainer from './AllHoldersListContainer';
+import PageError from '../../shared/components/PageError';
+import { translate } from '../../../../helpers/l10n';
+import { Organization } from '../../../../app/types';
+import '../../styles.css';
+
+interface Props {
+  organization?: Organization;
+}
+
+export default function App({ organization }: Props) {
+  return (
+    <div className="page page-limited">
+      <Helmet title={translate('global_permissions.permission')} />
+      <PageHeader organization={organization} />
+      <PageError />
+      <AllHoldersListContainer organization={organization} />
+    </div>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/components/GroupHolder.js b/server/sonar-web/src/main/js/apps/permissions/shared/components/GroupHolder.js
deleted file mode 100644 (file)
index 12a9c2a..0000000
+++ /dev/null
@@ -1,77 +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.
- */
-import React from 'react';
-import PropTypes from 'prop-types';
-import GroupIcon from '../../../../components/icons-components/GroupIcon';
-
-export default class GroupHolder extends React.PureComponent {
-  static propTypes = {
-    group: PropTypes.object.isRequired,
-    permissions: PropTypes.array.isRequired,
-    selectedPermission: PropTypes.string,
-    permissionsOrder: PropTypes.array.isRequired,
-    onToggle: PropTypes.func.isRequired
-  };
-
-  handleClick(permission, e) {
-    e.preventDefault();
-    e.target.blur();
-    this.props.onToggle(this.props.group, permission);
-  }
-
-  render() {
-    const { selectedPermission } = this.props;
-    const permissionCells = this.props.permissionsOrder.map(p => (
-      <td
-        key={p.key}
-        className="text-center text-middle"
-        style={{ backgroundColor: p.key === selectedPermission ? '#d9edf7' : 'transparent' }}>
-        <button className="button-clean" onClick={this.handleClick.bind(this, p.key)}>
-          {this.props.permissions.includes(p.key) ? (
-            <i className="icon-checkbox icon-checkbox-checked" />
-          ) : (
-            <i className="icon-checkbox" />
-          )}
-        </button>
-      </td>
-    ));
-
-    const { group } = this.props;
-
-    return (
-      <tr>
-        <td className="nowrap">
-          <div className="display-inline-block text-middle big-spacer-right">
-            <GroupIcon />
-          </div>
-          <div className="display-inline-block text-middle">
-            <div>
-              <strong>{group.name}</strong>
-            </div>
-            <div className="little-spacer-top" style={{ whiteSpace: 'normal' }}>
-              {group.description}
-            </div>
-          </div>
-        </td>
-        {permissionCells}
-      </tr>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/components/GroupHolder.tsx b/server/sonar-web/src/main/js/apps/permissions/shared/components/GroupHolder.tsx
new file mode 100644 (file)
index 0000000..3fc076c
--- /dev/null
@@ -0,0 +1,73 @@
+/*
+ * 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 * as React from 'react';
+import Checkbox from '../../../../components/controls/Checkbox';
+import GroupIcon from '../../../../components/icons-components/GroupIcon';
+import { PermissionGroup } from '../../../../api/permissions';
+
+interface Props {
+  group: PermissionGroup;
+  permissions: string[];
+  selectedPermission?: string;
+  permissionsOrder: string[];
+  onToggle: (group: PermissionGroup, permission: string) => void;
+}
+
+export default class GroupHolder extends React.PureComponent<Props> {
+  handleCheck = (_checked: boolean, permission?: string) =>
+    permission && this.props.onToggle(this.props.group, permission);
+
+  render() {
+    const { selectedPermission } = this.props;
+    const permissionCells = this.props.permissionsOrder.map(permission => (
+      <td
+        key={permission}
+        className="text-center text-middle"
+        style={{ backgroundColor: permission === selectedPermission ? '#d9edf7' : 'transparent' }}>
+        <Checkbox
+          checked={this.props.permissions.includes(permission)}
+          id={permission}
+          onCheck={this.handleCheck}
+        />
+      </td>
+    ));
+
+    const { group } = this.props;
+
+    return (
+      <tr>
+        <td className="nowrap">
+          <div className="display-inline-block text-middle big-spacer-right">
+            <GroupIcon />
+          </div>
+          <div className="display-inline-block text-middle">
+            <div>
+              <strong>{group.name}</strong>
+            </div>
+            <div className="little-spacer-top" style={{ whiteSpace: 'normal' }}>
+              {group.description}
+            </div>
+          </div>
+        </td>
+        {permissionCells}
+      </tr>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/components/HoldersList.js b/server/sonar-web/src/main/js/apps/permissions/shared/components/HoldersList.js
deleted file mode 100644 (file)
index da8bc9a..0000000
+++ /dev/null
@@ -1,137 +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.
- */
-import React from 'react';
-import PropTypes from 'prop-types';
-import UserHolder from './UserHolder';
-import GroupHolder from './GroupHolder';
-import Tooltip from '../../../../components/controls/Tooltip';
-import { translate, translateWithParameters } from '../../../../helpers/l10n';
-
-export default class HoldersList extends React.PureComponent {
-  static propTypes = {
-    permissions: PropTypes.array.isRequired,
-    users: PropTypes.array.isRequired,
-    groups: PropTypes.array.isRequired,
-    selectedPermission: PropTypes.string,
-    showPublicProjectsWarning: PropTypes.bool,
-    onSelectPermission: PropTypes.func.isRequired,
-    onToggleUser: PropTypes.func.isRequired,
-    onToggleGroup: PropTypes.func.isRequired
-  };
-
-  static defaultProps = {
-    showPublicProjectsWarning: false
-  };
-
-  handlePermissionClick = event => {
-    event.preventDefault();
-    event.currentTarget.blur();
-    this.props.onSelectPermission(event.currentTarget.dataset.key);
-  };
-
-  renderTooltip = permission =>
-    this.props.showPublicProjectsWarning &&
-    (permission.key === 'user' || permission.key === 'codeviewer') ? (
-      <div>
-        {permission.description}
-        <div className="alert alert-warning spacer-top">
-          {translate('projects_role.public_projects_warning')}
-        </div>
-      </div>
-    ) : (
-      permission.description
-    );
-
-  renderTableHeader() {
-    const { selectedPermission } = this.props;
-    const cells = this.props.permissions.map(p => (
-      <th
-        key={p.key}
-        className="permission-column text-center"
-        style={{
-          backgroundColor: p.key === selectedPermission ? '#d9edf7' : 'transparent'
-        }}>
-        <div className="permission-column-inner">
-          <Tooltip
-            overlay={translateWithParameters('global_permissions.filter_by_x_permission', p.name)}>
-            <a data-key={p.key} href="#" onClick={this.handlePermissionClick}>
-              {p.name}
-            </a>
-          </Tooltip>
-          <Tooltip overlay={this.renderTooltip(p)}>
-            <i className="icon-help little-spacer-left" />
-          </Tooltip>
-        </div>
-      </th>
-    ));
-    return (
-      <thead>
-        <tr>
-          <td className="nowrap bordered-bottom">{this.props.children}</td>
-          {cells}
-        </tr>
-      </thead>
-    );
-  }
-
-  renderEmpty() {
-    const columns = this.props.permissions.length + 1;
-    return (
-      <tr>
-        <td colSpan={columns}>{translate('no_results_search')}</td>
-      </tr>
-    );
-  }
-
-  render() {
-    const users = this.props.users.map(user => (
-      <UserHolder
-        key={'user-' + user.login}
-        user={user}
-        permissions={user.permissions}
-        selectedPermission={this.props.selectedPermission}
-        permissionsOrder={this.props.permissions}
-        onToggle={this.props.onToggleUser}
-      />
-    ));
-
-    const groups = this.props.groups.map(group => (
-      <GroupHolder
-        key={'group-' + group.id}
-        group={group}
-        permissions={group.permissions}
-        selectedPermission={this.props.selectedPermission}
-        permissionsOrder={this.props.permissions}
-        onToggle={this.props.onToggleGroup}
-      />
-    ));
-
-    return (
-      <table className="data zebra permissions-table">
-        {this.renderTableHeader()}
-        <tbody>
-          {users.length === 0 && groups.length === 0 && this.renderEmpty()}
-          {users}
-          {groups}
-        </tbody>
-      </table>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/components/HoldersList.tsx b/server/sonar-web/src/main/js/apps/permissions/shared/components/HoldersList.tsx
new file mode 100644 (file)
index 0000000..189351b
--- /dev/null
@@ -0,0 +1,105 @@
+/*
+ * 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 * as React from 'react';
+import UserHolder from './UserHolder';
+import GroupHolder from './GroupHolder';
+import PermissionHeader, { Permission } from './PermissionHeader';
+import { PermissionUser, PermissionGroup } from '../../../../api/permissions';
+import { translate } from '../../../../helpers/l10n';
+
+interface Props {
+  permissions: Permission[];
+  users: PermissionUser[];
+  groups: PermissionGroup[];
+  selectedPermission?: string;
+  showPublicProjectsWarning?: boolean;
+  onSelectPermission: (permission: string) => void;
+  onToggleUser: (user: PermissionUser, permission: string) => void;
+  onToggleGroup: (group: PermissionGroup, permission: string) => void;
+}
+
+export default class HoldersList extends React.PureComponent<Props> {
+  renderTableHeader() {
+    const { onSelectPermission, selectedPermission, showPublicProjectsWarning } = this.props;
+    const cells = this.props.permissions.map(p => (
+      <PermissionHeader
+        key={p.key}
+        onSelectPermission={onSelectPermission}
+        permission={p}
+        selectedPermission={selectedPermission}
+        showPublicProjectsWarning={showPublicProjectsWarning}
+      />
+    ));
+    return (
+      <thead>
+        <tr>
+          <td className="nowrap bordered-bottom">{this.props.children}</td>
+          {cells}
+        </tr>
+      </thead>
+    );
+  }
+
+  renderEmpty() {
+    const columns = this.props.permissions.length + 1;
+    return (
+      <tr>
+        <td colSpan={columns}>{translate('no_results_search')}</td>
+      </tr>
+    );
+  }
+
+  render() {
+    const permissionsOrder = this.props.permissions.map(p => p.key);
+
+    const users = this.props.users.map(user => (
+      <UserHolder
+        key={'user-' + user.login}
+        user={user}
+        permissions={user.permissions}
+        selectedPermission={this.props.selectedPermission}
+        permissionsOrder={permissionsOrder}
+        onToggle={this.props.onToggleUser}
+      />
+    ));
+
+    const groups = this.props.groups.map(group => (
+      <GroupHolder
+        key={'group-' + group.id}
+        group={group}
+        permissions={group.permissions}
+        selectedPermission={this.props.selectedPermission}
+        permissionsOrder={permissionsOrder}
+        onToggle={this.props.onToggleGroup}
+      />
+    ));
+
+    return (
+      <table className="data zebra permissions-table">
+        {this.renderTableHeader()}
+        <tbody>
+          {users.length === 0 && groups.length === 0 && this.renderEmpty()}
+          {users}
+          {groups}
+        </tbody>
+      </table>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/components/PageError.js b/server/sonar-web/src/main/js/apps/permissions/shared/components/PageError.js
deleted file mode 100644 (file)
index e3d6266..0000000
+++ /dev/null
@@ -1,45 +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.
- */
-import React from 'react';
-import PropTypes from 'prop-types';
-import { connect } from 'react-redux';
-import { getPermissionsAppError } from '../../../../store/rootReducer';
-
-class PageError extends React.PureComponent {
-  static propTypes = {
-    message: PropTypes.string
-  };
-
-  render() {
-    const { message } = this.props;
-
-    if (!message) {
-      return null;
-    }
-
-    return <div className="alert alert-danger">{message}</div>;
-  }
-}
-
-const mapStateToProps = state => ({
-  message: getPermissionsAppError(state)
-});
-
-export default connect(mapStateToProps)(PageError);
diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/components/PageError.tsx b/server/sonar-web/src/main/js/apps/permissions/shared/components/PageError.tsx
new file mode 100644 (file)
index 0000000..4f38783
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+ * 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 * as React from 'react';
+import { connect } from 'react-redux';
+import { getPermissionsAppError } from '../../../../store/rootReducer';
+
+interface Props {
+  message: string;
+}
+
+function PageError({ message }: Props) {
+  if (!message) {
+    return null;
+  }
+
+  return <div className="alert alert-danger">{message}</div>;
+}
+
+const mapStateToProps = (state: any) => ({
+  message: getPermissionsAppError(state)
+});
+
+export default connect(mapStateToProps)(PageError);
diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/components/PermissionHeader.tsx b/server/sonar-web/src/main/js/apps/permissions/shared/components/PermissionHeader.tsx
new file mode 100644 (file)
index 0000000..96d9e40
--- /dev/null
@@ -0,0 +1,87 @@
+/*
+ * 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 * as React from 'react';
+import Tooltip from '../../../../components/controls/Tooltip';
+import { translate, translateWithParameters } from '../../../../helpers/l10n';
+
+export interface Permission {
+  key: string;
+  name: string;
+  description: string;
+}
+
+interface Props {
+  onSelectPermission: (permission: string) => void;
+  permission: Permission;
+  selectedPermission?: string;
+  showPublicProjectsWarning?: boolean;
+}
+
+export default class PermissionHeader extends React.PureComponent<Props> {
+  static defaultProps = {
+    showPublicProjectsWarning: false
+  };
+
+  handlePermissionClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
+    event.preventDefault();
+    event.currentTarget.blur();
+    this.props.onSelectPermission(this.props.permission.key);
+  };
+
+  renderTooltip = (permission: Permission) => {
+    if (this.props.showPublicProjectsWarning && ['user', 'codeviewer'].includes(permission.key)) {
+      return (
+        <div>
+          {permission.description}
+          <div className="alert alert-warning spacer-top">
+            {translate('projects_role.public_projects_warning')}
+          </div>
+        </div>
+      );
+    }
+    return permission.description;
+  };
+
+  render() {
+    const { permission, selectedPermission } = this.props;
+    return (
+      <th
+        className="permission-column text-center"
+        style={{
+          backgroundColor: permission.key === selectedPermission ? '#d9edf7' : 'transparent'
+        }}>
+        <div className="permission-column-inner">
+          <Tooltip
+            overlay={translateWithParameters(
+              'global_permissions.filter_by_x_permission',
+              permission.name
+            )}>
+            <a href="#" onClick={this.handlePermissionClick}>
+              {permission.name}
+            </a>
+          </Tooltip>
+          <Tooltip overlay={this.renderTooltip(permission)}>
+            <i className="icon-help little-spacer-left" />
+          </Tooltip>
+        </div>
+      </th>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/components/SearchForm.js b/server/sonar-web/src/main/js/apps/permissions/shared/components/SearchForm.js
deleted file mode 100644 (file)
index b680b49..0000000
+++ /dev/null
@@ -1,52 +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.
- */
-import React from 'react';
-import PropTypes from 'prop-types';
-import RadioToggle from '../../../../components/controls/RadioToggle';
-import SearchBox from '../../../../components/controls/SearchBox';
-import { translate, translateWithParameters } from '../../../../helpers/l10n';
-
-export default function SearchForm(props) {
-  const filterOptions = [
-    { value: 'all', label: translate('all') },
-    { value: 'users', label: translate('users.page') },
-    { value: 'groups', label: translate('user_groups.page') }
-  ];
-
-  return (
-    <div className="display-flex-row">
-      <RadioToggle
-        name="users-or-groups"
-        onCheck={props.onFilter}
-        options={filterOptions}
-        value={props.filter}
-      />
-
-      <div className="flex-1 spacer-left">
-        <SearchBox
-          minLength={3}
-          onChange={props.onSearch}
-          placeholder={translate('search.search_for_users_or_groups')}
-          value={props.query}
-        />
-      </div>
-    </div>
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/components/SearchForm.tsx b/server/sonar-web/src/main/js/apps/permissions/shared/components/SearchForm.tsx
new file mode 100644 (file)
index 0000000..903d3be
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * 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 * as React from 'react';
+import RadioToggle from '../../../../components/controls/RadioToggle';
+import SearchBox from '../../../../components/controls/SearchBox';
+import { translate } from '../../../../helpers/l10n';
+
+interface Props {
+  filter: string;
+  onFilter: (value: string) => void;
+  onSearch: (value: string) => void;
+  query: string;
+}
+
+export default function SearchForm(props: Props) {
+  const filterOptions = [
+    { value: 'all', label: translate('all') },
+    { value: 'users', label: translate('users.page') },
+    { value: 'groups', label: translate('user_groups.page') }
+  ];
+
+  return (
+    <div className="display-flex-row">
+      <RadioToggle
+        name="users-or-groups"
+        onCheck={props.onFilter}
+        options={filterOptions}
+        value={props.filter}
+      />
+
+      <div className="flex-1 spacer-left">
+        <SearchBox
+          minLength={3}
+          onChange={props.onSearch}
+          placeholder={translate('search.search_for_users_or_groups')}
+          value={props.query}
+        />
+      </div>
+    </div>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/components/UserHolder.js b/server/sonar-web/src/main/js/apps/permissions/shared/components/UserHolder.js
deleted file mode 100644 (file)
index d0b97ae..0000000
+++ /dev/null
@@ -1,89 +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.
- */
-import React from 'react';
-import PropTypes from 'prop-types';
-import Avatar from '../../../../components/ui/Avatar';
-import { translate } from '../../../../helpers/l10n';
-
-export default class UserHolder extends React.PureComponent {
-  static propTypes = {
-    user: PropTypes.object.isRequired,
-    permissions: PropTypes.array.isRequired,
-    selectedPermission: PropTypes.string,
-    permissionsOrder: PropTypes.array.isRequired,
-    onToggle: PropTypes.func.isRequired
-  };
-
-  handleClick(permission, e) {
-    e.preventDefault();
-    e.target.blur();
-    this.props.onToggle(this.props.user, permission);
-  }
-
-  render() {
-    const { selectedPermission } = this.props;
-    const permissionCells = this.props.permissionsOrder.map(p => (
-      <td
-        key={p.key}
-        className="text-center text-middle"
-        style={{ backgroundColor: p.key === selectedPermission ? '#d9edf7' : 'transparent' }}>
-        <button className="button-clean" onClick={this.handleClick.bind(this, p.key)}>
-          {this.props.permissions.includes(p.key) ? (
-            <i className="icon-checkbox icon-checkbox-checked" />
-          ) : (
-            <i className="icon-checkbox" />
-          )}
-        </button>
-      </td>
-    ));
-
-    const { user } = this.props;
-
-    const isCreator = user.login === '<creator>';
-
-    return (
-      <tr>
-        <td className="nowrap">
-          {!isCreator && (
-            <Avatar
-              hash={user.avatar}
-              name={user.name}
-              size={36}
-              className="text-middle big-spacer-right"
-            />
-          )}
-          <div className="display-inline-block text-middle">
-            <div>
-              <strong>{user.name}</strong>
-              {!isCreator && <span className="note spacer-left">{user.login}</span>}
-            </div>
-            {!isCreator && <div className="little-spacer-top">{user.email}</div>}
-            {isCreator && (
-              <div className="little-spacer-top" style={{ whiteSpace: 'normal' }}>
-                {translate('permission_templates.project_creators.explanation')}
-              </div>
-            )}
-          </div>
-        </td>
-        {permissionCells}
-      </tr>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/components/UserHolder.tsx b/server/sonar-web/src/main/js/apps/permissions/shared/components/UserHolder.tsx
new file mode 100644 (file)
index 0000000..068c98f
--- /dev/null
@@ -0,0 +1,93 @@
+/*
+ * 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 * as React from 'react';
+import Avatar from '../../../../components/ui/Avatar';
+import Checkbox from '../../../../components/controls/Checkbox';
+import { PermissionUser } from '../../../../api/permissions';
+import { translate } from '../../../../helpers/l10n';
+
+interface Props {
+  user: PermissionUser;
+  permissions: string[];
+  selectedPermission?: string;
+  permissionsOrder: string[];
+  onToggle: (user: PermissionUser, permission: string) => void;
+}
+
+export default class UserHolder extends React.PureComponent<Props> {
+  handleCheck = (_checked: boolean, permission?: string) =>
+    permission && this.props.onToggle(this.props.user, permission);
+
+  render() {
+    const { selectedPermission } = this.props;
+    const permissionCells = this.props.permissionsOrder.map(permission => (
+      <td
+        key={permission}
+        className="text-center text-middle"
+        style={{ backgroundColor: permission === selectedPermission ? '#d9edf7' : 'transparent' }}>
+        <Checkbox
+          checked={this.props.permissions.includes(permission)}
+          id={permission}
+          onCheck={this.handleCheck}
+        />
+      </td>
+    ));
+
+    const { user } = this.props;
+    if (user.login === '<creator>') {
+      return (
+        <tr>
+          <td className="nowrap">
+            <div className="display-inline-block text-middle">
+              <div>
+                <strong>{user.name}</strong>
+              </div>
+              <div className="little-spacer-top" style={{ whiteSpace: 'normal' }}>
+                {translate('permission_templates.project_creators.explanation')}
+              </div>
+            </div>
+          </td>
+          {permissionCells}
+        </tr>
+      );
+    }
+
+    return (
+      <tr>
+        <td className="nowrap">
+          <Avatar
+            hash={user.avatar}
+            name={user.name}
+            size={36}
+            className="text-middle big-spacer-right"
+          />
+          <div className="display-inline-block text-middle">
+            <div>
+              <strong>{user.name}</strong>
+              <span className="note spacer-left">{user.login}</span>
+            </div>
+            <div className="little-spacer-top">{user.email}</div>
+          </div>
+        </td>
+        {permissionCells}
+      </tr>
+    );
+  }
+}
index 00014d3b2536fe459500277b1af869b23a5bdc73..e17f7697ac18819aebfa8231f1a410fbda2dc504 100644 (file)
@@ -21,6 +21,7 @@
 import * as React from 'react';
 import { mount } from 'enzyme';
 import App, { Props } from '../App';
+import { Visibility } from '../../../app/types';
 
 jest.mock('react-dom');
 
@@ -42,7 +43,7 @@ jest.mock('../../../api/components', () => ({ getComponents: jest.fn() }));
 
 const getComponents = require('../../../api/components').getComponents as jest.Mock<any>;
 
-const organization = { key: 'org', name: 'org', projectVisibility: 'public' };
+const organization = { key: 'org', name: 'org', projectVisibility: Visibility.Public };
 
 const defaultSearchParameters = {
   organization: 'org',
index 61dce6dbfe4c4bd3fd57aa83efcf9e7397c98a56..4d3505afd22f8d94918b2a345e909bb34b37fbe6 100644 (file)
@@ -21,12 +21,13 @@ import * as React from 'react';
 import { shallow } from 'enzyme';
 import ChangeVisibilityForm, { Props } from '../ChangeVisibilityForm';
 import { click } from '../../../helpers/testUtils';
+import { Visibility } from '../../../app/types';
 
 const organization = {
   canUpdateProjectsVisibilityToPrivate: true,
   key: 'org',
   name: 'org',
-  projectVisibility: 'public'
+  projectVisibility: Visibility.Public
 };
 
 it('renders disabled', () => {
index c514345b78a3db06c609388d97aba8c7d5d758a9..2682a771619fd7eecfdd72498db3ac9180ee53c9 100644 (file)
@@ -28,10 +28,11 @@ import * as React from 'react';
 import { shallow } from 'enzyme';
 import CreateProjectForm from '../CreateProjectForm';
 import { change, submit, click } from '../../../helpers/testUtils';
+import { Visibility } from '../../../app/types';
 
 const createProject = require('../../../api/components').createProject as jest.Mock<any>;
 
-const organization = { key: 'org', name: 'org', projectVisibility: 'public' };
+const organization = { key: 'org', name: 'org', projectVisibility: Visibility.Public };
 
 it('creates project', async () => {
   const wrapper = shallow(
index b7cb62ab9a446839e0017e08aa436a14c6e722ea..7d5b637949b392a4ab6200448fec2cef5cb579a4 100644 (file)
@@ -23,7 +23,7 @@ import Header, { Props } from '../Header';
 import { Visibility } from '../../../app/types';
 import { click } from '../../../helpers/testUtils';
 
-const organization = { key: 'org', name: 'org', projectVisibility: 'public' };
+const organization = { key: 'org', name: 'org', projectVisibility: Visibility.Public };
 
 it('renders', () => {
   expect(shallowRender()).toMatchSnapshot();
index 824c6ab2a5e38863197022c2e6682ffa05f755be..71cf3dcbe80574e144eeb6d186ecf0044570f5a4 100644 (file)
@@ -21,8 +21,9 @@ import * as React from 'react';
 import { shallow } from 'enzyme';
 import Search, { Props } from '../Search';
 import { click } from '../../../helpers/testUtils';
+import { Visibility } from '../../../app/types';
 
-const organization = { key: 'org', name: 'org', projectVisibility: 'public' };
+const organization = { key: 'org', name: 'org', projectVisibility: Visibility.Public };
 
 it('renders', () => {
   expect(shallowRender()).toMatchSnapshot();