Procházet zdrojové kódy

SONAR-7920 Rewrite Links project page (#1127)

tags/6.1-RC1
Stas Vilchik před 7 roky
rodič
revize
0b1226871a
24 změnil soubory, kde provedl 963 přidání a 175 odebrání
  1. 2
    0
      it/it-tests/src/test/java/it/Category1Suite.java
  2. 153
    0
      it/it-tests/src/test/java/it/projectAdministration/ProjectLinksPageTest.java
  3. 5
    1
      it/it-tests/src/test/java/pageobjects/LoginPage.java
  4. 6
    0
      it/it-tests/src/test/java/pageobjects/Navigation.java
  5. 52
    0
      it/it-tests/src/test/java/pageobjects/ProjectLinkItem.java
  6. 47
    0
      it/it-tests/src/test/java/pageobjects/ProjectLinksPage.java
  7. 38
    0
      server/sonar-web/src/main/js/api/projectLinks.js
  8. 4
    0
      server/sonar-web/src/main/js/apps/project-admin/app.js
  9. 56
    0
      server/sonar-web/src/main/js/apps/project-admin/links/Header.js
  10. 113
    0
      server/sonar-web/src/main/js/apps/project-admin/links/LinkRow.js
  11. 82
    0
      server/sonar-web/src/main/js/apps/project-admin/links/Links.js
  12. 74
    0
      server/sonar-web/src/main/js/apps/project-admin/links/Table.js
  13. 46
    0
      server/sonar-web/src/main/js/apps/project-admin/links/utils.js
  14. 45
    0
      server/sonar-web/src/main/js/apps/project-admin/links/views/CreationModal.js
  15. 22
    0
      server/sonar-web/src/main/js/apps/project-admin/links/views/CreationModalTemplate.hbs
  16. 48
    0
      server/sonar-web/src/main/js/apps/project-admin/links/views/DeletionModal.js
  17. 13
    0
      server/sonar-web/src/main/js/apps/project-admin/links/views/DeletionModalTemplate.hbs
  18. 34
    0
      server/sonar-web/src/main/js/apps/project-admin/store/actions.js
  19. 45
    0
      server/sonar-web/src/main/js/apps/project-admin/store/links.js
  20. 49
    0
      server/sonar-web/src/main/js/apps/project-admin/store/linksByProject.js
  21. 12
    1
      server/sonar-web/src/main/js/apps/project-admin/store/rootReducer.js
  22. 8
    38
      server/sonar-web/src/main/webapp/WEB-INF/app/controllers/project_controller.rb
  23. 3
    135
      server/sonar-web/src/main/webapp/WEB-INF/app/views/project/links.html.erb
  24. 6
    0
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 2
- 0
it/it-tests/src/test/java/it/Category1Suite.java Zobrazit soubor

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

+ 153
- 0
it/it-tests/src/test/java/it/projectAdministration/ProjectLinksPageTest.java Zobrazit soubor

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

+ 5
- 1
it/it-tests/src/test/java/pageobjects/LoginPage.java Zobrazit soubor

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

+ 6
- 0
it/it-tests/src/test/java/pageobjects/Navigation.java Zobrazit soubor

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

+ 52
- 0
it/it-tests/src/test/java/pageobjects/ProjectLinkItem.java Zobrazit soubor

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

+ 47
- 0
it/it-tests/src/test/java/pageobjects/ProjectLinksPage.java Zobrazit soubor

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

+ 38
- 0
server/sonar-web/src/main/js/api/projectLinks.js Zobrazit soubor

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

+ 4
- 0
server/sonar-web/src/main/js/apps/project-admin/app.js Zobrazit soubor

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

+ 56
- 0
server/sonar-web/src/main/js/apps/project-admin/links/Header.js Zobrazit soubor

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

+ 113
- 0
server/sonar-web/src/main/js/apps/project-admin/links/LinkRow.js Zobrazit soubor

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

+ 82
- 0
server/sonar-web/src/main/js/apps/project-admin/links/Links.js Zobrazit soubor

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

+ 74
- 0
server/sonar-web/src/main/js/apps/project-admin/links/Table.js Zobrazit soubor

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

+ 46
- 0
server/sonar-web/src/main/js/apps/project-admin/links/utils.js Zobrazit soubor

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

+ 45
- 0
server/sonar-web/src/main/js/apps/project-admin/links/views/CreationModal.js Zobrazit soubor

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


+ 22
- 0
server/sonar-web/src/main/js/apps/project-admin/links/views/CreationModalTemplate.hbs Zobrazit soubor

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

+ 48
- 0
server/sonar-web/src/main/js/apps/project-admin/links/views/DeletionModal.js Zobrazit soubor

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


+ 13
- 0
server/sonar-web/src/main/js/apps/project-admin/links/views/DeletionModalTemplate.hbs Zobrazit soubor

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

+ 34
- 0
server/sonar-web/src/main/js/apps/project-admin/store/actions.js Zobrazit soubor

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

+ 45
- 0
server/sonar-web/src/main/js/apps/project-admin/store/links.js Zobrazit soubor

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

+ 49
- 0
server/sonar-web/src/main/js/apps/project-admin/store/linksByProject.js Zobrazit soubor

@@ -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] || [];

+ 12
- 1
server/sonar-web/src/main/js/apps/project-admin/store/rootReducer.js Zobrazit soubor

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

+ 8
- 38
server/sonar-web/src/main/webapp/WEB-INF/app/controllers/project_controller.rb Zobrazit soubor

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


+ 3
- 135
server/sonar-web/src/main/webapp/WEB-INF/app/views/project/links.html.erb Zobrazit soubor

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

+ 6
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties Zobrazit soubor

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


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

Načítá se…
Zrušit
Uložit