diff options
24 files changed, 963 insertions, 175 deletions
diff --git a/it/it-tests/src/test/java/it/Category1Suite.java b/it/it-tests/src/test/java/it/Category1Suite.java index 65ff384d431..b613cc3ec2b 100644 --- a/it/it-tests/src/test/java/it/Category1Suite.java +++ b/it/it-tests/src/test/java/it/Category1Suite.java @@ -43,6 +43,7 @@ import it.measureHistory.TimeMachineTest; import it.projectAdministration.BackgroundTasksTest; import it.projectAdministration.BulkDeletionTest; import it.projectAdministration.ProjectAdministrationTest; +import it.projectAdministration.ProjectLinksPageTest; import it.qualityGate.QualityGateNotificationTest; import it.qualityGate.QualityGateTest; import it.qualityGate.QualityGateUiTest; @@ -66,6 +67,7 @@ import static util.ItUtils.xooPlugin; // project administration BulkDeletionTest.class, ProjectAdministrationTest.class, + ProjectLinksPageTest.class, BackgroundTasksTest.class, // settings PropertySetsTest.class, diff --git a/it/it-tests/src/test/java/it/projectAdministration/ProjectLinksPageTest.java b/it/it-tests/src/test/java/it/projectAdministration/ProjectLinksPageTest.java new file mode 100644 index 00000000000..6faea2774f0 --- /dev/null +++ b/it/it-tests/src/test/java/it/projectAdministration/ProjectLinksPageTest.java @@ -0,0 +1,153 @@ +/* + * 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. + */ +package it.projectAdministration; + +import com.codeborne.selenide.Condition; +import com.sonar.orchestrator.Orchestrator; +import com.sonar.orchestrator.build.SonarScanner; +import it.Category1Suite; +import java.util.List; +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.sonarqube.ws.WsProjectLinks.CreateWsResponse; +import org.sonarqube.ws.client.WsClient; +import org.sonarqube.ws.client.projectlinks.CreateWsRequest; +import org.sonarqube.ws.client.projectlinks.DeleteWsRequest; +import pageobjects.Navigation; +import pageobjects.ProjectLinkItem; +import pageobjects.ProjectLinksPage; + +import static com.codeborne.selenide.Condition.hasText; +import static com.codeborne.selenide.Selenide.$; +import static util.ItUtils.newAdminWsClient; +import static util.ItUtils.projectDir; + +public class ProjectLinksPageTest { + + @ClassRule + public static Orchestrator ORCHESTRATOR = Category1Suite.ORCHESTRATOR; + + @Rule + public Navigation nav = Navigation.get(ORCHESTRATOR); + + private static WsClient wsClient; + private long customLinkId; + + @BeforeClass + public static void setUp() { + wsClient = newAdminWsClient(ORCHESTRATOR); + + ORCHESTRATOR.resetData(); + ORCHESTRATOR.executeBuild( + SonarScanner.create(projectDir("shared/xoo-sample")) + .setProperty("sonar.links.homepage", "http://example.com")); + } + + @Before + public void prepare() { + customLinkId = Long.parseLong(createCustomLink().getLink().getId()); + } + + @After + public void clean() { + deleteLink(customLinkId); + } + + @Test + public void should_list_links() { + ProjectLinksPage page = openPage(); + + page.getLinks().shouldHaveSize(2); + + List<ProjectLinkItem> links = page.getLinksAsItems(); + ProjectLinkItem homepageLink = links.get(0); + ProjectLinkItem customLink = links.get(1); + + homepageLink.getName().should(hasText("Home")); + homepageLink.getType().should(hasText("sonar.links.homepage")); + homepageLink.getUrl().should(hasText("http://example.com")); + homepageLink.getDeleteButton().shouldNot(Condition.present); + + customLink.getName().should(hasText("Custom")); + customLink.getType().shouldNot(Condition.present); + customLink.getUrl().should(hasText("http://example.org/custom")); + customLink.getDeleteButton().shouldBe(Condition.visible); + } + + @Test + public void should_create_link() { + ProjectLinksPage page = openPage(); + + page.getLinks().shouldHaveSize(2); + + $("#create-project-link").click(); + $("#create-link-name").setValue("Test"); + $("#create-link-url").setValue("http://example.com/test"); + $("#create-link-confirm").click(); + + page.getLinks().shouldHaveSize(3); + + ProjectLinkItem testLink = page.getLinksAsItems().get(2); + + testLink.getName().should(hasText("Test")); + testLink.getType().shouldNot(Condition.present); + testLink.getUrl().should(hasText("http://example.com/test")); + testLink.getDeleteButton().shouldBe(Condition.visible); + } + + @Test + public void should_delete_link() { + ProjectLinksPage page = openPage(); + + page.getLinks().shouldHaveSize(2); + + List<ProjectLinkItem> links = page.getLinksAsItems(); + ProjectLinkItem customLink = links.get(1); + + customLink.getDeleteButton().click(); + $("#delete-link-confirm").click(); + + page.getLinks().shouldHaveSize(1); + } + + private CreateWsResponse createCustomLink() { + return wsClient.projectLinks().create(new CreateWsRequest() + .setProjectKey("sample") + .setName("Custom") + .setUrl("http://example.org/custom")); + } + + private void deleteLink(long id) { + try { + wsClient.projectLinks().delete(new DeleteWsRequest().setId(id)); + } catch (Exception e) { + // fail silently + } + } + + private ProjectLinksPage openPage() { + nav.logIn().submitCredentials("admin", "admin"); + return nav.openProjectLinks("sample"); + } +} diff --git a/it/it-tests/src/test/java/pageobjects/LoginPage.java b/it/it-tests/src/test/java/pageobjects/LoginPage.java index bf946b27afa..2aea164f0cb 100644 --- a/it/it-tests/src/test/java/pageobjects/LoginPage.java +++ b/it/it-tests/src/test/java/pageobjects/LoginPage.java @@ -37,7 +37,10 @@ public class LoginPage { } public LoginPage submitWrongCredentials(String login, String password) { - return submitCredentials(login, password, LoginPage.class); + $("#login").val(login); + $("#password").val(password); + $(By.name("commit")).click(); + return page(LoginPage.class); } public SelenideElement getErrorMessage() { @@ -48,6 +51,7 @@ public class LoginPage { $("#login").val(login); $("#password").val(password); $(By.name("commit")).click(); + $("#login").should(Condition.disappear); return page(expectedResultPage); } } diff --git a/it/it-tests/src/test/java/pageobjects/Navigation.java b/it/it-tests/src/test/java/pageobjects/Navigation.java index 395509c5fad..46ee57ca425 100644 --- a/it/it-tests/src/test/java/pageobjects/Navigation.java +++ b/it/it-tests/src/test/java/pageobjects/Navigation.java @@ -50,6 +50,12 @@ public class Navigation extends ExternalResource { return open("/", Navigation.class); } + public ProjectLinksPage openProjectLinks(String projectKey) { + // TODO encode projectKey + String url = "/project/links?id=" + projectKey; + return open(url, ProjectLinksPage.class); + } + public void open(String relativeUrl) { Selenide.open(relativeUrl); } diff --git a/it/it-tests/src/test/java/pageobjects/ProjectLinkItem.java b/it/it-tests/src/test/java/pageobjects/ProjectLinkItem.java new file mode 100644 index 00000000000..cf5462009cd --- /dev/null +++ b/it/it-tests/src/test/java/pageobjects/ProjectLinkItem.java @@ -0,0 +1,52 @@ +/* + * 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. + */ +package pageobjects; + +import com.codeborne.selenide.SelenideElement; +import org.openqa.selenium.NoSuchElementException; + +public class ProjectLinkItem { + + private final SelenideElement elt; + + public ProjectLinkItem(SelenideElement elt) { + this.elt = elt; + } + + public SelenideElement getName() { + return elt.$(".js-name"); + } + + public SelenideElement getType() { + try { + return elt.$(".js-type"); + } catch (NoSuchElementException e) { + return null; + } + } + + public SelenideElement getUrl() { + return elt.$(".js-url"); + } + + public SelenideElement getDeleteButton() { + return elt.$(".js-delete-button"); + } +} diff --git a/it/it-tests/src/test/java/pageobjects/ProjectLinksPage.java b/it/it-tests/src/test/java/pageobjects/ProjectLinksPage.java new file mode 100644 index 00000000000..fb143e560b6 --- /dev/null +++ b/it/it-tests/src/test/java/pageobjects/ProjectLinksPage.java @@ -0,0 +1,47 @@ +/* + * 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. + */ +package pageobjects; + +import com.codeborne.selenide.Condition; +import com.codeborne.selenide.ElementsCollection; + +import java.util.List; +import java.util.stream.Collectors; + +import static com.codeborne.selenide.Selenide.$; +import static com.codeborne.selenide.Selenide.$$; + +public class ProjectLinksPage { + + public ProjectLinksPage() { + $("#project-links").should(Condition.exist); + } + + public ElementsCollection getLinks() { + return $$("#project-links tr[data-name]"); + } + + public List<ProjectLinkItem> getLinksAsItems() { + return getLinks() + .stream() + .map(ProjectLinkItem::new) + .collect(Collectors.toList()); + } +} diff --git a/server/sonar-web/src/main/js/api/projectLinks.js b/server/sonar-web/src/main/js/api/projectLinks.js new file mode 100644 index 00000000000..06e1eaa7d26 --- /dev/null +++ b/server/sonar-web/src/main/js/api/projectLinks.js @@ -0,0 +1,38 @@ +/* + * 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 { getJSON, post, postJSON } from '../helpers/request'; + +export function getProjectLinks (projectKey) { + const url = '/api/project_links/search'; + const data = { projectKey }; + return getJSON(url, data).then(r => r.links); +} + +export function deleteLink (linkId) { + const url = '/api/project_links/delete'; + const data = { id: linkId }; + return post(url, data); +} + +export function createLink (projectKey, name, url) { + const apiURL = '/api/project_links/create'; + const data = { projectKey, name, url }; + return postJSON(apiURL, data).then(r => r.link); +} diff --git a/server/sonar-web/src/main/js/apps/project-admin/app.js b/server/sonar-web/src/main/js/apps/project-admin/app.js index 7bcb91f9e1c..85fb770962f 100644 --- a/server/sonar-web/src/main/js/apps/project-admin/app.js +++ b/server/sonar-web/src/main/js/apps/project-admin/app.js @@ -24,6 +24,7 @@ import { Router, Route, useRouterHistory } from 'react-router'; import { createHistory } from 'history'; import Deletion from './deletion/Deletion'; import QualityProfiles from './quality-profiles/QualityProfiles'; +import Links from './links/Links'; import rootReducer from './store/rootReducer'; import configureStore from '../../components/store/configureStore'; @@ -48,6 +49,9 @@ window.sonarqube.appStarted.then(options => { <Route path="/quality_profiles" component={withComponent(QualityProfiles)}/> + <Route + path="/links" + component={withComponent(Links)}/> </Router> </Provider> ), el); diff --git a/server/sonar-web/src/main/js/apps/project-admin/links/Header.js b/server/sonar-web/src/main/js/apps/project-admin/links/Header.js new file mode 100644 index 00000000000..e6471ca9fb6 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/project-admin/links/Header.js @@ -0,0 +1,56 @@ +/* + * 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 React from 'react'; +import CreationModal from './views/CreationModal'; +import { translate } from '../../../helpers/l10n'; + +export default class Header extends React.Component { + static propTypes = { + onCreate: React.PropTypes.func.isRequired + }; + + handleCreateClick (e) { + e.preventDefault(); + e.target.blur(); + new CreationModal({ + onCreate: this.props.onCreate + }).render(); + } + + render () { + return ( + <header className="page-header"> + <h1 className="page-title"> + {translate('project_links.page')} + </h1> + <div className="page-actions"> + <button + id="create-project-link" + onClick={this.handleCreateClick.bind(this)}> + {translate('create')} + </button> + </div> + <div className="page-description"> + {translate('project_links.page.description')} + </div> + </header> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/project-admin/links/LinkRow.js b/server/sonar-web/src/main/js/apps/project-admin/links/LinkRow.js new file mode 100644 index 00000000000..e55651dce89 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/project-admin/links/LinkRow.js @@ -0,0 +1,113 @@ +/* + * 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 React from 'react'; +import shallowCompare from 'react-addons-shallow-compare'; +import { isProvided, isClickable } from './utils'; +import { translate } from '../../../helpers/l10n'; + +export default class LinkRow extends React.Component { + static propTypes = { + link: React.PropTypes.object.isRequired, + onDelete: React.PropTypes.func.isRequired + }; + + shouldComponentUpdate (nextProps, nextState) { + return shallowCompare(this, nextProps, nextState); + } + + handleDeleteClick (e) { + e.preventDefault(); + e.target.blur(); + this.props.onDelete(); + } + + renderIcon (iconClassName) { + return ( + <div className="display-inline-block text-top spacer-right"> + <i className={iconClassName}/> + </div> + ); + } + + renderNameForProvided (link) { + return ( + <div> + {this.renderIcon(`icon-${link.type}`)} + <div className="display-inline-block text-top"> + <div> + <span className="js-name">{link.name}</span> + </div> + <div className="note little-spacer-top"> + <span className="js-type">{`sonar.links.${link.type}`}</span> + </div> + </div> + </div> + ); + } + + renderName (link) { + if (isProvided(link)) { + return this.renderNameForProvided(link); + } + + return ( + <div> + {this.renderIcon('icon-detach')} + <div className="display-inline-block text-top"> + <span className="js-name">{link.name}</span> + </div> + </div> + ); + } + + renderUrl (link) { + if (isClickable(link)) { + return <a href={link.url} target="_blank">{link.url}</a>; + } + + return link.url; + } + + renderDeleteButton (link) { + if (isProvided(link)) { + return null; + } + + return ( + <button + className="button-red js-delete-button" + onClick={this.handleDeleteClick.bind(this)}> + {translate('delete')} + </button> + ); + } + + render () { + const { link } = this.props; + + return ( + <tr data-name={link.name}> + <td className="nowrap">{this.renderName(link)}</td> + <td className="nowrap js-url">{this.renderUrl(link)}</td> + <td className="thin nowrap">{this.renderDeleteButton(link)}</td> + </tr> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/project-admin/links/Links.js b/server/sonar-web/src/main/js/apps/project-admin/links/Links.js new file mode 100644 index 00000000000..88602532ea9 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/project-admin/links/Links.js @@ -0,0 +1,82 @@ +/* + * 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 React from 'react'; +import shallowCompare from 'react-addons-shallow-compare'; +import { connect } from 'react-redux'; +import Header from './Header'; +import Table from './Table'; +import DeletionModal from './views/DeletionModal'; +import { getProjectLinks } from '../store/rootReducer'; +import { + fetchProjectLinks, + deleteProjectLink, + createProjectLink +} from '../store/actions'; + +class Links extends React.Component { + static propTypes = { + component: React.PropTypes.object.isRequired, + links: React.PropTypes.array + }; + + componentWillMount () { + this.handleCreateLink = this.handleCreateLink.bind(this); + this.handleDeleteLink = this.handleDeleteLink.bind(this); + } + + componentDidMount () { + this.props.fetchProjectLinks(this.props.component.key); + } + + shouldComponentUpdate (nextProps, nextState) { + return shallowCompare(this, nextProps, nextState); + } + + handleCreateLink (name, url) { + return this.props.createProjectLink(this.props.component.key, name, url); + } + + handleDeleteLink (link) { + new DeletionModal({ link }).on('done', () => { + this.props.deleteProjectLink(this.props.component.key, link.id); + }).render(); + } + + render () { + return ( + <div className="page page-limited"> + <Header + onCreate={this.handleCreateLink}/> + <Table + links={this.props.links} + onDelete={this.handleDeleteLink}/> + </div> + ); + } +} + +const mapStateToProps = (state, ownProps) => ({ + links: getProjectLinks(state, ownProps.component.key) +}); + +export default connect( + mapStateToProps, + { fetchProjectLinks, createProjectLink, deleteProjectLink } +)(Links); diff --git a/server/sonar-web/src/main/js/apps/project-admin/links/Table.js b/server/sonar-web/src/main/js/apps/project-admin/links/Table.js new file mode 100644 index 00000000000..95f37f6841c --- /dev/null +++ b/server/sonar-web/src/main/js/apps/project-admin/links/Table.js @@ -0,0 +1,74 @@ +/* + * 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 React from 'react'; +import shallowCompare from 'react-addons-shallow-compare'; +import LinkRow from './LinkRow'; +import { orderLinks } from './utils'; +import { translate } from '../../../helpers/l10n'; + +export default class Table extends React.Component { + static propTypes = { + links: React.PropTypes.array.isRequired, + onDelete: React.PropTypes.func.isRequired + }; + + shouldComponentUpdate (nextProps, nextState) { + return shallowCompare(this, nextProps, nextState); + } + + handleDeleteLink (link) { + this.props.onDelete(link); + } + + renderHeader () { + // keep empty cell for actions + return ( + <thead> + <tr> + <th className="nowrap"> + {translate('project_links.name')} + </th> + <th className="nowrap width-100"> + {translate('project_links.url')} + </th> + <th className="thin"> </th> + </tr> + </thead> + ); + } + + render () { + const orderedLinks = orderLinks(this.props.links); + + const linkRows = orderedLinks.map(link => ( + <LinkRow + key={link.id} + link={link} + onDelete={this.handleDeleteLink.bind(this, link)}/> + )); + + return ( + <table id="project-links" className="data zebra"> + {this.renderHeader()} + <tbody>{linkRows}</tbody> + </table> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/project-admin/links/utils.js b/server/sonar-web/src/main/js/apps/project-admin/links/utils.js new file mode 100644 index 00000000000..9a34509d38b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/project-admin/links/utils.js @@ -0,0 +1,46 @@ +/* + * 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 partition from 'lodash/partition'; +import sortBy from 'lodash/sortBy'; + +const PROVIDED_TYPES = [ + 'homepage', + 'ci', + 'issue', + 'scm', + 'scm_dev' +]; + +export function isProvided (link) { + return PROVIDED_TYPES.includes(link.type); +} + +export function orderLinks (links) { + const [provided, unknown] = partition(links, isProvided); + return [ + ...sortBy(provided, link => PROVIDED_TYPES.indexOf(link.type)), + ...sortBy(unknown, link => link.name) + ]; +} + +export function isClickable (link) { + // stupid simple check + return link.url.indexOf('http') === 0; +} diff --git a/server/sonar-web/src/main/js/apps/project-admin/links/views/CreationModal.js b/server/sonar-web/src/main/js/apps/project-admin/links/views/CreationModal.js new file mode 100644 index 00000000000..a2fa0e36358 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/project-admin/links/views/CreationModal.js @@ -0,0 +1,45 @@ +/* + * 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 ModalForm from '../../../../components/common/modal-form'; +import Template from './CreationModalTemplate.hbs'; + +export default ModalForm.extend({ + template: Template, + + onFormSubmit () { + ModalForm.prototype.onFormSubmit.apply(this, arguments); + this.disableForm(); + + const name = this.$('#create-link-name').val(); + const url = this.$('#create-link-url').val(); + + this.options.onCreate(name, url) + .then(() => { + this.destroy(); + }) + .catch(function (e) { + e.response.json().then(r => { + this.showErrors(r.errors, r.warnings); + this.enableForm(); + }); + }); + } +}); + diff --git a/server/sonar-web/src/main/js/apps/project-admin/links/views/CreationModalTemplate.hbs b/server/sonar-web/src/main/js/apps/project-admin/links/views/CreationModalTemplate.hbs new file mode 100644 index 00000000000..7405f30d1b4 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/project-admin/links/views/CreationModalTemplate.hbs @@ -0,0 +1,22 @@ +<form> + <div class="modal-head"> + <h2>{{t 'project_links.create_new_project_link'}}</h2> + </div> + <div class="modal-body"> + <div class="js-modal-messages"></div> + + <div class="modal-field"> + <label for="create-link-name">{{t 'project_links.name'}}<em class="mandatory">*</em></label> + <input id="create-link-name" name="name" type="text" required> + </div> + + <div class="modal-field"> + <label for="create-link-url">{{t 'project_links.url'}}<em class="mandatory">*</em></label> + <input id="create-link-url" name="url" type="text" required> + </div> + </div> + <div class="modal-foot"> + <button id="create-link-confirm">{{t 'create'}}</button> + <a href="#" class="js-modal-close">{{t 'cancel'}}</a> + </div> +</form> diff --git a/server/sonar-web/src/main/js/apps/project-admin/links/views/DeletionModal.js b/server/sonar-web/src/main/js/apps/project-admin/links/views/DeletionModal.js new file mode 100644 index 00000000000..a1064dfb66e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/project-admin/links/views/DeletionModal.js @@ -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 ModalForm from '../../../../components/common/modal-form'; +import Template from './DeletionModalTemplate.hbs'; +import { deleteLink } from '../../../../api/projectLinks'; + +export default ModalForm.extend({ + template: Template, + + onFormSubmit () { + ModalForm.prototype.onFormSubmit.apply(this, arguments); + this.disableForm(); + + deleteLink(this.options.link.id) + .then(() => { + this.trigger('done'); + this.destroy(); + }) + .catch(function (e) { + e.response.json().then(r => { + this.showErrors(r.errors, r.warnings); + this.enableForm(); + }); + }); + }, + + serializeData () { + return { link: this.options.link }; + } +}); + diff --git a/server/sonar-web/src/main/js/apps/project-admin/links/views/DeletionModalTemplate.hbs b/server/sonar-web/src/main/js/apps/project-admin/links/views/DeletionModalTemplate.hbs new file mode 100644 index 00000000000..b8d744c83d9 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/project-admin/links/views/DeletionModalTemplate.hbs @@ -0,0 +1,13 @@ +<form> + <div class="modal-head"> + <h2>{{t 'project_links.delete_project_link'}}</h2> + </div> + <div class="modal-body"> + <div class="js-modal-messages"></div> + {{tp 'project_links.are_you_sure_to_delete_x_link' link.name}} + </div> + <div class="modal-foot"> + <button id="delete-link-confirm" class="button-red">{{t 'delete'}}</button> + <a href="#" class="js-modal-close">{{t 'cancel'}}</a> + </div> +</form> diff --git a/server/sonar-web/src/main/js/apps/project-admin/store/actions.js b/server/sonar-web/src/main/js/apps/project-admin/store/actions.js index cad25a70e7a..083a28bfa92 100644 --- a/server/sonar-web/src/main/js/apps/project-admin/store/actions.js +++ b/server/sonar-web/src/main/js/apps/project-admin/store/actions.js @@ -23,6 +23,7 @@ import { dissociateProject } from '../../../api/quality-profiles'; import { getProfileByKey } from './rootReducer'; +import { getProjectLinks, createLink } from '../../../api/projectLinks'; export const RECEIVE_PROFILES = 'RECEIVE_PROFILES'; export const receiveProfiles = profiles => ({ @@ -68,3 +69,36 @@ export const setProjectProfile = (projectKey, oldKey, newKey) => dispatch(setProjectProfileAction(projectKey, oldKey, newKey)); }); }; + +export const RECEIVE_PROJECT_LINKS = 'RECEIVE_PROJECT_LINKS'; +export const receiveProjectLinks = (projectKey, links) => ({ + type: RECEIVE_PROJECT_LINKS, + projectKey, + links +}); + +export const fetchProjectLinks = projectKey => dispatch => { + getProjectLinks(projectKey).then(links => { + dispatch(receiveProjectLinks(projectKey, links)); + }); +}; + +export const ADD_PROJECT_LINK = 'ADD_PROJECT_LINK'; +const addProjectLink = (projectKey, link) => ({ + type: ADD_PROJECT_LINK, + projectKey, + link +}); + +export const createProjectLink = (projectKey, name, url) => dispatch => { + return createLink(projectKey, name, url).then(link => { + dispatch(addProjectLink(projectKey, link)); + }); +}; + +export const DELETE_PROJECT_LINK = 'DELETE_PROJECT_LINK'; +export const deleteProjectLink = (projectKey, linkId) => ({ + type: DELETE_PROJECT_LINK, + projectKey, + linkId +}); diff --git a/server/sonar-web/src/main/js/apps/project-admin/store/links.js b/server/sonar-web/src/main/js/apps/project-admin/store/links.js new file mode 100644 index 00000000000..9a79d3d7905 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/project-admin/store/links.js @@ -0,0 +1,45 @@ +/* + * 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 keyBy from 'lodash/keyBy'; +import omit from 'lodash/omit'; +import { RECEIVE_PROJECT_LINKS, DELETE_PROJECT_LINK } from './actions'; +import { ADD_PROJECT_LINK } from './actions'; + +const links = (state = {}, action = {}) => { + if (action.type === RECEIVE_PROJECT_LINKS) { + const newLinksById = keyBy(action.links, 'id'); + return { ...state, ...newLinksById }; + } + + if (action.type === ADD_PROJECT_LINK) { + return { ...state, [action.link.id]: action.link }; + } + + if (action.type === DELETE_PROJECT_LINK) { + return omit(state, action.linkId); + } + + return state; +}; + +export default links; + +export const getLink = (state, id) => + state[id]; diff --git a/server/sonar-web/src/main/js/apps/project-admin/store/linksByProject.js b/server/sonar-web/src/main/js/apps/project-admin/store/linksByProject.js new file mode 100644 index 00000000000..dd48ff45f98 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/project-admin/store/linksByProject.js @@ -0,0 +1,49 @@ +/* + * 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 without from 'lodash/without'; +import { + RECEIVE_PROJECT_LINKS, + DELETE_PROJECT_LINK, + ADD_PROJECT_LINK +} from './actions'; + +const linksByProject = (state = {}, action = {}) => { + if (action.type === RECEIVE_PROJECT_LINKS) { + const linkIds = action.links.map(link => link.id); + return { ...state, [action.projectKey]: linkIds }; + } + + if (action.type === ADD_PROJECT_LINK) { + const byProject = state[action.projectKey] || []; + const ids = [...byProject, action.link.id]; + return { ...state, [action.projectKey]: ids }; + } + + if (action.type === DELETE_PROJECT_LINK) { + const ids = without(state[action.projectKey], action.linkId); + return { ...state, [action.projectKey]: ids }; + } + + return state; +}; + +export default linksByProject; + +export const getLinks = (state, projectKey) => state[projectKey] || []; diff --git a/server/sonar-web/src/main/js/apps/project-admin/store/rootReducer.js b/server/sonar-web/src/main/js/apps/project-admin/store/rootReducer.js index 5f5dc3d7899..e8174f53aa0 100644 --- a/server/sonar-web/src/main/js/apps/project-admin/store/rootReducer.js +++ b/server/sonar-web/src/main/js/apps/project-admin/store/rootReducer.js @@ -23,10 +23,14 @@ import profiles, { getAllProfiles as nextGetAllProfiles } from './profiles'; import profilesByProject, { getProfiles } from './profilesByProject'; +import links, { getLink } from './links'; +import linksByProject, { getLinks } from './linksByProject'; const rootReducer = combineReducers({ profiles, - profilesByProject + profilesByProject, + links, + linksByProject }); export default rootReducer; @@ -40,3 +44,10 @@ export const getAllProfiles = state => export const getProjectProfiles = (state, projectKey) => getProfiles(state.profilesByProject, projectKey) .map(profileKey => getProfileByKey(state, profileKey)); + +export const getLinkById = (state, linkId) => + getLink(state.links, linkId); + +export const getProjectLinks = (state, projectKey) => + getLinks(state.linksByProject, projectKey) + .map(linkId => getLinkById(state, linkId)); diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/project_controller.rb b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/project_controller.rb index 5754eccd086..dbd5d6a434e 100644 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/project_controller.rb +++ b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/project_controller.rb @@ -44,6 +44,14 @@ class ProjectController < ApplicationController redirect_to(url_for({:action => 'quality_profiles'}) + '?id=' + url_encode(params[:id])) end + def background_tasks + @project = get_current_project(params[:id]) + end + + def links + @project = get_current_project(params[:id]) + end + # GET /project/qualitygate?id=<project id> def qualitygate require_parameters :id @@ -154,44 +162,6 @@ class ProjectController < ApplicationController :include => 'events', :order => 'snapshots.created_at DESC') end - def background_tasks - @project = get_current_project(params[:id]) - end - - def links - @project = get_current_project(params[:id]) - - if !@project.project? - redirect_to :action => 'index', :id => params[:id] - end - @snapshot = @project.last_snapshot - end - - def set_links - project = get_current_project(params[:project_id]) - - project.custom_links.each { |link| link.delete } - - params.each_pair do |param_key, value| - if (param_key.starts_with?('name_')) - id = param_key[5..-1] - name=value - url=params["url_#{id}"] - key=params["key_#{id}"] - if key.blank? - key=ProjectLink.name_to_key(name) - end - unless key.blank? || name.blank? || url.blank? - project.links.create(:href => url, :name => name, :link_type => key) - end - end - end - project.save! - - flash[:notice] = 'Links updated.' - redirect_to :action => 'links', :id => project.id - end - def settings @resource = get_current_project(params[:id]) diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/links.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/links.html.erb index 0c64f689e8f..e9dd9ae3410 100644 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/links.html.erb +++ b/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/links.html.erb @@ -1,135 +1,3 @@ -<div class="page"> - <header class="page-header"> - <h1 class="page-title"><%= message('project_links.page') -%></h1> - <p class="page-description"><%= message('project_links.page.description') -%></p> - </header> - - <style> - #widget_links td, #widget_links th { - white-space: nowrap; - vertical-align: top; - text-align: left; - padding-left: 20px; - padding-right: 20px; - } - </style> - - <div class="yui-g widget" id="widget_links"> - - <% form_for( 'set_links', :url => { :action => 'set_links', :project_id => @project.id } ) do |form| - links_by_key={} - @project.links.each do |link| - links_by_key[link.link_type]=link - end - %> - <div class="yui-u first"> - <table class="data"> - <thead><tr><th >Title</th><th width="100%">URL</th></tr></thead> - - <tr class="even"> - <td> - <%= image_tag("links/homepage.png") -%> <%= message('project_links.homepage') -%> - <br/> - <span class="note" style="margin-left: 20px">sonar.links.homepage</span> - </td> - <td> - <% - link = links_by_key['homepage'] ? links_by_key['homepage'].href : '' - if link.starts_with?("http") - link = "<a href=\"#{h link}\">#{h link}</a>" - end - %> - <%= link -%> - </td> - </tr> - <tr class="odd"> - <td> - <%= image_tag("links/ci.png") -%> <%= message('project_links.ci') -%> - <br/> - <span class="note" style="margin-left: 20px">sonar.links.ci</span> - </td> - <td> - <% - link = links_by_key['ci'] ? links_by_key['ci'].href : '' - if link.starts_with?("http") - link = "<a href=\"#{h link}\">#{h link}</a>" - end - %> - <%= link -%> - </td> - </tr> - <tr class="even"> - <td> - <%= image_tag("links/issue.png") -%> <%= message('project_links.issue') -%> - <br/> - <span class="note" style="margin-left: 20px">sonar.links.issue</span> - </td> - <td> - <% - link = links_by_key['issue'] ? links_by_key['issue'].href : '' - if link.starts_with?("http") - link = "<a href=\"#{h link}\">#{h link}</a>" - end - %> - <%= link -%> - </td> - </tr> - <tr class="odd"> - <td> - <%= image_tag("links/scm.png") -%> <%= message('project_links.scm') -%> - <br/> - <span class="note" style="margin-left: 20px">sonar.links.scm</span> - </td> - <td> - <% - link = links_by_key['scm'] ? links_by_key['scm'].href : '' - if link.starts_with?("http") - link = "<a href=\"#{h link}\">#{h link}</a>" - end - %> - <%= link -%> - </td> - </tr> - <tr class="even"> - <td> - <%= image_tag("links/scm_dev.png") -%> <%= message('project_links.scm_dev') -%> - <br/> - <span class="note" style="margin-left: 20px">sonar.links.scm_dev</span> - </td> - <td> - <% - link = links_by_key['scm_dev'] ? links_by_key['scm_dev'].href : '' - if link.starts_with?("http") - link = "<a href=\"#{h link}\">#{h link}</a>" - end - %> - <%= link -%> - </td> - </tr> - - <% index = 0 - @project.custom_links.each do |custom_link| - index += 1 %> - <tr class="<%= cycle('odd','even') -%>"> - <td align="left"> - <%= image_tag("links/external.png") -%> <%= text_field_tag( "name_#{index}", h(custom_link.name), :size => 15 ) -%> - </td> - <td> - <%= text_field_tag( "url_#{index}", custom_link.href, :size => 30 ) -%> - </td> - </tr> - <% end %> - <% index += 1 - for var in index..5 %> - <tr class="<%= cycle('odd','even') -%>"> - <td align="left"><%= image_tag("links/external.png") -%> <%= text_field_tag( "name_#{var}", '', :size => 15) %></td> - <td><%= text_field_tag( "url_#{var}", '', :size => 30 ) %></td> - </tr> - <% end %> - </table> - <br/> - <%= submit_tag( "Save links" ) %> - </div> - <% end %> - </div> -</div> +<% content_for :extra_script do %> + <script src="<%= ApplicationController.root_context -%>/js/bundles/project-admin.js?v=<%= sonar_version -%>"></script> +<% end %> 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 0a86b12052e..7878ece8129 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -419,6 +419,12 @@ project_links.scm=Sources project_links.scm_ro=Read-only connection project_links.scm_dev=Developer connection +project_links.create_new_project_link=Create New Project Link +project_links.delete_project_link=Delete Project Link +project_links.are_you_sure_to_delete_x_link=Are you sure you want to delete the "{0}" link? +project_links.name=Name +project_links.url=URL + #------------------------------------------------------------------------------ # |