aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/apps
diff options
context:
space:
mode:
Diffstat (limited to 'server/sonar-web/src/main/js/apps')
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/components/OrganizationProjectsManagement.js2
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/deletion/Form.js2
-rw-r--r--server/sonar-web/src/main/js/apps/projects-admin/__tests__/projects-test.js60
-rw-r--r--server/sonar-web/src/main/js/apps/projects-admin/delete-view.js31
-rw-r--r--server/sonar-web/src/main/js/apps/projects-admin/form-view.js37
-rw-r--r--server/sonar-web/src/main/js/apps/projects-admin/templates/BulkApplyTemplateTemplate.hbs68
-rw-r--r--server/sonar-web/src/main/js/apps/projects-admin/templates/projects-delete.hbs13
-rw-r--r--server/sonar-web/src/main/js/apps/projects-admin/views/BulkApplyTemplateView.js121
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/App.tsx (renamed from server/sonar-web/src/main/js/apps/projects-admin/main.js)207
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/AppContainer.tsx (renamed from server/sonar-web/src/main/js/apps/projects-admin/AppContainer.js)50
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/BulkApplyTemplateModal.tsx217
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/ChangeVisibilityForm.tsx (renamed from server/sonar-web/src/main/js/apps/projects-admin/ChangeVisibilityForm.js)57
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/CreateProjectForm.tsx (renamed from server/sonar-web/src/main/js/apps/projects-admin/CreateProjectForm.js)85
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/DeleteModal.tsx105
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/Header.tsx (renamed from server/sonar-web/src/main/js/apps/projects-admin/header.js)41
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx (renamed from server/sonar-web/src/main/js/apps/projects-admin/projects.js)91
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/Projects.tsx68
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/Search.tsx (renamed from server/sonar-web/src/main/js/apps/projects-admin/search.js)131
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/__tests__/App-test.tsx148
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/__tests__/BulkApplyTemplateModal-test.tsx128
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ChangeVisibilityForm-test.tsx73
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/__tests__/CreateProjectForm-test.tsx72
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/__tests__/DeleteModal-test.tsx63
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Header-test.tsx64
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRow-test.tsx61
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Projects-test.tsx67
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Search-test.tsx107
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/BulkApplyTemplateModal-test.tsx.snap643
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ChangeVisibilityForm-test.tsx.snap308
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/CreateProjectForm-test.tsx.snap468
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/DeleteModal-test.tsx.snap98
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Header-test.tsx.snap56
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRow-test.tsx.snap101
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Projects-test.tsx.snap37
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Search-test.tsx.snap234
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/routes.ts (renamed from server/sonar-web/src/main/js/apps/projects-admin/routes.ts)0
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/utils.ts (renamed from server/sonar-web/src/main/js/apps/projects-admin/constants.js)22
37 files changed, 3453 insertions, 683 deletions
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationProjectsManagement.js b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationProjectsManagement.js
index 8222ab2df1c..a8ae5dd4b09 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationProjectsManagement.js
+++ b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationProjectsManagement.js
@@ -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'; */
diff --git a/server/sonar-web/src/main/js/apps/project-admin/deletion/Form.js b/server/sonar-web/src/main/js/apps/project-admin/deletion/Form.js
index 3dc4dc144c6..8c8352a27d3 100644
--- a/server/sonar-web/src/main/js/apps/project-admin/deletion/Form.js
+++ b/server/sonar-web/src/main/js/apps/project-admin/deletion/Form.js
@@ -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/__tests__/projects-test.js b/server/sonar-web/src/main/js/apps/projects-admin/__tests__/projects-test.js
deleted file mode 100644
index e17e8668963..00000000000
--- a/server/sonar-web/src/main/js/apps/projects-admin/__tests__/projects-test.js
+++ /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/delete-view.js b/server/sonar-web/src/main/js/apps/projects-admin/delete-view.js
deleted file mode 100644
index 99bc391cbc5..00000000000
--- a/server/sonar-web/src/main/js/apps/projects-admin/delete-view.js
+++ /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
index cb2ed3ec2da..00000000000
--- a/server/sonar-web/src/main/js/apps/projects-admin/form-view.js
+++ /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/templates/BulkApplyTemplateTemplate.hbs b/server/sonar-web/src/main/js/apps/projects-admin/templates/BulkApplyTemplateTemplate.hbs
deleted file mode 100644
index b4933eb193c..00000000000
--- a/server/sonar-web/src/main/js/apps/projects-admin/templates/BulkApplyTemplateTemplate.hbs
+++ /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
index 2ab69b28f72..00000000000
--- a/server/sonar-web/src/main/js/apps/projects-admin/templates/projects-delete.hbs
+++ /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
index 5dd9e8ab54c..00000000000
--- a/server/sonar-web/src/main/js/apps/projects-admin/views/BulkApplyTemplateView.js
+++ /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/projects-admin/main.js b/server/sonar-web/src/main/js/apps/projectsManagement/App.tsx
index eea76fcf10d..5a860abb5f7 100644
--- a/server/sonar-web/src/main/js/apps/projects-admin/main.js
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/App.tsx
@@ -17,48 +17,42 @@
* 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 * 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 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 { PAGE_SIZE, Type, Project } from './utils';
+import { getComponents, getProvisioned, getGhosts } from '../../api/components';
+import { Organization } from '../../app/types';
import { translate } from '../../helpers/l10n';
-/*:: import type { Organization } from '../../store/organizations/duck'; */
-/*::
-type Props = {|
- hasProvisionPermission: boolean,
- onVisibilityChange: string => void,
- onRequestFail: Object => void,
- organization: Organization
-|};
-*/
+export interface Props {
+ hasProvisionPermission?: boolean;
+ onVisibilityChange: (visibility: string) => void;
+ organization: Organization;
+ topLevelQualifiers: string[];
+}
-/*::
-type State = {
- createProjectForm: boolean,
- ready: boolean,
- projects: Array<{ key: string }>,
- total: number,
- page: number,
- query: string,
- qualifiers: string,
- type: string,
- selection: Array<string>
-};
-*/
+interface State {
+ createProjectForm: boolean;
+ page: number;
+ projects: Project[];
+ qualifiers: string;
+ query: string;
+ ready: boolean;
+ selection: string[];
+ total: number;
+ type: Type;
+}
-export default class Main extends React.PureComponent {
- /*:: props: Props; */
- /*:: state: State; */
+export default class App extends React.PureComponent<Props, State> {
+ mounted: boolean;
- constructor(props /*: Props */) {
+ constructor(props: Props) {
super(props);
this.state = {
createProjectForm: false,
@@ -68,86 +62,87 @@ export default class Main extends React.PureComponent {
page: 1,
query: '',
qualifiers: 'TRK',
- type: TYPE.ALL,
+ type: Type.All,
selection: []
};
this.requestProjects = debounce(this.requestProjects, 250);
}
componentDidMount() {
+ this.mounted = true;
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;
- };
+ 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:
+ case Type.All:
this.requestAllProjects();
break;
- case TYPE.PROVISIONED:
+ case Type.Provisioned:
this.requestProvisioned();
break;
- case TYPE.GHOSTS:
+ 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);
+ 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 });
}
- 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);
+ 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 });
}
- this.setState({ ready: true, projects, total: r.paging.total });
});
};
requestAllProjects = () => {
const data = this.getFilters();
- data.qualifiers = this.state.qualifiers;
+ Object.assign(data, { qualifiers: this.state.qualifiers });
getComponents(data).then(r => {
- let projects = r.components;
- if (this.state.page > 1) {
- projects = [].concat(this.state.projects, projects);
+ 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 });
}
- this.setState({ ready: true, projects, total: r.paging.total });
});
};
@@ -155,53 +150,31 @@ export default class Main extends React.PureComponent {
this.setState({ ready: false, page: this.state.page + 1 }, this.requestProjects);
};
- onSearch = (query /*: string */) => {
- this.setState(
- {
- ready: false,
- page: 1,
- query,
- selection: []
- },
- this.requestProjects
- );
+ onSearch = (query: string) => {
+ this.setState({ ready: false, page: 1, query, selection: [] }, this.requestProjects);
};
- onTypeChanged = (newType /*: string */) => {
+ onTypeChanged = (newType: Type) => {
this.setState(
- {
- ready: false,
- page: 1,
- query: '',
- type: newType,
- qualifiers: 'TRK',
- selection: []
- },
+ { ready: false, page: 1, query: '', type: newType, qualifiers: 'TRK', selection: [] },
this.requestProjects
);
};
- onQualifierChanged = (newQualifier /*: string */) => {
+ onQualifierChanged = (newQualifier: string) => {
this.setState(
- {
- ready: false,
- page: 1,
- query: '',
- type: TYPE.ALL,
- qualifiers: newQualifier,
- selection: []
- },
+ { 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));
+ onProjectSelected = (project: string) => {
+ const newSelection = uniq([...this.state.selection, project]);
this.setState({ selection: newSelection });
};
- onProjectDeselected = (project /*: { key: string } */) => {
- const newSelection = without(this.state.selection, project.key);
+ onProjectDeselected = (project: string) => {
+ const newSelection = without(this.state.selection, project);
this.setState({ selection: newSelection });
};
@@ -214,18 +187,6 @@ export default class Main extends React.PureComponent {
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 });
};
@@ -249,18 +210,17 @@ export default class Main extends React.PureComponent {
<Search
{...this.props}
{...this.state}
- onSearch={this.onSearch}
- onTypeChanged={this.onTypeChanged}
- onQualifierChanged={this.onQualifierChanged}
onAllSelected={this.onAllSelected}
onAllDeselected={this.onAllDeselected}
- deleteProjects={this.deleteProjects}
+ onDeleteProjects={this.requestProjects}
+ onQualifierChanged={this.onQualifierChanged}
+ onSearch={this.onSearch}
+ onTypeChanged={this.onTypeChanged}
/>
<Projects
ready={this.state.ready}
projects={this.state.projects}
- refresh={this.requestProjects}
selection={this.state.selection}
onProjectSelected={this.onProjectSelected}
onProjectDeselected={this.onProjectDeselected}
@@ -278,7 +238,6 @@ export default class Main extends React.PureComponent {
<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/AppContainer.js b/server/sonar-web/src/main/js/apps/projectsManagement/AppContainer.tsx
index 1531bb344dc..0b00f3eba76 100644
--- a/server/sonar-web/src/main/js/apps/projects-admin/AppContainer.js
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/AppContainer.tsx
@@ -17,16 +17,28 @@
* 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 * as React from 'react';
import { connect } from 'react-redux';
-import Main from './main';
+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';
-class AppContainer extends React.PureComponent {
+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
@@ -35,12 +47,10 @@ class AppContainer extends React.PureComponent {
}
}
- componentWillUnmount() {
- this.mounted = false;
- }
-
- handleVisibilityChange = visibility => {
- this.props.onVisibilityChange(this.props.organization, visibility);
+ handleVisibilityChange = (visibility: string) => {
+ if (this.props.organization) {
+ this.props.onVisibilityChange(this.props.organization, visibility);
+ }
};
render() {
@@ -53,24 +63,25 @@ class AppContainer extends React.PureComponent {
const topLevelQualifiers = organization.isDefault ? this.props.appState.qualifiers : ['TRK'];
return (
- <Main
+ <App
hasProvisionPermission={organization.canProvisionProjects}
- topLevelQualifiers={topLevelQualifiers}
onVisibilityChange={this.handleVisibilityChange}
- onRequestFail={this.props.onRequestFail}
organization={organization}
+ topLevelQualifiers={topLevelQualifiers}
/>
);
}
}
-const mapStateToProps = (state, ownProps) => ({
+const mapStateToProps = (state: any, ownProps: Props) => ({
appState: getAppState(state),
organization:
ownProps.organization || getOrganizationByKey(state, getAppState(state).defaultOrganization)
});
-const onVisibilityChange = (organization, visibility) => dispatch => {
+const onVisibilityChange = (organization: Organization, visibility: string) => (
+ dispatch: Function
+) => {
const currentVisibility = organization.projectVisibility;
dispatch(receiveOrganizations([{ ...organization, projectVisibility: visibility }]));
changeProjectVisibility(organization.key, visibility).catch(error => {
@@ -79,11 +90,10 @@ const onVisibilityChange = (organization, visibility) => dispatch => {
});
};
-const mapDispatchToProps = dispatch => ({
- fetchOrganization: key => dispatch(fetchOrganization(key)),
- onVisibilityChange: (organization, visibility) =>
- dispatch(onVisibilityChange(organization, visibility)),
- onRequestFail: error => onFail(dispatch)(error)
+const mapDispatchToProps = (dispatch: Function) => ({
+ fetchOrganization: (key: string) => dispatch(fetchOrganization(key)),
+ onVisibilityChange: (organization: Organization, visibility: string) =>
+ dispatch(onVisibilityChange(organization, visibility))
});
-export default connect(mapStateToProps, mapDispatchToProps)(AppContainer);
+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
index 00000000000..a5f967fd04a
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/BulkApplyTemplateModal.tsx
@@ -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/projects-admin/ChangeVisibilityForm.js b/server/sonar-web/src/main/js/apps/projectsManagement/ChangeVisibilityForm.tsx
index b04c4c3e8ae..9c04349c688 100644
--- a/server/sonar-web/src/main/js/apps/projects-admin/ChangeVisibilityForm.js
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/ChangeVisibilityForm.tsx
@@ -17,53 +17,45 @@
* 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 * as React from 'react';
import Modal from 'react-modal';
-import classNames from 'classnames';
+import * as classNames from 'classnames';
+import { Organization } from '../../app/types';
import UpgradeOrganizationBox from '../../components/common/UpgradeOrganizationBox';
import { translate } from '../../helpers/l10n';
-/*:: import type { Organization } from '../../store/organizations/duck'; */
+import { Visibility } from './utils';
-/*::
-type Props = {
- onClose: () => void,
- onConfirm: string => void,
- organization: Organization
-};
-*/
-
-/*::
-type State = {
- visibility: string
-};
-*/
+export interface Props {
+ onClose: () => void;
+ onConfirm: (visiblity: Visibility) => void;
+ organization: Organization;
+}
-export default class ChangeVisibilityForm extends React.PureComponent {
- /*:: props: Props; */
- /*:: state: State; */
+interface State {
+ visibility: Visibility;
+}
- constructor(props /*: Props */) {
+export default class ChangeVisibilityForm extends React.PureComponent<Props, State> {
+ constructor(props: Props) {
super(props);
- this.state = { visibility: props.organization.projectVisibility };
+ this.state = { visibility: props.organization.projectVisibility as Visibility };
}
- handleCancelClick = (event /*: Event */) => {
+ handleCancelClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
event.preventDefault();
this.props.onClose();
};
- handleConfirmClick = (event /*: Event */) => {
+ handleConfirmClick = (event: React.SyntheticEvent<HTMLButtonElement>) => {
event.preventDefault();
this.props.onConfirm(this.state.visibility);
this.props.onClose();
};
- handleVisibilityClick = (visibility /*: string */) => (
- event /*: Event & { currentTarget: HTMLElement } */
- ) => {
+ handleVisibilityClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
event.preventDefault();
event.currentTarget.blur();
+ const visibility = event.currentTarget.dataset.visibility as Visibility;
this.setState({ visibility });
};
@@ -84,10 +76,10 @@ export default class ChangeVisibilityForm extends React.PureComponent {
</header>
<div className="modal-body">
- {['public', 'private'].map(visibility =>
+ {[Visibility.Public, Visibility.Private].map(visibility =>
<div className="big-spacer-bottom" key={visibility}>
<p>
- {visibility === 'private' && !canUpdateProjectsVisibilityToPrivate
+ {visibility === Visibility.Private && !canUpdateProjectsVisibilityToPrivate
? <span className="text-muted cursor-not-allowed">
<i
className={classNames('icon-radio', 'spacer-right', {
@@ -98,8 +90,9 @@ export default class ChangeVisibilityForm extends React.PureComponent {
</span>
: <a
className="link-base-color link-no-underline"
+ data-visibility={visibility}
href="#"
- onClick={this.handleVisibilityClick(visibility)}>
+ onClick={this.handleVisibilityClick}>
<i
className={classNames('icon-radio', 'spacer-right', {
'is-checked': this.state.visibility === visibility
@@ -122,10 +115,10 @@ export default class ChangeVisibilityForm extends React.PureComponent {
</div>
<footer className="modal-foot">
- <button onClick={this.handleConfirmClick}>
+ <button className="js-confirm" onClick={this.handleConfirmClick}>
{translate('organization.change_visibility_form.submit')}
</button>
- <a href="#" onClick={this.handleCancelClick}>
+ <a className="js-modal-close" href="#" onClick={this.handleCancelClick}>
{translate('cancel')}
</a>
</footer>
diff --git a/server/sonar-web/src/main/js/apps/projects-admin/CreateProjectForm.js b/server/sonar-web/src/main/js/apps/projectsManagement/CreateProjectForm.tsx
index fa8e2ea0f6d..22a81acf160 100644
--- a/server/sonar-web/src/main/js/apps/projects-admin/CreateProjectForm.js
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/CreateProjectForm.tsx
@@ -17,50 +17,44 @@
* 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 * 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';
-/*:: 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 */) {
+
+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 ? props.organization.projectVisibility : 'public'
+ visibility: props.organization.projectVisibility
};
}
@@ -72,32 +66,30 @@ export default class CreateProjectForm extends React.PureComponent {
this.mounted = false;
}
- handleCancelClick = (event /*: Event */) => {
+ handleCancelClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
event.preventDefault();
this.props.onClose();
};
- handleInputChange = (event /*: { currentTarget: HTMLInputElement } */) => {
+ handleInputChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
const { name, value } = event.currentTarget;
this.setState({ [name]: value });
};
- handleVisibilityChange = (visibility /*: string */) => {
+ handleVisibilityChange = (visibility: string) => {
this.setState({ visibility });
};
- handleFormSubmit = (event /*: Event */) => {
+ handleFormSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
event.preventDefault();
- const data /*: { [string]: string } */ = {
+ 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
};
- if (this.props.organization) {
- data.organization = this.props.organization.key;
- }
this.setState({ loading: true });
createProject(data).then(
@@ -165,7 +157,7 @@ export default class CreateProjectForm extends React.PureComponent {
<input
autoFocus={true}
id="create-project-name"
- maxLength="2000"
+ maxLength={2000}
name="name"
onChange={this.handleInputChange}
required={true}
@@ -179,7 +171,7 @@ export default class CreateProjectForm extends React.PureComponent {
</label>
<input
id="create-project-branch"
- maxLength="200"
+ maxLength={200}
name="branch"
onChange={this.handleInputChange}
type="text"
@@ -193,7 +185,7 @@ export default class CreateProjectForm extends React.PureComponent {
</label>
<input
id="create-project-key"
- maxLength="400"
+ maxLength={400}
name="key"
onChange={this.handleInputChange}
required={true}
@@ -203,18 +195,15 @@ export default class CreateProjectForm extends React.PureComponent {
</div>
<div className="modal-field">
<label>
- {' '}{translate('visibility')}{' '}
+ {translate('visibility')}
</label>
<VisibilitySelector
- canTurnToPrivate={
- organization == null || organization.canUpdateProjectsVisibilityToPrivate
- }
+ canTurnToPrivate={organization.canUpdateProjectsVisibilityToPrivate}
className="little-spacer-top"
onChange={this.handleVisibilityChange}
visibility={this.state.visibility}
/>
- {organization != null &&
- !organization.canUpdateProjectsVisibilityToPrivate &&
+ {!organization.canUpdateProjectsVisibilityToPrivate &&
<div className="spacer-top">
<UpgradeOrganizationBox organization={organization.key} />
</div>}
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
index 00000000000..7b4a45fc61b
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/DeleteModal.tsx
@@ -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/projects-admin/header.js b/server/sonar-web/src/main/js/apps/projectsManagement/Header.tsx
index 9fdf7d45ba0..ef77181c740 100644
--- a/server/sonar-web/src/main/js/apps/projects-admin/header.js
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/Header.tsx
@@ -17,37 +17,32 @@
* 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 * as React from 'react';
import ChangeVisibilityForm from './ChangeVisibilityForm';
+import { Visibility } from './utils';
+import { Organization } from '../../app/types';
import { translate } from '../../helpers/l10n';
-/*:: import type { Organization } from '../../store/organizations/duck'; */
-/*::
-type Props = {|
- hasProvisionPermission: boolean,
- onProjectCreate: () => void,
- onVisibilityChange: string => void,
- organization: Organization
-|};
-*/
+export interface Props {
+ hasProvisionPermission?: boolean;
+ onProjectCreate: () => void;
+ onVisibilityChange: (visibility: Visibility) => void;
+ organization: Organization;
+}
-/*::
-type State = {
- visibilityForm: boolean
-};
-*/
+interface State {
+ visibilityForm: boolean;
+}
-export default class Header extends React.PureComponent {
- /*:: props: Props; */
- state /*: State */ = { visibilityForm: false };
+export default class Header extends React.PureComponent<Props, State> {
+ state: State = { visibilityForm: false };
- handleCreateProjectClick = (event /*: Event */) => {
+ handleCreateProjectClick = (event: React.SyntheticEvent<HTMLButtonElement>) => {
event.preventDefault();
this.props.onProjectCreate();
};
- handleChangeVisibilityClick = (event /*: Event */) => {
+ handleChangeVisibilityClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
event.preventDefault();
this.setState({ visibilityForm: true });
};
@@ -64,12 +59,13 @@ export default class Header extends React.PureComponent {
<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"
+ className="js-change-visibility spacer-left icon-edit"
href="#"
onClick={this.handleChangeVisibilityClick}
/>
@@ -79,6 +75,7 @@ export default class Header extends React.PureComponent {
{translate('qualifiers.create.TRK')}
</button>}
</div>
+
<p className="page-description">
{translate('projects_management.page.description')}
</p>
diff --git a/server/sonar-web/src/main/js/apps/projects-admin/projects.js b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx
index 0d0f59342ad..60f951dd32c 100644
--- a/server/sonar-web/src/main/js/apps/projects-admin/projects.js
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx
@@ -1,7 +1,7 @@
/*
* SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
+ * 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
@@ -17,60 +17,42 @@
* 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 * as React from 'react';
import { Link } from 'react-router';
-import { getComponentPermissionsUrl } from '../../helpers/urls';
-import ApplyTemplateView from '../permissions/project/views/ApplyTemplateView';
+import { Project, Visibility } from './utils';
+import PrivateBadge from '../../components/common/PrivateBadge';
import Checkbox from '../../components/controls/Checkbox';
import QualifierIcon from '../../components/shared/QualifierIcon';
-import PrivateBadge from '../../components/common/PrivateBadge';
import { translate } from '../../helpers/l10n';
+import { getComponentPermissionsUrl } from '../../helpers/urls';
-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);
- }
- }
+interface Props {
+ onApplyTemplateClick: (project: Project) => void;
+ onProjectCheck: (project: Project, checked: boolean) => void;
+ project: Project;
+ selected: boolean;
+}
- onApplyTemplateClick(project, e) {
- e.preventDefault();
- e.target.blur();
- new ApplyTemplateView({
- project,
- organization: this.props.organization
- }).render();
- }
+export default class ProjectRow extends React.PureComponent<Props> {
+ handleProjectCheck = (checked: boolean) => {
+ this.props.onProjectCheck(this.props.project, checked);
+ };
- isProjectSelected(project) {
- return this.props.selection.indexOf(project.key) !== -1;
- }
+ handleApplyTemplateClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
+ event.preventDefault();
+ event.currentTarget.blur();
+ this.props.onApplyTemplateClick(this.props.project);
+ };
- renderProject(project) {
- const permissionsUrl = getComponentPermissionsUrl(project.key);
+ render() {
+ const { project, selected } = this.props;
return (
- <tr key={project.key}>
+ <tr>
<td className="thin">
- <Checkbox
- checked={this.isProjectSelected(project)}
- onCheck={this.onProjectCheck.bind(this, project)}
- />
+ <Checkbox checked={selected} onCheck={this.handleProjectCheck} />
</td>
+
<td className="nowrap">
<Link
to={{ pathname: '/dashboard', query: { id: project.key } }}
@@ -78,14 +60,17 @@ export default class Projects extends React.PureComponent {
<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 />}
+ {project.visibility === Visibility.Private && <PrivateBadge />}
</td>
+
<td className="thin nowrap">
<div className="dropdown">
<button className="dropdown-toggle" data-toggle="dropdown">
@@ -93,12 +78,12 @@ export default class Projects extends React.PureComponent {
</button>
<ul className="dropdown-menu dropdown-menu-right">
<li>
- <Link to={permissionsUrl}>
+ <Link to={getComponentPermissionsUrl(project.key)}>
{translate('edit_permissions')}
</Link>
</li>
<li>
- <a href="#" onClick={this.onApplyTemplateClick.bind(this, project)}>
+ <a className="js-apply-template" href="#" onClick={this.handleApplyTemplateClick}>
{translate('projects_role.apply_template')}
</a>
</li>
@@ -108,16 +93,4 @@ export default class Projects extends React.PureComponent {
</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/projectsManagement/Projects.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/Projects.tsx
new file mode 100644
index 00000000000..ff6264dce90
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/Projects.tsx
@@ -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/projects-admin/search.js b/server/sonar-web/src/main/js/apps/projectsManagement/Search.tsx
index ce42b2050e9..914883f8359 100644
--- a/server/sonar-web/src/main/js/apps/projects-admin/search.js
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/Search.tsx
@@ -17,38 +17,60 @@
* 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 * as React from 'react';
import { sortBy } from 'lodash';
-import { TYPE, QUALIFIERS_ORDER } from './constants';
-import DeleteView from './delete-view';
-import BulkApplyTemplateView from './views/BulkApplyTemplateView';
+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 default class Search extends React.PureComponent {
- static propTypes = {
- onSearch: PropTypes.func.isRequired
- };
+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;
+}
- onSubmit = e => {
- e.preventDefault();
+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 = () => {
- const q = this.refs.input.value;
+ search = (event?: React.SyntheticEvent<HTMLInputElement>) => {
+ const q = event ? event.currentTarget.value : this.input.value;
this.props.onSearch(q);
};
- getTypeOptions = () => {
- return [
- { value: TYPE.ALL, label: 'All' },
- { value: TYPE.PROVISIONED, label: 'Provisioned' },
- { value: TYPE.GHOSTS, label: 'Ghosts' }
- ];
- };
+ getTypeOptions = () => [
+ { value: Type.All, label: 'All' },
+ { value: Type.Provisioned, label: 'Provisioned' },
+ { value: Type.Ghosts, label: 'Ghosts' }
+ ];
getQualifierOptions = () => {
const options = this.props.topLevelQualifiers.map(q => {
@@ -57,7 +79,7 @@ export default class Search extends React.PureComponent {
return sortBy(options, option => QUALIFIERS_ORDER.indexOf(option.value));
};
- onCheck = checked => {
+ onCheck = (checked: boolean) => {
if (checked) {
this.props.onAllSelected();
} else {
@@ -65,20 +87,29 @@ export default class Search extends React.PureComponent {
}
};
- deleteProjects = () => {
- new DeleteView({
- deleteProjects: this.props.deleteProjects
- }).render();
+ handleDeleteClick = (event: React.SyntheticEvent<HTMLButtonElement>) => {
+ event.preventDefault();
+ event.currentTarget.blur();
+ this.setState({ deleteModal: true });
+ };
+
+ closeDeleteModal = () => {
+ this.setState({ deleteModal: false });
};
- bulkApplyTemplate = () => {
- new BulkApplyTemplateView({
- total: this.props.total,
- selection: this.props.selection,
- query: this.props.query,
- qualifier: this.props.qualifier,
- organization: this.props.organization
- }).render();
+ 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 = () => {
@@ -93,7 +124,7 @@ export default class Search extends React.PureComponent {
};
renderGhostsDescription = () => {
- if (this.props.type !== TYPE.GHOSTS || !this.props.ready) {
+ if (this.props.type !== Type.Ghosts || !this.props.ready) {
return null;
}
return (
@@ -120,8 +151,6 @@ export default class Search extends React.PureComponent {
);
};
- renderSpinner = () => <i className="spinner" />;
-
render() {
const isSomethingSelected = this.props.projects.length > 0 && this.props.selection.length > 0;
return (
@@ -130,7 +159,7 @@ export default class Search extends React.PureComponent {
<tbody>
<tr>
<td className="thin text-middle">
- {this.props.ready ? this.renderCheckbox() : this.renderSpinner()}
+ {this.props.ready ? this.renderCheckbox() : <i className="spinner" />}
</td>
{this.renderQualifierFilter()}
<td className="thin nowrap text-middle">
@@ -149,7 +178,7 @@ export default class Search extends React.PureComponent {
<input
onChange={this.search}
value={this.props.query}
- ref="input"
+ ref={node => (this.input = node!)}
className="search-box-input input-medium"
type="search"
placeholder="Search"
@@ -159,12 +188,12 @@ export default class Search extends React.PureComponent {
<td className="thin nowrap text-middle">
<button
className="spacer-right js-bulk-apply-permission-template"
- onClick={this.bulkApplyTemplate}>
+ onClick={this.handleBulkApplyTemplateClick}>
{translate('permission_templates.bulk_apply_permission_template')}
</button>
<button
- onClick={this.deleteProjects}
- className="button-red"
+ onClick={this.handleDeleteClick}
+ className="js-delete button-red"
disabled={!isSomethingSelected}>
{translate('delete')}
</button>
@@ -173,6 +202,26 @@ export default class Search extends React.PureComponent {
</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
index 00000000000..1c9569d5700
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/App-test.tsx
@@ -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
index 00000000000..1c8399e6eda
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/BulkApplyTemplateModal-test.tsx
@@ -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
index 00000000000..61dce6dbfe4
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ChangeVisibilityForm-test.tsx
@@ -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
index 00000000000..0212d094e8e
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/CreateProjectForm-test.tsx
@@ -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
index 00000000000..d24f30e9cdf
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/DeleteModal-test.tsx
@@ -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
index 00000000000..726c6b186cb
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Header-test.tsx
@@ -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
index 00000000000..7b7bb09d5ae
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRow-test.tsx
@@ -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
index 00000000000..b1f165cde7a
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Projects-test.tsx
@@ -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
index 00000000000..950b78e1624
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Search-test.tsx
@@ -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
index 00000000000..bc3e39d7f64
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/BulkApplyTemplateModal-test.tsx.snap
@@ -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
index 00000000000..d46e88f161f
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ChangeVisibilityForm-test.tsx.snap
@@ -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
index 00000000000..fad19337758
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/CreateProjectForm-test.tsx.snap
@@ -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
index 00000000000..e57788fbc92
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/DeleteModal-test.tsx.snap
@@ -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
index 00000000000..9fb16cfbed5
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Header-test.tsx.snap
@@ -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
index 00000000000..b306b2fe020
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRow-test.tsx.snap
@@ -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
index 00000000000..14bb03d1ec6
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Projects-test.tsx.snap
@@ -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
index 00000000000..84838c7bc2e
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Search-test.tsx.snap
@@ -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/projects-admin/routes.ts b/server/sonar-web/src/main/js/apps/projectsManagement/routes.ts
index 447c6ae73dd..447c6ae73dd 100644
--- a/server/sonar-web/src/main/js/apps/projects-admin/routes.ts
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/routes.ts
diff --git a/server/sonar-web/src/main/js/apps/projects-admin/constants.js b/server/sonar-web/src/main/js/apps/projectsManagement/utils.ts
index 057d08d9109..4e3f01888b3 100644
--- a/server/sonar-web/src/main/js/apps/projects-admin/constants.js
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/utils.ts
@@ -21,8 +21,20 @@ export const PAGE_SIZE = 50;
export const QUALIFIERS_ORDER = ['TRK', 'VW', 'APP', 'DEV'];
-export const TYPE = {
- ALL: 'ALL',
- PROVISIONED: 'PROVISIONED',
- GHOSTS: 'GHOSTS'
-};
+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'
+}