]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-9784 rewrite projects management page
authorStas Vilchik <stas.vilchik@sonarsource.com>
Tue, 5 Sep 2017 09:00:00 +0000 (11:00 +0200)
committerStas Vilchik <stas.vilchik@sonarsource.com>
Mon, 11 Sep 2017 09:28:29 +0000 (11:28 +0200)
71 files changed:
server/sonar-web/src/main/js/api/components.ts
server/sonar-web/src/main/js/api/permissions.ts
server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.js
server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.js.snap
server/sonar-web/src/main/js/app/types.ts [new file with mode: 0644]
server/sonar-web/src/main/js/app/utils/startReactApp.js
server/sonar-web/src/main/js/apps/organizations/components/OrganizationProjectsManagement.js
server/sonar-web/src/main/js/apps/project-admin/deletion/Form.js
server/sonar-web/src/main/js/apps/projects-admin/AppContainer.js [deleted file]
server/sonar-web/src/main/js/apps/projects-admin/ChangeVisibilityForm.js [deleted file]
server/sonar-web/src/main/js/apps/projects-admin/CreateProjectForm.js [deleted file]
server/sonar-web/src/main/js/apps/projects-admin/__tests__/projects-test.js [deleted file]
server/sonar-web/src/main/js/apps/projects-admin/constants.js [deleted file]
server/sonar-web/src/main/js/apps/projects-admin/delete-view.js [deleted file]
server/sonar-web/src/main/js/apps/projects-admin/form-view.js [deleted file]
server/sonar-web/src/main/js/apps/projects-admin/header.js [deleted file]
server/sonar-web/src/main/js/apps/projects-admin/main.js [deleted file]
server/sonar-web/src/main/js/apps/projects-admin/projects.js [deleted file]
server/sonar-web/src/main/js/apps/projects-admin/routes.ts [deleted file]
server/sonar-web/src/main/js/apps/projects-admin/search.js [deleted file]
server/sonar-web/src/main/js/apps/projects-admin/templates/BulkApplyTemplateTemplate.hbs [deleted file]
server/sonar-web/src/main/js/apps/projects-admin/templates/projects-delete.hbs [deleted file]
server/sonar-web/src/main/js/apps/projects-admin/views/BulkApplyTemplateView.js [deleted file]
server/sonar-web/src/main/js/apps/projectsManagement/App.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectsManagement/AppContainer.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectsManagement/BulkApplyTemplateModal.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectsManagement/ChangeVisibilityForm.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectsManagement/CreateProjectForm.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectsManagement/DeleteModal.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectsManagement/Header.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectsManagement/Projects.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectsManagement/Search.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/App-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/BulkApplyTemplateModal-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ChangeVisibilityForm-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/CreateProjectForm-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/DeleteModal-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Header-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRow-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Projects-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Search-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/BulkApplyTemplateModal-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ChangeVisibilityForm-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/CreateProjectForm-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/DeleteModal-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Header-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRow-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Projects-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Search-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectsManagement/routes.ts [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectsManagement/utils.ts [new file with mode: 0644]
server/sonar-web/src/main/js/components/common/PrivateBadge.js [deleted file]
server/sonar-web/src/main/js/components/common/PrivateBadge.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/common/VisibilitySelector.js [deleted file]
server/sonar-web/src/main/js/components/common/VisibilitySelector.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/common/__tests__/PrivateBadge-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/common/__tests__/VisibilitySelector-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/PrivateBadge-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/VisibilitySelector-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/components/controls/RadioToggle.js [deleted file]
server/sonar-web/src/main/js/components/controls/RadioToggle.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/controls/__tests__/RadioToggle-test.js [deleted file]
server/sonar-web/src/main/js/components/controls/__tests__/RadioToggle-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/RadioToggle-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/helpers/testUtils.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties
tests/src/test/java/org/sonarqube/pageobjects/ProjectsManagementPage.java
tests/src/test/java/org/sonarqube/tests/authorisation/ProvisioningPermissionTest.java
tests/src/test/resources/authorisation/ProvisioningPermissionTest/should-be-able-to-provision-project.html
tests/src/test/resources/authorisation/ProvisioningPermissionTest/should-not-be-able-to-provision-project.html

index 168f41d4a327783cd8e7650c57c9bd13681a3673..7e9ac772df9293d8b272afabf2fa7f3c9c46bcb5 100644 (file)
@@ -32,8 +32,8 @@ export function getGhosts(data: RequestData): Promise<any> {
   return getJSON('/api/projects/ghosts', data);
 }
 
-export function deleteComponents(data: { projects: string; organization?: string }): Promise<void> {
-  return post('/api/projects/bulk_delete', data);
+export function deleteComponents(projects: string[], organization: string): Promise<void> {
+  return post('/api/projects/bulk_delete', { projects: projects.join(), organization });
 }
 
 export function deleteProject(project: string): Promise<void> {
index de6b169104ff6c648bb455ed4eca5611e3626508..5c927bc369258562ce99448ec2605affa565416d 100644 (file)
@@ -85,10 +85,30 @@ export function revokePermissionFromGroup(
   return post('/api/permissions/remove_group', data);
 }
 
-/**
- * Get list of permission templates
- */
-export function getPermissionTemplates(organization?: string) {
+export interface PermissionTemplate {
+  id: string;
+  name: string;
+  description?: string;
+  projectKeyPattern?: string;
+  createdAt: string;
+  updatedAt?: string;
+  permissions: Array<{
+    key: string;
+    usersCount: number;
+    groupsCount: number;
+    withProjectCreator?: boolean;
+  }>;
+}
+
+interface GetPermissionTemplatesResponse {
+  permissionTemplates: PermissionTemplate[];
+  defaultTemplates: Array<{ templateId: string; qualifier: string }>;
+  permissions: Array<{ key: string; name: string; description: string }>;
+}
+
+export function getPermissionTemplates(
+  organization?: string
+): Promise<GetPermissionTemplatesResponse> {
   const url = '/api/permissions/search_templates';
   return organization ? getJSON(url, { organization }) : getJSON(url);
 }
index 21540b25a997864db0caf2323ea377e735efba97..903de6c74eab839de9b71143c866f5ceeb530967 100644 (file)
@@ -42,7 +42,7 @@ class SettingsNav extends React.PureComponent {
   }
 
   isProjectsActive() {
-    const urls = ['/projects_admin', '/background_tasks'];
+    const urls = ['/admin/projects_management', '/background_tasks'];
     return this.isSomethingActive(urls);
   }
 
@@ -158,7 +158,7 @@ class SettingsNav extends React.PureComponent {
             <ul className="dropdown-menu">
               {!this.props.customOrganizations &&
                 <li>
-                  <IndexLink to="/projects_admin" activeClassName="active">
+                  <IndexLink to="/admin/projects_management" activeClassName="active">
                     Management
                   </IndexLink>
                 </li>}
index 7f285d4906cef782cbd6d1c3804177b4d65130ff..c66d4b2885987e1d8f1231e5121d23ff67f9cf23 100644 (file)
@@ -154,7 +154,7 @@ exports[`should work with extensions 1`] = `
         <li>
           <IndexLink
             activeClassName="active"
-            to="/projects_admin"
+            to="/admin/projects_management"
           >
             Management
           </IndexLink>
diff --git a/server/sonar-web/src/main/js/app/types.ts b/server/sonar-web/src/main/js/app/types.ts
new file mode 100644 (file)
index 0000000..d5ff471
--- /dev/null
@@ -0,0 +1,120 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+export enum BranchType {
+  LONG = 'LONG',
+  SHORT = 'SHORT'
+}
+
+export interface MainBranch {
+  isMain: true;
+  name: string;
+  status?: {
+    qualityGateStatus: string;
+  };
+}
+
+export interface LongLivingBranch {
+  isMain: false;
+  name: string;
+  status?: {
+    qualityGateStatus: string;
+  };
+  type: BranchType.LONG;
+}
+
+export interface ShortLivingBranch {
+  isMain: false;
+  isOrphan?: true;
+  mergeBranch: string;
+  name: string;
+  status?: {
+    bugs: number;
+    codeSmells: number;
+    vulnerabilities: number;
+  };
+  type: BranchType.SHORT;
+}
+
+export type Branch = MainBranch | LongLivingBranch | ShortLivingBranch;
+
+export interface ComponentExtension {
+  key: string;
+  name: string;
+}
+
+export interface Component {
+  analysisDate?: string;
+  breadcrumbs: Array<{
+    key: string;
+    name: string;
+    qualifier: string;
+  }>;
+  configuration?: ComponentConfiguration;
+  extensions?: ComponentExtension[];
+  isFavorite?: boolean;
+  key: string;
+  name: string;
+  organization: string;
+  path?: string;
+  qualifier: string;
+  refKey?: string;
+  version?: string;
+}
+
+export interface ComponentConfiguration {
+  extensions?: ComponentExtension[];
+  showBackgroundTasks?: boolean;
+  showLinks?: boolean;
+  showManualMeasures?: boolean;
+  showQualityGates?: boolean;
+  showQualityProfiles?: boolean;
+  showPermissions?: boolean;
+  showSettings?: boolean;
+  showUpdateKey?: boolean;
+}
+
+export interface Metric {
+  custom?: boolean;
+  decimalScale?: number;
+  description?: string;
+  direction?: number;
+  domain?: string;
+  hidden?: boolean;
+  key: string;
+  name: string;
+  qualitative?: boolean;
+  type: string;
+}
+
+export interface Organization {
+  adminPages?: Array<{ key: string; name: string }>;
+  avatar?: string;
+  canAdmin?: boolean;
+  canDelete?: boolean;
+  canProvisionProjects?: boolean;
+  canUpdateProjectsVisibilityToPrivate?: boolean;
+  description?: string;
+  isDefault?: boolean;
+  key: string;
+  name: string;
+  pages?: Array<{ key: string; name: string }>;
+  projectVisibility: string;
+  url?: string;
+}
index cda4a172b5e3c8514c54e72afa6ac6b9d50815ca..910761f150c07af808e5b61e260d3031cfca22b9 100644 (file)
@@ -56,7 +56,7 @@ import permissionTemplatesRoutes from '../../apps/permission-templates/routes';
 import projectActivityRoutes from '../../apps/projectActivity/routes';
 import projectAdminRoutes from '../../apps/project-admin/routes';
 import projectsRoutes from '../../apps/projects/routes';
-import projectsAdminRoutes from '../../apps/projects-admin/routes';
+import projectsManagementRoutes from '../../apps/projectsManagement/routes';
 import qualityGatesRoutes from '../../apps/quality-gates/routes';
 import qualityProfilesRoutes from '../../apps/quality-profiles/routes';
 import sessionsRoutes from '../../apps/sessions/routes';
@@ -115,6 +115,7 @@ const startReactApp = () => {
           }}
         />
 
+        <Redirect from="/projects_admin" to="/admin/projects_management" />
         <Redirect from="/component/index" to="/component" />
         <Redirect from="/component_issues" to="/project/issues" />
         <Redirect from="/dashboard/index" to="/dashboard" />
@@ -203,7 +204,10 @@ const startReactApp = () => {
                     <Route path="groups" childRoutes={groupsRoutes} />
                     <Route path="metrics" childRoutes={metricsRoutes} />
                     <Route path="permission_templates" childRoutes={permissionTemplatesRoutes} />
-                    <Route path="projects_admin" childRoutes={projectsAdminRoutes} />
+                    <Route
+                      path="admin/projects_management"
+                      childRoutes={projectsManagementRoutes}
+                    />
                     <Route path="roles/global" childRoutes={globalPermissionsRoutes} />
                     <Route path="settings" childRoutes={settingsRoutes} />
                     <Route path="system" childRoutes={systemRoutes} />
index 8222ab2df1c2626e3decf4e4b189ac5ab6a97c85..a8ae5dd4b09b74820ffb807e051e250412747a96 100644 (file)
@@ -20,7 +20,7 @@
 // @flow
 import React from 'react';
 import { connect } from 'react-redux';
-import AppContainer from '../../projects-admin/AppContainer';
+import AppContainer from '../../projectsManagement/AppContainer';
 import { getOrganizationByKey } from '../../../store/rootReducer';
 /*:: import type { Organization } from '../../../store/organizations/duck'; */
 
index 3dc4dc144c65c7f39709c6e23815fe368fa53632..8c8352a27d3ca76b41ec50b38c5abb56dae2c660 100644 (file)
@@ -87,7 +87,7 @@ export default class Form extends React.PureComponent {
             <form onSubmit={this.handleSubmit}>
               <div className="modal-head">
                 <h2>
-                  {translate('qualifiers.delete.TRK')}
+                  {translate('qualifier.delete.TRK')}
                 </h2>
               </div>
               <div className="modal-body">
diff --git a/server/sonar-web/src/main/js/apps/projects-admin/AppContainer.js b/server/sonar-web/src/main/js/apps/projects-admin/AppContainer.js
deleted file mode 100644 (file)
index 1531bb3..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 { connect } from 'react-redux';
-import Main from './main';
-import { onFail } from '../../store/rootActions';
-import { getAppState, getOrganizationByKey } from '../../store/rootReducer';
-import { receiveOrganizations } from '../../store/organizations/duck';
-import { changeProjectVisibility } from '../../api/organizations';
-import { fetchOrganization } from '../../apps/organizations/actions';
-
-class AppContainer extends React.PureComponent {
-  componentDidMount() {
-    // if there is no organization, that means we are in the global scope
-    // let's fetch defails for the default organization in this case
-    if (!this.props.organization || !this.props.organization.projectVisibility) {
-      this.props.fetchOrganization(this.props.appState.defaultOrganization);
-    }
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  handleVisibilityChange = visibility => {
-    this.props.onVisibilityChange(this.props.organization, visibility);
-  };
-
-  render() {
-    const { organization } = this.props;
-
-    if (!organization) {
-      return null;
-    }
-
-    const topLevelQualifiers = organization.isDefault ? this.props.appState.qualifiers : ['TRK'];
-
-    return (
-      <Main
-        hasProvisionPermission={organization.canProvisionProjects}
-        topLevelQualifiers={topLevelQualifiers}
-        onVisibilityChange={this.handleVisibilityChange}
-        onRequestFail={this.props.onRequestFail}
-        organization={organization}
-      />
-    );
-  }
-}
-
-const mapStateToProps = (state, ownProps) => ({
-  appState: getAppState(state),
-  organization:
-    ownProps.organization || getOrganizationByKey(state, getAppState(state).defaultOrganization)
-});
-
-const onVisibilityChange = (organization, visibility) => dispatch => {
-  const currentVisibility = organization.projectVisibility;
-  dispatch(receiveOrganizations([{ ...organization, projectVisibility: visibility }]));
-  changeProjectVisibility(organization.key, visibility).catch(error => {
-    onFail(dispatch)(error);
-    dispatch(receiveOrganizations([{ ...organization, projectVisibility: currentVisibility }]));
-  });
-};
-
-const mapDispatchToProps = dispatch => ({
-  fetchOrganization: key => dispatch(fetchOrganization(key)),
-  onVisibilityChange: (organization, visibility) =>
-    dispatch(onVisibilityChange(organization, visibility)),
-  onRequestFail: error => onFail(dispatch)(error)
-});
-
-export default connect(mapStateToProps, mapDispatchToProps)(AppContainer);
diff --git a/server/sonar-web/src/main/js/apps/projects-admin/ChangeVisibilityForm.js b/server/sonar-web/src/main/js/apps/projects-admin/ChangeVisibilityForm.js
deleted file mode 100644 (file)
index b04c4c3..0000000
+++ /dev/null
@@ -1,135 +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 Modal from 'react-modal';
-import classNames from 'classnames';
-import UpgradeOrganizationBox from '../../components/common/UpgradeOrganizationBox';
-import { translate } from '../../helpers/l10n';
-/*:: import type { Organization } from '../../store/organizations/duck'; */
-
-/*::
-type Props = {
-  onClose: () => void,
-  onConfirm: string => void,
-  organization: Organization
-};
-*/
-
-/*::
-type State = {
-  visibility: string
-};
-*/
-
-export default class ChangeVisibilityForm extends React.PureComponent {
-  /*:: props: Props; */
-  /*:: state: State; */
-
-  constructor(props /*: Props */) {
-    super(props);
-    this.state = { visibility: props.organization.projectVisibility };
-  }
-
-  handleCancelClick = (event /*: Event */) => {
-    event.preventDefault();
-    this.props.onClose();
-  };
-
-  handleConfirmClick = (event /*: Event */) => {
-    event.preventDefault();
-    this.props.onConfirm(this.state.visibility);
-    this.props.onClose();
-  };
-
-  handleVisibilityClick = (visibility /*: string */) => (
-    event /*: Event & { currentTarget: HTMLElement } */
-  ) => {
-    event.preventDefault();
-    event.currentTarget.blur();
-    this.setState({ visibility });
-  };
-
-  render() {
-    const { canUpdateProjectsVisibilityToPrivate } = this.props.organization;
-
-    return (
-      <Modal
-        isOpen={true}
-        contentLabel="modal form"
-        className="modal"
-        overlayClassName="modal-overlay"
-        onRequestClose={this.props.onClose}>
-        <header className="modal-head">
-          <h2>
-            {translate('organization.change_visibility_form.header')}
-          </h2>
-        </header>
-
-        <div className="modal-body">
-          {['public', 'private'].map(visibility =>
-            <div className="big-spacer-bottom" key={visibility}>
-              <p>
-                {visibility === 'private' && !canUpdateProjectsVisibilityToPrivate
-                  ? <span className="text-muted cursor-not-allowed">
-                      <i
-                        className={classNames('icon-radio', 'spacer-right', {
-                          'is-checked': this.state.visibility === visibility
-                        })}
-                      />
-                      {translate('visibility', visibility)}
-                    </span>
-                  : <a
-                      className="link-base-color link-no-underline"
-                      href="#"
-                      onClick={this.handleVisibilityClick(visibility)}>
-                      <i
-                        className={classNames('icon-radio', 'spacer-right', {
-                          'is-checked': this.state.visibility === visibility
-                        })}
-                      />
-                      {translate('visibility', visibility)}
-                    </a>}
-              </p>
-              <p className="text-muted spacer-top" style={{ paddingLeft: 22 }}>
-                {translate('visibility', visibility, 'description.short')}
-              </p>
-            </div>
-          )}
-
-          {canUpdateProjectsVisibilityToPrivate
-            ? <div className="alert alert-warning">
-                {translate('organization.change_visibility_form.warning')}
-              </div>
-            : <UpgradeOrganizationBox organization={this.props.organization.key} />}
-        </div>
-
-        <footer className="modal-foot">
-          <button onClick={this.handleConfirmClick}>
-            {translate('organization.change_visibility_form.submit')}
-          </button>
-          <a href="#" onClick={this.handleCancelClick}>
-            {translate('cancel')}
-          </a>
-        </footer>
-      </Modal>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/projects-admin/CreateProjectForm.js b/server/sonar-web/src/main/js/apps/projects-admin/CreateProjectForm.js
deleted file mode 100644 (file)
index fa8e2ea..0000000
+++ /dev/null
@@ -1,237 +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 Modal from 'react-modal';
-import { Link } from 'react-router';
-import UpgradeOrganizationBox from '../../components/common/UpgradeOrganizationBox';
-import VisibilitySelector from '../../components/common/VisibilitySelector';
-import { createProject } from '../../api/components';
-import { translate } from '../../helpers/l10n';
-import { getProjectUrl } from '../../helpers/urls';
-/*:: import type { Organization } from '../../store/organizations/duck'; */
-
-/*::
-type Props = {|
-  onClose: () => void,
-  onProjectCreated: () => void,
-  onRequestFail: Object => void,
-  organization?: Organization
-|};
-*/
-
-/*::
-type State = {
-  branch: string,
-  createdProject?: Object,
-  key: string,
-  loading: boolean,
-  name: string,
-  visibility: string
-};
-*/
-
-export default class CreateProjectForm extends React.PureComponent {
-  /*:: mounted: boolean; */
-  /*:: props: Props; */
-  /*:: state: State; */
-
-  constructor(props /*: Props */) {
-    super(props);
-    this.state = {
-      branch: '',
-      key: '',
-      loading: false,
-      name: '',
-      visibility: props.organization ? props.organization.projectVisibility : 'public'
-    };
-  }
-
-  componentDidMount() {
-    this.mounted = true;
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  handleCancelClick = (event /*: Event */) => {
-    event.preventDefault();
-    this.props.onClose();
-  };
-
-  handleInputChange = (event /*: { currentTarget: HTMLInputElement } */) => {
-    const { name, value } = event.currentTarget;
-    this.setState({ [name]: value });
-  };
-
-  handleVisibilityChange = (visibility /*: string */) => {
-    this.setState({ visibility });
-  };
-
-  handleFormSubmit = (event /*: Event */) => {
-    event.preventDefault();
-
-    const data /*: { [string]: string } */ = {
-      name: this.state.name,
-      branch: this.state.branch,
-      project: this.state.key,
-      visibility: this.state.visibility
-    };
-    if (this.props.organization) {
-      data.organization = this.props.organization.key;
-    }
-
-    this.setState({ loading: true });
-    createProject(data).then(
-      response => {
-        if (this.mounted) {
-          this.setState({ createdProject: response.project, loading: false });
-          this.props.onProjectCreated();
-        }
-      },
-      () => {
-        if (this.mounted) {
-          this.setState({ loading: false });
-        }
-      }
-    );
-  };
-
-  render() {
-    const { organization } = this.props;
-    const { createdProject } = this.state;
-
-    return (
-      <Modal
-        isOpen={true}
-        contentLabel="modal form"
-        className="modal"
-        overlayClassName="modal-overlay"
-        onRequestClose={this.props.onClose}>
-        {createdProject
-          ? <div>
-              <header className="modal-head">
-                <h2>
-                  {translate('qualifiers.create.TRK')}
-                </h2>
-              </header>
-
-              <div className="modal-body">
-                <div className="alert alert-success">
-                  Project <Link to={getProjectUrl(createdProject.key)}>
-                    {createdProject.name}
-                  </Link>{' '}
-                  has been successfully created.
-                </div>
-              </div>
-
-              <footer className="modal-foot">
-                <a href="#" id="create-project-close" onClick={this.handleCancelClick}>
-                  {translate('close')}
-                </a>
-              </footer>
-            </div>
-          : <form id="create-project-form" onSubmit={this.handleFormSubmit}>
-              <header className="modal-head">
-                <h2>
-                  {translate('qualifiers.create.TRK')}
-                </h2>
-              </header>
-
-              <div className="modal-body">
-                <div className="modal-field">
-                  <label htmlFor="create-project-name">
-                    {translate('name')}
-                    <em className="mandatory">*</em>
-                  </label>
-                  <input
-                    autoFocus={true}
-                    id="create-project-name"
-                    maxLength="2000"
-                    name="name"
-                    onChange={this.handleInputChange}
-                    required={true}
-                    type="text"
-                    value={this.state.name}
-                  />
-                </div>
-                <div className="modal-field">
-                  <label htmlFor="create-project-branch">
-                    {translate('branch')}
-                  </label>
-                  <input
-                    id="create-project-branch"
-                    maxLength="200"
-                    name="branch"
-                    onChange={this.handleInputChange}
-                    type="text"
-                    value={this.state.branch}
-                  />
-                </div>
-                <div className="modal-field">
-                  <label htmlFor="create-project-key">
-                    {translate('key')}
-                    <em className="mandatory">*</em>
-                  </label>
-                  <input
-                    id="create-project-key"
-                    maxLength="400"
-                    name="key"
-                    onChange={this.handleInputChange}
-                    required={true}
-                    type="text"
-                    value={this.state.key}
-                  />
-                </div>
-                <div className="modal-field">
-                  <label>
-                    {' '}{translate('visibility')}{' '}
-                  </label>
-                  <VisibilitySelector
-                    canTurnToPrivate={
-                      organization == null || organization.canUpdateProjectsVisibilityToPrivate
-                    }
-                    className="little-spacer-top"
-                    onChange={this.handleVisibilityChange}
-                    visibility={this.state.visibility}
-                  />
-                  {organization != null &&
-                    !organization.canUpdateProjectsVisibilityToPrivate &&
-                    <div className="spacer-top">
-                      <UpgradeOrganizationBox organization={organization.key} />
-                    </div>}
-                </div>
-              </div>
-
-              <footer className="modal-foot">
-                {this.state.loading && <i className="spinner spacer-right" />}
-                <button disabled={this.state.loading} id="create-project-submit" type="submit">
-                  {translate('create')}
-                </button>
-                <a href="#" id="create-project-cancel" onClick={this.handleCancelClick}>
-                  {translate('cancel')}
-                </a>
-              </footer>
-            </form>}
-      </Modal>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/projects-admin/__tests__/projects-test.js b/server/sonar-web/src/main/js/apps/projects-admin/__tests__/projects-test.js
deleted file mode 100644 (file)
index e17e866..0000000
+++ /dev/null
@@ -1,60 +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 Projects from '../projects';
-import Checkbox from '../../../components/controls/Checkbox';
-
-it('should render list of projects with no selection', () => {
-  const projects = [
-    { id: '1', key: 'a', name: 'A', qualifier: 'TRK' },
-    { id: '2', key: 'b', name: 'B', qualifier: 'TRK' }
-  ];
-
-  const result = shallow(
-    <Projects
-      organization={{ key: 'foo' }}
-      projects={projects}
-      selection={[]}
-      refresh={jest.fn()}
-    />
-  );
-  expect(result.find('tr').length).toBe(2);
-  expect(result.find(Checkbox).filterWhere(n => n.prop('checked')).length).toBe(0);
-});
-
-it('should render list of projects with one selected', () => {
-  const projects = [
-    { id: '1', key: 'a', name: 'A', qualifier: 'TRK' },
-    { id: '2', key: 'b', name: 'B', qualifier: 'TRK' }
-  ];
-  const selection = ['a'];
-
-  const result = shallow(
-    <Projects
-      organization={{ key: 'foo' }}
-      projects={projects}
-      selection={selection}
-      refresh={jest.fn()}
-    />
-  );
-  expect(result.find('tr').length).toBe(2);
-  expect(result.find(Checkbox).filterWhere(n => n.prop('checked')).length).toBe(1);
-});
diff --git a/server/sonar-web/src/main/js/apps/projects-admin/constants.js b/server/sonar-web/src/main/js/apps/projects-admin/constants.js
deleted file mode 100644 (file)
index 057d08d..0000000
+++ /dev/null
@@ -1,28 +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.
- */
-export const PAGE_SIZE = 50;
-
-export const QUALIFIERS_ORDER = ['TRK', 'VW', 'APP', 'DEV'];
-
-export const TYPE = {
-  ALL: 'ALL',
-  PROVISIONED: 'PROVISIONED',
-  GHOSTS: 'GHOSTS'
-};
diff --git a/server/sonar-web/src/main/js/apps/projects-admin/delete-view.js b/server/sonar-web/src/main/js/apps/projects-admin/delete-view.js
deleted file mode 100644 (file)
index 99bc391..0000000
+++ /dev/null
@@ -1,31 +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 ModalForm from '../../components/common/modal-form';
-import Template from './templates/projects-delete.hbs';
-
-export default ModalForm.extend({
-  template: Template,
-
-  onFormSubmit() {
-    ModalForm.prototype.onFormSubmit.apply(this, arguments);
-    this.options.deleteProjects();
-    this.destroy();
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/projects-admin/form-view.js b/server/sonar-web/src/main/js/apps/projects-admin/form-view.js
deleted file mode 100644 (file)
index cb2ed3e..0000000
+++ /dev/null
@@ -1,37 +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 ModalForm from '../../components/common/modal-form';
-
-export default ModalForm.extend({
-  onRender() {
-    ModalForm.prototype.onRender.apply(this, arguments);
-    this.$('[data-toggle="tooltip"]').tooltip({ container: 'body', placement: 'bottom' });
-  },
-
-  onDestroy() {
-    ModalForm.prototype.onDestroy.apply(this, arguments);
-    this.$('[data-toggle="tooltip"]').tooltip('destroy');
-  },
-
-  onFormSubmit() {
-    ModalForm.prototype.onFormSubmit.apply(this, arguments);
-    this.sendRequest();
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/projects-admin/header.js b/server/sonar-web/src/main/js/apps/projects-admin/header.js
deleted file mode 100644 (file)
index 9fdf7d4..0000000
+++ /dev/null
@@ -1,95 +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 ChangeVisibilityForm from './ChangeVisibilityForm';
-import { translate } from '../../helpers/l10n';
-/*:: import type { Organization } from '../../store/organizations/duck'; */
-
-/*::
-type Props = {|
-  hasProvisionPermission: boolean,
-  onProjectCreate: () => void,
-  onVisibilityChange: string => void,
-  organization: Organization
-|};
-*/
-
-/*::
-type State = {
-  visibilityForm: boolean
-};
-*/
-
-export default class Header extends React.PureComponent {
-  /*:: props: Props; */
-  state /*: State */ = { visibilityForm: false };
-
-  handleCreateProjectClick = (event /*: Event */) => {
-    event.preventDefault();
-    this.props.onProjectCreate();
-  };
-
-  handleChangeVisibilityClick = (event /*: Event */) => {
-    event.preventDefault();
-    this.setState({ visibilityForm: true });
-  };
-
-  closeVisiblityForm = () => {
-    this.setState({ visibilityForm: false });
-  };
-
-  render() {
-    const { organization } = this.props;
-
-    return (
-      <header className="page-header">
-        <h1 className="page-title">
-          {translate('projects_management')}
-        </h1>
-        <div className="page-actions">
-          <span className="big-spacer-right">
-            {translate('organization.default_visibility_of_new_projects')}{' '}
-            <strong>{translate('visibility', organization.projectVisibility)}</strong>
-            <a
-              className="spacer-left icon-edit"
-              href="#"
-              onClick={this.handleChangeVisibilityClick}
-            />
-          </span>
-          {this.props.hasProvisionPermission &&
-            <button id="create-project" onClick={this.handleCreateProjectClick}>
-              {translate('qualifiers.create.TRK')}
-            </button>}
-        </div>
-        <p className="page-description">
-          {translate('projects_management.page.description')}
-        </p>
-
-        {this.state.visibilityForm &&
-          <ChangeVisibilityForm
-            onClose={this.closeVisiblityForm}
-            onConfirm={this.props.onVisibilityChange}
-            organization={organization}
-          />}
-      </header>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/projects-admin/main.js b/server/sonar-web/src/main/js/apps/projects-admin/main.js
deleted file mode 100644 (file)
index eea76fc..0000000
+++ /dev/null
@@ -1,287 +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 { debounce, uniq, without } from 'lodash';
-import Header from './header';
-import Search from './search';
-import Projects from './projects';
-import CreateProjectForm from './CreateProjectForm';
-import ListFooter from '../../components/controls/ListFooter';
-import { PAGE_SIZE, TYPE } from './constants';
-import { getComponents, getProvisioned, getGhosts, deleteComponents } from '../../api/components';
-import { translate } from '../../helpers/l10n';
-/*:: import type { Organization } from '../../store/organizations/duck'; */
-
-/*::
-type Props = {|
-  hasProvisionPermission: boolean,
-  onVisibilityChange: string => void,
-  onRequestFail: Object => void,
-  organization: Organization
-|};
-*/
-
-/*::
-type State = {
-  createProjectForm: boolean,
-  ready: boolean,
-  projects: Array<{ key: string }>,
-  total: number,
-  page: number,
-  query: string,
-  qualifiers: string,
-  type: string,
-  selection: Array<string>
-};
-*/
-
-export default class Main extends React.PureComponent {
-  /*:: props: Props; */
-  /*:: state: State; */
-
-  constructor(props /*: Props */) {
-    super(props);
-    this.state = {
-      createProjectForm: false,
-      ready: false,
-      projects: [],
-      total: 0,
-      page: 1,
-      query: '',
-      qualifiers: 'TRK',
-      type: TYPE.ALL,
-      selection: []
-    };
-    this.requestProjects = debounce(this.requestProjects, 250);
-  }
-
-  componentDidMount() {
-    this.requestProjects();
-  }
-
-  getFilters = () => {
-    const filters /*: { [string]: string | number } */ = {
-      organization: this.props.organization.key,
-      ps: PAGE_SIZE
-    };
-    if (this.state.page !== 1) {
-      filters.p = this.state.page;
-    }
-    if (this.state.query) {
-      filters.q = this.state.query;
-    }
-    return filters;
-  };
-
-  requestProjects = () => {
-    switch (this.state.type) {
-      case TYPE.ALL:
-        this.requestAllProjects();
-        break;
-      case TYPE.PROVISIONED:
-        this.requestProvisioned();
-        break;
-      case TYPE.GHOSTS:
-        this.requestGhosts();
-        break;
-      default:
-
-      // should never happen
-    }
-  };
-
-  requestGhosts = () => {
-    const data = this.getFilters();
-    getGhosts(data).then(r => {
-      let projects = r.projects.map(project => ({
-        ...project,
-        id: project.uuid,
-        qualifier: 'TRK'
-      }));
-      if (this.state.page > 1) {
-        projects = [].concat(this.state.projects, projects);
-      }
-      this.setState({ ready: true, projects, total: r.total });
-    });
-  };
-
-  requestProvisioned = () => {
-    const data = this.getFilters();
-    getProvisioned(data).then(r => {
-      let projects = r.projects.map(project => ({
-        ...project,
-        id: project.uuid,
-        qualifier: 'TRK'
-      }));
-      if (this.state.page > 1) {
-        projects = [].concat(this.state.projects, projects);
-      }
-      this.setState({ ready: true, projects, total: r.paging.total });
-    });
-  };
-
-  requestAllProjects = () => {
-    const data = this.getFilters();
-    data.qualifiers = this.state.qualifiers;
-    getComponents(data).then(r => {
-      let projects = r.components;
-      if (this.state.page > 1) {
-        projects = [].concat(this.state.projects, projects);
-      }
-      this.setState({ ready: true, projects, total: r.paging.total });
-    });
-  };
-
-  loadMore = () => {
-    this.setState({ ready: false, page: this.state.page + 1 }, this.requestProjects);
-  };
-
-  onSearch = (query /*: string */) => {
-    this.setState(
-      {
-        ready: false,
-        page: 1,
-        query,
-        selection: []
-      },
-      this.requestProjects
-    );
-  };
-
-  onTypeChanged = (newType /*: string */) => {
-    this.setState(
-      {
-        ready: false,
-        page: 1,
-        query: '',
-        type: newType,
-        qualifiers: 'TRK',
-        selection: []
-      },
-      this.requestProjects
-    );
-  };
-
-  onQualifierChanged = (newQualifier /*: string */) => {
-    this.setState(
-      {
-        ready: false,
-        page: 1,
-        query: '',
-        type: TYPE.ALL,
-        qualifiers: newQualifier,
-        selection: []
-      },
-      this.requestProjects
-    );
-  };
-
-  onProjectSelected = (project /*: { key: string } */) => {
-    const newSelection = uniq([].concat(this.state.selection, project.key));
-    this.setState({ selection: newSelection });
-  };
-
-  onProjectDeselected = (project /*: { key: string } */) => {
-    const newSelection = without(this.state.selection, project.key);
-    this.setState({ selection: newSelection });
-  };
-
-  onAllSelected = () => {
-    const newSelection = this.state.projects.map(project => project.key);
-    this.setState({ selection: newSelection });
-  };
-
-  onAllDeselected = () => {
-    this.setState({ selection: [] });
-  };
-
-  deleteProjects = () => {
-    this.setState({ ready: false });
-    const projects = this.state.selection.join(',');
-    const data = {
-      organization: this.props.organization.key,
-      projects
-    };
-    deleteComponents(data).then(() => {
-      this.setState({ page: 1, selection: [] }, this.requestProjects);
-    });
-  };
-
-  openCreateProjectForm = () => {
-    this.setState({ createProjectForm: true });
-  };
-
-  closeCreateProjectForm = () => {
-    this.setState({ createProjectForm: false });
-  };
-
-  render() {
-    return (
-      <div className="page page-limited" id="projects-management-page">
-        <Helmet title={translate('projects_management')} />
-
-        <Header
-          hasProvisionPermission={this.props.hasProvisionPermission}
-          onProjectCreate={this.openCreateProjectForm}
-          onVisibilityChange={this.props.onVisibilityChange}
-          organization={this.props.organization}
-        />
-
-        <Search
-          {...this.props}
-          {...this.state}
-          onSearch={this.onSearch}
-          onTypeChanged={this.onTypeChanged}
-          onQualifierChanged={this.onQualifierChanged}
-          onAllSelected={this.onAllSelected}
-          onAllDeselected={this.onAllDeselected}
-          deleteProjects={this.deleteProjects}
-        />
-
-        <Projects
-          ready={this.state.ready}
-          projects={this.state.projects}
-          refresh={this.requestProjects}
-          selection={this.state.selection}
-          onProjectSelected={this.onProjectSelected}
-          onProjectDeselected={this.onProjectDeselected}
-          organization={this.props.organization}
-        />
-
-        <ListFooter
-          ready={this.state.ready}
-          count={this.state.projects.length}
-          total={this.state.total}
-          loadMore={this.loadMore}
-        />
-
-        {this.state.createProjectForm &&
-          <CreateProjectForm
-            onClose={this.closeCreateProjectForm}
-            onProjectCreated={this.requestProjects}
-            onRequestFail={this.props.onRequestFail}
-            organization={this.props.organization}
-          />}
-      </div>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/projects-admin/projects.js b/server/sonar-web/src/main/js/apps/projects-admin/projects.js
deleted file mode 100644 (file)
index 0d0f593..0000000
+++ /dev/null
@@ -1,123 +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 classNames from 'classnames';
-import React from 'react';
-import PropTypes from 'prop-types';
-import { Link } from 'react-router';
-import { getComponentPermissionsUrl } from '../../helpers/urls';
-import ApplyTemplateView from '../permissions/project/views/ApplyTemplateView';
-import Checkbox from '../../components/controls/Checkbox';
-import QualifierIcon from '../../components/shared/QualifierIcon';
-import PrivateBadge from '../../components/common/PrivateBadge';
-import { translate } from '../../helpers/l10n';
-
-export default class Projects extends React.PureComponent {
-  static propTypes = {
-    projects: PropTypes.array.isRequired,
-    selection: PropTypes.array.isRequired,
-    organization: PropTypes.object.isRequired
-  };
-
-  componentWillMount() {
-    this.renderProject = this.renderProject.bind(this);
-  }
-
-  onProjectCheck(project, checked) {
-    if (checked) {
-      this.props.onProjectSelected(project);
-    } else {
-      this.props.onProjectDeselected(project);
-    }
-  }
-
-  onApplyTemplateClick(project, e) {
-    e.preventDefault();
-    e.target.blur();
-    new ApplyTemplateView({
-      project,
-      organization: this.props.organization
-    }).render();
-  }
-
-  isProjectSelected(project) {
-    return this.props.selection.indexOf(project.key) !== -1;
-  }
-
-  renderProject(project) {
-    const permissionsUrl = getComponentPermissionsUrl(project.key);
-
-    return (
-      <tr key={project.key}>
-        <td className="thin">
-          <Checkbox
-            checked={this.isProjectSelected(project)}
-            onCheck={this.onProjectCheck.bind(this, project)}
-          />
-        </td>
-        <td className="nowrap">
-          <Link
-            to={{ pathname: '/dashboard', query: { id: project.key } }}
-            className="link-with-icon">
-            <QualifierIcon qualifier={project.qualifier} /> <span>{project.name}</span>
-          </Link>
-        </td>
-        <td className="nowrap">
-          <span className="note">
-            {project.key}
-          </span>
-        </td>
-        <td className="width-20">
-          {project.visibility === 'private' && <PrivateBadge />}
-        </td>
-        <td className="thin nowrap">
-          <div className="dropdown">
-            <button className="dropdown-toggle" data-toggle="dropdown">
-              {translate('actions')} <i className="icon-dropdown" />
-            </button>
-            <ul className="dropdown-menu dropdown-menu-right">
-              <li>
-                <Link to={permissionsUrl}>
-                  {translate('edit_permissions')}
-                </Link>
-              </li>
-              <li>
-                <a href="#" onClick={this.onApplyTemplateClick.bind(this, project)}>
-                  {translate('projects_role.apply_template')}
-                </a>
-              </li>
-            </ul>
-          </div>
-        </td>
-      </tr>
-    );
-  }
-
-  render() {
-    const className = classNames('data', 'zebra', { 'new-loading': !this.props.ready });
-
-    return (
-      <table className={className} id="projects-management-page-projects">
-        <tbody>
-          {this.props.projects.map(this.renderProject)}
-        </tbody>
-      </table>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/projects-admin/routes.ts b/server/sonar-web/src/main/js/apps/projects-admin/routes.ts
deleted file mode 100644 (file)
index 447c6ae..0000000
+++ /dev/null
@@ -1,35 +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 { RouterState, IndexRouteProps } from 'react-router';
-
-const routes = [
-  {
-    getIndexRoute(_: RouterState, callback: (err: any, route: IndexRouteProps) => any) {
-      Promise.all([
-        import('./AppContainer').then(i => i.default),
-        import('../organizations/forSingleOrganization').then(i => i.default)
-      ]).then(([AppContainer, forSingleOrganization]) =>
-        callback(null, { component: forSingleOrganization(AppContainer) })
-      );
-    }
-  }
-];
-
-export default routes;
diff --git a/server/sonar-web/src/main/js/apps/projects-admin/search.js b/server/sonar-web/src/main/js/apps/projects-admin/search.js
deleted file mode 100644 (file)
index ce42b20..0000000
+++ /dev/null
@@ -1,179 +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 { sortBy } from 'lodash';
-import { TYPE, QUALIFIERS_ORDER } from './constants';
-import DeleteView from './delete-view';
-import BulkApplyTemplateView from './views/BulkApplyTemplateView';
-import RadioToggle from '../../components/controls/RadioToggle';
-import Checkbox from '../../components/controls/Checkbox';
-import { translate } from '../../helpers/l10n';
-
-export default class Search extends React.PureComponent {
-  static propTypes = {
-    onSearch: PropTypes.func.isRequired
-  };
-
-  onSubmit = e => {
-    e.preventDefault();
-    this.search();
-  };
-
-  search = () => {
-    const q = this.refs.input.value;
-    this.props.onSearch(q);
-  };
-
-  getTypeOptions = () => {
-    return [
-      { value: TYPE.ALL, label: 'All' },
-      { value: TYPE.PROVISIONED, label: 'Provisioned' },
-      { value: TYPE.GHOSTS, label: 'Ghosts' }
-    ];
-  };
-
-  getQualifierOptions = () => {
-    const options = this.props.topLevelQualifiers.map(q => {
-      return { value: q, label: translate('qualifiers', q) };
-    });
-    return sortBy(options, option => QUALIFIERS_ORDER.indexOf(option.value));
-  };
-
-  onCheck = checked => {
-    if (checked) {
-      this.props.onAllSelected();
-    } else {
-      this.props.onAllDeselected();
-    }
-  };
-
-  deleteProjects = () => {
-    new DeleteView({
-      deleteProjects: this.props.deleteProjects
-    }).render();
-  };
-
-  bulkApplyTemplate = () => {
-    new BulkApplyTemplateView({
-      total: this.props.total,
-      selection: this.props.selection,
-      query: this.props.query,
-      qualifier: this.props.qualifier,
-      organization: this.props.organization
-    }).render();
-  };
-
-  renderCheckbox = () => {
-    const isAllChecked =
-      this.props.projects.length > 0 && this.props.selection.length === this.props.projects.length;
-    const thirdState =
-      this.props.projects.length > 0 &&
-      this.props.selection.length > 0 &&
-      this.props.selection.length < this.props.projects.length;
-    const checked = isAllChecked || thirdState;
-    return <Checkbox checked={checked} thirdState={thirdState} onCheck={this.onCheck} />;
-  };
-
-  renderGhostsDescription = () => {
-    if (this.props.type !== TYPE.GHOSTS || !this.props.ready) {
-      return null;
-    }
-    return (
-      <div className="spacer-top alert alert-info">
-        {translate('bulk_deletion.ghosts.description')}
-      </div>
-    );
-  };
-
-  renderQualifierFilter = () => {
-    const options = this.getQualifierOptions();
-    if (options.length < 2) {
-      return null;
-    }
-    return (
-      <td className="thin nowrap text-middle">
-        <RadioToggle
-          options={this.getQualifierOptions()}
-          value={this.props.qualifiers}
-          name="projects-qualifier"
-          onCheck={this.props.onQualifierChanged}
-        />
-      </td>
-    );
-  };
-
-  renderSpinner = () => <i className="spinner" />;
-
-  render() {
-    const isSomethingSelected = this.props.projects.length > 0 && this.props.selection.length > 0;
-    return (
-      <div className="panel panel-vertical bordered-bottom spacer-bottom">
-        <table className="data">
-          <tbody>
-            <tr>
-              <td className="thin text-middle">
-                {this.props.ready ? this.renderCheckbox() : this.renderSpinner()}
-              </td>
-              {this.renderQualifierFilter()}
-              <td className="thin nowrap text-middle">
-                <RadioToggle
-                  options={this.getTypeOptions()}
-                  value={this.props.type}
-                  name="projects-type"
-                  onCheck={this.props.onTypeChanged}
-                />
-              </td>
-              <td className="text-middle">
-                <form onSubmit={this.onSubmit} className="search-box">
-                  <button className="search-box-submit button-clean">
-                    <i className="icon-search" />
-                  </button>
-                  <input
-                    onChange={this.search}
-                    value={this.props.query}
-                    ref="input"
-                    className="search-box-input input-medium"
-                    type="search"
-                    placeholder="Search"
-                  />
-                </form>
-              </td>
-              <td className="thin nowrap text-middle">
-                <button
-                  className="spacer-right js-bulk-apply-permission-template"
-                  onClick={this.bulkApplyTemplate}>
-                  {translate('permission_templates.bulk_apply_permission_template')}
-                </button>
-                <button
-                  onClick={this.deleteProjects}
-                  className="button-red"
-                  disabled={!isSomethingSelected}>
-                  {translate('delete')}
-                </button>
-              </td>
-            </tr>
-          </tbody>
-        </table>
-        {this.renderGhostsDescription()}
-      </div>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/projects-admin/templates/BulkApplyTemplateTemplate.hbs b/server/sonar-web/src/main/js/apps/projects-admin/templates/BulkApplyTemplateTemplate.hbs
deleted file mode 100644 (file)
index b4933eb..0000000
+++ /dev/null
@@ -1,68 +0,0 @@
-<form class="js-form" autocomplete="off">
-  <div class="modal-head">
-    <h2>Bulk Apply Permission Template</h2>
-  </div>
-
-  <div class="modal-body">
-    <div class="js-modal-messages"></div>
-
-    {{#if done}}
-      <div class="alert alert-success">
-        {{t 'projects_role.apply_template.success'}}
-      </div>
-    {{/if}}
-
-    {{#unless done}}
-      {{#notNull permissionTemplates}}
-        <div class="modal-field">
-          <label for="project-permissions-template">
-            Template<em class="mandatory">*</em>
-          </label>
-          <select id="project-permissions-template">
-            {{#each permissionTemplates}}
-              <option value="{{id}}">{{name}}</option>
-            {{/each}}
-          </select>
-        </div>
-      {{else}}
-        <i class="spinner"></i>
-      {{/notNull}}
-
-
-      <div class="modal-field">
-        <label>Apply To</label>
-        <ul style="padding-top: 4px;">
-          {{#if selectionTotal}}
-            <li>
-              <input value="selected" id="apply-to-selected" name="apply-to"
-                     type="radio" checked>
-              <label
-                  for="apply-to-selected"
-                  style="float: none; left: 0; display: inline; padding: 0;">
-                Only Selected ({{selectionTotal}})
-              </label>
-            </li>
-          {{/if}}
-          <li>
-            <input value="all" id="apply-to-all" name="apply-to" type="radio"
-                   {{#unless selectionTotal}}checked{{/unless}}>
-            <label
-                for="apply-to-all"
-                style="float: none; left: 0; display: inline; padding: 0;">
-              All ({{total}})
-            </label>
-          </li>
-        </ul>
-      </div>
-    {{/unless}}
-  </div>
-
-  <div class="modal-foot">
-    {{#unless done}}
-      {{#notNull permissionTemplates}}
-        <button class="js-apply">Apply</button>
-      {{/notNull}}
-    {{/unless}}
-    <a href="#" class="js-modal-close">Close</a>
-  </div>
-</form>
diff --git a/server/sonar-web/src/main/js/apps/projects-admin/templates/projects-delete.hbs b/server/sonar-web/src/main/js/apps/projects-admin/templates/projects-delete.hbs
deleted file mode 100644 (file)
index 2ab69b2..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-<form id="delete-project-form">
-  <div class="modal-head">
-    <h2>Delete Projects</h2>
-  </div>
-  <div class="modal-body">
-    <div class="js-modal-messages"></div>
-    Are you sure you want to delete selected projects?
-  </div>
-  <div class="modal-foot">
-    <button id="delete-project-submit" class="button-red">Delete</button>
-    <a href="#" class="js-modal-close" id="delete-project-cancel">Cancel</a>
-  </div>
-</form>
diff --git a/server/sonar-web/src/main/js/apps/projects-admin/views/BulkApplyTemplateView.js b/server/sonar-web/src/main/js/apps/projects-admin/views/BulkApplyTemplateView.js
deleted file mode 100644 (file)
index 5dd9e8a..0000000
+++ /dev/null
@@ -1,121 +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 ModalForm from '../../../components/common/modal-form';
-import {
-  applyTemplateToProject,
-  bulkApplyTemplate,
-  getPermissionTemplates
-} from '../../../api/permissions';
-import Template from '../templates/BulkApplyTemplateTemplate.hbs';
-
-export default ModalForm.extend({
-  template: Template,
-
-  initialize() {
-    this.loadPermissionTemplates();
-    this.done = false;
-  },
-
-  loadPermissionTemplates() {
-    const request = this.options.organization
-      ? getPermissionTemplates(this.options.organization.key)
-      : getPermissionTemplates();
-    return request.then(r => {
-      this.permissionTemplates = r.permissionTemplates;
-      this.render();
-    });
-  },
-
-  onRender() {
-    ModalForm.prototype.onRender.apply(this, arguments);
-    this.$('#project-permissions-template').select2({
-      width: '250px',
-      minimumResultsForSearch: 20
-    });
-  },
-
-  bulkApplyToAll(permissionTemplate) {
-    const data = { templateId: permissionTemplate };
-
-    if (this.options.query) {
-      data.q = this.options.query;
-    }
-
-    if (this.options.qualifier) {
-      data.qualifier = this.options.qualifier;
-    }
-
-    if (this.options.organization) {
-      data.organization = this.options.organization.key;
-    }
-
-    return bulkApplyTemplate(data);
-  },
-
-  bulkApplyToSelected(permissionTemplate) {
-    const { selection } = this.options;
-    let lastRequest = Promise.resolve();
-
-    selection.forEach(projectKey => {
-      const data = { templateId: permissionTemplate, projectKey };
-      if (this.options.organization) {
-        data.organization = this.options.organization.key;
-      }
-      lastRequest = lastRequest.then(() => applyTemplateToProject(data));
-    });
-
-    return lastRequest;
-  },
-
-  onFormSubmit() {
-    ModalForm.prototype.onFormSubmit.apply(this, arguments);
-    const permissionTemplate = this.$('#project-permissions-template').val();
-    const applyTo = this.$('[name="apply-to"]:checked').val();
-    this.disableForm();
-
-    const request =
-      applyTo === 'all'
-        ? this.bulkApplyToAll(permissionTemplate)
-        : this.bulkApplyToSelected(permissionTemplate);
-
-    request
-      .then(() => {
-        this.trigger('done');
-        this.done = true;
-        this.render();
-      })
-      .catch(e => {
-        e.response.json().then(r => {
-          this.showErrors(r.errors, r.warnings);
-          this.enableForm();
-        });
-      });
-  },
-
-  serializeData() {
-    return {
-      permissionTemplates: this.permissionTemplates,
-      selection: this.options.selection,
-      selectionTotal: this.options.selection.length,
-      total: this.options.total,
-      done: this.done
-    };
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/App.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/App.tsx
new file mode 100644 (file)
index 0000000..5a860ab
--- /dev/null
@@ -0,0 +1,246 @@
+/*
+ * 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 { debounce, uniq, without } from 'lodash';
+import Header from './Header';
+import Search from './Search';
+import Projects from './Projects';
+import CreateProjectForm from './CreateProjectForm';
+import ListFooter from '../../components/controls/ListFooter';
+import { PAGE_SIZE, Type, Project } from './utils';
+import { getComponents, getProvisioned, getGhosts } from '../../api/components';
+import { Organization } from '../../app/types';
+import { translate } from '../../helpers/l10n';
+
+export interface Props {
+  hasProvisionPermission?: boolean;
+  onVisibilityChange: (visibility: string) => void;
+  organization: Organization;
+  topLevelQualifiers: string[];
+}
+
+interface State {
+  createProjectForm: boolean;
+  page: number;
+  projects: Project[];
+  qualifiers: string;
+  query: string;
+  ready: boolean;
+  selection: string[];
+  total: number;
+  type: Type;
+}
+
+export default class App extends React.PureComponent<Props, State> {
+  mounted: boolean;
+
+  constructor(props: Props) {
+    super(props);
+    this.state = {
+      createProjectForm: false,
+      ready: false,
+      projects: [],
+      total: 0,
+      page: 1,
+      query: '',
+      qualifiers: 'TRK',
+      type: Type.All,
+      selection: []
+    };
+    this.requestProjects = debounce(this.requestProjects, 250);
+  }
+
+  componentDidMount() {
+    this.mounted = true;
+    this.requestProjects();
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  getFilters = () => ({
+    organization: this.props.organization.key,
+    p: this.state.page !== 1 ? this.state.page : undefined,
+    ps: PAGE_SIZE,
+    q: this.state.query ? this.state.query : undefined
+  });
+
+  requestProjects = () => {
+    switch (this.state.type) {
+      case Type.All:
+        this.requestAllProjects();
+        break;
+      case Type.Provisioned:
+        this.requestProvisioned();
+        break;
+      case Type.Ghosts:
+        this.requestGhosts();
+        break;
+    }
+  };
+
+  requestGhosts = () => {
+    const data = this.getFilters();
+    getGhosts(data).then(r => {
+      if (this.mounted) {
+        let projects: Project[] = r.projects.map((project: any) => ({
+          ...project,
+          id: project.uuid,
+          qualifier: 'TRK'
+        }));
+        if (this.state.page > 1) {
+          projects = [...this.state.projects, ...projects];
+        }
+        this.setState({ ready: true, projects, selection: [], total: r.total });
+      }
+    });
+  };
+
+  requestProvisioned = () => {
+    const data = this.getFilters();
+    getProvisioned(data).then(r => {
+      if (this.mounted) {
+        let projects: Project[] = r.projects.map((project: any) => ({
+          ...project,
+          id: project.uuid,
+          qualifier: 'TRK'
+        }));
+        if (this.state.page > 1) {
+          projects = [...this.state.projects, ...projects];
+        }
+        this.setState({ ready: true, projects, selection: [], total: r.paging.total });
+      }
+    });
+  };
+
+  requestAllProjects = () => {
+    const data = this.getFilters();
+    Object.assign(data, { qualifiers: this.state.qualifiers });
+    getComponents(data).then(r => {
+      if (this.mounted) {
+        let projects: Project[] = r.components;
+        if (this.state.page > 1) {
+          projects = [...this.state.projects, ...projects];
+        }
+        this.setState({ ready: true, projects, selection: [], total: r.paging.total });
+      }
+    });
+  };
+
+  loadMore = () => {
+    this.setState({ ready: false, page: this.state.page + 1 }, this.requestProjects);
+  };
+
+  onSearch = (query: string) => {
+    this.setState({ ready: false, page: 1, query, selection: [] }, this.requestProjects);
+  };
+
+  onTypeChanged = (newType: Type) => {
+    this.setState(
+      { ready: false, page: 1, query: '', type: newType, qualifiers: 'TRK', selection: [] },
+      this.requestProjects
+    );
+  };
+
+  onQualifierChanged = (newQualifier: string) => {
+    this.setState(
+      { ready: false, page: 1, query: '', type: Type.All, qualifiers: newQualifier, selection: [] },
+      this.requestProjects
+    );
+  };
+
+  onProjectSelected = (project: string) => {
+    const newSelection = uniq([...this.state.selection, project]);
+    this.setState({ selection: newSelection });
+  };
+
+  onProjectDeselected = (project: string) => {
+    const newSelection = without(this.state.selection, project);
+    this.setState({ selection: newSelection });
+  };
+
+  onAllSelected = () => {
+    const newSelection = this.state.projects.map(project => project.key);
+    this.setState({ selection: newSelection });
+  };
+
+  onAllDeselected = () => {
+    this.setState({ selection: [] });
+  };
+
+  openCreateProjectForm = () => {
+    this.setState({ createProjectForm: true });
+  };
+
+  closeCreateProjectForm = () => {
+    this.setState({ createProjectForm: false });
+  };
+
+  render() {
+    return (
+      <div className="page page-limited" id="projects-management-page">
+        <Helmet title={translate('projects_management')} />
+
+        <Header
+          hasProvisionPermission={this.props.hasProvisionPermission}
+          onProjectCreate={this.openCreateProjectForm}
+          onVisibilityChange={this.props.onVisibilityChange}
+          organization={this.props.organization}
+        />
+
+        <Search
+          {...this.props}
+          {...this.state}
+          onAllSelected={this.onAllSelected}
+          onAllDeselected={this.onAllDeselected}
+          onDeleteProjects={this.requestProjects}
+          onQualifierChanged={this.onQualifierChanged}
+          onSearch={this.onSearch}
+          onTypeChanged={this.onTypeChanged}
+        />
+
+        <Projects
+          ready={this.state.ready}
+          projects={this.state.projects}
+          selection={this.state.selection}
+          onProjectSelected={this.onProjectSelected}
+          onProjectDeselected={this.onProjectDeselected}
+          organization={this.props.organization}
+        />
+
+        <ListFooter
+          ready={this.state.ready}
+          count={this.state.projects.length}
+          total={this.state.total}
+          loadMore={this.loadMore}
+        />
+
+        {this.state.createProjectForm &&
+          <CreateProjectForm
+            onClose={this.closeCreateProjectForm}
+            onProjectCreated={this.requestProjects}
+            organization={this.props.organization}
+          />}
+      </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/AppContainer.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/AppContainer.tsx
new file mode 100644 (file)
index 0000000..0b00f3e
--- /dev/null
@@ -0,0 +1,99 @@
+/*
+ * 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 App from './App';
+import { Organization } from '../../app/types';
+import { onFail } from '../../store/rootActions';
+import { getAppState, getOrganizationByKey } from '../../store/rootReducer';
+import { receiveOrganizations } from '../../store/organizations/duck';
+import { changeProjectVisibility } from '../../api/organizations';
+import { fetchOrganization } from '../../apps/organizations/actions';
+
+interface Props {
+  appState: {
+    defaultOrganization: string;
+    qualifiers: string[];
+  };
+  fetchOrganization: (organization: string) => void;
+  onVisibilityChange: (organization: Organization, visibility: string) => void;
+  onRequestFail: (error: any) => void;
+  organization?: Organization;
+}
+
+class AppContainer extends React.PureComponent<Props> {
+  componentDidMount() {
+    // if there is no organization, that means we are in the global scope
+    // let's fetch defails for the default organization in this case
+    if (!this.props.organization || !this.props.organization.projectVisibility) {
+      this.props.fetchOrganization(this.props.appState.defaultOrganization);
+    }
+  }
+
+  handleVisibilityChange = (visibility: string) => {
+    if (this.props.organization) {
+      this.props.onVisibilityChange(this.props.organization, visibility);
+    }
+  };
+
+  render() {
+    const { organization } = this.props;
+
+    if (!organization) {
+      return null;
+    }
+
+    const topLevelQualifiers = organization.isDefault ? this.props.appState.qualifiers : ['TRK'];
+
+    return (
+      <App
+        hasProvisionPermission={organization.canProvisionProjects}
+        onVisibilityChange={this.handleVisibilityChange}
+        organization={organization}
+        topLevelQualifiers={topLevelQualifiers}
+      />
+    );
+  }
+}
+
+const mapStateToProps = (state: any, ownProps: Props) => ({
+  appState: getAppState(state),
+  organization:
+    ownProps.organization || getOrganizationByKey(state, getAppState(state).defaultOrganization)
+});
+
+const onVisibilityChange = (organization: Organization, visibility: string) => (
+  dispatch: Function
+) => {
+  const currentVisibility = organization.projectVisibility;
+  dispatch(receiveOrganizations([{ ...organization, projectVisibility: visibility }]));
+  changeProjectVisibility(organization.key, visibility).catch(error => {
+    onFail(dispatch)(error);
+    dispatch(receiveOrganizations([{ ...organization, projectVisibility: currentVisibility }]));
+  });
+};
+
+const mapDispatchToProps = (dispatch: Function) => ({
+  fetchOrganization: (key: string) => dispatch(fetchOrganization(key)),
+  onVisibilityChange: (organization: Organization, visibility: string) =>
+    dispatch(onVisibilityChange(organization, visibility))
+});
+
+export default connect<any, any, any>(mapStateToProps, mapDispatchToProps)(AppContainer);
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/BulkApplyTemplateModal.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/BulkApplyTemplateModal.tsx
new file mode 100644 (file)
index 0000000..a5f967f
--- /dev/null
@@ -0,0 +1,217 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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 Modal from 'react-modal';
+import * as Select from 'react-select';
+import { Type } from './utils';
+import {
+  getPermissionTemplates,
+  PermissionTemplate,
+  bulkApplyTemplate,
+  applyTemplateToProject
+} from '../../api/permissions';
+import { translate, translateWithParameters } from '../../helpers/l10n';
+
+export interface Props {
+  onClose: () => void;
+  organization: string;
+  qualifier: string;
+  query: string;
+  selection: string[];
+  total: number;
+  type: Type;
+}
+
+interface State {
+  done: boolean;
+  loading: boolean;
+  permissionTemplate?: string;
+  permissionTemplates?: PermissionTemplate[];
+  submitting: boolean;
+}
+
+export default class BulkApplyTemplateModal extends React.PureComponent<Props, State> {
+  mounted: boolean;
+  state: State = { done: false, loading: true, submitting: false };
+
+  componentDidMount() {
+    this.mounted = true;
+    this.loadPermissionTemplates();
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  loadPermissionTemplates() {
+    this.setState({ loading: true });
+    getPermissionTemplates(this.props.organization).then(
+      ({ permissionTemplates }) => {
+        if (this.mounted) {
+          this.setState({
+            loading: false,
+            permissionTemplate:
+              permissionTemplates.length > 0 ? permissionTemplates[0].id : undefined,
+            permissionTemplates: permissionTemplates
+          });
+        }
+      },
+      () => {
+        if (this.mounted) {
+          this.setState({ loading: false });
+        }
+      }
+    );
+  }
+
+  bulkApplyToAll = (permissionTemplate: string) => {
+    const data = {
+      organization: this.props.organization,
+      q: this.props.query ? this.props.query : undefined,
+      qualifier: this.props.qualifier,
+      templateId: permissionTemplate
+    };
+    return bulkApplyTemplate(data);
+  };
+
+  bulkApplyToSelected = (permissionTemplate: string) => {
+    const { selection } = this.props;
+    let lastRequest = Promise.resolve();
+
+    selection.forEach(projectKey => {
+      const data = {
+        organization: this.props.organization,
+        projectKey,
+        templateId: permissionTemplate
+      };
+      lastRequest = lastRequest.then(() => applyTemplateToProject(data));
+    });
+
+    return lastRequest;
+  };
+
+  handleCancelClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
+    event.preventDefault();
+    this.props.onClose();
+  };
+
+  handleConfirmClick = () => {
+    const { permissionTemplate } = this.state;
+    if (permissionTemplate) {
+      this.setState({ submitting: true });
+      const request = this.props.selection.length
+        ? this.bulkApplyToSelected(permissionTemplate)
+        : this.bulkApplyToAll(permissionTemplate);
+      request.then(
+        () => {
+          if (this.mounted) {
+            this.setState({ done: true, submitting: false });
+          }
+        },
+        () => {
+          if (this.mounted) {
+            this.setState({ submitting: false });
+          }
+        }
+      );
+    }
+  };
+
+  handlePermissionTemplateChange = ({ value }: { value: string }) => {
+    this.setState({ permissionTemplate: value });
+  };
+
+  renderWarning = () => {
+    return this.props.selection.length
+      ? <div className="alert alert-info">
+          {translateWithParameters(
+            'permission_templates.bulk_apply_permission_template.apply_to_selected',
+            this.props.selection.length
+          )}
+        </div>
+      : <div className="alert alert-warning">
+          {translateWithParameters(
+            'permission_templates.bulk_apply_permission_template.apply_to_all',
+            this.props.total
+          )}
+        </div>;
+  };
+
+  renderSelect = () =>
+    <div className="modal-field">
+      <label>
+        {translate('template')}
+        <em className="mandatory">*</em>
+      </label>
+      <Select
+        clearable={false}
+        disabled={this.state.submitting}
+        onChange={this.handlePermissionTemplateChange}
+        options={this.state.permissionTemplates!.map(t => ({ label: t.name, value: t.id }))}
+        searchable={false}
+        value={this.state.permissionTemplate}
+      />
+    </div>;
+
+  render() {
+    const { done, loading, permissionTemplates, submitting } = this.state;
+    const header = translate('permission_templates.bulk_apply_permission_template');
+
+    return (
+      <Modal
+        isOpen={true}
+        contentLabel={header}
+        className="modal"
+        overlayClassName="modal-overlay"
+        onRequestClose={this.props.onClose}>
+        <header className="modal-head">
+          <h2>
+            {header}
+          </h2>
+        </header>
+
+        <div className="modal-body">
+          {done &&
+            <div className="alert alert-success">
+              {translate('projects_role.apply_template.success')}
+            </div>}
+
+          {loading && <i className="spinner" />}
+
+          {!loading && !done && permissionTemplates && this.renderWarning()}
+          {!loading && !done && permissionTemplates && this.renderSelect()}
+        </div>
+
+        <footer className="modal-foot">
+          {submitting && <i className="spinner spacer-right" />}
+          {!loading &&
+            !done &&
+            permissionTemplates &&
+            <button disabled={submitting} onClick={this.handleConfirmClick}>
+              {translate('apply')}
+            </button>}
+          <a className="js-modal-close" href="#" onClick={this.handleCancelClick}>
+            {done ? translate('close') : translate('cancel')}
+          </a>
+        </footer>
+      </Modal>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/ChangeVisibilityForm.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/ChangeVisibilityForm.tsx
new file mode 100644 (file)
index 0000000..9c04349
--- /dev/null
@@ -0,0 +1,128 @@
+/*
+ * 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 Modal from 'react-modal';
+import * as classNames from 'classnames';
+import { Organization } from '../../app/types';
+import UpgradeOrganizationBox from '../../components/common/UpgradeOrganizationBox';
+import { translate } from '../../helpers/l10n';
+import { Visibility } from './utils';
+
+export interface Props {
+  onClose: () => void;
+  onConfirm: (visiblity: Visibility) => void;
+  organization: Organization;
+}
+
+interface State {
+  visibility: Visibility;
+}
+
+export default class ChangeVisibilityForm extends React.PureComponent<Props, State> {
+  constructor(props: Props) {
+    super(props);
+    this.state = { visibility: props.organization.projectVisibility as Visibility };
+  }
+
+  handleCancelClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
+    event.preventDefault();
+    this.props.onClose();
+  };
+
+  handleConfirmClick = (event: React.SyntheticEvent<HTMLButtonElement>) => {
+    event.preventDefault();
+    this.props.onConfirm(this.state.visibility);
+    this.props.onClose();
+  };
+
+  handleVisibilityClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
+    event.preventDefault();
+    event.currentTarget.blur();
+    const visibility = event.currentTarget.dataset.visibility as Visibility;
+    this.setState({ visibility });
+  };
+
+  render() {
+    const { canUpdateProjectsVisibilityToPrivate } = this.props.organization;
+
+    return (
+      <Modal
+        isOpen={true}
+        contentLabel="modal form"
+        className="modal"
+        overlayClassName="modal-overlay"
+        onRequestClose={this.props.onClose}>
+        <header className="modal-head">
+          <h2>
+            {translate('organization.change_visibility_form.header')}
+          </h2>
+        </header>
+
+        <div className="modal-body">
+          {[Visibility.Public, Visibility.Private].map(visibility =>
+            <div className="big-spacer-bottom" key={visibility}>
+              <p>
+                {visibility === Visibility.Private && !canUpdateProjectsVisibilityToPrivate
+                  ? <span className="text-muted cursor-not-allowed">
+                      <i
+                        className={classNames('icon-radio', 'spacer-right', {
+                          'is-checked': this.state.visibility === visibility
+                        })}
+                      />
+                      {translate('visibility', visibility)}
+                    </span>
+                  : <a
+                      className="link-base-color link-no-underline"
+                      data-visibility={visibility}
+                      href="#"
+                      onClick={this.handleVisibilityClick}>
+                      <i
+                        className={classNames('icon-radio', 'spacer-right', {
+                          'is-checked': this.state.visibility === visibility
+                        })}
+                      />
+                      {translate('visibility', visibility)}
+                    </a>}
+              </p>
+              <p className="text-muted spacer-top" style={{ paddingLeft: 22 }}>
+                {translate('visibility', visibility, 'description.short')}
+              </p>
+            </div>
+          )}
+
+          {canUpdateProjectsVisibilityToPrivate
+            ? <div className="alert alert-warning">
+                {translate('organization.change_visibility_form.warning')}
+              </div>
+            : <UpgradeOrganizationBox organization={this.props.organization.key} />}
+        </div>
+
+        <footer className="modal-foot">
+          <button className="js-confirm" onClick={this.handleConfirmClick}>
+            {translate('organization.change_visibility_form.submit')}
+          </button>
+          <a className="js-modal-close" href="#" onClick={this.handleCancelClick}>
+            {translate('cancel')}
+          </a>
+        </footer>
+      </Modal>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/CreateProjectForm.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/CreateProjectForm.tsx
new file mode 100644 (file)
index 0000000..22a81ac
--- /dev/null
@@ -0,0 +1,226 @@
+/*
+ * 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 Modal from 'react-modal';
+import { Link } from 'react-router';
+import { Organization } from '../../app/types';
+import UpgradeOrganizationBox from '../../components/common/UpgradeOrganizationBox';
+import VisibilitySelector from '../../components/common/VisibilitySelector';
+import { createProject } from '../../api/components';
+import { translate } from '../../helpers/l10n';
+import { getProjectUrl } from '../../helpers/urls';
+
+interface Props {
+  onClose: () => void;
+  onProjectCreated: () => void;
+  organization: Organization;
+}
+
+interface State {
+  branch: string;
+  createdProject?: { key: string; name: string };
+  key: string;
+  loading: boolean;
+  name: string;
+  visibility: string;
+  // add index declaration to be able to do `this.setState({ [name]: value });`
+  [x: string]: any;
+}
+
+export default class CreateProjectForm extends React.PureComponent<Props, State> {
+  mounted: boolean;
+
+  constructor(props: Props) {
+    super(props);
+    this.state = {
+      branch: '',
+      key: '',
+      loading: false,
+      name: '',
+      visibility: props.organization.projectVisibility
+    };
+  }
+
+  componentDidMount() {
+    this.mounted = true;
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  handleCancelClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
+    event.preventDefault();
+    this.props.onClose();
+  };
+
+  handleInputChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
+    const { name, value } = event.currentTarget;
+    this.setState({ [name]: value });
+  };
+
+  handleVisibilityChange = (visibility: string) => {
+    this.setState({ visibility });
+  };
+
+  handleFormSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
+    event.preventDefault();
+
+    const data = {
+      name: this.state.name,
+      branch: this.state.branch,
+      organization: this.props.organization && this.props.organization.key,
+      project: this.state.key,
+      visibility: this.state.visibility
+    };
+
+    this.setState({ loading: true });
+    createProject(data).then(
+      response => {
+        if (this.mounted) {
+          this.setState({ createdProject: response.project, loading: false });
+          this.props.onProjectCreated();
+        }
+      },
+      () => {
+        if (this.mounted) {
+          this.setState({ loading: false });
+        }
+      }
+    );
+  };
+
+  render() {
+    const { organization } = this.props;
+    const { createdProject } = this.state;
+
+    return (
+      <Modal
+        isOpen={true}
+        contentLabel="modal form"
+        className="modal"
+        overlayClassName="modal-overlay"
+        onRequestClose={this.props.onClose}>
+        {createdProject
+          ? <div>
+              <header className="modal-head">
+                <h2>
+                  {translate('qualifiers.create.TRK')}
+                </h2>
+              </header>
+
+              <div className="modal-body">
+                <div className="alert alert-success">
+                  Project <Link to={getProjectUrl(createdProject.key)}>
+                    {createdProject.name}
+                  </Link>{' '}
+                  has been successfully created.
+                </div>
+              </div>
+
+              <footer className="modal-foot">
+                <a href="#" id="create-project-close" onClick={this.handleCancelClick}>
+                  {translate('close')}
+                </a>
+              </footer>
+            </div>
+          : <form id="create-project-form" onSubmit={this.handleFormSubmit}>
+              <header className="modal-head">
+                <h2>
+                  {translate('qualifiers.create.TRK')}
+                </h2>
+              </header>
+
+              <div className="modal-body">
+                <div className="modal-field">
+                  <label htmlFor="create-project-name">
+                    {translate('name')}
+                    <em className="mandatory">*</em>
+                  </label>
+                  <input
+                    autoFocus={true}
+                    id="create-project-name"
+                    maxLength={2000}
+                    name="name"
+                    onChange={this.handleInputChange}
+                    required={true}
+                    type="text"
+                    value={this.state.name}
+                  />
+                </div>
+                <div className="modal-field">
+                  <label htmlFor="create-project-branch">
+                    {translate('branch')}
+                  </label>
+                  <input
+                    id="create-project-branch"
+                    maxLength={200}
+                    name="branch"
+                    onChange={this.handleInputChange}
+                    type="text"
+                    value={this.state.branch}
+                  />
+                </div>
+                <div className="modal-field">
+                  <label htmlFor="create-project-key">
+                    {translate('key')}
+                    <em className="mandatory">*</em>
+                  </label>
+                  <input
+                    id="create-project-key"
+                    maxLength={400}
+                    name="key"
+                    onChange={this.handleInputChange}
+                    required={true}
+                    type="text"
+                    value={this.state.key}
+                  />
+                </div>
+                <div className="modal-field">
+                  <label>
+                    {translate('visibility')}
+                  </label>
+                  <VisibilitySelector
+                    canTurnToPrivate={organization.canUpdateProjectsVisibilityToPrivate}
+                    className="little-spacer-top"
+                    onChange={this.handleVisibilityChange}
+                    visibility={this.state.visibility}
+                  />
+                  {!organization.canUpdateProjectsVisibilityToPrivate &&
+                    <div className="spacer-top">
+                      <UpgradeOrganizationBox organization={organization.key} />
+                    </div>}
+                </div>
+              </div>
+
+              <footer className="modal-foot">
+                {this.state.loading && <i className="spinner spacer-right" />}
+                <button disabled={this.state.loading} id="create-project-submit" type="submit">
+                  {translate('create')}
+                </button>
+                <a href="#" id="create-project-cancel" onClick={this.handleCancelClick}>
+                  {translate('cancel')}
+                </a>
+              </footer>
+            </form>}
+      </Modal>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/DeleteModal.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/DeleteModal.tsx
new file mode 100644 (file)
index 0000000..7b4a45f
--- /dev/null
@@ -0,0 +1,105 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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 Modal from 'react-modal';
+import { deleteComponents } from '../../api/components';
+import { translate } from '../../helpers/l10n';
+
+export interface Props {
+  onClose: () => void;
+  onConfirm: () => void;
+  organization: string;
+  qualifier: string;
+  selection: string[];
+}
+
+interface State {
+  loading: boolean;
+}
+
+export default class DeleteModal extends React.PureComponent<Props, State> {
+  mounted: boolean;
+  state: State = { loading: false };
+
+  componentDidMount() {
+    this.mounted = true;
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  handleCancelClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
+    event.preventDefault();
+    this.props.onClose();
+  };
+
+  handleConfirmClick = () => {
+    this.setState({ loading: true });
+    deleteComponents(this.props.selection, this.props.organization).then(
+      () => {
+        if (this.mounted) {
+          this.props.onConfirm();
+        }
+      },
+      () => {
+        if (this.mounted) {
+          this.setState({ loading: false });
+        }
+      }
+    );
+  };
+
+  render() {
+    const header = translate('qualifiers.delete', this.props.qualifier);
+
+    return (
+      <Modal
+        isOpen={true}
+        contentLabel={header}
+        className="modal"
+        overlayClassName="modal-overlay"
+        onRequestClose={this.props.onClose}>
+        <header className="modal-head">
+          <h2>
+            {header}
+          </h2>
+        </header>
+
+        <div className="modal-body">
+          {translate('qualifiers.delete_confirm', this.props.qualifier)}
+        </div>
+
+        <footer className="modal-foot">
+          {this.state.loading && <i className="spinner spacer-right" />}
+          <button
+            className="button-red"
+            disabled={this.state.loading}
+            onClick={this.handleConfirmClick}>
+            {translate('delete')}
+          </button>
+          <a className="js-modal-close" href="#" onClick={this.handleCancelClick}>
+            {translate('cancel')}
+          </a>
+        </footer>
+      </Modal>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/Header.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/Header.tsx
new file mode 100644 (file)
index 0000000..ef77181
--- /dev/null
@@ -0,0 +1,92 @@
+/*
+ * 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 ChangeVisibilityForm from './ChangeVisibilityForm';
+import { Visibility } from './utils';
+import { Organization } from '../../app/types';
+import { translate } from '../../helpers/l10n';
+
+export interface Props {
+  hasProvisionPermission?: boolean;
+  onProjectCreate: () => void;
+  onVisibilityChange: (visibility: Visibility) => void;
+  organization: Organization;
+}
+
+interface State {
+  visibilityForm: boolean;
+}
+
+export default class Header extends React.PureComponent<Props, State> {
+  state: State = { visibilityForm: false };
+
+  handleCreateProjectClick = (event: React.SyntheticEvent<HTMLButtonElement>) => {
+    event.preventDefault();
+    this.props.onProjectCreate();
+  };
+
+  handleChangeVisibilityClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
+    event.preventDefault();
+    this.setState({ visibilityForm: true });
+  };
+
+  closeVisiblityForm = () => {
+    this.setState({ visibilityForm: false });
+  };
+
+  render() {
+    const { organization } = this.props;
+
+    return (
+      <header className="page-header">
+        <h1 className="page-title">
+          {translate('projects_management')}
+        </h1>
+
+        <div className="page-actions">
+          <span className="big-spacer-right">
+            {translate('organization.default_visibility_of_new_projects')}{' '}
+            <strong>{translate('visibility', organization.projectVisibility)}</strong>
+            <a
+              className="js-change-visibility spacer-left icon-edit"
+              href="#"
+              onClick={this.handleChangeVisibilityClick}
+            />
+          </span>
+          {this.props.hasProvisionPermission &&
+            <button id="create-project" onClick={this.handleCreateProjectClick}>
+              {translate('qualifiers.create.TRK')}
+            </button>}
+        </div>
+
+        <p className="page-description">
+          {translate('projects_management.page.description')}
+        </p>
+
+        {this.state.visibilityForm &&
+          <ChangeVisibilityForm
+            onClose={this.closeVisiblityForm}
+            onConfirm={this.props.onVisibilityChange}
+            organization={organization}
+          />}
+      </header>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx
new file mode 100644 (file)
index 0000000..60f951d
--- /dev/null
@@ -0,0 +1,96 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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 { Link } from 'react-router';
+import { Project, Visibility } from './utils';
+import PrivateBadge from '../../components/common/PrivateBadge';
+import Checkbox from '../../components/controls/Checkbox';
+import QualifierIcon from '../../components/shared/QualifierIcon';
+import { translate } from '../../helpers/l10n';
+import { getComponentPermissionsUrl } from '../../helpers/urls';
+
+interface Props {
+  onApplyTemplateClick: (project: Project) => void;
+  onProjectCheck: (project: Project, checked: boolean) => void;
+  project: Project;
+  selected: boolean;
+}
+
+export default class ProjectRow extends React.PureComponent<Props> {
+  handleProjectCheck = (checked: boolean) => {
+    this.props.onProjectCheck(this.props.project, checked);
+  };
+
+  handleApplyTemplateClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
+    event.preventDefault();
+    event.currentTarget.blur();
+    this.props.onApplyTemplateClick(this.props.project);
+  };
+
+  render() {
+    const { project, selected } = this.props;
+
+    return (
+      <tr>
+        <td className="thin">
+          <Checkbox checked={selected} onCheck={this.handleProjectCheck} />
+        </td>
+
+        <td className="nowrap">
+          <Link
+            to={{ pathname: '/dashboard', query: { id: project.key } }}
+            className="link-with-icon">
+            <QualifierIcon qualifier={project.qualifier} /> <span>{project.name}</span>
+          </Link>
+        </td>
+
+        <td className="nowrap">
+          <span className="note">
+            {project.key}
+          </span>
+        </td>
+
+        <td className="width-20">
+          {project.visibility === Visibility.Private && <PrivateBadge />}
+        </td>
+
+        <td className="thin nowrap">
+          <div className="dropdown">
+            <button className="dropdown-toggle" data-toggle="dropdown">
+              {translate('actions')} <i className="icon-dropdown" />
+            </button>
+            <ul className="dropdown-menu dropdown-menu-right">
+              <li>
+                <Link to={getComponentPermissionsUrl(project.key)}>
+                  {translate('edit_permissions')}
+                </Link>
+              </li>
+              <li>
+                <a className="js-apply-template" href="#" onClick={this.handleApplyTemplateClick}>
+                  {translate('projects_role.apply_template')}
+                </a>
+              </li>
+            </ul>
+          </div>
+        </td>
+      </tr>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/Projects.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/Projects.tsx
new file mode 100644 (file)
index 0000000..ff6264d
--- /dev/null
@@ -0,0 +1,68 @@
+/*
+ * 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 ProjectRow from './ProjectRow';
+import { Project } from './utils';
+import ApplyTemplateView from '../permissions/project/views/ApplyTemplateView';
+import { Organization } from '../../app/types';
+
+interface Props {
+  onProjectDeselected: (project: string) => void;
+  onProjectSelected: (project: string) => void;
+  organization: Organization;
+  projects: Project[];
+  ready?: boolean;
+  selection: string[];
+}
+
+export default class Projects extends React.PureComponent<Props> {
+  onProjectCheck = (project: Project, checked: boolean) => {
+    if (checked) {
+      this.props.onProjectSelected(project.key);
+    } else {
+      this.props.onProjectDeselected(project.key);
+    }
+  };
+
+  onApplyTemplateClick = (project: Project) => {
+    new ApplyTemplateView({ project, organization: this.props.organization }).render();
+  };
+
+  render() {
+    return (
+      <table
+        className={classNames('data', 'zebra', { 'new-loading': !this.props.ready })}
+        id="projects-management-page-projects">
+        <tbody>
+          {this.props.projects.map(project =>
+            <ProjectRow
+              key={project.key}
+              onApplyTemplateClick={this.onApplyTemplateClick}
+              onProjectCheck={this.onProjectCheck}
+              project={project}
+              selected={this.props.selection.includes(project.key)}
+            />
+          )}
+        </tbody>
+      </table>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/Search.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/Search.tsx
new file mode 100644 (file)
index 0000000..914883f
--- /dev/null
@@ -0,0 +1,228 @@
+/*
+ * 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 { sortBy } from 'lodash';
+import BulkApplyTemplateModal from './BulkApplyTemplateModal';
+import DeleteModal from './DeleteModal';
+import { Type, QUALIFIERS_ORDER } from './utils';
+import { Project } from './utils';
+import { Organization } from '../../app/types';
+import RadioToggle from '../../components/controls/RadioToggle';
+import Checkbox from '../../components/controls/Checkbox';
+import { translate } from '../../helpers/l10n';
+
+export interface Props {
+  onAllDeselected: () => void;
+  onAllSelected: () => void;
+  onDeleteProjects: () => void;
+  onQualifierChanged: (qualifier: string) => void;
+  onSearch: (query: string) => void;
+  onTypeChanged: (type: Type) => void;
+  organization: Organization;
+  projects: Project[];
+  qualifiers: string;
+  query: string;
+  ready: boolean;
+  selection: any[];
+  topLevelQualifiers: string[];
+  total: number;
+  type: Type;
+}
+
+interface State {
+  bulkApplyTemplateModal: boolean;
+  deleteModal: boolean;
+}
+
+export default class Search extends React.PureComponent<Props, State> {
+  input: HTMLInputElement;
+  mounted: boolean;
+  state: State = { bulkApplyTemplateModal: false, deleteModal: false };
+
+  onSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
+    event.preventDefault();
+    this.search();
+  };
+
+  search = (event?: React.SyntheticEvent<HTMLInputElement>) => {
+    const q = event ? event.currentTarget.value : this.input.value;
+    this.props.onSearch(q);
+  };
+
+  getTypeOptions = () => [
+    { value: Type.All, label: 'All' },
+    { value: Type.Provisioned, label: 'Provisioned' },
+    { value: Type.Ghosts, label: 'Ghosts' }
+  ];
+
+  getQualifierOptions = () => {
+    const options = this.props.topLevelQualifiers.map(q => {
+      return { value: q, label: translate('qualifiers', q) };
+    });
+    return sortBy(options, option => QUALIFIERS_ORDER.indexOf(option.value));
+  };
+
+  onCheck = (checked: boolean) => {
+    if (checked) {
+      this.props.onAllSelected();
+    } else {
+      this.props.onAllDeselected();
+    }
+  };
+
+  handleDeleteClick = (event: React.SyntheticEvent<HTMLButtonElement>) => {
+    event.preventDefault();
+    event.currentTarget.blur();
+    this.setState({ deleteModal: true });
+  };
+
+  closeDeleteModal = () => {
+    this.setState({ deleteModal: false });
+  };
+
+  handleDeleteConfirm = () => {
+    this.closeDeleteModal();
+    this.props.onDeleteProjects();
+  };
+
+  handleBulkApplyTemplateClick = (event: React.SyntheticEvent<HTMLButtonElement>) => {
+    event.preventDefault();
+    event.currentTarget.blur();
+    this.setState({ bulkApplyTemplateModal: true });
+  };
+
+  closeBulkApplyTemplateModal = () => {
+    this.setState({ bulkApplyTemplateModal: false });
+  };
+
+  renderCheckbox = () => {
+    const isAllChecked =
+      this.props.projects.length > 0 && this.props.selection.length === this.props.projects.length;
+    const thirdState =
+      this.props.projects.length > 0 &&
+      this.props.selection.length > 0 &&
+      this.props.selection.length < this.props.projects.length;
+    const checked = isAllChecked || thirdState;
+    return <Checkbox checked={checked} thirdState={thirdState} onCheck={this.onCheck} />;
+  };
+
+  renderGhostsDescription = () => {
+    if (this.props.type !== Type.Ghosts || !this.props.ready) {
+      return null;
+    }
+    return (
+      <div className="spacer-top alert alert-info">
+        {translate('bulk_deletion.ghosts.description')}
+      </div>
+    );
+  };
+
+  renderQualifierFilter = () => {
+    const options = this.getQualifierOptions();
+    if (options.length < 2) {
+      return null;
+    }
+    return (
+      <td className="thin nowrap text-middle">
+        <RadioToggle
+          options={this.getQualifierOptions()}
+          value={this.props.qualifiers}
+          name="projects-qualifier"
+          onCheck={this.props.onQualifierChanged}
+        />
+      </td>
+    );
+  };
+
+  render() {
+    const isSomethingSelected = this.props.projects.length > 0 && this.props.selection.length > 0;
+    return (
+      <div className="panel panel-vertical bordered-bottom spacer-bottom">
+        <table className="data">
+          <tbody>
+            <tr>
+              <td className="thin text-middle">
+                {this.props.ready ? this.renderCheckbox() : <i className="spinner" />}
+              </td>
+              {this.renderQualifierFilter()}
+              <td className="thin nowrap text-middle">
+                <RadioToggle
+                  options={this.getTypeOptions()}
+                  value={this.props.type}
+                  name="projects-type"
+                  onCheck={this.props.onTypeChanged}
+                />
+              </td>
+              <td className="text-middle">
+                <form onSubmit={this.onSubmit} className="search-box">
+                  <button className="search-box-submit button-clean">
+                    <i className="icon-search" />
+                  </button>
+                  <input
+                    onChange={this.search}
+                    value={this.props.query}
+                    ref={node => (this.input = node!)}
+                    className="search-box-input input-medium"
+                    type="search"
+                    placeholder="Search"
+                  />
+                </form>
+              </td>
+              <td className="thin nowrap text-middle">
+                <button
+                  className="spacer-right js-bulk-apply-permission-template"
+                  onClick={this.handleBulkApplyTemplateClick}>
+                  {translate('permission_templates.bulk_apply_permission_template')}
+                </button>
+                <button
+                  onClick={this.handleDeleteClick}
+                  className="js-delete button-red"
+                  disabled={!isSomethingSelected}>
+                  {translate('delete')}
+                </button>
+              </td>
+            </tr>
+          </tbody>
+        </table>
+        {this.renderGhostsDescription()}
+
+        {this.state.bulkApplyTemplateModal &&
+          <BulkApplyTemplateModal
+            onClose={this.closeBulkApplyTemplateModal}
+            organization={this.props.organization.key}
+            qualifier={this.props.qualifiers}
+            query={this.props.query}
+            selection={this.props.selection}
+            total={this.props.total}
+            type={this.props.type}
+          />}
+
+        {this.state.deleteModal &&
+          <DeleteModal
+            onClose={this.closeDeleteModal}
+            onConfirm={this.handleDeleteConfirm}
+            organization={this.props.organization.key}
+            qualifier={this.props.qualifiers}
+            selection={this.props.selection}
+          />}
+      </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/App-test.tsx
new file mode 100644 (file)
index 0000000..1c9569d
--- /dev/null
@@ -0,0 +1,148 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+jest.mock('lodash', () => {
+  const lodash = require.requireActual('lodash');
+  lodash.debounce = (fn: Function) => (...args: any[]) => fn(args);
+  return lodash;
+});
+
+jest.mock('../../../api/components', () => ({
+  getComponents: jest.fn(),
+  getProvisioned: jest.fn(() => Promise.resolve({ paging: { total: 0 }, projects: [] })),
+  getGhosts: jest.fn(() => Promise.resolve({ projects: [], total: 0 }))
+}));
+
+import * as React from 'react';
+import { mount } from 'enzyme';
+import App, { Props } from '../App';
+import { Type } from '../utils';
+
+const getComponents = require('../../../api/components').getComponents as jest.Mock<any>;
+const getProvisioned = require('../../../api/components').getProvisioned as jest.Mock<any>;
+const getGhosts = require('../../../api/components').getGhosts as jest.Mock<any>;
+
+const organization = { key: 'org', name: 'org', projectVisibility: 'public' };
+
+const defaultSearchParameters = {
+  organization: 'org',
+  p: undefined,
+  ps: 50,
+  q: undefined
+};
+
+beforeEach(() => {
+  getComponents
+    .mockImplementation(() => Promise.resolve({ paging: { total: 0 }, components: [] }))
+    .mockClear();
+  getProvisioned.mockClear();
+  getGhosts.mockClear();
+});
+
+it('fetches all projects on mount', () => {
+  mountRender();
+  expect(getComponents).lastCalledWith({ ...defaultSearchParameters, qualifiers: 'TRK' });
+});
+
+it('changes type', () => {
+  const wrapper = mountRender();
+  wrapper.find('Search').prop<Function>('onTypeChanged')(Type.Provisioned);
+  expect(getProvisioned).lastCalledWith(defaultSearchParameters);
+  wrapper.find('Search').prop<Function>('onTypeChanged')(Type.Ghosts);
+  expect(getGhosts).lastCalledWith(defaultSearchParameters);
+});
+
+it('changes qualifier and resets type', () => {
+  const wrapper = mountRender();
+  wrapper.setState({ type: Type.Provisioned });
+  wrapper.find('Search').prop<Function>('onQualifierChanged')('VW');
+  expect(getComponents).lastCalledWith({ ...defaultSearchParameters, qualifiers: 'VW' });
+});
+
+it('searches', () => {
+  const wrapper = mountRender();
+  wrapper.find('Search').prop<Function>('onSearch')('foo');
+  expect(getComponents).lastCalledWith({ ...defaultSearchParameters, q: 'foo', qualifiers: 'TRK' });
+});
+
+it('loads more', async () => {
+  const wrapper = mountRender();
+  wrapper.find('ListFooter').prop<Function>('loadMore')();
+  expect(getComponents).lastCalledWith({ ...defaultSearchParameters, p: 2, qualifiers: 'TRK' });
+});
+
+it('selects and deselects projects', async () => {
+  getComponents.mockImplementation(() =>
+    Promise.resolve({ paging: { total: 2 }, components: [{ key: 'foo' }, { key: 'bar' }] })
+  );
+  const wrapper = mountRender();
+  await new Promise(setImmediate);
+
+  wrapper.find('Projects').prop<Function>('onProjectSelected')('foo');
+  expect(wrapper.state('selection')).toEqual(['foo']);
+
+  wrapper.find('Projects').prop<Function>('onProjectSelected')('bar');
+  expect(wrapper.state('selection')).toEqual(['foo', 'bar']);
+
+  // should not select already selected project
+  wrapper.find('Projects').prop<Function>('onProjectSelected')('bar');
+  expect(wrapper.state('selection')).toEqual(['foo', 'bar']);
+
+  wrapper.find('Projects').prop<Function>('onProjectDeselected')('foo');
+  expect(wrapper.state('selection')).toEqual(['bar']);
+
+  wrapper.find('Search').prop<Function>('onAllDeselected')();
+  expect(wrapper.state('selection')).toEqual([]);
+
+  wrapper.find('Search').prop<Function>('onAllSelected')();
+  expect(wrapper.state('selection')).toEqual(['foo', 'bar']);
+});
+
+it('creates project', () => {
+  const wrapper = mountRender();
+  expect(wrapper.find('CreateProjectForm').exists()).toBeFalsy();
+
+  wrapper.find('Header').prop<Function>('onProjectCreate')();
+  expect(wrapper.find('CreateProjectForm').exists()).toBeTruthy();
+
+  wrapper.find('CreateProjectForm').prop<Function>('onProjectCreated')();
+  expect(getComponents.mock.calls).toHaveLength(2);
+
+  wrapper.find('CreateProjectForm').prop<Function>('onClose')();
+  expect(wrapper.find('CreateProjectForm').exists()).toBeFalsy();
+});
+
+it('changes default project visibility', () => {
+  const onVisibilityChange = jest.fn();
+  const wrapper = mountRender({ onVisibilityChange });
+  wrapper.find('Header').prop<Function>('onVisibilityChange')('private');
+  expect(onVisibilityChange).toBeCalledWith('private');
+});
+
+function mountRender(props?: { [P in keyof Props]?: Props[P] }) {
+  return mount(
+    <App
+      hasProvisionPermission={true}
+      onVisibilityChange={jest.fn()}
+      organization={organization}
+      topLevelQualifiers={['TRK', 'VW', 'APP']}
+      {...props}
+    />
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/BulkApplyTemplateModal-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/BulkApplyTemplateModal-test.tsx
new file mode 100644 (file)
index 0000000..1c8399e
--- /dev/null
@@ -0,0 +1,128 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+jest.mock('../../../api/permissions', () => ({
+  applyTemplateToProject: jest.fn(),
+  bulkApplyTemplate: jest.fn(),
+  getPermissionTemplates: jest.fn()
+}));
+
+import * as React from 'react';
+import { mount, shallow } from 'enzyme';
+import BulkApplyTemplateModal, { Props } from '../BulkApplyTemplateModal';
+import { Type } from '../utils';
+import { click } from '../../../helpers/testUtils';
+
+const applyTemplateToProject = require('../../../api/permissions')
+  .applyTemplateToProject as jest.Mock<any>;
+const bulkApplyTemplate = require('../../../api/permissions').bulkApplyTemplate as jest.Mock<any>;
+const getPermissionTemplates = require('../../../api/permissions')
+  .getPermissionTemplates as jest.Mock<any>;
+
+beforeEach(() => {
+  applyTemplateToProject.mockImplementation(() => Promise.resolve()).mockClear();
+  bulkApplyTemplate.mockImplementation(() => Promise.resolve()).mockClear();
+  getPermissionTemplates
+    .mockImplementation(() => Promise.resolve({ permissionTemplates: [] }))
+    .mockClear();
+});
+
+it('fetches permission templates on mount', () => {
+  mount(render());
+  expect(getPermissionTemplates).toBeCalledWith('org');
+});
+
+it('bulk applies template to all results', async () => {
+  const wrapper = shallow(render());
+  (wrapper.instance() as BulkApplyTemplateModal).mounted = true;
+  expect(wrapper).toMatchSnapshot();
+
+  wrapper.setState({
+    loading: false,
+    permissionTemplate: 'foo',
+    permissionTemplates: [{ id: 'foo', name: 'Foo' }, { id: 'bar', name: 'Bar' }]
+  });
+  expect(wrapper).toMatchSnapshot();
+
+  click(wrapper.find('button'));
+  expect(bulkApplyTemplate).toBeCalledWith({
+    organization: 'org',
+    q: 'bla',
+    qualifier: 'TRK',
+    templateId: 'foo'
+  });
+  expect(wrapper).toMatchSnapshot();
+
+  await new Promise(setImmediate);
+  wrapper.update();
+  expect(wrapper).toMatchSnapshot();
+});
+
+it('bulk applies template to selected results', async () => {
+  const wrapper = shallow(render({ selection: ['proj1', 'proj2'] }));
+  (wrapper.instance() as BulkApplyTemplateModal).mounted = true;
+  expect(wrapper).toMatchSnapshot();
+
+  wrapper.setState({
+    loading: false,
+    permissionTemplate: 'foo',
+    permissionTemplates: [{ id: 'foo', name: 'Foo' }, { id: 'bar', name: 'Bar' }]
+  });
+  expect(wrapper).toMatchSnapshot();
+
+  click(wrapper.find('button'));
+  expect(wrapper).toMatchSnapshot();
+  await new Promise(setImmediate);
+  expect(applyTemplateToProject.mock.calls).toHaveLength(2);
+  expect(applyTemplateToProject).toBeCalledWith({
+    organization: 'org',
+    projectKey: 'proj1',
+    templateId: 'foo'
+  });
+  expect(applyTemplateToProject).toBeCalledWith({
+    organization: 'org',
+    projectKey: 'proj2',
+    templateId: 'foo'
+  });
+
+  wrapper.update();
+  expect(wrapper).toMatchSnapshot();
+});
+
+it('closes', () => {
+  const onClose = jest.fn();
+  const wrapper = shallow(render({ onClose }));
+  click(wrapper.find('.js-modal-close'));
+  expect(onClose).toBeCalled();
+});
+
+function render(props?: { [P in keyof Props]?: Props[P] }) {
+  return (
+    <BulkApplyTemplateModal
+      onClose={jest.fn()}
+      organization="org"
+      qualifier="TRK"
+      query="bla"
+      selection={[]}
+      total={17}
+      type={Type.All}
+      {...props}
+    />
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ChangeVisibilityForm-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ChangeVisibilityForm-test.tsx
new file mode 100644 (file)
index 0000000..61dce6d
--- /dev/null
@@ -0,0 +1,73 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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 ChangeVisibilityForm, { Props } from '../ChangeVisibilityForm';
+import { click } from '../../../helpers/testUtils';
+
+const organization = {
+  canUpdateProjectsVisibilityToPrivate: true,
+  key: 'org',
+  name: 'org',
+  projectVisibility: 'public'
+};
+
+it('renders disabled', () => {
+  expect(
+    shallowRender({
+      organization: { ...organization, canUpdateProjectsVisibilityToPrivate: false }
+    })
+  ).toMatchSnapshot();
+});
+
+it('closes', () => {
+  const onClose = jest.fn();
+  const wrapper = shallowRender({ onClose });
+  click(wrapper.find('.js-modal-close'));
+  expect(onClose).toBeCalled();
+});
+
+it('changes visibility', () => {
+  const onConfirm = jest.fn();
+  const wrapper = shallowRender({ onConfirm });
+  expect(wrapper).toMatchSnapshot();
+
+  click(wrapper.find('a[data-visibility="private"]'), {
+    currentTarget: {
+      blur() {},
+      dataset: { visibility: 'private' }
+    }
+  });
+  expect(wrapper).toMatchSnapshot();
+
+  click(wrapper.find('.js-confirm'));
+  expect(onConfirm).toBeCalledWith('private');
+});
+
+function shallowRender(props?: { [P in keyof Props]?: Props[P] }) {
+  return shallow(
+    <ChangeVisibilityForm
+      onClose={jest.fn()}
+      onConfirm={jest.fn()}
+      organization={organization}
+      {...props}
+    />
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/CreateProjectForm-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/CreateProjectForm-test.tsx
new file mode 100644 (file)
index 0000000..0212d09
--- /dev/null
@@ -0,0 +1,72 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+jest.mock('../../../api/components', () => ({
+  createProject: jest.fn(({ name }: { name: string }) =>
+    Promise.resolve({ project: { key: name, name } })
+  )
+}));
+
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import CreateProjectForm from '../CreateProjectForm';
+import { change, submit } from '../../../helpers/testUtils';
+
+const createProject = require('../../../api/components').createProject as jest.Mock<any>;
+
+const organization = { key: 'org', name: 'org', projectVisibility: 'public' };
+
+it('creates project', async () => {
+  const wrapper = shallow(
+    <CreateProjectForm
+      onClose={jest.fn()}
+      onProjectCreated={jest.fn()}
+      organization={organization}
+    />
+  );
+  (wrapper.instance() as CreateProjectForm).mounted = true;
+  expect(wrapper).toMatchSnapshot();
+
+  change(wrapper.find('input[name="name"]'), 'name', {
+    currentTarget: { name: 'name', value: 'name' }
+  });
+  change(wrapper.find('input[name="branch"]'), 'branch', {
+    currentTarget: { name: 'branch', value: 'branch' }
+  });
+  change(wrapper.find('input[name="key"]'), 'key', {
+    currentTarget: { name: 'key', value: 'key' }
+  });
+  wrapper.find('VisibilitySelector').prop<Function>('onChange')('private');
+  wrapper.update();
+  expect(wrapper).toMatchSnapshot();
+
+  submit(wrapper.find('form'));
+  expect(createProject).toBeCalledWith({
+    branch: 'branch',
+    name: 'name',
+    organization: 'org',
+    project: 'key',
+    visibility: 'private'
+  });
+  expect(wrapper).toMatchSnapshot();
+
+  await new Promise(resolve => setImmediate(resolve));
+  wrapper.update();
+  expect(wrapper).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/DeleteModal-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/DeleteModal-test.tsx
new file mode 100644 (file)
index 0000000..d24f30e
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+jest.mock('../../../api/components', () => ({
+  deleteComponents: jest.fn(() => Promise.resolve())
+}));
+
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import DeleteModal, { Props } from '../DeleteModal';
+import { click } from '../../../helpers/testUtils';
+
+const deleteComponents = require('../../../api/components').deleteComponents as jest.Mock<any>;
+
+it('deletes projects', async () => {
+  const onConfirm = jest.fn();
+  const wrapper = shallowRender({ onConfirm });
+  (wrapper.instance() as DeleteModal).mounted = true;
+  expect(wrapper).toMatchSnapshot();
+
+  click(wrapper.find('button'));
+  expect(wrapper).toMatchSnapshot();
+  expect(deleteComponents).toBeCalledWith(['foo', 'bar'], 'org');
+
+  await new Promise(setImmediate);
+  expect(onConfirm).toBeCalled();
+});
+
+it('closes', () => {
+  const onClose = jest.fn();
+  const wrapper = shallowRender({ onClose });
+  click(wrapper.find('.js-modal-close'));
+  expect(onClose).toBeCalled();
+});
+
+function shallowRender(props?: { [P in keyof Props]?: Props[P] }) {
+  return shallow(
+    <DeleteModal
+      onClose={jest.fn()}
+      onConfirm={jest.fn()}
+      organization="org"
+      qualifier="TRK"
+      selection={['foo', 'bar']}
+      {...props}
+    />
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Header-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Header-test.tsx
new file mode 100644 (file)
index 0000000..726c6b1
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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 Header, { Props } from '../Header';
+import { Visibility } from '../utils';
+import { click } from '../../../helpers/testUtils';
+
+const organization = { key: 'org', name: 'org', projectVisibility: 'public' };
+
+it('renders', () => {
+  expect(shallowRender()).toMatchSnapshot();
+});
+
+it('creates project', () => {
+  const onProjectCreate = jest.fn();
+  const wrapper = shallowRender({ onProjectCreate });
+  click(wrapper.find('#create-project'));
+  expect(onProjectCreate).toBeCalledWith();
+});
+
+it('changes default visibility', () => {
+  const onVisibilityChange = jest.fn();
+  const wrapper = shallowRender({ onVisibilityChange });
+
+  click(wrapper.find('.js-change-visibility'));
+
+  const modalWrapper = wrapper.find('ChangeVisibilityForm');
+  expect(modalWrapper).toMatchSnapshot();
+  modalWrapper.prop<Function>('onConfirm')(Visibility.Private);
+  expect(onVisibilityChange).toBeCalledWith(Visibility.Private);
+
+  modalWrapper.prop<Function>('onClose')();
+  expect(wrapper.find('ChangeVisibilityForm').exists()).toBeFalsy();
+});
+
+function shallowRender(props?: { [P in keyof Props]?: Props[P] }) {
+  return shallow(
+    <Header
+      hasProvisionPermission={true}
+      onProjectCreate={jest.fn()}
+      onVisibilityChange={jest.fn()}
+      organization={organization}
+      {...props}
+    />
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRow-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRow-test.tsx
new file mode 100644 (file)
index 0000000..7b7bb09
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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 ProjectRow from '../ProjectRow';
+import { Visibility } from '../utils';
+import { click } from '../../../helpers/testUtils';
+
+const project = {
+  key: 'project',
+  name: 'Project',
+  qualifier: 'TRK',
+  visibility: Visibility.Private
+};
+
+it('renders', () => {
+  expect(shallowRender()).toMatchSnapshot();
+});
+
+it('checks project', () => {
+  const onProjectCheck = jest.fn();
+  const wrapper = shallowRender({ onProjectCheck });
+  wrapper.find('Checkbox').prop<Function>('onCheck')(false);
+  expect(onProjectCheck).toBeCalledWith(project, false);
+});
+
+it('applies permission template', () => {
+  const onApplyTemplateClick = jest.fn();
+  const wrapper = shallowRender({ onApplyTemplateClick });
+  click(wrapper.find('.js-apply-template'));
+  expect(onApplyTemplateClick).toBeCalledWith(project);
+});
+
+function shallowRender(props?: any) {
+  return shallow(
+    <ProjectRow
+      onApplyTemplateClick={jest.fn()}
+      onProjectCheck={jest.fn()}
+      project={project}
+      selected={true}
+      {...props}
+    />
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Projects-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Projects-test.tsx
new file mode 100644 (file)
index 0000000..b1f165c
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+ * 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.
+ */
+jest.mock('../../permissions/project/views/ApplyTemplateView');
+
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import Projects from '../Projects';
+import { Visibility } from '../utils';
+import ApplyTemplateView from '../../permissions/project/views/ApplyTemplateView';
+
+const organization = { key: 'org', name: 'org', projectVisibility: 'public' };
+const projects = [
+  { key: 'a', name: 'A', qualifier: 'TRK', visibility: Visibility.Public },
+  { key: 'b', name: 'B', qualifier: 'TRK', visibility: Visibility.Public }
+];
+const selection = ['a'];
+
+it('renders list of projects', () => {
+  expect(shallowRender({ projects, selection })).toMatchSnapshot();
+});
+
+it('selects and deselects project', () => {
+  const onProjectDeselected = jest.fn();
+  const onProjectSelected = jest.fn();
+  const wrapper = shallowRender({ onProjectDeselected, onProjectSelected, projects });
+
+  wrapper.find('ProjectRow').first().prop<Function>('onProjectCheck')(projects[0], true);
+  expect(onProjectSelected).toBeCalledWith('a');
+
+  wrapper.find('ProjectRow').first().prop<Function>('onProjectCheck')(projects[0], false);
+  expect(onProjectDeselected).toBeCalledWith('a');
+});
+
+it('opens modal to apply permission template', () => {
+  const wrapper = shallowRender({ projects });
+  wrapper.find('ProjectRow').first().prop<Function>('onApplyTemplateClick')(projects[0]);
+  expect(ApplyTemplateView).toBeCalledWith({ organization, project: projects[0] });
+});
+
+function shallowRender(props?: any) {
+  return shallow(
+    <Projects
+      onProjectDeselected={jest.fn()}
+      onProjectSelected={jest.fn()}
+      organization={organization}
+      selection={[]}
+      {...props}
+    />
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Search-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Search-test.tsx
new file mode 100644 (file)
index 0000000..950b78e
--- /dev/null
@@ -0,0 +1,107 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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 Search, { Props } from '../Search';
+import { Type } from '../utils';
+import { change, click } from '../../../helpers/testUtils';
+
+const organization = { key: 'org', name: 'org', projectVisibility: 'public' };
+
+it('renders', () => {
+  expect(shallowRender()).toMatchSnapshot();
+});
+
+it('render qualifiers filter', () => {
+  expect(shallowRender({ topLevelQualifiers: ['TRK', 'VW', 'APP'] })).toMatchSnapshot();
+});
+
+it('updates qualifier', () => {
+  const onQualifierChanged = jest.fn();
+  const wrapper = shallowRender({ onQualifierChanged, topLevelQualifiers: ['TRK', 'VW', 'APP'] });
+  wrapper.find('RadioToggle[name="projects-qualifier"]').prop<Function>('onCheck')('VW');
+  expect(onQualifierChanged).toBeCalledWith('VW');
+});
+
+it('updates type', () => {
+  const onTypeChanged = jest.fn();
+  const wrapper = shallowRender({ onTypeChanged });
+  wrapper.find('RadioToggle[name="projects-type"]').prop<Function>('onCheck')(Type.Provisioned);
+  expect(onTypeChanged).toBeCalledWith(Type.Provisioned);
+});
+
+it('searches', () => {
+  const onSearch = jest.fn();
+  const wrapper = shallowRender({ onSearch });
+  change(wrapper.find('input[type="search"]'), 'foo');
+  expect(onSearch).toBeCalledWith('foo');
+});
+
+it('checks all or none projects', () => {
+  const onAllDeselected = jest.fn();
+  const onAllSelected = jest.fn();
+  const wrapper = shallowRender({ onAllDeselected, onAllSelected });
+
+  wrapper.find('Checkbox').prop<Function>('onCheck')(true);
+  expect(onAllSelected).toBeCalled();
+
+  wrapper.find('Checkbox').prop<Function>('onCheck')(false);
+  expect(onAllDeselected).toBeCalled();
+});
+
+it('deletes projects', () => {
+  const onDeleteProjects = jest.fn();
+  const wrapper = shallowRender({ onDeleteProjects, selection: ['foo', 'bar'] });
+  click(wrapper.find('.js-delete'));
+  expect(wrapper.find('DeleteModal')).toMatchSnapshot();
+  wrapper.find('DeleteModal').prop<Function>('onConfirm')();
+  expect(onDeleteProjects).toBeCalled();
+});
+
+it('bulk applies permission template', () => {
+  const wrapper = shallowRender({});
+  click(wrapper.find('.js-bulk-apply-permission-template'));
+  expect(wrapper.find('BulkApplyTemplateModal')).toMatchSnapshot();
+  wrapper.find('BulkApplyTemplateModal').prop<Function>('onClose')();
+  expect(wrapper.find('BulkApplyTemplateModal').exists()).toBeFalsy();
+});
+
+function shallowRender(props?: { [P in keyof Props]?: Props[P] }) {
+  return shallow(
+    <Search
+      onAllDeselected={jest.fn()}
+      onAllSelected={jest.fn()}
+      onDeleteProjects={jest.fn()}
+      onQualifierChanged={jest.fn()}
+      onSearch={jest.fn()}
+      onTypeChanged={jest.fn()}
+      organization={organization}
+      projects={[]}
+      qualifiers="TRK"
+      query=""
+      ready={true}
+      selection={[]}
+      topLevelQualifiers={['TRK']}
+      total={0}
+      type={Type.All}
+      {...props}
+    />
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/BulkApplyTemplateModal-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/BulkApplyTemplateModal-test.tsx.snap
new file mode 100644 (file)
index 0000000..bc3e39d
--- /dev/null
@@ -0,0 +1,643 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`bulk applies template to all results 1`] = `
+<Modal
+  ariaHideApp={true}
+  bodyOpenClassName="ReactModal__Body--open"
+  className="modal"
+  closeTimeoutMS={0}
+  contentLabel="permission_templates.bulk_apply_permission_template"
+  isOpen={true}
+  onRequestClose={[Function]}
+  overlayClassName="modal-overlay"
+  parentSelector={[Function]}
+  portalClassName="ReactModalPortal"
+  shouldCloseOnOverlayClick={true}
+>
+  <header
+    className="modal-head"
+  >
+    <h2>
+      permission_templates.bulk_apply_permission_template
+    </h2>
+  </header>
+  <div
+    className="modal-body"
+  >
+    <i
+      className="spinner"
+    />
+  </div>
+  <footer
+    className="modal-foot"
+  >
+    <a
+      className="js-modal-close"
+      href="#"
+      onClick={[Function]}
+    >
+      cancel
+    </a>
+  </footer>
+</Modal>
+`;
+
+exports[`bulk applies template to all results 2`] = `
+<Modal
+  ariaHideApp={true}
+  bodyOpenClassName="ReactModal__Body--open"
+  className="modal"
+  closeTimeoutMS={0}
+  contentLabel="permission_templates.bulk_apply_permission_template"
+  isOpen={true}
+  onRequestClose={[Function]}
+  overlayClassName="modal-overlay"
+  parentSelector={[Function]}
+  portalClassName="ReactModalPortal"
+  shouldCloseOnOverlayClick={true}
+>
+  <header
+    className="modal-head"
+  >
+    <h2>
+      permission_templates.bulk_apply_permission_template
+    </h2>
+  </header>
+  <div
+    className="modal-body"
+  >
+    <div
+      className="alert alert-warning"
+    >
+      permission_templates.bulk_apply_permission_template.apply_to_all.17
+    </div>
+    <div
+      className="modal-field"
+    >
+      <label>
+        template
+        <em
+          className="mandatory"
+        >
+          *
+        </em>
+      </label>
+      <Select
+        addLabelText="Add \\"{label}\\"?"
+        arrowRenderer={[Function]}
+        autosize={true}
+        backspaceRemoves={true}
+        backspaceToRemoveMessage="Press backspace to remove {label}"
+        clearAllText="Clear all"
+        clearRenderer={[Function]}
+        clearValueText="Clear value"
+        clearable={false}
+        deleteRemoves={true}
+        delimiter=","
+        disabled={false}
+        escapeClearsValue={true}
+        filterOptions={[Function]}
+        ignoreAccents={true}
+        ignoreCase={true}
+        inputProps={Object {}}
+        isLoading={false}
+        joinValues={false}
+        labelKey="label"
+        matchPos="any"
+        matchProp="any"
+        menuBuffer={0}
+        menuRenderer={[Function]}
+        multi={false}
+        noResultsText="No results found"
+        onBlurResetsInput={true}
+        onChange={[Function]}
+        onCloseResetsInput={true}
+        optionComponent={[Function]}
+        options={
+          Array [
+            Object {
+              "label": "Foo",
+              "value": "foo",
+            },
+            Object {
+              "label": "Bar",
+              "value": "bar",
+            },
+          ]
+        }
+        pageSize={5}
+        placeholder="Select..."
+        required={false}
+        scrollMenuIntoView={true}
+        searchable={false}
+        simpleValue={false}
+        tabSelectsValue={true}
+        value="foo"
+        valueComponent={[Function]}
+        valueKey="value"
+      />
+    </div>
+  </div>
+  <footer
+    className="modal-foot"
+  >
+    <button
+      disabled={false}
+      onClick={[Function]}
+    >
+      apply
+    </button>
+    <a
+      className="js-modal-close"
+      href="#"
+      onClick={[Function]}
+    >
+      cancel
+    </a>
+  </footer>
+</Modal>
+`;
+
+exports[`bulk applies template to all results 3`] = `
+<Modal
+  ariaHideApp={true}
+  bodyOpenClassName="ReactModal__Body--open"
+  className="modal"
+  closeTimeoutMS={0}
+  contentLabel="permission_templates.bulk_apply_permission_template"
+  isOpen={true}
+  onRequestClose={[Function]}
+  overlayClassName="modal-overlay"
+  parentSelector={[Function]}
+  portalClassName="ReactModalPortal"
+  shouldCloseOnOverlayClick={true}
+>
+  <header
+    className="modal-head"
+  >
+    <h2>
+      permission_templates.bulk_apply_permission_template
+    </h2>
+  </header>
+  <div
+    className="modal-body"
+  >
+    <div
+      className="alert alert-warning"
+    >
+      permission_templates.bulk_apply_permission_template.apply_to_all.17
+    </div>
+    <div
+      className="modal-field"
+    >
+      <label>
+        template
+        <em
+          className="mandatory"
+        >
+          *
+        </em>
+      </label>
+      <Select
+        addLabelText="Add \\"{label}\\"?"
+        arrowRenderer={[Function]}
+        autosize={true}
+        backspaceRemoves={true}
+        backspaceToRemoveMessage="Press backspace to remove {label}"
+        clearAllText="Clear all"
+        clearRenderer={[Function]}
+        clearValueText="Clear value"
+        clearable={false}
+        deleteRemoves={true}
+        delimiter=","
+        disabled={true}
+        escapeClearsValue={true}
+        filterOptions={[Function]}
+        ignoreAccents={true}
+        ignoreCase={true}
+        inputProps={Object {}}
+        isLoading={false}
+        joinValues={false}
+        labelKey="label"
+        matchPos="any"
+        matchProp="any"
+        menuBuffer={0}
+        menuRenderer={[Function]}
+        multi={false}
+        noResultsText="No results found"
+        onBlurResetsInput={true}
+        onChange={[Function]}
+        onCloseResetsInput={true}
+        optionComponent={[Function]}
+        options={
+          Array [
+            Object {
+              "label": "Foo",
+              "value": "foo",
+            },
+            Object {
+              "label": "Bar",
+              "value": "bar",
+            },
+          ]
+        }
+        pageSize={5}
+        placeholder="Select..."
+        required={false}
+        scrollMenuIntoView={true}
+        searchable={false}
+        simpleValue={false}
+        tabSelectsValue={true}
+        value="foo"
+        valueComponent={[Function]}
+        valueKey="value"
+      />
+    </div>
+  </div>
+  <footer
+    className="modal-foot"
+  >
+    <i
+      className="spinner spacer-right"
+    />
+    <button
+      disabled={true}
+      onClick={[Function]}
+    >
+      apply
+    </button>
+    <a
+      className="js-modal-close"
+      href="#"
+      onClick={[Function]}
+    >
+      cancel
+    </a>
+  </footer>
+</Modal>
+`;
+
+exports[`bulk applies template to all results 4`] = `
+<Modal
+  ariaHideApp={true}
+  bodyOpenClassName="ReactModal__Body--open"
+  className="modal"
+  closeTimeoutMS={0}
+  contentLabel="permission_templates.bulk_apply_permission_template"
+  isOpen={true}
+  onRequestClose={[Function]}
+  overlayClassName="modal-overlay"
+  parentSelector={[Function]}
+  portalClassName="ReactModalPortal"
+  shouldCloseOnOverlayClick={true}
+>
+  <header
+    className="modal-head"
+  >
+    <h2>
+      permission_templates.bulk_apply_permission_template
+    </h2>
+  </header>
+  <div
+    className="modal-body"
+  >
+    <div
+      className="alert alert-success"
+    >
+      projects_role.apply_template.success
+    </div>
+  </div>
+  <footer
+    className="modal-foot"
+  >
+    <a
+      className="js-modal-close"
+      href="#"
+      onClick={[Function]}
+    >
+      close
+    </a>
+  </footer>
+</Modal>
+`;
+
+exports[`bulk applies template to selected results 1`] = `
+<Modal
+  ariaHideApp={true}
+  bodyOpenClassName="ReactModal__Body--open"
+  className="modal"
+  closeTimeoutMS={0}
+  contentLabel="permission_templates.bulk_apply_permission_template"
+  isOpen={true}
+  onRequestClose={[Function]}
+  overlayClassName="modal-overlay"
+  parentSelector={[Function]}
+  portalClassName="ReactModalPortal"
+  shouldCloseOnOverlayClick={true}
+>
+  <header
+    className="modal-head"
+  >
+    <h2>
+      permission_templates.bulk_apply_permission_template
+    </h2>
+  </header>
+  <div
+    className="modal-body"
+  >
+    <i
+      className="spinner"
+    />
+  </div>
+  <footer
+    className="modal-foot"
+  >
+    <a
+      className="js-modal-close"
+      href="#"
+      onClick={[Function]}
+    >
+      cancel
+    </a>
+  </footer>
+</Modal>
+`;
+
+exports[`bulk applies template to selected results 2`] = `
+<Modal
+  ariaHideApp={true}
+  bodyOpenClassName="ReactModal__Body--open"
+  className="modal"
+  closeTimeoutMS={0}
+  contentLabel="permission_templates.bulk_apply_permission_template"
+  isOpen={true}
+  onRequestClose={[Function]}
+  overlayClassName="modal-overlay"
+  parentSelector={[Function]}
+  portalClassName="ReactModalPortal"
+  shouldCloseOnOverlayClick={true}
+>
+  <header
+    className="modal-head"
+  >
+    <h2>
+      permission_templates.bulk_apply_permission_template
+    </h2>
+  </header>
+  <div
+    className="modal-body"
+  >
+    <div
+      className="alert alert-info"
+    >
+      permission_templates.bulk_apply_permission_template.apply_to_selected.2
+    </div>
+    <div
+      className="modal-field"
+    >
+      <label>
+        template
+        <em
+          className="mandatory"
+        >
+          *
+        </em>
+      </label>
+      <Select
+        addLabelText="Add \\"{label}\\"?"
+        arrowRenderer={[Function]}
+        autosize={true}
+        backspaceRemoves={true}
+        backspaceToRemoveMessage="Press backspace to remove {label}"
+        clearAllText="Clear all"
+        clearRenderer={[Function]}
+        clearValueText="Clear value"
+        clearable={false}
+        deleteRemoves={true}
+        delimiter=","
+        disabled={false}
+        escapeClearsValue={true}
+        filterOptions={[Function]}
+        ignoreAccents={true}
+        ignoreCase={true}
+        inputProps={Object {}}
+        isLoading={false}
+        joinValues={false}
+        labelKey="label"
+        matchPos="any"
+        matchProp="any"
+        menuBuffer={0}
+        menuRenderer={[Function]}
+        multi={false}
+        noResultsText="No results found"
+        onBlurResetsInput={true}
+        onChange={[Function]}
+        onCloseResetsInput={true}
+        optionComponent={[Function]}
+        options={
+          Array [
+            Object {
+              "label": "Foo",
+              "value": "foo",
+            },
+            Object {
+              "label": "Bar",
+              "value": "bar",
+            },
+          ]
+        }
+        pageSize={5}
+        placeholder="Select..."
+        required={false}
+        scrollMenuIntoView={true}
+        searchable={false}
+        simpleValue={false}
+        tabSelectsValue={true}
+        value="foo"
+        valueComponent={[Function]}
+        valueKey="value"
+      />
+    </div>
+  </div>
+  <footer
+    className="modal-foot"
+  >
+    <button
+      disabled={false}
+      onClick={[Function]}
+    >
+      apply
+    </button>
+    <a
+      className="js-modal-close"
+      href="#"
+      onClick={[Function]}
+    >
+      cancel
+    </a>
+  </footer>
+</Modal>
+`;
+
+exports[`bulk applies template to selected results 3`] = `
+<Modal
+  ariaHideApp={true}
+  bodyOpenClassName="ReactModal__Body--open"
+  className="modal"
+  closeTimeoutMS={0}
+  contentLabel="permission_templates.bulk_apply_permission_template"
+  isOpen={true}
+  onRequestClose={[Function]}
+  overlayClassName="modal-overlay"
+  parentSelector={[Function]}
+  portalClassName="ReactModalPortal"
+  shouldCloseOnOverlayClick={true}
+>
+  <header
+    className="modal-head"
+  >
+    <h2>
+      permission_templates.bulk_apply_permission_template
+    </h2>
+  </header>
+  <div
+    className="modal-body"
+  >
+    <div
+      className="alert alert-info"
+    >
+      permission_templates.bulk_apply_permission_template.apply_to_selected.2
+    </div>
+    <div
+      className="modal-field"
+    >
+      <label>
+        template
+        <em
+          className="mandatory"
+        >
+          *
+        </em>
+      </label>
+      <Select
+        addLabelText="Add \\"{label}\\"?"
+        arrowRenderer={[Function]}
+        autosize={true}
+        backspaceRemoves={true}
+        backspaceToRemoveMessage="Press backspace to remove {label}"
+        clearAllText="Clear all"
+        clearRenderer={[Function]}
+        clearValueText="Clear value"
+        clearable={false}
+        deleteRemoves={true}
+        delimiter=","
+        disabled={true}
+        escapeClearsValue={true}
+        filterOptions={[Function]}
+        ignoreAccents={true}
+        ignoreCase={true}
+        inputProps={Object {}}
+        isLoading={false}
+        joinValues={false}
+        labelKey="label"
+        matchPos="any"
+        matchProp="any"
+        menuBuffer={0}
+        menuRenderer={[Function]}
+        multi={false}
+        noResultsText="No results found"
+        onBlurResetsInput={true}
+        onChange={[Function]}
+        onCloseResetsInput={true}
+        optionComponent={[Function]}
+        options={
+          Array [
+            Object {
+              "label": "Foo",
+              "value": "foo",
+            },
+            Object {
+              "label": "Bar",
+              "value": "bar",
+            },
+          ]
+        }
+        pageSize={5}
+        placeholder="Select..."
+        required={false}
+        scrollMenuIntoView={true}
+        searchable={false}
+        simpleValue={false}
+        tabSelectsValue={true}
+        value="foo"
+        valueComponent={[Function]}
+        valueKey="value"
+      />
+    </div>
+  </div>
+  <footer
+    className="modal-foot"
+  >
+    <i
+      className="spinner spacer-right"
+    />
+    <button
+      disabled={true}
+      onClick={[Function]}
+    >
+      apply
+    </button>
+    <a
+      className="js-modal-close"
+      href="#"
+      onClick={[Function]}
+    >
+      cancel
+    </a>
+  </footer>
+</Modal>
+`;
+
+exports[`bulk applies template to selected results 4`] = `
+<Modal
+  ariaHideApp={true}
+  bodyOpenClassName="ReactModal__Body--open"
+  className="modal"
+  closeTimeoutMS={0}
+  contentLabel="permission_templates.bulk_apply_permission_template"
+  isOpen={true}
+  onRequestClose={[Function]}
+  overlayClassName="modal-overlay"
+  parentSelector={[Function]}
+  portalClassName="ReactModalPortal"
+  shouldCloseOnOverlayClick={true}
+>
+  <header
+    className="modal-head"
+  >
+    <h2>
+      permission_templates.bulk_apply_permission_template
+    </h2>
+  </header>
+  <div
+    className="modal-body"
+  >
+    <div
+      className="alert alert-success"
+    >
+      projects_role.apply_template.success
+    </div>
+  </div>
+  <footer
+    className="modal-foot"
+  >
+    <a
+      className="js-modal-close"
+      href="#"
+      onClick={[Function]}
+    >
+      close
+    </a>
+  </footer>
+</Modal>
+`;
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ChangeVisibilityForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ChangeVisibilityForm-test.tsx.snap
new file mode 100644 (file)
index 0000000..d46e88f
--- /dev/null
@@ -0,0 +1,308 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`changes visibility 1`] = `
+<Modal
+  ariaHideApp={true}
+  bodyOpenClassName="ReactModal__Body--open"
+  className="modal"
+  closeTimeoutMS={0}
+  contentLabel="modal form"
+  isOpen={true}
+  onRequestClose={[Function]}
+  overlayClassName="modal-overlay"
+  parentSelector={[Function]}
+  portalClassName="ReactModalPortal"
+  shouldCloseOnOverlayClick={true}
+>
+  <header
+    className="modal-head"
+  >
+    <h2>
+      organization.change_visibility_form.header
+    </h2>
+  </header>
+  <div
+    className="modal-body"
+  >
+    <div
+      className="big-spacer-bottom"
+    >
+      <p>
+        <a
+          className="link-base-color link-no-underline"
+          data-visibility="public"
+          href="#"
+          onClick={[Function]}
+        >
+          <i
+            className="icon-radio spacer-right is-checked"
+          />
+          visibility.public
+        </a>
+      </p>
+      <p
+        className="text-muted spacer-top"
+        style={
+          Object {
+            "paddingLeft": 22,
+          }
+        }
+      >
+        visibility.public.description.short
+      </p>
+    </div>
+    <div
+      className="big-spacer-bottom"
+    >
+      <p>
+        <a
+          className="link-base-color link-no-underline"
+          data-visibility="private"
+          href="#"
+          onClick={[Function]}
+        >
+          <i
+            className="icon-radio spacer-right"
+          />
+          visibility.private
+        </a>
+      </p>
+      <p
+        className="text-muted spacer-top"
+        style={
+          Object {
+            "paddingLeft": 22,
+          }
+        }
+      >
+        visibility.private.description.short
+      </p>
+    </div>
+    <div
+      className="alert alert-warning"
+    >
+      organization.change_visibility_form.warning
+    </div>
+  </div>
+  <footer
+    className="modal-foot"
+  >
+    <button
+      className="js-confirm"
+      onClick={[Function]}
+    >
+      organization.change_visibility_form.submit
+    </button>
+    <a
+      className="js-modal-close"
+      href="#"
+      onClick={[Function]}
+    >
+      cancel
+    </a>
+  </footer>
+</Modal>
+`;
+
+exports[`changes visibility 2`] = `
+<Modal
+  ariaHideApp={true}
+  bodyOpenClassName="ReactModal__Body--open"
+  className="modal"
+  closeTimeoutMS={0}
+  contentLabel="modal form"
+  isOpen={true}
+  onRequestClose={[Function]}
+  overlayClassName="modal-overlay"
+  parentSelector={[Function]}
+  portalClassName="ReactModalPortal"
+  shouldCloseOnOverlayClick={true}
+>
+  <header
+    className="modal-head"
+  >
+    <h2>
+      organization.change_visibility_form.header
+    </h2>
+  </header>
+  <div
+    className="modal-body"
+  >
+    <div
+      className="big-spacer-bottom"
+    >
+      <p>
+        <a
+          className="link-base-color link-no-underline"
+          data-visibility="public"
+          href="#"
+          onClick={[Function]}
+        >
+          <i
+            className="icon-radio spacer-right"
+          />
+          visibility.public
+        </a>
+      </p>
+      <p
+        className="text-muted spacer-top"
+        style={
+          Object {
+            "paddingLeft": 22,
+          }
+        }
+      >
+        visibility.public.description.short
+      </p>
+    </div>
+    <div
+      className="big-spacer-bottom"
+    >
+      <p>
+        <a
+          className="link-base-color link-no-underline"
+          data-visibility="private"
+          href="#"
+          onClick={[Function]}
+        >
+          <i
+            className="icon-radio spacer-right is-checked"
+          />
+          visibility.private
+        </a>
+      </p>
+      <p
+        className="text-muted spacer-top"
+        style={
+          Object {
+            "paddingLeft": 22,
+          }
+        }
+      >
+        visibility.private.description.short
+      </p>
+    </div>
+    <div
+      className="alert alert-warning"
+    >
+      organization.change_visibility_form.warning
+    </div>
+  </div>
+  <footer
+    className="modal-foot"
+  >
+    <button
+      className="js-confirm"
+      onClick={[Function]}
+    >
+      organization.change_visibility_form.submit
+    </button>
+    <a
+      className="js-modal-close"
+      href="#"
+      onClick={[Function]}
+    >
+      cancel
+    </a>
+  </footer>
+</Modal>
+`;
+
+exports[`renders disabled 1`] = `
+<Modal
+  ariaHideApp={true}
+  bodyOpenClassName="ReactModal__Body--open"
+  className="modal"
+  closeTimeoutMS={0}
+  contentLabel="modal form"
+  isOpen={true}
+  onRequestClose={[Function]}
+  overlayClassName="modal-overlay"
+  parentSelector={[Function]}
+  portalClassName="ReactModalPortal"
+  shouldCloseOnOverlayClick={true}
+>
+  <header
+    className="modal-head"
+  >
+    <h2>
+      organization.change_visibility_form.header
+    </h2>
+  </header>
+  <div
+    className="modal-body"
+  >
+    <div
+      className="big-spacer-bottom"
+    >
+      <p>
+        <a
+          className="link-base-color link-no-underline"
+          data-visibility="public"
+          href="#"
+          onClick={[Function]}
+        >
+          <i
+            className="icon-radio spacer-right is-checked"
+          />
+          visibility.public
+        </a>
+      </p>
+      <p
+        className="text-muted spacer-top"
+        style={
+          Object {
+            "paddingLeft": 22,
+          }
+        }
+      >
+        visibility.public.description.short
+      </p>
+    </div>
+    <div
+      className="big-spacer-bottom"
+    >
+      <p>
+        <span
+          className="text-muted cursor-not-allowed"
+        >
+          <i
+            className="icon-radio spacer-right"
+          />
+          visibility.private
+        </span>
+      </p>
+      <p
+        className="text-muted spacer-top"
+        style={
+          Object {
+            "paddingLeft": 22,
+          }
+        }
+      >
+        visibility.private.description.short
+      </p>
+    </div>
+    <UpgradeOrganizationBox
+      organization="org"
+    />
+  </div>
+  <footer
+    className="modal-foot"
+  >
+    <button
+      className="js-confirm"
+      onClick={[Function]}
+    >
+      organization.change_visibility_form.submit
+    </button>
+    <a
+      className="js-modal-close"
+      href="#"
+      onClick={[Function]}
+    >
+      cancel
+    </a>
+  </footer>
+</Modal>
+`;
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/CreateProjectForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/CreateProjectForm-test.tsx.snap
new file mode 100644 (file)
index 0000000..fad1933
--- /dev/null
@@ -0,0 +1,468 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`creates project 1`] = `
+<Modal
+  ariaHideApp={true}
+  bodyOpenClassName="ReactModal__Body--open"
+  className="modal"
+  closeTimeoutMS={0}
+  contentLabel="modal form"
+  isOpen={true}
+  onRequestClose={[Function]}
+  overlayClassName="modal-overlay"
+  parentSelector={[Function]}
+  portalClassName="ReactModalPortal"
+  shouldCloseOnOverlayClick={true}
+>
+  <form
+    id="create-project-form"
+    onSubmit={[Function]}
+  >
+    <header
+      className="modal-head"
+    >
+      <h2>
+        qualifiers.create.TRK
+      </h2>
+    </header>
+    <div
+      className="modal-body"
+    >
+      <div
+        className="modal-field"
+      >
+        <label
+          htmlFor="create-project-name"
+        >
+          name
+          <em
+            className="mandatory"
+          >
+            *
+          </em>
+        </label>
+        <input
+          autoFocus={true}
+          id="create-project-name"
+          maxLength={2000}
+          name="name"
+          onChange={[Function]}
+          required={true}
+          type="text"
+          value=""
+        />
+      </div>
+      <div
+        className="modal-field"
+      >
+        <label
+          htmlFor="create-project-branch"
+        >
+          branch
+        </label>
+        <input
+          id="create-project-branch"
+          maxLength={200}
+          name="branch"
+          onChange={[Function]}
+          type="text"
+          value=""
+        />
+      </div>
+      <div
+        className="modal-field"
+      >
+        <label
+          htmlFor="create-project-key"
+        >
+          key
+          <em
+            className="mandatory"
+          >
+            *
+          </em>
+        </label>
+        <input
+          id="create-project-key"
+          maxLength={400}
+          name="key"
+          onChange={[Function]}
+          required={true}
+          type="text"
+          value=""
+        />
+      </div>
+      <div
+        className="modal-field"
+      >
+        <label>
+          visibility
+        </label>
+        <VisibilitySelector
+          className="little-spacer-top"
+          onChange={[Function]}
+          visibility="public"
+        />
+        <div
+          className="spacer-top"
+        >
+          <UpgradeOrganizationBox
+            organization="org"
+          />
+        </div>
+      </div>
+    </div>
+    <footer
+      className="modal-foot"
+    >
+      <button
+        disabled={false}
+        id="create-project-submit"
+        type="submit"
+      >
+        create
+      </button>
+      <a
+        href="#"
+        id="create-project-cancel"
+        onClick={[Function]}
+      >
+        cancel
+      </a>
+    </footer>
+  </form>
+</Modal>
+`;
+
+exports[`creates project 2`] = `
+<Modal
+  ariaHideApp={true}
+  bodyOpenClassName="ReactModal__Body--open"
+  className="modal"
+  closeTimeoutMS={0}
+  contentLabel="modal form"
+  isOpen={true}
+  onRequestClose={[Function]}
+  overlayClassName="modal-overlay"
+  parentSelector={[Function]}
+  portalClassName="ReactModalPortal"
+  shouldCloseOnOverlayClick={true}
+>
+  <form
+    id="create-project-form"
+    onSubmit={[Function]}
+  >
+    <header
+      className="modal-head"
+    >
+      <h2>
+        qualifiers.create.TRK
+      </h2>
+    </header>
+    <div
+      className="modal-body"
+    >
+      <div
+        className="modal-field"
+      >
+        <label
+          htmlFor="create-project-name"
+        >
+          name
+          <em
+            className="mandatory"
+          >
+            *
+          </em>
+        </label>
+        <input
+          autoFocus={true}
+          id="create-project-name"
+          maxLength={2000}
+          name="name"
+          onChange={[Function]}
+          required={true}
+          type="text"
+          value="name"
+        />
+      </div>
+      <div
+        className="modal-field"
+      >
+        <label
+          htmlFor="create-project-branch"
+        >
+          branch
+        </label>
+        <input
+          id="create-project-branch"
+          maxLength={200}
+          name="branch"
+          onChange={[Function]}
+          type="text"
+          value="branch"
+        />
+      </div>
+      <div
+        className="modal-field"
+      >
+        <label
+          htmlFor="create-project-key"
+        >
+          key
+          <em
+            className="mandatory"
+          >
+            *
+          </em>
+        </label>
+        <input
+          id="create-project-key"
+          maxLength={400}
+          name="key"
+          onChange={[Function]}
+          required={true}
+          type="text"
+          value="key"
+        />
+      </div>
+      <div
+        className="modal-field"
+      >
+        <label>
+          visibility
+        </label>
+        <VisibilitySelector
+          className="little-spacer-top"
+          onChange={[Function]}
+          visibility="private"
+        />
+        <div
+          className="spacer-top"
+        >
+          <UpgradeOrganizationBox
+            organization="org"
+          />
+        </div>
+      </div>
+    </div>
+    <footer
+      className="modal-foot"
+    >
+      <button
+        disabled={false}
+        id="create-project-submit"
+        type="submit"
+      >
+        create
+      </button>
+      <a
+        href="#"
+        id="create-project-cancel"
+        onClick={[Function]}
+      >
+        cancel
+      </a>
+    </footer>
+  </form>
+</Modal>
+`;
+
+exports[`creates project 3`] = `
+<Modal
+  ariaHideApp={true}
+  bodyOpenClassName="ReactModal__Body--open"
+  className="modal"
+  closeTimeoutMS={0}
+  contentLabel="modal form"
+  isOpen={true}
+  onRequestClose={[Function]}
+  overlayClassName="modal-overlay"
+  parentSelector={[Function]}
+  portalClassName="ReactModalPortal"
+  shouldCloseOnOverlayClick={true}
+>
+  <form
+    id="create-project-form"
+    onSubmit={[Function]}
+  >
+    <header
+      className="modal-head"
+    >
+      <h2>
+        qualifiers.create.TRK
+      </h2>
+    </header>
+    <div
+      className="modal-body"
+    >
+      <div
+        className="modal-field"
+      >
+        <label
+          htmlFor="create-project-name"
+        >
+          name
+          <em
+            className="mandatory"
+          >
+            *
+          </em>
+        </label>
+        <input
+          autoFocus={true}
+          id="create-project-name"
+          maxLength={2000}
+          name="name"
+          onChange={[Function]}
+          required={true}
+          type="text"
+          value="name"
+        />
+      </div>
+      <div
+        className="modal-field"
+      >
+        <label
+          htmlFor="create-project-branch"
+        >
+          branch
+        </label>
+        <input
+          id="create-project-branch"
+          maxLength={200}
+          name="branch"
+          onChange={[Function]}
+          type="text"
+          value="branch"
+        />
+      </div>
+      <div
+        className="modal-field"
+      >
+        <label
+          htmlFor="create-project-key"
+        >
+          key
+          <em
+            className="mandatory"
+          >
+            *
+          </em>
+        </label>
+        <input
+          id="create-project-key"
+          maxLength={400}
+          name="key"
+          onChange={[Function]}
+          required={true}
+          type="text"
+          value="key"
+        />
+      </div>
+      <div
+        className="modal-field"
+      >
+        <label>
+          visibility
+        </label>
+        <VisibilitySelector
+          className="little-spacer-top"
+          onChange={[Function]}
+          visibility="private"
+        />
+        <div
+          className="spacer-top"
+        >
+          <UpgradeOrganizationBox
+            organization="org"
+          />
+        </div>
+      </div>
+    </div>
+    <footer
+      className="modal-foot"
+    >
+      <i
+        className="spinner spacer-right"
+      />
+      <button
+        disabled={true}
+        id="create-project-submit"
+        type="submit"
+      >
+        create
+      </button>
+      <a
+        href="#"
+        id="create-project-cancel"
+        onClick={[Function]}
+      >
+        cancel
+      </a>
+    </footer>
+  </form>
+</Modal>
+`;
+
+exports[`creates project 4`] = `
+<Modal
+  ariaHideApp={true}
+  bodyOpenClassName="ReactModal__Body--open"
+  className="modal"
+  closeTimeoutMS={0}
+  contentLabel="modal form"
+  isOpen={true}
+  onRequestClose={[Function]}
+  overlayClassName="modal-overlay"
+  parentSelector={[Function]}
+  portalClassName="ReactModalPortal"
+  shouldCloseOnOverlayClick={true}
+>
+  <div>
+    <header
+      className="modal-head"
+    >
+      <h2>
+        qualifiers.create.TRK
+      </h2>
+    </header>
+    <div
+      className="modal-body"
+    >
+      <div
+        className="alert alert-success"
+      >
+        Project 
+        <Link
+          onlyActiveOnIndex={false}
+          style={Object {}}
+          to={
+            Object {
+              "pathname": "/dashboard",
+              "query": Object {
+                "id": "name",
+              },
+            }
+          }
+        >
+          name
+        </Link>
+         
+        has been successfully created.
+      </div>
+    </div>
+    <footer
+      className="modal-foot"
+    >
+      <a
+        href="#"
+        id="create-project-close"
+        onClick={[Function]}
+      >
+        close
+      </a>
+    </footer>
+  </div>
+</Modal>
+`;
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/DeleteModal-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/DeleteModal-test.tsx.snap
new file mode 100644 (file)
index 0000000..e57788f
--- /dev/null
@@ -0,0 +1,98 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`deletes projects 1`] = `
+<Modal
+  ariaHideApp={true}
+  bodyOpenClassName="ReactModal__Body--open"
+  className="modal"
+  closeTimeoutMS={0}
+  contentLabel="qualifiers.delete.TRK"
+  isOpen={true}
+  onRequestClose={[Function]}
+  overlayClassName="modal-overlay"
+  parentSelector={[Function]}
+  portalClassName="ReactModalPortal"
+  shouldCloseOnOverlayClick={true}
+>
+  <header
+    className="modal-head"
+  >
+    <h2>
+      qualifiers.delete.TRK
+    </h2>
+  </header>
+  <div
+    className="modal-body"
+  >
+    qualifiers.delete_confirm.TRK
+  </div>
+  <footer
+    className="modal-foot"
+  >
+    <button
+      className="button-red"
+      disabled={false}
+      onClick={[Function]}
+    >
+      delete
+    </button>
+    <a
+      className="js-modal-close"
+      href="#"
+      onClick={[Function]}
+    >
+      cancel
+    </a>
+  </footer>
+</Modal>
+`;
+
+exports[`deletes projects 2`] = `
+<Modal
+  ariaHideApp={true}
+  bodyOpenClassName="ReactModal__Body--open"
+  className="modal"
+  closeTimeoutMS={0}
+  contentLabel="qualifiers.delete.TRK"
+  isOpen={true}
+  onRequestClose={[Function]}
+  overlayClassName="modal-overlay"
+  parentSelector={[Function]}
+  portalClassName="ReactModalPortal"
+  shouldCloseOnOverlayClick={true}
+>
+  <header
+    className="modal-head"
+  >
+    <h2>
+      qualifiers.delete.TRK
+    </h2>
+  </header>
+  <div
+    className="modal-body"
+  >
+    qualifiers.delete_confirm.TRK
+  </div>
+  <footer
+    className="modal-foot"
+  >
+    <i
+      className="spinner spacer-right"
+    />
+    <button
+      className="button-red"
+      disabled={true}
+      onClick={[Function]}
+    >
+      delete
+    </button>
+    <a
+      className="js-modal-close"
+      href="#"
+      onClick={[Function]}
+    >
+      cancel
+    </a>
+  </footer>
+</Modal>
+`;
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Header-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Header-test.tsx.snap
new file mode 100644 (file)
index 0000000..9fb16cf
--- /dev/null
@@ -0,0 +1,56 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`changes default visibility 1`] = `
+<ChangeVisibilityForm
+  onClose={[Function]}
+  onConfirm={[Function]}
+  organization={
+    Object {
+      "key": "org",
+      "name": "org",
+      "projectVisibility": "public",
+    }
+  }
+/>
+`;
+
+exports[`renders 1`] = `
+<header
+  className="page-header"
+>
+  <h1
+    className="page-title"
+  >
+    projects_management
+  </h1>
+  <div
+    className="page-actions"
+  >
+    <span
+      className="big-spacer-right"
+    >
+      organization.default_visibility_of_new_projects
+       
+      <strong>
+        visibility.public
+      </strong>
+      <a
+        className="js-change-visibility spacer-left icon-edit"
+        href="#"
+        onClick={[Function]}
+      />
+    </span>
+    <button
+      id="create-project"
+      onClick={[Function]}
+    >
+      qualifiers.create.TRK
+    </button>
+  </div>
+  <p
+    className="page-description"
+  >
+    projects_management.page.description
+  </p>
+</header>
+`;
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRow-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRow-test.tsx.snap
new file mode 100644 (file)
index 0000000..b306b2f
--- /dev/null
@@ -0,0 +1,101 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders 1`] = `
+<tr>
+  <td
+    className="thin"
+  >
+    <Checkbox
+      checked={true}
+      onCheck={[Function]}
+      thirdState={false}
+    />
+  </td>
+  <td
+    className="nowrap"
+  >
+    <Link
+      className="link-with-icon"
+      onlyActiveOnIndex={false}
+      style={Object {}}
+      to={
+        Object {
+          "pathname": "/dashboard",
+          "query": Object {
+            "id": "project",
+          },
+        }
+      }
+    >
+      <QualifierIcon
+        qualifier="TRK"
+      />
+       
+      <span>
+        Project
+      </span>
+    </Link>
+  </td>
+  <td
+    className="nowrap"
+  >
+    <span
+      className="note"
+    >
+      project
+    </span>
+  </td>
+  <td
+    className="width-20"
+  >
+    <PrivateBadge />
+  </td>
+  <td
+    className="thin nowrap"
+  >
+    <div
+      className="dropdown"
+    >
+      <button
+        className="dropdown-toggle"
+        data-toggle="dropdown"
+      >
+        actions
+         
+        <i
+          className="icon-dropdown"
+        />
+      </button>
+      <ul
+        className="dropdown-menu dropdown-menu-right"
+      >
+        <li>
+          <Link
+            onlyActiveOnIndex={false}
+            style={Object {}}
+            to={
+              Object {
+                "pathname": "/project_roles",
+                "query": Object {
+                  "id": "project",
+                },
+              }
+            }
+          >
+            edit_permissions
+          </Link>
+        </li>
+        <li>
+          <a
+            className="js-apply-template"
+            href="#"
+            onClick={[Function]}
+          >
+            projects_role.apply_template
+          </a>
+        </li>
+      </ul>
+    </div>
+  </td>
+</tr>
+`;
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Projects-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Projects-test.tsx.snap
new file mode 100644 (file)
index 0000000..14bb03d
--- /dev/null
@@ -0,0 +1,37 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders list of projects 1`] = `
+<table
+  className="data zebra new-loading"
+  id="projects-management-page-projects"
+>
+  <tbody>
+    <ProjectRow
+      onApplyTemplateClick={[Function]}
+      onProjectCheck={[Function]}
+      project={
+        Object {
+          "key": "a",
+          "name": "A",
+          "qualifier": "TRK",
+          "visibility": "public",
+        }
+      }
+      selected={true}
+    />
+    <ProjectRow
+      onApplyTemplateClick={[Function]}
+      onProjectCheck={[Function]}
+      project={
+        Object {
+          "key": "b",
+          "name": "B",
+          "qualifier": "TRK",
+          "visibility": "public",
+        }
+      }
+      selected={false}
+    />
+  </tbody>
+</table>
+`;
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Search-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Search-test.tsx.snap
new file mode 100644 (file)
index 0000000..84838c7
--- /dev/null
@@ -0,0 +1,234 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`bulk applies permission template 1`] = `
+<BulkApplyTemplateModal
+  onClose={[Function]}
+  organization="org"
+  qualifier="TRK"
+  query=""
+  selection={Array []}
+  total={0}
+  type="ALL"
+/>
+`;
+
+exports[`deletes projects 1`] = `
+<DeleteModal
+  onClose={[Function]}
+  onConfirm={[Function]}
+  organization="org"
+  qualifier="TRK"
+  selection={
+    Array [
+      "foo",
+      "bar",
+    ]
+  }
+/>
+`;
+
+exports[`render qualifiers filter 1`] = `
+<div
+  className="panel panel-vertical bordered-bottom spacer-bottom"
+>
+  <table
+    className="data"
+  >
+    <tbody>
+      <tr>
+        <td
+          className="thin text-middle"
+        >
+          <Checkbox
+            checked={false}
+            onCheck={[Function]}
+            thirdState={false}
+          />
+        </td>
+        <td
+          className="thin nowrap text-middle"
+        >
+          <RadioToggle
+            disabled={false}
+            name="projects-qualifier"
+            onCheck={[Function]}
+            options={
+              Array [
+                Object {
+                  "label": "qualifiers.TRK",
+                  "value": "TRK",
+                },
+                Object {
+                  "label": "qualifiers.VW",
+                  "value": "VW",
+                },
+                Object {
+                  "label": "qualifiers.APP",
+                  "value": "APP",
+                },
+              ]
+            }
+            value="TRK"
+          />
+        </td>
+        <td
+          className="thin nowrap text-middle"
+        >
+          <RadioToggle
+            disabled={false}
+            name="projects-type"
+            onCheck={[Function]}
+            options={
+              Array [
+                Object {
+                  "label": "All",
+                  "value": "ALL",
+                },
+                Object {
+                  "label": "Provisioned",
+                  "value": "PROVISIONED",
+                },
+                Object {
+                  "label": "Ghosts",
+                  "value": "GHOSTS",
+                },
+              ]
+            }
+            value="ALL"
+          />
+        </td>
+        <td
+          className="text-middle"
+        >
+          <form
+            className="search-box"
+            onSubmit={[Function]}
+          >
+            <button
+              className="search-box-submit button-clean"
+            >
+              <i
+                className="icon-search"
+              />
+            </button>
+            <input
+              className="search-box-input input-medium"
+              onChange={[Function]}
+              placeholder="Search"
+              type="search"
+              value=""
+            />
+          </form>
+        </td>
+        <td
+          className="thin nowrap text-middle"
+        >
+          <button
+            className="spacer-right js-bulk-apply-permission-template"
+            onClick={[Function]}
+          >
+            permission_templates.bulk_apply_permission_template
+          </button>
+          <button
+            className="js-delete button-red"
+            disabled={true}
+            onClick={[Function]}
+          >
+            delete
+          </button>
+        </td>
+      </tr>
+    </tbody>
+  </table>
+</div>
+`;
+
+exports[`renders 1`] = `
+<div
+  className="panel panel-vertical bordered-bottom spacer-bottom"
+>
+  <table
+    className="data"
+  >
+    <tbody>
+      <tr>
+        <td
+          className="thin text-middle"
+        >
+          <Checkbox
+            checked={false}
+            onCheck={[Function]}
+            thirdState={false}
+          />
+        </td>
+        <td
+          className="thin nowrap text-middle"
+        >
+          <RadioToggle
+            disabled={false}
+            name="projects-type"
+            onCheck={[Function]}
+            options={
+              Array [
+                Object {
+                  "label": "All",
+                  "value": "ALL",
+                },
+                Object {
+                  "label": "Provisioned",
+                  "value": "PROVISIONED",
+                },
+                Object {
+                  "label": "Ghosts",
+                  "value": "GHOSTS",
+                },
+              ]
+            }
+            value="ALL"
+          />
+        </td>
+        <td
+          className="text-middle"
+        >
+          <form
+            className="search-box"
+            onSubmit={[Function]}
+          >
+            <button
+              className="search-box-submit button-clean"
+            >
+              <i
+                className="icon-search"
+              />
+            </button>
+            <input
+              className="search-box-input input-medium"
+              onChange={[Function]}
+              placeholder="Search"
+              type="search"
+              value=""
+            />
+          </form>
+        </td>
+        <td
+          className="thin nowrap text-middle"
+        >
+          <button
+            className="spacer-right js-bulk-apply-permission-template"
+            onClick={[Function]}
+          >
+            permission_templates.bulk_apply_permission_template
+          </button>
+          <button
+            className="js-delete button-red"
+            disabled={true}
+            onClick={[Function]}
+          >
+            delete
+          </button>
+        </td>
+      </tr>
+    </tbody>
+  </table>
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/routes.ts b/server/sonar-web/src/main/js/apps/projectsManagement/routes.ts
new file mode 100644 (file)
index 0000000..447c6ae
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * 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 { RouterState, IndexRouteProps } from 'react-router';
+
+const routes = [
+  {
+    getIndexRoute(_: RouterState, callback: (err: any, route: IndexRouteProps) => any) {
+      Promise.all([
+        import('./AppContainer').then(i => i.default),
+        import('../organizations/forSingleOrganization').then(i => i.default)
+      ]).then(([AppContainer, forSingleOrganization]) =>
+        callback(null, { component: forSingleOrganization(AppContainer) })
+      );
+    }
+  }
+];
+
+export default routes;
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/utils.ts b/server/sonar-web/src/main/js/apps/projectsManagement/utils.ts
new file mode 100644 (file)
index 0000000..4e3f018
--- /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.
+ */
+export const PAGE_SIZE = 50;
+
+export const QUALIFIERS_ORDER = ['TRK', 'VW', 'APP', 'DEV'];
+
+export enum Type {
+  All = 'ALL',
+  Provisioned = 'PROVISIONED',
+  Ghosts = 'GHOSTS'
+}
+
+export interface Project {
+  key: string;
+  name: string;
+  qualifier: string;
+  visibility: Visibility;
+}
+
+export enum Visibility {
+  Public = 'public',
+  Private = 'private'
+}
diff --git a/server/sonar-web/src/main/js/components/common/PrivateBadge.js b/server/sonar-web/src/main/js/components/common/PrivateBadge.js
deleted file mode 100644 (file)
index 0336cf7..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 classNames from 'classnames';
-import Tooltip from '../controls/Tooltip';
-import { translate } from '../../helpers/l10n';
-
-/*::
-type Props = {
-  className?: string,
-  tooltipPlacement?: string
-};
-*/
-
-export default function PrivateBadge({ className, tooltipPlacement = 'bottom' } /*: Props */) {
-  return (
-    <Tooltip overlay={translate('visibility.private.description')} placement={tooltipPlacement}>
-      <div className={classNames('outline-badge', className)}>
-        {translate('visibility.private')}
-      </div>
-    </Tooltip>
-  );
-}
diff --git a/server/sonar-web/src/main/js/components/common/PrivateBadge.tsx b/server/sonar-web/src/main/js/components/common/PrivateBadge.tsx
new file mode 100644 (file)
index 0000000..d360d21
--- /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 * as classNames from 'classnames';
+import Tooltip from '../controls/Tooltip';
+import { translate } from '../../helpers/l10n';
+
+interface Props {
+  className?: string;
+  tooltipPlacement?: string;
+}
+
+export default function PrivateBadge({ className, tooltipPlacement = 'bottom' }: Props) {
+  return (
+    <Tooltip overlay={translate('visibility.private.description')} placement={tooltipPlacement}>
+      <div className={classNames('outline-badge', className)}>
+        {translate('visibility.private')}
+      </div>
+    </Tooltip>
+  );
+}
diff --git a/server/sonar-web/src/main/js/components/common/VisibilitySelector.js b/server/sonar-web/src/main/js/components/common/VisibilitySelector.js
deleted file mode 100644 (file)
index 4023309..0000000
+++ /dev/null
@@ -1,97 +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 classNames from 'classnames';
-import { translate } from '../../helpers/l10n';
-
-/*::
-type Props = {|
-  canTurnToPrivate: boolean,
-  className?: string,
-  onChange: string => void,
-  visibility: string
-|};
-*/
-
-export default class VisibilitySelector extends React.PureComponent {
-  /*:: props: Props; */
-
-  handlePublicClick = (event /*: Event & { currentTarget: HTMLElement } */) => {
-    event.preventDefault();
-    event.currentTarget.blur();
-    this.props.onChange('public');
-  };
-
-  handlePrivateClick = (event /*: Event & { currentTarget: HTMLElement } */) => {
-    event.preventDefault();
-    event.currentTarget.blur();
-    this.props.onChange('private');
-  };
-
-  render() {
-    return (
-      <div className={this.props.className}>
-        <a
-          className="link-base-color link-no-underline"
-          id="visibility-public"
-          href="#"
-          onClick={this.handlePublicClick}>
-          <i
-            className={classNames('icon-radio', {
-              'is-checked': this.props.visibility === 'public'
-            })}
-          />
-          <span className="spacer-left">
-            {translate('visibility.public')}
-          </span>
-        </a>
-
-        {this.props.canTurnToPrivate
-          ? <a
-              className="link-base-color link-no-underline huge-spacer-left"
-              id="visibility-private"
-              href="#"
-              onClick={this.handlePrivateClick}>
-              <i
-                className={classNames('icon-radio', {
-                  'is-checked': this.props.visibility === 'private'
-                })}
-              />
-              <span className="spacer-left">
-                {translate('visibility.private')}
-              </span>
-            </a>
-          : <span
-              className="huge-spacer-left text-muted cursor-not-allowed"
-              id="visibility-private">
-              <i
-                className={classNames('icon-radio', {
-                  'is-checked': this.props.visibility === 'private'
-                })}
-              />
-              <span className="spacer-left">
-                {translate('visibility.private')}
-              </span>
-            </span>}
-      </div>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/components/common/VisibilitySelector.tsx b/server/sonar-web/src/main/js/components/common/VisibilitySelector.tsx
new file mode 100644 (file)
index 0000000..4e3642d
--- /dev/null
@@ -0,0 +1,92 @@
+/*
+ * 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 { translate } from '../../helpers/l10n';
+
+interface Props {
+  canTurnToPrivate?: boolean;
+  className?: string;
+  onChange: (x: string) => void;
+  visibility: string;
+}
+
+export default class VisibilitySelector extends React.PureComponent<Props> {
+  handlePublicClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
+    event.preventDefault();
+    event.currentTarget.blur();
+    this.props.onChange('public');
+  };
+
+  handlePrivateClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
+    event.preventDefault();
+    event.currentTarget.blur();
+    this.props.onChange('private');
+  };
+
+  render() {
+    return (
+      <div className={this.props.className}>
+        <a
+          className="link-base-color link-no-underline"
+          id="visibility-public"
+          href="#"
+          onClick={this.handlePublicClick}>
+          <i
+            className={classNames('icon-radio', {
+              'is-checked': this.props.visibility === 'public'
+            })}
+          />
+          <span className="spacer-left">
+            {translate('visibility.public')}
+          </span>
+        </a>
+
+        {this.props.canTurnToPrivate
+          ? <a
+              className="link-base-color link-no-underline huge-spacer-left"
+              id="visibility-private"
+              href="#"
+              onClick={this.handlePrivateClick}>
+              <i
+                className={classNames('icon-radio', {
+                  'is-checked': this.props.visibility === 'private'
+                })}
+              />
+              <span className="spacer-left">
+                {translate('visibility.private')}
+              </span>
+            </a>
+          : <span
+              className="huge-spacer-left text-muted cursor-not-allowed"
+              id="visibility-private">
+              <i
+                className={classNames('icon-radio', {
+                  'is-checked': this.props.visibility === 'private'
+                })}
+              />
+              <span className="spacer-left">
+                {translate('visibility.private')}
+              </span>
+            </span>}
+      </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/components/common/__tests__/PrivateBadge-test.tsx b/server/sonar-web/src/main/js/components/common/__tests__/PrivateBadge-test.tsx
new file mode 100644 (file)
index 0000000..02bdfca
--- /dev/null
@@ -0,0 +1,26 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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 PrivateBadge from '../PrivateBadge';
+
+it('renders', () => {
+  expect(shallow(<PrivateBadge />)).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/components/common/__tests__/VisibilitySelector-test.tsx b/server/sonar-web/src/main/js/components/common/__tests__/VisibilitySelector-test.tsx
new file mode 100644 (file)
index 0000000..477df57
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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 VisibilitySelector from '../VisibilitySelector';
+import { click } from '../../../helpers/testUtils';
+
+it('changes visibility', () => {
+  const onChange = jest.fn();
+  const wrapper = shallow(
+    <VisibilitySelector canTurnToPrivate={true} onChange={onChange} visibility="public" />
+  );
+  expect(wrapper).toMatchSnapshot();
+
+  click(wrapper.find('#visibility-private'));
+  expect(onChange).toBeCalledWith('private');
+
+  wrapper.setProps({ visibility: 'private' });
+  expect(wrapper).toMatchSnapshot();
+
+  click(wrapper.find('#visibility-public'));
+  expect(onChange).toBeCalledWith('public');
+});
+
+it('renders disabled', () => {
+  expect(
+    shallow(
+      <VisibilitySelector canTurnToPrivate={false} onChange={jest.fn()} visibility="public" />
+    )
+  ).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/PrivateBadge-test.tsx.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/PrivateBadge-test.tsx.snap
new file mode 100644 (file)
index 0000000..d8f24f2
--- /dev/null
@@ -0,0 +1,14 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders 1`] = `
+<Tooltip
+  overlay="visibility.private.description"
+  placement="bottom"
+>
+  <div
+    className="outline-badge"
+  >
+    visibility.private
+  </div>
+</Tooltip>
+`;
diff --git a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/VisibilitySelector-test.tsx.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/VisibilitySelector-test.tsx.snap
new file mode 100644 (file)
index 0000000..745a71b
--- /dev/null
@@ -0,0 +1,104 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`changes visibility 1`] = `
+<div>
+  <a
+    className="link-base-color link-no-underline"
+    href="#"
+    id="visibility-public"
+    onClick={[Function]}
+  >
+    <i
+      className="icon-radio is-checked"
+    />
+    <span
+      className="spacer-left"
+    >
+      visibility.public
+    </span>
+  </a>
+  <a
+    className="link-base-color link-no-underline huge-spacer-left"
+    href="#"
+    id="visibility-private"
+    onClick={[Function]}
+  >
+    <i
+      className="icon-radio"
+    />
+    <span
+      className="spacer-left"
+    >
+      visibility.private
+    </span>
+  </a>
+</div>
+`;
+
+exports[`changes visibility 2`] = `
+<div>
+  <a
+    className="link-base-color link-no-underline"
+    href="#"
+    id="visibility-public"
+    onClick={[Function]}
+  >
+    <i
+      className="icon-radio"
+    />
+    <span
+      className="spacer-left"
+    >
+      visibility.public
+    </span>
+  </a>
+  <a
+    className="link-base-color link-no-underline huge-spacer-left"
+    href="#"
+    id="visibility-private"
+    onClick={[Function]}
+  >
+    <i
+      className="icon-radio is-checked"
+    />
+    <span
+      className="spacer-left"
+    >
+      visibility.private
+    </span>
+  </a>
+</div>
+`;
+
+exports[`renders disabled 1`] = `
+<div>
+  <a
+    className="link-base-color link-no-underline"
+    href="#"
+    id="visibility-public"
+    onClick={[Function]}
+  >
+    <i
+      className="icon-radio is-checked"
+    />
+    <span
+      className="spacer-left"
+    >
+      visibility.public
+    </span>
+  </a>
+  <span
+    className="huge-spacer-left text-muted cursor-not-allowed"
+    id="visibility-private"
+  >
+    <i
+      className="icon-radio"
+    />
+    <span
+      className="spacer-left"
+    >
+      visibility.private
+    </span>
+  </span>
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/components/controls/RadioToggle.js b/server/sonar-web/src/main/js/components/controls/RadioToggle.js
deleted file mode 100644 (file)
index 2041eba..0000000
+++ /dev/null
@@ -1,79 +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';
-
-export default class RadioToggle extends React.PureComponent {
-  static propTypes = {
-    value: PropTypes.string,
-    options: PropTypes.arrayOf(
-      PropTypes.shape({
-        value: PropTypes.string.isRequired,
-        label: PropTypes.string.isRequired
-      })
-    ).isRequired,
-    name: PropTypes.string.isRequired,
-    onCheck: PropTypes.func.isRequired
-  };
-
-  static defaultProps = {
-    disabled: false,
-    value: null
-  };
-
-  componentWillMount() {
-    this.renderOption = this.renderOption.bind(this);
-    this.handleChange = this.handleChange.bind(this);
-  }
-
-  handleChange(e) {
-    const newValue = e.currentTarget.value;
-    this.props.onCheck(newValue);
-  }
-
-  renderOption(option) {
-    const checked = option.value === this.props.value;
-    const htmlId = this.props.name + '__' + option.value;
-    return (
-      <li key={option.value}>
-        <input
-          type="radio"
-          name={this.props.name}
-          value={option.value}
-          id={htmlId}
-          checked={checked}
-          onChange={this.handleChange}
-        />
-
-        <label htmlFor={htmlId}>
-          {option.label}
-        </label>
-      </li>
-    );
-  }
-
-  render() {
-    return (
-      <ul className="radio-toggle">
-        {this.props.options.map(this.renderOption)}
-      </ul>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/components/controls/RadioToggle.tsx b/server/sonar-web/src/main/js/components/controls/RadioToggle.tsx
new file mode 100644 (file)
index 0000000..fd27b37
--- /dev/null
@@ -0,0 +1,68 @@
+/*
+ * 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';
+
+interface Props {
+  name: string;
+  onCheck: (value: string) => void;
+  options: Array<{ label: string; value: string }>;
+  value?: string;
+}
+
+export default class RadioToggle extends React.PureComponent<Props> {
+  static defaultProps = {
+    disabled: false,
+    value: null
+  };
+
+  handleChange = (e: React.SyntheticEvent<HTMLInputElement>) => {
+    const newValue = e.currentTarget.value;
+    this.props.onCheck(newValue);
+  };
+
+  renderOption = (option: { label: string; value: string }) => {
+    const checked = option.value === this.props.value;
+    const htmlId = this.props.name + '__' + option.value;
+    return (
+      <li key={option.value}>
+        <input
+          type="radio"
+          name={this.props.name}
+          value={option.value}
+          id={htmlId}
+          checked={checked}
+          onChange={this.handleChange}
+        />
+
+        <label htmlFor={htmlId}>
+          {option.label}
+        </label>
+      </li>
+    );
+  };
+
+  render() {
+    return (
+      <ul className="radio-toggle">
+        {this.props.options.map(this.renderOption)}
+      </ul>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/RadioToggle-test.js b/server/sonar-web/src/main/js/components/controls/__tests__/RadioToggle-test.js
deleted file mode 100644 (file)
index b23e070..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.
- */
-import { shallow } from 'enzyme';
-import React from 'react';
-import RadioToggle from '../RadioToggle';
-import { change } from '../../../helpers/testUtils';
-
-function getSample(props) {
-  const options = [{ value: 'one', label: 'first' }, { value: 'two', label: 'second' }];
-  return <RadioToggle options={options} name="sample" onCheck={() => true} {...props} />;
-}
-
-it('should render', () => {
-  const radioToggle = shallow(getSample());
-  expect(radioToggle.find('input[type="radio"]').length).toBe(2);
-  expect(radioToggle.find('label').length).toBe(2);
-});
-
-it('should call onCheck', () => {
-  const onCheck = jest.fn();
-  const radioToggle = shallow(getSample({ onCheck }));
-  change(radioToggle.find('input[value="two"]'), 'two');
-  expect(onCheck).toBeCalledWith('two');
-});
diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/RadioToggle-test.tsx b/server/sonar-web/src/main/js/components/controls/__tests__/RadioToggle-test.tsx
new file mode 100644 (file)
index 0000000..f1a231b
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import RadioToggle from '../RadioToggle';
+import { change } from '../../../helpers/testUtils';
+
+it('renders', () => {
+  expect(shallow(getSample())).toMatchSnapshot();
+});
+
+it('calls onCheck', () => {
+  const onCheck = jest.fn();
+  const wrapper = shallow(getSample({ onCheck }));
+  change(wrapper.find('input[value="two"]'), 'two');
+  expect(onCheck).toBeCalledWith('two');
+});
+
+function getSample(props?: any) {
+  const options = [{ value: 'one', label: 'first' }, { value: 'two', label: 'second' }];
+  return <RadioToggle options={options} name="sample" onCheck={() => true} {...props} />;
+}
diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/RadioToggle-test.tsx.snap b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/RadioToggle-test.tsx.snap
new file mode 100644 (file)
index 0000000..df95798
--- /dev/null
@@ -0,0 +1,38 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders 1`] = `
+<ul
+  className="radio-toggle"
+>
+  <li>
+    <input
+      checked={false}
+      id="sample__one"
+      name="sample"
+      onChange={[Function]}
+      type="radio"
+      value="one"
+    />
+    <label
+      htmlFor="sample__one"
+    >
+      first
+    </label>
+  </li>
+  <li>
+    <input
+      checked={false}
+      id="sample__two"
+      name="sample"
+      onChange={[Function]}
+      type="radio"
+      value="two"
+    />
+    <label
+      htmlFor="sample__two"
+    >
+      second
+    </label>
+  </li>
+</ul>
+`;
index 02e2ce1c37c10c7759feb62e0b3df0f1ad558d39..6675a6791b1c45ffc94ce1956d6023121b96b4d1 100644 (file)
@@ -42,10 +42,11 @@ export function submit(element: ShallowWrapper): void {
   });
 }
 
-export function change(element: ShallowWrapper, value: string): void {
+export function change(element: ShallowWrapper, value: string, event = {}): void {
   element.simulate('change', {
     target: { value },
-    currentTarget: { value }
+    currentTarget: { value },
+    ...event
   });
 }
 
index a99fd9f00c67efc4d8694c186243a19f92d942de..963655f5f6735170c36b1a8246115ed533b92a63 100644 (file)
@@ -422,15 +422,21 @@ qualifiers.new.VW=New Portfolio
 qualifiers.new.DEV=New Developer
 qualifiers.new.APP=New Application
 
-qualifiers.delete.TRK=Delete Project
-qualifiers.delete.VW=Delete Portfolio
-qualifiers.delete.DEV=Delete Developer
-qualifiers.delete.APP=Delete Application
+qualifier.delete.TRK=Delete Project
+qualifier.delete.VW=Delete Portfolio
+qualifier.delete.APP=Delete Application
 
-qualifiers.delete_confirm.TRK=Do you want to delete this project?
-qualifiers.delete_confirm.VW=Do you want to delete this portfolio?
-qualifiers.delete_confirm.DEV=Do you want to delete this developer?
-qualifiers.delete_confirm.APP=Do you want to delete this application?
+qualifiers.delete.TRK=Delete Projects
+qualifiers.delete.VW=Delete Portfolios
+qualifiers.delete.APP=Delete Applications
+
+qualifier.delete_confirm.TRK=Do you want to delete this project?
+qualifier.delete_confirm.VW=Do you want to delete this portfolio?
+qualifier.delete_confirm.APP=Do you want to delete this application?
+
+qualifiers.delete_confirm.TRK=Do you want to delete these projects?
+qualifiers.delete_confirm.VW=Do you want to delete these portfolios?
+qualifiers.delete_confirm.APP=Do you want to delete these applications?
 
 qualifiers.create.TRK=Create Project
 qualifiers.create.VW=Create Portfolio
@@ -1398,6 +1404,7 @@ project_quality_gate.successfully_updated=Quality gate has been successfully upd
 #------------------------------------------------------------------------------
 
 project_deletion.delete_resource_confirmation=Are you sure you want to delete "{0}"?
+projects_management.delete_resource_confirmation=Are you sure you want to delete "{0}"?
 
 
 #------------------------------------------------------------------------------
@@ -2596,6 +2603,8 @@ permission_templates.project_creators=Project Creators
 permission_templates.project_creators.explanation=When a new project is created, the user who creates the project will receive this permission on the project.
 permission_templates.grant_permission_to_project_creators=Grant the "{0}" permission to project creators
 permission_templates.bulk_apply_permission_template=Bulk Apply Permission Template
+permission_templates.bulk_apply_permission_template.apply_to_selected=You're about to apply the selected permission template to {0} selected item(s).
+permission_templates.bulk_apply_permission_template.apply_to_all=You're about to apply the selected permission template to {0} item(s).
 
 
 #------------------------------------------------------------------------------
index f359ed761d502a1a8e6412d32092601e12cfa42e..c6dd13c2cbd0252991a95d80512f06e1f8a78618 100644 (file)
  */
 package org.sonarqube.pageobjects;
 
-import com.codeborne.selenide.CollectionCondition;
-
 import static com.codeborne.selenide.Condition.exist;
 import static com.codeborne.selenide.Condition.text;
+import static com.codeborne.selenide.Condition.visible;
 import static com.codeborne.selenide.Selenide.$;
 import static com.codeborne.selenide.Selenide.$$;
 
@@ -52,13 +51,11 @@ public class ProjectsManagementPage {
   }
 
   public ProjectsManagementPage bulkApplyPermissionTemplate(String template) {
-    $(".js-bulk-apply-permission-template").should(exist).click();
-    $(".modal .select2-choice").should(exist).click();
-    $$(".select2-results li")
-      .shouldHave(CollectionCondition.sizeGreaterThan(0))
-      .findBy(text("foo-template")).should(exist).click();
-    $(".modal .js-apply").should(exist).click();
-    $(".modal-body .alert-success").should(exist);
+    $(".js-bulk-apply-permission-template").click();
+    $(".modal .Select-value").click();
+    $$(".modal .Select-option").findBy(text(template)).click();
+    $(".modal-foot button").click();
+    $(".modal-body .alert-success").shouldBe(visible);
     return this;
   }
 }
index 8d3b7ac007ab878d1aec7e1b8ff8f2d09cb5ddba..a71af6eecc56731dd992bc88b4a3bd73f565357e 100644 (file)
@@ -95,7 +95,7 @@ public class ProvisioningPermissionTest {
    * SONAR-4709
    */
   @Test
-  public void organization_administrator_cannot_provision_project_if_he_doesnt_have_provisioning_permission() {
+  public void organization_administrator_cannot_provision_project_if_he_does_not_have_provisioning_permission() {
     runSelenese(orchestrator, "/authorisation/ProvisioningPermissionTest/should-not-be-able-to-provision-project.html");
   }
 
@@ -131,7 +131,7 @@ public class ProvisioningPermissionTest {
    * SONAR-4709
    */
   @Test
-  public void user_cannot_provision_project_through_ws_if_he_doesnt_have_provisioning_permission() {
+  public void user_cannot_provision_project_through_ws_if_he_does_not_have_provisioning_permission() {
     thrown.expect(HttpException.class);
     thrown.expectMessage("403");
 
index 506a921dd4b4189230811dd72d7caec781d11a87..c43f94871a0df3562ffa1c07f4412f0c98cc567d 100644 (file)
   </tr>
   <tr>
     <td>open</td>
-    <td>/projects_admin</td>
+    <td>/admin/projects_management</td>
     <td></td>
   </tr>
   <tr>
     <td>waitForElementPresent</td>
-    <td>css=#projects-type__ALL</td>
+    <td>css=#create-project</td>
     <td></td>
   </tr>
-  <tr>
-    <td>assertText</td>
-    <td>css=.page-actions button</td>
-    <td>*Create Project*</td>
-  </tr>
   </tbody>
 </table>
 </body>
index 634e71e1926630d49382bb4794c4d475b096e75b..8b907e20c2ccdf44841592ac29b008ceaf95704c 100644 (file)
   </tr>
   <tr>
     <td>open</td>
-    <td>/projects_admin</td>
+    <td>/admin/projects_management</td>
     <td></td>
   </tr>
   <tr>
     <td>waitForElementPresent</td>
-    <td>css=#projects-type__ALL</td>
+    <td>css=#projects-management-page</td>
     <td></td>
   </tr>
   <tr>
-    <td>assertNotText</td>
-    <td>css=.page-actions button</td>
-    <td>*Create Project*</td>
+    <td>assertElementNotPresent</td>
+    <td>css=#create-project</td>
+    <td></td>
   </tr>
   </tbody>
 </table>