Browse Source

SONAR-9784 rewrite projects management page

tags/6.6-RC1
Stas Vilchik 6 years ago
parent
commit
71fec25c40
57 changed files with 3895 additions and 770 deletions
  1. 2
    2
      server/sonar-web/src/main/js/api/components.ts
  2. 24
    4
      server/sonar-web/src/main/js/api/permissions.ts
  3. 2
    2
      server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.js
  4. 1
    1
      server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.js.snap
  5. 120
    0
      server/sonar-web/src/main/js/app/types.ts
  6. 6
    2
      server/sonar-web/src/main/js/app/utils/startReactApp.js
  7. 1
    1
      server/sonar-web/src/main/js/apps/organizations/components/OrganizationProjectsManagement.js
  8. 1
    1
      server/sonar-web/src/main/js/apps/project-admin/deletion/Form.js
  9. 0
    60
      server/sonar-web/src/main/js/apps/projects-admin/__tests__/projects-test.js
  10. 0
    37
      server/sonar-web/src/main/js/apps/projects-admin/form-view.js
  11. 0
    68
      server/sonar-web/src/main/js/apps/projects-admin/templates/BulkApplyTemplateTemplate.hbs
  12. 0
    13
      server/sonar-web/src/main/js/apps/projects-admin/templates/projects-delete.hbs
  13. 0
    121
      server/sonar-web/src/main/js/apps/projects-admin/views/BulkApplyTemplateView.js
  14. 83
    124
      server/sonar-web/src/main/js/apps/projectsManagement/App.tsx
  15. 30
    20
      server/sonar-web/src/main/js/apps/projectsManagement/AppContainer.tsx
  16. 217
    0
      server/sonar-web/src/main/js/apps/projectsManagement/BulkApplyTemplateModal.tsx
  17. 25
    32
      server/sonar-web/src/main/js/apps/projectsManagement/ChangeVisibilityForm.tsx
  18. 37
    48
      server/sonar-web/src/main/js/apps/projectsManagement/CreateProjectForm.tsx
  19. 105
    0
      server/sonar-web/src/main/js/apps/projectsManagement/DeleteModal.tsx
  20. 19
    22
      server/sonar-web/src/main/js/apps/projectsManagement/Header.tsx
  21. 32
    59
      server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx
  22. 68
    0
      server/sonar-web/src/main/js/apps/projectsManagement/Projects.tsx
  23. 90
    41
      server/sonar-web/src/main/js/apps/projectsManagement/Search.tsx
  24. 148
    0
      server/sonar-web/src/main/js/apps/projectsManagement/__tests__/App-test.tsx
  25. 128
    0
      server/sonar-web/src/main/js/apps/projectsManagement/__tests__/BulkApplyTemplateModal-test.tsx
  26. 73
    0
      server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ChangeVisibilityForm-test.tsx
  27. 72
    0
      server/sonar-web/src/main/js/apps/projectsManagement/__tests__/CreateProjectForm-test.tsx
  28. 63
    0
      server/sonar-web/src/main/js/apps/projectsManagement/__tests__/DeleteModal-test.tsx
  29. 64
    0
      server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Header-test.tsx
  30. 61
    0
      server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRow-test.tsx
  31. 67
    0
      server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Projects-test.tsx
  32. 107
    0
      server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Search-test.tsx
  33. 643
    0
      server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/BulkApplyTemplateModal-test.tsx.snap
  34. 308
    0
      server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ChangeVisibilityForm-test.tsx.snap
  35. 468
    0
      server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/CreateProjectForm-test.tsx.snap
  36. 98
    0
      server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/DeleteModal-test.tsx.snap
  37. 56
    0
      server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Header-test.tsx.snap
  38. 101
    0
      server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRow-test.tsx.snap
  39. 37
    0
      server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Projects-test.tsx.snap
  40. 234
    0
      server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Search-test.tsx.snap
  41. 0
    0
      server/sonar-web/src/main/js/apps/projectsManagement/routes.ts
  42. 17
    5
      server/sonar-web/src/main/js/apps/projectsManagement/utils.ts
  43. 7
    10
      server/sonar-web/src/main/js/components/common/PrivateBadge.tsx
  44. 11
    16
      server/sonar-web/src/main/js/components/common/VisibilitySelector.tsx
  45. 7
    12
      server/sonar-web/src/main/js/components/common/__tests__/PrivateBadge-test.tsx
  46. 48
    0
      server/sonar-web/src/main/js/components/common/__tests__/VisibilitySelector-test.tsx
  47. 14
    0
      server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/PrivateBadge-test.tsx.snap
  48. 104
    0
      server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/VisibilitySelector-test.tsx.snap
  49. 12
    23
      server/sonar-web/src/main/js/components/controls/RadioToggle.tsx
  50. 11
    13
      server/sonar-web/src/main/js/components/controls/__tests__/RadioToggle-test.tsx
  51. 38
    0
      server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/RadioToggle-test.tsx.snap
  52. 3
    2
      server/sonar-web/src/main/js/helpers/testUtils.ts
  53. 17
    8
      sonar-core/src/main/resources/org/sonar/l10n/core.properties
  54. 6
    9
      tests/src/test/java/org/sonarqube/pageobjects/ProjectsManagementPage.java
  55. 2
    2
      tests/src/test/java/org/sonarqube/tests/authorisation/ProvisioningPermissionTest.java
  56. 2
    7
      tests/src/test/resources/authorisation/ProvisioningPermissionTest/should-be-able-to-provision-project.html
  57. 5
    5
      tests/src/test/resources/authorisation/ProvisioningPermissionTest/should-not-be-able-to-provision-project.html

