@@ -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, |
@@ -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"); | |||
} | |||
} |
@@ -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); | |||
} | |||
} |
@@ -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); | |||
} |
@@ -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"); | |||
} | |||
} |
@@ -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()); | |||
} | |||
} |
@@ -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); | |||
} |
@@ -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); |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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); |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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; | |||
} |
@@ -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(); | |||
}); | |||
}); | |||
} | |||
}); | |||
@@ -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> |
@@ -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 }; | |||
} | |||
}); | |||
@@ -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> |
@@ -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 | |||
}); |
@@ -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]; |
@@ -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] || []; |
@@ -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)); |
@@ -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]) | |||
@@ -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 %> |
@@ -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 | |||
#------------------------------------------------------------------------------ | |||
# |