aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorStas Vilchik <stas.vilchik@sonarsource.com>2017-09-06 17:53:37 +0200
committerStas Vilchik <stas.vilchik@sonarsource.com>2017-09-11 11:28:29 +0200
commit3ac9340f013701017f8dbab5b0686d2682f1768f (patch)
tree2be2f877ee0a92fe2e3ed652c730474dc239ba4a
parent048982bb3d8d5b2c715f95bc7818c90314e72a14 (diff)
downloadsonarqube-3ac9340f013701017f8dbab5b0686d2682f1768f.tar.gz
sonarqube-3ac9340f013701017f8dbab5b0686d2682f1768f.zip
SONAR-9181 bulk delete projects or bulk apply template in one go
-rw-r--r--server/sonar-web/src/main/js/api/components.ts41
-rw-r--r--server/sonar-web/src/main/js/api/permissions.ts3
-rw-r--r--server/sonar-web/src/main/js/app/types.ts11
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/App.tsx45
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/BulkApplyTemplateModal.tsx68
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/ChangeVisibilityForm.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/DeleteModal.tsx35
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/Header.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/Search.tsx13
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/__tests__/App-test.tsx13
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/__tests__/BulkApplyTemplateModal-test.tsx29
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/__tests__/DeleteModal-test.tsx38
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Header-test.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRow-test.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Projects-test.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Search-test.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/BulkApplyTemplateModal-test.tsx.snap20
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/DeleteModal-test.tsx.snap133
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Search-test.tsx.snap11
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/utils.ts15
-rw-r--r--server/sonar-web/src/main/less/components/alerts.less8
-rw-r--r--sonar-core/src/main/resources/org/sonar/l10n/core.properties4
23 files changed, 353 insertions, 151 deletions
diff --git a/server/sonar-web/src/main/js/api/components.ts b/server/sonar-web/src/main/js/api/components.ts
index 572875aa790..50faefec07d 100644
--- a/server/sonar-web/src/main/js/api/components.ts
+++ b/server/sonar-web/src/main/js/api/components.ts
@@ -19,17 +19,46 @@
*/
import { getJSON, postJSON, post, RequestData } from '../helpers/request';
import throwGlobalError from '../app/utils/throwGlobalError';
+import { Paging, Visibility } from '../app/types';
-export function getComponents(data: RequestData): Promise<any> {
- return getJSON('/api/projects/search', data);
+export interface BaseSearchProjectsParameters {
+ analyzedBefore?: string;
+ onProvisionedOnly?: boolean;
+ organization: string;
+ projects?: string;
+ q?: string;
+ qualifiers?: string;
+ visibility?: Visibility;
+}
+
+export interface SearchProjectsParameters extends BaseSearchProjectsParameters {
+ p?: number;
+ ps?: number;
+}
+
+export interface SearchProjectsResponseComponent {
+ id: string;
+ key: string;
+ lastAnalysisDate?: string;
+ name: string;
+ organization: string;
+ qualifier: string;
+ visibility: Visibility;
+}
+
+export interface SearchProjectsResponse {
+ components: SearchProjectsResponseComponent[];
+ paging: Paging;
}
-export function getProvisioned(data: RequestData): Promise<any> {
- return getJSON('/api/projects/provisioned', data);
+export function getComponents(
+ parameters: SearchProjectsParameters
+): Promise<SearchProjectsResponse> {
+ return getJSON('/api/projects/search', parameters);
}
-export function deleteComponents(projects: string[], organization: string): Promise<void> {
- return post('/api/projects/bulk_delete', { projects: projects.join(), organization });
+export function bulkDeleteProjects(parameters: BaseSearchProjectsParameters): Promise<void> {
+ return post('/api/projects/bulk_delete', parameters);
}
export function deleteProject(project: string): Promise<void> {
diff --git a/server/sonar-web/src/main/js/api/permissions.ts b/server/sonar-web/src/main/js/api/permissions.ts
index 5c927bc3692..8d2cf3e5596 100644
--- a/server/sonar-web/src/main/js/api/permissions.ts
+++ b/server/sonar-web/src/main/js/api/permissions.ts
@@ -18,6 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { getJSON, post, postJSON, RequestData } from '../helpers/request';
+import { BaseSearchProjectsParameters } from './components';
const PAGE_SIZE = 100;
@@ -136,7 +137,7 @@ export function applyTemplateToProject(data: RequestData): Promise<void> {
return post('/api/permissions/apply_template', data);
}
-export function bulkApplyTemplate(data: RequestData): Promise<void> {
+export function bulkApplyTemplate(data: BaseSearchProjectsParameters): Promise<void> {
return post('/api/permissions/bulk_apply_template', data);
}
diff --git a/server/sonar-web/src/main/js/app/types.ts b/server/sonar-web/src/main/js/app/types.ts
index d5ff4713845..2d0bc46db36 100644
--- a/server/sonar-web/src/main/js/app/types.ts
+++ b/server/sonar-web/src/main/js/app/types.ts
@@ -118,3 +118,14 @@ export interface Organization {
projectVisibility: string;
url?: string;
}
+
+export interface Paging {
+ pageIndex: number;
+ pageSize: number;
+ total: number;
+}
+
+export enum Visibility {
+ Public = 'public',
+ Private = 'private'
+}
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/App.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/App.tsx
index da819c20f4f..6ede35e8917 100644
--- a/server/sonar-web/src/main/js/apps/projectsManagement/App.tsx
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/App.tsx
@@ -26,7 +26,7 @@ import Projects from './Projects';
import CreateProjectForm from './CreateProjectForm';
import ListFooter from '../../components/controls/ListFooter';
import { PAGE_SIZE, Project } from './utils';
-import { getComponents, getProvisioned } from '../../api/components';
+import { getComponents } from '../../api/components';
import { Organization } from '../../app/types';
import { translate } from '../../helpers/l10n';
@@ -78,38 +78,17 @@ export default class App extends React.PureComponent<Props, State> {
this.mounted = false;
}
- getFilters = () => ({
- analyzedBefore: this.state.analyzedBefore,
- 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 = () =>
- this.state.provisioned ? this.requestProvisioned() : this.requestAllProjects();
-
- requestProvisioned = () => {
- const data = this.getFilters();
- getProvisioned(data).then(r => {
- if (this.mounted) {
- let projects: Project[] = r.projects.map((project: any) => ({
- ...project,
- id: project.uuid,
- qualifier: 'TRK'
- }));
- if (this.state.page > 1) {
- projects = [...this.state.projects, ...projects];
- }
- this.setState({ ready: true, projects, selection: [], total: r.paging.total });
- }
- });
- };
-
- requestAllProjects = () => {
- const data = this.getFilters();
- Object.assign(data, { qualifiers: this.state.qualifiers });
- getComponents(data).then(r => {
+ requestProjects = () => {
+ const parameters = {
+ analyzedBefore: this.state.analyzedBefore,
+ onProvisionedOnly: this.state.provisioned || undefined,
+ organization: this.props.organization.key,
+ p: this.state.page !== 1 ? this.state.page : undefined,
+ ps: PAGE_SIZE,
+ q: this.state.query || undefined,
+ qualifiers: this.state.qualifiers
+ };
+ getComponents(parameters).then(r => {
if (this.mounted) {
let projects: Project[] = r.components;
if (this.state.page > 1) {
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/BulkApplyTemplateModal.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/BulkApplyTemplateModal.tsx
index 60c55b7f5fc..60a361c1dbb 100644
--- a/server/sonar-web/src/main/js/apps/projectsManagement/BulkApplyTemplateModal.tsx
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/BulkApplyTemplateModal.tsx
@@ -22,13 +22,14 @@ import Modal from 'react-modal';
import * as Select from 'react-select';
import {
getPermissionTemplates,
- PermissionTemplate,
bulkApplyTemplate,
- applyTemplateToProject
+ PermissionTemplate
} from '../../api/permissions';
import { translate, translateWithParameters } from '../../helpers/l10n';
+import AlertWarnIcon from '../../components/icons-components/AlertWarnIcon';
export interface Props {
+ analyzedBefore?: string;
onClose: () => void;
organization: string;
provisioned: boolean;
@@ -80,32 +81,6 @@ export default class BulkApplyTemplateModal extends React.PureComponent<Props, S
);
}
- 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();
@@ -115,10 +90,21 @@ export default class BulkApplyTemplateModal extends React.PureComponent<Props, S
const { permissionTemplate } = this.state;
if (permissionTemplate) {
this.setState({ submitting: true });
- const request = this.props.selection.length
- ? this.bulkApplyToSelected(permissionTemplate)
- : this.bulkApplyToAll(permissionTemplate);
- request.then(
+ const parameters = this.props.selection.length
+ ? {
+ organization: this.props.organization,
+ projects: this.props.selection.join(),
+ templateId: permissionTemplate
+ }
+ : {
+ analyzedBefore: this.props.analyzedBefore,
+ onProvisionedOnly: this.props.provisioned || undefined,
+ organization: this.props.organization,
+ qualifiers: this.props.qualifier,
+ q: this.props.query || undefined,
+ templateId: permissionTemplate
+ };
+ bulkApplyTemplate(parameters).then(
() => {
if (this.mounted) {
this.setState({ done: true, submitting: false });
@@ -137,21 +123,19 @@ export default class BulkApplyTemplateModal extends React.PureComponent<Props, S
this.setState({ permissionTemplate: value });
};
- renderWarning = () => {
- return this.props.selection.length
- ? <div className="alert alert-info">
- {translateWithParameters(
+ renderWarning = () =>
+ <div className="alert alert-warning modal-alert">
+ <AlertWarnIcon className="spacer-right" />
+ {this.props.selection.length
+ ? translateWithParameters(
'permission_templates.bulk_apply_permission_template.apply_to_selected',
this.props.selection.length
- )}
- </div>
- : <div className="alert alert-warning">
- {translateWithParameters(
+ )
+ : translateWithParameters(
'permission_templates.bulk_apply_permission_template.apply_to_all',
this.props.total
)}
- </div>;
- };
+ </div>;
renderSelect = () =>
<div className="modal-field">
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/ChangeVisibilityForm.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/ChangeVisibilityForm.tsx
index 9c04349c688..41261da71e3 100644
--- a/server/sonar-web/src/main/js/apps/projectsManagement/ChangeVisibilityForm.tsx
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/ChangeVisibilityForm.tsx
@@ -20,10 +20,9 @@
import * as React from 'react';
import Modal from 'react-modal';
import * as classNames from 'classnames';
-import { Organization } from '../../app/types';
+import { Organization, Visibility } from '../../app/types';
import UpgradeOrganizationBox from '../../components/common/UpgradeOrganizationBox';
import { translate } from '../../helpers/l10n';
-import { Visibility } from './utils';
export interface Props {
onClose: () => void;
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/DeleteModal.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/DeleteModal.tsx
index 7b4a45fc61b..c04cd202ab4 100644
--- a/server/sonar-web/src/main/js/apps/projectsManagement/DeleteModal.tsx
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/DeleteModal.tsx
@@ -19,15 +19,20 @@
*/
import * as React from 'react';
import Modal from 'react-modal';
-import { deleteComponents } from '../../api/components';
-import { translate } from '../../helpers/l10n';
+import { bulkDeleteProjects } from '../../api/components';
+import { translate, translateWithParameters } from '../../helpers/l10n';
+import AlertWarnIcon from '../../components/icons-components/AlertWarnIcon';
export interface Props {
+ analyzedBefore?: string;
onClose: () => void;
onConfirm: () => void;
organization: string;
+ provisioned: boolean;
qualifier: string;
+ query: string;
selection: string[];
+ total: number;
}
interface State {
@@ -53,7 +58,19 @@ export default class DeleteModal extends React.PureComponent<Props, State> {
handleConfirmClick = () => {
this.setState({ loading: true });
- deleteComponents(this.props.selection, this.props.organization).then(
+ const parameters = this.props.selection.length
+ ? {
+ organization: this.props.organization,
+ projects: this.props.selection.join()
+ }
+ : {
+ analyzedBefore: this.props.analyzedBefore,
+ onProvisionedOnly: this.props.provisioned || undefined,
+ organization: this.props.organization,
+ qualifiers: this.props.qualifier,
+ q: this.props.query || undefined
+ };
+ bulkDeleteProjects(parameters).then(
() => {
if (this.mounted) {
this.props.onConfirm();
@@ -67,6 +84,17 @@ export default class DeleteModal extends React.PureComponent<Props, State> {
);
};
+ renderWarning = () =>
+ <div className="alert alert-warning modal-alert">
+ <AlertWarnIcon className="spacer-right" />
+ {this.props.selection.length
+ ? translateWithParameters(
+ 'projects_management.delete_selected_warning',
+ this.props.selection.length
+ )
+ : translateWithParameters('projects_management.delete_all_warning', this.props.total)}
+ </div>;
+
render() {
const header = translate('qualifiers.delete', this.props.qualifier);
@@ -84,6 +112,7 @@ export default class DeleteModal extends React.PureComponent<Props, State> {
</header>
<div className="modal-body">
+ {this.renderWarning()}
{translate('qualifiers.delete_confirm', this.props.qualifier)}
</div>
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/Header.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/Header.tsx
index ef77181c740..905c873d42e 100644
--- a/server/sonar-web/src/main/js/apps/projectsManagement/Header.tsx
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/Header.tsx
@@ -19,8 +19,7 @@
*/
import * as React from 'react';
import ChangeVisibilityForm from './ChangeVisibilityForm';
-import { Visibility } from './utils';
-import { Organization } from '../../app/types';
+import { Organization, Visibility } from '../../app/types';
import { translate } from '../../helpers/l10n';
export interface Props {
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx
index 9ddf15b20a5..e6612a26d19 100644
--- a/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx
@@ -19,7 +19,8 @@
*/
import * as React from 'react';
import { Link } from 'react-router';
-import { Project, Visibility } from './utils';
+import { Project } from './utils';
+import { Visibility } from '../../app/types';
import PrivateBadge from '../../components/common/PrivateBadge';
import Checkbox from '../../components/controls/Checkbox';
import QualifierIcon from '../../components/shared/QualifierIcon';
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/Search.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/Search.tsx
index eecfd7b4917..50e94342396 100644
--- a/server/sonar-web/src/main/js/apps/projectsManagement/Search.tsx
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/Search.tsx
@@ -186,7 +186,7 @@ export default class Search extends React.PureComponent<Props, State> {
inputClassName="input-medium"
name="analyzed-before"
onChange={this.props.onDateChanged}
- placeholder={translate('analyzed_before')}
+ placeholder={translate('last_analysis_before')}
value={this.props.analyzedBefore}
/>
</td>
@@ -194,7 +194,6 @@ export default class Search extends React.PureComponent<Props, State> {
};
render() {
- const isSomethingSelected = this.props.projects.length > 0 && this.props.selection.length > 0;
return (
<div className="big-spacer-bottom">
<table className="data">
@@ -224,13 +223,14 @@ export default class Search extends React.PureComponent<Props, State> {
<td className="thin nowrap text-middle">
<button
className="spacer-right js-bulk-apply-permission-template"
+ disabled={this.props.total === 0}
onClick={this.handleBulkApplyTemplateClick}>
{translate('permission_templates.bulk_apply_permission_template')}
</button>
<button
- onClick={this.handleDeleteClick}
className="js-delete button-red"
- disabled={!isSomethingSelected}>
+ disabled={this.props.total === 0}
+ onClick={this.handleDeleteClick}>
{translate('delete')}
</button>
</td>
@@ -240,6 +240,7 @@ export default class Search extends React.PureComponent<Props, State> {
{this.state.bulkApplyTemplateModal &&
<BulkApplyTemplateModal
+ analyzedBefore={this.props.analyzedBefore}
onClose={this.closeBulkApplyTemplateModal}
organization={this.props.organization.key}
provisioned={this.props.provisioned}
@@ -251,11 +252,15 @@ export default class Search extends React.PureComponent<Props, State> {
{this.state.deleteModal &&
<DeleteModal
+ analyzedBefore={this.props.analyzedBefore}
onClose={this.closeDeleteModal}
onConfirm={this.handleDeleteConfirm}
organization={this.props.organization.key}
+ provisioned={this.props.provisioned}
qualifier={this.props.qualifiers}
+ query={this.props.query}
selection={this.props.selection}
+ total={this.props.total}
/>}
</div>
);
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/App-test.tsx
index 07339957c4c..0fb6b1d21d0 100644
--- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/App-test.tsx
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/App-test.tsx
@@ -30,17 +30,13 @@ jest.mock('rc-tooltip', () => ({
}
}));
-jest.mock('../../../api/components', () => ({
- getComponents: jest.fn(),
- getProvisioned: jest.fn(() => Promise.resolve({ paging: { total: 0 }, projects: [] }))
-}));
+jest.mock('../../../api/components', () => ({ getComponents: jest.fn() }));
import * as React from 'react';
import { mount } from 'enzyme';
import App, { Props } from '../App';
const getComponents = require('../../../api/components').getComponents as jest.Mock<any>;
-const getProvisioned = require('../../../api/components').getProvisioned as jest.Mock<any>;
const organization = { key: 'org', name: 'org', projectVisibility: 'public' };
@@ -55,7 +51,6 @@ beforeEach(() => {
getComponents
.mockImplementation(() => Promise.resolve({ paging: { total: 0 }, components: [] }))
.mockClear();
- getProvisioned.mockClear();
});
it('fetches all projects on mount', () => {
@@ -66,7 +61,11 @@ it('fetches all projects on mount', () => {
it('selects provisioned', () => {
const wrapper = mountRender();
wrapper.find('Search').prop<Function>('onProvisionedChanged')(true);
- expect(getProvisioned).lastCalledWith(defaultSearchParameters);
+ expect(getComponents).lastCalledWith({
+ ...defaultSearchParameters,
+ onProvisionedOnly: true,
+ qualifiers: 'TRK'
+ });
});
it('changes qualifier and resets provisioned', () => {
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/BulkApplyTemplateModal-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/BulkApplyTemplateModal-test.tsx
index 6f11a5d48da..cdb33cf7f33 100644
--- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/BulkApplyTemplateModal-test.tsx
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/BulkApplyTemplateModal-test.tsx
@@ -18,9 +18,8 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
jest.mock('../../../api/permissions', () => ({
- applyTemplateToProject: jest.fn(),
- bulkApplyTemplate: jest.fn(),
- getPermissionTemplates: jest.fn()
+ bulkApplyTemplate: jest.fn(() => Promise.resolve()),
+ getPermissionTemplates: jest.fn(() => Promise.resolve({ permissionTemplates: [] }))
}));
import * as React from 'react';
@@ -28,18 +27,13 @@ import { mount, shallow } from 'enzyme';
import BulkApplyTemplateModal, { Props } from '../BulkApplyTemplateModal';
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();
+ bulkApplyTemplate.mockClear();
+ getPermissionTemplates.mockClear();
});
it('fetches permission templates on mount', () => {
@@ -61,9 +55,11 @@ it('bulk applies template to all results', async () => {
click(wrapper.find('button'));
expect(bulkApplyTemplate).toBeCalledWith({
+ analyzedBefore: '2017-04-08T00:00:00.000Z',
+ onProvisionedOnly: true,
organization: 'org',
q: 'bla',
- qualifier: 'TRK',
+ qualifiers: 'TRK',
templateId: 'foo'
});
expect(wrapper).toMatchSnapshot();
@@ -88,15 +84,9 @@ it('bulk applies template to selected results', async () => {
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({
+ expect(bulkApplyTemplate).toBeCalledWith({
organization: 'org',
- projectKey: 'proj2',
+ projects: 'proj1,proj2',
templateId: 'foo'
});
@@ -114,6 +104,7 @@ it('closes', () => {
function render(props?: { [P in keyof Props]?: Props[P] }) {
return (
<BulkApplyTemplateModal
+ analyzedBefore="2017-04-08T00:00:00.000Z"
onClose={jest.fn()}
organization="org"
provisioned={true}
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/DeleteModal-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/DeleteModal-test.tsx
index d24f30e9cdf..976be4403a8 100644
--- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/DeleteModal-test.tsx
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/DeleteModal-test.tsx
@@ -18,7 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
jest.mock('../../../api/components', () => ({
- deleteComponents: jest.fn(() => Promise.resolve())
+ bulkDeleteProjects: jest.fn(() => Promise.resolve())
}));
import * as React from 'react';
@@ -26,9 +26,13 @@ import { shallow } from 'enzyme';
import DeleteModal, { Props } from '../DeleteModal';
import { click } from '../../../helpers/testUtils';
-const deleteComponents = require('../../../api/components').deleteComponents as jest.Mock<any>;
+const bulkDeleteProjects = require('../../../api/components').bulkDeleteProjects as jest.Mock<any>;
-it('deletes projects', async () => {
+beforeEach(() => {
+ bulkDeleteProjects.mockClear();
+});
+
+it('deletes all projects', async () => {
const onConfirm = jest.fn();
const wrapper = shallowRender({ onConfirm });
(wrapper.instance() as DeleteModal).mounted = true;
@@ -36,7 +40,27 @@ it('deletes projects', async () => {
click(wrapper.find('button'));
expect(wrapper).toMatchSnapshot();
- expect(deleteComponents).toBeCalledWith(['foo', 'bar'], 'org');
+ expect(bulkDeleteProjects).toBeCalledWith({
+ analyzedBefore: '2017-04-08T00:00:00.000Z',
+ onProvisionedOnly: undefined,
+ organization: 'org',
+ q: 'bla',
+ qualifiers: 'TRK'
+ });
+
+ await new Promise(setImmediate);
+ expect(onConfirm).toBeCalled();
+});
+
+it('deletes selected projects', async () => {
+ const onConfirm = jest.fn();
+ const wrapper = shallowRender({ onConfirm, selection: ['proj1', 'proj2'] });
+ (wrapper.instance() as DeleteModal).mounted = true;
+ expect(wrapper).toMatchSnapshot();
+
+ click(wrapper.find('button'));
+ expect(wrapper).toMatchSnapshot();
+ expect(bulkDeleteProjects).toBeCalledWith({ organization: 'org', projects: 'proj1,proj2' });
await new Promise(setImmediate);
expect(onConfirm).toBeCalled();
@@ -52,11 +76,15 @@ it('closes', () => {
function shallowRender(props?: { [P in keyof Props]?: Props[P] }) {
return shallow(
<DeleteModal
+ analyzedBefore="2017-04-08T00:00:00.000Z"
onClose={jest.fn()}
onConfirm={jest.fn()}
organization="org"
+ provisioned={false}
qualifier="TRK"
- selection={['foo', 'bar']}
+ query="bla"
+ selection={[]}
+ total={17}
{...props}
/>
);
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Header-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Header-test.tsx
index 726c6b186cb..64b7d42366b 100644
--- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Header-test.tsx
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Header-test.tsx
@@ -20,7 +20,7 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import Header, { Props } from '../Header';
-import { Visibility } from '../utils';
+import { Visibility } from '../../../app/types';
import { click } from '../../../helpers/testUtils';
const organization = { key: 'org', name: 'org', projectVisibility: 'public' };
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRow-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRow-test.tsx
index 982ef5ce35a..b1d052dbb86 100644
--- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRow-test.tsx
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRow-test.tsx
@@ -20,7 +20,7 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import ProjectRow from '../ProjectRow';
-import { Visibility } from '../utils';
+import { Visibility } from '../../../app/types';
import { click } from '../../../helpers/testUtils';
const project = {
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Projects-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Projects-test.tsx
index b1f165cde7a..2cc0ec841ef 100644
--- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Projects-test.tsx
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Projects-test.tsx
@@ -22,8 +22,8 @@ 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';
+import { Visibility } from '../../../app/types';
const organization = { key: 'org', name: 'org', projectVisibility: 'public' };
const projects = [
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Search-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Search-test.tsx
index 66e0439de3f..e7ec8d71e94 100644
--- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Search-test.tsx
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Search-test.tsx
@@ -118,7 +118,7 @@ function shallowRender(props?: { [P in keyof Props]?: Props[P] }) {
ready={true}
selection={[]}
topLevelQualifiers={['TRK']}
- total={0}
+ total={17}
{...props}
/>
);
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/BulkApplyTemplateModal-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/BulkApplyTemplateModal-test.tsx.snap
index bc3e39d7f64..4cadeebb4a0 100644
--- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/BulkApplyTemplateModal-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/BulkApplyTemplateModal-test.tsx.snap
@@ -67,8 +67,11 @@ exports[`bulk applies template to all results 2`] = `
className="modal-body"
>
<div
- className="alert alert-warning"
+ className="alert alert-warning modal-alert"
>
+ <AlertWarnIcon
+ className="spacer-right"
+ />
permission_templates.bulk_apply_permission_template.apply_to_all.17
</div>
<div
@@ -183,8 +186,11 @@ exports[`bulk applies template to all results 3`] = `
className="modal-body"
>
<div
- className="alert alert-warning"
+ className="alert alert-warning modal-alert"
>
+ <AlertWarnIcon
+ className="spacer-right"
+ />
permission_templates.bulk_apply_permission_template.apply_to_all.17
</div>
<div
@@ -388,8 +394,11 @@ exports[`bulk applies template to selected results 2`] = `
className="modal-body"
>
<div
- className="alert alert-info"
+ className="alert alert-warning modal-alert"
>
+ <AlertWarnIcon
+ className="spacer-right"
+ />
permission_templates.bulk_apply_permission_template.apply_to_selected.2
</div>
<div
@@ -504,8 +513,11 @@ exports[`bulk applies template to selected results 3`] = `
className="modal-body"
>
<div
- className="alert alert-info"
+ className="alert alert-warning modal-alert"
>
+ <AlertWarnIcon
+ className="spacer-right"
+ />
permission_templates.bulk_apply_permission_template.apply_to_selected.2
</div>
<div
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/DeleteModal-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/DeleteModal-test.tsx.snap
index e57788fbc92..12d6e222a77 100644
--- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/DeleteModal-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/DeleteModal-test.tsx.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`deletes projects 1`] = `
+exports[`deletes all projects 1`] = `
<Modal
ariaHideApp={true}
bodyOpenClassName="ReactModal__Body--open"
@@ -24,6 +24,14 @@ exports[`deletes projects 1`] = `
<div
className="modal-body"
>
+ <div
+ className="alert alert-warning modal-alert"
+ >
+ <AlertWarnIcon
+ className="spacer-right"
+ />
+ projects_management.delete_all_warning.17
+ </div>
qualifiers.delete_confirm.TRK
</div>
<footer
@@ -47,7 +55,7 @@ exports[`deletes projects 1`] = `
</Modal>
`;
-exports[`deletes projects 2`] = `
+exports[`deletes all projects 2`] = `
<Modal
ariaHideApp={true}
bodyOpenClassName="ReactModal__Body--open"
@@ -71,6 +79,127 @@ exports[`deletes projects 2`] = `
<div
className="modal-body"
>
+ <div
+ className="alert alert-warning modal-alert"
+ >
+ <AlertWarnIcon
+ className="spacer-right"
+ />
+ projects_management.delete_all_warning.17
+ </div>
+ 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>
+`;
+
+exports[`deletes selected 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"
+ >
+ <div
+ className="alert alert-warning modal-alert"
+ >
+ <AlertWarnIcon
+ className="spacer-right"
+ />
+ projects_management.delete_selected_warning.2
+ </div>
+ 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 selected 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"
+ >
+ <div
+ className="alert alert-warning modal-alert"
+ >
+ <AlertWarnIcon
+ className="spacer-right"
+ />
+ projects_management.delete_selected_warning.2
+ </div>
qualifiers.delete_confirm.TRK
</div>
<footer
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Search-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Search-test.tsx.snap
index 89a31381713..9e0a31ce941 100644
--- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Search-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Search-test.tsx.snap
@@ -8,7 +8,7 @@ exports[`bulk applies permission template 1`] = `
qualifier="TRK"
query=""
selection={Array []}
- total={0}
+ total={17}
/>
`;
@@ -17,13 +17,16 @@ exports[`deletes projects 1`] = `
onClose={[Function]}
onConfirm={[Function]}
organization="org"
+ provisioned={false}
qualifier="TRK"
+ query=""
selection={
Array [
"foo",
"bar",
]
}
+ total={17}
/>
`;
@@ -176,13 +179,14 @@ exports[`render qualifiers filter 1`] = `
>
<button
className="spacer-right js-bulk-apply-permission-template"
+ disabled={false}
onClick={[Function]}
>
permission_templates.bulk_apply_permission_template
</button>
<button
className="js-delete button-red"
- disabled={true}
+ disabled={false}
onClick={[Function]}
>
delete
@@ -277,13 +281,14 @@ exports[`renders 1`] = `
>
<button
className="spacer-right js-bulk-apply-permission-template"
+ disabled={false}
onClick={[Function]}
>
permission_templates.bulk_apply_permission_template
</button>
<button
className="js-delete button-red"
- disabled={true}
+ disabled={false}
onClick={[Function]}
>
delete
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/utils.ts b/server/sonar-web/src/main/js/apps/projectsManagement/utils.ts
index aa549df26c1..0bbc7d40843 100644
--- a/server/sonar-web/src/main/js/apps/projectsManagement/utils.ts
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/utils.ts
@@ -17,19 +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 { SearchProjectsResponseComponent } from '../../api/components';
+
export const PAGE_SIZE = 50;
export const QUALIFIERS_ORDER = ['TRK', 'VW', 'APP'];
-export interface Project {
- key: string;
- lastAnalysisDate?: string;
- name: string;
- qualifier: string;
- visibility: Visibility;
-}
-
-export enum Visibility {
- Public = 'public',
- Private = 'private'
-}
+export type Project = SearchProjectsResponseComponent;
diff --git a/server/sonar-web/src/main/less/components/alerts.less b/server/sonar-web/src/main/less/components/alerts.less
index fb874b2ecab..65694046261 100644
--- a/server/sonar-web/src/main/less/components/alerts.less
+++ b/server/sonar-web/src/main/less/components/alerts.less
@@ -38,6 +38,14 @@
vertical-align: middle;
}
+.modal-alert {
+ margin: -10px -10px 16px;
+ padding: 10px;
+ border-top: none;
+ border-left: none;
+ border-right: none;
+}
+
// Color
.alert-emphasis-variant(@color, @background-color, @border-color) {
diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
index 0345b483fb7..a14132565a9 100644
--- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties
+++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
@@ -225,7 +225,6 @@ added_since_previous_version_detailed=Added since previous version ({0})
added_since_version=Added since version {0}
all_violations=All violations
all_issues=All issues
-analyzed_before=Analyzed before
and_worse=and worse
are_you_sure=Are you sure?
assigned_to=Assigned to
@@ -253,6 +252,7 @@ full_source=Full source
greater_or_equals=Greater or equals
greater_than=Greater than
help_tips=Help tips
+last_analysis_before=Last analysis before
less_or_equals=Less or equals
less_than=Less than
logging_out=You're logging out, please wait...
@@ -1406,6 +1406,8 @@ 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}"?
+projects_management.delete_selected_warning=You're about to delete {0} selected items.
+projects_management.delete_all_warning=You're about to delete all {0} items.
#------------------------------------------------------------------------------