+ 2
- 2
server/sonar-web/src/main/js/api/components.ts View File

@@ -32,8 +32,8 @@ export function getGhosts(data: RequestData): Promise<any> {
return getJSON('/api/projects/ghosts', data);
}

export function deleteComponents(data: { projects: string; organization?: string }): Promise<void> {
return post('/api/projects/bulk_delete', data);
export function deleteComponents(projects: string[], organization: string): Promise<void> {
return post('/api/projects/bulk_delete', { projects: projects.join(), organization });
}

export function deleteProject(project: string): Promise<void> {

+ 24
- 4
server/sonar-web/src/main/js/api/permissions.ts View File

@@ -85,10 +85,30 @@ export function revokePermissionFromGroup(
return post('/api/permissions/remove_group', data);
}

/**
* Get list of permission templates
*/
export function getPermissionTemplates(organization?: string) {
export interface PermissionTemplate {
id: string;
name: string;
description?: string;
projectKeyPattern?: string;
createdAt: string;
updatedAt?: string;
permissions: Array<{
key: string;
usersCount: number;
groupsCount: number;
withProjectCreator?: boolean;
}>;
}

interface GetPermissionTemplatesResponse {
permissionTemplates: PermissionTemplate[];
defaultTemplates: Array<{ templateId: string; qualifier: string }>;
permissions: Array<{ key: string; name: string; description: string }>;
}

export function getPermissionTemplates(
organization?: string
): Promise<GetPermissionTemplatesResponse> {
const url = '/api/permissions/search_templates';
return organization ? getJSON(url, { organization }) : getJSON(url);
}

+ 2
- 2
server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.js View File

@@ -42,7 +42,7 @@ class SettingsNav extends React.PureComponent {
}

isProjectsActive() {
const urls = ['/projects_admin', '/background_tasks'];
const urls = ['/admin/projects_management', '/background_tasks'];
return this.isSomethingActive(urls);
}

@@ -158,7 +158,7 @@ class SettingsNav extends React.PureComponent {
<ul className="dropdown-menu">
{!this.props.customOrganizations &&
<li>
<IndexLink to="/projects_admin" activeClassName="active">
<IndexLink to="/admin/projects_management" activeClassName="active">
Management
</IndexLink>
</li>}

+ 1
- 1
server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.js.snap View File

@@ -154,7 +154,7 @@ exports[`should work with extensions 1`] = `
<li>
<IndexLink
activeClassName="active"
to="/projects_admin"
to="/admin/projects_management"
>
Management
</IndexLink>

+ 120
- 0
server/sonar-web/src/main/js/app/types.ts View File

@@ -0,0 +1,120 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
export enum BranchType {
LONG = 'LONG',
SHORT = 'SHORT'
}

export interface MainBranch {
isMain: true;
name: string;
status?: {
qualityGateStatus: string;
};
}

export interface LongLivingBranch {
isMain: false;
name: string;
status?: {
qualityGateStatus: string;
};
type: BranchType.LONG;
}

export interface ShortLivingBranch {
isMain: false;
isOrphan?: true;
mergeBranch: string;
name: string;
status?: {
bugs: number;
codeSmells: number;
vulnerabilities: number;
};
type: BranchType.SHORT;
}

export type Branch = MainBranch | LongLivingBranch | ShortLivingBranch;

export interface ComponentExtension {
key: string;
name: string;
}

export interface Component {
analysisDate?: string;
breadcrumbs: Array<{
key: string;
name: string;
qualifier: string;
}>;
configuration?: ComponentConfiguration;
extensions?: ComponentExtension[];
isFavorite?: boolean;
key: string;
name: string;
organization: string;
path?: string;
qualifier: string;
refKey?: string;
version?: string;
}

export interface ComponentConfiguration {
extensions?: ComponentExtension[];
showBackgroundTasks?: boolean;
showLinks?: boolean;
showManualMeasures?: boolean;
showQualityGates?: boolean;
showQualityProfiles?: boolean;
showPermissions?: boolean;
showSettings?: boolean;
showUpdateKey?: boolean;
}

export interface Metric {
custom?: boolean;
decimalScale?: number;
description?: string;
direction?: number;
domain?: string;
hidden?: boolean;
key: string;
name: string;
qualitative?: boolean;
type: string;
}

export interface Organization {
adminPages?: Array<{ key: string; name: string }>;
avatar?: string;
canAdmin?: boolean;
canDelete?: boolean;
canProvisionProjects?: boolean;
canUpdateProjectsVisibilityToPrivate?: boolean;
description?: string;
isDefault?: boolean;
key: string;
name: string;
pages?: Array<{ key: string; name: string }>;
projectVisibility: string;
url?: string;
}

+ 6
- 2
server/sonar-web/src/main/js/app/utils/startReactApp.js View File

@@ -56,7 +56,7 @@ import permissionTemplatesRoutes from '../../apps/permission-templates/routes';
import projectActivityRoutes from '../../apps/projectActivity/routes';
import projectAdminRoutes from '../../apps/project-admin/routes';
import projectsRoutes from '../../apps/projects/routes';
import projectsAdminRoutes from '../../apps/projects-admin/routes';
import projectsManagementRoutes from '../../apps/projectsManagement/routes';
import qualityGatesRoutes from '../../apps/quality-gates/routes';
import qualityProfilesRoutes from '../../apps/quality-profiles/routes';
import sessionsRoutes from '../../apps/sessions/routes';
@@ -115,6 +115,7 @@ const startReactApp = () => {
}}
/>

<Redirect from="/projects_admin" to="/admin/projects_management" />
<Redirect from="/component/index" to="/component" />
<Redirect from="/component_issues" to="/project/issues" />
<Redirect from="/dashboard/index" to="/dashboard" />
@@ -203,7 +204,10 @@ const startReactApp = () => {
<Route path="groups" childRoutes={groupsRoutes} />
<Route path="metrics" childRoutes={metricsRoutes} />
<Route path="permission_templates" childRoutes={permissionTemplatesRoutes} />
<Route path="projects_admin" childRoutes={projectsAdminRoutes} />
<Route
path="admin/projects_management"
childRoutes={projectsManagementRoutes}
/>
<Route path="roles/global" childRoutes={globalPermissionsRoutes} />
<Route path="settings" childRoutes={settingsRoutes} />
<Route path="system" childRoutes={systemRoutes} />

+ 1
- 1
server/sonar-web/src/main/js/apps/organizations/components/OrganizationProjectsManagement.js View File

@@ -20,7 +20,7 @@
// @flow
import React from 'react';
import { connect } from 'react-redux';
import AppContainer from '../../projects-admin/AppContainer';
import AppContainer from '../../projectsManagement/AppContainer';
import { getOrganizationByKey } from '../../../store/rootReducer';
/*:: import type { Organization } from '../../../store/organizations/duck'; */


+ 1
- 1
server/sonar-web/src/main/js/apps/project-admin/deletion/Form.js View File

@@ -87,7 +87,7 @@ export default class Form extends React.PureComponent {
<form onSubmit={this.handleSubmit}>
<div className="modal-head">
<h2>
{translate('qualifiers.delete.TRK')}
{translate('qualifier.delete.TRK')}
</h2>
</div>
<div className="modal-body">

+ 0
- 60
server/sonar-web/src/main/js/apps/projects-admin/__tests__/projects-test.js View File

@@ -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);
});

+ 0
- 37
server/sonar-web/src/main/js/apps/projects-admin/form-view.js View File

@@ -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();
}
});

+ 0
- 68
server/sonar-web/src/main/js/apps/projects-admin/templates/BulkApplyTemplateTemplate.hbs View File

@@ -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>

+ 0
- 13
server/sonar-web/src/main/js/apps/projects-admin/templates/projects-delete.hbs View File

@@ -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>

+ 0
- 121
server/sonar-web/src/main/js/apps/projects-admin/views/BulkApplyTemplateView.js View File

@@ -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
};
}
});

server/sonar-web/src/main/js/apps/projects-admin/main.js → server/sonar-web/src/main/js/apps/projectsManagement/App.tsx View File

@@ -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>

server/sonar-web/src/main/js/apps/projects-admin/AppContainer.js → server/sonar-web/src/main/js/apps/projectsManagement/AppContainer.tsx View File

@@ -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);

+ 217
- 0
server/sonar-web/src/main/js/apps/projectsManagement/BulkApplyTemplateModal.tsx View File

@@ -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>
);
}
}

server/sonar-web/src/main/js/apps/projects-admin/ChangeVisibilityForm.js → server/sonar-web/src/main/js/apps/projectsManagement/ChangeVisibilityForm.tsx View File

@@ -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>

server/sonar-web/src/main/js/apps/projects-admin/CreateProjectForm.js → server/sonar-web/src/main/js/apps/projectsManagement/CreateProjectForm.tsx View File

@@ -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>}

+ 105
- 0
server/sonar-web/src/main/js/apps/projectsManagement/DeleteModal.tsx View File

@@ -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>
);
}
}

server/sonar-web/src/main/js/apps/projects-admin/header.js → server/sonar-web/src/main/js/apps/projectsManagement/Header.tsx View File

@@ -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>

server/sonar-web/src/main/js/apps/projects-admin/projects.js → server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx View File

@@ -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>
);
}
}

+ 68
- 0
server/sonar-web/src/main/js/apps/projectsManagement/Projects.tsx View File

@@ -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>
);
}
}

server/sonar-web/src/main/js/apps/projects-admin/search.js → server/sonar-web/src/main/js/apps/projectsManagement/Search.tsx View File

@@ -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>
);
}

+ 148
- 0
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/App-test.tsx View File

@@ -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}
/>
);
}

+ 128
- 0
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/BulkApplyTemplateModal-test.tsx View File

@@ -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}
/>
);
}

+ 73
- 0
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ChangeVisibilityForm-test.tsx View File

@@ -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}
/>
);
}

+ 72
- 0
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/CreateProjectForm-test.tsx View File

@@ -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();
});

+ 63
- 0
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/DeleteModal-test.tsx View File

@@ -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}
/>
);
}

+ 64
- 0
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Header-test.tsx View File

@@ -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}
/>
);
}

+ 61
- 0
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRow-test.tsx View File

@@ -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}
/>
);
}

+ 67
- 0
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Projects-test.tsx View File

@@ -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}
/>
);
}

+ 107
- 0
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Search-test.tsx View File

@@ -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}
/>
);
}

+ 643
- 0
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/BulkApplyTemplateModal-test.tsx.snap View File

@@ -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>
`;

+ 308
- 0
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ChangeVisibilityForm-test.tsx.snap View File

@@ -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>
`;

+ 468
- 0
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/CreateProjectForm-test.tsx.snap View File

@@ -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>
`;

+ 98
- 0
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/DeleteModal-test.tsx.snap View File

@@ -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>
`;

+ 56
- 0
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Header-test.tsx.snap View File

@@ -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>
`;

+ 101
- 0
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRow-test.tsx.snap View File

@@ -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>
`;

+ 37
- 0
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Projects-test.tsx.snap View File

@@ -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>
`;

+ 234
- 0
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Search-test.tsx.snap View File

@@ -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>
`;

server/sonar-web/src/main/js/apps/projects-admin/routes.ts → server/sonar-web/src/main/js/apps/projectsManagement/routes.ts View File


server/sonar-web/src/main/js/apps/projects-admin/constants.js → server/sonar-web/src/main/js/apps/projectsManagement/utils.ts View File

@@ -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'
}

server/sonar-web/src/main/js/components/common/PrivateBadge.js → server/sonar-web/src/main/js/components/common/PrivateBadge.tsx View File

@@ -17,20 +17,17 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import classNames from 'classnames';
import * as React from 'react';
import * as classNames from 'classnames';
import Tooltip from '../controls/Tooltip';
import { translate } from '../../helpers/l10n';

/*::
type Props = {
className?: string,
tooltipPlacement?: string
};
*/
interface Props {
className?: string;
tooltipPlacement?: string;
}

export default function PrivateBadge({ className, tooltipPlacement = 'bottom' } /*: Props */) {
export default function PrivateBadge({ className, tooltipPlacement = 'bottom' }: Props) {
return (
<Tooltip overlay={translate('visibility.private.description')} placement={tooltipPlacement}>
<div className={classNames('outline-badge', className)}>

server/sonar-web/src/main/js/components/common/VisibilitySelector.js → server/sonar-web/src/main/js/components/common/VisibilitySelector.tsx View File

@@ -17,30 +17,25 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import classNames from 'classnames';
import * as React from 'react';
import * as classNames from 'classnames';
import { translate } from '../../helpers/l10n';

/*::
type Props = {|
canTurnToPrivate: boolean,
className?: string,
onChange: string => void,
visibility: string
|};
*/

export default class VisibilitySelector extends React.PureComponent {
/*:: props: Props; */
interface Props {
canTurnToPrivate?: boolean;
className?: string;
onChange: (x: string) => void;
visibility: string;
}

handlePublicClick = (event /*: Event & { currentTarget: HTMLElement } */) => {
export default class VisibilitySelector extends React.PureComponent<Props> {
handlePublicClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
event.preventDefault();
event.currentTarget.blur();
this.props.onChange('public');
};

handlePrivateClick = (event /*: Event & { currentTarget: HTMLElement } */) => {
handlePrivateClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
event.preventDefault();
event.currentTarget.blur();
this.props.onChange('private');

server/sonar-web/src/main/js/apps/projects-admin/delete-view.js → server/sonar-web/src/main/js/components/common/__tests__/PrivateBadge-test.tsx View File

@@ -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,15 +17,10 @@
* 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';
import * as React from 'react';
import { shallow } from 'enzyme';
import PrivateBadge from '../PrivateBadge';

export default ModalForm.extend({
template: Template,

onFormSubmit() {
ModalForm.prototype.onFormSubmit.apply(this, arguments);
this.options.deleteProjects();
this.destroy();
}
it('renders', () => {
expect(shallow(<PrivateBadge />)).toMatchSnapshot();
});

+ 48
- 0
server/sonar-web/src/main/js/components/common/__tests__/VisibilitySelector-test.tsx View File

@@ -0,0 +1,48 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { shallow } from 'enzyme';
import VisibilitySelector from '../VisibilitySelector';
import { click } from '../../../helpers/testUtils';

it('changes visibility', () => {
const onChange = jest.fn();
const wrapper = shallow(
<VisibilitySelector canTurnToPrivate={true} onChange={onChange} visibility="public" />
);
expect(wrapper).toMatchSnapshot();

click(wrapper.find('#visibility-private'));
expect(onChange).toBeCalledWith('private');

wrapper.setProps({ visibility: 'private' });
expect(wrapper).toMatchSnapshot();

click(wrapper.find('#visibility-public'));
expect(onChange).toBeCalledWith('public');
});

it('renders disabled', () => {
expect(
shallow(
<VisibilitySelector canTurnToPrivate={false} onChange={jest.fn()} visibility="public" />
)
).toMatchSnapshot();
});

+ 14
- 0
server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/PrivateBadge-test.tsx.snap View File

@@ -0,0 +1,14 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders 1`] = `
<Tooltip
overlay="visibility.private.description"
placement="bottom"
>
<div
className="outline-badge"
>
visibility.private
</div>
</Tooltip>
`;

+ 104
- 0
server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/VisibilitySelector-test.tsx.snap View File

@@ -0,0 +1,104 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`changes visibility 1`] = `
<div>
<a
className="link-base-color link-no-underline"
href="#"
id="visibility-public"
onClick={[Function]}
>
<i
className="icon-radio is-checked"
/>
<span
className="spacer-left"
>
visibility.public
</span>
</a>
<a
className="link-base-color link-no-underline huge-spacer-left"
href="#"
id="visibility-private"
onClick={[Function]}
>
<i
className="icon-radio"
/>
<span
className="spacer-left"
>
visibility.private
</span>
</a>
</div>
`;

exports[`changes visibility 2`] = `
<div>
<a
className="link-base-color link-no-underline"
href="#"
id="visibility-public"
onClick={[Function]}
>
<i
className="icon-radio"
/>
<span
className="spacer-left"
>
visibility.public
</span>
</a>
<a
className="link-base-color link-no-underline huge-spacer-left"
href="#"
id="visibility-private"
onClick={[Function]}
>
<i
className="icon-radio is-checked"
/>
<span
className="spacer-left"
>
visibility.private
</span>
</a>
</div>
`;

exports[`renders disabled 1`] = `
<div>
<a
className="link-base-color link-no-underline"
href="#"
id="visibility-public"
onClick={[Function]}
>
<i
className="icon-radio is-checked"
/>
<span
className="spacer-left"
>
visibility.public
</span>
</a>
<span
className="huge-spacer-left text-muted cursor-not-allowed"
id="visibility-private"
>
<i
className="icon-radio"
/>
<span
className="spacer-left"
>
visibility.private
</span>
</span>
</div>
`;

server/sonar-web/src/main/js/components/controls/RadioToggle.js → server/sonar-web/src/main/js/components/controls/RadioToggle.tsx View File

@@ -17,38 +17,27 @@
* 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';

export default class RadioToggle extends React.PureComponent {
static propTypes = {
value: PropTypes.string,
options: PropTypes.arrayOf(
PropTypes.shape({
value: PropTypes.string.isRequired,
label: PropTypes.string.isRequired
})
).isRequired,
name: PropTypes.string.isRequired,
onCheck: PropTypes.func.isRequired
};
interface Props {
name: string;
onCheck: (value: string) => void;
options: Array<{ label: string; value: string }>;
value?: string;
}

export default class RadioToggle extends React.PureComponent<Props> {
static defaultProps = {
disabled: false,
value: null
};

componentWillMount() {
this.renderOption = this.renderOption.bind(this);
this.handleChange = this.handleChange.bind(this);
}

handleChange(e) {
handleChange = (e: React.SyntheticEvent<HTMLInputElement>) => {
const newValue = e.currentTarget.value;
this.props.onCheck(newValue);
}
};

renderOption(option) {
renderOption = (option: { label: string; value: string }) => {
const checked = option.value === this.props.value;
const htmlId = this.props.name + '__' + option.value;
return (
@@ -67,7 +56,7 @@ export default class RadioToggle extends React.PureComponent {
</label>
</li>
);
}
};

render() {
return (

server/sonar-web/src/main/js/components/controls/__tests__/RadioToggle-test.js → server/sonar-web/src/main/js/components/controls/__tests__/RadioToggle-test.tsx View File

@@ -17,25 +17,23 @@
* 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 React from 'react';
import RadioToggle from '../RadioToggle';
import { change } from '../../../helpers/testUtils';

function getSample(props) {
const options = [{ value: 'one', label: 'first' }, { value: 'two', label: 'second' }];
return <RadioToggle options={options} name="sample" onCheck={() => true} {...props} />;
}

it('should render', () => {
const radioToggle = shallow(getSample());
expect(radioToggle.find('input[type="radio"]').length).toBe(2);
expect(radioToggle.find('label').length).toBe(2);
it('renders', () => {
expect(shallow(getSample())).toMatchSnapshot();
});

it('should call onCheck', () => {
it('calls onCheck', () => {
const onCheck = jest.fn();
const radioToggle = shallow(getSample({ onCheck }));
change(radioToggle.find('input[value="two"]'), 'two');
const wrapper = shallow(getSample({ onCheck }));
change(wrapper.find('input[value="two"]'), 'two');
expect(onCheck).toBeCalledWith('two');
});

function getSample(props?: any) {
const options = [{ value: 'one', label: 'first' }, { value: 'two', label: 'second' }];
return <RadioToggle options={options} name="sample" onCheck={() => true} {...props} />;
}

+ 38
- 0
server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/RadioToggle-test.tsx.snap View File

@@ -0,0 +1,38 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders 1`] = `
<ul
className="radio-toggle"
>
<li>
<input
checked={false}
id="sample__one"
name="sample"
onChange={[Function]}
type="radio"
value="one"
/>
<label
htmlFor="sample__one"
>
first
</label>
</li>
<li>
<input
checked={false}
id="sample__two"
name="sample"
onChange={[Function]}
type="radio"
value="two"
/>
<label
htmlFor="sample__two"
>
second
</label>
</li>
</ul>
`;

+ 3
- 2
server/sonar-web/src/main/js/helpers/testUtils.ts View File

@@ -42,10 +42,11 @@ export function submit(element: ShallowWrapper): void {
});
}

export function change(element: ShallowWrapper, value: string): void {
export function change(element: ShallowWrapper, value: string, event = {}): void {
element.simulate('change', {
target: { value },
currentTarget: { value }
currentTarget: { value },
...event
});
}


+ 17
- 8
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -422,15 +422,21 @@ qualifiers.new.VW=New Portfolio
qualifiers.new.DEV=New Developer
qualifiers.new.APP=New Application

qualifiers.delete.TRK=Delete Project
qualifiers.delete.VW=Delete Portfolio
qualifiers.delete.DEV=Delete Developer
qualifiers.delete.APP=Delete Application
qualifier.delete.TRK=Delete Project
qualifier.delete.VW=Delete Portfolio
qualifier.delete.APP=Delete Application

qualifiers.delete_confirm.TRK=Do you want to delete this project?
qualifiers.delete_confirm.VW=Do you want to delete this portfolio?
qualifiers.delete_confirm.DEV=Do you want to delete this developer?
qualifiers.delete_confirm.APP=Do you want to delete this application?
qualifiers.delete.TRK=Delete Projects
qualifiers.delete.VW=Delete Portfolios
qualifiers.delete.APP=Delete Applications

qualifier.delete_confirm.TRK=Do you want to delete this project?
qualifier.delete_confirm.VW=Do you want to delete this portfolio?
qualifier.delete_confirm.APP=Do you want to delete this application?

qualifiers.delete_confirm.TRK=Do you want to delete these projects?
qualifiers.delete_confirm.VW=Do you want to delete these portfolios?
qualifiers.delete_confirm.APP=Do you want to delete these applications?

qualifiers.create.TRK=Create Project
qualifiers.create.VW=Create Portfolio
@@ -1398,6 +1404,7 @@ project_quality_gate.successfully_updated=Quality gate has been successfully upd
#------------------------------------------------------------------------------

project_deletion.delete_resource_confirmation=Are you sure you want to delete "{0}"?
projects_management.delete_resource_confirmation=Are you sure you want to delete "{0}"?


#------------------------------------------------------------------------------
@@ -2596,6 +2603,8 @@ permission_templates.project_creators=Project Creators
permission_templates.project_creators.explanation=When a new project is created, the user who creates the project will receive this permission on the project.
permission_templates.grant_permission_to_project_creators=Grant the "{0}" permission to project creators
permission_templates.bulk_apply_permission_template=Bulk Apply Permission Template
permission_templates.bulk_apply_permission_template.apply_to_selected=You're about to apply the selected permission template to {0} selected item(s).
permission_templates.bulk_apply_permission_template.apply_to_all=You're about to apply the selected permission template to {0} item(s).


#------------------------------------------------------------------------------

+ 6
- 9
tests/src/test/java/org/sonarqube/pageobjects/ProjectsManagementPage.java View File

@@ -19,10 +19,9 @@
*/
package org.sonarqube.pageobjects;

import com.codeborne.selenide.CollectionCondition;

import static com.codeborne.selenide.Condition.exist;
import static com.codeborne.selenide.Condition.text;
import static com.codeborne.selenide.Condition.visible;
import static com.codeborne.selenide.Selenide.$;
import static com.codeborne.selenide.Selenide.$$;

@@ -52,13 +51,11 @@ public class ProjectsManagementPage {
}

public ProjectsManagementPage bulkApplyPermissionTemplate(String template) {
$(".js-bulk-apply-permission-template").should(exist).click();
$(".modal .select2-choice").should(exist).click();
$$(".select2-results li")
.shouldHave(CollectionCondition.sizeGreaterThan(0))
.findBy(text("foo-template")).should(exist).click();
$(".modal .js-apply").should(exist).click();
$(".modal-body .alert-success").should(exist);
$(".js-bulk-apply-permission-template").click();
$(".modal .Select-value").click();
$$(".modal .Select-option").findBy(text(template)).click();
$(".modal-foot button").click();
$(".modal-body .alert-success").shouldBe(visible);
return this;
}
}

+ 2
- 2
tests/src/test/java/org/sonarqube/tests/authorisation/ProvisioningPermissionTest.java View File

@@ -95,7 +95,7 @@ public class ProvisioningPermissionTest {
* SONAR-4709
*/
@Test
public void organization_administrator_cannot_provision_project_if_he_doesnt_have_provisioning_permission() {
public void organization_administrator_cannot_provision_project_if_he_does_not_have_provisioning_permission() {
runSelenese(orchestrator, "/authorisation/ProvisioningPermissionTest/should-not-be-able-to-provision-project.html");
}

@@ -131,7 +131,7 @@ public class ProvisioningPermissionTest {
* SONAR-4709
*/
@Test
public void user_cannot_provision_project_through_ws_if_he_doesnt_have_provisioning_permission() {
public void user_cannot_provision_project_through_ws_if_he_does_not_have_provisioning_permission() {
thrown.expect(HttpException.class);
thrown.expectMessage("403");


+ 2
- 7
tests/src/test/resources/authorisation/ProvisioningPermissionTest/should-be-able-to-provision-project.html View File

@@ -36,19 +36,14 @@
</tr>
<tr>
<td>open</td>
<td>/projects_admin</td>
<td>/admin/projects_management</td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>css=#projects-type__ALL</td>
<td>css=#create-project</td>
<td></td>
</tr>
<tr>
<td>assertText</td>
<td>css=.page-actions button</td>
<td>*Create Project*</td>
</tr>
</tbody>
</table>
</body>

+ 5
- 5
tests/src/test/resources/authorisation/ProvisioningPermissionTest/should-not-be-able-to-provision-project.html View File

@@ -35,18 +35,18 @@
</tr>
<tr>
<td>open</td>
<td>/projects_admin</td>
<td>/admin/projects_management</td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>css=#projects-type__ALL</td>
<td>css=#projects-management-page</td>
<td></td>
</tr>
<tr>
<td>assertNotText</td>
<td>css=.page-actions button</td>
<td>*Create Project*</td>
<td>assertElementNotPresent</td>
<td>css=#create-project</td>
<td></td>
</tr>
</tbody>
</table>

Loading…
Cancel
Save