import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
+import org.sonarqube.ws.WsComponents;
+import org.sonarqube.ws.client.component.SearchProjectsRequest;
import org.sonarqube.ws.client.permission.RemoveGroupWsRequest;
import org.sonarqube.ws.client.project.UpdateVisibilityRequest;
import pageobjects.Navigation;
+import pageobjects.ProjectsManagementPage;
import util.ItUtils;
-import static com.codeborne.selenide.Condition.text;
-import static com.codeborne.selenide.Selenide.$;
+import static org.assertj.core.api.Assertions.assertThat;
import static util.ItUtils.newAdminWsClient;
import static util.ItUtils.projectDir;
// Remove 'Admin' permission for admin group on project 2 -> No one can access or admin this project, expect System Admin
newAdminWsClient(orchestrator).permissions().removeGroup(new RemoveGroupWsRequest().setProjectKey("sample2").setGroupName("sonar-administrators").setPermission("admin"));
- nav.logIn().asAdmin().open("/projects_admin");
- $(".data.zebra")
- .shouldHave(text("sample1"))
- .shouldHave(text("sample2"));
+ nav.logIn().asAdmin().openProjectsManagement()
+ .shouldHaveProject("sample1")
+ .shouldHaveProject("sample2");
+ }
+
+ @Test
+ public void create_public_project() {
+ createProjectAndVerify("public");
+ }
+
+ @Test
+ public void create_private_project() {
+ createProjectAndVerify("private");
+ }
+
+ private void createProjectAndVerify(String visibility) {
+ ProjectsManagementPage page = nav.logIn().asAdmin().openProjectsManagement();
+ page
+ .shouldHaveProjectsCount(0)
+ .createProject("foo", "foo", visibility)
+ .shouldHaveProjectsCount(1);
+
+ WsComponents.SearchProjectsWsResponse response = newAdminWsClient(orchestrator).components().searchProjects(
+ SearchProjectsRequest.builder().build());
+ assertThat(response.getComponentsCount()).isEqualTo(1);
+ assertThat(response.getComponents(0).getKey()).isEqualTo("foo");
+ assertThat(response.getComponents(0).getName()).isEqualTo("foo");
+ assertThat(response.getComponents(0).getVisibility()).isEqualTo(visibility);
}
}
return open(url, ProjectPermissionsPage.class);
}
+ public ProjectsManagementPage openProjectsManagement() {
+ return open("/projects_admin", ProjectsManagementPage.class);
+ }
+
public LoginPage openLogin() {
return open("/sessions/login", LoginPage.class);
}
--- /dev/null
+/*
+ * 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.
+ */
+package pageobjects;
+
+import static com.codeborne.selenide.Condition.exist;
+import static com.codeborne.selenide.Condition.text;
+import static com.codeborne.selenide.Selenide.$;
+import static com.codeborne.selenide.Selenide.$$;
+
+public class ProjectsManagementPage {
+
+ public ProjectsManagementPage() {
+ $("#projects-management-page").should(exist);
+ }
+
+ public ProjectsManagementPage shouldHaveProjectsCount(int count) {
+ $$("#projects-management-page-projects tr").shouldHaveSize(count);
+ return this;
+ }
+
+ public ProjectsManagementPage shouldHaveProject(String key) {
+ $("#projects-management-page-projects").shouldHave(text(key));
+ return this;
+ }
+
+ public ProjectsManagementPage createProject(String key, String name, String visibility) {
+ $("#create-project").click();
+ $("#create-project-name").val(key);
+ $("#create-project-key").val(name);
+ $("#visibility-" + visibility).click();
+ $("#create-project-submit").submit();
+ return this;
+ }
+}
import React from 'react';
import { without } from 'lodash';
import PageHeader from './PageHeader';
-import VisibilitySelector from './VisibilitySelector';
+import VisibilitySelector from '../../../../components/common/VisibilitySelector';
import AllHoldersList from './AllHoldersList';
import PublicProjectDisclaimer from './PublicProjectDisclaimer';
import PageError from '../../shared/components/PageError';
<PageError />
{this.props.component.qualifier === 'TRK' &&
<VisibilitySelector
+ className="big-spacer-top big-spacer-bottom"
onChange={this.handleVisibilityChange}
visibility={this.props.component.visibility}
/>}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-// @flow
-import React from 'react';
-import classNames from 'classnames';
-import { translate } from '../../../../helpers/l10n';
-
-type Props = {
- onChange: string => void,
- visibility: string
-};
-
-export default class VisibilitySelector extends React.PureComponent {
- props: Props;
-
- handlePublicClick = (event: Event & { currentTarget: HTMLElement }) => {
- event.preventDefault();
- event.currentTarget.blur();
- this.props.onChange('public');
- };
-
- handlePrivateClick = (event: Event & { currentTarget: HTMLElement }) => {
- event.preventDefault();
- event.currentTarget.blur();
- this.props.onChange('private');
- };
-
- render() {
- return (
- <div className="big-spacer-top big-spacer-bottom">
- <a
- className="link-base-color link-no-underline"
- id="visibility-public"
- href="#"
- onClick={this.handlePublicClick}>
- <i
- className={classNames('icon-radio', {
- 'is-checked': this.props.visibility === 'public'
- })}
- />
- <span className="spacer-left">{translate('visibility.public')}</span>
- </a>
-
- <a
- className="link-base-color link-no-underline huge-spacer-left"
- id="visibility-private"
- href="#"
- onClick={this.handlePrivateClick}>
- <i
- className={classNames('icon-radio', {
- 'is-checked': this.props.visibility === 'private'
- })}
- />
- <span className="spacer-left">{translate('visibility.private')}</span>
- </a>
- </div>
- );
- }
-}
hasProvisionPermission={hasProvisionPermission}
topLevelQualifiers={topLevelQualifiers}
onVisibilityChange={props.onVisibilityChange}
+ onRequestFail={props.onRequestFail}
organization={props.organization}
/>
);
};
const mapDispatchToProps = (dispatch, ownProps) => ({
- onVisibilityChange: visibility => dispatch(onVisibilityChange(ownProps.organization, visibility))
+ onVisibilityChange: visibility => dispatch(onVisibilityChange(ownProps.organization, visibility)),
+ onRequestFail: error => onFail(dispatch)(error)
});
export default connect(mapStateToProps, mapDispatchToProps)(AppContainer);
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import Modal from 'react-modal';
+import { Link } from 'react-router';
+import VisibilitySelector from '../../components/common/VisibilitySelector';
+import { createProject } from '../../api/components';
+import { translate } from '../../helpers/l10n';
+import { getProjectUrl } from '../../helpers/urls';
+import type { Organization } from '../../store/organizations/duck';
+
+type Props = {|
+ onClose: () => void,
+ onProjectCreated: () => void,
+ onRequestFail: Object => void,
+ organization?: Organization
+|};
+
+type State = {
+ branch: string,
+ createdProject?: Object,
+ key: string,
+ loading: boolean,
+ name: string,
+ visibility: string
+};
+
+export default class CreateProjectForm extends React.PureComponent {
+ mounted: boolean;
+ props: Props;
+ state: State;
+
+ constructor(props: Props) {
+ super(props);
+ this.state = {
+ branch: '',
+ key: '',
+ loading: false,
+ name: '',
+ visibility: props.organization ? props.organization.projectVisibility : 'public'
+ };
+ }
+
+ componentDidMount() {
+ this.mounted = true;
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ handleCancelClick = (event: Event) => {
+ event.preventDefault();
+ this.props.onClose();
+ };
+
+ handleInputChange = (event: { currentTarget: HTMLInputElement }) => {
+ const { name, value } = event.currentTarget;
+ this.setState({ [name]: value });
+ };
+
+ handleVisibilityChange = (visibility: string) => {
+ this.setState({ visibility });
+ };
+
+ handleFormSubmit = (event: Event) => {
+ event.preventDefault();
+
+ const data: { [string]: string } = {
+ name: this.state.name,
+ branch: this.state.branch,
+ project: this.state.key,
+ visibility: this.state.visibility
+ };
+ if (this.props.organization) {
+ data.organization = this.props.organization.key;
+ }
+
+ this.setState({ loading: true });
+ createProject(data).then(
+ response => {
+ if (this.mounted) {
+ this.setState({ createdProject: response.project, loading: false });
+ this.props.onProjectCreated();
+ }
+ },
+ error => {
+ if (this.mounted) {
+ this.setState({ loading: false });
+ this.props.onRequestFail(error);
+ }
+ }
+ );
+ };
+
+ render() {
+ const { createdProject } = this.state;
+
+ return (
+ <Modal
+ isOpen={true}
+ contentLabel="modal form"
+ className="modal"
+ overlayClassName="modal-overlay"
+ onRequestClose={this.props.onClose}>
+
+ {createdProject
+ ? <div>
+ <header className="modal-head">
+ <h2>{translate('qualifiers.create.TRK')}</h2>
+ </header>
+
+ <div className="modal-body">
+ <div className="alert alert-success">
+ Project
+ {' '}
+ <Link to={getProjectUrl(createdProject.key)}>{createdProject.name}</Link>
+ {' '}
+ has been successfully created.
+ </div>
+ </div>
+
+ <footer className="modal-foot">
+ <a href="#" id="create-project-close" onClick={this.handleCancelClick}>
+ {translate('close')}
+ </a>
+ </footer>
+ </div>
+ : <form id="create-project-form" onSubmit={this.handleFormSubmit}>
+ <header className="modal-head">
+ <h2>{translate('qualifiers.create.TRK')}</h2>
+ </header>
+
+ <div className="modal-body">
+ <div className="modal-field">
+ <label htmlFor="create-project-name">
+ {translate('name')}
+ <em className="mandatory">*</em>
+ </label>
+ <input
+ autoFocus={true}
+ id="create-project-name"
+ maxLength="2000"
+ name="name"
+ onChange={this.handleInputChange}
+ required={true}
+ type="text"
+ value={this.state.name}
+ />
+ </div>
+ <div className="modal-field">
+ <label htmlFor="create-project-branch">
+ {translate('branch')}
+ </label>
+ <input
+ id="create-project-branch"
+ maxLength="200"
+ name="branch"
+ onChange={this.handleInputChange}
+ type="text"
+ value={this.state.branch}
+ />
+ </div>
+ <div className="modal-field">
+ <label htmlFor="create-project-key">
+ {translate('key')}
+ <em className="mandatory">*</em>
+ </label>
+ <input
+ id="create-project-key"
+ maxLength="400"
+ name="key"
+ onChange={this.handleInputChange}
+ required={true}
+ type="text"
+ value={this.state.key}
+ />
+ </div>
+ <div className="modal-field">
+ <label> {translate('visibility')} </label>
+ <VisibilitySelector
+ className="little-spacer-top"
+ onChange={this.handleVisibilityChange}
+ visibility={this.state.visibility}
+ />
+ </div>
+ </div>
+
+ <footer className="modal-foot">
+ {this.state.loading && <i className="spinner spacer-right" />}
+ <button disabled={this.state.loading} id="create-project-submit" type="submit">
+ {translate('create')}
+ </button>
+ <a href="#" id="create-project-cancel" onClick={this.handleCancelClick}>
+ {translate('cancel')}
+ </a>
+ </footer>
+ </form>}
+
+ </Modal>
+ );
+ }
+}
+++ /dev/null
-/*
- * 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 { createProject } from '../../api/components';
-import Template from './templates/projects-create-form.hbs';
-
-export default ModalForm.extend({
- template: Template,
-
- 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();
- },
-
- sendRequest() {
- const data = {
- name: this.$('#create-project-name').val(),
- branch: this.$('#create-project-branch').val(),
- project: this.$('#create-project-key').val()
- };
- if (this.options.organization) {
- data.organization = this.options.organization.key;
- }
- this.disableForm();
- return createProject(data)
- .then(project => {
- if (this.options.refresh) {
- this.options.refresh();
- }
- this.enableForm();
- this.createdProject = project;
- this.render();
- })
- .catch(error => {
- this.enableForm();
- error.response.json().then(r => this.showErrors(r.errors, r.warnings));
- });
- },
-
- serializeData() {
- return {
- ...ModalForm.prototype.serializeData.apply(this, arguments),
- createdProject: this.createdProject
- };
- }
-});
*/
// @flow
import React from 'react';
-import CreateView from './create-view';
import ChangeVisibilityForm from './ChangeVisibilityForm';
import { translate } from '../../helpers/l10n';
import type { Organization } from '../../store/organizations/duck';
type Props = {|
hasProvisionPermission: boolean,
+ onProjectCreate: () => void,
onVisibilityChange: string => void,
- organization?: Organization,
- refresh: () => void
+ organization?: Organization
|};
type State = {
props: Props;
state: State = { visibilityForm: false };
- createProject() {
- new CreateView({
- refresh: this.props.refresh,
- organization: this.props.organization
- }).render();
- }
+ handleCreateProjectClick = (event: Event) => {
+ event.preventDefault();
+ this.props.onProjectCreate();
+ };
handleChangeVisibilityClick = (event: Event) => {
event.preventDefault();
this.setState({ visibilityForm: false });
};
- renderCreateButton() {
- if (!this.props.hasProvisionPermission) {
- return null;
- }
- return (
- <button onClick={this.createProject.bind(this)}>
- Create Project
- </button>
- );
- }
-
render() {
const { organization } = this.props;
onClick={this.handleChangeVisibilityClick}
/>
</span>}
- {this.renderCreateButton()}
+ {this.props.hasProvisionPermission &&
+ <button id="create-project" onClick={this.handleCreateProjectClick}>
+ {translate('qualifiers.create.TRK')}
+ </button>}
</div>
<p className="page-description">
{translate('projects_management.page.description')}
import Header from './header';
import Search from './search';
import Projects from './projects';
+import CreateProjectForm from './CreateProjectForm';
import { PAGE_SIZE, TYPE } from './constants';
import { getComponents, getProvisioned, getGhosts, deleteComponents } from '../../api/components';
import ListFooter from '../../components/controls/ListFooter';
type Props = {|
hasProvisionPermission: boolean,
onVisibilityChange: string => void,
+ onRequestFail: Object => void,
organization?: Organization
|};
type State = {
+ createProjectForm: boolean,
ready: boolean,
projects: Array<{ key: string }>,
total: number,
constructor(props: Props) {
super(props);
this.state = {
+ createProjectForm: false,
ready: false,
projects: [],
total: 0,
});
};
+ openCreateProjectForm = () => {
+ this.setState({ createProjectForm: true });
+ };
+
+ closeCreateProjectForm = () => {
+ this.setState({ createProjectForm: false });
+ };
+
render() {
return (
- <div className="page page-limited">
+ <div className="page page-limited" id="projects-management-page">
<Header
hasProvisionPermission={this.props.hasProvisionPermission}
- refresh={this.requestProjects}
+ onProjectCreate={this.openCreateProjectForm}
onVisibilityChange={this.props.onVisibilityChange}
organization={this.props.organization}
/>
total={this.state.total}
loadMore={this.loadMore}
/>
+
+ {this.state.createProjectForm &&
+ <CreateProjectForm
+ onClose={this.closeCreateProjectForm}
+ onProjectCreated={this.requestProjects}
+ onRequestFail={this.props.onRequestFail}
+ organization={this.props.organization}
+ />}
</div>
);
}
const className = classNames('data', 'zebra', { 'new-loading': !this.props.ready });
return (
- <table className={className}>
+ <table className={className} id="projects-management-page-projects">
<tbody>{this.props.projects.map(this.renderProject)}</tbody>
</table>
);
+++ /dev/null
-<form id="create-project-form" autocomplete="off">
- <div class="modal-head">
- <h2>Create Project</h2>
- </div>
- <div class="modal-body">
- <div class="js-modal-messages"></div>
- {{#if createdProject}}
- <div class="alert alert-success">
- Project <a href="{{componentPermalink createdProject.k}}">{{createdProject.nm}}</a> has been successfully
- created.
- </div>
- {{else}}
- <div class="modal-field">
- <label for="create-project-name">Name<em class="mandatory">*</em></label>
- {{! keep this fake field to hack browser autofill }}
- <input id="create-project-name-fake" name="name-fake" type="text" class="hidden">
- <input id="create-project-name" name="name" type="text" maxlength="2000" required>
- </div>
- <div class="modal-field">
- <label for="create-project-branch">Branch</label>
- {{! keep this fake field to hack browser autofill }}
- <input id="create-project-branch-fake" name="branch-fake" type="text" class="hidden">
- <input id="create-project-branch" name="branch" type="text" maxlength="200">
- </div>
- <div class="modal-field">
- <label for="create-project-key">Key<em class="mandatory">*</em></label>
- {{! keep this fake field to hack browser autofill }}
- <input id="create-project-key-fake" name="key-fake" type="text" class="hidden">
- <input id="create-project-key" name="key" type="text" maxlength="400" required>
- </div>
- {{/if}}
- </div>
- <div class="modal-foot">
- {{#if createdProject}}
- <a href="#" class="js-modal-close">{{t 'close'}}</a>
- {{else}}
- <button id="create-project-submit">Create</button>
- <a href="#" class="js-modal-close" id="create-project-cancel">Cancel</a>
- {{/if}}
- </div>
-</form>
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import classNames from 'classnames';
+import { translate } from '../../helpers/l10n';
+
+type Props = {|
+ className?: string,
+ onChange: string => void,
+ visibility: string
+|};
+
+export default class VisibilitySelector extends React.PureComponent {
+ props: Props;
+
+ handlePublicClick = (event: Event & { currentTarget: HTMLElement }) => {
+ event.preventDefault();
+ event.currentTarget.blur();
+ this.props.onChange('public');
+ };
+
+ handlePrivateClick = (event: Event & { currentTarget: HTMLElement }) => {
+ event.preventDefault();
+ event.currentTarget.blur();
+ this.props.onChange('private');
+ };
+
+ render() {
+ return (
+ <div className={this.props.className}>
+ <a
+ className="link-base-color link-no-underline"
+ id="visibility-public"
+ href="#"
+ onClick={this.handlePublicClick}>
+ <i
+ className={classNames('icon-radio', {
+ 'is-checked': this.props.visibility === 'public'
+ })}
+ />
+ <span className="spacer-left">{translate('visibility.public')}</span>
+ </a>
+
+ <a
+ className="link-base-color link-no-underline huge-spacer-left"
+ id="visibility-private"
+ href="#"
+ onClick={this.handlePrivateClick}>
+ <i
+ className={classNames('icon-radio', {
+ 'is-checked': this.props.visibility === 'private'
+ })}
+ />
+ <span className="spacer-left">{translate('visibility.private')}</span>
+ </a>
+ </div>
+ );
+ }
+}
view=View
views=Views
violations=Violations
+visibility=Visibility
with=With
worst=Worst